PyTorch 入门与实践(七)循环神经网络(RNN)

来自 B 站刘二大人的《PyTorch深度学习实践》P12 的学习笔记

RNN Cell

循环神经网络的隐藏层都是线性层(Linear),由于它主要用于预测有前后关系的序列输入,所以它像斐波那契数列一样,后一次循环要输入前一次的输出,即,递归地求出下一次输出,故弹幕里有不少人称之为递归神经网络。
1
下图中的左边就是一层 RNN 的隐藏层,右边是它运行的过程(RNN Cell 一直是同一个,只是可视化运行的过程)。

指向下一次输入的红色箭头就是前一次的输出 h t h_t ht x t x_t xt 是数据加载器每次迭代得到的训练数据。
2
用一个 for 循环会更直观:

h = 0
for x in x_loader:
    h = RNNCell(x, h)

R N N C e l l ( ⋅ ) \mathrm {RNNCell} (\cdot) RNNCell() 对输入进行线性计算,再相加(信息融合),默认最后用 tanh 做激活函数,可选 ReLU
3
这里会有一个小小的疑惑: h t h_t ht.shape:(batch_size, hidden_size) x t x_t xt.shape:(batch_size, input_size) 不同,它们矩阵怎么相加?

如果我们去阅读 nn.RNNCell 的官方文档就会发现奥妙在两个线性变换 W h h h t − 1 + b h h W_{hh}h_{t-1} + b_{hh} Whhht1+bhh W i h h t + b i h W_{ih}h_{t} + b_{ih} Wihht+bih 之中。

RNNCell 的权重参数中:

  • W i h W_{ih} Wih(weight_ih)的 shape 是 (hidden_size, input_size)

  • W h h W_{hh} Whh(weight_hh)的 shape 是 (hidden_size, hidden_size)

  • b i h b_{ih} bih(bias_ih)和 b h h b_{hh} bhh(bias_hh)的 shape 都是 (hidden_size)

    所以,经过矩阵乘法 W h h h t − 1 + b h h W_{hh}h_{t-1} + b_{hh} Whhht1+bhh W i h h t + b i h W_{ih}h_{t} + b_{ih} Wihht+bih 后,输出是同样形状的矩阵 Output: (batch_size, hidden_size)
    00

RNN Cell in PyTorch

实例化一个 RNNCell 只需要两个参数:
3
我们不能忘记,pytorch 中任何数据输入都是 Mini-Batch:
4

RNNCell 示例

参数初始化:
5
输入输出的 shape:(batch_size, data_size)
6
运行一下代码示例更加容易理解:

import torch


batch_size = 1
seq_len = 3
input_size = 4
hidden_size = 2

cell = torch.nn.RNNCell(input_size=input_size, hidden_size=hidden_size)

# dataset shape: (seq, batch, features)
dataset = torch.randn(seq_len, batch_size, input_size)
hidden = torch.randn(batch_size, hidden_size)  # 初始化 hidden, 它是隐藏层的输出

for idx, input in enumerate(dataset):
    print("=" * 20, idx, "=" * 20)
    print("input size:", input.shape)
    
    # input: (batch_size, input_size); hidden: (batch_size, hidden_size)
    hidden = cell(input, hidden)  # 递归(循环)地计算 hidden

    print("hidden size:", hidden.shape)
    print(hidden)
RNN 示例

torch.nn.RNN(input_size, hidden_size, num_layers, batch_first=False) 要注意三点:

  • 输入:

    • input - tensor of shape ( s e q _ l e n , b a t c h , i n p u t _ s i z e ) (seq\_len, batch, input\_size) (seq_len,batch,input_size)
    • hidden - tensor of shape ( n u m _ l a y e r s , b a t c h , h i d d e n _ s i z e ) (num\_layers, batch, hidden\_size) (num_layers,batch,hidden_size)
  • num_layers - RNN 层数。例如,设置 num_layers=2 意味着将两个 RNN 堆叠在一起形成一个 stacked RNN,第二个 RNN 接收第一个 RNN 的输出并计算最终结果。默认值:1

  • torch.nn.RNN() 返回两个值:

    • output - tensor of shape ( s e q _ l e n , b a t c h , h i d d e n _ s i z e ) (seq\_len, batch, hidden\_size) (seq_len,batch,hidden_size)
    • hidden - tensor of shape ( n u m _ l a y e r s , b a t c h , h i d d e n _ s i z e ) (num\_layers, batch, hidden\_size) (num_layers,batch,hidden_size)
  • batch_first - if True, the input and output tensors are provided as:(𝑏𝑎𝑡𝑐ℎ𝑆𝑖𝑧𝑒, 𝑠𝑒𝑞𝐿𝑒𝑛, 𝑖𝑛𝑝𝑢𝑡_𝑠𝑖𝑧𝑒)

0
多层 RNN:
00
示例代码:

import torch

batch_size = 1
seq_len = 3
input_size = 4
hidden_size = 2
num_layers = 5

cell = torch.nn.RNN(input_size=input_size, hidden_size=hidden_size, num_layers=num_layers)

inputs = torch.randn(seq_len, batch_size, input_size)
hidden = torch.zeros(num_layers, batch_size, hidden_size)
out, hidden = cell(inputs, hidden)
print('Output size:', out.shape)  # shape(seq_len, batch, hidden_size) [3, 1, 2]
print('Output:', out)
print('Hidden size: ', hidden.shape)  # shape(num_layers, batch, hidden_size) [5, 1, 2]
print('Hidden: ', hidden)
训练一个 seq2seq RNN
一、用 RNNCell 训练一个能把 hello 翻译成 ohlol 的模型

遇到输入序列 h -> e -> l -> l -> o,我们期望这个 RNN 的输出是 ohlol
8
第一步,我们要用 one-hot 向量来表示字符串 hello 和 ohlol:
9
显然,input_size = features = one-hot矩阵的列数 = 4,则,inputs.shape = (seq_len, batch, 4),其中,seq_len = len("hello") = 5

那 output_size(hidden_size) 是多少?

因为我们每次迭代要预测出 4 个字母中的一个,相当于 4 分类问题,所以 output_size 是一个 4 维向量,故,output_size = 4

对于 多分类问题,我们使用 CrossEntropyLoss 做损失函数来训练网络。
10
训练输入是 one-hot 矩阵,label 是目标字符串 ohlol 对应的向量
9.5
相比 CNN,RNN 需要多实现一个初始化 hidden 的方法 init_hidden()
11
接下来就是常规的训练流程了,完整代码如下:

需要注意的一点是使用 RNNCell 的时候,loss 在反传前需要先累积所有的 seq,因为内层 for 循环一次得到一个 seq,输入 RNNCell 得到一个输出,RNNCell 一般在遍历完所有的 seq 后再反传。

import numpy as np
import torch
from torch import nn, optim


input_size = 4
hidden_size = 4
batch_size = 1

idx2char = ['e', 'h', 'l', 'o']
x_data = [1, 0, 2, 2, 3]
y_data = [3, 1, 2, 3, 2]

one_hot_lookup = np.eye(4)
x_one_hot = one_hot_lookup[x_data]

inputs = torch.Tensor(x_one_hot).view(-1, batch_size, input_size)  # torch.Size([5, 1, 4])
labels = torch.LongTensor(y_data).view(-1, 1)  # [5, 1]


class Model(nn.Module):
    def __init__(self, batch_size, input_size, hidden_size):
        super(Model, self).__init__()
        self.batch_size = batch_size
        self.input_size = input_size
        self.hidden_size = hidden_size

        self.rnncell = nn.RNNCell(self.input_size, self.hidden_size)

    def init_hidden(self):
        # 这个函数后面通过实例来调用,这不是方法覆写
        return torch.zeros(self.batch_size, self.hidden_size)

    def forward(self, input, hidden):
        hidden = self.rnncell(input, hidden)
        return hidden


model = Model(batch_size, input_size, hidden_size)

criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.1)

for epoch in range(10):
    loss = 0
    optimizer.zero_grad()
    hidden = model.init_hidden()
    print("predicted string: ", end="")

    for input, label in zip(inputs, labels):
        # 遍历第一维,即 seq_len,取出 seq_len 个 (batch, input_size) 的输入数据
        hidden = model(input, hidden)
        loss += criterion(hidden, label)  # 把每个 seq 的 loss 加起来,后面再反传
        _, idx = hidden.max(dim=1)   # 返回最大值和它的位置
        print(idx2char[idx.item()], end='')
    loss.backward()
    optimizer.step()
    print(f", epoch {epoch+1} loss={round(loss.item(), 4)}")
二、把 RNNCell 换成 RNN

数据不变,数据的形状要变。

有必要提一下,torch.nn.CrossEntropyLoss() 的实例 criterion(preds, targets) 的两个参数中,模型预测出的 preds 要求是一个形状为 (minibatch, C) 的 2D Tensor,targets 要求是一个 size 为 minibatch 的 1D Tensor。C 是类别总数。

idx2char = np.asarray(['e', 'h', 'l', 'o'])  # 方便后面取元素

inputs = torch.Tensor(x_one_hot).view(-1, batch_size, input_size)  # torch.Size([5, 1, 4])
labels = torch.LongTensor(y_data)  # [5]

class Model(nn.Module):
    def __init__(self, batch_size, input_size, hidden_size, num_layers):
        super(Model, self).__init__()
        self.batch_size = batch_size
        self.input_size = input_size
        self.hidden_size = hidden_size
        self.num_layers = num_layers  # num_layer

        self.rnn = nn.RNN(self.input_size, self.hidden_size, self.num_layers)

    def init_hidden(self):
        return torch.zeros(self.num_layers, self.batch_size, self.hidden_size)

    def forward(self, input, hidden):
        out, _ = self.rnn(input, hidden)  # 注意输出是两个,一般指返回第一个 out
        return out.view(-1, self.hidden_size)  # 改变形状为(seq_len × batch, hidden_size) 才能输入 criterion()

model = Model(batch_size, input_size, hidden_size, num_layers)

criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.1)

for epoch in range(10):
    # 只需要一层 for 循环
    optimizer.zero_grad()
    hidden = model.init_hidden()
    print("predicted string: ", end="")

    outputs = model(inputs, hidden)
    loss = criterion(outputs, labels)
    _, idx = outputs.max(dim=1)
    # print(idx.data)
    print(''.join(idx2char[idx.data]), end='')  # 不得不承认我这里比老师那里短
    loss.backward()
    optimizer.step()

    print(f", epoch {epoch+1} loss={round(loss.item(), 4)}")
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Skr.B

WUHOOO~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值