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