基于LSTM的情绪分析
1. 摘要
自然语言处理是当代机器学习一块很重要的分支,而情绪分析也是NLP中的重要研究部分。本文为基于简单的“情绪数据集”,通过词向量模型,LSTM等方法训练神经网络模型,对句子进行情绪上的分类与识别。最终识别准确率可达到90.95%。
关键词:NLP, 文本情感分析,情绪分析,词向量模型,LSTM,神经网络,深度学习。
2. 引言
自然语言处理(NLP)是一种专业分析人类语言的人工智能。它接收自然语言,
转译自然语言,最后分析自然语言并输出结果。文本情感分析(Sentiment Analysis)是指利用自然语言处理和文本挖掘技术,对带有情感色彩的主观性文本进行分析、处理和抽取的过程。
深度学习技术发展到今天,在自然语言处理领域有很多的应用。而由于人类语言的多样性、多意性,使得NLP的难度成倍增加。例如由相同的三个字形成的组合“不怕辣”、“辣不怕”、“怕不辣”、“怕辣不”表达了不同的含义。因此,对于自然语言的处理仍具有挑战性与发展空间。本文旨在分析自然语言语句(英语为例),来对其表达的情绪进行归类于分析。
3. 主要研究技术与方法
3.1 词向量模型
人类想要读懂一句话往往非常容易,然而对于机器来讲却是一个需要不断学习的漫长而复杂的过程。想要让机器读懂一句话,我们必须将自然语言转化成计算机可识别的数字。文本向量化表示就是用数值向量来表示文本的语义。我们人类在读一段文本后立刻就能明白它要表达的内容,如何让机器也能拥有这样的能力呢?
文本分类领域使用了信息检索领域的词袋模型,词袋模型在部分保留文本语义的前提下对文本进行向量化表示。通过词袋模型(bag of words)对句子进行分词并向量化,形成D维向量,再输入网络中计算:
- One-Hot:
One-Hot表示法的数值计算规则为:词语序列中出现的词语其数值为1,词语序列中未出现的词语其数值为0。对应关系如下图所示:
- Word2Vec:
这个模型根据上下文的语境来推断出每个词的词向量。如果两个词在上下文的语境中,可以被互相替换,那么这两个词的距离就非常近。在自然语言中,上下文的语境对分析词语的意义是非常重要的。所以,这个模型的作用就是从一大堆句子(以Wikipedia为例)中为每个独一无二的单词进行建模,并且输出一个唯一的向量。Word2Vec模型的输出被称为一个嵌入矩阵。
- Tokenizer:
Tokenizer 是keras中一个用于向量化文本,或将文本转换为序列的类。计算机在处理语言文字时,是无法理解文字的含义,通常会把一个词(中文单个字或者词组认为是一个词)转化为一个正整数,于是一个文本就变成了一个序列。Tokenizer 的核心任务就是做这个事情。本文将单词向量化用的方法就是此方法。
3.2 循环神经网络RNN(Recurrent Neural Networks)
在得到了神经网络的输入数据——词向量后,我们需要确定将要构建的神经网络。NLP数据的一个独特之处是它是时间序列数据。每个单词的出现都依赖于它的前一个单词和后一个单词。由于这种依赖的存在,我们使用循环神经网络来处理这种时间序列数据。
循环神经网络的结构和你之前看到的那些前馈神经网络的结构可能有一些不一样。前馈神经网络由三部分组成,输入层、隐藏层和输出层。
前馈神经网络和RNN之前的主要区别就是RNN考虑了时间的信息。在RNN中,句子中的每个单词都被考虑上了时间步骤。实际上,时间步长的数量将等于最大序列长度。
与每个时间步骤相关联的中间状态也被作为一个新的组件,称为隐藏状态向量h(t)。从抽象的角度来看,这个向量是用来封装和汇总前面时间步骤中所看到的所有信息。就像x(t)表示一个向量,它封装了一个特定单词的所有信息。
隐藏状态是当前单词向量和前一步的隐藏状态向量的函数。并且这两项之和需要通过激活函数来进行激活。
然而,RNN存在一个致命的缺点——即梯度消失的问题,很难处理长序列的数据。为了解决此问题,我们又引入了本文所用到的方法:LSTM。
3.3 长时间的短期记忆模型LSTM(Long Short-Term Memory)
为了解决RNN存在问题,后续人们对RNN做了改进,得到了RNN的特例LSTM,它可以避免常规RNN的梯度消失,因此在工业界得到了广泛的应用。LSTM模型是RNN的变体,它能够学习长期依赖,允许信息长期存在。
举个例子来讲:比如人们读文章的时候,人们会根据已经阅读过的内容来对后面的内容进行理解,不会把之前的东西都丢掉从头进行思考,对内容的理解是贯穿的。
传统的神经网络即RNN做不到这一点,LSTM是具有循环的网络,解决了信息无法长期存在的问题,在工业界普遍使用有良好的效果。
-
RNN与LSTM之间的联系:
RNN具有如下的结构,每个序列索引位置t都有一个隐藏状态h(t):
如果略去每层都有的o(t),L(t),y(t),则RNN的模型可以简化成如下图的形式:
可以看出h(t)由x(t)和h(t−1)得到。得到h(t)后一方面用于当前层的模型损失计算,另一方面用于计算下一层的h(t+1)。
为了避免RNN的梯度消失,LSTM将tanh激活函数转为更为复杂的结构。LSTM的结构如下图:
粉红色圆圈表示点向运算,如向量加法、点乘,而黄色框是学习神经网络层。 线的合并表示连接,而线的交叉表示其内容正在复制,副本将转到不同的位置。
-
LSTM的工作原理:
对于一个典型的RNN网络,隐藏状态向量对于第二句的存储信息量可能比第一句的信息量会大很多。但是LSTM,基本上就会判断哪些信息是有用的,哪些是没用的,并且把有用的信息在LSTM中进行保存。LSTM的单元根据输入数据x(t),隐藏层输出h(t)。在这些单元中,h(t)的表达形式比经典的RNN网络会复杂很多。这些复杂组件分为四个部分:输入门、输出门、遗忘门和一个记忆控制器。
每个门都将x(t)和h(t-1)作为输入,并且利用这些输入来计算一些中间状态。每个中间状态都会被送入不同的管道,并且这些信息最终会汇集到h(t)。这些门可以被认为是不同的模块,各有不同的功能。Ct是控制参数,控制什么样的值保留,什么样的值舍弃,输入门决定在每个输入上施加多少强调,遗忘门决定我们将丢弃什么信息,输出门根据中间状态来决定最终的h(t)。
简要来说,LSTM 单元能够学习到识别重要输入(输入门作用),存储进长时状态,并保存必要的时间(遗忘门功能),并学会提取当前输出所需要的记忆。这也解释了 LSTM 单元能够在提取长时序列,长文本,录音等数据中的长期模式的惊人成功的原因。
4. 神经网络的搭建与分析
4.1数据集描述及导入
本次的数据集来自Kaggel上专门为情感分析提供的数据集。地址 https://www.kaggle.com/praveengovi/emotions-dataset-for-nlp
该数据集包含了三个文档
- 16000行的训练数据集:train.txt
- 2000行的测试数据集:test.txt
- 2000行的验证数据集:val.txt
通过这三个文档我们将建立机器学习模型。以下是train.txt的部分数据:
Eg: i didnt fell humiliated;sadness,这条数据中包含了:1. 句子的具体内容: i didnt fell humiliated; 2. 事先人为分类号的情绪标签: sadness。
导入并展示:
# 导入数据集 train = pd.read_csv('E:/大三下/机器学习/NLP情感分析/train.txt', sep=';', header=None) # 重命名Dataframe列名 train.columns=['Sentence', 'Sentiment'] # 统计数据集中情感种类以及出现次数 train['Sentiment'].value_counts() joy 5362 sadness 4666 anger 2159 fear 1937 love 1304 surprise 572 Name: Sentiment, dtype: int64 train| i didnt feel humiliated | sadness |
| i can go from feeling so hopeless to so damned... | sadness |
| im grabbing a minute to post i feel greedy wrong | anger |
| i am ever feeling nostalgic about the fireplac... | love |
| i am feeling grouchy | anger |
| ... | ... |
| i just had a very brief time in the beanbag an... | sadness |
| i am now turning and i feel pathetic that i am... | sadness |
| i feel strong and good overall | joy |
| i feel like this was such a rude comment and i... | anger |
| i know a lot but i feel so stupid because i ca... | sadness |
16000 rows × 2 columns
4.2 数据预处理:数据清洗及向量化
在拿到一份数据集时,首先要做的事就是对数据集进行预处理,以便得到可用于模型训练的数据集。
在清洗完成过后,我们进行词的向量化:
作为该领域的一个最大玩家,Google已经帮我们在大规模数据集上训练出来了Word2Vec模型,包括1000亿个不同的词。在这个模型中,谷歌能创建300万个词向量,每个向量维度为300。在理想情况下,我们将使用这些向量来构建模型,但是因为这个单词向量矩阵想当大(3.6G),跑起来十分缓慢,同时自己训练Word2Vec的效果也不是最佳,因此本文采用更为轻量化的keras提供的Tokenizer分词器。我定义最大特征的数量为2000,并使用Tokenizer向量化和将文本转换为序列,以便网络可以处理它作为输入。
max_fatures = 2000 tokenizer = Tokenizer(num_words = max_fatures, split=' ') tokenizer.fit_on_texts(train['Sentence'].values) X = tokenizer.texts_to_sequences(train['Sentence'].values) X = pad_sequences(X)4.3 LSTM神经网络的搭建
超参数调优: 选择合适的超参数来训练你的神经网络是至关重要的。训练损失值与你选择的优化器、学习率和网络架构都有很大的关系。特别是在RNN和LSTM中,单元数量和词向量的大小都是重要因素。embed_dim、lstm_out、batch_size、droupout_x变量都是超参数,它们的值在某种程度上是直观的,正确地调整使用它们才能获得良好的结果。文中没有设置的参数按照默认参数配置。
- max_fatures: 词汇表大小。这里设置为2000;
- embed_dim: 词向量的维度。这里设置为128;
- input_length: 输入序列的长度,当它是固定的时。如果你需要连接Flatten和Dense层,则这个参数是必须的。这里设置为61;
- SpatialDropout1D: 一种dropout方法。随机地将部分区域置零。这里设置为0.4;
- lstm_out: LSTM的输出维度。这里设置为196;
- dropout: 使多少比重的神经元输出(unit的输出)激活失效,默认为0。这里设置为0.2;
- recurrent_dropout: recurrent_dropout是给递归状态 C 设置的Dropout参数。这里设置为0.2;
- Dense: 全连接层,最后输出为6。并且最后一层的激活函数选用 softmax;
- loss: 损失函数。这里选用交叉熵损失函数。交叉熵损失函数经常用于分类问题中,它能衡量同一个随机变量中的两个不同概率分布的差异程度,在机器学习中就表示为真实概率分布与预测概率分布之间的差异。交叉熵的值越小,模型预测效果就越好;
- optimizer: 优化器。这里选用Adam。它结合AdaGrad和RMSProp两种优化算法的优点。对梯度的一阶矩估计和二阶矩估计进行综合考虑,计算出更新步长;
- metrics: 定义评价函数。这里选用accuracy;
查看神经网络情况:
print(model.summary()) _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= embedding_1 (Embedding) (None, 61, 128) 256000 _________________________________________________________________ spatial_dropout1d_1 (Spatial (None, 61, 128) 0 _________________________________________________________________ lstm_1 (LSTM) (None, 196) 254800 _________________________________________________________________ dense_1 (Dense) (None, 6) 1182 ================================================================= Total params: 511,982 Trainable params: 511,982 Non-trainable params: 0 _______________________________________________________________ None分离训练集,测试集:
Y = pd.get_dummies(train['SentiID']).values X_train, X_test, Y_train, Y_test = train_test_split(X,Y, test_size = 0.33, random_state = 42) print(X_train.shape,Y_train.shape) print(X_test.shape,Y_test.shape) (10720, 61) (10720, 6) (5280, 61) (5280, 6)训练网络:
batch_size = 32 history = model.fit(X_train, Y_train, epochs = 20, batch_size = batch_size, verbose = 1, validation_split = 0.2) print(model.evaluate(X_test, Y_test)) Instructions for updating: Use tf.cast instead. Train on 8576 samples, validate on 2144 samples Epoch 1/20 8576/8576 [==============================] - 24s 3ms/step - loss: 1.5044 - acc: 0.3981 - val_loss: 1.1840 - val_acc: 0.5648 Epoch 2/20 8576/8576 [==============================] - 24s 3ms/step - loss: 0.7453 - acc: 0.7556 - val_loss: 0.5421 - val_acc: 0.8321 Epoch 3/20 8576/8576 [==============================] - 25s 3ms/step - loss: 0.3271 - acc: 0.8935 - val_loss: 0.3255 - val_acc: 0.8839 Epoch 4/20 8576/8576 [==============================] - 26s 3ms/step - loss: 0.2076 - acc: 0.9254 - val_loss: 0.2561 - val_acc: 0.9072 Epoch 5/20 8576/8576 [==============================] - 25s 3ms/step - loss: 0.1603 - acc: 0.9405 - val_loss: 0.2710 - val_acc: 0.8969 Epoch 6/20 8576/8576 [==============================] - 24s 3ms/step - loss: 0.1316 - acc: 0.9492 - val_loss: 0.2504 - val_acc: 0.9128 Epoch 7/20 8576/8576 [==============================] - 26s 3ms/step - loss: 0.1128 - acc: 0.9574 - val_loss: 0.2466 - val_acc: 0.9104 Epoch 8/20 8576/8576 [==============================] - 27s 3ms/step - loss: 0.1048 - acc: 0.9595 - val_loss: 0.2413 - val_acc: 0.9165 Epoch 9/20 8576/8576 [==============================] - 26s 3ms/step - loss: 0.0837 - acc: 0.9668 - val_loss: 0.2586 - val_acc: 0.9109 Epoch 10/20 8576/8576 [==============================] - 25s 3ms/step - loss: 0.0819 - acc: 0.9684 - val_loss: 0.2777 - val_acc: 0.9095 Epoch 11/20 8576/8576 [==============================] - 24s 3ms/step - loss: 0.0749 - acc: 0.9722 - val_loss: 0.2732 - val_acc: 0.9067 Epoch 12/20 8576/8576 [==============================] - 24s 3ms/step - loss: 0.0700 - acc: 0.9734 - val_loss: 0.2782 - val_acc: 0.9137 Epoch 13/20 8576/8576 [==============================] - 25s 3ms/step - loss: 0.0567 - acc: 0.9788 - val_loss: 0.2935 - val_acc: 0.9062 Epoch 14/20 8576/8576 [==============================] - 27s 3ms/step - loss: 0.0596 - acc: 0.9782 - val_loss: 0.2943 - val_acc: 0.9090 Epoch 15/20 8576/8576 [==============================] - 26s 3ms/step - loss: 0.0548 - acc: 0.9788 - val_loss: 0.3360 - val_acc: 0.9086 Epoch 16/20 8576/8576 [==============================] - 24s 3ms/step - loss: 0.0454 - acc: 0.9836 - val_loss: 0.3751 - val_acc: 0.9016 Epoch 17/20 8576/8576 [==============================] - 27s 3ms/step - loss: 0.0519 - acc: 0.9823 - val_loss: 0.3499 - val_acc: 0.9072 Epoch 18/20 8576/8576 [==============================] - 25s 3ms/step - loss: 0.0381 - acc: 0.9869 - val_loss: 0.3477 - val_acc: 0.9114 Epoch 19/20 8576/8576 [==============================] - 26s 3ms/step - loss: 0.0391 - acc: 0.9865 - val_loss: 0.3552 - val_acc: 0.9062 Epoch 20/20 8576/8576 [==============================] - 26s 3ms/step - loss: 0.0454 - acc: 0.9848 - val_loss: 0.3569 - val_acc: 0.9058 5280/5280 [==============================] - 4s 695us/step [0.3139003471513702, 0.9094696969696969]由结果可看出,最终模型准确率达到90.95%。
最终画出模型准确率以及损失随迭代次数的变化图像:
# 准确率的变化 plt.plot(history.history['acc'], color = 'orange') plt.title("Train_history") plt.ylabel('accuracy') plt.xlabel('Epoch') plt.show() # 训练中的损失图像 plt.plot(history.history['loss']) plt.title("Train_history") plt.ylabel('loss') plt.xlabel('Epoch') plt.show()5.模型测试
# "joy": 0, "sadness": 1, "anger": 2, "fear": 3, "love": 4, "surprise": 5 txt = ['The sentence you are going to predict'] txt = tokenizer.texts_to_sequences(txt) txt = pad_sequences(txt, maxlen=61, dtype='int32', value=0) sentiment = model.predict(txt, batch_size=1, verbose = 1)[0] if(np.argmax(sentiment) == 0):print("情绪为:joy") elif (np.argmax(sentiment) == 1):print("情绪为:sadness") elif (np.argmax(sentiment) == 2):print("情绪为:anger") elif (np.argmax(sentiment) == 3):print("情绪为:fear") elif (np.argmax(sentiment) == 4):print("情绪为:love") elif (np.argmax(sentiment) == 5):print("情绪为:surprise")i had been talking to coach claudia barcomb and coach ali boe for a long time and they both made me feel very welcomed at union,预分类为joy:
txt = ['i had been talking to coach claudia barcomb and coach ali boe for a long time and they both made me feel very welcomed at union'] 1/1 [==============================] - 0s 7ms/step情绪为:joy预测成功;
im feeling rather rotten so im not very ambitious right now,预分类为sadness:
txt = ['im feeling rather rotten so im not very ambitious right now'] 1/1 [==============================] - 0s 8ms/step情绪为:sadness预测成功;
i jest i feel grumpy tired and pre menstrual which i probably am but then again its only been a week and im about as fit as a walrus on vacation for the summer,预分类为anger:
txt = ['i jest i feel grumpy tired and pre menstrual which i probably am but then again its only been a week and im about as fit as a walrus on vacation for the summer'] 1/1 [==============================] - 0s 7ms/step情绪为:anger预测成功;
i cant walk into a shop anywhere where i do not feel uncomfortable,预分类为fear:
txt = ['i cant walk into a shop anywhere where i do not feel uncomfortable'] 1/1 [==============================] - 0s 8ms/step情绪为:fear预测成功;
i were to go overseas or cross the border then i become a foreigner and will feel that way but never in my beloved land,预分类为love:
txt = ['i were to go overseas or cross the border then i become a foreigner and will feel that way but never in my beloved land'] 1/1 [==============================] - 0s 7ms/step情绪为:love预测成功;
i am right handed however i play billiards left handed naturally so me trying to play right handed feels weird,预分类为surprise:
txt = ['i am right handed however i play billiards left handed naturally so me trying to play right handed feels weird'] 1/1 [==============================] - 0s 8ms/step情绪为:surprise预测成功。
总结
以上是生活随笔为你收集整理的基于LSTM的情绪分析的全部内容,希望文章能够帮你解决所遇到的问题。
- 上一篇: Premiere Pro入门
- 下一篇: 阿里云SLB实现负载均衡