
简介以自然语言处理情感分析为案例的完整建模资料包面向初学者与算法工程师。资料从无标签语料出发演示分词、去除停用词、人工情感标注等预处理流程并分别使用支持向量机与长短期记忆网络两种模型完成情感分类建模。支持向量机部分包含特征构造、核函数选择与参数调优长短期记忆网络部分涵盖词嵌入、双向网络、随机失活等深度学习要点内容体系完整。压缩包共二十四个文件其中文本语料与情感词典十四个、建模脚本四个、说明文档两个、示意图两张整体约三十兆字节目录结构清晰便于对照学习已有一百二十六人学习下载。通过阅读文档和运行脚本可完整掌握情感分析从语料标注到模型评估的实战流程适合作为自然语言处理入门或课堂实验的参考资料。1. 以 nlp 中情感分析为例用 SVM 和 LSTM 去建模这个选题在考什么这个文件名在课程设计和机器学习期末作业里出现频率极高标准形态是一份中文评论语料二分类正负情感先用传统机器学习模型 SVM 做基线再用深度学习模型 LSTM 建一个对比模型最后交报告对比。这个选题好落地是因为数据好找、流程标准、指标直观能把机器学习应用流程完整跑一遍。可我见过太多同学把 LSTM 当黑匣子调用把 SVM 当成默认分类器填个准确率就交差答辩被问一句“为什么 LSTM 不如 SVM”就说不清了。这篇笔记会把选题拆成可直接照做的路径数据处理、模型搭建、公平对比、常见翻车点一步一步落到能交差、能讲清楚的结果上。适合正在做课程设计或期末项目的在校生、想用情感分析练第一个 nlp 项目的初学者以及准备做传统模型和深度模型 benchmark 的复现型选手。2. SVM 基线文本先向量化C、kernel、gamma 三个参数逐个调SVM 在情感分析里扮演的是基线模型角色结构简单、训练快、参数可解释性强。情感分析本质上是文本二分类而 SVM 不能直接吃文本必须先把成段文字变成数值向量。这一步就是机器学习中数据处理的核心环节非结构化文本转结构化特征。选什么特征、怎么控制特征规模直接决定后续所有调参的上限。2.1 为什么情感分析首选 TF-IDF 加 SVM而不是词袋TF-IDF 与 SVM 的组合是文本分类里的经典搭配。词袋模型只统计词频停用词和常见词权重虚高TF-IDF 会把“在本文档出现得多、但在整个语料中不常见”的词权重拉起来比如“好吃”“难吃”“值”这类意见词。SVM 在高维稀疏向量上表现稳定而 TF-IDF 产生的特征矩阵正是典型的稀疏矩阵两者天然匹配。中文文本这里有个选型用词级别还是字级别。词级别时“不好吃”里的“不”和“好吃”被切到不同位置SVM 在线性核下感受不到词序容易把“好吃”和“不好吃”当成同一边的特征。行业里常见的做法有两种一是把 ngram_range 设成 (1,2)让“不好”“好吃”这类相邻组合也变成特征二是把词序问题直接甩给 LSTM 处理这也正是对比实验的意义之一。我在短文本上习惯先用 ngram_range(1,2)它带来的维度增加可控对 SVM 的增益通常很明显。参数初始值方面max_features 设 30000 对入门语料已经够用再多只是增加训练时间。min_df 设 2把整个语料里只出现过一次的词直接扔掉对中文短文本尤其有效。sublinear_tf 会让词频取对数压住长文本里的高频词过大的权重这个参数在文档长短不一时很值得开。2.2 用 Pipeline 封装向量化与分类器防止测试集泄漏我最早做这个项目时犯过一个低级错误先对全量语料调用了 TfidfVectorizer 的 fit_transform再切训练集和测试集。测试集的词频统计混进了训练过程SVM 准确率虚高到 98%换了测试集立刻掉到 85%。后来我把所有预处理和模型收进 Pipeline才从根本上避免了这类问题。from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.svm import SVC from sklearn.pipeline import Pipeline # 停用词表按自己的语料维护常见做法是一个 txt 文件逐行读入 stop_words load_my_stopwords() pipeline Pipeline([ (tfidf, TfidfVectorizer( max_features30000, min_df2, ngram_range(1, 2), sublinear_tfTrue, stop_wordsstop_words )), (clf, SVC( kernelrbf, C1.0, gammascale, class_weightbalanced, probabilityFalse )) ])这段代码把向量化和分类器串成一条流水线。Pipeline 在调用 fit 时只让 TfidfVectorizer 在传入的训练集上学习词表与 IDF 值之后对测试集做 predict向量化器只调用 transform不会再改变词表。这样就保证了测试集的信息不会提前进入训练环节。各参数初始值建议如下参数作用建议max_features限制特征总量去掉低频长尾30000min_df词至少出现次数过滤噪声2ngram_range是否加入双词组合特征(1, 2)sublinear_tf对词频取对数平衡长文本Trueclass_weight样本不均衡时自动加权balancedgammarbf 核的宽度控制scaleclass_weight 的 balanced 选项在正负样本比例偏离 1:1 时尤其有用它会让少数类的误判代价更高避免 SVM 把全部样本都预测成多数类。提示绝对不要在 train_test_split 之前对全量语料做向量化这是整个项目里最容易翻车、又最隐蔽的一步。2.3 网格搜索 C、gamma、kernel核函数与参数的影响SVM 真正需要手工干预的核函数只有三个kernel、C、gamma。kernel 决定超平面能弯成什么形状C 控制对误分类样本的惩罚力度gamma 只对 rbf 核生效决定每个训练样本的影响半径。这三个参数不是玄学它们会直接改变模型的偏差和方差。from sklearn.model_selection import GridSearchCV param_grid { clf__kernel: [linear, rbf], clf__C: [0.1, 1, 10], clf__gamma: [scale, auto] } grid GridSearchCV( pipeline, param_grid, cv5, scoringf1_macro, n_jobs-1, verbose1 ) grid.fit(x_train, y_train) print(grid.best_params_) print(grid.best_score_)这段代码一次性跑了 2×3×2 共 12 组参数组合每组做 5 折交叉验证。scoring 用 f1_macro 而不是 accuracy是因为情感语料经常正负样本不均匀准确率会被多数类带偏。C 的搜索范围我一般从 [0.01, 0.1, 1, 10] 出发。C 太小模型欠拟合决策边界过于简单C 太大模型会把噪声点也记住过拟合。gamma 的 scale 选项会自动按特征数量计算初始值在 TF-IDF 这种稀疏矩阵上通常比 auto 更稳。如果数据量到十万条级别5 折网格搜索会非常慢常见替代做法是先用 linear 核跑通再单独对 C 做一轮搜索最后才尝试 rbf 加 gamma。这样可以把 12 组组合压缩到 5 组左右省下的时间足够做后面的 LSTM 实验。3. LSTM 流程中文分词、Embedding 与训练策略一起设计LSTM 的引入是为了解决 SVM 看不到词序的问题。它不再需要手工特征而是把每个词映射成稠密向量再按句子顺序逐个读入让“虽然”“但是”“不是一般的差”这类结构信息进入模型。代价是需要更多数据和更长的训练时间。在几千条短文本上LSTM 不一定比 SVM 强但作为深度学习基线这个对比本身就有价值。3.1 中文文本预处理与序列化Tokenizer 的参数别只设 num_words中文 nlp 入门最容易卡住的地方不是模型结构而是文本怎么变成序列。很多人直接把英文项目的 Tokenizer 抄过来只设 num_words结果测试集里生词全被抹成空白序列长度缩水一半。这里有一个必设参数oov_token。from tensorflow.keras.preprocessing.text import Tokenizer from tensorflow.keras.preprocessing.sequence import pad_sequences MAX_WORDS 20000 MAX_LEN 100 tokenizer Tokenizer( num_wordsMAX_WORDS, oov_tokenOOV, # 词表外的词统一映射到这个 token避免被静默丢弃 filters # 不过滤标点和符号保留“”这类情感信号 ) tokenizer.fit_on_texts(x_train) x_train_seq tokenizer.texts_to_sequences(x_train) x_test_seq tokenizer.texts_to_sequences(x_test) x_train_pad pad_sequences( x_train_seq, maxlenMAX_LEN, paddingpost, truncatingpost ) x_test_pad pad_sequences( x_test_seq, maxlenMAX_LEN, paddingpost, truncatingpost )这里的关键是 tokenizer 只允许在 x_train 上调用 fit_on_texts在全部语料上拟合词表同样属于数据泄漏。oov_token 必须设置否则测试集里新出现的表情、网络词会被直接忽略。filters 默认会清掉标点和符号但情感分析里表情和连续感叹号确实带情感倾向所以我把它设成空字符串把是否清洗交给模型自己学。空文本问题需要主动处理如果原始数据里有一整行只有标点或空格texts_to_sequences 之后会得到空列表pad_sequences 后整条向量全是 0。这种样本会让 LSTM 学到“空文本是负面”之类的虚假规律。我的习惯是在预处理开头把 len(text.strip()) 小于 1 的行直接丢掉并记录数量写进报告。3.2 用 Keras 搭建 Embedding 加 LSTM网络结构参数逐个解释模型结构并不复杂Embedding 负责把词索引变成稠密向量LSTM 负责按顺序读入并输出一个固定维度的语义表示最后接一个 sigmoid 做二分类输出。from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Embedding, LSTM, Dense, Dropout, Bidirectional model Sequential() model.add(Embedding( input_dimMAX_WORDS, output_dim128, input_lengthMAX_LEN )) model.add(Bidirectional(LSTM( units64, dropout0.3, return_sequencesFalse ))) model.add(Dense(1, activationsigmoid)) model.compile( optimizeradam, lossbinary_crossentropy, metrics[accuracy] ) model.summary()Embedding 层等价于建一个 20000 行乘 128 列的查找表每行对应一个词的向量训练中不断更新。output_dim 设 128 是短文本任务里比较稳妥的起点太小会欠拟合太大在小数据上容易过拟合。LSTM 的 units 设 64 已经足够因为情感分类句子一般在 20 到 50 个词信息密度远低于长文档。这里我特意只设置了 dropout 而没有设置 recurrent_dropout原因是 LSTM 的循环 dropout 在 GPU 上往往极慢部分 TensorFlow 版本还会和优化器产生兼容问题而大部分入门数据量根本用不到这么强的正则。如果训练集只有几千条网络已经很容易过拟合靠 dropout 和早停控制就够了。二分类任务用 sigmoid 加 binary_crossentropy这是标准配置。3.3 训练策略早停、学习率衰减、随机种子一个都不能少LSTM 学得快也过拟合得快死等固定 epoch 是不行的。我在这个模型上见过太多 val_loss 在第三个 epoch 之后直线上升的报告原因都是没加早停。下面这套回调是这个项目的最低配置。from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau callbacks [ EarlyStopping(monitorval_loss, patience3, restore_best_weightsTrue), ReduceLROnPlateau(monitorval_loss, factor0.5, patience2, min_lr1e-5) ] history model.fit( x_train_pad, y_train, validation_split0.2, batch_size32, epochs30, callbackscallbacks, verbose1 )EarlyStopping 会在验证集 loss 连续三个 epoch 不再下降时停止并恢复表现最好的权重这就是后悔药。ReduceLROnPlateau 在验证集 loss 停滞两个 epoch 后把学习率减半常常能把 loss 从震荡中救回来避免提前停在一个不理想的局部点。batch_size 设 32 在几千条语料上是稳妥值batch 太大会让梯度估计过于平滑小数据下收敛不稳。随机种子问题放在训练配置里一起说因为它直接影响可复现性。对比实验里如果每次跑出来的结果都不一样报告的数字就没有说服力。每份脚本开头都要固定一套种子。import os import random import numpy as np import tensorflow as tf SEED 42 os.environ[PYTHONHASHSEED] str(SEED) random.seed(SEED) np.random.seed(SEED) tf.random.set_seed(SEED)需要提醒的是如果刚做完 LSTM 时间序列预测很容易照搬那套滑动窗口思路来处理文本这是常见误用。时间序列预测是连续值回归输入是数值窗口情感分析是离散文本分类输入是词索引序列。两者除了都叫 LSTM数据形态、输出层和 loss 都不一样。4. 公平对比统一数据集与指标SVM 和 LSTM 才比得起来SVM 和 LSTM 的对比实验最怕一件事数据集切分不一致。SVM 在 80% 训练数据上调参LSTM 在另一个 80% 上训练两者验证集都不一样最后得出的结论没有任何说服力。公平对比的核心是让两个模型吃同一份数据、看同一份测试集。4.1 训练集/验证集/测试集怎么切两个模型才公平正确的顺序是先把原始数据一次性切出测试集剩下的训练集再根据模型需要切验证集。测试集只能看一次这是底线。from sklearn.model_selection import train_test_split # 第一次切分分出测试集两个模型共用 x_train, x_test, y_train, y_test train_test_split( texts, labels, test_size0.2, random_state42, stratifylabels # 按标签比例分层抽样正负样本比例在切分后保持不变 ) # 第二次切分为 LSTM 固定验证集供早停使用 x_train, x_val, y_train, y_val train_test_split( x_train, y_train, test_size0.1, random_state42, stratifyy_train )stratify 参数是很多入门代码里被漏掉的关键。如果不分层正负样本比例在切分后可能漂移SVM 和 LSTM 各自看到的类别分布就会不同。对 LSTM 侧我没用 validation_split而是单独切出验证集固定下来这样早停监控的 val_loss 始终是同一批样本不会因为训练过程内部重新 shuffle 而产生差异。SVM 的 GridSearchCV 内部用 5 折交叉验证这和在固定验证集上早停不是同一套机制不必强行一致。只要两个模型共用同一个测试集对比结论就有意义。提示测试集在整个调参过程中只能用于最终评估。如果反复在测试集上验证并回头调参测试集就变成了训练集的一部分最终数字不可信。4.2 除了准确率F1、训练耗时与模型体积也要对比只用准确率对比 SVM 和 LSTM 是最常见的不严谨做法。情感语料只要稍微不均衡准确率就会被多数类撑高。至少要看四类指标准确率、宏平均 F1、训练耗时、模型体积。指标作用对比要点accuracy整体判断正确比例样本不均衡时参考价值有限macro-F1正负两类的 F1 取平均少数类分类能力的关键指标训练耗时从 fit 到完成调参的时间体现两种路线成本差异模型体积保存后的文件大小影响部署和迭代成本评估代码建议统一写成一份脚本避免两个模型各写各的导致阈值和接口不一致。from sklearn.metrics import classification_report # SVM直接输出类别标签 y_pred_svm grid.best_estimator_.predict(x_test) print(classification_report(y_test, y_pred_svm, target_names[negative, positive])) # LSTM输出的是 sigmoid 概率需要手动阈值化为 0/1 标签 y_prob_lstm model.predict(x_test_pad) y_pred_lstm (y_prob_lstm 0.5).astype(int).reshape(-1) print(classification_report(y_test, y_pred_lstm, target_names[negative, positive]))这段代码里最容易出错的是 LSTM 的输出形态。SVM 的 predict 直接给出类别而 Keras 二分类模型给的是概率数组形状是 (样本数, 1)需要先 reshape 再用 0.5 阈值切分。这个 0.5 只是默认阈值如果标签分布不均匀可以把阈值往下调观察后再定。分类报告里的 macro avg 行是答辩时最容易讲清楚的一个数。4.3 混淆矩阵和预测样例答辩现场最好用的两张图指标数字只给人一个抽象感受把结果画出来才能快速定位问题。混淆矩阵能直接看出模型偏向把哪一类分错预测样例则能让老师看到你确实做过错误分析。import matplotlib.pyplot as plt from sklearn.metrics import ConfusionMatrixDisplay fig, axes plt.subplots(1, 2, figsize(10, 4)) ConfusionMatrixDisplay.from_predictions( y_test, y_pred_svm, display_labels[negative, positive], axaxes[0], colorbarFalse, cmapBlues ) axes[0].set_title(SVM) ConfusionMatrixDisplay.from_predictions( y_test, y_pred_lstm, display_labels[negative, positive], axaxes[1], colorbarFalse, cmapBlues ) axes[1].set_title(LSTM) plt.tight_layout() plt.savefig(confusion_matrix.png, dpi150)如果混淆矩阵显示 LSTM 把正面评论误判成负面的数量明显多于 SVM下一步就去翻这些样本把几条预测错的原文打出来看看是“不像负面”的双重否定句还是包含明显反讽。模型做错的地方才是报告中真正值钱的内容。一个简单的技巧是保存一份 DataFrame包含原文、真实标签、两个模型的预测标签和预测概率按字段排序后人工扫一遍。5. 避坑指南从数据到训练常见的 5 个翻车现场我把自己做这个题目时遇到过的、以及帮别人排查过的高频问题整理成了五条踩坑记录每一条都按现象、原因、解决三个层面讲清楚。这些问题几乎不依赖具体数据集换任何中文情感语料都可能遇到。5.1 标签分布不均衡准确率虚高到 90% 却不可信现象训练完成打印准确率超过 90%但第二个类别的 F1 只有 0.3人工抽查预测结果发现模型几乎把所有样本都预测成了多数类。原因情感语料里差评往往比好评少如果正负比例是 4:1模型只要全部预测为好评准确率就是 80%再学一点皮毛就能到 90%。准确率在这里失去了区分能力。解决训练前先统计标签比例用 value_counts 看分布。SVM 侧设置 class_weightbalancedLSTM 侧在 model.fit 时传入 class_weight 字典。评估指标改用 f1_macro把少数类性能拉回视野。如果正负比例差距极大还要考虑阈值调整不要默认使用 0.5。5.2 向量化在全部语料上执行测试集信息提前泄漏现象SVM 在测试集上准确率 98%LSTM 只有 90%两者差距大到不真实怀疑是模型类型差距实际是 SVM 管线的预处理环节出了泄漏。原因在 train_test_split 之前先对 texts 整体调用了 TfidfVectorizer 的 fit_transform或者 Tokenizer 在全部语料上做了 fit_on_texts。测试集词频和 IDF 信息提前参与了训练让 SVM 白捡了便宜。解决严格先切分数据再在训练集子集上做一切 fit 操作。SVM 侧用 Pipeline 封装LSTM 侧只在 x_train 上调用 tokenizer.fit_on_texts。怀疑存在泄漏时可以做一个快速实验把 test_size 从 0.2 改成 0.3如果准确率几乎不下降大概率是泄漏了。5.3 LSTM 的 loss 变 NaN或者准确率一直不动现象训练到第二个 epochloss 打印出 nan或者 val_accuracy 一直卡在 50% 上下震荡怎么调 epoch 都没用。原因loss 为 NaN 通常是学习率过大或输入数据有问题比如空文本经过 pad 后全是 0或者词索引出现越界。准确率不动则更常见于数据标签顺序问题训练集和标签错位模型没有可学信号。解决第一过滤掉所有空文本和纯标点文本第二检查 x_train_pad 的最大索引值是否小于 MAX_WORDS第三把 Adam 学习率降到 1e-4或者给优化器加 clipnorm第四确认 y_train 和 x_train 经过相同的 shuffle 对齐。如果数据量太小LSTM 学不动也正常这时不要硬调模型结构先回到 SVM 基线确认数据本身是否有区分度。5.4 SVM 在 TF-IDF 大矩阵上训得极慢现象几万条样本、max_features300005 折网格搜索跑了三四个小时还没出结果n_jobs-1 也没有明显加速。原因rbf 核的 SVM 需要对样本两两计算核函数复杂度接近 O(n^2)。n_jobs 在网格搜索里只是并行跑不同折SVM 的单折训练本身无法利用多线程数据量上来后耗时是指数级增长。解决先确认样本量级。几千条数据跑 rbf 没问题超过两万条就要考虑先用 linear 核。线性 SVM 在大样本上训练快得多而且文本 TF-IDF 特征维度很高线性边界通常已经够用。如果一定要 rbf可以在训练集上随机抽样 5000 条做网格搜索确定参数后再用全量数据训练。还有一个替代方案是 LinearSVC它基于不同的优化算法大样本下比 SVC 的 linear 核快很多适合作为快速验证。5.5 对比实验没固定随机种子结果无法复现现象同一条代码隔天再跑SVM 准确率波动 1%LSTM 准确率波动 5% 甚至更多报告里的数字和你说口头汇报的数字对不上。原因sklearn 的 train_test_split、GridSearchCV 内部随机性Keras 的权重初始化、训练 shuffle、dropout各自使用独立的随机源。只固定 np.random.seed 是管不到 TensorFlow 的。解决每个脚本开头固定全套种子包括 PYTHONHASHSEED、random、numpy、tensorflow。train_test_split 和一切可传 random_state 的地方全部显式传入。如果用了 GPU还要在训练前设置 tf.config 的确定性选项不过即使这样cuDNN 某些算法仍可能带来微小波动这点在写报告时如实说明即可。固定种子的意义不是让两次运行百分百一致而是把波动控制在不影响结论的范围内。6. 进阶从二分类扩展到三分类附带模型保存与 AUC 验证做完正负二分类把这个项目扩展成三分类正面、负面、中性是顺理成章的下一步很多答辩老师会当场追问“如果加一个中性类你要改哪里”。改动点其实就三处SVM 侧不需要改结构多分类是 SVC 的默认能力只要把标签从 0/1 变成 0/1/2重跑网格搜索即可但评估指标必须看 macro-F1因为中性类样本通常最少。LSTM 侧把最后一层 Dense 的神经元数改成 3激活函数换成 softmaxloss 换成 categorical_crossentropy如果标签保持整数形态可以直接用 sparse_categorical_crossentropy省去 one-hot 转换。模型保存也是个容易忽略的收尾工作。SVM 的 Pipeline 里带着 TF-IDF 向量化器LSTM 的 Tokenizer 却不在模型文件里这是最常见的部署坑。import joblib from tensorflow.keras.models import load_model # SVM 保存整个 pipelineTF-IDF 和分类器一起存 joblib.dump(grid.best_estimator_, svm_model.pkl) # LSTM 只保存网络权重tokenizer 需要单独存 model.save(lstm_model.h5) # tokenizer 用 pickle 单独保存预测新文本时先加载它再做 sequences预测新数据时SVM 只要一句 load 然后直接 predictLSTM 则要先加载 tokenizer对新文本调用 texts_to_sequences 和 pad_sequences再送入模型。很多人在模型保存这一步翻车就是因为忘了 tokenizer 不在 h5 文件里。最后补一个概率型模型的专属验证ROC 曲线和 AUC。ROC 不依赖分类阈值适合用来回答“LSTM 的排序能力到底有没有比 SVM 好”这个问题。from sklearn.metrics import roc_curve, auc fpr, tpr, _ roc_curve(y_test, y_prob_lstm) roc_auc auc(fpr, tpr) print(LSTM AUC:, roc_auc)如果两个模型的 accuracy 相近但 LSTM 的 AUC 明显更高说明它的概率输出质量更好阈值还有进一步优化的空间。SVM 侧如果也想画 ROC需要把 SVC 的 probability 参数打开但打开后训练会明显变慢这一点在对报告中要如实体现。我个人做这类对比实验时习惯在最后一步把随机种子、TensorFlow 版本、sklearn 版本和截图时间一起记录在代码注释里免得隔一周回来补实验时找不到原始环境。这个习惯救过我很多次希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。