【动手学习pytorch笔记】21.语言模型

语言模型

理论基础

给定文本序列 x 1 , . . . , x T x_1,...,x_T x1,...,xT,语言模型的目标是估计联合概率 p ( x 1 , . . . , x T ) p(x_1,...,x_T) p(x1,...,xT)

它的应用包括

  • 做预训练模型(BERT,GPT)
  • 生成文本,给定前面几个词,不断使用前面给的词预测下面的词,和前面预测sin函数一样
  • 给定多个序列,判断哪个序列更常见

使用计数来建模

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是总词数

拓展到长度为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 ′ , x ′ ′ ) n ( 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^{'},x^{''})}{n(x,x^{'})} p(x,x,x)=p(x)p(xx)p(xx,x)=nn(x)n(x)n(x,x)n(x,x)n(x,x,x)

N原语法

然而,当序列很长时,因为文本两不够大,很可能 n ( x 1 , . . . , x T ) ≤ 1 n(x_1,...,x_T)\le1 n(x1,...,xT)1

使用马尔可夫假设可以缓解这个问题

在这里插入图片描述

这样的好处是,在二元语法中,我们只需要关心长度为2和长度为1的序列,不必再关心所有长度的序列(指数级复杂度)

比如词表大小=10000 ,只需要 10000 * 10000的大小存储所有长度为2的序列

基于N元语法的语言模型和数据集

构建词表

import random
import torch
from d2l import torch as d2l

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

corpus长这样

['the', 'time', 'machine', 'by', 'h', 'g', 'wells',...,'heart', 'of', 'man']

对其直接构建词表Vocab(corpus),就是一元语法

输出

[('the', 2261),
 ('i', 1267),
 ('and', 1245),
 ('of', 1155),
 ('a', 816),
 ('to', 695),
 ('was', 552),
 ('in', 541),
 ('that', 443),
 ('my', 440)]

看看词频

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

在这里插入图片描述

注意坐标轴是log,不是均匀的。能看到大多数词出现的概率还是很低的,20%的词占据了文本总量的80%

接下来试试二元语法

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

使用了zip()函数,方式和之前预测sin函数构建特征时的方法差不多,很巧妙。

>>> a = [1,2,3]
>>> b = [4,5,6]
>>> zipped = zip(a,b)     # 打包为元组的列表
[(1, 4), (2, 5), (3, 6)]

输出

[(('of', 'the'), 309),
 (('in', 'the'), 169),
 (('i', 'had'), 130),
 (('i', 'was'), 112),
 (('and', 'the'), 109),
 (('the', 'time'), 102),
 (('it', 'was'), 99),
 (('to', 'the'), 85),
 (('as', 'i'), 78),
 (('of', 'a'), 73)]

类似的三元语法

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

输出

[(('the', 'time', 'traveller'), 59),
 (('the', 'time', 'machine'), 30),
 (('the', 'medical', 'man'), 24),
 (('it', 'seemed', 'to'), 16),
 (('it', 'was', 'a'), 15),
 (('here', 'and', 'there'), 15),
 (('seemed', 'to', 'me'), 14),
 (('i', 'did', 'not'), 14),
 (('i', 'saw', 'the'), 13),
 (('i', 'began', 'to'), 13)]

查看词频

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'])

在这里插入图片描述

之前说过,复杂度是随着n成指数增长的,1000个词,二元语法的词表大小1000 * 1000,三元1000 * 1000 * 1000,但其实过滤掉一些低频词之后,实际的占用是非常小的

生成数据集(随机取样)

现在我们要生成和之前预测sin函数类似的,特征以及标签

特征:一段文本序列

标签:其后面对应的下一个词

随机取样的两种方法

方法一:

思想:把整个corpus按num_steps切成很多段,然后随机选batch_size个当作一个batch

假设corpus文本诗歌1000个词组成的list,num_steps = 4,batch_size = 50

即能切出250个样本,5个batch,每个batch包含50个样本

[[…],[…],[…],[…],[…],… …,[…],[…],[…],[…],[…]]

def seq_data_iter_random(corpus, batch_size, num_steps):  #@save
    """使用随机抽样生成一个小批量子序列"""
    # 从随机偏移量开始对序列进行分区,随机范围包括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))
    # 在随机抽样的迭代过程中,
    # 来自两个相邻的、随机的、小批量中的子序列不一定在原始序列上相邻
    random.shuffle(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)

seq_data_iter_random(corpus, batch_size, num_steps)

corpus:一个List[],里面输入的文本的所有单词,

['the', 'time', 'machine', 'by', 'h', 'g', 'wells',...,'heart', 'of', 'man']

batch_size:每个批次有多少个样本

num_steps:一个样本的特征个数(窗口大小),根据多少个特征进行预测

corpus = corpus[random.randint(0, num_steps - 1):]

为了保证我每次切的都不一样,corpus 删掉前面几个,起点不同,之后切出来的段就不同了

num_subseqs = (len(corpus) - 1) // num_steps

一共能有这么多样本

initial_indices = list(range(0, num_subseqs * num_steps, num_steps))

得到每一个样本起始的下标,长这样

[0, 4, 8, 12, 16, 20, 24, 28,...,32752, 32756, 32760, 32764, 32768] (250个)

num_batches = num_subseqs // batch_size

一共能有这么多批次

for i in range(0, batch_size * num_batches, batch_size):

i:[0, 51, 101, 151, 201]

initial_indices_per_batch = initial_indices[i: i + batch_size]

从 initial_indices 中获取 batch_size (50)个打乱下标,也就拿到了一个batch中所有样本的起始下标,再用data()函数获取下标对应的单词作为特征和标注

一共这样拿了5次,即5个batch,每个batch中50个样本,200个单词。

torch.tensor(X), torch.tensor(Y)

最终得到了X和Y的向量

验证一下

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)

输出

X:  tensor([[ 4,  5,  6,  7,  8],
        [24, 25, 26, 27, 28]]) 
Y: tensor([[ 5,  6,  7,  8,  9],
        [25, 26, 27, 28, 29]])
X:  tensor([[ 9, 10, 11, 12, 13],
        [19, 20, 21, 22, 23]]) 
Y: tensor([[10, 11, 12, 13, 14],
        [20, 21, 22, 23, 24]])
X:  tensor([[14, 15, 16, 17, 18],
        [29, 30, 31, 32, 33]]) 
Y: tensor([[15, 16, 17, 18, 19],
        [30, 31, 32, 33, 34]])

方法二:

前后两个batch的对应位置的样本是连续的,注意不是同一个batch中的两个样本是连续的

def seq_data_iter_sequential(corpus, batch_size, num_steps):  #@save
    """使用顺序分区生成一个小批量子序列"""
    # 从随机偏移量开始划分序列
    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]
        yield X, Y
for X, Y in seq_data_iter_sequential(my_seq, batch_size=2, num_steps=5):
    print('X: ', X, '\nY:', Y)
X:  tensor([[ 4,  5,  6,  7,  8],
        [19, 20, 21, 22, 23]]) 
Y: tensor([[ 5,  6,  7,  8,  9],
        [20, 21, 22, 23, 24]])
X:  tensor([[ 9, 10, 11, 12, 13],
        [24, 25, 26, 27, 28]]) 
Y: tensor([[10, 11, 12, 13, 14],
        [25, 26, 27, 28, 29]])
X:  tensor([[14, 15, 16, 17, 18],
        [29, 30, 31, 32, 33]]) 
Y: tensor([[15, 16, 17, 18, 19],
        [30, 31, 32, 33, 34]])

将两个方法封装到一个类中

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

这是我们接下来模型需要用到的加载输入和词表的方法

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值