PyTorch中RNN、LSTM和GRU使用和理解

最近在看李沐老师动手学深度学习的序列模型,结合网上的一些资料做了一些汇总,解决了对于模型的理解和使用,在这里记录一下。

PyTorch中RNN、LSTM和GRU使用和理解

参考:简单易懂深入PyTorch中RNN、LSTM和GRU使用和理解-CSDN博客

nn.RNN
RNN 类描述

torch.nn.RNN 是 PyTorch 中用于构建一个简单的 Elman 循环神经网络(RNN)的类。它可以应用于输入序列,使用 tanhReLU 作为激活函数。下面将详细描述这个类的功能、参数和注意事项。

RNN 类的参数
  1. input_size: 输入特征的数量。
  2. hidden_size: 隐藏状态的特征数量。
  3. num_layers: RNN层的数量。例如,num_layers=2 表示两个 RNN 层堆叠在一起。
  4. nonlinearity: 使用的非线性激活函数,可以是 'tanh''relu'
  5. bias: 如果为 False,则层不使用偏置权重 b_ihb_hh
  6. batch_first: 如果为 True,则输入和输出张量的格式为 (batch, seq, feature);否则为 (seq, batch, feature)
  7. dropout: 如果非零,将在每个 RNN 层的输出上引入一个 Dropout 层,除了最后一层。
  8. bidirectional: 如果为 True,则变为双向 RNN。
输入和输出
  • 输入: input, h_0
    • input: 形状为 (seq_len, batch, input_size)(batch, seq_len, input_size)(如果 batch_first=True)的张量,包含输入序列的特征。
    • h_0: 形状为 (num_layers * num_directions, batch, hidden_size) 的张量,包含初始隐藏状态。如果未提供,默认为零。
  • 输出: output, h_n
    • output: 包含最后一层 RNN 的输出特征(h_t)的张量。
    • h_n: 形状为 (num_layers * num_directions, batch, hidden_size) 的张量,包含批次中每个元素的最终隐藏状态。
示例代码
import torch
import torch.nn as nn
# 创建 RNN 实例
rnn = nn.RNN(input_size=10, hidden_size=20, num_layers=2, nonlinearity='tanh')
# 对于hidden_size的理解:每一个时间步对应的神经元内部的参数个数
# 输入数据
input = torch.randn(5, 3, 10)  # (seq_len, batch, input_size)
h0 = torch.randn(2, 3, 20)     # (num_layers, batch, hidden_size)
# 前向传播
output, hn = rnn(input, h0)

这段代码展示了如何创建一个简单的 RNN 网络并进行前向传播。在这个例子中,input 是一个随机生成的输入张量,其维度是序列长度、批大小和输入大小。h0 是初始隐藏状态。输出 output 包含了每个时间步的隐藏状态,而 hn 是最终的隐藏状态。

nn.LSTM
LSTM 类描述

torch.nn.LSTM 是 PyTorch 中用于构建长短期记忆(LSTM)网络的类。LSTM 是一种特殊类型的循环神经网络(RNN),特别适合处理和预测时间序列数据中的长期依赖关系。

LSTM 类的参数
  1. input_size: 输入特征的数量。
  2. hidden_size: 隐藏状态的特征数量。
  3. num_layers: RNN层的数量。例如,num_layers=2 表示两个 LSTM 层堆叠在一起。
  4. bias: 如果为 False,则层不使用偏置权重 b_ihb_hh
  5. batch_first: 如果为 True,则输入和输出张量的格式为 (batch, seq, feature);否则为 (seq, batch, feature)
  6. dropout: 如果非零,将在每个 LSTM 层的输出上引入一个 Dropout 层,除了最后一层。
  7. bidirectional: 如果为 True,则变为双向 LSTM。
  8. proj_size: 如果大于 0,则使用带有投影的 LSTM。这将改变 LSTM 单元的输出维度和某些权重矩阵的形状。
输入和输出
  • 输入: input, (h_0, c_0)
    • input: 形状为 (seq_len, batch, input_size)(batch, seq_len, input_size)(如果 batch_first=True)的张量,包含输入序列的特征。
    • h_0: 形状为 (num_layers * num_directions, batch, hidden_size) 的张量,包含初始隐藏状态。
    • c_0: 形状为 (num_layers * num_directions, batch, hidden_size) 的张量,包含初始细胞状态。
  • 输出: output, (h_n, c_n)
    • output: 包含最后一层 LSTM 的输出特征(h_t)的张量。
    • h_n: 形状为 (num_layers * num_directions, batch, hidden_size) 的张量,包含序列中每个元素的最终隐藏状态。
    • c_n: 形状为 (num_layers * num_directions, batch, hidden_size) 的张量,包含序列中每个元素的最终细胞状态。
示例代码
import torch
import torch.nn as nn
# 创建 LSTM 实例
lstm = nn.LSTM(input_size=10, hidden_size=20, num_layers=2)
# 输入数据
input = torch.randn(5, 3, 10)  # (seq_len, batch, input_size)
h0 = torch.randn(2, 3, 20)     # (num_layers, batch, hidden_size)
c0 = torch.randn(2, 3, 20)     # (num_layers, batch, hidden_size)
# 前向传播
output, (hn, cn) = lstm(input, (h0, c0))

这段代码展示了如何创建一个 LSTM 网络并进行前向传播。在这个例子中,input 是一个随机生成的输入张量,其维度是序列长度、批大小和输入大小。h0c0 分别是初始隐藏状态和细胞状态。输出 output 包含了每个时间步的隐藏状态,而 (hn, cn) 是最终的隐藏状态和细胞状态。

nn.GRU
GRU 类描述

torch.nn.GRU 是 PyTorch 中用于构建门控循环单元(GRU)网络的类。GRU 是一种循环神经网络,类似于 LSTM,但结构更为简单,用于处理序列数据。

GRU 类的参数
  1. input_size: 输入特征的数量。
  2. hidden_size: 隐藏状态的特征数量。
  3. num_layers: RNN层的数量。例如,num_layers=2 表示两个 GRU 层堆叠在一起。
  4. bias: 如果为 False,则层不使用偏置权重 b_ihb_hh
  5. batch_first: 如果为 True,则输入和输出张量的格式为 (batch, seq, feature);否则为 (seq, batch, feature)
  6. dropout: 如果非零,将在每个 GRU 层的输出上引入一个 Dropout 层,除了最后一层。
  7. bidirectional: 如果为 True,则变为双向 GRU。
输入和输出
  • 输入: input, h_0
    • input: 形状为 (seq_len, batch, input_size)(batch, seq_len, input_size)(如果 batch_first=True)的张量,包含输入序列的特征。
    • h_0: 形状为 (num_layers * num_directions, batch, hidden_size) 的张量,包含初始隐藏状态。
  • 输出: output, h_n
    • output: 包含最后一层 GRU 的输出特征(h_t)的张量。
    • h_n: 形状为 (num_layers * num_directions, batch, hidden_size) 的张量,包含序列中每个元素的最终隐藏状态。
示例代码
import torch
import torch.nn as nn
# 创建 GRU 实例
gru = nn.GRU(input_size=10, hidden_size=20, num_layers=2)
# 输入数据
input = torch.randn(5, 3, 10)  # (seq_len, batch, input_size)
h0 = torch.randn(2, 3, 20)     # (num_layers, batch, hidden_size)
# 前向传播
output, hn = gru(input, h0)

这段代码展示了如何创建一个 GRU 网络并进行前向传播。在这个例子中,input 是一个随机生成的输入张量,其维度是序列长度、批大小和输入大小。h0 是初始隐藏状态。输出 output 包含了每个时间步的隐藏状态,而 hn 是最终的隐藏状态。

LSTM怎么得到最终的输出

参考:(94 封私信 / 80 条消息) pytorch对于双向LSTM,对于output,(h_n,c_n)=nn.LSTM(x)怎么取输出? - 知乎 (zhihu.com)

假设最后我们得到了output(batch_size, seq_len, 2 * hidden_size),我们需要将其输入到线性层,有以下两种方法可以参考:

(1)直接输入

和单向一样,我们可以将output直接输入到Linear。在单向LSTM中:

self.linear = nn.Linear(self.hidden_size, self.output_size)

而在双向LSTM中:

self.linear = nn.Linear(2 * self.hidden_size, self.output_size)

模型:

class BiLSTM(nn.Module):
    def __init__(self, input_size, hidden_size, num_layers, output_size, batch_size):
        super().__init__()
        self.input_size = input_size
        self.hidden_size = hidden_size
        self.num_layers = num_layers
        self.output_size = output_size
        self.num_directions = 2
        self.batch_size = batch_size
        self.lstm = nn.LSTM(self.input_size, self.hidden_size, self.num_layers, batch_first=True, bidirectional=True)
        self.linear = nn.Linear(self.num_directions * self.hidden_size, self.output_size)

    def forward(self, input_seq):
        h_0 = torch.randn(self.num_directions * self.num_layers, self.batch_size, self.hidden_size).to(device)
        c_0 = torch.randn(self.num_directions * self.num_layers, self.batch_size, self.hidden_size).to(device)
        # print(input_seq.size())
        seq_len = input_seq.shape[1]
        # input(batch_size, seq_len, input_size)
        # output(batch_size, seq_len, num_directions * hidden_size)
        output, _ = self.lstm(input_seq, (h_0, c_0))
        pred = self.linear(output)  # pred()
        pred = pred[:, -1, :] # 注意,这是batch_first=True的情况
        return pred

(2)处理后再输入

在LSTM中,经过线性层后的output的shape为(batch_size, seq_len, output_size)。假设我们用前24个小时(1 to 24)预测后2个小时的负荷(25 to 26),那么seq_len=24, output_size=2。根据LSTM的原理,最终的输出中包含了所有位置的预测值,也就是((2 3), (3 4), (4 5)…(25 26))。很显然我们只需要最后一个预测值,即output[:, -1, :]。

而在双向LSTM中,一开始output(batch_size, seq_len, 2 * hidden_size),这里面包含了所有位置的两个方向的输出。简单来说,output[0]为序列从左往右第一个隐藏层状态输出序列从右往左最后一个隐藏层状态输出的拼接;output[-1]为序列从左往右最后一个隐藏层状态输出和序列从右往左第一个隐藏层状态输出的拼接。

如果我们想要同时利用前向和后向的输出,我们可以将它们从中间切割,然后求平均。比如output的shape为(30, 24, 2 * 64),我们将其变成(30, 24, 2, 64),然后在dim=2上求平均,得到一个shape为(30, 24, 64)的输出,此时就与单向LSTM的输出一致了。

具体处理方法:

output = output.contiguous().view(self.batch_size, seq_len, self.num_directions, self.hidden_size)
output = torch.mean(output, dim=2)

模型代码:

class BiLSTM(nn.Module):
    def __init__(self, input_size, hidden_size, num_layers, output_size, batch_size):
        super().__init__()
        self.input_size = input_size
        self.hidden_size = hidden_size
        self.num_layers = num_layers
        self.output_size = output_size
        self.num_directions = 2
        self.batch_size = batch_size
        self.lstm = nn.LSTM(self.input_size, self.hidden_size, self.num_layers, batch_first=True, bidirectional=True)
        self.linear = nn.Linear(self.hidden_size, self.output_size)

    def forward(self, input_seq):
        h_0 = torch.randn(self.num_directions * self.num_layers, self.batch_size, self.hidden_size).to(device)
        c_0 = torch.randn(self.num_directions * self.num_layers, self.batch_size, self.hidden_size).to(device)
        # print(input_seq.size())
        seq_len = input_seq.shape[1]
        # input(batch_size, seq_len, input_size)
        input_seq = input_seq.view(self.batch_size, seq_len, self.input_size)
        # output(batch_size, seq_len, num_directions * hidden_size)
        output, _ = self.lstm(input_seq, (h_0, c_0))
        output = output.contiguous().view(self.batch_size, seq_len, self.num_directions, self.hidden_size)
        output = torch.mean(output, dim=2)
        pred = self.linear(output)
        # print('pred=', pred.shape)
        pred = pred[:, -1, :]
        return pred
输出层or隐藏层?

首先,pytorch中LSTM的输出一般用到的是输出层和隐藏层这两个,另一个细胞状态,我没咋用过,就不讲了。

一般两种用法,要么将输出层全连接然后得出结果,要么用隐藏层全连接,然后得出结果,有学长说用隐藏层效果会好一点。两种用法应该都可以。如果网络只有一层的时候,用输出层和隐藏层是没有任何区别的,当网络层数大于1时,才会有区别。

举个例子就懂了:

# coding: utf-8
import torch
import torch.nn as nn
import torchvision.transforms as transforms
import torchvision.datasets as datasets
import matplotlib.pyplot as plt

input_dim = 28  # 输入维度
hidden_dim = 100  # 隐层的维度
layer_dim = 1  # 1层LSTM
output_dim = 10  # 输出维度
BATCH_SIZE = 32  # 每批读取的
EPOCHS = 10  # 训练10轮


trainsets = datasets.MNIST(root="./data", train=True, download=True, transform=transforms.ToTensor())
testsets = datasets.MNIST(root="./data", train=False, transform=transforms.ToTensor())

train_loader = torch.utils.data.DataLoader(dataset=trainsets, batch_size=BATCH_SIZE, shuffle=True)
test_loader = torch.utils.data.DataLoader(dataset=testsets, batch_size=BATCH_SIZE, shuffle=True)


class LSTM_Model(nn.Module):
    def __init__(self, input_dim, hidden_dim, layer_dim, output_dim):
        super(LSTM_Model, self).__init__()
        self.hidden_dim = hidden_dim
        self.layer_dim = layer_dim
        self.lstm = nn.LSTM(input_dim, hidden_dim, layer_dim, batch_first=True)
        # 全连接层
        self.fc = nn.Linear(hidden_dim, output_dim)

    def forward(self, x):
        # layer_dim, batch_size, hidden_dim
        h0 = torch.zeros(self.layer_dim, x.size(0), self.hidden_dim).requires_grad_().to(device)
        # 初始化cell, state
        c0 = torch.zeros(self.layer_dim, x.size(0), self.hidden_dim).requires_grad_().to(device)
        # 分离隐藏状态,避免梯度爆炸
        lstm_out, (h_n, cn) = self.lstm(x, (h0.detach(), c0.detach()))
        print("-" * 10)
        print("lstm_out.shape", lstm_out.shape)
        print("lstm_out[:, -1].shape", lstm_out[:, -1].shape)
        print("-" * 10)
        print("h_n.shape", h_n.shape)
        print("-" * 10)
        feature_map = torch.cat([h_n[i, :, :] for i in range(h_n.shape[0])], dim=-1)
        print("feature_map.shape", feature_map.shape)
        print("-" * 10)
        print("lstm_out[:, -1]", lstm_out[:, -1])
        print("-" * 10)
        print("feature_map", feature_map)
        print("-" * 10)
        out = self.fc(feature_map)
        print("out", out.shape)
        return out

model = LSTM_Model(input_dim, hidden_dim, layer_dim, output_dim)

device = torch.device("cuda:0" if torch.cuda.is_available() else 'cpu')

# 损失函数
criterion = nn.CrossEntropyLoss()

# 优化器
learning_rate = 0.01
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)

# 模型训练
sequence_dim = 28  # 序列长度
loss_list = []
accuracy_list = []
iteration_list = []  # 迭代次数

iter = 0
for epoch in range(EPOCHS):
    for i, (images, labels) in enumerate(train_loader):
        model.train()
        images = images.view(-1, sequence_dim, input_dim).requires_grad_().to(device)
        labels = labels.to(device)
        optimizer.zero_grad()
        # 前向传播
        outputs = model(images)
        # 计算损失
        loss = criterion(outputs, labels)
        # 反向传播
        loss.backward()
        # 更新参数
        optimizer.step()
        # 计数器加1
        iter += 1
        # 模型验证
        if iter % 500 == 0:
            model.eval()  # 声明
            # 计算验证的accuracy
            correct = 0.0
            total = 0.0
            # 迭代测试集,获取数据、预测
            for images, labels in test_loader:
                images = images.view(-1, sequence_dim, input_dim).to(device)
                # 模型预测
                outputs = model(images)
                # 获取预测概率最大值的下标
                predict = torch.max(outputs.data, 1)[1]
                # 统计测试集的大小
                total += labels.size(0)
                # 统计判断预测正确的数量
                if torch.cuda.is_available():
                    correct += (predict.gpu() == labels.gpu()).sum()
                else:
                    correct += (predict == labels).sum()

                    # 计算accuracy
            accuracy = correct / total * 100
            loss_list.append(loss.data)
            accuracy_list.append(accuracy)
            iteration_list.append(iter)
            # 打印信息
            print("loos:{}, Loss:{}, Accuracy:{}".format(iter, loss.item(), accuracy))

plt.plot(iteration_list, loss_list)
plt.xlabel("Number of Iteration")
plt.ylabel("Loss")
plt.title("LSTM")
plt.show()


plt.plot(iteration_list, accuracy_list, color='r')
plt.xlabel("Number of iteration")
plt.ylabel("Accuracy")
plt.title("LSTM")
plt.show()

分析:

input_dim = 28 # 输入步长(seq_len)

hidden_dim = 100 # 隐层的维度(hidden_size)

layer_dim = 1 # 1层LSTM(num_layers)

output_dim = 10 # 输出维度

BATCH_SIZE = 32 # 每批读取的(batch_size)

lstm_out:【batch_size, seq_len, hidden_size * num_directions】

lstm_hn:【num_directions * num_layers, batch_size, hidden_size】

img

此时我们看到,输出层的最后一步的shape和拼接后的隐藏层的shape是一样的,我们可以打印出具体的数值。

img

可以看到,这两层的值是一样的,因此当网络为1层的时候输出层和隐藏层的结果没有任何区别。

img

最终输出是【batch_size, num_classes】,应该没什么疑问,因为是十分类任务。

但是当网络超过一层时,可以把上面的代码将层数直接改为2,不出意外应该会报错。

img

分析结果可以发现,隐藏层的结果比输出层的结果多了一层,同时观察打印出来的结果,可以发现,隐藏层不仅有输出层的信息,还有前一层隐藏层的信息,即隐藏层最终的信息是大于输出层的,因此,说隐藏层效果会好一点也很容易解释,因为包含的信息更多了。

结论

通过上述实例可以发现,用隐藏层代替输出层进入全连接层理论上应该会有更好的效果,且模型batch_first=True or False只对输出层有影响,而对隐藏层的输出是没有影响的,因此,以后都用隐藏层来全连接比好,也不容易搞错。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值