深度学习这个方向我断断续续做了五年多,从最早拿MATLAB跑一个三层全连接网络都能兴奋半天,到后来在项目里上手CNN做图像分类、用LSTM处理序列数据、用GCN搞图结构建模、拿GAN做数据增强,踩过的坑比看过的论文多得多。这篇总结不是教科书式的推导手册,而是把我自己在DNN、CNN、RNN、LSTM、GCN、GAN这六类模型上的理解脉络、实操细节和踩坑经验整理出来。如果你刚入门深度学习,正在啃CNN原理或者搞不清LSTM的门控到底怎么工作,又或者你已经有基础但想快速对齐这几类模型的适用边界和核心差异,那这篇内容应该能帮你省下不少翻文档和试错的时间。
1. 从DNN说起:一切深度模型的骨架基础
DNN(深度神经网络)是我建议每一个入门深度学习的人第一个吃透的结构,原因很简单:后面要讲的CNN、RNN、LSTM、GCN、GAN,底层都离不开全连接层和反向传播这套机制。你如果连一个三层的DNN为什么能拟合非线性函数都说不清楚,那去看LSTM的门控公式基本就是背课文。
1.1 前向传播与反向传播的核心逻辑
DNN的基本单元是神经元,每个神经元做的事情就是加权求和再经过激活函数。假设第 $l$ 层有 $n^{[l]}$ 个神经元,那么这一层的输出可以写成:
$$ z^{[l]} = W^{[l]} a^{[l-1]} + b^{[l]}, \quad a^{[l]} = \sigma(z^{[l]}) $$
其中 $W^{[l]}$ 是权重矩阵,$b^{[l]}$ 是偏置向量,$\sigma$ 是激活函数。前向传播就是从输入层一路算到输出层,得到预测值。
反向传播的核心是链式法则。损失函数 $L$ 对某一层权重的梯度可以写成:
$$ \frac{\partial L}{\partial W^{[l]}} = \frac{\partial L}{\partial z^{[l]}} \cdot \frac{\partial z^{[l]}}{\partial W^{[l]}} $$
这里的关键点是 $\frac{\partial L}{\partial z^{[l]}}$ 这一项要从后往前递推,也就是所谓的"误差反向传播"。我刚开始学的时候一直搞不懂为什么叫"反向",后来自己手推了一遍两层网络才明白:输出层的误差算起来最直接,越往前层,梯度需要乘的中间项越多。
实际写代码的时候,你不需要自己手推这些公式,PyTorch的autograd会自动帮你算。但理解这个过程对调试非常关键。比如你发现loss不下降,如果知道梯度是从后往前传的,你就会去检查是不是某一层的梯度被激活函数压得太小了。
激活函数的选择上,现在隐藏层基本默认用ReLU或者它的变体。Sigmoid在深层网络里容易导致梯度消失,这个结论在DNN时代就已经被验证得很清楚了。ReLU的导数在正区间恒为1,梯度不会因为层数增加而指数衰减,这就是它成为默认选择的根本原因。
1.2 全连接层的参数爆炸问题与实际应对
DNN最大的问题在于参数量。假设你输入一张 $224 \times 224 \times 3$ 的彩色图片,展平之后是150528维。如果第一层隐藏层有1000个神经元,那这一层的参数量就是:
$$ 150528 \times 1000 + 1000 = 150,529,000 $$
也就是说仅第一层就超过1.5亿个参数。这还只是一层,如果要堆十几层,参数量会直接爆炸,训练所需的显存和计算资源根本扛不住。这也是为什么图像任务里几乎没有人直接用DNN处理原始像素,而是用CNN来做特征提取。
我在早期做一个简单的图像二分类项目时就犯过这个错误:直接用全连接网络处理 $256 \times 256$ 的图片,结果模型参数量接近2亿,训练一轮要十几分钟,最后准确率还不如后来用一个小型CNN跑得快。这个教训让我彻底理解了为什么卷积操作是图像任务的必需品。
不过在结构化数据(比如表格数据、特征向量)上,DNN依然是非常实用的选择。特征维度不高的情况下,几层全连接就能拿到不错的效果,而且训练速度快、调试简单。我的经验是:如果你的输入特征维度在几百以内,先拿DNN跑一个baseline,再考虑要不要上更复杂的结构。
注意:DNN的层数不是越多越好。我在实际项目中试过把全连接层从3层加到8层,结果测试集准确率反而下降了,原因是过拟合和梯度传播效率降低。一般来说,结构化数据任务上3到5层全连接已经足够覆盖大部分场景。
2. CNN:图像任务中替代前馈网络的关键设计
经常有人问"图像处理为啥用CNN不用前馈神经网络",这个问题我自己也被问过很多次。答案其实不复杂,但需要从三个层面来理解:参数效率、空间信息保留和特征层次化提取。
2.1 局部感受野、权值共享与平移不变性的直观理解
CNN相比DNN的核心优势来自两个设计:局部感受野和权值共享。
局部感受野的意思是,每个卷积核只跟输入的一个小区域做运算,而不是像全连接层那样跟所有输入都连接。一个 $3 \times 3$ 的卷积核每次只看9个像素,这就把参数量从百万级降到了个位数。
权值共享的意思是,同一个卷积核在整张图片上滑动,所有位置共用同一组权重。这背后的直觉是:如果一个特征在图片左上角有用,那它在右下角同样有用。比如边缘检测这个操作,不管边缘出现在哪里,检测的方式是一样的。
这两个设计带来的直接好处就是平移不变性:图片里的物体稍微移动几个像素,卷积网络的输出不会发生剧烈变化。而全连接网络对输入位置极其敏感,图片平移之后输出可能完全不同。
我用一个具体的例子来说明参数差异。输入是 $32 \times 32 \times 3$ 的图片,如果用全连接层输出64维特征,参数量是 $3072 \times 64 = 196608$。如果用64个 $3 \times 3$ 的卷积核,参数量是 $3 \times 3 \times 3 \times 64 = 1728$。差了100多倍。
2.2 卷积核计算与通道数设计的实操细节
卷积操作的输出尺寸计算公式是:
$$ H_{out} = \frac{H_{in} - K + 2P}{S} + 1 $$
其中 $H_{in}$ 是输入高度,$K$ 是卷积核大小,$P$ 是padding,$S$ 是stride。这个公式我在早期调网络结构时经常用,因为你需要保证特征图尺寸在层与层之间能正确衔接。
举个例子:输入 $224 \times 224$,卷积核 $3 \times 3$,padding=1,stride=1,那么输出是:
$$ \frac{224 - 3 + 2}{1} + 1 = 224 $$
也就是说,$3 \times 3$ 卷积核配合padding=1、stride=1,特征图尺寸保持不变。这个配置在VGG和ResNet里被大量使用,因为它允许你堆叠很多层而不改变空间尺寸。
通道数的设计上,我遵循的经验是:随着空间尺寸减小,通道数逐步增加。比如从32到64到128到256。这样做的原因是,空间尺寸缩小后,每个位置承载的信息变得更加抽象,需要更多通道来表达不同的语义特征。这个设计模式在几乎所有的经典CNN架构里都能看到。
卷积核大小的选择上,$3 \times 3$ 是最常用的。两个 $3 \times 3$ 卷积堆叠的感受野等于一个 $5 \times 5$ 卷积,但参数量更少,而且中间多了一次非线性激活。这就是VGG网络的核心设计思想。
2.3 池化层的取舍与常见误区
池化层的作用是降低特征图的空间尺寸,减少计算量,同时提供一定的平移鲁棒性。最大池化是最常用的方式,取每个窗口内的最大值。
但池化层并不是必须的。现在很多网络结构(比如一些轻量级网络)用stride=2的卷积来替代池化,效果也很好。池化的缺点在于它丢弃了位置信息,这在分类任务里可能无所谓,但在分割、检测这类需要精确定位的任务里就是问题。
我在做一个缺陷检测项目时,一开始用了大量最大池化层,结果模型对缺陷位置的定位精度很差。后来改成用stride卷积做下采样,并且加了跳跃连接把浅层特征传到深层,定位精度才上来。这个经验让我明白:池化的取舍取决于你的任务是否依赖空间位置信息。
实操心得:如果你的任务是分类,池化层放心用;如果涉及定位、分割或者需要输出热力图,尽量用stride卷积替代池化,或者至少保留浅层的高分辨率特征。
3. RNN与LSTM:序列建模的演化路径
序列数据的核心特点是前后依赖。文本里一个词的含义取决于前面的上下文,时间序列里当前值跟历史值相关。DNN和CNN本身不具备这种记忆能力,RNN就是为解决这个问题设计的。
3.1 从RNN的梯度消失说起
RNN的核心思想是在每个时间步维护一个隐藏状态 $h_t$,它同时接收当前输入 $x_t$ 和上一步的隐藏状态 $h_{t-1}$:
$$ h_t = \tanh(W_{xh} x_t + W_{hh} h_{t-1} + b_h) $$
这个递推结构让网络有了"记忆",但也带来了严重的问题。当你做反向传播时,梯度需要沿着时间步一路往回传。假设序列长度是100,那梯度就要连乘100次。如果每次乘的系数小于1,梯度会指数衰减到接近零;如果大于1,梯度会爆炸。
这就是RNN在实际中很难训练的原因。短序列(比如长度10以内)还能work,一旦序列变长,前面的信息基本就传不到后面了。我在用一个基础RNN做中文文本情感分析时,序列长度设到50以上,模型基本就学不到东西了,loss一直卡在一个较高的值下不去。
3.2 LSTM门控机制的结构拆解
LSTM通过引入门控机制来解决梯度消失问题。它的核心是三个门:遗忘门、输入门和输出门。
遗忘门决定上一步的细胞状态 $c_{t-1}$ 有多少需要保留:
$$ f_t = \sigma(W_f [h_{t-1}, x_t] + b_f) $$
输入门决定当前信息有多少需要写入细胞状态:
$$ i_t = \sigma(W_i [h_{t-1}, x_t] + b_i) $$
然后计算候选细胞状态:
$$ \tilde{c}t = \tanh(W_c [h{t-1}, x_t] + b_c) $$
细胞状态更新:
$$ c_t = f_t \odot c_{t-1} + i_t \odot \tilde{c}_t $$
输出门决定细胞状态有多少需要输出到隐藏状态:
$$ o_t = \sigma(W_o [h_{t-1}, x_t] + b_o), \quad h_t = o_t \odot \tanh(c_t) $$
关键点在于细胞状态的更新路径:$c_t = f_t \odot c_{t-1} + \dots$。这条路径上梯度反向传播时只需要乘以 $f_t$,而不是像RNN那样乘以整个权重矩阵。$f_t$ 是sigmoid的输出,范围在0到1之间,但它是一个可学习的值,网络可以学会让 $f_t$ 接近1来保留长距离信息。这就是LSTM能缓解梯度消失的根本原因。
我刚开始看LSTM原理的时候被这一堆公式绕晕了,后来用一个生活类比才搞清楚:把细胞状态想象成一条传送带,遗忘门是传送带上的刮板,决定丢掉哪些旧货物;输入门是新货物入口,决定往传送带上放什么;输出门是出货口,决定当前时刻从传送带上取什么出来用。
3.3 LSTM在时间序列预测中的实操要点
用LSTM做时间序列预测(比如股价、销量、传感器数据),有几个实操细节直接影响效果。
第一是序列长度的选择。太短捕捉不到长期依赖,太长会增加训练难度。我的经验是先看数据的自相关函数,找到相关性显著衰减的滞后阶数,把它作为序列长度的参考。一般从20到50之间开始试。
第二是归一化。时间序列的数值范围可能很大,不归一化的话LSTM很难收敛。我通常用滑动窗口的均值和标准差做标准化,而不是用全局统计量,这样能避免未来信息泄漏。
第三是单步预测和多步预测的区别。单步预测是给模型一个序列,预测下一个时间点;多步预测是预测未来多个时间点。多步预测有两种策略:直接多输出和滚动预测。直接多输出是让模型一次性输出未来N个值,滚动预测是每次预测一个然后把它拼回输入再预测下一个。我实测下来,直接多输出在短期预测上更稳,滚动预测在长期预测上误差累积更明显。
下面是一个PyTorch里LSTM做单步预测的核心代码结构:
import torch import torch.nn as nn class LSTMPredictor(nn.Module): def __init__(self, input_size, hidden_size, num_layers, output_size): super().__init__() self.lstm = nn.LSTM( input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, dropout=0.2 ) self.fc = nn.Linear(hidden_size, output_size) def forward(self, x): out, (h_n, c_n) = self.lstm(x) # 取最后一个时间步的输出 last_out = out[:, -1, :] return self.fc(last_out)这段代码里有几个点值得注意:batch_first=True让输入维度是(batch, seq_len, feature),dropout=0.2在多层LSTM之间加正则化,最后取最后一个时间步的输出做预测。
注意:LSTM的
num_layers过多(超过3层)在小数据集上很容易过拟合,而且训练速度明显变慢。我一般从1到2层开始,除非数据量确实很大。
4. GCN:非欧几里得空间上的特征聚合
GCN(图卷积网络)处理的是图结构数据。社交网络、分子结构、知识图谱、推荐系统里的用户-物品关系,这些数据不是规则网格,传统的CNN和RNN都没法直接套用。
4.1 图结构数据的特殊性
图由节点和边组成。每个节点可能有自己的特征向量,边表示节点之间的关系。图结构数据跟图像数据最大的区别在于:图像的邻居是规则的(每个像素上下左右固定),而图的邻居是任意的(每个节点的度可能不同)。
这意味着你没法像CNN那样用一个固定大小的卷积核在图上来回滑动。GCN的解决方案是从邻居节点聚合信息来更新当前节点的表示。
4.2 谱域与空域视角的GCN理解
GCN的原始推导来自谱图理论,涉及拉普拉斯矩阵和傅里叶变换,数学门槛不低。但如果只从实操角度理解,可以简化为一个消息传递的过程。
每一层GCN做的事情可以概括为三个步骤:
第一步,每个节点从邻居节点收集特征。第二步,对所有收集到的特征做加权求和。第三步,用一个线性变换和激活函数处理聚合后的特征。
这个过程可以用一个简化的公式表达:
$$ H^{(l+1)} = \sigma(\tilde{D}^{-1/2} \tilde{A} \tilde{D}^{-1/2} H^{(l)} W^{(l)}) $$
其中 $\tilde{A} = A + I$ 是加了自连接的邻接矩阵,$\tilde{D}$ 是对应的度矩阵,$H^{(l)}$ 是第 $l$ 层的节点特征矩阵,$W^{(l)}$ 是可学习的权重。
加自连接的目的是让节点在聚合邻居信息的同时也保留自己的信息。如果不加自连接,节点更新后就完全丢失了自己的原始特征。
归一化项 $\tilde{D}^{-1/2} \tilde{A} \tilde{D}^{-1/2}$ 是为了防止度数大的节点在聚合后数值过大,影响训练稳定性。
4.3 实际应用场景与踩坑记录
我在一个用户行为预测项目里用过GCN。场景是把用户和商品构建成二部图,用户节点和商品节点之间有交互就连边。目标是预测用户对未交互商品的偏好。
实际操作中遇到的第一个问题是图的规模。用户和商品加起来上百万个节点,边有上千万条,直接全图训练显存不够。解决方案是采用邻居采样,每个batch只采样一部分节点和它们的邻居子图来训练。PyTorch Geometric这个库提供了NeighborLoader可以直接做这件事。
第二个问题是特征质量。GCN的效果高度依赖节点特征的质量。如果用户特征只有ID embedding,那GCN能学到的信息很有限。后来我们补充了用户的行为统计特征、商品的内容特征,效果才有明显提升。
第三个问题是过平滑。GCN层数多了之后,所有节点的表示会趋于一致,失去区分度。这是因为每层都在做邻居聚合,几层之后每个节点的感受野覆盖了大部分图。我的经验是GCN层数控制在2到3层,再深就需要用残差连接或者JKNet之类的结构来缓解。
实操心得:GCN的层数不是越多越好。2层GCN在实际任务中经常能拿到不错的效果,3层以上就要小心过平滑问题。如果你需要更大的感受野,可以考虑用图采样或者层次化池化。
5. GAN:生成对抗网络的训练逻辑
GAN是我觉得最有意思也最难训的一类模型。它的核心思想是让两个网络互相对抗:生成器负责造假,判别器负责鉴假。
5.1 判别器与生成器的博弈本质
GAN的目标函数是:
$$ \min_G \max_D V(D, G) = \mathbb{E}{x \sim p{data}}[\log D(x)] + \mathbb{E}_{z \sim p_z}[\log(1 - D(G(z)))] $$
判别器 $D$ 希望最大化这个函数,也就是把真实样本判为1、生成样本判为0。生成器 $G$ 希望最小化这个函数,也就是让判别器把生成样本判为1。
训练过程是交替进行的:先固定生成器,更新判别器几轮;再固定判别器,更新生成器一轮。这个交替比例是一个超参数,实践中判别器和生成器的更新比例常见的是1:1到5:1。
有人问过"原始GAN公式的交叉熵为什么没有负号",这个问题其实涉及实现层面的细节。在理论公式里,判别器最大化的目标里生成样本那一项是 $\log(1 - D(G(z)))$,但实际代码里通常写成最小化 $-\log D(G(z))$,这是为了让生成器的梯度更稳定。两个形式在数学上等价,但梯度特性不同:前者在生成器效果差的时候梯度会消失,后者不会。
5.2 训练不稳定问题的排查与应对
GAN训练不稳定是出了名的。我踩过的坑包括:判别器太强导致生成器梯度消失、生成器太强导致判别器无法区分、模式崩塌导致生成器只输出少数几种样本。
针对判别器太强的问题,常见的做法是降低判别器的学习率,或者减少判别器的更新次数。我在一个图像生成任务里把判别器的学习率设成生成器的0.5倍,训练稳定性明显改善。
模式崩塌的应对方法包括使用WGAN的损失函数、加小批量判别、使用unrolled GAN等。我实测下来,WGAN-GP(带梯度惩罚的Wasserstein GAN)在稳定性上确实比原始GAN好很多,代价是每次更新判别器需要额外计算梯度惩罚项,训练速度慢一些。
5.3 常见变体与适用场景
GAN的变体非常多,但常用的就那么几类:
| 变体 | 核心改进 | 适用场景 |
|---|---|---|
| DCGAN | 用卷积替代全连接 | 图像生成 |
| WGAN-GP | 用Wasserstein距离+梯度惩罚 | 需要稳定训练的生成任务 |
| CycleGAN | 无需配对数据的域转换 | 图像风格迁移 |
| StyleGAN | 风格解耦+渐进式生成 | 高质量人脸生成 |
| Pix2Pix | 配对数据的条件生成 | 图像到图像的转换 |
我在数据增强场景里用过DCGAN来生成额外的训练样本。效果上,生成的样本确实能提升下游分类模型的准确率,但提升幅度有限,大概在1到2个百分点。如果数据量本来就够,GAN做增强的性价比不高;如果某一类样本特别少,用GAN生成补充是有意义的。
import torch import torch.nn as nn class Generator(nn.Module): def __init__(self, latent_dim=100, img_channels=3, feature_dim=64): super().__init__() self.net = nn.Sequential( nn.ConvTranspose2d(latent_dim, feature_dim * 8, 4, 1, 0, bias=False), nn.BatchNorm2d(feature_dim * 8), nn.ReLU(True), nn.ConvTranspose2d(feature_dim * 8, feature_dim * 4, 4, 2, 1, bias=False), nn.BatchNorm2d(feature_dim * 4), nn.ReLU(True), nn.ConvTranspose2d(feature_dim * 4, feature_dim * 2, 4, 2, 1, bias=False), nn.BatchNorm2d(feature_dim * 2), nn.ReLU(True), nn.ConvTranspose2d(feature_dim * 2, img_channels, 4, 2, 1, bias=False), nn.Tanh() ) def forward(self, z): z = z.view(z.size(0), z.size(1), 1, 1) return self.net(z)这段代码是一个标准DCGAN的生成器结构。输入是一个100维的噪声向量,通过转置卷积逐步上采样到 $64 \times 64$ 的图像。每一层转置卷积的参数含义是:输入通道、输出通道、卷积核大小、stride、padding。
注意:GAN训练时判别器和生成器的损失值不能作为训练好坏的直接指标。D loss下降不一定是好事,可能是判别器太强了。我一般通过定期可视化生成样本来判断训练状态,而不是盯loss曲线。
6. 模型选型与常见问题速查
把这六类模型放在一起看,你会发现它们各自解决了不同数据结构上的建模问题。DNN处理固定维度的向量,CNN处理网格结构,RNN/LSTM处理序列,GCN处理图,GAN处理生成。实际项目中,很多任务需要组合使用,比如用CNN提取图像特征再用LSTM做时序建模。
6.1 不同任务的模型选择对照
| 任务类型 | 数据类型 | 推荐模型 | 理由 |
|---|---|---|---|
| 表格数据分类/回归 | 结构化特征向量 | DNN | 特征维度低,全连接足够 |
| 图像分类 | 规则网格 | CNN | 局部感受野+权值共享 |
| 文本分类 | 词序列 | LSTM/Transformer | 捕捉上下文依赖 |
| 时间序列预测 | 连续数值序列 | LSTM | 门控机制保留长距离信息 |
| 社交网络分析 | 图结构 | GCN | 邻居聚合保留结构信息 |
| 图像生成/数据增强 | 图像 | GAN | 对抗训练生成逼真样本 |
| 图像分割 | 规则网格 | CNN(U-Net等) | 编码器-解码器+跳跃连接 |
这个对照表不是绝对的,但能帮你快速定位方向。比如你拿到一个推荐系统的任务,用户-物品关系是图结构,优先考虑GCN;如果你拿到一个传感器时序数据,优先考虑LSTM。
6.2 训练过程中的典型问题与排查方法
下表是我在实际项目中遇到的高频问题和对应的排查思路:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| Loss不下降 | 学习率过大/过小 | 尝试1e-3到1e-5之间的学习率 |
| Loss震荡剧烈 | Batch size太小 | 增大batch size或加梯度裁剪 |
| 训练集好测试集差 | 过拟合 | 加dropout、权重衰减、数据增强 |
| 梯度为NaN | 学习率过大或数值不稳定 | 加梯度裁剪,检查归一化 |
| LSTM不收敛 | 序列未归一化 | 对输入做标准化处理 |
| GCN效果差 | 特征质量不够或层数过多 | 检查节点特征,减少层数 |
| GAN模式崩塌 | 判别器太强 | 降低判别器学习率或改用WGAN |
我在调试一个LSTM时间序列模型时,遇到loss在前几轮下降后突然变成NaN的情况。排查了一圈发现是学习率设成了0.01,对LSTM来说太大了。改成0.001之后问题消失。这个经验告诉我,RNN类模型对学习率比CNN更敏感,默认从小学习率开始试。
还有一个常见问题是batch size和序列长度的配合。LSTM处理长序列时,如果batch size也很大,显存占用会非常高。我的做法是先用小batch size跑通流程,确认模型能收敛之后再逐步增大batch size或者用梯度累积来模拟大batch。
GCN这边,我遇到最多的问题是节点特征的维度不一致。有些节点有完整的特征向量,有些节点只有部分特征。处理方法是对缺失特征做填充,或者训练一个特征补全模块。这个问题在图数据里非常普遍,因为实际系统中用户信息往往是不完整的。
最后分享一个我在多个项目里都用过的调试技巧:先用一个极小的数据集(比如几十条样本)跑训练,看模型能不能过拟合到这个小子集上。如果能过拟合,说明模型结构和前向传播没问题,问题出在数据量或正则化上;如果连小子集都过拟合不了,那一定是代码有bug或者模型结构设计有问题。这个方法帮我省下了大量盲目调参的时间。