ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

中文情感分析实战:从词典标注到SVM与LSTM建模全流程

中文情感分析实战:从词典标注到SVM与LSTM建模全流程 简介以NLP情感分析为背景的建模教程主要面向希望系统入门传统机器学习与深度学习的开发者。内容从无标签语料库出发逐步演示情感标注、分词、去停用词、TF-IDF特征构造再分别使用SVM和LSTM完成情感分类任务并比较两者在特征工程成本、训练速度与上下文建模能力上的不同。SVM部分涉及核函数、正则参数C与γ的调优LSTM部分则涵盖词嵌入、门机制、双向LSTM及dropout等细节。包内共24个文件包含4个Python脚本覆盖数据标注、情感字典构建、SVM建模与LSTM建模、14个txt词表及语料、2个Markdown文档说明、2张流程图以及LICENSE和.gitignore等辅助文件整体约30.7MB。目前已有126人学习。通过这份材料读者可完整走通从原始文本到模型预测的流程也可对比两种建模思路在实际任务中的表现适合作为课程设计或NLP入门实践的参考。1. 情感分析从无标签语料起步SVM与LSTM这条链路值不值得跑一条外卖评论“餐盒漏了但味道还行”词典规则大概率会给它判成负面可用户实际想表达的是好评。这种真实语料里的转折和否定恰恰是情感分析最难处理、也最能体现建模价值的地方。这份资源从完全没有标签的corpus.txt开始先用词典脚本给评论打上正负标签再分别用传统机器学习SVM与深度学习LSTM建模最后用predict_test.txt做预测验证是一条可以在本地跑通的完整链路适合做NLP课设、机器学习课程设计或者想认真入门中文情感分析的人。这里有个反直觉的结论当语料规模只有几千条、标注又是词典自动生成的时候LSTM并不一定比SVM强。SVM在特征处理好之后往往更稳训练也快得多。与其迷信模型不如先把从“原始语料→打标签→建特征→训练→评估”这条路走通再回过头决定生产环境到底该用哪一套。2. 无标签语料打标签jieba分词与情感词典把点评文本变成有监督数据先搞清楚一个前提无论SVM还是LSTM都是监督学习模型每条训练语料必须有一个情感标签。资源里的corpus.txt是原始评论一行一条没有任何标注。所以整条链路的第一步不是建模而是给这些评论打标签。打标签的质量直接决定后面所有模型的上限这一步偷懒后面全是白做。2.1 为什么不能拿原始文本直接训练中文文本和英文不一样词与词之间没有天然空格。如果不分词整条句子会被拆成稀疏字符序列“好吃”和“难吃”这种带语义关系的词会被拆散SVM的特征空间会被字频噪声淹没。常见的做法是先做分词、去停用词、词性标注把原句转成有意义的词序列。资源里的04_sentiment_classify.py做的就是这件事底层依赖jieba并在分词时加载自定义词典。import jieba import jieba.posseg as pseg # 加载自定义词典保证“肉夹馍”“凉皮”这类词不被切碎 jieba.load_userdict(dict/user_dict.txt) stop_words set() with open(dict/stop_words.txt, encodingutf-8) as f: for line in f: stop_words.add(line.strip()) def cut_and_tag(text): words [] for word, flag in pseg.cut(text): # 过滤空白字符、标点和停用词 if word.strip() and word not in stop_words and flag not in [x, w]: words.append((word, flag)) return words代码逻辑不复杂pseg.cut会返回每个词的词性比如“好吃”是形容词、“失望”是动词后面打标签时可以用词性做过滤条件。stop_words.txt里是“的、了、就、都”这类高频无意义词滤掉它们能让特征维度明显下降。user_dict.txt的每一行是一个自定义词条可以带词频和词性格式类似“肉夹馍 10 n”。点评场景里的菜品名、店铺名如果不加进词典很容易被jieba按通用词库切错导致“味道好”变成“味”“道”“好”。2.2 词典打分脚本否定词、程度副词与边界阈值分词之后就要给每条评论算一个情感倾向分数。资源里的03_sentiment_dict_label_for_dianping_comments.py使用了一套词典集合positive_dict.txt和negative_dict.txt提供基础情感词denial_dict.txt负责否定词adverb_dict.txt负责程度副词conjunction_dict.txt处理转折和并列关系。这套词典打标的思路是每个情感词给固定权重否定词翻转极性程度副词放大或缩小权重最终按分数阈值映射成正面、负面或中性。def label_sentence(sent, weight1.0): words [w for w, _ in pseg.cut(sent) if w.strip() and w not in stop_words] score 0.0 negated False for i, w in enumerate(words): if w in denial_dict: negated True continue base 0.0 if w in positive_dict: base weight elif w in negative_dict: base -weight if w in adverb_dict: base * adverb_dict[w] if negated: base -base negated False score base return score这段代码的核心在否定词处理。遇到“不”“没”“别”时先置一个negated标记下一个情感词出现时翻转符号。比如“不值”这个词“值”在正面词典里被“不”翻转让整体变成负面。adverb_dict里的系数建议以倍率方式配置比如“非常”给1.8“有点”给0.4而不是给固定加减分否则长文本里堆叠程度副词会产生离谱的极端分数。score出来后常见做法是设定一对阈值大于0.3判正面小于-0.3判负面中间归为中性。阈值越宽中性样本越多阈值越窄模型越容易两极化。初次尝试建议先跑一遍全量语料看分数分布再定阈值不要拍脑袋写死。提示dict_def.txt和user_dict_def.txt是词典格式说明文件往词典里加词前先看一眼避免格式错误导致整个词典加载失败。2.3 打标后的抽样校验先看分布再看准确率自动打标最大的风险是“看起来标了实际上标错了”。所以打标完成后我一般会做两件事统计类别分布抽样打印正负各20条人工核对。import random from collections import Counter with open(data/corpus.txt, encodingutf-8) as f: corpus [line.strip() for line in f if line.strip()] labels [1 if label_sentence(s) 0.3 else (-1 if label_sentence(s) -0.3 else 0) for s in corpus] print(Counter(labels)) positive_samples [s for s, l in zip(corpus, labels) if l 1] negative_samples [s for s, l in zip(corpus, labels) if l -1] for s in random.sample(positive_samples, min(20, len(positive_samples))): print([正], s) for s in random.sample(negative_samples, min(20, len(negative_samples))): print([负], s)Counter输出的分布能直接暴露问题如果三个类别比例是9:1:0说明阈值设得太激进或者词典漏词严重需要回查。人工抽样这步不能省词典打标是规则方案规则永远有盲区。像“性价比很高但位置太难找”这种句子分数会同时覆盖正面和负面最后落在中性或错误方向这些情况在抽样时一眼就能看到。抽样确认过的label再存成带标签文件作为后续SVM和LSTM的训练集这一步做完才真正进入建模环节。3. 特征工程与SVM建模TF-IDF向量化与核函数调参的取舍SVM不会直接读中文文本它只能处理数值向量。所以SVM路线里最关键的工作就是特征工程——把打标好的文本变成一组能区分正负情感的数值特征。这个阶段做的每一个选择包括向量化方式、n-gram范围、核函数和正则参数都会直接反映在分类报告上。3.1 TF-IDF与词袋的选型词袋模型是最简单的做法每条评论统计每个词出现次数生成一个稀疏向量。但纯词频有两个问题一是“的、了、这”这类词频很高却没什么情感信息二是长评论天然词频高容易被误判为高分。TF-IDF解决了这两点TF部分保留词频IDF部分给常见词降权给“难吃”“差评”这类只在少数文本出现的词提权。实际用的时候还可以打开sublinear_tf参数把词频做log平滑避免单个词反复出现导致特征值爆炸。from sklearn.feature_extraction.text import TfidfVectorizer tfidf TfidfVectorizer( max_features15000, ngram_range(1, 2), sublinear_tfTrue, min_df2 ) X tfidf.fit_transform(train_texts)max_features设成15000是控制特征维度点评语料通常几万到几十万条维度太高会让SVM训练变慢而且很多生僻词对分类没有贡献。ngram_range(1,2)表示同时保留单个词和相邻两词的组合像“味道好”这种bigram会比单独的“味道”和“好”携带更完整的情感信号。min_df2表示至少在两句话里出现过的词才保留过滤掉只出现一次的噪声词。这个参数在数据量较小时要谨慎语料只有几百条的话min_df2会直接砍掉一半特征。3.2 SVM端到端管线SVM的情感分析任务适合用Pipeline把向量化和分类器串起来避免在交叉验证时把测试集信息泄露进TF-IDF词典。资源里的05_sentiment_analysis_ml.py就是按这个思路写的我把核心部分拆开看。from sklearn.svm import SVC from sklearn.pipeline import Pipeline from sklearn.model_selection import GridSearchCV pipe Pipeline([ (tfidf, TfidfVectorizer()), (svm, SVC()) ]) param_grid { tfidf__ngram_range: [(1, 1), (1, 2)], tfidf__max_features: [5000, 15000], svm__kernel: [linear, rbf], svm__C: [0.1, 1.0, 10.0] } grid GridSearchCV( pipe, param_grid, cv5, scoringf1_macro, n_jobs-1 ) grid.fit(X_train, y_train) print(grid.best_params_)C是SVM的正则化参数C越小对误分类的惩罚越轻决策边界越平滑适合抵抗标注噪声C越大越追求把所有训练样本分对但容易把词典打标的错误也学进去。情感分析这种标注本身带有噪声的任务我一般不会把C调到很大。核函数方面线性核在TF-IDF高维稀疏特征下通常表现够用而且训练快、可解释性强RBF核能拟合更复杂的非线性边界但需要额外调gamma且在高维稀疏特征上容易过拟合。训练失败时先看grid搜索报错如果SVC耗时长优先检查max_features是不是设太高、C是不是偏大。还有一个常见失误直接用SVC默认参数跑几千条样本等到测试集出结果已经过去十几分钟才发现忘了限制特征维度。3.3 分类报告该看什么SVM训完后别只盯着准确率。情感分析类别不均衡时准确率会骗人如果90%的样本是正面模型全猜正面也有90%准确率但负面样本一条都找不出来。正确做法是打印classification_report看每个类别的precision、recall和F1。from sklearn.metrics import classification_report y_pred grid.predict(X_test) print(classification_report(y_test, y_pred, target_names[负面, 中性, 正面]))对负面类来说precision表示“预测为负面的评论里真实有多少是负面”recall表示“真实负面的评论里被找回来多少”。情感分析场景里recall太低意味着大量差评被漏掉precision太低意味着好评价被误杀。macro avg是三个类别指标的直接平均比准确率靠谱得多。如果负面类F1明显低于正面类大概率是词典打标时负面语料太少或者阈值偏向正面这时候回第2章补词典而不是继续调SVM参数。4. 词嵌入与LSTM建模让模型自动学习文本序列特征SVM路线的瓶颈在于特征工程靠人做。LSTM这条线不一样它把分词后的文本转成词嵌入再通过门控机制逐词阅读整句话让模型自己学习哪些词组合代表正向、哪些代表负向。这条路的好处是省去手工特征坏处是可解释性差、训练资源要求高。在数据量不足时LSTM不会比SVM自动变强这一点后面避坑章节会展开。4.1 为什么先embedding再进LSTMLSTM输入不能是字符串也不能是词频向量。常见做法是先给每个词分配一个整数ID再把ID序列映射成稠密向量这一步由Embedding层完成。Embedding层本质上是一个可训练的查找表训练过程中会自动调整词向量方向让“好”“赞”“棒”这类语义相近的词在向量空间里靠得更近。相比one-hot编码向量维度从几万降到128或256同时保留了语义相似性。from tensorflow.keras.preprocessing.text import Tokenizer from tensorflow.keras.preprocessing.sequence import pad_sequences tokenizer Tokenizer(num_words30000) tokenizer.fit_on_texts(train_texts) X_train tokenizer.texts_to_sequences(train_texts) X_train pad_sequences(X_train, maxlen100)maxlen设成100意思是每条评论最多保留100个词超过直接截断不足补零。点评类文本通常很短80%以上的句子在50个词以内maxlen100已经够用。这里踩过一个坑maxlen设太小比如30长评论里的转折信息会被截掉“前面全是牢骚、最后一句才是重点”的句子就会漏判maxlen设太大比如500大部分序列是补零计算量浪费在无效位置上。所以先统计语料分词后长度分布取90分位数作为maxlen才合理。4.2 LSTM建模代码与训练参数资源里的05_sentiment_analysis_dl.py搭建了一个标准的文本分类网络Embedding层接LSTM层再接全连接层和softmax输出。三分类任务可以用3个输出节点对应负面、中性、正面。from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Embedding, LSTM, Dense, Dropout from tensorflow.keras.optimizers import Adam model Sequential([ Embedding(input_dim30000, output_dim128), LSTM(128, dropout0.3), Dense(64, activationrelu), Dropout(0.3), Dense(3, activationsoftmax) ]) model.compile( optimizerAdam(learning_rate2e-3), losssparse_categorical_crossentropy, metrics[accuracy] ) history model.fit( X_train, y_train, validation_split0.2, epochs20, batch_size64 )loss用sparse_categorical_crossentropy是因为y_train是整数标签比如0、1、2不需要再转成one-hot。Embedding的input_dim必须大于等于tokenizer的num_words否则会出现越界错误。LSTM层的dropout0.3只作用于该层输出如果想对输入也加噪声需要额外设recurrent_dropout但recurrent_dropout会明显拖慢训练速度小数据集上不建议同时开两个。学习率2e-3配合Adam在大多数文本分类任务上都算稳定如果loss震荡不下降优先把learning_rate降到1e-3而不是改网络结构。训练过程中要盯validation loss而不是accuracy。如果训练loss一直降、验证loss在第5个epoch后开始回升就是过拟合信号需要早停或增大dropout。这个过程就是机器学习里的经典数据处理和模型验证流程和SVM路线没有本质区别。4.3 双向LSTM、dropout与早停的取舍单向LSTM从前往后读句子双向LSTM同时从前往后和从后往前各读一遍再把两个方向的隐状态拼接。对否定句式来说双向有明显优势“性价比高”和“价格高”里“高”字情感方向完全相反只看前文不够必须结合后面的名词才能判断。资源里如果任务偏向这种上下文相关的情感判断建议直接换成双向LSTM。from tensorflow.keras.layers import Bidirectional from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint model Sequential([ Embedding(input_dim30000, output_dim128), Bidirectional(LSTM(64, dropout0.3)), Dense(64, activationrelu), Dropout(0.3), Dense(3, activationsoftmax) ]) early EarlyStopping(monitorval_loss, patience3, restore_best_weightsTrue) ckpt ModelCheckpoint(best_model.h5, monitorval_loss, save_best_onlyTrue) model.fit( X_train, y_train, validation_split0.2, epochs30, batch_size64, callbacks[early, ckpt] )双向LSTM的隐状态维度会翻倍LSTM(64)实际输出是128维所以全连接层输入不用再往大设。dropout和early stopping是防过拟合的双保险early stopping的patience设为3意思是验证loss连续3个epoch不降就停止训练并回滚到最优权重。训练完成后best_model.h5就是验证集上表现最好的模型文件不需要手动记录最佳epoch再重训一遍。LSTM训练时间和SVM不是一个量级几千条数据还好一旦到十万条级别最好先确认机器有GPU用CPU跑会很煎熬。5. 避坑情感分析建模里最容易翻车的五个细节模型调参是最后一个环节前面任何一步出错后面都白调。以下五个坑是这个资源场景下最容易踩的基本覆盖从打标签到训练完的完整链路。5.1 打标签不准模型再怎么调也白搭现象SVM和LSTM训练集准确率都很高但验证集F1一直上不去人工抽查发现不少“味道很好但上菜慢”被标成负面。 原因词典打标脚本按局部否定词翻转极性但没有处理“但”字转折。“前面好、后面坏”这类句式词典只看到“上菜慢”被否定词或负面词覆盖给了负分。 解决回03打标脚本把conjunction_dict里的转折词利用起来——检测到“但是、可是、不过”后只统计转折词之后子句的情感权重。补完词典后重新打标签、重新训练验证集F1会有肉眼可见的提升。这个教训说明词典规则的质量就是监督数据的质量数据不对模型没有后悔药吃。5.2 训练语料和预测文本不是一个领域现象用点评评论训练出来的模型拿去预测一段新闻文本预测结果几乎全是中性。 原因点评语料里的情感词集中在“好吃、难吃、服务差”新闻文本常用“质疑、批评、支持、反对”词典和TF-IDF特征空间重叠太少SVM只能给出默认判断。 解决如果predict_test.txt和训练语料是同一来源直接预测没问题如果来自不同领域先对目标语料做50条样本的人工标注和训练语料混在一起增量训练。文本分类模型没有跨领域泛化能力这是NLP里被反复验证的事实。5.3 否定词翻转只看相邻词不管整句语境现象“不是不好吃”被模型判成负面“味道不是一般的好”也被判成负面。 原因双层否定“不是不”“不是一般的”被脚本当成单层否定处理翻转一次就结束了实际应该翻转两次回到正面。 解决阳性词“普通、一般”和否定词组合要当成固定短语处理常见做法是在否定词后连续扫描情感词并累计否定次数偶数次否定恢复原极性。这类现象在词典里很难穷举所以打标完成后人工抽样校对这一步真的不能跳过。5.4 LSTM在小样本上过拟合现象LSTM训练集准确率95%验证集只有70%SVM反而更高。 原因LSTM参数量大几千条样本根本喂不饱epochs设20且没有early stopping时模型会把训练集噪声也背下来。 解决先看数据量低于5000条优先用双向LSTM64维embedding0.3dropout同时打开EarlyStopping如果拉了dropout还是过拟合把词嵌入维度降到64LSTM隐藏层降到32。LSTM不是自动更优的模型它只是把特征学习从人手里接过来数据量不够时照样翻车。5.5 类别不均衡导致SVM偏向多数类现象训练集中中性样本占65%模型预测时几乎把所有文本都推给中性负面召回率只有20%。 原因SVM在最大化间隔时天然偏向样本多的类别词典自动打标也容易产生大量中性样本。 解决SVM训练时设class_weightbalanced让少数类获得更高惩罚权重或者在2.3抽样校验阶段调整阈值把中性区间收紧从源头减少中性样本数量。分类报告打出来后如果多数类F1虚高、少数类F1偏低第一个怀疑的就是类别分布而不是模型不行。6. 模型验证与落地用predict_test.txt做一次鲁棒性检查模型训练完不是终点predict_test.txt才是真正的考卷。这份文件是没打过标签的待预测语料我会用它做三类检查分布合理性、阈值稳定性、误判样本回看。import pickle from tensorflow.keras.models import load_model # SVM管线 with open(svm_pipeline.pkl, rb) as f: svm_pipe pickle.load(f) # LSTM模型 lstm_model load_model(best_model.h5) with open(predict_test.txt, encodingutf-8) as f: test_sents [line.strip() for line in f if line.strip()] # SVM概率预测SVC需要probabilityTrue或决策距离预测 svm_pred svm_pipe.predict(test_sents) from collections import Counter print(SVM 分布:, Counter(svm_pred)) # LSTM概率输出 probs lstm_model.predict(sequences_test) print(LSTM 分布:, Counter(probs.argmax(axis1)))第一步看分布如果predict_test.txt和corpus.txt同分布正负比例应该不会离谱。偏斜太大说明语料来源不一致上文5.2的坑就踩中了。第二步看置信度LSTM输出的是概率向量比如[0.2, 0.1, 0.7]如果大量样本的最高概率集中在0.4左右说明模型对这批语料根本没把握这时候可以加一个置信度阈值把低置信度样本筛出来人工复核而不是全部交给模型。第三步是这条链路最有价值的收尾把SVM和LSTM预测不一致的样本打印出来逐条看。两份模型都判错的样本往往是词典漏词把关键词补进dict里的negative_dict.txt或positive_dict.txt再重跑打标和训练。从那以后我每次拿到一份新语料都强制走一遍“先抽50条打标校验→SVM出基线→LSTM对比→误判样本回看→补词典重训”的闭环而不是急着换模型结构。先保证数据和标注可靠再谈模型选型这条顺序在NLP情感分析里永远成立。希望帮到你。本文还有配套的精品资源点击获取
返回列表