Press "Enter" to skip to content

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

我们构建一个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

 

init
forward
都是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. 准备训练数据,校验数据和测试数据(每个数据集的一组数据都是一个数字序列)

 

    1. 循环数数字序列,当前数字作为输入,下一个数字作为标签(即真实结果)

 

    1. 每次循环都经过一个rnn网络

 

    1. 计算每一组的损失t_loss并记录

 

    1. 优化器优化参数

 

    1. 重复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
...
# 结果不一一列出,大家可以自行尝试

 

总结

 

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

Be First to Comment

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注