《动手学深度学习》-学习笔记task2


《动手学深度学习》的学习内容链接在github上:
Dive-into-DL-PyTorch. 本次学习过程的coding 是用PyTorch实现。

0. 学习任务

文本预处理、语言模型与数据集、循环神经网络

1. 文本预处理

文本预处理通常包括四个步骤:

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

1.1 读入文本

def read_time_machine():
    with open('/Users/janti/Boyu/0-DeepLearning/input/timemachine7163/timemachine.txt', 'r') as f:
        lines = [re.sub('[^a-z]+', ' ', line.strip().lower()) for line in f]      # re.sub 正则化   
    return lines

核心代码解析:

 lines = [re.sub('[^a-z]+', ' ', line.strip().lower()) for line in f] 

这句话的是:先将句子中的首尾空格去掉 (line.strip()),并将所有字符转为小写(lower()),然后用空格去替换非a到z([^a-z]))的字符。
比如:

" % I shouldn’t go to school ! % "

  1. 执行line.strip() 后,变为 “% I shouldn’t go to school ! %”
  2. 执行line.strip().lower() 后,变为 “% i shouldn’t go to school ! %”
  3. re.sub(’[^a-z] +’, ’ ‘, sents) : 指将sents中的非 a-z 替换成空格(’ ');变为 " i shouldn t go to school "

1.2 建立字典

为了方便模型处理,我们需要将字符串转换为数字,将每个词映射到一个唯一的索引编号。
建立字典的步骤

  1. 统计词频并去重
  2. 处理特殊tokens :padding, begin of sentence, end of sentence, unknown
  3. 构建一个list列表,建立index to token,用列表的下标表示index
  4. 将lists转为dict,从而 实现token to index,实现 每个词映射到一个唯一的索引编号。
class Vocab(object):
    def __init__(self, tokens, min_freq=0, use_special_tokens=False):
        counter = count_corpus(tokens)  # : 
        self.token_freqs = list(counter.items())
        self.idx_to_token = []   #设置一个list 
        if use_special_tokens:
            # padding, begin of sentence, end of sentence, unknown
            self.pad, self.bos, self.eos, self.unk = (0, 1, 2, 3)  # 特殊字符的索引编号
            self.idx_to_token += ['<pad>', '<bos>', '<eos>', '<unk>']  # 这里面表示上面所示的四种特殊字符放在字典的前四个位置 index为0,1,2,3
        else:
            self.unk = 0
            self.idx_to_token += ['unk']
        self.idx_to_token += [token for token, freq in self.token_freqs 
                        if freq >= min_freq and token not in self.idx_to_token]  # 设置min_freq 是为了过滤掉极少出现的字符
        self.token_to_idx = dict()
        for idx, token in enumerate(self.idx_to_token):
            self.token_to_idx[token] = idx
    
    
    def __len__(self):
        return len(self.idx_to_token)

    def __getitem__(self, tokens):
        if not isinstance(tokens, (list, tuple)):
            return self.token_to_idx.get(tokens, self.unk)
        return [self.__getitem__(token) for token in tokens]

    def to_tokens(self, indices):
        if not isinstance(indices, (list, tuple)):
            return self.idx_to_token[indices]
        return [self.idx_to_token[index] for index in indices]

def count_corpus(sentences):
    tokens = [tk for st in sentences for tk in st]   # 这里面的st怎么理解?  次序为啥要这样? 比如 [tk for tk in st for st in sentences]
    return collections.Counter(tokens)  # 返回一个字典,记录每个词的出现次数

2. 语言模型

假设序列 w 1 , w 2 , … , w T w_1, w_2, \ldots, w_T w1,w2,,wT中的每个词是依次生成的,我们有
在这里插入图片描述

例如,一段含有4个词的文本序列的概率

P ( w 1 , w 2 , w 3 , w 4 ) = P ( w 1 ) P ( w 2 ∣ w 1 ) P ( w 3 ∣ w 1 , w 2 ) P ( w 4 ∣ w 1 , w 2 , w 3 ) . 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,w2,w3,w4)=P(w1)P(w2w1)P(w3w1,w2)P(w4w1,w2,w3).

语言模型的参数就是词的概率以及给定前几个词情况下的条件概率。设训练数据集为一个大型文本语料库,如维基百科的所有条目,词的概率可以通过该词在训练数据集中的相对词频来计算,例如, w 1 w_1 w1的概率可以计算为:

P ^ ( w 1 ) = n ( w 1 ) n \hat P(w_1) = \frac{n(w_1)}{n} P^(w1)=nn(w1)

其中 n ( w 1 ) n(w_1) n(w1)为语料库中以 w 1 w_1 w1作为第一个词的文本的数量, n n n为语料库中文本的总数量。

类似的,给定 w 1 w_1 w1情况下, w 2 w_2 w2的条件概率可以计算为:

P ^ ( w 2 ∣ w 1 ) = n ( w 1 , w 2 ) n ( w 1 ) \hat P(w_2 \mid w_1) = \frac{n(w_1, w_2)}{n(w_1)} P^(w2w1)=n(w1)n(w1,w2)

其中 n ( w 1 , w 2 ) n(w_1, w_2) n(w1,w2)为语料库中以 w 1 w_1 w1作为第一个词, w 2 w_2 w2作为第二个词的文本的数量。

2.1 n 元语法

序列长度增加,计算和存储多个词共同出现的概率的复杂度会呈指数级增加。 n n n元语法通过马尔可夫假设简化模型,马尔科夫假设是指一个词的出现只与前面 n n n个词相关,即 n n n阶马尔可夫链(Markov chain of order n n n),如果 n = 1 n=1 n=1,那么有 P ( w 3 ∣ w 1 , w 2 ) = P ( w 3 ∣ w 2 ) P(w_3 \mid w_1, w_2) = P(w_3 \mid w_2) P(w3w1,w2)=P(w3w2)。基于 n − 1 n-1 n1阶马尔可夫链,我们可以将语言模型改写为

P ( w 1 , w 2 , … , w T ) = ∏ t = 1 T P ( w t ∣ w t − ( n − 1 ) , … , w t − 1 ) . P(w_1, w_2, \ldots, w_T) = \prod_{t=1}^T P(w_t \mid w_{t-(n-1)}, \ldots, w_{t-1}) . P(w1,w2,,wT)=t=1TP(wtwt(n1),,wt1).

以上也叫 n n n元语法( n n n-grams),它是基于 n − 1 n - 1 n1阶马尔可夫链的概率语言模型。例如,当 n = 2 n=2 n=2时,含有4个词的文本序列的概率就可以改写为:

在这里插入图片描述

n n n分别为1、2和3时,我们将其分别称作一元语法(unigram)、二元语法(bigram)和三元语法(trigram)。例如,长度为4的序列 w 1 , w 2 , w 3 , w 4 w_1, w_2, w_3, w_4 w1,w2,w3,w4在一元语法、二元语法和三元语法中的概率分别为

P ( w 1 , w 2 , w 3 , w 4 ) = P ( w 1 ) P ( w 2 ) P ( w 3 ) P ( w 4 ) , P ( w 1 , w 2 , w 3 , w 4 ) = P ( w 1 ) P ( w 2 ∣ w 1 ) P ( w 3 ∣ w 2 ) P ( w 4 ∣ w 3 ) , P ( w 1 , w 2 , w 3 , w 4 ) = P ( w 1 ) P ( w 2 ∣ w 1 ) P ( w 3 ∣ w 1 , w 2 ) P ( w 4 ∣ w 2 , w 3 ) . \begin{aligned} P(w_1, w_2, w_3, w_4) &= P(w_1) P(w_2) P(w_3) P(w_4) ,\\ P(w_1, w_2, w_3, w_4) &= P(w_1) P(w_2 \mid w_1) P(w_3 \mid w_2) P(w_4 \mid w_3) ,\\ 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_2, w_3) . \end{aligned} P(w1,w2,w3,w4)P(w1,w2,w3,w4)P(w1,w2,w3,w4)=P(w1)P(w2)P(w3)P(w4),=P(w1)P(w2w1)P(w3w2)P(w4w3),=P(w1)P(w2w1)P(w3w1,w2)P(w4w2,w3).

n n n较小时, n n n元语法往往并不准确。例如,在一元语法中,由三个词组成的句子“你走先”和“你先走”的概率是一样的。然而,当 n n n较大时, n n n元语法需要计算并存储大量的词频和多词相邻频率。

2.2 时序数据的采样

在训练中我们需要每次随机读取小批量样本和标签。与之前章节的实验数据不同的是,时序数据的一个样本通常包含连续的字符。假设时间步数为5,样本序列为5个字符,即“想”“要”“有”“直”“升”。该样本的标签序列为这些字符分别在训练集中的下一个字符,即“要”“有”“直”“升”“机”,即 X X X=“想要有直升”, Y Y Y=“要有直升机”。

现在我们考虑序列“想要有直升机,想要和你飞到宇宙去”,如果时间步数为5,有以下可能的样本和标签:

  • X X X:“想要有直升”, Y Y Y:“要有直升机”
  • X X X:“要有直升机”, Y Y Y:“有直升机,”
  • X X X:“有直升机,”, Y Y Y:“直升机,想”
  • X X X:“要和你飞到”, Y Y Y:“和你飞到宇”
  • X X X:“和你飞到宇”, Y Y Y:“你飞到宇宙”
  • X X X:“你飞到宇宙”, Y Y Y:“飞到宇宙去”

可以看到,如果序列的长度为 T T T,时间步数为 n n n,那么一共有 T − n T-n Tn个合法的样本,但是这些样本有大量的重合,我们通常采用更加高效的采样方式。我们有两种方式对时序数据进行采样,分别是随机采样和相邻采样。

2.2.1 随机采样

下面的代码每次从数据里随机采样一个小批量。其中批量大小batch_size是每个小批量的样本数,num_steps是每个样本所包含的时间步数。
在随机采样中,每个样本是原始序列上任意截取的一段序列,相邻的两个随机小批量在原始序列上的位置不一定相毗邻。

2.2.2 相邻采样

在相邻采样中,相邻的两个随机小批量在原始序列上的位置相毗邻。

3. 循环神经网络

本节介绍循环神经网络,下图展示了如何基于循环神经网络实现语言模型。我们的目的是基于当前的输入与过去的输入序列,预测序列的下一个字符。循环神经网络引入一个隐藏变量 H H H,用 H t H_{t} Ht表示 H H H在时间步 t t t的值。 H t H_{t} Ht的计算基于 X t X_{t} Xt H t − 1 H_{t-1} Ht1,可以认为 H t H_{t} Ht记录了到当前字符为止的序列信息,利用 H t H_{t} Ht对序列的下一个字符进行预测。
Image Name

3.1 循环神经网络的构造

我们先看循环神经网络的具体构造。假设 X t ∈ R n × d \boldsymbol{X}_t \in \mathbb{R}^{n \times d} XtRn×d是时间步 t t t的小批量输入, H t ∈ R n × h \boldsymbol{H}_t \in \mathbb{R}^{n \times h} HtRn×h是该时间步的隐藏变量,则:

H t = ϕ ( X t W x h + H t − 1 W h h + b h ) . \boldsymbol{H}_t = \phi(\boldsymbol{X}_t \boldsymbol{W}_{xh} + \boldsymbol{H}_{t-1} \boldsymbol{W}_{hh} + \boldsymbol{b}_h). Ht=ϕ(XtWxh+Ht1Whh+bh).
备注: R n × d \mathbb{R}^{n \times d} Rn×d中的n为批量大小, d 为向量长度。

其中, W x h ∈ R d × h \boldsymbol{W}_{xh} \in \mathbb{R}^{d \times h} WxhRd×h W h h ∈ R h × h \boldsymbol{W}_{hh} \in \mathbb{R}^{h \times h} WhhRh×h b h ∈ R 1 × h \boldsymbol{b}_{h} \in \mathbb{R}^{1 \times h} bhR1×h ϕ \phi ϕ函数是非线性激活函数。由于引入了 H t − 1 W h h \boldsymbol{H}_{t-1} \boldsymbol{W}_{hh} Ht1Whh H t H_{t} Ht能够捕捉截至当前时间步的序列的历史信息,就像是神经网络当前时间步的状态或记忆一样。由于 H t H_{t} Ht的计算基于 H t − 1 H_{t-1} Ht1,上式的计算是循环的,使用循环计算的网络即循环神经网络(recurrent neural network)。

在时间步 t t t,输出层的输出为:

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

其中 W h q ∈ R h × q \boldsymbol{W}_{hq} \in \mathbb{R}^{h \times q} WhqRh×q b q ∈ R 1 × q \boldsymbol{b}_q \in \mathbb{R}^{1 \times q} bqR1×q

3.2 循环神经网络代码实现

跟之前章节的模型训练函数相比,这里的模型训练函数有以下几点不同:

  1. 使用困惑度评价模型。
  2. 在迭代模型参数前裁剪梯度。
  3. 对时序数据采用不同采样方法将导致隐藏状态初始化的不同。

3.2.1 裁剪梯度

循环神经网络中较容易出现梯度衰减或梯度爆炸,这会导致网络几乎无法训练。裁剪梯度(clip gradient)是一种应对梯度爆炸的方法。假设我们把所有模型参数的梯度拼接成一个向量 g \boldsymbol{g} g,并设裁剪的阈值是 θ \theta θ。裁剪后的梯度

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

L 2 L_2 L2范数不超过 θ \theta θ

例题:关于梯度裁剪描述错误的是: (D)

  • A. 梯度裁剪之后的梯度小于或者等于原梯度
  • B. 梯度裁剪是应对梯度爆炸的一种方法
  • C. 裁剪之后的梯度L2范数小于阈值 θ θ \thetaθ θθ
  • D. 梯度裁剪也是应对梯度消失的一种方法

梯度裁剪只能应对梯度爆炸。

代码实现:

def grad_clipping(params, theta, device):
    norm = torch.tensor([0.0], device=device)  #L2 范数,初始值为0
    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.2.2 困惑度

我们通常使用困惑度(perplexity)来评价语言模型的好坏。回忆一下“softmax回归”一节中交叉熵损失函数的定义。困惑度是对交叉熵损失函数做指数运算后得到的值。特别地,

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

显然,任何一个有效模型的困惑度必须小于类别个数。在本例中,困惑度必须小于词典大小vocab_size

一个随机分类模型(基线模型)的困惑度等于分类问题的类别个数,有效模型的困惑度应小于类别个数。

3.2.3 具体代码实现

def train_and_predict_rnn(rnn, get_params, init_rnn_state, num_hiddens,
                          vocab_size, device, corpus_indices, idx_to_char,
                          char_to_idx, is_random_iter, num_epochs, num_steps,
                          lr, clipping_theta, batch_size, pred_period,
                          pred_len, prefixes):
    if is_random_iter:
        data_iter_fn = d2l.data_iter_random
    else:
        data_iter_fn = d2l.data_iter_consecutive
    params = get_params()
    loss = nn.CrossEntropyLoss()

    for epoch in range(num_epochs):
        if not is_random_iter:  # 如使用相邻采样,在epoch开始时初始化隐藏状态
            state = init_rnn_state(batch_size, num_hiddens, device)
        l_sum, n, start = 0.0, 0, time.time()
        data_iter = data_iter_fn(corpus_indices, batch_size, num_steps, device)
        for X, Y in data_iter:
            if is_random_iter:  # 如使用随机采样,在每个小批量更新前初始化隐藏状态
                state = init_rnn_state(batch_size, num_hiddens, device)
            else:  # 否则需要使用detach函数从计算图分离隐藏状态
                for s in state:
                    s.detach_()
            # inputs是num_steps个形状为(batch_size, vocab_size)的矩阵
            inputs = to_onehot(X, vocab_size)
            # outputs有num_steps个形状为(batch_size, vocab_size)的矩阵
            (outputs, state) = rnn(inputs, state, params)
            # 拼接之后形状为(num_steps * batch_size, vocab_size)
            outputs = torch.cat(outputs, dim=0)  # 拼接起来
            # Y的形状是(batch_size, num_steps),转置后再变成形状为
            # (num_steps * batch_size,)的向量,这样跟输出的行一一对应
            y = torch.flatten(Y.T)
            # 使用交叉熵损失计算平均分类误差
            l = loss(outputs, y.long())
            
            #反向传播
            # 梯度清0
            if params[0].grad is not None:
                for param in params:
                    param.grad.data.zero_()
            l.backward()
            grad_clipping(params, clipping_theta, device)  # 裁剪梯度
            d2l.sgd(params, lr, 1)  # 因为误差已经取过均值,梯度不用再做平均
            l_sum += l.item() * y.shape[0]
            n += y.shape[0]

        # 训练过程中输出一些loss信息    
        if (epoch + 1) % pred_period == 0:
            print('epoch %d, perplexity %f, time %.2f sec' % (
                epoch + 1, math.exp(l_sum / n), time.time() - start))
            for prefix in prefixes:
                print(' -', predict_rnn(prefix, pred_len, rnn, params, init_rnn_state,
                    num_hiddens, vocab_size, device, idx_to_char, char_to_idx))
  • 采用相邻采样仅在每个训练周期开始的时候初始化隐藏状态是因为相邻的两个批量在原始数据上是连续的
  • 采用随机采样需要在每个小批量更新前初始化隐藏状态是因为随机采样中每个样本只包含局部的时间序列信息,由于样本不完整所以每个批量需要重新初始化隐藏状态

4. 疑问与解决方案

4.1 无法正确读取中文文本

遇到的问题:

'gbk' codec can't decode byte 0xa6 in position 4: illegal multibyte sequence

在这里插入图片描述
解决方法:
读取文件时添加encoding方式:
也就是将

with open('/Users/janti/Boyu/0-DeepLearning/input/jaychou_lyrics4703/jaychou_lyrics.txt') as f:
    corpus_chars = f.read()

改为:

with open('/Users/janti/Boyu/0-DeepLearning/input/jaychou_lyrics4703/jaychou_lyrics.txt',encoding='utf-8') as f:
    corpus_chars = f.read()

参考链接: 如何一劳永逸的解决UnicodeDecodeError: ‘gbk’ codec can’t decode byte 0xaf in position 60: illegal multibyte sequence问题

4.2 n元语法可能有哪些缺陷?

  1. 参数空间过大
    若是三元语法,那么需要维护 p ( w 1 ) p ( w 2 ∣ w 1 ) p ( w 3 ∣ w 1 w 2 ) p(w1)p(w2|w1)p(w3|w1w2) p(w1)p(w2w1)p(w3w1w2)
    若字典大小为V,则需要的参数有 V + V 2 + V 3 V+V^2+V^3 V+V2+V3, 指数增长,所以参数空间很大

  2. 数据稀疏
    由于有很多词的组合方式,出现次数极少,词频较低,该词组组合概率概率会接近0,所以矩阵会非常稀少

4.3 如何理解梯度裁剪后不超过 θ \theta θ

也就是求证:
∥ min ⁡ ( θ ∥ g ∥ , 1 ) g ∥ < = θ \|\boldsymbol{ \min\left(\frac{\theta}{\|\boldsymbol{g}\|}, 1\right)\boldsymbol{g}}\| < = \theta min(gθ,1)g<=θ
分类讨论:
(1)当 θ ∥ g ∥ > 1 \frac{\theta}{\|\boldsymbol{g}\|}>1 gθ>1 时,
∥ min ⁡ ( θ ∥ g ∥ , 1 ) g ∥ = ∥ g ∥ < θ \|\boldsymbol{ \min\left(\frac{\theta}{\|\boldsymbol{g}\|}, 1\right)\boldsymbol{g}}\| = \|\boldsymbol{g}\| < \theta min(gθ,1)g=g<θ
(2) 当 θ ∥ g ∥ < = 1 \frac{\theta}{\|\boldsymbol{g}\|}<=1 gθ<=1 时,
∥ min ⁡ ( θ ∥ g ∥ , 1 ) g ∥ = ∥ θ ∥ g ∥ g ∥ = θ ∥ g ∥ ∥ g ∥ = θ \|\boldsymbol{ \min\left(\frac{\theta}{\|\boldsymbol{g}\|}, 1\right)\boldsymbol{g}}\| = \|\boldsymbol{ \frac{\theta}{\|\boldsymbol{g}\|}\boldsymbol{g}}\| = \frac{\theta}{\|\boldsymbol{g}\|} \|\boldsymbol{ \boldsymbol{g}} \| = \theta min(gθ,1)g=gθg=gθg=θ
**所以梯度裁剪后不超过 θ \theta θ **

在这里插入图片描述

5. 参考链接:

[1] https://www.jianshu.com/p/e68193d3d998
[2] https://blog.csdn.net/rocling/article/details/90290576
[3] https://github.com/ShusenTang/Dive-into-DL-PyTorch

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值