基于长短时神经网络(LSTM)+word2vec的情感分析

只谈情不闲聊 提交于 2020-01-20 00:53:29

前言:
毕业前的项目,最近终于有时间整理个博客出来。使用的keras+gensim完成,也参考了互联网很多相关资料。最终效果只有88%左右,不过优化空间很大,只用作学习demo

数据集使用的是谭松波酒店评论数据集 停用词我自己整理了一个停用词词典 分享给大家
链接:https://pan.baidu.com/s/1ZkMGAUH7VSxJALWBs41iKQ
提取码:2c1e

1.数据处理

这一步主要是对评论文本做清洗,在这里只做简单的去停用词
首先写一个去停用词的方法

import jieba

f = open('./stop_words.txt', encoding='utf-8')         # 加载停用词
stopwords = [i.replace("\n", "") for i in f.readlines()]    # 停用词表

def del_stop_words(text):
	"""
	删除每个文本中的停用词
	:param text:
	:return:
	"""
	word_ls = jieba.lcut(text)
	word_ls = [i for i in word_ls if i not in stopwords]
	return word_ls

然后读取正面评论与负面评论的语料 并进行清洗

with open("./test_data/neg.txt", "r", encoding='UTF-8') as e:     # 加载负面语料
    neg_data1 = e.readlines()

with open("./test_data/pos.txt", "r", encoding='UTF-8') as s:     # 加载正面语料
    pos_data1 = s.readlines()

neg_data = sorted(set(neg_data1), key=neg_data1.index)  #列表去重 保持原来的顺序
pos_data = sorted(set(pos_data1), key=pos_data1.index)

neg_data = [del_stop_words(data.replace("\n", "")) for data in neg_data]   # 处理负面语料
pos_data = [del_stop_words(data.replace("\n", "")) for data in pos_data]
all_sentences= neg_data + pos_data  # 全部语料 用于训练word2vec

2. 文本向量化

对于文本的向量化其实有很多方式,包括独热(one-hot),词袋模型(bag of words),逆文本特征频率(tf-idf)和word2vec等。

本项目我们使用word2vec(据说效果很好)进行词向量的提取,word2vec是使用深度学习的方式将词映射为一个多维向量。

首先顶级模型结构 并进行训练及保存

from gensim.models.word2vec import Word2Vec
from gensim.corpora.dictionary import Dictionary
import pockle
import logging

logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)  # 将日志输出到控制台

model = Word2Vec(all_sentences,     # 上文处理过的全部语料
                 size=150,  # 词向量维度 默认100维
                 min_count=1,  # 词频阈值 词出现的频率 小于这个频率的词 将不予保存
                 window=5  # 窗口大小 表示当前词与预测词在一个句子中的最大距离是多少
                 )
model.save('./models/Word2vec_v1')  # 保存模型

然后加载模型 提取出词的索引与向量

def create_dictionaries(model):
	"""
	创建词语字典,并返回word2vec模型中词语的索引,词向量
	"""
    gensim_dict = Dictionary()    # 创建词语词典
    gensim_dict.doc2bow(model.wv.vocab.keys(), allow_update=True)

    w2indx = {v: k + 1 for k, v in gensim_dict.items()}  # 词语的索引,从1开始编号
    w2vec = {word: model[word] for word in w2indx.keys()}  # 词语的词向量
    return w2indx, w2vec


model = Word2Vec.load('./models/Word2vec_v1')         # 加载模型
index_dict, word_vectors= create_dictionaries(model)  # 索引字典、词向量字典

使用 pickle 存储序列化数据 pickle是一个非常方便的库 可以将py的字典 列表等等程序运行过程中的对象存储为实体数据

output = open(pkl_name + ".pkl", 'wb')      
pickle.dump(index_dict, output)  # 索引字典
pickle.dump(word_vectors, output)  # 词向量字典
output.close()

3. LSTM训练

接下来 使用keas库搭建LSTM模型 来进行训练
首先我们定义几个必要参数

# 参数设置
vocab_dim = 150 # 向量维度
maxlen = 150 # 文本保留的最大长度
batch_size = 100 # 训练过程中 每次传入模型的特征数量
n_epoch = 4   # 迭代次数

加载词向量数据 并填充词向量矩阵

f = open("./model/评价语料索引及词向量2.pkl", 'rb')  # 预先训练好的
index_dict = pickle.load(f)    # 索引字典,{单词: 索引数字}
word_vectors = pickle.load(f)  # 词向量, {单词: 词向量(100维长的数组)}

n_symbols = len(index_dict) + 1  # 索引数字的个数,因为有的词语索引为0,所以+1
embedding_weights = np.zeros((n_symbols, 150))  # 创建一个n_symbols * 1000矩阵

for w, index in index_dict.items():  # 从索引为1的词语开始,用词向量填充矩阵
    embedding_weights[index, :] = word_vectors[w]  # 词向量矩阵,第一行是0向量(没有索引为0的词语,未被填充)

接下来将所有的评论数据映射成为数字
因为之前通过加载词向量 已经拥有了一个索引字典
只要将出现在的索引字典中的单词转换为其索引数字 未出现的转换为0即可

def text_to_index_array(p_new_dic, p_sen): 
    """
    文本或列表转换为索引数字
    :param p_new_dic:
    :param p_sen:
    :return:
    """
    if type(p_sen) == list:
        new_sentences = []
        for sen in p_sen:
            new_sen = []
            for word in sen:
                try:
                    new_sen.append(p_new_dic[word])  # 单词转索引数字
                except:
                    new_sen.append(0)  # 索引字典里没有的词转为数字0
            new_sentences.append(new_sen)
        return np.array(new_sentences)   # 转numpy数组
    else:
        new_sentences = []
        sentences = []
        p_sen = p_sen.split(" ")
        for word in p_sen:
            try:
                sentences.append(p_new_dic[word])  # 单词转索引数字
            except:
                sentences.append(0)  # 索引字典里没有的词转为数字0
        new_sentences.append(sentences)
        return new_sentences

加载特征与标签 将特征全部映射成数字 并且分割验证集和测试集

with open("./原始语料/neg.txt", "r", encoding='UTF-8') as f:
            neg_data1 = f.readlines()

with open("./原始语料/pos.txt", "r", encoding='UTF-8') as g:
    pos_data1 = g.readlines()

neg_data = sorted(set(neg_data1), key=neg_data1.index)  #列表去重 保持原来的顺序
pos_data = sorted(set(pos_data1), key=pos_data1.index)

neg_data = [process_txt(data) for data in neg_data]
pos_data = [process_txt(data) for data in pos_data]
data = neg_data + pos_data


# 读取语料类别标签
label_list = ([0] * len(neg_data) + [1] * len(pos_data))


# 划分训练集和测试集,此时都是list列表
X_train_l, X_test_l, y_train_l, y_test_l = train_test_split(data, label_list, test_size=0.2)

# 转为数字索引形式

# token = Tokenizer(num_words=3000)   #字典数量
# token.fit_on_texts(train_text)

X_train = text_to_index_array(index_dict, X_train_l)
X_test = text_to_index_array(index_dict, X_test_l)

y_train = np.array(y_train_l)  # 转numpy数组
y_test = np.array(y_test_l)

print("训练集shape: ", X_train.shape)
print("测试集shape: ", X_test.shape)

因为模型输入的每一个特征长度需要相同,所以我们需要定义一个最大的长度max_len。
当特征小于max_len时,根据max_len填充其余位数为0。
当特征大于max_len,则进行截断。
在本项目中,我定义的max_len为150,是一个平均长度。有时候为了保证不丢失信息,可以打印出所有特征中最大的长度,并将其设置为max_len

X_train = sequence.pad_sequences(X_train, maxlen=maxlen)
X_test = sequence.pad_sequences(X_test, maxlen=maxlen)

定义模型 训练模型 验证模型

ef train_lstm(p_n_symbols, p_embedding_weights, p_X_train, p_y_train, p_X_test, p_y_test, X_test_l):
    print('创建模型...')
    model = Sequential()
    model.add(Embedding(output_dim=vocab_dim,  # 输出向量维度
                        input_dim=p_n_symbols,  # 输入向量维度
                        mask_zero=True,         # 使我们填补的0值在后续训练中不产生影响(屏蔽0值)
                        weights=[p_embedding_weights],   # 对数据加权
                        input_length=maxlen ))      # 每个特征的长度

    model.add(LSTM(output_dim=100,
                   activation='sigmoid',
                   inner_activation='hard_sigmoid'))
    model.add(Dropout(0.5))   # 每次迭代丢弃50神经元 防止过拟合
    model.add(Dense(units=512,
                    activation='relu'))
    model.add(Dropout(0.5))
    model.add(Dense(units=1,  # 输出层1个神经元 1代表正面 0代表负面
                    activation='sigmoid'))
    model.summary()

    print('编译模型...')
    model.compile(loss='binary_crossentropy',
                  optimizer='adam',
                  metrics=['accuracy'])

    print("训练...")
    train_history = model.fit(p_X_train, p_y_train, batch_size=batch_size, nb_epoch=n_epoch,
              validation_data=(p_X_test, p_y_test))

    print("评估...")
    score, acc = model.evaluate(p_X_test, p_y_test, batch_size=batch_size)
    label = model.predict(p_X_test)
    print('Test score:', score)
    print('Test accuracy:', acc)
    for (a, b, c) in zip(p_y_test, X_test_l, label):
        print("原文为:"+ "".join(b))
        print("预测倾向为", a)
        print("真实倾向为", c)

    show_train_history(train_history, 'acc', 'val_acc')    # 训练集准确率与验证集准确率 折线图
    show_train_history(train_history, 'loss', 'val_loss')  # 训练集误差率与验证集误差率 折线图

    """保存模型"""
    model.save('./model/emotion_model_LSTM.h5')
    print("模型保存成功")

可以通过show_train_history函数打印的训练集曲线 来判断模型是否过拟合。
方便确定迭代次数 进行调参 函数如下

def show_train_history(train_history,train, velidation):
    """
    可视化训练过程 对比
    :param train_history:
    :param train:
    :param velidation:
    :return:
    """
    plt.plot(train_history.history[train])
    plt.plot(train_history.history[velidation])
    plt.title("Train History")   #标题
    plt.xlabel('Epoch')    #x轴标题
    plt.ylabel(train)  #y轴标题
    plt.legend(['train', 'test'], loc='upper left')  #图例 左上角
    plt.show()

项目github:https://github.com/sph116/lstm_emotion
没仔细检查,可能会有些小问题,望海涵,欢迎交流。
在这里插入图片描述

后续优化:
1.增大训练word2vec 语料数量
2.数据清洗不止进行简单的去停用词
3.增加模型结构及复杂度

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!