欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

《动手学深度学习》task2——文本预处理,语言模型,循环神经网络基础笔记

程序员文章站 2024-03-14 11:50:04
...

系统学习《动手学深度学习》点击这里:

《动手学深度学习》task1_1 线性回归
《动手学深度学习》task1_2 Softmax与分类模型
《动手学深度学习》task1_3 多层感知机
《动手学深度学习》task2_1 文本预处理
《动手学深度学习》task2_2 语言模型
《动手学深度学习》task2_3 循环神经网络基础

1 文本预处理

文本是一类序列数据,一篇文章可以看作是字符或单词的序列,本节将介绍文本数据的常见预处理步骤,预处理通常包括四个步骤:

  1. 读入文本
  2. 分词
  3. 建立字典,将每个词映射到一个唯一的索引(index)
  4. 将文本从词的序列转换为索引的序列,方便输入模型

1.1 关于建立词典

这里是对Vocab类实现的理解,首先是这个类想干什么?

Vocab类想实现将词映射成一个索引,既然是索引那么相同的词就应该具有相同的索引,所以这里对于输入的文本还会进行一个去重的操作。

此外,Vocab还想方便的获取给定某个词对应的索引,以及给定一个索引获取这个索引所对应的词。除了上面说的两个功能,还有一个就是

统计了每一个词的词频。

代码部分主要是由几个列表的复杂操作,理解了那几行代码,应该就能完全看懂代码在干什么了。

1.2 关于在词典中的pad,bos, eos, unk

  1. pad的作用是在采用批量样本训练时,对于长度不同的样本(句子),对于短的样本采用pad进行填充,使得每个样本的长度是一致的
  2. bos( begin of sentence)和eos(end of sentence)是用来表示一句话的开始和结尾
  3. unk(unknow)的作用是,处理遇到从未出现在预料库的词时都统一认为是unknow ,在代码中还可以将一些频率特别低的词也归为这一类

1.3 关于用现有工具进行分词

前面的分词方式非常简单,它至少有以下几个缺点:

  1. 标点符号通常可以提供语义信息,但是我们的方法直接将其丢弃了
  2. 类似“shouldn’t", "doesn’t"这样的词会被错误地处理
  3. 类似"Mr.", "Dr."这样的词会被错误地处理

我们可以通过引入更复杂的规则来解决这些问题,但是事实上,有一些现有的工具可以很好地进行分词,我们在这里简单介绍其中的两个:spaCyNLTK

家里网太差了,这两个包下载不了,等网好了再实验

2 语言模型

2.1 n-gram

NN元语法是基于n1n-1阶马尔可夫链的概率语言模型,其中nn权衡了计算复杂度和模型准确性

语言模型可用于提升语音识别机器翻译的性能。例如,在语音识别中,给定一段“厨房里食油用完了”的语音,有可能会输出“厨房里食油用完了”和“厨房里石油用完了”这两个读音完全一样的文本序列。如果语言模型判断出前者的概率大于后者的概率,我们就可以根据相同读音的语音输出“厨房里食油用完了”的文本序列。在机器翻译中,如果对英文“you go first”逐词翻译成中文的话,可能得到“你走先”“你先走”等排列方式的文本序列。如果语言模型判断出“你先走”的概率大于其他排列方式的文本序列的概率,我们就可以把“you go first”翻译成“你先走”。

2.2 统计语言模型——n-gram模型的缺陷

  1. 参数空间过大

    P(w1,w2,w3,w4)=P(w1)P(w2w1)P(w3w1,w2)P(w4w1,w2,w3).P(w_1, w_2, w_3, w_4) = P(w_1) P(w_2 \mid w_1) P(w_3 \mid w_1, w_2) P(w_4 \mid w_1, w_2, w_3).

    假设计算P(w1)P(w_1) 需要的参数空间为v,则上述式子需要的总参数空间为v + v^2 + v^3 + v^4

  2. 数据稀疏

    齐夫定律:在自然语言的语料库中,一个单词出现的频率与它在频率表中的排名成反比,表明大部分单词出现的频率会很小,甚至不会出现,这就会出现概率估计不准确的问题,比如「荸荠」这个单词,很可能在我们所给的数据集中不会出现,所以他的频率为0,但是我们可以确定,他真的在真实世界中是不会出现的单词么?

    如果使用n元语法模型存在数据稀疏问题,最终计算出来的大部分参数都为0

2.3 关于one-hot编码

"""
n_class,x为索引,比如x = torch.tensor([0, 2]),x.shape = (n, class),
词典大小为n,向量的长度等于词典的大小
"""
def one_hot(x, n_class, dtype=torch.float32):
    result = torch.zeros(x.shape[0], n_class, dtype=dtype, device=x.device)  # shape: (n, n_class)
    # print(x.long())
    # print('x.long().view(-1, 1)', x.long().view(-1, 1))
    # result[i][x[i][j]] = 1
    result.scatter_(1, x.long().view(-1, 1), 1)  # result[i, x[i, 0]] = 1
    return result

scatter_()函数:

scatter()scatter_() 的作用是一样的,只不过 scatter() 不会直接修改原来的 Tensor,而 scatter_()

PyTorch 中,一般函数加下划线代表直接在原来的 Tensor 上修改

scatter(dim, index, src) 的参数有 3 个

  • **dim:**沿着哪个维度进行索引
  • **index:**用来 scatter 的元素索引
  • **src:**用来 scatter 的源元素,可以是一个标量或一个张量

这个 scatter 可以理解成放置元素或者修改元素

简单说就是通过一个张量 src 来修改另一个张量,哪个元素需要修改、用 src 中的哪个元素来修改由 dim 和 index 决定

官方文档给出了 3维张量 的具体操作说明,如下所示

self[index[i][j][k]][j][k] = src[i][j][k]  # if dim == 0
self[i][index[i][j][k]][k] = src[i][j][k]  # if dim == 1
self[i][j][index[i][j][k]] = src[i][j][k]  # if dim == 2

详细解析见此链接

2.4 随机采样和相邻采样的区别:

  • 在随机采样中,每个样本是原始序列上任意截取的一段序列。相邻的两个随机小批量在原始序列上的位置不一定相毗邻。因此,我们无法用一个小批量最终时间步的隐藏状态来初始化下一个小批量的隐藏状态。在训练模型时,每次随机采样前都需要重新初始化隐藏状态

  • 在相邻采样中,用一个小批量最终时间步的隐藏状态来初始化下一个小批量的隐藏状态,从而使下一个小批量的输出也取决于当前小批量的输入,并如此循环下去。这对实现循环神经网络造成了两方面影响:一方面,
    在训练模型时,我们只需在每一个迭代周期开始时初始化隐藏状态;另一方面,当多个相邻小批量通过传递隐藏状态串联起来时,模型参数的梯度计算将依赖所有串联起来的小批量序列。同一迭代周期中,随着迭代次数的增加,梯度的计算开销会越来越大
    为了使模型参数的梯度计算只依赖一次迭代读取的小批量序列,我们可以在每次读取小批量前将隐藏状态从计算图中分离出来
    《动手学深度学习》task2——文本预处理,语言模型,循环神经网络基础笔记

3 循环神经网络

现在我们考虑输入数据存在时间相关性的情况。假设XtRn×d\boldsymbol{X}_t \in \mathbb{R}^{n \times d}是序列中时间步tt的小批量输入,HtRn×h\boldsymbol{H}_t \in \mathbb{R}^{n \times h}是该时间步的隐藏变量。与多层感知机不同的是,这里我们保存上一时间步的隐藏变量Ht1\boldsymbol{H}_{t-1},并引入一个新的权重参数WhhRh×h\boldsymbol{W}_{hh} \in \mathbb{R}^{h \times h},该参数用来描述在当前时间步如何使用上一时间步的隐藏变量。具体来说,时间步tt的隐藏变量的计算由当前时间步的输入和上一时间步的隐藏变量共同决定:

Ht=ϕ(XtWxh+Ht1Whh+bh).\boldsymbol{H}_t = \phi(\boldsymbol{X}_t \boldsymbol{W}_{xh} + \boldsymbol{H}_{t-1} \boldsymbol{W}_{hh} + \boldsymbol{b}_h).

与多层感知机相比,我们在这里添加了Ht1Whh\boldsymbol{H}_{t-1} \boldsymbol{W}_{hh}一项。由上式中相邻时间步的隐藏变量Ht\boldsymbol{H}_tHt1\boldsymbol{H}_{t-1}之间的关系可知,这里的隐藏变量能够捕捉截至当前时间步的序列的历史信息,就像是神经网络当前时间步的状态或记忆一样。因此,该隐藏变量也称为隐藏状态。由于隐藏状态在当前时间步的定义使用了上一时间步的隐藏状态,上式的计算是循环的。使用循环计算的网络即循环神经网络(recurrent neural network)。

循环神经网络有很多种不同的构造方法。含上式所定义的隐藏状态的循环神经网络是极为常见的一种。若无特别说明,本章中的循环神经网络均基于上式中隐藏状态的循环计算。在时间步tt,输出层的输出和多层感知机中的计算类似:

Ot=HtWhq+bq.\boldsymbol{O}_t = \boldsymbol{H}_t \boldsymbol{W}_{hq} + \boldsymbol{b}_q.

循环神经网络的参数包括隐藏层的权重WxhRd×h\boldsymbol{W}_{xh} \in \mathbb{R}^{d \times h}WhhRh×h\boldsymbol{W}_{hh} \in \mathbb{R}^{h \times h}和偏差 bhR1×h\boldsymbol{b}_h \in \mathbb{R}^{1 \times h},以及输出层的权重WhqRh×q\boldsymbol{W}_{hq} \in \mathbb{R}^{h \times q}和偏差bqR1×q\boldsymbol{b}_q \in \mathbb{R}^{1 \times q}。值得一提的是,即便在不同时间步,循环神经网络也始终使用这些模型参数。因此,循环神经网络模型参数的数量不随时间步的增加而增长。

  • W_xh: 状态-输入权重
  • W_hh: 状态-状态权重
  • W_hq: 状态-输出权重
  • b_h: 隐藏层的偏置
  • b_q: 输出层的偏置

循环神经网络的参数就是上述的三个权重和两个偏置,并且在沿着时间训练(参数的更新),参数的数量没有发生变化,仅仅是上述的参数的值在更新。循环神经网络可以看作是沿着时间维度上的权值共享

在卷积神经网络中,一个卷积核通过在特征图上滑动进行卷积,是空间维度的权值共享。在卷积神经网络中通过控制特征图的数量来控制每一层模型的复杂度,而循环神经网络是通过控制W_xh和W_hh中h的维度来控制模型的复杂度。


3.1 关于创建RNN模型,当前的最大预测字符

        if i < (len(prefix) - 1):
            output.append(char_to_idx[prefix[i+1]])
        else:
            output.append(int(Y[0].argmax(dim=1).item()))

argmax函数torch.argmax(input, dim=None, keepdim=False)返回指定维度最大值的序号,dim给定的定义是:the demention to reduce.也就是把dim这个维度的,变成这个维度的最大值的index。例如:
《动手学深度学习》task2——文本预处理,语言模型,循环神经网络基础笔记

torch.argmax(x, dim=0),取每一列的数据的最大值,返回行序号

torch.argmax(x, dim=1),取每一行的数据的最大值,返回列序号

3.2 关于裁剪梯度

循环神经网络中较容易出现梯度衰减或梯度爆炸。为了应对梯度爆炸,我们可以裁剪梯度(clip gradient)。假设我们把所有模型参数梯度的元素拼接成一个向量 g\boldsymbol{g},并设裁剪的阈值是θ\theta。裁剪后的梯度

min(θg,1)g \min\left(\frac{\theta}{\|\boldsymbol{g}\|}, 1\right)\boldsymbol{g}

L2L_2范数不超过θ\theta

先计算所有参数的梯度的L2范数,然后与theta比较,如果大于theta,就乘以这个系数

# 本函数已保存在d2lzh_pytorch包中方便以后使用
def grad_clipping(params, theta, device):
    norm = torch.tensor([0.0], device=device)
    for param in params:
        norm += (param.grad.data ** 2).sum()
    norm = norm.sqrt().item()
    if norm > theta:
        for param in params:
            param.grad.data *= (theta / norm)

3.3 关于语言模型评价指标(perplexity)

《动手学深度学习》task2——文本预处理,语言模型,循环神经网络基础笔记

S代表sentence,N是句子长度,p(wi)是第i个词的概率。第一个词就是 p(w1|w0),而w0是START,表示句子的起始,是个占位符。

详细见链接

3.4 关于困惑度

困惑度是对交叉熵损失函数做指数运算后得到的值。特别地,

  • 最佳情况下,模型总是把标签类别的概率预测为1,此时困惑度为1;
  • 最坏情况下,模型总是把标签类别的概率预测为0,此时困惑度为正无穷;
  • 基线情况下,模型总是预测所有类别的概率都相同,此时困惑度为类别个数。

显然,任何一个有效模型的困惑度必须小于类别个数。

3.5 关于RNN模型训练函数

《动手学深度学习》task2——文本预处理,语言模型,循环神经网络基础笔记

当我们再训练网络的时候可能希望保持一部分的网络参数不变,只对其中一部分的参数进行调整;或者值训练部分分支网络,并不让其梯度对主网络的梯度造成影响,这时候我们就需要使用detach()函数来切断一些分支的反向传播

detach()

返回一个新的Variable,从当前计算图中分离下来的,但是仍指向原变量的存放位置,不同之处只是requires_grad为false,得到的这个Variable永远不需要计算其梯度,不具有grad。 即使之后重新将它的requires_grad置为true,它也不会具有梯度grad .这样我们就会继续使用这个新的Variable进行计算,后面当我们进行反向传播时,到该调用detach()的Variable就会停止,不能再继续向前进行传播

detach_()

将一个Variable从创建它的图中分离,并把它设置成叶子variable .其实就相当于变量之间的关系本来是x -> m -> y,这里的叶子variable是x,但是这个时候对m进行了.detach_()操作,其实就是进行了两个操作:

1.将m的grad_fn的值设置为None,这样m就不会再与前一个节点x关联,这里的关系就会变成x, m -> y,此时的m就变成了叶子结点。

2.然后会将m的requires_grad设置为False,这样对y进行backward()时就不会求m的梯度。

其实detach()detach_()很像,两个的区别就是detach_()是对本身的更改,detach()则是生成了一个新的variable 。比如x -> m -> y中如果对m进行detach(),后面如果反悔想还是对原来的计算图进行操作还是可以的 。但是如果是进行了detach_(),那么原来的计算图也发生了变化,就不能反悔了。

详细见解析

3.6 其他的一些tips

《动手学深度学习》task2——文本预处理,语言模型,循环神经网络基础笔记

transpose()函数:转置

contiguous()函数:

在PyTorch中,有一些对Tensor的操作不会真正改变Tensor的内容,改变的仅仅是Tensor中字节位置的索引。这些操作有:

narrow(), view(), expand(), transpose()

例如执行view操作之后,不会开辟新的内存空间来存放处理之后的数据,实际上新数据与原始数据共享同一块内存。

而在调用contiguous()之后,PyTorch会开辟一块新的内存空间存放变换之后的数据,并会真正改变Tensor的内容,按照变换之后的顺序存放数据。

4 RNN的简洁实现

使用Pytorch中的nn.RNN来构造循环神经网络。在本节中,我们主要关注nn.RNN的以下几个构造函数参数:

  • input_size - The number of expected features in the input x
  • hidden_size – The number of features in the hidden state h
  • nonlinearity – The non-linearity to use. Can be either ‘tanh’ or ‘relu’. Default: ‘tanh’
  • batch_first – If True, then the input and output tensors are provided as (batch_size, num_steps, input_size). Default: False

这里的batch_first决定了输入的形状,我们使用默认的参数False,对应的输入形状是 (num_steps, batch_size, input_size)。

forward函数的参数为:

  • input of shape (num_steps, batch_size, input_size): tensor containing the features of the input sequence.
  • h_0 of shape (num_layers * num_directions, batch_size, hidden_size): tensor containing the initial hidden state for each element in the batch. Defaults to zero if not provided. If the RNN is bidirectional, num_directions should be 2, else it should be 1.

forward函数的返回值是:

  • output of shape (num_steps, batch_size, num_directions * hidden_size): tensor containing the output features (h_t) from the last layer of the RNN, for each t.
  • h_n of shape (num_layers * num_directions, batch_size, hidden_size): tensor containing the hidden state for t = num_steps.

4.1 Pytorch中的RNN的实现

  • PyTorch的nn模块提供了循环神经网络层的实现。
  • PyTorch的nn.RNN实例在前向计算后会分别返回输出和隐藏状态。该前向计算并不涉及输出层计算。