第一章:人工智能之不同数据类型及其特点梳理
第二章:自然语言处理(NLP):文本向量化从文字到数字的原理
第三章:循环神经网络RNN:理解 RNN的工作机制与应用场景(附代码)
第四章:循环神经网络RNN、LSTM以及GRU 对比(附代码)
第五章:理解Seq2Seq的工作机制与应用场景(附代码)
第六章:深度学习架构Seq2Seq-添加并理解注意力机制(一)
第七章:深度学习架构Seq2Seq-添加并理解注意力机制(二)
第八章:深度学习模型Transformer初步认识整体架构
第九章:深度学习模型Transformer核心组件—自注意力机制
第十章:理解梯度下降、链式法则、梯度消失/爆炸
第十一章:Transformer核心组件—残差连接与层归一化
第十二章:Transformer核心组件—位置编码
第十三章:Transformer核心组件—前馈网络FFN
第十四章:深度学习模型Transformer 手写核心架构一
第十五章:深度学习模型Transformer 手写核心架构二
循环神经网络(Recurrent Neural Networks, RNN)是一种专门用于处理序列数据的神经网络。RNN能够利用数据中的时间顺序信息,因此在处理如文本、语音等序列数据时表现尤为出色。
一、产生背景
随着机器学习和深度学习技术的发展,人们开始探索如何让计算机理解和生成人类语言。然而,早期的模型大多是基于固定大小输入的设计,这限制了它们在处理变长序列数据上的能力。为了克服这一局限性,研究人员开发了循环神经网络(RNN),通过引入循环机制来记忆先前的信息,从而可以有效地处理序列数据。
二、为什么需要 RNN
- 序列数据:许多实际问题涉及序列数据,例如自然语言处理中的句子、时间序列分析中的股票价格预测等。这些数据的特点是每个元素之间存在某种形式的时间或逻辑顺序。
- 上下文依赖:理解一句话不仅需要知道单个单词的意思,还需要理解单词之间的关系以及整个句子的语境。RNN通过其内部状态来捕捉这种上下文依赖关系。
- 变长输入:传统神经网络难以处理长度不定的输入序列,而RNN可以通过共享参数的方式处理任意长度的输入。
这种类型的任务在时间序列分析、股票价格预测、天气预报等领域中非常常见。比如,我给已经训练好的模型,输入开始的几个文字,让模型能够自动生成一首诗。目标是根据过去的时间序列数据预测下一个值。
三、RNN 的技术原理
从网上找了一张 RNN 的结构图:
上面最左边是RNN的结构图,右边是按照时间展开视图
为了更清晰地展示RNN如何处理序列数据,可以将左边的RNN按照时间维度展开。这样可以看到,在每个时间步,RNN都会接收一个新的输入并更新其隐藏状态。在时间展开视图中,可以看到:
- 每个时间步都有一个独立的输入 x t x_t xt。
- 隐藏状态 h t h_{t} ht 依赖于当前输入 x t x_t xt 和前一个时间步的隐藏状态 h t − 1 h_{t-1} ht−1。
- 输出 y t y_{t} yt 只依赖于当前时间步的隐藏状态 h t h_{t} ht。
上图中红框标出来的是 RNN基础单元,一个基础的RNN单元可以被可视化为一个接受当前时刻输入
x
t
x_t
xt 和前一时刻隐藏状态
h
t
−
1
h_{t-1}
ht−1 的单元,并输出当前时刻的隐藏状态
h
t
h_{t}
ht 和可能的输出
y
t
y_{t}
yt
在这个图中:
- x t x_t xt 表示在时间步 t 的输入。
- h t h_{t} ht 是在时间步 t 的隐藏状态。
- y t y_{t} yt 是在时间步 t 的输出。
- W, U, 和 V 分别表示从输入到隐藏层、从隐藏层到隐藏层以及从隐藏层到输出层的权重矩阵。
RNN的核心在于它能够记住之前的信息并通过隐藏状态传递给后续的时间步,当前时刻的输出不仅取决于当前时刻的输入,还受到之前时刻的状态影响。具体来说:
-
隐藏状态更新公式:
h t = f ( W h x t + U h h t − 1 + b h ) h_t = f(W_hx_t + U_hh_{t-1} + b_h) ht=f(Whxt+Uhht−1+bh)
其中, h t h_t ht 是当前时刻的隐藏状态,直白一点理解,就是一个多维矩阵, x t x_t xt 是当前时刻的输入,直白一点,就是把输入的词或者 tokens 转换成一个多维矩阵,输入到模型就是 x t x_t xt ,也是一个多维矩阵, W h W_h Wh 和 U h U_h Uh 分别是输入到隐藏层和上一时刻隐藏状态到当前隐藏状态的权重矩阵, b h b_h bh 是偏置项,f 是激活函数(如tanh或ReLU)。 -
隐藏层输出公式:
y t = g ( V y h t + b y ) y_t = g(V_yh_t + b_y) yt=g(Vyht+by)
其中, y t y_t yt 是当前时刻的输出, V y V_y Vy 是隐藏层到输出层的权重矩阵,g 是输出层的激活函数(对于分类任务通常是softmax)。
通过上述结构图,可以清楚地看到RNN是如何利用循环连接来保持对过去信息的记忆。
从网上找了一张图,这个例子说的很到位
需要在说明一下
x1,x2,.....,xt
:如果是中文,x可以先简单理解就是一个字,
y1,y2,.....,yt
:如果是中文,y可以先简单理解就是一个字,
x 对应着输入,y 对应着输出,但 x 或者 y也可以是一个词,或者一句话。
也不难理解,我给模型输入一个词,模型出来一个词,如果是英文,x 或者 y 就是一个单词,或者一句话。
四、RNN示例
下面是一个简单的RNN实现示例,使用PyTorch框架构建一个基本的RNN模型,并展示如何对其进行训练。
import torch
import torch.nn as nn
# 定义一个简单的RNN模型
class SimpleRNN(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super(SimpleRNN, self).__init__()
self.hidden_size = hidden_size
# rnn: 使用给定的输入大小(input_size)、隐藏层大小(hidden_size)以及参数 batch_first=True 初始化的RNN层。
# batch_first=True 表示输入张量的第一维是批次大小。
self.rnn = nn.RNN(input_size, hidden_size, batch_first=True)
# fc: 全连接层(线性变换),用于将RNN层的输出映射到指定的输出维度(output_size)。
self.fc = nn.Linear(hidden_size, output_size)
# forward 定义了数据如何通过网络流动。
def forward(self, x, hidden):
# 首先,输入张量 x 和初始隐藏状态 hidden 被传递给RNN层,产生新的输出 out 和更新后的隐藏状态 hidden。
out, hidden = self.rnn(x, hidden)
# 然后,从 out 中提取最后一个时间步的输出,并通过全连接层 fc 进行处理,得到最终输出。
# 获取最后一个时间步的输出
out = self.fc(out[:, -1, :])
return out, hidden
def init_hidden(self):
"""
返回一个新的初始隐藏状态张量,其形状为 (num_layers, batch_size, hidden_size)。
在这个例子中,由于我们只有一个RNN层且批次大小为1,所以形状为 (1, 1, hidden_size)。
"""
return torch.zeros(1, 1, self.hidden_size)
# 参数设置
input_size = 10 # 输入特征维度
hidden_size = 20 # 隐藏层维度
output_size = 1 # 输出维度
# 初始化模型
model = SimpleRNN(input_size, hidden_size, output_size)
seq_length = 5
batch_size = 1
# 创建一个随机输入张量 x,其形状为 (batch_size, seq_length, input_size)。
x = torch.randn(batch_size, seq_length, input_size)
# 调用 init_hidden 方法初始化隐藏状态。
hidden = model.init_hidden()
# 前向传播
# 将输入张量 x 和隐藏状态 hidden 传入模型进行前向传播,
# 获取输出 output 和更新后的隐藏状态 hidden。
output, hidden = model(x, hidden)
print("输出:", output)
这段代码展示了如何定义和使用一个简单的RNN模型。首先定义了一个SimpleRNN
类,其中包含了RNN层和全连接层。然后初始化模型,并创建一些随机输入数据进行测试。
上面这段代码,不需要理解 RNN 能干什么,只是为了混一个眼熟,知道 RNN 的调用方式,不用深入理解,后面接着分析。
五、理解隐藏状态
隐藏状态 是 RNN 的核心记忆单元,其本质是一个 动态更新的向量,向量!向量!向量!把这个向量取了一个名字叫做隐藏状态,作用如下:
- 携带历史信息:通过公式 h t = s i g m a ( W x h x t + U h h h t − 1 + b h ) h_t = sigma(W_{xh}x_t + U_{hh}h_{t-1} + b_h) ht=sigma(Wxhxt+Uhhht−1+bh),将当前输入 x t x_t xt 和上一步隐藏状态 h t − 1 h_{t-1} ht−1融合,形成新的记忆,这个地方把 2 个向量融合的方式,就是向量相加。
- 控制信息流动:决定哪些历史信息保留、哪些被遗忘(受权重矩阵 U h h U_{hh} Uhh和激活函数 s i g m a sigma sigma影响)。
- 生成输出:通过 y t = V h y h t + b y y_t = V_{hy}h_t + b_y yt=Vhyht+by,将隐藏状态映射为当前时间步的输出。
类比理解:
将隐藏状态想象成一个人的“短期记忆”——每看到一个新词(输入),他会结合之前的记忆
h
t
−
1
h_{t-1}
ht−1更新自己的理解
h
t
h_t
ht,并基于此做出反应(输出
y
t
y_t
yt)。
六、理解权重和偏置共享
RNN 的所有时间步共享同一组权重 W x h , U h h , V h y W_{xh}, U_{hh}, V_{hy} Wxh,Uhh,Vhy和偏置 b h , b y b_h, b_y bh,by。这是 RNN 的核心设计特性,具体表现如下:
- 参数共享的数学验证
以隐藏状态计算为例,对任意时间步 t t t,计算方式均为: h t = σ ( W x h x t + U h h h t − 1 + b h ) h_t = \sigma(\textcolor{red}{W_{xh}} x_t + \textcolor{blue}{U_{hh}} h_{t-1} + \textcolor{green}{b_h}) ht=σ(Wxhxt+Uhhht−1+bh)
- 红色部分 W x h W_{xh} Wxh、蓝色部分 U h h U_{hh} Uhh、绿色部分 b h b_h bh始终不变。
- 无论序列多长,同一套参数被重复使用。
- 参数共享的意义
- 降低参数量:假设序列长度为 100,若不用参数共享,需要 100 组不同的权重,导致参数爆炸。
- 泛化能力:强迫模型在不同位置学习相同规律(如动词在不同位置的语法规则)。
- 处理变长序列:无论输入多长,模型结构保持不变。
- 参数共享的代码验证
import torch.nn as nn
rnn = nn.RNN(input_size=3, hidden_size=2, batch_first=True)
# 查看权重名称(所有时间步共享)
print(rnn._parameters.keys())
# 输出:odict_keys(['weight_ih_l0', 'weight_hh_l0', 'bias_ih_l0', 'bias_hh_l0'])
# 权重矩阵维度验证
print(rnn.weight_ih_l0.shape) # W_{xh} 形状:(hidden_size, input_size) → (2,3)
print(rnn.weight_hh_l0.shape) # W_{hh} 形状:(hidden_size, hidden_size) → (2,2)
输出:
dict_keys(['weight_ih_l0', 'weight_hh_l0', 'bias_ih_l0', 'bias_hh_l0'])
torch.Size([2, 3])
torch.Size([2, 2])
参数共享的直观示例
假设处理序列 ["我", "爱", "你"]
,参数共享过程如下:
-
时间步 t=1(输入“我”)
- 计算 h 1 = σ ( W x h ⋅ “我” + U h h ⋅ h 0 + b h ) h_1 = \sigma(W_{xh} \cdot \text{“我”} + U_{hh} \cdot h_0 + b_h) h1=σ(Wxh⋅“我”+Uhh⋅h0+bh)
-
时间步 t=2(输入“爱”)
- 使用 相同的 W x h , U h h , b h W_{xh}, U_{hh}, b_h Wxh,Uhh,bh
- 计算: h 2 = σ ( W x h ⋅ “爱” + U h h ⋅ h 1 + b h ) h_2 = \sigma(W_{xh} \cdot \text{“爱”} + U_{hh} \cdot h_1 + b_h) h2=σ(Wxh⋅“爱”+Uhh⋅h1+bh)
-
时间步 t=3(输入“你”)
- 继续使用 相同的参数
- 计算: h 3 = σ ( W x h ⋅ “你” + U h h ⋅ h 1 + b h ) h_3 = \sigma(W_{xh} \cdot \text{“你”} + U_{hh} \cdot h_1 + b_h) h3=σ(Wxh⋅“你”+Uhh⋅h1+bh)
总结
- 隐藏状态:动态更新的记忆单元,传递序列的上下文信息。
- 参数共享:所有时间步使用同一组权重和偏置,这是 RNN 的核心特性。
- 优势:减少参数量、增强泛化能力、支持变长序列处理。
七、RNN案例
以下是一个使用 RNN 进行文本生成 的实际案例,通过训练模型学习酒店评论,生成类似的新文本。我们将使用 PyTorch 实现一个基于字符的 RNN 模型。
7.1 任务描述
案例:酒店评论文本生成
1. 任务描述
- 输入:字符序列(如 “早餐”)
- 输出:预测下一个字符(如 “很” → “丰” → “富” → …)
- 目标:让模型学会字符间的依赖关系,生成符合语法和风格的文本。
文件下载《酒店评论》,这个资料是可以用来做情感识别的,识别一条评论是正面的,还是负面,还是中性,此处用来做简单文本生成训练。
文件结构如下:
hotel
test
neg
00001.txt
00002.txt
pos
00001.txt
00002.txt
train
neg
00001.txt
00002.txt
pos
00001.txt
00002.txt
7.2 数据准备
先把文件合并在一起,形成一个大文件,
import os
def merge_txt_files(source_dir: str, target_file: str,file_num: int) -> None:
"""
合并指定目录下所有txt文件到目标文件
Args:
source_dir: 包含txt文件的源目录路径
target_file: 合并后的目标文件路径
file_num: 合并文件数量限制
"""
# 确保目标目录存在
os.makedirs(os.path.dirname(target_file), exist_ok=True)
i = 0
with open(target_file, 'w', encoding='utf-8') as outfile:
for root, _, files in os.walk(source_dir):
for filename in sorted(f for f in files if f.endswith('.txt')):
if i >= file_num:
print(f"解析文件数量达到上限:{file_num}")
return
filepath = os.path.join(root, filename)
try:
with open(filepath, 'r', encoding='gbk') as infile:
# outfile.write(f"\n\n──▶ 文件来源:{filepath} ◀──\n\n")
content = infile.read()
content = content.strip()
content = content + "\n"
#print(f"文件来源:{filepath},文件内容:{content}")
outfile.write(content)
except UnicodeDecodeError:
print(f"跳过无法解码的文件:{filepath}")
except Exception as e:
print(f"处理文件 {filepath} 时发生错误:{str(e)}")
i = i + 1
可以使用file_num指定你要合并的文件个数,只是做做实验,没必要把全部的文件合并到一个文件。
# 示例调用
merge_txt_files(
source_dir="./hotel/train/pos",
target_file="./merged_hotel_comment.txt",
file_num=20
)
7.3 分词建索引
下面是定义参数、分词、建索引
import torch
import torch.nn as nn
import numpy as np
# 超参数调整
# 隐藏层大小设置为512,适合处理复杂的中文字符。
# 增大隐藏层维度以适应中文复杂性
hidden_size = 512
# RNN层数量设为2,表示将使用两层RNN。
num_layers = 2
# 每个训练样本的序列长度设为50。
seq_length = 50 # 加长序列长度
# 批次大小为1,意味着每次仅处理一个序列。
batch_size = 1
# 学习率
learning_rate = 0.005
# 训练轮次设定为2000轮。
epochs = 2000
# 从文件 'merged_hotel_comment.txt' 中读取文本数据,并去除所有的换行符。
# 然后只保留前10000个字符以减少计算量。
with open('merged_hotel_comment.txt', 'r', encoding='utf-8') as f:
text = f.read().replace('\n', '')[:10000] # 截取部分数据
# 将文本中的所有不同字符(包括汉字、标点符号等)提取出来,并按字典顺序排序。
# 创建字符到索引的映射
# 这个地方简单处理,按照字或者标点符号来分词
# 拆分成这样:'、', '。', '一', '万', '三',
chars = sorted(list(set(text)))
# 建立词和索引的映射关系
# 创建两个字典:
# char_to_idx 将每个字符映射到一个唯一的整数索引;
# idx_to_char 则相反,将索引映射回字符。
char_to_idx = {ch: i for i, ch in enumerate(chars)}
idx_to_char = {i: ch for i, ch in enumerate(chars)}
# vocab_size 表示词汇表的大小,即不同字符的数量。
vocab_size = len(chars)
print("实际字符数",vocab_size)
# 将文本转换为索引序列
# 遍历整个文本,根据之前创建的 char_to_idx 字典,将文本中的每一个字符替换为其对应的索引值,
# 从而形成一个整数列表 data。
# 这一步是将原始文本转换成机器学习模型可以处理的形式。
data = [char_to_idx[ch] for ch in text]
该段代码主要完成了以下几件事:
- 定义了模型的一些关键超参数。
- 读取并初步处理了一个包含中文文本的文件。
- 根据文本内容创建了一个字符到索引的双向映射,这对于后续的字符嵌入和模型输入至关重要。
- 将原始文本转换成了由字符索引组成的列表,以便于模型训练。
这个过程为接下来的模型构建和训练做好了数据上的准备。
7.4 定义RNN模型
下面定义一个字符级循环神经网络(CharRNN),用于处理文本生成的任务。
# 定义模型
class CharRNN(nn.Module):
def __init__(self, vocab_size, hidden_size, num_layers):
super(CharRNN, self).__init__()
# 创建一个嵌入层 (nn.Embedding),将词汇表中的每个单词或字符映射到一个高维向量空间中。
# 这里,vocab_size 是词汇表的大小,而 hidden_size 是嵌入向量的维度。
self.embedding = nn.Embedding(vocab_size, hidden_size) # 添加嵌入层
# 创建一个 RNN 层 (nn.RNN),
# 其输入大小为 hidden_size(即嵌入层的输出维度),
# 隐藏层大小也为 hidden_size,
# 层数由 num_layers 指定,
# 并设置 batch_first=True 表示输入张量的第一个维度是批次大小。
self.rnn = nn.RNN(
input_size=hidden_size, # 使用嵌入层的输出维度作为RNN输入大小
hidden_size=hidden_size,
num_layers=num_layers,
batch_first=True
)
# 全连接层 (nn.Linear),用于将 RNN 的最后一个时间步的输出映射回词汇表大小,以便预测下一个字符。
self.fc = nn.Linear(hidden_size, vocab_size)
# 定义数据如何通过网络流动。
def forward(self, x, hidden):
# 输入 x 首先通过嵌入层转换成嵌入向量。
embeds = self.embedding(x)
# 然后这些嵌入向量被传递给 RNN 层,产生输出 out 和更新后的隐藏状态 hidden。
out, hidden = self.rnn(embeds, hidden)
# 输出 torch.Size([1, 50, 512]) torch.Size([2, 1, 512])
# print(out.shape,hidden.shape)
# 最后,RNN 的输出经过全连接层映射回词汇表大小,但不在此处调整形状,而是留待损失计算时进行。
out = self.fc(out)
return out, hidden
# 初始化模型、损失函数和优化器
# 模型实例化:使用之前定义的参数 (vocab_size, hidden_size, num_layers) 实例化 CharRNN 模型。
model = CharRNN(vocab_size, hidden_size, num_layers)
# 损失函数:nn.CrossEntropyLoss() 是一个多分类问题常用的损失函数,适用于目标是预测类别的情况。
criterion = nn.CrossEntropyLoss()
# 优化器:使用 Adam 优化算法 (torch.optim.Adam) 来更新模型的权重,学习率由 learning_rate 参数指定。
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
上面这段代码,构建一个简单的字符级语言模型,它能够:
- 将输入字符序列转换为嵌入向量。
- 通过多层 RNN 处理这些向量以捕捉序列信息。
- 使用全连接层将 RNN 的输出映射回词汇表大小,从而预测下一个字符。
7.5 训练RNN模型
接下来就可以开始训练模型,训练一个字符级语言模型的循环部分。它展示如何使用准备好的数据来训练之前定义的 CharRNN 模型。
# 训练代码
# 外层循环:通过 epochs 变量指定的次数迭代整个训练过程。每次迭代称为一个 epoch。
for epoch in range(epochs):
# 随机选取起始点:在每个 epoch 开始时,从数据中随机选择一个起始索引 start_idx。
# 这个索引用于从数据序列中提取一段长度为 seq_length 的子序列作为输入。
start_idx = np.random.randint(0, len(data) - seq_length)
# inputs: 提取从 start_idx 开始的连续 seq_length 个字符,并将它们转换为 PyTorch 张量。
# 由于模型期望输入的形状为 (batch_size, seq_length),这里使用 .unsqueeze(0) 来增加一个批次维度(即使批次大小为1)
inputs = torch.tensor(data[start_idx:start_idx+seq_length]).long().unsqueeze(0) # 增加批次维度
# 输出inputs 形状 torch.Size([1, 50])
# print("inputs 形状",inputs.shape)
# targets: 目标是预测下一个字符,因此从 start_idx + 1 开始提取与 inputs 对应的 seq_length 长度的字符序列。
# 这样,对于 inputs 中的每一个字符,其对应的目标是序列中的下一个字符。
targets = torch.tensor(data[start_idx+1:start_idx+seq_length+1]).long()
# 输出targets 形状 torch.Size([50])
# print("targets 形状",targets.shape)
# 初始化隐藏状态:创建一个全零的张量作为初始隐藏状态。
# num_layers 表示 RNN 层的数量,
# batch_size 是批次大小(在这个例子中为1),
# hidden_size 是隐藏层的大小。
hidden = torch.zeros(num_layers, batch_size, hidden_size)
# 前向传播:将 inputs 和 hidden 状态传递给模型进行前向计算,
# 得到输出 outputs 和更新后的隐藏状态 hidden。
outputs, hidden = model(inputs, hidden)
# 输出torch.Size([1, 50, 508]) torch.Size([2, 1, 512])
# print(outputs.shape,hidden.shape)
# 计算损失:使用交叉熵损失函数 criterion 来计算预测值与真实值之间的损失。
# 这里需要调整 outputs 的形状以匹配 targets 的形状,
# 具体来说,将 outputs 调整为二维张量 (batch_size * seq_length, vocab_size),
# 并将 targets 调整为一维张量 (batch_size * seq_length)。
# outputs.view(-1, vocab_size)的形状为 torch.Size([50, 508])
# targets.view(-1)的状态为 torch.Size([50])
loss = criterion(outputs.view(-1, vocab_size), targets.view(-1)) # 确保形状匹配
# 清空之前的梯度信息。
optimizer.zero_grad()
# 执行反向传播,计算损失相对于模型参数的梯度。
loss.backward()
# 使用优化器更新模型参数。
optimizer.step()
# 打印损失:每500个 epoch 打印一次当前的 epoch 数和对应的损失值。
# 监控训练进度和模型的学习情况。
if epoch % 500 == 0:
print(f'Epoch {epoch}, Loss: {loss.item():.4f}')
训练过程中,损失函数输出:
Epoch 0, Loss: 6.1834
Epoch 500, Loss: 1.0388
Epoch 1000, Loss: 0.2663
Epoch 1500, Loss: 4.1099
这段代码实现一个简单的训练循环,用于训练一个基于RNN的字符级语言模型。通过不断地从前向传播、计算损失、反向传播到更新权重,模型逐渐学习如何根据前面的字符预测下一个字符。
通过调整超参数如 learning_rate, epochs, hidden_size 等,可以进一步优化模型的表现。
7.7 验证RNN模型
接下来代码定义一个生成中文文本的函数 generate_chinese_text,该函数基于之前训练好的字符级RNN模型。
# 设置批次大小为1,意味着每次只处理一个序列。
batch_size = 1
# 生成函数(需适配中文字符)
def generate_chinese_text(seed_str, length=100, temperature=0.5):
# 初始化隐藏状态,确保批次大小为1
# 创建一个全零张量作为初始隐藏状态,形状为 (num_layers, batch_size, hidden_size)。
hidden = torch.zeros(num_layers, batch_size, hidden_size)
# 生成变量:generated 用来存储生成的文本,首先包含种子字符串 seed_str。
generated = seed_str
# 初始化输入序列:将种子字符串中的每个字符转换为其对应的索引值,并形成形状为 (len(seed_str),1) 的张量。
input_seq = torch.tensor([[char_to_idx[ch]] for ch in seed_str]).long()
# 然后使用 torch.permute 将其转置为形状 (1,len(seed_str)),即 (batch_size,seq_length)。
# torch.Size([1, 2]),一句话 2个字
input_seq = torch.permute(input=input_seq, dims=(1, 0))
# 禁用梯度计算:在生成阶段,我们不需要进行反向传播,因此使用 torch.no_grad() 来减少内存占用和加速计算。
# 禁用梯度计算提高效率
with torch.no_grad():
for _ in range(length):
# 前向传播:将当前的 input_seq 和 hidden 状态传递给模型,
# 得到输出 outputs 和更新后的隐藏状态 hidden。
outputs, hidden = model(input_seq, hidden)
# 获取最后一个时间步的输出并调整温度
# 使用 Softmax 函数对最后一个时间步的输出进行归一化,
# 并通过 temperature 参数调整分布的尖锐度(temperature 越低,选择高概率字符的可能性越大;越高,则更倾向于均匀分布)。
prob = torch.softmax(outputs[0, -1, :] / temperature, dim=0).detach()
# 使用 torch.multinomial 根据调整后的概率分布随机采样下一个字符的索引。
next_char_idx = torch.multinomial(prob, 1).item()
# 将采样的字符添加到 generated 字符串中。
generated += idx_to_char[next_char_idx]
# 更新输入序列:将新生成的字符包装成形状为 (1, 1) 的张量,准备用于下一次迭代。
input_seq = torch.tensor([[next_char_idx]], dtype=torch.long)
# 返回生成的完整文本字符串。
return generated
# 示例生成
print(generate_chinese_text(seed_str="早餐", length=50))
示例输出
早餐实还不错,属气差计在墙,如果(非价格的人象,
是海的房间还是早餐的房间实在是早餐的房间实在是,蔡陆陆线
从输出结果上来看,虽然可以完成上面设定的目标,输入一个字或者几个字,模型会自动生成接下来的字,一直到我指定的长度,但是从效果来看,并不理想,有些地方上一个字和下一个字,都不挨边。
八、RNN 的不足
-
梯度消失/爆炸
问题:RNN 在反向传播时,梯度需要沿时间步连乘。当序列较长时:
如果梯度值 <1 → 多次连乘后趋近于零(梯度消失),无法更新早期层的参数;
如果梯度值 >1 → 多次连乘后趋向无穷大(梯度爆炸),参数更新不稳定。
影响:难以捕捉长距离依赖(如句子开头和结尾的关系)。 -
短期记忆
原因:RNN 的隐藏状态通过简单加权和更新,早期输入信息会被后续输入逐步稀释。
示例:在句子“The cat, which ate a lot of fish, was very hungry”中,RNN 可能遗忘主语 “cat”,导致无法正确关联 “was hungry”。 -
参数更新冲突
问题:同一组权重需要同时学习短期和长期依赖,导致优化困难。
RNN长序列训练时梯度不稳定,LSTM、GRU 通过门控机制解决。下一篇将RNN 升级为LSTM或者GRU
九、CNN和RNN区别
卷积神经网络CNN 和 循环神经网络RNN 的有什么区别,分别适合解决什么问题?
核心区别及其适用场景对比:
特性 | CNN | RNN |
---|---|---|
数据维度 | 处理 空间数据(如图像、网格) | 处理 序列数据(如文本、时间序列) |
参数共享方式 | 空间维度共享(卷积核滑动) | 时间维度共享(循环权重复用) |
结构特点 | 卷积层、池化层、局部感受野 | 隐藏状态循环传递、时间展开结构 |
依赖关系建模 | 局部空间相关性(如相邻像素) | 时间/序列顺序依赖(如前后单词) |
输入输出长度 | 固定尺寸输入输出(如224x224图像) | 可变长度输入输出(如不同长度句子) |
典型应用 | 图像分类、目标检测 | 语言模型、时间序列预测 |
解决的问题
-
CNN 的核心任务
- 空间特征提取:通过卷积核捕捉局部模式(如边缘、纹理)。
- 平移不变性:无论目标在图像中的位置如何,都能识别(如猫在左上角或右下角)。
- 降维与高效计算:通过池化层减少参数量,保留关键特征。
典型应用场景:
- 图像分类(ResNet、VGG)
- 目标检测(YOLO、Faster R-CNN)
- 图像分割(U-Net)
- 人脸识别
-
RNN 的核心任务
- 序列建模:捕捉时间或顺序依赖关系(如前文影响后续语义)。
- 变长序列处理:动态适应输入/输出长度(如翻译不同长度句子)。
- 记忆历史信息:通过隐藏状态传递上下文。
典型应用场景:
- 文本生成(如 GPT 早期版本)
- 机器翻译(如 LSTM-based Seq2Seq)
- 时间序列预测(股票价格、天气)
- 语音识别
直观对比示例
-
CNN 示例(图像分类)
- 输入:一张 224x224 的猫的图片
- 处理过程:
- 卷积层检测边缘 → 2. 深层卷积识别局部特征(如眼睛、耳朵) → 3. 全连接层分类为“猫”
- 特点:关注局部空间模式,忽略位置变化。
-
RNN 示例(文本生成)
- 输入:句子开头“The cat sat on the…”
- 处理过程:
- 逐词输入,隐藏状态记住“cat”是主语 → 2. 预测下一个词为“mat”而非“cloud”(依赖上下文)
- 特点:依赖序列顺序和历史信息。
混合使用场景
虽然 CNN 和 RNN 设计目标不同,但在某些任务中需要结合使用:
-
图像描述生成(Image Captioning)
- CNN:提取图像特征(如 VGG 编码图像内容)。
- RNN:基于图像特征生成文字描述(如 LSTM 解码生成句子)。
- 流程:
图像 → CNN → 特征向量 → RNN → 文本
-
视频动作识别
- CNN:提取每一帧的空间特征。
- RNN:分析帧之间的时序关系(如 LSTM 捕捉动作变化)。
现代替代方案
尽管 RNN 是序列建模的经典方法,但在许多场景中已被 Transformer 取代(如 BERT、GPT),原因包括:
- 并行计算:Transformer 的自注意力机制可并行处理序列,训练更快。
- 长距离依赖:直接建模任意位置的关系,避免梯度消失。
但 RNN 仍在小规模序列任务或资源受限场景中有应用价值。
- 用 CNN ,当数据有空间结构(如图像、视频帧、频谱图)。
- 用 RNN/Transformer ,当数据有时序依赖(如文本、传感器数据、语音)。
- 混合架构可结合两者优势(如视频理解、多模态任务)。