尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

微博评论四分类情感分析:Keras LSTM-CNN 实战与避坑指南

微博评论四分类情感分析:Keras LSTM-CNN 实战与避坑指南 简介这份资源面向自然语言处理初学者与情感分析实践者提供一套基于Keras的LSTM-CNN微博评论情感分析完整项目目标是将评论划分为喜悦、愤怒、厌恶、低落四类情绪。资源包共6个文件包含4个Python脚本、1份PDF设计说明和1份Word训练文档压缩包约2.82MB脚本覆盖数据爬取、词云可视化、模型训练与预测全流程文档则记录神经网络模型训练细节与微信小程序设计思路。项目将LSTM的序列建模能力与CNN的局部特征提取能力融合配合词嵌入、Dropout、BatchNormalization等技巧并涉及训练集验证集测试集划分、早停策略及准确率、F1分数等评估指标。目前已有149人学习适合希望快速上手文本分类、复用可扩展框架并理解情感分析完整链路的读者参考。1. 微博评论四分类情感分析从 Keras 搭 LSTM-CNN 到能跑通的工程路径微博评论的情感分析很多人第一反应是二分类——正面还是负面。但真实业务里运营团队要的不是「好/坏」这种粗粒度标签而是能直接指导动作的四分类喜悦、愤怒、厌恶、低落。喜悦的评论可以拿来做口碑素材愤怒的要优先安抚厌恶的涉及品牌形象得单独处理低落的往往对应产品体验问题。这四类在语义上高度重叠愤怒和厌恶经常混在一起低落和厌恶也有交叉用传统的词袋模型加 SVM 基本分不开。我试过用 Keras 搭 LSTM-CNN 混合网络来做这件事核心思路是让 CNN 先提取局部 n-gram 特征再交给 LSTM 捕捉长距离依赖最后接全连接层做四分类。这套方案适合有一定深度学习基础、想快速跑通一个中文情感分析项目的工程师也适合需要给运营团队交付可解释分类结果的技术负责人。下面从数据准备、模型搭建、训练调参到踩坑排查把整条路径拆开讲清楚。2. 数据准备与标签体系四分类的标注一致性怎么保证2.1 微博评论的采集与清洗边界微博评论的数据来源常见做法有两种一是通过公开 API 按关键词和时间段拉取二是用已有的公开数据集做二次标注。不管哪种方式原始数据里一定混着大量噪声——广告、表情符号、提及、话题标签、重复刷屏。清洗时我一般按这个顺序走先去重再过滤掉纯表情和纯符号的评论然后处理 和话题标签。 提及直接删掉因为对情感判断没贡献话题标签保留标签内的文字去掉 # 符号本身。import re import pandas as pd def clean_weibo_comment(text): # 去掉 用户 text re.sub(r[\w\u4e00-\u9fa5], , text) # 话题标签保留内容去掉 # text re.sub(r#([^#])#, r\1, text) # 去掉 URL text re.sub(rhttp[s]?://\S, , text) # 去掉多余空白 text re.sub(r\s, , text).strip() # 过滤纯表情和纯符号保留中文、英文、数字和常用标点 if not re.search(r[\u4e00-\u9fa5a-zA-Z0-9], text): return None return text df pd.read_csv(weibo_comments_raw.csv) df[clean_text] df[text].apply(clean_weibo_comment) df df.dropna(subset[clean_text]) df df.drop_duplicates(subset[clean_text]) print(f清洗后剩余 {len(df)} 条评论)这段代码的关键点在于话题标签的处理方式直接影响后续分词效果保留标签内容能让模型学到「某个话题下的情感倾向」纯符号过滤用正则判断是否包含中文、英文或数字比按长度过滤更准。参数上re.sub的替换顺序不能乱先去 再去 URL否则 后面的 URL 可能被误删。2.2 四分类标签的标注规则与一致性校验喜悦、愤怒、厌恶、低落这四个标签边界模糊是最大的坑。我的经验是给标注团队一份明确的判定优先级先看情绪强度再看指向对象。愤怒通常指向具体对象且有攻击性厌恶更多是排斥和反感但不一定激烈低落是消极但无明确攻击对象喜悦就是正向情绪。如果一条评论同时包含愤怒和厌恶优先标愤怒同时包含低落和厌恶优先标厌恶。标注一致性用 Cohens Kappa 系数来校验低于 0.7 就说明标注规则需要重新对齐。实际操作中我会先让三个人独立标 500 条算 Kappa不达标就开会讨论分歧案例修订规则后再标一轮。这个过程通常要重复两到三次别想着一次到位。from sklearn.metrics import cohen_kappa_score # 假设 annotator1 和 annotator2 是两位标注者的标签列表 kappa cohen_kappa_score(annotator1, annotator2) print(fKappa 系数: {kappa:.3f}) # 低于 0.7 需要重新对齐标注规则提示标注一致性比标注数量重要。宁可花三天把 5000 条标准也不要花一天标 50000 条然后发现标签噪声太大导致模型学不动。3. Keras 搭建 LSTM-CNN 混合模型结构设计与参数含义3.1 为什么是 LSTM-CNN 而不是单独用其中一个单独用 LSTM 做中文情感分析问题是它擅长捕捉序列依赖但对局部关键短语的提取能力弱。比如「这手机续航太差了但拍照还行」LSTM 能记住「但是」前后的转折关系但「续航太差」这个局部特征容易被平均掉。单独用 CNN 则相反它能很好地提取 n-gram 特征但没法处理长距离依赖遇到「虽然……但是……」这种结构就抓瞎。LSTM-CNN 的混合思路是先用 CNN 做局部特征提取把每个窗口内的 n-gram 信息压缩成特征向量再把这些向量按时间顺序喂给 LSTM让 LSTM 在局部特征的基础上学习序列关系。这样既保留了 CNN 对关键短语的敏感度又有了 LSTM 对上下文的建模能力。在微博评论这种短文本场景下这个组合比单独用任何一个的 F1 通常能高 3 到 5 个百分点。3.2 模型结构的 Keras 实现与层参数说明from tensorflow.keras import Model from tensorflow.keras.layers import Input, Embedding, Conv1D, MaxPooling1D from tensorflow.keras.layers import LSTM, Dense, Dropout, Concatenate, GlobalMaxPooling1D from tensorflow.keras.optimizers import Adam def build_lstm_cnn_model(vocab_size, embedding_dim128, max_len100, conv_filters64, kernel_sizes[2,3,4], lstm_units64, num_classes4): inputs Input(shape(max_len,), dtypeint32) # 嵌入层vocab_size 是词表大小embedding_dim 是词向量维度 embedding Embedding(input_dimvocab_size, output_dimembedding_dim, input_lengthmax_len, mask_zeroTrue)(inputs) # 多尺度 CNN 提取不同窗口的 n-gram 特征 conv_outputs [] for ks in kernel_sizes: conv Conv1D(filtersconv_filters, kernel_sizeks, activationrelu, paddingsame)(embedding) conv MaxPooling1D(pool_size2)(conv) conv_outputs.append(conv) # 拼接多尺度特征 merged Concatenate(axis-1)(conv_outputs) if len(conv_outputs) 1 else conv_outputs[0] # LSTM 层捕捉序列依赖 lstm_out LSTM(lstm_units, return_sequencesTrue, dropout0.3, recurrent_dropout0.3)(merged) # 全局最大池化取每个维度上最强的信号 pooled GlobalMaxPooling1D()(lstm_out) # 全连接分类层 dense Dense(64, activationrelu)(pooled) dense Dropout(0.5)(dense) outputs Dense(num_classes, activationsoftmax)(dense) model Model(inputsinputs, outputsoutputs) model.compile(optimizerAdam(learning_rate1e-3), losscategorical_crossentropy, metrics[accuracy]) return model model build_lstm_cnn_model(vocab_size20000, embedding_dim128, max_len100) model.summary()这段代码里几个关键参数需要解释清楚。embedding_dim128是词向量维度微博评论这种短文本用 128 足够再大容易过拟合。kernel_sizes[2,3,4]对应二元、三元、四元 n-gram覆盖了中文里最常见的短语长度。conv_filters64是每个卷积核的数量三个尺度加起来就是 192 个特征图。lstm_units64控制 LSTM 隐藏状态维度太大训练慢且容易过拟合太小又学不到长距离依赖。dropout0.3和recurrent_dropout0.3是防止过拟合的关键微博评论数据量通常不大不加 dropout 很容易在训练集上跑到 95% 但验证集只有 70%。3.3 词表构建与序列填充的工程细节Keras 的 Tokenizer 默认按词频取前 N 个词但中文需要先分词。我一般用 jieba 做分词然后设置num_words20000把低频词过滤掉。序列填充用pad_sequencesmaxlen100覆盖了 95% 以上的微博评论长度超过的截断不足的补零。import jieba from tensorflow.keras.preprocessing.text import Tokenizer from tensorflow.keras.preprocessing.sequence import pad_sequences # 分词 df[segmented] df[clean_text].apply(lambda x: .join(jieba.cut(x))) tokenizer Tokenizer(num_words20000, oov_tokenOOV) tokenizer.fit_on_texts(df[segmented]) # 转序列并填充 sequences tokenizer.texts_to_sequences(df[segmented]) X pad_sequences(sequences, maxlen100, paddingpost, truncatingpost) # 标签转 one-hot from tensorflow.keras.utils import to_categorical label_map {喜悦: 0, 愤怒: 1, 厌恶: 2, 低落: 3} y to_categorical(df[label].map(label_map), num_classes4)paddingpost和truncatingpost表示在序列末尾补零或截断这样能保证 LSTM 读到的有效信息在前半部分。如果数据里长文本多可以改成pre但微博评论一般短post就够了。4. 训练调参与评估让四分类模型真正可用的关键设置4.1 训练集验证集划分与类别不平衡处理四分类的情感数据喜悦和愤怒通常占大头厌恶和低落样本少直接训练会导致模型偏向多数类。我一般用分层抽样保证每个类别在训练集和验证集里的比例一致然后对少数类做过采样或者用class_weight给损失函数加权。from sklearn.model_selection import train_test_split X_train, X_val, y_train, y_val train_test_split( X, y, test_size0.2, random_state42, stratifyy.argmax(axis1) ) # 计算类别权重 from sklearn.utils.class_weight import compute_class_weight import numpy as np class_weights compute_class_weight( balanced, classesnp.unique(y_train.argmax(axis1)), yy_train.argmax(axis1) ) class_weight_dict dict(enumerate(class_weights)) print(类别权重:, class_weight_dict)compute_class_weight的balanced模式会根据样本数自动反比加权样本少的类别权重高。这个权重在model.fit里传给class_weight参数能让模型在训练时更关注少数类。4.2 训练过程中的回调配置与早停策略from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint, ReduceLROnPlateau callbacks [ EarlyStopping(monitorval_loss, patience3, restore_best_weightsTrue), ModelCheckpoint(best_lstm_cnn.h5, monitorval_accuracy, save_best_onlyTrue, modemax), ReduceLROnPlateau(monitorval_loss, factor0.5, patience2, min_lr1e-6, verbose1) ] history model.fit( X_train, y_train, validation_data(X_val, y_val), epochs20, batch_size64, class_weightclass_weight_dict, callbackscallbacks )EarlyStopping的patience3表示验证损失连续 3 个 epoch 不下降就停restore_best_weightsTrue保证最后用的是最佳 epoch 的权重。ReduceLROnPlateau在验证损失停滞时把学习率减半帮助模型跳出局部最优。batch_size64是经验值显存够可以调到 128但太小会导致梯度噪声大。4.3 评估指标的选择准确率不够要看每类的 F1四分类问题里准确率会被多数类主导。比如喜悦占 50%模型全猜喜悦也有 50% 准确率但厌恶和低落的 F1 可能是 0。所以评估必须看每类的 precision、recall 和 F1。from sklearn.metrics import classification_report, confusion_matrix y_pred model.predict(X_val) y_pred_labels y_pred.argmax(axis1) y_true_labels y_val.argmax(axis1) print(classification_report(y_true_labels, y_pred_labels, target_names[喜悦, 愤怒, 厌恶, 低落])) print(confusion_matrix(y_true_labels, y_pred_labels))classification_report会输出每类的 precision、recall、F1 和 support。重点看厌恶和低落的 F1如果低于 0.6说明模型在这两类上还没学好需要检查标注质量或者增加这两类的样本。混淆矩阵能看出具体是哪两类在互相混淆比如愤怒和厌恶经常混那就需要回头看看标注规则是不是不够清晰。5. 避坑与排查LSTM-CNN 做微博情感分析时最容易翻车的五个地方5.1 损失不下降准确率卡在 25% 左右现象训练几个 epoch 后loss 一直在 1.38 附近震荡准确率约 25%跟随机猜差不多。原因通常是标签没转成 one-hot或者num_classes设错了。检查y_train.shape是不是(样本数, 4)如果是(样本数,)说明没做to_categorical。另外检查Dense(num_classes)是不是写成了 2 或 3。解决确认标签编码和输出层维度一致重新跑一遍。5.2 验证集准确率远低于训练集过拟合严重现象训练集准确率 95%验证集只有 65%差距超过 30 个百分点。原因是模型参数太多而数据量不够或者 dropout 没加。解决先把lstm_units从 64 降到 32conv_filters从 64 降到 32然后在 LSTM 和全连接层后面都加Dropout(0.5)。如果还不行考虑用预训练词向量初始化 Embedding 层或者做数据增强同义词替换、随机插入。5.3 厌恶和低落的 F1 始终上不去现象喜悦和愤怒的 F1 都在 0.8 以上但厌恶和低落只有 0.4 到 0.5。原因是这两类样本少且语义边界模糊。解决先检查标注一致性把这两类的标注样本抽出来人工复核一遍。如果标注没问题用class_weight加大这两类的权重或者对这两类做过采样。还可以考虑把四分类拆成两个二分类先分正向/负向再在负向里分愤怒/厌恶/低落。5.4 预测时出现维度不匹配错误现象model.predict时报ValueError: Input 0 of layer ... is incompatible。原因是预测数据的序列长度和训练时不一致。解决预测前必须用同一个tokenizer做texts_to_sequences并且用pad_sequences填充到同样的maxlen。不要重新 fit tokenizer否则词表索引会变。5.5 模型保存后加载预测结果全一样现象model.save后重新load_model对任何输入都输出同一个类别。原因是保存时没保存 tokenizer 的配置加载后词表对不上。解决用pickle把 tokenizer 一起保存加载时先恢复 tokenizer 再恢复模型。import pickle # 保存 with open(tokenizer.pkl, wb) as f: pickle.dump(tokenizer, f) model.save(lstm_cnn_model.h5) # 加载 with open(tokenizer.pkl, rb) as f: tokenizer pickle.load(f) model load_model(lstm_cnn_model.h5)6. 从四分类到可解释用注意力权重看模型到底在关注什么模型跑通之后下一步是让它变得可解释。运营团队不满足于「这条评论是愤怒」他们想知道「为什么是愤怒」。LSTM-CNN 本身没有注意力机制但可以在 LSTM 后面加一个注意力层让模型对每个时间步输出一个权重权重高的词就是模型认为重要的词。from tensorflow.keras.layers import Layer import tensorflow.keras.backend as K class AttentionLayer(Layer): def __init__(self, **kwargs): super(AttentionLayer, self).__init__(**kwargs) def build(self, input_shape): self.W self.add_weight(nameattention_weight, shape(input_shape[-1], 1), initializerglorot_uniform, trainableTrue) self.b self.add_weight(nameattention_bias, shape(input_shape[1], 1), initializerzeros, trainableTrue) super(AttentionLayer, self).build(input_shape) def call(self, x): # x shape: (batch, timesteps, features) e K.tanh(K.dot(x, self.W) self.b) a K.softmax(e, axis1) output x * a return K.sum(output, axis1) def compute_output_shape(self, input_shape): return (input_shape[0], input_shape[-1])把这个注意力层接在 LSTM 后面替换原来的GlobalMaxPooling1D训练完后取出注意力权重就能看到每个词对最终分类的贡献。比如一条「这手机续航太差了但拍照还行」的评论如果模型判为愤怒注意力权重应该集中在「太差」上而不是「还行」。这个可解释性对运营团队来说比单纯的分类标签有价值得多。验证注意力是否合理的方法抽 100 条验证集样本人工看注意力权重最高的三个词如果和人的直觉一致说明模型学到了正确的模式。如果注意力集中在「的」「了」这种停用词上说明模型还没学好需要检查分词和停用词过滤。我自己的习惯是每次训练完新模型先不看准确率先抽 20 条样本看注意力权重。这个习惯帮我省了很多「后悔药」——有几次准确率看着不错但注意力全在标点符号上说明模型在走捷径换一批数据就崩。希望这个思路能帮到你。本文还有配套的精品资源点击获取
返回列表