6. 循环神经网络实现 2025-06-22

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使用投影。他的运作方式包括以下几步。首先,h_t的维度将从hidden_size 转换为proj_size ( W_{hi}的维度也会同时被改变)。第二,每一个层的隐含状态输出将与一个(可学习)的投影矩阵相乘:

。注意,这种投影模式同样对LSTM的输出有影响,即变成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值做交叉熵损失,进而误差方向传播,梯度更新。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容