👨🎓作者简介:一位即将上大四,正专攻机器学习的保研er
🌌上期文章:机器学习&&深度学习——文本预处理
📚订阅专栏:机器学习&&深度学习
希望文章对你们有所帮助
引入
假设长度为T的文本序列,那么语言模型的目标是估计序列的联合概率:
P
(
x
1
,
x
2
,
.
.
.
,
x
T
)
P(x_1,x_2,...,x_T)
P(x1,x2,...,xT)
在抽取一个词元:
x
t
符合
P
(
x
t
∣
x
t
−
1
,
.
.
.
,
x
1
)
x_t符合P(x_t|x_{t-1},...,x_1)
xt符合P(xt∣xt−1,...,x1)
的时候,一个理想的语言模型就能基于模型本身生成自然文本。
而从这样的模型中提取的样本要作为自然语言来传递,因此我们需要生成一个有意义的对话,也就是说,我们的设计需要理解文本,而不仅仅是生成语法合理的内容。
例如:“狗咬人”就比“人咬狗”看起来更正常;“我想吃老妈”看起来语法没问题,但是很emmm,而“我想吃,老妈”就看起来合理多了。
学习语言模型
从基本概率规则开始:
P
(
x
1
,
x
2
,
.
.
.
,
x
T
)
=
∏
t
=
1
T
P
(
x
t
∣
x
1
,
.
.
.
,
x
t
−
1
)
P(x_1,x_2,...,x_T)=\prod_{t=1}^TP(x_t|x_1,...,x_{t-1})
P(x1,x2,...,xT)=t=1∏TP(xt∣x1,...,xt−1)
那么包含了四个单词的一个文本序列的概率是:
P
(
d
e
e
p
,
l
e
a
r
n
i
n
g
,
i
s
,
f
u
n
)
=
P
(
d
e
e
p
)
P
(
l
e
a
r
n
i
n
g
∣
d
e
e
p
)
P
(
i
s
∣
d
e
e
p
,
l
e
a
r
n
i
n
g
)
P
(
f
u
n
∣
d
e
e
p
,
l
e
a
r
n
i
n
g
,
i
s
)
P(deep,learning,is,fun)=P(deep)P(learning|deep)P(is|deep,learning)P(fun|deep,learning,is)
P(deep,learning,is,fun)=P(deep)P(learning∣deep)P(is∣deep,learning)P(fun∣deep,learning,is)
而其中的P,也就是数据集中词的概率可以根据给定词的相对词频来计算,如:
P
^
(
l
e
a
r
n
i
n
g
∣
d
e
e
p
)
=
n
(
d
e
e
p
,
l
e
a
r
n
i
n
g
)
n
(
d
e
e
p
)
\hat{P}(learning|deep)=\frac{n(deep,learning)}{n(deep)}
P^(learning∣deep)=n(deep)n(deep,learning)
其中,n(x)和n(x,x’)分别是单个单词和连续单词对的出现次数。
但连续的单词出现的概率会低很多,特别可能有些是三个或更多的单词组合( 除非我们提供某种解决方案,来将这些单词组合指定为非零计数,否则将无法在语言模型中使用它们。如果数据集很小,或者单词非常罕见,那么这类单词出现一次的机会可能都找不到。)
马尔可夫模型与n元语法
之前讲过马尔可夫模型,比如一阶马尔可夫性质:
P
(
x
t
+
1
∣
x
t
,
.
.
.
,
x
1
)
=
P
(
x
t
+
1
∣
x
t
)
P(x_{t+1}|x_t,...,x_1)=P(x_{t+1}|x_t)
P(xt+1∣xt,...,x1)=P(xt+1∣xt)
阶数越高,对应的依赖关系就越长,这种性质推导出了很多可以应用于序列建模的公式:
P
(
x
1
,
x
2
,
x
3
,
x
4
)
=
P
(
x
1
)
P
(
x
2
)
P
(
x
3
)
P
(
x
4
)
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
)
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)P(x_3)P(x_4)\\ 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)\\ 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(x2)P(x3)P(x4)P(x1,x2,x3,x4)=P(x1)P(x2∣x1)P(x3∣x2)P(x4∣x3)P(x1,x2,x3,x4)=P(x1)P(x2∣x1)P(x3∣x1,x2)P(x4∣x2,x3)
上面上式分别被称为一元语法、二元语法和三元语法。
自然语言统计
我们首先在《时光机器》数据集总构建词表,并打印前10个最常用单词:
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)
print(vocab.token_freqs[:10])
运行结果:
[(‘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')
d2l.plt.show()
这个图可以发现,词频在以一种明确的方式迅速衰减。将前几个单词作为例外消除后,剩余的单词大致遵循双对数坐标图上的一条直线(单词满足着齐普夫定律),即第i个最常用单词的频率为:
n
i
≈
1
i
α
n_i≈\frac{1}{i^α}
ni≈iα1
等价于
l
o
g
n
i
=
−
α
l
o
g
i
+
c
logn_i=-αlogi+c
logni=−αlogi+c
其中α是刻画分布的质数,c是常数。
我们可以验证二元语法、三元语法等的频率是否和一元语法的频率表现出相同的行为方式:
# 二元语法
bigram_tokens = [pair for pair in zip(corpus[:-1], corpus[1:])] # 第0位到最后一位以及第1位到最后一位组合起来就是二元组
bigram_vocab = d2l.Vocab(bigram_tokens)
# 三元语法
trigram_tokens = [triple for triple in zip(
corpus[:-2], corpus[1:-1], corpus[2:]
)]
trigram_vocab = d2l.Vocab(trigram_tokens)
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()
运行结果:
这张图可以看出:除了一元语法,单词序列也遵循齐普夫定律,区别也就是α更小(指数大小受序列长度影响)
读取长序列数据
由于文本序列可以是任意长的,于是任意长的序列可以被我们划分为具有相同时间步数的子序列。当训练我们的神经网络时,这样的小批量子序列将被输入到模型中。下图给出了n=5的情况:
可以看出,我们也可以选择任意偏移量来指示初始位置,所以我们有很好的自由度。
上图中的他们都一样好,但是我们如果只能选一个偏移量,那么用来训练网络的、所有可能的子序列的覆盖范围是有限的。
因此我们可以从随机偏移量开始划分序列,以同时获得覆盖性和随机性。
随机采样
在随机采样中,每个样本都是在原始的长序列上任意捕获的子序列。 在迭代过程中,来自两个相邻的、随机的、小批量中的子序列不一定在原始序列上相邻。 对于语言建模,目标是基于到目前为止我们看到的词元来预测下一个词元, 因此标签是移位了一个词元的原始序列。
下面的代码每次可以从数据中随机生成一个小批量。 在这里,参数batch_size指定了每个小批量中子序列样本的数目, 参数num_steps是每个子序列中预定义的时间步数。
import torch
import random
from d2l import torch as d2l
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)
如果我们生成一个0到34的序列,批量大小为2,时间步为5,那么我们可以生成(35-1)/5=6个“特征-标签”子序列对。如果设置小批量大小为2,我们只能得到3个小批量。
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([[22, 23, 24, 25, 26],
[27, 28, 29, 30, 31]])
Y: tensor([[23, 24, 25, 26, 27],
[28, 29, 30, 31, 32]])
X: tensor([[ 7, 8, 9, 10, 11],
[12, 13, 14, 15, 16]])
Y: tensor([[ 8, 9, 10, 11, 12],
[13, 14, 15, 16, 17]])
X: tensor([[ 2, 3, 4, 5, 6],
[17, 18, 19, 20, 21]])
Y: tensor([[ 3, 4, 5, 6, 7],
[18, 19, 20, 21, 22]])
可以看出Y比起对应的X同位置都是多一个的。
顺序分区
在迭代过程中,除了对原始序列可以随机抽样外,我们还可以保证两个相邻的小批量中的子序列在原始序列上也是相邻的。这种策略在基于小批量的迭代过程中保留了拆分的子序列的顺序,因此称为顺序分区。
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
基于相同的设置,通过顺序分区读取每个小批量的子序列的特征X和标签Y。 通过将它们打印出来可以发现: 迭代期间来自两个相邻的小批量中的子序列在原始序列中确实是相邻的(每个相邻X之间数据都是接在一起的)。
my_seq = list(range(35))
for X, Y in seq_data_iter_sequential(my_seq, batch_size=2, num_steps=5):
print('X: ', X, '\nY:', Y)
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],
[22, 23, 24, 25, 26]])
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]])
包装
我们将上面的两个采样函数包装到一个类中,以便以后可以将其用作数据迭代器。
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)
最后定义一个load_data_time_machine函数,同时返回数据迭代器和词表:
def load_data_time_machine(batch_size, num_steps, use_random_iter=False, max_tokens=10000): #@save
"""返回时光机器数据集的迭代器和词表"""
data_iter = SeqDataLoader(
batch_size, num_steps, use_random_iter, max_tokens)
return data_iter, data_iter.vocab
小结
1、语言模型是自然语言处理的关键。
2、n元语法通过截断相关性,为处理长序列提供了一种实用的模型。
3、长序列存在一个问题:它们很少出现或者从不出现。
4、齐普夫定律支配着单词的分布,这个分布不仅适用于一元语法,还适用于其他n元语法。
5、读取长序列的主要方式是随机采样和顺序分区。在迭代过程中,后者可以保证来自两个相邻的小批量中的子序列在原始序列上也是相邻的。