ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

神经网络模型原理与实战:从全连接到CNN的对比解析

神经网络模型原理与实战:从全连接到CNN的对比解析 “2-神经网络模型”这个标题如果放在学习路线图里大概率是深度学习入门课程的第二个大章节。我在带新人或自己回看学习笔记时最深的感受是神经网络模型这个概念听上去像黑盒子但拆开之后它不过是一个带大量参数的复合函数。这篇文章我就以“第二课”的思路把神经网络模型从原理到代码、从全连接到卷积完整走一遍。内容既覆盖核心概念也搭配可以直接跑的PyTorch代码适合刚学完Python基础、正被各种术语绕晕的初学者也适合想快速回顾模型搭建细节的开发者。1. 神经网络模型到底在学什么1.1 从感知机到多层网络模型结构的进化逻辑神经网络模型并不是凭空出现的。它的前身是感知机一个最简单的线性分类器输入特征加权求和通过阈值判断输出。感知机解决不了异或问题原因在于它只能画一条直线来分割数据。后来多层感知机MLP加了隐藏层本质上是在做特征的逐层变换让原本线性不可分的数据在高维空间里变得可分。深度学习里的“深度”指的就是隐藏层多。每一层都在做一次非线性变换把输入逐步映射到更抽象的特征空间。比如输入一张图片的像素值靠前几层可能学到边缘、纹理靠后几层才能学到“眼睛”“轮胎”这种高层语义。这个逐层抽象的过程和人类识别物体时先看轮廓再看局部细节的直觉是一致的。我之前带过一个朋友做手写数字识别他问过一个问题“为什么不能直接让计算机记住所有数字图片的样子”这其实是传统模板匹配的思路但现实中的手写数字有无数种写法大小、倾斜、笔迹都不同模板根本覆盖不完。神经网络模型的思路是学习“分布规律”而不是死记硬背样本这让它面对未见过的变化时也能保持稳定。1.2 神经网络模型的三个核心要素数据、损失、优化模型本身只是一堆参数和计算图真正让它“学到东西”的是训练过程。训练过程可以拆成三个核心要素数据是学习的基础没有数据模型就没有“经验来源”损失函数是模型的学习信号它衡量当前预测结果与真实标签差了多少优化器是利用损失计算出的梯度来更新参数的工具。前向传播就是从输入计算到输出的过程。假设输入是一个向量经过每一层的矩阵乘法加上偏置再送入激活函数直到输出层。反向传播则是利用链式法则从损失开始逐层计算出每个参数对损失的贡献也就是梯度。优化器拿着这个梯度沿着负梯度方向调整参数让损失越来越小。这里我想强调一个初学者常有的误区神经网络不是一次性算完就能用的。参数更新是迭代式的每跑完一批数据参数就微调一次。这个过程需要反复执行很多个epoch模型才会收敛。很多新手训练一两轮就看准确率发现效果不好就以为代码错了其实只是训练时长不够。2. 从零搭建一个可运行的神经网络模型2.1 环境准备与数据集选择实操环节我建议用PyTorch动态计算图写起来直观调试也方便。环境用Anaconda创建虚拟环境装好Python 3.9以上版本通过pip安装torch就行。如果你用GPU版本的PyTorch一定要提前确认CUDA版本我最初装的时候没注意结果torch.cuda.is_available()一直返回False排查了半天才发现是CUDA和PyTorch版本不匹配。数据集选FashionMNIST这是MNIST的进阶替代品包含10类服饰灰度图图片尺寸28x28。比MNIST更有区分度训练出来的准确率不会虚高更贴近真实项目里“模型可能犯错”的体验。FashionMNIST数据集的结构很清楚训练集60000张测试集10000张。每张图对应一个0到9的标签分别代表T恤、裤子、套头衫、裙子、外套、凉鞋、衬衫、运动鞋、包、短靴。这里要注意类别之间有些很相似比如衬衫和外套、运动鞋和凉鞋模型分类出错是正常的这也是为什么需要多次迭代调参。2.2 网络结构定义与参数计算我们先用最经典的多层感知机来搭建模型。输入层是784个节点28x28像素拉平隐藏层128个节点激活函数用ReLU输出层10个节点对应10个类别。代码如下import torch import torch.nn as nn class MLP(nn.Module): def __init__(self): super().__init__() self.fc1 nn.Linear(784, 128) self.relu nn.ReLU() self.fc2 nn.Linear(128, 10) def forward(self, x): x x.view(x.size(0), -1) # 展平成 784 维向量 x self.fc1(x) x self.relu(x) x self.fc2(x) return x这里为什么用128个隐藏节点这是我常用的起步配置。隐藏层太小会欠拟合模型学不到足够特征太大则容易过拟合且训练变慢。128对于这个任务规模来说参数量适中计算快效果也不差。参数数量的计算是理解模型结构的好方法。第一层参数784x128 128 100480其中一个是权重矩阵一个是偏置项第二层128x10 10 1290。整个模型总共101770个可训练参数。你可以打印model.summary()验证一下这种“算得清参数”的感觉能帮你快速理解模型复杂度。2.3 训练循环里最容易被忽略的细节模型定义好之后训练循环的代码看起来不难但有几个细节直接影响能否收敛。我见过不少新手踩坑这里把标准模板写出来再逐个说明。import torch.optim as optim from torch.utils.data import DataLoader, TensorDataset from torchvision import datasets, transforms transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,)) ]) train_set datasets.FashionMNIST(root./data, trainTrue, downloadTrue, transformtransform) test_set datasets.FashionMNIST(root./data, trainFalse, downloadTrue, transformtransform) train_loader DataLoader(train_set, batch_size64, shuffleTrue) test_loader DataLoader(test_set, batch_size64, shuffleFalse) model MLP() criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr0.001) epochs 10 for epoch in range(epochs): running_loss 0.0 for images, labels in train_loader: optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() print(fEpoch {epoch1}/{epochs}, Loss: {running_loss/len(train_loader):.4f})先说optimizer.zero_grad()。PyTorch的梯度是累积的如果不清零每个batch的梯度会叠加到上一轮上这会导致参数更新方向完全错乱。我在刚开始学的时候漏过这一步loss不降反增排查了很久才发现这个低级错误。再说model.train()和model.eval()模式切换。训练的时候要调用model.train()让Dropout、BatchNorm等层按训练模式工作验证时调用model.eval()关闭随机性使用稳定的统计量。很多人在验证时忘了切换结果同一份模型预测结果却不同还以为模型不稳定。损失函数nn.CrossEntropyLoss()在PyTorch里已经包含了Softmax所以模型输出层不需要手动加Softmax。如果你在forward里加了Softmax再传给CrossEntropyLoss就会出现梯度计算两次的问题效果很差。训练完成后在测试集上评估model.eval() correct 0 total 0 with torch.no_grad(): for images, labels in test_loader: outputs model(images) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() print(fTest Accuracy: {100 * correct / total:.2f}%)用torch.no_grad()包裹验证代码告诉PyTorch不需要计算梯度这一方面能加快计算速度另一方面省显存。我第一次跑验证时忘了加显存直接报警加了之后问题立刻解决。3. 卷积神经网络模型当神经网络遇到图像3.1 为什么普通全连接网络处理图像吃力在FashionMNIST上上面那个简单的MLP准确率大概能到87%到89%左右看起来还行但已经触到瓶颈了。原因在于全连接网络处理图像时有两大痛点参数爆炸和局部特征提取能力差。28x28的图片已经是小尺寸了如果换成224x224的彩色图片输入就是150528个像素值第一层隐藏层如果设定512个节点那就是7700多万个参数算力和显存都扛不住。更关键的是图片里的有用信息往往有局部性比如“衣领”这个特征只存在于一小块区域全连接网络却把每个像素都当独立特征处理完全丢失了空间位置关系。卷积神经网络模型通过两个机制解决这个问题局部连接和参数共享。局部连接意味着每个卷积核只关注输入的一个小窗口提取局部特征参数共享指的是同一个卷积核在图片所有位置滑动时使用同一组权重这样参数量大幅减少同时还具备了一定的平移不变性。3.2 卷积层、池化层、全连接层各司其职CNN的标准结构是“卷积池化全连接”的组合。卷积层负责提取特征池化层负责压缩信息全连接层负责汇总特征并做分类。卷积层里最重要的概念是卷积核和感受野。卷积核就是一个小的权重矩阵比如3x3它像一个滑动窗口在输入图上从左到右、从上到下扫描每次做一个点积操作得到一个特征图。多个卷积核就能提取多种不同的特征比如第一个卷积核可能提取横线第二个提取竖线第三个提取角点。池化层的常见操作是最大池化或平均池化。它在一个小窗口里取最大值或平均值效果是缩小特征图尺寸减少计算量同时增强模型对微小位置变化的容忍度。最大池化更常用因为它保留了最强烈的激活信号相当于“这个区域里最明显的特征是什么”。全连接层放在网络的最后它的作用是把前面卷积提取到的特征图展平成一维向量再做类似MLP的分类决策。从“特征提取器”到“分类器”这个分工是CNN设计里非常清晰的第一性原则你可以先记住这个认识框架再深入下去会顺畅很多。还需要提一下激活函数ReLU。CNN中卷积层后面一般都会接ReLU它能引入非线性让网络有能力拟合复杂函数。ReLU在正区间导数为1梯度消失问题比Sigmoid轻收敛速度也更快。3.3 用CNN替换全连接网络效果差多少下面用代码来验证CNN的威力。把前面的MLP替换成一个简单的卷积网络结构class SimpleCNN(nn.Module): def __init__(self): super().__init__() self.conv1 nn.Conv2d(1, 32, kernel_size3, padding1) self.conv2 nn.Conv2d(32, 64, kernel_size3, padding1) self.pool nn.MaxPool2d(2, 2) self.fc1 nn.Linear(64 * 7 * 7, 128) self.fc2 nn.Linear(128, 10) self.relu nn.ReLU() def forward(self, x): x self.pool(self.relu(self.conv1(x))) # 28x28 - 14x14 x self.pool(self.relu(self.conv2(x))) # 14x14 - 7x7 x x.view(x.size(0), -1) # 展平 x self.relu(self.fc1(x)) x self.fc2(x) return x这个网络结构很经典两个卷积层每个卷积后面接ReLU和最大池化最后接两层全连接。conv1把1通道变成32通道输出特征图尺寸28x28池化后变成14x14。conv2把32通道变64通道池化后再减半到7x7。展平后是64x7x73136个特征经过全连接层输出10类得分。我用同样的训练配置跑了10个epoch测试准确率稳定在92%左右明显优于MLP的88%。这个差距会随着图片复杂度提高而变大。我当时跑完对比实验的感受是数据维度高、空间信息重要的场景CNN几乎是“降维打击”。具体训练时我建议把CNN和MLP的loss曲线对比着看。你会注意到CNN的训练loss下降更快也更平滑这是因为它在一开始就能捕捉到图片的空间局部特征而不是像MLP那样把784个像素当成完全没有结构的序列去硬学。4. 模型训练路上的常见问题排查实录4.1 损失值不下降怎么办损失值不降是最常见的入门困惑。我的排查顺序是先确认代码逻辑有没有致命错误再逐步调参。有几个高频原因值得先说。数据归一化没做好是第一个如果输入像素值在0到255之间梯度数值会偏大参数更新容易震荡不收敛第二个是学习率设置不合适学习率过大导致loss在较大数值附近来回震荡过小则收敛极慢让人误以为不降第三个是激活函数选错全连接层用Sigmoid在小网络里可能还能收敛一旦网络加深梯度消失问题就会出现导致前面的层几乎学不到东西。我自己的做法是先用很小的数据子集做一次过拟合测试选16到32个样本训练几十个epoch如果loss能降到接近0说明模型结构和梯度链没有问题然后再放大到全量数据。这个过程很有效可以快速把“模型写错”和“训练参数不合适”两类问题区分开。我还遇到过一种特殊情况CrossEntropyLoss的标签是整数但我在预处理时把标签转成了one-hot编码结果loss反而不降。PyTorch的CrossEntropyLoss期望输入是原始标签索引传one-hot进去会直接报错或者出现诡异的梯度这一点务必留意。4.2 过拟合与欠拟合的长远判断将训练准确率与验证准确率作对比很容易判断模型状态。训练准确率高、验证准确率低基本就是过拟合模型把训练集特征记得太牢泛化能力不行两者都低则是欠拟合模型还没学到足够的规律。解决过拟合最直接的办法是增加数据量可以用数据增强比如随机旋转、裁剪、翻转让模型看到更多样化的输入变化其次是加Dropout训练时随机丢弃一部分神经元连接打断神经元之间的复杂依赖这样模型不会过分依赖某个特征。解决欠拟合则更简单——把网络加深或加宽增加特征提取能力。我调试模型时会对训练loss和验证loss的实际趋势保持高度敏感比绝对数值更有参考意义。如果验证loss在某个epoch之后开始回升而训练loss还在降那就在这个位置附近早停保存之前验证loss最低的模型权重。4.3 显存不足与训练太慢显存不足在图像任务里是很常见的痛点。调小batch size是最直接的方案把64改成32甚至16也可以降低输入图片尺寸很多任务不需要用原始分辨率如果图片尺寸不能改就减少特征图数量也就是减少卷积层输出的通道数。训练太慢的问题可以从以下几个角度来排查数据加载是否成为瓶颈、模型是否过大、学习率是否太低。数据加载方面启用DataLoader的num_workers参数把数据异步读入内存学习率方面尝试使用学习率调度器训练初期用较大的学习率加速收敛后期逐步降低让loss落到更低的位置。我用过一个很实用的“经验法则”如果训练一个epoch的时间超过你吃一顿饭的时间就要考虑优化了。先用小模型跑通流程确保代码正确再上大模型别一上来就追求精准率把时间浪费在等待上。最后再分享一个实际观察我在写FashionMNIST分类器时第一次MLP训练后测试准确率只有88%换成CNN后达到92%如果再在CNN后面加一层BatchNorm、把训练轮数从10增加到15准确率还能继续涨到94%左右。但再往上提升就越来越难需要更复杂的网络结构比如ResNet、正则化技巧和更细致的超参调优。这其实就是深度学习实践的常态——不是“一个模型吃遍天下”而是不断对比实验、堆叠细节让每一个百分点的提升都有据可依。第一次搭模型时建议保持耐心稳扎稳打跑通一个基础版本再一步步优化这种循序渐进的经验才是真正属于你自己的积累。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进