人工神经网络入门:从感知机到反向传播的原理与实践 1. 从厨房盐罐到神经元为什么理解人工神经网络必须先理解“学习”本身你有没有试过第一次做一道新菜严格按照食谱操作结果端上桌的成品咸得发苦我第一次做四川麻婆豆腐时就犯了这个错——食谱写“适量豆瓣酱”我手一抖舀了整整三勺。尝一口舌尖瞬间麻木眼泪都快下来了。但第二次我记住了这个教训豆瓣酱不是调味品是整道菜的“权重因子”。它和牛肉末、花椒、豆豉之间存在一种微妙的平衡关系多一分则燥少一分则寡。这个过程就是最朴素的“机器学习”用错误反馈过咸修正参数豆瓣酱用量直到输出味道逼近理想状态麻辣鲜香。人工神经网络ANN的本质从来就不是什么高深莫测的黑箱它只是把这个厨房里的直觉用数学语言和电路逻辑一丝不苟地复刻了一遍。这恰恰解释了为什么“Perceptron感知机”是所有深度学习的起点。它不是一个复杂的模型而是一个极简的决策单元就像厨房里那个最基础的“尝味”动作。它接收几个输入食材每个输入乘以一个权重用量加总后经过一个“判断门限”你舌头的味觉阈值最终给出一个二元输出“太咸”或“刚好”。它的伟大不在于能力而在于结构——它第一次把“学习”这件事从哲学思辨拉进了可计算、可调试的工程领域。当你看到一个三层的前馈神经网络Feedforward Neural Network时别被“多层”吓住。它不过是把成百上千个这样的“厨房尝味员”组织起来第一层尝的是“有没有豆瓣酱的辣味”第二层尝的是“辣味麻味豆豉香是否协调”第三层才综合所有信息给出最终判决“这道菜合格”。整个网络的训练过程就是让每一个“尝味员”不断调整自己对每种味道的敏感度权重直到整套流程能稳定产出符合要求的成品。所以理解ANN核心不是背诵公式而是重建这种“试错-反馈-修正”的直觉。它解决的从来不是“如何计算”而是“如何让系统自己学会计算”。这也是它与传统规则引擎的根本分野后者像一本印在纸上的《川菜大全》条目清晰但无法应对新菜式而ANN则像一位在川菜馆后厨熬了十年的老师傅他的经验不在书本里而在每一次翻锅、每一次尝味、每一次微调火候的肌肉记忆中。这种基于经验的泛化能力正是它横跨医疗影像诊断、金融风控、自动驾驶等领域的底层逻辑——因为现实世界的问题从来就没有标准答案手册。2. 从单个神经元到多层网络架构演进背后的物理与数学必然性2.1 感知机一个有“偏置”的开关而非万能钥匙我们先拆解最原始的感知机Perceptron。它的数学表达极其简洁output step(w1*x1 w2*x2 ... wn*xn b)。这里的step是一个阶跃函数当加权和大于零时输出1否则输出0。初看之下它似乎能解决一切分类问题。但1969年Minsky和Papert在《Perceptrons》一书中给了它致命一击它无法解决最简单的“异或XOR”问题。XOR的真值表是(0,0)-0, (0,1)-1, (1,0)-1, (1,1)-0。你会发现无论你怎么画一条直线都无法将这四个点 cleanly 地分成两类。这暴露了单层感知机的根本局限——它只能学习线性可分的模式就像一个只会用直尺画线的裁缝永远做不出曲线剪裁的礼服。那么解决方案是什么不是给它换一把更锋利的“直尺”而是给它增加一个“缝纫机”。这个“缝纫机”就是隐藏层Hidden Layer。当我们引入至少一个隐藏层后网络结构就从Input - Output变成了Input - Hidden - Output。这个看似微小的改变带来了质的飞跃。隐藏层中的每个神经元都在学习输入数据的一个非线性组合。比如在图像识别中第一个隐藏层的神经元可能在学习“边缘检测”第二个隐藏层的神经元则可能在学习“边缘的特定排列方式如一个角”而输出层则综合所有这些“角”的信息最终判断这是“一只猫的耳朵”。这个过程本质上是在高维空间中用无数个微小的、可学习的“平面”去拟合一个极其复杂的“曲面”。单层网络只能画一个平面而多层网络则能用成千上万个平面堆叠出任何你想要的曲面形状。这就是“万能近似定理Universal Approximation Theorem”的直观含义一个具有足够多神经元的单隐藏层网络理论上可以以任意精度逼近任何连续函数。它不是魔法而是数学上严谨的“分段线性逼近”。2.2 前馈网络信息流的单行道与“无环图”的工程智慧多层感知机MLP是前馈神经网络Feedforward Neural Network最典型的代表。它的名字“前馈”二字精准地定义了其核心特征信息只能沿着一个方向流动从输入层经过一个或多个隐藏层最终到达输出层绝无回头路。这与人脑中神经元之间复杂的、双向甚至循环的连接截然不同。这种设计并非模仿生物而是源于深刻的工程考量。首先是可计算性。如果网络中存在循环即反馈那么一个信号可能会在环路中无限次地传递、放大导致系统不稳定甚至发散。前馈结构彻底规避了这个问题保证了每一次输入都会产生一个确定、唯一的输出这是构建可靠工程系统的基石。其次是训练的可行性。训练神经网络的核心是反向传播Backpropagation它依赖于链式法则来计算损失函数对每一个权重的梯度。如果存在循环链式法则的路径将变得无限长且不可穷举梯度计算将失去意义。前馈网络的“有向无环图DAG”结构为梯度的精确、高效计算提供了完美的拓扑基础。你可以把它想象成一条装配流水线原材料输入数据进入起点经过一系列固定的、顺序的加工工位隐藏层每个工位只负责自己那部分的“特征提取”最后在终点输出层组装成最终产品预测结果。这条流水线的设计确保了每一个工位的“工作绩效”即该层权重对最终误差的贡献都能被清晰、独立地衡量出来。提示很多初学者会困惑为什么RNN循环神经网络又允许循环答案是RNN的循环是“时间维度”上的它处理的是序列数据如一句话、一段音频循环代表的是“当前时刻的状态依赖于上一时刻的状态”。而MLP处理的是静态的、独立的数据点如一张图片其内部结构必须是严格前馈的以保证单次推理的确定性和训练的稳定性。2.3 激活函数非线性的“灵魂”决定网络的表达上限如果把神经网络比作一座大厦那么权重是钢筋偏置是水泥而激活函数就是赋予这座大厦以生命和功能的“门窗与隔断”。没有激活函数无论网络有多少层其整体输入输出关系都只是一个巨大的线性变换Output W3*(W2*(W1*Input b1) b2) b3。这完全可以被简化为一个单层网络Output W_final*Input b_final。所有的“深度”都将沦为冗余。因此激活函数的核心使命就是引入非线性Non-linearity。它像一个智能阀门根据输入信号的强度决定是“全开”、“半开”还是“关闭”从而打破线性束缚让网络有能力学习和表达世间万物那错综复杂的非线性关系。我们来对比几种主流激活函数的“性格”激活函数数学形式输出范围核心优势致命缺陷最佳应用场景Sigmoid1 / (1 e^(-x))(0, 1)输出可解释为“概率”平滑可导梯度消失输入过大或过小时导数趋近于0导致深层网络权重几乎无法更新输出层二分类问题Tanh(e^x - e^(-x)) / (e^x e^(-x))(-1, 1)相比Sigmoid输出均值为0中心化更好收敛更快同样存在梯度消失问题且计算比ReLU稍慢隐藏层早期网络已被ReLU大幅取代ReLUmax(0, x)[0, ∞)计算极快仅需一次比较有效缓解梯度消失正区间导数恒为1神经元死亡负输入时导数为0可能导致部分神经元永久失效绝大多数隐藏层现代深度学习的默认选择Leaky ReLUmax(α*x, x), α≈0.01(-∞, ∞)解决ReLU的“死亡”问题负区间有微小梯度引入额外超参数α效果提升有时不明显ReLU失效时的备选方案Softmaxe^(x_i) / Σe^(x_j)(0, 1)且和为1将任意实数向量转换为概率分布仅适用于多分类输出层不能用于隐藏层输出层多分类问题如ImageNet我亲身经历的一个教训是在训练一个图像分类模型时我固执地在所有隐藏层都用了Sigmoid。结果网络在训练初期进展神速但到了第50个epoch损失函数的下降曲线就彻底“躺平”了无论怎么调学习率都纹丝不动。后来我才明白是Sigmoid在深层网络中制造了“梯度沙漠”误差信号在回传途中被层层削弱最终抵达底层权重时已经微弱到无法驱动任何有效的更新。换成ReLU后同样的模型30个epoch就达到了之前100个epoch都达不到的精度。这个血泪教训让我深刻体会到激活函数不是可有可无的装饰而是决定网络能否“活”起来的关键器官。3. 反向传播一场精密的“责任追溯”与梯度更新实战3.1 反向传播的本质不是算法而是“责任田”划分法很多人把反向传播Backpropagation当成一个神秘的、需要死记硬背的算法。其实它最本质的内核是一种极其精妙的责任分配机制。想象一下你是一家大型公司的CEO公司刚发布了一款新产品但市场反馈惨淡。作为CEO你需要知道问题出在哪里。是研发部设计错了是市场部推广不力还是生产部品控失守反向传播要做的就是把这次“失败”的总责任即损失函数的值按照一套严格的数学规则一层一层、一个神经元一个神经元地精确地分摊到网络中的每一个权重w和偏置b头上。这个过程完全基于链式法则Chain Rule。假设我们的损失函数是L它最终依赖于输出层的激活值a^L而a^L又依赖于输出层的加权输入z^Lz^L又依赖于上一层的激活值a^(L-1)和权重w^L……如此层层递进。链式法则告诉我们∂L/∂w^L (∂L/∂a^L) * (∂a^L/∂z^L) * (∂z^L/∂w^L)。这个公式就是整个反向传播的“宪法”。它清晰地定义了要计算损失L对某一层权重w^L的影响就必须知道L对a^L的影响、a^L对z^L的影响以及z^L对w^L的影响。这三个“影响”就是我们在反向传播中需要依次计算的三个关键梯度。3.2 实战推演手把手走完一个完整迭代让我们用一个极简的两层网络1个输入、1个隐藏、1个输出来走一遍。假设输入x 1.0隐藏层权重w1 0.5, 偏置b1 0.1输出层权重w2 0.8, 偏置b2 0.2激活函数隐藏层用ReLU输出层用Sigmoid真实标签y 1.0损失函数二元交叉熵L -[y*log(a2) (1-y)*log(1-a2)]第一步前向传播Forward Pass计算隐藏层加权输入z1 w1*x b1 0.5*1.0 0.1 0.6应用ReLUa1 max(0, 0.6) 0.6计算输出层加权输入z2 w2*a1 b2 0.8*0.6 0.2 0.68应用Sigmoida2 1/(1e^(-0.68)) ≈ 0.663计算损失L -[1.0*log(0.663) 0.0*log(1-0.663)] ≈ -(-0.412) 0.412第二步反向传播Backward Pass我们的目标是计算∂L/∂w1,∂L/∂b1,∂L/∂w2,∂L/∂b2。计算输出层梯度∂L/∂a2 (a2 - y) / (a2*(1-a2))二元交叉熵对Sigmoid输出的导数(0.663 - 1.0) / (0.663*0.337) ≈ -1.52∂a2/∂z2 a2*(1-a2)Sigmoid导数0.663*0.337 ≈ 0.223∂z2/∂w2 a1 0.6∂z2/∂b2 1所以∂L/∂w2 ∂L/∂a2 * ∂a2/∂z2 * ∂z2/∂w2 (-1.52) * 0.223 * 0.6 ≈ -0.203∂L/∂b2 ∂L/∂a2 * ∂a2/∂z2 * ∂z2/∂b2 (-1.52) * 0.223 * 1 ≈ -0.339计算隐藏层梯度关键∂L/∂z2 ∂L/∂a2 * ∂a2/∂z2 (-1.52) * 0.223 ≈ -0.339这是“责任”从输出层传递到隐藏层的桥梁∂z2/∂a1 w2 0.8∂a1/∂z1 1 if z1 0 else 0ReLU导数1因为z10.60∂z1/∂w1 x 1.0∂z1/∂b1 1所以∂L/∂w1 ∂L/∂z2 * ∂z2/∂a1 * ∂a1/∂z1 * ∂z1/∂w1 (-0.339) * 0.8 * 1 * 1.0 ≈ -0.271∂L/∂b1 ∂L/∂z2 * ∂z2/∂a1 * ∂a1/∂z1 * ∂z1/∂b1 (-0.339) * 0.8 * 1 * 1 ≈ -0.271第三步权重更新Gradient Descent假设学习率η 0.1则w2_new w2 - η*∂L/∂w2 0.8 - 0.1*(-0.203) 0.8203b2_new b2 - η*∂L/∂b2 0.2 - 0.1*(-0.339) 0.2339w1_new w1 - η*∂L/∂w1 0.5 - 0.1*(-0.271) 0.5271b1_new b1 - η*∂L/∂b1 0.1 - 0.1*(-0.271) 0.1271你看整个过程就是一个严密的“因果链”。输出层的误差∂L/∂a2是源头它通过∂a2/∂z2这个“转化器”变成了对z2的责任∂L/∂z2再通过∂z2/∂a1这个“传导器”把这份责任的一部分精准地传递给了隐藏层的激活值a1。而a1的变化又由z1决定z1的变化则由w1和b1决定。反向传播就是沿着这条因果链逆向追溯把总责任分解到每一个螺丝钉上。这个过程不需要任何“智能”只需要扎实的微积分和一丝不苟的执行。3.3 梯度下降在“损失山峦”中寻找最低谷的登山策略反向传播计算出了梯度∂L/∂w但这只是指明了“下山”的方向。如何迈出每一步才是梯度下降Gradient Descent的学问。它就像是一个盲人在一座由损失函数构成的、高低起伏的山峦中摸索下山。基础版Batch GD每次迭代都用全部训练数据计算一次损失和梯度然后更新一次权重。优点是路径稳定缺点是计算量巨大对于百万级数据集一次迭代就要遍历全部数据效率极低。随机版SGD每次迭代只随机抽取一个样本计算其损失和梯度然后立即更新权重。优点是速度飞快内存占用小且随机性有助于跳出局部最优。缺点是路径极其“崎岖”损失曲线像心电图一样上下剧烈波动收敛过程不稳定。小批量版Mini-batch GD这是工业界的绝对标准。每次迭代随机抽取一个小批量Batch Size的样本如32、64、128个计算它们的平均损失和平均梯度再更新权重。它完美地平衡了Batch GD的稳定性和SGD的高效性。我的经验是batch_size32是一个非常稳健的起点对于大多数CV任务64或128也能获得很好的效果。选择过大的batch size如1024虽然单次迭代更快但往往会导致模型收敛到一个“尖锐”的最小值泛化能力反而变差而过小的batch size如8则会让训练过程过于嘈杂难以收敛。注意学习率Learning Rate是梯度下降中最关键的超参数。它决定了你每一步迈得多大。太大你会在山谷两侧疯狂弹跳永远落不到谷底loss震荡不降太小你爬得比蜗牛还慢可能一辈子都到不了loss下降极其缓慢。一个实用的技巧是使用“学习率预热Learning Rate Warmup”在训练初期让学习率从一个极小的值如1e-6线性增长到设定的初始值如1e-3这能帮助网络在训练初期更平稳地建立初始特征表示避免因初始梯度过大而崩溃。4. 模型评估、调优与避坑从“能跑”到“跑得稳、跑得远”的实战心法4.1 数据集划分不是60-20-20而是“三权分立”的治理结构教科书上常说“按60:20:20划分数据集”但这只是一个粗略的指导。在真实项目中数据集的划分是一场关乎模型生死的“权力制衡”。训练集Training Set这是模型的“练兵场”。它的唯一使命就是让模型尽可能多地接触各种各样的数据学习其中的模式。我通常会把70%-80%的数据放在这里。但有一个铁律训练集必须是“纯净”的不能包含任何未来的信息No Future Leakage。例如如果你在做股票价格预测训练集里绝不能出现测试集日期之后的新闻事件哪怕只是时间戳标错了都可能导致模型学到虚假的相关性。验证集Validation Set这是模型的“考官”和“教练”。它的核心价值不在于评估最终性能而在于指导训练过程。你用它来监控过拟合当训练损失持续下降而验证损失开始上升时这就是模型在“死记硬背”训练数据却丧失了泛化能力的明确信号。此时你应该立刻停止训练Early Stopping。调优超参数学习率、网络层数、每层神经元数量、Dropout比率……这些无法通过反向传播学习的参数都需要在验证集上反复试验找到最优组合。选择模型架构是用ResNet-18还是ResNet-50是加一个Attention模块还是不加这些重大决策都应该由验证集的性能说了算。测试集Test Set这是模型的“终审法庭”。它必须是绝对神圣不可侵犯的。在整个开发周期中你只能用它进行最后一次、最终的评估。在此之前任何对测试集的“偷看”哪怕只是用它来挑一个最好的checkpoint都会污染你的评估结果让你对模型的真实能力产生幻觉。我有个习惯会在项目初期就把测试集文件夹设置为“只读”并用密码保护直到所有开发和调优工作彻底完成才解锁它。4.2 过拟合与欠拟合模型的两种“亚健康”状态诊断指南模型的性能表现常常会陷入两种极端的“亚健康”状态它们的诊断和治疗是每个从业者必须掌握的基本功。状态训练损失验证损失根本原因典型症状解决方案欠拟合Underfitting高高模型太“笨”容量不足连训练数据的规律都学不会模型在训练集上就表现很差准确率远低于基线水平增加模型复杂度加层、加神经元、换更强的架构如从MLP换到CNN减少正则化降低Dropout比率、减小L2权重衰减系数延长训练时间过拟合Overfitting很低很高模型太“聪明”过度记忆了训练数据的噪声和细节失去了泛化能力模型在训练集上准确率接近100%但在验证/测试集上骤降至70%甚至更低增加正则化提高Dropout比率、增大L2权重衰减系数数据增强对图像做旋转、裁剪、色彩抖动早停Early Stopping在验证损失不再下降时立即停止训练简化模型减层、减神经元我曾经在一个文本情感分析项目中遭遇了典型的过拟合。模型在训练集上F1分数高达0.98但在验证集上只有0.72。我最初的反应是“加数据”但客户能提供的标注数据非常有限。后来我尝试了“早停”效果甚微。最终我启用了Dropout在隐藏层后加入Dropout(0.5)并配合L2正则化在损失函数中加入λ*Σw²项λ0.001。结果验证集F1分数立刻提升到了0.85。这个案例让我明白对抗过拟合不是靠蛮力堆数据而是靠精巧的“约束”艺术。Dropout就像在训练时随机地“蒙住”一部分神经元的眼睛强迫网络不能依赖任何一个特定的神经元而必须学会一种更鲁棒、更分布式的特征表示。4.3 常见问题排查速查表那些让你抓耳挠腮的“幽灵Bug”在无数次的模型训练中我总结了一份高频问题的“幽灵Bug”排查清单这些都是血泪教训换来的问题现象最可能原因排查与解决步骤我的实操心得Loss为NaNNot a Number1. 学习率过大导致权重爆炸2. 损失函数中出现log(0)或除零3. 输入数据包含NaN或Inf1.立刻将学习率降低10倍2. 在代码中加入torch.isnan(x).any()或np.isnan(x).any()检查所有张量3. 使用torch.nn.utils.clip_grad_norm_()对梯度进行裁剪这是最紧急的Bug。一旦出现不要犹豫立刻中断训练。我习惯在训练脚本开头就加上梯度裁剪max_norm1.0是一个安全的起点。Loss不下降卡在高位1. 学习率过小2. 激活函数选择错误如在隐藏层用了Sigmoid3. 权重初始化不当全零或过大4. 数据未归一化/标准化1. 尝试将学习率提高10倍2.检查并更换为ReLU3. 确保使用He初始化ReLU或Xavier初始化Sigmoid/Tanh4. 对输入特征做StandardScaler或MinMaxScaler“卡住”是新手最常见的问题。我的第一反应永远是检查数据、检查激活函数、检查初始化。三者齐备90%的问题都能解决。训练Loss下降验证Loss上升过拟合1. 模型容量过大2. 训练轮次过多3. 缺乏正则化1. 启用Early Stopping耐心等待2. 加入Dropout3. 增加L2权重衰减4. 对图像数据启用数据增强不要迷信“多训几轮”。我见过太多人为了追求训练集上的0.1%提升把模型训到验证集性能腰斩。早停是性价比最高的正则化手段。GPU显存不足OOM1. Batch Size过大2. 模型过于庞大3. 中间变量未及时释放1.将Batch Size减半这是最快捷的方案2. 使用torch.utils.checkpoint进行梯度检查点3. 在with torch.no_grad():块中进行推理避免保存中间梯度显存是硬件瓶颈没有银弹。我的黄金法则是先用最小的Batch Size如8跑通整个流程再逐步增大直到显存告警。这样能快速定位是模型问题还是数据问题。模型预测结果全是同一个类别1. 输出层激活函数错误如多分类用了Sigmoid2. 损失函数与任务不匹配如多分类用了Binary Cross Entropy3. 数据严重不平衡且未加类别权重1.多分类必须用Softmax CrossEntropyLoss2. 检查损失函数文档确认其输入要求3. 使用sklearn.utils.class_weight.compute_class_weight计算类别权重这个Bug很隐蔽。有一次我把nn.CrossEntropyLoss和nn.Softmax连用结果模型直接“瘫痪”。记住CrossEntropyLoss内部已经包含了Softmax切勿重复添加。5. 从理论到实践一个端到端的手写数字识别项目复现5.1 项目蓝图用最简架构跑通深度学习全流程现在让我们把前面所有理论揉进一个真实的、可运行的项目里MNIST手写数字识别。这不是一个玩具而是深度学习的“Hello World”它完美地涵盖了数据加载、模型定义、训练、验证、测试、评估的全部环节。我们将用PyTorch实现一个经典的三层MLP目标是达到97%以上的测试准确率。核心组件与版本框架PyTorch 2.0数据torchvision.datasets.MNIST模型nn.Sequential定义的MLP784 - 128 - 64 - 10优化器torch.optim.Adam损失函数nn.CrossEntropyLoss硬件CPU或GPU代码自动适配5.2 关键代码解析每一行都是经验之谈import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader from torchvision import datasets, transforms # 1. 数据预处理这是成败的关键一步 # MNIST图像是28x28的灰度图像素值0-255。我们必须将其归一化到[0,1]。 transform transforms.Compose([ transforms.ToTensor(), # 自动将PIL Image转为Tensor并将像素值除以255 transforms.Normalize((0.1307,), (0.3081,)) # 使用MNIST数据集的全局均值和标准差进行标准化 ]) # 提示Normalize的均值和标准差不是随便写的。它们是通过对整个训练集计算得到的。使用正确的值能让模型收敛更快、更稳。 # 2. 数据集加载与划分 train_dataset datasets.MNIST(root./data, trainTrue, downloadTrue, transformtransform) test_dataset datasets.MNIST(root./data, trainFalse, downloadTrue, transformtransform) # 划分验证集从训练集中拿出10% train_size int(0.9 * len(train_dataset)) val_size len(train_dataset) - train_size train_dataset, val_dataset torch.utils.data.random_split(train_dataset, [train_size, val_size]) # 创建DataLoader启用shuffle和num_workers加速 train_loader DataLoader(train_dataset, batch_size64, shuffleTrue, num_workers2) val_loader DataLoader(val_dataset, batch_size1000, shuffleFalse, num_workers2) test_loader DataLoader(test_dataset, batch_size1000, shuffleFalse, num_workers2) # 3. 模型定义一个标准的MLP class SimpleMLP(nn.Module): def __init__(self, input_size784, hidden1128, hidden264, num_classes10): super(SimpleMLP, self).__init__() self.network nn.Sequential( nn.Linear(input_size, hidden1), nn.ReLU(), # 必须 nn.Dropout(0.2), # 正则化防止过拟合 nn.Linear(hidden1, hidden2), nn.ReLU(), nn.Dropout(0.2), nn.Linear(hidden2, num_classes) # 注意这里没有SoftmaxCrossEntropyLoss内部已包含 ) def forward(self, x): x x.view(x.size(0), -1) # 将28x28的图像展平为784维向量 return self.network(x) model SimpleMLP() # 4. 训练循环核心逻辑在此 device torch.device(cuda if torch.cuda.is_available() else cpu) model.to(device) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr0.001) # Early Stopping参数 best_val_acc 0.0 patience 5 trigger_times 0 for epoch in range(10): model.train() running_loss 0.0 for i, (images, labels) in enumerate(train_loader): images, labels images.to(device), labels.to(device) # 前向传播 outputs model(images) loss criterion(outputs, labels) # 反向传播 optimizer.zero_grad() # 清空上一轮的梯度这是新手最常忘的一步 loss.backward() optimizer.step() running_loss loss.item()