简介:这份资源是面向高校学生与机器学习入门者的商品评论情感分析完整项目包,以SVM与LSTM双模型对比为核心,适合用作毕业设计、课程设计或NLP实战练手。包内共43个文件,涵盖14个Python源码、7个CSV与2个XLS数据集、4个NPY与3个PKL中间文件、2个H5模型权重,以及XML、YML等配置文件和chromedriver爬虫驱动,压缩包约66.66MB。项目包含数据爬取、评论预处理、Word2vec词向量训练、SVM与LSTM模型训练及可视化绘图等模块,并附带训练好的模型与GUI界面,可直接运行演示。已有417人学习下载。读者能获得一套从数据采集到模型部署的完整流程代码,理解传统机器学习与深度学习方法在情感分类任务上的差异,同时可参考目录结构与使用说明快速复现实验,适合需要完整项目方案与排错思路的开发者。
1. 商品评论情感分析:SVM 与 LSTM 两条路线怎么选、怎么落地
电商后台每天沉淀几千条评论,人工翻到第三页就眼花,老板还催着要「本周差评原因分布」。这个场景下,情感分析不是学术玩具,而是能直接省人力的工具。标题里这套东西——SVM、LSTM、Python 源码、数据集、训练好的模型、GUI 界面——本质是一个完整的二分类/多分类文本分类工程:把「好评/差评」甚至「物流/质量/服务」的倾向自动判出来。SVM 靠 TF-IDF 特征在短文本上又快又稳,LSTM 靠词序建模在长句和反讽上更有余地,两者放一起对比,正好覆盖从传统机器学习到深度学习的完整链路。适合做毕设的学生、想给后台加个舆情模块的开发者,以及需要一套能跑起来、能改、能演示的现成方案的人。
2. 数据准备与特征工程:把中文评论变成模型能吃的数字
2.1 数据集长什么样、怎么划分才不翻车
常见的中文商品评论数据集一般是 CSV 或 Excel,字段至少两列:review(评论文本)和label(情感标签,0/1 或 1-5 星映射成三类)。拿到手先别急着喂模型,第一步是看分布。如果 95% 是好评,模型全猜好评也能有 95% 准确率,这种「准确率陷阱」在毕设答辩时最容易被老师一句话问穿。
我一般会先做三件事:去重、去空、看标签比例。去重是因为爬虫抓下来的评论经常整段重复;去空是防止空字符串把 TF-IDF 的维度撑歪;看比例是为了决定要不要做重采样。如果差评只占 8%,要么上采样差评,要么在损失函数里给差评更高权重,否则 LSTM 会学会「偷懒」。
划分比例上,训练/验证/测试用 8:1:1 或 7:2:1 都行,但必须用stratify保持每份里标签比例一致。下面这段是标准操作:
import pandas as pd from sklearn.model_selection import train_test_split # 读取数据,假设列名为 review 和 label df = pd.read_csv("comments.csv", encoding="utf-8") df = df.dropna(subset=["review", "label"]).drop_duplicates(subset=["review"]) # 标签分布看一眼,差评太少后面要处理 print(df["label"].value_counts(normalize=True)) # 分层划分,保证训练集和测试集标签比例一致 train_df, test_df = train_test_split( df, test_size=0.2, random_state=42, stratify=df["label"] ) train_df, val_df = train_test_split( train_df, test_size=0.125, random_state=42, stratify=train_df["label"] )逻辑说明:drop_duplicates按评论文本去重,避免同一句话同时出现在训练和测试里造成虚高。stratify参数是关键,少了它,小类别可能全被分到测试集,验证结果会莫名其妙地差。random_state固定后,每次跑结果可复现,答辩演示时不会「上次 92% 这次 88%」说不清。
参数说明:test_size=0.2表示测试集占 20%,剩下 80% 再切出 12.5% 做验证,最终是 70/10/20。如果数据量小于 5000 条,验证集可以再小一点,但测试集不要低于 10%,否则指标波动太大。
2.2 中文分词与停用词:jieba 的三个必调参数
中文不像英文有空格,必须先分词。jieba是最常用的,但默认模式对商品评论里的「续航不行」「客服态度差」这类短语切得不够准。我一般会加载自定义词典,把「续航」「性价比」「物流速度」这类领域词加进去,否则「性价比高」会被切成「性价」「比高」,特征直接废掉。
import jieba # 加载领域词典,每行一个词 jieba.load_userdict("domain_dict.txt") # 停用词表,去掉「的」「了」「是」这类无意义词 with open("stopwords.txt", encoding="utf-8") as f: stopwords = set([line.strip() for line in f]) def cut_text(text): # 精确模式,适合短文本分类 words = jieba.lcut(text, cut_all=False) # 过滤停用词和单字,单字噪声大 return " ".join([w for w in words if w not in stopwords and len(w) > 1]) train_df["cut"] = train_df["review"].apply(cut_text) test_df["cut"] = test_df["review"].apply(cut_text)逻辑说明:cut_all=False是精确模式,比全模式更适合分类任务,全模式会切出大量冗余词。过滤单字是因为「好」「差」单独出现时歧义大,但注意「差」如果出现在「差评」里已经被词典保护了。停用词表不要直接用网上的通用版,要针对商品评论删掉「买」「用」这种高频但无区分度的词。
参数说明:len(w) > 1这个阈值可以调成 2,看效果。如果发现「不」被过滤了,要把它从停用词里拿掉,因为「不好」和「好」靠「不」区分,删了就翻车。
2.3 TF-IDF 与词向量:SVM 和 LSTM 的输入分叉口
SVM 吃的是 TF-IDF 矩阵,LSTM 吃的是词向量序列,这是两条路线的分叉点。TF-IDF 用sklearn的TfidfVectorizer就能生成,关键是max_features和ngram_range两个参数。max_features=5000表示只保留权重最高的 5000 个词,防止维度爆炸;ngram_range=(1,2)表示同时考虑单字和双字词组,能捕捉「不」「好」连在一起的否定结构。
from sklearn.feature_extraction.text import TfidfVectorizer # SVM 用的 TF-IDF 特征 tfidf = TfidfVectorizer(max_features=5000, ngram_range=(1, 2), min_df=2) X_train_tfidf = tfidf.fit_transform(train_df["cut"]) X_test_tfidf = tfidf.transform(test_df["cut"])逻辑说明:fit_transform只在训练集上做,测试集用transform,这是防止数据泄露的铁律。min_df=2表示词至少出现在 2 篇文档里才保留,去掉只出现一次的噪声词。ngram_range=(1,2)会让特征数翻倍,如果内存吃紧可以改成(1,1)。
LSTM 这边需要把词映射成索引序列,再用Embedding层转成向量。常见做法是构建词表,把每个词映射到一个整数,序列长度统一截断或填充到 100。
from tensorflow.keras.preprocessing.text import Tokenizer from tensorflow.keras.preprocessing.sequence import pad_sequences # 词表大小设 10000,OOV 词用 <unk> 代替 tokenizer = Tokenizer(num_words=10000, oov_token="<unk>") tokenizer.fit_on_texts(train_df["cut"]) # 转成整数序列,统一长度 100 X_train_seq = pad_sequences(tokenizer.texts_to_sequences(train_df["cut"]), maxlen=100) X_test_seq = pad_sequences(tokenizer.texts_to_sequences(test_df["cut"]), maxlen=100)逻辑说明:num_words=10000只保留频率最高的 10000 个词,低频词直接归为 OOV。maxlen=100是经验值,商品评论一般不会太长,超过 100 个词的截断,不足的补零。补零在 LSTM 里会被 mask 掉,不影响梯度。
参数说明:oov_token必须设,否则测试集里出现训练集没见过的词会报错。maxlen可以统计一下评论长度分布,取 95 分位数,太长浪费算力,太短丢信息。
3. SVM 路线:小样本快跑,参数怎么调才不玄学
3.1 线性核还是 RBF 核:先看数据量再决定
SVM 在文本分类里默认用线性核,因为 TF-IDF 维度高、样本相对少,线性核已经够用,而且训练快。RBF 核在低维数据上强,但文本动辄几千维,RBF 计算距离会非常慢,还容易过拟合。我一般先用线性核跑一版基线,如果准确率卡在 85% 上不去,再考虑换 RBF 或者调C值。
from sklearn.svm import LinearSVC from sklearn.metrics import classification_report # 线性 SVM,C 是正则化强度的倒数 svm_clf = LinearSVC(C=1.0, class_weight="balanced", max_iter=2000) svm_clf.fit(X_train_tfidf, train_df["label"]) # 在测试集上评估 pred = svm_clf.predict(X_test_tfidf) print(classification_report(test_df["label"], pred))逻辑说明:class_weight="balanced"会自动给少数类更高权重,解决前面说的差评太少问题。max_iter=2000是防止不收敛的保险,如果训练时报警告说没收敛,就往上加。LinearSVC比SVC(kernel='linear')快很多,文本任务优先用它。
参数说明:C越大,对训练误差容忍越低,容易过拟合;C越小,正则化越强,可能欠拟合。我一般从 0.1、1、10 三个值里试,看验证集 F1 哪个高。注意LinearSVC的输出是距离,不是概率,如果要概率得用CalibratedClassifierCV包一层,但毕设演示用不上。
3.2 网格搜索找最优参数:别手动试,写循环
手动调参是血泪经验里最浪费时间的一种。GridSearchCV能自动跑交叉验证,把C和loss组合都试一遍。注意loss参数在LinearSVC里有hinge和squared_hinge两种,默认是后者,前者对噪声更鲁棒。
from sklearn.model_selection import GridSearchCV param_grid = { "C": [0.1, 1, 10], "loss": ["hinge", "squared_hinge"] } grid = GridSearchCV( LinearSVC(class_weight="balanced", max_iter=5000), param_grid, cv=5, scoring="f1_macro", n_jobs=-1 ) grid.fit(X_train_tfidf, train_df["label"]) print("最佳参数:", grid.best_params_) print("最佳 F1:", grid.best_score_)逻辑说明:cv=5是 5 折交叉验证,比单次划分更稳。scoring="f1_macro"是因为类别不平衡时准确率会骗人,F1 的宏平均对每个类别一视同仁。n_jobs=-1用满 CPU 核数,跑得快。
参数说明:如果数据超过 5 万条,cv=3就够了,5 折太慢。param_grid里的值不要设太密,先粗调再细调,比如先试 0.1/1/10,再在最优值附近试 0.5/2/5。
3.3 模型保存与加载:pickle 和 joblib 选哪个
训练好的 SVM 要存下来给 GUI 用,joblib比pickle更适合存 numpy 数组,TF-IDF 矩阵本质就是稀疏矩阵,用joblib快且文件小。
import joblib # 保存模型和 TF-IDF 向量器,两个都要存 joblib.dump(svm_clf, "svm_model.pkl") joblib.dump(tfidf, "tfidf_vectorizer.pkl") # 加载时两个一起加载,缺一不可 svm_loaded = joblib.load("svm_model.pkl") tfidf_loaded = joblib.load("tfidf_vectorizer.pkl") # 预测新评论 new_text = cut_text("质量很好,物流也快") new_vec = tfidf_loaded.transform([new_text]) print(svm_loaded.predict(new_vec))逻辑说明:只存模型不存向量器是新手最常犯的错,加载后transform会报维度不匹配。cut_text函数必须和训练时完全一致,包括词典和停用词,否则分词结果不同,特征对不上。
参数说明:joblib.dump的compress参数可以设 3,压缩后文件更小,但加载稍慢。毕设演示无所谓,生产环境看情况。
4. LSTM 路线:词序建模,训练慢但上限高
4.1 模型结构:Embedding + LSTM + 全连接的三层套路
LSTM 的核心是记住词序,比如「不」在「好」前面还是后面,情感完全相反。标准结构是Embedding层把词索引转成 128 维向量,LSTM层提取序列特征,最后Dense层输出分类。Embedding的输入维度是词表大小,输出维度 128 是经验值,太大容易过拟合,太小表达不够。
from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Embedding, LSTM, Dense, Dropout model = Sequential([ # 词表 10000,每个词 128 维向量,输入长度 100 Embedding(input_dim=10000, output_dim=128, input_length=100), # LSTM 64 个单元,return_sequences=False 只取最后输出 LSTM(64, dropout=0.3, recurrent_dropout=0.3), # 全连接层 32 个神经元,relu 激活 Dense(32, activation="relu"), Dropout(0.3), # 二分类用 sigmoid,多分类改 softmax 并调整单元数 Dense(1, activation="sigmoid") ]) model.compile( loss="binary_crossentropy", optimizer="adam", metrics=["accuracy"] ) model.summary()逻辑说明:dropout=0.3和recurrent_dropout=0.3是防过拟合的关键,LSTM 参数量大,不加 dropout 训练集准确率能到 99% 但测试集崩。return_sequences=False表示只取最后一个时间步的输出,如果后面还要接 LSTM 层就设True。
参数说明:LSTM(64)的 64 是隐藏单元数,可以改成 128 看效果,但训练时间翻倍。Dense(1, activation="sigmoid")是二分类,如果标签是三类,改成Dense(3, activation="softmax"),损失函数换成categorical_crossentropy。
4.2 训练过程:早停和模型检查点怎么配
LSTM 训练容易过拟合,EarlyStopping能在验证集损失不再下降时自动停,ModelCheckpoint保存验证集上最好的模型,而不是最后一个 epoch 的。
from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint callbacks = [ # 验证集 loss 连续 3 轮不降就停 EarlyStopping(monitor="val_loss", patience=3, restore_best_weights=True), # 只保存 val_loss 最低的模型 ModelCheckpoint("lstm_best.h5", monitor="val_loss", save_best_only=True) ] history = model.fit( X_train_seq, train_df["label"], validation_data=(X_test_seq, test_df["label"]), epochs=20, batch_size=64, callbacks=callbacks )逻辑说明:patience=3表示给模型 3 轮机会,如果 3 轮后验证损失还没降就停。restore_best_weights=True会自动恢复到最佳轮次的权重,不用手动加载。batch_size=64是折中值,太小训练慢,太大内存吃紧。
参数说明:epochs=20是上限,实际可能 8 轮就停了。如果验证损失一直在降但很慢,可以把patience调到 5。ModelCheckpoint的.h5格式兼容性好,.keras是新格式,看 TensorFlow 版本选。
4.3 用训练好的 LSTM 模型做单条预测
GUI 里用户输入一条评论,要立刻出结果,不能重新训练。加载保存的模型,把文本转成序列,注意pad_sequences的maxlen必须和训练时一致。
from tensorflow.keras.models import load_model from tensorflow.keras.preprocessing.sequence import pad_sequences # 加载最佳模型 lstm_model = load_model("lstm_best.h5") def predict_sentiment(text): # 分词,和训练时同一套逻辑 cut = cut_text(text) # 转序列并填充到 100 seq = tokenizer.texts_to_sequences([cut]) padded = pad_sequences(seq, maxlen=100) # 输出概率,大于 0.5 判为正面 prob = lstm_model.predict(padded)[0][0] return "正面" if prob > 0.5 else "负面", float(prob) print(predict_sentiment("用了三天就坏了,客服还不理人"))逻辑说明:tokenizer必须是训练时那个,不能重新fit,否则词索引全乱。predict返回的是 numpy 数组,取[0][0]拿到标量概率。阈值 0.5 可以调,如果差评漏判代价高,就降到 0.4。
参数说明:maxlen=100和训练时严格一致,不一致会报错或结果错乱。prob转成 float 是为了 GUI 显示方便,numpy 的 float32 在某些界面库会显示成0.9234567这种长串。
5. 避坑与排查:这五个坑我踩过,你别再踩
5.1 训练集和测试集分词不一致,准确率虚高
现象:训练时准确率 95%,换一批新评论预测,结果乱七八糟。原因:训练时用了自定义词典,预测时忘了加载,分词结果不同,TF-IDF 特征对不上。解决:把分词逻辑封装成一个函数,训练和预测都调同一个,词典和停用词表路径写死在配置里。
5.2 LSTM 的 maxlen 改了,加载模型报维度错误
现象:训练时maxlen=100,预测时手滑写成 200,model.predict直接抛异常。原因:Embedding层的input_length固定了 100,输入 200 长度不匹配。解决:把maxlen定义成全局常量,训练和预测都引用它,改一处全改。
5.3 标签没做映射,SVM 把「1-5 星」当成连续值回归
现象:LinearSVC训练报错说标签类型不对,或者结果全是某一类。原因:原始标签是 1 到 5 的整数,SVM 分类器要求标签是离散的类别,虽然整数也能跑,但语义上它会把 5 和 1 当成两个极端,中间值处理混乱。解决:先做映射,1-2 星归为负面 0,4-5 星归为正面 1,3 星看业务决定丢弃还是归入某一类。
5.4 GUI 卡死:预测函数在主线程里跑
现象:点「分析」按钮后界面无响应,转圈半天才出结果。原因:LSTM 的predict即使单条也要几百毫秒,放在主线程会阻塞界面刷新。解决:用threading把预测放到子线程,或者用after方法异步更新结果。Tkinter 里我一般开一个线程跑预测,完成后用queue传回主线程更新标签。
5.5 模型文件太大,打包成 exe 后启动慢
现象:毕设演示时双击 exe,等了十几秒才出界面。原因:LSTM 模型.h5文件可能几十 MB,PyInstaller打包后解压到临时目录需要时间。解决:SVM 模型通常只有几 MB,如果对启动速度要求高,演示时优先用 SVM 路线;LSTM 模型可以用model.save时开include_optimizer=False,文件能小三分之一。
6. 把两条路线接进 GUI:一个输入框背后的调度逻辑
GUI 的核心不是界面多花哨,而是让用户输入一句话,后台自动选模型出结果。我一般会做一个下拉框,让用户选「SVM(快)」还是「LSTM(准)」,然后统一走一个analyze函数。这个函数先判断模型类型,再调对应的预处理和预测逻辑,最后把标签和置信度返回到界面。
import threading import tkinter as tk from tkinter import ttk class SentimentApp: def __init__(self, root): self.root = root self.root.title("商品评论情感分析") # 下拉框选模型 self.model_var = tk.StringVar(value="SVM") ttk.Combobox(root, textvariable=self.model_var, values=["SVM", "LSTM"]).pack() # 输入框 self.entry = tk.Text(root, height=5, width=50) self.entry.pack() # 结果标签 self.result_label = tk.Label(root, text="等待分析") self.result_label.pack() # 按钮 tk.Button(root, text="分析", command=self.on_click).pack() def on_click(self): text = self.entry.get("1.0", "end").strip() if not text: return # 开子线程跑预测,避免卡界面 threading.Thread(target=self.predict, args=(text,), daemon=True).start() def predict(self, text): if self.model_var.get() == "SVM": cut = cut_text(text) vec = tfidf_loaded.transform([cut]) label = svm_loaded.predict(vec)[0] result = "正面" if label == 1 else "负面" else: result, prob = predict_sentiment(text) # 回到主线程更新界面 self.root.after(0, lambda: self.result_label.config(text=result)) root = tk.Tk() app = SentimentApp(root) root.mainloop()逻辑说明:threading.Thread的daemon=True保证关窗口时线程自动结束,不会残留。root.after(0, ...)是 Tkinter 的线程安全更新方式,直接在子线程里改Label会随机崩溃。model_var用StringVar绑定下拉框,切换模型不用重启程序。
参数说明:height=5, width=50是输入框大小,评论一般不会超过这个范围。如果要做批量分析,把Text换成Listbox,循环调predict并显示进度条。daemon=True在打包成 exe 后同样有效,不用担心线程泄漏。
一个具体技巧:把cut_text、tfidf_loaded、svm_loaded、lstm_model、tokenizer全部在程序启动时加载一次,放在全局或App的__init__里,不要每次点按钮都重新加载。我见过有人在predict里joblib.load,结果每点一次卡三秒,血泪教训。另外,如果老师要求现场演示,提前把测试用例准备好,比如「质量很好」「三天就坏」「物流快但包装烂」各一条,覆盖正面、负面和混合情感,比现场瞎输靠谱得多。希望帮到你。
本文还有配套的精品资源,点击获取