1. 从两条不同的信息流说起:CNN和RNN各自在解决什么问题
先把最容易搞混的一件事说清楚:CNN和RNN都属于神经网络这个大家族,也都是深度学习里最常被拿来当"主力军"的两类结构,但它们根本不是竞争关系,而是针对两种完全不同的数据形态长出来的两套解法。我在带新人的时候,最常说的一句话是:先别急着背结构图,先问自己一个问题——你手里的数据,是"空间上排布的一堆数",还是"时间上排布的一串数"?这个问题答对了,选CNN还是RNN基本就定了七成。
图像、视频帧、频谱图、医学影像,本质上是二维或三维网格上排布的值,相邻像素之间强相关,隔得远的像素基本没关系。这类数据吃的是局部性和平移不变性两碗饭,CNN就是为它们量身定做的。而文本、语音波形、股票序列、传感器读数、用户行为点击流,这些数据的关键特征是顺序——把"我打你"和"你打我"的序列打乱,含义就变了。RNN天生带一个循环结构,能把上一步的信息带到下一步,处理的就是这种"顺序敏感"的活儿。
1.1 前馈网络的硬伤:为什么不能直接堆全连接
很多人第一次学神经网络是从前馈神经网络(也叫BP神经网络、多层感知机)入门的,用MATLAB或者Python跑一个拟合曲线的demo,几个隐藏层,配个反向传播,效果看着挺好。于是自然会产生一个想法:图像不也就是一堆数字吗?把32×32×3的图片拉平成一个3072维向量,丢进全连接网络不就行了?
能做,但代价大得离谱。假设第一层隐藏层只有1000个神经元,那么这一层就要 3072 × 1000 = 3,072,000 个权重,再加上1000个偏置,三百万级别的参数只换来一层。而一张稍微大点的图,比如224×224×3,拉平就是150528维,第一层1000个神经元直接飙到1.5亿参数。这不是"训练慢"的问题,是根本训不动、必过拟合的问题。
更致命的是,全连接层把空间结构彻底抹平了。像素(0,0)和像素(0,1)本来是紧邻的,拉平之后它们变成了向量里两个普通的维度,网络得从零开始学"这两个位置相邻"这件事。而当我们把图片整体右移一个像素,对网络来说输入向量全体变了,它得重新学一遍——平移不变性完全没被利用起来。
1.2 CNN的两把钥匙:局部连接与权值共享
CNN解决上面两个问题的办法非常朴素,朴素到你第一次看懂会觉得"就这么简单?"
第一把钥匙是局部连接。既然图像里真正有意义的模式(边缘、角点、纹理)都是局部的,那就别让一个神经元去管整张图,只让它看一个小窗口。比如用5×5的窗口,一个神经元只管25个像素,参数量立刻从"整图维度"降到25。
第二把钥匙是权值共享。同一个5×5的卷积核在整张图上滑动,位置不同但用的是同一组权重。这意味着:如果这个卷积核学会了"检测左上到右下的斜边",那它在图片任何位置都能检测这条斜边。参数量跟图像尺寸彻底解耦了——不管你输入是32×32还是224×224,这个5×5的核永远只要25个参数。
用LeNet时代的经典对比:输入32×32单通道图,接6个5×5卷积核,参数量是 5×5×1×6 + 6 = 156。同样是连接到一个1000神经元的全连接层,参数是32×32×1000 + 1000 = 1,025,000。差了六千多倍,而表达能力一点不差——因为图像本身的结构已经被编码进"卷积"这个先验里了。
1.3 RNN的必要性:序列里没有"滑动窗口"这种便宜可占
序列数据的问题不一样。你看一段文本"今天天气不错,我打算去____",要预测空格,你得记住前面的语义。但序列的长度是不固定的,今天10个词,明天500个词。如果用全连接网络,你就得固定输入长度,超出的截断,不足的补零,非常别扭。
RNN的思路是:让网络有一个隐藏状态,每一步接收当前输入和上一步的隐藏状态,输出新的隐藏状态。同一个权重矩阵在所有时间步上共享——这是序列维度上的"权值共享"。这样不管序列多长,参数量都不变,而且理论上能记住任意长度的历史。
代价也很明显:序列是没法并行计算的,第t步必须等第t-1步算完。这跟CNN可以一次性卷完整张图形成鲜明对比,也是后来Transformer能崛起的关键原因之一。但在很多中小规模任务上,RNN依然是性价比很高的选择,尤其是数据量不大、序列不太长的时候。
2. 卷积神经网络:把"看见"拆解成可计算的小动作
理解CNN最有效的方式,不是背"卷积-激活-池化"这个流水线,而是搞清楚每一层在物理上到底干了什么。我习惯把它类比成人眼的一个极简版:视网膜先检测边缘和光斑,往上传一层组合成纹理和角点,再往上组合成部件和物体。CNN的层次结构几乎是照着这个逻辑设计的。
2.1 卷积核到底在算什么:从一个3×3的边缘检测器说起
拿一个最经典的3×3卷积核举例:
-1 -1 -1 -1 8 -1 -1 -1 -1把它盖在图像上任意一个3×3区域,做逐元素相乘再求和。如果这9个像素完全一样(一片平坦区域),结果是 -1×9c + 8×c = -c,接近0(严格说是 -c,如果核做了归一化就是0);如果中心像素明显比其他亮(一个亮点或角点),结果就是个很大的正数。这个核干的事就是边缘/角点检测。
关键在于:在训练之前,我们并不手工设计这些核,而是初始化成随机数,然后靠反向传播让网络自己学出该用什么样的核。在浅层,网络几乎总会学出类似边缘检测器的核;在深层,核变得抽象,可能是"猫耳朵""轮胎纹理"这种人类说不清但确实有判别力的模式。
这里有个细节值得强调:一次卷积只输出一个特征图(feature map)。实际网络里一层往往有几十到上千个卷积核,输出就是几十到上千个通道的特征图叠在一起。所以"层数越深通道越多"不是随便设计的,而是为了让网络同时并行地检测越来越多种类的模式。
2.2 输出尺寸怎么算:一个必须刻进肌肉记忆的公式
初学者最常见的报错来源就是尺寸对不上。公式如下:
输出尺寸 = (输入尺寸 - 卷积核大小 + 2 × padding) / 步长 + 1(除法结果向下取整)
举几个具体例子,方便你对照:
| 输入 | 核 | padding | 步长 | 输出 | 说明 |
|---|---|---|---|---|---|
| 32×32 | 5×5 | 0 | 1 | 28×28 | LeNet第一层,尺寸缩小 |
| 32×32 | 3×3 | 1 | 1 | 32×32 | "same"卷积,尺寸不变 |
| 32×32 | 3×3 | 0 | 2 | 15×15 | 下采样两倍 |
| 7×7 | 3×3 | 0 | 1 | 5×5 | 再卷两层就变1×1 |
padding=1配3×3核是深度网络里最常用的组合,因为它能保持特征图尺寸不变,方便堆很多层。深度网络里堆几十层卷积并不奇怪,尺寸控制不住的话几层之后图就变没了。
感受野是另一个必须理解的概念:深层一个像素,对应到原图上覆盖多大区域。3×3卷积堆两层,感受野是5×5;堆三层是7×7;而一个7×7的卷积核感受野也是7×7,但参数量是49对27。这就是为什么现代网络几乎清一色用3×3小核堆叠——同样的感受野,更少的参数,更多的非线性(每层后面都有激活函数)。
2.3 池化层干了什么,以及它为什么在慢慢退场
池化(Pooling)最常见的是最大池化和平均池化,2×2窗口步长2,把特征图长宽各砍一半。它的作用有三个:一是降低计算量;二是扩大后续层的感受野;三是提供一点平移鲁棒性——目标移动一两个像素,池化后的输出基本不变。
但池化有个问题:它丢信息,而且是不可逆地丢。在分割、检测、超分辨率这类需要精确定位的任务里,池化是有害的。所以从ResNet以后,很多网络用步长卷积代替池化(stride=2的3×3卷积),让下采样这个过程也变成可学习的。你在读最近几年的论文时会发现,纯池化结构已经越来越少了,这算是一个明显的趋势。
顺带说一句行业工具的事。工业视觉里,像HALCON这类商用视觉软件、以及MATLAB的深度学习工具箱,都把CNN封装得相当好,几行配置就能训一个分类器。上手快是优点,但理解不了卷积尺寸和通道的变化规律,你在调参和排错时依然会被卡住。我建议至少用PyTorch或TensorFlow手写过一遍,再回去用封装工具,效率会高得多。
3. 循环神经网络:让历史留在记忆里
如果说CNN是在空间上做文章,RNN就是在时间上做文章。它的核心结构简单到可以用一行公式写清楚,但就是这一行公式,衍生出了梯度消失、长短期记忆、门控循环单元这一整条技术线。理解RNN的关键不在结构,而在于理解它为什么"记不住"以及怎么让它"记住"。
3.1 一行公式背后的结构
RNN的隐藏状态更新可以写成:
h_t = tanh(W_xh · x_t + W_hh · h_{t-1} + b_h)
其中 x_t 是第t步的输入,h_{t-1} 是上一步的隐藏状态,W_xh 和 W_hh 是两组权重矩阵。注意,W_hh在所有时间步共享,这是RNN参数量与序列长度无关的根本原因。
参数量估算:假设输入维度100,隐藏维度128,那么 W_xh 是 128×100 = 12800,W_hh 是 128×128 = 16384,加上偏置128,总共约29312个参数。不管你处理10个时间步还是1000个时间步,都是这么多参数。
把这行公式按时间展开,你会得到一个"横着躺下的深层网络":第1步的输出喂给第2步,第2步喂给第3步……所以RNN在训练时依然要靠反向传播,只不过是在时间维度上传播,叫BPTT(Backpropagation Through Time)。这就是它慢的根源——时间步之间必须串行。
3.2 梯度消失与爆炸:RNN记不住长距离依赖的数学原因
假使我们要求第100步的梯度,需要一路链式回传到第1步,中间会连着乘100次 W_hh(以及tanh的导数)。这带来两个后果:
- 如果 W_hh 的谱半径(最大奇异值)小于1,连乘之后梯度指数级衰减,第1步的权重几乎收不到有效梯度更新,网络就"忘了"早先的信息。这就是梯度消失。
- 如果大于1,梯度指数级爆炸,训练直接发散成NaN。这是梯度爆炸。
梯度爆炸相对好治——梯度裁剪(gradient clipping)就能压住,把梯度范数限制在一个阈值内(比如5.0)再更新。但梯度消失是结构性问题,裁剪帮不上忙,只能靠换结构解决。
实验上有个很直观的现象:普通RNN在序列长度超过20~30之后,对早期信息的记忆就基本失效了。你可以自己写个实验验证——让RNN记住序列第一个词,然后在最后一步输出它,随着序列变长,准确率会肉眼可见地掉下去。
3.3 LSTM和GRU:用"门"来管理记忆的读写
LSTM(长短期记忆网络)的设计思路非常工程化:既然一条直路传不下去,那就修一条高速公路。它引入了一个细胞状态C_t,这条线上只有加法和逐元素乘法,梯度回传时不会反复经过矩阵乘,因此能传很远。
控制这条路的是三个门:
- 遗忘门 f_t:决定上一时刻的细胞状态保留多少。它的输出经过sigmoid压到0~1,接近0就"忘掉",接近1就"全留"。
- 输入门 i_t:决定当前这一步的新信息写多少进细胞状态。
- 输出门 o_t:决定细胞状态的多少内容暴露给隐藏状态 h_t 输出出去。
用一句话概括:LSTM把RNN的"每次全量覆盖"改成了"有选择地遗忘、有选择地写入、有选择地输出"。这个改动带来的效果是量级的——序列长度上千,LSTM依然能保持对早期信息的感知。
GRU是LSTM的简化版,把三个门合并成两个(更新门和重置门),参数量少约三分之一,在很多任务上效果和LSTM打平甚至更好,训练还更快。我的实际经验是:先试GRU,效果不够再上LSTM,尤其是数据量不大的场景,GRU不容易过拟合。
| 结构 | 门数量 | 参数量 | 长依赖能力 | 适用场景 |
|---|---|---|---|---|
| 普通RNN | 0 | 最少 | 弱(<30步) | 短序列、教学演示 |
| GRU | 2 | 中 | 较强 | 中小数据、追求训练速度 |
| LSTM | 3 | 最多 | 强 | 长序列、数据充足 |
4. CNN和RNN不是二选一:组合、替代与选型逻辑
新手常问"做项目该用CNN还是RNN",这个提问本身就有点问题。真实项目里,经常是两者一起用,或者干脆用别的东西替代它们。搞清楚这几条演变路线,你对这两类结构的理解会完整很多。
4.1 经典组合:CNN打底提取空间特征,RNN负责序列输出
最典型的例子是图像描述生成(看图说话)。一张图进来,先用CNN(比如ResNet)提特征,得到一组空间特征向量;然后把这组向量作为初始输入,喂给一个LSTM,让它一个词一个词地生成描述。CNN负责"看懂",RNN负责"说清楚",分工极其清晰。
视频理解也是同一个套路:每一帧过CNN抽特征,抽出来的特征序列送进RNN或LSTM建模时间关系。音频领域也一样,先做频谱图(本质是二维图),CNN卷一遍,再交给RNN处理时序。
这类结构在2015到2020年间是绝对主流。现在虽然被Transformer大量替代,但在数据量小、算力有限、延迟要求高的场景下,CNN+RNN的组合依然是很难被超越的性价比之选——参数量小、推理快、部署到边缘设备轻松。
4.2 一维卷积做序列:被低估的TCN路线
RNN有个改不掉的毛病:不能并行。这在大规模训练时是致命的。于是有人提出:用一维卷积处理序列行不行?
答案是行,而且效果往往出人意料地好。TCN(时序卷积网络)用因果一维卷积(保证不看未来)+ 空洞卷积(扩大感受野)来建模序列。它能并行计算,感受野可以精确控制,梯度也更稳。在一些时序预测任务上,TCN打得过LSTM,还快得多。
真正在NLP领域掀起革命的Transformer,本质上也可以理解为一种"用注意力机制替代循环"的序列建模方法。它的核心优势之一就是彻底摆脱了时间步的串行依赖。
我的个人建议是这样的:如果你的序列不长(比如几百步以内),数据量也不大,RNN/LSTM/GRU是最省心的选择,代码短、调参少。如果序列很长或者数据量很大,或者需要并行训练,就该考虑一维卷积或注意力机制了。
4.3 一份可直接对照的选型决策表
| 数据形态 | 首要候选 | 备选 | 关键理由 |
|---|---|---|---|
| 二维图像分类 | CNN | ViT(数据量大时) | 局部性与平移不变性 |
| 一维信号(振动/心电) | 一维CNN | LSTM、TCN | 局部模式检测强 |
| 文本分类(短文本) | 一维CNN / GRU | 预训练语言模型 | 训练快、够用 |
| 长文本生成 | LSTM / Transformer | GRU | 长依赖建模 |
| 视频动作识别 | 3D CNN + RNN | 时空注意力 | 时空双维度 |
| 时序预测(传感器) | LSTM / GRU / TCN | 一维CNN | 长期趋势依赖 |
提示:别一上来就选最复杂的结构。先用最简单的方案跑出一个能用的baseline,量出它的短板在哪,再决定要不要升级。我见过太多项目,一开始就上最花哨的模型,结果调了两周还没跑通,最后退回简单方案三天就交付了。
5. 手写一遍才算真的会:两个最小可复现案例
看一百篇原理文章,不如自己跑通一次。下面两个案例我都刻意选了最小规模,保证在一台普通笔记本上几分钟内能跑完。代码用PyTorch,装好环境就能直接跑。
5.1 环境准备与数据形状的坑
环境没什么花头,一条命令:
pip install torch torchvision matplotlib numpy重点说数据形状,这是90%初学者卡住的地方。PyTorch里:
- CNN要求的输入是
(N, C, H, W),即批大小、通道数、高、宽,四个维度一个都不能少。 - RNN要求的输入默认是
(L, N, F),即序列长度、批大小、特征维度。
这俩顺序不一样,特别容易搞混。如果你发现报错信息是"Expected 4D input"或者"input.size(-1) must be equal to input_size",八成就是形状问题。数据处理时最保险的做法是每一步都print(x.shape)确认一下,别嫌麻烦。
5.2 CNN做FashionMNIST分类:20行核心代码
import torch import torch.nn as nn import torchvision import torchvision.transforms as T class SimpleCNN(nn.Module): def __init__(self, num_classes=10): super().__init__() self.features = nn.Sequential( nn.Conv2d(1, 16, 3, padding=1), # 28x28 -> 28x28 nn.ReLU(), nn.MaxPool2d(2), # 28x28 -> 14x14 nn.Conv2d(16, 32, 3, padding=1), # 14x14 -> 14x14 nn.ReLU(), nn.MaxPool2d(2), # 14x14 -> 7x7 ) self.classifier = nn.Sequential( nn.Flatten(), nn.Linear(32 * 7 * 7, 128), nn.ReLU(), nn.Dropout(0.3), nn.Linear(128, num_classes), ) def forward(self, x): return self.classifier(self.features(x)) transform = T.Compose([T.ToTensor(), T.Normalize((0.5,), (0.5,))]) train_set = torchvision.datasets.FashionMNIST( root="./data", train=True, download=True, transform=transform) loader = torch.utils.data.DataLoader(train_set, batch_size=64, shuffle=True) model = SimpleCNN() opt = torch.optim.Adam(model.parameters(), lr=1e-3) loss_fn = nn.CrossEntropyLoss() for epoch in range(3): for x, y in loader: opt.zero_grad() out = model(x) loss = loss_fn(out, y) loss.backward() opt.step() print(f"epoch {epoch}, last loss {loss.item():.4f}")这份代码里有三个地方值得你单独琢磨。第一,两个卷积层的通道数从1到16再到32,是递增的——因为浅层特征简单但空间分辨率高,深层特征复杂但分辨率低,通道递增能让每层的"信息容量"大致平衡。第二,Dropout(0.3)放在全连接层之前,是为了抑制过拟合,因为全连接层参数量往往占整个网络的大头。第三,Flatten之后的输入维度是32*7*7=1568,这个数字必须跟前面卷积层输出的尺寸严格对上。如果你改了网络结构,这个数一定要重算一遍。
跑三个epoch,这个简单模型在FashionMNIST上能到90%左右的准确率。想加验证集的话,把train=True改成False再建一个loader就行。
5.3 RNN做正弦波预测:看它怎么"记住"趋势
这个任务比MNIST更能说明RNN在干什么——让模型看前20个点,预测第21个点。
import torch import torch.nn as nn import numpy as np # 构造数据:正弦波滑窗 t = np.linspace(0, 100, 4000) data = np.sin(t).astype(np.float32) seq_len = 20 X, Y = [], [] for i in range(len(data) - seq_len): X.append(data[i:i+seq_len]) Y.append(data[i+seq_len]) X = torch.tensor(np.array(X)).unsqueeze(-1) # (N, L, 1) Y = torch.tensor(np.array(Y)).unsqueeze(-1) # (N, 1) class RNNRegressor(nn.Module): def __init__(self, hidden=32): super().__init__() self.rnn = nn.GRU(1, hidden, batch_first=True) self.fc = nn.Linear(hidden, 1) def forward(self, x): out, _ = self.rnn(x) # out: (N, L, hidden) return self.fc(out[:, -1, :]) # 只取最后一步 model = RNNRegressor() opt = torch.optim.Adam(model.parameters(), lr=5e-3) loss_fn = nn.MSELoss() for epoch in range(200): opt.zero_grad() pred = model(X) loss = loss_fn(pred, Y) loss.backward() opt.step() if epoch % 50 == 0: print(epoch, loss.item())这里有个非常容易写错的细节:out[:, -1, :]。RNN的输出是每个时间步的隐藏状态,我们做预测只关心最后一步的,因为最后一步的隐藏状态包含了对整段序列的记忆。新手经常直接拿out去接全连接层,形状对不上直接报错,或者误取了第一步导致效果极差。
另外注意batch_first=True这个参数。默认RNN的输入是(L, N, F),加了这个参数才变成(N, L, F),跟CNN的习惯一致。忘了加这个参数,模型照跑不误,但结果会一塌糊涂——因为它把序列长度当成了批大小,等于把数据完全打乱训练。
5.4 训练过程中最容易踩的四个坑
跑通代码只是开始,能不能训出好结果又是另一回事。下面几条是我自己反复踩过的:
- 学习率是第一优先级。Adam从1e-3起步,SGD从1e-2起步,然后观察loss曲线。如果loss在前几十步就震荡甚至往上飙,先降学习率一个数量级再试。
- epoch不是越多越好。要同时看训练loss和验证loss。训练loss一直降、验证loss开始升,那就是过拟合的开始,该停或者加正则了。这个转折点就是所谓early stopping的判断依据。
- 归一化不能省。输入数据不做归一化(比如减均值除标准差、或者缩放到[0,1]),训练会慢好几倍,甚至根本训不动。RNN对这点尤其敏感,因为激活函数是tanh,输入过大会直接饱和。
- 随机种子要固定。调试阶段加一行
torch.manual_seed(42),不然每次跑出来的结果都不一样,你根本分不清是改了模型有效还是随机波动。
6. 一些文档里不会写的实战经验
前面讲的都是"应该怎么做",这一节聊聊"实际做的时候会遇上什么"。这些东西教科书上基本不会写,但每一个都实实在在地拖慢过我的进度。
6.1 从CNN迁移到RNN时,最反直觉的是"序列长度不是特征维度"
做图像做久了,脑子里会形成"输入维度越高越好"的惯性。切到序列任务时,很多人会想:我有20个时间点,那不就是20维特征吗?然后把它当成一个20维向量丢进全连接。
这个做法丢掉的是顺序信息和位置共享。第1个点和第20个点在模型眼里变成了两个独立的特征,跟"它们是同一条序列上的相邻元素"这个事实毫无关系。用滑窗做数据增强时会立刻暴露问题——换个起点,模型就得重新学。
正确的理解是:序列任务里的每个时间点都是同一种东西的不同时刻,所以它们应该共享权重。这也是为什么RNN的参数量与序列长度无关,而全连接的参数量随输入维度线性增长。想通这一点,你就明白为什么CNN和RNN本质上都是"共享权重",只是一个在空间上共享,一个在时间上共享。
6.2 显存不够时,先动batch size还是先动模型
显存爆掉的时候(CUDA out of memory),我的处理顺序是这样的:
- 先降batch size,从64降到32、16。这是最不影响模型结构的操作。但注意,batch size小了之后,梯度噪声变大,学习率最好相应调小一点。
- 再考虑降输入尺寸。图像任务里把224降到160,显存占用能降一半以上,精度损失往往可以接受。序列任务里把seq_len从512降到256同理。
- 最后才动模型宽度。把通道数或隐藏维度砍掉,这是伤筋动骨的,精度掉得明显。
- 梯度累积是保精度降显存的好办法。用小batch跑4次,累积梯度再更新一次,等效于大batch,代价只是训练慢一点。
另外提一句,训练完做推理时记得torch.no_grad(),能省下大量显存,还更快。
6.3 什么时候该放下CNN和RNN,转向注意力机制
这两年最常被问的问题就是:"我是不是该直接用Transformer?"我的判断标准比较务实:
- 数据量在万级以下:老老实实用CNN或RNN,Transformer在小数据上很容易欠拟合,因为它没有CNN那种内置的归纳偏置(局部性、平移不变性),得靠大量数据自己学出来。
- 数据量在十万级以上,且算力充足:可以考虑Transformer系结构,它在长依赖建模和并行训练上的优势会显现出来。
- 延迟敏感的端侧部署:CNN依然是首选,参数量小、推理快、生态成熟,各种量化剪枝工具支持得也最好。
- 需要精细控制感受野和结构的场景:一维卷积或TCN比纯注意力更好调。
说白了,模型没有绝对的先进与落后,只有跟数据规模、算力预算、部署环境是否匹配。我做过的一个振动信号故障诊断项目,数据只有八千条样本,试过一圈最后回到一维CNN+LSTM,准确率比硬上Transformer高了将近6个点,推理速度还快了一个数量级。
6.4 最后分享几个调试小技巧
第一个技巧:先把模型调到能过拟合一个小数据集。比如取100张图,关掉所有正则,训练到loss接近0。如果连过拟合都做不到,说明代码里有bug,而不是模型不够强。这个步骤能帮你排除掉80%的代码错误。
第二个技巧:打印每一层的输出形状。在forward里加一行print(x.shape),跑一个batch看一遍,比对着论文猜尺寸快得多。CNN和RNN的形状问题几乎都能靠这一招定位。
第三个技巧:损失函数和输出层要配对。分类任务用交叉熵,PyTorch的CrossEntropyLoss内部自带softmax,所以网络最后一层不要再加softmax,否则相当于做了两次,梯度会出问题。这个坑我自己踩过,表现为loss下降特别慢,排查了很久才发现是重复softmax。
第四个技巧:RNN的初始隐藏状态默认是全零,大多数情况下这样就行。但如果你的序列特别长,或者任务对起始状态敏感,可以考虑把隐藏状态设为可学习参数。这个改动很小,在某些任务上能带来明显提升。
这些经验说白了都不复杂,但都是得自己撞过墙才记得住的。CNN和RNN这两套结构从提出到现在已经十几年,中间虽然被各种新架构冲击过,但它们在中小规模任务上的地位一直没有真正被取代——参数量小、训练快、对数据量要求低、部署友好,这四条优势在工业界比"刷榜指标高0.5个点"值钱得多。等你把它们吃透了,再去看注意力机制、图神经网络这些新东西,会发现很多设计思路其实是一脉相承的。