简介:一套围绕word2vec与FastText词向量结合支持向量机(SVM)的中英文情感分类实战方案,面向希望用Python实现情感分类的初学者与研究人员,适用于社交媒体分析、客户反馈处理等场景。资源提供完整可运行的Python代码、训练/预测脚本、微博情感数据CSV、停用词表与说明文档,覆盖从文本清洗、分词、词向量训练到SVM建模、评估与预测的完整链路。压缩包共19个文件,包含5个py脚本、8个txt词表/停用词、3个csv数据集,以及效果展示图片、动图和1份md说明文档,整体仅23.5MB,便于下载与复现。目前已有9889人学习下载,是入门文本情感分类的实用素材。通过这套资源,读者可以直观理解CBOW与Skip-gram的差异、FastText对未登录词的处理优势,以及如何将词向量作为SVM输入特征完成分类,同时借助wordCloud脚本和可视化图片直观呈现分析结果。
1. 拿到这份 word2vec / FastText + SVM 情感分类资源:先搞懂它解决什么问题
做文本情感分类的同行应该都遇到过这种尴尬:拿一个公开情感数据集跑 SVM,结果准确率卡在 60% 出头,换什么核函数都没用。问题往往不出在 SVM 本身,而是喂给它的特征太弱。这份资源把 word2vec 和 FastText 两种词向量方案,跟 SVM 分类器串成了一整套可复现的中英文情感分类流程,从微博中文语料到英文通用文本都能用。它不仅给你pos_neg.py、train_svm.py、predict.py这些可以直接跑的脚本,还配了多份停用词词典和搜狗、百度、腾讯扩展词典,以及词云生成和最大句子长度统计的小工具。适合正在做舆情分析、在线评论分类、客服工单打标的工程师,也适合刚接触 NLP 想把分类流程完整走一遍的入门者。它不会帮你调出最顶尖的深度学习模型,但能把「词向量 + 线性分类器」这条经典路线的坑提前蹚平。
2. 先拆资源包:从文件清单到中英文数据预处理流水线
2.1 资源包组成:不是单一脚本,而是一条可替换的数据链
拿到sentimentAnalysis_SVM.zip之后,解开看到的是下面这批文件,我先按用途把它们分成了四组:
- 原始数据与中间产物:
data目录、neg.csv、pos.csv、weibo_senti_100k.csv - 词典资源:
Stopword.txt、dict目录下的dict_sougou_utf8.txt、dict_baidu_utf8.txt、dict_tencent_utf8.txt、my_dict.txt、SogouLabDic.txt、dict_pangu.txt - 核心处理脚本:
pos_neg.py(把原始语料拆成正负样本)、train_svm.py(训练模型)、predict.py(对未知文本预测)、get_maxlen.py(统计句长)、wordCloud.py(生成词云) - 辅助文件:
readme.md、requirements.txt、model目录、background.png、background.gif
这里值得注意的一点是:weibo_senti_100k.csv是中文微博情感语料,已经是带标签的格式,但它未必直接是pos.csv/neg.csv的结构。pos_neg.py干的事就是把原始宽表拆成两条干净的路径:一条是训练用的正负样本文本,另一条是后续train_svm.py要读的 CSV。我习惯的做法是,拿到资源后先用head -n 5看一眼文件头,确认字段分隔符和是否带表头,再决定pos_neg.py里的delimiter参数用逗号还是制表符。
2.2 中文清洗与分词:先处理标点、数字,再用 jieba 切开
对于中文情感分类,直接拿原始句子做词向量训练基本是自找麻烦。标点、数字、语气词都会变成大量低频 token,稀释真正有情感倾向的词。这个资源里pos_neg.py的数据预处理部分,我把它精简成一个标准函数,逻辑如下:
import re import jieba def clean_chinese_text(text: str, stopwords: set) -> str: # 去掉URL和@用户——微博语料里这两类噪声特别多 text = re.sub(r"https?://\S+", "", text) text = re.sub(r"@\w+", "", text) # 统一小写,中文不受影响,但对英文部分友好 text = text.lower() # 去掉数字和标点,保留中英文和基础空白 text = re.sub(r"[0-9a-zA-Z]+|[\W_]+", "", text) # 中文分词,返回list words = jieba.lcut(text) # 过滤停用词和单字(单字通常没有情感判别力) filtered = [w for w in words if w not in stopwords and len(w) > 1] return " ".join(filtered)这段逻辑里,第一个正则把 URL 和 @用户去掉,是因为微博语料里http://...和@某人跟情感极性基本无关,留着只会增加向量空间的维度。[0-9a-zA-Z]+和[\W_]+的组合容易误伤中文,所以我一般把数字、英文单词单独剥离,再用空白连起来。jieba.lcut返回的是列表,注意别用jieba.cut直接和join混用;停用词过滤要放在分词之后,才能正确删掉「的」「了」这类高频但无语义的词。这个函数里我没有做词性过滤,因为在短文本情感分类场景里,保留动词和形容词反而对词向量聚类有帮助。
2.3 停用词表和扩展词典:为什么一份资源要塞六七个字典
Stopword.txt是基础停用词表,但资源里还放了搜狗、百度、腾讯三份领域词典,外加my_dict.txt和SogouLabDic.txt、dict_pangu.txt。很多人会问,停用词表和扩展词典不是矛盾的吗?其实分工不同。停用词表用于删除:删掉对情感判断没帮助的虚词,降低噪声。扩展词典用于分词加固:把「好high哦」「这波不亏」这类网络新词以整体形式切出来,避免 jieba 把它们切成碎片后丢失情感信息。
我一般会这样合并处理:
def load_all_dicts(dict_dir: str) -> set: extra_words = set() for fname in ["dict_sougou_utf8.txt", "dict_baidu_utf8.txt", "dict_tencent_utf8.txt", "my_dict.txt", "SogouLabDic.txt", "dict_pangu.txt"]: path = f"{dict_dir}/{fname}" with open(path, "r", encoding="utf-8", errors="ignore") as f: for line in f: word = line.strip() if word: extra_words.add(word) # 把词直接加进jieba的用户词典,提高分词命中率 jieba.add_word(word) return extra_words这段代码里的关键是jieba.add_word(word),它让 jieba 在切分时优先把这串字符保留为一个完整词。比如「我太难了」如果没有词典干预,可能会被切成「我 / 太难 / 了」;加了「我太难了」后就能整体保留,语义更完整。注意errors="ignore"很有必要,那些第三方词典里偶尔混入无效编码,不忽略的话脚本会在读到第几行时突然崩掉。文件编码这里也是个常见坑,很多公开词典是 GBK 编码,这里统一用utf-8加errors="ignore"是折中方案,稳妥的做法是先file命令确认编码再决定用哪个参数。
3. 词向量特征提取:word2vec 和 FastText 的选型与工程实现
3.1 word2vec 训练:CBOW 与 Skip-gram 在情感任务里怎么选
词向量是整个情感分类管道的特征来源。直接用词频统计的 BoW 特征会丢失「好」和「棒」在语义空间里的相近关系;而 word2vec 学习到的向量,是把语义相近的词聚到邻近区域。CBOW 用上下文词预测中心词,Skip-gram 用中心词预测上下文。在小规模微博语料上,我倾向于先用 CBOW 试试,因为它平均了上下文信息,训练速度更快;而 Skip-gram 对低频词更友好,但训练更耗时。
这里用 Gensim 训练一个简单模型:
from gensim.models import Word2Vec def train_word2vec(texts, vector_size=100, window=5, workers=4, sg=0): model = Word2Vec( sentences=texts, # 已经是分词后以空格连接的句子列表 vector_size=vector_size, window=window, min_count=1, # 微博短文本里低频词不少,min_count用1保留更多词 sg=sg, # 0表示CBOW,1表示Skip-gram workers=workers, epochs=10 ) model.save("model/word2vec.model") return modelsentences要求是可迭代的 list,每个元素是分词后的 token 列表。vector_size=100对于 10 万条微博级语料来说够用,做 200 维也不会过拟合;window=5能兼顾前后文。min_count=1会让词汇表很大,但情感分类任务有一些表情词和网络缩写出现次数极少却极有判别力,所以我保留全部 token。训练好后,模型文件里存了词向量矩阵,但注意Word2Vec默认会对词向量做归一化,SVM 用的就是这份归一化向量。
3.2 FastText 引入字符级信息:中文未登录词的底气
FastText 解决的核心问题是未登录词。word2vec 遇到训练集里没见过的词直接返回 KeyError,FastText 因为把每个词拆成了字符 n-gram(比如「开心」拆成开、心、开心等),所以即使遇到新词,也能用字符 n-gram 的向量凑出这个词的表示。
用 Gensim 训练 FastText 的成本几乎和 word2vec 一样低:
from gensim.models import FastText def train_fasttext(texts, vector_size=100, window=5, min_n=2, max_n=4): model = FastText( sentences=texts, vector_size=vector_size, window=window, min_count=1, sg=0, min_n=min_n, max_n=max_n, # n-gram长度范围,中文2-4比较合理 epochs=10 ) model.save("model/fasttext.model") return modelmin_n=2, max_n=4这套参数对中文比较合适:单个汉字没有太多语义,两个字的双字词和三个字的词是中文信息的主要载体;四字成语也能覆盖。比英文默认的min_n=3, max_n=6在中文场景里更稳。训练完成后,你可以用model.wv["没见过的词"]拿到一个不报错的向量,这在预测脚本里是最大的优势——不用对未知词做填充零向量的「后悔药」处理。
3.3 从词向量到句子向量:加权平均、均值池化,还是直接拼接
单个词的向量不能直接丢给 SVM,SVM 需要一条固定维度的特征向量。最常见做法是把句子中所有词的向量做均值池化。但也有人用 TF-IDF 加权平均。我在这份资源里看到他们把文本转换成「词向量的加权和」,所以以下做法是一线工位最常用的:
import numpy as np def text_to_vector(text_tokens: list, wv_model, vector_size: int = 100) -> np.ndarray: vectors = [] for token in text_tokens: try: vec = wv_model[token] except KeyError: # word2vec遇到词表外词直接跳过;FastText基本不会到这里 continue vectors.append(vec) if not vectors: return np.zeros(vector_size) # 全平均,保留所有维度的scale信息 return np.mean(vectors, axis=0)这里对 word2vec 和 FastText 共用同一个函数:对于 word2vec,遇到缺词就跳过;对于 FastText,理论上wv_model[token]总能返回一个向量。np.mean(vectors, axis=0)是最简单的池化,也有人试过 max pooling,我个人的经验是:在情感分类短文本上,均值池化通常比 max 池化稳定,因为 max 会把每个维度上最极端的信号挑出来,容易放大噪声词的贡献。如果你想微调,可以把平均改成加权平均,权重用 TF-IDF,但我实测在 10 万条微博语料上收益不到 1 个点,不值得为此增加依赖。
4. SVM 模型训练与评估:把词向量变成情感分类器
4.1 用 scikit-learn 训练线性 SVM:核函数选择与网格搜索
词向量特征通常维度是 100 或 200,样本数在几万到十几万之间。对这种「维度不算高、样本数偏多」的场景,线性 SVM 是最优选。高斯核 RBF 我在这类任务上试过,训练慢,而且和线性核的准确率差距通常小于 0.5 个百分点,但调参成本高得多。所以train_svm.py里的核心逻辑应该是:
from sklearn.svm import LinearSVC from sklearn.model_selection import GridSearchCV def train_svm(train_vectors, train_labels): param_grid = {"C": [0.1, 0.5, 1.0, 2.0]} svm = LinearSVC( loss="hinge", # 真正的SVM损失 max_iter=5000, # 不设置的话容易收敛警告 random_state=42 ) grid = GridSearchCV(svm, param_grid, cv=5, scoring="f1_macro") grid.fit(train_vectors, train_labels) return grid.best_estimator_, grid.best_score_loss="hinge"是 LinearSVC 的正统配置,默认的squared_hinge在 sklearn 的 old 接口里非常容易踩坑,两者预测边界有细微差别。max_iter=5000是在大样本下有必要的,默认 1000 次迭代很容易到不了收敛条件,然后抛出的警告不会让程序崩溃,但模型可能没有达到最小损失。用 5 折交叉验证跑C的搜索,我一般会让C在对数尺度上排列,从 0.1 到 2.0 就够,因为词向量本身做过归一化,C=1 附近通常是较优区域。
4.2 模型评估指标:宏观 F1 和类别分布一样重要
情感分类数据通常三类(正面、负面、中性),微博数据集里中性样本往往偏少。只看准确率会被多数类主导,所以脚本里要同时输出precision,recall,f1-score并且用macro平均:
from sklearn.metrics import classification_report, confusion_matrix def evaluate_svm(model, test_vectors, test_labels, label_names): preds = model.predict(test_vectors) print(classification_report(test_labels, preds, target_names=label_names)) cm = confusion_matrix(test_labels, preds) print("Confusion Matrix:\n", cm)classification_report会给出每个类的精确率、召回率和 F1,这样你能很快看出是哪一类拖累了整体。混淆矩阵里如果发现「中性」被大量错分成「正面」,说明词向量没有捕捉到中性的边界,应该回到数据预处理阶段检查是否中性样本本身存在标注噪声。这条进阶验证路径,是这套资源相对良心的设计——它不强求一个漂亮的分类准确率,而是给你看模型错在哪。
4.3 从 pos_neg.py 到 train_svm.py:完整流程怎么串起来
资源包里的脚本是分步的:pos_neg.py负责把weibo_senti_100k.csv处理成pos.csv、neg.csv,train_svm.py负责从 CSV 读数据、训练、保存模型,predict.py负责对单条或批量文本打分。我平时会把这个流程串成一个 makefile 风格的 shell 命令,减少中间步骤手滑出错:
python pos_neg.py --input weibo_senti_100k.csv --output data/ python train_svm.py --pos data/pos.csv --neg data/neg.csv \ --model_dir model/ --w2v_size 100 python predict.py --model model/svm.pkl --text "这个产品性价比很高,推荐"第一行做数据切分,第二行训练词向量和 SVM(注意train_svm.py内部要先调用分词和词向量训练,再进入 SVM),第三行对单条新文本返回positive/negative的标签。这里的--w2v_size参数贯穿到 SVM 的输入维度,如果你改大了,记得同步text_to_vector里的维度参数,否则会撞上矩阵形状不匹配。这一点后面避坑章节还要单独说。
5. 避坑与常见问题排查:从预测脚本到中文乱码的四条血泪经验
5.1 未知词向量为 0,F1 从 85% 掉到 70%
现象:用 word2vec 训练好的 SVM 在测试集上准确率正常,但拿一条包含不少新词的真实评论去predict.py,模型把明显正面的句子判成了负面。观察 A 后发现,这条文本里多个新词都没有向量,text_to_vector把这些词直接跳过,最终句子向量变成少数几个停用词的均值,甚至变成全零向量。
原因:word2vec 的词表来自训练语料,真实世界的用户评论总会出现训练时没见过的用法、缩写、网络新词。全零向量进入线性 SVM 时,相当于把样本放到了坐标原点附近,而原点离决策边界方向恰恰容易被分到大多数类。
解决:把text_to_vector里的continue改成兜底逻辑。首选方案是用 FastText 替换 word2vec 做预测;如果坚持 word2vec,可以保留<UNK>的随机小向量,但更简单的是对全零向量返回一个之前训练集句向量的均值。我后来把这个逻辑写成了函数参数:
def text_to_vector(text_tokens, wv_model, vector_size, fallback="mean"): if fallback == "mean": # 加载训练集所有句向量的均值作为fallback ...从那以后我再没在预测阶段因为新词翻过车。
5.2 jieba 分词把整个句子切成一个超大词
现象:pos_neg.py处理中文时,某些长度较长的句子分词后出现了一个包含十几个字的超长词,比如把「这个手机屏幕摔碎了但还能用性价比很高」整体当成一个词。词向量训练时这个超长词出现次数低,根本学不到有效向量,还会占用词表空间。
原因:资源里的停用词表把「这个」「但」「还」等词都删掉了,jieba 在连续一段去掉了停用词的文本上,有可能因为缺少切分锚点而把相邻词合并。另外 jieba 不少版本对长文本连续字母数字串的分词策略激进。
解决:在清洗阶段,不要把所有停用词在分词前去掉。正确顺序是先分词,再删停用词。我写的clean_chinese_text里已经遵守了「先jieba.lcut再过滤过滤词」的顺序。另外给 jieba 加载dict_sougou_utf8.txt等词典也可以让切分更细粒度。验证方法是用get_maxlen.py统计pos.csv中最长句子的长度,如果出现异常的长词或长句,把那条样本单独拉出来看分词结果。
5.3 读取 CSV 时因引号不配对导致ParserError
现象:运行train_svm.py读pos.csv时抛ParserError: expected after line 12, I got ...,用文本编辑器打开 CSV 却看不出明显问题。
原因:pos.csv里的评论文本包含英文引号或半角引号,且内容里出现了不配对的情况。csv 模块默认启用QUOTE_MINIMAL,在遇到"I said "wow""这类文本时字段会被切断,于是后面的列错位,同一行出现过少的字段数量。
解决:读 CSV 时显式关闭无关的引号语法,或者用 pandas 指定quoting=csv.QUOTE_NONE,如果文本内部还有英文引号,最好在写入时就转成全角引号。我通常修改读取的参数:
import pandas as pd df = pd.read_csv("data/pos.csv", encoding="utf-8", engine="python", on_bad_lines="skip")engine="python"会更宽容地处理多行字段,on_bad_lines="skip"会把问题行直接跳过,但注意跳过的样本会造成类别分布偏移,所以跳过之后要打印日志记录行号。
5.4 词向量维度与 SVM 输入维度不一致导致矩阵相乘错误
现象:训练时使用--w2v_size 50,模型保存后修改了配置想用 100 维重新预测。predict 时model.predict(features)报ValueError: X has 50 features, but SVC is expecting 100 features。
原因:train_svm.py训练时把 SVM 和词向量模型都保存在model目录,但predict.py只加载了 SVM 模型文件,没有同步加载新词向量模型,或者加载了不同名字的旧模型。
解决:每次训练后把词向量维度写入元信息文件(比如vector_size.txt),预测前先读取该文件。更稳妥的是在predict.py里同时加载word2vec.model和 SVM 模型,从词向量模型中获取vector_size,杜绝人工配置不一致。这个坑看似低级,但项目调试越到后期越容易触发,尤其是在技能迁移到另一个项目后。
6. 进阶验证:用词云和最长句子长度校准文本处理流水线
6.1 get_maxlen.py 与 wordCloud.py 的实战用途
get_maxlen.py统计语料中的句子长度分布,很多人以为这只是看看数据,其实它对模型选择影响很大。如果pos_neg.py按固定截断长度切分句文本,get_maxlen能告诉你截断长度是否合理;如果句子普遍很短(少于 20 个 token),词向量窗口可以缩小到 3,训练更快。我通常这样运行并观察分布:
python get_maxlen.py --input data/pos.csv --tokenized输出的第 95 百分位长度,就是后续 SVC 输入向量不产生截断损失的参考线。如果发现异常的高值(比如超过 200 个词),大概率是清洗没干净,文本里混入了超长拼写或没有换行的网页源码。
wordCloud.py可以生成正面和负面词云,它的诊断价值在于:词云里如果出现大量中性词(例如「东西」「时候」),说明停用词表还不完整;如果正负词云的高频词高度重叠,说明数据标签可能存在噪声,或者词向量空间没有把情感方向拉开。比如background.gif/background.png是词云的背景图,脚本应该支持替换成自定义图案。
6.2 给新语料做一次性体检的脚本套路
拿到任何新语料(不限于这份微博数据),我都会先跑一遍类似下面的体检:
def inspect_corpus(filepath: str): df = pd.read_csv(filepath) print("样本数:", len(df)) print("标签分布:\n", df["label"].value_counts()) df["text_len"] = df["text"].map(lambda x: len(x)) print("句子长度分位数:q50", df["text_len"].median(), "q90", df["text_len"].quantile(0.9))这一步能提前发现:类别严重不平衡、空文本字段、超长异常样本。接着做一遍清洗和分词,随机挑几条检查分词质量。我自己的习惯是:第一次练兵用 2000 条样本跑完整流程,确认每个脚本都通了,再上全量 10 万条。因为词向量如果有一行脏数据混进去,训练时不会报错,但会把某个词的向量污染,SVM 决策边界也随之偏移。
6.3 从训练到预测的最短闭环:留一份可以反复调用的经验
这套资源最大的价值不在单脚本,而在于它的文件设计逼着你按「预处理 → 词向量 → SVM → 评估 → 预测」的顺序走完。我把最短闭环浓缩成下面这段可以直接抄的代码块,用于验证环境是否全部就绪:
# 单批推理:给定一条文本,输出情感标签 def predict_one(text: str, svm_model, wv_model, stopwords: set): cleaned = clean_chinese_text(text, stopwords) tokens = cleaned.split() vec = text_to_vector(tokens, wv_model, wv_model.vector_size) vec = vec.reshape(1, -1) label = svm_model.predict(vec)[0] return {0: "负面", 1: "正面"}[label]这段代码把前面所有函数串成一条线:clean_chinese_text负责清洗分词,text_to_vector负责获得句向量,svm_model.predict负责分类。注意wv_model.vector_size在 Gensim 4.x 中通过属性访问,在 3.x 里是wv_model.vector_size也通用,但早期版本为model.wv.vector_size。这个闭环跑通后,你就能把任何一条输入文本快速放入现有模型,看到模型对具体词向量的敏感度。从那以后,我每次换新语料都强制先跑一遍get_maxlen+ 词云 + 单条预测的体检流程,确认数据形态没变,再上全量训练。希望这套关键词相连的事前检查,能帮你也少走几次弯路。
本文还有配套的精品资源,点击获取