1. 数据准备
batch_size, num_steps = 32, 35
# 返回迭代器(X,Y)元组,X Y的尺寸均为(batch_size, num_steps),里面是编码数字
# 默认前10000词元,不采用随机采样
train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps)
def load_data_time_machine(batch_size, num_steps, #@save
use_random_iter=False, max_tokens=10000):
"""返回时光机器数据集的迭代器和词表"""
data_iter = SeqDataLoader(
batch_size, num_steps, use_random_iter, max_tokens)
return data_iter, data_iter.vocab
class SeqDataLoader: #@save
"""加载序列数据的迭代器"""
def __init__(self, batch_size, num_steps, use_random_iter, max_tokens):
self.corpus, self.vocab = d2l.load_corpus_time_machine(max_tokens)
def load_corpus_time_machine(max_tokens=-1): #@save
vocab = Vocab(tokens)
# 所以将所有文本行展平到一个列表中
corpus = [vocab[token] for line in tokens for token in line]
2. rnn的实现
下面是rnn的正向传播的实现,
输入为数据inputs和初始隐状态state,注意:inputs将时间维度提前了,形状为(时间步数量,批量大小,词表大小)。
输出为预测logits,和最末隐状态H,注意:预测logits的形状为(时间步数量*批量大小,词表大小)。
循环神经网络的结构就是一个输入按顺序循环计算,一个时间步就是对应一个位置的词元,将小批量数据全部送入网络。单独拿出来一个时间步,输入就是(批量大小,词表大小),跟全连接网络是类似的。
在本质上,也是计算完整个batch内的样本预测输出后,再进行整个batch损失的计算
def rnn(inputs, state, params):
# inputs的形状:(时间步数量,批量大小,词表大小)
# 下面是初始化好了的权重
W_xh, W_hh, b_h, W_hq, b_q = params
# 此为隐状态H,由于rnn没有记忆元(memory cell) C,故只取隐状态H即可
H, = state
# 保存输出
outputs = []
# X的形状:(批量大小,词表大小)
for X in inputs:
# 在每一个时间步计算小批量的隐状态和输出
# 这里H是不断更新的,保证循环网络进行
H = torch.tanh(torch.mm(X, W_xh) + torch.mm(H, W_hh) + b_h)
Y = torch.mm(H, W_hq) + b_q
# 将每一个时间步所有样本的输出保存到outputs中,
# outputs最终保存所有时间步(在前),小批量的输出预测
outputs.append(Y)
# torch.cat() 能够将多个形状相似的张量沿指定维度进行拼接
# outputs 是一个包含多个张量的序列(如列表或元组),dim=0 表示在第 0 维上进行拼接。
# 例如,若outputs中的每个张量形状为 (a, b),拼接后在第 0 维上的大小会变为所有张量在第 0 维大小之和,而第 1 维大小b保持不变,新张量形状为 (a1 + a2 + ... + an, b)。
# num_inputs = num_outputs = vocab_size
# 即输出(时间步数量*批量大小,词表大小)
# 在本质上,也是计算完整个batch内的样本预测输出后,进行整个batch损失的计算
return torch.cat(outputs, dim=0), (H,)
问题:outputs.append(Y) 这条语句能在GPU上运行吗
outputs.append(Y) 是 Python 列表的方法调用,属于 CPU 端的操作逻辑,始终在 CPU 上执行。
无论 Y 是 CPU 张量还是 GPU 张量,append 操作本身只是将张量的 引用 添加到列表中,不会触发数据在设备间的移动。
后续操作torch.cat(outputs, dim=0)也在 GPU 上执行,可直接使用这些张量,无需手动转移设备。相当于CPU发号司令,GPU执行操作。
看官网nn.RNN、nn.LSTM、nn.GRU的实现
- 初始化:最重要的是前2个参数,即输入和隐状态的最后一个维度,输入最后一个维度也就是词元的特征向量维度;可以看到batch_first=False,即时间步优先,在每一个时间步执行小批量操作。
class RNN(RNNBase):
__init__(input_size,hidden_size,num_layers=1,nonlinearity='tanh',bias=True,batch_first=False,dropout=0.0,bidirectional=False,device=None,dtype=None)
class LSTM(RNNBase):
__init__(input_size,hidden_size,num_layers=1,bias=True,batch_first=False,dropout=0.0,bidirectional=False,proj_size=0,device=None,dtype=None)
class GRU(RNNBase):
__init__(input_size,hidden_size,num_layers=1,bias=True,batch_first=False,dropout=0.0,bidirectional=False,device=None,dtype=None)
- 输入、输出:
输入与上述实现一致(默认时间步优先),
输出与上述不一样,由初始化中hidden_size参数可以看出,仅输出隐状态,如下代码可验证
# 1. 数据准备
batch_size, num_steps = 32, 35
train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps)
# 2. 定义模型
num_hiddens = 256
rnn_layer = nn.RNN(len(vocab), num_hiddens)
# 使用张量来初始化隐状态,它的形状是(隐藏层数,批量大小,隐藏单元数)
# 由于上述RNN使用默认1层,故这里直接初始化1个隐藏层
state = torch.zeros((1, batch_size, num_hiddens))
# 3. 模拟正向传播
# 通过一个隐状态和一个输入,就可以用更新后的隐状态计算输出。
# 需要强调的是,rnn_layer的“输出”(Y)不涉及输出层的计算: 它是指每个时间步的隐状态,这些隐状态可以用作后续输出层的输入。
X = torch.rand(size=(num_steps, batch_size, len(vocab)))
Y, state_new = rnn_layer(X, state)
Y.shape, state_new.shape
# 输出为(torch.Size([35, 32, 256]), torch.Size([1, 32, 256]))
# Y为35个时间步,32个样本(小批量)中每个词元的隐状态,包含最后一个时间步32个样本(小批量)state_new
3. 完整的循环神经网络模型
定义一个RNNModel类。 注意,rnn_layer只包含隐藏的循环层,我们还需要创建一个单独的输出层。
#@save
class RNNModel(nn.Module):
"""循环神经网络模型"""
def __init__(self, rnn_layer, vocab_size, **kwargs):
super(RNNModel, self).__init__(**kwargs)
self.rnn = rnn_layer
self.vocab_size = vocab_size
self.num_hiddens = self.rnn.hidden_size
# 如果RNN是双向的(之后将介绍),num_directions应该是2,否则应该是1
if not self.rnn.bidirectional:
self.num_directions = 1
self.linear = nn.Linear(self.num_hiddens, self.vocab_size)
else:
self.num_directions = 2
self.linear = nn.Linear(self.num_hiddens * 2, self.vocab_size)
def forward(self, inputs, state):
# inputs.T对输入转置,时间维度提前,并在最后一维进行one-hot编码
# 方便后续进行单个时间步的操作,也符合rnn的正向传播要求(时间步优先)
X = F.one_hot(inputs.T.long(), self.vocab_size)
X = X.to(torch.float32)
Y, state = self.rnn(X, state)
# 全连接层首先将Y的形状改为(时间步数*批量大小,隐藏单元数)
# 它的输出形状是(时间步数*批量大小,词表大小)。
output = self.linear(Y.reshape((-1, Y.shape[-1])))
return output, state
def begin_state(self, device, batch_size=1):
if not isinstance(self.rnn, nn.LSTM):
# nn.GRU以张量作为隐状态
return torch.zeros((self.num_directions * self.rnn.num_layers,
batch_size, self.num_hiddens),
device=device)
else:
# nn.LSTM以元组作为隐状态
return (torch.zeros((
self.num_directions * self.rnn.num_layers,
batch_size, self.num_hiddens), device=device),
torch.zeros((
self.num_directions * self.rnn.num_layers,
batch_size, self.num_hiddens), device=device))
这是一个通用的循环神经网络类,可传入RNN、LSTM、GRU等模块,即rnn_layer、lstm_layer、gru_layer;由于RNN、LSTM、GRU在网络内部可设置深度num_layers、双向bidirectional学习,进而也可进行深度和双向模型的构建。
3. LSTM、GRU
参考https://blog.csdn.net/weixin_53146190/article/details/120341669
LSTM初始化比GRU多了proj_size=0参数,
如果参数指定proj_size > 0,则将对LSTM使用投影。他的运作方式包括以下几步。首先,的维度将从hidden_size 转换为proj_size (
的维度也会同时被改变)。第二,每一个层的隐含状态输出将与一个(可学习)的投影矩阵相乘:
输入Inputs: input, (h_0, c_0)
input:当batch_first = False 时形状为(L,N,H_in),当 batch_first = True 则为(N, L, H_in) ,包含批量样本的时间序列输入。该输入也可是一个可变换长度的时间序序列,参见 torch.nn.utils.rnn.pack_padded_sequence() 或者是 torch.nn.utils.rnn.pack_sequence() 了解详情。
h_0:形状为(D∗num_layers, N, H_out),指的是包含每一个批量样本的初始隐含状态。如果模型未提供(h_0, c_0) ,默认为是全0矩阵。
c_0:形状为(D∗num_layers, N, H_cell), 指的是包含每一个批量样本的初始记忆细胞状态。 如果模型未提供(h_0, c_0) ,默认为是全0矩阵。
其中:
N = 批量大小
L = 序列长度
D = 2 如果模型参数bidirectional = 2,否则为1
H_in = 输入的特征大小(input_size)
H_cell = 隐含单元数量(hidden_size)
H_out = proj_size, 如果proj_size > 0, 否则的话 = 隐含单元数量(hidden_size)
输出Outputs: output, (h_n, c_n)
output: 当batch_first = False 形状为(L, N, D∗H_out) ,当batch_first = True 则为 (N, L, D∗H_out) ,包含LSTM最后一层每一个时间步长 的输出特征()。如果输入的是torch.nn.utils.rnn.PackedSequence,则输出同样将是一个packed sequence。
h_n: 形状为(D∗num_layers, N, H_out),包括每一个批量样本最后一个时间步的隐含状态。
c_n: 形状为(D∗num_layers, N, H_cell),包括每一个批量样本最后一个时间步的记忆细胞状态。
https://blog.csdn.net/weixin_41744192/article/details/115270178
这个博文详细讲解了LSTM输入矩阵的尺寸。
可以想象,多层多方向的权重在一个小批量所有时间步(所有词元)计算好之后,输出小批量所有时间步(所有词元)预测标签,与真实的Y值做交叉熵损失,进而误差方向传播,梯度更新。