
简介利用LSTM模型对酒店中文评论进行情感分析的开源学习资源面向NLP初学者及课程设计、毕业设计等场景帮助快速掌握循环神经网络在文本分类中的完整落地流程。压缩包共3个文件包括可直接运行的Python训练脚本、整理好的酒店评论CSV数据集和说明文档整体大小约887KB脚本与数据无需额外预处理即可本地复现。目前已有775人浏览学习。资源提供从数据读取、中文分词、序列填充到LSTM模型构建与训练评估的完整代码说明文档梳理运行环境与常见排错要点评论数据按情感标签组织便于理解中文文本的预处理方式与特征表达。整体结构轻量既可作为入门LSTM情感分类任务的参考模板也适合在此基础上调整超参数、替换数据集或扩展可视化分析具有较强的可操作性与迁移性。1. 为什么酒店中文评论分析要选LSTM这种黑匣子模型能换来真实可用吗接手一个酒店评论情感分析需求时我最先否掉的方案不是深度学习而是规则匹配。客户给的数据里有大量“酒店还行吧”“房间小得可怜”“早餐一般般”这类委婉表达规则词表越列越长标注成本直线上升而且稍微换个说法就漏判。转用LSTM神经网络后模型直接学习整条评论的句序信息不再依赖人工罗列情感词准确率反而稳住了。本文要解决的就是这条完整链路从拿到一份可直接运行的酒店评论数据集开始做完中文分词、构建词表、搭建EmbeddingLSTM模型最后把新评论送进去输出情感概率。内容适合两类人刚入门NLP、想跑通第一个深度学习文本模型的读者已经会调接口、但没自己从零训练过中文文本模型的从业者。你可以照着代码一步步复现也能直接看到每个参数动了以后会发生什么以及容易翻车的几个地方在哪。2. 数据集与预处理把中文评论变成向量张量决定了模型一半的命运2.1 拿到数据后先想清楚原数据长什么样又该用什么格式喂给模型常规的酒店评论情感分析数据集核心信息是两列评论文本和情感标签。标签通常用 0 表示负面、1 表示正面有些数据集还会加一列“星级”或者“酒店名称”但跑基线模型时用不到。我先说通用的落地做法下载或整理好数据后放在项目根目录下命名为hotel_comments.csv。文件不需要很复杂能放进 Pandas 就行例如import pandas as pd df pd.read_csv(hotel_comments.csv, encodingutf-8-sig, enginepython) print(df.head()) print(df.shape) print(df[label].value_counts())代码逻辑说明这里用utf-8-sig而不是utf-8是因为很多从 Excel 导出的 CSV 会带 BOM 头直接读会把第一列列名识别成乱码。enginepython是应对文件里夹杂特殊分隔符时的保险选项如果文件行数不多这个引擎足够可靠。读进来后先看value_counts()确认正负样本是否均衡如果严重不均衡后面训练时要给少数类加权否则模型会偷懒地把多数类全部猜对得到虚高的准确率。这一步多数人都会做但容易忽略的是重复评论检查有些数据集会同一个用户文本复制多遍不做去重训练集和验证集之间可能混进完全相同的句子导致线上效果严重高估。2.2 中文文本清洗与分词既然是中文评论就不能跳过 jieba中文评论和英文评论最大的区别是没有天然空格所以必须分词。常见的做法是使用jieba分词它支持精确模式和全模式这里我们用精确模式。分词之前先做一层基础清洗把 URL、连续空格、以及不影响情感的标点符号去掉。但要注意一个坑不要机械地把所有标点都删掉问号和感叹号在情感分析里是有信号的不过为了跑通第一个版本先统一移除后面调优时再把这部分特征收回来。import re import jieba def clean_and_segment(text: str) - list: text re.sub(rhttps?://\S|www\.\S, , text) text re.sub(r\s, , text) text re.sub(r[^\u4e00-\u9fa5a-zA-Z0-9], , text) words jieba.lcut(text.strip()) return [w for w in words if w.strip() and w not in STOP_WORDS] STOP_WORDS set([的, 了, 是, 在, 也, 都, 和, 及, 与, 等, 被, 把, 就, 而]) df[words] df[comment].apply(clean_and_segment) print(df[words].head())代码逻辑说明lcut返回的是分词后的列表比cut更适合后续直接塞给 Keras 的 Tokenizer。停用词表先给一小部分高频无意义词不用上来就加载一个 10 万词的通用停用词表那种表会把“不会”“不太”这类否定词拆乱。这里正则[^\u4e00-\u9fa5a-zA-Z0-9]会把所有非中文、非英文、非数字的字符都删掉包括表情符号和特殊符号。新手最容易踩的坑是清洗和分词顺序搞反先分词再清洗会得到一堆碎词先清洗再分词才是正路。2.3 词表构建与序列填充用 Keras 的 Tokenizer 把词转成索引并把长度统一LSTM 需要定长输入而每条评论长度不一样因此要做 padding。这一步通常用 Keras 的Tokenizer和pad_sequences但有两个参数会影响模型表现num_words和max_len。num_words不是让 Tokenizer 只统计前 N 个高频词而是只保留这些词参与建模冷门词会被统一映射成oov_token对应的编号这对抑制过拟合很有效。from keras.preprocessing.text import Tokenizer from keras.preprocessing.sequence import pad_sequences MAX_LEN 40 NUM_WORDS 10000 tokenizer Tokenizer(num_wordsNUM_WORDS, oov_tokenUNK) tokenizer.fit_on_texts(df[words].apply(lambda x: .join(x))) sequences tokenizer.texts_to_sequences(df[words].apply(lambda x: .join(x))) X pad_sequences(sequences, maxlenMAX_LEN, paddingpost, truncatingpost) y df[label].values print(词表大小, len(tokenizer.word_index)) print(X shape, X.shape)参数说明MAX_LEN取 40是因为酒店评论文本普遍较短超过 40 个词的评论占比很低。paddingpost表示在尾部补 0truncatingpost表示超过长度时截断尾部而不是头部这符合评论语义分布在全文而非开头的特点。oov_token必须设置否则新评论里出现训练集没见过的词时Tokenizer 会直接扔掉该词导致序列长度不足最终模型行为不可控。经过这三步原始评论已经变成一个[样本数, 40]的整数矩阵每个整数对应一个词在词表中的位置接下来可以进入模型阶段。3. 搭建 LSTM 模型把 Embedding、双向 LSTM 和输出层一次说透3.1 为什么筑基用 Embedding 而非独热编码直接把词编号喂给 LSTM 是一个常见错误。词的编号只有相对大小关系而Embedding层会把每个编号映射为一个稠密向量让模型在训练过程中自动学习到词与词之间的语义距离。对于酒店评论场景“干净”和“卫生”这两个词如果都在评论里高频共现它们的向量就会逐渐靠近。Embedding 层有两个参数需要关注input_dim和output_dim。前者是词表容量按前面的NUM_WORDS来后者是向量维度常见做法是取 128数据集小的时候 64 也够用。接下来是 LSTM 层核心参数。units决定隐藏状态维度越大表示记忆容量越大但训练参数也会暴涨。对酒店评论这样的小规模文本64 或 128 是稳妥区间。return_sequences如果为True则会输出每个时间步的隐藏状态适合做序列到序列任务而情感分析只需要最后一个时间步的输出所以要设为False。dropout和recurrent_dropout分别控制输入和循环连接的随机失活比例设 0.2 起步防止过拟合。我还建议在 LSTM 前面接一层Bidirectional虽然参数翻倍但对短文本情感分类的提升非常明显模型能同时看到前文和后文的信息相当于既看了“虽然房间小”又看了“但是很干净”。3.2 最小可运行模型代码Keras 搭建和训练入口模型结构采用 Embedding 加双向 LSTM 加全连接输出。输出层用单个神经元加sigmoid因为这是二分类。损失函数用binary_crossentropy优化器优先选 Adam学习率用默认的 0.001这个组合在绝大多数文本分类任务上都能较快收敛。from keras.models import Sequential from keras.layers import Embedding, Bidirectional, LSTM, Dense, Dropout model Sequential([ Embedding(input_dimNUM_WORDS, output_dim128, input_lengthMAX_LEN), Bidirectional(LSTM(units64, dropout0.2, recurrent_dropout0.2, return_sequencesFalse)), Dense(32, activationrelu), Dropout(0.5), Dense(1, activationsigmoid) ]) model.compile(optimizeradam, lossbinary_crossentropy, metrics[accuracy]) model.summary()代码逻辑说明Embedding的input_length要和之前的MAX_LEN一致否则后面 reshape 会直接报错。中间加了一层有 32 个神经元的全连接层目的是让 LSTM 输出的高维特征先做一次压缩直接接输出层会让决策边界过于简单。Dropout(0.5)放在全连接层之后是文本分类任务里一个很常用的位置能有效防止模型把训练集里的细节噪声背下来。summary()值得认真看一次它会打印每一层的参数数量如果你看到参数量是几十万甚至上百万而你的训练集只有几千条就要警惕过拟合了要么增大 dropout要么减小NUM_WORDS。训练时还要加两个回调函数。EarlyStopping监控验证集 loss连续多个 epoch 不下降就提前结束省时间也能防过拟合。ModelCheckpoint把验证集上表现最好的权重存下来避免最后一次 epoch 的权重是次优的。这里我习惯用val_loss而不是val_accuracy作为监控目标因为 loss 对概率校准更敏感准确率在类别不平衡时会有很大的迷惑性。from keras.callbacks import EarlyStopping, ModelCheckpoint callbacks [ EarlyStopping(monitorval_loss, patience5, restore_best_weightsTrue), ModelCheckpoint(best_model.h5, monitorval_loss, save_best_onlyTrue) ] history model.fit( X_train, y_train, validation_data(X_val, y_val), batch_size64, epochs30, callbackscallbacks )参数说明batch_size64适合当前数据集规模太小会让梯度更新过于频繁、训练震荡太大则每个 epoch 时间长而且容易收敛到尖锐极小值。patience5表示验证集 loss 连续 5 个 epoch 不下降就停。如果你发现模型在第三个 epoch 就停了不是坏事说明模型已经收敛如果前几个 epoch 训练 loss 降得很快但验证 loss 不降那就重点检查分词和清洗是否有信息泄露。4. 验证与测试别只盯着准确率混淆矩阵和单条预测才能反映真实效果4.1 准确率高不等于效果好样本不均衡和数据泄露怎么暴露二分类情感模型如果正负样本比例是 9:1模型只需要把全部预测成正面就能拿到 90% 的准确率但这显然没有交付价值。所以第一件事是用sklearn做分层划分让训练集和验证集保持同样的类别比例。from sklearn.model_selection import train_test_split X_train, X_val, y_train, y_val train_test_split( X, y, test_size0.2, stratifyy, random_state42 ) from sklearn.metrics import classification_report, confusion_matrix y_pred (model.predict(X_val) 0.5).astype(int32) print(classification_report(y_val, y_pred)) print(confusion_matrix(y_val, y_pred))代码逻辑说明stratifyy是必须的它会按原始标签比例切分数据否则可能把数据重的那一类全分到验证集里。预测输出是概率值需要手动用 0.5 做阈值转成 0 或 1。打印出来的classification_report里重点看 F1-score 而不是 accuracy因为 F1 综合了精确率和召回率能反映模型对少数类的判断能力。我见过不少团队在这个环节发现训练时 loss 正常下降但验证集 F1 很差原因就是原始的 CSV 里有大量重复评论且没有去重导致训练集和验证集之间重叠。4.2 单条评论测试训练集里跑得好不代表线上能用模型训练完必须用一个从没见过的句子走通完整流程。这里最容易出问题的是预处理不一致训练时做了清洗、分词、去停用词预测时如果漏了清洗步骤模型的输入分布就和训练时不一致结果自然不对。常见做法是把清洗、分词、编码、padding 封装成一个函数这个函数就是模型唯一的前置入口。def predict_sentiment(text: str) - float: words clean_and_segment(text) seq tokenizer.texts_to_sequences([ .join(words)]) padded pad_sequences(seq, maxlenMAX_LEN, paddingpost, truncatingpost) prob model.predict(padded)[0][0] return prob print(predict_sentiment(酒店位置很好服务员态度特别热情但隔音效果一般))代码逻辑说明这里把词表编码和 padding 全部收紧到一个函数内避免手动分步操作时遗漏。model.predict返回的是一个二维数组第[0][0]个元素就是负面到正面的概率分数。拿这个输出试试几条不同倾向的评论观察分数是否落在合理区间。如果一条明显负面的评论给出了 0.8 的概率那不是模型出错而是训练语料里缺少类似表达或者分词把关键否定词切碎了。5. 避坑指南从数据清洗到预测的全链路常见问题与排查5.1 现象训练集准确率 99%验证集准确率只有 70%原因评论里有大量重复行同一句话既出现在训练集也出现在验证集模型在验证集上看到的其实是被背下来的句子。另一种原因是数据划分时没有stratify导致验证集里全是某个类别模型当然表现差。解决在划分数据集前先df.drop_duplicates()最好把评论文本作为去重键。如果去重后样本量太少再考虑用交叉验证。去重后重新跑训练验证集准确率会真实很多。这个坑最容易出现在公开下载的酒店评论数据集中很多数据集本身就是爬虫多次抓取拼接出来的重复率远超你想象。5.2 现象预测时程序报错提示sequence length mismatch原因训练时pad_sequences使用maxlen40而预测时传入的新句子长度超过 40且没有走同一个截断逻辑导致矩阵维度不一致。解决确保预测函数里clean_and_segment之后直接调用texts_to_sequences和pad_sequences不要单独手动构造序列。另外在pad_sequences里明确设置truncatingpost这样长句子会从尾部截掉多余部分而不是从开头截避免了关键信息被切掉。5.3 现象训练 loss 一直在降但 loss 曲线震荡幅度很大原因学习率设置过高。Adam 默认学习率 0.001 多数情况下够用但如果你在model.compile里自定义了优化器给了偏大的学习率LSTM 这种循环结构会放大梯度波动。解决把优化器换成显式Adam(learning_rate0.0005)甚至 0.0001同时观察 loss 曲线是否平稳下降。如果仍然震荡把batch_size从 64 提高到 128让每个 batch 的梯度估计更稳定。5.4 现象模型对“酒店还可以吧”这种中性偏负面的句子预测成正面原因分词把“还”和“可以”切成了两个独立词模型没有学到“还可以”在评论语境里的保留态度。另外训练数据集里这类微妙表达本身很少模型没有见过足够多的相似模式。解决在清洗阶段把“还可以”“一般般”“还行”这类高频词组作为整体加入分词词表用jieba.add_word(还可以)强制不拆分。进一步的做法是统计训练集里出现次数高但情感倾向模糊的 n-gram把它们补进词表。这一步对情感分析的提升往往比调 LSTM 结构更明显。分词是中文 NLP 中最有性价比的优化点。5.5 现象模型保存和加载后预测结果和训练结束时的结果不一样原因ModelCheckpoint保存的是权重但如果你重新加载模型时没有重新编译或测试阶段没有调用model.compile某些 Keras 版本会丢失优化器状态影响结果复现。解决保存时直接用model.save(best_model.h5)保存完整模型而不是只存权重。加载时用load_model(best_model.h5)这个函数会同时恢复结构和权重。如果只存了权重就要在加载后重新compile一次再预测。另一个细节是随机种子训练前对 NumPy 和 TensorFlow 统一设seed保证多次实验可比较。6. 最终验证技巧用一组自定义句子让模型跑一遍识别出它到底学到了什么模型训练完成之后我不建议直接拿客户给的测试集跑一次就收工。那只能说明模型在这份数据集上有效无法体现真实场景下的鲁棒性。我的习惯是准备一组带明显倾向的中文短句覆盖好评、差评、带转折的评论和口语化表达逐个输入到预测函数里看输出概率比如“性价比很高”“半夜停电没人管”“虽然位置偏但房间很大”“真的绝了”这种反讽表达。你很快会发现模型对字面意义上的正负词比较敏感但遇到反讽和转折时会露出原形。如果反讽句输出错了不用急着改模型结构。先检查这类句子在训练集里的比例如果极少甚至没有模型的失败是正常的。解决方向有两个一是收集更多带转折和口语化表达的数据做数据增强二是把文本长度上限加大一些因为反讽和转折通常依赖上下文太短的文本会让 LSTM 没有足够上下文去识别。另外最后一步还要检查模型的“不确定区”。把所有验证集样本按预测概率排序找出概率落在 0.4 到 0.6 之间的样本打印出来逐条查看。这些样本往往是最有价值的观察对象因为它们反映了模型真实的困惑所在。如果大量模棱两可的样本聚集在某种表达模式上比如“服务员态度一般但环境不错”就该考虑用Bidirectional LSTM搭配更精细的分词策略来增强模型对转折的建模能力。调参时我会先固定Embedding维度为 128然后只调 LSTM 的units值在 32、64、128 之间做三组对比实验每组训练两遍取平均避免单次随机浮动干扰判断。这套流程走下来我最大的体会是LSTM 本身不是效果瓶颈文本预处理的细腻程度才是。每一处清洗规则和分词处理都可能让最终准确率有 5% 到 8% 的波动而调整 LSTM 层数反而不容易有这么明显的变化。现在我已经习惯把所有预处理步骤封装成一个函数任何新数据进来都先跑同一套流程确保训练和预测之间没有偏差。希望这篇文章能帮你少走几段弯路把第一个中文情感分析模型稳稳跑起来。本文还有配套的精品资源点击获取