Task8 循环神经网络

目录

一、RNN基础

1. 1 RNN的结构

1.2 循环神经网络的提出背景

1.3 BPTT算法

二、双向RNN

三、 递归神经网络

四、LSTM、GRU的结构

4.1 LSTM

4.2 GRU( Gated Recurrent Unit,LSTM变体)

五、针对梯度消失(LSTM等其他门控RNN)

六、 Memory Network

七、 Text-RNN的原理

八、利用Text-RNN模型来进行文本分类

九、 Recurrent Convolutional Neural Networks(RCNN)原理

十、 利用RCNN模型来进行文本分类(自选)


一、RNN基础

循环神经网络RNN,是一类用于处理序列数据的神经网络。就像卷积网络是专门用于处理网格化数据的神经网络,循环神经网络是专门用于处理序列Task8 循环神经网络的神经网络。正如卷积网络可以很容易地扩展到具有很大宽度的高度的图像,以及处理大小可变的图像,循环网络可以扩展到更长的序列,大多数循环网络也能处理可变长的序列。

1. 1 RNN的结构

Task8 循环神经网络

1.2 循环神经网络的提出背景

RNN通过每层之间节点的连接结构来记忆之前的信息,并利用这些信息来影响后面节点的输出。RNN可充分挖掘序列数据中的时序信息以及语义信息,这种在处理时序数据时比全连接神经网络和CNN更具有深度表达能力,RNN已广泛应用于语音识别、语言模型、机器翻译、时序分析等各个领域。

1.3 BPTT算法

RNN的训练方法——BPTT算法(back-propagation through time)

BPTT(back-propagation through time)算法是常用的训练RNN的方法,其实本质还是BP算法,只不过RNN处理时间序列数据,所以要基于时间反向传播,故叫随时间反向传播。BPTT的中心思想和BP算法相同,沿着需要优化的参数的负梯度方向不断寻找更优的点直至收敛。综上所述,BPTT算法本质还是BP算法,BP算法本质还是梯度下降法,那么求各个参数的梯度便成了此算法的核心。 这里寻优的参数有三个,分别是U、V、W。与BP算法不同的是,其中W和U两个参数的寻优过程需要追溯之前的历史数据,参数V相对简单只需关注目前,那么我们就来先求解参数V的偏导数。 Task8 循环神经网络

这个式子看起来简单但是求解起来很容易出错,因为其中嵌套着**函数函数,是复合函数的求导过程。RNN的损失也是会随着时间累加的,所以不能只求t时刻的偏导。 W和U的偏导的求解由于需要涉及到历史数据,其偏导求起来相对复杂,我们先假设只有三个时刻,那么在第三个时刻 L对W的偏导数为:

Task8 循环神经网络

相应的,L在第三个时刻对U的偏导数为:

Task8 循环神经网络

二、双向RNN

Bidirectional RNN(双向RNN)假设当前t的输出不仅仅和之前的序列有关,并且 还与之后的序列有关,例如:预测一个语句中缺失的词语那么需要根据上下文进 行预测;Bidirectional RNN是一个相对简单的RNNs,由两个RNNs上下叠加在 一起组成。输出由这两个RNNs的隐藏层的状态决定。

Task8 循环神经网络

三、 递归神经网络

在RNN中,信息只在一个方向上移动。当它作出决定时,会考虑当前的输入以及它从之前收到的输入中学到的内容。下面的两张图片说明了RNN和前馈神经网络之间的信息流的差异。  

Task8 循环神经网络

通常RNN是具有短期记忆的,结合LSTM,他们也有长期记忆,这一点,我们将在下面进一步讨论。

  说明RNN记忆概念的另一个好方法是用一个例子来解释它:假设你有一个正常的前馈神经网络,并给它一个单词“neuron(神经元)”作为输入,并逐字处理这个单词。当它到达字符“r”时,它已经忘记了“n”,“e”和“u”,这使得这种类型的神经网络几乎不可能预测接下来会出现什么字符。而经常性的神经网络则能够准确记住,因为它是内部记忆。它产生输出,复制输出并将其循环回网络。递归神经网络有两个输入,现在和最近的过去。这很重要,因为数据序列包含关于接下来会发生什么的重要信息,这就是为什么RNN可以做其他算法无法做的事情。与所有其他深度学习算法一样,前馈神经网络将权重矩阵分配给其输入,然后生成输出。请注意,RNN将权重应用于当前以及之前的输入。此外,他们还通过梯度下降和反向传播时间调整权重,我们将在下面的部分讨论。还要注意,尽管前馈神经网络将一个输入映射到一个输出,但RNN可以映射一对多,多对多(翻译)和多对一(分类语音)。

四、LSTM、GRU的结构

4.1 LSTM

上面介绍的RNN模型,存在“长期依赖”的问题。模型在预测“大海的颜色是”下一个单词时,很容易判断为“蓝色”,因为这里相关信息与待预测词的位置相差不大,模型不需要记忆这个短句子之前更长的上下文信息。但当模型预测“十年前,北京的天空很蓝,但随着大量工厂的开设,废气排放监控不力,空气污染开始变得越来越严重,渐渐地,这里的天空变成了”下一个单词时,依靠“短期依赖”就不能很好的解决这类问题,因为仅仅根据“这里的天空变成了”这一小段,后一个单词可以是“蓝色”,也可以是“灰色”。上节描述的简单RNN结构可能无法学习到这种“长期依赖”的信息,LSTM可以很好的解决这类问题。

与简单RNN结构中单一tanh循环体不同的是,LSTM使用三个“门”结构来控制不同时刻的状态和输出。所谓的“门”结构就是使用了sigmoid**函数的全连接神经网络和一个按位做乘法的操作,sigmoid**函数会输出一个0~1之间的数值,这个数值描述的是当前有多少信息能通过“门”,0表示任何信息都无法通过,1表示全部信息都可以通过。其中,“遗忘门”和“输入门”是LSTM单元结构的核心。下面我们来详细分析下三种“门”结构。

遗忘门,用来让RNN“忘记”之前没有用的信息。比如“十年前,北京的天空是蓝色的”,但当看到“空气污染开始变得越来越严重”后,RNN应该忘记“北京的天空是蓝色的”这个信息。遗忘门会根据当前时刻节点的输入Xt、上一时刻节点的状态C(t-1)和上一时刻节点的输出h(t-1)来决定哪些信息将被遗忘。

输入门,用来让RNN决定当前输入数据中哪些信息将被留下来。在RNN使用遗忘门“忘记”部分之前的信息后,还需要从当前的输入补充最新的记忆。输入门会根据当前时刻节点的输入Xt、上一时刻节点的状态C(t-1)和上一时刻节点的输出h(t-1)来决定哪些信息将进入当前时刻节点的状态Ct,比如看到“空气污染开始变得越来越严重”后,模型需要记忆这个最新的信息。

输出门,LSTM在得到最新节点状态Ct后,结合上一时刻节点的输出h(t-1)和当前时刻节点的输入Xt来决定当前时刻节点的输出。比如当前时刻节点状态为被污染,那么“天空的颜色”后面的单词应该是“灰色”。

在TensorFlow中可以使用lstm = rnn_cell.BasicLSTMCell(lstm_hidden_size)来声明一个LSTM结构。

4.2 LSTM、GRU优缺点

LSTM是一种特殊的RNN类型,一般的RNN结构如下图所示,是一种将以往学习的结果应用到当前学习的模型,但是这种一般的RNN存在着许多的弊端。举个例子,如果我们要预测“the clouds are in the sky”的最后一个单词,因为只在这一个句子的语境中进行预测,那么将很容易地预测出是这个单词是sky。在这样的场景中,相关的信息和预测的词位置之间的间隔是非常小的,RNN 可以学会使用先前的信息。

Task8 循环神经网络Task8 循环神经网络

标准的RNN结构中只有一个神经元,一个tanh层进行重复的学习,这样会存在一些弊端。例如,在比较长的环境中,例如在“I grew up in France… I speak fluent French”中去预测最后的French,那么模型会推荐一种语言的名字,但是预测具体是哪一种语言时就需要用到很远以前的Franch,这就说明在长环境中相关的信息和预测的词之间的间隔可以是非常长的。在理论上,RNN 绝对可以处理这样的长环境问题。人们可以仔细挑选参数来解决这类问题中的最初级形式,但在实践中,RNN 并不能够成功学习到这些知识。然而,LSTM模型就可以解决这一问题。

Task8 循环神经网络

如图所示,标准LSTM模型是一种特殊的RNN类型,在每一个重复的模块中有四个特殊的结构,以一种特殊的方式进行交互。在图中,每一条黑线传输着一整个向量,粉色的圈代表一种pointwise 操作(将定义域上的每一点的函数值分别进行运算),诸如向量的和,而黄色的矩阵就是学习到的神经网络层。

LSTM模型的核心思想是“细胞状态”。“细胞状态”类似于传送带。直接在整个链上运行,只有一些少量的线性交互。信息在上面流传保持不变会很容易。

Task8 循环神经网络

LSTM 有通过精心设计的称作为“门”的结构来去除或者增加信息到细胞状态的能力。门是一种让信息选择式通过的方法。他们包含一个 sigmoid 神经网络层和一个 pointwise 乘法操作。 Task8 循环神经网络Sigmoid 层输出 0 到 1 之间的数值,描述每个部分有多少量可以通过。0 代表“不许任何量通过”,1 就指“允许任意量通过”。LSTM 拥有三个门,来保护和控制细胞状态。

在LSTM模型中,第一步是决定我们从“细胞”中丢弃什么信息,这个操作由一个忘记门层来完成。该层读取当前输入x和前神经元信息h,由ft来决定丢弃的信息。输出结果1表示“完全保留”,0 表示“完全舍弃”。

第二步是确定细胞状态所存放的新信息,这一步由两层组成。sigmoid层作为“输入门层”,决定我们将要更新的值i;tanh层来创建一个新的候选值向量~Ct加入到状态中。在语言模型的例子中,我们希望增加新的主语到细胞状态中,来替代旧的需要忘记的主语。

第三步就是更新旧细胞的状态,将Ct-1更新为Ct。我们把旧状态与 ft相乘,丢弃掉我们确定需要丢弃的信息。接着加上 it * ~Ct。这就是新的候选值,根据我们决定更新每个状态的程度进行变化。在语言模型的例子中,这就是我们实际根据前面确定的目标,丢弃旧代词的信息并添加新的信息的地方。

最后一步就是确定输出了,这个输出将会基于我们的细胞状态,但是也是一个过滤后的版本。首先,我们运行一个 sigmoid 层来确定细胞状态的哪个部分将输出出去。接着,我们把细胞状态通过 tanh 进行处理(得到一个在 -1 到 1 之间的值)并将它和 sigmoid 门的输出相乘,最终我们仅仅会输出我们确定输出的那部分。在语言模型的例子中,因为语境中有一个代词,可能需要输出与之相关的信息。例如,输出判断是一个动词,那么我们需要根据代词是单数还是负数,进行动词的词形变化。

4.2 GRU( Gated Recurrent Unit,LSTM变体)

Task8 循环神经网络

GRU作为LSTM的一种变体,将忘记门和输入门合成了一个单一的更新门。同样还混合了细胞状态和隐藏状态,加诸其他一些改动。最终的模型比标准的 LSTM 模型要简单,也是非常流行的变体。

五、针对梯度消失(LSTM等其他门控RNN)

5.1 梯度爆炸(梯度截断)的解决方案

LSTM只能避免RNN的梯度消失(gradient vanishing),但是不能对抗梯度爆炸问题(Exploding Gradient)。梯度膨胀(gradient explosion)不是个严重的问题,一般靠裁剪后的优化算法即可解决,比如gradient clipping(如果梯度的范数大于某个给定值,将梯度同比收缩)。

梯度剪裁的方法一般有两种:

1.一种是当梯度的某个维度绝对值大于某个上限的时候,就剪裁为上限。

2.另一种是梯度的L2范数大于上限后,让梯度除以范数,避免过大。

LSTM如何避免梯度消失?

Task8 循环神经网络

六、 Memory Network

传统的RNN/LSTM等模型的隐藏状态或者Attention机制的记忆存储能力太弱,无法存储太多的信息,很容易丢失一部分语义信息,所以记忆网络通过引入外部存储来记忆信息.记忆网络的一般框架如下图所示:

Task8 循环神经网络

它包括四个模块:I(Input),G(Generalization),O(Output),R(Response),另外还包括一些记忆单元用于存储记忆.

Input:输入模块,用于将文本资源(文档或这KB)和问题(question)等文本内容编码成向量.然后文本资源向量会作为Generalization模块的输入写入记忆单元中,而问题向量会作为Output模块的输入.

Generalization:泛化模块,用于对记忆单元的读写,也就是更新记忆的作用.

Output:输出模块,Output模块会根据Question(也会进过Input模块进行编码)对memory的内容进行权重处理,将记忆按照与Question的相关程度进行组合得到输出向量.

Response:响应模块,将Output输出的向量转为用于回复的自然语言答案.

七、 Text-RNN的原理

TextCNN擅长捕获更短的序列信息,但是TextRNN擅长捕获更长的序列信息。具体到文本分类任务中,BiLSTM从某种意义上可以理解为可以捕获变长且双向的N-Gram信息。

Task8 循环神经网络

八、利用Text-RNN模型来进行文本分类

1.preprocess.py

# coding: utf-8
 
import sys
from collections import Counter
 
import numpy as np
import tensorflow.contrib.keras as kr
 
if sys.version_info[0] > 2:
    is_py3 = True
else:
    reload(sys)
    sys.setdefaultencoding("utf-8")
    is_py3 = False
 
 
def native_word(word, encoding='utf-8'):
    """如果在python2下面使用python3训练的模型,可考虑调用此函数转化一下字符编码"""
    if not is_py3:
        return word.encode(encoding)
    else:
        return word
 
 
def native_content(content):
    if not is_py3:
        return content.decode('utf-8')
    else:
        return content
 
 
def open_file(filename, mode='r'):
    """
    常用文件操作,可在python2和python3间切换.
    mode: 'r' or 'w' for read or write
    """
    if is_py3:
        return open(filename, mode, encoding='utf-8', errors='ignore')
    else:
        return open(filename, mode)
 
 
def read_file(filename):
    """读取文件数据"""
    contents, labels = [], []
    with open_file(filename) as f:
        for line in f:
            try:
                label, content = line.strip().split('\t')
                if content:
                    contents.append(list(native_content(content)))
                    labels.append(native_content(label))
            except:
                pass
    return contents, labels
 
 
def build_vocab(train_dir, vocab_dir, vocab_size=5000):
    """根据训练集构建词汇表,存储"""
    data_train, _ = read_file(train_dir)
 
    all_data = []
    for content in data_train:
        all_data.extend(content)
 
    counter = Counter(all_data)
    count_pairs = counter.most_common(vocab_size - 1)
    words, _ = list(zip(*count_pairs))
    # 添加一个 <PAD> 来将所有文本pad为同一长度
    words = ['<PAD>'] + list(words)
    open_file(vocab_dir, mode='w').write('\n'.join(words) + '\n')
 
 
def read_vocab(vocab_dir):
    """读取词汇表"""
    # words = open_file(vocab_dir).read().strip().split('\n')
    with open_file(vocab_dir) as fp:
        # 如果是py2 则每个值都转化为unicode
        words = [native_content(_.strip()) for _ in fp.readlines()]
    word_to_id = dict(zip(words, range(len(words))))
    return words, word_to_id
 
 
def read_category():
    """读取分类目录,固定"""
    categories = ['体育', '财经', '房产', '家居', '教育', '科技', '时尚', '时政', '游戏', '娱乐']
 
    categories = [native_content(x) for x in categories]
 
    cat_to_id = dict(zip(categories, range(len(categories))))
 
    return categories, cat_to_id
 
 
def to_words(content, words):
    """将id表示的内容转换为文字"""
    return ''.join(words[x] for x in content)
 
 
def process_file(filename, word_to_id, cat_to_id, max_length=600):
    """将文件转换为id表示"""
    contents, labels = read_file(filename)
 
    data_id, label_id = [], []
    for i in range(len(contents)):
        data_id.append([word_to_id[x] for x in contents[i] if x in word_to_id])
        label_id.append(cat_to_id[labels[i]])
 
    # 使用keras提供的pad_sequences来将文本pad为固定长度
    x_pad = kr.preprocessing.sequence.pad_sequences(data_id, max_length)
    y_pad = kr.utils.to_categorical(label_id, num_classes=len(cat_to_id))  # 将标签转换为one-hot表示
 
    return x_pad, y_pad
 
 
def batch_iter(x, y, batch_size=64):
    """生成批次数据"""
    data_len = len(x)
    num_batch = int((data_len - 1) / batch_size) + 1
 
    indices = np.random.permutation(np.arange(data_len))
    x_shuffle = x[indices]
    y_shuffle = y[indices]
 
    for i in range(num_batch):
        start_id = i * batch_size
        end_id = min((i + 1) * batch_size, data_len)
        yield x_shuffle[start_id:end_id], y_shuffle[start_id:end_id]

2.rnn_model.py

# -*- coding: utf-8 -*-
 
import tensorflow as tf
 
class TRNNConfig(object):
    """RNN配置参数"""
 
    # 模型参数
    embedding_dim = 64      # 词向量维度
    seq_length = 600        # 序列长度
    num_classes = 10        # 类别数
    vocab_size = 5000       # 词汇表达小
 
    num_layers= 2           # 隐藏层层数
    hidden_dim = 128        # 隐藏层神经元
    rnn = 'gru'             # lstm 或 gru
 
    dropout_keep_prob = 0.8 # dropout保留比例
    learning_rate = 1e-3    # 学习率
 
    batch_size = 128         # 每批训练大小
    num_epochs = 10          # 总迭代轮次
 
    print_per_batch = 100    # 每多少轮输出一次结果
    save_per_batch = 10      # 每多少轮存入tensorboard
 
 
class TextRNN(object):
    """文本分类,RNN模型"""
    def __init__(self, config):
        self.config = config
 
        # 三个待输入的数据
        self.input_x = tf.placeholder(tf.int32, [None, self.config.seq_length], name='input_x')
        self.input_y = tf.placeholder(tf.float32, [None, self.config.num_classes], name='input_y')
        self.keep_prob = tf.placeholder(tf.float32, name='keep_prob')
 
        self.rnn()
 
    def rnn(self):
        """rnn模型"""
 
        def lstm_cell():   # lstm核
            return tf.contrib.rnn.BasicLSTMCell(self.config.hidden_dim, state_is_tuple=True)
 
        def gru_cell():  # gru核
            return tf.contrib.rnn.GRUCell(self.config.hidden_dim)
 
        def dropout(): # 为每一个rnn核后面加一个dropout层
            if (self.config.rnn == 'lstm'):
                cell = lstm_cell()
            else:
                cell = gru_cell()
            return tf.contrib.rnn.DropoutWrapper(cell, output_keep_prob=self.keep_prob)
 
        # 词向量映射
        with tf.device('/cpu:0'):
            embedding = tf.get_variable('embedding', [self.config.vocab_size, self.config.embedding_dim])
            embedding_inputs = tf.nn.embedding_lookup(embedding, self.input_x)
 
        with tf.name_scope("rnn"):
            # 多层rnn网络
            cells = [dropout() for _ in range(self.config.num_layers)]
            rnn_cell = tf.contrib.rnn.MultiRNNCell(cells, state_is_tuple=True)
 
            _outputs, _ = tf.nn.dynamic_rnn(cell=rnn_cell, inputs=embedding_inputs, dtype=tf.float32)
            last = _outputs[:, -1, :]  # 取最后一个时序输出作为结果
 
        with tf.name_scope("score"):
            # 全连接层,后面接dropout以及relu**
            fc = tf.layers.dense(last, self.config.hidden_dim, name='fc1')
            fc = tf.contrib.layers.dropout(fc, self.keep_prob)
            fc = tf.nn.relu(fc)
 
            # 分类器
            self.logits = tf.layers.dense(fc, self.config.num_classes, name='fc2')
            self.y_pred_cls = tf.argmax(tf.nn.softmax(self.logits), 1)  # 预测类别
 
        with tf.name_scope("optimize"):
            # 损失函数,交叉熵
            cross_entropy = tf.nn.softmax_cross_entropy_with_logits(logits=self.logits, labels=self.input_y)
            self.loss = tf.reduce_mean(cross_entropy)
            # 优化器
            self.optim = tf.train.AdamOptimizer(learning_rate=self.config.learning_rate).minimize(self.loss)
 
        with tf.name_scope("accuracy"):
            # 准确率
            correct_pred = tf.equal(tf.argmax(self.input_y, 1), self.y_pred_cls)
            self.acc = tf.reduce_mean(tf.cast(correct_pred, tf.float32))

3.run_rnn.py

# -*- coding: utf-8 -*-
 
import tensorflow as tf
 
class TRNNConfig(object):
    """RNN配置参数"""
 
    # 模型参数
    embedding_dim = 64      # 词向量维度
    seq_length = 600        # 序列长度
    num_classes = 10        # 类别数
    vocab_size = 5000       # 词汇表达小
 
    num_layers= 2           # 隐藏层层数
    hidden_dim = 128        # 隐藏层神经元
    rnn = 'gru'             # lstm 或 gru
 
    dropout_keep_prob = 0.8 # dropout保留比例
    learning_rate = 1e-3    # 学习率
 
    batch_size = 128         # 每批训练大小
    num_epochs = 10          # 总迭代轮次
 
    print_per_batch = 100    # 每多少轮输出一次结果
    save_per_batch = 10      # 每多少轮存入tensorboard
 
 
class TextRNN(object):
    """文本分类,RNN模型"""
    def __init__(self, config):
        self.config = config
 
        # 三个待输入的数据
        self.input_x = tf.placeholder(tf.int32, [None, self.config.seq_length], name='input_x')
        self.input_y = tf.placeholder(tf.float32, [None, self.config.num_classes], name='input_y')
        self.keep_prob = tf.placeholder(tf.float32, name='keep_prob')
 
        self.rnn()
 
    def rnn(self):
        """rnn模型"""
 
        def lstm_cell():   # lstm核
            return tf.contrib.rnn.BasicLSTMCell(self.config.hidden_dim, state_is_tuple=True)
 
        def gru_cell():  # gru核
            return tf.contrib.rnn.GRUCell(self.config.hidden_dim)
 
        def dropout(): # 为每一个rnn核后面加一个dropout层
            if (self.config.rnn == 'lstm'):
                cell = lstm_cell()
            else:
                cell = gru_cell()
            return tf.contrib.rnn.DropoutWrapper(cell, output_keep_prob=self.keep_prob)
 
        # 词向量映射
        with tf.device('/cpu:0'):
            embedding = tf.get_variable('embedding', [self.config.vocab_size, self.config.embedding_dim])
            embedding_inputs = tf.nn.embedding_lookup(embedding, self.input_x)
 
        with tf.name_scope("rnn"):
            # 多层rnn网络
            cells = [dropout() for _ in range(self.config.num_layers)]
            rnn_cell = tf.contrib.rnn.MultiRNNCell(cells, state_is_tuple=True)
 
            _outputs, _ = tf.nn.dynamic_rnn(cell=rnn_cell, inputs=embedding_inputs, dtype=tf.float32)
            last = _outputs[:, -1, :]  # 取最后一个时序输出作为结果
 
        with tf.name_scope("score"):
            # 全连接层,后面接dropout以及relu**
            fc = tf.layers.dense(last, self.config.hidden_dim, name='fc1')
            fc = tf.contrib.layers.dropout(fc, self.keep_prob)
            fc = tf.nn.relu(fc)
 
            # 分类器
            self.logits = tf.layers.dense(fc, self.config.num_classes, name='fc2')
            self.y_pred_cls = tf.argmax(tf.nn.softmax(self.logits), 1)  # 预测类别
 
        with tf.name_scope("optimize"):
            # 损失函数,交叉熵
            cross_entropy = tf.nn.softmax_cross_entropy_with_logits(logits=self.logits, labels=self.input_y)
            self.loss = tf.reduce_mean(cross_entropy)
            # 优化器
            self.optim = tf.train.AdamOptimizer(learning_rate=self.config.learning_rate).minimize(self.loss)
 
        with tf.name_scope("accuracy"):
            # 准确率
            correct_pred = tf.equal(tf.argmax(self.input_y, 1), self.y_pred_cls)
            self.acc = tf.reduce_mean(tf.cast(correct_pred, tf.float32))

九、 Recurrent Convolutional Neural Networks(RCNN)原理

首先对于每一个文本D,用一串序列w1,w2,....wn表示,并且把文本属于那个类别k的概率为p(k|D,xita),xita表示模型中的参数,RCNN模型主要包含三部分分别是递归CNN层、max-pooling层、输出层。在递归CNN层,对于每个词汇,RCNN会递归地计算其左侧上下文向量和右侧上下文向量,然后将这两部分向量与当前词汇的词向量进行拼接作为该词汇的向量表示,如图1所示。记Task8 循环神经网络Task8 循环神经网络分别为词汇Task8 循环神经网络的左侧上下文向量和右侧上下文向量,它们都是长度为Task8 循环神经网络的实数向量,计算公式分别如下:

Task8 循环神经网络

其中,Task8 循环神经网络Task8 循环神经网络分别表示前一个词汇和后一个词汇wi-1、wi+1的词向量,其向量长度为|e|,Task8 循环神经网络Task8 循环神经网络分别表示前一个词汇的左侧上下文向量和后一个词汇的右侧上下文向量,对于每个文本的第一个词汇的左侧上下文向量和最后一个词汇的右侧上下文向量,分别采用共享的参数向量Task8 循环神经网络Task8 循环神经网络表示,Task8 循环神经网络为权重矩阵,f为一个非线性**函数。接着,将这三个向量拼接起来作为当前词汇的向量表示,这样一来,每个词汇的向量就囊括了左侧和右侧的语义信息,使得词汇的向量表示更具有区分性,其表示如下:Task8 循环神经网络

当获取到每个词汇的向量表示Task8 循环神经网络后,RCNN会将每个向量传入一个带有tanh**函数的全连接层,其计算公式如下:Task8 循环神经网络

Task8 循环神经网络为潜在的语义向量。在max-pooling层,RCNN将每个潜在语义向量Task8 循环神经网络传入一个带softmax的全连接层,得到当前文本在各个类别的概率分布,其计算公式如下:Task8 循环神经网络

RCNN模型结构:

Task8 循环神经网络

十、 利用RCNN模型来进行文本分类(自选)

import os
import numpy as np
import tensorflow as tf
from eval.evaluate import accuracy
from tensorflow.contrib import slim
from loss.loss import cross_entropy_loss


class RCNN(object):
    def __init__(self,
                 num_classes,
                 seq_length,
                 vocab_size,
                 embedding_dim,
                 learning_rate,
                 learning_decay_rate,
                 learning_decay_steps,
                 epoch,
                 dropout_keep_prob,
                 context_dim,
                 hidden_dim):
        self.num_classes = num_classes
        self.seq_length = seq_length
        self.vocab_size = vocab_size
        self.embedding_dim = embedding_dim
        self.learning_rate = learning_rate
        self.learning_decay_rate = learning_decay_rate
        self.learning_decay_steps = learning_decay_steps
        self.epoch = epoch
        self.dropout_keep_prob = dropout_keep_prob
        self.context_dim = context_dim
        self.hidden_dim = hidden_dim
        self.input_x = tf.placeholder(tf.int32, [None, self.seq_length], name='input_x')
        self.input_y = tf.placeholder(tf.float32, [None, self.num_classes], name='input_y')
        self.model()

    def model(self):
        # 词向量映射
        with tf.name_scope("embedding"):
            embedding = tf.get_variable('embedding', [self.vocab_size, self.embedding_dim])
            embedding_inputs = tf.nn.embedding_lookup(embedding, self.input_x)

        # Recurrent Structure(CNN)
        with tf.name_scope("bi_rnn"):
            fw_cell = tf.nn.rnn_cell.BasicLSTMCell(self.context_dim)
            fw_cell = tf.nn.rnn_cell.DropoutWrapper(fw_cell, output_keep_prob=self.dropout_keep_prob)
            bw_cell = tf.nn.rnn_cell.BasicLSTMCell(self.context_dim)
            bw_cell = tf.nn.rnn_cell.DropoutWrapper(bw_cell, output_keep_prob=self.dropout_keep_prob)
            (output_fw, output_bw), states = tf.nn.bidirectional_dynamic_rnn(cell_fw=fw_cell,
                                                                             cell_bw=bw_cell,
                                                                             inputs=embedding_inputs,
                                                                             dtype=tf.float32)

        with tf.name_scope("context"):
            shape = [tf.shape(output_fw)[0], 1, tf.shape(output_fw)[2]]
            c_left = tf.concat([tf.zeros(shape), output_fw[:, :-1]], axis=1, name="context_left")
            c_right = tf.concat([output_bw[:, 1:], tf.zeros(shape)], axis=1, name="context_right")

        with tf.name_scope("word_representation"):
            y2 = tf.concat([c_left, embedding_inputs, c_right], axis=2, name="word_representation")
            embedding_size = 2 * self.context_dim + self.embedding_dim

        # max_pooling层
        with tf.name_scope("max_pooling"):
            fc = tf.layers.dense(y2, self.hidden_dim, activation=tf.nn.relu, name='fc1')
            fc_pool = tf.reduce_max(fc, axis=1)

        # output层
        with tf.name_scope("output"):
            self.logits = tf.layers.dense(fc_pool, self.num_classes, name='fc2')
            self.y_pred_cls = tf.argmax(tf.nn.softmax(self.logits), 1, name="pred")

        # 损失函数
        self.loss = cross_entropy_loss(logits=self.logits, labels=self.input_y)

        # 优化函数
        self.global_step = tf.train.get_or_create_global_step()
        learning_rate = tf.train.exponential_decay(self.learning_rate, self.global_step,
                                                   self.learning_decay_steps, self.learning_decay_rate,
                                                   staircase=True)

        optimizer = tf.train.AdamOptimizer(learning_rate)
        update_ops = tf.get_collection(tf.GraphKeys.UPDATE_OPS)
        self.optim = slim.learning.create_train_op(total_loss=self.loss, optimizer=optimizer, update_ops=update_ops)

        # 准确率
        self.acc = accuracy(logits=self.logits, labels=self.input_y)

    def fit(self, train_x, train_y, val_x, val_y, batch_size):
        # 创建模型保存路径
        if not os.path.exists('./saves/rcnn'): os.makedirs('./saves/rcnn')
        if not os.path.exists('./train_logs/rcnn'): os.makedirs('./train_logs/rcnn')

        # 开始训练
        train_steps = 0
        best_val_acc = 0
        # summary
        tf.summary.scalar('val_loss', self.loss)
        tf.summary.scalar('val_acc', self.acc)
        merged = tf.summary.merge_all()

        # 初始化变量
        sess = tf.Session()
        writer = tf.summary.FileWriter('./train_logs/rcnn', sess.graph)
        saver = tf.train.Saver(max_to_keep=10)
        sess.run(tf.global_variables_initializer())

        for i in range(self.epoch):
            batch_train = self.batch_iter(train_x, train_y, batch_size)
            for batch_x, batch_y in batch_train:
                train_steps += 1
                feed_dict = {self.input_x: batch_x, self.input_y: batch_y}
                _, train_loss, train_acc = sess.run([self.optim, self.loss, self.acc], feed_dict=feed_dict)

                if train_steps % 1000 == 0:
                    feed_dict = {self.input_x: val_x, self.input_y: val_y}
                    val_loss, val_acc = sess.run([self.loss, self.acc], feed_dict=feed_dict)

                    summary = sess.run(merged, feed_dict=feed_dict)
                    writer.add_summary(summary, global_step=train_steps)

                    if val_acc >= best_val_acc:
                        best_val_acc = val_acc
                        saver.save(sess, "./saves/rcnn/", global_step=train_steps)

                    msg = 'epoch:%d/%d,train_steps:%d,train_loss:%.4f,train_acc:%.4f,val_loss:%.4f,val_acc:%.4f'
                    print(msg % (i, self.epoch, train_steps, train_loss, train_acc, val_loss, val_acc))

        sess.close()

    def batch_iter(self, x, y, batch_size=32, shuffle=True):
        """
        生成batch数据
        :param x: 训练集特征变量
        :param y: 训练集标签
        :param batch_size: 每个batch的大小
        :param shuffle: 是否在每个epoch时打乱数据
        :return:
        """
        data_len = len(x)
        num_batch = int((data_len - 1) / batch_size) + 1

        if shuffle:
            shuffle_indices = np.random.permutation(np.arange(data_len))
            x_shuffle = x[shuffle_indices]
            y_shuffle = y[shuffle_indices]
        else:
            x_shuffle = x
            y_shuffle = y
        for i in range(num_batch):
            start_index = i * batch_size
            end_index = min((i + 1) * batch_size, data_len)
            yield (x_shuffle[start_index:end_index], y_shuffle[start_index:end_index])

    def predict(self, x):
        sess = tf.Session()
        sess.run(tf.global_variables_initializer())
        saver = tf.train.Saver(tf.global_variables())
        ckpt = tf.train.get_checkpoint_state('./saves/rcnn/')
        saver.restore(sess, ckpt.model_checkpoint_path)

        feed_dict = {self.input_x: x}
        logits = sess.run(self.logits, feed_dict=feed_dict)
        y_pred = np.argmax(logits, 1)
        return y_pred

在训练时,RCNN模型的LSTM隐藏层维度设置为200,前后文向量的维度也是设置为200,中间全连接层的隐藏层维度也是设置为200,其他的参数与之前FastText模型的一致,最终的模型在验证集上的效果如图2所示,其中,在经过762000次迭代后,模型在验证集上的准确率达到最高,为95.1%,在3000个测试集上的准确率是97.87%,比之前的LSTM_CNN大概高0.1%,可以发现RCNN在文本分类任务上的效果还是比较不错的。

Task8 循环神经网络

最终,对RCNN模型做个总结吧:

  • RCNN对词汇的向量表示进行拓展,加入了前、后上下文向量,可以捕捉更大范围的上下文信息。
  • 训练速度上还是没有FastText、TextCNN快