恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

深度学习序列建模进阶:从RNN到Transformer实践手记

  • 首页
  • 资讯中心
  • /
  • 深度学习序列建模进阶:从RNN到Transformer实践手记

相关资讯

Python ctypes 加载 OpenMPI 动态库报 MCA 错误排查与解决 2026/10/16 22:43:58
弱监督音素预训练:低资源语言语音识别的破局之道 2026/10/16 22:38:58
K3 Flight 项目全解:2.8万亿参数 Kimi K3 为什么能在纯 CPU 上本地运行? 2026/10/16 22:38:58

最新资讯

RBF_BP神经网络自校正PID控制器Matlab仿真详解
大模型——MCP Server 开发实战指南(Python版):从零搭建到 TaoToken 统一 Key 接入
机器视觉:工业镜头——从光学成像到镜头选型
机器视觉:光学成像基础——从物体到传感器的光学映射
算力依然不足?用 TaoToken 统一 Key 打通 Cline MCP 与 Windsurf BYOK 的配置思路
DeepSeek-R1接入实战:API调用与本地部署全流程指南

今日推荐

Qwen3.5-122B-A10B-singprobe幻觉检测实战:AUC 0.7806,如何实时揪出大模型一本正经的胡话?
NetSpot 实战:用信道分析与频宽优化解决 Wi-Fi 卡顿
C++ split() 字符串分割的五种实现与性能实测

本周热门

UE动画修改实战:从资产编辑到重定向与蒙太奇驱动
统计随机数生成器攻击下的KLJN安全密钥交换协议Matlab仿真
政务API安全治理:资产测绘、低代码编排与行标对标实践

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

深度学习序列建模进阶:从RNN到Transformer实践手记

发布时间:2026/10/16 22:43:58
深度学习序列建模进阶:从RNN到Transformer实践手记 深度学习笔记二序列建模进阶从RNN到Transformer的实践手记上一篇笔记我们聊了神经网络的基础从反向传播讲到了简单的全连接网络怎么在手写数字识别上跑起来。当时就有朋友留言问像文本翻译、语音识别这种输入输出长度不固定的任务普通网络根本搞不定下一步该往哪个方向走。所以这一轮笔记我打算集中梳理一下序列建模这条线重点聊聊RNN家族到底解决了什么问题、又卡在了哪里以及现在大家实际项目里几乎默认首选的Transformer是怎么一步步把问题拆解掉的。这篇笔记适合刚学完深度学习基础、想进入自然语言处理或者时序预测这类方向的同学参考。我会按照自己的学习路线从循环神经网络的结构讲起接着拆解Transformer的核心机制最后附上一份可以直接运行的最小实现和踩坑记录。不堆公式也不跳过原理尽量用大白话把“为什么这么做”讲明白。1. 为什么序列问题这么难搞我记着刚开始接触序列建模的时候第一反应是这不就是个多输入多输出的网络吗把每个词喂进去不就行了。后来自己动手做英文分词再转成向量才意识到事情没那么简单。1.1 输入输出长度不固定带来的麻烦普通全连接网络有个硬性要求输入维度是固定的。你可以把一张224乘224的图片展平成一大堆像素点喂进去但没法把一个长度不定的句子直接塞进去。万一这个句子有10个词、下个句子有30个词呢总不能在网络前面加一个“自动裁剪”的开关吧。更麻烦的是输出。做翻译的时候源语言句子和目标语言句子的长度往往不对应英文一个长句翻成中文可能拆成两三个短句。你要是用固定维度的输出层就得预先定死最大长度短了浪费计算量长了又不知道拿什么填充。这就是序列建模第一个核心矛盾信息长度本身是动态的但传统网络结构要求一个静态的骨架。1.2 词与词之间不是孤立的第二个难点在于句子里的每个词都要结合上下文才能确定含义。比如“苹果”这个词出现在“苹果很好吃”和“苹果发布了新手机”两句话里语义完全不同。你再怎么把单个词编码成向量如果没有上下文信息模型根本分不清。我当时试过最简单的办法把一句话里所有词的词向量直接相加取平均然后接一个分类器。效果嘛只能说不堪入目。不仅词序信息全丢了而且“苹果”在这种做法里永远只有一个固定的表示完全没有办法体现语境的变化。所以序列建模要解决的问题其实很清楚第一结构必须能处理变长输入第二每个位置的表示必须包含上下文信息第三也是容易被忽略的长距离依赖不能因为距离增大就断掉。2. 循环神经网络家族曾经的序列建模主力既然普通网络搞不定自然就有人想到那我能不能把网络“循环”起来每到一个新词网络不仅看当前输入还把上一个时刻的记忆一起带进来。2.1 RNN的基本工作方式循环神经网络的核心就是一个带隐藏状态的单元。你输入当前词同时接收上一步的隐藏状态然后输出一个新的隐藏状态。这个隐藏状态就像是模型的“工作记忆”理论上能把前面所有信息压缩在里面。用代码写个简化版的RNN单元其实也就几行import torch import torch.nn as nn class SimpleRNNCell(nn.Module): def __init__(self, input_size, hidden_size): super().__init__() self.input_proj nn.Linear(input_size, hidden_size) self.hidden_proj nn.Linear(hidden_size, hidden_size) def forward(self, x, h_prev): # x: [batch, input_size] # h_prev: [batch, hidden_size] combined self.input_proj(x) self.hidden_proj(h_prev) h_next torch.tanh(combined) return h_next这样一个单元沿着时间步反复调用就是最朴素的RNN。它比固定窗口的方案强在隐藏状态理论上可以记忆任意远的过去。2.2 为什么朴素的RNN撑不住长句子但等到你真把句子拉长到二三十个词问题就暴露了。首先是梯度消失或者说梯度爆炸。你反向传播的时候梯度要沿着时间步一路传回去每一步都要乘上隐藏层权重的雅可比矩阵。这个矩阵如果特征值小于1连乘起来梯度指数级衰减最后前面几步的权重根本得不到有效更新。这就好比你往一列很长的队伍里喊话每个传话的人都会丢掉一点信息传到最后基本就是噪音了。后来业界出了两个改进版本一个叫LSTM一个叫GRU。LSTM搞出了输入门、遗忘门、输出门三个结构GRU简化成两个门。它们用“门”来控制哪些信息保留、哪些信息丢弃确实能把有效记忆长度延长到百步量级。2.3 LSTM和GRU的真实能力边界我在实际训练里体会过LSTM在句子长度不超过50个token的文本分类任务上效果还是相当能打的。GRU参数更少、训练更快在小数据集上表现往往不输LSTM。但它们的瓶颈也很明显整个序列必须逐词处理没法并行。你算第10个词的时候必须等第9个词的隐藏状态算完。当时我在单卡上训练一个字符级的LSTM语言模型一批数据才几十条一个epoch要跑将近二十分钟。你要想换更大的模型对不起训练时间直接撑不住。另外还有一个比较尴尬的事实LSTM所谓的“长记忆”也只是相对RNN而言。你要是真拿它去做动辄几百个token的文本摘要任务后面的内容大概率已经把开头忘得差不多了。3. Transformer把注意力机制变成主角2017年那篇“Attention Is All You Need”出来以后整个序列建模的格局基本就变了。Transformer放弃循环结构直接让序列里每个位置都跟其他位置做交互一次性算出整条序列的表示。3.1 自注意力到底在算什么一句话解释自注意力给定一个序列每个位置的输出是所有位置信息的加权求和权重由当前位置和每个其他位置的“相关性”决定。这个“相关性”怎么算简单来说就是把每个词映射成三个向量查询(Query)、键(Key)、值(Value)。Query代表“我想找什么信息”Key代表“我能提供什么信息”Value就是“我真正携带的信息”。两两算Query和Key的点积归一化一下就得到权重。拿个生活类比你在会场里想找一个聊过天的朋友你的“Query”就是你对他的印象描述每个人的“Key”就是他们胸前挂的名牌点积大说明匹配度高你就把更多注意力放在那个人身上。代码里自注意力是这样的import torch import torch.nn.functional as F def scaled_dot_product_attention(q, k, v, maskNone): # q, k, v: [batch, seq_len, head_dim] d_k q.size(-1) scores torch.matmul(q, k.transpose(-2, -1)) / (d_k ** 0.5) if mask is not None: scores scores.masked_fill(mask 0, float(-inf)) weights F.softmax(scores, dim-1) return torch.matmul(weights, v), weights我说一下为什么要除以根号d_k。如果不做缩放当维度比较大的时候点积的数值会变得很大softmax之后梯度会趋近于零模型学不动了。除以根号维度本质上是把点积的方差拉回到一个合适的范围。3.2 多头注意力为什么比单头强代码里你经常看到head_size64这种设置多头就是把这个维度拆成几份并行算。比如8个头每个头只管64维最后拼回去变成512维。这样做的原因不复杂不同的头可以关注不同的信息类型。有的头可能天然学会了关注句法结构比如“动词”和它的“主语”有的头可能学会了关注指代关系比如“它”到底指代前面哪只动物。我也试过把注意力权重可视化确实能看到某些头存在非常清晰的对角线倾向——它就是在注意前一个词或者后一个词。这种多样性是单个注意力头很难同时具备的。3.3 位置编码让模型知道词的先后顺序注意力机制本身是“无序”的。你把一句话的单词顺序打乱注意力计算出来的结果完全一样因为所有位置都在做同样的两两交互。但语言里词序显然重要“我打你”和“你打我”含义天差地别。Transformer的解决方法是加位置编码。原论文用的是正弦余弦函数其实到了今天直接用可学习的位置嵌入也很常见。我个人的经验是数据量大的时候两者差别不大但可学习位置嵌入在微调阶段稍微灵活一些。位置编码的加法操作是这样的def add_positional_encoding(x, max_len512): # x: [batch, seq_len, d_model] batch, seq_len, d_model x.shape pe torch.zeros(seq_len, d_model) position torch.arange(seq_len).unsqueeze(1).float() div_term torch.exp(torch.arange(0, d_model, 2).float() * (-torch.log(torch.tensor(10000.0)) / d_model)) pe[:, 0::2] torch.sin(position * div_term) pe[:, 1::2] torch.cos(position * div_term) return x pe.unsqueeze(0).to(x.device)你要是第一次见到这个公式不用慌它本质上就是在每个位置上生成一组低频到高频的波形不同位置的编码模式天然不同且唯一。3.4 残差连接与层归一化的作用Transformer里每个子层后面都跟着残差连接和层归一化这两兄弟是训练稳定的关键。残差连接保证梯度至少有一条直通路径避免深层网络退化层归一化则是把特征的均值和方差拉回正常范围大幅降低对学习率的敏感度。我第一次不加残差连接的时候训练loss在0.5附近就死活降不下去了。加上残差同样的数据轻轻松松降到0.1以下。这种差异在十层以上的模型里尤其明显。伪代码习惯上是这么组织的class TransformerBlock(nn.Module): def __init__(self, d_model, n_heads, d_ff, dropout0.1): super().__init__() self.attn nn.MultiheadAttention(d_model, n_heads, dropoutdropout, batch_firstTrue) self.ffn nn.Sequential( nn.Linear(d_model, d_ff), nn.GELU(), nn.Dropout(dropout), nn.Linear(d_ff, d_model), ) self.norm1 nn.LayerNorm(d_model) self.norm2 nn.LayerNorm(d_model) self.dropout nn.Dropout(dropout) def forward(self, x, maskNone): # 子层一多头注意力 残差 归一化 attn_out, _ self.attn(x, x, x, attn_maskmask) x self.norm1(x self.dropout(attn_out)) # 子层二前馈网络 残差 归一化 ffn_out self.ffn(x) x self.norm2(x self.dropout(ffn_out)) return x这个结构看着简单但真的把“并行计算”和“全局交互”两个刚需同时满足了这也是它取代RNN成为主流的根本原因。4. 从零手写一个轻量级Transformer编码器理论说多了容易飘咱们直接上手写一个能跑的编码器。这个代码我在模拟项目X里用过拿来做短文本分类效果和训练速度都令人满意。整个模型去掉注释不到80行。4.1 搭建嵌入层与编码器主体首先要处理的是词嵌入。用一个可学习的Embedding层把token id映射成稠密向量然后加上位置编码。如果你用的框架比较新可以直接用nn.Embedding加nn.Parameter组合。然后按上一节的TransformerBlock堆叠。编码器里有两个微妙的设计点一是注意力掩码编码器不需要掩码因为每个位置都可以看到整句二是前馈网络中间层维度通常设成4 * d_model这个比例是从原论文沿用下来的工程实践里也基本不踩坑。完整模型代码如下class TinyTransformerEncoder(nn.Module): def __init__(self, vocab_size, d_model64, n_heads4, d_ff256, num_layers3, max_len128, dropout0.1, num_classes2): super().__init__() self.token_emb nn.Embedding(vocab_size, d_model) self.pos_emb nn.Parameter(torch.zeros(1, max_len, d_model)) self.blocks nn.ModuleList([ TransformerBlock(d_model, n_heads, d_ff, dropout) for _ in range(num_layers) ]) self.norm nn.LayerNorm(d_model) self.head nn.Linear(d_model, num_classes) self.dropout nn.Dropout(dropout) self.max_len max_len def forward(self, input_ids): seq_len input_ids.size(1) x self.token_emb(input_ids) self.pos_emb[:, :seq_len, :] x self.dropout(x) for block in self.blocks: x block(x) x self.norm(x) # 取第一个token作为分类表示类似BERT的CLS策略 pooled x[:, 0, :] logits self.head(pooled) return logits4.2 参数选择背后的计算逻辑我挑这几个参数都是有依据的不是随手填的。d_model64意味着每个词的语义向量是64维对中小规模任务来说容量够用在GPU上训练也很快。n_heads4意味着每个头分管16维注意力的多样性有了计算开销也不大。num_layers3对于短文本分类来说已经能捕捉足够复杂的交互模式你堆到8层不会有本质提升反而增加过拟合风险。有一个容易被忽略的细节是pos_emb初始化为零向量而且我是把它定义为可学习参数。相比原版的三角函数编码可学习位置嵌入在这个小模型上收敛更快。初始化为零是为了让模型一开始不受到位置信息的干扰逐渐自己学出合适的位置模式。4.3 训练循环和损失计算训练循环跟普通分类网络没有本质差别。我用AdamW优化器学习率设成3e-4warmup步数设成总步数的10%然后余弦退火到1e-5。这里有个经验Transformer对学习率非常敏感1e-3以上的学习率几乎铁定发散低于1e-5又学得太慢。一个完整训练片段长这样model TinyTransformerEncoder(vocab_size10000) optimizer torch.optim.AdamW(model.parameters(), lr3e-4, weight_decay0.01) criterion nn.CrossEntropyLoss() for epoch in range(30): model.train() total_loss 0 for batch_inputs, batch_labels in train_loader: optimizer.zero_grad() logits model(batch_inputs) loss criterion(logits, batch_labels) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() total_loss loss.item() print(fepoch {epoch}, loss: {total_loss / len(train_loader):.4f})clip_grad_norm_这行是我强烈建议加上的尤其是在你第一次训练Transformer的时候。虽然比RNN时代的梯度爆炸问题温和不少但偶尔还是会出现异常batch导致梯度飙高的情况。限幅到1.0基本不会影响正常收敛却能阻止训练发散。5. 训练过程中踩过的坑和排查思路这个部分我觉得比前面的理论更值钱。写模型一时爽训练调参火葬场。我把自己反复踩过的几个坑整理出来按出现频率排序。5.1 损失一直在高位震荡降不下去最常见的原因是学习率偏高。Transformer的优化曲面在某些方向特别陡峭学习率稍微一高loss就像心电图一样上蹿下跳。我建议先降到1e-4如果还震荡再往1e-5去试。还有一个隐蔽原因是数据没做mask。说得更具体一点就是padding token也被模型正常参与了注意力计算。你在一批长度不一的句子里统一pad到max_lenpadding位全部是0模型会去注意这些毫无意义的“空词”自然会把loss顶上去。解决方法是构造一个attention mask让padding位变成负无穷。5.2 训练集上损失很低验证集上一塌糊涂这个就是过拟合。Transformer参数量大在小数据集上特别容易直接背答案。我当时的模拟项目X只有几万条样本3层Transformer就明显出现过拟合。应对手段按优先级第一增大dropout到0.2甚至0.3第二加weight_decay从0.01调到0.05第三早停验证loss连续三个epoch不降就停第四最直接的上数据增强。文本任务里可以用回译、随机替换同义词、对token做随机mask等方式扩充数据。5.3 显存爆了怎么办这是我被问得最多的问题之一。如果你的显存在16G以下一个batch可能放不下几十条长文本。优先检查max_len很多情况下128足够你非要设成512就是自找麻烦。其次是batch size减半配合梯度累积来补偿accumulation_steps 4 for step, (batch_inputs, batch_labels) in enumerate(train_loader): loss criterion(model(batch_inputs), batch_labels) loss loss / accumulation_steps loss.backward() if (step 1) % accumulation_steps 0: optimizer.step() optimizer.zero_grad()当然也可以启用混合精度训练半精度下显存占用直接减半我这边的经验是损失函数和梯度计算几乎不受影响。6. 从编码器到完整的序列到序列模型如果你做的任务不只是分类而是翻译、摘要这种输入输出都是序列的任务那就得把Transformer补全成编码器-解码器结构。6.1 解码器和编码器的核心区别解码器比编码器多了两个机制一个是交叉注意力也就是解码器每一层都要去看编码器输出的整体信息另一个是因果掩码也就是解码器在生成第i个词时绝对不能看到第i1个词否则就是作弊。因果掩码的实现方式是在注意力分数矩阵上把上三角位置全部置为负无穷def create_causal_mask(seq_len): mask torch.tril(torch.ones(seq_len, seq_len)).bool() return mask # True保留False掩码这个掩码配合之前的masked_fill就能保证自回归式的逐词生成。6.2 推理阶段的生成策略训练完解码器之后生成阶段是逐词往外套的。你先往解码器输入一个开始符号拿到下一个词的概率分布然后选择词加回输入序列再继续预测。这里面有几个常用的策略贪心搜索每次直接选概率最大的词。速度快但容易陷入重复循环。束搜索维护K条候选路径最后选择整体概率最高的。质量比贪心好但计算量大。采样按概率分布随机抽配合温度参数调节随机性。温度接近0时退化成贪心温度大于1时更发散。我实际跑机器翻译Demo的时候贪心的结果经常出现重复短语换成beam size为4之后明显改善。6.3 尺度问题模型越大未必越好很多人一上来就想用几百亿参数的大模型但在个人项目或者小团队场景里这是不现实的。我始终建议第一步用小模型跑通全流程确认数据质量没问题再逐步扩大规模。扩大的时候优先加层数而不是加维度因为层数增加带来的表达能力提升更明显而且显存占用增长速度相对温和。我做过一个对比实验3层64维涨到6层64维任务准确率提升了约4个百分点但6层64维涨到6层128维只提升了约1.5个百分点显存却翻了接近两倍。7. 这篇笔记的工程落地建议前面理论和代码都有了最后再分享几个工程层面的建议这些是我在实际做项目时沉淀下来的。7.1 数据质量和预处理优先级最高模型结构再先进也拯救不了垃圾数据。我在一个文本分类项目里一开始用正则表达式简单清洗效果平平。后来花了整整一天做规则清洗、去掉乱码、合并重复样本同样的模型直接涨了好几个点。数据质量带来的提升往往比调参和换模型更大。具体实操上有几个点值得注意分词器的词表大小要跟语料规模匹配训练语料只有几万条的情况下词表10万就是浪费要做严格的训练集和验证集划分避免时间序列泄漏中文任务还要注意分词粒度有时候按字切比按词切效果更好。7.2 不要迷信复杂的训练技巧网上经常看到各种花式训练技巧什么Layer-wise学习率衰减、什么梯度裁剪的变体、什么EMA权重滑动平均。这些东西在大模型训练里确实有用但在小项目里未必能带来明显收益。我自己的原则是先把基线跑通再加技巧。每一轮只改一个变量精确记录对比结果。你要是同时改了学习率、dropout、优化器、数据增强最后模型变好了你根本不知道是哪个环节起了作用复现也说不清楚。7.3 环境配置与可复现性最后提一下环境问题。深度学习框架的版本差异有时候能直接改变模型行为这真不是玄学。我在一个项目里同样的代码在框架A的2.0版本跑出91%的准确率在1.8版本只有88%排查了很久才发现是默认初始化方式变了。所以强烈建议你把环境依赖固定住。用requirements.txt或者容器镜像把你用的框架版本、CUDA版本、Python版本全部锁定。作为个人笔记我也习惯把每次跑实验的随机种子固定下来保证结果可复现def set_seed(seed): import random import numpy as np import torch random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed)模型训练本身就是随机性很大的过程不固定种子你连自己复现自己的结果都困难。最后再补充一点关于笔记习惯的体会。我写这些笔记的时候习惯把每次实验的关键参数、loss曲线、踩坑过程都记录在案哪怕只是一个简陋的表格也比没有强。深度学习的学习曲线很陡但不代表它没有规律可循。那些踩过的坑、试过的错、调过的参数在当时觉得是浪费时间后来回头看恰恰是最值钱的部分。这套笔记目前只讲了编码器这一支下一篇我会把注意力可视化和解码器训练展开细说也会把一些实际项目里遇到的具体问题拿来做案例拆解。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号