Pytorch 语言模型和数据集
0. 环境介绍
环境使用 Kaggle 里免费建立的 Notebook
小技巧:当遇到函数看不懂的时候可以按 Shift+Tab
查看函数详解。
1. 语言模型
假设长度为的文本序列中的词元依次为
x
1
,
x
2
,
…
,
x
T
x_1, x_2, \ldots, x_T
x1,x2,…,xT。 于是,
x
t
x_t
xt(
1
≤
t
≤
T
1 \leq t \leq T
1≤t≤T) 可以被认为是文本序列在时间步处的观测或标签。 在给定这样的文本序列时,语言模型(language model)的目标是估计序列的联合概率:
P
(
x
1
,
x
2
,
…
,
x
T
)
P(x_1, x_2, \ldots, x_T)
P(x1,x2,…,xT)
它的应用包括:
- 做预训练模型(如 BERT、GPT-3)
- 生成文本,给定前面几个词,不断使用 x t ∼ P ( x t ∣ x t − 1 , … , x 1 ) x_t \sim P(x_t \mid x_{t-1}, \ldots, x_1) xt∼P(xt∣xt−1,…,x1) 来生成后续文本
- 判断多个序列中哪个更常见,比如:“to recognize speech ” vs “to wreck a nice beach”。
1.1 若使用计数来进行建模
假设序列长度为 2,我们预测:
p
(
x
,
x
′
)
=
p
(
x
)
p
(
x
′
∣
x
)
=
n
(
x
)
n
n
(
x
,
x
′
)
n
(
x
)
p\left(x, x^{\prime}\right)=p(x) p\left(x^{\prime} \mid x\right)=\frac{n(x)}{n} \frac{n\left(x, x^{\prime}\right)}{n(x)}
p(x,x′)=p(x)p(x′∣x)=nn(x)n(x)n(x,x′)
- 这里的 n n n 是总词数, n ( x ) n(x) n(x), n ( x , x ′ ) n(x, x^{\prime}) 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
′
,
x
′
′
)
n
(
x
,
x
′
)
p\left(x, x^{\prime}, x^{\prime \prime}\right)=p(x) p\left(x^{\prime} \mid x\right) p\left(x^{\prime \prime} \mid x, x^{\prime}\right)=\frac{n(x)}{n} \frac{n\left(x, x^{\prime}\right)}{n(x)} \frac{n\left(x, x^{\prime}, x^{\prime \prime}\right)}{n\left(x, x^{\prime}\right)}
p(x,x′,x′′)=p(x)p(x′∣x)p(x′′∣x,x′)=nn(x)n(x)n(x,x′)n(x,x′)n(x,x′,x′′)
这种方式只统计词频,这完全忽略了单词的意思。“猫”(cat)和“猫科动物”(feline)可能出现在相关的上下文中, 但是想根据上下文调整这类模型其实是相当困难的。 最后,长单词序列大部分是没出现过的, 因此一个模型如果只是简单地统计先前“看到”的单词序列频率, 那么模型面对这种问题肯定是表现不佳的。
1.2 马尔科夫模型和 N 元语法
当序列很长时,因为文本量不够大,很可能
n
(
x
1
,
…
,
x
T
)
≤
1
n(x_1, \ldots, x_T) \le 1
n(x1,…,xT)≤1。
使用马尔科夫假设可以缓解这个问题(拆词):
- 一元语法(
τ
=
0
\tau=0
τ=0,相互独立,一般不用):
p ( x 1 , x 2 , x 3 , x 4 ) = p ( x 1 ) p ( x 2 ) p ( x 3 ) p ( x 4 ) = n ( x 1 ) n n ( x 2 ) n n ( x 3 ) n n ( x 4 ) n \begin{aligned} p\left(x_{1}, x_{2}, x_{3}, x_{4}\right) &=p\left(x_{1}\right) p\left(x_{2}\right) p\left(x_{3}\right) p\left(x_{4}\right) \\ &=\frac{n\left(x_{1}\right)}{n} \frac{n\left(x_{2}\right)}{n} \frac{n\left(x_{3}\right)}{n} \frac{n\left(x_{4}\right)}{n} \end{aligned} p(x1,x2,x3,x4)=p(x1)p(x2)p(x3)p(x4)=nn(x1)nn(x2)nn(x3)nn(x4) - 二元语法(
τ
=
1
\tau=1
τ=1,跟前面一个值相关):
p ( x 1 , x 2 , x 3 , x 4 ) = p ( x 1 ) p ( x 2 ∣ x 1 ) p ( x 3 ∣ x 2 ) p ( x 4 ∣ x 3 ) = 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 ) \begin{aligned} p\left(x_{1}, x_{2}, x_{3}, x_{4}\right) &=p\left(x_{1}\right) p\left(x_{2} \mid x_{1}\right) p\left(x_{3} \mid x_{2}\right) p\left(x_{4}\left|x_{3}\right)\right.\\ &=\frac{n\left(x_{1}\right)}{n} \frac{n\left(x_{1}, x_{2}\right)}{n\left(x_{1}\right)} \frac{n\left(x_{2}, x_{3}\right)}{n\left(x_{2}\right)} \frac{n\left(x_{3}, x_4) \right.}{n(x_3)} \end{aligned} p(x1,x2,x3,x4)=p(x1)p(x2∣x1)p(x3∣x2)p(x4∣x3)=nn(x1)n(x1)n(x1,x2)n(x2)n(x2,x3)n(x3)n(x3,x4) - 三元语法(
τ
=
2
\tau=2
τ=2,跟前面两个值相关):
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\left(x_{1}, x_{2}, x_{3}, x_{4}\right)=p\left(x_{1}\right) p\left(x_{2} \mid x_{1}\right) p\left(x_{3} \mid x_{1}, x_{2}\right) p\left(x_{4} \mid x_{2}, x_{3}\right) p(x1,x2,x3,x4)=p(x1)p(x2∣x1)p(x3∣x1,x2)p(x4∣x2,x3)
2. 代码
2.1 建立词表
时光机器数据集构建词表,并打印前 10 个频率最高的单词:
!pip install -U d2l
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]
这些出现频率最高的词看起来很无聊,通常被称为停用词(stop words)。
2.2 词频图
freqs = [freq for token, freq in vocab.token_freqs]
d2l.plot(freqs, xlabel='token: x', ylabel='frequency: n(x)',
xscale='log', yscale='log')
通过此图我们可以发现:词频以一种明确的方式迅速衰减。 将前几个单词作为例外消除后,剩余的所有单词大致遵循双对数坐标图上的一条直线。
2.3 二元语法
bigram_tokens = [pair for pair in zip(corpus[:-1], corpus[1:])]
bigram_vocab = d2l.Vocab(bigram_tokens)
bigram_vocab.token_freqs[:10]
2.4 三元语法
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]
2.5 三种模型中的词元频率
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'])
2.6 读取长序列
将长序列变成 mini-batch。
2.6.1 随机采样
在随机采样中,每个样本都是在原始的长序列上任意捕获的子序列。
在迭代过程中,来自两个相邻的、随机的、小批量中的子序列不一定在原始序列上相邻。 对于语言建模,目标是基于到目前为止我们看到的词元来预测下一个词元, 因此标签是移位了一个词元的原始序列:
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)
每次分 batch 时,丢弃前 k 个元素,从 k 位置开始往后划分。扫一遍数据,每次每个数据只用过一次。
生成一个从 0 0 0 到 34 34 34 的序列,假设批量大小为 2 2 2,时间步长为 5 5 5,这意味着可以生成 ⌊ ( 35 − 1 ) / 5 ⌋ = 6 \lfloor (35 - 1) / 5 \rfloor= 6 ⌊(35−1)/5⌋=6 个 “特征-标签” 子序列对, 3 3 3 个 batch。
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)
2.6.2 顺序分区
保证两个相邻的小批量中的子序列在原始序列上也是相邻的,这种策略在基于小批量的迭代过程中保留了拆分的子序列的顺序:
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)
2.7 封装
将上面的两个采样函数包装到一个类中, 以便稍后可以将其用作数据迭代器:
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)
max_tokens
:当加载的数据特别大,取个小点的值训练快一点。
定义一个函数 load_data_time_machine
, 它同时返回数据迭代器和词表, 因此可以与之前在 CNN 定义的 d2l.load_data_fashion_mnist
类似地使用:
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
3. Q&A
Q:文本预处理中,所构建的词汇表把文本映射成数字,文本数据量越大,映射的数字就越大,这些数字还需要做预处理吗?
A:不需要,这些数字会进行 Embedding 再使用的,不会直接传入的。