没人告诉你实话深度学习的第三天才是大多数人真正放弃的时候。第一天新鲜感还在pip install 敲得噼里啪啦响跟着教程跑通了第一个 MNIST 手写数字识别感觉自己已经站在人工智能之巅。第二天稍微有点不对劲反向传播的公式开始像天书但还能咬牙往下面看。到了第三天问题彻底暴露了你发现前面跑通的代码只是别人的搬运轮到自己从头写一个模型脑子里一片空白。你开始怀疑自己是不是太笨又或者深度学习这件事压根不适合你。但我作为过来人负责任地讲——第三天学不下去不是因为难而是因为你还在用“看教程”的方式学一个“需要上手练”的技能。这个阶段的信息密度和实操强度和前两天完全不在一个量级。这篇内容就是写给处于第三天前夜的你帮你把这个阶段的学习路径理顺梳理哪些事必须亲手做、哪些可以暂缓我只讲实际的、可复现的操作。既然已经坚持到了第三天别让进度止步于此。1. 第三天究竟卡在哪儿1.1 前两天的“假会”与“真不懂”交织先回顾一下前两天的典型体验。第一天你大概率知道了什么是神经网络、什么是感知机、激活函数到底在干什么然后用某个深度学习框架跑通了一个图像分类的 Demo。整个过程像坐滑梯顺得太快顺利得让人误以为自己已经掌握了这项技能。第二天剧情急转直下。你开始接触反向传播、梯度下降、学习率、损失函数这些概念。当你试图把一个三层的全连接网络从零开始用 Numpy 实现一遍时一切都变得模糊起来。梯度到底怎么传的为什么要对权重求偏导学习率调大调小又有什么区别这些疑问像滚雪球一样越滚越大而你手头的教程只是轻描淡写地说一句“反向传播是深度学习的核心”连个像样的展开都没有。于是你卡住了闷头看了一整天公式推导代码一行没写。1.2 第三天要完成一次认知升级其实走到这里恰恰说明你在做正确的事。深度学习这种工程属性极强的领域恰恰禁止一直停留在“看懂”的舒适区。第三天的一个核心目标就是要完成一次认知升级从“我知道概念”变成“我能用这些概念解决一个像样的任务”。这个像样的任务不该再是教程里手把手教你做的 MNIST而是那种只给了数据和目标、剩下全凭自己折腾的小型实战题。比如找一份猫狗图片数据集自己动手写数据加载、搭建模型、定义损失函数、写训练循环、观察指标、保存模型、做预测。我做这个选题的初衷就是帮你走通这样一趟独立实战的流程。你不用一次达到多高的准确率关键是把每个环节亲手操作一遍遇到问题再回来查资料。下面所有内容都围绕这条主线展开。2. 第三天必须亲手做对的一件事从数据到模型的完整闭环2.1 确立一个“跳一跳够得着”的目标学深度学习最怕一开始就搞大动作比如想复现某个顶会论文或者做一个堪比商业级的产品。第三天的体力只够支撑一个中等偏小的目标。我建议你找一个公开的、二分类的图像数据集或者把手上能找到的图片素材按类别分好。核心标准有三条数据总量不宜太大几百到一两千张足够、类别不要超过三类、图片内容差异要明显。就以猫狗分类为例。拿到这批数据后先别急着打开编辑器写模型先把文件夹结构整理成下面这个形态data/ train/ cat/ cat_001.jpg cat_002.jpg dog/ dog_001.jpg val/ cat/ dog/训练集和验证集分开存放这是所有后续操作的基础。很多初学者喜欢把所有图片一股脑塞进去等代码跑起来才想起连训练集和测试集都混在一起这是极为危险的因为它会直接污染后续的一切指标评估。你在第三步搭建训练代码时所有数据划分的假设都会被这份目录结构影响。我之前带过一位朋友他用了乱七八糟的目录结果模型训练时验证集里混进了大量训练图片准确率高得吓人一测真实场景立刻露馅了。2.2 写一个属于自己的 DataLoader现在要做的不是调用框架里现成的高层数据接口而是自己动手写数据加载逻辑。这一步看似繁琐实际上能帮你建立对数据流最直接的体感。以 PyTorch 为例核心要做的事就两件找到所有图片路径和标签然后在训练时一批一批地取出来、做预处理。from torch.utils.data import Dataset from PIL import Image import os class CatDogDataset(Dataset): def __init__(self, root_dir, transformNone): self.root_dir root_dir self.transform transform self.samples [] for label_name in [cat, dog]: class_dir os.path.join(root_dir, label_name) label 0 if label_name cat else 1 for fname in os.listdir(class_dir): if fname.lower().endswith((.jpg, .jpeg, .png)): self.samples.append((os.path.join(class_dir, fname), label)) def __len__(self): return len(self.samples) def __getitem__(self, idx): path, label self.samples[idx] image Image.open(path).convert(RGB) if self.transform: image self.transform(image) return image, label这份代码没有任何魔法就是在遍历目录、把路径存起来、按索引读取。但请注意我做了这样几个关键设计用 label 的数字和字符串一一对应后续计算损失函数时不需要再额外转换。读取图片时调用 convert(RGB) 强制统一通道避免某些灰度图或带透明通道的图片导致后面对齐出错。缓存了所有路径到内存里避免训练时反复 os.listdir 拖慢速度。注意训练的图片如果尺寸参差不齐必须先统一尺寸。图像缩放的插值方式也有讲究一般用双线性就行。要是做分类任务千万别忘记在数据预处理里顺手做归一化把像素范围缩到[0,1]附近。这一步漏了模型可能收敛得极慢甚至完全不收敛。2.3 搭建一个“小得合理”的模型第三天搭模型重心放在“结构逻辑清晰、容易训练出效果”上。通常我从一个简单的卷积网络开始写import torch.nn as nn class SimpleCNN(nn.Module): def __init__(self, num_classes2): super().__init__() self.features nn.Sequential( nn.Conv2d(3, 16, kernel_size3, padding1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(16, 32, kernel_size3, padding1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size3, padding1), nn.ReLU(), nn.MaxPool2d(2), ) self.classifier nn.Sequential( nn.Flatten(), nn.Linear(64 * 4 * 4, 64), nn.ReLU(), nn.Linear(64, num_classes), ) def forward(self, x): x self.features(x) x self.classifier(x) return x这个网络结构非常常见就是“卷积 池化 全连接”的老三段。它不追求刷指标胜在简单且能平稳收敛。如果你输入的图片尺寸是 64×64 或者 128×128这个结构都能跑得动。我特别想提醒你的是写模型的时点其实暴露你对张量形状变化的理解程度。conv 层不改变高宽池化层每经过一次高宽减半FC 层的输入维度必须和最后的特征图尺寸严格对应。我见过太多人到了全连接层随便写了一个 Linear(1024, 64)结果一跑就报 shape 不匹配。其实有一个很土但实用的办法在 forward 里临时 print(x.shape)看一遍就能把维度算清楚改完再删掉即可。这种办法看着不优雅却极其高效。2.4 亲手写训练循环反向传播其实就这么回事到了第三步终于要直面训练循环了。你可能已经发现网上教程都会刻意回避自己写训练循环推荐你直接用框架的高阶封装比如 PyTorch Lightning 或者 Keras 的 model.fit。第三天我不建议你直接用这些封装。原因很简单你还没有真正理解一个 batch 的数据是如何前向传播、算损失、清零梯度、反向传播、更新权重的。这些步骤一旦被封装遮住后面排查问题会非常被动。下面是一个标准的 PyTorch 训练循环import torch import torch.optim as optim def train_one_epoch(model, dataloader, criterion, optimizer, device): model.train() total_loss 0.0 correct 0 total 0 for inputs, labels in dataloader: inputs, labels inputs.to(device), labels.to(device) optimizer.zero_grad() outputs model(inputs) loss criterion(outputs, labels) loss.backward() optimizer.step() total_loss loss.item() * inputs.size(0) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() avg_loss total_loss / total accuracy correct / total return avg_loss, accuracy注意第 10~13 行这四个操作是训练循环的灵魂optimizer.zero_grad() 把上一轮累积的梯度清零。PyTorch 的梯度默认是累积的不清零的话梯度会在不同 batch 之间叠加导致优化方向彻底错误。loss.backward() 根据当前 batch 的损失值计算所有参数的梯度。optimizer.step() 拿着这些梯度去更新权重这一步才是真正改变模型参数的时刻。很多初学者难以理解这种参数更新模式我打个比方你手里有一个旋钮用来调模型里几百万个参数每看一批数据就评估一下当前旋钮的各个刻度哪个方向能让损失变小然后朝那个方向拧一点点。这个“评估方向”的动作就是反向传播“拧一点”的动作就是优化器更新。如此反复几千次模型就慢慢从混乱走向靠谱。2.5 训练中的学习率选择最容易被低估的参数关于优化器我建议第三天不要一上来就试 Adam 以外的选择。Adam 默认的学习率 0.001 在绝大多数小规模任务上都能跑出正常效果。相比它SGD 会让训练过程更敏感、更依赖学习率新手很容易陷入“要么发散、要么龟速”的困境。如果你有兴趣做对比实验可以在同一个小数据集上分别用下面两个配置跑 5 个 epoch配置优化器学习率预期现象ASGD0.01损失下降较慢后期略抖BAdam0.001损失下降快曲线平滑看过对比结果你会记住优化器和学习率对训练的影响比网络结构本身更直接。我就亲眼见过有人把一个 ReLU 换成 GELU 后测试精度纹丝不动但学习率从 0.1 调到 0.01模型从发散直接变成收敛。第三天学习的目标不是一次性掌握所有细节而是积累这种“变量影响了什么”的第一手体感。这种体感是任何教程都教不出来的。3. 写出你自己的训练脚本完整参考方案3.1 数据变换与加载的常用配置数据预处理部分我给出一个比较标准的组合方案。这里强调一下验证集只做基础变换不能做随机增强。from torchvision import transforms from torch.utils.data import DataLoader train_transform transforms.Compose([ transforms.Resize((64, 64)), transforms.RandomHorizontalFlip(p0.5), transforms.ColorJitter(brightness0.2, contrast0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) val_transform transforms.Compose([ transforms.Resize((64, 64)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) train_dataset CatDogDataset(data/train, transformtrain_transform) val_dataset CatDogDataset(data/val, transformval_transform) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers2) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse, num_workers2)关于数据集增强说一句实在话RandomHorizontalFlip 这种低成本增强对猫狗分类这个任务非常有效。因为猫和狗的照片天然存在左右对称性翻转不会破坏类别信息却能有效扩充样本形态。而像随机裁剪这种操作如果没调好尺度很容易裁掉主体让人工标注完全失效。3.2 损失函数选择分类问题默认从交叉熵开始关于分类任务第三天直接使用 CrossEntropyLoss几乎不会错。需要提醒的是PyTorch 的 CrossEntropyLoss 内部已经包含了 Softmax 操作所以在模型最后一层不要再额外加 Softmax否则会得到双 Softmax 的效果既浪费计算还可能影响训练梯度。下面这段代码负责把模型、损失函数、优化器组装到一起import torch.optim as optim device torch.device(cuda if torch.cuda.is_available() else cpu) model SimpleCNN(num_classes2).to(device) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr0.001)如果你是 CPU 训练又不想等太久我建议把图片统一缩到 64×64网络通道数再缩水一半迭代轮数控制在 10 轮以内。训练一两个小时后看到损失曲线下降就已经完成第三天的使命了没必要非要用大模型和大分辨率证明什么。3.3 完整训练文件组织方式如果你想把今天的练习沉淀成一个清晰的小项目推荐按下面的文件结构组织代码catdog_project/ data/ train/ cat/ dog/ val/ cat/ dog/ dataset.py model.py train.py predict.pydataset.py 负责放自定义数据集类model.py 负责放网络结构train.py 是训练脚本predict.py 是加载模型对单张图片做推理的脚本。这样结构化以后哪怕只写了四五十行代码也具备了小型工程项目的基本雏形后续加功能会非常顺手。3.4 加入验证逻辑训练过程中不光要看训练集的损失还要每完成一个 epoch 就做一次验证。这种做法意义有两个第一监督是否过拟合训练损失在降、验证损失反而在升说明模型开始背答案了第二验证集指标才是你向外界展示模型真实水平的数据依据。验证集的标准写法是 model.eval() 加 torch.no_grad()前者关闭 Dropout 和 BatchNorm 的训练行为后者彻底停掉梯度计算减少内存占用和多余算力消耗。def evaluate(model, dataloader, criterion, device): model.eval() total_loss 0.0 correct 0 total 0 with torch.no_grad(): for inputs, labels in dataloader: inputs, labels inputs.to(device), labels.to(device) outputs model(inputs) loss criterion(outputs, labels) total_loss loss.item() * inputs.size(0) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() avg_loss total_loss / total accuracy correct / total return avg_loss, accuracy注意一个重要细节验证代码虽然在 for 循环之外写了一段但循环里依然要沿用 train 函数中那种从 batch 里取出 input 和 label 的结构只是少了 loss.backward() 和 optimizer.step()。如果你把训练和验证写成一个函数通过参数控制是否反向传播我劝你尽早拆开避免后续在复杂项目里踩坑。4. 训练现场真实反馈与调试记录4.1 损失一直在高位震荡怎么办有一种第三天很常见的情况跑了十个 epoch训练损失稳定在 0.69 附近既不涨也不怎么跌。0.69 这个数字在二分类里很有意思它约等于 -ln(0.5)说明模型训练半天和随机猜测没什么区别。要排查的因素主要包括学习率过大导致 loss 在震荡、标签和数据没对齐、数据增强过猛把图片折腾得不像原图、网络结构里使用了不合适的初始化方式。其中最容易被忽略的其实是数据加载环节。你可以单独写几行代码把一个 batch 的图片用 matplotlib 显示出来肉眼看一下图片内容和标签是否匹配。import matplotlib.pyplot as plt import numpy as np def show_batch(dataloader): images, labels next(iter(dataloader)) img images[0].permute(1, 2, 0).numpy() img np.clip(img, 0, 1) plt.imshow(img) plt.title(flabel: {labels[0].item()}) plt.show()注意如果你做了 Normalize 标准化显示图片前一定要把像素值范围还原到 [0,1] 附近否则你会看到一张花掉的红绿图容易误以为数据出了问题。4.2 BatchNorm 和 Dropout 在训练与验证时的双重行为前期如果模型里加了 BatchNorm 或 Dropout就会遇到一个很隐蔽的坑它们在训练模式和验证模式下的行为不一致。BatchNorm 在训练时使用当前 batch 的均值和方差在验证时使用训练阶段累积的全局统计量Dropout 在训练时随机丢弃部分神经元验证时不丢弃。因此在写代码时必须在训练循环开始时调用 model.train()在验证循环开始前调用 model.eval()。很多第三天的学习者会因为忘记调用 eval()导致验证指标忽高忽低隔几分钟跑一次结果完全不一样。4.3 过拟合信号训练指标一路飞升验证指标开始回头跑了一段时间如果你发现训练准确率已经到 95% 以上验证准确率却卡在 72% 左右不再向上甚至开始往下掉这就是标准的过拟合信号。第三天不需要对这个信号产生恐慌反而应该感到兴奋因为你已经能独立复现出深度学习中最经典的现象之一。在过拟合出现时几个常见的操作思路如下提示训练流程比预想中更容易出错但这正是深度学习工程能力养成的开始。不要追求一次训练出完美模型而是追求快速定位问题、修正问题的能力。第三天能顺利走完一遍“数据 → 模型 → 训练 → 验证”的全流程就已经达到目标。5. 第三天最后要做完整的一次推理验证5.1 保存模型两种方式模型训练完毕后要把参数保存下来。PyTorch 里常见的有两种状态保存方式只保存模型权重或者把整个模型结构一起保存。前者推荐后者在实际开发中坑更多。# 保存权重 torch.save(model.state_dict(), catdog_model.weights.pth) # 加载权重 model SimpleCNN(num_classes2) model.load_state_dict(torch.load(catdog_model.weights.pth)) model.to(device)使用 load_state_dict 前必须先把模型实例化出来然后才能把权重灌进去。如果模型结构定义和保存时不一致会直接报 KeyError 或者尺寸不匹配的错误。这个细节很基础但我见过不少朋友在这里卡了一晚上。5.2 单张图片推理加载模型后最好再写一个 predict.py 脚本对任意一张图片输出它属于猫还是狗。这样整个流程就完成了闭环模型不再只是黑盒而是能动手调用的工具。def predict_image(image_path, model, transform, device): image Image.open(image_path).convert(RGB) image_tensor transform(image).unsqueeze(0).to(device) model.eval() with torch.no_grad(): outputs model(image_tensor) probs torch.softmax(outputs, dim1) predicted torch.argmax(probs, dim1).item() return predicted, probs[0, predicted].item()这个推理脚本是一个非常好的“工具箱”后续你想接入摄像头、接入 Web 服务都可以从这段代码开始改造。它充分体现了一个事实深度学习模型部署的基础本质上就是数据预处理加模型前向传播。5.3 把结果记录下来第三天结束前我强烈建议你写一份一页纸的实践笔记把数据来源、模型结构、训练轮数、学习率、最终验证准确率、踩过的坑、对结果的理解这七项内容记录下来。这样做对后续学习有极大帮助三天、十天、三十天后再翻回来你会发现自己那时候对深度学习的理解已经完全不同了。6. 第三天最容易忽视的实战习惯6.1 固定随机种子训练结果不稳定很多时候不是代码逻辑出了问题而是随机性在作祟。模型初始化权重不同、数据加载顺序不同、数据增强产生的随机变换不同都可能导致微小的结果差异。为了能复现结果建议在最开头固定随机种子import random import numpy as np import torch def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic True这里特别注意 cudnn.deterministic True 和 torch.backends.cudnn.benchmark False 的搭配。前者强制算法确定性后者禁止自动选择性能最快的卷积算法。代价是训练速度可能略降但换来的是可复现的结果。6.2 养成用 TensorBoard 或简单日志记录训练过程的习惯很多初学者训练时只看终端里 print 出来的 loss还盯着满屏滚动的数字看得头昏眼花。我更推荐用最简单的 CSV 日志记录每个 epoch 的训练损失、训练准确率、验证损失、验证准确率。后面你随时可以拿这份 CSV 画出曲线来。你甚至不需要一开始就使用 TensorBoard直接用 matplotlib 画 loss 曲线就足够。6.3 学会看报错信息而不是复制报错去聊天工具问第三天最应该建立的工程习惯是阅读 Python traceback。遇到 shape mismatch 或者 RuntimeError先顺着报错信息找到具体文件的第几行再从那行往前面推通常看一遍数据形状就能找到答案。这一步不到位的话以后每次调试都会高度依赖别人成长速度会大打折扣。真实经验与后续扩展按照我自己的经验第三天的学习闭环做到这里你实际上已经跑完了“从数据到模型再到部署前验证”的核心链路。很多人直到找工作面试时才被要求手写一个训练循环、解释 DataLoader 的作用、说明模型验证为什么需要单独的目录。而这些早就在第三天的实战中被消化吸收了。这个项目后续自然延伸的方向非常多把二分类换成多分类、在模型结构中加入预训练权重做迁移学习、把推理脚本包装成一个 Flask 接口、引入 TensorBoard 可视化训练曲线。我可以肯定地告诉你等到你回头进入这些进阶课题时会明显感觉到自己的每一步都踩得很扎实。最后分享一个只有自己动手写代码才会发现的经验深度学习入门阶段真正阻碍你的往往不是公式推导而是工程习惯。目录怎么组织、代码怎么复用、遇到报错怎么排查。这些基本功越早补上后面的路走得越顺畅。第三天你的目标不是成为高手而是把基本功练到形成肌肉记忆从这个角度来说今天已经值得了。
阅读完成 · 觉得有帮助?