背景

  1. RNN 主要用来处理序列数据,目前大部分 LLM 都是基于 Transformer
  2. 通过学习 RNN,有助于理解 Transformer
    • 有助于理解神经网络如何处理序列中的依赖关系记忆过去的信息,并在此基础上生成预测
    • 有助于理解关键问题 - 梯度消失 / 梯度爆炸

RNN

Recurrent neural network - 循环神经网络

  1. RNN 是一类用于处理序列数据的神经网络,RNN 能够处理序列长度变化的数据 - 文本 / 语音
  2. RNN 的特点是在模型中引入了循环,使得网络能够保持某种状态,表现出更好的性能

image-20240818215624681

  1. 左边
    • $x$ 为输入层,$o$ 为输出层,中间的 $s$ 为隐藏层,在 $s$ 层进行一个循环 $W$
  2. 右边(展开循环)
    • 与时间 $t$ 相关的状态变化
    • 神经网络在处理数据时,能看到前后时刻的状态,即上下文
    • RNN 因为隐藏层时序状态,那么在推理的时候,可以借助上下文,从而理解语义更加准确

优劣

优势

  1. RNN 具有记忆能力,通过隐藏层的循环结构来捕捉序列的长期依赖关系
  2. 特别适用于文本生成语音识别等领域

局限

  1. 存在梯度消失梯度爆炸的问题,可以通过引入 LSTM缓解

反向传播

  1. 深度学习中,训练神经网络涉及到两个主要的传播阶段 - 前向传播 + 反向传播
  2. 前向传播 - 根据当前的网络参数、权重和偏置等得到预测输出
    • 输入数据从网络的输入层开始,逐层向前传递至输出层
    • 每层都会对其输入进行计算 - 如加权求和,然后应用激活函数
    • 并将计算结果传递给下一层,直到最终产生输出
  3. 反向传播
    • 一旦输出层得到了预测输出,就会**计算损失函数**
      • 预测输出实际目标输出之间的差异(损失)
    • 然后,这个损失会被用来计算损失函数相对于网络中每个参数梯度
      • 这些梯度的内涵 - 为了减少损失,各个参数需要如何调整
    • 链式法则 - 从输出层开始,沿着网络向后(向输入层方向),逐层进行
    • 最后这些梯度会用来更新网络的参数 - 通过梯度下降或者其变体算法实现
    • 在反向传播过程中,每到达一层,都会触发激活函数
      • tanh 函数可能会导致梯度消失

结构原理

数学

RNN 的核心在于隐藏层 - 随着时间的变化更新隐藏状态

$$
h_t=f(W_{xh}x_t+W_{hh}x_{t-1}+b_h)
$$

  1. $h_t$ 是当前时间步的隐藏状态,$x_t$ 是当前时间步的输入,$h_{t-1}$ 是前一个时间步的隐藏状态
  2. $W_{xh}$ 和 $W_{hh}$ 为权重矩阵,$b_h$ 是偏置项,$f$​​ 是激活函数(如 tanh 函数)

过程

任务 - 假设字符集只有 A B C,给定序列 AB,预测下一个字符

  1. 输入层,将字符串转换为数值形式 - Embedding
    • 可以采用 One-hot 编码,A=[1,0,0] B=[0,1,0] C=[0,0,1]
    • 序列 AB,表示为两个向量 [1,0,0] 和 [0,1,0]
  2. 隐藏层,假设只有一个隐藏层(实际应用可能会有多个),使用 tanh 作为激活函数
    • 时间步 1 - 处理 A
      • 输入 [1,0,0]
      • 假设 $W_{xh}$ 和 $W_{hh}$ 的值均为 1,初始隐藏状态 $h_0=0$
      • 计算新的隐藏状态 $h_1=tanh(1*[1,0,0]+1*0)=tanh(1)≈0.76$​
    • 时间步 2 - 处理 B
      • 输入 [0,1,0]
      • 使用上一时间步的隐藏状态 $h_1≈0.76$
      • 计算新的隐藏状态 $h_2=tanh(1*[0,1,0]+1*0.76)=tanh(0.76)≈0.64$

每个时间步的隐藏状态 $h_t$ 基于当前的输入 $x_t$ 和上一时间步的隐藏状态 $h_{t-1}$ 计算得到的

RNN 能够记住之前的输入,并使用这些信息影响后续的处理,如预测下一个字符,使得模型具备了记忆功能

One-hot

image-20240818223729923

tanh

压缩器

tan

tanh

关键挑战

  1. RNN 通过当前的隐藏状态来记住序列之前的信息
  2. 这种记忆一般是短期的,随着时间步的增加早期输入对当前状态的影响会逐步减弱
  3. 标准 RNN 中,可能会遇到梯度消失的问题,导致几乎无法更新权重
挑战 影响
梯度消失 权重无法更新
梯度爆炸 无法收敛,甚至发散

梯度消失

无法更新权重

概述

  1. 梯度是指函数在某一点上的斜率 - 导数
  2. 深度学习中,该函数一般指具有多个变量(模型参数)的损失函数
  3. 寻找损失函数最小值的方法 - 梯度下降
    • 梯度下降 - 需要不断调整模型参数,使得损失函数降到最小
    • 梯度的语义 - 告知如何调整参数

原因

  1. 深层网络中的连乘效应

    • 在深层网络中,梯度是通过链式法则进行反向传播

    • 如果每一层的梯度都小于 1,随着层数的叠加,导致最终的梯度会非常小

  2. 激活函数的选择 - 反向传播会调用激活函数

    • 使用某些激活函数,如 tanh,函数的取值范围在 -1 ~ 1

    • 小于 1 的数进行连乘,会快速降低梯度值

方案

  1. 长短期记忆(LSTM)和门控循环单元(GRU) - 专门为了避免梯度消失问题而设计

    • 通过引入门控机制调节信息的流动,保留长期依赖信息

    • 从而避免梯度在反向传播过程中消失

  2. 使用 ReLU 及其变体激活函数 - 在正区间梯度保持恒定

    • 不会随着输入的增加而减少到 0,有助于减轻梯度消失的问题

ReLU 函数

relu

梯度爆炸

模型无法收敛,甚至发散

概述

  1. 当模型的梯度在反向传播过程中变得非常大
  2. 以至于更新后的权重**偏离最优解,导致模型无法收敛,甚至发散**

原因

  1. 深层网络连乘效应

    • 在深层网络中,梯度是通过链式法则进行反向传播

    • 如果每一层的梯度都大于 1,随着层数的增加,会导致梯度非常大

  2. 权重初始化不当

    • 如果网络的权重初始化得太大

    • 前向传播的过程中,信号大小会迅速增加

    • 同样,反向传播梯度也会迅速增加

  3. 使用不恰当的激活函数

    • 某些激活函数(如 ReLU)在正区间梯度常数

    • 如果网络架构设计不当,使用这些激活函数也可能会导致梯度爆炸

方案

  1. 使用长短期记忆(LSTM)和门控循环单元(GRU)来调整网络
  2. 替换激活函数
  3. 进行梯度裁剪
    • 在训练过程中,通过限制梯度的最小最大值来防止梯度消失爆炸问题,间接保持梯度的稳定性

长短期记忆

Long Short-Term Memory - LSTM - 记住该记住的,忘记该忘记的 - 优化记忆的效率

概述

  1. LSTM 是具有类似大脑记忆功能的模块
  2. LSTM 在处理数据(如文本、时间序列数据时) - 能记住对当前任务重要的信息,而忘记不重要的信息

机制

lstm

Mechanism 描述
遗忘门 - 移除 决定哪些存量信息是过时的,不重要的,应该从模型的记忆中移除
输入门 - 添加 决定哪些新信息是重要的,应该被添加到模型的记忆中
输出门 - 相关 决定在当前时刻,哪些记忆是相关的,应该要被用来生成输出

效果

  1. LSTM 能够在处理序列数据时,**有效地保留长期的依赖信息,避免了标准 RNN** 中常见的梯度消失问题
  2. LSTM 特别适用于需要理解整个序列背景的任务
    • 语言翻译 - 需要理解整个句子的含义
    • 股票价格预测 - 需要考虑长期的价格变化趋势

文本生产

通过学习大量的文本数据,RNN 能够生成具有相似风格的文本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
import torch
import torch.nn as nn
import torch.optim as optim
from torch.nn.utils.rnn import pad_sequence
from torch.utils.data import DataLoader, Dataset

# 数据预处理
text = "Here is some sample text to demonstrate text generation with RNN. This is a simple example."
tokens = text.lower().split()
tokenizer = {word: i + 1 for i, word in enumerate(set(tokens))}
total_words = len(tokenizer) + 1

# 创建输入序列
sequences = []
for line in text.split('.'):
token_list = [tokenizer[word] for word in line.lower().split() if word in tokenizer]
for i in range(1, len(token_list)):
n_gram_sequence = token_list[:i + 1]
sequences.append(n_gram_sequence)
max_sequence_len = max([len(x) for x in sequences])
sequences = [torch.tensor(seq) for seq in sequences]
sequences = pad_sequence(sequences, batch_first=True, padding_value=0)


class TextDataset(Dataset):
def __init__(self, sequences):
self.x = sequences[:, :-1]
self.y = sequences[:, -1]

def __len__(self):
return len(self.x)

def __getitem__(self, idx):
return self.x[idx], self.y[idx]


dataset = TextDataset(sequences)
dataloader = DataLoader(dataset, batch_size=2, shuffle=True)


# 构建模型
class RNNModel(nn.Module):
def __init__(self, vocab_size, embed_size, hidden_size):
super(RNNModel, self).__init__()
self.embedding = nn.Embedding(vocab_size, embed_size)
self.lstm = nn.LSTM(embed_size, hidden_size, batch_first=True)
self.fc = nn.Linear(hidden_size, vocab_size)

def forward(self, x):
x = self.embedding(x)
x, _ = self.lstm(x)
x = self.fc(x[:, -1, :])
return x


model = RNNModel(total_words, 64, 20)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

# 模型训练
for epoch in range(100):
for x_batch, y_batch in dataloader:
optimizer.zero_grad()
output = model(x_batch)
loss = criterion(output, y_batch)
loss.backward()
optimizer.step()
if epoch % 10 == 0:
print(f'Epoch {epoch + 1}, Loss: {loss.item()}')


# 文本生成
def generate_text(seed_text, next_words, model, max_sequence_len):
model.eval()
for _ in range(next_words):
token_list = [tokenizer[word] for word in seed_text.lower().split() if word in tokenizer]
token_list = torch.tensor(token_list).unsqueeze(0)
token_list = nn.functional.pad(token_list, (max_sequence_len - 1 - token_list.size(1), 0), 'constant', 0)
with torch.no_grad():
predicted = model(token_list)
predicted = torch.argmax(predicted, dim=-1).item()
output_word = ""
for word, index in tokenizer.items():
if index == predicted:
output_word = word
break
seed_text += " " + output_word
return seed_text


print(generate_text("Here is", 4, model, max_sequence_len))