據(jù)集深度解析:從預(yù)處理到語言模型訓(xùn)練與PPL評(píng)估)
簡(jiǎn)介PTBPenn Treebank Dataset文本數(shù)據(jù)集是自然語言處理與深度學(xué)習(xí)領(lǐng)域的經(jīng)典語料主要包含摘自《華爾街日?qǐng)?bào)》的約100萬單詞包含訓(xùn)練、驗(yàn)證與測(cè)試三部分標(biāo)準(zhǔn)劃分常用于詞嵌入、語言模型及序列模型訓(xùn)練。資源包為RAR壓縮格式共62個(gè)文件以shell腳本、readme說明、txt語料和C/C源碼為主整體僅31.2MB其中18個(gè)sh腳本覆蓋訓(xùn)練與測(cè)試流程11個(gè)readme便于查閱說明txt文件提供已劃分的語料數(shù)據(jù)C/C代碼則為參考實(shí)現(xiàn)。壓縮包內(nèi)含simple-examples基礎(chǔ)示例、rnnlm-0.2b經(jīng)典實(shí)現(xiàn)以及字符級(jí)語言模型、動(dòng)態(tài)評(píng)估、2-nbest-rescore等擴(kuò)展模塊目錄結(jié)構(gòu)清晰讀者可對(duì)照示例快速掌握PTB的預(yù)處理、詞嵌入和語言模型訓(xùn)練評(píng)估流程還可借助模型文件了解訓(xùn)練結(jié)果。目前已有1825人學(xué)習(xí)下載是開展NLP實(shí)驗(yàn)和模型對(duì)比的高性價(jià)比資源適合剛接觸語言模型與詞嵌入的深度學(xué)習(xí)初學(xué)者。 最近我在調(diào)一版語言模型的數(shù)據(jù)管線翻來翻去又把PTB(Penn Treebank Dataset拿出來做基準(zhǔn)測(cè)試。說實(shí)話這個(gè)1989年誕生、上世紀(jì)九十年代開始廣泛傳播的老數(shù)據(jù)集放在今天的大模型時(shí)代看規(guī)模小得可憐卻依然在自然語言處理教科書、經(jīng)典論文和各類開源項(xiàng)目中占據(jù)固定席位。原因很簡(jiǎn)單它干凈、可控、自帶標(biāo)準(zhǔn)劃分是驗(yàn)證模型思路的絕佳“試驗(yàn)田”。這篇博文我想把PTB里那些容易被忽略的細(xì)節(jié)系統(tǒng)撈一遍從數(shù)據(jù)集的真實(shí)構(gòu)成、目錄結(jié)構(gòu)到預(yù)處理管線、詞表構(gòu)建再到批量化訓(xùn)練和困惑度評(píng)估的完整實(shí)操最后聊幾個(gè)我實(shí)際踩過的坑。不管你是剛?cè)肟覰LP、想復(fù)現(xiàn)經(jīng)典論文還是打算在輕量級(jí)語料上快速驗(yàn)證想法這篇內(nèi)容都能直接拿來回用。1. PTB到底是什么1.1 一句線話介紹PTBPTB全稱Penn Treebank Dataset原項(xiàng)目本質(zhì)是喬姆斯基范式下的樹庫語料庫由美國賓夕法尼亞大學(xué)計(jì)算語言學(xué)系牽頭構(gòu)建團(tuán)隊(duì)成員包括Mitchell Marcus、Beatrice Santorini、Mary Ann Marcinkiewicz等人。最初的標(biāo)注對(duì)象是華爾街日?qǐng)?bào)的英語文章經(jīng)過詞性標(biāo)注和句法樹標(biāo)注后形成了著名的Penn Treebank項(xiàng)目。而NLP社區(qū)常說的“PTB文本數(shù)據(jù)集”通常指的是從完整樹庫中抽取出的純文本句子集合附帶標(biāo)準(zhǔn)的訓(xùn)練集、驗(yàn)證集、測(cè)試集切分。這套文本數(shù)據(jù)在Mikolov等人2010年前后一系列RNN語言模型論文中被用成了事實(shí)標(biāo)準(zhǔn)后來無論是LSTM、GRU還是Transformer早期工作大家?guī)缀醵加肞TB作為語言模型任務(wù)的默認(rèn)測(cè)試集。所以今天你看到某篇論文里寫著“數(shù)據(jù)集PTB”基本就是指這個(gè)版本。1.2 為什么PTB能活到今天有人會(huì)問都大模型時(shí)代了這老數(shù)據(jù)集還有什么價(jià)值我個(gè)人的看法是PTB的價(jià)值并不在于“大”而在于“標(biāo)準(zhǔn)”和“可控”。標(biāo)準(zhǔn)體現(xiàn)在劃分固定。訓(xùn)練集、驗(yàn)證集、測(cè)試集大家都用同一份文件實(shí)驗(yàn)結(jié)果可以直接跨論文比較這在今天的數(shù)據(jù)集生態(tài)里反而成了稀缺品??煽伢w現(xiàn)在詞表規(guī)模和語料規(guī)模都很小模型可以在普通筆記本電腦上幾分鐘內(nèi)跑完一個(gè)完整的訓(xùn)練過程特別適合驗(yàn)證思路、跑消融實(shí)驗(yàn)、做教學(xué)演示。當(dāng)然PTB的缺點(diǎn)也很明顯領(lǐng)域單一全部來自華爾街日?qǐng)?bào)、詞匯量小、句式相對(duì)正式、存在明顯的數(shù)據(jù)偏置。所以它不適合作為通用語言模型的終級(jí)評(píng)測(cè)更適合作為算法驗(yàn)證和基線對(duì)比的工具。這也提醒我們使用數(shù)據(jù)集時(shí)一定要清楚它的邊界不能拿著PTB的結(jié)果去推斷模型在新聞、對(duì)話、社交媒體等場(chǎng)景下的表現(xiàn)。2. 數(shù)據(jù)集內(nèi)部結(jié)構(gòu)與標(biāo)準(zhǔn)切分2.1 文件劃分與詞數(shù)規(guī)模PTB常見版本訓(xùn)練集約4.2萬句驗(yàn)證集約3370句測(cè)試集約3761句總詞數(shù)含句首尾標(biāo)記約100萬量級(jí)。不同來源的版本在切分細(xì)節(jié)上有一點(diǎn)點(diǎn)差異比如LDC官方版與Mikolov重新整理的版本但大框架一致。日常使用中我們拿到的基本是三個(gè)文件ptb.train.txt、ptb.valid.txt、ptb.test.txt。文件句子數(shù)約詞數(shù)用途ptb.train.txt42068929k訓(xùn)練語言模型參數(shù)ptb.valid.txt337073k驗(yàn)證調(diào)參、早停ptb.test.txt376182k最終評(píng)估泛化能力注意這里的“約詞數(shù)”沒有嚴(yán)格統(tǒng)一因?yàn)橛?jì)數(shù)時(shí)是否包含s、/s會(huì)改變數(shù)字不同論文里也會(huì)有細(xì)微差異。所以實(shí)驗(yàn)記錄里一定要注明自己用的是哪個(gè)版本和統(tǒng)計(jì)口徑。2.2 是否需要自己切分很多人第一次用PTB會(huì)困惑三個(gè)文件已經(jīng)給好了為什么還要提“標(biāo)準(zhǔn)切分”原因是樹庫原始語料中句子是有順序的如果不按公認(rèn)切分而自己隨機(jī)洗牌會(huì)導(dǎo)致訓(xùn)練集、驗(yàn)證集、測(cè)試集分布偏移結(jié)果無法與歷史論文比較。所以無論你用框架內(nèi)置接口還是自己寫讀取腳本第一原則就是永遠(yuǎn)使用自帶劃分不要自行打亂重新分配。2.3 句子標(biāo)記與詞表細(xì)節(jié)PTB中每句話以s開頭以/s結(jié)尾這是語言模型訓(xùn)練時(shí)的重要標(biāo)記。在詞表構(gòu)建階段這兩個(gè)標(biāo)記都要作為獨(dú)立詞條保留因?yàn)樗鼈儏⑴c了每個(gè)句子的概率計(jì)算相當(dāng)于模型學(xué)到了“一句話從哪里開始到哪里結(jié)束”的信號(hào)。詞表規(guī)模通??刂圃?0000詞左右。具體做法是統(tǒng)計(jì)訓(xùn)練集中所有詞的出現(xiàn)次數(shù)保留高頻詞低頻詞統(tǒng)一映射為unk。Mikolov那版的做法是把出現(xiàn)次數(shù)不足3次的詞替換掉作為未登錄詞處理。這一步驟直接影響了模型對(duì)未知詞的處理方式也是后面容易出問題的地方。3. 數(shù)據(jù)預(yù)處理與詞表構(gòu)建實(shí)操3.1 從原始文本到id序列我習(xí)慣把PTB的預(yù)處理分成四步讀入、清洗、建詞表、轉(zhuǎn)id。讀入環(huán)節(jié)只要按行讀取文本文件即可每一行是一句已經(jīng)標(biāo)注好的句子。清洗環(huán)節(jié)PTB相對(duì)簡(jiǎn)單因?yàn)槿A爾街日?qǐng)?bào)文本已經(jīng)做過基本歸一化不需要去HTML標(biāo)簽或者處理emoji。需要保留的就是s、/s、unk三個(gè)特殊token以及文件中可能出現(xiàn)的N數(shù)字歸一化標(biāo)記。下面是我在項(xiàng)目里經(jīng)常使用的初始化代碼兼容PyTorch生態(tài)import torch from torch.nn.utils.rnn import pad_sequence from collections import Counter PAD_TOKEN pad UNK_TOKEN unk SOS_TOKEN s EOS_TOKEN /s def read_ptb_file(filepath): sentences [] with open(filepath, r, encodingutf-8) as f: for line in f: line line.strip() if line: sentences.append(line.split()) return sentences def build_vocab(sentences, max_size10000, min_freq3): counter Counter() for sent in sentences: counter.update(sent) vocab_list [token for token, cnt in counter.most_common(max_size - 4) if cnt min_freq] vocab_list [PAD_TOKEN, UNK_TOKEN, SOS_TOKEN, EOS_TOKEN] vocab_list word2idx {w: i for i, w in enumerate(vocab_list)} idx2word {i: w for w, i in word2idx.items()} return word2idx, idx2word這里max_size - 4是因?yàn)轭A(yù)留了4個(gè)特殊token的位置包括pad、unk、s、/s。實(shí)際詞表大小可能在10000左右浮動(dòng)如果高頻詞不足一萬詞表會(huì)略小于1萬如果低頻詞較多可能卡在max_size上限附近。3.2 為什么要單獨(dú)保留unkunk是PTB整個(gè)數(shù)據(jù)集中最關(guān)鍵的一個(gè)token。模型在訓(xùn)練時(shí)見過它測(cè)試時(shí)遇到新詞也會(huì)替換成它這樣模型不會(huì)因?yàn)檩斎朐~表中不存在的詞而崩潰。我在構(gòu)建詞表時(shí)踩過一次坑一開始只基于訓(xùn)練集構(gòu)建詞表測(cè)試時(shí)遇到了訓(xùn)練集沒見過的詞但我沒有把它們映射到unk結(jié)果模型直接key error。后來我在encode_sentence函數(shù)里統(tǒng)一加入了映射邏輯def encode_sentence(sent, word2idx): return [word2idx.get(w, word2idx[UNK_TOKEN]) for w in sent]這樣訓(xùn)練、驗(yàn)證、測(cè)試三份文件都走同一條編碼路徑未登錄詞自動(dòng)落入unk與詞表構(gòu)建時(shí)的處理保持一致。3.3 預(yù)處理結(jié)果驗(yàn)證在正式訓(xùn)練之前我會(huì)做一次快速驗(yàn)證打印出編碼后的前兩句看看s和/s是否在正確位置特殊token是否映射正確詞頻閾值低頻詞是否變成了unk。這一步雖小卻能避免大量后期返工。如果你打印出來發(fā)現(xiàn)整句話都是未知詞大概率是詞表構(gòu)建時(shí)詞典沒有正確讀取而不是數(shù)據(jù)本身的問題。此時(shí)建議回到build_vocab檢查counter.most_common的參數(shù)是否設(shè)置正確。4. 用PTB訓(xùn)練語言模型4.1 批處理與BPTTPTB語料本身是完整文章被切成句子后的順序排列雖然存儲(chǔ)為逐行句子但在語言模型訓(xùn)練中我們通常不按句子為單位獨(dú)立訓(xùn)練而是將整個(gè)語料視為一個(gè)超長(zhǎng)token序列然后按固定長(zhǎng)度截?cái)喑善闻浜螧PTT(Backpropagation Through Time進(jìn)行訓(xùn)練。具體來說我常用的做法是把訓(xùn)練集中所有token拼成一個(gè)長(zhǎng)序列然后分成若干個(gè)batch。經(jīng)典參數(shù)來自Zaremba等人的LSTM基準(zhǔn)batch size設(shè)為20序列長(zhǎng)度bptt設(shè)為35。這樣每個(gè)batch包含20個(gè)獨(dú)立的子序列每個(gè)子序列長(zhǎng)度35模型在每個(gè)時(shí)間步都能看到35個(gè)歷史token反向傳播也只截?cái)嘣谶@35步內(nèi)。4.2 批量化處理的經(jīng)典實(shí)現(xiàn)這里我給出一個(gè)在PyTorch中非常通用的batch化代碼片段def process_into_batches(data, batch_size, bptt): num_batches len(data) // bptt data data[:num_batches * bptt] data torch.tensor(data, dtypetorch.long) data data.view(batch_size, -1).t().contiguous() return data # shape: [num_steps, batch_size]這段代碼的核心在于先截?cái)鄶?shù)據(jù)使其能被bptt整除再按batch_size改變形狀并轉(zhuǎn)置。轉(zhuǎn)置后第一個(gè)維度是時(shí)間步第二個(gè)維度是batch索引語言模型每一步輸入一個(gè)[batch_size]的token向量。注意這里訓(xùn)練時(shí)是把整個(gè)語料庫當(dāng)作一個(gè)序列來切而不是按句子切。所以一句話可能被切成兩半一半在前一個(gè)batch一半在后一個(gè)batch。這符合語言模型對(duì)長(zhǎng)距離依賴建模的需求也符合PTB作為連續(xù)文本語料的使用規(guī)范。4.3 模型結(jié)構(gòu)與評(píng)估指標(biāo)PTB上最常見的語言模型結(jié)構(gòu)就是單層或雙層LSTM。以單層LSTM為例隱層維度1500、詞嵌入維度400是經(jīng)典的配置。訓(xùn)練時(shí)采用CrossEntropyLoss輸出層將每個(gè)時(shí)間步的隱狀態(tài)映射到詞表大小的logits。評(píng)估指標(biāo)只有一個(gè)困惑度Perplexity, PPL。數(shù)學(xué)上PPL等于交叉熵?fù)p失取指數(shù)即PPL exp(loss)直觀理解是模型在每個(gè)位置預(yù)測(cè)下一個(gè)詞時(shí)平均有多少個(gè)候選詞讓它“猶豫”。PPL越低說明模型對(duì)下一個(gè)詞的預(yù)測(cè)越確定語言建模能力越強(qiáng)。在PTB標(biāo)準(zhǔn)劃分下經(jīng)典LSTM模型測(cè)試PPL大概在70-80之間而較早的RNN模型通常只能到100以上。如果你只是拿PTB來驗(yàn)證思路并不需要追求刷榜只要測(cè)試集PPL能正常下降并穩(wěn)定就說明數(shù)據(jù)管線和模型代碼沒有大問題。4.4 加載預(yù)訓(xùn)練詞向量的誤區(qū)有些人習(xí)慣在PTB上使用外部預(yù)訓(xùn)練詞向量比如GloVe或word2vec。我的建議是不要這么做至少在對(duì)比實(shí)驗(yàn)時(shí)不要混用。原因是PTB的詞表和外部預(yù)訓(xùn)練詞向量詞表往往不一致很多PTB中的特殊token和低頻詞在外部詞表中不存在最終導(dǎo)致模型只有少部分token能拿到預(yù)訓(xùn)練向量其余token還是隨機(jī)初始化這種不一致反而會(huì)對(duì)訓(xùn)練產(chǎn)生干擾。更合理的做法是把PTB當(dāng)作一個(gè)封閉詞表世界直接在訓(xùn)練集上學(xué)習(xí)詞嵌入。這既簡(jiǎn)單又容易復(fù)現(xiàn)也符合該數(shù)據(jù)集本身的設(shè)計(jì)邏輯。5. 常見問題與排查技巧實(shí)錄5.1 訓(xùn)練集與測(cè)試集詞表不一致這是最容易被忽略的問題。有些代碼在訓(xùn)練前構(gòu)建了詞表但測(cè)試時(shí)沒有使用同一個(gè)詞表去編碼測(cè)試集而是用測(cè)試集重新構(gòu)建詞表導(dǎo)致訓(xùn)練和測(cè)試階段unk的數(shù)量與分布完全不同最終評(píng)估結(jié)果失去意義。排查方法很簡(jiǎn)單訓(xùn)練階段把word2idx保存下來測(cè)試階段直接加載用同一套映射處理所有數(shù)據(jù)。同時(shí)打印測(cè)試集中被映射為unk的token數(shù)量如果異常偏高就要檢查是不是詞表本身構(gòu)建出了問題。5.2 內(nèi)存不足與序列長(zhǎng)度過大的平衡PTB雖然整體規(guī)模不大但如果把整個(gè)訓(xùn)練集一次性轉(zhuǎn)成長(zhǎng)序列再切分內(nèi)存占用還是能感受到的。尤其是在process_into_batches這一步如果data是Python list而不是numpy array或torch tensor拼接和切片會(huì)明顯變慢。建議從一開始就使用torch.tensor或numpy.array來存儲(chǔ)id序列并在切分前先計(jì)算好長(zhǎng)度避免隱含的Python循環(huán)。對(duì)于PTB這種量級(jí)只要用tensor操作幾秒鐘就能完成預(yù)處理內(nèi)存也不是問題。5.3 老版本預(yù)處理代碼與新庫不兼容PTB年代久遠(yuǎn)網(wǎng)上很多開源的預(yù)處理腳本用的是老版torchtext的LanguageModelingDataset接口或者torch.legacy里的舊函數(shù)。如果你用新版本PyTorch直接跑這些代碼大概率會(huì)報(bào)錯(cuò)。遇到這種情況我的習(xí)慣是丟掉框架自帶的dataset類自己用純Python加上PyTorch基礎(chǔ)API重構(gòu)數(shù)據(jù)處理。代碼量不大邏輯透明也方便后續(xù)替換成其他數(shù)據(jù)集。經(jīng)過幾次改造后我認(rèn)為純手寫的方式才是最適合PTB的因?yàn)槔蠋斓慕涌诒旧矸庋b層次太多調(diào)試起來反而費(fèi)勁。5.4 結(jié)果與論文對(duì)不上很多人跑完P(guān)TB后會(huì)發(fā)現(xiàn)PPL和論文里的數(shù)值差了不少。這個(gè)現(xiàn)象太正常了原因主要出在幾個(gè)變量上初始化方式、學(xué)習(xí)率衰減策略、梯度裁剪閾值、隱層維度、dropout比例甚至隨機(jī)種子都會(huì)影響最終結(jié)果。所以比較合理的態(tài)度是PTB上的絕對(duì)值并不重要重要的是在相同實(shí)驗(yàn)配置下你對(duì)不同模型或不同模塊的比較是否有效。比如你想驗(yàn)證注意力機(jī)制是否有效那就保證除了注意力之外的一切設(shè)置完全相同這樣跑出來的PPL差異才有參考價(jià)值。5.5 不合理的預(yù)處理“創(chuàng)新”見過有人在PTB上“創(chuàng)新”比如把句子按標(biāo)點(diǎn)再切碎或者把所有詞轉(zhuǎn)成小寫并去掉unk理由是“這樣可以提高PPL”。這種操作我是不建議的因?yàn)樗茐牧藬?shù)據(jù)集的原始分布做的實(shí)驗(yàn)無法與其他工作比較也就失去了用標(biāo)準(zhǔn)數(shù)據(jù)集測(cè)量的意義。還有人在讀入PTB文件時(shí)把文件中的s當(dāng)成了HTML標(biāo)簽去掉導(dǎo)致模型永遠(yuǎn)學(xué)不到句子起始信號(hào)這屬于對(duì)數(shù)據(jù)集理解不足帶來的低級(jí)錯(cuò)誤。如果你看到某篇博客聲稱用PTB訓(xùn)練模型時(shí)沒有使用s標(biāo)記要警覺其結(jié)果是否規(guī)范。6. 實(shí)操心得PTB還能怎么玩坦白說純比PPL數(shù)字的話PTB到今天已經(jīng)被刷得很高常規(guī)模型已接近飽和。但這不代表它沒有繼續(xù)使用的價(jià)值。我現(xiàn)在一般把PTB用在三個(gè)方向上第一是快速驗(yàn)證新模塊。比如想嘗試一種新的參數(shù)初始化方式、一個(gè)新的激活函數(shù)或者修改了RNN循環(huán)結(jié)構(gòu)與其在大型數(shù)據(jù)集上跑幾天再發(fā)現(xiàn)問題不如先在PTB上跑一兩個(gè)小時(shí)看看PPL是否正常下降。PTB跑不通大模型多半也跑不通。第二是教學(xué)與源碼理解。PTB的簡(jiǎn)單性使得它成為閱讀源碼、理解訓(xùn)練流程的最佳樣例。我教過幾個(gè)新人跟著一行行看PTB數(shù)據(jù)加載、batch劃分、BPTT反向傳播的代碼比看十遍理論講解都有用。第三是組合實(shí)驗(yàn)。有人把PTB和WikiText-2配合使用一個(gè)做小規(guī)模驗(yàn)證一個(gè)做中等規(guī)模驗(yàn)證兩者結(jié)合就可以覆蓋很多消融實(shí)驗(yàn)場(chǎng)景。PTB定位為“快速反饋回路”WikiText-2定位為“穩(wěn)定復(fù)現(xiàn)實(shí)驗(yàn)”這樣一個(gè)流程既快又不失說服力。我在實(shí)際使用中還有一個(gè)體會(huì)使用PTB時(shí)一定要把版本、詞表大小、劃分方式、特殊token處理這些信息記錄在實(shí)驗(yàn)筆記里。看似瑣碎但一旦實(shí)驗(yàn)結(jié)果要寫進(jìn)論文或者跨團(tuán)隊(duì)復(fù)現(xiàn)這些細(xì)節(jié)會(huì)成為最可靠的信息源。比起數(shù)據(jù)集本身的大小我們更該在乎的是復(fù)用它時(shí)保持的嚴(yán)謹(jǐn)性。本文還有配套的精品資源點(diǎn)擊獲取