简介:一套面向毕业设计场景的商品评论情感分析项目资料,基于机器学习与Python构建,涵盖情感分析从数据获取、文本预处理、词向量训练到模型搭建与界面展示的完整流程。适用于计算机、人工智能、电子信息等专业的在校生完成毕业论文或课程设计,也适合开发者以此项目深入理解自然语言处理与机器学习整合应用。压缩包共44个文件,主要包含可执行的Python程序、CSV与Excel格式的数据集、训练好的模型权重文件、配置说明和项目文档等,可支撑直接运行、复现实验和二次扩展,压缩包大小约66.66MB,整体结构按爬虫、预处理、模型、界面等模块划分,便于按需查找。目前已有47人学习下载,项目代码经实际运行测试通过,并获导师指导认可、答辩评审分达到95分,除作为毕业设计完整交付物外,还可基于现有框架改进算法、增加细粒度情感维度,功能性与完整性兼具。
1. 商品评论情感分析:这是一条能直接跑通的 NLP 毕设流水线
拿到这份“基于机器学习的商品评论情感分析”项目时,我最直观的感受是:它不是那种只丢给你一个 train.py 就跑路的半成品,而是把数据、特征、模型、爬虫、GUI 全部串成了一条完整的链路。做商品评论情感分析,真正让人头大的不是“用哪个模型”,而是从原始评论数据到能喂进模型的向量,中间那段又臭又长的预处理管道——这份资料恰好把这段路完整走了一遍。它适合要做毕设、课设,或者想从零跟一遍中文 NLP 分类流程的同学,不光是看代码,还能看到一份高分项目该有的完整度和工程组织方式。
2. 数据预处理:从 pos.xls、neg.xls 到 train_cut.csv 的清洗与分词链路
2.1 原始数据格式与分类标注
打开资源里的数据文件,可以看到三类原始数据:pos.xls、neg.xls和neutral.csv,分别对应正面、负面和中性评论。pos.xls和neg.xls是 Excel 格式,里面存的是用户对商品的文字评价;neutral.csv是 CSV 格式,用于扩充中性样本。还有一个test_set.csv,是后续测试模型用的独立样本集。
| 文件 | 格式 | 内容 | 用途 |
|---|---|---|---|
| pos.xls | Excel | 正面商品评论 | 训练集正样本 |
| neg.xls | Excel | 负面商品评论 | 训练集负样本 |
| neutral.csv | CSV | 中性商品评论 | 训练集中性样本 |
| test_set.csv | CSV | 混合评论 | 测试集 |
| train_cut.csv | CSV | 分词后带标签语料 | 特征工程输入 |
这三类数据合成三分类任务,为后续打标签做准备。注意:原始文本是未分词的完整句子,Excel 文件里可能掺杂着换行、HTML 片段和 URL,这些都会直接影响后续特征质量,需要统一清洗。
2.2 预处理脚本 review_pretreatment.py 的清洗、分词与打标逻辑
review_treatment/review_pretreatment.py是整条数据管线的起点。它做四件事:读原始文件、清洗文本、分词、打标签并合并输出。用 Python 代码展开这个流程,大概是下面这样:
import pandas as pd import jieba import re def clean_text(text): # 去掉 HTML 标签,评论里经常混入网页片段 text = re.sub(r'<.*?>', '', str(text)) # 去掉 URL text = re.sub(r'https?://\S+', '', text) # 去掉多余空白 text = re.sub(r'\s+', ' ', text).strip() return text # 读取正面评价,engine 指定为 xlrd 处理 .xls 老格式 pos_df = pd.read_excel('pos.xls', engine='xlrd', header=None, names=['comment']) pos_df['label'] = 1 # 正面标签记为 1 # 读取负面评价 neg_df = pd.read_excel('neg.xls', engine='xlrd', header=None, names=['comment']) neg_df['label'] = 0 # 负面标签记为 0 # 读取中性评价 neu_df = pd.read_csv('neutral.csv', encoding='utf-8') neu_df['label'] = 2 # 中性标签记为 2 def cut_and_filter(text): # 分词后过滤停用词和单字 words = [w for w in jieba.cut(text) if w.strip() and len(w) > 1] return ' '.join(words) # 合并、清洗、分词 all_df = pd.concat([pos_df, neg_df, neu_df], ignore_index=True) all_df['cut_comment'] = all_df['comment'].apply(clean_text).apply(cut_and_filter) # 打乱顺序,防止同类别样本扎堆 all_df = all_df.sample(frac=1, random_state=42) all_df.to_csv('train_cut.csv', index=False, encoding='utf-8')代码里的engine='xlrd'是踩过坑的人才写得出来的细节。pandas 读取.xls老格式时,新版 xlrd 2.x 只支持.xlsx,不指定引擎的话大概率直接抛异常。清洗阶段我把评论里的 HTML 标签和 URL 全部剥掉,因为它们对情感判断没有贡献,反而会让 word2vec 学出奇怪的词向量。分词阶段过滤掉了长度为 1 的词,中文里大量单字在情感分析里是噪音。
标签定义值得注意:正面为 1,负面为 0,中性为 2。这个标号方案在后面对应了 SVM 和 LSTM 的输出层设计——SVM 用多分类策略,LSTM 如果是categorical_crossentropy就要用 3 个输出节点。有一类常见的错误做法是把中性评论丢到测试集里不管,或者干脆删掉,结果是模型在实际预测时对中性评论特别容易翻车。
2.3 分词的边界:为什么这套方案在小样本评论数据上够用
train_cut.csv最终的行数取决于原始语料规模。对这个毕设体量的项目,训练语料通常在几千条量级,jieba 完全够用,不需要上 BERT 的分词器。分词的质量对后续 Word2Vec 影响很大,尤其是评价语里常见的叠词和口语化表达,比如“非常非常好”“物流贼快”这类词,jieba 可能会拆出“非常”“非常”“好”,这种重复不是冗余,反而是情感强度的信号。所以我一般不会做严格的去重,只做停用词过滤。
预处理输出的文件里有两个关键字段:cut_comment和label。cut_comment的格式是空格分隔的词序列,这正好是后面 Word2Vec 训练需要的输入格式;label是整数标签。这个文件也是一份独立的数据资产,哪怕后续完全换一个模型,只要把train_cut.csv重新读进来就可以再造特征,不必回头再碰原始 Excel。
预处理这条链路最容易被忽略的是编码问题。资源里的负样本 CSV 并不是 UTF-8,如果脚本里不设置encoding='gbk'或errors='ignore',读出来的内容就是一堆乱码甚至直接报 UnicodeDecodeError。这个问题在后面的避坑章节里会单独展开。
3. 特征工程与机器学习基线:Word2Vec 词向量与 SVM 分类器的配合
3.1 为什么不用 TF-IDF 而选 Word2Vec
商品评论本质上是一段短文本,TF-IDF 会把每条评论表示成一个长度等于词表大小的稀疏向量,维度上千甚至上万,而且词与词之间的关系完全丢失——“好吃”和“美味”在 TF-IDF 里是两列完全不相关的特征。而 Word2Vec 把每个词映射成稠密向量,语义相近的词在向量空间里距离更近。这就是这份项目选择 Word2Vec 的核心原因:评论里的情感表达方式太多样了,用稀疏特征很难覆盖同义改写的情况。
Word2Vec 训练完成后,句子向量怎么获得?常见做法是取句子内所有词向量的平均值。这个操作虽然简单,但在短文本情感分析上是性价比最高的特征表达方式。不用 TF-IDF 加权平均是因为评论本身就是短文本,高频词往往才是真正的评价主体,TF-IDF 加权反而会把“这个”“那个”这类词权重压低,把低频但情感明确的词权重抬上来,结果反而偏离了评论者的真实意图。算平均的代码很简洁:
import numpy as np def sentence_vector(words, w2v_model, vec_dim=100): vectors = [] for w in words: if w in w2v_model.wv: vectors.append(w2v_model.wv[w]) # 句子中全部词都不在词表里时,返回零向量 if len(vectors) == 0: return np.zeros(vec_dim) return np.mean(vectors, axis=0)参数vec_dim=100对应的是 Word2Vec 训练时的vector_size=100,两者不一致会导致后续 SVM 训练时维度报错。这个函数里我特意把“词不在词表”的情况兜底成了零向量,因为评论里总会有错别字或者未登录词,直接跳过会让句子向量丢失语义主心骨。
3.2 训练词向量:comment_text.model 与 w2v_model.pkl 的关系
资源里有两个词向量相关文件:comment_text.model和w2v_model.pkl。前者是 gensimWord2Vec训练后直接保存的原生模型,里面包含了词表、向量和训练参数;后者是把同一份模型用pickle或者 joblib 重新序列化后的结果,目的是方便在 SVM 训练脚本里快速加载。两者内容等价,区别只是加载方式。
from gensim.models import Word2Vec # 加载训练语料 sentences = [] with open('train_cut.csv', encoding='utf-8') as f: for line in f: # 假设第二列是分词后的评论文本 parts = line.strip().split(',') if len(parts) > 1: sentences.append(parts[1].split()) # 训练词向量,这里是最核心的三个参数 w2v_model = Word2Vec( sentences, vector_size=100, # 向量维度,太小语义表达不够,太大训练慢且容易过拟合 window=5, # 窗口大小,评论里情感词常与程度副词相邻 min_count=1, # 出现次数少于这个值的词会被丢弃 workers=4, # 并行线程数,取决于 CPU sg=0, # 0 表示 CBOW,小样本上比 skip-gram 更稳 epochs=20 # 训练轮次,评论语料小,多跑几轮能收敛得更好 ) w2v_model.save('comment_text.model') # 保存词向量文本格式,方便后续直接读取 w2v_model.wv.save_word2vec_format('comment_text.vector')min_count=1值得展开说。正规项目里这个词一般设成 5 或 10,用来过滤低频噪音词。但这个毕设项目的语料量有限,词频本来就低,如果设成 5,很多情感词直接就被过滤掉了,导致句子向量大量落到零向量上。sg=0选择 CBOW 也是同样的考虑,CBOW 在语料不足时比 skip-gram 更能平滑估计词义。
3.3 句子向量化与 train_svm.py 训练细节
train_svm.py做的事情就是把train_cut.csv里的每条评论通过sentence_vector变成 100 维向量,然后丢进 sklearn 的SVC训练。这里的 SVM 不是最终杀手锏,而是一个强基线——它用来证明“用向量平均+线性分类器就能达到 XX 准确率”,后面 LSTM 才有对照价值。
from sklearn.svm import SVC from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report from gensim.models import Word2Vec w2v_model = Word2Vec.load('comment_text.model') X, y = [], [] with open('train_cut.csv', encoding='utf-8') as f: for line in f: parts = line.strip().split(',') X.append(sentence_vector(parts[1].split(), w2v_model)) y.append(int(parts[0])) X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) svm = SVC(kernel='linear', C=1.0, class_weight='balanced') svm.fit(X_train, y_train) print(classification_report(y_test, svm.predict(X_test)))kernel='linear'是短文本分类的默认选择。RBF 核在样本量小的情况下极其容易过拟合,测试集准确率漂移得很厉害;线性核在这里不仅训练快,而且可解释性好——从分类报告里看每个类别的 precision 和 recall,能快速定位是哪一类评论被混淆。class_weight='balanced'解决的是样本不均衡问题,如果原始数据里中性评论明显少于正负评论,这个参数会让小类别的误判代价变高。
SVM 在这个项目里的角色是“基线+对照”。毕设答辩时老师大概率会问“为什么不用神经网络就行,还搭一个 SVM”,答案是 SVM 用一个线性分类器在同样的特征上就能给出可直接对比的准确率,LSTM 如果不如 SVM,那说明是特征或调参出了问题,而不是模型结构就一定优越。
4. 深度学习路线:用 Keras 训练 LSTM 情感分类模型的完整过程
4.1 情感分类任务里 LSTM 到底学到了什么
如果说 SVM 的特征是“句子向量的平均”,那 LSTM 的输入是“每个词的向量序列”。评论里的情感表达是有顺序的,比如“好吃但量少”——前半段是正面,后半段是负面,整个句子的情感倾向是复杂的。词向量平均会把这两个信号中和成一个向量,而 LSTM 通过门控机制按顺序处理每个词,保留住“先扬后抑”这种词序信息。这就是同项目里放 SVM 又放 LSTM 的学术意义:LSTM 不是简单地换一个分类器,而是换了一种文本建模方式。
资源里的train_lstm.py是一个完整的训练脚本,从读取语料到保存模型一次性完成。我拆解的常见结构是这样:
from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Embedding, LSTM, Dense, Dropout from tensorflow.keras.preprocessing.text import Tokenizer from tensorflow.keras.preprocessing.sequence import pad_sequences import pandas as pd # 加载预处理后的语料 df = pd.read_csv('train_cut.csv', encoding='utf-8') # Tokenizer 负责把词序列映射成整数索引 tokenizer = Tokenizer() tokenizer.fit_on_texts(df['cut_comment']) sequences = tokenizer.texts_to_sequences(df['cut_comment']) # 评论长度差异很大,必须 padding 到同一长度 max_len = 50 X = pad_sequences(sequences, maxlen=max_len, padding='post', truncating='post') y = pd.get_dummies(df['label']).values # 3 分类转 one-hot vocab_size = len(tokenizer.word_index) + 1 model = Sequential() model.add(Embedding(input_dim=vocab_size, output_dim=100, mask_zero=True)) model.add(LSTM(128, return_sequences=False)) model.add(Dropout(0.5)) model.add(Dense(3, activation='softmax')) model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy']) model.summary() # 分出一部分验证集,每个 epoch 结束后能看到过拟合苗头 history = model.fit(X, y, epochs=30, batch_size=32, validation_split=0.2, verbose=1) model.save('lstm_new.h5')pad_sequences里的padding='post'我遇到过坑:默认是前向补零,但这会破坏模型对位置的感知;在 Embedding 层设了mask_zero=True的情况下,后向补零能保证零向量不会参与 attention 或 LSTM 状态更新,语义上更合理。max_len=50是我根据商品评论长度分布选的折中值——大部分评论在 20 到 40 个词之间,设太大训练慢,设太小长评论会被截断。
LSTM 中间层用 128 个记忆单元,这是一个经验值。评论这种短文本不需要很大的隐藏层维度,128 已经能捕捉词序间的组合关系。Dropout(0.5)放在 LSTM 输出之后,是防止它在 30 个 epoch 里直接背下训练集。Dense 层用 3 个节点对应三类标签,激活函数是 softmax,所以损失函数必须配categorical_crossentropy,如果标签是整数形式就得换sparse_categorical_crossentropy,这是新手最容易搞混的搭配。
4.2 train_lstm.py 的网络结构与训练参数
刚才的代码用的是快速搭建的 Sequential 结构。在train_lstm.py里可能还会涉及早期停止或者学习率调度,这些不是必需的,但对最终准确率有明显帮助。我通常会在fit里加入:
from tensorflow.keras.callbacks import EarlyStopping early_stop = EarlyStopping( monitor='val_loss', patience=5, restore_best_weights=True ) history = model.fit( X, y, epochs=30, batch_size=32, validation_split=0.2, callbacks=[early_stop], verbose=1 )patience=5的含义是连续 5 个 epoch 验证集损失不再下降就停止。对毕设项目来说,30 个 epoch 足够长,但如果不加早停,模型大概率在第 15 到 20 个 epoch 后开始过拟合,训练集准确率逼近 99%,验证集却一路下滑。资源里能看到lstm_new.h5和lstm_three.h5两个模型文件,说明训练者曾经多次调整结构重新训练,这对最终 95 分的答辩成绩是很关键的一手实验记录。
4.3 模型保存与加载:lstm_three.h5、lstm_new.h5 和 lstm.yml 的区别
lstm_three.h5保存的可能是一个三层 LSTM 堆叠的实验版本——三层结构的表达能力强,但参数量大,在几千条评论的小语料上极易过拟合,除非配合很重的 dropout,否则效果反而不如单层。lstm_new.h5是调整后的最终版本,对应单层 LSTM+Dropout 结构。lstm.yml是模型结构的 YAML 描述文件,里面记录了每一层的类型、神经元数和激活函数。
如果你拿到这批文件想混合使用,要注意.h5文件里同时包含结构和权重,直接load_model就能用;lstm.yml只是一个结构描述,需要配合model_from_yaml再手动加载权重才能完成恢复。加载后的预测代码:
from tensorflow.keras.models import load_model import numpy as np model = load_model('lstm_new.h5') # 假设 tokenizer 已经从训练阶段保存 text = "这个手机电池太不耐用了,半天就没电" words = ' '.join([w for w in jieba.cut(text) if len(w) > 1]) seq = tokenizer.texts_to_sequences([words]) seq = pad_sequences(seq, maxlen=50, padding='post', truncating='post') # pred 是三个类别的概率分布 pred = model.predict(seq)[0] label = np.argmax(pred)np.argmax(pred)把概率最大的下标映射回标签,也就是 0、1、2 三类。这里有一个易错点:如果训练时标签是正面 1、负面 0、中性 2,那 argmax 的结果也要对齐到这个映射表,不能想当然认为 0 就是负面。这个对齐错误我见过不止一次,尤其在把别人的模型接入自己数据时。
5. 避坑记录:编码、内存、过拟合和 chromedriver 的五个真实案例
5.1 Excel 读取直接报错
现象:pd.read_excel('pos.xls')运行时抛异常,提示xlrd.biffh.XLRDError: Excel xls file; not supported。
原因:xlrd 从 2.0 版本起移除了对 xls 格式的支持,而 pandas 在未指定引擎时会默认使用 xlrd。这个项目 2021 年后创建,依赖环境里很可能装了新版本 xlrd。
解决:把读取语句改成pd.read_excel('pos.xls', engine='xlrd'),同时安装旧版依赖pip install xlrd==1.2.0。这个坑的根因不是代码逻辑错误,而是环境版本漂移,我一般会在虚拟环境里固定依赖版本,避免下次跑的时候静默升级。
5.2 分词结果全是空行
现象:train_cut.csv的cut_comment列大面积是空字符串,每行只剩一个逗号和标签。
原因:清洗函数clean_text把原始文本过滤得太狠,比如去掉 HTML 后又执行了空白压缩,但原始评论里大量内容确实只有 HTML 而无正文;另一个可能是读入 Excel 时 encoding 没对上,pandas 默认读成无效字符,后续正则全被剥掉。
解决:在clean_text里加一个判断,清洗后如果字符串长度小于 2 就直接标记为无效样本并丢弃,同时在读取 xls 时确认列名是否与预期一致——有时 Excel 第一行是表头,header=None会错误地把表头当成数据。
5.3 训练 LSTM 时内存直接耗尽
现象:model.fit刚开始跑,内存占用持续飙升,最后进程被杀。
原因:Tokenizer没有设置num_words上限,词表大小等于语料里全部词的个数。如果语料有几万条评论,词表轻松到几万甚至十几万,Embedding 层input_dim=vocab_size会把参数量撑到几百万,再加上 padding 后的序列矩阵,内存直接爆掉。
解决:给Tokenizer加上num_words=5000,只保留出现频率最高的 5000 个词;同时max_len控制为 50,batch_size调到 32。这样一来参数总量降了几个数量级,准确率不会有明显损失,因为低频词的向量本来就学不充分。
5.4 模型预测始终输出同一个类别
现象:测试集上准确率看着还可以,但拿真实评论去预测,无论说什么都返回“正面”,或者都返回“中性”。
原因:两类问题最常见——样本不均衡和阈值漂移。数据集里正面评论数量远大于负面和中性,模型学会把所有样本都判成多数类,在测试集上因为同样分布所以准确率不低;另一个原因是 LSTM 的Dense(3, activation='softmax')输出是概率分布,直接 argmax 可能过度自信,边界样本全部落到了多数类侧。
解决:确认class_weight是否启用了,LSTM 对应的方案则是改造 Dense 层输出为单个 sigmoid 节点,把任务从三分类改成正负二分类,中性评论归入负面或干脆去掉。对三分类,我一般人为调整 argmax 的阈值,比如只有概率超过 0.6 才输出对应类别,否则输出中性。
5.5 chromedriver 启动失败,爬虫跑不起来
现象:运行review_crawler/restaurant_crawler.py时 Selenium 报错WebDriverException: unknown error: cannot find Chrome binary,或者 Chrome 启动后立刻崩溃。
原因:chromedriver 版本和本机 Chrome 主版本不匹配。这是最常见的环境问题,项目里的 chromedriver 是作者当时下载的,后来 Chrome 自动更新到了新版本,老驱动就失效了。
解决:进设置查看本机 Chrome 的版本号,然后去对应站点下载匹配版本的 chromedriver,替换资源里的同名文件。这个坑与项目代码无关,但几乎所有人都会遇到,我习惯在启动爬虫脚本前加一个版本检查:driver = webdriver.Chrome()包在 try 里,捕获异常后提示用户更新驱动,而不是抛一整屏晦涩堆栈。
6. 进阶:把模型接进 GUI 并用一句话快速验证模型文件
6.1 main_page.py 的界面与模型调用逻辑
main_page.py是整个项目的对外窗口。它的逻辑不复杂:左侧是输入框,右侧是结果展示,用户粘贴一段评论后,点击按钮触发预处理和模型预测,然后把结果更新到界面。实现上会同时加载model.pkl(SVM)和lstm_new.h5(LSTM),用同一个输入跑两个模型,显示各自的分类结果和概率置信度。这种“双模型对照”的展示方式在答辩时很加分,评审老师一眼就能看到两种技术路线的差异。
import tkinter as tk from tkinter import scrolledtext from tensorflow.keras.models import load_model import joblib class SentimentApp: def __init__(self): self.root = tk.Tk() self.root.title("商品评论情感分析") self.lstm_model = load_model('lstm_new.h5') self.svm_model = joblib.load('model.pkl') # 省略界面布局代码…… def predict_sentiment(self, text): # 对文本做同样的预处理,然后分别调用两个模型 lstm_result = self.lstm_model.predict(text_sequence) svm_result = self.svm_model.predict(text_vector) return lstm_result, svm_result这里有一个细节容易被忽略:SVM 模型在训练时用的是 Word2Vec 平均向量,model.pkl里只保存了分类器参数,词向量模型需要单独加载;而 LSTM 模型的 tokenizer 也必须从训练状态恢复。如果 GUI 脚本里没有保留 tokenizer 对象,简单粗暴的做法是在训练脚本里把 tokenizerpickle保存,然后在 GUI 启动时加载进来。缺少这一步的话,界面能打开,但一预测就报错。
6.2 快速验证模型文件是否损坏的技巧
每次拿到.h5、.pkl、.model这类模型文件,第一件事不是急着加载,而是做一个十秒钟的冒烟测试。对一个随机输入跑一次 predict,确认输入输出维度匹配、不抛异常、结果是一个合法的概率分布。我把这个动作固化成了一个小脚本:
import numpy as np from tensorflow.keras.models import load_model model = load_model('lstm_new.h5') fake_input = np.zeros((1, 50)) # 假设 max_len=50 try: out = model.predict(fake_input) print("output shape:", out.shape) print("prob sum:", out.sum(axis=1)) except Exception as e: print("模型加载异常:", e)如果这个脚本能跑通,说明模型文件至少格式正确、能加载、能计算,接下来才值得花时间去接业务数据。这个习惯帮我避开过至少两次“辛辛苦苦接到项目里却因为模型文件损坏而全部重来”的情况。
对这份项目,我最后想补一句实际操作的体会:资料里的模型文件是作者在特定数据分布和预处理参数下训练出来的,如果你的数据格式、分词方式、停用词表跟原作者不一致,直接调用会得到偏低的准确率。最稳的做法是把train_cut.csv重新跑一遍预处理,再在它之上重新训练自己的模型。从那以后我每次接别人的毕设项目,都强制自己先走一遍完整的数据管线,而不是直接跳去调模型。希望帮到你。
本文还有配套的精品资源,点击获取