別實(shí)戰(zhàn):TF-IDF與SVM中文文本分類項(xiàng)目拆解)
簡(jiǎn)介這份資源面向計(jì)算機(jī)相關(guān)專業(yè)本科生及NLP入門學(xué)習(xí)者提供一套完整的中文文本分類實(shí)戰(zhàn)方案以垃圾短信識(shí)別為具體場(chǎng)景幫助讀者理解從數(shù)據(jù)到模型落地的全流程。壓縮包共8個(gè)文件約38.02MB包含3個(gè)txt數(shù)據(jù)與停用詞文件、2個(gè)pkl序列化模型、1個(gè)Python訓(xùn)練腳本、1份Markdown說(shuō)明文檔及1張流程圖覆蓋數(shù)據(jù)、模型與代碼三類核心內(nèi)容。數(shù)據(jù)集采用“標(biāo)簽\t文本”格式正樣本為垃圾短信、負(fù)樣本為正常短信便于直接訓(xùn)練與評(píng)估。技術(shù)?;赑ython3.6、jieba與Scikit-learn默認(rèn)使用SVM支持向量機(jī)完成分類并可根據(jù)需要替換為其他模型運(yùn)行train.py即可啟動(dòng)訓(xùn)練。目前已有4450人學(xué)習(xí)下載適合作為畢業(yè)設(shè)計(jì)參考、課程實(shí)踐或NLP分類任務(wù)的入門模板幫助讀者快速掌握中文分詞、特征提取與模型訓(xùn)練的關(guān)鍵環(huán)節(jié)。1. 垃圾短信識(shí)別項(xiàng)目拆包一份能跑通的 NLP 中文文本分類實(shí)戰(zhàn)資源拿到一個(gè)壓縮包解壓后看到train.py、tfidf.pkl、svm_model.pkl和兩個(gè) txt 數(shù)據(jù)文件這種結(jié)構(gòu)其實(shí)比很多“大而全”的畢業(yè)設(shè)計(jì)清爽得多。它解決的是一個(gè)非常具體的問(wèn)題給你一條中文短信判斷它是垃圾短信還是正常短信。標(biāo)簽只有 0 和 1正樣本 1 代表垃圾短信負(fù)樣本 0 代表正常短信數(shù)據(jù)格式是“標(biāo)簽 Tab 文本”。適合誰(shuí)正在做 NLP 中文文本分類畢設(shè)、需要快速?gòu)?fù)現(xiàn)一條完整 baseline 的人或者想拿 SVM TF-IDF 這套經(jīng)典組合練手、后面再換模型的從業(yè)者。它不炫技但每一步都能落地。這個(gè)資源的核心鏈路是原始短信文本 → jieba 分詞 → 去停用詞 → TF-IDF 向量化 → SVM 分類 → 模型持久化。環(huán)境依賴只有 Python3.6、jieba、Scikit-learn沒(méi)有 GPU 要求沒(méi)有深度學(xué)習(xí)框架一臺(tái)普通筆記本就能跑完。train.py是唯一入口tfidf.pkl和svm_model.pkl是訓(xùn)練后落盤的向量器和分類器hit_stopwords.txt是停用詞表test.txt和train.txt是數(shù)據(jù)劃分。下面按“先理解為什么這么選再動(dòng)手復(fù)現(xiàn)最后看坑”的順序拆開(kāi)講。2. 數(shù)據(jù)格式與預(yù)處理從“標(biāo)簽\t文本”到干凈語(yǔ)料2.1 為什么先看數(shù)據(jù)格式而不是先跑代碼很多畢設(shè)項(xiàng)目翻車不是模型不行是數(shù)據(jù)讀進(jìn)來(lái)就錯(cuò)了。這個(gè)資源的數(shù)據(jù)格式是標(biāo)簽\t文本標(biāo)簽和文本之間用制表符分隔。如果你用空格切分遇到短信正文里本身帶空格的情況標(biāo)簽列就會(huì)錯(cuò)位。常見(jiàn)做法是用split(\t, 1)只切第一個(gè) Tab保證后面文本原樣保留。另外要確認(rèn)文件編碼中文短信數(shù)據(jù)常見(jiàn) UTF-8但 Windows 下可能是 GBK讀的時(shí)候加encodingutf-8或encodinggbk試一下報(bào)UnicodeDecodeError就換。def load_data(file_path): labels, texts [], [] with open(file_path, r, encodingutf-8) as f: for line in f: line line.strip() if not line: continue # 只按第一個(gè) Tab 切分防止文本內(nèi) Tab 干擾 parts line.split(\t, 1) if len(parts) ! 2: continue label, text parts labels.append(int(label)) texts.append(text) return texts, labels邏輯說(shuō)明逐行讀取跳過(guò)空行split(\t, 1)保證標(biāo)簽和文本正確分離標(biāo)簽轉(zhuǎn) int 方便后續(xù) SVM 訓(xùn)練。參數(shù)說(shuō)明file_path傳train.txt或test.txt如果數(shù)據(jù)里有異常行l(wèi)en(parts) ! 2直接跳過(guò)避免一條臟數(shù)據(jù)把整個(gè)訓(xùn)練打斷。2.2 jieba 分詞與停用詞過(guò)濾的實(shí)操細(xì)節(jié)中文文本分類繞不開(kāi)分詞。這個(gè)資源用 jieba默認(rèn)精確模式即可。停用詞表hit_stopwords.txt里通常是“的、了、在、是”這類高頻但無(wú)區(qū)分度的詞。過(guò)濾停用詞能降維但注意別把否定詞“不、沒(méi)、別”誤刪垃圾短信里“不需要”“沒(méi)興趣”這類表達(dá)有信號(hào)。我一般會(huì)先看一眼停用詞表里有沒(méi)有否定詞有就手動(dòng)移出來(lái)。import jieba def load_stopwords(stopwords_path): with open(stopwords_path, r, encodingutf-8) as f: return set(line.strip() for line in f if line.strip()) def preprocess(texts, stopwords): processed [] for text in texts: # 精確模式分詞適合短文本 words jieba.lcut(text) # 過(guò)濾停用詞和單字單字噪聲大 words [w for w in words if w not in stopwords and len(w) 1] processed.append( .join(words)) return processed邏輯說(shuō)明jieba.lcut返回列表比jieba.cut直接可用過(guò)濾停用詞同時(shí)過(guò)濾長(zhǎng)度為 1 的字減少噪聲。參數(shù)說(shuō)明stopwords是 set查找 O(1)len(w) 1這個(gè)閾值可以調(diào)如果發(fā)現(xiàn)“貸”“款”這類單字有區(qū)分度可以放開(kāi)到 1但通常短文本里單字噪聲更大。注意停用詞表不是越大越好。我見(jiàn)過(guò)有人拿通用停用詞表直接套短信場(chǎng)景結(jié)果“中獎(jiǎng)”“轉(zhuǎn)賬”這類關(guān)鍵詞被誤傷召回率掉得厲害。建議先跑一版看混淆矩陣再?zèng)Q定要不要精簡(jiǎn)。3. TF-IDF 向量化與 SVM 訓(xùn)練參數(shù)怎么設(shè)、模型怎么存3.1 TF-IDF 的max_features和ngram_range怎么定TF-IDF 把分詞后的文本轉(zhuǎn)成數(shù)值向量。TfidfVectorizer有幾個(gè)關(guān)鍵參數(shù)max_features控制詞表大小ngram_range控制是否用二元詞組。短信文本短一元詞通常夠用但“中獎(jiǎng) 鏈接”這種組合有信號(hào)可以試(1,2)。max_features設(shè)太大容易過(guò)擬合設(shè)太小丟信息常見(jiàn)做法是從 5000 起步看驗(yàn)證集效果再調(diào)。from sklearn.feature_extraction.text import TfidfVectorizer def build_vectorizer(): return TfidfVectorizer( max_features5000, # 詞表上限防止維度爆炸 ngram_range(1, 2), # 一元和二元詞組 min_df2, # 至少出現(xiàn)在 2 篇文檔中過(guò)濾低頻噪聲 max_df0.9 # 出現(xiàn)在 90% 以上文檔中的詞視為無(wú)區(qū)分度 )邏輯說(shuō)明min_df2過(guò)濾只出現(xiàn)一次的詞max_df0.9過(guò)濾幾乎每篇都有的詞這兩個(gè)參數(shù)配合停用詞表能進(jìn)一步降噪。參數(shù)說(shuō)明max_features根據(jù)數(shù)據(jù)量調(diào)幾千條短信 5000 夠用ngram_range(1,2)會(huì)讓特征數(shù)翻倍訓(xùn)練變慢如果時(shí)間緊可以先(1,1)。3.2 SVM 訓(xùn)練與模型持久化train.py里該盯哪幾行SVM 用LinearSVC還是SVC(kernellinear)數(shù)據(jù)量幾千到幾萬(wàn)LinearSVC更快SVC配合probabilityTrue能輸出概率但慢。這個(gè)資源用的是 SVM常見(jiàn)做法是LinearSVC。訓(xùn)練完用pickle把 vectorizer 和 model 分別存成tfidf.pkl和svm_model.pkl預(yù)測(cè)時(shí)先加載 vectorizer 再 transform順序不能反。import pickle from sklearn.svm import LinearSVC from sklearn.metrics import classification_report def train_and_save(train_texts, train_labels, test_texts, test_labels): vectorizer build_vectorizer() # 注意fit_transform 只在訓(xùn)練集上做測(cè)試集只能 transform X_train vectorizer.fit_transform(train_texts) X_test vectorizer.transform(test_texts) model LinearSVC(C1.0, class_weightbalanced) model.fit(X_train, train_labels) preds model.predict(X_test) print(classification_report(test_labels, preds)) # 持久化預(yù)測(cè)時(shí)按同樣順序加載 with open(tfidf.pkl, wb) as f: pickle.dump(vectorizer, f) with open(svm_model.pkl, wb) as f: pickle.dump(model, f)邏輯說(shuō)明fit_transform只在訓(xùn)練集調(diào)用測(cè)試集用transform否則數(shù)據(jù)泄漏評(píng)估結(jié)果虛高。class_weightbalanced在正負(fù)樣本不均衡時(shí)有用垃圾短信通常少于正常短信。參數(shù)說(shuō)明C是正則化參數(shù)越大越容易過(guò)擬合從 1.0 開(kāi)始調(diào)classification_report看 precision、recall、f1垃圾短信識(shí)別更關(guān)注 recall漏判比誤判代價(jià)高。提示pickle存模型有版本兼容問(wèn)題。Python3.6 存的 pkl 在 3.8 以上可能加載報(bào)錯(cuò)換環(huán)境時(shí)重新訓(xùn)練一遍比折騰兼容更省事。4. 避坑與排查跑train.py時(shí)最容易翻車的五個(gè)地方4.1 現(xiàn)象報(bào)UnicodeDecodeError: utf-8 codec cant decode byte原因數(shù)據(jù)文件不是 UTF-8 編碼Windows 下常見(jiàn) GBK。解決把open里的encodingutf-8改成encodinggbk或者用chardet檢測(cè)編碼后統(tǒng)一轉(zhuǎn) UTF-8。4.2 現(xiàn)象訓(xùn)練集準(zhǔn)確率 99%測(cè)試集只有 60%原因fit_transform用在了全量數(shù)據(jù)上或者停用詞表把關(guān)鍵信號(hào)詞過(guò)濾了。解決檢查代碼里fit_transform是否只作用于訓(xùn)練集打印測(cè)試集里被過(guò)濾的詞看有沒(méi)有“中獎(jiǎng)”“轉(zhuǎn)賬”這類關(guān)鍵詞被誤刪。4.3 現(xiàn)象pickle.load報(bào)ModuleNotFoundError: No module named sklearn.svm._classes原因訓(xùn)練和預(yù)測(cè)的 Scikit-learn 版本不一致舊版 pkl 在新版加載失敗。解決統(tǒng)一環(huán)境版本或者不加載 pkl直接用train.py重新訓(xùn)練并預(yù)測(cè)。畢設(shè)場(chǎng)景下重新訓(xùn)練成本很低。4.4 現(xiàn)象預(yù)測(cè)時(shí)transform報(bào)dimension mismatch原因加載的tfidf.pkl和當(dāng)前文本預(yù)處理方式不一致比如訓(xùn)練時(shí)用了二元詞組預(yù)測(cè)時(shí)只分詞沒(méi)保留詞組。解決預(yù)測(cè)代碼必須復(fù)用訓(xùn)練時(shí)的preprocess函數(shù)和同一個(gè) vectorizer不能重新fit。4.5 現(xiàn)象jieba分詞后全是單字特征沒(méi)區(qū)分度原因短信文本短jieba 默認(rèn)詞典對(duì)網(wǎng)絡(luò)新詞覆蓋不夠比如“返現(xiàn)”“秒殺”可能被切碎。解決加自定義詞典jieba.load_userdict(userdict.txt)把業(yè)務(wù)關(guān)鍵詞加進(jìn)去或者改用jieba.lcut_for_search試試效果。5. 換模型與調(diào)參進(jìn)階從 SVM 到樸素貝葉斯、XGBoost 的對(duì)比驗(yàn)證5.1 為什么先跑 SVM 再換模型SVM 在小樣本、高維稀疏文本上表現(xiàn)穩(wěn)定TF-IDF 加LinearSVC是經(jīng)典 baseline。但畢設(shè)如果只做一個(gè)模型答辯時(shí)容易被問(wèn)“為什么不用別的”。我一般會(huì)在這個(gè)資源基礎(chǔ)上再跑兩個(gè)對(duì)比MultinomialNB和XGBoost。樸素貝葉斯訓(xùn)練極快適合做下限參考XGBoost 在特征工程到位時(shí)上限更高但需要把稀疏矩陣轉(zhuǎn)成稠密或直接用scipy.sparse支持。下面是一個(gè)對(duì)比腳本的骨架。from sklearn.naive_bayes import MultinomialNB from sklearn.metrics import f1_score import xgboost as xgb def compare_models(X_train, y_train, X_test, y_test): results {} # SVM 基準(zhǔn) svm LinearSVC(C1.0, class_weightbalanced) svm.fit(X_train, y_train) results[SVM] f1_score(y_test, svm.predict(X_test)) # 樸素貝葉斯 nb MultinomialNB(alpha0.1) nb.fit(X_train, y_train) results[NB] f1_score(y_test, nb.predict(X_test)) # XGBoost注意稀疏矩陣直接支持 xgb_clf xgb.XGBClassifier( n_estimators100, max_depth6, learning_rate0.1, eval_metriclogloss ) xgb_clf.fit(X_train, y_train) results[XGBoost] f1_score(y_test, xgb_clf.predict(X_test)) return results邏輯說(shuō)明統(tǒng)一用 f1 對(duì)比垃圾短信識(shí)別里 f1 比準(zhǔn)確率更能反映漏判和誤判的平衡。參數(shù)說(shuō)明MultinomialNB的alpha是平滑系數(shù)0.1 到 1.0 之間調(diào)XGBoost的n_estimators和max_depth是主要調(diào)參對(duì)象數(shù)據(jù)量小的時(shí)候max_depth6可能過(guò)擬合降到 3 到 4 試試。5.2 驗(yàn)證方法別只看一次劃分的測(cè)試集單次train_test_split的結(jié)果波動(dòng)大尤其是數(shù)據(jù)量幾千條時(shí)。我習(xí)慣跑 5 折交叉驗(yàn)證看 f1 的均值和方差。如果方差大說(shuō)明模型不穩(wěn)定要么加數(shù)據(jù)要么簡(jiǎn)化特征。另外垃圾短信識(shí)別要單獨(dú)看召回率因?yàn)槁┡幸粭l垃圾短信可能比誤判一條正常短信代價(jià)高。用cross_val_score時(shí)指定scoringf1再手動(dòng)算 recall。from sklearn.model_selection import cross_val_score def cross_validate(model, X, y, cv5): f1_scores cross_val_score(model, X, y, cvcv, scoringf1) recall_scores cross_val_score(model, X, y, cvcv, scoringrecall) print(fF1: {f1_scores.mean():.4f} ± {f1_scores.std():.4f}) print(fRecall: {recall_scores.mean():.4f} ± {recall_scores.std():.4f})邏輯說(shuō)明cross_val_score自動(dòng)做 K 折返回每折得分。參數(shù)說(shuō)明cv5是折數(shù)數(shù)據(jù)量小可以設(shè) 10但每折訓(xùn)練集更小scoringrecall直接看正樣本召回。5.3 一個(gè)具體技巧用Pipeline把預(yù)處理和模型串起來(lái)這個(gè)資源里預(yù)處理、向量化、訓(xùn)練是分開(kāi)寫的改參數(shù)要?jiǎng)雍脦滋?。我后?lái)習(xí)慣用Pipeline把TfidfVectorizer和LinearSVC包在一起交叉驗(yàn)證和網(wǎng)格搜索都方便還能避免數(shù)據(jù)泄漏。下面是我在這個(gè)項(xiàng)目上改過(guò)的版本直接替換train.py里的訓(xùn)練部分即可。from sklearn.pipeline import Pipeline from sklearn.model_selection import GridSearchCV def build_pipeline(): return Pipeline([ (tfidf, TfidfVectorizer(max_features5000, ngram_range(1,2))), (svm, LinearSVC(class_weightbalanced)) ]) def grid_search(X_train, y_train): pipeline build_pipeline() param_grid { tfidf__max_features: [3000, 5000, 8000], svm__C: [0.1, 1.0, 10.0] } grid GridSearchCV(pipeline, param_grid, cv5, scoringf1, n_jobs-1) grid.fit(X_train, y_train) print(grid.best_params_) return grid.best_estimator_邏輯說(shuō)明Pipeline保證fit時(shí) vectorizer 只 fit 訓(xùn)練折predict時(shí)自動(dòng) transform杜絕泄漏。GridSearchCV在 5 折上搜參數(shù)n_jobs-1用滿 CPU。參數(shù)說(shuō)明param_grid里tfidf__max_features和svm__C是重點(diǎn)ngram_range也可以加進(jìn)去搜但組合爆炸先固定(1,2)。從那以后我每次拿到文本分類項(xiàng)目都強(qiáng)制先跑一遍Pipeline加交叉驗(yàn)證再去看單次劃分的結(jié)果。單次高分可能是運(yùn)氣交叉驗(yàn)證的均值和方差才是真實(shí)水平。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取