第20天:NLP实战(四)——用GRU模型实现电影评论情感分析

时间:2022-07-24
本文章向大家介绍第20天:NLP实战(四)——用GRU模型实现电影评论情感分析,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

  接着上次的项目,主要是为了更加熟悉我们对NLP知识的实际应用,接着上次对深度学习中的CNN的简单应用相信大家对深度学习的相关知识以及相应的实现流程有了一个更深的了解,今天接着上次的项目,通过用RNN中的其中最简单的GRU模型对电影评论情感分析进行实现。其实,很明显这个案例和上一篇的微博谣言检测是一样的,也是个二分类的问题,因此,我们可以用到前篇文章提到的各种方法,即:朴素贝叶斯或者逻辑回归以及支持向量机都可以解决这个问题,另外在深度学习中,我们可以用CNN-Text或者RNN以及LSTM等模型最好,之所以本次用到GRU就是通过本次项目介绍让大家对RNN中的GRU模型有一个更深层次的了解。当然在构建网络中也相对简单,相对而言,LSTM就比较复杂了,为了让不同层次的读者可以接受,我们就用了相对简单的GRU模型。如果大家想了解LSTM,可以看这篇文章。接下来,我们详细给大家介绍项目。

项目介绍

  其实情感分析在自然语言处理中,情感分析一般指判断一段文本所表达的情绪状态,属于文本分类问题。一般而言:情绪类别:正面/负面。当然,这就是为什么本人在前面提到情感分析实际上也是二分类问题的原因。

数据集介绍

  我们本次使用的是非常典型的IMDB数据集。该数据集包含来自互联网的50000条严重两极分化的评论,该数据被分为用于训练的25000条评论和用于测试的25000条评论,训练集和测试集都包含50%的正面评价和50%的负面评价。该数据集已经经过预处理:评论(单词序列)已经被转换为整数序列,其中每个整数代表字典中的某个单词。下载的地址可以在网上找到。为了防止大家数据集下载不下来的问题,本人将这份数据集上传到了百度网盘,大家可以将本地的数据集[验证码:q7br]下载下来。接下来我们看看数据集。   前面也介绍过IMDB数据集分为正面和负面两类。因此,我们查看其数据集的文件夹:这是train和test文件夹。

  接下来就是以train文件夹介绍里面的内容:

  然后就是以neg文件夹介绍里面的内容,里面会有若干的text文件:

  最后我们打开任何一个txt文件查看里面的相关内容:

  实验环境搭建以及实验平台前几篇文章均介绍过,只要是跑了上一篇文章的这次实验环境不用动,要是没跑的可以去跑一遍上一个项目:第19天:NLP实战(三)——用CNN实现微博谣言检测,当然也可以按照前面介绍搭建好环境可以直接跑本项目。

项目实现

1、数据预处理

  首先导入必要的第三方库:

#导入必要的包
import zipfile
import os
import io
import random
import json
import matplotlib.pyplot as plt
import numpy as np
import paddle
import paddle.fluid as fluid
from paddle.fluid.dygraph.nn import Conv2D, Pool2D, Linear, Embedding
from paddle.fluid.dygraph.base import to_variable
from paddle.fluid.dygraph import GRUUnit
import paddle.dataset.imdb as imdb

  接下来就是数据预处理,需要注意的是:数据是以数据标签的方式表示一个句子,因此,每个句子都是以一串整数来表示的,每个数字都是对应一个单词。当然,数据集就会有一个数据集字典,这个字典是训练数据中出现单词对应的数字标签。具体实现如下:

#加载字典
def load_vocab():
    vocab = imdb.word_dict()
    return vocab
#定义数据生成器
class SentaProcessor(object):
    def __init__(self):
        self.vocab = load_vocab()

    def data_generator(self, batch_size, phase='train'):
        if phase == "train":
            return paddle.batch(paddle.reader.shuffle(imdb.train(self.vocab),25000), batch_size, drop_last=True)
        elif phase == "eval":
            return paddle.batch(imdb.test(self.vocab), batch_size,drop_last=True)
        else:
            raise ValueError(
                "Unknown phase, which should be in ['train', 'eval']")

2、配置网络

  这次的GRU模型分为以下的几个步骤:

  • 定义网络
  • 定义损失函数
  • 定义优化算法

  具体实现如下:

#定义动态GRU
class DynamicGRU(fluid.dygraph.Layer):
    def __init__(self,
                 size,
                 param_attr=None,
                 bias_attr=None,
                 is_reverse=False,
                 gate_activation='sigmoid',
                 candidate_activation='relu',
                 h_0=None,
                 origin_mode=False,
                 ):
        super(DynamicGRU, self).__init__()
        self.gru_unit = GRUUnit(
            size * 3,
            param_attr=param_attr,
            bias_attr=bias_attr,
            activation=candidate_activation,
            gate_activation=gate_activation,
            origin_mode=origin_mode)
        self.size = size
        self.h_0 = h_0
        self.is_reverse = is_reverse
    def forward(self, inputs):
        hidden = self.h_0
        res = []
        for i in range(inputs.shape[1]):
            if self.is_reverse:
                i = inputs.shape[1] - 1 - i
            input_ = inputs[ :, i:i+1, :]
            input_ = fluid.layers.reshape(input_, [-1, input_.shape[2]], inplace=False)
            hidden, reset, gate = self.gru_unit(input_, hidden)
            hidden_ = fluid.layers.reshape(hidden, [-1, 1, hidden.shape[1]], inplace=False)
            res.append(hidden_)
        if self.is_reverse:
            res = res[::-1]
        res = fluid.layers.concat(res, axis=1)
        return res
class GRU(fluid.dygraph.Layer):
    def __init__(self):
        super(GRU, self).__init__()
        self.dict_dim = train_parameters["vocab_size"]
        self.emb_dim = 128
        self.hid_dim = 128
        self.fc_hid_dim = 96
        self.class_dim = 2
        self.batch_size = train_parameters["batch_size"]
        self.seq_len = train_parameters["padding_size"]
        self.embedding = Embedding(
            size=[self.dict_dim + 1, self.emb_dim],
            dtype='float32',
            param_attr=fluid.ParamAttr(learning_rate=30),
            is_sparse=False)
        h_0 = np.zeros((self.batch_size, self.hid_dim), dtype="float32")
        h_0 = to_variable(h_0)
        
        self._fc1 = Linear(input_dim=self.hid_dim, output_dim=self.hid_dim*3)
        self._fc2 = Linear(input_dim=self.hid_dim, output_dim=self.fc_hid_dim, act="relu")
        self._fc_prediction = Linear(input_dim=self.fc_hid_dim,
                                output_dim=self.class_dim,
                                act="softmax")
        self._gru = DynamicGRU(size=self.hid_dim, h_0=h_0)
        
    def forward(self, inputs, label=None):
        emb = self.embedding(inputs)
        o_np_mask =to_variable(inputs.numpy().reshape(-1,1) != self.dict_dim).astype('float32')
        mask_emb = fluid.layers.expand(
            to_variable(o_np_mask), [1, self.hid_dim])
        emb = emb * mask_emb
        emb = fluid.layers.reshape(emb, shape=[self.batch_size, -1, self.hid_dim])
        fc_1 = self._fc1(emb)
        gru_hidden = self._gru(fc_1)
        gru_hidden = fluid.layers.reduce_max(gru_hidden, dim=1)
        tanh_1 = fluid.layers.tanh(gru_hidden)
        fc_2 = self._fc2(tanh_1)
        prediction = self._fc_prediction(fc_2)
        
        if label is not None:
            acc = fluid.layers.accuracy(prediction, label=label)
            return prediction, acc
        else:
            return prediction

  接下来就是参数配置

'''
参数配置
'''
train_parameters = {
    "epoch": 16,                              #训练轮次
    "batch_size": 128,                        #批次大小
    "lr":0.01,                              #学习率
    "padding_size": 150,                     #padding纬度
    "vocab_size": 89527,                     #padding的值
    "skip_steps":50,                         #每个N批次输出一次结果
    "save_steps": 150,                       #每N个批次保存一次
    "checkpoints":"data/"                    #训练时每个批次的大小
}

  为了在模型训练过程中更直观的查看我们训练的准确率,我们首先利用python的matplotlib.pyplt函数实现一个可视化图,具体的实现如下:

def draw_train_process(iters, train_loss, train_accs):
    title="training loss/training accs"
    plt.title(title, fontsize=24)
    plt.xlabel("iter", fontsize=14)
    plt.ylabel("loss/acc", fontsize=14)
    plt.plot(iters, train_loss, color='red', label='training loss')
    plt.plot(iters, train_accs, color='green', label='training accs')
    plt.legend()
    plt.grid()
    plt.show()

3、模型训练

def train():
    with fluid.dygraph.guard(place = fluid.CUDAPlace(0)): # # 因为要进行很大规模的训练,因此我们用的是GPU,如果没有安装GPU的可以使用下面一句,把这句代码注释掉即可
    # with fluid.dygraph.guard(place = fluid.CPUPlace()):

        processor = SentaProcessor()
        train_data_generator = processor.data_generator(batch_size=train_parameters["batch_size"], phase='train')

        model = GRU()
        sgd_optimizer = fluid.optimizer.Adagrad(learning_rate=train_parameters["lr"],parameter_list=model.parameters())

        steps = 0
        Iters, total_loss, total_acc = [], [], []
        for eop in range(train_parameters["epoch"]):
            for batch_id, data in enumerate(train_data_generator()):

                steps += 1
                doc = to_variable(
                    np.array([
                        np.pad(x[0][0:train_parameters["padding_size"]], 
                              (0, train_parameters["padding_size"] - len(x[0][0:train_parameters["padding_size"]])),
                               'constant',
                              constant_values=(train_parameters["vocab_size"]))
                        for x in data
                    ]).astype('int64').reshape(-1))
                label = to_variable(
                    np.array([x[1] for x in data]).astype('int64').reshape(
                        train_parameters["batch_size"], 1))
        
                model.train()
                prediction, acc = model(doc, label)
                loss = fluid.layers.cross_entropy(prediction, label)
                avg_loss = fluid.layers.mean(loss)
                avg_loss.backward()
                sgd_optimizer.minimize(avg_loss)
                model.clear_gradients()
 
                if steps % train_parameters["skip_steps"] == 0:
                    Iters.append(steps)
                    total_loss.append(avg_loss.numpy()[0])
                    total_acc.append(acc.numpy()[0])
                    print("step: %d, ave loss: %f, ave acc: %f" %
                         (steps,avg_loss.numpy(),acc.numpy()))

                if steps % train_parameters["save_steps"] == 0:
                    save_path = train_parameters["checkpoints"]+"/"+"save_dir_" + str(steps)
                    print('save model to: ' + save_path)
                    fluid.dygraph.save_dygraph(model.state_dict(),
                                                   save_path)
    draw_train_process(Iters, total_loss, total_acc)

  开始训练

train()

  训练的过程以及训练的结果如下:

  在这里需要说明的是,我们很明显发现训练的时候,到了后面已经过拟合了。我们可以通过一系列的方法来降低过拟合的问题。解决过拟合的方法很多,其中包括:重新清洗数据、增大数据的训练量、采用正则化方法以及采用dropout方法。详细关于过拟合的相关内容请看这篇文章

4、模型评估

def to_eval():
    with fluid.dygraph.guard(place = fluid.CUDAPlace(0)):
        processor = SentaProcessor()

        eval_data_generator = processor.data_generator(batch_size=train_parameters["batch_size"],phase='eval')
     
        model_eval = GRU()
        model, _ = fluid.load_dygraph("data/save_dir_750.pdparams")
        model_eval.load_dict(model)

        model_eval.eval()
        total_eval_cost, total_eval_acc = [], []
        for eval_batch_id, eval_data in enumerate(eval_data_generator()):
            eval_np_doc = np.array([np.pad(x[0][0:train_parameters["padding_size"]],
                                    (0, train_parameters["padding_size"] -len(x[0][0:train_parameters["padding_size"]])),
                                    'constant',
                                    constant_values=(train_parameters["vocab_size"]))
                            for x in eval_data
                            ]).astype('int64').reshape(-1)
            eval_label = to_variable(
                                    np.array([x[1] for x in eval_data]).astype(
                                    'int64').reshape(train_parameters["batch_size"], 1))
            eval_doc = to_variable(eval_np_doc)
            eval_prediction, eval_acc = model_eval(eval_doc, eval_label)
            loss = fluid.layers.cross_entropy(eval_prediction, eval_label)
            avg_loss = fluid.layers.mean(loss)
            total_eval_cost.append(avg_loss.numpy()[0])
            total_eval_acc.append(eval_acc.numpy()[0])

        print("Final validation result: ave loss: %f, ave acc: %f" %
            (np.mean(total_eval_cost), np.mean(total_eval_acc) ))
to_eval()

  评估准确率如下:

  结果还可以,这里说明的是,刚开始我们的模型训练评估不可能这么好,很明显是过拟合的问题,这就需要我们调整我们的epoch、batchsize、激活函数的选择以及优化器、学习率等各种参数,通过不断的调试、训练最好可以得到不错的结果,但是,如果还要更好的模型效果,其实可以将GRU模型换为更为合适的RNN中的LSTM以及bi-LSTM模型会好很多。

5、模型预测

def load_data(sentences):
    lod = []
    word_dict = imdb.word_dict()
    UNK = word_dict['<unk>']
    reviews = sentences.split(" ")
    for words in reviews:
            # 需要把单词进行字符串编码转换
            lod.append(word_dict.get(words.encode('utf-8'), UNK))
    return lod
train_parameters["batch_size"] = 1

with fluid.dygraph.guard(place = fluid.CUDAPlace(0)):

    sentences = 'this is a great movie'
    data = load_data(sentences)
    print(sentences)
    print(data)
    data_np = np.array(data)
    data_np = np.array(np.pad(data_np,(0,150-len(data_np)),"constant",constant_values =train_parameters["vocab_size"])).astype('int64').reshape(-1)
    infer_np_doc = to_variable(data_np)

    model_infer = GRU()
    model, _ = fluid.load_dygraph("data/save_dir_750.pdparams")
    model_infer.load_dict(model)
    model_infer.eval()
    result = model_infer(infer_np_doc)
    print('预测结果为:正面概率为:%0.5f,负面概率为:%0.5f' % (result.numpy()[0][0],result.numpy()[0][1]))

我们最后查看其预测的结果:

  训练的结果还是挺满意的,到此为止,我们的本次项目实验到此结束。

总结

  本文通过一个经典的数据集:电影评论情感分析来更好的理解深度学习RNN原理和GRU模型以及从数据的处理到模型的预测整个流程过了一遍,并且还详细解释了在训练过程中可能遇到的问题。这些代码均可运行,希望大家在有时间的情况下,可以动手运行一遍,感受一下RNN的原理以及在实际中是如何被应用的,另外就是感受一下一个网络的训练过程和预测过程。至此,我们在自然语言处理中的常用的深度学习算法框架已经学完了,接下来就是介绍自然语言中的词向量,其中的理论在前面的文章已经介绍过了。下篇文章给大家介绍词向量中的实践,练习完之后感觉收获满满。加油,希望我们都学有所获,坚持练习,我们未来可期。