:從功能詞到字符N元語法)
簡介本資源為《Python數(shù)據(jù)挖掘項目開發(fā)實戰(zhàn)》第9章「作者歸屬問題」的配套PDF面向具備一定Python與數(shù)據(jù)挖掘基礎(chǔ)、希望完整走通分類項目流程的開發(fā)者與學(xué)習(xí)者。內(nèi)容圍繞文本挖掘中的作者歸屬任務(wù)展開從問題界定、背景知識到特征抽取、流水線搭建與結(jié)果分析系統(tǒng)講解如何把交叉驗證、特征工程與分類算法整合應(yīng)用。資源重點比較功能詞與N元語法兩類特征的效果并涉及詞袋模型回顧、支持向量機、數(shù)據(jù)集清洗等關(guān)鍵環(huán)節(jié)實驗數(shù)據(jù)先采用古騰堡計劃圖書語料再進(jìn)階到噪音較多的真實電子郵件語料同時區(qū)分封閉問題與開放問題兼顧作者畫像、作者驗證、作者聚類等衍生場景。包內(nèi)共1個PDF文件大小約1.48MB便于隨時查閱與對照實踐。目前已有369人學(xué)習(xí)適合希望掌握數(shù)據(jù)挖掘完整流程、提升文本分類實戰(zhàn)能力的讀者參考。1. 作者歸屬問題從177本公版書到安然郵件語料這套分類流水線到底怎么跑手里有一堆匿名文檔想知道它們分別出自誰手——這事在文本挖掘里叫作者歸屬authorship attribution本質(zhì)是一個多分類任務(wù)。它不靠寫作時間、印刷形式或筆跡只用作品內(nèi)容本身從一組候選作者里找出文檔真正的主人。聽起來像玄學(xué)但背后的依據(jù)是文體學(xué)每個人在語言掌握上有微小差異這些差異會穩(wěn)定地反映到用詞、標(biāo)點和句式習(xí)慣里機器恰好擅長統(tǒng)計這些細(xì)微差別。這份《Python數(shù)據(jù)挖掘項目開發(fā)實戰(zhàn)》第9章的配套資源把前幾章的數(shù)據(jù)挖掘方法整合成一條完整流水線界定問題、抽取特征、創(chuàng)建Pipeline、交叉檢驗、對比特征效果。它用兩套數(shù)據(jù)集遞進(jìn)——先用古騰堡計劃的177本公版圖書7位作家跑通流程再上噪音更多的安然公司真實郵件語料增加難度。適合已經(jīng)會sklearn基礎(chǔ)操作、想找一個完整分類項目練手的人也適合想搞清楚功能詞和字符N元語法到底哪個更適合作者歸屬的從業(yè)者。下面按“資源是什么→怎么用→坑在哪”的順序拆開講。2. 圖書數(shù)據(jù)集從古騰堡下載到清洗把177本作品變成可訓(xùn)練矩陣2.1 下載與目錄結(jié)構(gòu)數(shù)據(jù)來自古騰堡計劃www.gutenberg.org7位作家共177篇作品塔金頓22篇、狄更斯44篇、內(nèi)斯比特10篇、柯南·道爾51篇、馬克·吐溫29篇、伯頓11篇、加博里奧10篇。配套代碼包里有一個getdata.py負(fù)責(zé)自動下載并按作者分文件夾存放。先在Data目錄下建好books文件夾路徑要和代碼里保持一致import os import sys data_folder os.path.join(os.path.expanduser(~), Data, books)然后在筆記本里加載下載腳本并運行# 在Jupyter單元格中執(zhí)行加載并運行g(shù)etdata.py !load getdata.py!load是IPython的魔術(shù)命令把腳本內(nèi)容讀進(jìn)當(dāng)前單元格再按ShiftEnter執(zhí)行下載邏輯。下載耗時取決于網(wǎng)絡(luò)跑完會給出提示。下載完成后books目錄下是按作者名命名的子文件夾每個子文件夾里是該作家的作品文本文件。提示下載腳本依賴requests庫如果環(huán)境里沒有先pip install requests。下載過程中斷的話重新運行會覆蓋已存在的文件不會重復(fù)堆積。2.2 清洗為什么不在磁盤上直接刪聲明每篇古騰堡作品前后都有一大段項目聲明文字從數(shù)據(jù)分析角度看全是噪音。直覺做法是打開文件把聲明刪掉再保存但這樣做有個隱患一旦改動原文件后續(xù)實驗就無法保證可重現(xiàn)——你沒法證明這次結(jié)果和上次用的是同一份數(shù)據(jù)。更穩(wěn)妥的做法是在加載時跳過聲明部分原文件保持不動。清洗函數(shù)按行切分找到作品開始和結(jié)束的標(biāo)識行取中間內(nèi)容def clean_book(document): lines document.split(\n) start 0 end len(lines) for i in range(len(lines)): line lines[i] if line.startswith(*** START OF THIS PROJECT GUTENBERG): start i 1 elif line.startswith(*** END OF THIS PROJECT GUTENBERG): end i - 1 return \n.join(lines[start:end])start初始為0、end初始為總行數(shù)是為了兜底——萬一某篇作品沒有標(biāo)準(zhǔn)標(biāo)識行就返回全文而不是空字符串。找到標(biāo)識行后start取標(biāo)識行的下一行end取標(biāo)識行的上一行中間就是純作品內(nèi)容。最后用換行符重新拼接。2.3 加載全部圖書并生成類別標(biāo)簽加載函數(shù)遍歷books下的作者子文件夾用enumerate給每個作者分配一個整數(shù)編號這個編號就是分類任務(wù)里的類別標(biāo)簽import numpy as np def load_books_data(folderdata_folder): documents [] authors [] subfolders [subfolder for subfolder in os.listdir(folder) if os.path.isdir(os.path.join(folder, subfolder))] for author_number, subfolder in enumerate(subfolders): full_subfolder_path os.path.join(folder, subfolder) for document_name in os.listdir(full_subfolder_path): with open(os.path.join(full_subfolder_path, document_name)) as inf: documents.append(clean_book(inf.read())) authors.append(author_number) return documents, np.array(authors, dtypeint)documents是文本列表authors是對應(yīng)的類別數(shù)組。enumerate保證每個子文件夾對應(yīng)唯一編號np.array(..., dtypeint)把類別轉(zhuǎn)成整型數(shù)組方便后續(xù)傳給sklearn的分類器。調(diào)用一次就能拿到全部數(shù)據(jù)documents, classes load_books_data(data_folder)177篇文本加載進(jìn)內(nèi)存沒有壓力。如果數(shù)據(jù)集大到內(nèi)存裝不下常見做法是每次只從一篇或幾篇文檔抽特征把特征矩陣寫到磁盤或內(nèi)存映射文件里而不是一次性把原始文本全讀進(jìn)來。3. 功能詞特征用CountVectorizer鎖定寫作習(xí)慣跑通第一條流水線3.1 功能詞為什么能區(qū)分作者功能詞是本身含義很弱、但構(gòu)成英語句子必不可少的成分比如this、which、the、of。它們的用法更多由作者的使用習(xí)慣決定而不是由文檔主題決定。一個典型例子很多美國人很在意區(qū)分that和which的用法澳大利亞等國家的人就不太在意有些人幾乎一律用that。這類微小差異疊加起來就成了區(qū)分作者的信號。選功能詞有個經(jīng)驗法則優(yōu)先選在所有文檔中使用頻率都高的詞。使用越頻繁對作者分析提供的信息越多使用頻率低的詞更適合做基于內(nèi)容的主題劃分而不是作者歸屬。下面這份功能詞表來自已發(fā)表研究可以直接用function_words [a, able, aboard, about, above, absent, according, accordingly, across, after, against, ahead, albeit, all, along, alongside, although, am, amid, amidst, among, amongst, amount, an, and, another, anti, any, anybody, anyone, anything, are, around, as, aside, astraddle, astride, at, away, bar, barring, be, because, been, before, behind, being, below, beneath, beside, besides, better, between, beyond, bit, both, but, by, can, certain, circa, close, concerning, consequently, considering, could, couple, dare, deal, despite, down, due, during, each, eight, eighth, either, enough, every, everybody, everyone, everything, except, excepting, excluding, failing, few, fewer, fifth, first, five, following, for, four, fourth, from, front, given, good, great, had, half, have, he, heaps, hence, her, hers, herself, him, himself, his, however, i, if, in, including, inside, instead, into, is, it, its, itself, keeping, lack, less, like, little, loads, lots, majority, many, masses, may, me, might, mine, minority, minus, more, most, much, must, my, myself, near, need, neither, nevertheless, next, nine, ninth, no, nobody, none, nor, nothing, notwithstanding, number, numbers, of, off, on, once, one, onto, opposite, or, other, ought, our, ours, ourselves, out, outside, over, part, past, pending, per, pertaining, place, plenty, plethora, plus, quantities, quantity, quarter, regarding, remainder, respecting, rest, round, save, saving, second, seven, seventh, several, shall, she, should, similar, since, six, sixth, so, some, somebody, someone, something, spite, such, ten, tenth, than, thanks, that, the, their, theirs, them, themselves, then, thence, therefore, these, they, third, this, those, though, three, through, throughout, thru, thus, till, time, to, tons, top, toward, towards, two, under, underneath, unless, unlike, until, unto, up, upon, us, used, various, versus, via, view, wanting, was, we, were, what, whatever, when, whenever, where, whereas, wherever, whether, which, whichever, while, whilst, who, whoever, whole, whom, whomever, whose, will, with, within, without, would, yet, you, your, yours, yourself, yourselves]把這份列表傳給CountVectorizer的vocabulary參數(shù)就鎖定了特征空間——只統(tǒng)計這些詞不再從數(shù)據(jù)里學(xué)習(xí)詞匯表from sklearn.feature_extraction.text import CountVectorizer extractor CountVectorizer(vocabularyfunction_words)注意傳了vocabulary之后訓(xùn)練集里沒出現(xiàn)的功能詞會被忽略測試集里出現(xiàn)但詞匯表里沒有的詞也不會被統(tǒng)計。這保證了訓(xùn)練和預(yù)測階段用的是同一套特征不會因為數(shù)據(jù)不同導(dǎo)致維度錯位。3.2 支持向量機與網(wǎng)格搜索分類器用SVM。它的基本思想是找一個能把兩類數(shù)據(jù)分開的最優(yōu)超平面讓每個點到超平面的距離最大化。多分類時常見做法是為每個類別建一個一對多分類器把“屬于該類”和“其他所有類”分開預(yù)測時選最匹配的那個。scikit-learn的SVC會自動處理這個過程。SVM有兩個關(guān)鍵參數(shù)kernel和C。kernel決定用什么核函數(shù)線性核最簡單rbf核高斯核適合線性不可分的數(shù)據(jù)。C控制對誤分類的懲罰C值越高間隔越小盡量把所有訓(xùn)練數(shù)據(jù)分對但有過擬合風(fēng)險C值越低間隔越大允許一些數(shù)據(jù)分錯泛化可能更好。用網(wǎng)格搜索在這兩個參數(shù)上找最優(yōu)組合from sklearn.svm import SVC from sklearn.cross_validation import cross_val_score from sklearn.pipeline import Pipeline from sklearn import grid_search parameters {kernel: (linear, rbf), C: [1, 10]} svr SVC() grid grid_search.GridSearchCV(svr, parameters)GridSearchCV會遍歷kernel和C的所有組合對每組做交叉檢驗選平均得分最高的那組。parameters字典里kernel是元組、C是列表這是網(wǎng)格搜索的標(biāo)準(zhǔn)寫法。提示rbf核只適合數(shù)據(jù)集相對較小的情況比如特征數(shù)少于10000。功能詞表大約200個詞特征維度不高rbf完全扛得住。但如果換成字符N元語法特征數(shù)會暴漲得留意。3.3 組裝流水線并交叉檢驗把特征抽取和分類器串成Pipeline避免手動分步調(diào)用時訓(xùn)練集和測試集特征不一致的問題pipeline1 Pipeline([ (feature_extraction, extractor), (clf, grid) ]) scores cross_val_score(pipeline1, documents, classes, scoringf1) print(Score: {:.3f}.format(np.mean(scores)))cross_val_score默認(rèn)做3折交叉檢驗每折里Pipeline會先在訓(xùn)練折上fit特征抽取器再在測試折上transform保證不泄露測試集信息。用f1作為評分標(biāo)準(zhǔn)是因為7個作者的類別分布不完全均衡f1比準(zhǔn)確率更能反映每個類別的分類質(zhì)量。這套功能詞流水線的正確率大約0.811對7個作者來說已經(jīng)不錯。4. 字符N元語法換掉特征抽取器看稀疏矩陣怎么提升分類效果4.1 字符N元語法的原理與參數(shù)N元語法是由N個連續(xù)對象組成的序列對文本來說N通常取2到6。基于單詞的N元語法常用于主題相關(guān)研究而基于字符的N元語法在作者歸屬上效果很好。廣為接受的理論是人們寫作時傾向于選自己講起來容易的單詞字符N元模型N取2到6跟音素組成單詞發(fā)音的聲音有很好的相似關(guān)系因此能模擬寫作風(fēng)格。用CountVectorizer抽取字符N元語法關(guān)鍵是設(shè)置analyzerchar和ngram_rangepipeline Pipeline([ (feature_extraction, CountVectorizer(analyzerchar, ngram_range(3, 3))), (classifier, grid) ]) scores cross_val_score(pipeline, documents, classes, scoringf1) print(Score: {:.3f}.format(np.mean(scores)))analyzerchar告訴向量化器按字符切分而不是按詞切分。ngram_range(3, 3)表示只抽取長度為3的字符N元語法兩個值相同就鎖定單一長度。如果想同時抽2到4的N元語法寫成(2, 4)即可但特征維度會成倍增長。字符N元語法矩陣的一個顯著特點是稀疏。N取2時大約75%的項為0N取5時93%以上為0。不過比起基于詞語的N元語法矩陣稀疏程度還是低一些常見分類器處理起來問題不大。4.2 功能詞與字符N元語法的差異兩者存在大量隱式重合因為字符序列更可能出現(xiàn)在功能詞中。但差別也很明顯字符N元語法能捕獲標(biāo)點的使用特點功能詞做不到。比如句號在字符N元語法里是一個特征而基于功能詞的方法只能用句號前的單詞。這意味著字符N元語法對作者的標(biāo)點習(xí)慣敏感而功能詞對語法詞選擇敏感。實際選型時如果文本經(jīng)過嚴(yán)格清洗、標(biāo)點規(guī)范兩種特征都可以試如果文本標(biāo)點混亂或作者有獨特標(biāo)點習(xí)慣字符N元語法往往更有優(yōu)勢。代價是特征維度高、訓(xùn)練慢需要更多內(nèi)存。4.3 參數(shù)怎么調(diào)ngram_range的N值選擇沒有萬能公式。N太小比如2特征區(qū)分度不夠N太大比如6特征矩陣極度稀疏很多N元語法只出現(xiàn)一兩次對分類幫助有限還增加計算量。常見做法是從3開始試再對比2、4、5的效果。CountVectorizer還有幾個參數(shù)值得關(guān)注lowercase默認(rèn)True會把所有字符轉(zhuǎn)小寫如果作者的大小寫習(xí)慣是區(qū)分信號可以設(shè)為Falsemax_features可以限制特征數(shù)量按詞頻取前N個適合特征爆炸時降維min_df可以過濾掉出現(xiàn)次數(shù)太少的N元語法減少噪音。注意字符N元語法對文本長度敏感。短文本抽不出足夠多的N元語法特征矩陣會非常稀疏分類效果可能不如功能詞。安然郵件里有些郵件很短這時候要留意樣本長度分布。5. 安然郵件語料從423MB原始郵件到均衡的作者分類數(shù)據(jù)集5.1 數(shù)據(jù)集背景與獲取安然公司曾是全球最大的能源公司之一2000年擁有20000余名員工年收入高達(dá)1000億美元。2001年財務(wù)造假丑聞曝光后股價從90多美元跌到1美元隨即破產(chǎn)。調(diào)查過程中公開了60多萬封電子郵件這些數(shù)據(jù)后來被廣泛用于社會媒體分析、欺詐分析等研究。用于作者歸屬的優(yōu)勢在于能明確知道發(fā)件人是誰語料規(guī)模遠(yuǎn)大于圖書數(shù)據(jù)集。數(shù)據(jù)集從卡內(nèi)基梅隆大學(xué)網(wǎng)站下載整個包423MBgzip壓縮格式。非Linux系統(tǒng)可以用7-zip解壓。解壓后目錄默認(rèn)為enron_mail_20110402。指定數(shù)據(jù)位置enron_data_folder os.path.join( os.path.expanduser(~), Data, enron_mail_20110402, maildir)5.2 郵件解析與類別均衡做作者歸屬只需要明確知道發(fā)件人的郵件所以查看每位用戶的發(fā)件箱——文件夾名里含“sent”的那些。郵件內(nèi)容需要用解析器從原始郵件里抽出來from email.parser import Parser p Parser()加載函數(shù)要解決一個現(xiàn)實問題有些用戶發(fā)了幾千封郵件有些只發(fā)了幾十封類別嚴(yán)重不均衡。用三個參數(shù)控制min_docs_author指定每個發(fā)件人至少發(fā)過多少封才納入max_docs_author指定最多從每個用戶抽多少封num_authors限定作者數(shù)量。為了實驗結(jié)果可重現(xiàn)還要固定隨機狀態(tài)from sklearn.utils import check_random_state def get_enron_corpus(num_authors10, data_folderdata_folder, min_docs_author10, max_docs_author100, random_stateNone): random_state check_random_state(random_state) email_addresses sorted(os.listdir(data_folder)) random_state.shuffle(email_addresses) documents [] classes [] author_num 0 authors {} for user in email_addresses: users_email_folder os.path.join(data_folder, user) mail_folders [os.path.join(users_email_folder, subfolder) for subfolder in os.listdir(users_email_folder) if sent in subfolder] # 后續(xù)遍歷mail_folders解析郵件按min/max限制抽取 # 滿足條件的作者才分配author_num并加入authors字典 return documents, np.array(classes, dtypeint), authors先sorted再shuffle是有意為之os.listdir每次返回順序不一定相同先排序保證基礎(chǔ)順序一致再用固定隨機狀態(tài)打亂這樣只要random_state相同每次選出的作者和郵件就完全一樣。authors字典把用戶名和類別編號關(guān)聯(lián)起來數(shù)據(jù)挖掘過程用不到但可視化時能定位到具體是誰。5.3 在郵件語料上跑流水線拿到documents和classes后直接復(fù)用前面的Pipeline。功能詞流水線和字符N元語法流水線都可以跑對比f1得分。郵件語料的噪音比圖書大得多——有簽名檔、轉(zhuǎn)發(fā)內(nèi)容、回復(fù)引用、拼寫錯誤這些都會影響特征質(zhì)量。常見做法是先做一輪輕量清洗去掉郵件頭、去掉引用行以“”開頭的行、統(tǒng)一換行符。但清洗過度也可能抹掉作者的個人習(xí)慣比如有人就是愛用特定簽名格式這本身就是信號。提示安然數(shù)據(jù)集里郵件正文的編碼格式不統(tǒng)一有些是latin-1有些是utf-8。讀取時如果報編碼錯誤用errorsignore或errorsreplace兜底別讓個別郵件中斷整個加載過程。6. 避坑與排查作者歸屬流水線里最容易翻車的五個地方6.1 交叉檢驗得分高得離譜現(xiàn)象功能詞流水線在圖書數(shù)據(jù)集上f1超過0.95換到郵件語料掉到0.6以下。原因圖書數(shù)據(jù)集里同一作者的多部作品被同時分到訓(xùn)練折和測試折模型可能記住了某部作品的特定內(nèi)容而不是作者的通用風(fēng)格。郵件語料里同一作者的郵件主題差異大模型泛化能力暴露出來。解決按作者分組做交叉檢驗確保同一作者的作品不會同時出現(xiàn)在訓(xùn)練和測試折。sklearn的GroupKFold可以指定分組標(biāo)簽把作者編號作為分組依據(jù)。6.2 CountVectorizer的vocabulary和訓(xùn)練集不匹配現(xiàn)象預(yù)測時報維度錯誤或者某些特征全為0。原因手動傳了vocabulary但訓(xùn)練集里實際出現(xiàn)的詞和詞匯表對不上或者訓(xùn)練時用了一套詞匯表預(yù)測時換了另一套。解決把CountVectorizer放進(jìn)Pipeline里讓它在每折訓(xùn)練時自己fit。不要在外面先fit好再傳進(jìn)去否則交叉檢驗時特征抽取器已經(jīng)見過全部數(shù)據(jù)造成信息泄露。6.3 字符N元語法內(nèi)存爆掉現(xiàn)象ngram_range設(shè)成(2, 6)后程序卡死或報MemoryError。原因字符N元語法的特征數(shù)量隨N值指數(shù)增長177本書的字符總量很大同時抽2到6的所有N元語法會產(chǎn)生海量特征。解決先用單一N值比如3跑通再逐步增加。用min_df5過濾掉出現(xiàn)次數(shù)少于5的N元語法能大幅降低維度?;蛘哂肏ashingVectorizer替代它不需要維護(hù)詞匯表內(nèi)存占用更可控。6.4 郵件解析后正文為空現(xiàn)象加載安然郵件后很多documents元素是空字符串或只有幾個字符。原因郵件是多部分MIME結(jié)構(gòu)正文可能在某個子部分里直接取msg.get_payload()拿到的是附件或HTML部分。有些郵件正文是base64編碼沒解碼就是亂碼。解決用email庫的walk()方法遍歷所有部分判斷content_type是否為text/plain是的話取get_payload(decodeTrue)再解碼。遇到text/html可以跳過或做標(biāo)簽清洗。6.5 隨機狀態(tài)沒固定導(dǎo)致結(jié)果不可重現(xiàn)現(xiàn)象同樣的代碼跑兩次f1得分不一樣。原因get_enron_corpus里用了隨機打亂選作者但random_state傳了None或者SVM的random_state沒設(shè)某些核函數(shù)內(nèi)部有隨機初始化。解決加載數(shù)據(jù)時固定random_state比如傳42。SVC的random_state也設(shè)成固定值。train_test_split、cross_val_score如果有隨機成分同樣要固定。把隨機種子寫進(jìn)配置別散落在代碼各處。7. 進(jìn)階技巧用混淆矩陣定位“誰被認(rèn)成了誰”再回頭調(diào)特征跑完流水線拿到f1得分只是開始真正有價值的是看模型在哪些作者之間混淆。用混淆矩陣能直觀看到哪些作者的文檔被大量分錯錯分給了誰。這一步往往能反過來指導(dǎo)特征工程。from sklearn.metrics import confusion_matrix from sklearn.cross_validation import cross_val_predict # 用cross_val_predict拿到每折的預(yù)測結(jié)果避免手動分折 predicted cross_val_predict(pipeline1, documents, classes, cv3) cm confusion_matrix(classes, predicted) print(cm)cross_val_predict返回的是每個樣本在它所在測試折上的預(yù)測類別拼接起來就是全量預(yù)測。confusion_matrix的行是真實類別列是預(yù)測類別對角線是分對的非對角線是分錯的。如果發(fā)現(xiàn)作者A和作者B之間互相錯分特別多說明這兩個人的寫作風(fēng)格在所選特征下太接近。這時候有幾個調(diào)整方向。一是換特征功能詞分不開的試試字符N元語法特別是加入標(biāo)點相關(guān)的N元語法。二是調(diào)SVM參數(shù)如果混淆集中在某幾類可能是C值太大導(dǎo)致過擬合或者核函數(shù)不合適可以擴大網(wǎng)格搜索范圍。三是檢查數(shù)據(jù)有沒有同一作者的文檔被錯誤標(biāo)注成另一個作者或者某個作者的文檔里混入了別人的作品。我一般會先看混淆矩陣的對角線占比如果某類召回率明顯低于其他類優(yōu)先查那類的數(shù)據(jù)質(zhì)量。有一次跑圖書數(shù)據(jù)集柯南·道爾的作品被大量分給狄更斯查下來發(fā)現(xiàn)下載的某幾本書其實是合集里面混了狄更斯的作品。數(shù)據(jù)問題不解決調(diào)參調(diào)到天亮也沒用。從那以后我每次跑作者歸屬之前都強制走一遍混淆矩陣先確認(rèn)數(shù)據(jù)層面沒有明顯錯標(biāo)再動特征和參數(shù)。希望幫到你。本文還有配套的精品資源點擊獲取