用LSTM让《鹿鼎记》学会写小说:字符级文本生成实战

📅 发布时间:2026/9/24 1:06:24
用LSTM让《鹿鼎记》学会写小说:字符级文本生成实战
简介基于金庸《鹿鼎记》全文数据的LSTM文本生成项目提供了一套完整可运行的代码与说明适合自然语言处理入门、毕业设计或课程设计参考。项目覆盖数据爬取到模型训练的全流程GetLu.py负责抓取小说章节并保存为txtWord_LSTM.py实现数据预处理、词典映射与LSTM模型训练训练时取前5万字符、以40个字符为一句构造输入序列另附README说明文档、清洗后的鹿鼎记文本数据及训练完成的hdf5权重文件。压缩包共5个文件包含2个Python脚本、1份Markdown说明、1个txt数据集和1个模型权重文件整体约18.78MB结构紧凑便于复现。目前已有192人浏览学习代码经测试运行成功并支持远程教学适合想了解字符级语言模型、序列生成及文本风格模仿的读者进阶提升。1. 让 LSTM 读《鹿鼎记》写小说这个标题到底值得不值得做先别急着把“基于鹿鼎记的数据集”和“LSTM”当成两个割裂的东西。这个标题本质上在描述一个最小闭环拿一部百万字级别的中文公版小说当语料用 Python 搭一个字符级的 LSTM 模型训练之后用温度采样让它“续写”出风格接近原文的段落。它不是什么巨头产品却是一个能在一台普通笔记本上跑完的完整项目而且正好把数据清洗、序列建模、训练调参、文本生成四条线全部串起来。这个项目能解决什么问题对想从分类任务往生成任务跳的人来说它是最好的过渡——LSTM 不需要大显卡不需要分布式几百万字符喂进去就能看见 loss 往下掉对想理解“序列预测”本质的人来说写小说和做时间序列预测在代码层面高度同构都是给定前面一堆值预测下一个值。生成的小说是一个能直观检查的“预测结果”比看股票曲线更有反馈感。适合读这篇东西的人是那些已经会用 Python、但还没完整跑通过一个文本生成模型正犹豫先拿 RNN 还是 Transformer 入手的开发者。我先给结论这个方向值得做但坑比想象中多下文每一章都是照着复现的顺序写的。2. 语料预处理把《鹿鼎记》切成 LSTM 能吃的定长序列2.1 拿到 txt 后先做三件事编码、去全角空格、压缩空行市面上能找到的《鹿鼎记》文本大多是 TXT 格式但格式五花八门。有的是每行一句的“网文排版”有的是整章一个超长段落还有的混入了大量全角空格和零星 OCR 错字。我的习惯是第一步不碰任何模型代码先把原始文档读进来看一眼字符分布再动手。import re from pathlib import Path raw Path(鹿鼎记.txt).read_text(encodingutf-8) print(原始字符数:, len(raw)) # 第1件事去掉全角空格和 \r统一成 \n 换行 text raw.replace(\u3000, ).replace(\r, ) # 第2件事把行内连续空白压成一个空格避免“韦 小 宝”这种残次 text re.sub(r[ \t], , text) # 第3件事把连续 3 个以上的换行压成 2 个保留段落边界 text re.sub(r\n{3,}, \n\n, text) Path(鹿鼎记_clean.txt).write_text(text, encodingutf-8) print(清洗后字符数:, len(text))上面这段的逻辑是\u3000是中文全角空格在旧排版文本里几乎必然出现不删掉会让词表无谓膨胀\r是 Windows 老文本的换行符残留混在\n里会让后面按行切分时出现空串把行内连续空格压缩是为了避免“金庸 著”这种排版残留影响字符统计。这里有个原则——清洗规则宁少勿多只处理确定有害的噪声别去做繁体转简体、也别用词典修正错字因为 LSTM 是字符级学习它自己能容忍一定噪声清洗过度反而会破坏原文的用字统计特征。我一般会在这步跑一个字符频次统计确认文本里没有大段看不懂的乱码区块。操作方法很简单collections.Counter(text)取出现次数最高的 20 个字符看一眼如果是“的、了、道、说、他、你”这类常见字就说明文本基本干净。这一步不写进脚本也行但强烈建议跑一次因为后续所有建模都建立在词表质量上而这一步最容易被跳过。2.2 按“字”建词表中文生成用小 vocab 比用 jieba 分词更稳文本生成项目第一道分岔路分词还是分字。常见的做法是分字不是分词。原因非常实际现在 NLP 里用的 jieba 分词面向的是理解任务分类、NER分词结果会让词表膨胀到几万甚至十几万而《鹿鼎记》全文就是一个作者的语言习惯集合常用汉字加标点撑死一万上下。字符级建模的词表通常只有 60008000这意味着最后全连接层的参数少一个数量级LSTM 学起来负担小得多。from collections import Counter char_counter Counter(text) # 过滤掉只出现 1 次的字符避免把噪声学进 embedding vocab [ch for ch, cnt in char_counter.items() if cnt 2] vocab [PAD, UNK, BOS, EOS] vocab char2idx {ch: i for i, ch in enumerate(vocab)} idx2char {i: ch for ch, i in char2idx.items()} print(词表大小:, len(vocab))这段代码的要点在于特殊符号的设计。PAD用于把 batch 内序列对齐到相同长度UNK兜底那些被过滤掉的生僻字BOS和EOS这次用不上但保留它们有两个好处一是训练时可以显式告诉模型“一句话从哪里开始、到哪里结束”二是以后想换 GPT 风格模型时词表不用重建。实际训练中过滤阈值cnt 2对一部小说是合适的如果语料更大可以调到 5但《鹿鼎记》这个量级没必要。这里还想纠正一个新手常踩的误区不要用 one-hot 向量直接喂 LSTM。词表 8000one-hot 就是 8000 维的稀疏向量而 embedding 层只需 128 维稠密向量就能表达字符间相似度训练速度和最终效果都明显更好。所以下面的模型结构里第一层一定是nn.Embedding。2.3 滑动窗口造样本seq_len、step 怎么配合才不会让验证集泄漏文本生成任务的训练样本形式是“给定前 n 个字预测第 n1 个字”。造样本的通用做法是滑动窗口用seq_len个字做输入后面错开一位的seq_len个字做标签窗口按固定步长滑动。这里有两个参数要一起调seq_len窗口长度和step滑动步长。seq_len 64 step 8 xs, ys [], [] for para in text.split(\n): para para.strip() if len(para) 2: continue ids [char2idx.get(c, char2idx[UNK]) for c in para] for i in range(0, len(ids) - seq_len, step): x ids[i:i seq_len] y ids[i 1:i seq_len 1] if len(x) seq_len and len(y) seq_len: xs.append(x) ys.append(y) print(样本总量:, len(xs))逻辑说明以段落为单位切分而不是把全文拼成一个超长序列是为了让每个样本都保持语句相对完整避免窗口跨过章节边界造成莫名其妙的拼接。x是输入y是输入整体右移一位的结果第 t 个位置的标签就是第 t1 个位置的字符这样每个样本内部天然构成 64 组 (输入序列, 下一个字符) 的监督对。参数说明seq_len64是一个性价比很高的默认值。太短比如 16模型只能学到词语搭配学不到句间逻辑太长比如 256会显著增加 LSTM 时间步数和显存占用而且《鹿鼎记》的句子平均长度也就 20 字上下64 已经覆盖了小半个段落。step8意味着相邻窗口重叠 56 个字这样一段 200 字的段落能产出约 24 个样本数据量放大好几倍又不至于像step1那样让相邻样本几乎完全相同导致训练集内部高度冗余。样本造好后要按顺序切分数据集这里有个隐蔽的坑不要用随机切分。小说文本前后文有关联性随机切分会让训练集和验证集里出现大量“神似”的片段验证集就失去了意义。常规做法是按顺序切前 80% 段落做训练、后 20% 做验证split int(len(xs) * 0.8) train_x, train_y xs[:split], ys[:split] val_x, val_y xs[split:], ys[split:] print(训练样本:, len(train_x), 验证样本:, len(val_x))3. 用 PyTorch 从零搭 LSTM 生成模型网络结构、损失函数与训练循环3.1 Embedding 双层 LSTM Linear这三层为什么够用文本生成模型在 PyTorch 里写起来特别短核心结构就三块Embedding 把字符 ID 变成向量LSTM 在序列上做时间步递推Linear 把隐状态映射回词表大小。我见过很多新手在这一步直接抄 Transformer其实对百万字级别的小说语料LSTM 是更务实的起点——参数少、收敛快、对乱序数据不敏感而且源码逻辑一目了然出问题容易定位。import torch import torch.nn as nn class CharLSTM(nn.Module): def __init__(self, vocab_size, embed_dim128, hidden_size256, num_layers2, dropout0.3): super().__init__() self.embed nn.Embedding(vocab_size, embed_dim, padding_idx0) self.lstm nn.LSTM(embed_dim, hidden_size, num_layers, batch_firstTrue, dropoutdropout) self.fc nn.Linear(hidden_size, vocab_size) self.dropout nn.Dropout(dropout) def forward(self, x, hiddenNone): # x: [batch, seq_len] emb self.dropout(self.embed(x)) # [batch, seq_len, embed_dim] out, hidden self.lstm(emb, hidden) # out: [batch, seq_len, hidden_size] logits self.fc(out) # [batch, seq_len, vocab_size] return logits, hidden这里给每个参数一个明确的名分。embed_dim128字符向量的维度128 对 8000 词表足够升到 256 收益很小但参数翻倍。hidden_size256LSTM 隐状态宽度这决定模型记忆能力256 是文本生成里最常用的甜点值再大就容易在小数据上过拟合。num_layers2两层 LSTM 可以捕捉到“字符→词语→句间”的部分层级关系三层以上在单机 CPU/入门 GPU 上收益迅速衰减。dropout0.3只加在层间和 embedding 输出上这是 PyTorch LSTM 自带dropout参数的职责范围注意单层 LSTM 传dropout不会生效这是源码实现决定的不用纠结。为什么要用 2 层而不是 1 层我自己的体验是1 层 LSTM 生成出来的句子语法正确但连续几句话之间几乎没有情节衔接2 层之后模型更容易记住“刚才在说哪个人”生成结果开始有短篇叙事的样子。这背后的直觉是第一层做局部语法建模第二层在更高抽象级别维护上下文状态两层各司其职。3.2 交叉熵和 reshape 对齐loss 计算里最容易翻车的维度问题训练 LSTM 生成模型损失函数用交叉熵没得选。但新手写 loss 这行时十有八九会碰到维度报错因为模型输出是四维视角下的三维张量[batch, seq_len, vocab_size]而标签是二维[batch, seq_len]。PyTorch 的CrossEntropyLoss期望输入是[N, C]形状所以必须先把序列维度和 batch 维度合并def compute_loss(logits, targets): # logits: [batch, seq_len, vocab_size] # targets: [batch, seq_len] V logits.size(-1) loss nn.CrossEntropyLoss()( logits.reshape(-1, V), # [batch*seq_len, vocab_size] targets.reshape(-1) # [batch*seq_len] ) return loss这段代码的逻辑是把每个时间步的预测都当成独立分类问题模型在batch*seq_len个位置上各自预测一个字符再和真实字符做交叉熵。这里有个值得说透的点虽然我们把每个位置当成独立样本计算 loss但 LSTM 的前向传播是串行的第 t 步的隐状态携带了前 t-1 步的信息所以误差反传时梯度依然能沿着时间步流动这正是“BPTT时间反向传播”的含义。有人会问为什么不把标签做 one-hot 和 logits 算 MSE因为交叉熵直接优化概率分布梯度更陡、收敛更快MSE 把分类问题当成回归问题会被高频字符“的、了”带偏生成的文本会更平更呆。这个区别在训练曲线上能直接看出来MSE 的 loss 值会以极小步长缓慢下降而交叉熵的下降肉眼可见。3.3 训练循环的四个参数lr、梯度裁剪、dropout、epochs训练循环本身不难真正决定成败的是几个细节参数。我直接给一个完整可跑的训练代码块然后逐个参数讲清楚为什么这么设。import torch.optim as optim from torch.utils.data import DataLoader, TensorDataset train_ds TensorDataset(torch.tensor(train_x), torch.tensor(train_y)) val_ds TensorDataset(torch.tensor(val_x), torch.tensor(val_y)) train_loader DataLoader(train_ds, batch_size64, shuffleTrue) val_loader DataLoader(val_ds, batch_size64, shuffleFalse) model CharLSTM(vocab_sizelen(vocab)) optimizer optim.Adam(model.parameters(), lr1e-3, weight_decay1e-4) scheduler optim.lr_scheduler.ReduceLROnPlateau(optimizer, modemin, patience2, factor0.5) best_val_loss float(inf) for epoch in range(30): model.train() total_loss, total_acc 0, 0 for x, y in train_loader: optimizer.zero_grad() logits, _ model(x) loss compute_loss(logits, y) loss.backward() nn.utils.clip_grad_norm_(model.parameters(), 5.0) # 梯度裁剪 optimizer.step() total_loss loss.item() * x.size(0) total_acc (logits.argmax(-1) y).float().mean().item() * x.size(0) model.eval() val_loss, val_acc 0, 0 with torch.no_grad(): for x, y in val_loader: logits, _ model(x) loss compute_loss(logits, y) val_loss loss.item() * x.size(0) val_acc (logits.argmax(-1) y).float().mean().item() * x.size(0) train_loss total_loss / len(train_ds) val_loss val_loss / len(val_ds) scheduler.step(val_loss) if val_loss best_val_loss: best_val_loss val_loss torch.save(model.state_dict(), lstm_鹿鼎记_best.pt) print(fepoch {epoch1:02d} | train_loss {train_loss:.3f} | fval_loss {val_loss:.3f} | best {best_val_loss:.3f})这里四个参数值得单独拿出来说。lr1e-3加ReduceLROnPlateauAdam 的默认学习率 1e-3 在这个任务上适用但训练后期 loss 会在一个平台期来回震荡直接用固定 lr 会卡住。所以我给 scheduler 设了patience2意思是验证集 loss 连续两轮不创新低就把学习率减半这个机制比手动调 lr 省心得多。clip_grad_norm_(..., 5.0)LSTM 训练中梯度范数偶尔会暴涨这是 RNN 家族的通病。裁剪到 5.0 的意思是如果梯度范数超过 5就整体缩放到 5。不设这个参数训练 20 轮左右可能出现某个 batch 直接把 loss 打回初始值的“返祖现象”前期全部白练。weight_decay1e-4L2 正则对 8000 词表的全连接层有稳定作用设太大比如 0.01会让生成结果趋同所有句子都往高频词上靠。batch_size64在 CPU 上跑这个值偏大但 GPU 上完全没问题。如果显存不够不要直接缩小 batch改用梯度累积每 4 个 batch 累加一次梯度再 step效果等价于 batch_size256还省显存。3.4 参数速查表自己跑一遍时会用到的默认值参数默认值说明调整方向seq_len64输入序列长度生成长段落时调大到 128显存吃紧时调小到 32step8滑动窗口步长数据量不足时调到 4步长为 1 会导致样本高度重合embed_dim128字符向量维度语料大、有 GPU 时可上调到 256hidden_size256LSTM 隐状态维度想增强记忆能力优先调这个其次才调层数num_layers2LSTM 层数1 层生成太散3 层过拟合风险大dropout0.3正则化强度过拟合时上调到 0.5lr1e-3初始学习率loss 震荡剧烈时降到 5e-4clip5.0梯度裁剪阈值训练不稳定时收紧到 1.0epochs30训练轮数看验证集 loss不涨就早点停看过这张表你大概会有个感觉LSTM 文本生成没有魔法参数九成参数在合理区间内都能收敛真正影响生成质量的只有两个——seq_len 决定模型能看多远temperature 决定生成有多“敢说”。后者在下一章展开。4. 验证与生成loss 曲线别只看训练集temperature 才是玄学4.1 early stoppingval loss 连续五轮不降就存 best 模型训练代码里我已经顺带写了 best 模型保存逻辑但这只是 half 的功夫。实践中更常见的问题是训练集 loss 一路降到 1.2 左右看起来很漂亮但验证集 loss 从第 15 轮开始反弹这就是教科书式的过拟合。文本生成任务的过拟合和分类任务不太一样——分类过拟合是准确率不再涨文本生成过拟合是模型开始逐字背诵训练语料。检测方法很简单每轮结束把当前模型拿去生成一段“韦小宝走进扬州城”如果第 10 轮生成的句子还是含糊的套话但第 18 轮生成的句子和你训练集里某段几乎一模一样恭喜模型开始背课文了。此时回退到 best model 就能得到泛化能力最强的那个版本。我的习惯是训练时每轮都记录val_loss若连续 5 轮没有刷新最低值就提前终止——比死等 30 轮省下三分之一时间而且生成质量更好。这里多说一句字符级模型的验证集 loss 数值本身没有绝对意义因为 vocab_size 有 8000即使模型达到 40% 的逐字准确率loss 也就在 2.0 上下浮动。所以判断模型好坏不能只看 loss要配合生成样本做人工评估这是文本生成和分类任务最大的不同——分类有明确的准确率指标生成没有。4.2 generation 函数temperature、top_k 和 multinomial 配合训练完的模型要变成“能写小说”的程序核心是采样策略。如果每步都取概率最大的字符argmax生成结果会陷入“韦小宝说道说道说道说道”的循环因为最高频的字符一旦被选中它的隐状态会不断强化自己。解决办法是从概率分布中随机采样再用 temperature 控制分布的尖锐程度。torch.no_grad() def generate(model, char2idx, idx2char, seed韦小宝走进扬州, length200, temperature0.8, top_k40, repetition_penalty1.2): model.eval() ids [char2idx.get(c, char2idx[UNK]) for c in seed] out list(seed) hidden None for _ in range(length): x torch.tensor([ids[-seq_len:]], dtypetorch.long) logits, hidden model(x, hidden) logits logits[0, -1] / temperature # top_k 截断只保留概率最高的 k 个候选 if top_k: vals, _ torch.topk(logits, top_k) logits[logits vals[-1]] -float(inf) # 重复惩罚已出现的字logits 整体除以惩罚系数 if repetition_penalty ! 1.0: for idx in set(ids[-100:]): logits[idx] / repetition_penalty probs torch.softmax(logits, dim-1) next_id torch.multinomial(probs, 1).item() out.append(idx2char[next_id]) ids.append(next_id) return .join(out)这段代码有三个关键决策点。第一logits[0, -1]取的是最后一个时间步的输出因为生成时前面的字符都是已知的只有最后一步需要预测。第二temperature的语义是值越小分布越尖锐生成的文本越保守、越接近训练集中的高频表达值越大分布越平文本越跳脱但超过 1.5 就开始胡言乱语。对《鹿鼎记》这个语料0.71.0 之间最合适——低于 0.5 会整段重复高于 1.2 会出现“韦小宝拔出一把剑剑是一把剑”这种语义断裂。第三top_k40是为了把那些概率极低的生僻字和标点排除在采样池外防止生成突然冒出“魑魅魍魉”级别的冷字打乱叙事。重复惩罚repetition_penalty的作用是对最近 100 步内出现过的字符把它的 logits 除以 1.2。这样模型不是完全禁止重复而是让重复的可能性降低。注意惩罚系数不要大于 2.0否则模型会刻意回避常用字生成结果变得拗口。4.3 把“说道说道说道”压下去的采样组合实际调参时你会碰到一个现象单独调 temperature 或单独调 top_k 都压不住重复。我建议把三个旋钮按下面的顺序配合而不是单靠某一个先用temperature0.8保证基本的语言连贯性再用top_k40砍掉长尾最后用repetition_penalty1.2处理顽固的重复片段。如果你跑出来的结果还是重复先检查训练是否充分。我之前在数据量只有几十万字符的语料上训练发现 temperature 怎么调都没用后来把样本生成时的step从 8 改成 4样本量翻倍重复问题自然缓解。这说明了文本生成领域的一个底层规律生成质量的上限由训练数据决定采样参数只是在概率分布里做取舍无法无中生有。5. 常见问题与避坑生成重复、乱码、OOM 的五个血泪经验5.1 现象输出全是“说道说道说道”这是 LSTM 文本生成最经典的现象几乎人人都会碰到一次。现象是生成的文本从某句话开始同一个词反复出现不换词也不换结构。原因分两层第一层是训练不充分模型没有学到足够丰富的词汇转移规律概率分布被高频词主导第二层是采样策略不对argmax 会让模型沿着概率最高的路径一路滑下去而这条路径往往就是语料里最常见的搭配。解决优先调采样策略。我一般会先把temperature设到 0.9再开top_k40如果还重复就逐步加大repetition_penalty到 1.5。训练侧也有一个补救把seq_len从 64 调大到 128让模型能看到更长的上下文有时短序列样本太多是重复的病根。5.2 现象生成结果是标点符号刷屏有时候生成的文本前面几句正常后面开始疯狂输出逗号、引号和句号。这不是模型坏了而是语料里标点的数量占比太高。《鹿鼎记》的对话极多对话必然伴随引号和逗号如果小说原文里还有大量“说道”这种结构模型很容易学到“引号后面大概率跟逗号”这种统计规律进而陷入标点循环。解决在字符统计阶段看一眼标点占比。我见过极端情况标点字符占了全文字符数的 20% 以上此时有两种处理路径一是把标点中占比过高的字符比如逗号按比例降采样比如每 5 个逗号保留 3 个二是在训练时把连续的标点压缩成一个比如把合并成。注意动作不能太大对话的情感全靠标点表达压缩到极致小说就没味道了。5.3 现象训练 loss 纹丝不动或者直接变 NaNloss 完全不动的原因最常见的是学习率太大导致梯度在 8000 类分类层上震荡模型始终在同一个局部区域徘徊。另一种情况是文本里有异常字符比如\x00或\ufffd替换符混进了词表导致 embedding 层对特定 ID 学不出有效表示。解决思路是分两步排查。先打印词表里出现次数最少的 10 个字符确认没有不可见字符然后把lr从 1e-3 降到 3e-4重启训练观察前 3 轮。如果是 NaN八成是梯度爆炸把clip_grad_norm从 5.0 降到 1.0 一般能救回来。另外检查DataLoader里有没有打开pin_memoryTrue但没做数据转换——这个组合在某些 PyTorch 版本里会导致 loss 随机变 NaN。5.4 现象Windows 下跑出乱码Linux 下正常文本生成模型本身没有编码问题但 Windows 控制台的默认编码是 GBK而你的脚本和模型文件是 UTF-8。训练时打印 loss 没事print(generate(...))一跑输出全是“鈥斺€斺€”这类乱码。这不是模型问题是控制台编码不匹配。解决Python 3.7 里可以直接在脚本开头加一行环境设置把标准输出的编码强制改成 UTF-8import sys sys.stdout.reconfigure(encodingutf-8)如果这样改了还乱码就把生成结果写入文件再用编辑器打开绕开控制台。这也是为什么我总建议生成脚本直接保存到 txt 文件而不是打印到屏幕——即使不涉及乱码小说级长度的输出在控制台里也会滚动到看不到开头。5.5 现象显存 OOM 或者一个 epoch 要跑半小时字符级 LSTM 的显存消耗和batch_size * seq_len * hidden_size成正比。64 的 seq_len 加 256 的 hidden_size 对 6GB 显存毫无压力但如果想跑 seq_len512 去学长段落显存可能直接拉满。OOM 的解决思路有三个按性价比排序第一包一个torch.no_grad()在验证循环里很多人漏了这步验证阶段白占了和训练一样的显存第二用小 batch 加梯度累积替代大 batch第三确认输入张量是torch.long而不是torch.floatLSTM 的 embedding 查表不会因为 float 输入报错但显存会翻四倍。CPU 跑得慢是另一个维度的常态问题。一个百万字符级别的语料在普通 i5 上跑 30 轮大约需要 35 小时。如果想缩短优先把hidden_size从 256 降到 128这是对速度影响最明显的参数效果损失能在可接受范围内。GPU 上如果显存够把batch_size加到 128 反而能更充分利用算力训练总时间比 64 的配置少 20% 左右。6. 进阶技巧让 LSTM 学出“章回体”结构的三个办法到这一步你的模型已经能生成文字通顺的段落了但大概率它写不出“回目”——也就是“第一回 纵横钩党清流祸”这种结构。这类问题是字符级 LSTM 的天然短板它只朝前看 64 个字而回目和正文的呼应关系跨越了上千字。三个办法可以在不换模型的前提下改善。第一个办法是给段落边界显式编码。训练数据里把每段的换行符替换成特殊 tokenP让模型把“换段”当成一个普通字符来学。这样它会在隐状态里维护“现在处于段首还是段中”的信息生成结果会自带分段结构。做法就一行text text.replace(\n\n, P)同时把这个 token 加入词表。第二个办法是把回目当训练单元的分隔锚点。每个回目开头的“第X回”前面加一个BOStoken模型读到它就知道自己该在“回目模式”下输出。生成时输入BOS 第模型就有概率接出“第一回”而不是直接说“韦小宝”。理论上这个技巧依赖模型对BOS后文法的记忆层数不够时效果有限但聊胜于无。第三个办法是合成式生成。先用 beam search 思路生成 5 个候选开头挑一句回目风格最浓的再以它为 seed 继续生成正文。这个办法的实现成本最低本质是把模型当成采样器而不是生成器适合不想动训练代码的人。我对这个技巧的评价是它治标不治本但效果立竿见影——生成结果的结构感至少提升一个档次。训练文本生成模型久了之后我自己的一个习惯是每调一次参数固定生成同一个 seed比如“韦小宝走进扬州”把新旧结果放一起对比。这样能直观看出改动带来了什么而不是靠玄学感觉。你能把这一步坚持住胜过读十篇调参心得。希望帮到你。本文还有配套的精品资源点击获取