尝试应用LSTM和B-LSTM改进RNN的情感分析模型
实验简介
首先,进行实验的数据集简单介绍,本次试验数据集是Keras上的标准IMDB数据集,包括50000条偏向明确的影视评论,数据集本身非常适合进行情感分析,适合用来对新的评论进行偏向预测。本次实验首先对原始数据及进行拼接,并根据实验设计使用部分数据作为训练集和测试集,数据集中包含label标签分别代表积极的评价和消极的评价,该数据集的格式类型有很多,有官方Keras提供的.ngz格式数据集,同时也有Kaggle提供的csv格式的数据集,但本次实验使用的是官方斯坦福大学提供的最原始的文件拼接式的数据集,在代码中专门定义了一个方法用于获取数据集的压缩格式,并对文件进行解压和清洗,因而特征提取过程比较复杂,在下文会给出。
其次,实验的基础model是建立在改进的6层RNN循环神经网络模型上的,在原始RNN基础上添加了1层Embedding用于初始化和2层dropout用于防止过拟合,同时最后用两个全连接层来控制输出维度和归一化。随后,同时测试了使用LSTM和支持正向反向计算的LSTM来进行model改进并使用原始Simple_RNN的数据进行参照,对比给出模型最优解的大致最优拟合迭代范围。
本次试验和之前的最大不同在于,第一次使用了实验室集群进行迭代试验,所以,一方面结果呈现方面会有些差异(不能像之前一样直接得到Matplotlib绘制的图形,但是可以保存下来训练好的模型去对比预测得到一组观测图形自拟数据图像),另一方面对于数据集的使用也要进行改变(集群目前无法自行下载Keras-datasets,所以在编码和数据清洗的过程中无法基于Tensorflow_V2的方法去进行数据及加载,最好的办法仍然是使用本地导入的数据集)。
在数据呈现方面本实验对比了多组Epoch次数不同的实验结果,以此为依据去对比模型改进的效果,在基本模型的基础上对RNN层进行调整,同时为确保试验结果真实可靠进行控制变量,并严格按照参数表设置对照组。随后在模型确定之后开始对Epoch和Batch_size进行测试,意图获得一个最优模型,通过trainning_loss和val_loss的变化趋势推测出最合理的参数设置,并导出最优模型。
实验实施过程分为3个阶段,第一个阶段是数据集分析阶段,对数据集的格式进行清洗,对需要使用的特征进行提取。第二阶段,对模型进行构建,并对可行的改进方案进行设计,设计对照实验,设计观测。第三阶段,编码测试,使用集群对神经元数量比较庞大的Model进行学习,并观测结果进行比较,并通过结果对试验数据进行总结,同时调整参数实现模型最优化,导出目标模型。
实验环境
软件环境:
Centos OS7操作系统,Pycharm IDE编码环境,Anacodna 3 Interpreter。
硬件环境:
实验室集群单计算节点。
第三方库使用:
- Tensorflow_v1函数库:实验的核心库,是模型构建的基础,也是盛景网络搭建的经典工具。
- re库:这是个很少使用的库,它的作作用是用来创建正则函数区队数据集进行清洗的过程中会使用到它。
- os库:由于数据集的特殊结构,需要使用os方法来判定数据集的路径和构建数据文件列表。
- urllib_request库:用于访问目标地址下载数据集。
- tarfile库:标准的文件解压处理类库,在先前的实验中也曾使用过。
- Keras库:强大的模型类库,提供了大部分以实现并且优化效果良好的model_layers。
实验目标
当前,获取到了一个电影影评的数据集IMDB,其中所包含的影评是带有偏向的,所以很适合用于解决情感分析内容预测问题,可以训练出一个得分较好的模型去对新的评论进行打分判定这是否是一条积极的评论。
本次试验将通过多次对照实验,统计比较优秀的模型参数,并在过程中评价出不同改进方案的优劣,寻找导致实验结果变化的原因,并进行证实。同时对多次试验结果进行整理,给出统计分析结果,并将最终模型输出。
对个人而言,进行本实验的主要目的是学习模型调优的过程,并找出改进模型和评价改进模型的依据,对模型各参数调整的意义进行归纳和记录。
实验过程
首先,数据集分析阶段事实上并不顺利,由于集群不能下载Keras的datasets导致第一次的数据清洗和大部分编码都失败了(第一次使用的是Keras_datasets上的IMDB.npz格式)。而本地的计算机在v2的环境又没有足够的算力去支持模型神经网络的训练过程(曾经在跑到epoch(2/10)时出现溢出错误,算力不足导致)。所以为了能够使用集群的算力,更改了第一次的设计和数据集使用方法,寻找到了同样的数据集(格式不同,由斯坦福大学提供镜像下载)来进行实验。但由于数据集格式发生了变化,所以清洗方式也要随之进行调整。首先在代码中定义一了一个单独的getIMDB.py文件用于数据的下载和解压。随后设计了dataPreformation部分用于数据清洗和文本拼接,接下来详细描述数据清洗过程:
首先定义了一个方法rm_tags用于对文本进行正则判断,将不符合正则式的非常规文本置为空串“”(此处正则式引用来自CSDN上一位老师之前对dataset console分析后的规律所总结)。然后开始定义详细的数据集读取方法readFiles,这里为了方便集群工作,选择相对路径读取,但是为了方便执行代码需要将文本和数字建立起联系,在这个方法中将两种偏向的评论进行分别读取,排序并统计个数,以相同的矩阵规模创建0-1标签来标记‘否定’和‘肯定’两种评论偏向(实验设计的是先读取pos积极评论,再读取neg消极评论,所以先设置[1]*12500再设置[0]*12500)。定义完标签后将所有评论文本按顺序读入缓冲区,方法在最终返回标签和文本集合。至此数据清洗任务结束,开始进行字典和模型的构建过程
使用刚刚定义好的方法readFiles去对数据集进行读取,将返回值分别赋给训练集和测试集的标签和文本变量。构建单词数字映射字典,用于Embedding这里使用到的就是Keras的Tokenizer来进行转化操作了,它在初始化的过程中可以传一个num_words作为参数,但是它的默认值其实是None,也就是会处理所有字词,但是如果设置成一个整数,那么最后返回的是最常见的、出现频率最高的前num_words个字词(可以视作字典长度)。本次试验在这里设置的为4000(作为初始值,随后对照组里有5000的设置)所以说,Tokenizer是一个用于向量化文本,或将文本转换为序列(即单个字词以及对应下标构成的列表,从1算起)的类。被用来进行文本预处理的第一步:分词。随后分次结束后,使用texts_to_sequences方法将将影评的单词映射到数字,经过这一步处理影评文本内容已经被与处理成了词下标构成的序列。并且由上文知单词的下标号是基于它在数据集中出现的频率的号越大,越代表该单词出现的频率高。随后,为了实现的简便,keras只能接受长度相同的序列输入。因此如果目前序列长度参差不齐,这时需要使用pad_sequences()。该函数是将序列转化为经过填充以后的一个长度相同的新序列新序列。本实验选择序列长度为400,对照组为500。(序列长度越长训练时间越长,这个要取决于评论的平均长度来决定)
然后进行模型的设计,模型是总共包含6层的序列深度学习模型,首先是应用刚刚定义好的字典的一个Embedding层,然后接一个Dropout正则化层丢弃50%的神经元降低冗余链接,然后接入一个SimpleRNN层(对照组使用的是LSTM以及带前向后向计算封装器的LSTM)。再接入一个64神经元的全连接层(RELU激活函数),再接入一个Dropout层,最后接入一个归一化层结束。模型采用二元交叉熵作为损失函数,模型优化方法设置为Adam,并且以accuracy精确模式定义模型结果。模型搭建完成后,将模型应用于测试集,计算得分并统计模型迭代过程中的数据,记录epoch过程中loss和val_loss用于模型参数选拔。
最后,将参数设计为成组对照实验来进行,对比并分析测试数据结果,不断改进模型。随后,找到应用LSTM和B-LSTM的区别,记录数据并保存最优模型。接下来先给出来最终模型的代码设计再给出对照实验实施过程和参数设计。
实验代码
本实验设计的代码包含两个主要文件getIMDB.py和exeIMDB.py,第一个方法用于下载和解压数据集。第二个方法用于数据清洗和模型实现。
1.getIMDB.py
库函数引用部分:
# -*- coding: utf-8 -*-
# Time : 2020/9/14 14:32
# Author : JinyuZ1996
# File : getIMDB.py
import urllib.request
import os
import tarfile
具体实现部分:
# 定义下载链接和安装地址用于判断是否已经存在
url = "http://ai.stanford.edu/~amaas/data/sentiment/aclImdb_v1.tar.gz"
# 如果已经下载过则不执行下载
if not os.path.isfile("data/aclImdb_v1.tar.gz"):
result = urllib.request.urlretrieve(url, "data/aclImdb_v1.tar.gz")
# 打印下载过程
print('Processing download:', result)
# 解压数据集(如果已经解压就跳过)exists方法可用于判定解压后的文件夹存在与否
if not os.path.exists("data/aclImdb"):
# 经典的解压库tarfile
tfile = tarfile.open("data/aclImdb_v1.tar.gz", 'r:gz')
# 将东西解压到data文件夹下
tfile.extractall('data/')
2.exeIMDB.py
库函数引用部分:
# -*- coding: utf-8 -*-
# Time : 2020/9/15 14:37
# Author : JinyuZ1996
# File : exeIMDB.py
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Dropout
from tensorflow.keras.layers import Embedding
from tensorflow.keras.preprocessing import sequence
from tensorflow.keras.preprocessing.text import Tokenizer
import tensorflow.keras.layers as layers
import re
import os
函数定义部分:
# 创建正则表达式预处理数据
# re.compile方法将正则表达式的字符串形式编译成正则表达式对象
reExpression = re.compile(r'<[^>]+>')
# 方法定义部分
# 定义正则方法去区分评论数据的所在(行中位置)
# sub方法在一个字符串中替换所有匹配正则表达式的子串,返回替换后的字符串
def regularTransform(text):
return reExpression.sub('', text)
# 读取数据集的方法
def readFiles(filetype):
# 定位并找到数据集
path = "data/aclImdb/"
# 创建缓冲区
file_list = []
# 拼接出积极评价路径(这里存在训练集和测试集)(IMDB中是分积极评价和消极评价的,是有倾向的数据集)
posDataPath = path + filetype + "/pos/"
# 循环读取所有文件到文件列表
for f in os.listdir(posDataPath):
file_list += [posDataPath + f]
# 消极评价读取
negDataPath = path + filetype + "/neg/"
for f in os.listdir(negDataPath):
file_list += [negDataPath + f]
# 将读取的文本长度打印出来,在集群中可以方便定位程序运行阶段(训练集测试集各25000个样本)
print('read', filetype, 'files:', len(file_list))
# 两种标签各包含一半(手动初始化1为pos,0为neg)
labelSets = ([1] * 12500 + [0] * 12500)
# 为所有文本创建缓冲区
all_texts = []
# 用拼接的方法读取所有评论内容
for fi in file_list:
with open(fi, encoding='utf8') as file_input:
# 用自定义的正则式去替换文本中的噪声,同时将所有碎片文本拼合
all_texts += [regularTransform(" ".join(file_input.readlines()))]
# 方法返回的第一个是标签集合,第二个参数是文本集合
return labelSets, all_texts
具体实现部分:
# 使用定义的方法读取训练集和测试集标签的数据
labelTrain, txtTrain = readFiles("train")
labelTest, txtTest = readFiles("test")
# 建立单词和数字映射的字典
token = Tokenizer(num_words=4000)
token.fit_on_texts(txtTrain)
# 将影评的单词映射到数字
text_train_seq = token.texts_to_sequences(txtTrain)
text_test_seq = token.texts_to_sequences(txtTest)
# 让所有影评保持在400个数字如果长度不足的话该方法会补齐长度来满足Keras训练的需求(以数字形式去进行训练)
trainSet = sequence.pad_sequences(text_train_seq, maxlen=400)
testSet = sequence.pad_sequences(text_test_seq, maxlen=400)
# 模型设计开始(定义序列模型)
model = Sequential()
# Embedding层input_dim代表的是字典的长度,output_dim代表的是全连接嵌入的维度
# input_length当输入序列的长度固定时,该值为其长度。如果要在该层后接Flatten层,然后接Dense层,则必须指定该参数,否则Dense层的输出维度无法自动推断。
model.add(Embedding(output_dim=64, input_dim=4000, input_length=400))
# 使用Dropout方法进行正则化丢弃50%的神经元降低冗余链
model.add(Dropout(0.5))
# 加了一个双向LSTM层,LSTM能够在更长的序列中有更好的表现,前面的是一个双向封装器实现LSTM前后向进行计算
model.add(layers.Bidirectional(layers.LSTM(64)))
# 接一个64神经元的全连接层
model.add(Dense(units=64, activation='relu'))
# 使用Dropout方法进行正则化丢弃50%的神经元降低冗余链接
model.add(Dropout(0.5))
# 用sigmoid归一化约束输出为单一输出产生预测结果
model.add(Dense(units=1, activation='sigmoid'))
# 将模型进行参数打印
model.summary()
# 定义模型的损失函数,优化方法,以及精确度
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
# 训练的过程记录会被返回回来可以使用matplotlib进行绘制,但是集群无法使用matplotlib绘制,所以注释掉了后面额展示部分
train_history = model.fit(trainSet, labelTrain, batch_size=400, epochs=7, verbose=1, validation_split=0.2)
scores = model.evaluate(testSet, labelTest, verbose=1)
print(scores[1])
实验对照设计
事实上实验开始进行之后的最主要模型是首先确定优化的选择,本次实验主要从LSTM和Bidirectional_LSTM进行了两种核心层上的修改,因为我们知道LSTM诞生的原因就是为了解决SimpleRNN的长程依赖问题,而引入了长期记忆和短期记忆的概念,以门控机制为核心对原始的RNN进行改良。而双向循环B-LSTM旨在既考虑长期记忆的基础上,同时考虑后续时刻的信息的影响,及同时考虑一个词的上下文,所以它它相当于一个增加了按照时间逆序来传递信息的LSTM。
实验设计(含顺序)和结果如下:
实验开始首先使用Keras提供的SimpleRNN进行实验,设置默认Embedding层字典大小为4000,dropout层默认保留50%神经元,batch_size默认为400,Epoch设置为10(一开始设置一个较大的Epoch为了观测过拟合点)
在通过第一轮实验后,模型严重过拟合,根据集群反馈的迭代结果对实验参数进行调整,将Epoch调整至5(可以看到在Epoch=5后loss下降趋于平稳,但val_loss开始上升,为过拟合发生的临界点)然后,其他参数不变,开始进行第二轮实验。
如Chart_1所示第二轮实验缺确实的解决了过拟合问题,模型正常拟合,我们现在想要得到一个更好的改进型模型(得到更好的测试集评分),首先考虑的策略是改变核心层的SimpleRNN,于是,启动第三组和第四组实验,保证方才第二轮实验过程的所有参数不变,分别测试LSTM和B-LSTM的表达效果,并记录数据。
根据第三组和第四组实验的结果来看,B-LSTM作为核心层有更好的表达,所以接下来控制核心层为B-LSTM不变进行参数调优,找到可能改进参数的方向并记录。当然在此之前,为了确保迭代次数的确达到了最优拟合默认值,在设计实验的过程中多设计了第五组合第六组实验分别测试B-LSTM在Epoch+和Epoch-的表达效果,发现,在其他参数不变的条件下,模型的表达效果均变差(分别发生了过拟合和欠拟合现象)。所以后续实验以Epoch=5为默认值开始进行,第七组实验当中,对batch_size进行了调整,增大到了500,然后通过观察反馈结果发现在测试集上的表达效果并不理想,分析迭代数据发现模型在Epoch4-5时val_loss出现上升而trainning_loss仍在下降过程中,初步推测模型可能在此时已经有过拟合倾向,所以得到第一个结论:batch_size参数在进行扩大时,为保证拟合效果最佳,应相应地降低Epoch迭代次数。
为了测试其他参数对模型的影响,仍然控制迭代次数Epoch=5来进行后续的实验,在第八组实验修改Embedding层的字典长度,增加字典长度到5000,控制其他变量与第四组实验相同。根据反馈分析,结论与第7组类似,如果扩大了字典的长度,则应该相应的降低Epoch迭代次数来保证更好的拟合效果(在此处猜测如果字典缩减了的话,应该需要增大Epoch)。
第九组实验,尝试降低Dropout的强度,保留更多的神经元连接,模型同样产生了过拟合倾向,可选的改进方法现在增加了很多,可以选择降低batch_size(如果过大的batch_size难以负担),可以选择减少字典长度,当然也可以和前两组一样选择降低Epoch次数。
第十组实验,其实是作为补充实验来进行的,刚才仅仅进行了Embedding层字典长度的增加,这次尝试对字典进行缩减,通过观察实验结果,发现在Epoch=5条件下,模型的val_loss处于震荡状态,而loss处于近似线性下降状态,所以可以判定此时模型欠拟合。应该提高迭代Epoch次数。符合之前实验八的结论猜测。
根据第十组反馈的实验数据,可以发现模型Embedding层的字典长度并不是越长越好,虽然增加字典的长度可以相应的降低Epoch次数但是,小而精确的前N个常用词组成的词典能够在IMDB数据集上实现更好的拟合效果(尽管这可能需要花费更大的时间成本)
实验总结
在实验过程中,首先使用最基础的SimpleRNN模型作为核心层进行拟合,得到一个基准,随后根据设计对两种可行的改进方案进行了测试,经过实验测试,在相同测试条件下表达效果最好的模型应该是B-LSTM,但是对于核心层和Model本身而言仍然有很多影响拟合最终效果的参数需要进行测试与调整,为了得到最优的模型,从第四次实验开始,进行了很多次实验的尝试来试图发现参数和最终拟合效果之间的关系。根据初步实验结果,作出如下归纳:
首先,迭代Epoch步数是直接对拟合效果起最直观影响的变量。在随后实验过程中,基本原则是控制Epoch不变的情况下讨论其他参数的变动为模型拟合效果所带来的变化。
其次,在其他可变参数中,batch_size的选择对模型训练速度和拟合的影响也很大,小的batch_size对应着更高的epoch time。也就是说,可以通过增加batch_size来降低epoch迭代次数。
然后,Embedding层的词典容量也是对模型拟合程度影响比较大的一个参数,经过三组对照,可以发现高的字典容量能够加速学习收敛过程,减少epoch次数。但是相对来说,更为精简的字典可以提供更高的测试集得分,相应的是以牺牲收敛速度和训练时间为代价的。
最后,事实上在本次实验过程中,也进行了对dropout层百分比的改动,但是经过测试发现50%的随机抛弃程度依然是很好的防止过拟合的水平,不应该降低其强度。
对于实验宏观来说,通过一些列的调参可以得到了一个近似最优模型,因为事实上还有很多可以搭配的改进方法存在,而且模型的深度并不是很高,是一个相对简单的实验,但是通过设计实验的过程和分析结果的过程,一方面,帮助个人提升了集群使用的熟练度,提高了对拟合变量调参的熟练度,也学会了观察loss去界定拟合好坏的方法,同时累了对模型改进的一些经验。仍有很多不足,今后的研究过程中会不断学习,尽可能予以避免。