1. 什么是RNN(循环神经网络)在普通的神经网络中,信息的传递是单向的,这种限制虽然使得网络变得更容易学习,但在一定程度上也减弱了神经网络模型的能力。特别是在很多现实任务中,网络的输出不仅和当前时刻的输入相关,也和其过去一段时间的输出相关。此外,普通网络难以处理时序数据,比如视频、语音、文本等,时序数据的长度一般是不固定的,而前馈神经网络要求输入和输出的维数都是固定的,不能任意改变。因此,当处理这一类和时序相关的问题时,就需要一种能力更强的模型。循环神经网络(Recurrent Neural Network,RNN)是一类具有短期记忆能力的神经网络。在循环神经网络中,神经元不但可以接受其它神经元的信息,也可以接受自身的信息,形成具有环路的网络结构。换句话说:神经元的输出可以在下一个时间步直接作用到自身。 RNN一般以序列数据为输入, 通过网络内部的结构设计有效捕捉序列之间的关系特征, 一般也是以序列形式进行输出.
    图片
    由于RNN的循环特性,和自然语言文本的序列特性(句子是由单词组成的序列)十分匹配,因此被大量应用于自然语言处理研究中。数据准备本节使用情感分类的经典数据集IMDB影评数据集,数据集包含Positive和Negative两类此外,需要使用预训练词向量对自然语言单词进行编码,以获取文本的语义特征,本节选取Glove词向量作为Embedding。数据下载模块下载地址 https://mindspore-website.obs.myhuaweicloud.com/notebook/data...
    图片
    加载IMDB数据集下载好的IMDB数据集为tar.gz文件,使用Python的tarfile库对其进行读取,并将所有数据和标签分别进行存放。数据集已分割为train和test两部分,且每部分包含neg和pos两个分类的文件夹,因此需分别train和test进行读取并处理数据和标签。import re
    import six
    import string
    import tarfile

class IMDBData():

"""IMDB数据集加载器
加载IMDB数据集并处理为一个Python迭代对象。
"""
label_map = {
    "pos": 1,
    "neg": 0
}
def __init__(self, path, mode="train"):
    self.mode = mode
    self.path = path
    self.docs, self.labels = [], []

    self._load("pos")
    self._load("neg")

def _load(self, label):
    pattern = re.compile(r"aclImdb/{}/{}/.*\.txt$".format(self.mode, label))
    # 将数据加载至内存
    with tarfile.open(self.path) as tarf:
        tf = tarf.next()
        while tf is not None:
            if bool(pattern.match(tf.name)):
                # 对文本进行分词、去除标点和特殊字符、小写处理
                self.docs.append(str(tarf.extractfile(tf).read().rstrip(six.b("\n\r"))
                                     .translate(None, six.b(string.punctuation)).lower()).split())
                self.labels.append([self.label_map[label]])
            tf = tarf.next()

def __getitem__(self, idx):
    return self.docs[idx], self.labels[idx]

def __len__(self):
    return len(self.docs)加载预训练词向量预训练词向量是对输入单词的数值化表示,通过nn.Embedding层,采用查表的方式,输入单词对应词表中的index,获得对应的表达向量。 因此进行模型构造前,需要将Embedding层所需的词向量和词表进行构造。这里我们使用Glove(Global Vectors for Word Representation)这种经典的预训练词向量我们直接使用第一列的单词作为词表,使用dataset.text.Vocab将其按顺序加载;同时读取每一行的Vector并转为numpy.array,用于nn.Embedding加载权重使用。由于数据集中可能存在词表没有覆盖的单词,因此需要加入<unk>标记符;同时由于输入长度的不一致,在打包为一个batch时需要将短的文本进行填充,因此需要加入<pad>标记符。完成后的词表长度为原词表长度+2。下载Glove词向量,并加载生成词表和词向量权重矩阵。地址 https://mindspore-website.obs.myhuaweicloud.com/notebook/datasets/glove.6B.zipimport zipfile

import numpy as np

def load_glove(glove_path):

glove_100d_path = os.path.join(cache_dir, 'glove.6B.100d.txt')
if not os.path.exists(glove_100d_path):
    glove_zip = zipfile.ZipFile(glove_path)
    glove_zip.extractall(cache_dir)

embeddings = []
tokens = []
with open(glove_100d_path, encoding='utf-8') as gf:
    for glove in gf:
        word, embedding = glove.split(maxsplit=1)
        tokens.append(word)
        embeddings.append(np.fromstring(embedding, dtype=np.float32, sep=' '))
# 添加 <unk>, <pad> 两个特殊占位符对应的embedding
embeddings.append(np.random.rand(100))
embeddings.append(np.zeros((100,), np.float32))

vocab = ds.text.Vocab.from_list(tokens, special_tokens=["<unk>", "<pad>"], special_first=False)
embeddings = np.array(embeddings).astype(np.float32)
return vocab, embeddings使用词表将the转换为index id,并查询词向量矩阵对应的词向量:

图片
数据集预处理通过加载器加载的IMDB数据集进行了分词处理,但不满足构造训练数据的需要,因此要对其进行额外的预处理。其中包含的预处理如下:通过Vocab将所有的Token处理为index id。将文本序列统一长度,不足的使用<pad>补齐,超出的进行截断。模型构建完成数据集的处理后,我们设计用于情感分类的模型结构。首先需要将输入文本(即序列化后的index id列表)通过查表转为向量化表示,此时需要使用nn.Embedding层加载Glove词向量;然后使用RNN循环神经网络做特征提取;最后将RNN连接至一个全连接层,即nn.Dense,将特征转化为与分类数量相同的size,用于后续进行模型优化训练。整体模型结构如下:nn.Embedding -> nn.RNN -> nn.DenseEmbeddingEmbedding层又可称为EmbeddingLookup层,其作用是使用index id对权重矩阵对应id的向量进行查找,当输入为一个由index id组成的序列时,则查找并返回一个相同长度的矩阵。这里我们使用前文处理好的Glove词向量矩阵,设置nn.Embedding的embedding_table为预训练词向量矩阵。对应的vocab_size为词表大小400002,embedding_size为选用的glove.6B.100d向量大小,即100。nn.RNN这里我们使用能够一定程度规避RNN梯度消失问题的变种LSTM(Long short-term memory)做特征提取层。RNN单个Cell的结构简单,因此也造成了梯度消失(Gradient Vanishing)问题,具体表现为RNN网络在序列较长时,在序列尾部已经基本丢失了序列首部的信息。为了克服这一问题,LSTM(Long short-term memory)被提出,通过门控机制(Gating Mechanism)来控制信息流在每个循环步中的留存和丢弃。下图为LSTM的结构拆解:
图片
Dense在经过LSTM编码获取句子特征后,将其送入一个全连接层,即nn.Dense,将特征维度变换为二分类所需的维度1,经过Dense层后的输出即为模型预测结果。损失函数与优化器完成模型主体构建后,首先根据指定的参数实例化网络;然后选择损失函数和优化器。针对本节情感分类问题的特性,即预测Positive或Negative的二分类问题,我们选择nn.BCEWithLogitsLoss(二分类交叉熵损失函数)。hidden_size = 256
output_size = 1
num_layers = 2
bidirectional = True
lr = 0.001
pad_idx = vocab.tokens_to_ids('<pad>')

model = RNN(embeddings, hidden_size, output_size, num_layers, bidirectional, pad_idx)
loss_fn = nn.BCEWithLogitsLoss(reduction='mean')
optimizer = nn.Adam(model.trainable_params(), learning_rate=lr)训练逻辑在完成模型构建,进行训练逻辑的设计。一般训练逻辑分为一下步骤:读取一个Batch的数据;送入网络,进行正向计算和反向传播,更新权重;返回loss。评估指标和逻辑训练逻辑完成后,需要对模型进行评估。即使用模型的预测结果和测试集的正确标签进行对比,求出预测的准确率。由于IMDB的情感分类为二分类问题,对预测值直接进行四舍五入即可获得分类标签(0或1),然后判断是否与正确标签相等即可。有了准确率计算函数后,类似于训练逻辑,对评估逻辑进行设计, 分别为以下步骤:读取一个Batch的数据;送入网络,进行正向计算,获得预测结果;计算准确率。模型训练与保存前序完成了模型构建和训练、评估逻辑的设计,下面进行模型训练。这里我们设置训练轮数为5轮。同时维护一个用于保存最优模型的变量best_valid_loss,根据每一轮评估的loss值,取loss值最小的轮次,将模型进行保存。
图片
可以看到每轮Loss逐步下降,在验证集上的准确率逐步提升。模型加载与测试模型训练完成后,一般需要对模型进行测试或部署上线,此时需要加载已保存的最优模型(即checkpoint),供后续测试使用。这里我们直接使用MindSpore提供的Checkpoint加载和网络权重加载接口:1.将保存的模型Checkpoint加载到内存中,2.将Checkpoint加载至模型。param_dict = ms.load_checkpoint(ckpt_file_name)
ms.load_param_into_net(model, param_dict)对测试集打batch,然后使用evaluate方法进行评估,得到模型在测试集上的效果。
图片
自定义输入测试最后我们设计一个预测函数,实现开头描述的效果,输入一句评价,获得评价的情感分类。具体包含以下步骤:将输入句子进行分词;使用词表获取对应的index id序列;index id序列转为Tensor;送入模型获得预测结果;打印输出预测结果。具体实现如下:score_map = {

1: "Positive",
0: "Negative"

}

def predict_sentiment(model, vocab, sentence):

model.set_train(False)
tokenized = sentence.lower().split()
indexed = vocab.tokens_to_ids(tokenized)
tensor = ms.Tensor(indexed, ms.int32)
tensor = tensor.expand_dims(0)
prediction = model(tensor)
return score_map[int(np.round(ops.sigmoid(prediction).asnumpy()))]最后我们预测开头的样例,可以看到模型可以很好地将评价语句的情感进行分类。

图片


文良_颜丑
1 声望0 粉丝