RNN文本生成實(shí)戰(zhàn):從數(shù)據(jù)預(yù)處理到采樣調(diào)優(yōu)的完整指南)
簡介面向自然語言處理初學(xué)者與文本生成實(shí)踐者的代碼包演示了如何用循環(huán)神經(jīng)網(wǎng)絡(luò)RNN訓(xùn)練語言模型并自動(dòng)生成歌詞風(fēng)格文本。項(xiàng)目以周杰倫歌詞為訓(xùn)練語料涵蓋數(shù)據(jù)清洗、詞匯表構(gòu)建、序列填充、嵌入層與RNN層搭建、交叉熵?fù)p失訓(xùn)練及采樣生成等完整流程也提及可替換詩歌等語料拓展應(yīng)用。壓縮包內(nèi)含3個(gè)文件包括2個(gè)Python腳本輔助函數(shù)模塊與主訓(xùn)練生成程序和1個(gè)原始文本數(shù)據(jù)集整體僅72KB輕量易讀適合快速理解RNN文本生成的核心代碼結(jié)構(gòu)。目前已有1933人瀏覽學(xué)習(xí)對(duì)于希望通過實(shí)際代碼掌握RNN語言建模、LSTM/GRU門控機(jī)制及生成采樣方法的讀者是一份不錯(cuò)的入門參考。通過閱讀代碼和運(yùn)行示例可以學(xué)習(xí)到數(shù)據(jù)預(yù)處理的常見細(xì)節(jié)、模型訓(xùn)練的超參數(shù)調(diào)整思路以及如何利用訓(xùn)練好的模型生成新文本便于在此基礎(chǔ)上遷移到其他風(fēng)格文本生成任務(wù)。1. 文本生成到底難在哪先看懂 RNN 為什么是這一題的標(biāo)準(zhǔn)解文本生成不是“根據(jù)上一句接下一句”那么簡單。你把一句話拆成字符或詞它天然是變長的序列前后有依賴關(guān)系而且同一個(gè)詞在不同上下文里語義完全不同。用普通前饋網(wǎng)絡(luò)做輸入長度一固定短句子補(bǔ) padding 補(bǔ)到浪費(fèi)長句子截?cái)嘤謥G信息更別提“剛才提到的主語在隔了 20 個(gè)詞之后決定了現(xiàn)在這個(gè)動(dòng)詞的形態(tài)”這種長距離依賴前饋網(wǎng)絡(luò)根本接不住。RNN 就是為這種序列建模設(shè)計(jì)的它把上一個(gè)時(shí)間步的隱狀態(tài)和當(dāng)前輸入一起送進(jìn)循環(huán)單元理論上每個(gè)時(shí)間步都能攜帶歷史信息所以“用 RNN 實(shí)現(xiàn)文本生成”才成了這一類任務(wù)最經(jīng)典、最容易上手、也是最值得先跑通的方向。它解決的問題很具體給定一段前綴文本讓模型逐個(gè)預(yù)測下一個(gè)字符或下一個(gè)詞再把預(yù)測結(jié)果當(dāng)作輸入繼續(xù)往后滾從而生成一段全新但風(fēng)格相近的內(nèi)容。適合誰剛接觸 NLP 生成任務(wù)、想親手跑通一個(gè)最小閉環(huán)的開發(fā)者以及需要在對(duì)話、文案、詩歌等場景里做原型驗(yàn)證的工程師。這一篇就按我自己實(shí)踐時(shí)的順序講先說清楚數(shù)據(jù)怎么處理再搭一個(gè)能跑的字符級(jí) RNN接著把訓(xùn)練坑和采樣技巧講透最后給一個(gè)能直接復(fù)用的進(jìn)階方案。2. 一分為二看 RNN模型結(jié)構(gòu)選型與數(shù)據(jù)處理里的三個(gè)決定要?jiǎng)邮种跋劝褍杉露ㄏ聛硪皇沁x哪種 RNN 變體二是把原始文本切成什么樣的訓(xùn)練樣本。這兩件事決定后面所有代碼長什么樣也決定生成效果的上限。2.1 為什么默認(rèn)選 LSTM 而不是原生 RNN梯度問題的本質(zhì)原生 RNN 的循環(huán)公式是 h_t tanh(W·[h_{t-1}, x_t] b)結(jié)構(gòu)簡單但反向傳播時(shí)梯度要沿著時(shí)間步連乘。如果隱狀態(tài)維度過大、序列過長連乘的結(jié)果要么指數(shù)爆炸要么指數(shù)衰減前者讓 loss 劇烈震蕩后者讓模型完全學(xué)不到長距離依賴——這就是常說的梯度消失/爆炸。LSTM 引入門控機(jī)制用遺忘門、輸入門、輸出門來控制信息流動(dòng)梯度可以通過細(xì)胞狀態(tài)這條“高速公路”傳得更遠(yuǎn)。我一般不會(huì)糾結(jié)選型除非做實(shí)驗(yàn)對(duì)比否則直接上 LSTM。GRU 參數(shù)更少、訓(xùn)練更快效果通常和 LSTM 接近但 LSTM 的可控性更好。如果是新手我建議直接用 LSTM理由很實(shí)際教程多、調(diào)試經(jīng)驗(yàn)多、踩坑時(shí)能找到的參考最多。實(shí)際代碼里PyTorch 的nn.LSTM一行就能拿到輸出和最終隱狀態(tài)import torch.nn as nn lstm nn.LSTM( input_size64, # 每個(gè)時(shí)間步輸入的特征維度 hidden_size128, # 隱狀態(tài)維度控制模型容量 num_layers2, # 層數(shù)2 層能建模更復(fù)雜的變換 batch_firstTrue # 輸入 shape 為 (batch, seq_len, input_size) ) # output: (batch, seq_len, hidden_size)每個(gè)時(shí)間步的隱狀態(tài) # (h_n, c_n): 最后一層在最后時(shí)間步的隱狀態(tài)和細(xì)胞狀態(tài)邏輯說明output是每個(gè)時(shí)間步的隱狀態(tài)序列生成時(shí)每一步都要用到它來預(yù)測下一個(gè)字符h_n和c_n是最后一步的狀態(tài)做句向量或啟動(dòng)生成時(shí)用。參數(shù)說明input_size要和詞嵌入維度一致hidden_size是模型容量關(guān)鍵參數(shù)太大會(huì)過擬合、太小學(xué)不動(dòng)num_layers一般 1~3再深對(duì)小數(shù)據(jù)集沒收益。2.2 字符級(jí)還是詞級(jí)生成質(zhì)量、訓(xùn)練速度和參數(shù)量的一次權(quán)衡這是最容易被忽略的決策。字符級(jí) RNN 把每個(gè)字符當(dāng)作一個(gè)時(shí)間步詞表大小通常只有幾十到幾百模型輕、訓(xùn)練快而且不會(huì)遇到未登錄詞問題什么稀奇古怪的輸入都能硬生成缺點(diǎn)是序列長度暴增一個(gè) 100 字的句子就是 100 個(gè)時(shí)間步長距離依賴更難學(xué)。詞級(jí) RNN 用分詞后的 token 作為時(shí)間步序列短語義單元更完整但詞表動(dòng)輒上萬嵌入層參數(shù)量占了大頭還需要一個(gè)穩(wěn)定的分詞器遇到生僻詞或新詞直接崩。我的習(xí)慣是原型驗(yàn)證、詩歌/短文案、中文生成一律走字符級(jí)先看模型能不能“說人話”如果要做長文本生成或?qū)υ捪到y(tǒng)才轉(zhuǎn)向詞級(jí)或子詞級(jí)。字符級(jí)數(shù)據(jù)準(zhǔn)備是最容易跑通的起點(diǎn)。2.3 把文本切成訓(xùn)練樣本滑動(dòng)窗口、映射表與批處理字符級(jí)模型的做法是把所有文本拼成一個(gè)超長字符串按固定窗口長度切樣本。比如窗口長度是 100那么text[i:i100]作為輸入text[i1:i101]作為標(biāo)簽相當(dāng)于每個(gè)位置都在“預(yù)測下一個(gè)字符”。窗口要覆蓋足夠上下文太短學(xué)不到風(fēng)格太長訓(xùn)練慢。數(shù)據(jù)清洗也很關(guān)鍵我一般會(huì)統(tǒng)一換行、過濾掉非目標(biāo)語言字符、把連續(xù)空格壓成一個(gè)避免模型學(xué)到“輸出一堆空白”這種無聊模式。import torch import pickle # 假設(shè) text 是清洗后的完整文本比如幾萬行的源碼/歌詞/小說 chars sorted(set(text)) char2idx {ch: i for i, ch in enumerate(chars)} idx2char {i: ch for ch, i in char2idx.items()} SEQ_LEN 100 # 滑動(dòng)窗口長度 STEP 1 # 滑動(dòng)步長越小樣本越多訓(xùn)練越慢 sequences, targets [], [] for i in range(0, len(text) - SEQ_LEN, STEP): seq text[i:i SEQ_LEN] tgt text[i 1:i SEQ_LEN 1] sequences.append([char2idx[ch] for ch in seq]) targets.append([char2idx[ch] for ch in tgt]) # 轉(zhuǎn)成張量 X torch.tensor(sequences, dtypetorch.long) y torch.tensor(targets, dtypetorch.long) print(X.shape, y.shape) # 例如 torch.Size([19901, 100]) torch.Size([19901, 100])邏輯說明STEP1會(huì)讓相鄰樣本高度重疊模型收斂快但訓(xùn)練集很大STEP調(diào)大可以降冗余。參數(shù)說明SEQ_LEN100是經(jīng)驗(yàn)值中文文本 50~150 都合理字符集chars要先用sorted(set(text))固定順序否則每次運(yùn)行映射表不同保存模型后推理對(duì)不上。注意建好映射表后一定pickle.dump保存下來否則訓(xùn)練完再推理時(shí)字符順序一變生成結(jié)果就全亂了。3. 最小可跑的字符級(jí) RNN模型定義、訓(xùn)練循環(huán)與損失計(jì)算的完整代碼數(shù)據(jù)準(zhǔn)備好了接下來用一個(gè)能真正跑起來的代碼把訓(xùn)練閉環(huán)打通。這里我用兩層的 LSTM 加一個(gè)輸出全連接層損失函數(shù)用交叉熵。代碼不長但每一段都有講究。3.1 定義模型嵌入層、LSTM 與輸出層的連接方式輸入是字符索引先經(jīng)過嵌入層變成稠密向量再進(jìn)入 LSTM。LSTM 的輸出是(batch, seq_len, hidden_size)而我們要預(yù)測的是每個(gè)位置的“下一個(gè)字符”等價(jià)于對(duì)每個(gè)時(shí)間步做一次分類。所以把輸出 reshape 成(batch * seq_len, hidden_size)再接一個(gè)線性層映射到詞表大小。import torch.nn as nn import torch.nn.functional as F class CharRNN(nn.Module): def __init__(self, vocab_size, embed_dim128, hidden_dim256, num_layers2, dropout0.3): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim) self.lstm nn.LSTM(embed_dim, hidden_dim, num_layers, dropoutdropout, batch_firstTrue) self.fc nn.Linear(hidden_dim, vocab_size) def forward(self, x, hiddenNone): emb self.embedding(x) # (batch, seq_len, embed_dim) out, hidden self.lstm(emb, hidden) # out: (batch, seq_len, hidden_dim) out out.reshape(-1, out.shape[-1]) # (batch*seq_len, hidden_dim) logits self.fc(out) # (batch*seq_len, vocab_size) return logits, hidden邏輯說明reshape(-1, hidden_dim)把 batch 和序列長度合并原因在于交叉熵?fù)p失函數(shù)在 PyTorch 里要求(N, C)和(N,)的輸入N 是樣本點(diǎn)數(shù)C 是類別數(shù)。這樣每個(gè)時(shí)間步都是一個(gè)獨(dú)立的“預(yù)測下一個(gè)字符”任務(wù)。參數(shù)說明dropout0.3只對(duì)多層 LSTM 的層間連接生效num_layers1時(shí) dropout 不生效embed_dim和hidden_dim是容量核心數(shù)據(jù)量小就各減半避免過擬合。3.2 訓(xùn)練循環(huán)把標(biāo)簽對(duì)齊、loss 計(jì)算和隱狀態(tài)重置一次說清訓(xùn)練里最容易被新手繞暈的是標(biāo)簽對(duì)齊。數(shù)據(jù)準(zhǔn)備時(shí)輸入是text[i:iSEQ_LEN]標(biāo)簽是text[i1:iSEQ_LEN1]所以在計(jì)算 loss 前要把標(biāo)簽 reshape 成(batch * seq_len)和模型輸出一一對(duì)應(yīng)。def train(model, X, y, epochs30, batch_size128, lr1e-3): optimizer torch.optim.Adam(model.parameters(), lrlr) criterion nn.CrossEntropyLoss() dataset torch.utils.data.TensorDataset(X, y) loader torch.utils.data.DataLoader(dataset, batch_sizebatch_size, shuffleTrue) for epoch in range(epochs): total_loss 0 for xb, yb in loader: optimizer.zero_grad() logits, _ model(xb) loss criterion(logits, yb.reshape(-1)) loss.backward() # 梯度裁剪防止 LSTM 長序列訓(xùn)練時(shí)梯度爆炸 nn.utils.clip_grad_norm_(model.parameters(), max_norm5.0) optimizer.step() total_loss loss.item() * xb.size(0) print(fepoch {epoch1}/{epochs}, loss{total_loss / len(X):.4f})邏輯說明yb.reshape(-1)是關(guān)鍵輸入xb的 shape 是(batch, SEQ_LEN)對(duì)應(yīng)的標(biāo)簽 shape 也是(batch, SEQ_LEN)reshape 后變成(batch * SEQ_LEN,)的一維向量正好和logits的每一行對(duì)應(yīng)。參數(shù)說明batch_size128是默認(rèn)值小數(shù)據(jù)集可以降到 32lr1e-3對(duì) Adam 是安全的起始值但訓(xùn)練后期 loss 震蕩時(shí)可以降到5e-4。clip_grad_norm_是 RNN 訓(xùn)練里幾乎必須的一步不裁剪的話長序列反向傳播很容易在某一步梯度爆炸loss 直接變 NaN。3.3 保存檢查點(diǎn)而不是只存最終模型訓(xùn)練時(shí)間越長檢查點(diǎn)越重要。我的習(xí)慣是每個(gè) epoch 存一次只保留 loss 最低的那份。恢復(fù)訓(xùn)練也要把優(yōu)化器狀態(tài)一起存否則 lr 調(diào)度和動(dòng)量信息全丟。checkpoint { model_state: model.state_dict(), optimizer_state: optimizer.state_dict(), char2idx: char2idx, idx2char: idx2char, hidden_dim: 256, num_layers: 2, embed_dim: 128 } torch.save(checkpoint, best_model.pt)參數(shù)說明把char2idx和idx2char存進(jìn) checkpoint 是我踩過坑后加上的。只存模型權(quán)重推理時(shí)如果字符映射表對(duì)不上生成的文字大概率是亂碼?;謴?fù)時(shí)用torch.load讀取再按model.load_state_dict加載。4. 訓(xùn)練收斂與生成效果優(yōu)化溫度采樣、Top-k 與手動(dòng)調(diào)參清單模型訓(xùn)練出來了不代表生成效果好。直接取每個(gè)位置的 argmax概率最大的字符會(huì)讓輸出變成無限重復(fù)的“死循環(huán)”因?yàn)槟P鸵坏┻M(jìn)入高置信度的重復(fù)模式就沒法自己跳出。這一步需要用采樣策略來“加點(diǎn)隨機(jī)性”。4.1 溫度參數(shù)temperature怎么調(diào)從確定到胡言亂語的刻度溫度的核心思想是把 logits 除以一個(gè)大于 0 的數(shù)再經(jīng)過 softmax。溫度越低概率分布越尖銳輸出越保守溫度越高分布越平滑輸出越隨機(jī)。def sample_from_logits(logits, temperature1.0, top_kNone): logits logits / temperature if top_k is not None: # 只保留概率最高的 k 個(gè)候選其余設(shè)為負(fù)無窮 v, _ torch.topk(logits, top_k) logits[logits v[-1]] float(-inf) probs F.softmax(logits, dim-1) return torch.multinomial(probs, num_samples1).item()邏輯說明temperature 1時(shí) logits 被放大softmax 后概率集中的 token 更占優(yōu)勢temperature 1時(shí) logits 縮小概率分布趨于均勻輸出更“放飛”。top_k是另一道保險(xiǎn)只在前 k 個(gè)候選中采樣去掉長尾噪聲。參數(shù)說明中文短文案我一般用temperature0.8, top_k50歌詞或創(chuàng)意文本用temperature1.2, top_k100需要穩(wěn)定輸出時(shí)用temperature0.4, top_k20但基本等于近似貪心。經(jīng)驗(yàn)是溫度極低時(shí)模型會(huì)把高頻字“的”“了”無限重復(fù)溫度極高時(shí)會(huì)生成完全不像訓(xùn)練文本的亂碼。4.2 完整生成循環(huán)把每一步輸出作為下一步輸入生成是一個(gè)循環(huán)初始給一段種子文本模型預(yù)測下一個(gè)字符把這個(gè)字符拼接回去再取最后SEQ_LEN個(gè)字符作為下一輪輸入。注意輸入必須保持在SEQ_LEN長度內(nèi)否則模型會(huì)越推越偏。def generate(model, seed_text, length200, temperature0.8, top_k50): model.eval() # 把種子文本轉(zhuǎn)成索引不足 SEQ_LEN 時(shí)用空格補(bǔ)位 chars seed_text[-SEQ_LEN:] idx [char2idx.get(c, char2idx.get( , 0)) for c in chars] x torch.tensor(idx, dtypetorch.long).unsqueeze(0) # (1, seq_len) hidden None with torch.no_grad(): for _ in range(length): logits, hidden model(x, hidden) # 取最后一個(gè)時(shí)間步的 logits next_char sample_from_logits(logits[0, -1, :], temperature, top_k) seed_text idx2char[next_char] # 更新輸入把新字符追加到末尾丟掉最前面的字符 x torch.cat([x[:, 1:], torch.tensor([[next_char]])], dim1) return seed_text邏輯說明logits[0, -1, :]取的是當(dāng)前輸入序列最后一個(gè)字符對(duì)應(yīng)的輸出也就是“根據(jù)當(dāng)前全部上下文預(yù)測出的下一字符分布”。hidden在循環(huán)中持續(xù)向后傳這是 RNN 生成和普通分類預(yù)測最不一樣的地方。參數(shù)說明種子文本長度不必等于SEQ_LEN短于它時(shí)直接全部送入如果字符沒在詞表里我用空格兜底不直接報(bào)錯(cuò)。注意推理時(shí)一定要model.eval()并把生成代碼包在torch.no_grad()里否則啟用了 dropout 會(huì)讓生成結(jié)果隨機(jī)漂移而且顯存會(huì)被梯度計(jì)算無意義地占滿。4.3 手動(dòng)調(diào)參清單從 loss 曲線到文本效果的一次完整檢查Loss 曲線正常下降但生成文本亂大概率是采樣參數(shù)問題Loss 不降或者降得很慢優(yōu)先檢查數(shù)據(jù)。我這里給一份按優(yōu)先級(jí)排序的檢查清單現(xiàn)象優(yōu)先檢查項(xiàng)常用解法Loss 一直不降字符映射表錯(cuò)亂、標(biāo)簽沒對(duì)齊打印一批X[0]和y[0]對(duì)照原文Loss 降到 0.5 以下但生成重復(fù)采樣溫度過低或 top_k 過小調(diào)高 temperature 到 0.9top_k 到 100生成的全是常見停用詞嵌入維度和隱層維度過大導(dǎo)致過擬合減小 hidden_dim增加 dropout訓(xùn)練后期 loss 震蕩學(xué)習(xí)率太高把 lr 降到 5e-4 或加余弦退火生成的文本風(fēng)格不像訓(xùn)練集窗口長度太短把 SEQ_LEN 從 50 提到 150 再訓(xùn)練5. 訓(xùn)練與生成避坑指南五個(gè)典型翻車現(xiàn)場及修復(fù)方法5.1 現(xiàn)象一訓(xùn)練 loss 一直不下降還經(jīng)常出現(xiàn) NaN原因最常見的是梯度爆炸尤其序列長、層數(shù)多、lr 偏高時(shí)。LSTM 雖然有門控但輸出層的梯度仍可能爆炸。解決加梯度裁剪我通常把max_norm設(shè)在1.0~5.0之間同時(shí)把學(xué)習(xí)率降到5e-4或1e-4再試。如果還是 NaN檢查輸入里有沒有 NaN 或 Inf文本處理時(shí)可能混入了損壞字符。5.2 現(xiàn)象二生成的文本大量重復(fù)同一個(gè)字或短語原因模型陷入局部最優(yōu)概率最高的字符是“的”“了”這類高頻字加上采樣時(shí)溫度過低模型就繞不出這個(gè)循環(huán)。解決把temperature提高到0.9~1.2并用 top-k 把長尾噪聲截掉。還有一個(gè)隱藏坑重復(fù)也可能是因?yàn)镾EQ_LEN太短模型根本沒有足夠的上下文來“記住”剛生成過什么試試把窗口從 50 提到 150。5.3 現(xiàn)象三加載保存的模型后生成亂碼或報(bào) key 不匹配原因訓(xùn)練完保存模型時(shí)用了model.state_dict()但推理時(shí)重新定義了新的CharRNN(vocab_size, ...)vocab_size 和 checkpoint 里的嵌入層/輸出層維度對(duì)不上。解決建立模型前先讀 checkpoint 里的embed_dim、hidden_dim、num_layers用這些參數(shù)重建模型再load_state_dict。還要確認(rèn)char2idx用的是 checkpoint 里那份而不是重新對(duì)輸入文本set生成的。5.4 現(xiàn)象四生成速度越來越慢批量生成時(shí)卡頓明顯原因生成過程是一個(gè)字符一個(gè)字符推的無法并行序列越長推理越慢。如果沒開torch.no_grad()還會(huì)疊加顯存分配開銷。解決推理時(shí)固定用no_grad()如果一次要生成很多條可以把種子文本拼成一個(gè) batch 同時(shí)推只在最后采樣時(shí)逐條處理。批量生成時(shí)輸入 shape 變?yōu)?batch, seq_len)hidden 也要保持 batch 維一致。5.5 現(xiàn)象五訓(xùn)練時(shí)模型很快過擬合訓(xùn)練 loss 低但生成質(zhì)量反而更差原因模型把訓(xùn)練文本的細(xì)節(jié)“背”下來了而不是學(xué)到語言規(guī)律。這在小數(shù)據(jù)集上特別明顯。解決增大 dropoutLSTM 內(nèi)部dropout0.5也可以減小hidden_dim或者增加數(shù)據(jù)量。生成時(shí)用更高的溫度可以掩蓋一部分過擬合劣化但治標(biāo)不治本最有效的是把訓(xùn)練文本量做到至少 1MB 以上。6. 進(jìn)階用兩層 LSTM 加注意力生成中文詩詞或代碼風(fēng)格文本的實(shí)踐方案前面講的字符級(jí)模型已經(jīng)能跑通但如果你想讓生成結(jié)果更有“結(jié)構(gòu)感”——比如生成七言詩、四行歌詞或帶有縮進(jìn)和關(guān)鍵字的偽代碼需要給模型一點(diǎn)外部約束。常見做法是“約束解碼 結(jié)構(gòu)提示”。以詩詞生成為例我會(huì)在種子文本里帶上[七絕]這樣的標(biāo)記讓模型學(xué)到標(biāo)記和格式的對(duì)應(yīng)關(guān)系同時(shí)在采樣階段強(qiáng)行控制每一句的長度和韻腳。def generate_poem(model, prefix[七絕], lines4, chars_per_line7): out generate(model, seed_textprefix, lengthlines * chars_per_line, temperature0.75, top_k30) # 按每行長度切分硬性保證七言格式 poem out.replace([七絕], ).strip() lines_split [poem[i:ichars_per_line] for i in range(0, len(poem), chars_per_line)] return \n.join(lines_split[:lines])邏輯說明先用通用生成函數(shù)按總長度生成再按固定行寬切分是“事后糾正”的做法。更強(qiáng)的做法是在每生成一個(gè)字符時(shí)判斷當(dāng)前行是否已滿強(qiáng)行采樣換行符。實(shí)踐里事后切分更穩(wěn)強(qiáng)行控制容易讓模型生成不自然的截?cái)?。除了詩詞這段思路直接平移到代碼生成訓(xùn)練語料用縮進(jìn)敏感的 Python/C 風(fēng)格文本生成時(shí)限制 tab 和換行符的概率就能得到結(jié)構(gòu)基本正確的偽代碼。最后的驗(yàn)證習(xí)慣我建議保留一個(gè)每次訓(xùn)練完不要只看 loss真正要“驗(yàn)收”的是讓模型生成 10 組不同溫度的輸出人眼掃一遍重復(fù)率、語法錯(cuò)誤和風(fēng)格相似度。我吃過的虧是 loss 降到 0.3生成結(jié)果卻是一堆高頻詞無意義堆疊原因是數(shù)據(jù)本身噪音太大——清洗語料這一步花的功夫比調(diào)模型參數(shù)更值錢。后面再做類似任務(wù)時(shí)我都會(huì)先把訓(xùn)練文本的字符分布打印出來看一眼高頻字符占比是否過高再?zèng)Q定優(yōu)先洗數(shù)據(jù)還是調(diào)模型。文本生成沒有銀彈但“先跑通、再調(diào)采樣、最后回頭查數(shù)據(jù)”這條路徑基本不會(huì)走偏。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取