跳到主要内容

循环神经网络

循环神经网络 (RNN) 是一种旨在通过利用隐藏状态来捕获时间信息以处理序列数据的神经网络。它们特别适用于语言建模等任务,在这些任务中,目标是根据先前标记的历史序列来预测下一个标记。

RNN 的基础知识

  • 潜变量模型:RNN 利用潜变量模型来近似给定所有先前标记 x1,,xt1x_1, \ldots, x_{t-1} 时标记 xtx_t 的概率。这在数学上表示为:

    P(xtxt1,,x1)P(xtht1),P(x_t \mid x_{t-1}, \ldots, x_1) \approx P(x_t \mid h_{t-1}),

    其中 ht1h_{t-1} 表示 t1t-1 时刻的隐藏状态。

  • 隐藏状态计算:隐藏状态 hth_t 在每个时间步通过函数 ff 利用当前输入 xtx_t 和先前的隐藏状态 ht1h_{t-1} 进行更新,如下所示:

    ht=f(xt,ht1).h_t = f(x_t, h_{t-1}).

    这个函数通常是非线性的,它允许 RNN 紧凑地表示截至当前时间步的观测数据历史。

  • 与隐藏层的区别:RNN 中的隐藏状态不应与其它类型神经网络中的隐藏层混淆。隐藏状态作为 RNN 每一步的输入,反映了序列到该时间点为止的记忆。

不带隐藏状态的神经网络

对于像单隐藏层多层感知机 (MLP) 这样更简单的神经网络模型,其计算不涉及任何时间动态:

H=ϕ(XWxh+bh),\mathbf{H} = \phi(\mathbf{X} \mathbf{W}_{\textrm{xh}} + \mathbf{b}_\textrm{h}),

其中 ϕ\phi 是激活函数,Wxh\mathbf{W}_{\textrm{xh}}bh\mathbf{b}_\textrm{h} 分别是权重和偏置参数。

带隐藏状态的循环神经网络

与非循环模型相反,RNN 在各个时间步之间保持一个隐藏状态,并利用当前输入和先前的隐藏状态循环更新它:

Ht=ϕ(XtWxh+Ht1Whh+bh),\mathbf{H}_t = \phi(\mathbf{X}_t \mathbf{W}_{\textrm{xh}} + \mathbf{H}_{t-1} \mathbf{W}_{\textrm{hh}} + \mathbf{b}_\textrm{h}),

这种循环更新机制使 RNN 能够在许多时间步中记住信息,使其成为时间序列预测和语言建模等任务的理想选择。

基于 RNN 的字符级语言模型

RNN 可用于在字符级别建模语言,其中网络根据过去的字符序列预测下一个字符。这种方法包括:

  • 移动序列以对齐输入和标签进行训练(例如,输入:"machine",标签:"achine")。
  • 使用 softmax 和交叉熵损失来训练模型以预测序列中的下一个字符。

示例:

Python 中的 RNN

下面是一个使用 PyTorch 的基本示例:

import torch
import torch.nn as nn

class SimpleRNN(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super(SimpleRNN, self).__init__()
self.hidden_size = hidden_size
self.rnn = nn.RNN(input_size, hidden_size, batch_first=True)
self.fc = nn.Linear(hidden_size, output_size)

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

# 示例用法
rnn = SimpleRNN(input_size=10, hidden_size=20, output_size=1)
input = torch.randn(5, 10, 10) # (批次大小, 序列长度, 输入大小)
output = rnn(input)
print(output)

这段 Python 代码使用 PyTorch 的 nn.RNN 层定义了一个简单的 RNN 模块。它处理输入序列,并在处理完最后一个序列元素后,使用全连接层返回输出。

在 PyTorch 中定制 RNN

这是一个 PyTorch 教程中的综合示例,展示了如何构建一个字符级 RNN 以将名称分类到其来源语言。此示例包括数据准备过程、RNN 模型构建和训练循环。

import torch
import torch.nn as nn
import torch.nn.functional as F
import random
import time
import math

# 辅助函数,将 Unicode 字符串转换为纯 ASCII
def unicodeToAscii(s):
return ''.join(c for c in unicodedata.normalize('NFD', s)
if unicodedata.category(c) != 'Mn')

# 读取文件并按行分割
def readLines(filename):
lines = open(filename, encoding='utf-8').read().strip().split('\n')
return [unicodeToAscii(line) for line in lines]

# RNN 模型
class RNN(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super(RNN, self).__init__()
self.hidden_size = hidden_size
self.i2h = nn.Linear(input_size + hidden_size, hidden_size)
self.i2o = nn.Linear(input_size + hidden_size, output_size)
self.softmax = nn.LogSoftmax(dim=1)

def forward(self, input, hidden):
combined = torch.cat((input, hidden), 1)
hidden = self.i2h(combined)
output = self.i2o(combined)
output = self.softmax(output)
return output, hidden

def initHidden(self):
return torch.zeros(1, self.hidden_size)

# 训练模型
def train(category_tensor, line_tensor):
hidden = rnn.initHidden()
rnn.zero_grad()
for i in range(line_tensor.size()[0]):
output, hidden = rnn(line_tensor[i], hidden)
loss = criterion(output, category_tensor)
loss.backward()
for p in rnn.parameters():
p.data.add_(p.grad.data, alpha=-learning_rate)
return output, loss.item()

# 训练循环
def timeSince(since):
now = time.time()
s = now - since
m = math.floor(s / 60)
s -= m * 60
return '%d m %d s' % (m, s)

n_iters = 100000
print_every = 5000
plot_every = 1000
all_losses = []
current_loss = 0
start = time.time()

for iter in range(1, n_iters + 1):
category, line, category_tensor, line_tensor = randomTrainingExample()
output, loss = train(category_tensor, line_tensor)
current_loss += loss

if iter % print_every == 0:
guess, guess_i = categoryFromOutput(output)
correct = '✓' if guess == category else '✗ (%s)' % category
print('%d %d%% (%s) %.4f %s / %s %s' % (
iter, iter / n_iters * 100, timeSince(start), loss, line, guess, correct))

if iter % plot_every == 0:
all_losses.append(current_loss / plot_every)
current_loss = 0

参考资料和有用链接