:核心原理與工程實(shí)踐)
1. 項(xiàng)目概述與設(shè)計(jì)思路1.1 為什么文本翻譯選 Transformer 而不是 LSTM前陣子我在本機(jī)搭了一套中英文本翻譯演示系統(tǒng)跑通之后最大的感觸是如果放在幾年前拿循環(huán)神經(jīng)網(wǎng)絡(luò)做同一個(gè)任務(wù)光是把代碼調(diào)成能并行訓(xùn)練這一步就能磨掉我大半天的耐心?,F(xiàn)在用 Transformer 作為序列到序列翻譯模型的骨干整條鏈路從數(shù)據(jù)清洗到端到端推理邏輯清晰太多了。翻譯本質(zhì)是一個(gè)序列到序列問(wèn)題輸入是源語(yǔ)言的一句話輸出是目標(biāo)語(yǔ)言的一句話。早年的主力方案是編碼器-解碼器結(jié)構(gòu)的 LSTM編碼器把源句壓成一個(gè)固定長(zhǎng)度的隱狀態(tài)向量解碼器從這個(gè)向量出發(fā)逐步生成目標(biāo)詞。問(wèn)題是句子稍微長(zhǎng)一點(diǎn)固定向量根本裝不下全部語(yǔ)義越靠前的信息越容易被后續(xù)內(nèi)容沖淡。Transformer 繞開(kāi)了“順序壓縮”的思路用自注意力機(jī)制讓每個(gè)詞直接對(duì)全句其他詞做加權(quán)關(guān)聯(lián)這就等于把“逐詞理解”變成了“全局關(guān)系建?!?。初學(xué)者最容易問(wèn)的問(wèn)題翻譯任務(wù)這么依賴上下文為什么不用更適合處理序列的循環(huán)結(jié)構(gòu)答案是并行效率。LSTM 必須按時(shí)間步串行計(jì)算第 t 步依賴第 t-1 步的隱狀態(tài)GPU 等于被迫排隊(duì)跑而 Transformer 的注意力計(jì)算在單層內(nèi)可以完全并行訓(xùn)練速度上一個(gè)量級(jí)。模型表達(dá)力更強(qiáng)速度還更快自然成為主流選項(xiàng)。1.2 這套系統(tǒng)到底做了什么、適合誰(shuí)參考這個(gè)項(xiàng)目最終完成的功能很實(shí)在給定一句中文系統(tǒng)輸出一句英文給定一句英文系統(tǒng)也能反向輸出中文。嚴(yán)格來(lái)說(shuō)這是典型的開(kāi)放詞表翻譯任務(wù)而不是那種只能翻幾十個(gè)固定句式的玩具 Demo。整個(gè)系統(tǒng)包含數(shù)據(jù)預(yù)處理、詞表構(gòu)建、模型訓(xùn)練、推理解碼四個(gè)模塊我用 PyTorch 復(fù)現(xiàn)了 Transformer 的標(biāo)準(zhǔn)結(jié)構(gòu)編碼器六層解碼器六層注意力頭數(shù)八模型維度 512。訓(xùn)練時(shí)使用 WMT 新聞?wù)Z料的一個(gè)子集清洗之后保留約一百萬(wàn)對(duì)中英平行句對(duì)在單卡 GPU 上跑了兩輪多。這篇博文適合兩類人一是剛接觸 NLP、想做文本翻譯但不知道從哪下手的初學(xué)者跟著把每個(gè)模塊拆開(kāi)之后能建立 Transformer 的完整直覺(jué)二是已經(jīng)有工程經(jīng)驗(yàn)、想快速驗(yàn)證 Attention 機(jī)制實(shí)際效果的朋友可以直接參考我貼出來(lái)的關(guān)鍵實(shí)現(xiàn)和踩坑記錄。我始終覺(jué)得與其照著教程“抄代碼”不如先把設(shè)計(jì)思路想明白。1.3 把“翻譯任務(wù)”拆成四個(gè)關(guān)鍵環(huán)節(jié)如果只給一句話概括這套系統(tǒng)的工程鏈路我會(huì)寫數(shù)據(jù)清洗與句子對(duì)齊 → 子詞切分與詞表構(gòu)建 → Transformer 編碼器-解碼器訓(xùn)練 → 束搜索解碼輸出。這四個(gè)環(huán)節(jié)每一步都有坑每個(gè)坑都會(huì)影響最終翻譯質(zhì)量。有意思的是很多人在模型結(jié)構(gòu)上花大功夫調(diào)參反而忽視前兩步。實(shí)際上平行語(yǔ)料里中文和英文句子經(jīng)常存在數(shù)量不對(duì)齊、標(biāo)點(diǎn)錯(cuò)位、長(zhǎng)句亂斷的問(wèn)題如果輸入是“你好世界”這種中英文之間詞序相差很大的句子分詞方式又直接決定了模型能否學(xué)到詞對(duì)齊關(guān)系。我把清洗規(guī)則寫得很保守去掉長(zhǎng)度超過(guò) 80 個(gè) token 的長(zhǎng)句過(guò)濾雙語(yǔ)長(zhǎng)度比例超過(guò) 3:1 的對(duì)齊對(duì)統(tǒng)一把全角標(biāo)點(diǎn)轉(zhuǎn)半角。這套規(guī)則不做特別聰明的處理但很可靠。2. 核心細(xì)節(jié)解析與實(shí)操要點(diǎn)2.1 輸入表示詞嵌入和位置編碼缺一不可自注意力機(jī)制本身不具備天然的順序感如果不加位置編碼句子“貓追老鼠”和“老鼠追貓”對(duì)模型來(lái)說(shuō)沒(méi)有任何區(qū)別。這是 Transformer 和 RNN 最大的結(jié)構(gòu)差異RNN 靠遞推順序隱式獲得位置信息Transformer 必須顯式給每個(gè) token 注入位置信號(hào)。我采用的是原論文里的正弦位置編碼位置編碼向量直接和詞嵌入向量相加然后一起送入第一層。公式如下PE(pos, 2i) sin(pos / 10000^(2i / d_model))PE(pos, 2i1) cos(pos / 10000^(2i / d_model))其中 pos 是 token 在句子里的位置索引i 是向量維度序號(hào)。選擇正弦函數(shù)而不是學(xué)出來(lái)的位置向量主要是為了處理超長(zhǎng)句子模型沒(méi)見(jiàn)過(guò)的長(zhǎng)度也能生成對(duì)應(yīng)的位置編碼不依賴訓(xùn)練數(shù)據(jù)的長(zhǎng)度分布。這里有個(gè)很容易被忽略的細(xì)節(jié)位置編碼的振幅和詞嵌入初始化標(biāo)準(zhǔn)差要保持在同一個(gè)量級(jí)。如果詞嵌入初始化成均值為 0、方差 1而位置編碼系數(shù)是固定三角函數(shù)的振幅 1兩者相加之后輸入分布會(huì)被位置信號(hào)主導(dǎo)。我實(shí)際實(shí)驗(yàn)時(shí)把詞嵌入層先乘以 sqrt(d_model) 做縮放再疊加位置編碼訓(xùn)練穩(wěn)定度明顯好很多。2.2 多頭注意力Q、K、V 的角色分工注意力機(jī)制可以類比成一個(gè)“信息檢索”過(guò)程。每個(gè) token 生成三個(gè)向量Query 表示“我關(guān)心什么信息”Key 表示“我能提供什么信息”Value 是“我擁有的信息內(nèi)容本身”。當(dāng)前 token 拿 Query 去和所有 token 的 Key 做點(diǎn)積得到一個(gè)相似度分?jǐn)?shù)經(jīng)過(guò) softmax 歸一化成權(quán)重再去加權(quán)所有 token 的 Value完成信息融合。多頭就是把這個(gè)過(guò)程復(fù)制多份。每個(gè)頭使用獨(dú)立的投影矩陣把詞向量投影到不同的子空間讓模型分別關(guān)注不同的關(guān)系。比如一個(gè)頭學(xué)會(huì)關(guān)注句法主語(yǔ)和謂語(yǔ)的關(guān)系另一個(gè)頭關(guān)注代詞和先行詞的關(guān)系最后的輸出把所有頭的向量拼接起來(lái)再經(jīng)過(guò)一次線性變換。八個(gè)頭可以理解為八套“并行的關(guān)系透鏡”。我在項(xiàng)目里比較特殊的處理是把注意力分?jǐn)?shù)除以 sqrt(d_k)也就是 Query 向量維度的平方根。d_k 是 64512 除以 8 個(gè)頭的數(shù)量所以除以 8。如果不縮放兩組隨機(jī)初始化向量做點(diǎn)積方差會(huì)隨著維度增大變得很大softmax 之后概率分布接近獨(dú)熱梯度無(wú)法有效回傳。這個(gè)縮放是整個(gè)自注意力能夠穩(wěn)定訓(xùn)練的基石之一新手調(diào)參時(shí)往往忽略這個(gè)“常數(shù)”。2.3 編碼器和解碼器掩碼遮住的未來(lái)信息翻譯模型解碼時(shí)有一個(gè)關(guān)鍵約束生成第 t 個(gè)詞時(shí)模型只能看到已經(jīng)生成的 t-1 個(gè)詞絕不能偷看后面的目標(biāo)詞。想像一下考試寫作文你寫到第五個(gè)字的時(shí)候當(dāng)然不知道接下來(lái)要寫的字是什么——模型也一樣。因此解碼器里的自注意力必須施加“未來(lái)掩碼”把矩陣右上角的部分置為負(fù)無(wú)窮softmax 之后這些位置的注意力權(quán)重會(huì)趨近于零。編碼器和解碼器的另一個(gè)區(qū)別是解碼器中間多了一個(gè)交叉注意力層cross-attention。這個(gè)層的 Query 來(lái)自解碼器上一層Key 和 Value 來(lái)自編碼器輸出。也就是解碼器在生成詞的時(shí)候不僅參考自己已經(jīng)生成的內(nèi)容還持續(xù)參考源句的全局語(yǔ)義。這是機(jī)器翻譯最核心的“對(duì)齊”機(jī)制每生成一個(gè)目標(biāo)詞都要決定源句哪些詞貢獻(xiàn)最大。我在實(shí)現(xiàn)里把三種 mask 放在一起做了統(tǒng)一封裝。padding mask 把填充符位忽略look-ahead mask 防止解碼器偷看未來(lái)目標(biāo)詞交叉注意力的 padding mask 去掉編碼器填充位。這塊代碼最容易被初學(xué)者寫亂尤其要小心 mask 廣播的維度。我在調(diào)試時(shí)用過(guò)一個(gè)很笨但有效的方法把 mask 值可視化生成一個(gè)上三角是深色、下三角是淺色的矩陣圖一眼就能看出未來(lái)掩碼是否生效。2.4 殘差連接、層歸一化和前饋層Transformer 每一層都在做“先變換再繞過(guò)變換”的操作。子層輸出先經(jīng)過(guò)殘差連接與輸入相加再做層歸一化。殘差連接保證了深層網(wǎng)絡(luò)的梯度不會(huì)因?yàn)閷訑?shù)加深而消失層歸一化則讓每一層的激活值保持穩(wěn)定的分布。在翻譯這種生成任務(wù)里訓(xùn)練和推理階段最怕的就是“小波動(dòng)被不斷放大”某層輸入均值和方差異常最終輸出幾乎一定會(huì)崩。LayerNorm 和 BatchNorm 的差別在于LayerNorm 是對(duì)單個(gè)樣本的所有特征維度做歸一化不依賴 batch 大小mini-batch 特別小的時(shí)候也非常穩(wěn)定。我在訓(xùn)練時(shí)把 batch 壓到 32 個(gè)句子LayerNorm 依然表現(xiàn)良好換成 BatchNorm 很可能已經(jīng)無(wú)法收斂了。前饋層就是兩層的全連接網(wǎng)絡(luò)中間加 ReLU 激活維度從 512 上升到 2048再壓縮回 512。這個(gè)層位置在注意力子層之后可以理解為“注意力負(fù)責(zé)融合全局信息前饋層負(fù)責(zé)逐 token 做非線性變換”。整個(gè) block 的堆疊過(guò)程就是 6 層編碼器 6 層解碼器層與層之間互相傳遞表征最后一層輸出再接進(jìn)線性層和 softmax產(chǎn)生目標(biāo)詞的概率分布。2.5 分詞與詞表BPE 子詞切分到底解決什么問(wèn)題中英翻譯里一個(gè)棘手問(wèn)題是未登錄詞。日常語(yǔ)料中經(jīng)常出現(xiàn)人名、地名、機(jī)構(gòu)名如果按詞為單位建模詞表動(dòng)輒幾十萬(wàn)稀疏問(wèn)題非常嚴(yán)重。我采用的方案是 BPEByte Pair Encoding先把語(yǔ)料切到字符級(jí)別再反復(fù)合并出現(xiàn)頻率最高的相鄰字符對(duì)最終得到一套規(guī)模適中、包含完整語(yǔ)義單元中又保留字符彈性的子詞表。子詞化的價(jià)值體現(xiàn)在翻譯英文單詞 understanding 時(shí)模型可以選擇生成 unprefix、stand 和 ing 多個(gè)片段組合出完整含義。遇到詞表外的生詞時(shí)至少能降級(jí)用字符合成避免全部變成 UNK。我的詞表規(guī)模控制在 32000 個(gè)子詞中文部分按字為單位切分這種組合在翻譯質(zhì)量和小語(yǔ)料適配性之間取得了平衡。動(dòng)手實(shí)踐的時(shí)候我建議先用現(xiàn)成的分詞工具做快速驗(yàn)證比如 SentencePiece 或 HuggingFace 的 tokenizer等整體流程跑通之后再嘗試做自定義標(biāo)簽化。分詞水平直接決定模型上限如果詞表里中文常用字都缺后面的 Transformer 結(jié)構(gòu)再精巧也是白搭。3. 實(shí)操過(guò)程與核心環(huán)節(jié)實(shí)現(xiàn)3.1 數(shù)據(jù)準(zhǔn)備與清洗流程這個(gè)項(xiàng)目的訓(xùn)練語(yǔ)料來(lái)自公開(kāi)的中英平行語(yǔ)料子集。這里要特別注意語(yǔ)料下載之后不要直接進(jìn)模型要做三層清洗。第一層是格式清洗。把編碼統(tǒng)一成 UTF-8繁體中文轉(zhuǎn)成簡(jiǎn)體英文大小寫歸一剔除包含亂碼、HTML 標(biāo)簽或異常 Unicode 字符的句子。第二層是對(duì)齊質(zhì)量過(guò)濾。中英句對(duì)經(jīng)常存在“一句中文對(duì)應(yīng)兩句英文”的情況我按照標(biāo)點(diǎn)切分后做兩兩組合盡量保證句對(duì)語(yǔ)意完整。第三層是長(zhǎng)度過(guò)濾。過(guò)短的句子少于 4 個(gè) token信息量小過(guò)長(zhǎng)的句子超過(guò) 80 個(gè) token訓(xùn)練成本高且容易爆顯存直接去掉。清洗完之后我給數(shù)據(jù)做了打亂和分批。批次大小按 token 數(shù)來(lái)算而不是按句子條數(shù)。一個(gè) batch 大約包含 2500 個(gè)源端 token這樣能避免批次內(nèi)句子長(zhǎng)短懸殊導(dǎo)致大量 pad浪費(fèi)計(jì)算量。實(shí)際操作時(shí)我會(huì)先按長(zhǎng)度大致排序再隨機(jī)打亂緩沖區(qū)達(dá)到類似分桶的效果。3.2 編碼器-解碼器結(jié)構(gòu)的要點(diǎn)代碼下面是我整理的 Transformer 核心模塊的結(jié)構(gòu)示意去掉了無(wú)關(guān)的工程細(xì)節(jié)保留最容易寫錯(cuò)的注意力部分。整個(gè)項(xiàng)目代碼大約 600 行核心網(wǎng)絡(luò)模塊集中在 200 行以內(nèi)。import torch import torch.nn as nn import math class PositionalEncoding(nn.Module): def __init__(self, d_model, max_len5000): super().__init__() pe torch.zeros(max_len, d_model) pos torch.arange(0, max_len).unsqueeze(1).float() div_term torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model)) pe[:, 0::2] torch.sin(pos * div_term) pe[:, 1::2] torch.cos(pos * div_term) self.register_buffer(pe, pe.unsqueeze(0)) def forward(self, x): return x self.pe[:, :x.size(1)] def attention(q, k, v, maskNone): d_k q.size(-1) scores torch.matmul(q, k.transpose(-2, -1)) / math.sqrt(d_k) if mask is not None: scores scores.masked_fill(mask 0, float(-inf)) weights torch.softmax(scores, dim-1) return torch.matmul(weights, v) class MultiHeadAttention(nn.Module): def __init__(self, d_model, num_heads): super().__init__() self.d_model d_model self.num_heads num_heads self.d_k d_model // num_heads self.w_q nn.Linear(d_model, d_model) self.w_k nn.Linear(d_model, d_model) self.w_v nn.Linear(d_model, d_model) self.w_out nn.Linear(d_model, d_model) def forward(self, q, k, v, maskNone): batch_size q.size(0) q self.w_q(q).view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2) k self.w_k(k).view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2) v self.w_v(v).view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2) attn attention(q, k, v, mask) attn attn.transpose(1, 2).contiguous().view(batch_size, -1, self.d_model) return self.w_out(attn)這里我故意把 mask 的維度留作了待傳參數(shù)。實(shí)際調(diào)用時(shí)編碼器自注意力傳 padding mask解碼器自注意力傳 padding mask 與 look-ahead mask 相加的結(jié)果交叉注意力傳針對(duì)編碼器輸出的 padding mask。維度不匹配是高頻報(bào)錯(cuò)點(diǎn)有一個(gè)簡(jiǎn)單的自查方法注意力分?jǐn)?shù)矩陣的形狀一般是 (batch, heads, tgt_len, src_len)mask 必須是和這個(gè)形狀兼容的 4 維張量或者廣播后的形狀。3.3 訓(xùn)練配置與損失函數(shù)選擇訓(xùn)練翻譯模型我用的損失是交叉熵配合 label smoothing標(biāo)簽平滑。標(biāo)準(zhǔn)的交叉熵要求模型對(duì)正確詞輸出概率為 1這會(huì)導(dǎo)致模型過(guò)度自信、生成譯文僵化。標(biāo)簽平滑把正確詞的概率目標(biāo)設(shè)置為 0.9剩余 0.1 平均分配給其他詞給模型一點(diǎn)容忍度。平滑系數(shù)太小沒(méi)效果太大會(huì)讓模型喪失自信、譯文反而變差我試下來(lái) 0.1 是比較穩(wěn)的默認(rèn)值。優(yōu)化器選擇 Adambeta 參數(shù)為 (0.9, 0.98)epsilon 設(shè)為 1e-9。這里最需要注意的是千萬(wàn)不要直接用一個(gè)固定的學(xué)習(xí)率。Transformer 論文用的是 warmup 策略前 4000 步學(xué)習(xí)率從很小線性升到峰值之后按步數(shù)的負(fù)平方根緩慢衰減。原因是訓(xùn)練早期參數(shù)變化劇烈學(xué)習(xí)率太大會(huì)把注意力權(quán)重推入飽和區(qū)之后很難修正。超參數(shù)配置我整理成了表格這也是我最終跑通實(shí)驗(yàn)的一組可用參數(shù)參數(shù)名數(shù)值說(shuō)明d_model512嵌入維度注意力輸出維度num_layers6編碼器/解碼器層數(shù)num_heads8多頭注意力的頭數(shù)d_ff2048前饋網(wǎng)絡(luò)隱藏維度dropout0.1各子層之間的隨機(jī)失活概率batch_tokens2500每個(gè) batch 的源端 token 上限warmup_steps4000學(xué)習(xí)率預(yù)熱步數(shù)label_smoothing0.1標(biāo)簽平滑系數(shù)max_length80訓(xùn)練時(shí)允許的最大 token 數(shù)訓(xùn)練時(shí)我每 500 步用一個(gè) 500 句的驗(yàn)證集測(cè)一次 BLEU。BLEU 值在 1 萬(wàn)步以前增長(zhǎng)緩慢屬于正常現(xiàn)象不需要焦慮。我自己跑到約 3 萬(wàn)步時(shí)驗(yàn)證集 BLEU 從 7 漲到 26 左右后面越漲越慢到 6 萬(wàn)步基本進(jìn)入平臺(tái)期。3.4 推理解碼貪心搜索換成束搜索訓(xùn)練結(jié)束之后推理階段的目標(biāo)是給定源句逐步生成目標(biāo)句。最簡(jiǎn)單的貪心搜索每一步只取概率最高的詞作為當(dāng)前輸出。問(wèn)題是一旦第一步選錯(cuò)后續(xù)全部被帶偏無(wú)法回頭修正。束搜索的思路是每一步保留概率最高的前 beam size 條路徑最終選總體概率最高的那條。我用的是 beam size4再加一個(gè)長(zhǎng)度懲罰length penalty。模型傾向于輸出較短的句子因?yàn)槎叹涿總€(gè)詞的概率乘起來(lái)數(shù)值更大長(zhǎng)句反而吃虧。長(zhǎng)度懲罰項(xiàng)的做法是給每條候選句子的得分除以一個(gè)關(guān)于長(zhǎng)度的懲罰系數(shù)公式近似為 score / ((5 len) / 6) 的 alpha 次方alpha 取 0.6。這個(gè)操作不用動(dòng)訓(xùn)練代碼只在推理 decode 時(shí)加卻能顯著改善長(zhǎng)句譯文的完整性。推理過(guò)程中每條 beam 路徑都在按自回歸方式追加 token直到出現(xiàn)句尾符或長(zhǎng)度達(dá)到上限。我會(huì)額外設(shè)置一個(gè)重復(fù)懲罰如果當(dāng)前候選序列中某個(gè)詞出現(xiàn)次數(shù)過(guò)多就給它降權(quán)。這個(gè)技巧在翻譯新聞?wù)Z料時(shí)非常管用能避免模型陷入“不斷重復(fù)最后一個(gè)詞”的死循環(huán)。4. 常見(jiàn)問(wèn)題與排查技巧實(shí)錄4.1 訓(xùn)練時(shí)顯存耗盡翻譯模型的顯存消耗大頭是注意力矩陣。假設(shè) batch 內(nèi)某句話長(zhǎng) 80自注意力的分?jǐn)?shù)矩陣是 80x80乘以 batch 大小和注意力頭數(shù)之后內(nèi)存占用非??捎^。我遇到 OOM 之后的第一反應(yīng)不是換顯卡而是先檢查 batch_tokens 是否設(shè)置過(guò)大以及序列是否真的被截?cái)嗟?mask 約束的 80。老手都會(huì)用梯度累積來(lái)繞過(guò)單卡顯存限制也就是每 2 到 4 個(gè)小 batch 更新一次參數(shù)等效地?cái)U(kuò)大 batch size。我把 batch_tokens 從 2500 降到 1000再用梯度累積步數(shù) 2 補(bǔ)回來(lái)顯存占用大幅下降收斂速度幾乎沒(méi)有損失。如果你的顯卡支持混合精度絕大多數(shù)情況下可以無(wú)腦開(kāi)啟在損失不增的前提下把顯存占用再壓掉三分之一。4.2 損失不降或模型不收斂損失完全不下降十有八九是注意力 mask 寫錯(cuò)了。這里有個(gè)特別隱蔽的問(wèn)題padding mask 的值為 0 的位置用負(fù)無(wú)窮填充但在某些實(shí)現(xiàn)里 mask 的值反過(guò)來(lái)1 表示有效0 表示無(wú)效一旦方向?qū)懛淳拖喈?dāng)于全部 token 都被屏蔽模型什么都學(xué)不到。排查的辦法很簡(jiǎn)單把某一個(gè) batch 里 mask 的值打印出來(lái)數(shù)一下有效占比是不是大致符合預(yù)期。如果損失下降但幅度極慢檢查學(xué)習(xí)率是否太小或者位置編碼是否沒(méi)有正確生效。我遇到過(guò)一種情況寫位置編碼時(shí)忘記用 register_buffer導(dǎo)致它被當(dāng)成可訓(xùn)練參數(shù)參與優(yōu)化整體參數(shù)量增加了幾十萬(wàn)但損失反而不降。把位置編碼改為不可訓(xùn)練常量之后訓(xùn)練立刻恢復(fù)正常。4.3 訓(xùn)練 BLEU 高、驗(yàn)證 BLEU 低這種情況就是過(guò)擬合在百萬(wàn)級(jí)別的語(yǔ)料上尤其常見(jiàn)。我的處理順序比較簡(jiǎn)單先加 dropout把每層 dropout 從 0.1 提到 0.2如果仍然過(guò)擬合就減小 d_model 到 256最后實(shí)在不行才降低層數(shù)。很多初學(xué)者的第一反應(yīng)是加更多語(yǔ)料但實(shí)際上先減模型容量往往更有效因?yàn)檎Z(yǔ)料清洗不干凈時(shí)加數(shù)據(jù)只會(huì)把噪聲也灌進(jìn)去。4.4 譯文出現(xiàn)大量重復(fù)詞重復(fù)詞問(wèn)題多半出現(xiàn)在解碼時(shí)。除了前面提過(guò)的重復(fù)懲罰還可以檢查輸出序列是否有“重復(fù)片段”。這個(gè)現(xiàn)象在中文翻譯里特別明顯因?yàn)橹形臒o(wú)明顯詞空格單字“的”很容易被反復(fù)生成。除了調(diào)懲罰項(xiàng)另一個(gè)取巧做法是增加驗(yàn)證集里長(zhǎng)句的比例讓模型多學(xué)會(huì)處理平穩(wěn)的長(zhǎng)距離依賴。4.5 出現(xiàn)大量 UNK子詞詞表仍然覆蓋不到的低頻詞會(huì)成為 UNK。真正有效的解決方法是把語(yǔ)料重新過(guò)一遍 BPE合并閾值調(diào)低一點(diǎn)比如從出現(xiàn)頻率 2 降到出現(xiàn)頻率 1這樣可以保留更多低頻字符合并。另外中英翻譯中數(shù)字和日期非常容易成為 UNK我專門加了一個(gè)預(yù)處理規(guī)則把孤立數(shù)字替換為同一個(gè)保留 token解碼之后再映射回原數(shù)字。這個(gè)小技巧立竿見(jiàn)影比擴(kuò)容詞表省事得多。下面把上述問(wèn)題整理成速查表方便大家直接對(duì)照現(xiàn)象可能原因排查與處理OOMbatch token 數(shù)過(guò)大減 batch_tokens / 梯度累積 / 混合精度損失不降mask 方向錯(cuò)誤 / 位置編碼可訓(xùn)練打印 mask / 用 register_buffer 固定位置編碼驗(yàn)證 BLEU 低過(guò)擬合提高 dropout / 減小模型容量輸出重復(fù)解碼約束不夠加重復(fù)懲罰 / 提高長(zhǎng)度懲罰 alpha大量 UNK詞表覆蓋不足調(diào)節(jié) BPE 合并閾值 / 預(yù)替換數(shù)字符號(hào)5. 一些個(gè)人的實(shí)操心得項(xiàng)目做到最后我發(fā)現(xiàn)比起模型結(jié)構(gòu)工程習(xí)慣對(duì)結(jié)果的影響被很多人低估了。常常有人問(wèn)我“注意力頭數(shù)改成 16 會(huì)不會(huì)更好層數(shù)加到 12 是不是就能超過(guò)論文”我不否認(rèn)調(diào)參能漲點(diǎn)但在我這套中英翻譯任務(wù)里真正把 BLEU 從 20 拉到 25 的改動(dòng)反而是數(shù)據(jù)清洗得更干凈、詞表構(gòu)建得更穩(wěn)、標(biāo)簽平滑系數(shù)調(diào)得更準(zhǔn)。對(duì)我個(gè)人來(lái)說(shuō)還有一個(gè)印象很深的體會(huì)不要在一開(kāi)始就追求在最大數(shù)據(jù)集上訓(xùn)練先用一個(gè) 5 萬(wàn)句的小樣本集把完整流程跑通確認(rèn)代碼沒(méi)有 bug 之后再切換到大語(yǔ)料正式訓(xùn)練。小樣本十分鐘就能出一個(gè)結(jié)果哪怕模型效果差也能快速驗(yàn)證 mask、位置編碼、推理解碼這些關(guān)鍵環(huán)節(jié)都正常。否則直接上百萬(wàn)句數(shù)據(jù)訓(xùn)練三個(gè)小時(shí)發(fā)現(xiàn)損失不降排查成本會(huì)高得讓人想放棄。最后分享一個(gè)小技巧解碼時(shí)把注意力權(quán)重單獨(dú)存下來(lái)選幾個(gè)典型句子畫成熱力圖。你可以直觀看到“I”這個(gè)詞在翻譯“我”的時(shí)候注意力主要落在了源句的哪個(gè)位置??吹綗崃D清晰地呈現(xiàn)出中英文的詞對(duì)齊關(guān)系我那時(shí)候才真正覺(jué)得自己理解了 Transformer 翻譯的精髓。這個(gè)項(xiàng)目跑通之后還可以往兩個(gè)方向繼續(xù)擴(kuò)展一是替換成更大規(guī)模的預(yù)訓(xùn)練語(yǔ)言模型做初始 checkpoint用領(lǐng)域語(yǔ)料微調(diào)二是加入多模態(tài)信息比如圖片描述、語(yǔ)音轉(zhuǎn)錄和文本翻譯聯(lián)合訓(xùn)練。每一步都是新的坑但也都很值得走。