语言模型
- 给定文本序列 x 1 , … , x T x_1, \ldots, x_T x1,…,xT,语言模型的目标是估计联合概率 p ( x 1 , … , x T ) p(x_1, \ldots, x_T) p(x1,…,xT)
- 它的应用包括
- 做预训练模型(eg BERT,GPT-3)
- 生成本文,给定前面几个词,不断的使用 x t ∼ p ( x t ∣ x 1 , … , x t − 1 ) x_t \sim p(x_t|x_1, \ldots, x_{t-1}) xt∼p(xt∣x1,…,xt−1) 来生成后续文本
- 判断多个序列中哪个更常见,e.g. “to recognize speech” vs “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′)- 这里 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 ′ , 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′′)
N 元语法
- 当序列很长时,因为文本量不够大,很可能 n ( x 1 , … , x T ) ≤ 1 n(x_1, \ldots, x_T) \leq 1 n(x1,…,xT)≤1
- 使用马尔科夫假设可以缓解这个问题
- 一元语法:(这里
τ
\tau
τ = 0 ,也就是不用管前面的数据,数据间相互独立)
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 2 ) n n ( x 3 ) n n ( x 4 ) n 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) = \frac{n(x_1)}{n} \frac{n(x_2)}{n} \frac{n(x_3)}{n} \frac{n(x_4)}{n} p(x1,x2,x3,x4)=p(x1)p(x2∣x1)p(x3∣x2)p(x4∣x3)=nn(x1)nn(x2)nn(x3)nn(x4) - 二元语法:
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) - 三元语法:(每个词和前面两个词是有关系的)
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 1 , 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_1,x_2,x_3) p(x1,x2,x3,x4)=p(x1)p(x2∣x1)p(x3∣x1,x2)p(x4∣x1,x2,x3)
- 一元语法:(这里
τ
\tau
τ = 0 ,也就是不用管前面的数据,数据间相互独立)
总结
- 语言模型估计文本序列的联合概率
- 使用统计方法时常采用 n n n 元语法
代码实现
语言模型和数据集
在 RNN - 文本预处理 中,了解了如何将文本数据映射为词元,以及将这些词元可以视为一系列离散的观测,例如单词或字符。假设长度为
T
T
T的文本序列中的词元依次为
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)可以被认为是文本序列在时间步
t
t
t处的观测或标签。在给定这样的文本序列时,语言模型(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”读音上听起来非常相似。这种相似性会导致语音识别中的歧义,但是这很容易通过语言模型来解决,因为第二句的语义很奇怪。同样,在文档摘要生成算法中,“狗咬人”比“人咬狗”出现的频率要高得多,或者“我想吃奶奶”是一个相当匪夷所思的语句,而“我想吃,奶奶”则要正常得多。
学习语言模型
显而易见,我们面对的问题是如何对一个文档,甚至是一个词元序列进行建模。假设在单词级别对文本数据进行词元化,可以依靠在 RNN - 序列模型 中对序列模型的分析。
首先从基本概率规则开始:
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).例如,包含了四个单词的一个文本序列的概率是:
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
的一个相当原始的变形。
然而,这样的模型很容易变得无效,原因如下:首先,我们需要存储所有的计数;其次,这完全忽略了单词的意思。例如,“猫”(cat)和“猫科动物”(feline)可能出现在相关的上下文中,
但是想根据上下文调整这类模型其实是相当困难的。最后,长单词序列大部分是没出现过的,
因此一个模型如果只是简单地统计先前“看到”的单词序列频率,那么模型面对这种问题肯定是表现不佳的。
马尔可夫模型与 n n n元语法
在讨论包含深度学习的解决方案之前,需要了解更多的概念和术语。在 RNN - 序列模型 中对马尔可夫模型的讨论,并且将其应用于语言建模。如果
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)模型。下面,我们将学习如何去设计更好的模型。
自然语言统计
我们看看在真实数据上如果进行自然语言统计。根据 RNN - 文本预处理 中介绍的时光机器数据集构建词表,并打印前 10 10 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)
vocab.token_freqs[:10]
正如我们所看到的,(最流行的词)看起来很无聊,这些词通常(被称为停用词)(stop words),因此可以被过滤掉。尽管如此,它们本身仍然是有意义的,会在模型中使用它们。此外,还有个明显的问题是词频衰减的速度相当地快。例如,最常用单词的词频对比,第
10
10
10个还不到第
1
1
1个的
1
/
5
1/5
1/5。为了更好地理解,我们可以[画出的词频图]:
通过此图我们可以发现:词频以一种明确的方式迅速衰减。将前几个单词作为例外消除后,剩余的所有单词大致遵循双对数坐标图上的一条直线。这意味着单词的频率满足齐普夫定律(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是常数。这告诉我们想要通过计数统计和平滑来建模单词是不可行的,因为这样建模的结果会大大高估尾部单词的频率,也就是所谓的不常用单词。那么[其他的词元组合,比如二元语法、三元语法等等,又会如何呢?]我们来看看二元语法的频率是否与一元语法的频率表现出相同的行为方式。
bigram_tokens = [pair for pair in zip(corpus[:-1], corpus[1:])]
bigram_vocab = d2l.Vocab(bigram_tokens)
bigram_vocab.token_freqs[:10]
最后,[直观地对比三种模型中的词元频率]:一元语法、二元语法和三元语法。
这张图非常令人振奋!原因有很多:
- 除了一元语法词,单词序列似乎也遵循齐普夫定律,尽管指数的大小受序列长度的影响;
- 词表中 n n n元组的数量并没有那么大,这说明语言中存在相当多的结构,这些结构给了我们应用模型的希望;
- 很多 n n n元组很少出现,这使得拉普拉斯平滑非常不适合语言建模。作为代替,我们将使用基于深度学习的模型。
读取长序列数据
由于序列数据本质上是连续的,因此我们在处理数据时需要解决这个问题。在 RNN - 序列模型 中我们以一种相当特别的方式做到了这一点:当序列变得太长而不能被模型一次性全部处理时,我们可能希望拆分这样的序列方便模型读取。
在介绍该模型之前,我们看一下总体策略。假设我们将使用神经网络来训练语言模型,模型中的网络一次处理具有预定义长度(例如 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): # @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
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个小批量。
顺序分区
在迭代过程中,除了对原始序列可以随机抽样外,我们还可以[保证两个相邻的小批量中的子序列在原始序列上也是相邻的]。这种策略在基于小批量的迭代过程中保留了拆分的子序列的顺序,因此称为顺序分区。
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
]。通过将它们打印出来可以发现:迭代期间来自两个相邻的小批量中的子序列在原始序列中确实是相邻的。
现在,我们[将上面的两个采样函数包装到一个类中],以便稍后可以将其用作数据迭代器。
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
,它同时返回数据迭代器和词表],因此可以与其他带有load_data
前缀的函数类似地使用。
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
小结
- 语言模型是自然语言处理的关键。
- n n n元语法通过截断相关性,为处理长序列提供了一种实用的模型。
- 长序列存在一个问题:它们很少出现或者从不出现。
- 齐普夫定律支配着单词的分布,这个分布不仅适用于一元语法,还适用于其他 n n n元语法。
- 通过拉普拉斯平滑法可以有效地处理结构丰富而频率不足的低频词词组。
- 读取长序列的主要方式是随机采样和顺序分区。在迭代过程中,后者可以保证来自两个相邻的小批量中的子序列在原始序列上也是相邻的。
QA 思考
Q1:在文本预处理中,所构建的词汇表把文本映射成数字,文本数据量越大,映射的数字也就越大,这些数字还需要做预处理吗?例如归一化处理等。是否对模型有影响?
A1:数字在此处主要是起到一个 id 的作用。这个数字只是index,真正的东西是embedding。
Q2:连续单词对是指有固定先后顺序的还是任何顺序都可行呢?
A2:是有一个先后顺序的,顺序不一样值就完全不一样。
Q3:o(T),T 指什么
A3:T 是 num_of_steps 也就是每次需要看一个多长的序列
后记
整理了一下代码:
text_preprocessing 在 RNN - 文本预处理 后记中
sequence 在 RNN - 序列模型 后记中
import random
import matplotlib.pyplot as plt
import torch
# 从 text_preprocessing 模块导入所需的函数和类
from text_preprocessing import load_corpus_time_machine
from text_preprocessing import tokenize, read_time_machine, Vocab
# 从 sequence 模块导入 plot 函数
from sequence import plot
# 打印前十个最常用的单词
# 对时间机器文本进行分词
tokens = tokenize(read_time_machine())
# 将二维的分词结果展平为一维列表
corpus = [token for line in tokens for token in line]
# 创建词表对象
vocab = Vocab(corpus)
# 打印词表中出现频率最高的前 10 个词元及其频率
print(vocab.token_freqs[:10])
# 获取词元频率列表
freqs = [freq for token, freq in vocab.token_freqs]
# 绘制词元频率图,使用对数坐标轴
plot(freqs, xlabel='token: x', ylabel='frequency: n(x)',
xscale='log', yscale='log')
# 显示图形
plt.show()
# 二元语法的频率
# 生成二元语法词元列表
bigram_tokens = [pair for pair in zip(corpus[:-1], corpus[1:])]
# 创建二元语法词表对象
bigram_vocab = Vocab(bigram_tokens)
# 打印二元语法词表中出现频率最高的前 10 个词元及其频率
print(bigram_vocab.token_freqs[:10])
# 三元语法的频率
# 生成三元语法词元列表
trigram_tokens = [triple for triple in zip(
corpus[:-2], corpus[1:-1], corpus[2:]
)]
# 创建三元语法词表对象
trigram_vocab = Vocab(trigram_tokens)
# 打印三元语法词表中出现频率最高的前 10 个词元及其频率
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]
# 绘制一元、二元和三元语法词元频率图,使用对数坐标轴
plot([freqs, bigram_freqs, trigram_freqs], xlabel='token: x',
ylabel='frequency: n(x)', xscale='log', yscale='log',
legend=['unigram', 'bigram', 'trigram'])
# 显示图形
plt.show()
def seq_data_iter_random(corpus, batch_size, num_steps):
"""
使用随机抽样生成一个小批量子序列。
例子:
假设 corpus = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],batch_size = 2,num_steps = 3
首先随机选择一个起始位置,假设从位置 1 开始,即从 1 开始
可以得到子序列:[1, 2, 3], [4, 5, 6], [7, 8, 9]
然后打乱这些子序列的顺序,随机选取 batch_size 个子序列作为一个批次
例如选取 [4, 5, 6] 和 [1, 2, 3] 作为一个批次
该批次的 X 为 [[4, 5, 6], [1, 2, 3]],Y 为 [[5, 6, 7], [2, 3, 4]]
:param corpus: 语料库,一个一维的词元列表
:param batch_size: 每个小批量中的样本数量
:param num_steps: 每个子序列的长度
:return: 生成器,每次生成一个小批量的特征 X 和标签 Y
"""
# 随机选择一个起始位置
corpus = corpus[random.randint(0, num_steps - 1):]
# 计算可生成的子序列数量
num_subseqs = (len(corpus) - 1) // num_steps
# 生成每个子序列的起始索引
initial_indices = list(range(0, num_subseqs * num_steps, num_steps))
# 打乱起始索引的顺序
random.shuffle(initial_indices)
def data(pos):
"""
获取从位置 pos 开始的长度为 num_steps 的子序列
:param pos: 起始位置
:return: 子序列
"""
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_per_batch = initial_indices[i:i + batch_size]
# 生成当前批次的特征 X
X = [data(j) for j in initial_indices_per_batch]
# 生成当前批次的标签 Y
Y = [data(j + 1) for j in initial_indices_per_batch]
# 生成当前批次的特征 X 和标签 Y
yield torch.tensor(X), torch.tensor(Y)
# 生成一个从 0 到 34 的序列
my_seq = list(range(35))
# 使用随机抽样生成小批量子序列,并打印每个批次的特征 X 和标签 Y
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):
"""
使用顺序分区生成一个小批量子序列。
例子:
假设 corpus = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],batch_size = 2,num_steps = 3
首先随机选择一个偏移量,假设偏移量为 1,即从 1 开始
然后将语料库按 batch_size 进行分区,得到:
[[1, 2, 3, 4], [5, 6, 7, 8]]
再将每个分区按 num_steps 进行划分,得到:
第一个分区:[[1, 2, 3], [4]]
第二个分区:[[5, 6, 7], [8]]
选取每个分区的第一个子序列作为一个批次,即 [[1, 2, 3], [5, 6, 7]]
该批次的 X 为 [[1, 2, 3], [5, 6, 7]],Y 为 [[2, 3, 4], [6, 7, 8]]
:param corpus: 语料库,一个一维的词元列表
:param batch_size: 每个小批量中的样本数量
:param num_steps: 每个子序列的长度
:return: 生成器,每次生成一个小批量的特征 X 和标签 Y
"""
# 随机选择一个偏移量
offset = random.randint(0, num_steps)
# 计算可用于生成子序列的词元数量
num_tokens = ((len(corpus) - offset - 1) // batch_size) * batch_size
# 截取语料库,得到特征 X 的数据
Xs = torch.tensor(corpus[offset:offset + num_tokens])
# 截取语料库,得到标签 Y 的数据
Ys = torch.tensor(corpus[offset + 1:offset + num_tokens + 1])
# 调整 Xs 和 Ys 的形状,使其适合按批次划分
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
X = Xs[:, i:i + num_steps]
# 选取当前批次的标签 Y
Y = Ys[:, i:i + num_steps]
# 生成当前批次的特征 X 和标签 Y
yield X, Y
# 基于相同的设置,读取每个小批量的子序列的特征 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:
"""
序列数据加载器类,用于生成小批量的序列数据。
可以选择使用随机抽样或顺序分区的方式生成数据。
"""
def __init__(self, batch_size, num_steps, use_random_iter, max_tokens):
"""
初始化序列数据加载器。
:param batch_size: 每个小批量中的样本数量
:param num_steps: 每个子序列的长度
:param use_random_iter: 是否使用随机抽样的方式生成数据
:param max_tokens: 语料库中使用的最大词元数量
"""
if use_random_iter:
# 如果使用随机抽样,选择 seq_data_iter_random 函数
self.data_iter_fn = seq_data_iter_random
else:
# 如果使用顺序分区,选择 seq_data_iter_sequential 函数
self.data_iter_fn = seq_data_iter_sequential
# 加载时间机器语料库和词表
self.corpus, self.vocab = load_corpus_time_machine(max_tokens)
# 保存批次大小和子序列长度
self.batch_size, self.num_steps = batch_size, num_steps
def __iter__(self):
"""
实现迭代器方法,返回一个生成器,用于生成小批量的序列数据。
:return: 生成器,每次生成一个小批量的特征 X 和标签 Y
"""
return self.data_iter_fn(self.corpus, self.batch_size, self.num_steps)
def load_data_time_machine(batch_size, num_steps,
use_random_iter=False, max_token=10000):
"""
加载时间机器数据集,返回数据迭代器和词表。
:param batch_size: 每个小批量中的样本数量
:param num_steps: 每个子序列的长度
:param use_random_iter: 是否使用随机抽样的方式生成数据
:param max_token: 语料库中使用的最大词元数量
:return: 数据迭代器和词表
"""
data_iter = SeqDataLoader(
batch_size=batch_size, num_steps=num_steps,
use_random_iter=use_random_iter, max_tokens=max_token
)
return data_iter, data_iter.vocab