☰
电商评论情感分析:Word2Vec+SVM实战指南
2026/10/1 12:49:53 网站建设 项目流程

简介:这份课程设计资源面向NLP入门与进阶学习者,围绕电商评论情感分析任务,完整演示如何用Word2Vec将评论文本转为词向量、再以SVM完成正负情感分类。包内共18个文件,含6个csv数据集与划分结果、5个py脚本(数据清洗、词向量训练、模型训练与测试)、4个npy特征数组、1个pkl已训练模型及说明文档,压缩包约28.14MB,结构清晰、可直接运行。已有354人学习下载。读者可据此掌握停用词过滤、词干还原等预处理流程,理解CBOW与Skip-gram的向量表示差异,并通过交叉验证调整惩罚项C与核函数参数γ,最后用准确率、召回率、F1分数评估模型效果。资源还预留了虚假评论检测与实时评论分析的拓展思路,适合作为课程设计、毕业项目或NLP实战练手的参考方案。

1. 电商评论情感分析:Word2Vec+SVM 这条老路线为什么还值得做

电商评论的情感分析,说白了就是把「好评」「差评」「中评」这些带情绪的文本,自动判成正面或负面。你可能觉得这活儿早被 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, na=False)] # 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_weight='balanced'。

2.2 中文分词与停用词:jieba 的两种用法

Word2Vec 吃的是词序列,中文必须先分词。jieba 有两种模式:精确模式和搜索引擎模式。情感分析里我一般用精确模式,因为搜索模式会把「质量不错」切成「质量」「不错」「质量不错」,反而引入冗余。停用词表要针对电商场景定制,像「包邮」「物流」「客服」这些词本身不带情感,但「物流快」「客服差」又带情感,所以不能一刀切删掉,得看它们和情感词的搭配。

import jieba # 加载通用停用词,再补充电商场景词 stopwords = set() with open("stopwords.txt", "r", encoding="utf-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_all=False) # 精确模式 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_all=False是精确模式,保证「质量不错」不会被拆出重叠词。len(w) > 1过滤单字,因为中文单字歧义太大,「好」「差」单独出现时反而可能是噪声。extra_stopwords里放的是「单独出现无情感、组合出现才有情感」的词,如果你把它们全删了,「物流快」就只剩「快」,反而丢了主语。这一步的输出tokens是列表,后面 Word2Vec 直接吃这个格式。

2.3 训练集/测试集划分:别让同一商品的评论跨集

随机划分是最常见的翻车点。电商评论里同一商品的评论高度相似,如果随机分,训练集和测试集里会出现同一商品的评论,模型相当于「见过答案」,测试准确率虚高。正确做法是按商品 ID 分组划分,保证同一商品的评论只出现在训练集或测试集一侧。

from sklearn.model_selection import GroupShuffleSplit # 假设有 product_id 字段 gss = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42) train_idx, test_idx = next(gss.split(df, groups=df["product_id"])) train_df = df.iloc[train_idx].reset_index(drop=True) test_df = df.iloc[test_idx].reset_index(drop=True) print("train:", len(train_df), "test:", len(test_df))

GroupShuffleSplit的groups参数就是商品 ID,它保证同一组不跨集。test_size=0.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-gram,sg=1。

另一个关键参数是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( sentences=sentences, vector_size=100, # 词向量维度 window=5, # 上下文窗口 min_count=2, # 忽略低频词 sg=1, # 1 表示 Skip-gram workers=4, # 并行线程 epochs=10, # 训练轮数 seed=42 ) model.save("word2vec.model") print("词表大小:", len(model.wv))

sg=1选 Skip-gram,理由上面说了。vector_size=100是经验和数据量的平衡点,你可以试 50 和 200 对比。window=5覆盖「质量 非常 好」这种短搭配。min_count=2过滤只出现一次的词,这些词训出来的向量基本是随机初始化,留着反而添乱。epochs=10对几万条评论足够,再多收益递减。seed=42固定随机种子,保证每次训练结果一致。训练完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(tokenizer=lambda x: x, lowercase=False) 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, axis=0) / 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, axis=0) / 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 归一化。归一化后每个样本的向量长度都是 1,SVM 的核函数计算更稳定。这一步不做,后面调参时你会发现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(kernel="linear", class_weight="balanced", random_state=42) linear_params = {"C": [0.1, 1, 10]} # RBF 核 rbf_svm = SVC(kernel="rbf", class_weight="balanced", random_state=42) rbf_params = {"C": [0.1, 1, 10], "gamma": ["scale", 0.01, 0.1]} # 交叉验证 linear_grid = GridSearchCV(linear_svm, linear_params, cv=5, scoring="f1", n_jobs=-1) linear_grid.fit(X_train, y_train) rbf_grid = GridSearchCV(rbf_svm, rbf_params, cv=5, scoring="f1", 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_weight="balanced"是处理标签不均衡的关键,它让 SVM 自动给少数类更高权重。scoring="f1"而不是accuracy,因为不均衡数据里准确率会骗人。cv=5是五折交叉验证,数据量小于 1000 时用cv=3更稳。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_weight="balanced"可能还不够。这时候可以上重采样:对少数类过采样(SMOTE)或对多数类欠采样。SMOTE 是在少数类样本之间插值生成新样本,适合连续特征,我们的句向量正好是连续的。

from imblearn.over_sampling import SMOTE smote = SMOTE(random_state=42, k_neighbors=3) 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(kernel="rbf", C=1, gamma="scale", random_state=42) final_svm.fit(X_train_res, y_train_res)

k_neighbors=3是 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_weight="balanced"和 SMOTE。如果还不够,针对负面评论做数据增强,比如同义词替换。另外检查负面样本的标注质量,有些「中评」被误标成负面,会拉低召回。反讽是 Word2Vec + SVM 的天然短板,如果业务里反讽多,这条路线就不够用了,得换预训练模型。

6. 进阶技巧:用情感词典给句向量加一维「极性特征」

Word2Vec + SVM 的瓶颈在于词向量不带情感极性。一个低成本改进是:引入情感词典,给每个句子算一个极性得分,把这个得分作为额外一维拼到句向量后面。这样 SVM 既有语义信息,又有显式的情感信号。

# 加载情感词典,格式:词 极性值(正数正面,负数负面) sentiment_dict = {} with open("sentiment_dict.txt", "r", encoding="utf-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(kernel="rbf", C=1, gamma="scale", class_weight="balanced", random_state=42) 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,错误分析发现一半错误是「不推荐」被切成「不」「推荐」,后来把否定词和后面的词合并成一个 token,F1 直接到 0.85。这种细节,调参调不出来,只能靠看错例。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询