
简介一套针对中文文本情感分析的完整可运行项目基于gensim Word2Vec与支持向量机SVM实现情感倾向分类面向自然语言处理初学者、算法工程师和研究者可直接用于电商评论、社交媒体舆情等场景的正面、负面与中性判断。资源包共9个文件主要包含npy向量数据、xls原始情感语料、pkl模型文件和py主脚本整体约64.21MB其中有两个xls情感标注数据、训练好的Word2Vec与SVM模型、测试向量及标签文件目录结构清晰便于按步骤复现。项目完整覆盖文本清洗去标点与停用词、Word2Vec词向量训练、文本向量化、SVM分类器构建、评估与参数优化等流程并已封装为可直接运行的Python代码具备高复用性。已有1814人学习使用适合希望快速上手词嵌入与机器学习情感分析同时减少环境配置与调参成本的读者可为目标领域快速搭建基线系统提供参考。1. gensim-word2vec SVM 做文本情感分析中小语料上的快速落地方案几千条已经标好正负情感的中文评论摆在面前老板要求当天看到结论。上 BERT 没有 GPU 也没有标注量手写规则又覆盖不住口语的千变万化。这种场景下用 gensim 训练 Word2Vec 词向量把每条评论切成词、去停用词、合成句向量再交给 SVM 做情感分类是我最常用的落地组合。它不需要 GPU不依赖几万条标注数据训练一次按分钟计算而且支持向量还能反查误判根因不是黑匣子。这套方案在中小规模文本情感分析上尤其能打。这篇笔记按可直接运行的完整流程来写把数据格式、分词、Word2Vec 参数、SVM 调参和避坑点串成一条线新手能从零复现熟手能直接抄参数和边界判断。2. 完整方案先看懂一条中文评论如何变成 SVM 能吃的向量2.1 数据格式与标签分布开工之前先检查这两件事先约定最通用的数据格式。CSV 两列第一列 text 存评论原文第二列 label 存情感标签1 代表正向0 代表负向。这个格式足够简单任何公开的中文情感数据集稍微整理一下就能变成这样后面所有代码也都围绕这个格式展开。import pandas as pd df pd.read_csv(reviews.csv, encodingutf-8-sig) print(df.head()) print(df[label].value_counts()) df df.dropna(subset[text]) df[label] df[label].astype(int)encodingutf-8-sig是为了兼容 Windows 下带 BOM 的 CSV如果直接用utf-8第一列列名经常会被读成\ufefftext后面所有取值都会踩坑。打印value_counts()是为了确认两个类别的比例如果差评只占 5%训练完的 SVC 很可能会把所有样本都预测成好评这时候需要在模型里设置class_weightbalanced让少数类获得更高的惩罚权重。这是最简单也最容易被忽略的处理方式不要等模型跑完发现结果一边倒再返工。2.2 从分词到分类四步流水线怎么组织整套方案的流水线是四步jieba 分词、去停用词、gensim 训练词向量并合成句向量、SVM 分类。建议先在心里把这条线拉直再去调任何参数因为后面几章的所有坑几乎都出在这四步的衔接处。jieba 分词把“这家店的服务态度真的很好”切成[这家店, 服务态度, 真的, 很好]。去停用词去掉“的”“了”“吗”这类没有语义、只占向量空间的高频虚词。Word2Vec 训练gensim 把每个词转成一个固定长度的稠密向量再把一条评论里所有词向量平均成一条句向量。SVM 分类把句向量作为特征训练一个二分类器。# 流程示意完整实现见第 3 章和第 4 章 df[words] df[text].apply(cut_to_words) w2v_model Word2Vec(df[words], vector_size100, ...) X np.vstack([sentence_to_vec(w, w2v_model) for w in df[words]]) X_train, X_test, y_train, y_test train_test_split(X, df[label], ...) svm.fit(X_train, y_train)这里有一个新手最容易纠结的问题为什么 Word2Vec 不放在train_test_split之后分别训练因为 Word2Vec 是无监督的完全不看 label让全语料参与词向量训练可以让低频词获得更多上下文这不属于测试集信息泄漏。真正的泄漏防线在 SVM 一侧GridSearchCV和fit只能接触X_train和y_train。反过来如果先把数据切了再各自训练词向量测试集里特有的词汇拿不到向量分类效果反而会明显变差。2.3 为什么选 SVM 而不是 CNN硬间隔、软间隔、求解方式的区别SVM 在二分类上的目标不是让训练误差为零而是找一个间隔最大的超平面。硬间隔假设样本严格可分但真实评论里大量存在噪声和错标所以实际都用软间隔引入松弛变量允许少部分样本落在错误一侧用惩罚系数 C 控制容忍程度。C 越大对训练集误分类越不宽容间隔越窄越容易过拟合C 越小边界越宽松泛化通常更好。SVM 的训练被转化成二次规划一般用 SMO 算法在支持向量上迭代求解不是神经网络那种用梯度下降逐层反传的过程。很多人刚接触时总拿它跟神经网络的梯度下降对比其实两者目标函数和求解方式都不一样正是这个区别让 SVM 在小样本、高维特征上不容易暴露过拟合问题。拿 SVM 和 CNN 的原理对比CNN 擅长自动抽取局部特征翻译到文本任务里就是在 n-gram 空间抓模式但要让卷积核学出有意义的短语组合通常需要更多训练样本。几千条评论对 CNN 来说太少对 SVM 却正好是舒适区。SVM 还有一个隐藏优势决策边界只由支持向量决定而不是全部样本。训练完之后可以把这些支持向量捞出来看原文它们就是最难分类、最容易被误判的样本。这个特性在第 6 章会派上大用场也是我把这套方案称为“可解释的快速文本情感分析方案”的原因。3. 训练 gensim Word2Vecjieba 分词、去停用词与词向量参数怎么设3.1 读 CSV、jieba 分词、过滤停用词的完整代码特征工程的质量直接决定 SVM 的上限而不是 SVM 的参数。这一步的目标是把每一条评论变成一个干净的词列表不干净的词表后面所有环节都是白费。import pandas as pd import jieba df pd.read_csv(reviews.csv, encodingutf-8-sig) df df.dropna(subset[text]) df[label] df[label].astype(int) stopwords set() with open(stopwords.txt, encodingutf-8) as f: for line in f: word line.strip() if word: stopwords.add(word) def cut_to_words(text): words jieba.lcut(str(text)) result [] for w in words: w w.strip().lower() if w and w not in stopwords and len(w) 1: result.append(w) return result df[words] df[text].apply(cut_to_words) sentences df[words].tolist()这段代码里的cut_to_words做了三件事去掉首尾空格、统一转小写、过滤停用词和单字词。统一小写是为了处理中英混排评论里的OK和ok避免同一个词被拆成两个向量。过滤单字词会牺牲掉“好”“差”“脏”这类单字情感词但保留它们又会把大量“我”“你”“都”这类单字虚词带进来。我一般倾向先过滤因为单字词的上下文信息太少Word2Vec 很难给它们学到稳定向量如果你的语料里单字情感词特别多可以把len(w) 1改成len(w.strip()) 1只过滤停用词表里的词。停用词表不需要贪大。几十到几百个高频虚词足够关键是别把否定词放进去。“不”“没”“别”如果被过滤掉情感方向直接反转这类错误比不过滤停用词更致命。建议停用词表只收“的、了、是、在、和、就、都、而、及、与、着、或、一个、没有”这类没有情感倾向的词拿不准的词宁可不放。3.2 Word2Vec 训练参数vector_size、window、min_count、sg 怎么选词列表准备好之后直接交给 gensim 训练。这段代码是核心参数建议尽量靠近下面这组再根据语料规模微调。from gensim.models import Word2Vec model Word2Vec( sentencessentences, vector_size100, window5, min_count2, sg1, epochs6, workers4, seed42, ) model.save(w2v.model)注意 gensim 4.0 之后的版本把训练轮数参数从iter改成了epochs网上大量旧教程还在用iter5直接复制会报TypeError。另外词向量索引建议统一用model.wv不要再用旧版model[词]的写法。参数常见取值调整方向vector_size100语料小用 64语料达到几万条可以提到 200 或 300window5短评 3~5长文本 8 附近太大容易引入无关词min_count2几千条语料设 2 或 3太小低频词学不充分sg11 用 skip-gram对低频词更友好0 用 CBOW训练更快epochs6语料少可以加到 8~10轮次太多反而会过拟合训练语料workers4按 CPU 核数设别超过物理核数word2vec的训练是无监督的所以全语料都能参与。min_count2意味着只出现过一次的词直接丢弃这是必须的一个只出现一次的词根本没有足够上下文去学习向量强行保留只会变成随机向量。sg1选择 skip-gram 是因为中文情感词往往是低频词skip-gram 对低频词的向量质量比 CBOW 好代价是训练时间略长。seed42固定随机种子保证每次训练结果可复现这一点在调试时非常重要不固定种子你调了半天 SVM 参数结果发现性能波动来自词向量初始化。3.3 把句子合成定长句向量等权平均的代码与边界SVM 要求每条样本是一个定长向量而一条评论的词数是可变的。最常见的做法是把句子中所有词的向量做等权平均得到一条句向量。这个方法简单效果在短文本上通常不差后面第 6 章再给加权版本。import numpy as np def sentence_to_vec(word_list, model, vector_size): vecs [] for w in word_list: if w in model.wv: vecs.append(model.wv[w]) if len(vecs) 0: return np.zeros(vector_size) return np.mean(vecs, axis0) X np.vstack([sentence_to_vec(ws, model, 100) for ws in df[words]]) print(X.shape)np.mean(vecs, axis0)是在词向量维度上做平均比如 100 维向量每条句子最终也是一个 100 维向量。X.shape应该是(样本数, 100)这就是 SVM 的输入。这段代码有一个边界情况如果某条评论分词后所有词都不在词表里vecs为空函数返回全零向量。零向量对 SVM 影响很大它会变成一个离原点很近的特殊样本干扰决策边界。遇到这种情况我一般直接把这条样本从训练集里剔除或者用一个全局词向量的平均值填充不要留全零。另一个边界是model.wv里没有的词会被直接跳过。对训练集来说这是正常现象因为min_count2已经过滤了低频词但如果测试阶段来了一个完全陌生的词它也会被跳过。这也是为什么这套方案对“训练时没见过的新词”比较脆弱后面调阈值时要记住这个特性。4. 训练 SVM交叉验证调参、核函数选择与评估不看准确率4.1 数据划分与标准化先 fit 训练集再 transform 测试集句向量生成后进入 SVM 环节。第一个动作不是直接训练而是划分数据和标准化。顺序错了测试集的信息就会泄漏到训练流程里交叉验证分数会虚高。from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler y df[label].values X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, stratifyy, random_state42 ) scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test)stratifyy是分层抽样保证训练集和测试集里正负样本比例一致特别是差评占比低的时候这一参数必须加。StandardScaler对每个维度做零均值单位方差原因在于 SVM 的软间隔和 RBF 核都依赖样本之间的距离Word2Vec 各维度的尺度如果差异很大距离度量会被少数几个大尺度维度主导。这里最忌讳的写法是对X_train和X_test分别调用fit_transform那会让测试集单独学一套均值和方差等于变相引入测试集信息。正确做法是训练集fit_transform测试集只transform。4.2 用 GridSearchCV 搜索 C 和 gamma参数别拍脑袋SVC 有四个参数最影响结果C、gamma、kernel、class_weight。与其凭经验拍脑袋不如直接在五折交叉验证上搜一遍。几千条样本的网格搜索最多几十秒完全跑得起。from sklearn.svm import SVC from sklearn.model_selection import GridSearchCV param_grid { C: [0.1, 1, 10], gamma: [0.01, 0.1, 1], kernel: [rbf], } svc SVC(class_weightbalanced, random_state42) grid GridSearchCV(svc, param_grid, cv5, scoringf1_macro, n_jobs-1) grid.fit(X_train_scaled, y_train) print(grid.best_params_) best_svm grid.best_estimator_参数调参方向说明C0.1~10 起步C 越大越不放过训练集错误C 太小会欠拟合gamma0.01~1 起步RBF 核的半径参数太大会过拟合太小会欠拟合kernelrbf 或 linear特征维度高且线性可分时linear 更稳class_weightbalanced标签不均衡时必开我用scoringf1_macro而不是默认的accuracy是因为准确率在类别不平衡时没有意义。假设差评只占 5%模型全预测成好评也能拿到 95% 准确率但这个模型对业务毫无价值。f1_macro对两个类别的 F1 取平均任何一个类别崩了分数都会明显下降。搜索结果里C10, gamma0.1这类组合经常出现但如果数据集很小我会倾向于选择 C 更小的那一档因为小样本上边界越宽泛线上泛化越好。4.3 精确率、召回率与 F1对差评误判和误杀分别有多痛训练完之后不要只打印一行 accuracy直接用classification_report把两个类别的精确率、召回率、F1 全部打出来。情感分析业务里“把好评判成差评”和“漏掉差评”的代价往往完全不同。from sklearn.metrics import classification_report, confusion_matrix y_pred grid.predict(X_test_scaled) print(classification_report(y_test, y_pred, target_names[好评, 差评])) print(confusion_matrix(y_test, y_pred))以差评这个类别为例精确率是“预测为差评的样本里真的差评的比例”召回率是“真实差评里被找出来的比例”。电商场景里漏掉一个差评会让客诉升级这时候优先保召回率反过来如果差评审核需要人工介入误杀太多好评会增加人工成本这时候优先保精确率。classification_report里的 macro avg 和 weighted avg 也要看一眼两者差异大说明模型在某个类别上明显偏科。如果发现差评召回率低常见做法不是调 SVM 的 C而是先看标签分布和特征质量。标签分布用class_weight已经处理过剩下的瓶颈多半在 3.2 节的词向量参数或 3.3 节的句向量生成方式上。这也是为什么我把调参顺序放在特征工程之后Word2Vec 没训练好SVM 调出花来也没用。5. 避坑文本情感分析里我踩过的 5 个翻车点现象、原因、解决5.1 CSV 中文乱码读进来全是“锟斤拷”模型跟着学乱码现象print(df.head())输出一堆乱码分词结果里全是“锟斤拷”“烫烫烫”这类无意义符号标签分布正常但准确率极低。原因文件实际编码和read_csv指定的编码不一致。Windows 下 Excel 另存的 CSV 默认是 GBK用encodingutf-8读取就会乱码反过来Linux 下生成的 UTF-8 文件用 GBK 读同样会花。解决统一用encodingutf-8-sig读入这个编码兼容 UTF-8 和带 BOM 的 UTF-8。如果读出来还是乱码先用文本编辑器打开 CSV 另存为 UTF-8 编码再重新读。调试时可以打印一条原文和对应分词结果乱码问题必须在第一步消灭否则后面所有词向量和 SVM 都是在噪声上学习。5.2 分词后不过停用词向量空间被“的、了、吗”塞满现象Word2Vec 的most_similar结果里“的”和“很好”的相似度很高SVM 的分类依据看起来像句子长度而不是语义。原因高频虚词占了语料的大头Word2Vec 在训练时被这些虚词主导情感词之间的语义关系被稀释。等权平均句向量时每个虚词都贡献一个维度“差”和“好”的区分度被大量无意义向量拉低。解决在 3.1 节的分词函数里加停用词过滤。停用词表宁缺毋滥绝对不要COPY网上那种包含“不”“没”“无”的巨型词表那会把否定信息全部删掉情感方向直接反转。一个几十词的常见虚词表足够大部分场景。5.3 min_count 设太小低频词向量没学出来还拖慢训练现象词表膨胀到几万训练时间翻倍但model.wv[惊艳]的向量和随机初始化差别不大SVM 分数反而下降。原因min_count1时只出现一次的词没有足够的上下文窗口来学习向量训练出来的基本是噪声。这些噪声向量进入句向量平均后把真实信号冲淡了。解决把min_count设成 2 或 3。可以先跑一句from collections import Counter; print(Counter([w for ws in df[words] for w in ws]).most_common(20))看词频分布再决定阈值。词频不到 2 的词直接丢弃不要心疼。如果某个低频词对你特别重要正确做法是扩充语料而不是调低min_count。5.4 等权平均句向量“不”和“没”被高频词稀释现象“这家店不好吃”和“这家店很好吃”的句向量相似度极高SVM 经常把带否定词的差评预测成好评。原因等权平均把所有词一视同仁。“好吃”的情感分量强出现一次就把“不”的否定意义压过去了。一次否定词在一句六个词里只有六分之一的权重平均向量几乎感觉不到它的存在。解决不建议一上来就换模型先做两件事。第一分词阶段保留否定词停用词表里绝不能有“不”。第二把否定词和后面的核心词做拼接比如“不好吃”“不太行”合并成一个词再去训练 Word2Vec这样“不”和“好吃”的共现关系被锁定在同一个 token 里。这一步非常玄学但治否定词稀释确实有效。更系统的做法是第 6 章的 TF-IDF 加权给“不”“没”这类转折词更高的区分权重。5.5 gensim 4.0 后 API 变化加载旧模型报错与参数名失效现象按旧教程写Word2Vec(sentences, iter5)直接报TypeError使用model[词]报KeyError加载别人给的模型文件报各种AttributeError。原因gensim 4.0 把训练轮数参数iter改名成了epochs词向量的索引方式统一收敛到model.wv旧版本保存的模型文件格式也有变化。解决新代码统一用model.wv[word]和epochs。保存模型用model.save(w2v.model)加载用Word2Vec.load(w2v.model)如果拿到的是纯词向量文本文件用from gensim.models import KeyedVectors; KeyedVectors.load_word2vec_format(w2v.txt, binaryFalse)加载。不要拿 gensim 3.x 保存的模型在 4.x 里硬加载直接让提供方重新导出一份或者你自己用新版本重训一版。6. 进阶技巧输出情感置信度并用支持向量找误判根因6.1 从“差评”变成“差评概率”用 probabilityTrue 校准SVM 默认只输出硬分类标签业务侧做差评拦截时需要的是置信度。把probabilityTrue加上SVC 会用 Platt 缩放把决策值映射到 0~1输出一个近似概率。阈值可以不用 0.5比如电商客服压力大时把差评拦截阈值调低到 0.4宁可多拦几条再人工复核也不要漏掉真正的差评。final_svm SVC( Cgrid.best_params_[C], gammagrid.best_params_[gamma], kernelrbf, class_weightbalanced, probabilityTrue, random_state42, ) final_svm.fit(X_train_scaled, y_train) proba final_svm.predict_proba(X_test_scaled)[:, 1]注意probabilityTrue会让训练时间变长因为内部要做交叉验证来校准概率但几千条样本完全扛得住。这个概率不是真正的后验概率只能用于排序和定阈值定阈值时在验证集上算一遍不同阈值下的精确率和召回率选一个业务能接受的点。6.2 把支持向量翻出来SVM 告诉你哪些样本决定了边界SVM 的决策边界只由支持向量决定这意味着训练完成后模型可以告诉你它最“纠结”的样本是哪几条。调参调不动的时候回去看支持向量原文往往比随机抽错误样本更有价值。support_idx final_svm.support_ for idx in support_idx[:10]: print(df.iloc[idx][text], df.iloc[idx][label])这里有个坑support_返回的是训练集里的位置索引如果你在划分时没有保留训练集的原始 DataFrame 位置df.iloc[idx]会错位。我一般会在train_test_split之前给df加一列sample_id划分后带着它走反查就有锚点。支持向量里出现最多的通常是带否定词、程度副词或者口语省略的句子这些正是 5.4 节说的难点样本。每次我调不动 SVM 参数时翻支持向量到第 20 条左右就会找到问题根源不是停用词误杀就是分词边界不对。这已经成了我固定的排查习惯比盲目改 C 和 gamma 有效得多。希望帮到你。本文还有配套的精品资源点击获取