桑簭腅ncoder-Decoder到注意力機制的實踐)
又到了一年一度的畢設(shè)季后臺好幾個讀者問我同一個題目“基于深度學習的新聞?wù)伤惴▽崿F(xiàn)”。這個題選得挺聰明新聞?wù)獙儆谖谋旧衫镒钣写硇缘娜蝿?wù)之一數(shù)據(jù)好找、效果直觀、還能往上堆注意力機制和預(yù)訓練模型不管是本科還是碩士都有足夠的發(fā)揮空間。但真上手你會發(fā)現(xiàn)單是“能用Encoder-Decoder跑通一個模型”和“把摘要做出可用效果”之間隔著數(shù)據(jù)清洗、調(diào)參、解碼策略、評估指標好幾座大山。這篇文章就把我做這個題目時踩過的坑和驗證過可行的方案完整梳理一遍。從模型選型、數(shù)據(jù)處理、訓練細節(jié)到推理解碼和評估盡量說清楚每一步“為什么這么干”不光是給代碼。適合正在做相關(guān)畢設(shè)的同學也適合想認真入坑自然語言生成的新手??赐昴阒辽倌苌僮咭粋€月的彎路。1. 整體方案設(shè)計與核心思路拆解1.1 為什么新聞?wù)m合做深度學習畢設(shè)選畢設(shè)題目有個樸素的標準數(shù)據(jù)要夠多、任務(wù)要夠明確、效果要能被看懂。新聞?wù)獎偤萌龡l全占。新聞?wù)Z料在網(wǎng)上有大量公開來源爬下來就是現(xiàn)成的“原文-標題”配對不需要像很多 NLP 任務(wù)那樣請人標注天然就是弱監(jiān)督數(shù)據(jù)。摘要這個東西普通人一眼就能看出好不好答辯演示的時候老師哪怕不懂深度學習也能直觀感受到“這個模型生成的標題通不通順、有沒有抓住重點”。另外這個題目的技術(shù)含量也可以循序漸進。最基礎(chǔ)的版本用 Encoder-Decoder 注意力就能跑想拿高分可以加 Pointer-Generator 解決未登錄詞問題、加 Beam Search 改善生成質(zhì)量、甚至換 BART/T5 這類生成預(yù)訓練模型作為對比實驗。一套下來從原理到工程都有了寫到論文里的“實驗對比”部分也會非常好看。1.2 抽取式還是生成式方案選型的關(guān)鍵決策做摘要之前首先要搞清楚一件事你是要做抽取式Extractive還是生成式Abstractive。這兩個方向的技術(shù)路線完全不同。抽取式是從原文里挑出關(guān)鍵句子拼成摘要技術(shù)上更像是“排序問題”經(jīng)典算法有 TextRank、基于圖的排序等。它的優(yōu)點是結(jié)果一定通順、沒有幻覺但缺點也很明顯——摘要是原文句子的拼接不夠凝練。比如一條 500 字新聞好的標題可能是一句原文里根本沒有的話抽取式做不出來。生成式則是“理解原文之后重新寫一句話”跟人類寫標題的思路更接近。它需要模型具備一定的“改寫”和“壓縮”能力這正是 Encoder-Decoder 框架擅長的。畢設(shè)題目既然點名了 Encoder-Decoder那主線當然走生成式。不過實操里我會建議你做一個抽取式作為 Baseline比如簡單的 TextRank 或者抽取第一句拼標題拿來和深度學習模型對比。別小看這個 Baseline它能從兩個角度幫你第一畢業(yè)論文里需要對照實驗Baseline 的存在讓你的深度學習模型提升有了參照第二如果深度學習模型連 TextRank 都打不過你也能早點發(fā)現(xiàn)問題而不是拖到最后。1.3 Encoder-Decoder 為什么是文本生成的地基說 Encoder-Decoder 之前先想想“生成摘要”這件事的本質(zhì)。輸入是一篇幾百詞甚至上千詞的新聞?wù)妮敵鍪且痪鋷资~以內(nèi)的標題模型要做的核心事情是先把長的東西“壓縮理解”再把這個理解“展開成一句話”。這正好對應(yīng)兩個模塊。Encoder 負責把整個輸入序列“看一遍”編碼成一個包含語義信息的向量序列Decoder 負責從這個向量序列出發(fā)一個詞一個詞地把摘要生成出來。你可以把整個過程類比成人類做閱讀理解先讀完整篇文章在腦子里形成“這篇文章在說什么”的把握然后動筆寫一句話梗概——寫的時候每寫一個詞都會回顧一下原文的重點確保自己沒跑偏。這個流程在數(shù)學上的表達就是Encoder 將輸入序列 x (x?, x?, ..., x?) 編碼為隱狀態(tài)序列 h (h?, h?, ..., h?)Decoder 在第 t 步根據(jù)之前的輸出詞 y?, y?, ..., y??? 以及編碼信息預(yù)測下一個詞 y? 的概率分布 p(y? | y?, ..., y???, x)整個模型訓練的目標就是讓每一步的條件概率乘積盡可能大也就是最大化 P(y?, y?, ..., y? | x) 。這個過程聽起來不復(fù)雜但從這個基礎(chǔ)框架出發(fā)能延伸出注意力機制、復(fù)制機制、指針網(wǎng)絡(luò)等大量改進方案所以我一直覺得把 Encoder-Decoder 吃透比直接調(diào)包用現(xiàn)成的摘要模型更有價值寫論文的時候也更有話可說。2. 核心技術(shù)拆解從RNN到注意力再到復(fù)制機制2.1 編碼器用雙向LSTM把新聞讀進隱狀態(tài)在最基礎(chǔ)的版本里Encoder 用的是單層 LSTM但我在實際項目里強烈建議你用雙向 LSTMBiLSTM。原因很簡單新聞標題的寫作經(jīng)常會用到全文前后文的信息。舉個例子“球隊在落后 20 分的情況下完成逆轉(zhuǎn)”如果你想在摘要里準確體現(xiàn)“逆轉(zhuǎn)”這個動作只看前半句是不夠的必須結(jié)合后文才知道發(fā)生了什么。雙向 LSTM 的 forward 方向能捕捉上文信息backward 方向能捕捉下文信息把兩者拼接起來每個位置的隱狀態(tài)就同時攜帶了上下文信息這對摘要任務(wù)來說非常關(guān)鍵。具體參數(shù)上我當時的設(shè)置是詞向量維度 d??? 256LSTM 隱藏層維度 d??dd?? 512。注意這里有個小坑——如果你用 PyTorch 的 nn.LSTM需要注意 batch_first 參數(shù)。我習慣設(shè)置 batch_firstTrue這樣輸入張量的形狀是 (batch_size, seq_len, embedding_dim)數(shù)據(jù)的組織方式更符合直覺。編碼器輸出的隱狀態(tài) h? 會組成一個矩陣維度是 (seq_len, hidden_dim * 2)雙向所以要乘 2。這個矩陣后半部分的工作——也就是注意力機制——要靠它提供原始素材所以這里盡量別省顯存hidden_dim 設(shè)成 512 是我試下來成本和效果比較平衡的點。2.2 解碼器與注意力機制每次生成都回顧原文重點如果只有一個編碼器的最后一個隱狀態(tài)傳給解碼器模型就相當于逼著一個幾百詞的新聞“壓縮成一個向量”這對長文本來說信息損失太嚴重了而且解碼器越到后面對原文的記憶越模糊。解決方案就是注意力機制。它的核心思想是解碼器在生成每個詞的時候不是機械地依賴一個固定向量而是動態(tài)地“回看”原文的每一個位置找出當前步驟最應(yīng)該關(guān)注的部分。我使用的是經(jīng)典的 Bahdanau Attention也叫加性注意力。它的計算過程可以拆成三步解碼器當前隱狀態(tài) s? 和編碼器的每個隱狀態(tài) h? 做一個打分函數(shù)得到一個分數(shù) e?? v? tanh(W?h? W?s?)對所有分數(shù)做 softmax得到注意力權(quán)重 α??用權(quán)重對所有編碼器隱狀態(tài)做加權(quán)求和得到上下文向量 c? Σ? α?? h?然后把這個上下文向量和解碼器隱狀態(tài)拼接起來用于預(yù)測下一個詞。這個機制就類似于人寫作時“重讀原文相關(guān)段落”的行為注意力權(quán)重越大說明原文那個位置對當前生成詞的貢獻越大。我經(jīng)常跟人說注意力機制的美妙之處在于它不光帶來了效果提升還能可視化——訓練完把注意力權(quán)重畫出來你能直接看到模型在生成“冠軍”這個詞時確實在重點看原文里“奪冠”那句話這玩意拿到答辯現(xiàn)場演示效果拉滿。2.3 指針生成網(wǎng)絡(luò)解決未登錄詞問題新聞里人名、機構(gòu)名、地名密集出現(xiàn)比如“某跨國公司的 CEO 宣布了什么政策”這些專有名詞往往不在詞表里是未登錄詞OOV。標準的 Seq2Seq 模型遇到這種情況只有一個辦法生成一個UNK占位符。如果摘要里頻繁出現(xiàn)UNK整個輸出就廢了老師一看就是“模型沒訓練好”。解決方案是在基礎(chǔ)架構(gòu)上加入指針生成網(wǎng)絡(luò)Pointer-Generator Network。它的核心思想是在生成每個詞時模型先算一個“拷貝概率”p_gen這個概率表示當前這個詞是從詞表里“生成”的還是直接從原文里“復(fù)制”過來的。具體來說解碼器每一步會計算 p_gen σ(Wc · c? Wy · y? Ws · s? b)其中 y? 是當前輸入詞。然后最終的詞概率分布是P(w) p_gen × P_vocab(w) (1 - p_gen) × Σ?:??? α??這個式子右邊第一項就是“從詞表生成詞”第二項是把原文里出現(xiàn)的詞按注意力權(quán)重“復(fù)制”過來。有了這個機制新聞里的人名地名就能原封不動地出現(xiàn)在摘要里效果提升非常明顯。我在實驗里對比過指針網(wǎng)絡(luò)把 ROUGE-1 的分數(shù)提高了大約 4 到 5 個點而且是肉眼可見的改善——沒有它摘要里一遇到人名就出現(xiàn)UNK有了它摘要瞬間像樣多了。2.4 覆蓋機制防止摘要陷入循環(huán)重復(fù)生成模型有個臭名昭著的問題生成到后面模型經(jīng)常會重復(fù)剛才已經(jīng)說過的詞比如“某某球隊 獲勝 獲勝 獲勝”。這是因為解碼器在生成第 t 個詞時沒有“記憶”自己之前已經(jīng)覆蓋過哪些原文信息注意力可能反復(fù)落在同一個位置。解決辦法是 Coverage Mechanism覆蓋機制。做法是維護一個覆蓋向量 cov?表示編碼器第 i 個位置已經(jīng)被注意力“看過”多少輪。每次計算注意力打分時把這個覆蓋向量加權(quán)進去相當于對重復(fù)關(guān)注過的位置進行“懲罰”e?? v? tanh(W?h? W?s? W?cov?)同時在損失函數(shù)里加一個覆蓋損失項鼓勵注意力權(quán)重不要重復(fù)分配。加了覆蓋機制之后模型生成重復(fù)詞的頻率顯著下降尤其是在新聞這種信息密集的文本上效果非常值得做。實測下來覆蓋機制 指針網(wǎng)絡(luò)這兩個改進是畢設(shè)拿高分的關(guān)鍵組合。3. 數(shù)據(jù)處理與模型訓練的實操細節(jié)3.1 數(shù)據(jù)集選擇與清洗流程做摘要任務(wù)數(shù)據(jù)質(zhì)量基本決定模型效果的天花板。我在做的時候用的是某公開的中文新聞?wù)Z料庫里面有幾十萬條新聞每條包含“正文”和“標題”兩個字段。但原始數(shù)據(jù)不能直接用里面的臟數(shù)據(jù)比你想象的多得多。我梳理了一套清洗流程你直接照著做就行:去掉正文長度小于 100 的條目太短的新聞沒有摘要價值而且會給數(shù)據(jù)引入大量噪聲去掉正文和標題長度比小于 2 的條目正常新聞的標題應(yīng)該遠比正文短如果標題比正文還長這個數(shù)據(jù)八成有問題刪除含 HTML 標簽、廣告關(guān)鍵詞如“點擊查看”“了解更多”的文本用正則表達式一次性處理全文統(tǒng)一轉(zhuǎn)換成簡體中文這里我用到了 OpenCC 庫繁簡轉(zhuǎn)換比較可靠把標題里的引號、書名號統(tǒng)一成半角格式避免同一個詞因為引號不同被切成兩個 token這套流程走完數(shù)據(jù)從最初的 40 萬條左右清洗到 32 萬條左右清洗率大約 20%。別嫌洗掉得多摘要模型對數(shù)據(jù)的整齊度極其敏感一條帶廣告尾巴的樣本就能讓模型在驗證集上多出兩三個“點擊查看”之類的垃圾輸出。3.2 分詞、構(gòu)建詞典與數(shù)據(jù)截斷中文 NLP 跟英文有個關(guān)鍵區(qū)別英文天然按空格分詞中文必須先分詞。我試過 jieba 分詞和基于 BERT 的切詞實際使用中 jieba 就夠用了畢竟我們用的是 LSTM 這類傳統(tǒng)模型不是預(yù)訓練模型。分詞之后是構(gòu)建詞典。這里要給一個最低配置參考詞表大小設(shè)成 5 萬把出現(xiàn)次數(shù)低于 5 的詞歸入UNK。構(gòu)建詞典時一定要把PAD、UNK、BOS、EOS這四個特殊 token 預(yù)留出來。其中PAD用來把 batch 里長度不一的句子補齊到相同長度UNK代表未登錄詞BOS和EOS分別用于告訴解碼器“開始生成”和“結(jié)束生成”。關(guān)于截斷長度我做了一組簡單的統(tǒng)計分析新聞?wù)拈L度集中在 300 到 600 字標題集中在 15 到 50 字。所以我設(shè)置了正文最大長度 512標題最大長度 80。長度不夠就 pad超過就截斷。這個設(shè)定在顯存利用率和效果之間比較平衡建議你跟著自己數(shù)據(jù)的分布去統(tǒng)計一下而不是拍腦袋隨便定。3.3 模型參數(shù)與訓練策略模型結(jié)構(gòu)搭建完成之后關(guān)鍵就是怎么訓練。我把自己的參數(shù)配置整理成了表格照著跑基本不會有方向性的錯誤參數(shù)設(shè)置值說明詞向量維度256隨機初始化沒有用預(yù)訓練詞向量LSTM 隱層維度512編碼器是雙向所以實際維度 1024LSTM 層數(shù)2超過 2 層提升有限訓練成本成倍增加Batch Size64視顯存調(diào)整12G 顯存夠用優(yōu)化器Adam學習率 0.001注意配合梯度裁剪梯度裁剪5.0防止 RNN 梯度爆炸必須加學習率衰減每 2 個 epoch 乘以 0.8后期收斂更穩(wěn)定Teacher Forcing 比例1.0 開始逐步降低前期強制用真實詞后期讓模型自己生成Epoch 數(shù)15 左右根據(jù)驗證集 ROUGE 早停這里重點說兩個只有實際跑過才會懂的坑。第一個坑是Teacher Forcing。Decoder 訓練時一種做法是每一步都把模型上一步生成錯誤的詞喂回模型結(jié)果誤差會像滾雪球一樣越滾越大模型根本訓練不動。所以我用 Teacher Forcing訓練時以一定概率把真實的目標詞喂給解碼器作為下一步輸入而不是用模型上一步生成的詞。但在訓練后期要逐步降低這個比例讓模型逐漸學會“自己生成自己吃”否則訓練出來的模型在測試時會因為推理和訓練分布不一致而崩掉。第二個坑是損失函數(shù)只計算 Decoder 部分的損失。新手很容易把 Encoder 的損失也算進去但實際上 Encoder 沒有直接的監(jiān)督信號它只是把輸入編碼成向量損失全部來自解碼器每個 step 的交叉熵。實現(xiàn)時記得把 padding 位置的損失 mask 掉不然PAD位置會讓損失函數(shù)出現(xiàn)偏差。我見過有人沒做 padding mask訓練了十幾個 epoch 后 loss 還是降不下去查了半天發(fā)現(xiàn)是這個問題。3.4 核心代碼框架一個簡化但完整的實現(xiàn)下面是訓練循環(huán)的核心框架代碼基于 PyTorch我做了刪減保留關(guān)鍵部分。要注意這里只是讓你看懂數(shù)據(jù)流完整代碼里還需要補上注意力打分、覆蓋向量這些細節(jié)class Encoder(nn.Module): def __init__(self, vocab_size, embed_size, hidden_size, num_layers2): super().__init__() self.embedding nn.Embedding(vocab_size, embed_size, padding_idx0) self.lstm nn.LSTM(embed_size, hidden_size, num_layers, batch_firstTrue, bidirectionalTrue) def forward(self, src, src_len): embedded self.embedding(src) # [batch, seq_len, embed_size] packed nn.utils.rnn.pack_padded_sequence( embedded, src_len, batch_firstTrue, enforce_sortedFalse) outputs, (hidden, cell) self.lstm(packed) outputs, _ nn.utils.rnn.pad_packed_sequence(outputs, batch_firstTrue) # outputs: [batch, seq_len, hidden_size * 2] return outputs, hidden, cell class Decoder(nn.Module): def __init__(self, vocab_size, embed_size, hidden_size, attention, num_layers2): super().__init__() self.embedding nn.Embedding(vocab_size, embed_size, padding_idx0) self.attention attention self.lstm nn.LSTM(embed_size hidden_size * 2, hidden_size, num_layers, batch_firstTrue) self.fc_out nn.Linear(hidden_size * 3, vocab_size) def forward(self, trg, encoder_outputs, hidden, cell): embedded self.embedding(trg) # [batch, trg_len, embed_size] # 將編碼器輸出展平到每個時間步 outputs [] for t in range(trg.size(1)): step_input embedded[:, t:t1, :] # [batch, 1, embed_size] context, attn_weights self.attention(step_input, encoder_outputs) lstm_input torch.cat((step_input, context), dim-1) output, (hidden, cell) self.lstm(lstm_input, (hidden, cell)) output torch.cat((output, context), dim-1) pred self.fc_out(output) # [batch, 1, vocab_size] outputs.append(pred) return torch.cat(outputs, dim1)上面代碼里我特意省略了注意力實現(xiàn)因為注意力機制根據(jù)覆蓋機制的有無會有幾個不同的版本。但你要看清楚核心邏輯Decoder 每個時間步都要把上一步的隱狀態(tài)、當前輸入詞向量、注意力機制產(chǎn)生的上下文向量這三者拼接之后再經(jīng)過 LSTM 和全連接層才能得到當前詞的概率分布。這種“拼接輸入”的模式是解碼器的標準玩法。訓練循環(huán)里有個關(guān)鍵點數(shù)據(jù)要裝成 DataLoader但要處理變長序列。我的做法是在 collate_fn 里對 batch 內(nèi)的樣本按長度排序然后用 pack_padded_sequence 壓縮 LSTM 計算。一句話總結(jié)就是RNN 處理變長序列時直接 pad 到相同長度會浪費大量算力pack_padded_sequence 才是正解。很多人圖省事直接 pad結(jié)果訓練速度慢一倍顯存還爆了。4. 推理階段、評估指標與調(diào)優(yōu)實戰(zhàn)4.1 用Beam Search而不是貪心搜索訓練完成之后生成摘要的推理階段同樣有很多門道。最樸素的解碼方式是貪心搜索每一步都選概率最大的詞然后把這個詞作為下一步輸入。這種辦法簡單但問題在于每一步最優(yōu)不等于整句話最優(yōu)。類似于下棋只走當前最好的一步可能后面全崩了。我用的是 Beam Search思路是每一步保留 top-k 個候選序列k 叫 beam size一般取 4 或 5最后從候選中選出概率乘積最大的一條。這就好比一次走棋保留幾種最好的走法同時往下推演。具體操作上需要注意 trg 端要按步數(shù)循環(huán)維護一個“候選序列集合”每輪擴展這個集合然后截取 top-k。這里貼上核心的推理邏輯def beam_search(model, encoder_outputs, src_mask, beam_size4, max_len80): batch_size encoder_outputs.size(0) # 初始化解碼器狀態(tài)和起始tokenBOS decoder_input torch.tensor([[BOS_IDX]] * batch_size) hidden, cell model.decoder.init_state(encoder_outputs) # 每個樣本維護多個候選: (log_prob_sum, sequence) sequences [[0.0, [BOS_IDX]]] completed [] for step in range(max_len): all_candidates [] for log_prob, seq in sequences: decoder_input torch.tensor([seq[-1]]).unsqueeze(0) output, hidden, cell model.decoder( decoder_input, encoder_outputs, hidden, cell) log_probs torch.log_softmax(output[0, -1], dim-1) topk_log_probs, topk_idx torch.topk(log_probs, beam_size) for k in range(beam_size): candidate (log_prob topk_log_probs[k].item(), seq [topk_idx[k].item()]) all_candidates.append(candidate) # 按得分排序保留當前長度下最好的beam_size個 all_candidates.sort(keylambda x: x[0], reverseTrue) sequences [] for _, seq in all_candidates: if seq[-1] EOS_IDX: completed.append((log_prob, seq)) else: sequences.append((log_prob, seq)) if len(sequences) beam_size: break if len(sequences) 0: break if completed: best sorted(completed, keylambda x: x[0], reverseTrue)[0] else: best sorted(sequences, keylambda x: x[0], reverseTrue)[0] return best[1]這里有一個容易出錯的細節(jié)Beam Search 的 log 概率用的是累加。因為每個詞的概率都小于 1連乘會讓數(shù)值越來越小甚至下溢所以統(tǒng)一取 log 之后做加法。另外beam 里所有序列盡量保持在相近的長度避免短的序列因為 token 少而得分偏高所以在最終排序時我會對長度做歸一化具體做法是除以 sequence length 的某個冪次比如 0.7這個冪次是經(jīng)驗值。4.2 ROUGE指標到底怎么算如何看懂它的含義做摘要畢設(shè)評估指標繞不開 ROUGE。ROUGE 的全稱是 Recall-Oriented Understudy for Gisting Evaluation簡單理解就是把生成的摘要和參考摘要拿來對比重疊詞的數(shù)量。它有幾個變種ROUGE-1單個詞的匹配率ROUGE-2相鄰兩個詞bigram的匹配率ROUGE-L最長公共子序列的匹配率不要只盯著一個指標看。我的經(jīng)驗是ROUGE-1 高不一定說明摘要好因為它只看單個詞模型只要多輸出幾個高頻詞就能刷上去ROUGE-2 更嚴格需要連續(xù)兩個詞都匹配ROUGE-L 關(guān)注的是語序信息。三個指標放在一起看才能反映模型的真實水平。實操里我直接用 py-rouge 或者 HuggingFace 的 evaluate 庫里面已經(jīng)封裝好了計算邏輯?;€水平大致這樣在一個幾十萬規(guī)模的中文新聞數(shù)據(jù)集上簡單的抽取式 Baseline取正文第一句做標題ROUGE-1 能有 20 上下。我的指針生成網(wǎng)絡(luò) 覆蓋機制模型ROUGE-1 到了 31 左右ROUGE-2 在 17 左右ROUGE-L 在 27 左右。別小看這個數(shù)字新聞標題的寫作自由度高同一個新聞不同媒體可能起完全不同的標題所以 ROUGE 分數(shù)天花板沒有想象中那么高ROUGE-1 能到 30 以上已經(jīng)算個不錯的模型了。4.3 常見問題排查速查表做這個項目時遇到的 90% 的問題都可以歸到下面幾類。我直接整理成一張表你碰到類似現(xiàn)象對著查就行問題現(xiàn)象可能原因排查與解法Loss 不下降學習率過高或過低嘗試 0.001、0.0005 從小往大調(diào)觀察前 500 步趨勢Loss 下降到 3 左右就停滯沒有做 padding mask檢查交叉熵是否把 位置的損失也算進去了生成摘要通篇都是 未登錄詞問題加入指針生成網(wǎng)絡(luò)或增大詞表生成內(nèi)容重復(fù)嚴重注意力崩潰加入覆蓋機制第 2 個 epoch 之后開始生效生成摘要比原文還長沒有設(shè)置 終止條件Beam Search 里強制遇到 or 達到 max_len 就停止訓練時顯存溢出batch size 太大從 64 降到 32或開啟梯度累積驗證 ROUGE 高但人工看起來差訓練數(shù)據(jù)太短檢查數(shù)據(jù)集里是不是有大量只能提取原句的短新聞同一個 batch 內(nèi)長度差異巨大訓練慢沒有用 pack_padded_sequence改用長度排序 pack 機制其中我想展開說兩個最容易被忽視的坑。第一個是驗證集不干凈導致 ROUGE 虛高。我遇到過一次驗證集里混了一些從網(wǎng)上抓的“臟數(shù)據(jù)”標題就是正文第一句話的截斷導致模型只要機械地復(fù)制開頭ROUGE 就非常高。但拿到真實新聞上一測效果慘不忍睹。這個問題必須警惕驗證集也得經(jīng)過清洗。第二個是訓練和驗證的預(yù)處理不一致。我一度在訓練時做了繁轉(zhuǎn)簡和全角轉(zhuǎn)半角但驗證時因為腳本寫漏了全角轉(zhuǎn)半角導致所有分數(shù)掉了 2 到 3 個點??雌饋硇嶋H影響巨大這種低級的預(yù)處理不一致問題我建議你在跑實驗之前寫一個單測腳本驗證 train/val 的預(yù)處理輸出是否一致。5. 實驗設(shè)計、論文展示與答辯經(jīng)驗5.1 對比實驗怎么設(shè)計才好看畢設(shè)能不能拿高分一半在模型的創(chuàng)新度另一半在實驗設(shè)計的完整度。單跑一個模型是不行的你需要一個遞進式的實驗序列。我當時設(shè)計的四組實驗是Baseline抽取式方法比如 TextRank 或直接取首句用于證明“簡單方法”的上限基礎(chǔ) Seq2Seq Attention證明深度學習方法至少不輸給抽取式Seq2Seq Attention Pointer-Generator證明復(fù)制機制對新聞?wù)陵P(guān)重要上面全部 Coverage 機制證明覆蓋機制能解決重復(fù)問題四組實驗結(jié)果畫成折線圖一組比一組高而且每一組提升都有明確的原理解釋。答辯的時候老師問“你這個模型對比抽取式有什么優(yōu)勢”“為什么加 Pointer 有用”你都能拿出數(shù)據(jù)逐條講。這種“遞進式”實驗設(shè)計比只放一個最終模型的單項結(jié)果有說服力得多。另外我強烈建議做一組人工評估。隨機抽 100 條新聞讓 3 個人打分1 到 5 分評估維度是流暢度、信息完整度、概括準確度。人工評估的好處是能捕捉到 ROUGE 漏掉的語義信息比如生成標題是否和原文事實矛盾。這部分內(nèi)容寫進論文里能體現(xiàn)你對問題的全面思考而不是只會跑指標。5.2 可視化與系統(tǒng)演示的加分項很多同學把精力全放在模型上結(jié)果演示環(huán)節(jié)只會開個 Jupyter Notebook 跑代碼給老師看那效果肯定大打折扣。我建議做一個簡單的 Web 演示頁面用 Gradio 或 Flask 包一個界面。用戶輸入新聞?wù)狞c擊提交模型自動生成標題。再順手把注意力熱力圖顯示出來讓老師看到生成某個詞時模型到底在看原文哪個位置。這一步不需要特別復(fù)雜的工程但交互式的效果遠勝于命令行輸出。另外一個細節(jié)把生成失敗的案例也準備幾個。答辯現(xiàn)場如果全是成功案例老師會覺得你挑了好的展示準備 2 到 3 個反面案例并主動解釋失敗原因比如“文本太長了長度超過訓練分布”“出現(xiàn)了訓練集里沒有的地名”反而能證明你對模型邊界有清晰的認識。5.3 從基礎(chǔ)版本到進階方向如果你的時間充??梢栽诨A(chǔ)模型之上再探索一到兩個進階方向這在論文里作為 Future Work 或者擴展實驗都很有分量。用生成預(yù)訓練模型做對比比如 BART 或 T5在中文上可以用中文版的生成模型?,F(xiàn)在很多開源模型都已經(jīng)支持新聞?wù)蝿?wù)它們的生成效果會顯著超過從頭訓練 LSTM但參數(shù)量大、訓練慢。你不需要復(fù)現(xiàn)整個預(yù)訓練過程只需要做微調(diào)對比。加入關(guān)鍵詞信息在很多新聞場景中標題其實就是最能概括全文的幾個關(guān)鍵詞的組合??梢試L試用無監(jiān)督方式抽取關(guān)鍵詞把它們作為輔助輸入注入 Decoder。多任務(wù)聯(lián)合訓練比如同時讓模型做“新聞分類”和“標題生成”共享 Encoder兩個任務(wù)互相促進。這在論文里屬于方法論創(chuàng)新可以增加理論深度。5.4 我自己踩過的那些坑經(jīng)驗總結(jié)做這個題目的過程中有幾個事情如果我一開始就知道能省出至少一周時間。整理成幾條個人體會放在這里第一不要在訓練腳本上過度糾結(jié)代碼優(yōu)雅。我一開始把所有模塊抽象成六個文件結(jié)果每次改一個超參數(shù)都要跳來跳去。后來我直接把訓練循環(huán)、模型定義、數(shù)據(jù)處理放在三個文件里簡單清晰跑實驗反而快很多。畢設(shè)的核心是驗證想法不是寫工程框架。第二每次實驗必須保存完整的配置記錄。我用的是最笨的辦法——每次跑實驗把參數(shù)自動保存成一個 JSON 文件命名里帶上日期和關(guān)鍵參數(shù)。否則你跑十組實驗之后根本分不清哪個結(jié)果對應(yīng)哪組參數(shù)。第三要盡早檢查生成結(jié)果而不是只盯著 loss。Loss 下降不等于生成質(zhì)量變好。我建議第一個 epoch 跑完就手動生成幾條摘要看一下哪怕模型還很爛至少能確認 pipeline 是通的。我曾經(jīng)歷過 loss 正常下降但生成結(jié)果全是重復(fù)詞的情況等發(fā)現(xiàn)問題的時候已經(jīng)白跑了三四個 epoch。最后再分享一個小技巧如果你在訓練時發(fā)現(xiàn)顯卡顯存不夠用又不想降 batch size 影響收斂速度可以用梯度累積——每幾個 batch 累加梯度再更新一次。我當時把 batch size 降到 32用梯度累積 2 步等效 batch size 還是 64效果幾乎沒有下降。但要注意用了梯度累積學習率可能需要同步微調(diào)我自己的經(jīng)驗是等效 batch size 翻倍時學習率可以稍微加大 10% 到 20%收斂速度會快一點。