器人源碼解析:從編碼器-解碼器到訓(xùn)練避坑)
簡(jiǎn)介基于Transformer架構(gòu)的聊天機(jī)器人源碼包面向自然語(yǔ)言處理初學(xué)者與Python開(kāi)發(fā)人員輸入日常中文文本后系統(tǒng)即可自動(dòng)生成語(yǔ)義相關(guān)的應(yīng)答可用于智能客服、閑聊陪伴等場(chǎng)景。壓縮包共367個(gè)文件以308個(gè)Python腳本文件為主輔以JSON詞典數(shù)據(jù)、運(yùn)行說(shuō)明、虛擬環(huán)境配置等總計(jì)約25.85兆字節(jié)。資源內(nèi)包含完整模型實(shí)現(xiàn)、已處理好的字典數(shù)據(jù)及分步運(yùn)行說(shuō)明參考百度WebQA、青云、豆瓣等中文對(duì)話數(shù)據(jù)集訓(xùn)練而成下載后直接運(yùn)行主程序即可體驗(yàn)效果若需重新訓(xùn)練可依據(jù)說(shuō)明準(zhǔn)備數(shù)據(jù)并調(diào)整超參數(shù)配置運(yùn)行說(shuō)明還詳細(xì)指出了依賴庫(kù)安裝與參數(shù)文件放置位置能顯著降低環(huán)境搭建難度。目前已有六百一十二人學(xué)習(xí)下載非常適合用來(lái)理解Transformer對(duì)話系統(tǒng)原理、快速搭建原型以及進(jìn)行二次開(kāi)發(fā)。1. 基于Transformer的聊天機(jī)器人源碼先說(shuō)清楚它能不能直接跑Transformer 聊天機(jī)器人源碼在 Python 生態(tài)里不少見(jiàn)但打開(kāi)就能直接用的情況很少。這個(gè)項(xiàng)目是典型的「待裝配」工程源碼用 keras-transformer 實(shí)現(xiàn)了一個(gè)編碼器-解碼器結(jié)構(gòu)的日常對(duì)話模型輸入一句中文返回一句回答適合做課程設(shè)計(jì)、NLP 入門基線或者拿自己語(yǔ)料重新訓(xùn)練的實(shí)驗(yàn)臺(tái)。動(dòng)手前必須把一件事說(shuō)清楚zip 里只有源碼、運(yùn)行說(shuō)明和處理好的詞表ListData預(yù)訓(xùn)練權(quán)重和原始數(shù)據(jù)集并沒(méi)有打包進(jìn)去。直接運(yùn)行前得先確認(rèn) ModelTrainedParameters 目錄下有模型文件沒(méi)有就要按第四章自己訓(xùn)。下面按拆項(xiàng)目、搭環(huán)境、跑通、訓(xùn)練、避坑、驗(yàn)證的順序走命令和參數(shù)都給可以直接抄的版本。2. 項(xiàng)目拆解Transformer 編碼器-解碼器、四份數(shù)據(jù)集與推理鏈路2.1 keras-transformer 在項(xiàng)目里承擔(dān)的角色v1.0 版本明確寫的是基于 keras-transformer 包也就是說(shuō)模型不是從零手寫的而是拿現(xiàn)成的 TransformerEncoder 和 TransformerDecoder 拼出來(lái)的。這個(gè)選擇很務(wù)實(shí)聊天機(jī)器人在項(xiàng)目里被建模成一個(gè) sequence-to-sequence 任務(wù)用戶的輸入是源序列模型的回答是目標(biāo)序列。編碼器讀完整句話解碼器一個(gè)詞一個(gè)詞地往外生成——這正是 Transformer 架構(gòu)里最核心的編碼器-解碼器設(shè)計(jì)圖解 Transformer、transformer 模型詳解這類內(nèi)容反復(fù)強(qiáng)調(diào)的就是這一點(diǎn)。它和 BERT 那類純編碼器分類模型最大的區(qū)別在輸出端分類模型吐一個(gè)標(biāo)簽這里要吐一段長(zhǎng)度不定的文本所以解碼器必須有逐 token 生成的能力。理解這一點(diǎn)之后回去看 keras-transformer 的常見(jiàn)用法就順了from keras_transformer import get_model model get_model( token_dicttoken_dict, # 詞表token - id max_seq_len64, # 輸入輸出最大序列長(zhǎng)度 embed_dim128, # embedding 維度 head_num8, # 多頭注意力頭數(shù) hidden_dim512, # 前饋網(wǎng)絡(luò)隱藏層維度 dropout_rate0.1, # 丟棄率 )這組參數(shù)基本就是 HyperParameters.py 里會(huì)出現(xiàn)的關(guān)鍵項(xiàng)。head_num 一般要能整除 embed_dim否則多頭拆分時(shí)維度對(duì)不齊訓(xùn)練時(shí) loss 會(huì)異常max_seq_len 決定顯存占用閑聊場(chǎng)景 32 到 64 夠用設(shè)到 128 以上訓(xùn)練速度會(huì)明顯變慢。get_model 返回的是一個(gè)按 Transformer 論文搭好的模型訓(xùn)練用 fit推理配合 decode 函數(shù)做貪心解碼這兩個(gè)環(huán)節(jié)會(huì)在后面分別展開(kāi)。2.2 四份數(shù)據(jù)集的分工WebQA、青云、豆瓣、chatterbot系統(tǒng)說(shuō)明里列了四份數(shù)據(jù)百度中文問(wèn)答 WebQA、青云數(shù)據(jù)集、豆瓣數(shù)據(jù)集、chatterbot 數(shù)據(jù)集。為什么要混著用有講究。WebQA 偏知識(shí)型問(wèn)答能教模型給出帶信息量的回答青云數(shù)據(jù)集是中文閑聊語(yǔ)料日常寒暄的主要來(lái)源豆瓣數(shù)據(jù)集里有大量多輪對(duì)話雖然 v1.0 模型本身不帶記憶但對(duì)話風(fēng)格的多樣性靠它撐chatterbot 是英文為主的老牌聊天語(yǔ)料用來(lái)補(bǔ)足模板類回答。四份數(shù)據(jù)加起來(lái)非常大所以項(xiàng)目沒(méi)有打包上傳需要的人去 issue 里要。這一點(diǎn)要有心理預(yù)期zip 里的 DataSet 目錄是空的屬于正常情況。直接運(yùn)行不依賴原始數(shù)據(jù)因?yàn)?ListData 目錄下已經(jīng)有處理好的 token 字典和映射關(guān)系Main.py 加載的是預(yù)處理產(chǎn)物不是原始語(yǔ)料。自己訓(xùn)練時(shí)四份數(shù)據(jù)不能一股腦全塞進(jìn)去各數(shù)據(jù)集格式和噪聲比例不一樣最好先按來(lái)源分文件存放再統(tǒng)一轉(zhuǎn)成問(wèn)答對(duì)格式第四章會(huì)給具體腳本。2.3 推理鏈路從 Main.py 輸入到回答輸出的完整流程把推理鏈路在腦子里過(guò)一遍比盲目跑腳本有用得多。整個(gè)流程可以拆成五步Main.py 讀取 HyperParameters.py 拿到模型配置從 ListData 加載 token_dict用 get_model 重建模型結(jié)構(gòu)從 ModelTrainedParameters 加載權(quán)重用戶輸入文本經(jīng)過(guò)分詞、轉(zhuǎn) id、padding 后送入編碼器解碼器逐個(gè) token 生成最后拼成回答。對(duì)應(yīng)到代碼骨架是這個(gè)樣子# main.py 推理核心keras-transformer 的典型調(diào)用方式 from keras_transformer import get_model, decode def chat(user_text): # 1. 文本轉(zhuǎn) id 序列補(bǔ) padding 到 max_seq_len src_ids [token_dict.get(c, token_dict[UNK]) for c in user_text][:max_seq_len] src_ids pad_sequence(src_ids, max_seq_len) # 2. 貪心解碼每步取概率最高的 token 作為下一步輸入 answer_ids decode( model, src_ids, start_tokentoken_dict[START], end_tokentoken_dict[END], max_lenmax_seq_len, ) # 3. id 序列還原成中文過(guò)濾特殊標(biāo)記 return .join(id2token[i] for i in answer_ids if i not in (token_dict[START], token_dict[END], token_dict[PAD]))decode 是 keras-transformer 自帶的貪心搜索實(shí)現(xiàn)默認(rèn)每一步取 softmax 概率最高的 token。start_token 和 end_token 是訓(xùn)練時(shí)在回答首尾插入的特殊標(biāo)記一個(gè)決定生成從哪里開(kāi)始一個(gè)決定在哪里停。padding 的部分在還原文本時(shí)必須過(guò)濾掉否則回答后面會(huì)跟一串奇怪字符這是最容易被忽略的還原錯(cuò)誤。這里也順帶把模型邊界說(shuō)清楚它的輸入只包含當(dāng)前這一句話沒(méi)有把歷史對(duì)話拼進(jìn)去本質(zhì)是單輪聊天不是帶記憶的多輪 dialogue system。想要多輪效果得在喂給模型之前自己把歷史文本拼進(jìn)輸入。這個(gè)擴(kuò)展點(diǎn)會(huì)在第六章的驗(yàn)證環(huán)節(jié)再提到。3. 環(huán)境搭建與直接運(yùn)行Python 3.6、依賴鎖定與參數(shù)文件擺放3.1 解壓后的目錄結(jié)構(gòu)哪些是環(huán)境殘留哪些是項(xiàng)目本體解壓 zip 之后第一眼會(huì)被一堆看著像環(huán)境文件的內(nèi)容干擾activate、activate.csh、easy_install-3.6、pip3.6、pyvenv.cfg、setuptools-40.8.0-py3.6.egg還有 t64.exe。這些都是作者打包時(shí)把自己本機(jī)的 Python 3.6 虛擬環(huán)境一起塞進(jìn)來(lái)的痕跡。pyvenv.cfg 是 venv 的標(biāo)志文件activate 系列是環(huán)境激活腳本pip3.6 和 easy_install-3.6 是環(huán)境內(nèi)嵌的安裝工具。對(duì)使用者來(lái)說(shuō)這批文件基本沒(méi)用——它們綁定作者當(dāng)時(shí)的絕對(duì)路徑換一臺(tái)機(jī)器路徑就失效尤其是從 Windows 環(huán)境拷貝到 Linux 上時(shí)activate 腳本里的路徑直接就是錯(cuò)的。正確做法是忽略它們自己建一個(gè)干凈的 Python 3.6 環(huán)境。項(xiàng)目本體只有幾個(gè)文件Main.py入口、HyperParameters.py配置、ModelTrainedParameters 目錄權(quán)重、ListData 目錄詞表、DataSet 目錄原始語(yǔ)料。直接運(yùn)行前把目錄職責(zé)認(rèn)清楚目錄/文件職責(zé)直接運(yùn)行時(shí)要不要?jiǎng)覯ain.py程序入口加載模型并啟動(dòng)命令行對(duì)話否HyperParameters.py模型和訓(xùn)練的超參數(shù)否訓(xùn)練時(shí)再改ModelTrainedParameters/存放訓(xùn)練好的權(quán)重文件必須有權(quán)重才能跑ListData/已處理好的 token 字典、id 映射否DataSet/原始數(shù)據(jù)集只有自行訓(xùn)練才需要判斷文件歸屬有個(gè)笨辦法凡是文件名帶版本號(hào)或者帶 activate 的一律不碰真正要跑的是 Main.py 和 HyperParameters.py要放數(shù)據(jù)的是 DataSet要看結(jié)果的是 ModelTrainedParameters。3.2 創(chuàng)建虛擬環(huán)境并安裝依賴TensorFlow 版本是第一個(gè)坑keras-transformer 對(duì) TensorFlow 版本非常敏感屬于「裝得上但跑不動(dòng)」的高發(fā)區(qū)。這個(gè)包是幾年前的實(shí)現(xiàn)底層調(diào)用 tf.keras 的舊接口而 TensorFlow 2.16 之后把 Keras 3 作為默認(rèn)實(shí)現(xiàn)大量舊的 custom object 注冊(cè)機(jī)制失效模型構(gòu)建階段就會(huì)報(bào)錯(cuò)。實(shí)際踩下來(lái)的經(jīng)驗(yàn)是直接用 2.15.0這是內(nèi)置 Keras 2 的最后一個(gè)穩(wěn)定大版本與 keras-transformer 兼容性最好。# 創(chuàng)建 Python 3.6 虛擬環(huán)境 python3.6 -m venv chatbot_env # 激活環(huán)境 # Windows: chatbot_env\Scripts\activate # Linux/macOS: source chatbot_env/bin/activate # 安裝依賴先鎖 TensorFlow 版本再裝 keras-transformer pip install --upgrade pip pip install tensorflow2.15.0 pip install keras-transformer安裝順序上有講究先裝 TensorFlow 再裝 keras-transformer后者在安裝時(shí)要探測(cè)前者版本決定走哪套兼容邏輯。反過(guò)來(lái)裝偶爾會(huì)出現(xiàn)包裝上了但 import 時(shí)找不到 tf.keras 的情況。裝完用一條命令驗(yàn)證環(huán)境python -c from keras_transformer import get_model; print(ok)不報(bào)錯(cuò)說(shuō)明環(huán)境沒(méi)問(wèn)題。如果這一句報(bào)錯(cuò)先回看 TensorFlow 版本大概率是版本不匹配別急著懷疑代碼。3.3 運(yùn)行 Main.py第一次跑通要做的三個(gè)檢查環(huán)境就緒后運(yùn)行前做三個(gè)檢查能省掉大量折騰。第一ModelTrainedParameters 目錄下必須有權(quán)重文件沒(méi)有的話 Main.py 會(huì)在模型加載那行直接拋異常第二HyperParameters.py 里的 max_seq_len、head_num 等值保持原樣它們是和權(quán)重配套的改了會(huì)導(dǎo)致加載失敗第三ListData 目錄保留原樣詞表是預(yù)處理好的刪掉或改動(dòng)會(huì)直接導(dǎo)致推理時(shí) id 映射錯(cuò)亂。# 直接運(yùn)行 python main.py正常情況會(huì)進(jìn)入命令行交互循環(huán)顯示類似「你」的提示符輸入一句話回車模型返回一句中文回答輸入 exit 或 quit 退出。第一次跑通的關(guān)鍵判斷標(biāo)準(zhǔn)不是回答質(zhì)量而是有沒(méi)有報(bào)錯(cuò)——只要模型能對(duì)任意一句話給出一個(gè)像樣的回答鏈路就是通的?;卮饍?nèi)容比較呆很正常閑聊模型本身隨機(jī)性大而且預(yù)訓(xùn)練權(quán)重在不同機(jī)器上加載的效果可能有細(xì)微差異。注意如果缺權(quán)重文件要么去 issue 找作者要要么走第四章的自行訓(xùn)練。我建議教學(xué)演示先要現(xiàn)成權(quán)重把鏈路跑通再訓(xùn)練自己的模型——先有能跑的基線后面所有改動(dòng)才有對(duì)照。4. 自行訓(xùn)練把四份語(yǔ)料換成你自己的問(wèn)答數(shù)據(jù)4.1 語(yǔ)料格式與預(yù)處理問(wèn)答對(duì)到底長(zhǎng)什么樣自己訓(xùn)練時(shí)DataSet 目錄下放原始語(yǔ)料。項(xiàng)目沒(méi)有規(guī)定原始格式但按 keras-transformer 的常規(guī)預(yù)處理最省事的格式是每行一個(gè)問(wèn)答對(duì)問(wèn)題和回答用 Tab 分隔。選 Tab 而不是空格是因?yàn)橹形睦锟崭癫怀R?jiàn)但標(biāo)點(diǎn)半角全角混雜時(shí)按空格切分容易多出空段Tab 切分要穩(wěn)得多。# data_prepare.py —— 讀取問(wèn)答對(duì)并構(gòu)建詞表 def load_pairs(data_path): pairs [] with open(data_path, encodingutf-8) as f: for line in f: line line.strip() if \t not in line: continue # 跳過(guò)空行和格式錯(cuò)誤行 q, a line.split(\t, 1) if q and a: pairs.append((q, a)) return pairs def build_token_dict(pairs): token_set {PAD, START, END, UNK} for q, a in pairs: for c in q a: token_set.add(c) return {t: i for i, t in enumerate(token_set)}load_pairs 用 split(\t, 1) 只切第一刀這樣回答里即使含 Tab 以外的任何字符都不會(huì)破壞問(wèn)答對(duì)結(jié)構(gòu)。build_token_dict 按字切分是中文聊天場(chǎng)景最穩(wěn)的選擇詞表大小通常幾千到幾萬(wàn)訓(xùn)練速度快還不用引入外部分詞器。如果堅(jiān)持按詞切分詞表會(huì)膨脹到幾十萬(wàn)訓(xùn)練顯存和時(shí)間成倍上漲而且分詞錯(cuò)誤會(huì)直接污染生成結(jié)果。四種數(shù)據(jù)集混用時(shí)最好每個(gè)來(lái)源一個(gè)文件分別 load再合并 pairs 列表方便統(tǒng)計(jì)各集分布。4.2 HyperParameters.py 里的關(guān)鍵參數(shù)先懂調(diào)參邏輯再動(dòng)手訓(xùn)練前把超參數(shù)表過(guò)一遍比直接跑訓(xùn)練有意義得多。以下是這個(gè)項(xiàng)目最核心的參數(shù)和調(diào)節(jié)依據(jù)參數(shù)含義閑聊場(chǎng)景建議值調(diào)參依據(jù)max_seq_len序列最大長(zhǎng)度3264閑聊句子短太長(zhǎng)只加顯存embed_dim詞向量維度128256按字詞表小時(shí) 128 夠用head_num注意力頭數(shù)8必須整除 embed_dimhidden_dimFFN 隱藏層維度512embed_dim 的 24 倍dropout_rate隨機(jī)丟棄率0.10.2語(yǔ)料少就調(diào)大防過(guò)擬合batch_size批大小3264顯存不足先減半最容易被忽略的是 head_num 和 embed_dim 的整除關(guān)系。整除不了時(shí)代碼不報(bào)錯(cuò)但數(shù)學(xué)上多頭拆分已經(jīng)錯(cuò)了癥狀是 loss 高且不下降。hidden_dim 和 embed_dim 的比例直接決定參數(shù)量閑聊任務(wù)不需要堆太大512 配 128 已經(jīng)能出效果。batch_size 是顯存不夠時(shí)最先要?jiǎng)拥牡胤綔p半之后 loss 曲線會(huì)更抖但不會(huì)影響最終收斂。4.3 從原始語(yǔ)料到權(quán)重文件完整的訓(xùn)練流程訓(xùn)練流程分三步準(zhǔn)備數(shù)據(jù)、定義模型、訓(xùn)練并保存權(quán)重。核心代碼長(zhǎng)這樣# train.py —— 訓(xùn)練主流程結(jié)合 4.1 的 load_pairs / build_token_dict from keras_transformer import get_model import numpy as np # 1. 構(gòu)造訓(xùn)練數(shù)據(jù) pairs load_pairs(DataSet/train.tsv) token_dict build_token_dict(pairs) id2token {i: t for t, i in token_dict.items()} def encode_pair(q, a, max_len): # source問(wèn)題原文target回答加 STARTlabel回答加 END src [token_dict.get(c, token_dict[UNK]) for c in q][:max_len] tgt [token_dict[START]] \ [token_dict.get(c, token_dict[UNK]) for c in a][:max_len - 2] tgt_out tgt[1:] [token_dict[END]] # pad() 為輔助函數(shù)不足 max_len 的部分補(bǔ) token_dict[PAD] return pad(src, max_len), pad(tgt, max_len), pad(tgt_out, max_len) X_src, X_tgt, Y [], [], [] for q, a in pairs: s, t, o encode_pair(q, a, max_seq_len) X_src.append(s); X_tgt.append(t); Y.append(o) # 2. 構(gòu)建并訓(xùn)練 model get_model(token_dicttoken_dict, max_seq_lenmax_seq_len, embed_dim128, head_num8, hidden_dim512, dropout_rate0.1) model.compile(optimizeradam, losssparse_categorical_crossentropy) model.fit([np.array(X_src), np.array(X_tgt)], np.expand_dims(np.array(Y), -1), batch_size32, epochs30) # 3. 保存權(quán)重到約定路徑 model.save_weights(ModelTrainedParameters/model.h5)這里最需要理解的是 target 的移位邏輯解碼器輸入是「回答前面加 」監(jiān)督標(biāo)簽是「回答后面加 」。這就是 teacher forcing——每一步都拿上一步的標(biāo)準(zhǔn)答案而不是自己的預(yù)測(cè)當(dāng)輸入訓(xùn)練才穩(wěn)定是 seq2seq 聊天模型的標(biāo)配做法。Y 的 expand_dims 加上了 token 維因?yàn)?sparse_categorical_crossentropy 期望標(biāo)簽是整型索引而不是 one-hot少這一步 loss 直接算不出來(lái)。訓(xùn)練時(shí)間要做好心理準(zhǔn)備WebQA 加青云這類語(yǔ)料動(dòng)輒幾十萬(wàn)條CPU 上跑一個(gè) epoch 可能就要半小時(shí)以上30 個(gè) epoch 就是十幾個(gè)小時(shí)。我的習(xí)慣是先抽樣 1 萬(wàn)條跑 3 個(gè) epoch 驗(yàn)證流程確認(rèn) loss 在下降后再用全量跑正式訓(xùn)練。驗(yàn)證方法第六章給出量化版本。5. 避坑指南Transformer 聊天機(jī)器人最常見(jiàn)的五處翻車點(diǎn)5.1 現(xiàn)象python main.py 直接拋 FileNotFoundError提示找不到權(quán)重文件原因預(yù)訓(xùn)練參數(shù)沒(méi)有打包在 zip 里ModelTrainedParameters 目錄是空的Main.py 在加載權(quán)重時(shí)找不到 model.h5。壓縮包里那幾個(gè)帶著版本號(hào)的文件會(huì)讓人誤以為環(huán)境齊全但權(quán)重屬于另一回事解壓后第一件事就是看這個(gè)目錄里有沒(méi)有 .h5 文件。解決先確認(rèn) zip 里是否包含權(quán)重文件沒(méi)有就按第四章用自己的語(yǔ)料訓(xùn)練一份或者去項(xiàng)目 issue 里向作者要。在拿到權(quán)重之前任何改代碼的行為都沒(méi)有意義——鏈路根本沒(méi)建立起來(lái)先解決文件缺失再談其他。順手做一個(gè)最小驗(yàn)證ls -l ModelTrainedParameters/看到 .h5 再往下走。5.2 現(xiàn)象權(quán)重能加載但回答全是亂碼或 刷屏原因推理用的 token_dict 和訓(xùn)練時(shí)的詞表不是同一份或者你在直接運(yùn)行階段動(dòng)了 HyperParameters.py 里的 max_seq_len導(dǎo)致序列截?cái)辔恢煤陀?xùn)練時(shí)錯(cuò)位生成的 id 映射到錯(cuò)誤的字。ListData 里的字典是作者訓(xùn)練時(shí)沉淀下來(lái)的和訓(xùn)練權(quán)重嚴(yán)格配套任何一方被替換都會(huì)出現(xiàn)這種癥狀。解決直接運(yùn)行階段不要?jiǎng)?HyperParameters.py 和 ListData 的任何內(nèi)容。自己訓(xùn)練時(shí)訓(xùn)練腳本和推理腳本必須共用同一個(gè) build_token_dict 生成的詞表文件不要各生成一份。判斷依據(jù)很簡(jiǎn)單回答里 占比超過(guò)三成先查詞表對(duì)齊??梢栽趯?dǎo)入后打印len(token_dict)與訓(xùn)練日志里記錄的詞表大小對(duì)比不一致就是源頭。5.3 現(xiàn)象pip install keras-transformer 成功但 import 時(shí)報(bào) module 相關(guān)錯(cuò)誤原因TensorFlow 2.16 之后的版本默認(rèn)使用 Keras 3keras-transformer 依賴的舊式 tf.keras 接口被移除import 階段或模型構(gòu)建階段崩潰。報(bào)錯(cuò)信息里通常帶 keras 或 tf.keras 字樣很多人誤以為是項(xiàng)目代碼寫錯(cuò)了其實(shí)環(huán)境層面就已經(jīng)不對(duì)。解決把 TensorFlow 鎖到 2.15.0內(nèi)置 Keras 2.x。如果因?yàn)轱@卡驅(qū)動(dòng)等原因只能用新版 TensorFlow就得改用 keras 2 兼容層改動(dòng)量大新手不建議碰。裝完先執(zhí)行python -c from keras_transformer import get_model; print(ok)這條過(guò)了再往下走。每次重建環(huán)境都把這條命令寫進(jìn) README能省掉許多來(lái)回。5.4 現(xiàn)象loss 一直在 5 以上徘徊訓(xùn)練好幾個(gè) epoch 都不下降原因最常見(jiàn)是詞表或數(shù)據(jù)沒(méi)對(duì)齊其次是 head_num 不能整除 embed_dim 導(dǎo)致維度拆分出錯(cuò)還有一個(gè)隱蔽原因是學(xué)習(xí)率太大adam 在訓(xùn)練初期震蕩不收斂。這些問(wèn)題的共性是 loss 曲線不是「降得慢」而是「幾乎不動(dòng)」。解決先抽樣 1 萬(wàn)條跑 3 個(gè) epoch 看 loss 曲線正常應(yīng)逐 epoch 下降再檢查 token_dict 里 / 是否覆蓋全部回答最后把學(xué)習(xí)率調(diào)到 1e-4 到 3e-4 區(qū)間。訓(xùn)練時(shí)每 500 步打印一次 loss觀察前三個(gè) epoch 的下降斜率下降幅度小于 5% 就該停下來(lái)查問(wèn)題而不是繼續(xù)掛機(jī)。5.5 現(xiàn)象回答永遠(yuǎn)是同一句話或者一直復(fù)讀「嗯」「好的」這類詞原因mode collapse也叫注意力坍縮。常見(jiàn)觸發(fā)點(diǎn)是語(yǔ)料里高頻回答占比太大——chatterbot 語(yǔ)料里大量寒暄模板模型學(xué)到的是無(wú)腦復(fù)讀高頻句另一個(gè)觸發(fā)點(diǎn)是 max_seq_len 太短生成階段很快撞到 end_token 導(dǎo)致輸出退化。解決先統(tǒng)計(jì)回答長(zhǎng)度分布和最高頻回答占比把頻率過(guò)高的模板句做去重或降采樣生成階段如果復(fù)讀調(diào)高 end_token 的生成優(yōu)先級(jí)讓模型更早結(jié)束而不是硬撐到 padding 區(qū)。統(tǒng)計(jì)腳本就幾行collections.Counter([a for _, a in pairs]).most_common(20)高頻樣本占比超過(guò) 5% 就先清洗再訓(xùn)練。第六章的重復(fù)率指標(biāo)就是這里延伸出來(lái)的量化方法。6. 進(jìn)階驗(yàn)證用 loss、重復(fù)率和多輪拼接判斷模型學(xué)沒(méi)學(xué)好拿到訓(xùn)練好的模型不能只看「能聊幾句」就收工。我每次訓(xùn)練完對(duì)話模型都會(huì)強(qiáng)制自己過(guò)三關(guān)數(shù)值關(guān)、統(tǒng)計(jì)關(guān)、場(chǎng)景關(guān)。數(shù)值關(guān)看驗(yàn)證集 loss。在留出的驗(yàn)證數(shù)據(jù)上做 model.evaluate得到平均負(fù)對(duì)數(shù)似然取自然指數(shù)就是困惑度。聊天場(chǎng)景按字建模loss 低于 2.0 基本能正常對(duì)話2.5 以上經(jīng)常答非所問(wèn)3.0 以上基本是復(fù)讀機(jī)。這個(gè)閾值可以一句話記loss 下不了 2別急著交付。# evaluate.py —— 驗(yàn)證集 loss 與回答重復(fù)率 import numpy as np def check_model(model, val_pairs, decode_fn): # 數(shù)值關(guān)驗(yàn)證集 loss 與困惑度 loss model.evaluate(val_X, val_Y, verbose0) print(fval loss: {loss:.3f} | perplexity: {np.exp(loss):.2f}) # 統(tǒng)計(jì)關(guān)固定 50 個(gè)問(wèn)題看回答多樣性 answers [decode_fn(q) for q, _ in val_pairs[:50]] repeat_ratio 1 - len(set(answers)) / len(answers) print(frepeat ratio: {repeat_ratio:.2f}) # 低于 0.3 才算健康統(tǒng)計(jì)關(guān)看重復(fù)率。固定 50 個(gè)測(cè)試問(wèn)題統(tǒng)計(jì)不同回答的數(shù)量占比重復(fù)率超過(guò) 30% 基本就是注意力坍縮沒(méi)治好。場(chǎng)景關(guān)則要看多輪拼接——前面說(shuō)過(guò)模型本身是單輪的但我習(xí)慣把用戶上一句和當(dāng)前句用「前一輪 當(dāng)前輪」拼成一個(gè)輸入去測(cè)試雖然不在原設(shè)計(jì)內(nèi)但能檢驗(yàn)?zāi)P蛯?duì)拼接輸入的魯棒性順便驗(yàn)證數(shù)據(jù)預(yù)處理是否支持這個(gè)擴(kuò)展。如果拼接后回答明顯差于單輪說(shuō)明模型對(duì)長(zhǎng)輸入敏感那就要檢查 max_seq_len 是否截?cái)嗟锰?。三關(guān)跑完模型能不能用就是有數(shù)據(jù)支撐的判斷而不是玄學(xué)感受。從那以后我每次拿到這類聊天機(jī)器人源碼工程第一件事都是核對(duì)權(quán)重路徑和詞表對(duì)齊情況再?zèng)Q定是直接跑還是重新訓(xùn)——這個(gè)習(xí)慣幫我避開(kāi)了至少三次白折騰。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取