李沐动手学深度学习V2-NLP语言模型、数据集加载和数据迭代器实现以及代码实现

一. 语言模型和数据集

1. 介绍

将文本数据映射为词元,以及将这些词元可以视为一系列离散的观测,例如单词或字符。假设长度为 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 1tT)可以被认为是文本序列在时间步 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) xtP(xtxt1,,x1),一个理想的语言模型就能够基于模型本身生成自然文本。

假设训练数据集是一个大型的文本语料库。比如,维基百科的所有条目、古登堡计划,或者所有发布在网络上的文本。训练数据集中词的概率可以根据给定词的相对词频来计算。例如,可以将估计值 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^(learningdeep)=n(deep)n(deep, learning),
其中 n ( x ) n(x) n(x) n ( x , x ′ ) n(x, x') n(x,x)分别是单个单词和连续单词对的出现次数,由于连续单词对“deep learning”的出现频率要低得多,所以估计这类单词正确的概率要困难得多。特别是对于一些不常见的单词组合,要想找到足够的出现次数来获得准确的估计可能都不容易,对于三个或者更多的单词组合,情况会变得更糟,许多合理的三个单词组合可能是存在的,但是在数据集中却找不到。除非提供某种解决方案,来将这些单词组合指定为非零计数,否则将无法在语言模型中使用它们,如果数据集很小,或者单词非常罕见,那么这类单词出现一次的机会可能都找不到。

2. 马尔可夫模型和n元语法

如果 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+1xt,,x1)=P(xt+1xt),则序列上的分布满足一阶马尔可夫性质,阶数越高,对应的依赖关系就越长,这种性质推导出了许多可以应用于序列建模的近似公式:
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(x2x1)P(x3x2)P(x4x3),=P(x1)P(x2x1)P(x3x1,x2)P(x4x2,x3).
通常,涉及一个、两个和三个变量的概率公式分别被称为 “一元语法”(unigram)、“二元语法”(bigram)和“三元语法”(trigram)模型。

3. 自然语言统计

3.1 一元语法(把单独一个单词作为一个整体,统计它在文本中出现的频率): 根据时光机器数据集构建词表, 并打印前 10 个最常用的(频率最高的)单词。

import random
import torch
import d2l.torch
#一元语法
lines = d2l.torch.read_time_machine()
tokens = d2l.torch.tokenize(lines,'word')#list of list类型
# 每个文本行不一定是一个句子或一个段落,因此需要把所有文本行拼接到一起
tokens = [token for line in tokens for token in line]#list类型
vocab = d2l.torch.Vocab(tokens)
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.torch.plot(freqs,xlabel='token:x',ylabel='frequence: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}, niiα1,
等价于
log ⁡ n i = − α log ⁡ i + c , \log n_i = -\alpha \log i + c, logni=αlogi+c,
其中 α \alpha α是刻画分布的指数, c c c是常数。

3.2 二元语法(把两个单词作为一个整体,统计它在文本中出现的频率):在十个最频繁的词对中,有九个是由两个停用词组成的, 只有一个与“the time”有关,如下面结果所示

#二元语法
bigram_tokens = [pair for pair in zip(tokens[:-1],tokens[1:])] #bigram_tokens里面每个元素是一个二元组,由前后相邻的词组成,list类型
#bigram_tokens里面每个元素是一个二元组
bigram_vocab = d2l.torch.Vocab(bigram_tokens)
bigram_token_freqs = bigram_vocab.token_freqs #根据二元词组出现的次数得到排好序的二元词组,list类型,里面每个元素为一个二元组,格式为(二元词组,二元词组在bigram_tokens列表里面出现的次数)
bigram_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)]

3.3 三元语法(把三个单词作为一个整体,统计它在文本中出现的频率)

#三元语法
trigram_tokens = [pair for pair in zip(tokens[:-2],tokens[1:-1],tokens[2:])]#trigram_tokens里面每个元素是一个三元组,由前后相邻以及中间的词组成,list类型
trigram_vocab = d2l.torch.Vocab(trigram_tokens)
trigram_token_freqs = trigram_vocab.token_freqs #根据三元词组出现的次数得到排好序的三元词组,list类型,里面每个元素为一个三元组,格式为(三元词组,三元词组在trigram_tokens列表里面出现的次数)
trigram_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)]

3.4 直观对比一元语法、二元语法和三元语法三种模型中的词元频率,如下图所示。

bigram_freqs = [freq for token,freq in bigram_vocab.token_freqs]
trigram_freqs = [freq for token,freq in trigram_vocab.token_freqs]
d2l.torch.plot([freqs,bigram_freqs,trigram_freqs],xlabel='token:x',ylabel='frequence:n(x)',xscale='log',yscale='log',legend=['unigram','bigram','trigram'])

三种语法对比
如上图看出:首先除了一元语法词,二元语法和三元语法也遵循齐普夫定律, 尽管对齐普夫定律公式中的指数 𝛼 更小 (指数的大小受序列长度的影响);其次词表中 𝑛 元组的数量并没有那么大,这说明语言中存在相当多的结构, 这些结构给了我们应用模型的希望; 第三,很多 𝑛 元组很少出现,这使得拉普拉斯平滑非常不适合语言建模,作为代替,我们将使用基于深度学习的模型。

4. 读取长序列数据

由于序列数据本质上是连续的,因此在处理数据时需要解决这个问题,当序列变得太长而不能被模型一次性全部处理时,希望能拆分这样的序列方便模型读取。在介绍该模型之前,看一下总体策略,使用神经网络来训练语言模型,模型中的网络一次处理具有预定义长度(例如 n n n个时间步)的一个小批量序列,下面随机生成一个小批量数据的特征和标签以供读取。
首先,由于文本序列可以是任意长的,把任意长的序列划分为具有相同时间步数的子序列,当训练神经网络时小批量子序列将被输入到模型中。假设网络一次只处理具有 n n n个时间步的子序列,如下图所示画出了从原始文本序列获得子序列的所有不同的方式, 其中 𝑛=5 ,并且每个时间步的词元对应于一个字符。 注意因为可以选择任意偏移量来指示初始位置,所以我们有相当大的自由度。
5个时间步的子序列
因此从上图看出根据选择的任意偏移量来指示初始位置不同,所得到的子序列数据也会不同。如果我们只选择上面一个偏移量所得到的子序列,那么用于训练网络的、所有可能的子序列的覆盖范围将是有限的。因此我们可以从随机偏移量开始划分序列,以同时获得覆盖性(coverage)和随机性(randomness)。下面描述如何实现随机采样(random sampling)和
顺序分区(sequential partitioning)策略。

5. 随机采样

在随机采样中,每个样本都是在原始的长序列上任意捕获的子序列。
在迭代过程中,来自两个相邻的、随机的、小批量中的子序列不一定在原始序列上相邻。对于语言建模,目标是基于到目前为止我们看到的词元来预测下一个词元,因此标签是移位了一个词元的原始序列。
下面的代码每次可以从数据中随机生成一个小批量,参数batch_size指定了每个小批量中子序列样本的数目,参数num_steps是每个子序列中预定义的时间步数

"""使用随机抽样生成一个小批量子序列"""
#随机采样:将序列文本(这个序列文本刚开始会经过随机偏移来决定文本的开始索引)按固定长度切成一个个子序列作为一个样本,然后把这些样本随机打乱,然后根据batch_szie来得到一个批量大小,一个样本也即是一个子序列,由num_steps个词组成
def seg_data_iter_random(corpus,batch_size,num_steps):
    #corpus表示将文本序列词转换成对应的id的集合
    # 从随机偏移量开始对序列进行分区,随机范围包括num_steps-1
    corpus = corpus[random.randint(0,num_steps-1):]
    # 减去1,是因为需要考虑标签,因为这里的标签,是移位了一个词元的原始序	列。如果不减去这个1,就有可能使得原始序列中的最后一个元素被列进样本中而没有与之对应的标签(数组越界了)。
    num_examples = (len(corpus)-1)//num_steps
    # 长度为num_steps的子序列的起始索引
    start_index_examples = list(range(0,num_examples*num_steps,num_steps)) # 每个样本的起始索引
    # 在随机抽样的迭代过程中,
    # 来自两个相邻的、随机的、小批量中的子序列不一定在原始序列上相邻
    random.shuffle(start_index_examples)
    def get_example_data(start_index):
        # 返回从pos位置开始的长度为num_steps的序列
        return corpus[start_index:start_index+num_steps] #根据每个样本的起始索引,然后移动num_steps数得到这个由num_steps个词组成的一个样本
    num_batches = num_examples//batch_size
    for i in range(0,num_batches*batch_size,batch_size):
        # start_index_examples_pre_batch包含子序列的随机起始索引
        start_index_examples_pre_batch = start_index_examples[i:i+batch_size] #每个batch_size中每个样本的起始索引
        X = [get_example_data(start_index_example) for start_index_example in start_index_examples_pre_batch]
        Y = [get_example_data(start_index_example+1) for start_index_example in start_index_examples_pre_batch]
        yield torch.tensor(X),torch.tensor(Y) #X为数据,Y为label,也即是Y中每个样本中的元素是X中对应样本中的元素下一个,也即是模型预测下一个元素是什么

下面生成一个从 0 0 0 34 34 34的序列,假设批量大小为 2 2 2,时间步数为 5 5 5,这意味着可以生成 ⌊ ( 35 − 1 ) / 5 ⌋ = 6 \lfloor (35 - 1) / 5 \rfloor= 6 (351)/5=6个“特征-标签”子序列对,如果设置小批量大小为 2 2 2,我们只能得到 3 3 3个小批量。

my_seq = list(range(35))
for X,Y in seg_data_iter_random(my_seq,2,5):
    print('X: ',X,'\n Y: ',Y)
输出结果如下:
X:  tensor([[16, 17, 18, 19, 20],
        [21, 22, 23, 24, 25]]) 
 Y:  tensor([[17, 18, 19, 20, 21],
        [22, 23, 24, 25, 26]])
X:  tensor([[ 6,  7,  8,  9, 10],
        [ 1,  2,  3,  4,  5]]) 
 Y:  tensor([[ 7,  8,  9, 10, 11],
        [ 2,  3,  4,  5,  6]])
X:  tensor([[11, 12, 13, 14, 15],
        [26, 27, 28, 29, 30]]) 
 Y:  tensor([[12, 13, 14, 15, 16],
        [27, 28, 29, 30, 31]])

6. 顺序分区

在迭代过程中除了对原始序列可以随机抽样外, 还可以保证两个相邻的小批量中的子序列在原始序列上也是相邻的,这种策略在基于小批量的迭代过程中保留了拆分的子序列的顺序,因此称为顺序分区。

"""使用顺序分区生成一个小批量子序列"""
#每个批量样本数据是连续的
def seg_data_iter_sequential(corpus,batch_size,num_steps):
    #corpus表示将文本序列词转换成对应的id的集合
    # 从随机偏移量开始划分序列
    offset = random.randint(0,num_steps-1)
    num_tokens = ((len(corpus)-offset-1)//batch_size)*batch_size
    corpus_X = torch.tensor(corpus[offset:offset+num_tokens])
    corpus_Y = torch.tensor(corpus[offset+1:offset+1+num_tokens])
    Xs,Ys = corpus_X.reshape(batch_size,-1),corpus_Y.reshape(batch_size,-1)
    num_batches = Xs.shape[1]//num_steps
    for i in range(0,num_batches*num_steps,num_steps):
        X = Xs[:,i:i+num_steps]
        Y = Ys[:,i:i+num_steps]
        yield X,Y

基于相同的设置,通过顺序分区读取每个小批量的子序列的特征X和标签Y, 通过将它们打印出来可以发现: 迭代期间来自两个相邻的小批量中的子序列在原始序列中确实是相邻的。

for X,Y in seg_data_iter_sequential(my_seq,2,5):
    print('X: ',X,'\n Y: ',Y)
输出结果如下:
X:  tensor([[ 4,  5,  6,  7,  8],
        [19, 20, 21, 22, 23]]) 
 Y:  tensor([[ 5,  6,  7,  8,  9],
        [20, 21, 22, 23, 24]])
X:  tensor([[ 9, 10, 11, 12, 13],
        [24, 25, 26, 27, 28]]) 
 Y:  tensor([[10, 11, 12, 13, 14],
        [25, 26, 27, 28, 29]])
X:  tensor([[14, 15, 16, 17, 18],
        [29, 30, 31, 32, 33]]) 
 Y:  tensor([[15, 16, 17, 18, 19],
        [30, 31, 32, 33, 34]])

7. 数据迭代器

将上面两个采样函数包装到一个类中,可以将其用作数据迭代器:

"""加载序列数据的迭代器"""
class SeqDataLoader:
    def __init__(self,batch_size,num_steps,use_random_iter,max_tokens):
        if use_random_iter:
            self.data_iter_fn = d2l.torch.seq_data_iter_random
        else:
            self.data_iter_fn = d2l.torch.seq_data_iter_sequential
        self.corpus,self.vocab = d2l.torch.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, 它同时返回数据迭代器和词表Vocab类。

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

8. 小结

  • 语言模型是自然语言处理的关键。
  • n n n元语法通过截断相关性,为处理长序列提供了一种实用的模型。
  • 长序列存在一个问题:它们很少出现或者从不出现。
  • 齐普夫定律支配着单词的分布,这个分布不仅适用于一元语法,还适用于其他 n n n元语法。
  • 读取长序列的主要方式是随机采样和顺序分区。在迭代过程中,后者可以保证来自两个相邻的小批量中的子序列在原始序列上也是相邻的。

9. 数据集加载以及数据迭代器实现全部代码

import random
import torch
import d2l.torch

#一元语法
lines = d2l.torch.read_time_machine()
tokens = d2l.torch.tokenize(lines, 'word')  #list of list类型
# 每个文本行不一定是一个句子或一个段落,因此需要把所有文本行拼接到一起
tokens = [token for line in tokens for token in line]  #list类型
vocab = d2l.torch.Vocab(tokens)
vocab.token_freqs[:10]
freqs = [freq for token, freq in vocab.token_freqs]
d2l.torch.plot(freqs, xlabel='token:x', ylabel='frequence:n(x)', xscale='log', yscale='log')
#二元语法
bigram_tokens = [pair for pair in zip(tokens[:-1], tokens[1:])]  #bigram_tokens里面每个元素是一个二元组,由前后相邻的词组成,list类型
#bigram_tokens里面每个元素是一个二元组
bigram_vocab = d2l.torch.Vocab(bigram_tokens)
bigram_token_freqs = bigram_vocab.token_freqs  #根据二元词组出现的次数得到排好序的二元词组,list类型,里面每个元素为一个二元组,格式为(二元词组,二元词组在bigram_tokens列表里面出现的次数)
bigram_token_freqs[:10]
#三元语法
trigram_tokens = [pair for pair in
                  zip(tokens[:-2], tokens[1:-1], tokens[2:])]  #trigram_tokens里面每个元素是一个三元组,由前后相邻以及中间的词组成,list类型
trigram_vocab = d2l.torch.Vocab(trigram_tokens)
trigram_token_freqs = trigram_vocab.token_freqs  #根据三元词组出现的次数得到排好序的三元词组,list类型,里面每个元素为一个三元组,格式为(三元词组,三元词组在trigram_tokens列表里面出现的次数)
trigram_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]
d2l.torch.plot([freqs, bigram_freqs, trigram_freqs], xlabel='token:x', ylabel='frequence:n(x)', xscale='log',
               yscale='log', legend=['unigram', 'bigram', 'trigram'])
"""使用随机抽样生成一个小批量子序列"""


#随机采样:将序列文本(这个序列文本刚开始会经过随机偏移来决定文本的开始索引)按固定长度切成一个个子序列作为一个样本,然后把这些样本随机打乱,然后根据batch_szie来得到一个批量大小,一个样本也即是一个子序列,由num_steps个词组成
def seg_data_iter_random(corpus, batch_size, num_steps):
    #corpus表示将文本序列词转换成对应的id的集合
    # 从随机偏移量开始对序列进行分区,随机范围包括num_steps-1
    corpus = corpus[random.randint(0, num_steps - 1):]
    # 减去1,是因为需要考虑标签,因为这里的标签,是移位了一个词元的原始序列。如果不减去这个1,就有可能使得原始序列中的最后一个元素被列进样本中而没有与之对应的标签(数组越界了)。
    num_examples = (len(corpus) - 1) // num_steps
    # 长度为num_steps的子序列的起始索引
    start_index_examples = list(range(0, num_examples * num_steps, num_steps))  # 每个样本的起始索引
    # 在随机抽样的迭代过程中,
    # 来自两个相邻的、随机的、小批量中的子序列不一定在原始序列上相邻
    random.shuffle(start_index_examples)

    def get_example_data(start_index):
        # 返回从pos位置开始的长度为num_steps的序列
        return corpus[start_index:start_index + num_steps]  #根据每个样本的起始索引,然后移动num_steps数得到这个由num_steps个词组成的一个样本

    num_batches = num_examples // batch_size
    for i in range(0, num_batches * batch_size, batch_size):
        # start_index_examples_pre_batch包含子序列的随机起始索引
        start_index_examples_pre_batch = start_index_examples[i:i + batch_size]  #每个batch_size中每个样本的起始索引
        X = [get_example_data(start_index_example) for start_index_example in start_index_examples_pre_batch]
        Y = [get_example_data(start_index_example + 1) for start_index_example in start_index_examples_pre_batch]
        yield torch.tensor(X), torch.tensor(Y)  #X为数据,Y为label,也即是Y中每个样本中的元素是X中对应样本中的元素下一个,也即是模型预测下一个元素是什么


my_seq = list(range(35))
for X, Y in seg_data_iter_random(my_seq, 2, 5):
    print('X: ', X, '\n Y: ', Y)
"""使用顺序分区生成一个小批量子序列"""


#每个批量样本数据是连续的
def seg_data_iter_sequential(corpus, batch_size, num_steps):
    #corpus表示将文本序列词转换成对应的id的集合
    # 从随机偏移量开始划分序列
    offset = random.randint(0, num_steps - 1)
    num_tokens = ((len(corpus) - offset - 1) // batch_size) * batch_size
    corpus_X = torch.tensor(corpus[offset:offset + num_tokens])
    corpus_Y = torch.tensor(corpus[offset + 1:offset + 1 + num_tokens])
    Xs, Ys = corpus_X.reshape(batch_size, -1), corpus_Y.reshape(batch_size, -1)
    num_batches = Xs.shape[1] // num_steps
    for i in range(0, num_batches * num_steps, num_steps):
        X = Xs[:, i:i + num_steps]
        Y = Ys[:, i:i + num_steps]
        yield X, Y


for X, Y in seg_data_iter_sequential(my_seq, 2, 5):
    print('X: ', X, '\n Y: ', Y)
"""加载序列数据的迭代器"""


class SeqDataLoader:
    def __init__(self, batch_size, num_steps, use_random_iter, max_tokens):
        if use_random_iter:
            self.data_iter_fn = d2l.torch.seq_data_iter_random
        else:
            self.data_iter_fn = d2l.torch.seq_data_iter_sequential
        self.corpus, self.vocab = d2l.torch.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)


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

!nvidia-smi
import random
import torch
import d2l.torch

#一元语法
lines = d2l.torch.read_time_machine()
tokens = d2l.torch.tokenize(lines, 'word')  #list of list类型
# 每个文本行不一定是一个句子或一个段落,因此需要把所有文本行拼接到一起
tokens = [token for line in tokens for token in line]  #list类型
vocab = d2l.torch.Vocab(tokens)
vocab.token_freqs[:10]
freqs = [freq for token, freq in vocab.token_freqs]
d2l.torch.plot(freqs, xlabel='token:x', ylabel='frequence:n(x)', xscale='log', yscale='log')
#二元语法
bigram_tokens = [pair for pair in zip(tokens[:-1], tokens[1:])]  #bigram_tokens里面每个元素是一个二元组,由前后相邻的词组成,list类型
#bigram_tokens里面每个元素是一个二元组
bigram_vocab = d2l.torch.Vocab(bigram_tokens)
bigram_token_freqs = bigram_vocab.token_freqs  #根据二元词组出现的次数得到排好序的二元词组,list类型,里面每个元素为一个二元组,格式为(二元词组,二元词组在bigram_tokens列表里面出现的次数)
bigram_token_freqs[:10]
#三元语法
trigram_tokens = [pair for pair in
                  zip(tokens[:-2], tokens[1:-1], tokens[2:])]  #trigram_tokens里面每个元素是一个三元组,由前后相邻以及中间的词组成,list类型
trigram_vocab = d2l.torch.Vocab(trigram_tokens)
trigram_token_freqs = trigram_vocab.token_freqs  #根据三元词组出现的次数得到排好序的三元词组,list类型,里面每个元素为一个三元组,格式为(三元词组,三元词组在trigram_tokens列表里面出现的次数)
trigram_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]
d2l.torch.plot([freqs, bigram_freqs, trigram_freqs], xlabel='token:x', ylabel='frequence:n(x)', xscale='log',
               yscale='log', legend=['unigram', 'bigram', 'trigram'])
"""使用随机抽样生成一个小批量子序列"""


#随机采样:将序列文本(这个序列文本刚开始会经过随机偏移来决定文本的开始索引)按固定长度切成一个个子序列作为一个样本,然后把这些样本随机打乱,然后根据batch_szie来得到一个批量大小,一个样本也即是一个子序列,由num_steps个词组成
def seg_data_iter_random(corpus, batch_size, num_steps):
    #corpus表示将文本序列词转换成对应的id的集合
    # 从随机偏移量开始对序列进行分区,随机范围包括num_steps-1
    corpus = corpus[random.randint(0, num_steps - 1):]
    # 减去1,是因为需要考虑标签,因为这里的标签,是移位了一个词元的原始序列。如果不减去这个1,就有可能使得原始序列中的最后一个元素被列进样本中而没有与之对应的标签(数组越界了)。
    num_examples = (len(corpus) - 1) // num_steps
    # 长度为num_steps的子序列的起始索引
    start_index_examples = list(range(0, num_examples * num_steps, num_steps))  # 每个样本的起始索引
    # 在随机抽样的迭代过程中,
    # 来自两个相邻的、随机的、小批量中的子序列不一定在原始序列上相邻
    random.shuffle(start_index_examples)

    def get_example_data(start_index):
        # 返回从pos位置开始的长度为num_steps的序列
        return corpus[start_index:start_index + num_steps]  #根据每个样本的起始索引,然后移动num_steps数得到这个由num_steps个词组成的一个样本

    num_batches = num_examples // batch_size
    for i in range(0, num_batches * batch_size, batch_size):
        # start_index_examples_pre_batch包含子序列的随机起始索引
        start_index_examples_pre_batch = start_index_examples[i:i + batch_size]  #每个batch_size中每个样本的起始索引
        X = [get_example_data(start_index_example) for start_index_example in start_index_examples_pre_batch]
        Y = [get_example_data(start_index_example + 1) for start_index_example in start_index_examples_pre_batch]
        yield torch.tensor(X), torch.tensor(Y)  #X为数据,Y为label,也即是Y中每个样本中的元素是X中对应样本中的元素下一个,也即是模型预测下一个元素是什么


my_seq = list(range(35))
for X, Y in seg_data_iter_random(my_seq, 2, 5):
    print('X: ', X, '\n Y: ', Y)
"""使用顺序分区生成一个小批量子序列"""


#每个批量样本数据是连续的
def seg_data_iter_sequential(corpus, batch_size, num_steps):
    #corpus表示将文本序列词转换成对应的id的集合
    # 从随机偏移量开始划分序列
    offset = random.randint(0, num_steps - 1)
    num_tokens = ((len(corpus) - offset - 1) // batch_size) * batch_size
    corpus_X = torch.tensor(corpus[offset:offset + num_tokens])
    corpus_Y = torch.tensor(corpus[offset + 1:offset + 1 + num_tokens])
    Xs, Ys = corpus_X.reshape(batch_size, -1), corpus_Y.reshape(batch_size, -1)
    num_batches = Xs.shape[1] // num_steps
    for i in range(0, num_batches * num_steps, num_steps):
        X = Xs[:, i:i + num_steps]
        Y = Ys[:, i:i + num_steps]
        yield X, Y


for X, Y in seg_data_iter_sequential(my_seq, 2, 5):
    print('X: ', X, '\n Y: ', Y)
"""加载序列数据的迭代器"""


class SeqDataLoader:
    def __init__(self, batch_size, num_steps, use_random_iter, max_tokens):
        if use_random_iter:
            self.data_iter_fn = d2l.torch.seq_data_iter_random
        else:
            self.data_iter_fn = d2l.torch.seq_data_iter_sequential
        self.corpus, self.vocab = d2l.torch.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)


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

10. 链接

循环神经网络RNN第一篇:李沐动手学深度学习V2-NLP序列模型和代码实现
循环神经网络RNN第二篇:李沐动手学深度学习V2-NLP文本预处理和代码实现
循环神经网络RNN第三篇:李沐动手学深度学习V2-NLP语言模型、数据集加载和数据迭代器实现以及代码实现
循环神经网络RNN第四篇:李沐动手学深度学习V2-RNN原理
循环神经网络RNN第五篇:李沐动手学深度学习V2-RNN循环神经网络从零实现
循环神经网络RNN第六篇:李沐动手学深度学习V2-使用Pytorch框架实现RNN循环神经网络
循环神经网络GRU第七篇:李沐动手学深度学习V2-GRU门控循环单元以及代码实现
循环神经网络LSTM第八篇:李沐动手学深度学习V2-LSTM长短期记忆网络以及代码实现
深度循环神经网络第九篇:李沐动手学深度学习V2-深度循环神经网络和代码实现
双向循环神经网络第十篇:李沐动手学深度学习V2-双向循环神经网络Bidirectional RNN和代码实现

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值