来自 B 站刘二大人的《PyTorch深度学习实践》P12 的学习笔记
循环神经网络的隐藏层都是线性层(Linear),由于它主要用于预测有前后关系的序列输入,所以它像斐波那契数列一样,后一次循环要输入前一次的输出,即,递归地求出下一次输出,故弹幕里有不少人称之为递归神经网络。
下图中的左边就是一层 RNN 的隐藏层,右边是它运行的过程(RNN Cell 一直是同一个,只是可视化运行的过程)。
指向下一次输入的红色箭头就是前一次的输出 h t h_t ht, x t x_t xt 是数据加载器每次迭代得到的训练数据。
用一个 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
这里会有一个小小的疑惑: 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} Whhht−1+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} Whhht−1+bhh 和 W i h h t + b i h W_{ih}h_{t} + b_{ih} Wihht+bih 后,输出是同样形状的矩阵 Output: (batch_size, hidden_size)
实例化一个 RNNCell 只需要两个参数:
我们不能忘记,pytorch 中任何数据输入都是 Mini-Batch:
参数初始化:
输入输出的 shape:(batch_size, data_size)
运行一下代码示例更加容易理解:
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)
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:(ℎ, , _)
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)
遇到输入序列 h -> e -> l -> l -> o
,我们期望这个 RNN 的输出是 ohlol
第一步,我们要用 one-hot 向量来表示字符串 hello 和 ohlol:
显然,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 做损失函数来训练网络。
训练输入是 one-hot 矩阵,label 是目标字符串 ohlol 对应的向量
相比 CNN,RNN 需要多实现一个初始化 hidden 的方法 init_hidden()
接下来就是常规的训练流程了,完整代码如下:
需要注意的一点是使用 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)}")
数据不变,数据的形状要变。
有必要提一下,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)}")