



通常,涉及⼀个、两个和三个变量的概率公式分别被称为“⼀元语法”(unigram)、“⼆元语法”(bigram)和“三元语法”(trigram)模型。下⾯,我们将学习如何去设计更好的模型
我们看看在真实数据上如果进行自然语言统计,根据时光机器数据集构建词表,并打印前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]
[('the', 2261),
('i', 1267),
('and', 1245),
('of', 1155),
('a', 816),
('to', 695),
('was', 552),
('in', 541),
('that', 443),
('my', 440)]
正如我们所看到的,最流⾏的词看起来很⽆聊,这些词通常被称为停⽤词(stop words),因此可以被过滤掉。尽管如此,它们本⾝仍然是有意义的,我们仍然会在模型中使⽤它们。此外,还有个明显的问题是词频衰减的速度相当地快。例如,最常⽤单词的词频对⽐,第10个还不到第1个的1/5。为了更好地理解,我们可以画出的词频图
freqs = [freq for token,freq in vocab.token_freqs]
d2l.plot(freqs,xlabel='token: x',ylabel='frequency: n(x)',
xscale='log',yscale='log')
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lLC03wjq-1662805709004)(https://yingziimage.oss-cn-beijing.aliyuncs.com/img/202209101810578.svg)]

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)]
最后,我们直观地对比三种模型中的词元频率:一元语法、二元语法和三元语法
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'])
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gufgzyzt-1662805709004)(https://yingziimage.oss-cn-beijing.aliyuncs.com/img/202209101810579.svg)]
这张图⾮常令⼈振奋!原因有很多:⾸先,除了⼀元语法词,单词序列似乎也遵循⻬普夫定律,尽管公式(8.3.7)中的指数α更⼩(指数的⼤⼩受序列⻓度的影响)。其次,词表中n元组的数量并没有那么⼤,这说明语⾔中存在相当多的结构,这些结构给了我们应⽤模型的希望。第三,很多n元组很少出现,这使得拉普拉斯平滑⾮常不适合语⾔建模。作为代替,我们将使⽤基于深度学习的模型
由于序列数据本质上是连续的,因此我们在处理数据时需要解决这个问题。在 8.1节中我们以⼀种相当特别的⽅式做到了这⼀点:当序列变得太⻓⽽不能被模型⼀次性全部处理时,我们可能希望拆分这样的序列⽅便模型读取
在介绍该模型之前,我们看一下总体策略。假设我们将使用神经网络来训练语言模型,模型的中的网络一次处理具有预定义长度(例如n个时间步)的一个小批量序列。现在的问题是如何随机生成一个小批量数据的特征和标签以供读取

在随机采样中,每个样本都是在原始的长序列上任意捕获的子序列。在迭代过程中,来自两个相邻的、随机的、小批量中的子序列不一定在原始序列上相邻。对于语言建模,目标是基于到目前为止我们看到的词元来预测下一个词元,因此标签是移位了一个词元的原始序列
下面的代码每次可以从数据中随机生成一个小批量。在这里,参数bacth_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到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([[17, 18, 19, 20, 21],
[ 2, 3, 4, 5, 6]])
Y: tensor([[18, 19, 20, 21, 22],
[ 3, 4, 5, 6, 7]])
X: tensor([[ 7, 8, 9, 10, 11],
[22, 23, 24, 25, 26]])
Y: tensor([[ 8, 9, 10, 11, 12],
[23, 24, 25, 26, 27]])
X: tensor([[27, 28, 29, 30, 31],
[12, 13, 14, 15, 16]])
Y: tensor([[28, 29, 30, 31, 32],
[13, 14, 15, 16, 17]])
在迭代过程中,除了对原始序列可以随机抽样外,我们还可以保证两个相邻的小批量的子序列在原始序列上也是相邻的。这种策略在基于小批量的迭代过程中保留了拆分的子序列的顺序,因此也称为顺序分区
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。通过将它们打印出来可以发现:迭代期间来自两个相邻的小批量中的子序列在原始序列中确实是相邻的
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,它同时返回数据迭代器和词表,因此可以与其他带有load_data前缀的函数(如d2l.load_data_fashion_mnist)类似地使用
def load_data_time_machine(batch_size,num_steps,
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