在给定这样的文本序列时,语言模型(language model)的目标是估计序列的联合概率。
P ( x 1 , x 2 , … , x T ) . P(x_1, x_2, \ldots, x_T). P(x1,x2,…,xT). 语言模型是非常有用的。例如,只需要一次抽取一个标记 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”和“to wreck a nice beach”听起来非常相似。这种相似性会导致语音识别中的歧义,但是这很容易通过语言模型来解决,因为第二种翻译的感觉很奇怪。同样,在文档摘要生成算法中,“狗咬人”比“人咬狗”出现的频率要高得多,或者“我想吃奶奶”是一个相当令人不安的语句,而“我想吃,奶奶”则要和蔼得多。
语言模型的应用:
显而易见,我们面对的问题是如何对一个文档,甚至是一个标记序列进行建模。假设在单词级别对文本数据进行标记化,我们可以依靠序列模型中对序列模型的分析。让我们从基本概率规则开始:
假设序列长度为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(x′∣x)=nn(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 ′ , 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(x′∣x)p(x′′∣x,x′)=nn(x)n(x)n(x,x′)n(x,x′)n(x,x′,x′′)
拓展到任意长度的情况:
P ( x 1 , x 2 , … , x T ) = ∏ t = 1 T P ( x t ∣ x 1 , … , x t − 1 ) . P(x_1, x_2, \ldots, x_T) = \prod_{t=1}^T P(x_t \mid x_1, \ldots, x_{t-1}). P(x1,x2,…,xT)=t=1∏TP(xt∣x1,…,xt−1).
例如,包含了四个单词(deep learning is fun)的一个文本序列的概率是:
P ( deep , learning , is , fun ) = P ( deep ) P ( learning ∣ deep ) P ( is ∣ deep , learning ) P ( fun ∣ deep , learning , is ) . P(\text{deep}, \text{learning}, \text{is}, \text{fun}) = P(\text{deep}) P(\text{learning} \mid \text{deep}) P(\text{is} \mid \text{deep}, \text{learning}) P(\text{fun} \mid \text{deep}, \text{learning}, \text{is}). P(deep,learning,is,fun)=P(deep)P(learning∣deep)P(is∣deep,learning)P(fun∣deep,learning,is).
为了计算语言模型,我们需要计算单词的概率和给定前面几个单词后出现某个单词的条件概率。这些概率本质上就是语言模型的参数。
这里,我们假设训练数据集是一个大型的文本语料库,比如,所有维基百科的条目,古登堡计划,以及所有发布在网络上的文本。训练数据集中词的概率可以根据给定词的相对词频来计算。例如,可以将估计值 P ^ ( deep ) \hat{P}(\text{deep}) P^(deep) 计算为任何以单词“deep”开头的句子的概率。一种稍稍不太精确的方法是统计单词“deep”在数据集中的出现次数,然后将其除以整个语料库中的单词总数。这种方法效果不错,特别是对于频繁出现的单词。接下来,我们可以尝试估计
P ^ ( learning ∣ deep ) = n ( deep, learning ) n ( deep ) , \hat{P}(\text{learning} \mid \text{deep}) = \frac{n(\text{deep, learning})}{n(\text{deep})}, P^(learning∣deep)=n(deep)n(deep, learning),
其中 n ( x ) n(x) n(x) 和 n ( x , x ′ ) n(x, x') n(x,x′) 分别是单个单词和连续单词对的出现次数。不幸的是,由于连续单词对“deep learning”的出现频率要低得多,所以估计这类单词正确的概率要困难得多。特别是,对于一些不常见的单词组合,要想找到足够的出现次数来获得准确的估计可能都不容易。而对于三个或者更多单词的组合,情况会变得更糟。许多合理的三个单词组合可能是存在的,但是在数据集中却找不到。除非我们提供某种解决方案来将这些单词组合指定为非零计数,否则将无法在语言模型中使用它们。如果数据集很小,或者单词非常罕见,那么这类单词出现一次的机会可能都找不到。
一种常见的策略是执行某种形式的== 拉普拉斯平滑(Laplace smoothing)==,具体方法是在所有计数中添加一个小常量。用 n n n 表示训练集中的单词总数,用 m m m 表示唯一单词的数量。此解决方案有助于处理单元素问题,例如通过:
P ^ ( x ) = n ( x ) + ϵ 1 / m n + ϵ 1 , P ^ ( x ′ ∣ x ) = n ( x , x ′ ) + ϵ 2 P ^ ( x ′ ) n ( x ) + ϵ 2 , P ^ ( x ′ ′ ∣ x , x ′ ) = n ( x , x ′ , x ′ ′ ) + ϵ 3 P ^ ( x ′ ′ ) n ( x , x ′ ) + ϵ 3 . \begin{aligned} \hat{P}(x) & = \frac{n(x) + \epsilon_1/m}{n + \epsilon_1}, \\ \hat{P}(x' \mid x) & = \frac{n(x, x') + \epsilon_2 \hat{P}(x')}{n(x) + \epsilon_2}, \\ \hat{P}(x'' \mid x,x') & = \frac{n(x, x',x'') + \epsilon_3 \hat{P}(x'')}{n(x, x') + \epsilon_3}. \end{aligned} P^(x)P^(x′∣x)P^(x′′∣x,x′)=n+ϵ1n(x)+ϵ1/m,=n(x)+ϵ2n(x,x′)+ϵ2P^(x′),=n(x,x′)+ϵ3n(x,x′,x′′)+ϵ3P^(x′′).
其中, ϵ 1 , ϵ 2 \epsilon_1,\epsilon_2 ϵ1,ϵ2 和 ϵ 3 \epsilon_3 ϵ3 是超参数。以 ϵ 1 \epsilon_1 ϵ1 为例:当 ϵ 1 = 0 \epsilon_1 = 0 ϵ1=0 时,不应用平滑;当 ϵ 1 \epsilon_1 ϵ1 接近正无穷大时, P ^ ( x ) \hat{P}(x) P^(x) 接近均匀概率分布 1 / m 1/m 1/m。上面的公式是 :cite:Wood.Gasthaus.Archambeau.ea.2011
的其他技术实现方法的一个相当原始的变形。
不幸的是,像这样的模型很容易就会因为下面的原因变得无效:
在讨论包含深度学习的解决方案之前,我们需要知道更多的概念和术语。回想一下我们在序列模型中对马尔可夫模型的讨论,并且将其应用于语言建模。如果 P ( x t + 1 ∣ x t , … , x 1 ) = P ( x t + 1 ∣ x t ) P(x_{t+1} \mid x_t, \ldots, x_1) = P(x_{t+1} \mid 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 ) . \begin{aligned} 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 \mid x_1) P(x_3 \mid x_2) P(x_4 \mid x_3),\\ P(x_1, x_2, x_3, x_4) &= P(x_1) P(x_2 \mid x_1) P(x_3 \mid x_1, x_2) P(x_4 \mid x_2, x_3). \end{aligned} P(x1,x2,x3,x4)P(x1,x2,x3,x4)P(x1,x2,x3,x4)=P(x1)P(x2)P(x3)P(x4),=P(x1)P(x2∣x1)P(x3∣x2)P(x4∣x3),=P(x1)P(x2∣x1)P(x3∣x1,x2)P(x4∣x2,x3).
通常,涉及一个、两个和三个变量的概率公式分别被称为“一元语法”(unigram)、“二元语法”(bigram)和“三元语法”(trigram)模型。下面,我们将学习如何去设计更好的模型。
让我们看看在真实数据上效果如何。根据文本预处理中介绍的时光机器数据集构建词汇表,并打印前 10 10 10 个最常用的(频率最高的)单词。
import random
import torch
from d2l import torch as d2l
# 读取文本数据为words
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]
[('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')
词频以一种明确的方式迅速衰减。将前几个单词作为例外消除后,剩余的所有单词大致遵循双对数坐标图上的一条直线。这意味着单词的频率满足 齐普夫定律(Zipf’s law),即第 i i i 个最常用单词的频率 n i n_i ni 为:
n i ∝ 1 i α , n_i \propto \frac{1}{i^\alpha}, ni∝iα1,等价于
log n i = − α log i + c , \log n_i = -\alpha \log i + c, logni=−αlogi+c,
其中 α \alpha α 是刻画分布的指数, c c c 是常数。这就告诉我们想要通过计数统计和平滑来建模单词是不可行的,这样建模的结果会大大高估尾部单词的频率,也就是所谓的不常用单词。但是其他的单词组合,比如二元语法、三元语法等等,又会如何呢?让我们看看二元语法的频率是否与一元语法的频率表现出相同的行为方式。
# 二元语法的频率
# 将全部corpus和去除第一个word的corpus拼接就形成了二元的corpus语料库了
bigram_tokens = [pair for pair in zip(corpus[:-1], corpus[1:])]
bigram_vocab = d2l.Vocab(bigram_tokens)
bigram_vocab.token_freqs[:10]
[(('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)]
这里值得注意:在十个最频繁的词对中,有九个是由两个停用词组成的,只有一个与“the time”有关。进一步,让我们看看三元语法的频率是否表现出相同的行为方式。
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)]
# 将三个模型的frequency绘制出来
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 n n 个时间步)的一个小批量序列。现在的问题是如何随机地生成一个小批量数据的特征和标签以供读取。
首先,由于文本序列可以是任意长的,例如整本《时光机器》(The Time Machine),于是任意长的序列可以被我们划分为具有相同时间步数的子序列。当训练我们的神经网络时,这样的小批量子序列将被输入到模型中。假设网络一次只处理具有 n n n 个时间步的子序列。下图中画出了从原始文本序列获得子序列的所有不同的方式,其中 n = 5 n=5 n=5,并且每个时间步的标记对应于一个字符。请注意,因为我们可以选择任意偏移量来指示初始位置,所以我们有相当大的自由度。
因此,我们应该选择哪一个呢?事实上,他们都一样的好。然而,如果我们只选择一个偏移量,那么用于训练网络的、所有可能的子序列的覆盖范围将是有限的。因此,我们可以从随机偏移量开始划分序列,以同时获得 覆盖性(coverage)和 随机性(randomness)。下面,我们将描述如何实现 随机采样(random sampling)和 顺序分区(sequential partitioning)策略。
在随机采样中,每个样本都是在原始的长序列上任意捕获的子序列。在迭代过程中,来自两个相邻的、随机的、小批量中的子序列不一定在原始序列上相邻。对于语言建模,目标是基于到目前为止我们看到的标记来预测下一个标记,因此标签是移位了一个标记的原始序列。
下面的代码每次都从数据中随机生成一个小批量。在这里,参数 batch_size
指定了每个小批量中子序列样本的数目,参数 num_steps
是每个子序列中预定义的时间步数。
def seq_data_iter_random(corpus,batch_size,num_steps):
"""使用随机抽样生成一个小批量子序列"""
# 从随机偏移量开始对序列进行分区,随机范围包括`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 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 个“特征-标签”子序列对。设置小批量大小为 2 2 2 时,我们只能得到 3 3 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)
[3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34]
[0, 5, 10, 15, 20, 25]
X: tensor([[18, 19, 20, 21, 22],
[28, 29, 30, 31, 32]])
Y: tensor([[19, 20, 21, 22, 23],
[29, 30, 31, 32, 33]])
X: tensor([[ 8, 9, 10, 11, 12],
[ 3, 4, 5, 6, 7]])
Y: tensor([[ 9, 10, 11, 12, 13],
[ 4, 5, 6, 7, 8]])
X: tensor([[23, 24, 25, 26, 27],
[13, 14, 15, 16, 17]])
Y: tensor([[24, 25, 26, 27, 28],
[14, 15, 16, 17, 18]])
保证两个相邻的小批量中的子序列在原始序列上也是相邻的(在基于小批量的迭代过程中也保留了拆分的子序列的顺序)
def seq_data_iter_sequential(corpus,batch_size,num_steps):
"""使用顺序分区生成一个小批量子序列"""
# 从随机偏移量开始划分序列
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+num_tokens+1])
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([[ 0, 1, 2, 3, 4],
[17, 18, 19, 20, 21]])
Y: tensor([[ 1, 2, 3, 4, 5],
[18, 19, 20, 21, 22]])
X: tensor([[ 5, 6, 7, 8, 9],
[22, 23, 24, 25, 26]])
Y: tensor([[ 6, 7, 8, 9, 10],
[23, 24, 25, 26, 27]])
X: tensor([[10, 11, 12, 13, 14],
[27, 28, 29, 30, 31]])
Y: tensor([[11, 12, 13, 14, 15],
[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, #@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
假设训练数据集中有 100 , 000 100,000 100,000 个单词。一个四元语法需要存储多少个词频和相邻多词频率?
词频我认为就是 1 0 4 10^4 104,而相邻多词频率我认为是 1 0 5 ∗ 1 0 5 ∗ 1 0 5 ∗ 1 0 5 = ( 1 0 5 ) 4 10^5*10^5*10^5*10^5 = (10^5)^4 105∗105∗105∗105=(105)4
我们如何对一系列对话如何建模?
我不是做这个方向的,贴出一些别人的思考
https://zhuanlan.zhihu.com/p/296331236
一元语法、二元语法和三元语法的齐普夫定律的指数是不一样的,你能设法估计嘛?
想一想读取长序列数据的其他方法?
我瞎想:
考虑一下我们用于读取长序列的随机偏移量。
如果我们希望一个序列样本是一个完整的句子,那么这在小批量抽样中会带来怎样的问题?如何解决?