文本情感分析(Sentiment Analysis)是自然语言处理(NLP)方法中常见的应用,也是一个有趣的基本任务,尤其是以提炼文本情绪内容为目的的分类。它是对带有情感色彩的主观性文本进行分析、处理、归纳和推理的过程。
  本文将介绍情感分析中的情感极性(倾向)分析。所谓情感极性分析,指的是对文本进行褒义、贬义、中性的判断。在大多应用场景下,只分为两类。例如对于“喜爱”和“厌恶”这两个词,就属于不同的情感倾向。
  本文将详细介绍如何使用深度学习模型中的LSTM模型来实现文本的情感分析。

文本介绍及语料分析

  我们以某电商网站中某个商品的评论作为语料(corpus.csv),该数据集的下载网址为:https://github.com/renjunxiang/Text-Classification/blob/master/TextClassification/data/data_single.csv ,该数据集一共有4310条评论数据,文本的情感分为两类:“正面”和“反面”,该数据集的前几行如下:

evaluation,label
用了一段时间,感觉还不错,可以,正面
电视非常好,已经是家里的第二台了。第一天下单,第二天就到本地了,可是物流的人说车坏了,一直催,客服也帮着催,到第三天下午5点才送过来。父母年纪大了,买个大电视画面清晰,趁着耳朵还好使,享受几年。,正面
电视比想象中的大好多,画面也很清晰,系统很智能,更多功能还在摸索中,正面
不错,正面
用了这么多天了,感觉还不错。夏普的牌子还是比较可靠。希望以后比较耐用,现在是考量质量的时候。,正面
物流速度很快,非常棒,今天就看了电视,非常清晰,非常流畅,一次非常完美的购物体验,正面
非常好,客服还特意打电话做回访,正面
物流小哥不错,辛苦了,东西还没用,正面
送货速度快,质量有保障,活动价格挺好的。希望用的久,不出问题。,正面

接着我们需要对语料做一个简单的分析:

  • 数据集中的情感分布;
  • 数据集中的评论句子长度分布。

  使用以下Python脚本,我们可以统计出数据集中的情感分布以及评论句子长度分布。

import pandas as pd
import matplotlib.pyplot as plt
from matplotlib import font_manager
from itertools import accumulate

# 设置matplotlib绘图时的字体
my_font = font_manager.FontProperties(fname="/home/george/Documents/项目/simsun.ttc")

# 统计句子长度及长度出现的频数
df = pd.read_csv('./corpus.csv')
print(df.groupby('label')['label'].count())

df['length'] = df['evaluation'].apply(lambda x: len(x)) # 评价那一列的 评价长度
len_df = df.groupby('length').count()
sent_length =len_df.index.tolist()#把行序号 即各种长度 展开成一个list

sent_freq = len_df['evaluation'].tolist() # 每个长度有多少个
# 绘制句子长度及出现频数统计图

plt.bar(sent_length,sent_freq)#x轴,y轴数值
plt.title("句子长度及出现频数统计图", fontproperties=my_font)#标题
plt.xlabel("句子长度", fontproperties=my_font)#x轴标签
plt.ylabel("句子长度出现的频数", fontproperties=my_font)#y轴标签
plt.savefig("./句子长度及出现频数统计图.png")
plt.close()

# 绘制句子长度累积分布函数(CDF)
sent_pentage_list = [(count/sum(sent_freq)) for count in accumulate(sent_freq)]
#绘制CDF
plt.plot(sent_length,sent_pentage_list)

# 寻找分位点为quantile的句子长度
quantile = 0.91
#print(list(sent_pentage_list))
for length, per in zip(sent_length,sent_pentage_list):
    if round(per, 2) == quantile:
        index = length
        break
print("\n分位点为%s的句子长度:%d." % (quantile, index))

# 绘制句子长度累积分布函数图
plt.plot(sent_length,sent_pentage_list)
plt.hlines(quantile, 0, index, colors="c",linestyles="dashed")#加一条水平的线,指向(quantile, index) 这个点
plt.vlines(index,0,quantile,colors="c",linestyles="dashed")#加一条垂直的线,指向(quantile, index) 这个点
plt.text(0,quantile,str(quantile))
plt.text(index,0,str(index))
plt.title("句子长度累积分布函数图", fontproperties=my_font)
plt.xlabel("句子长度", fontproperties=my_font)
plt.ylabel("句子长度累积频率", fontproperties=my_font)
plt.savefig("./句子长度累积分布函数图.png")
plt.close()

输出的结果如下:

label
正面    1908
负面    2375
Name: label, dtype: int64

分位点为0.91的句子长度:183.

 

可以看到,正反面两类情感的比例差不多。句子长度及出现频数统计图如下:

NLP情感分类模型测评 情感分析lstm_NLP情感分类模型测评

 

句子长度累积分布函数图如下:

NLP情感分类模型测评 情感分析lstm_数据集_02

 

使用LSTM模型

  接着我们使用深度学习中的LSTM模型来对上述数据集做情感分析,笔者实现的模型框架如下:

NLP情感分类模型测评 情感分析lstm_数据集_03

思想就是:

1.数据处理:

首先将 评价(evaluation) 和 正负面(value) 取出来放到list, 然后去重(unique),然后分词放到词典vocabulary中,

然后做出两个词典映射 


#字典列表 #就是给 每个词编号 形成两个映射 word_dictionary 词:编号 inverse_word_dictionary 编号:词 自己编的号 word_dictionary = {word: i + 1 for i, word in enumerate(vocabulary)}


inverse_word_dictionary = {i + 1: word for i, word in enumerate(vocabulary)}


正反面:


output_dictionary = {i : labels for i, labels in enumerate(labels)}


然后根据词汇表,将所有的评价里面的分词变成序号,然后利用padding 补齐成一样的长度.

2 构建


# 创建深度学习模型, Embedding + LSTM + Softmax. # 通过语义库(训练的或者现成的wprd2Vec),利用embedding 实现将字词用高维向量表示,可以训练后得到前后文的意义的向量


Embedding层:将序号[1234,2324,...] 变成20维的向量,然后进行训练,训练出来的 20维向量数值是互相有关系的

然后再训练LSTM,保存模型就行了

 

import pickle
import numpy as np
import pandas as pd
from keras.utils import np_utils, plot_model
from keras.models import Sequential
from keras.preprocessing.sequence import pad_sequences
from keras.layers import LSTM, Dense, Embedding, Dropout
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score

# 导入数据
# 文件的数据中,特征为evaluation, 类别为label.
def load_data(filepath, input_shape = 20):
    df = pd.read_csv(filepath)

    # 标签及词汇表
    labels, vocabulary =  list(df['label'].unique()), list(df['evaluation'].unique())

    # 构造字符级别的特征
    string = ''
    for word in vocabulary:
        string += word # 一句话一句话加进去

    vocabulary = set(string)

    #字典列表
    #就是给 每个词编号 形成两个映射 word_dictionary 词:编号  inverse_word_dictionary 编号:词 自己编的号
    word_dictionary = {word: i + 1 for i, word in enumerate(vocabulary)}
    with open('word_dict.pk','wb') as f:
        pickle.dump(word_dictionary,f)
    inverse_word_dictionary = {i + 1: word for i, word in enumerate(vocabulary)}
    label_dictionary = {label: i for i, label in enumerate(labels)}
    with open('label_dict.pk','wb') as f:
        pickle.dump(label_dictionary, f)
    output_dictionary = {i : labels for i, labels in enumerate(labels)}

    vocab_size = len(word_dictionary.keys())# 词汇表大小
    label_size = len(label_dictionary.keys()) # 标签类别数量

    # 序列填充,按input_shape填充,长度不足的按0补充
    x = [[word_dictionary[word]for word in sent] for sent in df['evaluation']]
    x = pad_sequences(maxlen = input_shape,sequences=x,padding = 'post',value = 0)#
    #为了实现的简便,keras只能接受长度相同的序列输入。因此如果目前序列长度参差不齐,这时需要使用pad_sequences()。该函数是将序列转化为经过填充以后的一个长度相同的新序列新序列。
    y = [[label_dictionary[sent]]for sent in df['label']]#sent 因为是 for迭代过去的,所以需要一个列表(容器)来吸收
    y = [np_utils.to_categorical(label,num_classes = label_size) for label in y]
    y = np.array([list(_[0]) for _ in y])

    return x,y,output_dictionary,vocab_size,label_size,inverse_word_dictionary

# 创建深度学习模型, Embedding + LSTM + Softmax.
# 通过语义库(训练的或者现成的wprd2Vec),利用embedding 实现将字词用高维向量表示,可以得到前后文的关系,进行训练
# 具体看 https://www.jianshu.com/p/158c3f02a15b
def create_LSTM(n_units,input_shape, output_dim,filepath):
    x, y,output_dictionary, vocab_size,label_size,inverse_word_dictionary = load_data(filepath)

    model = Sequential()
    model.add(Embedding(input_dim = vocab_size+1, output_dim= output_dim,
                        input_length = input_shape, mask_zero = True))
# input_dim:这是文本数据中词汇的取值可能数。例如,如果您的数据是整数编码为0-9之间的值,那么词汇的大小就是10个单词;也就是one-hot
#从one-hot变20维的 180是多少个词也就是长度,也就是得到180*20 然后再进行训练
# output_dim:这是嵌入单词的向量空间的大小,这里是20。它为每个单词定义了这个层的输出向量的大小。例如,它可能是32或100甚至更大,可以视为具体问题的超参数;
# input_length:这是输入序列的长度,就像您为Keras模型的任何输入层所定义的一样,也就是一次输入带有的词汇个数。例如,如果您的所有输入文档都由1000个字组成,那么input_length就是1000。
# 
# 如果是 普通神经网络 那么 embedding 后面 需要 flatten 平铺, 但是当数量过大 平铺 的参数就会过大, 所以用 LSTM 保留了前面的优化信息,就不用平铺.
    model.add(LSTM(n_units, input_shape = (x.shape[0],x.shape[1])))#xshape = (4283,20)
    model.add(Dropout(0.2))
    model.add(Dense(label_size,activation='softmax'))
    model.compile(loss = 'categorical_crossentropy', optimizer = 'adam', metrics=['accuracy'])

    plot_model(model, to_file='./model_lstm.png', show_shapes = True) #自动给你生成一个 每层的维数的图 特别好
    model.summary()
    #预计到达时间: (ETA: Estimated Time of Arrival)
    return model

# 模型训练
def model_train(input_shape, filepath,model_save_path):

    # 将数据集分为训练集和测试集,占比为9:1
    # input_shape = 100
    x, y, output_dictionary, vocab_size,label_size,inverse_word_dictionary = load_data(filepath,input_shape)
    train_x, test_x, train_y, test_y = train_test_split(x,y,test_size=0.1,random_state = 42)    # 将数据集分为训练集和测试集,占比为9:1


    # 模型输入参数,需要自己根据需要调整
    n_units = 100
    batch_size = 32
    epochs = 5
    output_dim = 20

    # 模型训练
    lstm_model = create_LSTM(n_units, input_shape,output_dim,filepath)
    lstm_model.fit(train_x, train_y,epochs = epochs,batch_size = batch_size,verbose = 1)

    #模型保存
    lstm_model.save(model_save_path)

    N = test_x.shape[0]
    predict = []
    label = []
    for start, end in zip(range(0, N, 1), range(1,N + 1, 1)):
        sentence = [inverse_word_dictionary[i] for i in test_x[start] if i != 0]
        y_predict = lstm_model.predict(test_x[start:end])
        label_predict = output_dictionary[np.argmax(y_predict[0])]
        label_true = output_dictionary[np.argmax(test_y[start:end])]
        print(''.join(sentence),label_true,label_predict) # 输出预测结果
        predict.append(label_predict)
        label.append(label_true)


    acc = accuracy_score(predict,label) # 预测准确率
    print('模型在测试集上的准确率为: %s.' % acc)


if __name__ == '__main__':
    filepath = './corpus.csv'
    input_shape = 180
    model_save_path = './corpus_model.h5'
    model_train(input_shape,filepath,model_save_path)

接下来就是预测的代码了 model_perdict.py

# -*- coding: utf-8 -*-

# Import the necessary modules
import pickle
import numpy as np
from keras.models import load_model
from keras.preprocessing.sequence import pad_sequences

#导入字典
with open('word_dict.pk','rb') as f:
    word_dictionary = pickle.load(f)

with open('label_dict.pk','rb') as f:
    output_dictionary = pickle.load(f)


try:
    #数据预处理
    input_shape = 180
    sent = "很满意,电视非常好。护眼模式,很好,也很清晰。"
    x = [[word_dictionary[word] for word in sent]]
    x = pad_sequences(maxlen = input_shape, sequences=x,padding = 'post',value = 0)

    #载入模型
    model_save_path = './corpus_model.h5'

    lstm_model = load_model(model_save_path)

    #模型预测
    y_predict = lstm_model.predict(x)
    print(y_predict)
    label_dict = {v:k for k, v in output_dictionary.items()}
    print(label_dict)
    print('输入语句: %s' % sent)
    print('情感预测结果: %s' % label_dict[np.argmax(y_predict)])

except KeyError as err:
    print("您输入的句子有汉字不在词汇表中,请重新输入!")
    print("不在词汇表中的单词为:%s." % err)