循环神经网络(RNN)作为处理序列数据的利器,在自然语言处理、时间序列预测等领域有着广泛应用。本文将基于 PyTorch 框架,从零开始实现一个 RNN 模型,并用于文本预测任务,帮助读者深入理解 RNN 的工作原理。

一、RNN 基础理论

循环神经网络的核心优势在于其能够处理可变长度的序列数据,并通过 "记忆" 机制捕捉序列中的时序依赖关系。与前馈神经网络不同,RNN 在每一步计算时会接收当前输入和上一步的隐藏状态,从而实现信息的传递。

基本结构包含:

输入层:接收序列数据

隐藏层:保存历史信息,计算公式为:

H_t=\tanh(X_t W_{xh} + H_{hh} W_{hh} + b_h)

输出层:基于当前隐藏状态生成预测,计算公式为:

Y_t =H_t W_{hq} + b_q

二、数据集准备

本文使用《时间机器》文本数据作为训练集,首先需要对文本进行预处理:

import torch
import NaturalLanguage_Dataset

# 批量大小和序列长度
batch_size, num_steps = 32, 35
# 加载数据并构建词汇表
train_iter, vocab = NaturalLanguage_Dataset.load_data_time_machine(batch_size, num_steps, max_tokens=10000)

数据处理关键步骤:

  1. 将文本分割为词元(token)
  2. 构建词汇表(将词元映射到整数索引)
  3. 生成批量数据(每个批量包含batch_size个序列,每个序列长度为num_steps

三、RNN 核心组件实现

3.1 参数初始化

RNN 需要初始化三类参数:输入到隐藏层、隐藏层到隐藏层、隐藏层到输出层的权重和偏置:

def get_params(vocab_size, num_hiddens, device):
    num_inputs = num_outputs = vocab_size  # 输入输出维度等于词汇表大小
    
    # 正态分布初始化参数
    def normal(shape):
        return torch.randn(size=shape, device=device) * 0.01
    
    # 隐藏层参数
    w_xh = normal((num_inputs, num_hiddens))  # 输入到隐藏层权重
    w_hh = normal((num_hiddens, num_hiddens))  # 隐藏层到隐藏层权重
    b_h = torch.zeros(num_hiddens, device=device)  # 隐藏层偏置
    
    # 输出层参数
    w_hq = normal((num_hiddens, num_outputs))  # 隐藏层到输出层权重
    b_q = torch.zeros(num_outputs, device=device)  # 输出层偏置
    
    # 所有参数需要计算梯度
    params = [w_xh, w_hh, b_h, w_hq, b_q]
    for param in params:
        param.requires_grad_(True)
    
    return params

3.2 隐藏状态初始化

隐藏状态需要在序列开始时初始化,通常初始化为全零向量:

def init_rnn_state(batch_size, num_hiddens, device):
    return (torch.zeros((batch_size, num_hiddens), device=device),)

3.3 RNN 前向传播

下面的rnn函数定义了如何在一个时间步内计算隐状态和输出。循环神经网络模型通过inputs最外层的维度。实现循环,以便逐时间步更新小批量数据的隐状态H。此外,这里使用tanh函数作为激活函数。当元素在实数上满足均匀分布时,tanh函数的平均值为0。实现 RNN 的核心计算逻辑,循环处理序列中的每个时间步:

def rnn(inputs, state, params):
    w_xh, w_hh, b_h, w_hq, b_q = params
    H, = state  # 解包隐藏状态
    outputs = []
    
    # 遍历序列中的每个时间步
    for X in inputs:
        # 计算隐藏状态(使用tanh激活函数)
        H = torch.tanh(torch.mm(X, w_xh) + torch.mm(H, w_hh) + b_h)
        # 计算输出
        Y = torch.mm(H, w_hq) + b_q
        outputs.append(Y)
    
    # 拼接所有时间步的输出,并返回新的隐藏状态
    return torch.cat(outputs, dim=0), (H,)

3.4 封装 RNN 模型

将上述组件封装为一个类,方便使用:

class RNNModuleScratch:
    def __init__(self, vocab_size, num_hiddens, device, get_params, init_state, forward_fn):
        self.vocab_size = vocab_size  # 词汇表大小
        self.num_hiddens = num_hiddens  # 隐藏层维度
        self.params = get_params(vocab_size, num_hiddens, device)  # 模型参数
        self.init_state = init_state  # 初始化状态函数
        self.forward_fn = forward_fn  # 前向传播函数
    
    # 前向传播(处理输入)
    def __call__(self, X, state):
        # 将输入转换为独热编码(X形状:(批量大小, 时间步) -> 转换为(时间步, 批量大小, 词汇表大小))
        X = F.one_hot(X.T, self.vocab_size).type(torch.float32)
        return self.forward_fn(X, state, self.params)
    
    # 初始化隐藏状态
    def begin_state(self, batch_size, device):
        return self.init_state(batch_size, self.num_hiddens, device)

四、文本预测实现

让我们首先定义预测函数来生成prefix之后的新字符,其中的prefix是一个用户提供的包含多个字符的字符串。在循环中的开始字符时,我们将状态传递到下一个时间步,但不会产生任何输出。这被称为预热(warm-up)期,因为在此期间模型会自我更新(例如,更新状态),但不会进行预测。预热期结束后,隐状态的值通常比刚开始的初始值更适合预测,从而预测字符并输出它们。训练好的模型可以用于文本生成,核心是根据前缀预测后续字符:

def predict_ch8(prefix, num_preds, net, vocab, device):
    # 初始化隐藏状态
    state = net.begin_state(batch_size=1, device=device)
    # 前缀转换为索引序列
    outputs = [vocab[prefix[0]]]
    
    # 获取下一个输入(取最后一个输出作为输入)
    get_input = lambda: torch.tensor([outputs[-1]], device=device).reshape((1, 1))
    
    # 预热期:处理前缀中的字符,更新隐藏状态
    for y in prefix[1:]:
        _, state = net(get_input(), state)
        outputs.append(vocab[y])
    
    # 预测阶段:生成指定数量的字符
    for _ in range(num_preds):
        y, state = net(get_input(), state)
        outputs.append(int(y.argmax(dim=1).reshape(1)))  # 取概率最大的字符
    
    # 将索引转换为字符
    return ''.join([vocab.idx_to_token[i] for i in outputs])

五、模型训练

5.1 梯度裁剪

RNN 训练中容易出现梯度爆炸问题,通过梯度裁剪解决:

def grad_clipping(net, theta):
    if isinstance(net, nn.Module):
        params = [p for p in net.parameters() if p.requires_grad]
    else:
        params = net.params
    # 计算梯度范数
    norm = torch.sqrt(sum(torch.sum((p.grad ** 2)) for p in params))
    if norm > theta:  # 如果范数超过阈值,则裁剪
        for param in params:
            param.grad[:] *= theta / norm

5.2 训练循环

这段代码定义了训练循环的一个epoch过程。初始化隐藏状态和计时器,用Accumulator记录总损失和词元数。遍历训练数据,若首次迭代或随机抽样,初始化隐藏状态;否则分离状态避免梯度回流过远。调整输入输出形状并移至设备,前向传播得预测和新状态,计算损失。根据优化器类型,执行反向传播、梯度裁剪和参数更新。最后返回困惑度(损失指数)和训练速度,实现了RNN训练的核心流程。

实现完整的训练过程:

def train_epoch_ch8(net, train_iter, loss, updater, device, use_random_iter):
    state, timer = None, d2l.Timer()
    metric = d2l.Accumulator(2)  # 存储总损失和总词元数
    
    for X, Y in train_iter:
        # 初始化或重置隐藏状态
        if state is None or use_random_iter:
            state = net.begin_state(batch_size=X.shape[0], device=device)
        else:
            # 分离隐藏状态,避免梯度回流过远
            for s in state:
                s.detach_()
        
        # 准备输入输出
        y = Y.T.reshape(-1)  # 调整标签形状
        X, y = X.to(device), y.to(device)
        
        # 前向传播
        y_hat, state = net(X, state)
        l = loss(y_hat, y.long()).mean()  # 计算损失
        
        # 反向传播和参数更新
        if isinstance(updater, torch.optim.Optimizer):
            updater.zero_grad()
            l.backward()
            grad_clipping(net, 1)  # 梯度裁剪
            updater.step()
        else:
            l.backward()
            grad_clipping(net, 1)
            updater(batch_size=1)
        
        metric.add(l * y.numel(), y.numel())
    
    # 返回困惑度(指数化的平均损失)和训练速度
    return math.exp(metric[0] / metric[1]), metric[1] / timer.stop()

5.3 启动训练

这段代码是RNN模型的完整训练函数。首先定义交叉熵损失和动画器可视化困惑度。根据网络类型初始化优化器(PyTorch模块用SGD优化器,自定义网络用sgd函数)。定义预测函数生成文本。循环训练指定轮次,每轮调用train_epoch_ch8更新模型,每10轮打印预测结果并记录困惑度。训练结束后输出最终困惑度、训练速度及预测文本,实现了模型训练与效果跟踪的完整流程。

def train_ch8(net, train_iter, vocab, lr, num_epochs, device, use_random_iter=False):
    loss = nn.CrossEntropyLoss()
    animator = d2l.Animator(xlabel='epoch', ylabel='perplexity', legend=['train'], xlim=[10, num_epochs])
    
    # 初始化优化器
    if isinstance(net, nn.Module):
        updater = torch.optim.SGD(net.parameters(), lr)
    else:
        updater = lambda batch_size: d2l.sgd(net.params, lr, batch_size)
    
    # 定义预测函数
    predict = lambda prefix: predict_ch8(prefix, 50, net, vocab, device)
    
    # 训练循环
    for epoch in range(num_epochs):
        ppl, speed = train_epoch_ch8(net, train_iter, loss, updater, device, use_random_iter)
        if (epoch + 1) % 10 == 0:
            print(predict('time traveller'))  # 每10轮打印一次预测结果
            animator.add(epoch + 1, [ppl])
    
    # 输出最终结果
    print(f'困惑度 {ppl:.1f}, {speed:.1f} 词元/秒 {str(device)}')
    print(predict('time traveller'))
    print(predict('traveller'))

六、实验结果与分析

6.1 训练设置

num_hiddens = 512  # 隐藏层维度
device = d2l.try_gpu()  # 使用GPU(如果可用)
num_epochs, lr = 500, 1  # 训练轮数和学习率

6.2 两种抽样方法对比

  1. 顺序抽样:保持序列的连续性,隐藏状态在批次间传递
net = RNNModuleScratch(len(vocab), num_hiddens, device, get_params, init_rnn_state, rnn)
train_ch8(net, train_iter, vocab, lr, num_epochs, device)

输出结果:

  1. 随机抽样:每个批次的序列随机抽取,隐藏状态在批次间重置
net = RNNModuleScratch(len(vocab), num_hiddens, device, get_params, init_rnn_state, rnn)
train_ch8(net, train_iter, vocab, lr, num_epochs, device, use_random_iter=True)

输出结果:

6.3 结果分析

  • 困惑度(Perplexity):越低表示模型性能越好,理想情况下接近 1
  • 生成文本质量:训练充分的模型能生成语法相对通顺、与原文风格相似的文本
  • 训练速度:GPU 加速能显著提升训练效率

七、总结与展望

本文从零开始实现了一个基础 RNN 模型,包括参数初始化、前向传播、文本预测和模型训练等完整流程。通过实验可以发现,基础 RNN 在处理长序列时仍有局限(如梯度消失 / 爆炸、长期依赖捕捉能力弱)。

后续可以尝试:

  1. 改用 LSTM 或 GRU 等改进型循环神经网络
  2. 增加注意力机制提升长序列处理能力
  3. 使用预训练词向量替换独热编码
  4. 尝试更复杂的优化器(如 Adam)和正则化方法
     

通过从零实现的过程,我们能更深入地理解 RNN 的工作原理,为学习更复杂的序列模型打下基础。

八、代码汇总


 

import matplotlib.pyplot as plt
import math
import torch
import NaturalLanguage_Dataset
from torch import nn
from d2l import torch as d2l
from torch.nn import functional as F


batch_size,num_steps=32,35
train_iter,vocab=NaturalLanguage_Dataset.load_data_time_machine(batch_size,num_steps,max_tokens=10000)

#独热编码
# print(F.one_hot(torch.tensor([0,2]),len(vocab)))
X=torch.arange(10).reshape((2,5))
# print(X.shape)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            `````````````````````````````````````

#获取参数
def get_params(vocab_size,num_hiddens,device):
    num_inputs=num_outputs=vocab_size

    def normal(shape):
        return torch.randn(size=shape,device=device)*0.01

    #隐藏层参数
    w_xh=normal((num_inputs,num_hiddens))
    w_hh=normal((num_hiddens,num_hiddens))
    #一维,广播机制
    b_h=torch.zeros(num_hiddens,device=device)

    #输出层参数
    w_hq=normal((num_hiddens,num_outputs))
    #一维,广播机制
    b_q=torch.zeros(num_outputs,device=device)

    params=[w_xh,w_hh,b_h,w_hq,b_q]
    for param in params:
        param.requires_grad_(True)

    return params

def init_rnn_state(batch_size,num_hiddens,device):
    return (torch.zeros((batch_size,num_hiddens),device=device),)

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.mm(X,w_xh)+torch.mm(H,w_hh)+b_h)
        Y=torch.mm(H,w_hq)+b_q
        outputs.append(Y)
    return torch.cat(outputs,dim=0),(H,)

class RNNModuleScratch:
    #参数:vocab_size(词汇表大小)、num_hiddens(隐藏层维度)、device(运行设备,如 GPU/CPU)
    #get_params(获取模型参数的函数)、init_state(初始化隐藏状态的函数)、forward_fn(前向传播逻辑的函数)。
    def __init__(self,vocab_size,num_hiddens,device,get_params,init_state,forward_fn):
        self.vocab_size,self.num_hiddens=vocab_size,num_hiddens
        self.params=get_params(vocab_size,num_hiddens,device)
        self.init_state,self.forward_fn=init_state,forward_fn

    #向前传播
    def __call__(self,X,state):
        X=F.one_hot(X.T,self.vocab_size).type(torch.float32)
        return self.forward_fn(X,state,self.params)
    #初始化隐藏状态
    def begin_state(self,batch_size,device):
        return self.init_state(batch_size,self.num_hiddens,device)


num_hiddens=512
net=RNNModuleScratch(len(vocab),num_hiddens,d2l.try_gpu(),get_params,init_rnn_state,rnn)
state=net.begin_state(X.shape[0],d2l.try_gpu())
Y,new_state=net(X.to(d2l.try_gpu()),state)
# print(Y.shape,len(new_state),new_state[0].shape)



def predict_ch8(prefix,num_preds,net,vocab,device):
    #得到隐藏状态
    state=net.begin_state(batch_size=1,device=device)
    #初始化输出序列
    outputs=[vocab[prefix[0]]]

    get_input=lambda: torch.tensor([outputs[-1]],device=device).reshape((1,1))
    #预热期
    for y in prefix[1:]:
        _,state=net(get_input(),state)
        outputs.append(vocab[y])
    #预测num_steps
    for _ in range(num_preds):
        y,state=net(get_input(),state)
        outputs.append(int(y.argmax(dim=1).reshape(1)))
    return ''.join([vocab.idx_to_token[i] for i in outputs])

#梯度剪裁
def grad_clipping(net,theta):
    if isinstance(net,nn.Module):
        params=[p for p in net.parameters() if p.requires_grad]
    else:
        params=net.params
    norm=torch.sqrt(sum(torch.sum((p.grad**2)) for p in params))
    if norm >theta:
        for param in params:
            param.grad[:]*=theta/norm



#updater:优化器(或自定义更新规则)如:sgd
def train_epoch_ch8(net,train_iter,loss,updater,device,use_random_iter):
    state,timer=None,d2l.Timer()
    metric=d2l.Accumulator(2)
    for X,Y in train_iter:
        if state is None or use_random_iter:
            state=net.begin_state(batch_size=X.shape[0],device=device)
        else:
            if isinstance(net,nn.Module) and not isinstance((state,tuple)):
                #state对于nn.GRU是张量
                state.detach_()
            else:
                #state对于nn.LSTM或对于我们从零开始的模型是张量
                for s in state:
                    s.detach_()
        y=Y.T.reshape(-1)
        X,y=X.to(device),y.to(device)
        y_hat,state=net(X,state)
        l=loss(y_hat,y.long()).mean()
        if isinstance(updater,torch.optim.Optimizer):
            updater.zero_grad()
            l.backward()
            grad_clipping(net,l)
            updater.step()
        else:
            l.backward()
            grad_clipping(net,l)
            updater(batch_size=1)
        metric.add(l*y.numel(),y.numel())
    return math.exp(metric[0]/metric[1]),metric[1]/timer.stop()

def train_ch8(net,train_iter,vocab,lr,num_epochs,device,use_random_iter=False):
    loss=nn.CrossEntropyLoss()
    animator=d2l.Animator(xlabel='epoch',ylabel='perplexity',legend=['train'],xlim=[10,num_epochs])

    #初始化
    if isinstance(net,nn.Module):
        updater=torch.optim.SGD(net.parameters(),lr)
    else:
        updater=lambda batch_size:d2l.sgd(net.params,lr,batch_size)
    predict=lambda prefix:predict_ch8(prefix,50,net,vocab,device)
    for epoch in range(num_epochs):
        ppl,speed=train_epoch_ch8(net,train_iter,loss,updater,device,use_random_iter)
        if (epoch+1)%10==0:
            print(predict('time traveller'))
            animator.add(epoch+1,[ppl])
    print(f'困惑度{ppl:.1f},{speed:.1f}词元/秒{str(device)}')
    print(predict('time traveller'))
    print(predict('traveller'))

#顺序抽样方法
num_epochs,lr=500,1
train_ch8(net,train_iter,vocab,lr,num_epochs,d2l.try_gpu())

#随机抽样方法
net=RNNModuleScratch(len(vocab),num_hiddens,d2l.try_gpu(),get_params,init_rnn_state,rnn)
# train_ch8(net,train_iter,vocab,lr,num_epochs,d2l.try_gpu(),use_random_iter=True)
plt.show()

补充性知识

梯度剪裁

梯度剪裁(Gradient Clipping)是在深度学习训练过程中,尤其是在循环神经网络(RNN)及其变种(如 LSTM、GRU)等容易出现梯度问题的模型训练里,经常使用的一种技术手段,用于解决梯度爆炸问题,以下是详细介绍:

1. 梯度爆炸问题

在深度学习模型训练时,通过反向传播算法来计算梯度并更新模型参数。在一些情况下,比如使用深层神经网络或者处理长序列的 RNN 模型时,梯度在反向传播过程中会不断累积。当梯度的值变得非常大,以指数形式增长时,就会出现梯度爆炸现象。

梯度爆炸会导致模型训练不稳定,参数更新幅度过大,使得模型无法收敛,甚至出现参数变为无穷大,导致训练无法继续进行 。例如,在训练 RNN 对长文本进行建模时,随着序列长度增加,反向传播的路径变长,就更容易出现梯度爆炸。

2. 梯度剪裁原理

梯度剪裁的核心思想是限制梯度的大小,避免梯度值过大。具体做法是设置一个阈值(通常用 \(\theta\) 表示),当计算得到的梯度的范数(常用 L2 范数,即欧几里得范数)超过这个阈值时,就对梯度进行缩放,使其范数等于该阈值。

假设模型的所有可训练参数为 \(\theta_1, \theta_2, ..., \theta_n\),对应的梯度为 \(g_1, g_2, ..., g_n\),计算梯度的 L2 范数公式为:\(||g||_2 = \sqrt{\sum_{i=1}^{n} g_i^2}\)如果 \(||g||_2 > \theta\),则对梯度进行如下缩放:\(g_i' = g_i \times \frac{\theta}{||g||_2} \quad (i = 1, 2, ..., n)\)其中,\(g_i'\) 是剪裁后的梯度值。这样操作后,梯度的方向不变,但是大小被限制在指定的阈值范围内,使得模型参数更新更加稳定。

3. 梯度剪裁的实现

以 PyTorch 为例,实现梯度剪裁的代码如下:

import torch

def grad_clipping(net, theta):
    if isinstance(net, torch.nn.Module):
        params = [p for p in net.parameters() if p.requires_grad]
    else:
        params = net.params
    norm = torch.sqrt(sum(torch.sum((p.grad ** 2)) for p in params))
    if norm > theta:
        for param in params:
            param.grad[:] *= theta / norm

在上述代码中,net 是深度学习模型,theta 是设定的梯度剪裁阈值。首先,收集模型中所有需要计算梯度的参数,然后计算这些参数梯度的 L2 范数 。如果范数超过阈值,就对每个参数的梯度进行剪裁。

4. 梯度剪裁的作用与局限性

  • 作用:有效解决梯度爆炸问题,使得模型训练更加稳定,提高模型的收敛性,尤其在处理长序列数据或者深层神经网络时效果显著。例如在语音识别任务中,使用 RNN-LSTM 模型时,结合梯度剪裁技术,能够帮助模型更好地学习语音序列中的长期依赖关系。
  • 局限性:虽然梯度剪裁解决了梯度爆炸问题,但它并不能解决梯度消失问题。并且,剪裁阈值的选择较为关键,阈值设置不当可能会导致模型收敛速度变慢,或者仍然无法有效解决梯度爆炸问题。

梯度剪裁是深度学习训练中应对梯度爆炸的有效手段,合理使用能够提升模型训练的稳定性和效果。

在深度学习领域,独热编码(One-Hot Encoding)是一种常用的特征编码方式,以下是关于它的详细介绍:

独热编码

1. 定义与原理

独热编码,也叫一位有效编码,其核心思想是使用 N 位状态寄存器来对 N 个可能的取值进行编码,每个状态都由独立的寄存器位表示,并且在任意时刻只有一位有效。


 

以文本数据处理为例,假设我们有一个包含 “苹果”“香蕉”“橘子” 这三个类别的水果词汇表。在独热编码中,会将每个类别映射为一个长度等于词汇表大小的向量。对于 “苹果”,编码后可能是 [1, 0, 0];“香蕉” 编码后是 [0, 1, 0];“橘子” 编码后是 [0, 0, 1] 。可以看到,在每个向量中,只有对应类别位置上的值为 1,其余位置的值均为 0 。

2. 实现方式

  • Python 实现:借助scikit-learn库实现对离散特征的独热编码。假设我们有一个包含动物种类的列表:
from sklearn.preprocessing import OneHotEncoder
import numpy as np

data = np.array([['狗'], ['猫'], ['兔子']])
encoder = OneHotEncoder()
encoded_data = encoder.fit_transform(data).toarray()
print(encoded_data)
  • PyTorch 实现:在深度学习框架 PyTorch 中,可使用torch.nn.functional.one_hot函数对整数张量进行独热编码。例如,对一个包含类别索引的张量进行编码:
import torch
import torch.nn.functional as F

# 假设类别索引张量,这里有3个样本,每个样本的类别索引分别是0, 1, 2
idx = torch.tensor([0, 1, 2]) 
# 假设共有3个类别
encoded = F.one_hot(idx, num_classes=3)
print(encoded)

3. 在深度学习中的应用场景

  • 文本分类:在自然语言处理中,将单词或字符转换为独热编码向量,作为神经网络的输入。例如,构建一个简单的文本情感分类模型,需要先将文本中的单词通过独热编码的方式进行向量化处理,然后输入到神经网络中进行训练。但由于文本中单词数量往往非常庞大,会导致独热编码后的向量维度极高且稀疏,实际中常结合词嵌入(如 Word2Vec、GloVe)等方法来优化。
  • 图像分类:在一些简单的图像分类任务中,对于图像的标签可以使用独热编码。比如,将猫狗分类任务中的 “猫” 标签编码为 [1, 0],“狗” 标签编码为 [0, 1] ,方便模型进行学习和预测。
  • 多分类问题:对于具有多个离散类别的分类任务,都可以使用独热编码对类别标签进行处理,让神经网络更好地理解和学习不同类别之间的差异。

4. 优缺点

  • 优点
    • 编码简单直观:能够清晰地将离散的类别信息转化为机器可识别的向量形式,易于理解和实现。
    • 可扩展性好:当有新的类别加入时,只需在向量末尾增加一个维度即可,不会影响已有的编码结构。
    • 避免了类别间的顺序关系:对于像水果类别这样不存在顺序关系的离散变量,独热编码不会给模型引入错误的顺序信息。
  • 缺点
    • 高维度与稀疏性:当类别数量较多时,独热编码会生成高维度且稀疏的向量,占用大量内存,并且会增加模型的计算量和训练时间。
    • 缺乏语义信息:每个编码向量都是独立的,无法体现不同类别之间潜在的语义关联。例如在文本中,“苹果” 和 “香蕉” 都属于水果,具有一定语义相关性,但独热编码无法体现这种关系。

独热编码是一种基础且重要的特征编码方式,在深度学习中有着广泛应用,虽然存在一些局限性,但在很多场景下通过与其他技术结合,仍然能发挥重要作用。

detach()和detach_()的区别与介绍

在 PyTorch 中,detach()detach_() 都是用于处理张量(Tensor)的方法,主要用于切断计算图中的梯度传播,它们在功能上有相似之处,但也有一些区别,下面结合你提供的代码来详细解释它们的作用:

共同作用

两者的核心作用是将张量从计算图中 “分离” 出来,使得张量后续的计算不会再参与梯度的计算和传播。在训练循环神经网络(如代码中涉及的 GRU、LSTM 等)时,模型的状态(state)可能会在多个时间步或者多个批次中传递,如果不将其从计算图中分离,随着训练的进行,计算图会变得非常庞大,不仅会占用大量内存,还可能导致梯度计算异常复杂甚至出现梯度爆炸等问题。通过 detach()detach_() 操作,可以避免这种情况,只保留张量的数值,而不再关联其梯度相关的信息 。

区别

  • detach():这是一个返回新张量的方法,新张量与原张量共享数据(即视图,对新张量数据的修改会影响原张量,除非进行了拷贝等操作),但新张量的 requires_grad 属性被设置为 False,不再参与梯度计算。原张量的计算图连接被切断,但原张量本身的 requires_grad 等梯度相关属性不会改变 。例如:
import torch
x = torch.tensor([1.0, 2.0], requires_grad=True)
y = x * 2
z = y.detach()
# z 的 requires_grad 为 False,后续对 z 的计算不会影响 x 的梯度

  • detach_():这是一个原地(in-place)操作的方法,它直接修改原张量,将原张量从计算图中分离,原张量的 requires_grad 会被设置为 False,后续关于该张量的计算也不再参与梯度传播。它没有返回值,直接作用于原张量 。比如:
import torch
x = torch.tensor([1.0, 2.0], requires_grad=True)
y = x * 2
y.detach_()
# 此时 y 已经从计算图中分离,后续对 y 的操作不影响梯度

代码中的具体体现

代码中有针对循环神经网络的状态(state)进行 detach()detach_() 操作:

if isinstance(net, nn.Module) and not isinstance(state, tuple):
    # state对于nn.GRU是个张量
    state.detach_()
else:
    # state对于nn.LSTM或对于我们从零开始实现的模型是个张量
    for s in state:
        s.detach_()

这里根据 state 的具体类型(是针对 nn.GRU 单个张量形式的状态,还是针对 nn.LSTM 等多个张量组成的状态),使用 detach_() 来原地切断状态张量与计算图的连接。这样做的目的是,在不需要让循环神经网络的状态参与梯度计算和传播时(比如在一些特定的状态初始化和传递场景下),避免状态的梯度影响模型参数的更新,同时也能控制计算图的复杂度,提升训练效率和稳定性 。

总之,detach()detach_() 都能实现切断张量梯度传播的功能,detach() 返回新张量,detach_() 原地修改原张量,在实际使用中可根据需求选择合适的方法,在你给出的循环神经网络训练代码里,用 detach_() 来处理状态张量是为了合理管理计算图,保障训练过程的高效与稳定 。

介绍numel()的作用

def train_epoch_ch8(net,train_iter,loss,updater,device,use_random_iter):
    state,timer=None,d2l.Timer()
    metric=d2l.Accumulator(2)
    for X,Y in train_iter:
        if state is None or use_random_iter:
            state=net.begin_state(batch_size=X.shape[0],device=device)
        else:
            if isinstance(net,nn.Module) and not isinstance((state,tuple)):
                #state对于nn.GRU是张量
                state.detach_()
            else:
                #state对于nn.LSTM或对于我们从零开始的模型是张量
                for s in state:
                    s.detach_()
        y=Y.T.reshape(-1)
        X,y=X.to(device),y.to(device)
        y_hat,state=net(X,state)
        l=loss(y_hat,y.long()).mean()
        if isinstance(updater,torch.optim.Optimizer):
            updater.zero_grad()
            l.backward()
            grad_clipping(net,l)
            updater.step()
        else:
            l.backward()
            grad_clipping(net,l)
            updater(batch_size=1)
        metric.add(l*y.numel(),y.numel())
    return math.exp(metric[0]/metric[1]),metric[1]/timer.stop()

在 PyTorch 中,numel() 是张量(torch.Tensor)的一个方法,用于返回张量中元素的总数量。结合你提供的代码片段 metric.add(l * y.numel(), y.numel()) ,下面详细解释其作用:

1. y.numel() 的作用

  • 计算张量元素总数:对于标签张量 y(经过前面的 y = Y.T.reshape(-1) 等操作后,它是一个一维或多维的张量 ),y.numel() 会返回 y 中所有元素的个数。例如,如果 y 是形状为 torch.Size([64])(64 个元素的一维张量 ),那么 y.numel() 的结果就是 64;如果 y 是形状为 torch.Size([32, 10])(32 行 10 列的二维张量 ),y.numel() 结果就是 32 * 10 = 320

  • 在代码中的意义:在训练循环神经网络(从代码里的 grad_clipping 等操作能看出是在做循环网络相关训练,比如文本序列任务 )时,这里的 y 通常代表一个批次的标签数据。y.numel() 用于统计当前批次标签的总元素数量,也就是当前批次的样本相关的 “计数单元”(比如在语言模型中,可能是词元数量 )。

2. 在 metric.add(l * y.numel(), y.numel()) 中的作用

  • 累加总损失l 是当前批次计算得到的平均损失(通过 l = loss(y_hat, y.long()).mean() 计算而来 ),l * y.numel() 就是当前批次的总损失(平均损失乘以样本元素总数,把平均损失还原回总损失 )。然后 metric.add 方法(metricd2l.Accumulator 类型,用于累加指标 )会把这个总损失累加到 metric 中专门记录总损失的位置。

  • 累加样本(元素)总数y.numel() 作为当前批次的元素总数,会被累加到 metric 中记录样本(元素 )总数的位置。后续在计算整个训练周期的平均损失时,就可以用累加的总损失除以累加的元素总数(即 metric[0] / metric[1] ,假设 metric 第一个位置存总损失,第二个位置存总元素数 ),得到更准确的平均损失指标。

举个简单例子,假设当前批次 y10 个元素(y.numel() = 10 ),计算出的平均损失 l = 0.5 ,那么 l * y.numel() = 5(总损失 ),metric.add(5, 10) 就会把总损失 5 和元素总数 10 分别累加到 metric 里,方便后续统计整个训练过程的平均损失等信息。

总之,numel() 在这里的核心价值是精准统计每个批次的标签元素数量,让损失的累加和平均计算更准确,从而更好地监控模型在训练过程中的表现 。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐