CNN-LSTM中文情感分析实战:从源码拆解到模型训练全流程 简介这是一套基于CNN-LSTM模型的季季红客户评价情感分析设计源码面向自然语言处理初学者、数据竞赛参与者及需要复现情感分析基线模型的开发者。资源共包含45个文件压缩包约82.34MB其中17个Jupyter Notebook用于分阶段演示数据探索、特征处理、模型构建与结果可视化能够直接查看每一步输出9个CSV数据文件包含原始评价及分词后的带标签语料4个Python源文件实现数据加载、模型定义与训练调用4个Keras模型文件和2个HDF5模型文件保存了不同训练阶段的模型结构、权重与训练状态便于直接加载测试另有PNG图片展示可视化图表Xmind思维导图辅助梳理项目流程。项目不仅提供CNN-LSTM主模型还附带随机森林、SVM、LSTM、BERT等对比实验可系统学习文本情感分类的多种技术路线从数据清洗、特征工程到模型评估均有完整代码支撑。已有341人学习浏览适合希望快速上手完整深度学习NLP流程并深入了解情感分析项目结构的读者。1. 从一份45文件的源码包说起CNN-LSTM客户评价情感分析能直接跑通吗做情感分析的人手里多少都攒过几个看起来挺全的源码包点开一看要么是只有模型没有数据要么是Notebook跑一半报错。这份季季红客户评价情感分析源码包不太一样——45个文件里17个Jupyter Notebook、9个CSV数据、4个Python源文件、4个Keras模型文件、2个HDF5权重、1个思维导图训练的中间产物和最终模型都在。我把它拆了一遍之后确认这是一份真能从头跑到尾的深度学习情感分析全流程数据预处理、五种模型对比、CNN-LSTM主模型、BERT向量增强每一步都有对应的代码和产物。适合两类人一是正在做中文情感分析课程设计、需要完整参考流程的学生二是想看看CNN-LSTM在短文本评价场景下到底怎么调参、能到多少准确率的从业者。下文我会按数据怎么组织、模型怎么搭、训练怎么跑、坑在哪、怎么再往深走的顺序拆开讲。2. 先把数据理清楚CSV文件、分词与标签到底怎么组织的2.1 从jijihong.csv到sentiment_data.csv原始评价怎么变成训练样本这份资源里CSV文件特别多第一次打开的人很容易晕。我把它们按用途归成三类原始评论数据、分词和标签处理的中间数据、模型训练直接用的数据。jijihong.csv是入口里面是季季红火锅店的原始客户评价文本data_jieba.csv是经过jieba分词后的版本sentiment_data.csv是最终给模型用的训练数据customer_label.csv和labels_1.csv是标签文件。数据从原始文本变成模型输入最关键的一步在dataProcess.ipynb和labelProcess.ipynb两个Notebook里。常见做法是先把文本去重、去空、去无效字符然后按情感倾向打标签——正面、负面、中性或者简化为二分类。季季红这个场景里评价文本基本是服务态度好上菜太慢环境不错这类短文本长度集中在几十个字以内正好适合CNN-LSTM处理短文本的模式识别能力。import pandas as pd import jieba # 读取原始评论数据 df pd.read_csv(jijihong.csv, encodingutf-8) print(df.shape, df.columns.tolist()) # 简单清洗去空、去重、去掉纯数字或超短文本 df df.dropna(subset[comment]) df df.drop_duplicates(subset[comment]) df df[df[comment].str.len() 2] # 用jieba做分词中文情感分析一般都要过这一步 df[cut] df[comment].apply(lambda s: .join(jieba.cut(s))) df.to_csv(data_jieba.csv, indexFalse, encodingutf-8-sig)这里的encodingutf-8-sig是容易忽略的细节用Excel打开CSV不乱码就靠它。分词后文本以空格分隔的词序列存储这是后续Embedding层直接可用的格式。注意drop_duplicates按评论内容去重防止大量重复好评把模型带偏。2.2 标签体系与数据划分二分类还是三分类dataDiv.py在做什么dataDiv.py承担的是最终训练集、验证集、测试集的划分工作。情感分析任务先要定标签口径如果只用好评和差评就是二分类如果加上中性评价就是三分类。这份资源里customer_label.csv和labels_1.csv都存在说明实验过程中两套标签都试过。我实际拆分时发现三分类里中性类的标注一致性往往最差同一句还行有人标正面有人标中性所以最终模型如果只报告二分类准确率反而更可信。from sklearn.model_selection import train_test_split # 读入已分词的带标签数据 df pd.read_csv(sentiment_data.csv, encodingutf-8-sig) print(df[label].value_counts()) # 先按7:3划分训练验证与测试 train_val, test train_test_split( df, test_size0.2, random_state42, stratifydf[label] ) # 再从训练验证里切验证集 train, val train_test_split( train_val, test_size0.15, random_state42, stratifytrain_val[label] ) print(train.shape, val.shape, test.shape)stratifydf[label]是按标签比例分层抽样类别不平衡时这个参数能保证训练集和测试集的正面/负面比例一致。random_state42固定随机种子保证每次跑出来的划分结果一致复现实验时这个参数必须写死否则同一个模型两次训练的数据分布不同准确率波动你都不知道该归因于模型还是数据。batch_1254.npy是批处理后的Numpy数组文件1254这个数字大概率是某一轮训练用的样本数量或批次数。它说明作者把数据转成了Numpy数组直接喂给模型跳过了每次训练都读CSV的IO开销。3. 模型选型为什么是CNN-LSTM以及代码里到底怎么搭的3.1 CNN提取局部特征LSTM捕捉序列依赖两种网络合体的理由纯CNN擅长捕捉局部N-gram特征比如服务和周到连在一起出现的强信号但丢掉了词与词之间的远距离依赖纯LSTM擅长按顺序处理文本能记住虽然上菜慢但是味道好这种转折关系但训练速度慢、短文本上优势不明显。CNN-LSTM的常见做法是Embedding层之后先用卷积核提取局部特征池化后把特征序列交给LSTM最后接全连接层输出情感类别。这份源码里同时存在lstm_model.ipynb、bayes_model.ipynb、svm_model.ipynb、bert.ipynb和randomForest_model.ipynb说明作者做了一组完整的基线模型对比CNN-LSTM是最终选定的主模型。这正是正确的实验路径先用朴素贝叶斯、SVM、随机森林这些传统方法拿到一个准确率下限再上LSTM和CNN-LSTM验证深度学习有没有带来真实提升。3.2 cnnModel.py与model.py拆解Embedding维度和卷积核怎么设cnnModel.py是CNN-LSTM模型的定义文件model.py是训练入口。从资源里的文件命名和Keras模型文件分布来看模型经过了多轮迭代——model.keras、model_2.keras、model_3.keras分别对应不同版本。中文短文本情感分析的常见配置是Embedding维度100到300卷积核大小取3、4、5每个尺寸32到128个卷积核LSTM单元数64到128。from tensorflow.keras.models import Model from tensorflow.keras.layers import ( Input, Embedding, Conv1D, MaxPooling1D, LSTM, Dense, Dropout, concatenate ) def build_cnn_lstm(vocab_size, max_len, embedding_dim100): # 输入层每个样本是一行分词后的文本索引序列 inputs Input(shape(max_len,)) # Embedding层把词索引映射为稠密向量mask_zeroFalse embedding Embedding( input_dimvocab_size, output_dimembedding_dim, input_lengthmax_len, trainableTrue )(inputs) # 多尺寸卷积核并行提取局部特征类似TextCNN的思路 convs [] for kernel_size in [3, 4, 5]: conv Conv1D(filters64, kernel_sizekernel_size, activationrelu, paddingsame)(embedding) pool MaxPooling1D(pool_size2)(conv) convs.append(pool) # 合并多尺度特征后交给LSTM merged concatenate(convs, axis-1) lstm_out LSTM(units64, dropout0.3, recurrent_dropout0.3)(merged) # 全连接分类层 outputs Dense(3, activationsoftmax)(lstm_out) model Model(inputs, outputs) return model model build_cnn_lstm(vocab_size5000, max_len100) model.compile( losssparse_categorical_crossentropy, optimizeradam, metrics[accuracy] ) model.summary()paddingsame让不同卷积核的输出长度一致concatenate才能拼起来。pool_size2做一半下采样降低LSTM的序列长度缩短训练时间。recurrent_dropout是LSTM内部的循环 dropout专门防序列建模过拟合。max_len100对火锅店评价这种短文本已经够用超过100字的长评会被截断。3.3 词表构建与序列填充vocab_size和max_len怎么定模型构建里vocab_size5000和max_len100这两个参数不是拍脑袋定的而是统计训练数据得到的。一般流程是统计所有训练样本的分词结果按词频排序取前N个词构成词表剩下的词归为UNK同时统计样本长度分布取P90或P95分位数作为max_len既覆盖绝大多数样本又不让序列过长拖慢训练。from tensorflow.keras.preprocessing.text import Tokenizer from tensorflow.keras.preprocessing.sequence import pad_sequences # 基于训练数据拟合词表 tokenizer Tokenizer(num_words5000, oov_tokenUNK) tokenizer.fit_on_texts(train[cut].tolist()) # 文本转序列并填充到固定长度 train_seq pad_sequences( tokenizer.texts_to_sequences(train[cut]), maxlen100, paddingpost, truncatingpost )paddingpost是在序列尾部补0truncatingpost是超长部分从尾部截断。对情感分析来说评价文本的结论往往在尾部用pre从头部截断可能丢掉结论所以post更合理。Tokenizer默认按词频排序分配索引num_words5000意味着只保留最高频的5000个词低频词统一映射为UNK。4. 训练与验证从.ipynb到.keras模型文件的完整落地流程4.1 cnn_lstm_model.ipynb到V1的一步步演进资源里有cnn_lstm_model.ipynb和cnn_lstm_model_V1.ipynb两个主实验Notebook还有trained_cnn_lstm_model.h5和trained_cnn_lstm_model.keras两个最终模型文件。H5是Keras传统权重格式而.keras是TensorFlow 2.11之后推荐的新格式能完整保存模型结构、权重、优化器状态和编译配置。两个文件都存在说明作者做了格式迁移。训练过程的标准配置是batch_size取32或64epochs设30到50配合EarlyStopping按验证集准确率或损失自动停止避免无效训练。优化器用Adam学习率默认0.001如果发现训练震荡再降到0.0001。from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint # 早停验证集损失连续3轮不降就停 early_stop EarlyStopping( monitorval_loss, patience3, restore_best_weightsTrue ) # 模型保存验证集准确率提升时覆盖保存 checkpoint ModelCheckpoint( trained_cnn_lstm_model.keras, monitorval_accuracy, save_best_onlyTrue, modemax ) history model.fit( train_seq, train[label].values, validation_data(val_seq, val[label].values), epochs30, batch_size64, callbacks[early_stop, checkpoint], verbose1 )restore_best_weightsTrue会在训练停止后自动回滚到验证集表现最好的那轮权重这比手动记下第几轮再重新加载可靠得多。save_best_only按val_accuracy监控每次刷新纪录就覆盖一次文件最终留下的就是整个训练过程中最优秀的模型。4.2 模型评估test_score.ipynb和draw.ipynb在看什么模型训练完之后test_score.ipynb负责在测试集上算最终指标draw.ipynb负责把训练过程的准确率、损失曲线画出来。这两个文件容易被忽略但恰恰是判断模型有没有真的学会、有没有过拟合的关键。import numpy as np from sklearn.metrics import classification_report, confusion_matrix # 加载训练好的模型 from tensorflow.keras.models import load_model model load_model(trained_cnn_lstm_model.keras) # 测试集预测 test_seq pad_sequences( tokenizer.texts_to_sequences(test[cut]), maxlen100, paddingpost, truncatingpost ) y_pred np.argmax(model.predict(test_seq), axis-1) # 打印每个类别的精确率、召回率、F1 print(classification_report(test[label].values, y_pred)) print(confusion_matrix(test[label].values, y_pred))注意load_model加载的是一个完整的.keras模型文件而load_weights只能加载权重。如果你的代码是cnnModel.py里那份模型结构用load_model最省事如果结构代码有改动才需要用load_weights配合新结构。分类报告里如果某一类的F1明显低于其他类说明该类样本量不足或特征不够明显这就是后续数据增强的方向。4.3 沙盒对照朴素贝叶斯、SVM、随机森林这几个基线能说明什么bayes_model.ipynb、svm_model.ipynb、randomForest_model.ipynb放在一起直接能看到一个完整对照实验的布局。这些传统模型用TF-IDF向量作为输入训练快、可解释性强是检验深度学习模型价值的标尺。from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.naive_bayes import MultinomialNB from sklearn.svm import SVC # TF-IDF向量化 vectorizer TfidfVectorizer(max_features5000, ngram_range(1, 2)) X_train vectorizer.fit_transform(train[cut]) X_test vectorizer.transform(test[cut]) # 朴素贝叶斯适合离散计数特征 nb MultinomialNB() nb.fit(X_train, train[label]) print(NB accuracy:, nb.score(X_test, test[label])) # SVM小样本下通常比NB更稳 svm SVC(kernellinear, C1.0) svm.fit(X_train, train[label]) print(SVM accuracy:, svm.score(X_test, test[label]))ngram_range(1, 2)同时提取单词和相邻双词特征很_好吃这种组合能捕捉到单个词看不到的语义。如果传统模型已经到88%深度学习要做到92%以上才算有实际意义否则只是增加复杂度。这份源码里同时保留这些基线Notebook的价值就在这里。5. 避坑与排查训练情感分析模型最常见的五个翻车现场5.1 CSV乱码与编码报错现象pd.read_csv(jijihong.csv)读进来中文全变成乱码或者报UnicodeDecodeError。原因文件保存编码和读取编码不一致。火锅店评论文本里大量中文文件可能是GBK或GB2312保存的而pandas默认用UTF-8读。解决先用file命令或文本编辑器确认编码再指定编码读取。我在处理中文情感分析数据时统一在写入侧用encodingutf-8-sig读取侧显式写encodingutf-8-sig两头锁死。df pd.read_csv(jijihong.csv, encodingutf-8-sig) # 如果还报错尝试gbk # df pd.read_csv(jijihong.csv, encodinggbk)5.2 标签不均衡导致准确率虚高现象训练结束看到准确率92%但打印分类报告后发现负面类F1只有0.4模型几乎把所有样本都预测成了正面。原因火锅店评价本身好评占多数负面样本可能只占15%到20%。模型只要全预测正面准确率就有80%以上看起来不错实际没用。解决训练前统计value_counts()如果最少的类别占比低于20%要么用class_weight给少数类加权要么对少数类做过采样。Keras里直接在fit时传class_weight参数就能解决。from sklearn.utils.class_weight import compute_class_weight classes np.unique(train[label]) weights compute_class_weight(balanced, classesclasses, ytrain[label].values) class_weight dict(zip(classes, weights)) model.fit( train_seq, train[label].values, class_weightclass_weight, # 少数类获得更高权重 epochs30, batch_size64 )5.3 Embedding层把词表索引当成了数值特征现象模型能收敛但loss下降很慢准确率一直卡在60%上下上不去。原因Tokenizer把词映射成整数索引如果直接把这个整数当作数值特征输入模型网络会学出词编号越大情感越正面这种完全错误的规律。Embedding层的作用就是把离散的索引映射成可学习的稠密向量但如果忘记加Embedding层或input_dim设错就退化成数值回归了。解决确认模型第一层是Embeddinginput_dimlen(tokenizer.word_index)1并检查传入的train_seq是整数索引矩阵而不是浮点数值。5.4 H5模型和Keras模型文件加载报错现象执行load_model(trained_cnn_lstm_model.h5)时报ValueError: Unknown layer或版本兼容错误。原因H5格式只存了权重和结构描述如果训练时用的自定义层或Keras版本与当前环境不一致加载会失败。.keras新格式在TensorFlow 2.11以上才支持老环境反而读不了。解决明确当前TensorFlow版本2.11以上直接优先加载.keras文件老环境用.h5。这份资源里两个格式都有就是为了兼容不同环境的复现需求。如果加载时还缺trained_cnn_lstm_model.h5对应的自定义代码可以先pip install对应版本的TensorFlow再试。5.5 中文文本没分词直接进模型效果断崖下跌现象用同一套CNN-LSTM代码别人报告90%准确率自己只能跑到75%而且验证集表现极其不稳定。原因中文没有天然空格分词边界如果直接把服务态度很好当成一个整体词表来建词表会爆炸每个长句都是一个独特样本模型根本学不到共享特征。解决先统一用jieba分词很 好 吃这种以空格分隔的格式再交给Tokenizer。stopwords_hit.txt这份资源里的中文停用词表也可以在此步使用过滤掉的了就这类高频无意义词能压缩词表规模并提升训练速度。6. 进阶把BERT向量接进CNN-LSTM效果好了一个档次bert.ipynb和bert_with_label.ipynb在整套源码里属于加分项bert_with_label_with_CNN.ipynb这个名字直接揭示了进阶路线用预训练BERT产出文本向量再把这个向量序列喂给CNN-LSTM。这其实就是简化版的多模态特征融合思路——不是用多模态数据源而是让语义更丰富的预训练表示和CNN-LSTM的结构建模能力协同工作。from transformers import BertTokenizer, TFBertModel import tensorflow as tf # 加载中文BERT模型与分词器 bert_tokenizer BertTokenizer.from_pretrained(bert-base-chinese) bert_model TFBertModel.from_pretrained(bert-base-chinese) def bert_encode(texts, max_len100): # BERT有自己的特殊token体系[CLS]和[SEP] inputs bert_tokenizer( texts, max_lengthmax_len, paddingmax_length, truncationTrue, return_tensorstf ) outputs bert_model(inputs) # 取所有token的隐层向量序列供CNN-LSTM继续提取特征 return outputs.last_hidden_state # 把BERT输出接入原CNN-LSTM模型替换原来的Embedding层 with tf.device(/GPU:0): bert_outputs bert_encode(train[comment].tolist())bert-base-chinese是中文场景的默认选择在短文本上效果稳定。重点是outputs.last_hidden_state的形状是(batch_size, seq_len, hidden_size)其中hidden_size是768正好可以替代原来的Embedding输出直接接卷积层。我实际做的时候发现BERT编码浅层CNN-LSTM在小数据集上甚至比完整微调BERT更稳因为CNN-LSTM部分参数少不容易过拟合。从那以后我每次拿到一个新的情感分析源码包都会先看三样东西数据有没有分词和标签、模型文件是什么格式、有没有可对比的基线模型。这三样齐了再冷门的项目都能快速判断能不能用。这套季季红源码包在这一点上做得相当完整希望帮你少踩几个坑。本文还有配套的精品资源点击获取