文章目录
1、读取数据集
import tensorflow as tf
import random
import zipfile
with zipfile.ZipFile('../../data/jaychou_lyrics.txt.zip') as zin:
with zin.open('jaychou_lyrics.txt') as f:
corpus_chars = f.read().decode('utf-8')
corpus_chars[:40]
'想要有直升机\n想要和你飞到宇宙去\n想要和你融化在一起\n融化在宇宙里\n我每天每天每'
这个数据集有6万多个字符。为了打印方便,我们把换行符替换成空格,然后仅使用前1万个字符来训练模型。
corpus_chars = corpus_chars.replace('\n', ' ').replace('\r', ' ')
corpus_chars = corpus_chars[0:10000]
2、建立字符索引
我们将每个字符映射成一个从0开始的连续整数,又称索引,来方便之后的数据处理。为了得到索引,我们将数据集里所有不同字符取出来,然后将其逐一映射到索引来构造词典。接着,打印 vocab_size,即词典中不同字符的个数,又称词典大小。
2.1 删除数据集中重复的字符
使用set()函数将数据集中重复的字符删掉,然后放入列表中。
idx_to_char = list(set(corpus_chars))
len(idx_to_char)
1027
所以在数据集中由1027个不重复的字符。
2.2 将字符映射到索引
char_to_idx = dict([(char, i) for i, char in enumerate(idx_to_char)])
得到的字典 char_to_idx 的键为字符,值为索引值,如 {‘容’: 0, ‘午’: 1, ‘蜘’: 2, ‘蓝’: 3, ‘美’: 4, ‘印’: 5 ……}。
2.3 得到词典大小
vocab_size = len(char_to_idx)
vocab_size
1027
2.4 将字符转化成索引
corpus_indices = [char_to_idx[char] for char in corpus_chars]
len(corpus_indices)
10000
corpus_indices 中是 原数据集corpus_chars 中所有字符的索引值。
我们可以打印前20个字符机器对应的索引:
sample = corpus_indices[:20]
print('chars:', ''.join([idx_to_char[idx] for idx in sample]))
print('indices:', sample)
chars: 想要有直升机 想要和你飞到宇宙去 想要和
indices: [986, 669, 467, 984, 473, 554, 491, 986, 669, 881, 83, 420, 690, 750, 306, 55, 491, 986, 669, 881]
3、时序数据的采样
在训练中我们需要每次随机读取小批量样本和标签。与其他实验数据不同的是,时序数据的一个样本通常包含连续的字符。假设时间步数为5,样本序列为5个字符,即“想”“要”“有”“直”“升”。该样本的标签序列为这些字符分别在训练集中的下一个字符,即“要”“有”“直”“升”“机”。我们有两种方式对时序数据进行采样,分别是随机采样和相邻采样。
3.1 随机采样
下面的代码每次从数据里随机采样一个小批量。其中批量大小 batch_size 指每个小批量的样本数,num_steps 为每个样本所包含的时间步数。 在随机采样中,每个样本是原始序列上任意截取的一段序列。相邻的两个随机小批量在原始序列上的位置不一定相邻。因此,我们无法用一个小批量最终时间步的隐藏状态来初始化下一个小批量的隐藏状态。在训练模型时,每次随机采样前都需要重新初始化隐藏状态。
def data_iter_random(corpus_indices, batch_size, num_steps, ctx=None):
num_examples = (len(corpus_indices) - 1) // num_steps
epoch_size = num_examples // batch_size
example_indices = list(range(num_examples))
random.shuffle(example_indices)
def _data(pos):
return corpus_indices[pos: pos + num_steps]
for i in range(epoch_size):
# 每次读取batch_size个随机样本
i = i * batch_size
batch_indices = example_indices[i: i + batch_size]
X = [_data(j * num_steps) for j in batch_indices]
Y = [_data(j * num_steps + 1) for j in batch_indices]
yield np.array(X, ctx), np.array(Y, ctx)
让我们输入一个从0到29的连续整数的人工序列。设批量大小和时间步数分别为2和6。打印随机采样每次读取的小批量样本的输入 X 和标签 Y。可见,相邻的两个随机小批量在原始序列上的位置不一定相毗邻。
my_seq = list(range(30))
for X, Y in data_iter_random(my_seq, batch_size=2, num_steps=6):
print('X: ', X, '\nY:', Y, '\n')
X: tensor([[18., 19., 20., 21., 22., 23.],
[12., 13., 14., 15., 16., 17.]])
Y: tensor([[19., 20., 21., 22., 23., 24.],
[13., 14., 15., 16., 17., 18.]])
X: tensor([[ 0., 1., 2., 3., 4., 5.],
[ 6., 7., 8., 9., 10., 11.]])
Y: tensor([[ 1., 2., 3., 4., 5., 6.],
[ 7., 8., 9., 10., 11., 12.]])
代码解释
用上面的 my_seq,batch_size=2 和 num_steps=6 对代码进行解释。
num_examples = (len(corpus_indices) - 1) // num_steps
num_examples 是指有多少个样本,此时 num_examples=29 // 6=4。
减1是因为输出的索引(标签)是相应输入的索引(样本)加1。也就是说,如果不减1的话,一旦 X 取到 [24 25 26 27 28 29],Y 就只能取 [25 26 27 28 29],因为 corpus_indices (即 my_seq)就只到29。
epoch_size = num_examples // batch_size
epoch_size 是指遍历一遍数据集需要训练的次数,因为训练一次输入的样本数为 batch_size=2,那么遍历一次需要的次数为 epoch_size=2。
example_indices = list(range(num_examples))
example_indices 是一个包含着四个样本各自索引的列表,即 example_indices=[0, 1, 2, 3]。
random.shuffle(example_indices)
打乱 example_indices 列表的顺序。
def _data(pos):
return corpus_indices[pos: pos + num_steps]
这个函数返回的是从 pos 开始的长为 num_steps 的序列。
for i in range(epoch_size):
# 每次读取batch_size个随机样本
i = i * batch_size
batch_indices = example_indices[i: i + batch_size]
X = [_data(j * num_steps) for j in batch_indices]
Y = [_data(j * num_steps + 1) for j in batch_indices]
yield np.array(X), np.array(Y)
epoch_size 是2,所以总共循环两次,每次循环的具体操作:
- 读取 batch_size=2 个随机样本,比如读取到 batch_indices=[0, 1] ;
- 利用 _data(pos) 函数,从 my_seq 中提取样本 X 和对应的标签 Y。
yield 是指将 np.array(X), np.array(Y) 迭代输出。也就是说,每次只输出一个批次的样本和标签。
3.2 相邻采样
除对原始序列做随机采样之外,我们还可以令相邻的两个随机小批量在原始序列上的位置相邻。
这时候,我们就可以用一个小批量最终时间步的隐藏状态来初始化下一个小批量的隐藏状态,从而使下一个小批量的输出也取决于当前小批量的输入,并如此循环下去。
这对实现循环神经网络造成了两方面影响:
- 一方面, 在训练模型时,我们只需在每一个迭代周期开始时初始化隐藏状态;
- 另一方面,当多个相邻小批量通过传递隐藏状态串联起来时,模型参数的梯度计算将依赖所有串联起来的小批量序列。
同一迭代周期中,随着迭代次数的增加,梯度的计算开销会越来越大。 为了使模型参数的梯度计算只依赖一次迭代读取的小批量序列,我们可以在每次读取小批量前将隐藏状态从计算图中分离出来。
def data_iter_consecutive(corpus_indices, batch_size, num_steps, ctx=None):
corpus_indices = np.array(corpus_indices)
data_len = len(corpus_indices)
batch_len = data_len // batch_size
indices = corpus_indices[0: batch_size*batch_len].reshape((
batch_size, batch_len))
epoch_size = (batch_len - 1) // num_steps
for i in range(epoch_size):
i = i * num_steps
X = indices[:, i: i + num_steps]
Y = indices[:, i + 1: i + num_steps + 1]
yield X, Y
同样的设置下,打印相邻采样每次读取的小批量样本的输入 X 和标签 Y。相邻的两个随机小批量在原始序列上的位置相邻。
my_seq = list(range(30))
for X, Y in data_iter_random(my_seq, batch_size=2, num_steps=6):
print('X: ', X, '\nY:', Y, '\n')
X: tensor([[ 0., 1., 2., 3., 4., 5.],
[15., 16., 17., 18., 19., 20.]])
Y: tensor([[ 1., 2., 3., 4., 5., 6.],
[16., 17., 18., 19., 20., 21.]])
X: tensor([[ 6., 7., 8., 9., 10., 11.],
[21., 22., 23., 24., 25., 26.]])
Y: tensor([[ 7., 8., 9., 10., 11., 12.],
[22., 23., 24., 25., 26., 27.]])
代码解释
同样用上面的 my_seq,batch_size=2 和 num_steps=6 对代码进行解释。
corpus_indices = np.array(corpus_indices)
将列表转换成数组,方便之后的 reshape 操作。
data_len = len(corpus_indices)
data_len=30 表明样本一共有30个。
batch_len = data_len // batch_size
batch_len=15 表明一个批次中有15个字符。
indices = corpus_indices[0: batch_size*batch_len].reshape((batch_size, batch_len))
得到的 indices 为:
array([[ 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]])
即将数据集分成了两个数组。
epoch_size = (batch_len - 1) // num_steps
epoch_size 是指遍历一遍数据集需要训练的次数,因为训练一次输入的样本数为 batch_size=2,那么遍历一次需要的次数为 epoch_size=2。
for i in range(epoch_size):
i = i * num_steps
X = indices[:, i: i + num_steps]
Y = indices[:, i + 1: i + num_steps + 1]
yield X, Y
进一步将每个数组的前6个字符放到位于同一批次的两个样本中;后6个字符放到位于另一批次的两个样本中。即:
第一个批次中的样本:
[[ 0, 1, 2, 3, 4, 5],
[15, 16, 17, 18, 19, 20]]
第二个批次中的样本:
[[ 6, 7, 8, 9, 10, 11],
[21, 22, 23, 24, 25, 26]]