简介:基于TextCNN的中文文本情感分析实战资源包,面向NLP入门学习者与需要快速搭建情感分类模型的开发者,提供完整可运行的代码与标注数据,解决从数据预处理、模型训练到效果评估的全流程实践难题。压缩包共包含24个文件,核心代码由5个Python脚本与4个Jupyter Notebook组成,同时附有正负情感样本、中文停用词表等文本数据,以及已训练好的h5模型、TensorFlow checkpoint权重备份、模型结构图、数据分析图表和HTML演示报告,整体体积约89.49MB。读者借助脚本与Notebook两种形态对照学习,可快速理解TextCNN在中文短文本情感分类中的词嵌入、卷积、池化与输出层设计,并掌握从数据清洗到结果可视化的完整流程。目前已有499人学习下载,资源包保留完整工程结构,适合课堂实验、课程设计或作为企业级情感分析项目的起步基准,能显著减少环境配置与代码调试时间。
1. 基于TextCNN的中文文本情感分析:不调包也能跑通的完整实战
做中文文本情感分析,最怕的不是模型选型,而是数据没准备好、分词不一致、模型训练完却在预测阶段翻车。这个项目把整个链路都裁好了:训练脚本、评估脚本、正负样本语料、停用词表、预训练好的textcnn.h5,以及两个Jupyter分析文件,代码和数据都是完整的,解压后能直接跑起来出结果。对正在做评论情感分类、舆情监控或者产品反馈归类的工程师来说,这套资源最大的价值不是“能跑”,而是让你在半天内摸清TextCNN从数据预处理到模型落地的完整流程,顺带把那几个容易埋坑的细节一次性踩明白。
2. 数据与预处理:决定模型上限的第一关
2.1 数据文件结构与标签含义
项目压缩包解压后会出现一个data目录,里面放的是训练用的原始语料,其中neg.txt和pos.txt分别是负面和正面情感文本。文件格式是非常朴素的一行一条文本,UTF-8编码,没有表头。我用编辑器打开扫了一遍,样本规模在几千条的级别,对深度学习模型来说不算大,但用来训练一个入门级的TextCNN完全够用。
# 查看数据文件行数(Linux或macOS) wc -l data/neg.txt data/pos.txtwc -l输出两个文件的行数,这里的行数就是样本数。做情感分析前先确认正负样本比例是否均衡,这是最基础的一步。如果pos.txt比neg.txt多出太多,模型会偏向预测多数类,后面评估阶段会发现准确率不错但召回率失衡。常见做法是看行数后把多的那边随机抽到和少的那边一样多,再用shuf命令打乱顺序。
预处理脚本在utils.py里,核心逻辑是读入原始文本,调用jieba分词,再用chinese_stopwords.txt过滤停用词。整个流程可以拆成三步:清洗文本、分词、去停用词。清洗部分包含去掉URL、邮箱、数字串和特殊符号的正则表达式,这部分直接影响分词的准确性,比如“666”这种数字如果不处理,会被当成一个词喂给模型,纯属噪音。
import re import jieba def clean_text(text): # 去掉URL text = re.sub(r'https?://\S+|www\.\S+', '', text) # 去掉邮箱 text = re.sub(r'\S+@\S+', '', text) # 去掉数字和字母串 text = re.sub(r'[a-zA-Z0-9]+', '', text) # 去掉非中文字符和常用标点(保留句号、逗号、感叹号) text = re.sub(r'[^\u4e00-\u9fa5,。!?、;:""''()《》]', '', text) return text.strip()这段正则里字符范围\u4e00-\u9fa5是Unicode中基本汉字区段,保留的标点列表可以由你自己扩充。关键在于过滤顺序:先去掉URL和英文,再去标点,避免URL里的符号干扰后续正则。很多初学项目死在“先标点后数字”,导致“10086”被拆成“1”“0086”,后面模型怎么训都差一口气。
2.2 切词与停用词:效果差异的真正来源
分词这件事,粗看是调库,实际上纠结的地方不少。这个项目用的是jieba的精确模式,背后是统计词典和HMM的混合机制。情感分析场景里,切词粒度敏感度非常高——“不喜欢”如果被切成“不/喜欢”,模型能学到负面信号;但要是不小心被切成“不喜/欢”,特征就乱了。所以常见做法是给jieba加载自定义词典,把产品名、品牌词、网络用语预先固定。
# 加载自定义词典示例(示例路径,资源里没有可自行创建) jieba.load_userdict("user_dict.txt") # 每行格式:词语 词频 词性 # 例如:难用 50 n def preprocess_line(line, stopwords): line = clean_text(line) words = jieba.lcut(line) # 精确模式 words = [w for w in words if w not in stopwords] return " ".join(words)jieba.lcut返回一个分词后的列表,和jieba.cut的区别在于前者直接给列表,后者是生成器,数据量大时用生成器省内存。过滤停用词那行代码里,None判断不能漏——某些场景下分词结果可能混入空串,直接过滤会报错。停用词表的选取也直接影响效果,项目自带的是通用中文停用词表,但针对垂直领域,比如电商评论里的“快递”“包装”这类词要不要保留,得根据任务判断,高频但与情感无关的词往往会被归入停用词。
2.3 标签与数据集划分
预处理完成后,原始文本被转为空格分隔的词序列,而标签就是每个句子来自哪个文件。训练脚本的做法是给neg.txt打0、pos.txt打1,然后随机划分训练集和验证集。如果你的数据不是这个结构,自己准备时格式要跟齐——这个项目不读Excel或数据库,只认txt,每行一条样本。
import codecs def load_data(neg_path, pos_path, split_ratio=0.8): neg_samples = codecs.open(neg_path, encoding='utf-8').read().strip().split('\n') pos_samples = codecs.open(pos_path, encoding='utf-8').read().strip().split('\n') # 构造标签 neg_data = [(preprocess_line(s), 0) for s in neg_samples] pos_data = [(preprocess_line(s), 1) for s in pos_samples] all_data = neg_data + pos_data random.shuffle(all_data) split_idx = int(len(all_data) * split_ratio) return all_data[:split_idx], all_data[split_idx:]文件读取用了codecs.open,明确指定UTF-8编码,比内置open()更稳,Windows环境下尤其需要,不然有些机器默认用GBK打开会直接乱码。split_ratio取0.8意味着800条样本里640条用于训练、160条验证,对这个小数据集是合理比例。如果样本量到几万条,这个比例可以往后挪到0.9,因为深度学习模型吃数据。
3. TextCNN模型结构:卷积怎么“看”文本
3.1 从嵌入到卷积的完整链路
模型定义在train.py里。TextCNN的原理一句话就能讲明白:把句子里的每个词映射成向量,然后用多个不同宽度的卷积核在序列维上滑动,提取n-gram特征,最后接全局池化和全连接层输出情感概率。它在中文情感分析里表现不错的原因在于,卷积核宽度相当于n-gram窗口,能捕捉“ 太/差”这种局部语序特征。
def textcnn_model(vocab_size, embedding_dim=128, num_filters=256, sequence_length=50): model = tf.keras.Sequential([ # 嵌入层:把词索引映射为稠密向量 tf.keras.layers.Embedding(vocab_size, embedding_dim, input_length=sequence_length), # 三个不同宽度的卷积核并列提取特征 tf.keras.layers.Conv1D(num_filters, 2, activation='relu'), tf.keras.layers.GlobalMaxPooling1D(), tf.keras.layers.Conv1D(num_filters, 3, activation='relu'), tf.keras.layers.GlobalMaxPooling1D(), tf.keras.layers.Conv1D(num_filters, 4, activation='relu'), tf.keras.layers.GlobalMaxPooling1D(), # 拼接后接全连接层 tf.keras.layers.Concatenate(), tf.keras.layers.Dense(128, activation='relu'), tf.keras.layers.Dropout(0.5), tf.keras.layers.Dense(2, activation='softmax') ]) return model这段代码展示的是Keras风格实现,项目里同时有sa.tf.keras.ipynb和checkpoint目录下的TensorFlow原生实现,两份结构基本等价。这里有几个关键参数值得注意:embedding_dim=128是词向量维度,维度太低表达不了语义,太高在小数据集上容易过拟合;num_filters=256是每个卷积核的filter数量,控制特征提取能力;sequence_length=50是输入句子的固定长度,超过截断,不足补零。分词后的句子长度差异很大,必须统一到相同长度才能组成张量喂给模型。
3.2 为什么用三个不同宽度的卷积核
TextCNN最核心的设计思想就是多宽度卷积核并行。宽度为2的卷积核看到的是“不/好”这样的相邻词组合,宽度为3和4的能看到“非常/不/好”这类更长的局部片段。这种设计比单一宽度的卷积核更加贴合中文里情感表达的多粒度特点。
项目里的model_shape_out1.png是训练过程中某个层的输出shape打印结果,model_shape.png是模型整体结构图。看这两张图能直观感受到Tensor维度走完各层之后的变化:输入是(batch, 50)的整数索引,经过Embedding变成(batch, 50, 128)的浮点数张量,再经过三个不同的Conv1D分别输出三个(batch, 48, 256)的特征图。宽度为2的卷积核会把序列长度从50压到49,再经过全局池化变成(batch, 256)的向量。三个池化结果拼起来就是(batch, 768),最后两层全连接输出2分类概率。
理解了这个流程,你就明白为什么sequence_length必须一致,也明白为什么padding和截断策略要慎重——如果截断了文本里的关键情感词,比如“这部电影真是烂透了”截到“这部电影真是烂”,情感倾向可能还保留,但截到“这部电影”就信息全无了。项目里默认取前后50个字,对短文本影响不大,对长评论需要谨慎调整。
3.3 损失函数与优化器的选择逻辑
训练部分用的损失函数是交叉熵,优化器是Adam。这个组合在分类任务里属于默认搭配,但背后有个小细节值得讲:TextCNN的Embedding层到底要不要预训练词向量?项目里直接随机初始化,靠训练过程自己学习词嵌入。这种做法在小数据集上词向量的语义质量有限,但胜在实现简单,不必额外下载大规模预训练向量文件。如果你的数据量在几万条以上,可以考虑换成预训练词向量初始化Embedding,用load_embedding函数把word2vec或GloVe的输出填进去,效果通常会有提升。
# 训练核心代码(项目train.py精简版) model.compile( optimizer=tf.keras.optimizers.Adam(learning_rate=1e-3), loss='sparse_categorical_crossentropy', metrics=['accuracy'] ) history = model.fit( train_x, train_y, batch_size=64, epochs=20, validation_data=(val_x, val_y), callbacks=[tf.keras.callbacks.ModelCheckpoint( 'checkpoints/textcnn.ckpt', save_best_only=True )] )sparse_categorical_crossentropy这个损失函数对应整数标签,如果你的标签是one-hot编码,就要换成categorical_crossentropy,这是新手最容易搞混的地方。learning_rate设为1e-3是Adam的常见起点,小数据集可以从这个值开始,loss震荡不降就调小到1e-4。save_best_only=True意味着只有验证集loss比上一次好时才覆盖模型文件,这样训练结束后磁盘上留下的就是验证集表现最好的版本,而不是最后一个epoch的过拟合版本。
4. 训练与评估:让模型在十分钟内稳定收敛
4.1 训练流程与模型保存
项目里训练入口是train.py,运行时会在checkpoints目录下生成cnn.meta、cnn.data-00000-of-00001和cnn.index这三个文件。这是TensorFlow 1.x的checkpoint格式,cnn.meta保存计算图结构,.data文件保存权重,.index是索引。同时项目还提供了一个Keras格式的textcnn.h5,这个文件的价值在于——你不需要重新训练就能直接做预测。
训练时长取决于CPU还是GPU。数据量只有几千条、序列长度50、模型结构不算深,普通笔记本CPU上20个epoch大约需要10到20分钟,GPU上面几乎一两分钟就收敛。跑训练的时候重点关注两个输出维度:训练集loss和验证集loss。如果训练loss持续下降但验证loss在第10个epoch左右开始回升,这是过拟合的典型信号,早停回调可以解决,项目中epoch设20次,实际跑到15次左右基本就能看出收敛趋势。
# 查看checkpoint内容的命令行方式(可选) python -c " import tensorflow as tf checkpoint = tf.train.load_checkpoint('checkpoints') print(checkpoint.get_variable_to_shape_map()) "上面的命令会打印checkpoint里所有变量的名称和shape,你可以直观看到哪些层有可训练参数。Embedding层的变量名通常是embedding加上一串后缀,shape是(vocab_size, 128)。这一步有助于核对训练过程中保存的参数是否是你期望的模型结构。
4.2 预测流程:加载h5文件做单条预测
训练完模型之后,实际业务中使用的是evaluate.py和textcnn.h5。预测的链路和训练时的预处理必须保持完全一致,这一步是绝大多数人翻车的地方——模型训练时用jieba分词、去停用词,预测时却直接往里灌原始文本,结果要么报维度错误,要么预测结果全偏向某一类。
import tensorflow as tf import numpy as np from utils import preprocess_line def predict_sentiment(text, model_path='textcnn.h5'): # 加载训练好的模型 model = tf.keras.models.load_model(model_path) # 预处理必须先走一遍和训练时相同的流程 cleaned = preprocess_line(text, stopwords) # 按词表把词映射成索引序列 tokens = cleaned.split() seq = [vocab.get(token, 0) for token in tokens][:50] seq = seq + [0] * (50 - len(seq)) # padding到固定长度 # 预测 prob = model.predict(np.array([seq]))[0] idx = int(np.argmax(prob)) return idx, prob[idx]这里vocab是训练时构建的词表映射,训练脚本会存一份词表文件,预测时必须加载同一份词表,否则同一个词在预测阶段的索引编号和训练时的编号对不上,整个预测基本就是乱猜。[0] * (50 - len(seq))是右侧补零,补零方向也有影响——补在序列尾部更符合CNN对边界特征的提取逻辑,因为补零位置不携带语义信息。
4.3 在Jupyter里复现全流程
项目里的># 从ipynb抽取的绘图代码:观察训练曲线 import matplotlib.pyplot as plt plt.plot(history.history['accuracy'], label='train_acc') plt.plot(history.history['val_accuracy'], label='val_acc') plt.legend() plt.savefig('training_curve.png')
这个项目的可视化不是重点,但训练曲线一定要看——如果val_accuracy在某个epoch后突然直线下降,说明验证集里有异常样本;如果train和val一起低,那问题不在模型,在数据和预处理。
5. 避坑指南:四个高频翻车场景与排查方案
5.1 预测时报维度错误
现象:加载textcnn.h5后调用predict报错,提示Input 0 of layer dense is incompatible with the layer,或者维度不匹配。
原因:训练时输入的序列长度是50,但预测时输入的数据长度不是50。常见情况是预测脚本里忘了padding,或者sequence_length参数改过了但加载的模型还是旧的结构。
解决:统一改sequence_length。如果模型是用sequence_length=50训练的,预测时必须也用50,不能改。改法是在训练前就把这个参数定死,写入配置文件。预测代码里[:50]和(50 - len(seq))两个数值必须同时改。
5.2 checkpoing加载失败或h5文件无法导入
现象:运行load_model('textcnn.h5')时在Adam或Dense相关的层上报错,提示缺少自定义层。
原因:项目里同时存在TensorFlow 1.x的checkpoint和Keras保存的h5文件。h5文件若由老版Keras保存,而你的环境是新版TensorFlow,存在兼容性问题。
解决:优先用项目自带的sa.tf.keras.ipynb里的训练代码重新保存一次模型,保存为saved_model格式。新版TensorFlow加载h5的方案是加compile=False:
model = tf.keras.models.load_model('textcnn.h5', compile=False)这个参数跳过编译状态的恢复,只加载权重和网络结构,能够绕开优化器状态相关的兼容性问题。加载后如果要做预测,直接调用predict即可,不需要重新compile。
5.3 切词不一致导致预测结果失真
现象:训练时准确率很高,到了预测阶段,明明把正面句子也预测成负面,而且概率输出混乱。
原因:训练和预测两个阶段的分词方式不一致。比如训练时用jieba.lcut并且过滤了停用词,预测时直接text.split()按空格切分原始文本,或者用了不同的自定义词典。
解决:把预处理逻辑单独封装成公共函数,放在utils.py里,训练和评估都通过import utils调用,确保词表构建、切词、停用词过滤、padding这四个环节完全共用一份代码。项目里的preprocess_line就是干这个的,不要重建一套。
5.4 模型始终偏向预测某类
现象:验证集准确率在50%左右徘徊,或者所有样本都预测为同一类。
原因:正负样本比例失衡,或标签构建反了。neg.txt被打成0、pos.txt被打成1,如果代码里不小心把neg_samples和pos_samples的标签对调,模型也能收敛,但方向和真实语义完全反了。另一个常见原因是样本量太少,模型还没学好特征就到epoch上限了。
解决:先用分组统计确认验证集里两个类别的分布,再看训练日志里val_accuracy第一个epoch的初始值。如果初始值接近50%,说明模型在随机猜,数据或标签有问题;如果第一个epoch就冲到90%以上,大概率是数据泄露——训练集和验证集有重复样本。
6. 进阶:把情感分析模型用在自己的数据集上
这个项目自带的数据量不算大,如果你有自己的语料,核心改造只有三个地方:数据格式、词表、Sequence长度。数据格式上,把自己的文本整理成neg.txt和pos.txt同样的结构,改文件路径即可。词表方面,训练脚本会读取全量数据自动构建,不需要你手动维护。真正需要动脑子的是序列长度——如果业务场景中评论动辄几百字,建议用直方图先看一下长度分布,把sequence_length设成能覆盖80%样本的值,而不是盲目用50。
# 分析语料长度分布的实用脚本 lengths = [len(clean_text(line).split()) for line in all_lines] p80 = sorted(lengths)[int(len(lengths) * 0.8)] print(f"P80长度: {p80}")数据规模扩大后,建议把Embedding替换成预训练词向量。拿这个项目练手时,最值得深挖的是三个方向:一是给不同领域的数据做迁移学习,用预训练词向量初始化Embedding层,再在小数据集上微调;二是调num_filters和卷积核宽度组合,看看不同n-gram范围对短文本和长文本的效果差异;三是把二分类扩成三分类或五分类,比如“正面、负面、中性”,只需要改标签类别数和输出层的单元数,其余结构完全不动。
我在验证模型是否真的可用时,有一个强迫自己的习惯:每训练完一轮,就挑两条训练集里完全没有过的新评论去预测,一条明显正面、一条明显负面,先确认方向对了再跑批量评估。这个习惯帮我挡掉过好几次标签映射反了的低级错误,也让每个阶段的模型状态心里有数。做情感分析,数据、分词、模型结构、参数配置,每一环都扎实了,结果才可信。希望这篇拆解帮你把这个项目真正吃透,少走几段弯路。
本文还有配套的精品资源,点击获取