Bootstrap

4.3.语言模型

语言模型

​ 假设长度为 T T T的文本序列中的词元依次为 x 1 , x 2 , ⋯   , x T x_1,x_2,\cdots,x_T x1,x2,,xT。 于是, x T x_T xT 1 ≤ t ≤ T 1\le t\le T 1tT) 可以被认为是文本序列在时间步 t t t处的观测或标签。 在给定这样的文本序列时,语言模型(language model)的目标是估计序列的联合概率:
P ( x 1 , x 2 , ⋯   , x T ) P(x_1,x_2,\cdots,x_T) P(x1,x2,,xT)
​ 应用包括:

  1. 做预训练模型(BERT,GPT-3)
  2. 生成本文,给定前面几个词,不断的使用 x t ∼ p ( x t ∣ x 1 , ⋯   , x t − 1 ) x_t\sim p(x_t|x_1,\cdots,x_{t-1}) xtp(xtx1,,xt1)生成后续文本
  3. 判断多个序列中哪个更常见

1.使用计数来建模(n_gram)

​ 假设序列长度为2,我们预测:
p ( x , x ′ ) = p ( x ) p ( x ′ ∣ x ) = n ( x ) n n ( x , x ′ ) n ( x ) p(x,x')=p(x)p(x'|x)=\frac{n(x)}{n}\frac{n(x,x')}{n(x)} p(x,x)=p(x)p(xx)=nn(x)n(x)n(x,x)
​ 其中 n n n是总次数, n ( x ) , n ( x , x ′ ) n(x),n(x,x') n(x),n(x,x)是单个单词和连续单词对的出现次数

​ 序列长度为3的情况也很容易: p ( x , x ′ , x ′ ′ ) = p ( x ) p ( x ′ ∣ x ) p ( x ′ ′ ∣ x , x ′ ) = n ( x ) n n ( x , x ′ ) n ( x ) n ( x , x ′ ) n ( x , x ′ , x ′ ′ ) p(x,x',x'')=p(x)p(x'|x)p(x''|x,x')=\frac{n(x)}{n}\frac{n(x,x')}{n(x)}\frac{n(x,x')}{n(x,x',x'')} p(x,x,x′′)=p(x)p(xx)p(x′′x,x)=nn(x)n(x)n(x,x)n(x,x,x′′)n(x,x)

​ 显然我们得到了一种简单的建模方式:
p ( x t ∣ x 1 , ⋯   , x t ) = n ( x 1 , ⋯   , x t ) n p(x_t|x_1,\cdots,x_t)=\frac{n(x_1,\cdots,x_t)}{n} p(xtx1,,xt)=nn(x1,,xt)
​ 但这样有个问题:

​ 当序列很长时,因为文本量不够大,很可能 n ( x 1 , ⋯   , x T ) ≤ 1 n(x_1,\cdots,x_T)\le 1 n(x1,,xT)1,即某些序列是合理的,但文本中没有出现。可以使用马尔科夫假设缓解这个问题:

  1. 一元语法: p ( x 1 , x 2 , x 3 , x 4 ) = n ( x 1 ) n n ( x 2 ) n n ( x 3 ) n n ( x 5 ) n p(x_1,x_2,x_3,x_4)=\frac{n(x_1)}n\frac{n(x_2)}n\frac{n(x_3)}n\frac{n(x_5)}n p(x1,x2,x3,x4)=nn(x1)nn(x2)nn(x3)nn(x5)与前面无关
  2. 二元语法: p ( x 1 , x 2 , x 3 , x 4 ) = n ( x 1 ) n n ( x 1 , x 2 ) n ( x 1 ) n ( x 2 , x 3 ) n ( x 2 ) n ( x 3 , x 4 ) n ( x 3 ) p(x_1,x_2,x_3,x_4)=\frac{n(x_1)}n \frac{n(x_1,x_2)}{n(x_1)} \frac{n(x_2,x_3)}{n(x_2)} \frac{n(x_3,x_4)}{n(x_3)} p(x1,x2,x3,x4)=nn(x1)n(x1)n(x1,x2)n(x2)n(x2,x3)n(x3)n(x3,x4)与前面一个词有关系
  3. 三元语法: p ( x 1 , x 2 , x 3 , x 4 ) = p ( x 1 ) p ( x 2 ∣ x 1 ) p ( x 3 ∣ x 1 , x 2 ) p ( x 4 ∣ x 2 , x 3 ) p(x_1,x_2,x_3,x_4)=p(x_1)p(x_2|x_1)p(x_3|x_1,x_2)p(x_4|x_2,x_3) p(x1,x2,x3,x4)=p(x1)p(x2x1)p(x3x1,x2)p(x4x2,x3)与前面两个词有关系

​ 另一种常见的策略师执行某种形式的拉普拉斯平滑,具体方法是在所有计数中添加一个小常量,用 n n n表示训练集中的单词总数,用 m m m表示唯一单词的数量,这个方案有助于处理单元素问题

在这里插入图片描述

​ 其中 ϵ 1 , ϵ 2 , ϵ 3 \epsilon_1,\epsilon_2,\epsilon_3 ϵ1,ϵ2,ϵ3是超参数,这样的模型很容易变得无效,原因如下:首先,我们需要存储所有的计数; 其次,这完全忽略了单词的意思。 例如,“猫”(cat)和“猫科动物”(feline)可能出现在相关的上下文中, 但是想根据上下文调整这类模型其实是相当困难的。 最后,长单词序列大部分是没出现过的, 因此一个模型如果只是简单地统计先前“看到”的单词序列频率, 那么模型面对这种问题肯定是表现不佳的。

2.语言模型和数据集的代码实现

import random
import torch
import re
from d2l import torch as d2l

d2l.DATA_HUB['time_machine'] = (d2l.DATA_URL + 'timemachine.txt',
                                '090b5e7e70c295757f55df93cb0a180b9691891a')


def read_time_machine():  # @save
    """将时间机器数据集加载到文本行的列表中"""
    with open(d2l.download('time_machine'), 'r') as f:
        lines = f.readlines()
    return [re.sub('[^A-Za-z]+', ' ', line).strip().lower() for line in lines]


tokens = d2l.tokenize(read_time_machine())
# 因为每个文本行不一定是一个句子或一个段落,因此我们把所有文本行拼接到一起
corpus = [token for line in tokens for token in line]
vocab = d2l.Vocab(corpus)
print(vocab.token_freqs[:10])

freqs = [freq for token, freq in vocab.token_freqs]
print(freqs[:10])
d2l.plot(freqs, xlabel='token: x', ylabel='frequency: n(x)',
         xscale='log', yscale='log')
d2l.plt.show()

在这里插入图片描述

​ 词频图:词频以一种明确的方式迅速衰减,将前几个单词作为例外消除后(停用词,类似the,a,I,and之类的),生育的所有单词大致遵循双对数坐标图上的一条直线,这意味着单词的频率满足齐普夫定律(Zipf’s law),即第 i i i个最常用的频率 n i n_i ni为:
n i ∝ 1 n i n_i\propto \frac 1{n_i} nini1
等价于:


l o g   n i = − α l o g   i + c log\ n_i = -\alpha log\ i+c log ni=αlog i+c
​ 这意味着通过计数统计和平滑来建模单词是不可行的,因为这样建模的结果会大大高估尾部单词的频率。

多元语法

​ 注意最新的d2l库中的Vocab类好像有问题,处理不了元组,将库中的Vocab类替换一下,再加上count_corpus函数就可以正常处理元组。


bigram_tokens = [pair for pair in zip(corpus[:-1], corpus[1:])]
bigram_vocab = d2l.Vocab(bigram_tokens)
print('二元:',bigram_vocab.token_freqs[:10])

trigram_tokens = [triple for triple in zip(
    corpus[:-2], corpus[1:-1], corpus[2:])]
trigram_vocab = d2l.Vocab(trigram_tokens)
print('三元:',trigram_vocab.token_freqs[:10])

bigram_freqs = [freq for token, freq in bigram_vocab.token_freqs]
trigram_freqs = [freq for token, freq in trigram_vocab.token_freqs]
d2l.plot([freqs, bigram_freqs, trigram_freqs], xlabel='token: x',
         ylabel='frequency: n(x)', xscale='log', yscale='log',
         legend=['unigram', 'bigram', 'trigram'])
d2l.plt.show()

在这里插入图片描述

​ 发现:

  1. 除了一元语法词,单词序列似乎也遵循齐普夫定律, 尽管公式中的指数𝛼更小 (指数的大小受序列长度的影响);
  2. 词表中n元组的数量并没有那么大,这说明语言中存在相当多的结构, 这些结构给了我们应用模型的希望;
  3. 很多n元组很少出现,这使得拉普拉斯平滑非常不适合语言建模。 作为代替,我们将使用基于深度学习的模型。

随机采样

# 随机采样
def seq_data_iter_random(corpus, batch_size, num_steps):  #@save
    """使用随机抽样生成一个小批量子序列"""
    # num_steps意思是取多少个token来进行预测
    # 从随机偏移量开始对序列进行分区,随机范围包括num_steps-1
    corpus = corpus[random.randint(0, num_steps - 1):]
    # 减去1,是因为我们需要考虑标签,最后一个样本没有可预测的数据
    num_subseqs = (len(corpus) - 1) // num_steps
    # 长度为num_steps的子序列的起始索引
    initial_indices = list(range(0, num_subseqs * num_steps, num_steps))
    # 在随机抽样的迭代过程中,
    # 来自两个相邻的、随机的、小批量中的子序列不一定在原始序列上相邻
    print(initial_indices)
    random.shuffle(initial_indices) # 打乱顺序,不会生成新的列表,在原列表上打乱的
    print(initial_indices)

    def data(pos):
        # 返回从pos位置开始的长度为num_steps的序列
        return corpus[pos: pos + num_steps]

    num_batches = num_subseqs // batch_size
    for i in range(0, batch_size * num_batches, batch_size): #后面一个是步长
        # 在这里,initial_indices包含子序列的随机起始索引
        initial_indices_per_batch = initial_indices[i: i + batch_size]
        X = [data(j) for j in initial_indices_per_batch]
        Y = [data(j + 1) for j in initial_indices_per_batch]
        yield torch.tensor(X), torch.tensor(Y)

'''生成一个0到34的序列,假设批量大小为2,时间步数为5,这意味着可以生成(35-1)/5 = 6个特征-标签子序列对'''

my_seq = list(range(35))
for X, Y in seq_data_iter_random(my_seq, batch_size=2, num_steps=5):
    print('X: ', X, '\nY:', Y)

顺序分区

def seq_data_iter_sequential(corpus, batch_size, num_steps):  # @save
    """使用顺序分区生成一个小批量子序列"""
    # 顺序分区保证不同批次内的子序列在原始序列上是连续的,不会跳过任何元素,这对某些任务(语言建模)可能更合适
    # X:  tensor([[ 1,  2,  3,  4,  5],
    #         [17, 18, 19, 20, 21]])
    # Y: tensor([[ 2,  3,  4,  5,  6],
    #         [18, 19, 20, 21, 22]])
    # X:  tensor([[ 6,  7,  8,  9, 10], 和[1,2,3,4,5]是连续的
    #         [22, 23, 24, 25, 26]])    和[17,18,19,20,21]是连续的
    # Y: tensor([[ 7,  8,  9, 10, 11],
    #         [23, 24, 25, 26, 27]])
    # X:  tensor([[11, 12, 13, 14, 15],
    #         [27, 28, 29, 30, 31]])
    # Y: tensor([[12, 13, 14, 15, 16],
    #         [28, 29, 30, 31, 32]])
    # 从随机偏移量开始划分序列
    offset = random.randint(0, num_steps)
    num_tokens = ((len(corpus) - offset - 1) // batch_size) * batch_size
    Xs = torch.tensor(corpus[offset: offset + num_tokens])
    Ys = torch.tensor(corpus[offset + 1: offset + 1 + num_tokens])
    Xs, Ys = Xs.reshape(batch_size, -1), Ys.reshape(batch_size, -1)
    num_batches = Xs.shape[1] // num_steps
    for i in range(0, num_steps * num_batches, num_steps):
        X = Xs[:, i: i + num_steps]
        Y = Ys[:, i: i + num_steps]
        print('X: ', X, '\nY:', Y)
        yield X, Y #返回一个值序列,而不是一次性返回所有值。


for X, Y in seq_data_iter_sequential(my_seq, batch_size=2, num_steps=5):
    print('X: ', X, '\nY:', Y)

包装

class SeqDataLoader:  # @save
    """加载序列数据的迭代器"""

    def __init__(self, batch_size, num_steps, use_random_iter, max_tokens):
        if use_random_iter:
            self.data_iter_fn = d2l.seq_data_iter_random
        else:
            self.data_iter_fn = d2l.seq_data_iter_sequential
        self.corpus, self.vocab = d2l.load_corpus_time_machine(max_tokens)
        self.batch_size, self.num_steps = batch_size, num_steps

    def __iter__(self):
        return self.data_iter_fn(self.corpus, self.batch_size, self.num_steps)


def load_data_time_machine(batch_size, num_steps,  # @save
                           use_random_iter=False, max_tokens=10000):
    """返回时光机器数据集的迭代器和词表"""
    data_iter = SeqDataLoader(
        batch_size, num_steps, use_random_iter, max_tokens)
    return data_iter, data_iter.vocab

NLP BERT GPT等模型中 tokenizer 类别说明详解-腾讯云开发者社区-腾讯云 (tencent.com)

;