【注】:本文所述的实验的完整实现代码包括数据集的仓库链接会在文末给出(建议读者自行配置GPU来加速TensorFlow的相关模型,运行起来会快非常多)
目录
一、研究的背景和目的
二、文本数据集描述
1、数据集来源以及使用目的
2、数据规模、以及如何划分数据集
3、数据集的组成和类型
4、文本处理方式
5、数据集使用注意
三、实验方法介绍
1、实验环境及编程语言选择
2、实验方法
3、实验步骤
(1) 数据预处理
(2) CNN模型构建
(3) 模型编译和训练验证
四、实验结果分析
1、模型的准确性分析
2、模型评估指标分析
五、实验结论
一、研究的背景和目的
中文文本情感分析是一种研究人类情感表达的计算机技术,它可以自动地分析文本中的情感,并将其分类为积极、消极或中性。随着互联网的普及和社交媒体的兴起,人们在网上表达情感的方式变得越来越多样化和频繁化,这使得情感分析技术变得更加重要。
中文文本情感分析的研究背景可以追溯到计算机语言学和自然语言处理领域的起源。近年来,随着深度学习技术的发展,基于深度学习的情感分析算法取得了显著的进展,成为了研究热点。
中文文本情感分析的目的是提高计算机对人类情感表达的理解能力,帮助人们更好地了解和分析社会情感动态,为商业决策、舆情分析、个性化推荐等领域提供技术支持。同时,它也有助于为语义理解和自然语言生成等研究提供基础和应用场景。
二、文本数据集描述
1、数据集来源以及使用目的
本实验所使用的数据集weibo_sentiment.csv来自于GitHub上的开源数据集,可以用于机器学习中的中文文本情感分析的练习任务。
2、数据规模、以及如何划分数据集
weibo_sentiment.csv 数据集中的数据规模即共有119988条中文文本,基本正面和负面的评论都差不多,所以最终的结果也比较稳定;本实验使用的时K折交叉验证来训练模型,数据集随机按照9:1的比例来划分,并循环训练十次。
3、数据集的组成和类型
数据集主要是来自微博上的各种评论,包括各种各样不同类别的评论内容,每行文本的组成是情感标签和评论的具体内容。标签和内容的样式示例如下所示:
情感标签与情感类别的对应
情感标签 | 情感类别 |
0 | 负向评论 |
1 | 正向评论 |
4、文本处理方式
本实验所使用的数据集已经经过了初步的预处理(几乎标准的csv格式),形成了每行文本以“情感标签,评论内容”这样每个属性以“,”隔开的格式。本实验将会把这批数据的文本内容部分先进行分词并去除停用词,然后使用,使用Keras的Tokenizer方法将文本中的每个单词映射为一个数字编号,并将文本转换为数字列表形式的词向量,以便于后续的处理;由于每个文本的长度不一,为了能够将它们输入到相同形状的模型中,这里我们需要对词向量进行补齐操作,将每个词向量的长度填充为相同的值,本实验使用Keras的pad_sequences方法进行补齐操作,并将词向量的最大长度设置为280(因为根据实验前的统计,几乎每条文本的长度都小于280),本实验在补齐序列的时候是令truncating='post'、以及padding默认为pre,即主要保留后面部分,就是当长度超过280时截断前面多余的部分,长度不足280时,在前面部分补0。
# 读取我们本次实验的文本数据
df = pd.read_csv('../Emotional_text/weibo_sentiment.csv')# 分词并去除停用词
stop_words = load_stopwords()
df['review'] = df['review'].apply(lambda x: ' '.join(word for word in jieba.cut(x) if word not in stop_words))# 使用Keras的Tokenizer来转化词语为词向量,这里我们选择出现频率前25000个词作为词袋
tokenizer = Tokenizer(num_words=25000, oov_token='<OOV>')
tokenizer.fit_on_texts(df['review'])
seq = tokenizer.texts_to_sequences(df['review'])# 补齐每个样本序列,使其长度一样来方便后续的计算
max_len = 280
seq = pad_sequences(seq, maxlen=max_len, truncating='post')
5、数据集使用注意
本实验所使用的数据集来自网上开源的语料库,请不要用于商业用途欢迎自主学习使用。
三、实验方法介绍
1、实验环境及编程语言选择
实验环境:
操作系统: Windows10/Windows11
软件包管理部署工具: Anaconda3
Python语言版本: Python3.10.9
2、实验方法
本实验主要使用的是卷积神经网络(CNN)来进行中文文本的情感分析,其中主要的模型算法来自Keras以及Tensorflow,本实验的模型中先后添加了词嵌入层、卷积层、池化层、全连接层、和输出层,具体的原理和实现步骤方法将在下面的实验步骤中给出 。
3、实验步骤
(1) 数据预处理
数据预处理大部分的步骤在“二、文本数据集描述”一节中已经具体的阐述过了,这里就简单的说一下后续的处理,在将每一句的词向量补齐之后就开始了划分数据集,本实验使用K折交叉验证,其中本实验延续上一个实验继续将K值设为10,即把数据分成十份,训练集九份、验证集一份,然后每一份都做一次验证集来训练,这样可以是模型更加的具有鲁棒性和泛化性。
(2) CNN模型构建
在数据预处理之后,就需要开始构建一个基本符合本实验的CNN的算法模型了(包括词嵌入层、卷积层、池化层和全连接层等,其中激活函数使用ReLU,损失函数使用交叉熵,优化器使用Adam)。首先使用Keras的Sequential()方法定义一个可以按照层次结构依次添加神经网络层的序贯模型(这个模型容器在神经网络学习中非常的方便),然后添加一个词嵌入层 (Embedding Layer),将每个单词的整数编码映射为一个密集向量表示(本来此处是应该使用预训练的词嵌入矩阵的,它们是基于大型语料库训练的,能够更好表达单词之间的语义关系,但是由于被百度网盘限速下载数据集实在太慢,就放弃了),在这一层,我们设置了词汇表的大小为 25000(本来有十七万多的词袋,但是太大了实在是跑不动,截取前两万五的词做个代表,之所以不用TDF-IDF来根据重要程度排序是因为在情感分析中有些出现频率高的词可能对文本的情感影响更大,所以就直接默认按照出现频率降序来排),每个单词的嵌入维度为 150(根据ChatGPT的建议,从我们的数据集的大小出发,选了几个100到300维度的来试过几次),输入序列的最大长度为 280(在数据预处理时已经说过了),这里的结果是直接基于Embedding层的随机初始化的矩阵得出的,直接根据对应的整数ID编号输出(由于权重矩阵初始化的时候是随机生成的,所以不能很好的把不同词之间的不同关系表现出来,不过不用担心,后面会使用反向传播算法来更新这些权重矩阵)。
然后就是添加一维卷积层,在这里本实验设置了128个卷积核,每个卷积核的大小是5(一开始设置少了,最后跑出来的效果很不理想,这里每个单个的卷积核中每个元素(就是大小是5就有5个元素)的维度在一开始是会自适应词向量的维度的,所以不用担心维度不会匹配的情况,当然如果是自己去单独实现一个卷积的操作可能就需要自己去规定维度要相同了,这里使用的是现成的Keras中的一维卷积),这相当于对每个样本从前五个词向量开始,与每个卷积核做点乘,然后在一步一步向后滑动,正常来说对每个样本输入来说这里会输出一个由128个276维向量的组合特征图,本实验使用的激活函数为ReLU函数。激活函数是用来对神经元的输出进行非线性变换的,即输入大于零时就输出它本身,小于零时就输出零,可以防止过拟合以及提高运算速度,又可以防止梯度消失。
接下来就是利用池化层对卷积之后的结果进行一个降维操作,这里我们使用的是最大池化操作,使用的是Keras的GlobalMaxPooling1D()来做最大池化操作,其原理是把卷积层输出的特征图的每个通道的最大值选取出来组成一个新的特征向量,从而达到降维的效果(这里之所以不用平均池化,是应为最大池化会保留序列里面最显著的特征,对情感分析来说这种最显著的特征对文本的情感通常是由较大的影响的,而平均池化类似于一种平滑,会丢失这种显著的特征所以在这里我们选择使用最大池化操作),这个新的特征向量就是这个样本最显著的特征,就将作为池化层的输出进入到下一层。
接下来就是全连接层,就是把全连接层所有的神经元都和池化层的输出也就是全连接层的输入相连,如果全连接层的输入不是一维的特征向量,那么该特征向量将会被压缩成一维向量再参与运算,如果是自己单独实现这一层的话,而池化层输出的又不是一维的特征向量的话这里就需要自己去实现向量维度的压缩(池化层使用平均池化的话可能就会输出多维的特征向量),这里的全连接运算过程其实也就是使用它每个神经元的权重矩阵的转置(这是为了确保连个矩阵满足矩阵的乘积要求)和输入的特征向量做一个简易乘积运算。
在之后的Dropout层是一个作用于它前面的全连接层的防止过拟合的技术,它可以使前面一层的神经元随机失活(注意这里不是真正意义上的失活,而是会以一定概率(这个概率是我们自己设置的)把前面一层的神经元的输出置零(所以并不会影响输出的样本的特征向量的长度),以此来防止过拟合,使模型不会过度的依赖某些特定的神经元,让模型的鲁棒性更强)。
最后的就是整个卷积神经网络的输出层,这个输出层我们使用的是一个全连接层来实现,由于做的只是二分类任务,所以我们设置两个神经元,并使用softmax函数作为激活函数来使特征向量转化为概率密度,再从两个概率中输出最大概率的那个分类(神经网络在训练的时候会保证输出的概率分类的顺序和验证的标签分类顺序一致),这两个神经元的长度还是和特征向量的长度自适应,而权重矩阵一开始也是随机生成的对全连接层来说在Keras中默认的是使用Glorot均匀分布(也称为Xavier均匀分布)进行初始化,偏置向量默认初始化为0,主要就是根据神经元的输入和输出个数来进行随机数范围的限制(这里就不多做阐述了)。到这里模型的神经元层基本就算是构建完成了。
# 建立一个神经网络模型,一层层添加神经元层model = Sequential()model.add(Embedding(25000, 150, input_length=max_len))model.add(Conv1D(128, 5, activation='relu'))model.add(GlobalMaxPooling1D())model.add(Dense(128, activation='relu'))model.add(Dropout(0.5))model.add(Dense(2, activation='softmax'))
(3) 模型编译和训练验证
在构建完一个模型之后,我们就需要对这个模型进行编译来指定优化器(优化器就是用于更新权重矩阵和偏置的、除此之外还有学习率等)、损失函数、和评估指标等,本实验使用的是Adam模型优化器(主要是方便又可以在训练的过程中自适应学习率),而我们所使用的损失函数是categorical_crossentropy(多类别交叉熵损失函数),它能够直接衡量模型预测的概率分布和真实标签的概率分布的之间的差异,经常和softmax一起使用。最后在编译阶段再设置一个评估指标,这里我们使用的是accuracy(准确度)作为评估指标。
再经过模型编译之后就是对模型进行训练验证了,但是在这里我们为了更好的防止训练过程中出现过拟合现象,还需要设置两个Keras回调函数EarlyStopping(本实验中将将它的检测目标定为验证集上损失,其原理是当验证集上的损失不再下降的时候就提前终止训练防止过拟合,其中patience=3的意思是连续三个epoch的损失不再下降就结束训练) 和 ReduceLROnPlateau(它用于在验证集损失不再下降时降低学习率,monitor='val_loss' 表示监控验证集损失,factor=0.2 表示每次降低学习率时将其乘以 0.2,patience=2 表示当验证集损失在连续 2 个 epoch 内都没有下降时,降低学习率,min_lr=0.0001 表示学习率的最小值为 0.0001)来动态调整训练中的学习率和检测训练过程提前终止训练避免过拟合的现象出现。
在之后我们将进行模型的训练及验证直接使用Keras的fit方法对模型进行训练。train_seq和train_labels是训练数据和标签,batch_size=128表示批大小为128,epochs=20表示训练20个epoch,validation_data所设置的test_seq和test_labels表示使用 test_seq和test_labels作为验证数据,callbacks=[early_stop, reduce_lr]表示在训练过程中使用early_stop和reduce_lr两个回调函数,verbose=0表示不输出训练日志。在训练过程中,模型会在每个 epoch结束后计算在验证数据上的损失和准确度,并根据这些信息调整学习率或提前终止训练。训练完成后,返回一个History对象,其中包含了训练过程中的损失和评估指标等信息。最后在输出一下模型的评估结果就算是整体的实验步骤结束了。
最后说明一下,本实验使用了十折交叉验证加多批次训练,就是说所有的训练数据被分成十份分别以以9:1的比例来进行训练和验证,总共进行十次,而每次训练又将训练多个epoch(一般都会不到20个就会收敛),每个epoch又会被分成多个批次(每个批次的大小为128)分批进行训练,这样能够加快训练速度并提高模型性能,通过在每个epoch中分批处理数据,可以减少内存占用并利用矩阵运算加速计算。
# 编译刚刚建立的卷积神经网络模型model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])# 设置 EarlyStopping 和 ReduceLROnPlateau 来避免过拟合early_stop = EarlyStopping(monitor='val_loss', patience=3)reduce_lr = ReduceLROnPlateau(monitor='val_loss', factor=0.2, patience=2, min_lr=0.0001)# 训练以及验证卷积神经网络模型的效果history = model.fit(train_seq, tf.keras.utils.to_categorical(train_labels), batch_size=128, epochs=20, validation_data=(test_seq, tf.keras.utils.to_categorical(test_labels)), callbacks=[early_stop, reduce_lr], verbose=0)
四、实验结果分析
1、模型的准确性分析
从实验的结果来看,我们一共进行了十次(十折)整体的训练,每一折的训练都会输出一幅accuracy的图和loss图,上面展示了每次训练截至之前的每一个epoch对应的训练集和测试验证集上的准确度以及熵损失。从下面的部分结果图来看,每次训练基本都会在第二个epoch就会收敛了,超过这个epoch模型就出现了过拟合的现象,而在这十次训练中,基本上模型的最终的准确度都稳定在97%,算是一个比较高的准确度了。如下图(部分图例):
2、模型评估指标分析
由模型最终生成的评估结果分析可知本实验所构建的神经网络模型在每一折的训练中的正面和负面的文本的准确率都几乎维持在97%、98%左右,而且召回率和F1-score也差不多维持在这个水准,都是比较好的,而且模型最后的准确度也在97%左右,具体的生成图例如下所示(部分图例):
五、实验结论
使用卷积神经网络来进行文本情感分析可以在训练时进行并行化,也就是可以并行训练和执行;可以经过多层的叠加实现和RNN一样的长序列依赖。但是,使用卷积神经网络进行文本情感分析的模型可解释性不强,在调优模型的时候,很难根据训练的结果去针对性地调整具体的特征。
综上所述,此外如果还需要提升这个模型的性能的话,可以增加神经元的个数,也可以通过增大词袋的大小,也可以调整规定单个词向量的维度,或者使用其他的模型优化器来达到更好的效果。
【注】:深度模型的针对性可能有时候有些强,这和训练的数据集有关,即便有时候防止了模型的过拟合,但是数据集的来源单一,可能最后的泛化性都不会有那么好,对该来源的数据可能预测效果好,但是其它来源就不一定了;就比如说我所使用的是一些微博评论的数据集,导致的结果就是对微博评论这种类型的文本的预测效果好一些,对其他领域的可能就不太理想,它对符合它训练的数据集的那种语法、语言模式的文本的效果比较好,对其他的可能就不是很理想,所以各位读者在自行测试的时候可能就会发现有时候对于你输入的文本,模型给出的结果不太好。
本实验的代码仓库位置及数据集都在仓库的CNN目录下,仓库链接如下:
GitHub - XSJWF/MachineLearning: 一些机器学习分入门算法实现分享
或者下面这Gitee的仓库:
Gitee仓库
本篇文章到此结束,感谢各位读者的阅读!