
简介这份课程设计资源面向NLP入门与进阶学习者围绕电商评论情感分析任务完整演示如何用Word2Vec将评论文本转为词向量、再以SVM完成正负情感分类。包内共18个文件含6个csv数据集与划分结果、5个py脚本数据清洗、词向量训练、模型训练与测试、4个npy特征数组、1个pkl已训练模型及说明文档压缩包约28.14MB结构清晰、可直接运行。已有354人学习下载。读者可据此掌握停用词过滤、词干还原等预处理流程理解CBOW与Skip-gram的向量表示差异并通过交叉验证调整惩罚项C与核函数参数γ最后用准确率、召回率、F1分数评估模型效果。资源还预留了虚假评论检测与实时评论分析的拓展思路适合作为课程设计、毕业项目或NLP实战练手的参考方案。1. 电商评论情感分析Word2VecSVM 这条老路线为什么还值得做电商评论的情感分析说白了就是把「好评」「差评」「中评」这些带情绪的文本自动判成正面或负面。你可能觉得这活儿早被 BERT 和大模型包圆了但真到课程设计、小数据集、离线部署、算力受限的场景里Word2Vec SVM 这套组合依然是性价比最高的起点。它训练快、可解释、对几千到几万条评论的数据量足够友好一台普通笔记本就能跑完全流程。更关键的是这条链路把「文本怎么变成向量」「分类边界怎么找」两个核心问题讲得明明白白做完一遍你对情感分析的理解会比直接调库深得多。这篇笔记就按「数据怎么洗 → 词向量怎么训 → SVM 怎么调 → 坑在哪」的顺序把完整代码和参数设置讲透新手能照着复现熟手能看到边界和调参空间。2. 从原始评论到可训练语料清洗、分词与标签对齐2.1 电商评论数据的三个脏点电商评论和标准影评数据集不一样它的脏是「业务脏」。第一类是默认好评比如「此用户没有填写评价」「好评」「系统默认好评」这些文本没有情感信息标签却是正面直接喂进去就是噪声。第二类是刷单式重复同一段话在多个商品下反复出现或者「质量很好质量很好质量很好」这种堆砌会让词频统计失真。第三类是标签不均衡真实数据里正面评论往往占七成以上负面样本少SVM 会偏向多数类。常见做法是先做一轮规则过滤文本长度小于 4 个字符的丢掉命中默认好评模板的丢掉完全重复的文本只保留一条。标签方面如果原始是 1-5 星一般把 4-5 星归为正面、1-2 星归为负面3 星中性评论直接丢弃——因为中性样本在二分类里会拉低边界清晰度除非你做三分类。import pandas as pd import re # 读取原始评论假设字段为 comment 和 star df pd.read_csv(raw_comments.csv) # 1. 丢弃空值和过短文本 df df.dropna(subset[comment]) df df[df[comment].str.len() 4] # 2. 过滤默认好评模板 default_patterns [此用户没有填写评价, 系统默认好评, 好评, 默认好评] pattern |.join(default_patterns) df df[~df[comment].str.contains(pattern, naFalse)] # 3. 去重 df df.drop_duplicates(subset[comment]) # 4. 星级映射为二分类标签丢弃中性 def map_label(star): if star 4: return 1 elif star 2: return 0 else: return None df[label] df[star].apply(map_label) df df.dropna(subset[label]) df[label] df[label].astype(int) print(df[label].value_counts())这段代码的逻辑是「先减噪再对齐标签」。str.len() 4这个阈值不是拍脑袋中文里两个字的「好评」几乎无信息量四个字以上才可能带具体描述。default_patterns用|拼成正则一次匹配多个模板比循环快。标签映射里把 3 星置为None再 drop是为了避免中性样本污染二分类边界。跑完看value_counts()如果正负比超过 4:1后面训练时就要考虑class_weightbalanced。2.2 中文分词与停用词jieba 的两种用法Word2Vec 吃的是词序列中文必须先分词。jieba 有两种模式精确模式和搜索引擎模式。情感分析里我一般用精确模式因为搜索模式会把「质量不错」切成「质量」「不错」「质量不错」反而引入冗余。停用词表要针对电商场景定制像「包邮」「物流」「客服」这些词本身不带情感但「物流快」「客服差」又带情感所以不能一刀切删掉得看它们和情感词的搭配。import jieba # 加载通用停用词再补充电商场景词 stopwords set() with open(stopwords.txt, r, encodingutf-8) as f: for line in f: stopwords.add(line.strip()) # 电商场景里这些词单独出现时无情感但组合后有情感谨慎处理 extra_stopwords {包邮, 正品, 发货} stopwords stopwords | extra_stopwords def cut_text(text): words jieba.lcut(text, cut_allFalse) # 精确模式 words [w for w in words if w not in stopwords and len(w) 1] return words df[tokens] df[comment].apply(cut_text) print(df[tokens].head())cut_allFalse是精确模式保证「质量不错」不会被拆出重叠词。len(w) 1过滤单字因为中文单字歧义太大「好」「差」单独出现时反而可能是噪声。extra_stopwords里放的是「单独出现无情感、组合出现才有情感」的词如果你把它们全删了「物流快」就只剩「快」反而丢了主语。这一步的输出tokens是列表后面 Word2Vec 直接吃这个格式。2.3 训练集/测试集划分别让同一商品的评论跨集随机划分是最常见的翻车点。电商评论里同一商品的评论高度相似如果随机分训练集和测试集里会出现同一商品的评论模型相当于「见过答案」测试准确率虚高。正确做法是按商品 ID 分组划分保证同一商品的评论只出现在训练集或测试集一侧。from sklearn.model_selection import GroupShuffleSplit # 假设有 product_id 字段 gss GroupShuffleSplit(n_splits1, test_size0.2, random_state42) train_idx, test_idx next(gss.split(df, groupsdf[product_id])) train_df df.iloc[train_idx].reset_index(dropTrue) test_df df.iloc[test_idx].reset_index(dropTrue) print(train:, len(train_df), test:, len(test_df))GroupShuffleSplit的groups参数就是商品 ID它保证同一组不跨集。test_size0.2是常规比例数据量小于 5000 时建议用 0.3让测试集更有统计意义。random_state固定住方便复现。这一步做完后面所有训练都基于train_df测试只用test_df避免信息泄漏。3. Word2Vec 训练参数怎么设、向量怎么取3.1 CBOW 还是 Skip-gram电商评论的选型理由Word2Vec 有两个架构CBOW 用上下文预测中心词Skip-gram 用中心词预测上下文。电商评论里低频词多比如「续航」「掉漆」「色差」这些具体描述出现次数少但情感指向强。Skip-gram 对低频词更友好因为它每个中心词都会产生多组训练样本低频词能被多训几次。CBOW 训练快但容易把低频词的信息平均掉。所以做情感分析我一般选 Skip-gramsg1。另一个关键参数是vector_size也就是词向量维度。50-100 维适合小数据集200-300 维适合大数据集。电商评论通常几万条100 维够用再高容易过拟合。window是上下文窗口情感分析里 5 左右比较合适因为情感词和修饰词通常挨得近窗口太大反而引入无关词。min_count设 2 或 3丢掉只出现一次的词这些词没有足够上下文训出好向量。from gensim.models import Word2Vec # 用训练集的分词结果训练词向量 sentences train_df[tokens].tolist() model Word2Vec( sentencessentences, vector_size100, # 词向量维度 window5, # 上下文窗口 min_count2, # 忽略低频词 sg1, # 1 表示 Skip-gram workers4, # 并行线程 epochs10, # 训练轮数 seed42 ) model.save(word2vec.model) print(词表大小:, len(model.wv))sg1选 Skip-gram理由上面说了。vector_size100是经验和数据量的平衡点你可以试 50 和 200 对比。window5覆盖「质量 非常 好」这种短搭配。min_count2过滤只出现一次的词这些词训出来的向量基本是随机初始化留着反而添乱。epochs10对几万条评论足够再多收益递减。seed42固定随机种子保证每次训练结果一致。训练完model.wv就是词向量表可以用model.wv.most_similar(质量)看看近义词验证向量质量。3.2 句向量怎么来平均池化 vs TF-IDF 加权Word2Vec 给的是词向量SVM 需要的是句向量。最简单的是把句子里所有词向量取平均但这样会弱化关键词。比如「质量很好但物流太慢」平均之后「质量」和「物流」权重一样情感被稀释。更好的做法是用 TF-IDF 加权平均让重要词的向量占更大比重。import numpy as np from sklearn.feature_extraction.text import TfidfVectorizer # 先算 TF-IDF 权重 tfidf TfidfVectorizer(tokenizerlambda x: x, lowercaseFalse) tfidf.fit(train_df[tokens]) def sentence_vector(tokens, model, tfidf, feature_names): vecs [] weights [] for token in tokens: if token in model.wv: vecs.append(model.wv[token]) # 取该词的 TF-IDF 权重 if token in feature_names: idx feature_names[token] weights.append(tfidf.idf_[idx]) else: weights.append(1.0) if not vecs: return np.zeros(model.vector_size) vecs np.array(vecs) weights np.array(weights).reshape(-1, 1) return np.sum(vecs * weights, axis0) / np.sum(weights) feature_names tfidf.vocabulary_ train_df[vec] train_df[tokens].apply( lambda x: sentence_vector(x, model, tfidf, feature_names) ) test_df[vec] test_df[tokens].apply( lambda x: sentence_vector(x, model, tfidf, feature_names) )sentence_vector的核心是「加权平均」。tfidf.idf_是逆文档频率词越稀有 IDF 越高权重越大。feature_names是词到索引的映射用来查 IDF 值。如果词不在 TF-IDF 词表里比如只在测试集出现权重给 1.0 兜底。np.sum(vecs * weights, axis0) / np.sum(weights)是标准加权平均公式。这一步做完每条评论变成一个 100 维向量可以直接喂给 SVM。3.3 向量归一化SVM 的隐藏前提SVM 基于距离计算如果向量各维度量纲不一致距离会被大量纲维度主导。Word2Vec 训出来的向量各维度数值范围差不多但句向量加权平均后可能有偏移。标准做法是做 L2 归一化把每个句向量缩放到单位长度。from sklearn.preprocessing import normalize X_train normalize(np.vstack(train_df[vec].values)) X_test normalize(np.vstack(test_df[vec].values)) y_train train_df[label].values y_test test_df[label].values print(X_train shape:, X_train.shape)np.vstack把列表里的向量堆成矩阵normalize默认做 L2 归一化。归一化后每个样本的向量长度都是 1SVM 的核函数计算更稳定。这一步不做后面调参时你会发现C值怎么调都不对因为距离尺度乱了。4. SVM 分类核函数选择与参数调优4.1 线性核还是 RBF 核先看数据量SVM 的核函数选择有个经验法则特征维度高、样本量中等时线性核往往够用且快样本量小、边界非线性时RBF 核更强。我们的句向量是 100 维样本几千到几万属于「维度高、样本中等」线性核通常能跑到不错的效果。但电商评论的情感边界有时是非线性的比如「不差」和「差」只差一个字情感却相反这种细微差别 RBF 核可能捕捉得更好。我的做法是两个都跑用交叉验证比。线性核的C和 RBF 核的C、gamma都要调。C是惩罚系数越大越不容忍误分类容易过拟合越小越容忍容易欠拟合。gamma是 RBF 核的宽度参数越大影响范围越小容易过拟合。from sklearn.svm import SVC from sklearn.model_selection import GridSearchCV # 线性核 linear_svm SVC(kernellinear, class_weightbalanced, random_state42) linear_params {C: [0.1, 1, 10]} # RBF 核 rbf_svm SVC(kernelrbf, class_weightbalanced, random_state42) rbf_params {C: [0.1, 1, 10], gamma: [scale, 0.01, 0.1]} # 交叉验证 linear_grid GridSearchCV(linear_svm, linear_params, cv5, scoringf1, n_jobs-1) linear_grid.fit(X_train, y_train) rbf_grid GridSearchCV(rbf_svm, rbf_params, cv5, scoringf1, n_jobs-1) rbf_grid.fit(X_train, y_train) print(线性核最佳:, linear_grid.best_params_, linear_grid.best_score_) print(RBF核最佳:, rbf_grid.best_params_, rbf_grid.best_score_)class_weightbalanced是处理标签不均衡的关键它让 SVM 自动给少数类更高权重。scoringf1而不是accuracy因为不均衡数据里准确率会骗人。cv5是五折交叉验证数据量小于 1000 时用cv3更稳。n_jobs-1用满 CPU 核。跑完对比两个best_score_选高的那个。如果线性核和 RBF 核差距在 1% 以内选线性核因为快且可解释。4.2 用网格搜索定参数C 和 gamma 的联动C和gamma不是独立的。C大gamma大模型会死记硬背训练样本C小gamma小模型会欠拟合。网格搜索时要把它们放在一起搜而不是分开调。上面代码里 RBF 核的gamma给了scale、0.01、0.1 三档scale是 sklearn 的默认值等于1 / (n_features * X.var())通常是个合理起点。# 用最佳参数在测试集上评估 best_model rbf_grid.best_estimator_ if rbf_grid.best_score_ linear_grid.best_score_ else linear_grid.best_estimator_ from sklearn.metrics import classification_report, confusion_matrix y_pred best_model.predict(X_test) print(confusion_matrix(y_test, y_pred)) print(classification_report(y_test, y_pred, target_names[负面, 正面]))best_estimator_是交叉验证选出的最佳模型直接拿来预测测试集。confusion_matrix看混淆情况重点看负面类的召回率——如果负面召回低说明模型把差评误判成好评这在业务上比反过来更严重。classification_report的f1-score是综合指标正负两类都要看不能只看总体。4.3 类别不均衡的补救重采样与阈值调整如果负面样本太少class_weightbalanced可能还不够。这时候可以上重采样对少数类过采样SMOTE或对多数类欠采样。SMOTE 是在少数类样本之间插值生成新样本适合连续特征我们的句向量正好是连续的。from imblearn.over_sampling import SMOTE smote SMOTE(random_state42, k_neighbors3) X_train_res, y_train_res smote.fit_resample(X_train, y_train) print(重采样前:, np.bincount(y_train)) print(重采样后:, np.bincount(y_train_res)) # 用重采样后的数据重新训练 final_svm SVC(kernelrbf, C1, gammascale, random_state42) final_svm.fit(X_train_res, y_train_res)k_neighbors3是 SMOTE 的邻居数少数类样本少于 100 时设 3 比较稳太多会生成噪声样本。重采样只在训练集做测试集保持原始分布否则评估结果不可信。重采样后class_weight可以去掉因为样本已经平衡了。注意 SMOTE 对高维数据可能效果有限如果重采样后 F1 没提升说明问题不在不均衡而在特征质量。5. 避坑与排查这条链路上最容易翻车的五个点5.1 词向量训完发现「好」和「差」很像现象model.wv.most_similar(好)返回的前几个词里有「差」「不错」情感方向混乱。原因Word2Vec 学的是分布相似性不是情感极性。「好」和「差」在评论里出现的上下文可能很像比如都出现在「质量__」后面所以向量接近。这是 Word2Vec 的固有局限它不区分情感方向。解决不要指望 Word2Vec 单独搞定情感。它的作用是提供语义表示情感判别交给 SVM。如果向量质量确实差检查window是不是太大或者min_count是不是太低导致噪声词太多。另一个办法是用预训练的中文词向量替换自训练但课程设计里自训练更能体现流程。5.2 测试集准确率 95%换一批数据掉到 60%现象交叉验证和测试集上 F1 都很高但拿新爬的评论一跑效果断崖式下跌。原因大概率是数据泄漏。同一商品的评论跨了训练集和测试集模型记住了商品特有的表达换商品就失效。另一个可能是过拟合C或gamma太大。解决用GroupShuffleSplit按商品 ID 划分确保同一商品不跨集。如果已经这么做了还掉检查测试集和训练集的词表重叠率重叠率低于 70% 说明词向量覆盖不够需要扩充训练数据或降低min_count。5.3 训练时报「empty vocabulary」或句向量全是零现象TfidfVectorizer报empty vocabulary或者句向量矩阵里大量零向量。原因分词后所有词都被停用词过滤掉了或者min_count设太高词表里没几个词。电商评论里短文本多容易触发。解决检查stopwords是不是太激进把「好」「差」这种情感词误删了。min_count从 2 降到 1 试试。句向量全零说明该句所有词都不在model.wv里要么是生僻词要么是分词切错了。加个日志打印哪些句子产生了零向量针对性处理。5.4 SVM 训练慢到跑不完现象GridSearchCV跑了半小时还没结束。原因RBF 核的 SVM 训练复杂度是 O(n²) 到 O(n³)样本量上万时非常慢。网格搜索又放大了这个开销。解决先降采样训练集到 5000 条以内做粗调找到大致范围后再用全量数据精调。或者改用LinearSVC它针对线性核优化过速度快很多。如果必须用 RBF 核把gamma的搜索范围缩小别用太多档位。5.5 负面评论召回率极低现象正面评论 F1 0.95负面评论 F1 0.4模型几乎把所有评论判成正面。原因标签不均衡 情感表达不对称。正面评论往往直白「很好」「满意」负面评论常用反讽或委婉表达「呵呵」「也就那样」词向量对反讽的表示能力弱。解决先上class_weightbalanced和 SMOTE。如果还不够针对负面评论做数据增强比如同义词替换。另外检查负面样本的标注质量有些「中评」被误标成负面会拉低召回。反讽是 Word2Vec SVM 的天然短板如果业务里反讽多这条路线就不够用了得换预训练模型。6. 进阶技巧用情感词典给句向量加一维「极性特征」Word2Vec SVM 的瓶颈在于词向量不带情感极性。一个低成本改进是引入情感词典给每个句子算一个极性得分把这个得分作为额外一维拼到句向量后面。这样 SVM 既有语义信息又有显式的情感信号。# 加载情感词典格式词 极性值正数正面负数负面 sentiment_dict {} with open(sentiment_dict.txt, r, encodingutf-8) as f: for line in f: word, score line.strip().split() sentiment_dict[word] float(score) def polarity_score(tokens): scores [sentiment_dict.get(w, 0) for w in tokens] if not scores: return 0.0 return sum(scores) / len(scores) # 平均极性 train_df[polarity] train_df[tokens].apply(polarity_score) test_df[polarity] test_df[tokens].apply(polarity_score) # 拼接到句向量后面 X_train_aug np.hstack([ X_train, train_df[polarity].values.reshape(-1, 1) ]) X_test_aug np.hstack([ X_test, test_df[polarity].values.reshape(-1, 1) ]) # 重新训练 final_svm SVC(kernelrbf, C1, gammascale, class_weightbalanced, random_state42) final_svm.fit(X_train_aug, y_train) y_pred_aug final_svm.predict(X_test_aug) from sklearn.metrics import f1_score print(加极性特征 F1:, f1_score(y_test, y_pred_aug)) print(不加极性特征 F1:, f1_score(y_test, best_model.predict(X_test)))polarity_score用平均极性而不是求和是为了避免长文本得分虚高。np.hstack把 100 维句向量和 1 维极性拼成 101 维。极性特征相当于给 SVM 一个「作弊提示」让它知道这句话整体偏正还是偏负。实测里这个改动通常能把负面类 F1 拉高 3-5 个百分点尤其是反讽样本少的场景。参数上情感词典的质量决定效果。通用词典如知网 HowNet覆盖广但电商词少建议在通用词典基础上补充「掉漆」「色差」「续航」这类电商高频词。极性值不用太精细-1、0、1 三档就够太细反而引入噪声。验证方法上除了看 F1建议做一轮错误分析把预测错的样本导出来人工看 20 条归类是分词错、词典漏、还是模型边界问题。这个习惯比调参更能提升效果。我自己做课程设计时第一版 F1 只有 0.78错误分析发现一半错误是「不推荐」被切成「不」「推荐」后来把否定词和后面的词合并成一个 tokenF1 直接到 0.85。这种细节调参调不出来只能靠看错例。希望帮到你。本文还有配套的精品资源点击获取