RNN,即循环神经网络(Recurrent Neural Network),是一种用于处理序列数据的神经网络。与传统的前馈神经网络不同,RNN具有内部的记忆机制,可以记住之前输入的信息,并将其用于当前输入的处理中。这使得RNN非常适合于处理和预测时间序列数据、自然语言处理、语音识别等任务。
整体来看,RNN有被取代的趋势,但RNN是理解大模型的核心,大模型里的很多理念都来源于RNN。学习RNN不是为了怎么使用,而是分析为何消亡,这样才能更深刻的理解 transformer为何活着, transformer是点对点解决RNN的问题。
RNN的基本结构
在RNN中,信息不仅沿着输入向量传播,还从一个步骤传递到下一个步骤。这意味着每个输出都依赖于之前的计算,并且所有计算都涉及相同的参数集。这种结构允许网络学习到序列中的模式,即使这些模式出现在不同的时间点上。
RNN的问题
-
梯度消失/爆炸问题:在训练深度RNN时,可能会遇到梯度消失或梯度爆炸的问题。这是因为通过时间的反向传播(BPTT)会将错误信号传回很多时间步长,导致权重更新变得非常小(消失)或非常大(爆炸)。这使得网络难以学习长期依赖关系。
-
无法有效捕捉长距离依赖:由于上述梯度问题,标准RNN在处理非常长的序列时可能表现不佳,尤其是在需要记住很久之前的信息时。
解决方案
为了克服这些问题,研究人员开发了多种改进的RNN模型:
-
长短期记忆网络(LSTM, Long Short-Term Memory):LSTM是一种特殊的RNN类型,它能够学习长期依赖关系。LSTM通过引入“门”的概念来控制信息流,包括遗忘门、输入门和输出门,从而有效地解决了梯度消失问题。
-
门控循环单元(GRU, Gated Recurrent Unit):GRU是另一种改进型RNN,它简化了LSTM的设计,将遗忘门和输入门合并为更新门,并且没有单独的输出门。尽管结构更简单,但在许多任务中,GRU的表现与LSTM相当,有时甚至更好。
-
双向RNN(BiRNN, Bidirectional RNN):BiRNN由两个RNN组成,一个处理正向的时间序列,另一个处理反向的时间序列。这允许网络同时考虑过去和未来的信息,对于某些任务来说是非常有用的。
-
深度RNN:通过堆叠多个RNN层,可以创建深度RNN,以提高模型的能力。然而,这也增加了模型的复杂性和计算需求。
RNN的使用
1. RNN 要解决什么问题?
- 时序信号的抽取特征
- 顺序
2. RNN 的解决方法
- 循环
- 逐个处理
- 前后依赖
- 后一项直接依赖前一项,间接依赖前面所有的项
- 前后进行【手递手】传递
- 中间隐藏状态
3. RNN API 如何调用?
- nn.RNN
- 自动挡(自动循环)
- 用于编码器
- nn.RNNCell
- 手动挡(手动循环)
- 用于解码器

时序共享,每一步用的是同一个A;进来多少步就出去多少步,即(X)进来70步,出去的(h)也是70步。hn是最后一步,最后一步很重要,我们经常用最后一步去解决很多问题。因为理论上所有步的信息都聚合到了最后一步上。h0也需要写上,因为解码自回归的时候需要。
所有的网络都是在处理特征,这是人工智能的核心问题,不论是全链接、卷积、RNN循环和transformer,它们都是在处理特征,所以我们只关心特征的维度。
import torch
from torch import nn
"""
1. Simple RNN
"""
"""
时序类数据结构:[seq_len, batch_size, embedding_dim]
seq_len:序列长度
batch_size:批量数据有多少个
embedding_dim:嵌入维度,也就是每个词是多大的向量
"""
# 比如一个短信70个字,3个短信,每个字是256个维度
X = torch.randn(70, 3, 256)
h0 = torch.zeros(1, 3, 512, dtype=torch.float32)
X.shape, h0.shape
# 构建一个循环神经网络,输入层维度256,隐藏层维度512
rnn = nn.RNN(input_size=256, hidden_size=512)
# 调用RNN
out, hn = rnn(X, h0)
# [seq_len, batch_size, hidden_size]
out.shape
# [1, batch_size, hidden_size]
hn.shape
LSTM 博客
http://colah.github.io/posts/2015-08-Understanding-LSTMs/

当序列很长时,前面的都忘了,所以增加一个能够记忆的东西,这就有了LSTM。
"""
2、 LSTM 长短期记忆网络
Long 长
Short 短
Term 期
Memory 记忆
"""
"""
导入必要的模块
导入 PyTorch 库及其神经网络模块 nn
"""
import torch
from torch import nn
"""
(1)定义 LSTM 层
"""
# Inputs: input, (h_0, c_0)
# Outputs: output, (h_n, c_n)
# 创建一个 LSTM 层,输入维度为 256,隐藏层维度为 512
lstm = nn.LSTM(input_size=256, hidden_size=512)
"""
(2)准备输入数据和初始状态
"""
# 准备输入数据(70个时间步,每个时间步有3个样本,每个样本的特征维度为256)
X = torch.randn(70, 3, 256)
# 初始化隐藏状态h0和细胞状态c0,1个LSTM层,3个样本,每个样本的隐藏状态维度为512
h0 = torch.zeros(1, 3, 512, dtype=torch.float32)
c0 = torch.zeros(1, 3, 512, dtype=torch.float32)
"""
(3)前向传播
"""
#out是LSTM的输出,hn是最后一个时间步的隐藏状态,cn是最后一个时间步的细胞状态
out, (hn, cn) = lstm(X, (h0, c0))
"""
(4)检查输出的形状
"""
# [70, 3, 512]
out.shape
# [1, 3, 512]
hn.shape
# [1, 3, 512]
cn.shape
"""
(1)定义 LSTMCell 层
"""
# Inputs: input, (h_0, c_0)
# Outputs: (h_1, c_1)
#创建一个 LSTMCell 层,输入维度为 256,隐藏层维度为 512
lstm_cell = nn.LSTMCell(input_size=256, hidden_size=512)
"""
(2)准备输入数据和初始状态
"""
# 准备输入数据
X = torch.randn(70, 3, 256)
# 初始化隐藏状态 h0 和细胞状态 c0
h0 = torch.zeros(3, 512, dtype=torch.float32)
c0 = torch.zeros(3, 512, dtype=torch.float32)
"""
(3)处理第一个时间步的数据
"""
X0 = X[0, :, :]
X0.shape
"""
(4)前向传播
"""
hn, cn = lstm_cell(X0, (h0, c0))
"""
(5)检查输出的形状
"""
hn.shape
cn.shape
X.size(0)
"""
(6)循环处理所有时间步的数据
"""
out = []
for x in X:
h0, c0 = lstm_cell(x, (h0, c0))
out.append(h0)
"""
(7)拼接所有时间步的输出
"""
# 最终所有步的短期状态
out = torch.stack(tensors=out, dim=0)
"""
(8)获取最后一步的状态
"""
hn = h0
cn = c0
"""
总结:
LSTM 层:处理整个序列数据,返回所有时间步的输出和最后一个时间步的隐藏状态及细胞状态。
LSTMCell 层:逐个时间步处理数据,返回每个时间步的隐藏状态和细胞状态。
"""
信息
- CRUD工程师
- 增删改查
- Create(创建):向数据库中添加新的记录或数据项。
- Read(读取):检索或查询数据库中的现有数据。这可以是获取单个记录、一组记录或者执行复杂的查询以获取特定的数据视图。
- Update(更新):修改数据库中已有的记录。这可能涉及更改单个字段的值或多个字段的值。
- Delete(删除):从数据库中移除记录。请注意,删除操作通常是不可逆的,因此在执行删除操作之前应该谨慎考虑,并且在某些情况下,可能会有逻辑删除(例如,设置一个“已删除”标志)而不是物理删除。
- LSTM
- 1. 门控思想
- 2. CRUD思想
- (1)遗忘门:先丢掉一部分不重要信息
- (2)输入门:增加重要的信息
- GRU
- 核心思想:吃 LSTM 的红利,化简 LSTM
- 调用层面:跟Simple RNN是一样的
情感识别 sentiment analysis
1. 业务本质:

2. 技术本质:
- 文本分类
3. 流程:


"""
需求:酒店评论分析
"""
"""
1. 样本路径和类别读取
"""
"""
训练数据聚合
"""
import os
train_root = os.path.join("hotel", "train")
train_texts = []
train_labels = []
for label in os.listdir(train_root):
label_root = os.path.join(train_root, label)
for file in os.listdir(label_root):
file_path = os.path.join(label_root, file)
# 聚合结果
train_texts.append(file_path)
train_labels.append(label)
# 打印数据
len(train_texts), len(train_labels)
"""
测试数据聚合
"""
test_root = os.path.join("hotel","test")
test_texts = []
test_labels = []
for label in os.listdir(test_root):
label_root = os.path.join(test_root,label)
if os.path.isdir(label_root):
for file in os.listdir(label_root):
file_path = os.path.join(label_root,file)
test_texts.append(file_path)
test_labels.append(label)
else:
print(f"跳过非目录项:{label_root}")
# 打印数据
len(test_texts),len(test_labels)
"""
2. 构建分词器
分词,把句子变 token
把所有不同的token聚在一起
做 0 ~ N-1 的编码
"""
SEQ_LEN = 30
import jieba
# pip install opencc -U
import opencc
class Tokenizer(object):
"""
定义一个分词器
"""
def __init__(self, X, y):
"""
训练的语料
"""
self.X = X
self.y = y
self.t2s = opencc.OpenCC(config="t2s")
self._build_dict()
def _build_dict(self):
"""
构建字典
"""
# 1. 获取所有的 token
words = {"<PAD>", "<UNK>"}
for file in self.X:
# 打开文件
with open(file=file, mode="r", encoding="gbk", errors="ignore") as f:
text = f.read().replace("\n", "")
text = self.t2s.convert(text=text)
words.update(set(jieba.lcut(text)))
# 2. 构建文本字典
self.word2idx = {word: idx for idx, word in enumerate(words)}
self.idx2word = {idx: word for word, idx in self.word2idx.items()}
# 3. 删掉 数据集
del self.X
# 4. 构建标签字典
labels = set(train_labels)
self.label2idx = {label: idx for idx, label in enumerate(labels)}
self.idx2label = {idx: label for label, idx in self.label2idx.items()}
# 5. 删除 数据集
del self.y
def encode(self, text, seq_len=SEQ_LEN):
"""
text --> tokens --> ids
自我扩展:
- 右侧截断或填充
- 左边?
- 随机?
"""
# 1. 繁体转简体
text = text.replace("\n", "")
text = self.t2s.convert(text=text)
# 2. 分词
text = jieba.lcut(text)
# 3. 统一长度
text = (text + ["<PAD>"] * seq_len)[:seq_len]
# 4. 转 id
ids = [self.word2idx.get(word, self.word2idx.get("<UNK>")) for word in text]
return ids
def decode(self, ids):
"""
ids --> tokens --> text
"""
text = "".join([self.idx2word.get(_id, "") for _id in ids])
return text
def __str__(self):
"""
输: 分词器基本信息
"""
return f"""
Tokenizer Info:
--> Num of Tokens: {len(self.word2idx)}
--> Num of Labels: {len(self.label2idx)}
"""
def __repr__(self):
return self.__str__()
输出结果:

"""
3. 打包数据
"""
from torch.utils.data import Dataset
from torch.utils.data import DataLoader
import torch
class HotelCommentDataset(Dataset):
"""
自定义数据集
"""
def __init__(self, X, y, seq_len=SEQ_LEN):
"""
初始化
"""
self.X = X
self.y = y
self.seq_len = seq_len
def __getitem__(self, idx):
"""
索引操作
返回第idx个样本
"""
# 1. 文本
file = self.X[idx]
with open(file=file, mode="r", encoding="gbk", errors="ignore") as f:
text = f.read()
ids = tokenizer.encode(text=text, seq_len=self.seq_len)
ids = torch.tensor(data=ids, dtype=torch.long)
# 2. 标签
label = self.y[idx]
label = tokenizer.label2idx.get(label)
label = torch.tensor(data=label, dtype=torch.long)
return ids, label
def __len__(self):
return len(self.X)
# 1. 定义一个分词器
tokenizer = Tokenizer(X=train_texts, y=train_labels)
tokenizer
# 打包数据
train_dataset = HotelCommentDataset(X=train_texts, y=train_labels)
train_dataloader = DataLoader(dataset=train_dataset, shuffle=True, batch_size=128)
test_dataset = HotelCommentDataset(X=test_texts, y=test_labels)
test_dataloader = DataLoader(dataset=test_dataset, shuffle=False, batch_size=256)
for X, y in test_dataloader:
print(X.shape)
print(y.shape)
break
"""
4. 搭建模型
"""
import torch
from torch import nn
"""
每句话65个词,分为2类
"""
class TextCNN(nn.Module):
"""
搭建模型
- 卷积
# [N, C, L]
nn.Conv1d()
"""
def __init__(self, dict_len=len(tokenizer.word2idx), embedding_dim = 256):
super().__init__()
# 向量化
self.embed = nn.Embedding(num_embeddings=dict_len,
embedding_dim=embedding_dim,
padding_idx=tokenizer.word2idx.get("<PAD>"))
# 特征抽取
self.feature_extractor = nn.Sequential(
nn.Conv1d(in_channels=256, out_channels=512, kernel_size=3, stride=1, padding=1),
nn.BatchNorm1d(num_features=512),
nn.ReLU(),
nn.MaxPool1d(kernel_size=2, stride=2, padding=0),
nn.Conv1d(in_channels=512, out_channels=1024, kernel_size=3, stride=1, padding=1),
nn.BatchNorm1d(num_features=1024),
nn.ReLU(),
nn.MaxPool1d(kernel_size=2, stride=2, padding=0)
)
# 分类
self.classifier = nn.Sequential(
nn.Flatten(),
nn.Linear(in_features=1024 * (SEQ_LEN // 2 // 2), out_features=128),
nn.ReLU(),
nn.Linear(in_features=128, out_features=2)
)
def forward(self, x):
# 向量化
x = self.embed(x)
x = torch.permute(input=x, dims=(0, 2, 1))
# 抽特征
x = self.feature_extractor(x)
# 做分类
x = self.classifier(x)
return x
class TextRNN(nn.Module):
"""
搭建模型
- 卷积
# [N, C, L]
nn.Conv1d()
"""
def __init__(self, dict_len=len(tokenizer.word2idx), embedding_dim = 256):
super().__init__()
# 向量化
self.embed = nn.Embedding(num_embeddings=dict_len,
embedding_dim=embedding_dim,
padding_idx=tokenizer.word2idx.get("<PAD>"))
# 特征抽取
self.feature_extractor = nn.RNN(input_size=256, hidden_size=512, num_layers=1, bidirectional=False)
# 分类
self.classifier = nn.Sequential(
nn.Linear(in_features=512, out_features=128),
nn.ReLU(),
nn.Linear(in_features=128, out_features=2)
)
def forward(self, x):
# 向量化
x = self.embed(x)
x = torch.permute(input=x, dims=(1, 0, 2))
# 特征抽取
out, hn = self.feature_extractor(x)
# 分类输出
x = self.classifier(out.sum(dim=0))
return x
model= TextRNN()
for X, y in train_dataloader:
y_pred = model(X)
print(y_pred.shape)
break
"""
5. 训练模型
"""
# 检测设备
device = "cuda" if torch.cuda.is_available() else "cpu"
# 实例化模型
model = TextRNN().to(device=device)
# 损失函数
loss_fn = nn.CrossEntropyLoss()
# 优化器
optimizer = torch.optim.Adam(params=model.parameters(), lr=1e-3)
# 轮次
epochs = 20
"""
6. 评估
"""
import time
def get_acc(dataloader):
model.eval()
accs = []
with torch.no_grad():
for X, y in dataloader:
# 0. 数据搬家
X = X.to(device=device)
y = y.to(device=device)
# 1. 正向传播
y_pred = model(X)
# 2. 计算结果
y_pred = y_pred.argmax(dim=1)
# 3. 计算准确率
acc = (y_pred == y).to(dtype=torch.float32).mean().item()
# 4. 保存结果
accs.append(acc)
final_acc = round(number=sum(accs) / len(accs), ndigits=6)
return final_acc
def train():
train_acc = get_acc(dataloader=train_dataloader)
test_acc = get_acc(dataloader=test_dataloader)
print(f"初始 Train_acc: {train_acc}, Test_acc: {test_acc}")
for epoch in range(epochs):
model.train()
start_time = time.time()
for X, y in train_dataloader:
# 0. 数据搬家
X = X.to(device=device)
y = y.to(device=device)
# 1. 正向传播
y_pred = model(X)
# 2. 计算误差
loss = loss_fn(y_pred, y)
# 3. 反向传播
loss.backward()
# 4. 优化一步
optimizer.step()
# 5. 清空梯度
optimizer.zero_grad()
stop_time = time.time()
# 每轮结束后测试一下
train_acc = get_acc(dataloader=train_dataloader)
test_acc = get_acc(dataloader=test_dataloader)
print(f"Epoch: {epoch + 1}, Train_acc: {train_acc}, Test_acc: {test_acc}, Train_time: {stop_time-start_time}")
train()


910

被折叠的 条评论
为什么被折叠?



