(一)定义模型

Gluon的rnn模块提供了循环神经网络的实现。下面构造一个含单隐藏层、隐藏单元个数为256的循环神经网络层rnn_layer,并对权重做初始化。

首先回顾一下循环神经网络从零开始的模型过程:

  1. 首先利用init_rnn_state函数来返回初始化的隐藏状态,一个形状为(批量大小,隐藏单元个数)的值为0的NDArray组成的元组。
  2. 然后通过to_onehot函数把(批量大小,时间步数)转换成num_steps个形状为(batch_size,vocab_size)的矩阵。
  3. 然后通过get_params函数得到初始化的模型参数。
  4. 最后利用rnn函数得到输出和更新的隐藏状态。
  5. 循环神经网络预测天气 循环神经网络代码实现_循环神经网络预测天气

  6. 现在来看看gluon的rnn模块:
    class mxnet.gluon.rnn.RNN(hidden_size, num_layers=1, activatinotallow='relu', layout='TNC',dropout=0,bidirectinotallow=False, i2h_weight_initializer=None, h2h_weight_initializer=None, i2h_bias_initializer='zeros', h2h_bias_initializer='zeros', input_size=0, dtype='float32', **kwargs)

循环神经网络预测天气 循环神经网络代码实现_循环神经网络_02


举例:

layer = mx.gluon.rnn.RNN(100, 3)
layer.initialize()
input = mx.nd.random.uniform(shape=(5, 3, 10))
# by default zeros are used as begin state
output = layer(input)
# manually specify begin state.
h0 = mx.nd.random.uniform(shape=(3, 3, 100))
output, hn = layer(input, h0)

rnn_layer的输入形状为(时间步数,批量大小,输入个数)。其中输入个数即one-hot向量长度(词典大小)。rnn.RNN实例在前向计算返回的隐藏状态指的是隐藏层在最后时间步的可用于初始化下一时间步的隐藏状态:当隐藏层有多层时,每一层的隐藏状态都会记录在该变量中。

循环神经网络预测天气 循环神经网络代码实现_初始化_03


注意这里的Y是矩阵,而state是list型。

定义完整的循环神经网络class RNNModel
首先把输入数据用one-hot向量表示后输入到rnn_layer中,然后使用全连接层输出(输出个数为词典大小)

class RNNModel(nn.Block):
       def __init__(self,rnn_layer,vocab_size,**kwargs):
            super(RNNModel,self).__init__(**kwargs)
            self.rnn=rnn_layer
            self.vocab_size=vocab_size
            self.dense=nn.Dense(vocab_size)
       def forward(self,inputs,state):
            x=nd.one_hot(inputs.T,self.vocab_size)  #将输入转置为(num_steps,batch_size)后获取one-hot向量表示
            Y,state=self.rnn(X,state)
            output=self.dense(Y.reshape((-1,Y.shape[-1])))  # 通过Y.shape[-1]得到Y的最后一维的大小(num_hiddens),然后利用reshape得到Y的形状为(num_steps * batch_size, num_hiddens)
            #  最后全连接层得到的输出形状是(num_steps * batch_size, vocab_size)
            return output,state
       def begin_state(self,*args,**kwargs):
            return self.rnn.begin_state(*args,**kwargs)  # 调用rnn_layer的成员函数begin_state来返回初始化的隐藏状态列表

(二)训练模型

首先定义预测函数predict_rnn_gluon,和之前的预测函数基本一致,只是初始化和前向计算的接口有一点区别。

# 本函数已保存在d2lzh包中方便以后使用
def predict_rnn_gluon(prefix, num_chars, model, vocab_size, ctx, idx_to_char,
                      char_to_idx):
    # 使用model的成员函数来初始化隐藏状态
    state = model.begin_state(batch_size=1, ctx=ctx)
    output = [char_to_idx[prefix[0]]]
    for t in range(num_chars + len(prefix) - 1):
        X = nd.array([output[-1]], ctx=ctx).reshape((1, 1))
        (Y, state) = model(X, state)  # 前向计算不需要传入模型参数
        if t < len(prefix) - 1:
            output.append(char_to_idx[prefix[t + 1]])
        else:
            output.append(int(Y.argmax(axis=1).asscalar()))
    return ''.join([idx_to_char[i] for i in output])

使用权重为随机值的模型来预测一次:

ctx=d2l.try_gpu()
model.RNNModel(rnn_layer,vocab_size)
model.initialize(force_reinit=True,ctx=ctx)
predict_rnn_gluon('分开',10,model,vocab_size,idx_to_char,char_to_idx)

实现训练函数。算法同从零开始实现:

def train_and_predict_rnn_gluon(model,num_hiddens,vocab_size,ctx,corpus_indices,idx_to_char,char_to_idx,num_epochs_num_steps,lr,clipping_theta,batch_size,pred_period,pred_len,prefixes):
    loss=gloss.SoftmaxCrossEntropyLoss()
    model.initialize(ctx=ctx,force_reinit=True,init=iniit.Normal(0.01))
    # 由于采用的是相邻采样,所以在epoch开始时初始化隐藏状态
    trainer=gluon.Trainer(model.collect_params(),'sgd',{'learning_rate':lr,'moment':0,'wd':0})
    for epoch in range(num_epochs):
      l_sum,n,start=0.0,0,time.time()
      data_iter=d2l.data_iter_consecutive(corpus_indices,batch_size,num_steps,ctx)
      # 相邻采样
      state=model.begin_state(batch_size=batch_size,ctx=ctx)
      for X,Y in data_iter:
         for s in state:
             s.detach()  # 利用detach函数从计算图中分离隐藏状态
         with autograd.record():
             (output,state)=model(X,state)  # 直接在模型内部对X进行变换并求解
             y=Y.T.reshape((-1,))  # Y的形状和输入的Xxing'z形状一样,都是(batch_size,num_steps),xu'y需要变换
             l=loss(output,y).mean() # shi使用交叉熵损失计算平均分类误差
         l.backward()
         params=[p.data() for p in model.collect_params().values()]
         d2l.grad_clipping(params,clipping_theta,ctx)
         trainer.step(1)  # 因为误差已经取过均值,梯度不用再做平均
         l_sum+=l.asscalar()*y.size
         n+=y.size
      if (epoch+1) % pred_period == 0:
         print('epoch %d,perplexity %f,time %.2f sec' % (epoch+1,math.exp(l_sum/n),time.time()-start))
         for prefix in prefixes:
             print(' -',predict_rnn_gluon(prefix,pred_len,model,voab_size,ctx,dix_to_char,char_to_idx))

(三)训练模型

num_epochs, batch_size, lr, clipping_theta = 250, 32, 1e2, 1e-2
pred_period, pred_len, prefixes = 50, 50, ['分开', '不分开']
train_and_predict_rnn_gluon(model, num_hiddens, vocab_size, ctx,
                            corpus_indices, idx_to_char, char_to_idx,
                            num_epochs, num_steps, lr, clipping_theta,
                            batch_size, pred_period, pred_len, prefixes)