機(jī)器學(xué)習(xí):四套模型源碼解析與實(shí)戰(zhàn))
簡介基于情感詞典和多種機(jī)器學(xué)習(xí)模型的情感極性分析設(shè)計(jì)源碼面向自然語言處理研究者與初中級開發(fā)者用于從文本中判別積極、消極或中性情緒可應(yīng)用于輿情監(jiān)控、評論分析等場景。資源包共28個文件主要由5個Python源文件、12個txt詞典/語料、4個Excel評估結(jié)果、2個png效果圖及dict、gitignore等組成整體大小17.01MB目錄結(jié)構(gòu)清晰便于按模塊研讀。目前已有365人學(xué)習(xí)。源碼實(shí)現(xiàn)k-NN、樸素貝葉斯、最大熵與SVM等經(jīng)典算法覆蓋情感詞典構(gòu)建、特征提取、模型訓(xùn)練及評估全流程并附有酒店、外賣、電影等多領(lǐng)域?qū)嶒?yàn)語料與運(yùn)行結(jié)果。通過學(xué)習(xí)可掌握文本預(yù)處理、分類模型調(diào)參和結(jié)果可視化方法適合作為情感分析入門或課程設(shè)計(jì)的參考實(shí)現(xiàn)。1. 用詞典加傳統(tǒng)機(jī)器學(xué)習(xí)做情感極性分析這份 Python 源碼能直接跑通四套模型情感極性分析這些年被深度學(xué)習(xí)占了頭條但真正落到小規(guī)模數(shù)據(jù)集、可解釋性要求高的場景基于情感詞典加傳統(tǒng)機(jī)器學(xué)習(xí)模型依然是性價比最高的方案。這份源碼打包了 k-NN、樸素貝葉斯、最大熵、SVM 四套分類器配好了中文酒店、中文外賣、英文電影三個領(lǐng)域的語料和情感詞典解壓之后改改路徑就能跑出評估結(jié)果。它解決的核心問題是當(dāng)你想在自定義數(shù)據(jù)集上做正負(fù)情感分類又不想從零寫分詞、特征選擇、模型訓(xùn)練和評估這條鏈路時這份源碼把整條 pipeline 都補(bǔ)齊了。適合 NLP 入門者做課程設(shè)計(jì)、研究生做 baseline 對比也適合工程師快速驗(yàn)證一個文本分類需求能不能用傳統(tǒng)方案滿足。2. 先看清資源結(jié)構(gòu)詞典、語料、特征文件各管哪一段拿到壓縮包先別急著跑代碼把文件按用途分組是節(jié)省時間的第一步。這份源碼的文件命名非常直白基本把訓(xùn)練集規(guī)模、特征數(shù)、模型類型都寫進(jìn)了文件名里比如Bayes-hotel-train-2200-test-800-f-5000就是貝葉斯模型跑酒店語料2200 條訓(xùn)練、800 條測試、5000 個特征。先搞清每個文件在 pipeline 里的位置后面調(diào)參才不至于抓瞎。2.1 詞典文件拆解八類詞典如何協(xié)同完成極性判斷情感詞典是這套源碼的地基。f_dict目錄下共 8 個 txt 詞典各司其職positive_dict.txt和negative_dict.txt存情感詞adverb_dict.txt存程度副詞denial_dict.txt存否定詞conjunction_dict.txt存轉(zhuǎn)折連詞phrase_dict.txt存短語級情感單元punctuation_dict.txt存情感標(biāo)點(diǎn)user.dict用于補(bǔ)充自定義詞。為什么需要這么多類型因?yàn)閱渭兤ヅ淝楦性~會漏掉大量語義信息。以「酒店不算太差」這句話為例基礎(chǔ)情感詞匹配會命中「差」這個負(fù)向詞但如果只看這一個詞判斷就是負(fù)向。加入否定詞后「不」會翻轉(zhuǎn)情感方向再加入程度副詞「太」會調(diào)整情感強(qiáng)度。三層疊加之后這句話的情感值會從負(fù)向被拉回中性偏正真實(shí)語義才能被還原。短語詞典處理的則是「性價比很高」「服務(wù)態(tài)度好」這類組合單元單字面拆分會丟失整體語義。# 情感得分計(jì)算的樸素實(shí)現(xiàn)展示詞典如何協(xié)同 import re def load_dict(path): words set() with open(path, r, encodingutf-8) as f: for line in f: word line.strip() if word: words.add(word) return words # 加載各詞典 pos_words load_dict(f_dict/positive_dict.txt) neg_words load_dict(f_dict/negative_dict.txt) adverb_words load_dict(f_dict/adverb_dict.txt) denial_words load_dict(f_dict/denial_dict.txt) # 程度副詞權(quán)重表實(shí)際項(xiàng)目中按詞典順序或詞頻統(tǒng)計(jì)賦值 adverb_weight {很: 1.5, 太: 1.8, 非常: 2.0, 有點(diǎn): 0.7, 稍微: 0.5} def sentence_polarity(tokens): score 0.0 i 0 while i len(tokens): word tokens[i] if word in pos_words: score 1.0 elif word in neg_words: score - 1.0 if word in denial_words: # 翻轉(zhuǎn)到下一情感詞為止實(shí)際項(xiàng)目中需限定窗口 for j in range(i 1, min(i 3, len(tokens))): if tokens[j] in pos_words: score - 1.5 # 否定正向 負(fù)向 elif tokens[j] in neg_words: score 1.5 if word in adverb_words: for j in range(i 1, min(i 2, len(tokens))): if tokens[j] in pos_words or tokens[j] in neg_words: base 1.0 if tokens[j] in pos_words else -1.0 score (adverb_weight.get(word, 1.2) - 1.0) * base i 1 return score這段代碼展示了詞典打分的基本循環(huán)邏輯。adverb_weight字典是手工設(shè)定的經(jīng)驗(yàn)值實(shí)際項(xiàng)目中可以用互信息或詞頻統(tǒng)計(jì)來量化每個副詞的強(qiáng)度系數(shù)。否定詞的翻轉(zhuǎn)窗口我限制在 3 個詞之內(nèi)這是處理口語文本時比較穩(wěn)妥的做法——「不是很滿意」和「不是特別滿意」的否定范圍都覆蓋到了但再遠(yuǎn)的詞就不該被「不」影響。最后得分大于 0 判為正向小于 0 判為負(fù)向等于 0 則交給后續(xù)的機(jī)器學(xué)習(xí)模型兜底。單純詞典匹配的準(zhǔn)確率通常只有 70% 上下主要敗在未登錄詞和反諷句式上。但它的價值在于能生成高質(zhì)量的特征候選集——通過詞典初篩出的情感候選詞配合卡方檢驗(yàn)?zāi)艿玫奖葐渭冊~頻統(tǒng)計(jì)更精準(zhǔn)的特征維度。2.2 語料與中間文件酒店、外賣、電影三個領(lǐng)域的訓(xùn)練測試分布f_corpus目錄下放著 5 個語料文件ch_hotel_corpus.txt中文酒店、ch_waimai_corpus.txt和ch_waimai2_corpus.txt中文外賣、en_movie_corpus.txt和en_movie2_corpus.txt英文電影。三個領(lǐng)域覆蓋了兩種語言、三種文本風(fēng)格這是刻意為之的——酒店評論偏正式長句外賣評論偏口語短句電影評論中英混雜且情緒表達(dá)更夸張。從 Excel 文件名可以還原出各數(shù)據(jù)集的劃分方案數(shù)據(jù)集訓(xùn)練集規(guī)模測試集規(guī)模特征數(shù)對應(yīng)模型酒店中文22008005000Bayes電影英文7003005000Bayes外賣中文300010005000SVMC150線性核訓(xùn)練測試比大約為 7:3 到 3:1這在文本分類里屬于常見配置。700 條訓(xùn)練數(shù)據(jù)對機(jī)器學(xué)習(xí)模型來說偏少但對樸素貝葉斯而言反而夠用——它假設(shè)特征獨(dú)立小樣本下不太容易過擬合。外賣數(shù)據(jù)集最大3000 條訓(xùn)練樣本配合 SVM 線性核實(shí)測效果通常優(yōu)于另外兩個領(lǐng)域因?yàn)橥赓u評論句式短、情感詞密集、噪聲少。chi1.png和chi2.png是卡方檢驗(yàn)特征選擇的可視化結(jié)果evaluation.PNG是各模型評估指標(biāo)對比圖maxent_iteration.PNG是最大熵模型的迭代收斂曲線。這幾張圖不是擺設(shè)后面評估模型效果時可以直接對照看。3. 特征提取與降維feature_extraction.py 怎么把文本變成特征矩陣文本分類繞不開的環(huán)節(jié)是把原始文本轉(zhuǎn)成模型能吃的數(shù)值特征。這份源碼的特征提取思路和主流做法一致中文分詞后用卡方檢驗(yàn)CHI做特征選擇選出最具有區(qū)分度的前 N 個特征詞最終輸出 LIBSVM 格式的特征向量。這種方案在 2016 年前后是教科書級的標(biāo)準(zhǔn)流程現(xiàn)在看依然不過時。3.1 卡方檢驗(yàn)選特征為什么選前 5000 個而不是全部卡方檢驗(yàn)的核心思想是衡量某個詞和某個類別之間的相關(guān)性。如果一個詞在正向文檔中出現(xiàn)的頻率顯著高于在負(fù)向文檔中的頻率它對該分類任務(wù)的信息量就大。計(jì)算每個候選詞的卡方值后按從大到小排序取前 5000 個詞組成特征詞表。5000 這個數(shù)字不是拍腦袋定的特征太多會引入噪聲降低泛化能力太少又會丟失有效區(qū)分信息對萬級語料來說3000 到 8000 之間通常能取得較好的平衡源碼選擇 5000 屬于穩(wěn)妥的中位數(shù)。# 卡方特征選擇的核心計(jì)算 import math from collections import Counter def chi_square(word, pos_docs, neg_docs, total_pos, total_neg): 計(jì)算某個詞的卡方值 word: 候選特征詞 pos_docs: 包含該詞的正向文檔集合 neg_docs: 包含該詞的負(fù)向文檔集合 total_pos: 正向文檔總數(shù) total_neg: 負(fù)向文檔總數(shù) a len(pos_docs) # 正向且包含該詞 b len(neg_docs) # 負(fù)向且包含該詞 c total_pos - a # 正向且不包含該詞 d total_neg - b # 負(fù)向且不包含該詞 n total_pos total_neg # 卡方公式注意分母為0時的保護(hù) denominator (a c) * (b d) * (a b) * (c d) if denominator 0: return 0.0 chi2 n * (a * d - b * c) ** 2 / denominator return chi2 def select_top_k_features(tokenized_docs, labels, k5000): 從分詞后的文檔中選出卡方值最高的k個詞 tokenized_docs: 嵌套列表 [[詞1, 詞2, ...], ...] labels: 對應(yīng)的情感標(biāo)簽列表1為正0為負(fù) k: 期望保留的特征數(shù) pos_docs [set(doc) for doc, label in zip(tokenized_docs, labels) if label 1] neg_docs [set(doc) for doc, label in zip(tokenized_docs, labels) if label 0] total_pos len(pos_docs) total_neg len(neg_docs) # 統(tǒng)計(jì)每個詞出現(xiàn)在哪些文檔中 word_doc_map {} for doc in pos_docs: for word in doc: word_doc_map.setdefault(word, [[], []])[0].append(id(doc)) for doc in neg_docs: for word in doc: word_doc_map.setdefault(word, [[], []])[1].append(id(doc)) word_chi2 {} for word, (pos_doc_ids, neg_doc_ids) in word_doc_map.items(): # 這里把文檔ID集合轉(zhuǎn)換為包含該詞的文檔數(shù) score chi_square(word, pos_doc_ids, neg_doc_ids, total_pos, total_neg) word_chi2[word] score # 按卡方值降序取前k個 top_k sorted(word_chi2.items(), keylambda x: x[1], reverseTrue)[:k] return [word for word, _ in top_k]卡方計(jì)算的輸入數(shù)據(jù)結(jié)構(gòu)需要事先準(zhǔn)備好pos_docs和neg_docs是包含某個詞的所有文檔的集合不是詞頻計(jì)數(shù)。我給出的word_doc_map用文檔 ID 做標(biāo)識避免同一個文檔重復(fù)計(jì)數(shù)。k值可以直接替換成代碼里的f-5000參數(shù)如果你想測試不同特征數(shù)對模型效果的影響改這個值重跑即可。需要留意的是chi_square函數(shù)的四個變量a、b、c、d中c和d的值來自文檔總數(shù)減去包含該詞的文檔數(shù)需要確保文檔 ID 的集合運(yùn)算正確否則算出來的卡方值全部失真。特征選擇完成后每篇文檔就會被表示成一個 5000 維的向量維度上只有 0 和 1 兩種取值——1 表示該特征詞在這篇文檔中出現(xiàn)0 表示未出現(xiàn)。這種 one-hot 式表示法雖然有信息損失丟失了詞頻但在 SVM 和樸素貝葉斯這類模型上二值特征往往比原始詞頻特征更抗噪聲也是 LIBSVM 最常用的輸入格式。3.2 從原始文本到特征向量分詞、去停用詞、向量化的完整鏈路特征提取的完整鏈路如下原始語料 → 分詞 → 去停用詞 → 卡方特征選擇 → 特征向量化 → 輸出到 Excel 或 LIBSVM 格式文件。中文部分依賴結(jié)巴分詞英文部分按空格切分即可。停用詞表直接復(fù)用punctuation_dict.txt再加進(jìn)去一批無實(shí)際情感含義的高頻詞比如「的」「了」「是」「在」。# 標(biāo)準(zhǔn)的中文文本清洗與向量化流程 import jieba import re def load_stopwords(pathf_dict/punctuation_dict.txt): stopwords set() with open(path, r, encodingutf-8) as f: for line in f: word line.strip() if word: stopwords.add(word) # 補(bǔ)充常見無意義高頻詞 extra_stops {的, 了, 是, 在, 就, 都, 也, 很, 還, 被} stopwords.update(extra_stops) return stopwords stopwords load_stopwords() def preprocess_text(raw_text): 原始文本清洗與分詞 返回: 去停用詞后的詞列表 # 去除URL、用戶、特殊符號 text re.sub(rhttps?://\S|www\.\S, , raw_text) text re.sub(r\w, , text) # 去除所有非中英文、數(shù)字、常見標(biāo)點(diǎn)的字符 text re.sub(r[^\u4e00-\u9fa5a-zA-Z0-9\s。、], , text) # 結(jié)巴分詞 words jieba.lcut(text) # 去停用詞和單字 filtered [w for w in words if w.strip() and w not in stopwords and len(w) 1] return filtered def text_to_vector(tokens, feature_words): 把分詞結(jié)果轉(zhuǎn)成特征向量 tokens: preprocess_text 返回的詞列表 feature_words: select_top_k_features 選出的特征詞表 返回: 與 feature_words 等長的0/1列表 token_set set(tokens) return [1 if word in token_set else 0 for word in feature_words]preprocess_text里的正則表達(dá)式[^\u4e00-\u9fa5a-zA-Z0-9\s。、]會把表情符號、emoji 全部剔除。這里有一個權(quán)衡新浪微博等社交媒體數(shù)據(jù)中 emoji 本身攜帶情感信息全部剔除會損失信號。但這份源碼針對的是酒店、外賣、電影評論這三類文本中 emoji 占比很低剔除不會明顯影響效果。如果你想處理社交媒體評論可以考慮單獨(dú)把 emoji 映射成情感標(biāo)簽而不是直接刪掉。text_to_vector返回的是 0/1 向量長度為特征詞表的大小。5000 個特征對應(yīng) 5000 維向量每條樣本占用的內(nèi)存大約幾十 KB3000 條訓(xùn)練樣本全部加載也不到幾百 MB內(nèi)存完全不是問題。真正耗時的環(huán)節(jié)是卡方檢驗(yàn)時的詞-文檔映射構(gòu)建如果語料超過 5 萬條建議用稀疏矩陣來存儲文檔-詞頻關(guān)系否則內(nèi)存消耗會指數(shù)級增長。4. 四套分類器實(shí)現(xiàn)classifiers.py 與 tools.py 的核心邏輯源碼里的classifiers.py封裝了 k-NN、樸素貝葉斯、最大熵、SVM 四個分類器的實(shí)現(xiàn)tools.py負(fù)責(zé)數(shù)據(jù)處理和評估指標(biāo)計(jì)算。四套模型在同一條特征提取鏈路下運(yùn)行對比結(jié)果時才能公平反映模型本身的差異。每套模型都可以獨(dú)立運(yùn)行控制臺會輸出準(zhǔn)確率、召回率、F1 值等評估指標(biāo)。4.1 樸素貝葉斯與最大熵從概率視角看文本分類樸素貝葉斯假設(shè)特征之間相互獨(dú)立計(jì)算每個類別下特征向量的條件概率然后取后驗(yàn)概率最大的類別作為預(yù)測結(jié)果。實(shí)際代碼中為了避免概率為 0 的極端情況會做拉普拉斯平滑。最大熵模型則放棄了獨(dú)立性假設(shè)通過約束條件逐步迭代調(diào)整特征權(quán)重在梯度下降或迭代縮放算法的驅(qū)動下逼近最優(yōu)解。兩者在中小數(shù)據(jù)集上的準(zhǔn)確率通常很接近但最大熵的收斂速度和特征維度的關(guān)系更密切。# 樸素貝葉斯與最大熵分類器的核心實(shí)現(xiàn) import math from collections import defaultdict class NaiveBayesClassifier: def __init__(self, alpha1.0): self.alpha alpha # 拉普拉斯平滑系數(shù) self.prior {} # 先驗(yàn)概率 P(c) self.cond_prob {} # 條件概率 P(w|c) self.classes [] def fit(self, X, y, feature_words): X: 0/1特征矩陣list of list y: 標(biāo)簽列表 feature_words: 特征詞表 self.classes list(set(y)) n_samples len(y) # 計(jì)算先驗(yàn)概率 class_counts defaultdict(int) for label in y: class_counts[label] 1 for c in self.classes: self.prior[c] class_counts[c] / n_samples # 計(jì)算條件概率 P(word|c) # 先統(tǒng)計(jì)每個類別下每個詞出現(xiàn)的文檔數(shù) word_class_count defaultdict(lambda: defaultdict(int)) class_total_count defaultdict(int) for idx, label in enumerate(y): class_total_count[label] 1 for j, val in enumerate(X[idx]): if val 1: word_class_count[label][j] 1 # 應(yīng)用拉普拉斯平滑計(jì)算條件概率 vocab_size len(feature_words) for c in self.classes: self.cond_prob[c] {} total class_total_count[c] for j in range(vocab_size): count word_class_count[c][j] self.cond_prob[c][j] (count self.alpha) / (total self.alpha * vocab_size) def predict(self, X): predictions [] for x in X: best_c None best_score -float(inf) for c in self.classes: # 用對數(shù)概率避免下溢 log_prob math.log(self.prior[c]) for j, val in enumerate(x): if val 1: log_prob math.log(self.cond_prob[c][j]) if log_prob best_score: best_score log_prob best_c c predictions.append(best_c) return predictions拉普拉斯平滑系數(shù)alpha默認(rèn)取 1.0這是經(jīng)驗(yàn)值。alpha越大對未在訓(xùn)練集中出現(xiàn)的特征的懲罰越強(qiáng)。在實(shí)際項(xiàng)目中如果特征維度很高超過 1 萬建議把a(bǔ)lpha下調(diào)到 0.5 左右否則大量未出現(xiàn)的特征會把真實(shí)信號淹沒。預(yù)測階段使用對數(shù)概率累加而不是概率連乘是因?yàn)?5000 個 0/1 特征的連乘結(jié)果會極小直接浮點(diǎn)運(yùn)算會下溢成 0對數(shù)變換是必做的一步。最大熵的實(shí)現(xiàn)在思路上完全不同它定義特征函數(shù)通常是「特征詞是否出現(xiàn)」的指示函數(shù)然后通過迭代縮放算法反復(fù)修正特征權(quán)重使得模型的經(jīng)驗(yàn)分布與真實(shí)分布接近。實(shí)際應(yīng)用中最大熵的訓(xùn)練時間通常是樸素貝葉斯的 5 到 10 倍配合maxent_iteration.PNG那張圖可以看到迭代到 100 輪左右時模型已經(jīng)收斂再多迭代只是浪費(fèi)時間。4.2 SVM 與 k-NN邊界劃分與距離度量的兩種哲學(xué)SVM 通過尋找最大間隔超平面來區(qū)分正負(fù)類泛化能力強(qiáng)是文本分類里的首選模型之一。SVM-waimai-train-3000-test-1000-f-5000-C-150-2016-06-02-23-46-11-lin.xls里的C-150表示懲罰系數(shù)取 150lin表示線性核。C 值控制對誤分類樣本的容忍度C 越大對訓(xùn)練集擬合越充分但過大會導(dǎo)致過擬合C 越小模型越偏向簡單的超平面。150 這個值偏高說明作者更看重訓(xùn)練集上的準(zhǔn)確率。k-NN 則完全不同它不做任何訓(xùn)練直接拿測試樣本與全部訓(xùn)練樣本計(jì)算距離取距離最近的 K 個鄰居中占比最高的類別作為預(yù)測結(jié)果。距離度量方式一般采用歐氏距離或余弦相似度對于 0/1 特征向量余弦相似度通常比歐氏距離更合理——因?yàn)樗皇芟蛄块L度影響。# SVM與k-NN的調(diào)用封裝 from sklearn.svm import SVC from sklearn.neighbors import KNeighborsClassifier from sklearn.metrics import classification_report def train_svm(X_train, y_train, C150, kernellinear): 訓(xùn)練SVM分類器 C: 懲罰系數(shù) kernel: 核函數(shù)文本分類默認(rèn)線性核 clf SVC(CC, kernelkernel, probabilityFalse, tol0.001) clf.fit(X_train, y_train) return clf def train_knn(X_train, y_train, k5, metriccosine): 訓(xùn)練k-NN分類器 k: 鄰居數(shù) metric: 距離度量方式 clf KNeighborsClassifier(n_neighborsk, metricmetric) clf.fit(X_train, y_train) return clf # 訓(xùn)練與評估示例 def run_evaluation(X_train, y_train, X_test, y_test, model_typesvm): if model_type svm: model train_svm(X_train, y_train, C150, kernellinear) elif model_type knn: model train_knn(X_train, y_train, k5, metriccosine) else: raise ValueError(Unsupported model type) y_pred model.predict(X_test) # 打印完整評估報(bào)告包含precision、recall、f1-score print(classification_report(y_test, y_pred, target_names[負(fù)向, 正向])) return y_predSVM 的tol0.001是收斂容差值越小訓(xùn)練越充分但耗時越長。文本分類中 5000 維特征、3000 條訓(xùn)練樣本的數(shù)據(jù)量線性 SVM 訓(xùn)練時間通常在秒級。k-NN 的 K 值調(diào)參有個常見誤區(qū)——K 越大模型越平滑但過大的 K 會把邊界樣本也拉入決策導(dǎo)致準(zhǔn)確率下降。從該源碼的場景看K 取 5 是不錯的默認(rèn)值但通過交叉驗(yàn)證往往會發(fā)現(xiàn) K3 或 K11 在這個數(shù)據(jù)集上效果更好這個只能實(shí)測。傳統(tǒng)機(jī)器學(xué)習(xí)的真實(shí)優(yōu)勢在特征維度遠(yuǎn)小于樣本數(shù)量的場景下最能體現(xiàn)。5000 維特征對應(yīng) 3000 條訓(xùn)練樣本特征數(shù)大于樣本數(shù)此時 SVM 的泛化能力依賴正則化C 值需要調(diào)小一些才能避免過擬合。作者選了 C150 可能在自己的測試集上有過驗(yàn)證你也可以在 50 到 200 之間做一輪網(wǎng)格搜索。5. 評估結(jié)果與避坑記錄evaluation.PNG 背后的數(shù)據(jù)怎么看evaluation.PNG是四套模型在三個數(shù)據(jù)集上的綜合評估對比圖maxent_iteration.PNG展示最大熵模型每輪迭代的準(zhǔn)確率變化。評估階段最容易出的問題不在于模型代碼寫沒寫對而在于訓(xùn)練測試集劃分是否合理、評估指標(biāo)是否選對、特征生成過程是否泄漏了測試集信息。很多跑出來的結(jié)果虛高就是因?yàn)榇a里不經(jīng)意間把測試集的信息混進(jìn)了特征選擇環(huán)節(jié)。5.1 從文件名解析評估結(jié)果train/test 比例與特征數(shù)對準(zhǔn)確率的影響四個 Excel 文件分別對應(yīng)不同模型在不同數(shù)據(jù)集上的跑分結(jié)果文件名已經(jīng)透露了關(guān)鍵信息Bayes-hotel-train-2200-test-800-f-5000樸素貝葉斯酒店語料2200 訓(xùn)練 / 800 測試特征數(shù) 5000Bayes-movie2-train-700-test-300-f-5000樸素貝葉斯電影語料700 訓(xùn)練 / 300 測試特征數(shù) 5000SVM-waimai-train-3000-test-1000-f-5000-C-150-linSVM 線性核外賣語料3000 訓(xùn)練 / 1000 測試特征數(shù) 5000C150一眼能看出的規(guī)律是訓(xùn)練數(shù)據(jù)越多的數(shù)據(jù)集模型準(zhǔn)確率普遍更高。外賣數(shù)據(jù)集的 SVM 結(jié)果通常能做到 85% 以上酒店數(shù)據(jù)的貝葉斯結(jié)果在 80% 上下電影數(shù)據(jù)只有 700 條訓(xùn)練樣本準(zhǔn)確率會掉到 75% 左右。這不是模型不行而是數(shù)據(jù)量不足的必然結(jié)果。特征數(shù)從幾百逐步提升到 5000 的過程中準(zhǔn)確率通常先快速上升后趨于平穩(wěn)。chi1.png和chi2.png兩張圖應(yīng)該展示了不同特征數(shù)下的準(zhǔn)確率變化曲線可以看到 2000 個特征以后曲線基本走平。這意味著在實(shí)際項(xiàng)目中如果你計(jì)算資源有限用 2000 到 3000 個特征可以在幾乎不損失準(zhǔn)確率的情況下大幅縮短訓(xùn)練時間。評估指標(biāo)上不能只盯準(zhǔn)確率。當(dāng)正負(fù)樣本比例失衡時比如外賣評論中負(fù)向樣本只占 20%準(zhǔn)確率 80% 可能只是因?yàn)槟P桶阉袠颖径寂谐闪苏?。正確的做法是同時觀察 precision、recall、F1-score這三個指標(biāo)在classification_report的輸出中都有。如果只關(guān)注準(zhǔn)確率而在樣本不均衡的數(shù)據(jù)集上做評估結(jié)果毫無參考價值。5.2 避坑清單詞典匹配翻車的五個高頻原因以下踩坑記錄來自實(shí)際運(yùn)行這類源碼時最常見的報(bào)錯和異常結(jié)果每一條都對應(yīng)具體的現(xiàn)象和修復(fù)方式。踩坑一中文語料用英文分詞器特征詞表全是空現(xiàn)象跑完特征提取feature_words為空列表后續(xù)所有模型訓(xùn)練直接報(bào)維度為 0。原因源碼里中英文語料共用了同一套預(yù)處理邏輯如果默認(rèn)用空格分詞中文文本會被當(dāng)成長字符串整段保留卡方檢驗(yàn)后不可能選出合理特征。解決預(yù)處理入口處按語料語言分流中文走jieba.lcut英文走str.split()。一個可復(fù)用的判斷方式是統(tǒng)計(jì)文本中的字符類別占比中文字符占比超過 30% 就按中文流程處理。def smart_tokenize(text): chinese_chars len(re.findall(r[\u4e00-\u9fa5], text)) total_chars len(text.strip()) chinese_ratio chinese_chars / max(total_chars, 1) if chinese_ratio 0.3: return jieba.lcut(text) else: return text.lower().split()踩坑二否定詞翻轉(zhuǎn)整句把「不是不好」判成負(fù)向現(xiàn)象包含「不是不好」「不得不點(diǎn)贊」的句子被模型判為負(fù)向人工復(fù)核明顯錯誤。原因否定詞處理只做了簡單的全局翻轉(zhuǎn)遇到雙重否定時邏輯徹底顛倒。not或「不」后面不管隔多遠(yuǎn)都把情感值翻轉(zhuǎn)一次變成了反向的疊加。解決否定詞翻轉(zhuǎn)限定窗口范圍一般取否定詞后 2 到 3 個詞同時遇到兩個否定詞時視為正向表達(dá)。源碼中denial_dict.txt的覆蓋范圍也要確認(rèn)是否包含「無」「莫」「非」等變體。踩坑三訓(xùn)練測試集劃分前做了歸一化測試集信息泄漏現(xiàn)象模型在測試集上的準(zhǔn)確率高達(dá) 95%但換一批新數(shù)據(jù)掉到 70%明顯不合常理。原因特征縮放或卡方特征選擇時用了全量數(shù)據(jù)的統(tǒng)計(jì)信息再把這套參數(shù)應(yīng)用到測試集上等于測試集的信息提前參與了訓(xùn)練。解決先劃分訓(xùn)練集和測試集特征選擇只基于訓(xùn)練集做??ǚ綑z驗(yàn)、TF-IDF 擬合、歸一化參數(shù)都只在訓(xùn)練集上計(jì)算測試集直接用訓(xùn)練集生成的參數(shù)做變換。這是數(shù)據(jù)泄漏的重災(zāi)區(qū)也是初學(xué)最容易忽略的點(diǎn)。踩坑四conjunction_dict.txt沒有參與邏輯判斷現(xiàn)象詞典文件齊全但「雖然環(huán)境好但是服務(wù)差」這類轉(zhuǎn)折句被判為正向。原因轉(zhuǎn)折連詞的作用是改變句子的情感重心——「但是」之后的語義權(quán)重應(yīng)大于之前。如果代碼里只加載了轉(zhuǎn)折詞詞典卻沒有實(shí)現(xiàn)權(quán)重調(diào)整邏輯這些詞典形同虛設(shè)。解決在情感得分計(jì)算中遇到轉(zhuǎn)折詞后對前半句得分打折扣后半句權(quán)重加倍。折扣系數(shù)推薦 0.3 到 0.5具體值靠驗(yàn)證集調(diào)參。踩坑五Excel 結(jié)果文件打不開或中文亂碼現(xiàn)象生成的.xls文件用 Excel 打開后中文全部亂碼或者提示文件格式損壞。原因?qū)懭?Excel 時用了錯誤的編碼或者xls與xlsx格式混用。老版本xlwt庫寫.xls時只支持有限字符集。解決寫入時明確指定編碼為 UTF-8優(yōu)先輸出.xlsx格式。Python 側(cè)用openpyxl或pandas.to_excel()更穩(wěn)妥。這個坑不影響模型本身但會浪費(fèi)十幾分鐘排查時間。6. 進(jìn)階調(diào)參與驗(yàn)證從 Excel 結(jié)果反推四個模型的適用邊界源碼跑通只是第一步真正有價值的是搞清楚四套模型各自適合什么場景以及如何調(diào)整參數(shù)獲得更好的效果。你可以打開evaluation.PNG對照各模型的準(zhǔn)確率和 F1 值結(jié)合下述原則選擇自己的主力模型。以這份源碼的數(shù)據(jù)分布來看樸素貝葉斯適合小樣本高維特征場景SVM 適合樣本量中等但特征區(qū)分度高的場景最大熵適合需要概率輸出的場景k-NN 則最依賴距離度量和 K 值選擇。特征數(shù)量的敏感性檢驗(yàn)是個值得做的實(shí)驗(yàn)把代碼中的f-5000依次改成f-1000、f-3000、f-8000分別跑一輪評估。你會看到 1000 個特征時準(zhǔn)確率明顯下降3000 到 5000 區(qū)間最穩(wěn)定8000 之后可能不升反降——噪聲特征開始干擾模型。以代碼里的select_top_k_features函數(shù)為例把k參數(shù)暴露出來寫個循環(huán)自動做這個敏感性分析輸出一張「特征數(shù)-準(zhǔn)確率」對照表這比只跑一次默認(rèn)參數(shù)更能說明問題。K 值調(diào)優(yōu)是 k-NN 模型的必做項(xiàng)。代碼中的train_knn(X_train, y_train, k5, metriccosine)可以把k分別設(shè)成 3、5、7、11、15 跑一組對比配合交叉驗(yàn)證觀察曲線。通常 K 太小時噪聲干擾大太大時類別邊界被模糊。余弦相似度在這個場景下通常優(yōu)于歐氏距離因?yàn)?0/1 特征向量的歐氏距離受向量稀疏程度影響明顯而余弦相似度只看方向。SVM 的 C 值調(diào)整策略也有套路先用默認(rèn)值比如 1.0跑通記錄準(zhǔn)確率再按對數(shù)刻度嘗試 C0.1、1、10、100、200找到準(zhǔn)確率峰值所在區(qū)間后在相鄰值之間做細(xì)粒度搜索。線性核在文本分類中通常已經(jīng)足夠RBF 核雖然理論上可以擬合更復(fù)雜的邊界但參數(shù)搜索成本翻倍在小數(shù)據(jù)集上還容易過擬合。# 交叉驗(yàn)證選擇最優(yōu)C值 from sklearn.model_selection import cross_val_score import numpy as np def select_best_c(X, y, c_values): best_c None best_score 0.0 results {} for c in c_values: model SVC(Cc, kernellinear, tol0.001) scores cross_val_score(model, X, y, cv5, scoringaccuracy) mean_score np.mean(scores) results[c] mean_score print(fC{c:.1f}, 平均準(zhǔn)確率{mean_score:.4f}) if mean_score best_score: best_score mean_score best_c c return best_c, results # 候選C值按對數(shù)均勻分布 best_c, results select_best_c(X_train, y_train, [0.1, 0.5, 1.0, 5.0, 10.0, 50.0, 100.0, 200.0]) print(f最優(yōu)C值: {best_c})交叉驗(yàn)證的cv5表示把訓(xùn)練集切成 5 份輪流用 4 份訓(xùn)練 1 份驗(yàn)證。這比單次劃分訓(xùn)練測試集更可靠能減少劃分隨機(jī)性帶來的評估波動。需要注意的是交叉驗(yàn)證只能在訓(xùn)練集上做最終評估還是要用獨(dú)立的測試集。跑完上述調(diào)參之后記住一個原則這種詞典加傳統(tǒng)機(jī)器學(xué)習(xí)的方案準(zhǔn)確率上限大約在 88% 左右想突破這個瓶頸需要引入詞向量或深度學(xué)習(xí)模型。但在樣本量小于 1 萬、需要快速落地、要求結(jié)果可解釋的場景下這套方案依然是第一選擇。我自己做文本分類項(xiàng)目時不管最終要不要上深度學(xué)習(xí)都會先跑一遍這份 pipeline 作為 baseline——用傳統(tǒng)方法拿到參考分?jǐn)?shù)再決定后續(xù)要不要加復(fù)雜度。從那以后我每次做情感分析類項(xiàng)目都強(qiáng)制要求自己先復(fù)現(xiàn)這個流程確認(rèn)數(shù)據(jù)質(zhì)量沒問題再談模型升級這個習(xí)慣幫我避開過不少數(shù)據(jù)泄漏和特征工程的大坑。希望這份源碼的拆解筆記能幫你在情感分析這條路上少走一段彎路。本文還有配套的精品資源點(diǎn)擊獲取