1.自然语言统计
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]
corpus
['the',
'time',
'machine',
'by',
'h',
'g',
'wells',
'i',
'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',
...]
vocab = d2l.Vocab(corpus)
vocab.token_freqs[:10]
#最流行的词看起来很无聊, 这些词通常被称为停用词(stop words),因此可以被过滤掉。
[('the', 2261),
('i', 1267),
('and', 1245),
('of', 1155),
('a', 816),
('to', 695),
('was', 552),
('in', 541),
('that', 443),
('my', 440)]
#此外,还有个明显的问题是词频衰减的速度相当地快。
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_tokens
[('the', 'time'),
('time', 'machine'),
('machine', 'by'),
('by', 'h'),
('h', 'g'),
('g', 'wells'),
('wells', 'i'),
('i', 'the'),
('the', 'time'),
('time', 'traveller'),
('traveller', 'for'),
('for', 'so'),
('so', 'it'),
('it', 'will'),
('will', 'be'),
('be', 'convenient'),
('convenient', 'to'),
('to', 'speak'),
('speak', 'of'),
('of', 'him'),
('him', 'was'),
('was', 'expounding'),
('expounding', 'a'),
('a', 'recondite'),
('recondite', 'matter'),
('matter', 'to'),
('to', 'us'),
('us', 'his'),
('his', 'grey'),
('grey', 'eyes'),
('eyes', 'shone'),
('shone', 'and'),
('and', 'twinkled'),
('twinkled', 'and'),
('and', 'his'),
('his', 'usually'),
('usually', 'pale'),
('pale', 'face'),
('face', 'was'),
('was', 'flushed'),
('flushed', 'and'),
('and', 'animated'),
('animated', 'the'),
('the', 'fire'),
('fire', 'burned'),
('burned', 'brightly'),
('brightly', 'and'),
('and', 'the'),
('the', 'soft'),
('soft', 'radiance'),
('radiance', 'of'),
('of', 'the'),
...]
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)]
#三元法
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'])
- 单词序列似乎也遵循齐普夫定律
- 词表中元组的数量并没有那么大,这说明语言中存在相当多的结构, 这些结构给了我们应用模型的希望。
- 很多元组很少出现,这使得拉普拉斯平滑非常不适合语言建模。
2.读取长序列数据
- 分割文本时,不同的偏移量会导致不同的子序列
2.1 随机采样
def seq_data_iter_random(corpus,batch_size,num_steps):
"""使用随机抽样生成一个小批量子序列"""
# 从随机偏移量开始对序列进行分区,随机范围包括num_steps-1
corpus = corpus[random.randint(0,num_steps-1):]#随机一个int数 在【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))#去掉最后不足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)
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([[21, 22, 23, 24, 25],
[16, 17, 18, 19, 20]])
Y: tensor([[22, 23, 24, 25, 26],
[17, 18, 19, 20, 21]])
X: tensor([[ 6, 7, 8, 9, 10],
[26, 27, 28, 29, 30]])
Y: tensor([[ 7, 8, 9, 10, 11],
[27, 28, 29, 30, 31]])
X: tensor([[11, 12, 13, 14, 15],
[ 1, 2, 3, 4, 5]])
Y: tensor([[12, 13, 14, 15, 16],
[ 2, 3, 4, 5, 6]])
2.2 顺序分区
- 在迭代过程中,除了对原始序列可以随机抽样外, 我们还可以保证两个相邻的小批量中的子序列在原始序列上也是相邻的。 这种策略在基于小批量的迭代过程中保留了拆分的子序列的顺序,因此称为顺序分区。
def seq_data_iter_sequential(corpus,batch_size,num_steps):
"""使用顺序分区生成一个小批量子序列"""
# 从随机偏移量开始划分序列
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
my_seq = list(range(35))
batch_size=2
num_steps=5
offset = random.randint(0, num_steps)
offset
3
num_tokens = ((len(my_seq) - offset - 1) // batch_size) * batch_size
num_tokens# (35-3-1)//2=15 *2
30
Xs = torch.tensor(my_seq[offset: offset + num_tokens])
Ys = torch.tensor(my_seq[offset + 1: offset + 1 + num_tokens])
Xs,Ys
(tensor([ 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, 30, 31, 32]),
tensor([ 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, 30, 31, 32, 33]))
Xs, Ys = Xs.reshape(batch_size, -1), Ys.reshape(batch_size, -1)
Xs, Ys
(tensor([[ 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, 30, 31, 32]]),
tensor([[ 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, 30, 31, 32, 33]]))
num_batches = Xs.shape[1] // num_steps #15//5
num_batches
3
for X, Y in seq_data_iter_sequential(my_seq, batch_size=2, num_steps=5):
print('X: ', X, '\nY:', 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]])
#我们将上面的两个采样函数包装到一个类中, 以便稍后可以将其用作数据迭代器。
class SeqDataLoader:
"""加载序列数据的迭代器"""
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)
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
batch_size,num_steps = 2,10
a,b = load_data_time_machine(batch_size,num_steps)
for i in a:
print(i)
break
(tensor([[ 2, 1, 13, 4, 15, 9, 5, 6, 2, 1],
[ 2, 1, 17, 4, 8, 1, 4, 12, 7, 6]]), tensor([[ 1, 13, 4, 15, 9, 5, 6, 2, 1, 21],
[ 1, 17, 4, 8, 1, 4, 12, 7, 6, 18]]))
b.token_freqs
[(' ', 29927),
('e', 17838),
('t', 13515),
('a', 11704),
('i', 10138),
('n', 9917),
('o', 9758),
('s', 8486),
('h', 8257),
('r', 7674),
('d', 6337),
('l', 6146),
('m', 4043),
('u', 3805),
('c', 3424),
('f', 3354),
('w', 3225),
('g', 3075),
('y', 2679),
('p', 2427),
('b', 1897),
('v', 1295),
('k', 1087),
('x', 236),
('z', 144),
('j', 97),
('q', 95)]
小结
-
语言模型是自然语言处理的关键。
-
n元语法通过截断相关性,为处理长序列提供了一种实用的模型。
-
长序列存在一个问题:它们很少出现或者从不出现。
-
齐普夫定律支配着单词的分布,这个分布不仅适用于一元语法,还适用于其他元语法。
-
通过拉普拉斯平滑法可以有效地处理结构丰富而频率不足的低频词词组。
-
读取长序列的主要方式是随机采样和顺序分区。在迭代过程中,后者可以保证来自两个相邻的小批量中的子序列在原始序列上也是相邻的。