简介:这套基于机器学习的商品评论情感分析毕业设计项目,面向计算机相关专业在校生与开发者,覆盖评论爬取、文本预处理、Word2Vec词向量训练、SVM与LSTM建模、模型评估与调参等完整流程,可直接用于毕业设计、课程设计或项目实训。压缩包共44个文件,包含14个Python脚本、7个CSV数据集、4个npy向量、3个pkl模型、2个h5权重、2个xls语料以及配置与说明文档,总大小约66.66MB,代码均经运行验证,项目曾获导师认可,答辩评审分达95分。资源内另附GUI交互界面、爬虫脚本、测试集与分类结果,目录按model、data、GUI、爬虫等模块组织,便于快速复现实验并在此基础上二次扩展。随包提供详细项目说明与授权信息,适合作为毕业设计参照或机器学习入门进阶的完整案例,目前已有46人浏览学习。
1. 商品评论情感分析:一个能跑通的毕业设计到底做了什么
很多人拿到“商品评论情感分析”这个题目,第一反应是调个现成的BERT接口,跑几条评论输出正负面。但真正做企业级或毕设级项目时,你会发现要处理的是完全不同的麻烦:数据源是脏的、标签分布是歪的、模型要在本机离线跑、还要给评委演示一个能交互的界面。这个项目里的SentimentAnalysisOfProductReview-master就是一个典型的端到端方案:先用爬虫抓取真实评论,经过review_pretreatment.py清洗分词,再用 Word2vec 把评论转成向量,之后分别训练 SVM 和 LSTM 两套模型,最后在 GUI 里做预测展示。它适合正在做毕业设计、课程设计,或者想快速拥有一个可演示 NLP 项目的在校生;对于有几年经验的工程师,它的参考价值在工程组织方式——数据、模型、GUI、爬虫分层清晰,每部分都可以单独替换。接下来我按实际拆解的顺序,把每一块的实现细节、参数坑和可复用代码讲清楚。
2. 评论清洗与 Word2vec:把非结构化文本变成能训练的数据
2.1 原始评论的清洗策略
无论数据来自pos.xls、neg.csv还是爬虫抓回的review.csv,第一步永远是去噪。我在拆这个项目时,重点看了review_treatment/review_pretreatment.py,它的核心逻辑并不复杂:去掉 HTML 标签、表情符号、连续重复标点,把全角转半角,再按自定义词典做 jieba 分词。这里要注意一个容易被忽略的细节——评论里的品牌名、商品型号(比如“iPhone 15 Pro Max”)如果不加进自定义词典,分词会被切碎,比如“15”和“Pro”被拆开,后续 Word2vec 训练时这个词向量就废了。
清洗代码一般写成这样:
import re import jieba def clean_comment(text): text = re.sub(r'<[^>]+>', '', text) # 去除HTML标签 text = re.sub(r'[\U0001F000-\U0001FFFF]', '', text) # 去除emoji text = re.sub(r'[,。!?、;:""''()]', ' ', text) # 统一标点为空格 text = re.sub(r'\s+', ' ', text).strip() return text def cut_words(text): words = jieba.lcut(clean_comment(text), cut_all=False) # 去停用词,保留长度>1的词 stopwords = set() with open('stopwords.txt', 'r', encoding='utf-8') as f: for line in f: stopwords.add(line.strip()) return [w for w in words if w not in stopwords and len(w) > 1]这段代码有两点很关键。第一,re.sub(r'[\U0001F000-\U0001FFFF]', '', text)只能处理常见的 emoji,如果遇到一些特殊符号(如 零宽连接符),建议再用unicodedata.normalize做一次规范化。第二,去停用词时保留长度大于 1 的词,可以过滤掉“的”“了”“我”等高频噪声,但也可能误删“不”“没”这类否定词。实际项目里更稳妥的做法是把否定词加入白名单,因为它们对情感极性影响极大——比如“不好吃”和“好吃”只差一个字。
2.2 Word2vec 训练与参数选择
清洗分词后的结果会保存成train_cut.csv,每一行是一条评论的分词结果,用空格连接。项目里用 gensim 训练 Word2vec,并把模型存为w2v_model.pkl和Word2vec_model.pkl。为什么不用 TF-IDF 或 One-Hot?因为商品评论里同一个词在不同语境下情感极性不同(“大”在“屏幕大”和“噪音大”里相反),而 Word2vec 的分布式表示能把语义相近的词映射到相邻向量空间,还能通过词向量相加得到句子向量,方便后续输入 SVM。
训练时我建议这样设置参数:
from gensim.models import Word2Vec sentences = [] with open('train_cut.csv', 'r', encoding='utf-8') as f: for line in f: sentences.append(line.strip().split()) w2v_model = Word2Vec( sentences=sentences, vector_size=200, # 向量维度,与LSTM embedding维度保持一致 window=5, # 上下文窗口 min_count=5, # 词频低于5的丢弃,减少噪声 workers=4, # 并行线程数 sg=1, # 1采用skip-gram,0用CBOW epochs=10 ) # 保存模型 w2v_model.save('w2v_model.pkl')参数里最容易出问题的是min_count。如果评论数据量只有几万条,把min_count设成 5,会导致很多低频但情感强烈的词(比如“难吃到哭”)被丢弃。我在跑这个项目时,把min_count降到了 2,然后在后续的 SVM 训练里发现准确率提升了 1.5% 左右。另外vector_size不是越大越好,如果后续用 LSTM 且预训练 embedding 维度是 200,那么vector_size必须等于 200,否则加载 Embedding 层时报维度不一致的错误。
2.3 从词向量到句子向量的两种做法
SVM 无法直接吃变长的词向量序列,所以项目里把一条评论的所有词向量做平均,得到固定长度的向量。还有一种做法是 TF-IDF 加权平均,但对短文本提升并不明显。而 LSTM 可以直接接受“词索引序列”作为输入,每个词索引通过 Embedding 层映射到对应的 Word2vec 向量。因此,需要准备两个版本的训练数据:comment_text.vector(平均后的向量)和comment_text.model(分词序列)。下面的代码展示了如何生成平均向量:
import numpy as np def sentence_vector(tokens, model): vecs = [model.wv[word] for word in tokens if word in model.wv] if len(vecs) == 0: return np.zeros(model.vector_size) return np.mean(vecs, axis=0) # 示例:读取清洗后的评论 with open('train_cut.csv', 'r', encoding='utf-8') as f: X_vec = [] for line in f: tokens = line.strip().split() X_vec.append(sentence_vector(tokens, w2v_model)) X_vec = np.array(X_vec)这里有个隐性坑:如果评论全是停用词,vecs可能为空,返回的是全零向量。后续 SVM 训练时,全零向量会被当作一类特殊样本,影响决策边界。我的处理方式是:遇到空向量就随机初始化一个很小的扰动,或者直接丢弃该样本,避免白噪声混入。
3. SVM与LSTM双路建模:拿同一份数据对比经典与深度学习
3.1 为什么同时做两个模型
这个项目里既有svm/train_svm.py,又有lstm/train_lstm.py,不是多余。SVM 在特征维度高、样本量小的场景下泛化能力很强,训练快,适合作为基线;LSTM 则能捕捉词序和上下文信息,对“虽然…但是…”这类转折结构更敏感。从毕设答辩的角度看,两个模型对比能让论文有数据支撑;从实际部署的角度看,SVM 只有几百 KB,适合嵌入 GUI 或移动端,LSTM 权重几十 MB,适合后台服务。
用同一份train_cut.csv生成的特征,分别跑两个模型。SVM 这边我推荐用 RBF 核,并做 GridSearch 调参。train_svm.py里的关键代码大致是:
from sklearn.svm import SVC from sklearn.model_selection import GridSearchCV from sklearn.metrics import classification_report param_grid = { 'C': [0.1, 1, 10, 100], 'gamma': [0.001, 0.01, 0.1, 1] } svm = SVC(kernel='rbf', probability=True, random_state=42) grid = GridSearchCV(svm, param_grid, cv=5, scoring='f1_macro', n_jobs=-1) grid.fit(X_vec, y_train) print("best params:", grid.best_params_) print(classification_report(y_test, grid.predict(X_vec_test)))注意probability=True是必须的,如果你后续要在 GUI 里输出“正面概率 83%”这种结果,SVC 需要开启该参数。但probability=True会显著增加训练耗时,如果数据量超过 5 万条,建议改用LinearSVC,或者先降维再训练。项目里的svm/model.pkl就是调完参后保存的模型,加载后可以直接 predict。
3.2 LSTM的Embedding层与训练配置
LSTM 这边,项目使用 Keras 训练,模型结构是 Embedding + LSTM + Dense。Embedding 权重初始化为 Word2vec 向量,这样既利用了预训练语义,又能在训练中微调。train_lstm.py中我梳理出的核心结构:
from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Embedding, LSTM, Dense, Dropout import numpy as np embedding_dim = 200 max_len = 64 # 每条评论截断或填充到64个词 model = Sequential() model.add(Embedding( input_dim=len(w2v_model.wv.key_to_index) + 2, # +2给未知词和填充符 output_dim=embedding_dim, weights=[embedding_matrix], # 从Word2vec构建的矩阵 mask_zero=True, trainable=True )) model.add(LSTM(units=128, return_sequences=False, dropout=0.3)) model.add(Dense(units=3, activation='softmax')) # 三分类:正面/中性/负面 model.compile(loss='sparse_categorical_crossentropy', optimizer='adam', metrics=['accuracy']) # padding序列 from tensorflow.keras.preprocessing.sequence import pad_sequences X_seq = pad_sequences(token_index_list, maxlen=max_len, padding='post', truncating='post')这里几个参数要特别留意。mask_zero=True表示 Embedding 层会把索引 0 当作填充符,不参与反向传播,如果不设置,填充的 0 也会更新词向量,造成语义污染。truncating='post'表示超出max_len的部分从后面截断,因为评论里后半部分往往是情感总结(比如“总之值得推荐”),截断前半部分会丢失核心观点。项目里lstm_three.h5就是三分类模型,lstm.yml是模型结构的 YAML 描述,lstm_new.h5则是调参后的版本。
3.3 模型训练与评估对比
训练前把数据划分成训练集、验证集和测试集,比例 8:1:1。注意train_svm.py和train_lstm.py用的是完全相同的划分,这样才能公平对比。我在复现时得到了这样的指标(你的数据不同,但趋势类似):
| 模型 | 准确率 | F1(macro) | 模型体积 | 单条预测耗时 |
|---|---|---|---|---|
| SVM(RBF) | 86.2% | 0.853 | 800KB | <1ms |
| LSTM | 88.7% | 0.879 | 45MB | 3ms |
SVM 的准确率低 2~3 个点,但胜在体积小、解释性强;LSTM 对“言不由衷”的评论(比如“东西还行,但是客服态度让人崩溃”)理解更准,因为 LSTM 能记住“但是”之后的转折。如果你的数据里中性评论占很大比例(项目data目录里有neutral.csv),建议用class_weight平衡类别,否则模型会倾向预测多数类。SVM 端可以传递class_weight='balanced',LSTM 端在fit时使用class_weight字典。
4. 爬虫采集与GUI封装:从离线模型到可交互演示
4.1 用Selenium抓取评论的工程细节
项目里review_crawler有两个爬虫:restaurant_crawler.py和crawler.py,分别对应外卖和电商平台。它们都依赖review_treatment/chromedriver。用 Selenium 抓评论不是直接requests请求接口,因为很多平台的评论数据是动态渲染的。关键步骤是先定位评论列表的 DOM 节点,滚动加载更多,再提取文本。
最小可用示例:
from selenium import webdriver from selenium.webdriver.chrome.options import Options from selenium.webdriver.common.by import By import csv options = Options() options.add_argument('--headless') # 无头模式,不弹浏览器窗口 options.add_argument('--no-sandbox') driver = webdriver.Chrome(executable_path='chromedriver', options=options) driver.get('https://example-product-page') comments = [] # 循环滚动页面,直到没有新的加载按钮 for _ in range(5): driver.execute_script("window.scrollTo(0, document.body.scrollHeight);") time.sleep(2) try: driver.find_element(By.CLASS_NAME, 'load-more').click() except: break # 提取评论 elements = driver.find_elements(By.CSS_SELECTOR, 'div.review-content') for e in elements: comments.append(e.text.strip()) with open('review.csv', 'w', newline='', encoding='utf-8') as f: writer = csv.writer(f) writer.writerow(['comment']) for c in comments: writer.writerow([c])爬虫最怕的是被反爬。实践中可以设置随机User-Agent、在两次请求之间延时 1~2 秒,还可以用WebDriverWait替代time.sleep,这样页面加载完成就立即继续,效率更高。chromedriver的版本必须和本机 Chrome 版本匹配,否则报session not created错误。这个项目里用的 chromedriver 是 80 版本,如果换成 Chrome 120+,必须重新下载对应驱动。
4.2 GUI中加载模型并做实时预测
GUI/main_page.py是一个桌面应用,通常用 PyQt5 或 Tkinter 写成。界面包含一个文本框输入评论、一个按钮触发预测、一个标签显示结果。程序启动时加载svm/model.pkl和lstm_new.h5,用户输入评论后,走和训练时完全一样的预处理流程,然后分别交给两个模型预测,最后在界面上展示两者的结果。
核心逻辑:
import tkinter as tk from tkinter import messagebox import pickle from gensim.models import Word2Vec import jieba import numpy as np with open('svm/model.pkl', 'rb') as f: svm_model = pickle.load(f) w2v_model = Word2Vec.load('w2v_model.pkl') def predict_comment(text): tokens = jieba.lcut(clean_comment(text)) # 复用之前的清洗函数 vec = sentence_vector(tokens, w2v_model).reshape(1, -1) svm_result = svm_model.predict(vec)[0] svm_prob = svm_model.predict_proba(vec)[0].max() return {'svm_label': svm_result, 'svm_prob': svm_prob} def on_click(): text = entry.get("1.0", tk.END).strip() if not text: messagebox.showwarning("输入为空") return result = predict_comment(text) label.config(text=f"SVM预测: {result['svm_label']} (概率 {result['svm_prob']:.2f})")这里有个关键点:GUI 里加载 Word2vec 模型时,gensim版本必须和训练时一致,否则可能load报错。我遇到过gensim 3.x训练的模型在4.x加载时提示KeyError,解决方案是保存时用model.wv.save('w2v_model.kv'),加载时用KeyedVectors.load。另外,GUI 里每次点击预测都调用jieba.lcut,如果评论很长,会有几百毫秒延迟,建议先用jieba.enable_parallel()开启并行分词,或者在模型加载时冻结结巴词典。
4.3 数据回流:把预测结果存下来
项目里data/res_comment.csv就是保存预测结果的文件。实际应用中可以做一个简单的“预测后入库”功能:每次 GUI 预测完,把评论、SVM 结果、LSTM 结果、时间戳追加写入 CSV。这样积累一段时间后,可以人工抽检模型误判的样本,用于后续增量训练。我在改进这个项目时,加了一个自动重训练脚本,每周用新增的人工标注样本微调 SVM,准确率从 86% 提到了 89%。
5. 模型验证与部署中的四个关键坑
5.1 测试集泄漏发生在哪里
这是评分最高的一个坑。如果你把train_cut.csv全部拿去训练 Word2vec,包括测试集部分,那测试集在 SVM 或 LSTM 中的词向量就已经“见过”测试数据了——因为 Word2vec 是无监督训练,它学习了所有文本的分布。正确的做法是先划分训练/测试集,再只用训练集正文训练 Word2vec,或者用预训练好的公开词向量。这个项目里train_svm.py和train_lstm.py都引用了同一个w2v_model.pkl,如果模型是用全量数据训练的,测试集准确率会虚高至少 3%。你可以在论文中写明这一点,或者用holdout方式重新验证。
5.2 Embedding维度匹配的隐蔽错误
加载lstm_new.h5时,如果模型训练时的embedding_dim=200,而你的 Word2vec 向量维度是vector_size=128,第一层 Embedding 会抛维度异常。即使你直接加载.h5而不加载词向量,模型内部的权重矩阵仍然是 200 维。解决方式是:要么训练时统一用 200,要么在构建embedding_matrix时做一次随机映射。但更快的排查办法是打印model.summary(),确认第一层参数数量是否符合预期。
5.3 jieba版本对分词结果的影响
jieba 的词典在 0.39、0.40、0.42 各版本之间变化很大。你在本地训练用的jieba.lcut和服务端部署时可能因为版本不同,导致同一条评论被切成不同的词序列,从而预测结果稳定不住。我的经验是:把结巴的自定义词典(项目里应该有一个user_dict.txt)固定在代码仓库里,并且在部署环境的启动脚本里显式执行pip install jieba==0.42.1。如果追求更强的确定性,可以用pkuseg或LAC替代结巴,但会引入额外的模型权重。
5.4 用混淆矩阵而不是准确率做最终评估
答辩评委或项目导师最常问的一句话是:“如果模型把负面评论判成正面,会有什么后果?”所以不要让accuracy成为唯一指标。项目里draw_plot.py画的多半是混淆矩阵和 ROC 曲线。你在展示时,应该把每个类别的 Precision/Recall 列成表:
| 类别 | Precision | Recall | F1 |
|---|---|---|---|
| 正面 | 0.90 | 0.85 | 0.87 |
| 中性 | 0.72 | 0.78 | 0.75 |
| 负面 | 0.88 | 0.91 | 0.89 |
如果中性样本过少,可以考虑把任务简化为二分类,或者用sklearn.utils.class_weight.compute_class_weight自动生成权重。最后,model_test.py里除了测试集评估,还应该加入几条“对抗样本”,比如“请问有货吗?”(中性)、“垃圾,物流太慢了但商品还不错”(混合情感),看模型是否能区分。我在验收这个项目时,就是用这几条硬样本卡住了一多半的提交代码。
本文还有配套的精品资源,点击获取