尧图精选

酒店评论中文情感分析实战:基于Word2Vec与SVM的完整流程

🕒 发布时间:2026/10/1 23:59:36 📁 来源:尧图网络
简介面向自然语言处理与文本挖掘入门者的中文情感分析实战资源以酒店评论为语料演示从原始评论文本到情感分类结果的完整流程。包内数据规模近2000个文件以txt格式的正负样本评论为主另有5个Python脚本和1份说明文档脚本分别承担数据清洗、中文分句、停用词过滤、词向量生成及PCA降维后SVM分类等任务覆盖文本预处理、特征工程与模型训练关键环节压缩包约950KB便于快速下载实践。已有1427人浏览学习适合课程设计、毕业设计或NLP项目练手。使用该资源可获得可直接运行的分类实验框架以及标注好评/差评的训练数据通过对照脚本和README能弄清酒店评论情感分析中分词、去停用词与降维分类的协作关系也可将代码迁移到其他中文评论数据集上。1. 酒店评论的中文情感分析先看清这份资源解决什么问题跑酒店评论的中文情感分析很多人第一反应是上BERT、直接加载大模型结果环境配置装半天单条样本推理还要等好几秒。这份资源走的是完全不同的路线5个按顺序执行的Python脚本配套3份txt样本文件和一个停用词表从数据清洗、分词、停用词过滤到Word2Vec词向量、PCA降维、SVM分类完整流水线在普通笔记本CPU上就能跑完。它解决的是「有一批酒店评论文本要把它按正面/负面自动分类」这个具体问题不解决情感细粒度、情感强度这一类更复杂的任务。适合三类人拿它做NLP课程设计或毕设的学生、需要快速跑通一个基线结果做对比的入门工程师以及想低成本验证「传统机器学习 词向量」这条路还能复用多少的研究型学习者。下面按我自己拆包复现的顺序来写每一步都是可以直接照抄的。2. 数据准备与分词1_process.py 与 2_cutsentence.py 的清洗规则2.1 先认清数据集三个txt文件里装了什么拿到这套代码第一步不是急着跑脚本而是先把数据文件看清楚。这套代码对IDE没有执念vscode python环境配置好之后直接开跑pycharm里设置一下解释器路径也行。这份资源里的文本样本很简单就是三个txt文件内容参考行数neg.670.txt负面评论每行一条约670条pos.719.txt正面评论每行一条约719条pos.709.txt正面评论可能是清洗后版本或另一批样本约709条stopWord.txt停用词表每行一个词视情况几百到上千正样本有两个文件这是最容易让人迷糊的地方。我的处理习惯是用pos.719.txt作为正样本主集pos.709.txt先不合并因为两份正面样本可能会有重叠如果你想让训练集更大可以把两个文件按行去重后合并后续标签不变。先用命令行确认文件不是空的wc -l neg.670.txt pos.719.txt pos.709.txt stopWord.txt head -3 neg.670.txt如果某个文件只有几行说明源码包里的样本被截断或者下载不完整后面所有流程都会跑空。确认完行数之后再head看首行内容一般能看到类似「服务态度极差早餐难吃」这种一句一条的评论。到这里数据格式就清楚了一行一条、无表头、无标签字段标签靠文件名区分。这种格式的好处是省了解析JSON或CSV的工序坏处是文件名一旦改乱标签跟着乱。所以复现第一步建议先把文件复制一份备份所有脚本都只动副本。2.2 清洗脚本1_process.py 在做什么酒店评论是用户手打的脏东西很多最常见的三类是HTML标签残留、多余空白和数字编号。1_process.py 干的活就是把这些噪音清掉给分词一个干净输入。常见做法是逐行读入、正则替换、再写回一个新文件。import re def clean_line(line: str) - str: line re.sub(r[^], , line) # 去掉 HTML 标签 line re.sub(r\s, , line) # 多个空白压成一个空格 line re.sub(r[0-9], , line) # 去掉数字 return line.strip() with open(pos.719.txt, r, encodingutf-8) as f: lines [clean_line(l) for l in f if l.strip()] with open(pos_clean.txt, w, encodingutf-8) as f: f.write(\n.join(lines))三个正则的用途分别是[^]匹配尖括号包裹的标签内容评论里偶尔会有br/这类来自网页的残留\s把换行和多个空格统一成一个空格避免后续按空格切分时出现空串[0-9]去掉数字是因为「房间301」「3点入住」这类数字对情感判断基本没有贡献而且数字在词向量里很难学到稳定含义。参数上有个值得注意的地方encodingutf-8是显式指定的Windows下用记事本另存过的txt可能是GBK或带BOM的UTF-8不指定编码很容易在运行时抛UnicodeDecodeError。如果你读文件报错把encoding换成gbk再试这个坑后面专章讲。清洗这一步不要做得太激进有个典型翻车是把「不干净」「不满意」的「不」字当成噪音删掉那负面情感信息就丢了。所以清洗脚本只处理HTML、空白、数字不碰中文标点和否定词。中文标点如「」「。」保留或去掉都行但不建议在这里处理因为分词阶段jieba本身能处理标点。2.3 分词脚本2_cutsentence.py 与 jieba 参数中文和英文不一样没有天然空格分词边界所以分词是所有中文NLP流程的地基。2_cutsentence.py 用的是jieba这个库够轻量pip装完直接能用精度对酒店评论这种短文本足够了。核心代码就几行import jieba # 加载自定义词典词之间用空格隔开 jieba.load_userdict(hotel_dict.txt) seg_list [] for line in lines: seg jieba.lcut(line) # 精确模式分词返回 list seg [w.strip() for w in seg if w.strip()] seg_list.append(seg)jieba.lcut返回的是list比jieba.cut生成的生成器更省事调试时能直接看内容。jieba默认是精确模式也就是按最可能的切分方式切不会把「酒店」切成「酒」「店」这种碎词全模式虽然召回更高但会产生大量重叠词像「如家酒店」会被切成「如家」「酒店」「如家酒店」喂给后面的词向量全是噪音所以这个场景用精确模式。自定义词典文件hotel_dict.txt是我建议自己加的把「隔音差」「浴缸」「前台」「洗漱用品」这类酒店领域词放进去每行一个词加词频和词性隔音差 5 n 洗漱用品 3 n 前台 5 n为什么要自定义词典jieba自带的词典偏向通用场景「隔音」「洗漱用品」这类组合词可能被切成「隔音」「洗漱」「用品」切碎之后词向量学习不到「隔音差」作为整体的情感含义。给它设置一个较高的词频比如5jieba就会优先保留这个整体。分词完成后一定要做一次抽样检查永不省略打印前20条评论的分词结果肉眼看有没有该合没合的、该切没切的。这一步比调模型参数还重要因为后面所有特征质量都取决于这一层。分词粒度错了词向量学得再好也是错的。提示分词结果的抽样检查不要跳过打印前20条肉眼过一遍比任何评估指标都更能暴露问题。3. 去停用词与词向量构建3_stopword.py 与 4_getwordvecs.py 的核心逻辑3.1 停用词表stopWord.txt 的加载与过滤分词之后接的不是直接向量化而是先过停用词。3_stopword.py 做的事很简单拿一个停用词集合把出现在集合里的词从分词结果里剔掉。停用词分成两类一类是「的了、是、就、都」这种无情感含义的语法功能词另一类是「酒店」「房间」「入住」这种话题相关词。前者必须删后者要看场景。先看加载逻辑stop_words set() with open(stopWord.txt, r, encodingutf-8) as f: for line in f: word line.strip() if word and word not in stop_words: stop_words.add(word) filtered [] for seg in seg_list: kept [w for w in seg if w not in stop_words] filtered.append(kept)用set而不是list来存储停用词是为了让w not in stop_words的查找变成O(1)几千个词的场景感受不明显但数据集一旦放大到几万条评论这个细节就是几秒和几毫秒的区别。这里我要专门说一个容易翻车的地方停用词表不要无脑加。很多公开停用词表会把「不」「没」「无」这类否定词也收进去理由是它们是高频虚词但在情感分析场景删掉否定词等于把「不干净」变成「干净」情感极性直接反转这属于数据层面的重大错误。我拿到任何一份stopWord.txt第一步永远是先搜一遍有没有「不」「没」「挺」「很」这类带情感强度的词有就先从停用词表里摘出来。酒店评论里还有一类是需要根据项目目标决定的词比如「酒店」这个词本身。如果你的目标只是区分正负情感删掉它没问题因为正负样本里都会高频出现对分类没有区分度。但如果你后续要做主题分析比如想知道大家到底在夸设施还是骂服务那就不能删。这个决定要在写脚本前做好而不是跑完再回头改。3.2 词向量脚本4_getwordvecs.py 怎么把句子变成数字情感分析和所有机器学习任务一样喂给模型的必须是数字。4_getwordvecs.py 做的就是把一串词变成一串实数向量。文件名叫getwordvecs常见实现是借助gensim训练Word2Vec然后把每条评论里所有词的向量取平均作为整条评论的向量。这样每条评论最终就是一个固定长度的向量可以直接进分类器。from gensim.models import Word2Vec # sentences 是分词并去停用词后的二维列表例如 [[服务,很差], [房间,干净]] model Word2Vec(sentences, vector_size100, window5, min_count2, sg0, epochs5) def sentence_vector(words, model): vecs [model.wv[w] for w in words if w in model.wv] if not vecs: return [0.0] * model.vector_size n len(vecs) return [sum(v[i] for v in vecs) / n for i in range(model.vector_size)]参数这里是最值得细看的部分。vector_size是词向量维度100是这个数据量级别下的稳妥选择维度太低表达力不够太高则后面SVM训练变慢而且容易过拟合真正要调大维度的时候等数据量到几万条再考虑200维。window5表示每个词只看前后5个词内的上下文酒店评论句子平均长度在20字以内5这个窗口刚好覆盖短语级搭配。min_count2的意思是出现次数小于2的词直接不训练向量这能滤掉大量只出现一次的人名、错别字是防止词表爆炸的第一道闸门。sg0用CBOW训练速度快如果你的硬性指标是更高质量的向量就改sg1用skip-gram代价是时间翻倍。epochs5是初值建议后面在5到20之间试。sentence_vector里那个if not vecs分支是很多人会漏掉的边界处理一条评论如果所有词都不在模型词表里vecs就是空列表直接求平均会除零报错。返回全零向量虽然也不是好办法但至少能保证流水线不中断而且这类样本通常数量极少对整体准确率影响可忽略。另一种更稳的做法是把这类样本直接丢进一个unknown列表训练结束后统计条数如果超过总样本的5%说明分词或min_count有问题。这里有个黑匣子要注意Word2Vec训练出来的向量是随机的初始化和训练顺序都会影响最终向量所以同样代码跑两次准确率会有细微浮动。这不是bug是算法本身的随机性。你要做对照实验的时候记得先固定随机种子否则两次结果之间的差异会混入随机噪声。提示做对比实验前先固定Word2Vec的随机种子不然你分不清准确率波动是特征变化还是随机性造成的。3.3 为什么用 Word2Vec 而不是 TF-IDF 或 BERT拆这套项目的人经常会问一个问题都2025年了情感分析不做BERT我的看法是这个问题要回到资源本身定义的任务——小样本、二分类、纯CPU、追求可解释。三种特征方案对比如下方案维度形态训练成本是否保留词序适合场景TF-IDF词袋稀疏向量维度词表大小低否快速基线、词频统计Word2Vec取平均稠密向量维度vector_size低否平均后丢失词序本项目主方案BERT句子级稠密向量高需GPU是数据量大、要求高精度Word2Vec在这条流水线里胜出不是因为理论最优而是和后面的PCASVM搭配最顺。它的稠密低维向量经过PCA后还能继续压缩SVM在小样本低维空间里收敛稳定TF-IDF的特征维度会膨胀到几千甚至上万PCA降维也不是不行但词频信息不一定比语义向量更有用。BERT那条路留给数据量和算力都充裕的下一步而不是在这个资源里硬磕。4. PCA 降维与 SVM 分类5_pca_svm.py 的流水线与调参4.1 从词向量到特征矩阵数据形状再确认前三步做完内存里应该有两样东西每条评论的句子向量和对应的标签。先把自己的数据形状理清楚再谈模型。一个非常常见的低级错误是X和y的行数对不上SVM一训练就报shape mismatch。我习惯在进模型前加一段断言import numpy as np X np.array(sentence_vectors) # 形状 (N, 100) y np.array(labels) # 形状 (N,)正面1负面0 assert X.shape[0] y.shape[0], fX和y行数不一致: {X.shape[0]} vs {y.shape[0]} assert X.shape[1] model.vector_size, 特征维度不等于词向量维度标签的编码方式也趁现在定好正样本记为1负样本记为0。注意数据顺序如果代码里先是全部正样本、再是全部负样本不shuffle直接切训练测试集会出现训练集全正、测试集全负的荒谬局面。从文件名拼数据的时候尤其要警惕这种顺序性。4.2 PCA 的参数选择保留多少维才算够5_pca_svm.py 里的PCA干的是降噪加压缩。Word2Vec平均后的100维向量里相邻维度之间有相关性SVM在RBF核下对这种冗余并不敏感但维度高会让核计算变慢、训练变久在小样本场景还可能引入过拟合。降维后的数据更稳。代码写起来很常规from sklearn.preprocessing import StandardScaler from sklearn.decomposition import PCA scaler StandardScaler() X_scaled scaler.fit_transform(X) pca PCA(n_components0.95) # 保留累计方差贡献率 95% X_pca pca.fit_transform(X_scaled) print(f原始维度: {X_scaled.shape[1]} - PCA后维度: {X_pca.shape[1]})参数上有个反直觉点n_components用0.95这个比例而不是固定50因为不同数据集的累计方差曲线不一样。100维的Word2Vec向量在酒店评论这种短文本上通常保留到95%方差只需要30到60个主成分如果固定取50有可能砍得太多或留得太多。先跑一次打印pca.explained_variance_ratio_.cumsum()看曲线再确定要不要把0.95调成0.99这是我每次都强制走一遍的步骤。StandardScaler必须在PCA之前先把每个维度标准化成均值为0、方差为1。这是因为Word2Vec各维度的数值范围天然不同不标准化的话数值大的维度会主导PCA主成分PCA就变成偏向「数值大小」而不是「方差结构」了。顺序反掉的翻车案例不少我见过有人把PCA放在缩放前面结果降维后可视化一团糟最后发现是这一步错了。4.3 支持向量机的核函数与参数降维之后接SVM是这个资源的技术主线也是5_pca_svm.py的最后一块。数据量在千条级别类别只有两类特征维度压缩后几十维这是SVM最舒服的区域——非线性靠核函数实现样本量小所以训练快不容易像深度模型那样欠拟合。from sklearn.svm import SVC from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test train_test_split( X_pca, y, test_size0.2, random_state42, stratifyy ) clf SVC(kernelrbf, C1.0, gammascale, class_weightbalanced) clf.fit(X_train, y_train)三个参数要单独说。kernelrbf是默认也最常用的非线性核适合这种没有明确线性边界的情感文本C1.0是软间隔惩罚系数C越大越强调正确分类、越容易过拟合小样本先给1.0起步后面网格搜索在0.1到10之间扫gammascale是RBF核的宽度自适应版本sklearn会自动按1/(特征数 * 方差)来算比手动填一个数字更稳普通项目直接scale起步。class_weightbalanced是个容易被忽略的好参数如果你的正负样本不是严格均衡它会让模型按类占比自动加权后面有专门一节讲这个。训练完不要急着看准确率。第一步是看训练集和测试集的差距如果训练集95%测试集也95%说明拟合正常如果训练集99%测试集只有85%这是过拟合信号C要往下调或者PCA保留维度再砍一点如果两边都只有70%特征质量有问题回到分词和停用词去查。4.4 跑通后先看准确率再看类别分布最后用一条最简单的评估命令收尾这个环节from sklearn.metrics import accuracy_score, classification_report y_pred clf.predict(X_test) print(accuracy:, accuracy_score(y_test, y_pred)) print(classification_report(y_test, y_pred, target_names[neg, pos]))classification_report会同时给出precision、recall、F1和各类别样本数。这里要刻意复盘一个判断逻辑准确率90%在正负样本七三开的数据集里可能只是「全猜正类」的表演所以只看accuracy等于把分类器的短板藏起来。哪怕你已经准备在下一章看混淆矩阵现在也要先扫一眼report里neg那一行的recall如果明显低于pos说明模型把不少负面评论错判成了正面这对情感分析任务来说是最贵的一类错误——用户差评被无视了。翻车案例里十个有八个是负类recall崩掉。5. 中文情感分析避坑清单编码、词向量与样本失衡这一章写给所有想把这套代码在自己机器上复现的人。下面四条是我实际拆包过程中踩过或看别人反复踩的坑按「现象→原因→解决」列清楚最后附一条自查顺序。5.1 编码报错UnicodeDecodeError 是最常见的第一道坎现象跑1_process.py或任何读txt的脚本立刻抛UnicodeDecodeError: utf-8 codec cant decode byte 0xd6 in position ...或者文件内容读出来全是乱码。原因Windows记事本默认保存为ANSI中文环境就是GBK而代码里读取时用的是utf-8另一类是文件带BOM头读取时第一个字符变成\ufeff分词结果里每个句子开头都多一个奇怪的字符。解决同一份txt在不同操作系统里编码可能不一样最稳的做法是读文件时先探测后指定。我自己习惯直接给打开函数加一个编码兜底for enc in (utf-8, gbk, utf-8-sig): try: with open(neg.670.txt, r, encodingenc) as f: lines f.readlines() break except UnicodeDecodeError: continueutf-8-sig专门处理带BOM的文件它会自动吃掉前缀的\ufeff。如果这三种都失败说明文件不是纯文本编码用二进制模式打开看一眼前几个字节再判断。治本的办法是拿到资源后先用编码工具把全部txt统一转成UTF-8无BOM后面所有脚本就不再需要编码兜底。5.2 分词后句子被停用词「洗空」现象跑完3_stopword.py打印过滤后的句子发现不少评论的长度变成0或者只剩一两个词更隐蔽的是后续sentence_vector返回全零向量的样本异常偏多。原因两种可能叠加。一是当前停用词表太大且包含常见情感否定词把「不」「没」都删了导致「一点都不干净」被过滤成「一点」「干净」二是分词阶段把「隔音差」切成「隔音」「差」「差」又刚好在停用词表里上下文信息直接被清掉。解决先把停用词表里所有否定词捞出来看一遍grep -n -E ^(不|没|无|别|莫|挺|很|太) stopWord.txt命中项一律从停用词表中移除。同时在3_stopword.py里加一条统计过滤后句子长度为0的样本单独打印前十条确认它们原本长什么样。如果总空样本超过2%回退到上一章查自定义词典是否加过头。这里我吃过一次亏为了压高频词把「太」加进了停用词表结果「太差了」直接变成「差了」负向强度被腰斩那版模型的负类recall一直卡在70%附近排查了两天才发现是停用词表的问题。从那以后停用词表的每次改动都要grep一遍否定词和程度副词。5.3 词向量训练结果几乎全是「unknown」现象4_getwordvecs.py跑完之后sentence_vector里if not vecs分支触发率超过20%大量评论返回零向量后续模型准确率掉到及格线以下。原因最典型的是min_count设得太大。假设数据总共才1300多条评论、词表规模几千min_count5会把大部分低频但情感明确的词如「吵」「臭」「惊喜」全部滤掉词表可能只剩几百个通用词每条评论能命中词表的词自然就少。次要原因是分词阶段自定义词典里词频设置太高jieba强行保留了一堆长尾组合词而这些词在Word2Vec里因为出现次数不足根本没训练出向量。解决先打印词表大小确认print(len(model.wv), model.wv.most_similar(干净))词表小于1000就要警惕。把min_count从5往下降先试2仍不行就试1代价是词表变大有少量噪音但对小数据集属于必须接受的权衡。另外用model.wv.most_similar(干净)抽查几个词的近邻结果如果「干净」的近邻是「利落」「整洁」说明向量质量正常如果是莫名其妙的词把sg改成1少吃几轮再试。5.4 正负样本失衡SVM 把几乎所有评论判成正面现象分类报告里pos那一行的recall接近100%负类recall只有50%或者模型对所有测试样本都预测为正类。原因正样本文件有两个pos.719和pos.709有人贪心把两份都合并进训练集负样本只有670条正负比接近2:1再加上train_test_split没有用stratify测试集里负类样本更是少得可怜SVM为了整体准确率选择牺牲负类。解决两条线同时做。第一尽量只用一个正样本文件按670:719这种接近1:1的比例跑实在要合并就做去重并保持正负比不超过1.5:1第二切分必须stratifyfrom sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy )stratifyy会让训练集和测试集里的正负比例与全量数据一致从源头堵住测试集类别分布畸变。如果合并之后比例还是失衡就要给分类器加class_weightbalanced这一步在上一章代码里已经写了现在解释它的作用SVM的损失函数里少数类的错分代价被自动放大模型不再无脑追多数类准确率。5.5 问题定位顺序到处乱试不如按链条查以上四条坑经常连锁出现编码问题会导致乱码进而影响分词分词影响词表词表影响向量向量影响模型。我建议遇到模型效果差时按下面顺序从头查而不是先调SVM的C和gamma读文件有没有报错或乱码统一编码为UTF-8无BOM打印10条分词结果确认否定词和领域词是否被切碎或删除检查停用词表里有没有「不/没/太/很」这类情感词检查词表大小和unknown比例min_count和vector_size是否匹配数据量最后才回头看train_test_split和SVM参数对比训练集/测试集准确率差。这五步走完还没解决再把数据分布和分类报告截图出来做针对性分析基本能定位九成问题。剩下的那一成多半是数据本身标签错误比如正面文件里混入了「住得很糟」这种负向句子这是我遇到的最让人无语的一种坑只能靠随机抽样人工核查。6. 验证与进阶用交叉验证和混淆矩阵把准确率「打假」6.1 交叉验证别再靠一次性切分自我安慰random_state42切出来的那一次测试集结果只能证明模型在那20%样本上没翻车不能代表模型稳定。我每次跑完单次切分都会补一遍交叉验证from sklearn.model_selection import cross_val_score scores cross_val_score(clf, X_pca, y, cv5, scoringaccuracy) print(fCV准确率: {scores.mean():.3f} ± {scores.std():.3f})如果均值比单次测试低3个点以上说明之前那一次是运气好模型实际能力没那么强。标准差的容忍线我一般设在0.02以内超过就优先怀疑样本量太小或特征里有异常样本。6.2 混淆矩阵手算 precision、recall 和 F1classification_report虽然方便但自己手算一遍混淆矩阵能更直观理解每个数字的含义这也是用python数据分析与可视化的思路在文本分类评估上的实际应用from sklearn.metrics import confusion_matrix tn, fp, fn, tp confusion_matrix(y_test, y_pred).ravel() precision tp / (tp fp) recall tp / (tp fn) f1 2 * precision * recall / (precision recall)多数类准确率再高也要盯着少数的recall看。算完再把矩阵可视化出来存图作为课程设计报告或论文里的评估配图比只贴一行accuracy有说服力得多。6.3 还能往哪进阶这套流水线已经给你留好了实验位SVM的C和gamma用GridSearchCV在C: [0.1, 1, 10]、gamma: [scale, 0.01, 0.1]里扫一遍特征侧把Word2Vec换成TF-IDF做对照实验分类目标从二分类扩到好评/中评/差评三分类前提是你得补一份中性评论数据。从那以后我每次复现文本分类项目都强制自己先查编码、再查样本比例、最后才碰模型参数这个顺序帮我省掉了太多无头绪的调参时间。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →