引用翻译:《动手学深度学习》
语言符号(又称词)的数量很大,而且分布很不均匀。因此,预测下一个符号的简单多类分类方法并不总是很有效。此外,我们需要把文本变成我们可以优化的格式,即我们需要把它映射到向量。在其极端情况下,我们有两种选择。一种是将每个词作为一个独特的实体,例如Salton.Wong.Yang.1975。这种策略的问题是,对于非常大的、多样化的语料库,我们很可能要处理100,000到1,000,000个向量。
另一个极端是每次预测一个字符的策略,如Ling等人,2015年提出的。两种策略之间的一个很好的平衡点是字节对编码,如Sennrich、Haddow和Birch, 2015年为神经机器翻译的目的所描述的。它将文本分解为经常出现的类似音节的片段。这使得模型能够根据先前查看的单词,如异质、同质、图和五边形,生成异质或五边形等单词。探讨这些模型的细节已经超出了本章的范围。我们将在以后讨论自然语言处理(chapter_nlp)时更详细地讨论这个问题。我只想说,它可以大大促进自然语言处理模型的准确性。
为了简单起见,我们将把自己限制在纯字符序列上。我们像以前一样使用H.G. Wells的The Timemachine。我们首先对文本进行过滤,并将其转换为一个字符ID的序列。
和以前一样,我们开始加载数据,并将其映射为一连串的空白处、标点符号和常规字符。预处理是最小的,我们只限于去除多个空白。
import sys
sys.path.insert(0, '..')
import torch
import random
import collections
with open('../data/timemachine.txt', 'r') as f:
raw_text = f.read()
print(raw_text[0:210]) # raw_text存储的是文本,未经过任何处理
The Time Machine, by H. G. Wells [1898]
The Time Traveller (for so it will be convenient to speak of him)
was expounding a recondite matter to us. His grey eyes shone and
twinkled, and his usually pale
接下来,我们需要将数据集,即一个字符串,分割成标记。一个标记是模型要训练和预测的一个数据点。我们通常使用一个词或一个字符作为一个标记。
lines = raw_text.split('\n')
text = ' '.join(' '.join(lines).lower().split()) # 全转化为小写,且将各句用空格连接起来
print('# of chars:', len(text))
print(text[0:70])
# of chars: 178605
the time machine, by h. g. wells [1898] i the time traveller (for so i
然后,我们需要将令牌映射成数字索引。我们通常称它为词汇表。它的输入是一个标记的列表,称为语料库。
然后,它计算每个标记在这个语料库中的频率,然后根据其频率给每个标记分配一个数字索引。很少出现的标记经常被删除以减少复杂性。
一个在语料库中不存在或已被删除的标记被映射为一个特殊的未知(“< unk>”)标记。我们还可以选择添加另外三个特殊标记。"< pad>“是一个用于填充的标记,”< bos>“表示一个句子的开始,”< eos>"表示一个句子的结束。
class Vocab(object):
def __init__(self, tokens, min_freq=0, use_special_tokens=False):
# 通过frequency and token排序
counter = collections.Counter(tokens) # 对词进行统计计数
token_freqs = sorted(counter.items(), key=lambda x: x[0]) # 根据统计的结果进行排序,全量的排序结果
token_freqs.sort(key=lambda x: x[1], reverse=True) # 根据token名称进行排序,相当于优先以频率排,再以名称排
if use_special_tokens:
# 填充,句首,句尾,未知
self.pad, self.bos, self.eos, self.unk = (0, 1, 2, 3)
tokens = ['' , '' , '' , '' ] # 对四种类型标记建立标签映射
else:
self.unk = 0
tokens = ['' ]
tokens += [token for token, freq in token_freqs if freq >= min_freq] # 对于频率较低的token进行过滤
self.idx_to_token = []
self.token_to_idx = dict() # 建立token和id的映射词典,即vocab.txt
for token in tokens:
self.idx_to_token.append(token) # 记录token词
self.token_to_idx[token] = len(self.idx_to_token) - 1 # 按顺序从0开始编号
def __len__(self):
return len(self.idx_to_token)
def __getitem__(self, tokens):
if not isinstance(tokens, (list, tuple)):
return self.token_to_idx.get(tokens, self.unk) # 未出现词添加unk标签
else:
return [self.__getitem__(token) for token in tokens] # 出现过的就不管
def to_tokens(self, indices):
if not isinstance(indices, (list, tuple)):
return self.idx_to_token[indices]
else:
return [self.idx_to_token[index] for index in indices]
我们以时间机器数据集为语料库构建了一个词汇表,然后打印标记与索引之间的映射。
vocab = Vocab(text)
print(vocab.token_to_idx)
{'': 0, ' ': 1, 'e': 2, 't': 3, 'a': 4, 'i': 5, 'n': 6, 'o': 7, 's': 8, 'h': 9, 'r': 10, 'd': 11, 'l': 12, 'm': 13, 'u': 14, 'c': 15, 'f': 16, 'w': 17, 'g': 18, 'y': 19, 'p': 20, ',': 21, 'b': 22, '.': 23, 'v': 24, 'k': 25, "'": 26, '-': 27, 'x': 28, 'z': 29, ';': 30, 'j': 31, '?': 32, 'q': 33, '!': 34, '"': 35, '_': 36, ':': 37, '(': 38, ')': 39, '8': 40, '[': 41, ']': 42, '1': 43, '9': 44}
之后,训练数据集中的每个字符都被转换为一个索引ID。为了说明问题,我们打印前20个字符和它们相应的索引。
corpus_indices = [vocab[char] for char in text] # 将字符转化为index编码
sample = corpus_indices[:15]
print('chars:', [vocab.idx_to_token[idx] for idx in sample]) # 将idx转化为字符
print('indices:', sample)
chars: ['t', 'h', 'e', ' ', 't', 'i', 'm', 'e', ' ', 'm', 'a', 'c', 'h', 'i', 'n']
indices: [3, 9, 2, 1, 3, 5, 13, 2, 1, 13, 4, 15, 9, 5, 6]
在训练过程中,我们需要随机读取小型批次的例子和标签。由于序列数据在本质上是有顺序的,我们需要解决处理它的问题。当我们在chapter_sequence中介绍时,我们是以一种相当临时的方式进行的。让我们把这个问题正式化一下。考虑一下我们刚刚处理的这本书的开头。如果我们想把它分割成每个5个符号的序列,我们有相当大的自由,因为我们可以选择一个任意的偏移量。
图:拆分文本时,不同的偏移量会导致不同的子序列。
事实上,这些偏移量中的任何一个都是可以的。因此,我们应该选择哪一个呢?
事实上,所有的偏移量都是一样好的。但是如果我们挑选所有的偏移量,由于重叠,我们最终会得到相当多余的数据,特别是如果序列很长的话。
仅仅选取一组随机的初始位置也不好,因为它不能保证阵列的均匀覆盖。
例如,如果我们从一组中随机抽取元素,并进行随机替换,那么某个特定元素未被抽取的概率是(1-1/)→-1。这意味着我们不能指望通过这种方式实现均匀覆盖。
即使是随机排列一组所有的偏移量也不能提供良好的保证。相反,我们可以使用一个简单的技巧来获得覆盖率和随机性:使用一个随机的偏移量,之后再按顺序使用这些术语。我们将在下面描述如何完成随机抽样和顺序划分策略的工作。
下面的代码每次都会从数据中随机生成一个mini batch。
这里,批次大小batch_size表示到每个小批次中的例子数量,num_steps是每个例子中包含的序列(或时间步骤,如果我们有一个时间序列)的长度。
在随机抽样中,每个例子都是在原始序列上任意抓取的一个序列。
原始序列上两个相邻的随机小批的位置不一定相邻。
目标是根据我们目前所看到的预测下一个字符,因此标签是原始序列,移位了一个字符。
请注意,对于潜伏变量模型来说,不建议这样做,因为在看到序列之前,我们无法接触到隐藏状态。
load_data_time_machine函数它返回四个变量:Corpus_indices,char_to_idx,idx_to_char,和vocab_size。
def data_iter_random(corpus_indices, batch_size, num_steps, ctx=None):
# 统一开始的数据的迭代器的偏移量
offset = int(random.uniform(0,num_steps)) # 在0,num_steps的范围内
print(offset)
corpus_indices = corpus_indices[offset:] # 从第offset截取
print(corpus_indices)
# 多减1,因为我们需要考虑到序列的长度。
num_examples = ((len(corpus_indices) - 1) // num_steps) - 1 # 计算能够划分为多少个examples
print(num_examples)
# 丢弃半空的批次,只保留有完整batch的部分
num_batches = num_examples // batch_size
print('num_examples * num_steps:',num_examples * num_steps)
example_indices = list(range(0, num_examples * num_steps, num_steps))
print('example_indices:',example_indices)
random.shuffle(example_indices)
# 这将返回一个长度为num_steps的序列,从pos开始。如果是文本,那就是返回的字符串
def _data(pos):
return corpus_indices[pos: pos + num_steps]
for i in range(0, batch_size * num_batches, batch_size):
# Batch_size表示每次读取的随机例子。
batch_indices = example_indices[i:(i+batch_size)]
X = [_data(j) for j in batch_indices]
Y = [_data(j + 1) for j in batch_indices]
yield torch.Tensor(X, device=ctx), torch.Tensor(Y, device=ctx)
让我们生成一个从0到30的人工序列。我们假设批量大小和时间步骤的数量分别为2和5。
这意味着,根据偏移量,我们可以生成4到5个(,)对。在mini_batch大小为2的情况下,我们只能得到2个mini_batch。
Y和X是对应关系,Y是目标值,X是输入值,比如输入X的第一个时间元素,需要预测下一个时间元素(即原始序列中X的第二个时间元素),将下一个时间元素作为目标值Y,以此建立预测模型
my_seq = list(range(30)) # 假设这是初始的序列,用于观察如何抽样的
print(my_seq)
for X, Y in data_iter_random(my_seq, batch_size=1, num_steps=5): # 时间跨度为5,若batch=1,则能生成5个(x,y)对,
print('X: ', X, '\nY:', Y)
print('\n')
[0, 1, 2, 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]
6
[6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29]
3
num_examples * num_steps: 15
example_indices: [0, 5, 10]
X: tensor([[ 6., 7., 8., 9., 10.]])
Y: tensor([[ 7., 8., 9., 10., 11.]])
X: tensor([[11., 12., 13., 14., 15.]])
Y: tensor([[12., 13., 14., 15., 16.]])
X: tensor([[16., 17., 18., 19., 20.]])
Y: tensor([[17., 18., 19., 20., 21.]])
除了对原始序列进行随机抽样外,我们还可以使两个相邻的随机小批在原始序列中的位置相邻。
现在,我们可以用一个小批的最后一个时间步骤的隐藏状态来初始化下一个小批的隐藏状态,这样,下一个小批的输出也取决于小批的输入,
这种模式在随后的小批中继续。这对递归神经网络的实施有两个影响。一方面,在训练模型时,我们只需要在每个周期的开始时初始化隐藏状态。
另一方面,当多个相邻的小批通过传递隐藏状态来串联时,模型参数的梯度计算将取决于所有被串联的小批序列。
在同一个历时中,随着迭代次数的增加,梯度计算的成本也会上升。
为了使模型参数的梯度计算只依赖于一个迭代所读取的小批序列,我们可以在读取小批序列之前将隐藏状态从计算图中分离出来(这可以通过分离图来完成)。我们将在下面的章节中更深入地了解这种方法。
def data_iter_consecutive(corpus_indices, batch_size, num_steps, ctx=None):
# 统一开始的数据的迭代器的偏移量
offset = int(random.uniform(0,num_steps))
# 分割数据 - 忽略num_steps,直接绕过
num_indices = ((len(corpus_indices) - offset) // batch_size) * batch_size
indices = torch.Tensor(corpus_indices[offset:(offset + num_indices)], device=ctx)
indices = indices.reshape((batch_size,-1))
# 需要留下最后一个代币,因为目标被转移了1。
num_epochs = ((num_indices // batch_size) - 1) // num_steps
for i in range(0, num_epochs * num_steps, num_steps):
X = indices[:,i:(i+num_steps)]
Y = indices[:,(i+1):(i+1+num_steps)]
yield X, Y
使用相同的设置,为每个通过随机抽样读取的迷你批次的例子打印输入X和标签Y。原始序列上两个相邻的随机小批的位置是相邻的。
for X, Y in data_iter_consecutive(my_seq, batch_size=2, num_steps=4):
print('X: ', X, '\nY:', Y)
X: tensor([[ 3., 4., 5., 6.],
[16., 17., 18., 19.]])
Y: tensor([[ 4., 5., 6., 7.],
[17., 18., 19., 20.]])
X: tensor([[ 7., 8., 9., 10.],
[20., 21., 22., 23.]])
Y: tensor([[ 8., 9., 10., 11.],
[21., 22., 23., 24.]])
X: tensor([[11., 12., 13., 14.],
[24., 25., 26., 27.]])
Y: tensor([[12., 13., 14., 15.],
[25., 26., 27., 28.]])
顺序分割将序列分解成batch_size的许多数据条,当我们在mini batch上迭代时,这些数据条被遍历。请注意,一个mini batch中的元素与下一个迷你批中的元素相匹配,而不是在一个mini batch中。
1、文档的预处理是通过对单词进行标记并将其映射为ID。有多种方法。
使用单个字符的字符编码(例如对中文来说是好的)。
词的编码(例如对英语来说很好)
字节对编码(适用于有大量语态的语言,如德语)。
2、序列划分的主要选择是我们是选择连续序列还是随机序列。特别是对于递归网络,前者至关重要。
3、考虑到整个文档的长度,通常可以接受对文档稍加浪费,并丢弃半空的小批。
1、你还能想到哪些其他的小批量数据采样方法?
2、为什么有一个随机偏移量是个好主意?
3、如果我们希望一个序列的例子是一个完整的句子,这在小批量抽样中会带来什么样的问题?为什么我们要这样做呢?