分享

从0开始入门循环神经网络

本帖最后由 levycui 于 2017-11-29 17:25 编辑
问题导读:
1、什么是循环神经网络?
2、如何使用Onehot编码?
3、如何初始化模型参数?
4、如何理解梯度剪裁?




前面的教程里我们使用的网络都属于前馈神经网络。为什么叫前馈是整个网络是一条链(回想下gluon.nn.Sequential),每一层的结果都是反馈给下一层。这一节我们介绍循环神经网络,这里每一层不仅输出给下一层,同时还输出一个隐藏状态,给当前层在处理下一个样本时使用。下图展示这两种网络的区别。
2017-11-29_165921.jpg
循环神经网络的这种结构使得它适合处理前后有依赖关系的样本。我们拿语言模型举个例子来解释这个是怎么工作的。语言模型的任务是给定句子的前T个字符,然后预测第T+1个字符。假设我们的句子是“你好世界”,使用前馈神经网络来预测的一个做法是,在时间1输入“你”,预测”好“,时间2向同一个网络输入“好”预测“世”。下图左边展示了这个过程。
2017-11-29_165950.jpg
意到一个问题是,当我们预测“世”的时候只给了“好”这个输入,而完全忽略了“你”。直觉上“你”这个词应该对这次的预测比较重要。虽然这个问题通常可以通过n-gram来缓解,就是说预测第T+1个字符的时候,我们输入前n个字符。如果n=1,那就是我们这里用的。我们可以增大n来使得输入含有更多信息。但我们不能任意增大n,因为这样通常带来模型复杂度的增加从而导致需要大量数据和计算来训练模型。

循环神经网络使用一个隐藏状态来记录前面看到的数据来帮助当前预测。上图右边展示了这个过程。在预测“好”的时候,我们输出一个隐藏状态。我们用这个状态和新的输入“好”来一起预测“世”,然后同时输出一个更新过的隐藏状态。我们希望前面的信息能够保存在这个隐藏状态里,从而提升预测效果。

在更加正式的介绍这个模型前,我们先去弄一个比“你好世界“稍微复杂点的数据。

《时间机器》数据集

我们用《时间机器》这本书做数据集主要是因为古登堡计划计划使得可以免费下载,而且我们看了太多用莎士比亚作为例子的教程。下面我们读取这个数据并看看前面500个字符(char)是什么样的:
with open("../data/timemachine.txt") as f:
    time_machine = f.read()
print(time_machine[0:500])

输出:
The Time Traveller (for so it will be convenient to speak of him)
was expounding a recondite matter to us. His grey eyes shone and
twinkled, and his usually pale face was flushed and animated. The
fire burned brightly, and the soft radiance of the incandescent
lights in the lilies of silver caught the bubbles that flashed and
passed in our glasses. Our chairs, being his patents, embraced and
caressed us rather than submitted to be sat upon, and the

接着我们稍微处理下数据集。包括全部改为小写,去除换行符,然后截去后面一段使得接下来的训练会快一点。
time_machine = time_machine.lower().replace('\n', '').replace('\r', '')
time_machine = time_machine[0:10000]

字符的数值表示

先把数据里面所有不同的字符拿出来做成一个字典:
character_list = list(set(time_machine))
character_dict = dict([(char,i) for i,char in enumerate(character_list)])
vocab_size = len(character_dict)
print('vocab size:', vocab_size)
print(character_dict)

输出:
{'x': 0, 'm': 1, 'o': 2, ')': 3, 'j': 4, 'h': 5, 'w': 6, 'l': 7, ':': 8, '-': 9, 'q': 10, 'k': 11, '!': 12, 'c': 13, '[': 14, "'": 15, 'p': 16, '9': 17, 'n': 18, '1': 19, 's': 20, 'a': 21, 'e': 22, '.': 23, ']': 24, '?': 25, ';': 26, 'd': 27, 'i': 28, '8': 29, 'f': 30, 'u': 31, ',': 32, ' ': 33, 'y': 34, 'z': 35, 'g': 36, '(': 37, 't': 38, '_': 39, 'r': 40, 'v': 41, 'b': 42}

然后可以把每个字符转成从0开始的指数(index)来方便之后的使用。
time_numerical = [character_dict[char] for char in time_machine]
sample = time_numerical[:40]
print('chars: \n', ''.join([character_list[idx] for idx in sample]))
print('\nindices: \n', sample)

输出:
chars:
the time machine, by h. g. wells [1898]i
indices:
[38, 5, 22, 33, 38, 28, 1, 22, 33, 1, 21, 13, 5, 28, 18, 22, 32, 33, 42, 34, 33, 5, 23, 33, 36, 23, 33, 6, 22, 7, 7, 20, 33, 14, 19, 29, 17, 29, 24, 28]


数据读取

同前一样我们需要每次随机读取一些(batch_size个)样本和其对用的标号。这里的样本跟前面有点不一样,这里一个样本通常包含一系列连续的字符(前馈神经网络里可能每个字符作为一个样本)。

如果我们把序列长度(seq_len)设成10,那么一个可能的样本是The Time T。其对应的标号仍然是长为10的序列,每个字符是对应的样本里字符的后面那个。例如前面样本的标号就是he Time Tr。

下面代码每次从数据里随机采样一个批量:
import random
from mxnet import nd

def data_iter(batch_size, seq_len, ctx=None):
    num_examples = (len(time_numerical)-1) // seq_len
    num_batches = num_examples // batch_size
    # 随机化样本
    idx = list(range(num_examples))
    random.shuffle(idx)
    # 返回seq_len个数据
    def _data(pos):
        return time_numerical[pos:pos+seq_len]
    for i in range(num_batches):
        # 每次读取batch_size个随机样本
        i = i * batch_size
        examples = idx[i:i+batch_size]
        data = nd.array(
            [_data(j*seq_len) for j in examples], ctx=ctx)
        label = nd.array(
            [_data(j*seq_len+1) for j in examples], ctx=ctx)
        yield data, label

看下读出来长什么样:
for data, label in data_iter(batch_size=3, seq_len=8):
    print('data: ', data, '\n\nlabel:', label)
    break

输出:
data:
[[ 40.   2.  18.  36.  33.  20.  28.  27.]
[ 33.  38.  28.   1.  22.  33.  38.  40.]
[  2.  41.  28.  18.  13.  28.  21.   7.]]
<NDArray 3x8 @cpu(0)>

label:
[[  2.  18.  36.  33.  20.  28.  27.  22.]
[ 38.  28.   1.  22.  33.  38.  40.  21.]
[ 41.  28.  18.  13.  28.  21.   7.  33.]]
<NDArray 3x8 @cpu(0)>

循环神经网络
在对输入输出数据有了解后,我们来正式介绍循环神经网络。
2017-11-29_170301.jpg
2017-11-29_170504.jpg

Onehot编码

注意到每个字符现在是用一个整数来表示,而输入进网络我们需要一个定长的向量。一个常用的办法是使用onehot来将其表示成向量。就是说,如果值是i
, 那么我们创建一个全0的长为vocab_size的向量,并将其第i

位表示成1.
nd.one_hot(nd.array([0,4]), vocab_size)

输出:
[[ 1.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.
   0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.
   0.  0.  0.  0.  0.  0.  0.]
[ 0.  0.  0.  0.  1.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.
   0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.
   0.  0.  0.  0.  0.  0.  0.]]
<NDArray 2x43 @cpu(0)>

记得前面我们每次得到的数据是一个batch_size x seq_len的批量。下面这个函数将其转换成seq_len个可以输入进网络的batch_size x vocba_size的矩阵
def get_inputs(data):
    return [nd.one_hot(X, vocab_size) for X in data.T]

inputs = get_inputs(data)
print('input length: ',len(inputs))
print('input[0] shape: ', inputs[0].shape)

input length:  8
input[0] shape:  (3, 43)

初始化模型参数

模型的输入和输出维度都是vocab_size。
import mxnet as mx
# 尝试使用 GPU
import sys
sys.path.append('..')
import utils
ctx = utils.try_gpu()
print('Will use ', ctx)

num_hidden = 256
weight_scale = .01

# 隐含层
Wxh = nd.random_normal(shape=(vocab_size,num_hidden), ctx=ctx) * weight_scale
Whh = nd.random_normal(shape=(num_hidden,num_hidden), ctx=ctx) * weight_scale
bh = nd.zeros(num_hidden, ctx=ctx)
# 输出层
Why = nd.random_normal(shape=(num_hidden,vocab_size), ctx=ctx) * weight_scale
by = nd.zeros(vocab_size, ctx=ctx)

params = [Wxh, Whh, bh, Why, by]
for param in params:
    param.attach_grad()

Will use  gpu(0)

定义模型

我们将前面的模型公式定义直接写成代码。

def rnn(inputs, H):
    # inputs: seq_len 个 batch_size x vocab_size 矩阵
    # H: batch_size x num_hidden 矩阵
    # outputs: seq_len 个 batch_size x vocab_size 矩阵
    outputs = []
    for X in inputs:
        H = nd.tanh(nd.dot(X, Wxh) + nd.dot(H, Whh) + bh)
        Y = nd.dot(H, Why) + by
        outputs.append(Y)
    return (outputs, H)

做个简单的测试:
state = nd.zeros(shape=(data.shape[0], num_hidden), ctx=ctx)
outputs, state_new = rnn(get_inputs(data.as_in_context(ctx)), state)
print('output length: ',len(outputs))
print('output[0] shape: ', outputs[0].shape)
print('state shape: ', state_new.shape)

输出:
output length:  8
output[0] shape:  (3, 43)
state shape:  (3, 256)

预测序列

在做预测时我们只需要给定时间0的输入和起始隐藏状态。然后我们每次将上一个时间的输出作为下一个时间的输入。
2017-11-29_170609.jpg
def predict(prefix, num_chars):
    # 预测以 prefix 开始的接下来的 num_chars 个字符
    prefix = prefix.lower()
    state = nd.zeros(shape=(1, num_hidden), ctx=ctx)
    output = [character_dict[prefix[0]]]
    for i in range(num_chars+len(prefix)):
        X = nd.array([output[-1]], ctx=ctx)
        Y, state = rnn(get_inputs(X), state)
        #print(Y)
        if i < len(prefix)-1:
            next_input = character_dict[prefix[i+1]]
        else:
            next_input = int(Y[0].argmax(axis=1).asscalar())
        output.append(next_input)
    return ''.join([character_list for i in output])


梯度剪裁
2017-11-29_170703.jpg

def grad_clipping(params, theta):
    norm = nd.array([0.0], ctx)
    for p in params:
        norm += nd.sum(p.grad ** 2)
    norm = nd.sqrt(norm).asscalar()
    if norm > theta:
        for p in params:
            p.grad[:] *= theta/norm

训练模型

下面我们可以还是训练模型。跟前面前置网络的教程比,这里只有两个不同。

通常我们使用Perplexit(PPL)这个指标。可以简单的认为就是对交叉熵做exp运算使得数值更好读。
在更新前我们对梯度做剪裁

from mxnet import autograd
from mxnet import gluon
from math import exp

epochs = 200
seq_len = 35
learning_rate = .1
batch_size = 32

softmax_cross_entropy = gluon.loss.SoftmaxCrossEntropyLoss()

for e in range(epochs+1):
    train_loss, num_examples = 0, 0
    state = nd.zeros(shape=(batch_size, num_hidden), ctx=ctx)
    for data, label in data_iter(batch_size, seq_len, ctx):
        with autograd.record():
            outputs, state = rnn(get_inputs(data), state)
            # reshape label to (batch_size*seq_len, )
            # concate outputs to (batch_size*seq_len, vocab_size)
            label = label.T.reshape((-1,))
            outputs = nd.concat(*outputs, dim=0)
            loss = softmax_cross_entropy(outputs, label)
        loss.backward()

        grad_clipping(params, 5)
        utils.SGD(params, learning_rate)

        train_loss += nd.sum(loss).asscalar()
        num_examples += loss.size

    if e % 20 == 0:
        print("Epoch %d. PPL %f" % (e, exp(train_loss/num_examples)))
        print(' - ', predict('The Time Ma', 100))
        print(' - ', predict("The Medical Man rose, came to the lamp,", 100), '\n')

输出:
Epoch 0. PPL 30.966309
-  the time maaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
-  the medical man rose, came to the lamp,aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

Epoch 20. PPL 11.129305
-  the time mathe the the the the the the the the the the the the the the the the the the the the the the the the t
-  the medical man rose, came to the lamp, and and and and and and and and and and and and and and and and and and and and and and and and and

Epoch 40. PPL 9.489771
-  the time mat the the the the the the the the the the the the the the the the the the the the the the the the the
-  the medical man rose, came to the lamp, the the the the the the the the the the the the the the the the the the the the the the the the the

Epoch 60. PPL 8.317870
-  the time mant and have the thang the thang the thang the thang the thang the thang the thang the thang the thang
-  the medical man rose, came to the lamp, and have than the than the than the than the than the than the than the than the than the than the t

Epoch 80. PPL 7.294700
-  the time mave are it and he be way he meding and the time traveller and he in way his some traveller and he in w
-  the medical man rose, came to the lamp, and he in way his some traveller and he in way his some traveller and he in way his some traveller a

Epoch 100. PPL 6.312849
-  the time mading an a ther thing to the pat an at le sand the time traveller thin the time traveller thin the tim
-  the medical man rose, came to the lamp, the time traveller thin the time traveller thin the time traveller thin the time traveller thin the

Epoch 120. PPL 5.235753
-  the time mant move thengthe time traveller chovegred an a some traveller sime thang his on the time traveller ch
-  the medical man rose, came to the lamp, and the time traveller chovegred an a some traveller sime thang his on the time traveller chovegred

Epoch 140. PPL 4.408807
-  the time mavere at and the limensions, ard the time traveller cand the time traveller spoce of hald and the pacc
-  the medical man rose, came to the lamp, and the pacced hal gan thought rover ancedt and the time, and the ond have a monting to the one of t

Epoch 160. PPL 3.712035
-  the time mading of that is alle to spee of che rider, and hr ghas the for the medical man. sursaid the time trav
-  the medical man rose, came to the lamp, and the pay ur maling of the ind in alle to spee of che rider, and hr ghas the for the medical man.

Epoch 180. PPL 3.219052
-  the time mannot move about in time traveller simelanother at our why gente is a forest anowhy trovel tha pexpers
-  the medical man rose, came to the lamp, and hover ancepter atint to bot spofper--fical ol motracollyon,' said the medical man. our carealll

Epoch 200. PPL 2.823239
-  the time machine, but bact asmolly he ghat uxishen the time traveller cand ner allage bet berace for the redinee
-  the medical man rose, came to the lamp, ank in a maning in anyally mowe coon ther the focrous cint the lact the time traveller cand nean ofr

可以看到一开始学到简单的字符,然后简单的词,接着是复杂点的词,然后看上去似乎像个句子了。

结论
通过隐藏状态,循环神经网络很够更好的使用数据里的时序信息。

练习
调调参数(数据集大小,模型复杂度,学习率),看看对Perplexity和预测的结果造成的区别。

来源:http://zh.gluon.ai/chapter_recurrent-neural-networks/rnn-scratch.html
作者:szha

已有(1)人评论

跳转到指定楼层
aaronping 发表于 2017-11-30 09:46:33
高大尚,还得多看几次。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

关闭

推荐上一条 /2 下一条