以一个简单的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, 翻译任务实操等基础而且丰富的知识点,我还会再回来的

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容