想象一个深秋的夜晚,一群朋友围坐在篝火旁玩「故事接龙」。第一个人开口:“昨天我走进一家面馆。”第二个人看看炉火,接了一句:“老板娘递来一碗热腾腾的牛肉面。”第三个人顺着往下讲……没有人能提前知道整个故事,但所有人都遵守同一条铁律:下一句话必须从已经讲出的内容里长出来。这正是 LSTM 做序列生成的样子——它不是一个劲地“背课文”,而是在一次一次的接龙中,把一段从未存在过的文本、旋律或代码,从记忆里“生长”出来。这篇文章将带你拆解三件事:LSTM 如何记住前文、训练时如何被“老师”带着走、又如何通过“温度”这个旋钮,在守规矩与有创意之间找到平衡。
掌握序列生成,等于解锁了文本续写、对话回复、代码补全、AI 作曲等一大批生成式应用。这是每个想做 NLP 或生成式 AI 的工程师必须掌握的看家本领。
可视化演示
动态演示LSTM序列生成的核心机制,通过采集/分析/决策/反馈四种模式展示关键概念与交互流程。
本节配有交互动效(进入视口后自动加载,约 3KB)。
LSTM序列生成:自回归序 自回归序列生成原理 Teacher Forcing 核心机制可视化 LSTM序列生成整体架构与关键组件
序列生成概述
篝火边的故事接龙里,每个人都没有剧本,手里只有“已经讲出来的故事”。轮到谁,谁就必须接着上文往下编一句。把这个场景搬进计算机,就成了 序列生成(Sequence Generation):模型根据已知的上下文,逐个预测下一个元素,再把这个元素拼回序列,作为新的上下文。文本是一串字符,音乐是一串音符,代码是一串 token——它们全都是“接龙”的对象。
传统的分类任务回答“这是什么”,回归任务回答“值是多少”,而序列生成回答的是“下一个该是什么”。要做对话系统,就要学会接话;要做代码补全,就要学会替程序员把后半句敲完。所有这类任务,都可以用一把统一的“数学钥匙”来打开:把整段序列出现的概率,拆成一个个条件概率的乘积。
回到篝火边:没有人能预测到故事的结局,但每个人都能做好眼前这一步——“看着已讲出的故事,接好下一句”。序列生成的第一性原理,就是这种“拆成一步接一步”的耐心。
- 序列元素$x_1, x_2, \ldots, x_T$ 表示整段序列的 T 个元素,可能是字符、单词或音符。
- 联合概率$P(x_1, x_2, \ldots, x_T)$ 表示“整段序列同时出现”的概率,生成任务的目标就是让这个值尽量大。
- 连乘符号$\prod_{t=1}^{T}$ 表示把每个位置的预测概率相乘,因为每个元素都是独立一步接龙的结果。
- 条件概率$P(x_t \mid x_1, \ldots, x_{t-1})$ 表示在已知前 t-1 个元素的条件下,第 t 个元素出现的概率。
自回归机制
故事接龙最核心的规则是“只许接,不许跳”。你说出的每句话,都必须建立在已经讲出的内容上;AI 生成时也一样——它把自己上一步的输出,当作下一步的输入。这种“用自己的过去预测未来”的结构,在机器学习里叫 自回归(Autoregressive)。注意这里的“回归”不是线性回归,而是“回头去看”的意思。
自回归的流程像一台永不停歇的织布机:LSTM 把当前已生成的前缀压缩成一个状态向量,然后从中“纺出”下一个元素的概率分布;采样得到一个新元素,把新元素接回序列末端,再送进 LSTM,开始下一轮。每一步都只往前挪一个位置,永远不能回头,也永远不能跳过。
由于每一步都要等上一步的结果,自回归生成天然无法并行——这也是为什么生成一大段文本总比训练慢得多。但正是这种“老老实实接龙”的结构,保证了语句的连贯性。篝火旁的故事越讲越完整,靠的不是某人剧透结局,而是一句接一句的耐心。
- 状态更新$h_t = f_{\theta}(h_{t-1}, x_{t-1})$ 表示上一步的隐藏状态 $h_{t-1}$ 与上一个元素 $x_{t-1}$ 融合,得到当前步的新状态。
- 函数 f$f_{\theta}$ 是 LSTM 内部的更新函数,参数为 $\theta$,它负责决定“新状态该怎么变”。
- 概率输出$\mathrm{Softmax}(W_o h_t)$ 把状态向量 $h_t$ 通过线性映射 $W_o$ 投影成每个候选元素的得分,再转成概率。
- 采样符号$x_t \sim$ 表示从概率分布中抽取一个具体元素,作为这一步的生成结果。
LSTM记忆状态
故事接龙玩到深夜,前面讲过什么很容易忘。于是大家会推选一个记性最好的人当“书记”,把关键情节记在本子上。LSTM 里也有这样一本“本子”——细胞状态(Cell State) $c_t$。普通 RNN 只有一条隐藏状态线,讲着讲着就把开头忘光了;LSTM 额外拉了一条细胞记忆线,像一根绳子横穿整段故事,把重要的情节一路带到最后。
这根绳子上挂了三扇门。遗忘门决定把旧记忆的哪几页撕掉;输入门决定把当前新信息写进哪几行;输出门决定把本子里多少内容“念出来”,作为给下一步看的隐藏状态。三道门都是神经网络自己学出来的,它们不看别的,只看“当前输入”和“上一步的记忆”,然后算出 0 到 1 之间的开关力度。
有了这本记忆笔记本,LSTM 在生成长文本时,既能记得“三句话前是晴天”,又不会被无关的细节打扰。篝火故事讲到最后,书记翻开本子还能准确说出第一句是谁说的——这就是 长程依赖(Long-term Dependency) 的由来。
- 旧记忆$c_{t-1}$ 是上一步的细胞记忆,像本子里已经写好的旧故事。
- 遗忘门$f_t \odot c_{t-1}$ 表示按元素相乘($\odot$),遗忘门 $f_t$ 决定旧记忆还剩多少。
- 新候选$\tilde{c}_t$ 是当前步根据输入和上一步状态提出的“候选新情节”。
- 输入门$i_t \odot \tilde{c}_t$ 表示输入门决定新情节写进多少。
- 记忆更新$c_t = f_t \odot c_{t-1} + i_t \odot \tilde{c}_t$ 把“留下的旧记忆”和“写入的新内容”相加,得到新的笔记本。
- 输出隐藏状态$h_t = o_t \odot \tanh(c_t)$ 把记忆缩放到 -1 到 1 区间,再由输出门决定念出多少,作为下一步的对外可见状态。
教师强制(Teacher Forcing)
新手玩接龙,最怕说错一句,后面所有人顺着错句越编越离谱。老手带新手时会在一旁及时提醒:“不对,这句应该接那个。”训练 LSTM 时也有一位这样的老师——真实数据。如果训练时把模型上一轮自己生成的(很可能是错的)元素当作下一步输入,错误就会滚雪球,模型永远学不会正确的接法。
于是研究者发明了 Teacher Forcing(教师强制):训练时,无论模型刚才输出什么,都强制把真实序列的元素作为下一步输入。模型的任务只有一个——专心学“看到真实前文时,该怎么预测下一个”。好处是输入永远是通顺的正常句子,训练稳定、还能并行展开;坏处是“比赛”(推理)时老师不能上场,模型只能吃自己生成的内容,训练与推理的输入分布不一致,这被称为 曝光偏差(Exposure Bias)。缓解办法是 计划采样(Scheduled Sampling):训练初期多用真实输入,后期逐步换成模型自己的输出,让学生慢慢学会独立行走。
教练带路时学员走得稳、学得快,但真正的考试总是独自上场。读懂 Teacher Forcing,你就明白为什么训练好的模型一到生成时偶尔会“放飞自我”——那不是模型笨,而是它第一次离开老师自己走路。
- 训练目标$\mathcal{L}(\theta)$ 表示模型的损失函数,数值越小说明模型对真实序列的预测越准。
- 对真实元素的求和$\sum_{t=1}^{T}$ 把每个时间步的“打分之差”累加起来,覆盖整段训练序列。
- 对数概率$\log P_{\theta}(x_t \mid x_1, \ldots, x_{t-1})$ 是模型给“真实下一个元素”分配的概率取对数;概率越接近 1,对数越接近 0。
- 负号$-$ 把“最大化对数概率”变成“最小化负对数”,方便用梯度下降统一优化。
采样温度控制
同样是讲接龙,不同人风格差别很大:有人照本宣科,每句话都选最稳妥的说法;有人天马行空,冷不防编出外星人登场。LSTM 生成时也有一颗调节“胆量”的旋钮——温度(Temperature) $T$。模型最后会输出一组得分(叫 logits,可以理解为每个候选元素的“底气值”),softmax 负责把得分变成概率。如果先给所有得分除以温度 T,再进 softmax,概率分布的形状就会改变。
温度调小(比如 0.5),概率分布变得尖锐,高得分元素几乎垄断概率,生成结果保守稳定、逻辑通顺,但容易一句话绕来绕去地重复;温度调大(比如 1.5),分布变得平坦,低得分元素也有机会被选中,内容多样更有惊喜,但也可能语无伦次。温度等于 1 时保持原样。实践中还会搭配 Top-k 采样:先把概率最低的一批候选直接“禁赛”,再按温度调整后的分布采样,兼顾多样与连贯。
火候是烹饪的灵魂。温度太低,故事干巴巴像白开水;温度太高,故事糊成一团听不懂。聪明的工程师会像调柴火一样调温度:写代码补全时用小温度求稳,做创意文案时用大温度求新。
- 原始得分$z_i$ 是模型给第 i 个候选元素打的原始分,越大代表越“想选它”。
- 温度除法$z_i / T$ 用温度缩放得分。$T<1$ 放大得分差距,$T>1$ 缩小差距。
- 指数变换$\exp(z_i / T)$ 把缩放后的得分转成正数,同时进一步拉开倍数关系。
- 归一化$\sum_{j=1}^{V} \exp(z_j / T)$ 对词表里全部 V 个候选求和,作为分母。
- 最终概率$P_i$ 是归一化后第 i 个候选的采样概率,所有 $P_i$ 加起来等于 1。
文本生成实现
把故事接龙真正变成程序,最简单实用的起点是“从字开始”。字符级模型(Character-level Model) 把训练语料切开成一个个字符,建立一张字符表,然后用前 N 个字符预测第 N+1 个字符。它不依赖分词工具,字符表也小,还能自动学到“偏旁组字、字组词”的统计规律——像小学生用生字本学说话一样。
实现时通常三步走:先把语料转成索引序列并切成长度固定的片段,喂给 LSTM 训练(最后一个字符是老师给出的标准答案);训练完成后,从任意起始字符出发,每步把刚生成的字符拼回输入,形成自回归循环。生成时的关键代码,是把隐藏状态投影到字符表维度,除以温度后用多项式分布采样,而不是永远选概率最大的那个。
字符级生成虽然不懂词义,但靠着“接龙”的统计规律,也能写出有模有样的句子。这就是序列生成最迷人的地方:模型没有剧本,没有大纲,只有一个接着一个字符的坚持,最终却能让一段全新的故事从篝火旁生长出来。
- 输出层投影$W_o h_{t+1} + b_o$ 把 LSTM 更新后的隐藏状态 $h_{t+1}$ 映射成每个字符的原始得分。
- 归一化概率$\mathrm{Softmax}$ 把得分转成和为 1 的概率分布 $p_{t+1}$。
- 类别采样$x_{t+1} \sim \mathrm{Categorical}(p_{t+1})$ 表示按这个概率分布抽一次签,抽中的字符作为生成结果。
- 自回归回填抽出的 $x_{t+1}$ 会拼回序列,成为下一步的输入,循环直到满足停止条件。
AI 应用场景
智能输入法与搜索联想
阶段:模型训练(Teacher Forcing)与在线推理(自回归采样)衔接
解决的问题:传统输入法只按字频和固定词典排序,不会根据语境判断你下一个想打什么字;LSTM 序列生成模型则把“你已输入的前文”当上下文,逐字预测最大概率的后续字符,让候选词贴合当前语境。
具体案例:当用户输入“今天的天气”,模型给“真好”的概率远高于“真好喝”,因为前文语境决定了“天气”后面最自然的接法是“真好”。温度调低时联想更保守,调高时能给出更有发散性的候选。
AI 作曲与旋律生成
阶段:数据准备 → LSTM 训练 → 温度调参生成
解决的问题:音乐本质上是音符与节拍的序列,人工作曲门槛高、灵感不稳定;把一段 MIDI 旋律转成序列后,LSTM 可以学习“这个音后面通常接哪个音”的接龙规律,源源不断地生成新旋律。
具体案例:用巴赫的众赞歌片段训练一个小型 LSTM,温度设为 0.6 时生成风格统一、适合伴奏的旋律;温度调到 1.2 时模型开始“发挥”,产出跳跃性更强、更有实验色彩的变奏,供作曲家筛选改编。
代码补全工具
阶段:字符级/词级自回归生成 + Top-k 截断
解决的问题:程序员手写重复性代码耗时且易错;代码补全工具把“光标前的代码”当作上下文,通过自回归生成后续 token,让 IDE 像接龙一样把函数体、参数、注释补全出来。
具体案例:当开发者输入 def add(a, b): 并回车缩进后,模型根据前文函数签名预测出 return a + b。这里温度通常设得很低(接近 0.3),保证补全结果准确稳定,偶尔用稍高温度生成样板代码的多种写法。
Python 演示
import torchimport torch.nn as nn class CharLSTM(nn.Module): def __init__(self, vocab_size, embed_size=32, hidden_size=64): super().__init__() self.embedding = nn.Embedding(vocab_size, embed_size) self.lstm = nn.LSTM(embed_size, hidden_size, batch_first=True) self.fc = nn.Linear(hidden_size, vocab_size) def forward(self, x, state=None): emb = self.embedding(x) out, state = self.lstm(emb, state) logits = self.fc(out) return logits, state def build_maps(corpus): chars = sorted(set(corpus)) stoi = {c: i for i, c in enumerate(chars)} itos = {i: c for i, c in enumerate(chars)} return stoi, itos def train_model(corpus, epochs=80, lr=1e-2): stoi, itos = build_maps(corpus) ids = [stoi[c] for c in corpus] xs = torch.tensor(ids[:-1]).unsqueeze(0) ys = torch.tensor(ids[1:]).unsqueeze(0) model = CharLSTM(len(itos)) criterion = nn.CrossEntropyLoss() opt = torch.optim.Adam(model.parameters(), lr=lr) torch.manual_seed(0) for _ in range(epochs): logits, _ = model(xs) loss = criterion(logits.reshape(-1, len(itos)), ys.reshape(-1)) opt.zero_grad() loss.backward() opt.step() return model, stoi, itos def generate(model, stoi, itos, start, length=12, temperature=0.8): model.eval() chars = list(start) state = None with torch.no_grad(): for _ in range(length): x = torch.tensor([[stoi[chars[-1]]]]) logits, state = model(x, state) logits = logits[0, -1] / temperature probs = torch.softmax(logits, dim=-1) idx = torch.multinomial(probs, 1).item() chars.append(itos[idx]) return ''.join(chars) if __name__ == '__main__': corpus = '空山新雨后,天气晚来秋。明月松间照,清泉石上流。' model, stoi, itos = train_model(corpus, epochs=80) print(generate(model, stoi, itos, '空', length=14))
out, state = self.lstm(emb, state)LSTM 沿着序列逐步更新记忆并输出每个时刻的状态,相当于故事接龙的“大脑”。logits = self.fc(out)把隐藏状态映射成每个字符的得分,相当于候选下一个字的“底气”。loss = criterion(logits.reshape(-1, len(itos)), ys.reshape(-1))拿真实下一个字符当老师,计算交叉熵,这就是 Teacher Forcing。logits = logits[0, -1] / temperature对最后一个位置的得分做温度缩放,控制“守规矩”还是“放开想象力”。idx = torch.multinomial(probs, 1).item()按概率分布采样出下一个字符,而不是永远选得分最高的那个。chars.append(itos[idx])把新字符拼回序列,作为下一步输入,形成自回归闭环。
常见误区
误区我以为模型生成的结果只是把训练语料里出现过的句子剪碎再重新排列,它并没有真正学到什么东西。
正解模型学的是“在什么上下文下什么元素概率高”的统计规律。生成时它是逐字符评估条件概率后采样的,不是在检索原句;当训练语料足够丰富时,它完全可以产出语料里从未出现过的全新组合。
为何常见:小模型在小语料上训练时,生成的句子和训练文本高度相似,看起来像在“背课文”,让人误以为它只会拼接。
误区训练时用 Teacher Forcing 效果很好,那推理的时候也把真实的“标准前文”喂给模型,它应该生成得更准吧?
正解推理时根本没有真实前文可用——整段内容都要模型自己生成。如果模型依赖真值输入,一旦自己生成的内容偏离了训练分布,错误就会滚雪球式累积,这正是曝光偏差的由来。训练时喂真值,推理时喂自己,是刻意设计的“从带教到独立”的过程。
为何常见:把训练技巧直接套到推理阶段,忽略了“训练时有老师、推理时没老师”这个本质区别。
误区模型的概率分布有高有低,把温度调高、让低概率的词也有机会被选中,生成内容更丰富,所以质量应该更好。
正解温度只调节多样性,不提升合理性。温度过高时分布接近均匀,模型像醉酒接龙,句法都会崩坏;温度过低又倾向重复最稳妥的套路。所谓质量,是在你的具体任务里找到“连贯”与“惊喜”的平衡点。
为何常见:把“多样”直接等同于“好”,忽略了语言生成还需要连贯性、语义一致性和语法正确性。
误区中文分词本身就麻烦,但至少得按词来建模吧?一个字一个字地生成,怎么可能学得会表达?
正解字符级 LSTM 是入门文本生成最常用的方案。字符表小、无分词依赖,模型还能自动捕获“字组成词、词组成句”的组合规律;代价只是序列更长、训练成本略高。实践中按任务需求选择字符级、词级或子词级(BPE)即可。
为何常见:受传统 NLP 以词为基本单位的习惯影响,忽略了字符级模型在生成任务中的可行性和优势。
面试题
Q1. 序列生成任务的核心是什么?
- A.输出单个分类标签
- B.逐元素预测并接续序列← 正确答案
- C.对序列做无监督聚类
- D.直接复制一段训练文本
答案:B。序列生成的核心是“根据前文逐元素预测下一个元素”,像故事接龙一样一步步把序列续写下去。分类输出单个标签、聚类是无监督任务、直接复制文本都不属于生成。答案选 B。
Q2. 自回归模型生成时的特点是什么?
- A.每个新元素依赖前文← 正确答案
- B.所有元素同时并行生成
- C.新元素完全随机产生
- D.新元素只依赖第一个元素
答案:A。自回归的含义就是“用自己的过去预测未来”,每个新元素都以上一步及更早的输出为条件,因此无法并行生成、也不是随机或只依赖首元素。答案选 A。
Q3. Teacher Forcing 训练时,模型每一步的输入是什么?
- A.用真实元素作为下一步输入← 正确答案
- B.用模型输出作为下一步输入
- C.用随机噪声作为下一步输入
- D.停止梯度回传只保留输出
答案:A。Teacher Forcing 的核心是训练时强制把真实序列的元素作为输入,让模型专心学习“看真前文预测下一个”,避免错误滚雪球。用模型输出是推理模式,随机噪声和停止梯度都不是 Teacher Forcing 的含义。答案选 A。
Q4. 关于曝光偏差,下列说法正确的是?
- A.训练与推理输入分布不一致← 正确答案
- B.推理时错误会逐渐累积← 正确答案
- C.温度越高偏差越不明显
- D.计划采样可以部分缓解← 正确答案
答案:ABD。训练时输入是真实前文,推理时只能输入模型自己的输出,两者分布不一致(A 对),一旦生成偏离就会越错越远(B 对)。计划采样通过在训练中逐步混入模型输出缓解该问题(D 对)。温度只影响采样随机性,与曝光偏差无关(C 错)。
Q5. 温度 T 趋近 0 时,softmax(z/T) 的效果接近什么?
- A.均匀随机分布采样
- B.贪心取最大概率项← 正确答案
- C.完全随机的噪声输出
- D.概率分布直接失效
答案:B。T 趋近 0 时,除以一个极小数会把得分差距放大到无穷,最大得分项的概率趋近 1,相当于贪心解码,总是选最稳的那个元素。均匀随机和噪声是 T 很大时的效果,分布不会失效。答案选 B。
Q6. LSTM 遗忘门的主要作用是什么?
- A.决定丢弃多少旧记忆← 正确答案
- B.决定输出层激活函数
- C.决定训练学习率大小
- D.决定隐藏层维度大小
答案:A。遗忘门输出 0 到 1 之间的值,与旧细胞记忆按元素相乘,决定旧记忆保留多少、丢弃多少,相当于决定故事本子里哪些旧页要撕掉。激活函数、学习率、维度都是超参或结构设计,不由遗忘门决定。答案选 A。
Q7. 以下哪些手段有助于提升生成质量?
- A.合理调节采样温度← 正确答案
- B.使用 Top-k 过滤低概率项← 正确答案
- C.一直使用模型输出做输入训练
- D.用计划采样缓解曝光偏差← 正确答案
答案:ABD。调节温度可以平衡多样性与连贯性(A 对);Top-k 先砍掉低概率候选再采样,能避免生成突兀的冷门词(B 对);计划采样缓解曝光偏差,让训练与推理分布更接近(D 对)。一直用模型输出训练会让错误滚雪球,训练不稳定(C 错)。
Q8. 为什么不总是用 argmax 贪心解码做生成?
- A.贪心解码计算开销过高
- B.贪心易重复且缺乏多样性← 正确答案
- C.贪心无法在 GPU 上执行
- D.贪心必然导致语法错误
答案:B。贪心每一步都选概率最大的元素,看似最“正确”,但容易陷入重复循环(比如一直说同一个词),且没有多样性,生成内容单调。它在 GPU 上完全可以执行,开销也不高,更不会必然造成语法错误。答案选 B。
Q9. 自回归生成通常何时停止?
- A.达到设定的最大长度
- B.输出概率低于某个阈值
- C.模型生成结束符号时
- D.以上几种方式都常见← 正确答案
答案:D。不同场景下停止策略不同:有的设定最大长度,有的输出概率过低就停,有的模型专门学习生成结束符号(如 EOS)来宣布故事讲完。三种方式在工程实践中都很常见,所以选 D。
Q10. 关于计划采样(Scheduled Sampling),下列说法正确的是?
- A.训练中用概率决定真实或模型输入← 正确答案
- B.训练后期更多使用模型自身输出← 正确答案
- C.训练初期主要使用真实输入← 正确答案
- D.直接将损失函数改成均方误差
答案:ABC。计划采样维护一个概率 p,训练早期 p 接近 1(多数用真实输入,C 对),后期 p 逐渐减小(更多用模型自身输出,B 对),每一步以概率决定当前输入是真实还是模型输出(A 对)。它的目标是弥合训练与推理的输入分布差异,并不会把交叉熵损失改成均方误差(D 错)。