
深度学习这个方向我断断续续做了五年多从最早拿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$padding1stride1那么输出是$$ \frac{224 - 3 2}{1} 1 224 $$也就是说$3 \times 3$ 卷积核配合padding1、stride1特征图尺寸保持不变。这个配置在VGG和ResNet里被大量使用因为它允许你堆叠很多层而不改变空间尺寸。通道数的设计上我遵循的经验是随着空间尺寸减小通道数逐步增加。比如从32到64到128到256。这样做的原因是空间尺寸缩小后每个位置承载的信息变得更加抽象需要更多通道来表达不同的语义特征。这个设计模式在几乎所有的经典CNN架构里都能看到。卷积核大小的选择上$3 \times 3$ 是最常用的。两个 $3 \times 3$ 卷积堆叠的感受野等于一个 $5 \times 5$ 卷积但参数量更少而且中间多了一次非线性激活。这就是VGG网络的核心设计思想。2.3 池化层的取舍与常见误区池化层的作用是降低特征图的空间尺寸减少计算量同时提供一定的平移鲁棒性。最大池化是最常用的方式取每个窗口内的最大值。但池化层并不是必须的。现在很多网络结构比如一些轻量级网络用stride2的卷积来替代池化效果也很好。池化的缺点在于它丢弃了位置信息这在分类任务里可能无所谓但在分割、检测这类需要精确定位的任务里就是问题。我在做一个缺陷检测项目时一开始用了大量最大池化层结果模型对缺陷位置的定位精度很差。后来改成用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_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropout0.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_firstTrue让输入维度是(batch, seq_len, feature)dropout0.2在多层LSTM之间加正则化最后取最后一个时间步的输出做预测。注意LSTM的num_layers过多超过3层在小数据集上很容易过拟合而且训练速度明显变慢。我一般从1到2层开始除非数据量确实很大。4. GCN非欧几里得空间上的特征聚合GCN图卷积网络处理的是图结构数据。社交网络、分子结构、知识图谱、推荐系统里的用户-物品关系这些数据不是规则网格传统的CNN和RNN都没法直接套用。4.1 图结构数据的特殊性图由节点和边组成。每个节点可能有自己的特征向量边表示节点之间的关系。图结构数据跟图像数据最大的区别在于图像的邻居是规则的每个像素上下左右固定而图的邻居是任意的每个节点的度可能不同。这意味着你没法像CNN那样用一个固定大小的卷积核在图上来回滑动。GCN的解决方案是从邻居节点聚合信息来更新当前节点的表示。4.2 谱域与空域视角的GCN理解GCN的原始推导来自谱图理论涉及拉普拉斯矩阵和傅里叶变换数学门槛不低。但如果只从实操角度理解可以简化为一个消息传递的过程。每一层GCN做的事情可以概括为三个步骤第一步每个节点从邻居节点收集特征。第二步对所有收集到的特征做加权求和。第三步用一个线性变换和激活函数处理聚合后的特征。这个过程可以用一个简化的公式表达$$ H^{(l1)} \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_dim100, img_channels3, feature_dim64): super().__init__() self.net nn.Sequential( nn.ConvTranspose2d(latent_dim, feature_dim * 8, 4, 1, 0, biasFalse), nn.BatchNorm2d(feature_dim * 8), nn.ReLU(True), nn.ConvTranspose2d(feature_dim * 8, feature_dim * 4, 4, 2, 1, biasFalse), nn.BatchNorm2d(feature_dim * 4), nn.ReLU(True), nn.ConvTranspose2d(feature_dim * 4, feature_dim * 2, 4, 2, 1, biasFalse), nn.BatchNorm2d(feature_dim * 2), nn.ReLU(True), nn.ConvTranspose2d(feature_dim * 2, img_channels, 4, 2, 1, biasFalse), 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对抗训练生成逼真样本图像分割规则网格CNNU-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或者模型结构设计有问题。这个方法帮我省下了大量盲目调参的时间。