文章目录
- 语言模型与数据集
- 1. 学习语言模型
- 2. 马尔可夫模型与n元语法
- 3. 自然语言统计
- 4. 读取长序列数据
- 4.1 随机采样
- 4.2 顺序分区
语言模型与数据集
了解到如何将文本数据映射到词元,以及将这些词元视作一系列的离散观测,例如单词或字符。
给定一个文本序列的,xt表示文本序列在时间步t的观测或标签,语言模型的目标是估计序列的联合概率
1. 学习语言模型
如何对一个文档或者是一个词元序列进行建模?
假设在单词级别对文本数据进行词元化,从基本概率规则开始:
则包含四个单词的一个文本序列的概率为:
为了训练语言模型,需要计算单词的概率, 以及给定前面几个单词后出现某个单词的条件概率。
这些概率本质上就是语言模型的参数。
2. 马尔可夫模型与n元语法
马尔可夫模型是一种描述随机过程的数学模型,它基于马尔可夫性质,即未来的状态只依赖于当前的状态,而与过去的状态无关。
马尔可夫性质: 阶数越高,对应的依赖关系就越长。下面是序列建模的近似公式:
通常,涉及一个、两个和三个变量的概率公式分别被称为 一元语法、二元语法和三元语法模型。
3. 自然语言统计
根据时光机器数据集构建词表, 并打印前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]
最流行的词通常被称为停用词,画出词频图
# 最流行的词通常被称为停用词
# 词频图
freqs = [freq for token, freq in vocab.token_freqs]
d2l.plot(freqs, xlabel='token: x', ylabel='frequency: n(x)',
xscale='log', yscale='log')
上图表示词频以一种明确的方式迅速衰减,接下来看二元语法的频率
# 二元语法的频率
bigram_tokens = [pair for pair in zip(corpus[:-1], corpus[1:])]
bigram_vocab = d2l.Vocab(bigram_tokens)
bigram_vocab.token_freqs[:10]
三元语法的频率
# 三元语法
# corpus词元索引列表
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]
直观地对比三种模型中的词元频率:一元语法、二元语法和三元语法
# 直观地对比三种模型中的词元频率:一元语法、二元语法和三元语法。
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'])
4. 读取长序列数据
当序列变得太长而不能被模型一次性全部处理时, 我们可能希望拆分这样的序列方便模型读取。 任意长的序列可以被划分为具有相同时间步数的子序列。
4.1 随机采样
在随机采样中,每个样本都是在原始的长序列上任意捕获的子序列。 对于语言建模,目标是基于到目前为止我们看到的词元来预测下一个词元, 因此标签是移位了一个词元的原始序列。
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)
4.2 顺序分区
保证两个相邻的小批量中的子序列在原始序列上也是相邻的。 这种策略在基于小批量的迭代过程中保留了拆分的子序列的顺序,因此称为顺序分区。
"""
生成小批量子序列的函数,它使用顺序分区的方式将一个长序列划分为若干个长度相同的子序列:
- corpus:一个包含所有词元索引的列表
- batch_size:每个小批量中样本的数量
- num_steps:每个输入序列的时间步数
"""
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的形状从原来的一维数组调整为一个二维数组,
# 其中有batch_size行,而列数根据原数组的长度和batch_size的值自动计算得出
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)
将上面的两个采样函数包装到一个类中, 以便稍后可以将其用作数据迭代器
class SeqDataLoader: #@save
"""加载序列数据的迭代器"""
# 根据use_random_iter的值选择使用随机迭代器或顺序迭代器
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, 它同时返回数据迭代器和词表
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