LSTM中文短文本情感分析全流程:从数据预处理到模型推理
简介面向Python深度学习与自然语言处理课程设计这套基于LSTM的中文短文本情感分析项目源码适合需要完成高分期末大作业或入门文本分类实战的学生。项目已封装完整流程从数据预处理、标注语料到模型训练与预测均有对应模块下载后按说明即可运行无需修改。压缩包共14个文件以Python脚本情感分析主程序、测试脚本、txt数据与说明、CSV标注语料、Markdown文档及模型权重等类型为主整体仅1.96MB轻量易部署。资源中既包含正负样本数据集也包含已训练好的pt权重文件与加载脚本方便快速复现模型效果。项目还划分了训练集与测试集并提供示例正负样本文本便于理解模型输入格式。目前已有244人学习下载目录结构清晰注释与readme配套齐全适合作为课程设计、毕业设计参考或LSTM情感分析的入门练习素材。通过阅读源码可掌握中文分词、词向量构建、LSTM网络搭建、训练评估等关键环节为后续进阶打下基础。1. 中文短文本情感分析大作业在做什么很多人拿到“Python实现基于LSTM的中文短文本情感分析源码”这类zip时第一反应是解压跑通然后改改界面交作业。但仅能跑通的源码在期末答辩里很容易被追问击穿为什么用LSTM不用RNN数据从文本到矩阵经历了什么loss曲线怎么解释真正的得分点不是一个训练好的黑盒而是从原始中文短文本到预测结果的完整链路包括分词、词表、Embedding、LSTM模型、训练评估和推理。这个标题的核心不是“源码”这两个字而是“基于LSTM的中文短文本情感分析”背后的一套技术栈zip只是交付形式。适合的人群是正在做NLP课程项目、需要把LSTM落到中文短文本场景的工程师以及想在答辩时把参数和边界讲清楚的学生。2. 数据准备与中文文本预处理先把句子切成词LSTM不擅长处理原始字符串中文又没有天然空格预处理做不好后面全是噪声。这一章先把数据和分词链路定下来后续所有数字转化都依赖这步输出。2.1 短文本数据集怎么组织才对期末作业里最常见的错误是把所有句子粘在一个文本文件里再用脚本来回切。规范做法是制作CSV或TSV包含text和label两列label取0和1其中1表示正面、0表示负面。用pandas读入后先看样本和类别分布import pandas as pd df pd.read_csv(train.tsv, sep\t, names[text, label]) print(df.head()) print(df[label].value_counts())读入时指定names避免表头被当成数据。value_counts()用来观察类别分布如果正负样本比例大于2:1后面需要专门处理类别不平衡否则模型只会学到大类。文本列不能有NaN可以用df df.dropna(subset[text])过滤。这是短文本场景句子控制在几十字内比如商品评价、弹幕、短微博。太长的样本要么截断要么从数据集中移除。数据划分的位置也影响最终精度。我习惯先切分train/val/test再分词这样验证集和测试集不会干扰分词阶段的词典统计。切分代码在第3章这里先把文本清洗函数写好。2.2 基于 jieba 的分词与停用词过滤中文分词常用jieba它用前缀词典和HMM识别未登录词。对短文本场景默认精确模式够用不需要搜索引擎模式。先清洗再分词顺序不能反import jieba import re def clean_text(text): text re.sub(rhttps?://\S, , text) # 去掉URL text re.sub(r#\S#, , text) # 去掉话题标签 text re.sub(r\s, , text) # 去掉所有空白 return text.strip() def tokenize(text): cleaned clean_text(str(text)) if len(cleaned) 0: return [] return [w for w in jieba.cut(cleaned) if w.strip()]re.sub第一个参数是正则第二个参数是替换成的内容。去掉URL是因为URL对情感没有贡献去掉话题标签是防止“#今天真开心#”被切成一个词后面反而学不出“开心”。jieba.cut返回生成器这里直接转成列表。注意clean_text里的\s同时去掉了换行和空格这符合短文本的单行场景。停用词过滤不是必须的。LSTM本身能学到停用词的上下文但停用词会占词表容量并引入噪声。如果要去停用词需要准备stopwords.txt每行一个词stopwords set(line.strip() for line in open(stopwords.txt, encodingutf-8)) def filter_stopword(tokens): return [w for w in tokens if w not in stopwords]这里用set而不是list因为in查询的时间复杂度不同。很多实现会在这一步顺手过滤len(w) 1的单字词但会误删“好”“差”这类单字情感词。我的建议是只过滤停用词不碰单字词。如果你用的数据集单字噪声严重再把这个条件加回来并单独验证句子“这个不好”里“不”和“好”是否都保留。2.3 预处理的几个必调参数与断言参数/配置建议值说明jieba模式jieba.cut(text)默认精确模式不额外调参HMM默认True识别新词时有效但会引入少量噪声停用词表通用中文停用词表不要混入领域词比如“手机”不能随便去掉空文本处理过滤LSTM在空序列上无法计算清洗规则训练/预测共用函数必须打包复用否则推理阶段精度下降一个肉眼很难发现的问题是训练时先清洗再分词而预测新句子时只调用了分词忘记清洗。因此把clean_text、tokenize、filter_stopword打包成一个函数给训练和推理两边共用。分词后的输出应当是词列表。虽然Tokenizer也接受带空格的字符串但列表形式能避免空格歧义。预处理完成后最好加一句断言all_tokens df[text].map(lambda x: filter_stopword(tokenize(x))) assert all_tokens.map(len).min() 0, 存在空句子需要过滤这行断言在数据清洗不彻底时直接报错而不是等到训练Embedding时出现维度不匹配再回去查数据。3. 从词序列到张量词表、Embedding 与 Padding3.1 为什么要用数字表示词而不是直接输中文LSTM是数值模型输入必须是数值张量。最简单的方式是给每个词一个整数索引再通过Embedding层映射成稠密向量。常见做法是使用Keras的Tokenizer构建从词到索引的映射然后用pad_sequences统一长度。这里有一个理解难点中文短文本分词后是变长列表而批量训练要求所有序列等长所以必须把长度规则定下来。3.2 构建词表并做序列填充from tensorflow.keras.preprocessing.text import Tokenizer from tensorflow.keras.preprocessing.sequence import pad_sequences num_words 5000 tokenizer Tokenizer(num_wordsnum_words, oov_token[UNK]) tokenizer.fit_on_texts(X_train) # X_train是分词后的列表不是字符串 sequences tokenizer.texts_to_sequences(X_train) X_train_pad pad_sequences(sequences, maxlen50, paddingpost, truncatingpost)Tokenizer的fit_on_texts可以接收“词列表的列表”内部统计词频并保留频率最高的num_words-1个词oov_token[UNK]把词表外的词统一映射到未知词。索引从1开始0预留给padding。pad_sequences的paddingpost表示在序列尾部补0truncatingpost表示截断尾部。对LSTM来说序列尾部通常是信息最集中的位置截断尾部比截断头部更容易保留关键内容。这里有个容易踩的坑Tokenizer默认按空格分词如果传入的是字符串它会把整句话当成一个“词单元”或者按空格误切只有传入分词后的列表word_index才是真正的词粒度。所以你从2.2拿到的all_tokens不要重新拼成字符串。maxlen的取值最好看数据分布import numpy as np lengths [len(seq) for seq in X_train] maxlen int(np.percentile(lengths, 90)) print(f90%样本长度不超过 {maxlen})直接用这个分位数作为maxlen比拍脑袋填50或100更可信。期末报告里写“maxlen取训练集90%分位”本身就是加分项。3.3 Embedding 层与预训练向量选择Embedding层可以随机初始化随训练更新对期末项目随机初始化配合LSTM已足够。如果想提高效果下限用word2vec预训练向量初始化Embedding。关键是要让词嵌入和分词粒度一致import numpy as np vocab_size min(num_words, len(tokenizer.word_index) 1) embed_dim 100 embedding_matrix np.zeros((vocab_size, embed_dim)) for word, index in tokenizer.word_index.items(): if index vocab_size: continue if word in w2v_model.wv: embedding_matrix[index] w2v_model.wv[word]这里的w2v_model是gensim加载的word2vec模型。如果训练词向量时使用了另一套分词规则这里的匹配率会大幅下降。匹配率低于30%时预训练向量反而会拖累模型因为大量随机向量和少量预训练向量混在一起。此时干脆不用预训练直接让Embedding自己学。Embedding层的参数如下参数值作用input_dimvocab_size词表大小output_dim100或128向量维度input_lengthmaxlen每个序列长度mask_zeroTrue让LSTM跳过padding位置mask_zeroTrue只对Embedding层作为第一层时生效它把索引0对应的向量设置为不参与训练并让后续LSTM自动跳过这些时刻。没有这个设置padding位置会被当成真实单词输入白白消耗计算量并引入噪声。数据切分和Tokenizer的拟合顺序要特别注意。正确流程是先切分出X_train、X_val、X_test然后只在X_train上fit_on_texts。如果对全部文本先拟合再切分测试集词表信息已经参与了统计这就是数据泄漏会导致测试精度虚高。4. 搭建并训练 LSTM 情感分类模型4.1 Keras 下 LSTM 网络结构设计用Keras的Sequential足以完成中文短文本情感分析。网络结构是Embedding、LSTM、全连接、输出层from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Embedding, LSTM, Dense, Dropout model Sequential() model.add(Embedding(input_dimvocab_size, output_dim128, input_lengthmaxlen, mask_zeroTrue)) model.add(LSTM(units64, return_sequencesFalse, dropout0.3, recurrent_dropout0.2)) model.add(Dense(32, activationrelu)) model.add(Dropout(0.5)) model.add(Dense(1, activationsigmoid)) model.compile(optimizeradam, lossbinary_crossentropy, metrics[accuracy]) model.summary()Embedding把词索引映射成128维向量LSTM读入整个序列默认输出最后一个时刻的隐状态。return_sequencesFalse表示只输出最后一步适合句子级分类如果后面还要接LSTM层需要设成True。中间的DenseDropout给模型增加非线性也降低过拟合。最后一层sigmoid把输出压缩到0-1区间正好解释为“正面概率”。LSTM层的关键参数参数含义常见范围units隐状态维度32、64、128dropout输入单元丢弃率0.2到0.5recurrent_dropout循环连接丢弃率0.1到0.3return_sequences是否返回全部时刻False用于分类recurrent_dropout在CPU训练时会显著变慢因为循环连接难以并行。如果只有CPU可以先设成0只保留dropout加快迭代速度。4.2 损失函数、优化器与类不平衡处理二分类最常用的是binary_crossentropy优化器adam基本不用调参。当正负样本比例失衡时比如正面样本只有负面的三分之一直接训练会导致模型把所有样本都判成负面。最简单的办法是计算class_weight并传给训练过程neg_num (y_train 0).sum() pos_num (y_train 1).sum() class_weight {0: 1.0, 1: neg_num / pos_num}这样小类别的loss会被放大。注意小类别权重不要太大超过10容易震荡如果数据量很小优先选择换一个更平衡的数据集而不是强行加权。4.3 编写训练主循环与早停机制不用手写训练循环Keras的fit配合回调足够了from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint, ReduceLROnPlateau callbacks [ EarlyStopping(monitorval_loss, patience3, restore_best_weightsTrue), ModelCheckpoint(best_model.keras, monitorval_accuracy, save_best_onlyTrue), ReduceLROnPlateau(monitorval_loss, factor0.5, patience1, min_lr1e-5) ] history model.fit( X_train_pad, y_train, validation_data(X_val_pad, y_val), batch_size64, epochs20, callbackscallbacks, shuffleTrue )EarlyStopping的patience3表示连续3个epoch验证loss不下降就停止并恢复最优权重避免拿到过拟合后的坏模型。ReduceLROnPlateau在验证loss出现平台期时把学习率减半很多时候比手动调学习率更省事。batch_size64对短文本是常用起点如果训练时内存或显存不够就减半到32。epochs20配合早停已经足够盲设100会让答辩时被问“跑这么久在等什么”。4.4 训练过程的资源与复现性问题期末大作业一般在笔记本上跑CPU训练LSTM也能接受但要注意maxlen和units越大越慢。先小规模验证只取5000条数据、maxlen50、units32跑通后再上全量。训练前固定随机种子保证每次运行结果一致import os import random import numpy as np import tensorflow as tf random.seed(42) np.random.seed(42) tf.random.set_seed(42) os.environ[TF_DETERMINISTIC_OPS] 1这里的TF_DETERMINISTIC_OPS在TensorFlow 2.9以上版本才有效。固定随机种子能保证实验报告里的复现实验站得住脚否则同一份代码跑两次准确率差1个百分点很难解释。训练完成后history.history里保存了每个epoch的loss和accuracy这些数据后面画图时要直接取所以不要重新训练一次。5. 评估、保存与单条预测让模型可用起来5.1 测试集评估准确率、F1 与混淆矩阵训练时看到的validation accuracy是模型调参的参考最终成绩要以测试集为准。Keras预测得到0-1之间的概率需要转成类别from sklearn.metrics import classification_report, confusion_matrix y_prob model.predict(X_test_pad) y_pred (y_prob 0.5).astype(int) print(classification_report(y_test, y_pred, target_names[负面, 正面])) print(confusion_matrix(y_test, y_pred))classification_report会输出precision、recall、f1-score和support。当正负样本不均衡时只报accuracy会被追问所以报告中要写macro avg的F1。confusion_matrix的行是真实标签列是预测标签如果负面样本大量被预测成正面说明阈值需要调整或者类别权重不够。5.2 把模型和词表打包成推理闭包模型本身不包含Tokenizer预测新句子时必须使用同一套tokenizer和预处理函数。保存和加载的方式如下model.save(sentiment_model.keras) import pickle with open(tokenizer.pkl, wb) as f: pickle.dump(tokenizer, f) def predict_sentiment(text): tokens [w for w in jieba.cut(clean_text(text)) if w not in stopwords and w.strip()] if len(tokens) 0: return 空/中性, 0.5 seq tokenizer.texts_to_sequences([tokens]) seq pad_sequences(seq, maxlenmaxlen, paddingpost, truncatingpost) prob model.predict(seq, verbose0)[0][0] return (正面 if prob 0.5 else 负面, float(prob))注意这里的clean_text、stopwords和maxlen必须与训练时完全一致。一个常见的翻车现场是训练代码里清除了URL推理函数里忘了清洗结果“我买https://t.cn/x 的鞋很差”因为URL占坑导致整条句子长度被截断。最好把clean_text、tokenize、filter_stopword、pad_sequences串成一个模块训练和预测都调用同一个入口。5.3 实际预测时的文本长度与阈值问题短文本的maxlen截断会直接丢信息。如果一句话本身超过maxlen测试集里这种样本的比例不能忽视。判断方法很简单长度占比处理建议超过maxlen的样本占比 10%增大maxlen到95%分位超过maxlen的样本占比 2%保持当前maxlen长文本集中在某个类别考虑不做截断改为分段预测再平均阈值0.5不是铁律。如果业务场景更看重positive的precision可以把阈值提到0.6如果更看重recall阈值降到0.4。调阈值时使用验证集不要用测试集。6. 让期末作业变成高分的 4 个实战技巧6.1 可视化训练曲线用 loss 曲线解释模型行为很多人交作业只贴accuracy缺少损失曲线。一张训练/验证loss的对比图能直接说明过拟合和早停的必要性import matplotlib.pyplot as plt plt.plot(history.history[loss], labeltrain_loss) plt.plot(history.history[val_loss], labelval_loss) plt.legend() plt.savefig(loss_curve.png)训练loss持续下降但val_loss先降后升是典型过拟合信号配合EarlyStopping的patience说明即可。答辩时被问“为什么只训练了这些epoch”把这张图拿出来比念概念有说服力。6.2 给预测结果加概率解释不要只输出“正面/负面”把置信度写进演示界面或脚本输出。prob越接近1代表正向置信度越高0.5附近代表模型不确定。这个信息能解释错分样本label, prob predict_sentiment(包装完好物流很快但手感一般) print(f判定结果{label}正面概率{prob:.3f})如果输出概率是0.51即使判为正面也应该标注“低置信度”防止用户把模糊结果当结论。6.3 用交叉验证代替单切分时间允许的话把一次切分升级成5折StratifiedKFoldfrom sklearn.model_selection import StratifiedKFold skf StratifiedKFold(n_splits5, shuffleTrue, random_state42)每一折独立做Tokenizer拟合、模型训练和测试集预测最后汇总计算平均F1。报告里写“5折平均F10.87”比单次“准确率0.88”更抗质疑。代价是训练时间变成5倍但短文本小数据集通常可以接受。6.4 防止数据泄漏不要把 tokenizer 拟合用到测试集许多参考代码先对全部语料fit_on_texts再切分这其实泄漏了测试集的词表信息。正确流程是原始文本切分后只在训练集上拟合tokenizer再转换验证集和测试集。测试集里出现的新词统一映射到[UNK]。期末答辩时常被问“为什么你的测试精度比别人的高”答案往往就在这里。把tokenizer泄漏关掉新词交给[UNK]剩下的事情就用val_loss曲线替你说实话。本文还有配套的精品资源点击获取