深度学习CV基础:从图像表示到CNN训练实践
发布时间:2026/10/2 5:25:54来源:尧图网络
说实话这份“深度学习-CV基础知识笔记”我在不同阶段翻来覆去写过好几遍。最早是刚入门时记给自己看的后来带新人时又整理过一版这次借着重新梳理的机会把概念、公式和踩坑记录放在一起按一条实际能走通的学习路径重新排了一遍。不管你是正在啃《动手学深度学习》、在B站追吴恩达还是被课程作业和期末复习逼着必须搞定CNN这份笔记都按“先建框架、再补细节、最后上手调参”的顺序来写。核心就围绕三件事图像在深度学习里到底怎么表示、卷积神经网络为什么适合做CV、训练一套分类模型时那些参数和报错到底该怎么处理。看完不敢说让你变高手但至少能把基础概念连成一条线遇到问题知道往哪个方向查。1. 动手之前先把这套知识框架搭起来1.1 深度学习解决CV问题的基本套路CV计算机视觉要处理的事情很多图像分类、目标检测、语义分割、姿态估计、图像生成每一类任务看起来差别巨大但落到深度学习里套路其实高度一致。先有一堆带标注的数据把数据预处理成张量扔进一个神经网络网络输出一个预测结果拿预测结果和真实标注算损失反向传播更新网络参数重复这个过程直到指标不再明显提升。这个套路说起来简单真正理解它需要你把几个概念彻底想明白什么是张量、什么是前向传播、什么是损失函数、什么是反向传播、什么是优化器。很多人一上来就调库跑通了LeNet觉得自己会了但换一个数据集、换一个任务就完全懵了。原因就是只记住了代码流程没理解每一步在干什么。我个人的建议是前期不急着上复杂模型先用全连接网络把一个最简单的二分类跑通把数据张量的形状变化画出来把每一层的输入输出维度写在纸上然后再引入卷积、池化这些CV专属组件。这个“慢就是快”的过程能帮你省掉后面大量的排查时间。1.2 图像在计算机里到底是什么这是整个笔记里最基础也最容易被忽略的问题。一张灰度图本质上就是一个二维矩阵矩阵里每个元素是0到255之间的整数代表该像素点的亮度0是纯黑255是纯白。一张彩色图则是三个这样的矩阵叠在一起分别对应红、绿、蓝三个通道。也就是说图像进入深度学习模型之前就是数值矩阵。把这些矩阵喂给深度学习框架之前还要再做两步标准化。第一步是调整尺寸因为网络通常要求固定输入大小比如224×224或32×32。第二步是归一化把像素值从0到255缩放到0到1或-1到1范围这样能避免数值过大导致梯度不稳定。很多初学者在归一化这一步偷懒结果模型训练时loss乱跳还以为是网络结构的问题。到了框架内部图像会被组织成四维张量形状一般是(batch_size, channels, height, width)简写为NCHW。batch_size是一次性喂给网络的图片数量channels是通道数灰度图为1彩色图为3。理解这个维度顺序特别重要因为你在代码里打印tensor.shape时看到的就是这四个数字后续所有reshape、permute操作都在跟它打交道。1.3 一张图从输入到输出的完整数据流把图像输入卷积神经网络后数据流的走向一般是这样的原始图像先经过预处理变成标准形状的张量然后进入若干个卷积层和池化层逐步提取低级特征边缘、颜色和高级特征纹理、部件最后经过展平操作变成一维向量送入全连接层或全局平均池化层输出一个长度等于类别数的向量再经过softmax转换成各类别的预测概率。这个流程里最需要理解的是“特征图”这个概念。每经过一次卷积图像的高度和宽度通常会变小通道数通常会变多。通道变多意味着网络在同时关注多种不同的特征而尺寸变小意味着特征逐渐从精细变得抽象。可以打个比方一开始你看一张猫的照片看到的是胡须、耳朵、毛发的纹理往后逐层抽象网络在更高层关注的是“有猫耳朵”“有猫脸轮廓”这些组合特征。学习阶段建议你随便拿一张图自己写代码看一下每一层输出的shape变化。这个习惯比你背十遍网络结构都管用因为shape就是网络结构的具象化表现。shape对不上一定某个环节理解错了。2. 卷积神经网络CV里躲不开的积木2.1 卷积到底在算什么卷积是CNN最核心的操作但初次接触的人很容易被那个“翻转卷积核”的数学定义绕晕。在实际的深度学习框架里卷积的实现就是一个滑动窗口的点乘求和操作不做核翻转严格来说叫“互相关”但大家习惯上都叫卷积。你可以这样理解一个3×3的卷积核就是在图像上按步长滑动每次取当前位置周围3×3的像素块和卷积核的9个数字逐位相乘再求和得到一个输出值。滑完整个图像就得到一张新的特征图。卷积核里的数字不是人设计的而是网络通过训练学出来的每个卷积核相当于一个特征检测器有的负责检测水平边缘有的负责检测垂直边缘。这里有个关键参数要注意填充padding和步长stride。padding是在图像周围补一圈0防止边缘像素参与计算次数太少导致输出尺寸缩小过快stride是卷积核每次滑动的距离stride为2时输出尺寸直接减半。输出特征图尺寸的计算公式是output_size (input_size - kernel_size 2 × padding) / stride 1。这个公式建议自己动手推一遍因为几乎所有shape对不上的报错都和它有关。2.2 池化层压缩信息保留关键特征池化层在早期CNN里几乎是标配它做的事情很简单在特征图的一个小窗口内取最大值最大池化或平均值平均池化。最常用的是2×2、步长为2的最大池化效果就是特征图的高和宽各减半通道数不变。池化带来两个好处。第一个是降低计算量特征图小了一半后续卷积层的计算量直接降到原来的四分之一。第二个是带来一定程度的平移不变性也就是说物体在图像里稍微挪动几个像素池化后的结果变化不大这对分类任务是有利的。但随着网络设计的发展很多现代网络比如ResNet已经不怎么用池化层了而是用卷积本身来降采样效果更好但理解池化仍然是读懂早期经典网络的基础。做笔记的时候我建议把卷积、池化、激活函数这三样放在一起理解。它们通常是交替出现的卷积提取特征激活函数引入非线性池化压缩尺寸。看任何经典网络结构图只要抓住这个交替模式就能把结构看懂一大半。2.3 激活函数没有它堆再多层也白搭如果神经网络里只有卷积和全连接没有激活函数那不管叠多少层整个网络依然是一个线性变换。打个比方你用几张透明塑料片叠在一起不管怎么叠透过它们看到的东西还是清清楚楚的不会有任何“扭曲变形”来丰富表达。激活函数的作用就是引入这种非线性扭曲让网络有能力拟合复杂函数。CV里最常见的激活函数是ReLU公式是f(x) max(0, x)正数不变负数全部置零。它的优点是计算极快、梯度在正数区间恒为1能有效缓解梯度消失。实际使用中你会看到很多网络在卷积层之后紧跟一个ReLU。后来出现的LeakyReLU、SiLU等变体本质上都是在解决ReLU在负数区间的“神经元死亡”问题初学者先把ReLU吃透就够用了。记住一个原则卷积层和全连接层后面通常都要接激活函数但输出层一般不接或者只在做分类时接softmax。原因很简单输出层要输出连续的置信度分数不需要做非线性截断。2.4 从全连接到输出层分类任务的最后一公里卷积层和池化层负责把图像转换成特征最后需要一个分类器来输出预测结果。传统做法是把特征图展平成一维向量接几个全连接层最后一层的神经元数量等于类别数输出一个得分向量。做多分类时再对这个得分向量做softmax把得分转换成概率分布所有类别的概率之和等于1。这里有一个值得注意的细节实际计算损失时PyTorch这类框架通常把softmax和交叉熵损失合并成了一个函数比如CrossEntropyLoss它会直接接收模型输出的原始得分logits内部帮你做softmax再算损失。很多新手踩过的坑是在模型输出层手动加了softmax然后又用CrossEntropyLoss算损失相当于做了两次softmax导致训练初期loss就异常。我自己也犯过这个错排查半天才发现问题出在这。全连接层参数量很大一张32×32×3的输入展平后就有3072维再接一个稍大的全连接层参数动辄几十万。现代网络倾向于用全局平均池化替代展平全连接对最后一张特征图的每个通道求平均得到一个长度等于通道数的向量再接一个线性层输出分类结果。这种方式参数量小还不太容易过拟合。3. 训练一套自己的分类模型3.1 环境与工具选型练手阶段我个人推荐PyTorch原因有三。第一是调试方便张量维度不对的时候报错信息比较清楚第二是生态完善torchvision里带了常用数据集和预训练模型下载就能用第三是社区资料多遇到问题一搜就有答案。TensorFlow也很好但在易用性上对新手确实有些门槛。环境的安装细节不展开了给一个能跑的最小组合显卡驱动能正常识别GPU的前提下用conda创建一个新环境Python版本用3.9或3.10PyTorch版本用官方命令安装最新的稳定版即可。没有NVIDIA显卡也不要紧纯CPU版本完全够跑通CIFAR-10小模型练习只是慢一些。集成的开发环境我比较推荐VS Code加Jupyter插件既能写脚本又能一段段跑代码观察中间结果。学习阶段能实时打印每一层的输出shape、loss变化曲线比任何调试器都直观。3.2 数据准备不要小看预处理很多人觉得数据处理是最没技术含量的环节恰恰相反数据质量直接决定模型上限。以CIFAR-10为例这是一个10类、每类6000张32×32彩色小图的数据集跑起来非常快非常适合做练习。用torchvision加载CIFAR-10时有两步必须做转成Tensor然后做归一化。转Tensor会把像素值从0到255变成0到1归一化则是按数据集的均值和标准差做标准化。CIFAR-10的常用均值是(0.4914, 0.4822, 0.4465)标准差是(0.2470, 0.2435, 0.2616)。这套数字是预先算好的直接抄来用就行。归一化不是可有可无的步骤它能让各个通道的数值分布保持一致训练时收敛更快更稳。数据加载还有一个重要概念DataLoader。它的作用是把数据集切成一个个batch、打乱顺序、在训练时按batch取数据。batch_size建议从64或128开始太小会导致梯度更新太频繁、训练震荡太大又可能显存不足。在数据流水线这块新手最容易忽略的是num_workers参数它控制用几个子进程来加载数据默认0表示只在主进程里加载数据量大时会拖慢训练。设置为4或8能明显提高GPU利用率前提是你的电脑内存足够。3.3 写一个最小可用的训练循环下面这份代码是能在CIFAR-10上跑到70%左右准确率的完整训练流程结构非常简单建议逐行看懂后再动手改网络结构。import torch import torch.nn as nn import torch.optim as optim import torchvision import torchvision.transforms as transforms # 1. 数据预处理 transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2470, 0.2435, 0.2616)) ]) trainset torchvision.datasets.CIFAR10(root./data, trainTrue, downloadTrue, transformtransform) trainloader torch.utils.data.DataLoader(trainset, batch_size128, shuffleTrue, num_workers4) testset torchvision.datasets.CIFAR10(root./data, trainFalse, downloadTrue, transformtransform) testloader torch.utils.data.DataLoader(testset, batch_size128, shuffleFalse, num_workers4) # 2. 定义一个简单的CNN class SimpleCNN(nn.Module): def __init__(self): super().__init__() self.conv1 nn.Conv2d(3, 32, kernel_size3, padding1) self.bn1 nn.BatchNorm2d(32) self.conv2 nn.Conv2d(32, 64, kernel_size3, padding1) self.bn2 nn.BatchNorm2d(64) self.pool nn.MaxPool2d(2, 2) self.fc nn.Linear(64 * 8 * 8, 10) self.relu nn.ReLU() def forward(self, x): x self.pool(self.relu(self.bn1(self.conv1(x)))) x self.pool(self.relu(self.bn2(self.conv2(x)))) x x.view(x.size(0), -1) x self.fc(x) return x # 3. 初始化模型、损失函数、优化器 model SimpleCNN() device torch.device(cuda if torch.cuda.is_available() else cpu) model.to(device) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr0.001) # 4. 训练循环 for epoch in range(20): running_loss 0.0 for images, labels in trainloader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() print(fEpoch {epoch1}: loss {running_loss / len(trainloader):.4f}) # 5. 在测试集上评估 correct 0 total 0 model.eval() with torch.no_grad(): for images, labels in testloader: images, labels images.to(device), labels.to(device) outputs model(images) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() print(fTest accuracy: {100 * correct / total:.2f}%)这个结构里每行都值得品味。optimizer.zero_grad()清空上一轮梯度因为PyTorch的梯度是累积的不清空会把多次反向传播的梯度叠加在一起。loss.backward()计算梯度optimizer.step()用梯度更新参数这三步是训练循环的固定三连。model.eval()和with torch.no_grad()在评估时一定要加前者会关闭BatchNorm和Dropout的训练逻辑后者会禁止计算梯度图省内存也省时间。有个容易忽略的小坑喂给模型的tensor要调用.to(device)把数据和模型放到同一个设备上。如果模型在GPU但数据还在CPU你会看到一个报错Found device cpu but expected cuda。这句话基本是每个PyTorch新手都遇到过的。3.4 训练参数怎么定epoch、batch size、学习率epoch、batch size、学习率这三个参数是训练阶段最常调的东西但很多教程只是丢给你一组默认值不讲为什么。epoch是遍历整个训练集的次数。每个epoch结束后模型把整个数据集看过一遍。epoch太小模型还没学够欠拟合epoch太大模型把训练集背下来了测试集上反而变差过拟合。CIFAR-10这样的小数据集20到50个epoch比较常见具体以验证集准确率不再提升为早停时机。batch size决定每次用多少张图算一次梯度。直观理解是batch size越大算出来的梯度越接近全局真实的梯度方向训练越稳batch size越小梯度噪声越大反而可能起到正则化作用帮助跳出局部最优。但也有一个微妙的问题batch size过大时训练收敛快但泛化性能可能略微下降业界把这种现象称为“大批量效应”。练手时不用纠结64或128起步遇到loss不稳定就调小一点。学习率是最重要的超参数它决定每一步参数更新的幅度。学习率太大loss曲线直接震荡甚至变成NaN学习率太小loss下降得极其缓慢像蜗牛爬。一个可行的策略是先用Adam优化器加0.001的初始学习率观察loss曲线的下降趋势然后逐步调整。如果loss前几个epoch就开始震荡果断调小十倍如果loss下降慢得像没学一样试试调大十倍。关于优化器的选择早期教程普遍用SGD加momentum因为它在很多任务上最终准确率更高但由于SGD对学习率很敏感新手不好掌握Adam这种自适应学习率的方法更省心。我自己的习惯是快速验证想法用Adam追求最终指标时再换回SGD配CosineAnnealing学习率调度。4. 训练中踩过的坑和排查方法4.1 Loss不降、梯度爆炸和NaN问题训练刚开始最常见的问题就是loss完全不动或者直接变成NaN。loss是NaN的排查顺序我建议这样走先看学习率是不是太大了调小十倍试试再看数据里有没有包含NaN或无穷大归一化代码有没有写上然后检查模型输出层有没有做两次softmax最后考虑是不是梯度爆炸可以在优化器里加一个clip_grad_norm_操作把梯度的范数裁剪到1.0以内。loss完全不降的情况则要复杂一些可能的原因包括数据没有归一化网络结构有Bug比如忘记加激活函数batch size太小导致梯度噪声太大或者是标签和数据没对齐。我遇到过最乌龙的一次是DataLoader的参数shuffle设成了False模型在每个epoch里看到的都是相同顺序的数据虽然还是能学会但泛化能力明显变差。遇到这种问题不要瞎猜把loss每个epoch的输出记录下来画成曲线。曲线能告诉你很多信息直线在0.7附近不动大概率是模型没学到任何有效特征先降后升说明过拟合了反复震荡但整体有下降趋势说明学习率略大。养成记录训练曲线的习惯排查效率至少提高一倍。4.2 过拟合的典型信号和处理手段如果训练准确率一路飙升到99%但测试准确率只有70%恭喜你你过拟合了。这是深度学习中“背诵”和“理解”的差别模型在训练集上背下了每个样本的答案但遇到没见过的样本就不会了。处理过拟合有一整套层层递进的手段。第一层是数据层面做数据增强比如随机裁剪、水平翻转、颜色抖动相当于免费增加了训练样本数量第二层是模型层面降低模型复杂度减少卷积核数量或全连接层神经元数量加Dropout层随机丢弃一部分神经元加BatchNorm层稳定分布第三层是训练策略层面早停也就是在测试准确率连续几个epoch不提升时停止训练加入权重衰减weight_decay正则项。初学者最容易犯的错误是一上来就用预训练模型微调结果模型本身就很大小数据集上必然过拟合。先在小数据集上把基础分类练好再尝试迁移学习这个顺序更合理。4.3 显存不足和训练速度慢的取舍显存不够是另一个高频问题。RuntimeError: CUDA out of memory这行报错处理方案按优先级排列依次是调小batch_size立竿见影但别太小影响梯度稳定性降低输入图像分辨率改用一个更小的模型开启梯度累积也就是多个小batch的梯度攒起来再更新一次参数效果等价于大batch size但显存占用小得多。训练速度慢的问题则要先定位瓶颈在哪。GPU利用率不高时打开任务管理器看一眼如果GPU利用率经常不到50%说明数据加载的速度跟不上模型计算的速度这时候调大num_workers、把数据格式改成LMDB或TFRecord、或者使用更快的SSD通常能解决问题。反过来如果GPU利用率已经接近100%还嫌慢那就只能从模型本身瘦身了减少卷积核数量、用深度可分离卷积替代普通卷积、开启混合精度训练。混合精度训练是个值得推荐的工程技巧。现代GPU对半精度浮点数计算速度是单精度的两倍以上而大部分深度学习模型的训练并不需要那么高的数值精度。PyTorch里用torch.cuda.amp.GradScaler几行代码就能开启在训练速度和显存占用上都能得到明显改善精度损失几乎可以忽略。4.4 几个值得长期保持的实操习惯最后分享几个我踩过无数次坑之后沉淀下来的习惯每一个都是用掉头发换来的。第一个习惯是改任何代码之前先想清楚你要验证什么假设改完之后记录下结果。模型训练不是玄学每一次调参都应该有明确的目的。如果连着试了好几种方案都没效果停下来重新审视数据多半是数据本身有问题。第二个习惯是第一次跑通模型时先只拿一小部分数据比如100张图来过拟合看loss能不能降到很低。如果100张图都过拟合不了赶紧排查代码Bug如果顺利过拟合再逐步加大数据量跑正式训练。这个方法能在几分钟内帮你发现绝大多数的代码错误省下几小时的无效等待。第三个习惯是固定随机种子。在代码开头设置torch.manual_seed(42)以及numpy.random.seed(42)这样每次实验的初始权重、数据打乱都是可复现的。没有固定随机种子的时候你很难判断模型效果的提升到底是你的改进起作用了还是仅仅是运气好。根据我个人经验深度学习CV方向的学习曲线不是线性上升的而是在很长一段时间内徘徊然后某一天突然感觉所有概念都通了。那个拐点往往不是因为你多看了某篇文章而是你把底层的数据流、梯度流亲手捋了一遍手写了一个哪怕很小的网络并把它训练到收敛。希望这份笔记能帮你更快走到那个拐点。
网站建设高端定制企业官网