简介:这是一套基于BP神经网络模型完成鸢尾花与红酒数据集分类的完整实践项目,适合作为机器学习课程设计、期末大作业或毕业设计的参考。项目包含Python源码、Jupyter Notebook演示、实验报告和答辩PPT,代码附有详细注释,即使基础薄弱的新手也能快速理解模型构建、训练与评估流程。资源共21个文件,涵盖py脚本、ipynb笔记本、doc实验报告、pptx答辩材料以及xls/xlsx数据文件,压缩包仅1.37MB,结构清晰,下载后简单配置即可运行。目前已有286人学习浏览,内容经作者整理为98分高分项目,尤其适合需要快速搭建完整分类任务方案并准备汇报答辩的学习者。
1. BP神经网络分类鸢尾花和红酒:为什么最后8成时间花在报告上
拿到「Python基于BP神经网络模型实现对鸢尾花、红酒数据集分类源码+实验报告+答辩PPT」这个题目时,大多数人以为难点在写模型代码。实际上手才发现:用numpy写一个BP网络大半天就能搞定,真正吃掉时间的是数据预处理、隐藏层参数调节,以及把原理在实验报告和答辩PPT里讲清楚。鸢尾花和红酒是机器学习里两个经典benchmark数据集,前者150条样本4个特征,后者178条样本13个特征,都是3分类问题,非常适合作为BP神经网络的入门验证。这个方案适合正在做课程设计的学生,也适合想用手写代码验证BP理解程度的工程师——它能让你把「原理能讲」「代码能跑」「结果能解释」三件事一次补齐。
2. BP神经网络原理与结构:先看懂结构图再写代码
2.1 BP神经网络结构图:输入层、隐藏层、输出层各自画什么
BP神经网络在结构上就是「输入层-隐藏层-输出层」的层叠结构。结构图画出来并不复杂:每个节点是一个神经元,每条边是一个权重。输入层的节点数由特征数量决定,鸢尾花是4个特征(花萼长宽、花瓣长宽),输入层就是4个节点;红酒是13个特征,输入层就是13个节点。输出层节点数由类别数决定,两个数据集都是3分类,输出层都是3个节点,配合softmax输出每个类别的概率。真正需要设计的是隐藏层:设几层、每层几个节点。
对这个数据集的规模,一层隐藏层完全够用。BP的万能逼近定理说的是单隐藏层网络可以逼近任意连续函数,但理论成立不代表实践就该堆层数。150条样本的网络做深了(3层以上隐藏层)只会过拟合,参数数量比样本量还大,没有意义。常见做法是1~2层隐藏层,节点数按经验法则试:比如输入输出维度的中间值,或者(输入+输出)/2附近选几个候选对比。鸢尾花我用hidden_dim=8,红酒用10,都是先按经验定,再跑几轮对比选出来的。
权重参数数量值得心里有数。以鸢尾花4→8→3为例:第一层权重4×8=32个再加8个偏置,第二层权重8×3=24个再加3个偏置,总共67个参数。150条样本训练67个参数,比例尚可。红酒13→10→3是130+30+10+3=173个参数,但样本只有178条,参数几乎和样本一样多,这就埋下了过拟合的隐患,后面第4章会专门处理。这个「样本量 vs 参数数量」的账,是决定网络结构的第一约束。
2.2 前向传播:加权求和过激活函数,信号一层层往前走
前向传播做的事就是逐层计算。每一层先做线性变换z = Wx + b,再过一个非线性激活函数a = f(z)。如果没有激活函数,多层网络就退化成一次线性变换,堆多少层都等效于一层,这就是必须加非线性的原因。常见的激活函数里,Sigmoid输出范围(0,1),但两端梯度接近0,深层网络里容易梯度消失;ReLU在正区间梯度恒为1,计算简单,是目前隐藏层的默认选择。我在这两个数据集上隐藏层都用ReLU。
输出层的情况不一样。多分类任务希望输出是一个概率分布,所以输出层用softmax:把三个神经元的输出映射成三个和为1的概率值。softmax的公式实现时要注意数值稳定性,直接算exp(z)在z很大时会溢出,常见做法是每个样本先减去该样本z的最大值再做exp,结果不变但数值稳定。这个细节初学容易忽略,但最终softmax输出的就是「这个样本属于第几类的置信度」。
隐藏层的节点数直接影响模型容量,但节点数翻倍带来的不是准确率线性上升,而是训练时间和过拟合风险一起上升。我的经验是:优先用一个小网络把流程跑通(这时准确率一般已经不错),再逐次增加节点数,观察测试集准确率的变化。节点数加到某个值以后测试准确率反而下降,就是过拟合的典型信号。两个数据集规模都很小,隐藏层节点数在4~16之间试就足够,不用往大了加。
2.3 反向传播:误差按链式法则传回去,梯度才是学习信号
有监督训练的最终目标是让模型输出和真实标签之间的差距最小,这个差距用损失函数度量。多分类的标准选择是交叉熵损失:真实类别的概率越接近1,损失越接近0。为什么用交叉熵而不是均方误差?因为交叉熵和softmax组合时,梯度形式特别干净,收敛也更快;均方误差配合sigmoid在输出层会出现梯度饱和,初学阶段不建议混用。
所谓反向传播,就是用链式法则从损失函数开始,一层一层往回求每个参数的偏导数。输出层的梯度在softmax+交叉熵组合下恰好等于a2 - y(预测概率减真实标签的one-hot编码),这个「恰好」是很多教材推导一页纸的原因。然后梯度继续往上一层传:先算隐藏层、输出层之间的权重梯度,再算输入层、隐藏层之间权重梯度。每层参数更新的规则都是同一个:w = w - lr * dw,lr是学习率,dw是该参数的梯度。梯度方向是损失增大的方向,所以沿负方向更新。
手写反向传播时最常翻车的点是矩阵的维度对不上。以鸢尾花为例,X是(120, 4),第一层权重是(4, 8),z1是(120, 8),a1也是(120, 8),第二层权重是(8, 3),z2是(120, 3),softmax后a2是(120, 3)。做梯度时,dz2是(120, 3),dW2 = a1.T @ dz2 / m,m是样本数120,这样dW2是(8, 3),形状和W2一致。任何一步形状对不上,基本就能确认推导错了。写代码时逐行print形状调试,比对着公式发呆快得多。
2.4 为什么这个题目选BP而不是决策树或SVM
为什么不直接上sklearn的MLPClassifier或者PyTorch?课程设计场景下,手写numpy版的价值在于:能完整画出一个网络结构图,能把前向、反向、权重更新每一步讲给别人听,这是任何框架都替代不了的。而且150条样本的训练在全量梯度下降下也就是几百次迭代,numpy跑下来毫秒级,性能完全不构成瓶颈。
和这个题目常搭配的其他算法比一比。决策树可解释性最强,但边界都是轴平行划分,对鸢尾花这类特征间有关联的数据,决策边界不够自然;很多MOOC实训里用决策树做鸢尾花,属于入门演示,效果可以但讲不出「神经网络」的深度。KNN在小样本上效果不差,但预测时要计算全部样本距离,红酒13维特征下欧氏距离的意义变弱,超参数k一改结果就跳。SVM在这个规模上效果其实很好,但核函数和C参数的选择有「调参玄学」成分,答辩时容易被追问住。BP的好处是:原理清晰、调参路径直观(学习率、隐藏层节点数、迭代次数)、可视化素材多,作为课程作业是性价比最高的选择。
| 对比维度 | BP神经网络 | 决策树 | KNN | SVM |
|---|---|---|---|---|
| 可解释性 | 中等(看权重) | 高 | 中 | 低 |
| 非线性边界 | 强 | 轴平行限制 | 强 | 靠核函数 |
| 调参复杂度 | 3~4个超参数 | 剪枝参数 | k值 | 核函数+C |
| 答辩可讲深度 | 高 | 中 | 低 | 中 |
3. 鸢尾花动手实现:从数据加载到BP神经网络完整Python代码
3.1 数据加载与标准化:先切分再标准化,顺序反了结果全错
环境准备一句带过:装上numpy和scikit-learn就够了,pip install numpy scikit-learn matplotlib 一次装齐,vscode或pycharm都行,不需要GPU也不需要深度学习框架。无论你用的是哪个Python发行版,只要这三个库装好,下面的代码就能直接跑。如果在Windows上装sklearn报错,常见原因是Python版本太旧,先升级到3.8以上再装。
import numpy as np from sklearn.datasets import load_iris from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler # 加载鸢尾花数据集:150条样本,4个特征,3个类别 data = load_iris() X, y = data.data, data.target print(f'特征矩阵形状: {X.shape}, 标签形状: {y.shape}') print(f'类别: {data.target_names}') # 先切分,再标准化——顺序不能反,否则测试集信息会泄漏进训练过程 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) print(f'训练集: {X_train.shape}, 测试集: {X_test.shape}') # 标准化:让每个特征均值0、方差1 scaler = StandardScaler().fit(X_train) # 只对训练集做fit X_train_scaled = scaler.transform(X_train) # 训练集transform X_test_scaled = scaler.transform(X_test) # 测试集只用transform # 把整数标签转成one-hot编码,方便和softmax输出比对 def onehot_encode(y): m = y.shape[0] res = np.zeros((m, 3)) res[np.arange(m), y] = 1.0 return res y_train_oh = onehot_encode(y_train) y_test_oh = onehot_encode(y_test) print(f'one-hot后的训练标签形状: {y_train_oh.shape}')train_test_split里的stratify=y表示按类别比例分层抽样。鸢尾花每类50条完全均衡,这里分层是给后面红酒数据集练习惯,红酒的类别分布不像鸢尾花那么整齐。StandardScaler先fit再transform的关键是:fit只计算训练集的均值和方差,测试集用同一组数值做变换,这样测试集的信息不会提前参与训练。如果先标准化整个数据集再切分,测试集的分布信息已经泄露进模型,准确率会虚高,这个问题在第5章还会展开。
test_size=0.2是留出20%做测试,120条训练、30条测试,这个比例对这个规模合适。random_state=42是固定随机种子,保证每次运行结果一致,答辩时可以复现。实际对比实验时建议把random_state换几个值看波动,别抱着一个结果说事。
提示:这个数据集规模很小,所有结果都依赖随机划分。写报告时固定种子没问题,但横向对比模型时一定要换2~3个种子取平均。
3.2 用numpy手写BP网络:前向、反向、训练循环
代码用类封装,把初始化、前向、损失、反向、训练五件事拆开。这样实验报告里讲原理时可以直接对应到方法上,比一段脚本式代码清楚得多。
def relu(x): return np.maximum(0, x) def relu_deriv(x): return (x > 0).astype(float) class BPNN: """三层BP神经网络:输入层 -> 隐层 -> 输出层,多分类""" def __init__(self, input_dim, hidden_dim, output_dim, lr=0.05): self.lr = lr # 小范围随机初始化权重,打破对称性,避免所有神经元学到相同特征 self.W1 = np.random.randn(input_dim, hidden_dim) * 0.5 self.b1 = np.zeros(hidden_dim) self.W2 = np.random.randn(hidden_dim, output_dim) * 0.5 self.b2 = np.zeros(output_dim) def softmax(self, z): # 减最大值防止exp溢出,数值稳定 exp_z = np.exp(z - np.max(z, axis=1, keepdims=True)) return exp_z / exp_z.sum(axis=1, keepdims=True) def forward(self, X): self.z1 = X @ self.W1 + self.b1 self.a1 = relu(self.z1) self.z2 = self.a1 @ self.W2 + self.b2 self.a2 = self.softmax(self.z2) return self.a2 def loss(self, y_true, y_pred): # 交叉熵损失,加1e-12防止log(0) return -np.mean(np.sum(y_true * np.log(y_pred + 1e-12), axis=1)) def backward(self, X, y_true): m = X.shape[0] # softmax+交叉熵的梯度恰好是 a2 - y_true dz2 = self.a2 - y_true dW2 = self.a1.T @ dz2 / m db2 = np.mean(dz2, axis=0) # 梯度经W2传回隐藏层,再过relu的导数 dz1 = (dz2 @ self.W2.T) * relu_deriv(self.z1) dW1 = X.T @ dz1 / m db1 = np.mean(dz1, axis=0) # 梯度下降更新 self.W1 -= self.lr * dW1 self.b1 -= self.lr * db1 self.W2 -= self.lr * dW2 self.b2 -= self.lr * db2 def fit(self, X, y_true, epochs=800, verbose=True): for i in range(epochs): y_pred = self.forward(X) loss = self.loss(y_true, y_pred) self.backward(X, y_true) if verbose and (i + 1) % 100 == 0: print(f'Epoch {i+1:4d} / {epochs}, loss = {loss:.4f}') def predict(self, X): y_pred = self.forward(X) return np.argmax(y_pred, axis=1)这个类是标准的全量梯度下降,每次迭代把整个训练集走一遍,对150条左右样本完全够用。forward里z1、a1保存为成员变量,是为了backward里复用中间结果,不用重复计算。softmax里减最大值只影响数值稳定性,不影响结果。
lr=0.05是学习率,太大loss会震荡,太小收敛慢。你跑的时候如果loss在200次迭代后还挂在0.6以上,优先降lr到0.01。权重初始化用randn乘0.5,让初始权重落在(-1.5, 1.5)区间附近,既不至于让初始输出饱和,又打破了对称性。如果初始化全零,所有神经元梯度一致,学到的东西完全一样,模型就废了。
这里有一个推导关键点:反向传播里dz2直接写成了a2 - y_true,这是softmax+交叉熵组合在数学上化简的结果。如果输出层激活函数换成sigmoid、损失用均方误差,梯度公式就完全不同。课程作业里经常有人把两种组合的梯度记串,写实验报告时把推导放上去,是个加分项。
3.3 训练与评估:loss曲线、准确率、混淆矩阵一次出齐
from sklearn.metrics import classification_report, confusion_matrix import matplotlib.pyplot as plt model = BPNN(input_dim=4, hidden_dim=8, output_dim=3, lr=0.05) model.fit(X_train_scaled, y_train_oh, epochs=800) train_pred = model.predict(X_train_scaled) test_pred = model.predict(X_test_scaled) # 训练集和测试集都看一下,方便判断是否过拟合 print('--- 训练集 ---') print(classification_report(y_train, train_pred, target_names=data.target_names)) print('--- 测试集 ---') print(classification_report(y_test, test_pred, target_names=data.target_names)) print('混淆矩阵:') print(confusion_matrix(y_test, test_pred))这段代码跑完,训练集准确率一般在98%以上,测试集也稳定在93%~96%。如果训练集接近100%但测试集跌破85%,就是过拟合了,优先减hidden_dim或加正则。
分类报告一次给出Precision、Recall、F1三个指标。鸢尾花三类完全均衡,准确率基本等于F1,差距不明显;但同样的代码换到红酒上,某个类别的召回率常常偏低,单独看准确率会漏掉这个信息。混淆矩阵要逐位看:对角线上的数字是预测正确的样本数,如果某个类别被系统性错认成别的类,矩阵里非对角线那一格会很显眼,这比一个综合数字更能定位问题。
如果想画loss曲线,在fit里自己收集每轮loss,最后plot出来。loss曲线是实验报告里最好用的素材:曲线平滑下降说明学习率合适,锯齿状震荡说明步长大了,前几百轮不降说明初始化或数据预处理有问题。用matplotlib画一张横轴epoch、纵轴loss的折线图,在「结果分析」部分写两句话,比贴一个准确率数字有说服力得多。
4. 红酒数据集迁移与调参:13维特征下BP网络怎么改
4.1 红酒数据集和鸢尾花到底差在哪
红酒数据集(sklearn的load_wine)是178条样本、13个特征、3个品种。和鸢尾花相比,最直观的差别是特征维度从4跳到13,而且特征量纲差异极大:酒精含量大约在11~15之间,苹果酸在0.7~6左右,而脯氨酸的数值能到几百上千。如果不做标准化直接把原始数值喂给BP,梯度计算时数值大的特征对权重更新的影响会盖过小数值特征,模型实际上只学到了一两个特征的规律。所以红酒数据集的处理流程和鸢尾花相同,标准化这一步不再是不起眼的过场,而是决定模型能不能收敛的前提。
另一个差异是样本量:红酒只有178条,而13维特征的网络参数数量轻松上百。参数比样本还多,模型有充分的空间记住训练集而学不到泛化规律,过拟合风险比鸢尾花高一个量级。这是做红酒分类时第一件要心里有数的事——准确率不如鸢尾花好看是正常的,稳定复现比追求一个虚高的测试分数更重要。
4.2 网络结构与超参数调整:从4特征到13特征的改动清单
迁移到红酒的代码改动很小,核心变化就是input_dim从4改成13,其他结构和流程完全复用。
from sklearn.datasets import load_wine wine = load_wine() X, y = wine.data, wine.target print(f'红酒特征矩阵: {X.shape}, 类别: {wine.target_names}') # 训练测试划分和标准化,流程和鸢尾花完全一致 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) scaler = StandardScaler().fit(X_train) X_train_scaled = scaler.transform(X_train) X_test_scaled = scaler.transform(X_test) y_train_oh = onehot_encode(y_train) y_test_oh = onehot_encode(y_test) # 输入维度改成13,隐藏层节点数适当上调 model = BPNN(input_dim=13, hidden_dim=10, output_dim=3, lr=0.02) model.fit(X_train_scaled, y_train_oh, epochs=1000) test_pred = model.predict(X_test_scaled) print(classification_report(y_test, test_pred, target_names=wine.target_names)) print(confusion_matrix(y_test, test_pred))改动只有两处:load_iris换成load_wine,input_dim换成13。但超参数的调整不止这些。lr从0.05降到0.02,是因为13维特征下梯度的噪声更大,大步长容易在最优解附近震荡;epochs从800加到1000,是因为收敛变慢了;hidden_dim从8加到10,是因为输入特征变多,模型需要更多容量拟合特征间关系。这三组数值不是拍脑袋,而是各跑5次后看loss曲线和测试集波动选的。
隐藏层节点数到底怎么定,我一般按这个流程走:先取(输入维数+输出维数)/2附近的整数值,这里(13+3)/2=8;然后试6、8、10、12四组,每组固定随机种子跑5次,比较测试集准确率的平均值和波动幅度。红酒数据集上hidden_dim=10的平均值最高,而且方差没有比8大多少,所以选10。鸢尾花同样处理,hidden_dim=8是平均值和波动性综合最优的选择。这个流程写进实验报告的「参数选择」一节,比写「经过多次试验确定」扎实得多。
4.3 两个数据集横向对比:为什么红酒更难稳定上90%
| 数据集 | 样本量 | 特征维度 | 隐藏层 | 学习率 | 迭代次数 | 测试准确率区间 |
|---|---|---|---|---|---|---|
| 鸢尾花 | 150 | 4 | 8 | 0.05 | 800 | 93%~96% |
| 红酒 | 178 | 13 | 10 | 0.02 | 1000 | 88%~93% |
红酒的测试准确率区间更宽,原因是多方面的:特征维度高、样本量小、类别之间存在部分重叠(class_1和class_2在PCA投影下边界比较模糊)。这不是模型不行,而是数据集本身的信噪比决定的。实际实验中,红酒数据集的准确率受random_state影响更大,固定一个种子可能跑出96%,换一个种子掉到88%,这种波动在答辩时很容易被追问。我一般会固定几个种子各跑一次,取均值和方差,写进实验报告的「稳定性分析」一节,比只报最高分诚实得多。
另一个值得做的对比实验是:同样切分方式下,跑一下sklearn的MLPClassifier,把结果和手写BP对照。如果两者差距在2个百分点以内,说明手写实现没有明显bug;如果差得离谱,大概率是学习率或初始化有问题。这个对照实验不用写进报告,但强烈建议在本地做一遍,它能帮你快速定位是模型问题还是实现问题。
5. 避坑指南:BP跑这两个数据集的5个常见问题
5.1 标准化顺序反了导致数据泄漏
现象:训练准确率95%,测试准确率也95%,看起来完美,但换到真实数据上效果断崖下跌。
原因:先对整个数据集做标准化再切分训练集测试集。StandardScaler用的是全量数据的均值和方差,测试集分布信息已经进入了训练时的输入范围,这叫数据泄漏。
解决:先train_test_split,再对训练集fit,对测试集只用transform。这个顺序在3.1的代码里已经体现。泄漏不会让训练代码报错,只会让成绩虚高,属于「看起来对但经不起推敲」的隐蔽错误。答辩时被问「为什么先切分再标准化」,这正是加分回答。
5.2 隐藏层用sigmoid导致梯度消失
现象:loss在前几百个epoch几乎不下降,或者下降极慢,训练准确率始终在40%左右(相当于乱猜)。
原因:隐藏层用了sigmoid当激活函数。sigmoid两端的导数趋近0,误差信号反向传播经过多层时梯度连乘会指数级缩小,前面的层学不到东西。
解决:隐藏层换成ReLU,导数在正区间恒为1,梯度能正常传回。注意ReLU也有自己的坑:如果学习率太大,神经元可能大量死亡(输出恒为0),表现为loss突然不降。此时降低学习率。这是BP入门最经典的一个组合陷阱,实验报告里值得专门写一段。
5.3 学习率没调,loss曲线原地打转
现象:loss曲线锯齿状上下跳,准确率也跟着抽风;或者loss平缓下降但500个epoch还没到底。
原因:lr偏大导致更新步长跨过最优区域;lr偏小导致走得慢。全量梯度下降对学习率尤其敏感,因为每次更新方向是全局平均梯度,步长错了整个训练过程都受影响。
解决:先用0.01起步跑100个epoch看loss曲线。loss震荡就降一个数量级,下降太慢就升到0.05。学习率的调节没有公式,看曲线是最直接的办法。我习惯把lr设成0.02~0.05之间,再微调,不走极端。
5.4 权重初始化翻车,所有样本被预测成同一类
现象:loss下降很快,但训练集和测试集准确率都稳定在33%左右(三类猜测概率),甚至出现所有样本被预测成同一类。
原因:权重初始化为全零。所有神经元输入相同、梯度相同、更新后仍相同,网络退化成单个神经元,学不到多特征组合。另一种翻车是权重初始化过大,relu输入饱和,梯度传不过去。
解决:用np.random.randn乘以一个缩放系数(如0.5)做初始化。缩放系数控制初始权重范围,一般让初始输出不要过大。判断初始化好坏的办法:训练前先跑一次forward,打印loss初始值,如果接近ln(3)≈1.0986,说明初始输出接近均匀分布,是合理的起点;如果初始loss特别大或接近0,初始化大概率有问题。
5.5 只看单次准确率,换个随机种子结果就变
现象:把random_state固定在42跑出95%,换个种子就只有86%,不知道模型真实水平。
原因:样本量太小,30条测试样本里多错一条就波动3个点以上。红酒更明显,测试集只有35条左右,结果方差天然就大。
解决:固定3~5个种子分别运行,报告均值±标准差;或者直接用sklearn的cross_val_score做5折交叉验证。交叉验证按5折分割训练数据本身也是一次完整训练,代码量增加不大,但得到的结果能扛住追问。我在报告里习惯写「5次独立运行,准确率均值92.4%,标准差3.1%」,这一句话比任何主观评价都管用。
6. 实验报告和答辩PPT:把源码价值讲清楚的三个落点
6.1 实验报告:三张图加一段流程说明
实验报告不是代码的复制粘贴,老师想看到的是「你确实理解了每个环节为什么这么做」。我的习惯是报告里固定放三张图:第一张是BP神经网络结构图,手动画出输入层4个节点、隐藏层8个节点、输出层3个节点,标清权重数量和前向/反向的信号去向;第二张是训练过程loss下降曲线,标注最终loss值;第三张是测试集混淆矩阵热力图,一眼看清哪两个类容易混。加上一小段「数据预处理流程图」文字描述,从加载、标准化、切分到训练的完整链路,这份报告的骨架就立住了。
6.2 答辩PPT:三个高频追问怎么接
PPT页面控制在10~12页,重点讲清三件事。第一,为什么用BP不用决策树——答非线性边界拟合能力和特征组合表达力,这个题目正好有鸢尾花和红酒两组数据集做对比,可以让评委看到可视化结果。第二,隐藏层节点数为什么选8和10——答经验法则加实验对比,把你试过hidden_dim=4、8、16的结果表格贴出来,说明8是综合准确率和稳定性的选择。第三,怎么判断过拟合——答训练集测试集准确率gap,加上loss曲线尾部是否回升,加上参数比样本量还多的实际情况。我每次答辩前习惯在本地多跑几组random_state的准确率波动记录在手边,评委真要细问时能直接报出均值和方差,这个细节比背课本定义加分得多。
这个标题的完整交付物是三件:能跑的代码、能自洽的实验报告、能应对追问的PPT。三件都到位,这个方向才算闭环。希望帮到你。
本文还有配套的精品资源,点击获取