双向RNN和双向LSTM

news/2024/12/22 14:05:08/

双向RNN和双向LSTM

一、双向循环神经网络BiRNN

1、为什么要用BiRNN

双向RNN,即可以从过去的时间点获取记忆,又可以从未来的时间点获取信息,也就是说具有以下两个特点:

捕捉前后文信息:传统的单向 RNN 只能利用先前的上下文信息,而 BiRNN 同时利用了输入序列的前后文信息。在很多任务中,如自然语言处理中的命名实体识别、机器翻译等,理解一个词的前后文语境至关重要。

例如:img

判断句子中Teddy是否是人名,如果只从前面两个词是无法得知Teddy是否是人名,如果能有后面的信息就很好判断了,这就需要用的双向循环神经网络。

提高精度:在处理某些序列数据时,单向 RNN 可能无法充分捕捉整个序列中的重要信息,导致性能欠佳。BiRNN 能够通过双向处理,提高模型的表达能力和准确度。

2、BiRNN的架构

双向循环神经网络(BRNN)的基本思想是提出每一个训练序列向前和向后分别是两个循环神经网络(RNN),而且这两个都连接着一个输出层。这个结构提供给输出层输入序列中每一个点的完整的过去和未来的上下文信息。下图展示的是一个沿着时间展开的双向循环神经网络。六个独特的权值在每一个时步被重复的利用,六个权值分别对应:输入到向前和向后隐含层(w1, w3),隐含层到隐含层自己(w2, w5),向前和向后隐含层到输出层(w4, w6)。值得注意的是:向前和向后隐含层之间没有信息流,这保证了展开图是非循环的。每一个输出都是综合考虑两个方向获得的结果再输出,如下图所示:

在这里插入图片描述

H → t = ϕ ( X t W x h ( f ) + H → t − 1 W h h ( f ) + b h ( f ) ) , H ← t = ϕ ( X t W x h ( b ) + H ← t + 1 W h h ( b ) + b h ( b ) ) , \begin{array}{l} \overrightarrow{\mathbf{H}}_{t}=\phi\left(\mathbf{X}_{t} \mathbf{W}_{x h}^{(f)}+\overrightarrow{\mathbf{H}}_{t-1} \mathbf{W}_{h h}^{(f)}+\mathbf{b}_{h}^{(f)}\right), \\ \overleftarrow{\mathbf{H}}_{t}=\phi\left(\mathbf{X}_{t} \mathbf{W}_{x h}^{(b)}+\overleftarrow{\mathbf{H}}_{t+1} \mathbf{W}_{h h}^{(b)}+\mathbf{b}_{h}^{(b)}\right), \end{array} H t=ϕ(XtWxh(f)+H t1Whh(f)+bh(f)),H t=ϕ(XtWxh(b)+H t+1Whh(b)+bh(b)),
拼接得到结果:
H t = [ H → t H ← t ] \mathbf{H}_{t}=\left[\overrightarrow{\mathbf{H}}_{t} \overleftarrow{\mathbf{H}}_{t}\right] Ht=[H tH t]
至于网络单元到底是标准的RNN还是GRU或者是LSTM是没有关系的

(GRU:把遗忘门和输入门合并成一个更新门(Update Gate),并且把Cell State和Hidden State也合并成一个Hidden State,它的计算如下图所示)

在这里插入图片描述

对于整个双向循环神经网络(BRNN)的计算过程如下:

向前推算(Forward pass):

对于双向循环神经网络(BRNN)的隐含层,向前推算跟单向的循环神经网络(RNN)一样,除了输入序列对于两个隐含层是相反方向的,输出层直到两个隐含层处理完所有的全部输入序列才更新:

img

向后推算(Backward pass):

双向循环神经网络(BRNN)的向后推算与标准的循环神经网络(RNN)通过时间反向传播相似,除了所有的输出层δ项首先被计算,然后返回给两个不同方向的隐含层:

img

3、代码(用IMDB进行情感分析)

import pandas as pd
import torch
import torch.nn as nn
import torch.optim as optim
from sklearn.model_selection import train_test_split
from torch.utils.data import Dataset, DataLoader
import spacy
from torchtext.vocab import GloVe# 加载数据集
base_csv = r'E:\kaggle\情感分析\archive (1)\IMDB Dataset.csv'
df = pd.read_csv(base_csv)# 分割数据集
train_data, test_data = train_test_split(df, test_size=0.2, random_state=42)# 保存为CSV文件
train_data.to_csv('train.csv', index=False)
test_data.to_csv('test.csv', index=False)# 加载Spacy分词器
spacy_en = spacy.load('en_core_web_sm')
tokenizer = spacy_en.tokenizerclass TextDataset(Dataset):def __init__(self, dataframe, text_field, label_field, vocab):self.dataframe = dataframeself.text_field = text_fieldself.label_field = label_fieldself.vocab = vocabdef __len__(self):return len(self.dataframe)def __getitem__(self, idx):text = self.dataframe.iloc[idx][self.text_field]label = self.dataframe.iloc[idx][self.label_field]tokens = tokenizer(text)token_ids = [self.vocab.get(token.text, self.vocab['<unk>']) for token in tokens]label = 1 if label == "positive" else 0return torch.tensor(token_ids, dtype=torch.long), torch.tensor(label, dtype=torch.long)# 加载预训练词向量
glove_embeddings = GloVe(name='6B', dim=100)
vocab = glove_embeddings.stoi
vocab['<unk>'] = len(vocab)  # 添加 <unk> 标记
glove_embeddings.vectors = torch.cat((glove_embeddings.vectors, torch.zeros(1, glove_embeddings.dim)), 0)# 创建数据集
train_dataset = TextDataset(train_data, 'review', 'sentiment', vocab)
test_dataset = TextDataset(test_data, 'review', 'sentiment', vocab)# 创建数据加载器
batch_size = 32def collate_fn(batch):texts, labels = zip(*batch)text_lengths = [len(text) for text in texts]padded_texts = nn.utils.rnn.pad_sequence(texts, batch_first=True, padding_value=0)return padded_texts, torch.tensor(labels, dtype=torch.long)train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True, collate_fn=collate_fn)
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False, collate_fn=collate_fn)# 定义模型
class BiRNN(nn.Module):def __init__(self, vocab_size, embed_size, hidden_size, num_layers, num_classes, pad_idx):super(BiRNN, self).__init__()self.hidden_size = hidden_sizeself.num_layers = num_layersself.num_directions = 2self.embedding = nn.Embedding(vocab_size, embed_size, padding_idx=pad_idx)self.rnn = nn.GRU(embed_size, hidden_size, num_layers, batch_first=True, bidirectional=True)self.fc = nn.Linear(hidden_size * self.num_directions, num_classes)def forward(self, x):h0 = torch.zeros(self.num_layers * self.num_directions, x.size(0), self.hidden_size).to(x.device)x = self.embedding(x)out, _ = self.rnn(x, h0)out = self.fc(out[:, -1, :])return out# 设置参数
vocab_size = len(vocab)
embed_size = 100
hidden_size = 256
num_layers = 2
num_classes = 2
pad_idx = 0model = BiRNN(vocab_size, embed_size, hidden_size, num_layers, num_classes, pad_idx)
model.embedding.weight.data.copy_(glove_embeddings.vectors)
model.embedding.weight.data[pad_idx] = torch.zeros(embed_size)# 定义损失函数和优化器
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = model.to(device)
criterion = criterion.to(device)# 训练模型
num_epochs = 5for epoch in range(num_epochs):model.train()epoch_loss = 0for texts, labels in train_loader:texts = texts.to(device)labels = labels.to(device)optimizer.zero_grad()outputs = model(texts)loss = criterion(outputs, labels)loss.backward()optimizer.step()epoch_loss += loss.item()print(f'Epoch {epoch + 1}/{num_epochs}, Loss: {epoch_loss / len(train_loader):.4f}')# 评估模型
model.eval()
with torch.no_grad():correct = 0total = 0for texts, labels in test_loader:texts = texts.to(device)labels = labels.to(device)outputs = model(texts)_, predicted = torch.max(outputs.data, 1)total += labels.size(0)correct += (predicted == labels).sum().item()accuracy = correct / totalprint(f'Accuracy: {accuracy * 100:.2f}%')# 保存模型
torch.save(model.state_dict(), 'model.ckpt')# 预测函数
def predict(text, model, vocab, device):model.eval()tokens = tokenizer(text)token_ids = [vocab.get(token.text, vocab['<unk>']) for token in tokens]token_tensor = torch.tensor(token_ids, dtype=torch.long).unsqueeze(0).to(device)with torch.no_grad():output = model(token_tensor)_, predicted = torch.max(output.data, 1)return 'positive' if predicted.item() == 1 else 'negative'# 示例使用
sample_text = "This movie was fantastic! I really enjoyed it."
prediction = predict(sample_text, model, vocab, device)
print(f'Prediction: {prediction}')

4、多层RNN

将许多RNN层堆叠,构成一个多层RNN网络。RNN每读取一个新的 x t \mathrm{x}_{\mathrm{t}} xt输就会生成状态向量 h t \mathrm{h}_{\mathrm{t}} ht作为当前时刻的输出和下一时刻的输入状态。 将T 个输入 x 0 ∼ x T \mathrm{x}_{0} \sim \mathrm{x}_{\mathrm{T}} x0xT 依次输入RNN,相应地会产生个输出。第一层RNN输出的T TT个状态向量可以作为第二层RNN的输入,第二层RNN拥有独立的参数,依次读取T个来自第一层RNN的状态向量,产生T个新的输出。第二层RNN输出的T个状态向量可以作为第三层RNN的输入,依此类推,构成一个多层RNN网络。
在这里插入图片描述

RNN自带层数和是否双向,只要把层数设置成你想要的层数就可以

class RNN(nn.Module):def __init__(self,vocab_size, embedding_dim, hidden_size, num_classes, num_layers,bidirectional):super(RNN, self).__init__()self.vocab_size = vocab_sizeself.embedding_dim = embedding_dimself.hidden_size = hidden_sizeself.num_classes = num_classesself.num_layers = num_layersself.embedding = nn.Embedding(self.vocab_size, embedding_dim, padding_idx=word2idx['<PAD>'])# 这里用了torch的embeding函数self.rnn = nn.RNN(input_size=self.embedding_dim, hidden_size=self.hidden_size,batch_first=True,num_layers=self.num_layers)# input_size:表示输入 xt 的特征维度,从embeding层的size# hidden_size:表示输出的特征维度# num_layers:表示网络的层数# nonlinearity:表示选用的非线性激活函数,默认是 ‘tanh’# bias:表示是否使用偏置,默认使用# batch_first:表示输入数据的形式,默认是 False,就是这样形式,(seq, batch, feature),也就是将序列长度放在第一位,batch 放在第二位# dropout:表示是否在输出层应用(随机丢掉一些特征,重要的调参)# bidirectional:表示是否使用双向的 rnn,默认是 False。def forward(self, x):batch_size, seq_len = x.shapeh0 = torch.randn(self.num_layers, batch_size, self.hidden_size).to(device)#初始化一个h0,也即c0,在RNN中一个Cell输出的ht和Ct是相同的,而LSTM的一个cell输出的ht和Ct是不同的#维度[layers, batch, hidden_len]x = self.embedding(x)   # 这里用的是nn的embedingout,_ = self.rnn(x, h0)  # 输入x,h0是初始化的特征,output = self.fc(out[:,-1,:]).squeeze(0) #因为有max_seq_len个时态,所以取最后一个时态即-1层return output   

二、双向LSTM----Bi-LSTM

BiLSTM无非就是把cell变成了LSTM的基本单元,其他同理如上所示

需要修改的内容:

  1. 替换RNN层为LSTM层:将nn.RNN替换为nn.LSTM
  2. 设置双向LSTM:将LSTM层的bidirectional参数设置为True
  3. 修改隐藏状态的初始化:LSTM的隐藏状态包含两个张量(隐藏状态和细胞状态),因此需要初始化这两个张量。
  4. 处理双向LSTM的输出:双向LSTM的输出维度会加倍(因为有两个方向)

在这里插入图片描述

class RNN(nn.Module):def __init__(self, vocab_size, embedding_dim, hidden_size, num_classes, num_layers, bidirectional):super(RNN, self).__init__()self.vocab_size = vocab_sizeself.embedding_dim = embedding_dimself.hidden_size = hidden_sizeself.num_classes = num_classesself.num_layers = num_layersself.bidirectional = bidirectionalself.num_directions = 2 if bidirectional else 1self.embedding = nn.Embedding(self.vocab_size, embedding_dim, padding_idx=word2idx['<PAD>'])self.lstm = nn.LSTM(input_size=self.embedding_dim, hidden_size=self.hidden_size, num_layers=self.num_layers, bidirectional=self.bidirectional, batch_first=True)#替换了原来的nn.RNN层,使其变为LSTM,并添加了双向参数# 全连接层的输入大小需要考虑双向LSTM的输出维度self.fc = nn.Linear(self.hidden_size * self.num_directions, self.num_classes)def forward(self, x):batch_size, seq_len = x.shape# 初始化隐藏状态和细胞状态,双向LSTM需要初始化2*num_layers的hidden statesh0 = torch.zeros(self.num_layers * self.num_directions, batch_size, self.hidden_size).to(device)c0 = torch.zeros(self.num_layers * self.num_directions, batch_size, self.hidden_size).to(device)x = self.embedding(x)  # Embedding layer# LSTM layerout, (hn, cn) = self.lstm(x, (h0, c0))# 取最后一个时间步的输出,双向LSTM会拼接两个方向的输出output = self.fc(out[:, -1, :])return output

三、两者异同

双向RNN(Bidirectional Recurrent Neural Network)和双向LSTM(Bidirectional Long Short-Term Memory)都是用于处理序列数据的神经网络模型。它们在处理时间序列、自然语言处理等任务中非常有用,因为它们能够考虑到序列中的前后信息。尽管它们有一些相似之处,但也有一些关键的不同点。

1、相同点

  1. 双向结构:无论是双向RNN还是双向LSTM,它们都有双向结构,这意味着它们有两个隐藏层,一个从前向后处理序列,另一个从后向前处理序列。这种结构允许模型在每个时间步上同时考虑前面和后面的信息,从而提高预测性能。

  2. 序列处理:两者都是为序列数据设计的,适用于自然语言处理、时间序列预测等任务。

  3. 隐层状态的结合:在每个时间步,它们都会结合来自两个方向的隐藏状态(通常是通过连接或求和)来产生输出。

2、不同点

  1. 基本单元

    • 双向RNN:基本单元是RNN,即简单的循环神经网络。它们依赖于隐藏状态将信息从一个时间步传播到下一个时间步。然而,RNN在处理长序列时容易遇到梯度消失和梯度爆炸问题,这限制了它们捕捉长距离依赖的能力。
    • 双向LSTM:基本单元是LSTM,即长短期记忆网络。LSTM通过引入遗忘门、输入门和输出门来控制信息的流动,从而更好地解决了梯度消失和梯度爆炸问题。因此,LSTM在处理长序列时比普通RNN更有效。
  2. 记忆能力

    • 双向RNN:由于其结构的限制,普通RNN在处理长期依赖关系时表现不佳。它们更适合短期依赖任务。
    • 双向LSTM:LSTM单元通过其门控机制能够有效地捕捉和保持长期依赖信息,因此在需要长时间记忆的任务中表现更优。
  3. 复杂性和计算成本

    • 双向RNN:结构相对简单,计算成本较低。但由于其局限性,可能需要更多层次的堆叠或者更复杂的架构来提升性能。
    • 双向LSTM:由于引入了门机制,LSTM单元比RNN单元复杂,计算成本也更高。但其增强的记忆能力通常能带来更好的性能。

参考https://zhuanlan.zhihu.com/p/519965073

https://fancyerii.github.io/books/rnn-intro/

https://www.cnblogs.com/Lee-yl/p/10066531.html


http://www.ppmy.cn/news/1461858.html

相关文章

C语言之旅:自定义类型(联合和枚举)

目录 一.联合体 1.1 联合体的声明 1.2 联合体的使用 1.3 为什么输出 4呢&#xff1f; 1.4 相同成员的结构体和联合体对比 1.5 联合体大小对比 1.6使用联合体判断大小端 二.枚举类型 2.1枚举类型的例举&#xff1a; 2.2枚举类型的优点 2.3 枚举类型的使用 在上篇文章中…

PyTorch之list、ndarray、tensor数据类型相互转换

温故而知新&#xff0c;可以为师矣&#xff01; 一、参考资料 python中list、numpy、torch.tensor之间的相互转换 二、常用操作 list 转 numpy ndarray np.array(list) import numpy as npa_list [[j for j in range(5)] for i in range(3)] a_ndarray np.array(a_lis…

智能终端RK3568主板在智慧公交条形屏项目的应用,支持鸿蒙,支持全国产化

基于AIoT-3568A的智慧公交条形屏&#xff0c;可支持公交线路动态展示&#xff0c;语音到站提醒&#xff0c;减少过乘、漏乘的情况&#xff0c;有效提高了公交服务效率和质量&#xff0c;为乘客提供了更舒适、更安全和更方便的出行体验&#xff0c;为城市的发展增添了新的活力。…

QT——tableWidget-跳变之舞V1.0-记录学习【1】

QT——tableWidget-跳变之舞V1.0-记录学习【1】 文章目录 QT——tableWidget-跳变之舞V1.0-记录学习【1】前言一、利用QT创建项目文件1.1 完整项目文件如下图所示:1.2 演示&#xff1a; 二、声明文件&#xff1a;2.1 主界面声明文件:mainwindow.h&#xff1b;2.2 控制窗口声明文…

Python中数据库操作pymysql和 sqlalchemy

在python中操作mysql数据库&#xff0c;主要用到两个库&#xff0c;pymysql和 sqlalchemy。分别进行介绍 安装 安装没啥好说的&#xff0c;其实就是pip install就完事 pip install pymysql pip install sqlalchemypymsql操作数据库 创建连接 以下语句省略了import语句&…

计算机网络-负载均衡算法

计算机网络中的负载均衡算法是决定如何将请求分发到各个服务器的关键。目前负载均衡算法主要分为静态负载均衡算法和动态负载均衡算法&#xff0c;具体包括以下几种&#xff1a; 静态负载均衡算法&#xff1a; 1.轮询法&#xff08;Round Robin&#xff09;&#xff1a;按照顺…

Ardupilot Rpanion iperf网络性能测试

Ardupilot Rpanion iperf网络性能测试 1. 源由2. 分析3. 安装4. 测试4.1 第一次测试4.1.1 iperf测试参数A4.1.1.1 测试链路14.1.1.2 测试链路24.1.1.3 测试链路3 4.1.2 iperf测试参数B - 测试链路34.1.2.1 测试数据4.1.2.2 数据简单分析4.1.2.3 数据深入分析4.1.2.4 模拟测试网…

element-plus 工作经验总结

Element-plus 文章目录 Element-plus忠告: 最好锁定版本, 免得更新更出 BUG 来了el-drawer 设置 modal"false" 后, 遮罩元素仍存在, 点不了空白的地方el-tree 大数据量时接收 check-change 事件报错导致涉及多个节点的操作没执行完毕el-table 表头 show-overflow-too…