深度学习第三天这个标题本身就很有意思。如果你正处于这个阶段大概率已经熬过了环境配置的第一天挺过了被矩阵运算和导数折磨的第二天正处在一种“好像懂了一点但又说不清自己到底会什么”的微妙状态里。这篇文章就是写给此刻的你一个刚摸到深度学习门槛、还没完全推开门的新手。今天不谈大模型不聊Transformer只把第三天真正该做的几件事讲透——手动跑通一次反向传播亲手训练一个最基础的多层感知机并且搞清楚每一个环节到底在做什么。对于已经在搬砖的开发者也可以用十分钟快速扫一眼看看有没有理解不到位的地方。1. 第三天开跑前先把前两天的基础盘点清楚1.1 环境与工具链别在起跑线上反复折腾关于环境我给新手一个直到今天仍然适用的建议不要追求一步到位也不要为了“显得专业”去折腾GPU版本。第三天这个阶段你的任务不是训练大规模模型而是理解梯度从loss一路回传到每个参数的过程CPU完全够用甚至更合适——因为CPU上更容易观察每一步的运行逻辑出了问题也更可控。我见过有个朋友第一天装环境就花了一整天从Python版本、显卡驱动一路排查下来最后发现只是某个镜像源配置不对。装环境这件事卡两天就算失败案例了。如果你现在还在环境里挣扎直接在干净的环境里用最新稳定版Python配合官方渠道安装不带GPU的深度学习框架即可。具体记不清命令也没关系搜索“对应框架 安装 官方”比记笔记更实际。第三天真正要用到的工具清单其实很短一个支持代码单元格的笔记本环境最新稳定版的深度学习框架matplotlib用来画loss曲线和数据分布再加一个sklearn用来生成实验数据。这四个就够一个都不需要多装。如果已经装了其他框架也没事但我个人建议第三天先统一用同一个框架练手免得被两套API来回切换搞到怀疑人生。1.2 从感知机到损失函数第三天需要的“最小前置知识”前两天你大概率接触过感知机。很多人学到这里会有一个困惑“单个感知机只能处理线性分类我学它干什么”实际上感知机是整个深度学习最小的积木。输入数据乘上权重加上偏置过一个激活函数得到输出——这句话第三天依然成立只不过积木更多了堆叠成了“多层感知机”。第三天开始动手前有三个概念必须真懂不是看过就算懂损失函数衡量模型预测和真实答案的差距是深度学习的“方向盘”。拿MSE举例预测值越接近真实值损失越小。梯度损失函数对每个参数的导数。梯度告诉你的是“如果把这个参数往某个方向调一点点损失是变大还是变小”。梯度下降就是沿着让损失变小的方向更新参数。学习率每次参数更新的步幅。步幅太大容易直接跳过最低点太小则需要很久才能走到。这三个概念不需要你数学推导得非常严谨但一定要能用自己的话解释清楚。一个特别形象的理解方式是把损失函数想象成一片起伏的山地你站在某个位置手里有一个高度计loss梯度就是脚下山坡的倾斜方向学习率是你每步迈多远。深度学习训练本质上就是在天黑之前“摸着坡往下走”走到最低点附近就算训练成功了。有了这个基础第三天的重头戏就可以开始了把“梯度到底怎么从最末尾的loss传到最前面的参数”这件事亲手拆开看一遍。这就是反向传播。2. 第三天的核心任务亲手跑通反向传播2.1 手动推一遍反向传播比看十遍视频都管用很多课程会把反向传播讲成“链式法则的应用”这句话对但对新手来说太抽象了。所以我建议第三天第一件事拿草稿纸把一个极小的网络手动算一遍。别看视频里别人推得轻松自己动手写过一次之后backward()在你眼里就不再是黑魔法了。我们构造一个最简单的网络。输入 x1真实标签 y0。模型只有两层隐藏层一个神经元输出层一个神经元激活函数都用sigmoid。初始参数随便定一组好算的w10.5b10w20.5b20。损失函数用均方误差写成 L 1/2 (y_pred - y)^2前面的1/2是为了求导之后式子干净不影响结果。前向传播就是从输入一路计算到lossz1 w1 * x b1 0.5 h sigmoid(0.5) ≈ 0.62246 z2 w2 * h b2 0.5 × 0.62246 ≈ 0.31123 y_pred sigmoid(0.31123) ≈ 0.57719 L 0.5 × (0.57719 - 0)^2 ≈ 0.16658这个loss告诉我们现在的预测离0还有点远需要更新参数。接下来就是反向传播核心只有一件事——用链式法则从后往前把loss对每个参数的“敏感度”即梯度算出来。先算loss对输出层的梯度dL/dy_pred y_pred - y ≈ 0.57719 dL/dz2 dL/dy_pred × sigmoid(z2) ≈ 0.57719 × 0.57719 × (1 - 0.57719) ≈ 0.14085 dl/dw2 dL/dz2 × h ≈ 0.14085 × 0.62246 ≈ 0.08767 dl/db2 dL/dz2 ≈ 0.14085然后梯度继续往前传到隐藏层dl/dh dL/dz2 × w2 ≈ 0.14085 × 0.5 ≈ 0.07043 dl/dz1 dl/dh × sigmoid(z1) ≈ 0.07043 × 0.62246 × (1 - 0.62246) ≈ 0.01655 dl/dw1 dl/dz1 × x ≈ 0.01655 dl/db1 dl/dz1 ≈ 0.01655注意观察梯度是从loss出发逐层往前“倒推”出来的。某一层的梯度一定依赖后一层传回来的梯度和这一层前向计算时的中间结果。这就是“反向传播”四个字的含义。有了梯度就可以用梯度下降更新参数了。设定学习率 lr0.5w1_new 0.5 - 0.5 × 0.01655 ≈ 0.49173 w2_new 0.5 - 0.5 × 0.08767 ≈ 0.45617 b1_new 0 - 0.5 × 0.01655 ≈ -0.00828 b2_new 0 - 0.5 × 0.14085 ≈ -0.07043更新完成后重新前向计算loss会从0.16658微微下降。训练一轮下来loss只要变小就说明参数更新的方向是对的。你可以自己再手推一两轮感受一下。如果推完第三轮发现loss继续下降你其实已经掌握了深度学习训练最核心的机制。2.2 用代码验证手算结果自动求导不是黑魔法很多初学者第一次用自动求导时看到一行backward就能拿到全部梯度会觉得很神奇。其实它的原理和上面手算一模一样只是把链式法则自动跑了一遍。为了让你彻底放心这里再用代码验证一下手算的结果。import torch x torch.tensor([1.0]) y torch.tensor([0.0]) w1 torch.tensor([0.5], requires_gradTrue) b1 torch.tensor([0.0], requires_gradTrue) w2 torch.tensor([0.5], requires_gradTrue) b2 torch.tensor([0.0], requires_gradTrue) z1 w1 * x b1 h torch.sigmoid(z1) z2 w2 * h b2 y_pred torch.sigmoid(z2) loss 0.5 * (y_pred - y).pow(2).sum() loss.backward() print(w1.grad.item(), b1.grad.item()) # 大约 0.01655, 0.01655 print(w2.grad.item(), b2.grad.item()) # 大约 0.08767, 0.14085运行结果应该会非常接近手算的数值。这里有几个细节新手容易忽略只有设置 requires_gradTrue 的张量会被记录梯度。backward() 之后梯度会累加到 .grad 里所以每次更新完要用 zero_grad() 手动清零否则下一轮梯度会叠加出错。中间变量也在计算图里等到内存释放后就不能再取它们的梯度了。实际工程里基本只关心参数层的梯度。你在笔记本里把这段代码跑一遍和手算结果对比。如果数值能对上说明你既能“看见”反向传播又会用工具验证自己学得对不对——这个能力会一直陪着你走完后期的路。3. 实战训练一个真正能分类的多层感知机3.1 数据准备与加载从二维月亮数据开始最合适手推和验证都做完后可以上一个有真实感的任务了。我推荐用二维月亮数据做二分类。为什么因为这种数据是从一个月牙形分布里采样出来的有明显的非线性边界用单层感知机很难分类但用一个小小的多层感知机就能轻松搞定——正好能让你体会到“多层”带来的效果提升。先造数据。下面的代码会生成200个二维样本标签只有0和1from sklearn.datasets import make_moons import torch from torch.utils.data import TensorDataset, DataLoader X, y make_moons(n_samples200, noise0.1, random_state0) X_tensor torch.tensor(X, dtypetorch.float32) y_tensor torch.tensor(y, dtypetorch.float32).unsqueeze(1) dataset TensorDataset(X_tensor, y_tensor) loader DataLoader(dataset, batch_size16, shuffleTrue)这里有两个细节值得说一下。第一y要加一个维度。用 torch.tensor(y) 得到的是形状 (200,) 的向量但模型输出的是 (200, 1)直接用BCELoss计算时会对不上。所以用 unsqueeze(1) 把它变成 (200, 1)。第二数据要先转成浮点型。很多人后面会遇到“expected float but got long”之类的报错就是因为整数张量没有转 float32。DataLoader的作用是把数据分成一小批一小批地喂给模型batch_size16 表示每批16个样本shuffleTrue 让每轮epoch的数据顺序被打乱。小批量训练的意义不只是省内存它还能让梯度更新带一点随机性有时候反而能帮助模型逃离局部较差的位置。3.2 模型搭建与训练循环四行代码跑通一个网络有了数据可以定义模型了。今天只需要一个最简单的多层感知机import torch.nn as nn model nn.Sequential( nn.Linear(2, 16), nn.ReLU(), nn.Linear(16, 1), nn.Sigmoid() ) criterion nn.BCELoss() optimizer torch.optim.SGD(model.parameters(), lr0.1)模型结构是2个输入特征 → 16个隐藏神经元 → 1个输出概率。隐藏层的激活函数选ReLU而不是sigmoid是因为sigmoid在两端饱和时梯度接近0多层堆叠后会快速传播消失ReLU在大范围内梯度稳定是1训练更稳。这个问题第三天先有个概念后续做深层网络时会深有体会。损失函数选BCELoss二值交叉熵而不是MSE原因有两个二分类问题模型输出是概率交叉熵衡量的是“预测分布和真实分布之间的差异”数学性质更匹配交叉熵的梯度在预测很差时会比较大而MSE在输出层可能因为sigmoid饱和导致梯度很小训练效率偏低。训练循环看起来非常简单核心就四行for epoch in range(200): for x_batch, y_batch in loader: pred model(x_batch) loss criterion(pred, y_batch) optimizer.zero_grad() loss.backward() optimizer.step()很多新手看到这段代码会以为“训练”的秘诀就在这里。实际上这四行的顺序是有讲究的先算预测再算losszero_grad清零上一轮梯度backward算出梯度step沿着梯度更新参数。这四行你会在后面的每一个模型里反复见到它们就是整个深度学习训练循环的核心骨架。如果你想知道训练是否有效最直接的办法是记录每一个epoch结束后的loss。我建议用一个列表把历史值存下来loss_history [] for epoch in range(200): epoch_loss 0.0 for x_batch, y_batch in loader: pred model(x_batch) loss criterion(pred, y_batch) optimizer.zero_grad() loss.backward() optimizer.step() epoch_loss loss.item() * x_batch.size(0) loss_history.append(epoch_loss / len(X_tensor))训练结束后用matplotlib把曲线画出来看一眼。一个正常的训练过程应该是loss整体向下走初期下降比较快后期趋于平缓。如果你看到的是锯齿状震荡多半是学习率偏大如果是平直线多半是梯度没更新出来。3.3 训练结果的解读loss降到多少才算正常第三天最容易出现的心理活动是“我的loss好像不降了是不是模型坏了”先放个标准今天这种小规模分类任务初始loss通常在0.69附近因为刚开始sigmoid输出接近0.5交叉熵计算值约为0.69训练到200轮左右loss应该能降到0.2左右准确率可以达到90%以上。这个数值本身不重要重要的是曲线呈现“下降→变缓→平稳”的形态。判断模型学得好不好不要只看loss还要看准确率。用下面这段代码把预测和标签对比一下preds model(X_tensor) 0.5 correct (preds.float() y_tensor).sum().item() accuracy correct / len(y_tensor) print(faccuracy: {accuracy:.2%})如果准确率超过90%说明这个两层的网络确实把非线性边界学出来了。如果只盯着loss看不下降其实很可能准确率已经很高只是尾部样本让loss无法再降。所以以后每次训练把loss曲线和准确率放在一起看才不会被单一指标误导。另一个值得做的事情是画决策边界。在二维平面上把每个点预测成0或1的区域用不同颜色画出来你能直观看到模型学到的分界线。这种可视化能力很重要——它能帮你在跑大模型之前快速判断“模型是不是在乱猜”。4. 第三天高频踩坑我从新手阶段总结的教训4.1 loss纹丝不动先查初始化、学习率和梯度我见过太多新手在第三天卡在这样一个现象上loss一开始是0.69训练了50轮还是0.69纹丝不动。很多人第一反应是“网络结构写错了”但大概率不是。排查顺序应该固定。先看模型输出的pred是不是几乎全是0.5——如果是说明参数根本没有有效更新。然后手动检查第一次backward之后的梯度如果梯度全部是0问题几乎一定出在输入数据范围异常、loss函数写错或者模型输出被某个操作“断开”了。如果梯度有值但loss不变大概率是学习率设得离谱地小或者大。对新手来说把学习率从0.1改成0.01再试一次比反复怀疑模型结构有效得多。如果换了SGD还不行可以直接换Adam优化器。不是说要迷信Adam只是在第三天这个阶段Adam对学习率的敏感度更低能让新手把注意力集中在“理解训练循环”上。等后面经验多了再回头研究原理。也别忘了检查数据范围。如果输入里有未归一化的极大数据sigmoid这类激活函数会直接饱和梯度趋近于0训练自然停摆。所以第三步先把输入范围压到0附近的小数值区域再重跑一次。如果loss下降了一点点但后面再也不动可以尝试调大学习率。另外如果模型初始化时权重全为0所有神经元算出来的梯度会完全相同网络会退化得很严重。这是第三天很经典的坑全0初始化对多层感知机是大忌框架默认的随机初始化方式反而是安全选择。4.2 过拟合出现得比预想早别慌这很正常很多新手以为过拟合是后面才需要考虑的事其实在第三天这个小网络里就会遇到。比如训练集200个样本训练100轮后训练集loss降到0.05但测试集loss停在0.3。这说明模型开始“背答案”而不是“找规律”了。第三天遇到过一次反而是好事。因为过拟合和欠拟合这对概念只有亲自看到一次现象才能真正理解。什么算欠拟合训练和测试的loss都高就是模型能力不够处理不了数据里的非线性结构。什么算过拟合训练还在降测试不降甚至上升就是模型在记忆训练集。这套判断标准适用于之后所有模型越早建立越好。如果你发现过拟合第三天不要求彻底解决但要至少知道两条缓解思路一是减小模型容量比如把16个隐藏神经元减成4个通常能直接改善二是加Dropout或L2正则让模型不要那么“用力”地记住每个样本。等后续学习的数据量变大再系统地学正则化技术。我自己的经验是新手第三天先不用刻意练习调参但要在训练日志里同时记录训练集和测试集的loss曲线。这一步的价值会在第四天、第十天逐渐显现出来。4.3 张量形状报错一个debug模板解决大部分问题第三天写代码时你遇到的报错里多半是维度匹配问题。这类报错其实很固定只要掌握打印shape的习惯基本都能快速定位。下面这些是初学者代码里最常见的场景报错场景可能原因标准处理model(x_batch) 时报错 mat1 and mat2 shapes cannot be multiplied输入张量不是二维 (batch, features)检查x_batch.shape必要时用x.unsqueeze(0)补一个batch维BCELoss 报错 target size must match input size预测是 (batch,1)标签是 (batch,)训练前给标签加维度 y.unsqueeze(1)backward() 报错 element 0 of tensors does not require grad参数忘了requires_gradTrue检查模型参数是否被包在nn.Linear里正常创建训练中 loss 出现 nan学习率过大或数据包含异常值调低学习率检查输入有无nan每次排查维度问题我都会在模型forward之前加两行print(input shape:, x_batch.shape) print(target shape:, y_batch.shape)看到形状之后问题通常一眼就能发现。想更严格一点可以在调用模型前加断言让程序在错误发生前就拦住问题。这个习惯会让调试变得非常舒服后面接触CNN时输入维度更复杂靠shape日志能帮你省下大量时间。这些坑看起来都很“小”但如果你在第三天就能自己解决说明你已经具备了新手阶段最重要的独立调试能力。不要觉得这些是不值一提的琐事工程上大部分时间都在处理形状、类型、范围这些小问题能快速定位它们是很实用的能力。5. 第三天结束后下一步怎么学才不会断档5.1 先别急着上CNN把优化器吃透再说把今天的手推反向传播和MLP分类都跑通之后很多人会想明天是不是可以上CNN了我的建议是先别急花一整天把优化器摸透收获会更大。CNN和MLP的区别主要在模型结构训练流程完全没变还是那四行训练循环。真正会影响你能不能稳定训出一个好模型的因素反而是优化器里的这些细节梯度下降、随机梯度下降、冲量Momentum、自适应学习率Adam。你要知道小批量训练为什么会让loss曲线震荡动量是怎么帮助越过局部洼地的Adam为什么对学习率没那么敏感这些问题看起来偏底层但它们决定了你以后训练任何一个网络时遇到loss不降能不能自己定位问题。建议的第四天清单是这样的先用SGD不同学习率训练同一个MLP画三条loss曲线观察差异然后换Momentum和Adam对比速度和稳定性最后手动实现一个非常简化的Adam更新公式哪怕运行结果和官方库不完全一致只要过程理解了你对“优化器无非也是梯度下降的变体”这句话会有非常扎实的感受。有了这个基础后面上CNN、RNN、Transformer时脑子里就有一条清晰的“模型换、流程不变”的主线不会被每个新架构带偏。5.2 把今天的成果浓缩成一张速查清单第三天的信息量很大我建议在结束前把今天做的事浓缩成一张属于你自己的清单。它以后可以当成模板每次接到新任务按这个清单走一遍思路会非常清晰准备数据装进TensorDataset DataLoader确认shape和dtype必要时归一化。定义模型小步快跑从最简单结构开始逐步加容量。配置学习信号损失函数、优化器、学习率。训练循环前向→算loss→zero_grad→backward→step永远保持这个顺序。记录过程每个epoch保存loss和准确率画曲线。观察判断loss是否稳定下降训练/测试是否分离形状是否对。这张清单的价值在于今后你换数据、换模型、换任务核心骨架都不变变的只是其中某一块。比如做图像分类时把“MLP模型”换成“CNN模型”数据加载换成图像读取做文本分类时再换成“嵌入加RNN”。框架不变替换组件这就是深度学习工程里最底层的应用模式。另外再提一个小技巧每次训练完把关键参数比如学习率、隐藏层大小、最终准确率记录在一个小表格里。这能帮你建立起数字直觉——同样是两层MLP隐藏层从16加到32准确率提升并不明显但训练时间翻倍了。有了这些真实数据后面调参就不是拍脑袋了。写到这里第三天该做的事基本就讲完了。我个人带过不少初学者三天是一个很明显的分水岭前一天还可以靠看视频、抄代码蒙混过关但从今天手动推反向传播开始不亲自动手算一遍后续每一个环节都会带着“好像懂了但又不会写”的别扭感。我自己当年也在这个位置卡过很久后来发现真正让我转过弯来的就是拿草稿纸把那个小网络一层一层算到底。所以如果你今天已经把反向传播推通了还亲手训出一个loss顺利下降的网络那恭喜你后面所有模型的学习本质上都是在今天这套流程上换组件。接下来就是见多识广的问题了慢慢来不用急。
阅读完成 · 觉得有帮助?