【笔记】记录对自主实现一个神经网络的步骤的理解

这几天在看循环神经网络,感觉每次理解代码都很费时间,于是总结出了一些自主写代码的思路框架。

以循环神经网络为例。

步骤1:实现参数初始化

实现初始化参数的函数,循环神经网络中包含五个参数,三个w、两个b。

初始化w和b,函数需要传入的参数有:词汇表大小、隐藏单元数(、储存设备)。

python 复制代码
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

输入的大小是[1, num_inputs],输出大小是[1, num_outputs],隐藏层大小是[1, num_hiddens]。

然后初始化h,需要传入的参数有:批量大小、隐藏单元数。

python 复制代码
def init_rnn_state(batch_size, num_hiddens, device):
    return (torch.zeros((batch_size, num_hiddens), device=device), )

这个函数初始化了一个批次的h,将其整合在了一个二元矩阵里。

步骤2:建立模型

建立一个前向传播的函数,用于根据模型参数和输入计算输出。

函数传入参数有:输入数组(形状:(批量数、批量大小、词表大小))、隐状态h(形状:(1、批量大小、隐藏单元数))、模型参数(w和b,形状与初始化时相同)。

将每一个批量执行循环,更新h并获得批次的输出y,将y存放在数组中以便最后合并成全部输出。

python 复制代码
def rnn(inputs, state, params):
    # inputs的形状:(时间步数量,批量大小,词表大小)
    W_xh, W_hh, b_h, W_hq, b_q = params
    H, = state 
    outputs = []
    # X的形状:(批量大小,词表大小)
    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,)

返回的内容中:输出是所有输出的整合,也就是一整个批次的输出;H是最后一个批次执行完的隐状态。

然后就可以建立模型,模型中可以传入之前的初始化函数和前向传播函数。

模型参数包含:字母表大小、隐藏单元数(、设备)、初始化wb函数、初始化h函数、前向传播函数。

python 复制代码
class RNNModelScratch: #@save
    """从零开始实现的循环神经网络模型"""
    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)

步骤3:预测函数

建立预测的函数,批量大小为1。

传入参数为输入字符串、预测步数、模型、词汇表(、设备)。

预测步骤:

  1. 初始化状态
  2. 建立输出数组,将输入的第一个字符转化为词汇表中对应数字放入
  3. 建立简单的函数,取输出数组中最后一个字符,并将其转化为张量
  4. 预热循环,循环次数等于输入长度,利用第三步的函数取输出数组中最后一个字符扔进模型进行预测,丢弃预测结果,遍历递推输入字符串得出最后的h,将遍历完的输入字母放入
  5. 预测循环,循环次数等于预测步数,利用第三步的函数取输出数组中最后一个字符扔进模型进行预测,将预测结果和推出的h保留,预测结果放进输出数组中。
python 复制代码
def predict_ch8(prefix, num_preds, net, vocab, device):  #@save
    """在prefix后面生成新字符"""
    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):  # 预测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])

步骤4:梯度截断

由于循环神经网络的特殊性,我们需要实现梯度截断防止梯度爆炸。

函数传入参数:模型、θ,梯度模长大于θ时就只能取到θ。

python 复制代码
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

通过遍历对模型所有参数(params中包含模型所有参数)的梯度进行截断。

步骤5:训练

先建立训练一轮的代码。

传入参数:模型、训练集(包含x和y的迭代器)、损失计算函数、梯度更新函数(、设备)、是否使用随机抽样。

根据之前写的训练基本流程,先初始化状态和记录,然后放进循环进行迭代。

在第一次迭代时初始化状态state,否则分离张量。(如果使用了随机抽样,各个样本之间没有联系,那么也要初始化state)

前向传播,得出预测y_hat和状态state。

对比y和y_hat,计算损失loss。

反向传播,更新参数。

将损失loss和词元数量存入记录。

python 复制代码
#@save
def train_epoch_ch8(net, train_iter, loss, updater, device, use_random_iter):
    """训练网络一个迭代周期(定义见第8章)"""
    state, timer = None, d2l.Timer()
    metric = d2l.Accumulator(2)  # 训练损失之和,词元数量
    for X, Y in train_iter:
        if state is None or use_random_iter:
            # 在第一次迭代或使用随机抽样时初始化state
            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, 1)
            updater.step()
        else:
            l.backward()
            grad_clipping(net, 1)
            # 因为已经调用了mean函数
            updater(batch_size=1)
        metric.add(l * y.numel(), y.numel())
    return math.exp(metric[0] / metric[1]), metric[1] / timer.stop()

然后再写训练n轮的函数:

传入的参数有:模型、训练集迭代器、词汇表、学习率、轮次数(、设备)、是否使用随机抽样。

先定义损失函数,直接使用nn中的模型,定义记录数组,后续记录损失。

然后初始化更新参数的函数,可以直接使用随机梯度下降,也可有自己实现小函数,存入模型参数、学习率。

接着进行轮次为设定轮次数的循环,每次循环调用循环一轮的函数,并记录损失值。

最后得到损失随轮次变化的图像。

相关推荐
Nil_cxc1 分钟前
机器学习周报(12.9-12.15)
人工智能·机器学习
sealaugh3233 分钟前
aws(学习笔记第十八课) 使用aws cdk(python)进行部署
笔记·学习·aws
黑贝是条狗41 分钟前
STT语音识别转文字工具 - 离线运行的本地语音识别服务
人工智能·语音识别
古希腊掌管学习的神1 小时前
[搜广推] 王树森推荐算法——概要
人工智能·算法·机器学习·推荐算法
江挽枫_Jangmiko1 小时前
JAVA课堂笔记24(反射+设计模式)
java·开发语言·笔记·设计模式
ኈ ቼ ዽ1 小时前
opencv小练习(未完成版)
人工智能·opencv·计算机视觉
1101 11011 小时前
STM32-笔记3-驱动蜂鸣器
笔记
天天代码码天天2 小时前
C# OnnxRuntime Gaze-LLE 凝视目标估计,通过利用冻结的DINOv2编码器的特征来简化注视目标估计,预测一个人在场景中看的位置。
人工智能·深度学习·神经网络·opencv·目标检测·计算机视觉·凝视目标估计
tomlone2 小时前
构建知识图谱(方法论初探)
人工智能·知识图谱
goomind2 小时前
OpenCV与Qt5构建卡尺找直线工具
人工智能·qt·opencv·计算机视觉·卡尺找直线·卡尺工具