从零实现循环神经网络:基于d2l-pytorch项目的实践指南
引言
循环神经网络(RNN)是处理序列数据的强大工具,在自然语言处理、时间序列预测等领域有广泛应用。本文将基于d2l-pytorch项目,带您从零开始实现一个字符级RNN语言模型,使用H.G. Wells的《时间机器》作为训练数据。
数据准备与预处理
首先我们需要加载并预处理文本数据。在字符级语言模型中,我们将文本视为字符序列而非单词序列。这种处理方式简化了模型结构,特别适合学习RNN的基本原理。
import d2l
import torch
import torch.nn.functional as F
# 加载《时间机器》数据集
corpus_indices, vocab = d2l.load_data_time_machine()
独热编码(One-hot Encoding)
独热编码是将离散特征表示为向量的常用方法。对于字符级模型,每个字符被映射为一个长度等于词汇表大小的向量,其中对应字符索引的位置为1,其余为0。
# 示例:字符索引0和2的独热编码
F.one_hot(torch.Tensor([0, 2]).long(), len(vocab))
输出结果将显示两个向量,每个向量的长度等于词汇表大小(44个唯一字符),在相应索引位置为1。
为了批量处理数据,我们需要将形状为(批量大小, 时间步数)的输入转换为一系列形状为(批量大小, 词汇表大小)的矩阵:
def to_onehot(X, size):
return F.one_hot(X.long().transpose(0,-1), size)
X = torch.arange(10).reshape((2, 5))
inputs = to_onehot(X, len(vocab))
模型参数初始化
RNN的核心参数包括输入到隐藏层、隐藏层到隐藏层以及隐藏层到输出层的权重矩阵和偏置项:
num_inputs, num_hiddens, num_outputs = len(vocab), 512, len(vocab)
ctx = d2l.try_gpu() # 尝试使用GPU
def get_params():
def _one(shape):
return torch.Tensor(size=shape, device=ctx).normal_(std=0.01)
# 初始化参数
W_xh = _one((num_inputs, num_hiddens)) # 输入到隐藏层
W_hh = _one((num_hiddens, num_hiddens)) # 隐藏层到隐藏层
b_h = torch.zeros(num_hiddens, device=ctx) # 隐藏层偏置
W_hq = _one((num_hiddens, num_outputs)) # 隐藏层到输出层
b_q = torch.zeros(num_outputs, device=ctx) # 输出层偏置
params = [W_xh, W_hh, b_h, W_hq, b_q]
for param in params:
param.requires_grad_(True)
return params
RNN模型实现
RNN的核心在于如何在时间步之间传递隐藏状态。我们首先定义初始化隐藏状态的函数:
def init_rnn_state(batch_size, num_hiddens, ctx):
return (torch.zeros(size=(batch_size, num_hiddens), device=ctx), )
接下来是RNN前向传播的实现,使用tanh作为激活函数:
def rnn(inputs, state, params):
W_xh, W_hh, b_h, W_hq, b_q = params
H, = state
outputs = []
for X in inputs:
H = torch.tanh(torch.matmul(X.float(), W_xh) +
torch.matmul(H.float(), W_hh) + b_h)
Y = torch.matmul(H.float(), W_hq) + b_q
outputs.append(Y)
return outputs, (H,)
预测函数
为了测试模型,我们实现一个预测函数,根据给定的前缀字符序列生成后续字符:
def predict_rnn(prefix, num_chars, rnn, params, init_rnn_state,
num_hiddens, vocab, ctx):
state = init_rnn_state(1, num_hiddens, ctx)
output = [vocab[prefix[0]]]
for t in range(num_chars + len(prefix) - 1):
X = to_onehot(torch.Tensor([output[-1]],device=ctx), len(vocab))
(Y, state) = rnn(X, state, params)
if t < len(prefix) - 1:
output.append(vocab[prefix[t + 1]])
else:
output.append(int(Y[0].argmax(dim=1).item()))
return ''.join([vocab.idx_to_token[i] for i in output])
测试未经训练的模型预测结果:
predict_rnn('traveller ', 10, rnn, params, init_rnn_state, num_hiddens,
vocab, ctx)
梯度裁剪
RNN训练中常遇到梯度爆炸问题,梯度裁剪是解决这一问题的有效技术:
def grad_clipping(params, theta, ctx):
norm = torch.Tensor([0], device=ctx)
for param in params:
norm += (param.grad ** 2).sum()
norm = norm.sqrt().item()
if norm > theta:
for param in params:
param.grad.data.mul_(theta / norm)
困惑度(Perplexity)
困惑度是评估语言模型性能的重要指标,衡量模型预测下一个字符的不确定性:
困惑度 = exp(平均负对数似然)
较低的困惑度表示模型对序列预测更有信心。在实践中,我们通常同时关注训练困惑度和验证困惑度,以避免过拟合。
总结
本文从零实现了字符级RNN语言模型的关键组件,包括:
- 数据预处理和独热编码
- 模型参数初始化
- RNN前向传播实现
- 预测函数
- 梯度裁剪技术
- 困惑度评估指标
这些基础组件为后续训练和优化RNN模型奠定了基础。在实际应用中,您可能需要进一步实现训练循环、学习率调度等组件来完善模型。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考