一、语言模型是做什么的?
给定一个文本序列,x1,x2,...,xt,语言模型的目标是估计联合概率p(x1,x2,...,xt)。
理解:例如,短语“to recognize speech”和“to wreck a nice beach”读音上听起来非常相似。 这种相似性会导致语音识别中的歧义,但是这很容易通过语言模型来解决, 因为第二句的语义很奇怪。 同样,在文档摘要生成算法中, “狗咬人”比“人咬狗”出现的频率要高得多。
二、思路实现
模型的输入是有限的,当序列变得太长而不能被模型一次性全部处理时, 我们可能希望拆分这样的序列方便模型读取,接下来说明应该如何提取特定时间步数的批量序列作为数据集训练语言模型。
假设网络一次只处理具有n个时间步的子序列。当我们需要从一个很长的文本中提取特定长度的子序列时,往往需要先随机生成一个初始偏移量,只要将这个偏移量的随机生成范围取值在[0, n),那么我们就可能从给定长文本中提取得到所有组合的子序列。如下图所示,分割文本时,不同的偏移量会导致不同的子序列。
那么我们应该从上图中选择哪一个呢? 事实上,他们都一样的好。 然而,如果我们只选择一个偏移量, 那么用于训练网络的、所有可能的子序列的覆盖范围将是有限的。 因此,我们可以从随机偏移量开始划分序列, 以同时获得覆盖性(coverage)和随机性(randomness)。 下面,我们将描述如何实现随机采样(random sampling)和 顺序分区(sequential partitioning)策略。
在随机采样中,每个样本都是在原始的长序列上任意捕获的子序列。 在迭代过程中,来自两个相邻的、随机的、小批量中的子序列不一定在原始序列上相邻。 对于语言建模,目标是基于到目前为止我们看到的词元来预测下一个词元, 因此标签是移位了一个词元的原始序列。
在迭代过程中,除了对原始序列可以随机抽样外, 我们还可以保证两个相邻的小批量中的子序列在原始序列上也是相邻的。 这种策略在基于小批量的迭代过程中保留了拆分的子序列的顺序,因此称为顺序分区。
三、代码实现
在编写程序前,先注明一些概念的定义:
corpus:给定文本中各个token在词典vocab中对应的索引。
num_steps:每个子序列包含的token数目
batch_size:每个批量包含的子序列数目
seqDataLoader.py:
导入依赖包:
import preprocessing as pre
import random
import torch
seq_data_iter_random函数:使用随机抽样生成小批量子序列。
def seq_data_iter_random(corpus, batch_size, num_steps):
# 在(0, num_steps - 1)区间随机生成一个初始偏移量
corpus = corpus[random.randint(0, num_steps - 1):]
# 计算corpus能够被分割成多少个长度为num_steps的子序列
num_subseqs = (len(corpus) - 1) // num_steps
# initial_indices用于储存子序列初始元素在corpus中的index
initial_indices = list(range(0, num_subseqs * num_steps, num_steps))
# 打乱次序,如:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] ——> [2, 5, 4, 8, 0, 3, 7, 9, 1, 6]
random.shuffle(initial_indices)
# 该函数用于返回从pos位置开始的长度为num_steps的序列
def data(pos):
return corpus[pos: pos + num_steps]
# 根据被划分出来的子序列的数量看看能够组成多少个batch
num_batches = num_subseqs // batch_size
# 通过循环依次将batch返回,一个batch包含batch_size个子序列
for i in range(0, batch_size * num_batches, batch_size):
# 获取batch中每个子序列初始元素在corpus中的index
initial_indices_per_batch = initial_indices[i: i + batch_size]
X = [data(j) for j in initial_indices_per_batch]
# 我想这里解释了为何前面计算num_subseqs时需要减1,如果初始偏移量是0,减1操作便能够为Y预留一个token
Y = [data(j + 1) for j in initial_indices_per_batch]
yield torch.tensor(X), torch.tensor(Y) # tensor的尺寸:batch_size * 1
seq_data_iter_sequential函数:使用顺序分区生成一个小批量子序列
def seq_data_iter_sequential(corpus, batch_size, num_steps):
# 在(0, num_steps - 1)区间随机生成一个初始偏移量
corpus = corpus[random.randint(0, num_steps - 1):]
# 计算去掉多余的token后,剩余token的数量
# 后续会用reshape方法将剩余的token整理成batch_size行的张量,所以此处是对batch_size求余
num_tokens = ((len(corpus) - 1) // batch_size) * batch_size
# num_tokens = ((len(corpus) - 1) // num_steps) * num_steps
# Xs:训练时带输入的序列;Ys:预测真值序列
Xs = torch.tensor(corpus[0: num_tokens])
Ys = torch.tensor(corpus[1: num_tokens + 1])
Xs, Ys = Xs.reshape(batch_size, -1), Ys.reshape(batch_size, -1)
# 计算num_tokens可被划分成多少个batch,(num_steps * batch_size):一个batch包含的token数量
num_batches = num_tokens // (num_steps * batch_size)
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
将上面的两个采样函数包装到一个类中:
class SeqDataLoader:
def __init__(self, batch_size, num_steps, path, use_random_iter, max_tokens):
if use_random_iter:
self.data_iter_fn = seq_data_iter_random
else:
self.data_iter_fn = seq_data_iter_sequential
# 文本预处理
self.corpus, self.vocab = pre.preprocessing(path, max_tokens)
self.batch_size, self.num_steps = batch_size, num_steps
# iter方法可以方便后续通过遍历该类的实例来获取子序列
def __iter__(self):
return self.data_iter_fn(self.corpus, self.batch_size, self.num_steps)
最后定义一个接口函数,其它模块可以通过调用该函数实现该模块已实现的功能:
def loadData(batch_size, num_steps, path, use_random_iter=False, max_tokens=-1):
data_iter = SeqDataLoader_txt(batch_size, num_steps, path, use_random_iter, max_tokens)
return data_iter, data_iter.vocab
参考链接:
《动手学深度学习》 — 动手学深度学习 2.0.0 documentationhttps://zh-v2.d2l.ai/