以一个简单的RNN为例梳理神经网络的训练过程

本文是学习完集智学园《PyTorch入门课程:火炬上的深度学习——自然语言处理(NLP)》系列课之后的梳理。

本次任务为预测字符(数字),让神经网络找到下面数字的规律。

012
00112
0001112
000011112
00000111112

当我们给定一组数据(如0000001)的时候,让神经网络去预测后面的数字应该是什么

1. 建立神经网络架构

我们构建一个RNN类

class simpleRNN(nn.Module):
    def __init():
        ...
    def forword():
        ...
    def initHidden():
        ...

其中函数initHidden的作用是初始化隐含层向量

def initHidden(self):
    # 对隐含单元的初始化
    # 注意尺寸是: layer_size, batch_size, hidden_size
    return Variable(torch.zeros(self.num_layers, 1, self.hidden_size))

使用init函数

init用于搭建神经网络的结构,网络的输入维度,输出维度,隐含层维度和数量,过程中需要用到的模型等等,都在init中定义。

其中nn是直接pytorch自带的模块,里面包含了内置的Embedding ,RNN, Linear, logSoftmax等模型,可以直接使用。

# 引入pytorch 中的 nn(模型模块)
import torch.nn as nn
def __init__(self, input_size, hidden_size, output_size, num_layers = 1):
        # 定义
        super(SimpleRNN, self).__init__()
        self.hidden_size = hidden_size
        self.num_layers = num_layers
        # 一个embedding层
        self.embedding = nn.Embedding(input_size, hidden_size)
        # PyTorch的RNN模型,batch_first标志可以让输入的张量的第一个维度表示batch指标
        self.rnn = nn.RNN(hidden_size, hidden_size, num_layers, batch_first = True)
        # 输出的全链接层
        self.linear = nn.Linear(hidden_size, output_size)
        # 最后的logsoftmax层
        self.softmax = nn.LogSoftmax()

使用forward函数作为神经网络的运算过程

运算过程也很好理解,就是将输入一步一步地走过嵌入层,rnn层,linear层,和softmax层

  • embedding(嵌入层):用于输入层到隐含层的嵌入。过程大致是把输入向量先转化为one-hot编码,再编码为一个hidden_size维的向量
  • RNN层:经过一层RNN模型
  • linear层(全链接层):将隐含层向量的所有维度一一映射到输出上,可以理解为共享信息
  • softmax:将数据归一化处理
 # 运算过程
def forward(self, input, hidden):
        # size of input:[batch_size, num_step, data_dim]
        
        # embedding层:
        # 从输入到隐含层的计算
        output = self.embedding(input, hidden)
        # size of output:[batch_size, num_step, hidden_size]
        
        output, hidden = self.rnn(output, hidden)
        # size of output:[batch_size, num_step, hidden_size]
      
        # 从输出output中取出最后一个时间步的数值,注意output输出包含了所有时间步的结果
        output = output[:,-1,:]
        # size of output:[batch_size, hidden_size]
        
        # 全链接层
        output = self.linear(output)
        # output尺寸为:batch_size, output_size
        
        # softmax层,归一化处理
        output = self.softmax(output)
         # size of output:batch_size, output_size
        return output, hidden

对RNN的训练结果中间有一个特别的操作

output = output[:, -1 ,:]

output尺寸为[batch_size, step, hidden_size], 这一步是把第二维时间步的数据只保留最后一个数。因为RNN的特征就是记忆,最后一步数据包含了之前所有步数的信息。所以这里只需要取最后一个数即可

使用这个init和forword

initforward都是python的class中内置的两个函数。

  • 如果你定义了__init__,那么在实例化类的时候就会自动运行init函数体,而且实例化的参数就是init函数的参数
  • 如果你定义了forward, 那么你在执行这个类的时候,就自动执行 forward函数
# 实例化类simpleRNN,此时执行__init__函数
rnn = simpleRNN(input_size = 4, hidden_size = 1, output_size = 3, num_layers = 1)

# 使用类simpleRNN
output, hidden = rnn(input, hidden)

那么执行一次forward就相当于一个训练过程:输入 -> 输出

2. 可以开始训练了

首先是构造损失函数优化器

强大的pytorch自带了通用的损失函数以及优化器模型。一句命令就搞定了一切。

criterion = torch.nn.NLLLoss()
optimizer = torch.optim.Adam(rnn.parameters(), lr = 0.001)

损失函数criterion: 用于记录训练损失,所有权重都会根据每一步的损失值来调整。这里使用的是NLLLoss损失函数,是一种比较简单的损失计算,计算真实值和预测值的绝对差值

# output是预测值,y是真实值
loss = criterion(output, y)

优化器optimizer: 训练过程的迭代操作。包括梯度反传和梯度清空。传入的参数为神经网络的参数rnn.parameters()以及学习率lr

# 梯度反传,调整权重
optimizer.zero_grad()
# 梯度清空
optimizer.step()

训练过程

训练的思路是:

  1. 准备训练数据,校验数据和测试数据(每个数据集的一组数据都是一个数字序列)
  2. 循环数数字序列,当前数字作为输入,下一个数字作为标签(即真实结果)
  3. 每次循环都经过一个rnn网络
  4. 计算每一组的损失t_loss并记录
  5. 优化器优化参数
  6. 重复1~5的训练步骤n次,n自定义

训练数据的准备不在本次的讨论范围内,所以这里直接给出处理好的结果如下。

train_set = [[3, 0, 0, 1, 1, 2],
            [3, 0, 1, 2],
            [3, 0, 0, 0, 1, 1, 1, 2],
            [3, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2]
            ...]

开始进行训练

# 重复进行50次试验
num_epoch = 50
loss_list = []
for epoch in range(num_epoch):
    train_loss = 0
    # 对train_set中的数据进行随机洗牌,以保证每个epoch得到的训练顺序都不一样。
    np.random.shuffle(train_set)
    # 对train_set中的数据进行循环
    for i, seq in enumerate(train_set):
        loss = 0
        # 对每一个序列的所有字符进行循环
        for t in range(len(seq) - 1):
            #当前字符作为输入
            x = Variable(torch.LongTensor([seq[t]]).unsqueeze(0))
            # x尺寸:batch_size = 1, time_steps = 1, data_dimension = 1
            # 下一个字符作为标签
            y = Variable(torch.LongTensor([seq[t + 1]]))
            # y尺寸:batch_size = 1, data_dimension = 1
            output, hidden = rnn(x, hidden) #RNN输出
            # output尺寸:batch_size, output_size = 3
            # hidden尺寸:layer_size =1, batch_size=1, hidden_size
            loss += criterion(output, y) #计算损失函数
        loss = 1.0 * loss / len(seq) #计算每字符的损失数值
        optimizer.zero_grad() # 梯度清空
        loss.backward() #反向传播
        optimizer.step() #一步梯度下降
        train_loss += loss #累积损失函数值
        # 把结果打印出来
        if i > 0 and i % 500 == 0:
            print('第{}轮, 第{}个,训练Loss:{:.2f}'.format(epoch, i, train_loss.data.numpy()[0] / i))
    loss_list.appand(train_loss)
            

这里的loss是对每一个训练循环(epoch)的损失,事实上无论训练的如何,这里的loss都会下降,因为神经网络就是会让最后的结果尽可能地靠近真实数据,所以训练集的loss其实并不能用来评价一个模型的训练好坏。

在实际的训练过程中,我们会在每一轮训练后,把得到的模型放入校验集去计算loss, 这样的结果更为客观。

校验集loss的计算和训练集完全一致,只不过把train_set替换成了valid_set,而且也不需要去根据结果优化参数,这在训练步骤中已经做了,校验集的作用就是看模型的训练效果:

for epoch in range(num_epoch):
    # 训练步骤
    ...
    valid_loss = 0
    for i, seq in enumerate(valid_set):
        # 对每一个valid_set中的字符串做循环
        loss = 0
        outstring = ''
        targets = ''
        hidden = rnn.initHidden() #初始化隐含层神经元
        for t in range(len(seq) - 1):
            # 对每一个字符做循环
            x = Variable(torch.LongTensor([seq[t]]).unsqueeze(0))
            # x尺寸:batch_size = 1, time_steps = 1, data_dimension = 1
            y = Variable(torch.LongTensor([seq[t + 1]]))
            # y尺寸:batch_size = 1, data_dimension = 1
            output, hidden = rnn(x, hidden)
            # output尺寸:batch_size, output_size = 3
            # hidden尺寸:layer_size =1, batch_size=1, hidden_size               
            loss += criterion(output, y) #计算损失函数
        loss = 1.0 * loss / len(seq)
        valid_loss += loss #累积损失函数值
#     # 打印结果
    print('第%d轮, 训练Loss:%f, 校验Loss:%f, 错误率:%f'%(epoch, train_loss.data.numpy() / len(train_set),valid_loss.data.numpy() / len(valid_set),1.0 * errors / len(valid_set)))

根据校验集的loss输出,我们可以绘制出最终的loss变化。

3. 测试模型预测效果

构造数据,测试模型是否能猜出当前数字的下一个数。成功率有多高
首先是构造数据,构造长度分别为0~20的数字序列

for n in range(20):
    inputs = [0] * n + [1] * n

然后对每一个序列进行测试

for n in range(20):
    inputs = [0] * n + [1] * n
    
    outstring = ''
    targets = ''
    diff = 0
    hiddens = []
    hidden = rnn.initHidden()
    for t in range(len(inputs) - 1):
        x = Variable(torch.LongTensor([inputs[t]]).unsqueeze(0))
        # x尺寸:batch_size = 1, time_steps = 1, data_dimension = 1
        y = Variable(torch.LongTensor([inputs[t + 1]]))
        # y尺寸:batch_size = 1, data_dimension = 1
        output, hidden = rnn(x, hidden)
        # output尺寸:batch_size, output_size = 3
        # hidden尺寸:layer_size =1, batch_size=1, hidden_size
        hiddens.append(hidden.data.numpy()[0][0])
        #mm = torch.multinomial(output.view(-1).exp())
        mm = torch.max(output, 1)[1][0]
        outstring += str(mm.data.numpy()[0])
        targets += str(y.data.numpy()[0])
         # 计算模型输出字符串与目标字符串之间差异的字符数量
        diff += 1 - mm.eq(y)
    # 打印出每一个生成的字符串和目标字符串
    print(outstring)
    print(targets)
    print('Diff:{}'.format(diff.data.numpy()[0]))

最终输出的结果为

[0, 1, 2]
[0, 1, 2]
Diff: 0
[0, 0, 1, 1, 2]
[0, 0, 1, 1, 2]
Diff: 0
[0, 0, 0, 1, 1, 1, 2]
[0, 0, 0, 1, 1, 1, 2]
Diff: 0
...
# 结果不一一列出,大家可以自行尝试

总结

神经网络可以理解为让计算机使用各种数学手段从一堆数据中找规律的过程。我们可以通过解剖一些简单任务来理解神经网络的内部机制。当面对复杂任务的时候,只需要把数据交给模型,它就能尽其所能地给你一个好的结果。

本文是学习完集智学园《PyTorch入门课程:火炬上的深度学习——自然语言处理(NLP)》系列课之后的梳理。课程中还有关于lstm, 翻译任务实操等基础而且丰富的知识点,我还会再回来的

你可能感兴趣的:(以一个简单的RNN为例梳理神经网络的训练过程)