☰
酒店中文评论情感分析实战:从TF-IDF到预训练模型的Python实现
2026/9/28 15:10:11 网站建设 项目流程

简介:这是一套面向毕业设计场景的Python情感分析实战资源,聚焦酒店中文评论的正负极性分类,适合具备基础Python知识的本科生或自然语言处理入门者。资源包共2000个文件,压缩后约7.47MB,主要包含5个Python脚本、1份设计报告Word文档、csv汇总表及大量原始与处理后的txt数据。脚本依次实现语料预处理、分句分词、停用词过滤、词向量生成以及PCA+SVM建模预测,txt数据覆盖2000条正向和2000条负向酒店评论的原始文本与切分结果。目前已有949人学习浏览。通过这套资源,读者可以对照代码和数据复现完整实验流程,理解从文本清洗、特征表示到分类器训练的每个环节;设计报告可用于毕业设计文档参考,中间产物与原始数据一一对应,便于调试、修改和扩展后续实验。

1. 酒店中文评论的情感分析:这个毕设到底在做什么

酒店中文评论的情感分析,是自然语言处理里少有的“数据集好找、任务定义清楚、Python 生态全覆盖”的题目,因此成了毕业设计里的常青树。它要解决的事情一句话能说清:给定一条用户评论,判断用户整体情绪是正向还是负向。但真动手你才会发现,中文点评里的否定、反讽、口语新词会把一个“简单分类”搅得鸡飞狗跳。以下按我自己的落地顺序写:先定数据和指标,再跑通最小流水线,然后讲我翻过车的坑,最后补上预训练模型和验证技巧。想用它做毕设或练手项目的同学,照这条路走能少熬两周夜。

2. 先定任务再写代码:数据口径和评价指标怎么立住

很多同学一上来就装库、跑预训练模型,结果答辩时被问“你的标签怎么来的”“为什么用准确率”就卡壳。情感分析的任务定义决定了后面所有环节,这一章把数据来源、标签口径和评价指标讲清楚,这些才是毕设论文里真正撑场面的部分。

2.1 任务定位:你要做的是“极性判断”,不是情绪识别

情感分析(Sentiment Analysis)在工程上通常落到文本分类:输入一条评论文本,输出情感极性。酒店场景最常见的形态是评分映射:用户打了 1-5 星,评论写了文字,我们把星级当成弱标签。这里有个前提要先说明白:星级和文字情绪并不完全一致。有人给四星但吐槽“电梯坏了”,有人给三星但夸“前台态度好”。毕设阶段我们不做细粒度情感,先把任务定成二分类:好评/差评。

映射规则我一般这样定:4 星及以上为正向,2 星及以下为负向,3 星直接丢弃或单独做中性类。为什么丢 3 星而不是把它并进负向?因为 3 星评论的语气往往模棱两可,强行二分类会引入大量标注噪声,模型学了也学不干净。如果你论文里想多写一个实验,可以把 3 星保留做三分类,作为对比实验的一部分,但主线别放在三分类上。答辩时老师大概率会追问这个决策,论文里把“丢弃 3 星”的理由写清楚,反而是加分项。

注意:丢弃 3 星不是“数据不行就删”,而是明确的实验设计,报告里要写明删除量和理由。

2.2 数据从哪来:采集和公开数据集怎么取舍

常见做法有两种。第一种是自己写 Python 爬虫去采集公开平台的评论(美团、携程、去哪儿都有酒店点评页),这类数据真实、时效性好,但要注意对方的服务条款和反爬策略,毕设场景下控制采集量、只做学术研究问题不大。第二种是直接用公开的酒店评论数据集,网上流传的携程酒店评论语料之类的资源并不难找,优点是已经按 1-5 星标好,省去自己标注的功夫。

我的建议是:如果答辩要求里有“数据是自己采集的”,那走爬虫路线,规模控制在 6000-12000 条够用;如果对数据来源没硬要求,用公开数据能省两周时间,把精力留给模型。无论哪种,拿到的原始评论都要做三件事:去重(同一条文本被多次粘贴)、去掉无意义占位文本(比如“此用户未填写评价内容”)、按用户 ID 和时间过滤明显重复刷评的数据。另外记得把采集结果结构化存成 CSV,字段至少包含 id、评分、评论文本、评论时间。

这里多说一句:网上那些号称“免费 Python 源码大全”的二手 NLP 包,一半以上依赖过期、路径写死,拿来跑还不如从干净环境自己搭。数据同理,二手数据集的标签口径往往不透明,用之前先抽样看 20 条评论核对一下评分和文本是否匹配。

2.3 数据量、类别平衡和样本划分

酒店评论天然地不平衡:好评通常占 70% 以上。如果直接拿原始分布训练,模型很容易学成“无脑好评”,准确率虚高,差评几乎全错。处理办法有两个:一是对好评做随机下采样,让训练集正负比接近 1:1;二是保留原始分布,但在模型里加class_weight='balanced'(逻辑回归、SVM 都支持)。我习惯先下采样再做训练,因为后面上预训练模型的时候class_weight不是所有框架都方便传,不如在数据层面就平衡掉。

样本量方面,二分类任务 4000 条以上就能跑出一个像样的基线模型,8000 条以上再做深度模型会比较稳。不要盲目追求大数据量,酒店评论的表达模式其实很集中,“干净、位置好、服务态度好、性价比高”来回出现,几千条足够覆盖 80% 的常见句式。如果采集到的数据差异很大(比如不同城市、不同价位酒店混在一起),建议先按属性分组统计一下,避免模型只学到“高档酒店好评多”这种无关规律。

2.4 评价指标:准确率是最没信息量的一个数字

二分类且类别不平衡的任务里,准确率(accuracy)会骗人:全预测好评都有 70% 的准确率。所以主指标要用宏平均 F1(macro-F1)再加上混淆矩阵。差评的召回率尤其重要,答辩时老师一定问“你的模型对差评的识别能力怎么样”。精确率、召回率、F1 用classification_report一键输出,后面第三章的代码里有。另一个额外指标可以选 ROC-AUC,但对类别不平衡且样本量中等的情况,它不如 F1 直观,论文里写清楚你用什么做主指标、为什么,比堆指标更有说服力。

3. 用 Python 跑通最小可复现流水线:清洗、分词、特征、训练

第三章是能直接抄作业的部分。我的原则是先跑通一个最小闭环,再用多长的时间去换更好的模型。下面 5 个小节对应 5 个步骤,每一步都给出代码、逻辑说明和参数含义。

3.1 环境准备:依赖和项目结构

先交代环境:Python 3.9 以上就行,IDE 用你顺手的即可,vscode 或 pycharm 都行,关键是虚拟环境要干净。新建一个项目目录,至少分data/、src/、output/三个文件夹。依赖我列一个最小集合,其余用到再装:

pip install pandas numpy jieba scikit-learn matplotlib

说明:jieba 做中文分词,scikit-learn 提供 TF-IDF 和分类器,pandas 做数据读取和清洗,matplotlib 最后画图用。如果后面要用预训练模型,再单独加 transformers 和 torch,不要一开始就把重依赖装进来,省得环境出问题排查半天。

3.2 读取数据与文本清洗

假设你已经有一份 CSV,字段至少包含 label(0/1)和 text(原始评论)。清洗这一步经常被低估,但它的影响比换模型还大。

import re import pandas as pd df = pd.read_csv('data/hotel_reviews.csv') def clean_review(text: str) -> str: if not isinstance(text, str): return '' # 去掉 HTML 标签,点评页爬下来的文本常有残留 text = re.sub(r'<[^>]+>', '', text) # 去掉 URL text = re.sub(r'http\S+|www\.\S+', '', text) # 压缩多余空白 text = re.sub(r'\s+', ' ', text).strip() return text df['clean_text'] = df['text'].apply(clean_review) df = df[df['clean_text'].str.len() >= 4] # 太短的文本没有判别意义

逻辑说明:三个正则分别处理三样东西——HTML 标签(爬虫常见残留)、链接、以及多余空白。最后一行过滤掉长度不足 4 个字符的评论,这类文本大多是“不错”“太差”这种极短评价,保留反而会放大噪声。清洗之后记得打印几条看看,确认没有把有效内容误删。

3.3 分词与停用词:中文 NLP 的第一道坎

分词用 jieba,它对点评类短文本的默认效果已经不错。停用词表我不用网上那种几百词的大表,而是自己维护一份小的,里面只放标点、语气助词、无意义的量词。

import jieba import re stopwords = set(['的', '了', '是', '在', '就', '都', '也', '还', '有', '非常', '比较', '很', '毕竟', '反正', '然后']) def seg_review(text: str) -> str: words = jieba.lcut(text) # 过滤停用词、纯标点符号 words = [w for w in words if w.strip() and w not in stopwords and not re.fullmatch(r'[\W_]+', w)] return ' '.join(words) df['seg_text'] = df['clean_text'].apply(seg_review)

逻辑说明:jieba.lcut返回词列表,' '.join把分词结果拼成以空格分隔的字符串,这样可以直接喂给 TF-IDF 的默认 tokenizer。特别注意停用词表里我没有删“不、没、别、太、这么”,这些词的删与不删会直接影响情感极性的判断,第四章第一坑就是栽在这上面。这里删掉的“非常、比较”属于程度副词,不是极性词,留着只会给特征空间加噪声。

提示:第一次跑通后,强烈建议随机打印 20 条分词结果检查一遍,再往下走。分词和清洗阶段的错误,后面模型再强也救不回来。

3.4 特征化与基线模型:TF-IDF + 逻辑回归

情感分类的基线模型,我强烈推荐 TF-IDF + 逻辑回归(LogisticRegression)。它训练快(几千条数据几秒钟)、系数可解释(直接看哪些词推正向、哪些词推负向)、而且效果不差,作为答辩时的对比基线无可挑剔。

from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression from sklearn.pipeline import make_pipeline from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split( df['seg_text'], df['label'], test_size=0.2, random_state=42, stratify=df['label'] ) pipe = make_pipeline( TfidfVectorizer( tokenizer=lambda x: x.split(' '), ngram_range=(1, 2), max_features=20000 ), LogisticRegression(C=1.0, max_iter=1000, class_weight='balanced') ) pipe.fit(X_train, y_train)

参数说明:ngram_range=(1, 2)会把相邻两个词拼成二元特征,能部分缓解否定词被切碎的问题;max_features=20000限制词表规模,防止出现频率过低的噪声词进入模型;class_weight='balanced'自动放大少数类权重。C=1.0是正则强度的默认值,如果过拟合(训练集 F1 远高于测试集)就调小,比如 0.5 或 0.1。tokenizer=lambda x: x.split(' ')直接按空格切分,因为我们前面已经分词并用空格连接好了。

3.5 评估:别只看准确率

from sklearn.metrics import classification_report, confusion_matrix y_pred = pipe.predict(X_test) print(classification_report(y_test, y_pred, target_names=['negative', 'positive'])) print(confusion_matrix(y_test, y_pred))

逻辑说明:classification_report会输出正向和负向各自的精确率、召回率、F1。重点看 negative 行的 recall,它衡量的是差评能找回多少,这才是情感分析在不平衡场景下的核心指标。如果 negative recall 低于 0.7,建议先回到数据层面做平衡,而不是急着换模型。混淆矩阵的四格一看就懂:左上角是负向预测对的数量,右下角是正向预测对的数量,右上角和左下角分别是两类错误。

4. 排查中文评论情感分析的 5 个典型翻车现场:现象、原因、解决办法

这一章写我实际踩过的坑,一条条按“现象 → 原因 → 修复”来。每条都能对应你跑实验时可能遇到的崩溃瞬间,也是论文“实验分析”部分最好写的素材。

4.1 否定词被停用词表删掉,差评全变好评

现象:训练完后模型对“服务不怎么样”“卫生不太好”这类评论全部判成正向,negative 召回率惨不忍睹。

原因:我把网上抄来的通用停用词表直接用了,那张表里赫然列着“不、没、别、太、很”。这些是否定词和程度词,是判断极性最关键的信息,删掉之后特征空间里只剩“服务、卫生、好、满意”这些正面词。

修复:停用词表一定要自己维护,只删标点和语气词。更稳的做法是分词后让二元特征把“不”和后面的词拼起来,ngram_range=(1, 2)已经能覆盖“不/满意”这类组合。如果你的分词把“不怎么样”切成“不/怎么样”,二元特征就是“不_怎么样”,同样能表达否定。

4.2 训练集和测试集来源不同,模型当场崩掉

现象:用公开数据集训练的表现挺好(macro-F1 0.9),换到我自己采集的 2000 条评论上测试,macro-F1 直接掉到 0.6。

原因:两个来源的评论风格、平台话术、时间跨度都不一样。公开数据集大多是几年前的文本,句式和用词跟新采集的有明显差异;更隐蔽的是评分口径不同,有的平台 3 星算中评,有的平台 3 星算差评,标签对齐本身就出错了。

修复:要么全部用自己的数据,要么至少保证测试集和训练集来自同一平台、同一时间段。交叉验证时用 KFold 按时间或按用户分组切分,而不是完全随机切分,这样评估结果更接近真实使用场景。

4.3 过长评论被截断,关键信息在尾巴上

现象:预训练模型的输入长度超过max_length时被直接截断,训练集 F1 高但测试集不稳定,badcase 里很多长评论的判断是错的。

原因:酒店点评虽然平均只有几十个字,但冷门长评论能到 300 字以上。常见配置max_length=128截断时默认从头保留、从尾丢弃,而很多吐槽的重点恰恰在最后一句(比如“总之不会再来了”)。

修复:先统计语料长度分布,df['clean_text'].str.len().describe()看 95 分位,占比超过 95% 的文本长度就是合理值,一般是 128 或 256。如果必须截断,可以改成保留头部加尾部各一半,或者用 TextRank 先抽出关键句再喂模型,但这属于加分项,不是必做。

4.4 类别不平衡导致“无脑好评”,差评召回率 0

现象:训练完打印分类报告,negative 的 recall 是 0.00,positive 是 1.00,但 accuracy 高达 85%,答辩老师一看混淆矩阵就皱眉头。

原因:原始数据好评占 80% 以上,模型学到的先验就是“全都预测好评”,损失函数里正确率已经很高,优化器没动力去改。

修复:两个手段叠加。数据层面做下采样,让训练集正负比接近 1:1;模型层面加class_weight='balanced'。测试集不要动,保持真实的自然分布,这样评估出来的 F1 才有参考意义。两组实验都要在报告里写:一组是原始分布,一组是平衡后分布,对比着讲“类别不平衡带来的影响”。

4.5 jieba 词典不认识新词,口语化表达被切得稀碎

现象:“房间巨吵”被切成“房间/巨/吵”,“价格小贵”被切成“价格/小/贵”,“yyds”直接变单字,模型对这些文本的预测基本靠猜。

原因:jieba 的默认词典是通用语料统计出来的,对点评里的口语化新词、网络热词覆盖很差。

修复:维护一份自定义词典hotel_words.txt,每行一个词,格式是“词 词频 词性”,词频给 1 就够,让 jieba 尽量保留整词:

巨吵 1 小贵 1 yyds 1 隔音差 1 性价比高 1

然后在分词前执行jieba.load_userdict('data/hotel_words.txt')。加载后先打印两条评论看分词结果,确认词典生效。自定义词典别贪多,30-50 个高频的领域词就够,加太多会把正常词组切开,反而产生新噪声。

5. 从基线到加分项:对比实验、预训练模型和可视化

第四章把坑填平之后,你的基线模型应该已经能用了。这一章讲怎么把它升级成一个“能拿出去答辩”的方案:对比实验怎么设计,预训练模型怎么最小接入,可视化怎么做,最后怎么把模型包装成交付物。

5.1 为什么必须做对比实验,基线怎么搭

毕设答辩的经典问题是“你的模型为什么比 baseline 好”。没有对比实验,这个问题的答案只能是“我不知道”。实验设计至少要三档:

  • 规则/词典基线:用常见中文情感词典对评论句子打分,正分判好评、负分判差评;
  • 机器学习基线:第三章的 TF-IDF + 逻辑回归,再加一个朴素贝叶斯或 SVM 都可以;
  • 深度模型:TextCNN 或 BiLSTM,加分项可以上 BERT。

下面是我在类似任务上见过的经验范围,不是严格基准,用来帮你判断自己的结果是不是正常区间:

模型训练时间量级macro-F1 常见区间是否可解释
情感词典规则秒级0.62-0.70是
TF-IDF + 逻辑回归秒级0.82-0.88是
TextCNN分钟级0.83-0.89部分
BERT-base小时级0.88-0.93部分

注意这个表的含义:传统机器学习不一定被深度学习碾压,尤其在几千条数据的小任务上,TF-IDF + LR 往往和 TextCNN 打成平手。答辩时把这个表做出来,能讲清楚“深度模型在这个任务上并没有绝对优势,成本反而更高”,这是一个有价值的独立结论。

5.2 预训练模型的最小接入方案

如果想让毕设有一个技术亮点,我建议用 Hugging Face transformers 的 AutoModelForSequenceClassification,以 bert-base-chinese 为例,代码骨架:

from transformers import AutoTokenizer, AutoModelForSequenceClassification tokenizer = AutoTokenizer.from_pretrained('bert-base-chinese') model = AutoModelForSequenceClassification.from_pretrained( 'bert-base-chinese', num_labels=2 ) def tokenize_fn(texts): return tokenizer(texts, truncation=True, padding='max_length', max_length=128) train_encodings = tokenize_fn(X_train.tolist())

参数说明:max_length=128按第四章第 3 条的长度统计来定;truncation=True截断超长文本;padding='max_length'把样本补齐到同样长度,显存小就用 128 而不是 256。tokenizer 的输出是 input_ids 和 attention_mask,两者都要喂给模型。

训练参数的核心手感:learning_rate 用 2e-5 到 5e-5,我习惯 2e-5 起步;batch_size 显存不够就 8,能上 16 就 16;epoch 设 2 到 3 个,几千条数据 2 个 epoch 就够,多了必过拟合。没钱没卡的同学不用慌,小数据集上 CPU 也能硬跑 BERT,就是慢几倍,提前把数据预处理和结果评估准备好,训练环节剩下的就是等。

5.3 数据可视化:词云、分布图和属性维度

Python 数据分析与可视化这一环是毕设报告里最出效果的。常见三项:情感分布饼图,统计预测结果里正负向占比;词云,分开生成好评词云和差评词云,看高频词差异;属性维度对比,把评论按关键词分组(卫生、位置、服务、设施、价格),分别算情感得分,出一条横向柱状图。

WordCloud 的坑在字体。中文字体需要指定font_path,Windows 一般用 simhei.ttf,Linux 可以用系统自带的文泉驿字体。代码骨架:

from wordcloud import WordCloud import matplotlib.pyplot as plt wc = WordCloud( font_path='C:/Windows/Fonts/simhei.ttf', # Linux/mac 换成自己环境里的中文字体 width=800, height=600, background_color='white' ) wc.generate(' '.join(positive_words)) plt.imshow(wc) plt.axis('off') plt.savefig('output/positive_wordcloud.png', dpi=150)

说明:positive_words是从所有被预测为好评的评论里收集的词语列表。词云只能做“高频词分布”的直观展示,不能当分析结论,报告里一定要配统计数字一起写,否则答辩老师的下一个问题就是“这词云能说明什么”。

5.4 把模型包装成能演示的东西

毕设交付一般有两种形式:一是论文加实验记录,二是可演示的小系统。如果你选第二种,别上大框架,FastAPI 或 Flask 起一个单接口就够了,这里给 FastAPI 的最小例子:

from fastapi import FastAPI app = FastAPI() def predict_one(text: str): seg = ' '.join(seg_review(text)) label = int(pipe.predict([seg])[0]) prob = pipe.predict_proba([seg])[0].tolist() return {'label': label, 'prob': prob} @app.post('/sentiment') def sentiment(text: str): return predict_one(text)

说明:predict_proba返回两个类别的概率,调试和演示都比只看 label 有用。FastAPI 起服务后在本地浏览器访问 /docs 就能直接测试接口,答辩现场演示很方便。如果你不想写服务,也可以用 streamlit 写一个上传 CSV 批量打分的页面,工作量不大,效果更直观。

6. 验证模型是不是“真的有用”:混淆矩阵、badcase 复议和一个小习惯

6.1 先看 badcase,再谈调参

我每次训练完第一步不是看 F1,而是把预测错的样本前 50 条拉出来打印:

import pandas as pd result = pd.DataFrame({'text': X_test, 'true': y_test, 'pred': y_pred}) bad = result[result['true'] != result['pred']] print(bad.head(50)['text'].to_string())

你马上会发现两类问题:一类是标签错了,用户三星说“还行”,你标成负向,这类不用改模型,回去修正标注;一类是文本里出现了训练集没见过的表达,比如“拉胯”“踩雷”,这类才需要补词典或加数据。数据修复的性价比永远高于模型调参。

6.2 用最简单的方式验证稳定性

好的验证不是测试集上跑一次,而是多次切分取均值。我习惯用 5 折交叉验证:

from sklearn.model_selection import cross_val_score scores = cross_val_score(pipe, df['seg_text'], df['label'], cv=5, scoring='f1_macro') print('macro-F1: %.4f ± %.4f' % (scores.mean(), scores.std()))

标准差小于 0.02 说明模型稳定;如果波动很大,多半是数据量不足或类别分布不稳,先回去加数据,而不是继续调参。

多年做文本任务的教训是:把 badcase 样本攒成一个固定文件,每次模型迭代后都对着同一个 badcase 集合看“哪些被修好了,哪些还在错”。分数是会骗人的,坏样本不会。多模态情感分析确实很火,但毕设做到文本单模态的“可解释、可复现、坑都讲得清”,已经比堆一堆说不清的黑匣子模型强得多。希望帮到你。

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

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

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

立即咨询