动手学深度学习 6.3 语言模型数据集(周杰伦专辑歌词)

前言

从零开始学习ai文章系列计划是个人在《动手学深度学习》和《磨菇书》两本书的学习中的个人笔记,文章也会以课本中的章节分开,即每个章节一片笔记。我会尽量的把主要内容以及遇到的难点进行记录与解决,如果哪里有错误的欢迎指正。或者不清晰的可以直接查看原文部分。

《动手学深度学习》原文(课本):https://tangshusen.me/Dive-into-DL-PyTorch/#/

《动手学深度学习》代码:https://github.com/ShusenTang/Dive-into-DL-PyTorch

(由于有时候公式太多,可能会直接贴图片)


本节将介绍如何预处理一个语言模型数据集,并将其转换成字符级循环神经网络所需要的输入格式。

为此,我们收集了周杰伦从第一张专辑《Jay》到第十张专辑《跨时代》中的歌词,并在后面几节里应用循环神经网络来训练一个语言模型。当模型训练好后,我们就可以用这个模型来创作歌词。

1. 读取数据集

1
2
3
4
5
6
7
8
9
import torch
import random
import zipfile

with zipfile.ZipFile('jaychou_lyrics.txt.zip') as zin:
with zin.open('jaychou_lyrics.txt') as f:
corpus_chars = f.read().decode('utf-8')

print(corpus_chars[:40])

这个数据集有6万多个字符。为了打印方便,我们把换行符替换成空格,然后仅使用前1万个字符来训练模型。

1
2
corpus_chars = corpus_chars.replace('\n', ' ').replace('\r', ' ')
corpus_chars = corpus_chars[0:10000]

2. 建立字符索引

我们将每个字符映射成一个从0开始的连续整数,又称索引,来方便之后的数据处理。为了得到索引,我们将数据集里所有不同字符取出来,然后将其逐一映射到索引来构造词典。接着,打印vocab_size,即词典中不同字符的个数,又称词典大小。

1
2
3
4
5
6
7
idx_to_char = list(set(corpus_chars))

# {'char': index}
char_to_idx = dict([(char, i) for i, char in enumerate(idx_to_char)])

vocab_size = len(char_to_idx)
print(vocab_size) # 1027

之后,将训练数据集中每个字符转化为索引,并打印前20个字符及其对应的索引。

1
2
3
4
5
# corpus_indices是文本中每个字都转换成数字来表示的list
corpus_indices = [char_to_idx[char] for char in corpus_chars]
sample = corpus_indices[:20]
print('chars:', ''.join([idx_to_char[idx] for idx in sample]))
print('indices:', sample)

3. 时序数据的采样

在训练中我们需要每次随机读取小批量样本和标签。与之前章节的实验数据不同的是,时序数据的一个样本通常包含连续的字符。

假设时间步数为5,样本序列为5个字符,即“想”“要”“有”“直”“升”。

我们有两种方式对时序数据进行采样,分别是 随机采样相邻采样

3.1 随机采样

下面的代码每次从数据里随机采样一个小批量。其中批量大小 batch_size 指每个小批量的样本数num_steps 为每个样本所包含的时间步数

在随机采样中,每个样本是原始序列上任意截取的一段序列。相邻的两个随机小批量在原始序列上的位置不一定相毗邻(也就是后续代码中的X中每个batch中的文本段内容都不是相连的,是随机的)。因此,我们无法用一个小批量最终时间步的隐藏状态来初始化下一个小批量的隐藏状态。

在训练模型时,每次随机采样前都需要重新初始化隐藏状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 本函数已保存在d2lzh_pytorch包中方便以后使用
def data_iter_random(corpus_indices, batch_size, num_steps, device=None):
# 减1是因为 输出的索引Y 是 相应输入的索引X 加1,所以得总长度-1才是X分布范围(后面Y部分代码:Y = [_data(j * num_steps + 1))
num_examples = (len(corpus_indices) - 1) // num_steps # corpus_indices 按照num_steps来分成n段
epoch_size = num_examples // batch_size # n段 分到训练的每个batch_size后,一共有epoch_size个 batch_size训练集
example_indices = list(range(num_examples)) # 为每个段分配索引,形成索引列表
random.shuffle(example_indices) # 打乱索引

# 返回从pos开始的长为num_steps的序列
def _data(pos):
return corpus_indices[pos: pos + num_steps] # 返回给出 索引 的对应段内容

if device is None:
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

for i in range(epoch_size):
# 每次读取batch_size个随机样本
i = i * batch_size
batch_indices = example_indices[i: i + batch_size] # 索引列表中取出 从i开始的batch_size大小的索引,即batch_size个样本开始索引
X = [_data(j * num_steps) for j in batch_indices] # 生成按照n个batch_size个样本开始索引(每个为j),每次请求corpus_indices[j,j+num_steps] 中的片段。
Y = [_data(j * num_steps + 1) for j in batch_indices] #
yield torch.tensor(X, dtype=torch.float32, device=device), torch.tensor(Y, dtype=torch.float32, device=device)

这里简单说明Y。

语言模型训练的目标是根据输入序列预测下一个词(即“语言建模”)。

假设 corpus_indices = [0, 1, 2, 3, 4, 5, 6],num_steps = 3。

那么对于 j = 1(也就是第 1 段,从0算起):

• start = j * num_steps = 3

• X = _data(3) = corpus_indices[3:6] = [3, 4, 5]

• Y = _data(4) = corpus_indices[4:7] = [4, 5, 6]

因此代码中一开始需要减1。

让我们输入一个从0到29的连续整数的人工序列。

设批量大小和时间步数分别为2和6。打印随机采样每次读取的小批量样本的输入X和标签Y。可见,相邻的两个随机小批量在原始序列上的位置不一定相毗邻。

1
2
3
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/Y中存在2(batch_size)个数据

3.2 相邻采样

除对原始序列做随机采样之外,我们还可以令相邻的两个随机小批量在原始序列上的位置相毗邻。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def data_iter_consecutive(corpus_indices, batch_size, num_steps, device=None):
if device is None:
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

corpus_indices = torch.tensor(corpus_indices, dtype=torch.float32, device=device)
data_len = len(corpus_indices)

# 先按照batch_size分段,batch_len即为每个batch中的字符串长度
batch_len = data_len // batch_size

# 将 总字符串corpus_indices 变形成(batch_size,batch_len)的矩阵
indices = corpus_indices[0: batch_size*batch_len].view(batch_size, batch_len)

# batch_len-1后除以num_steps,求出每个batch中的字符串长度能分出多少个XY的配套数据。
# 即每个batch中,存在epoch_size个XY数据。
epoch_size = (batch_len - 1) // num_steps

for i in range(epoch_size):
i = i * num_steps
X = indices[:, i: i + num_steps] # batch_sieze个样本中的第i个num_step的数据
Y = indices[:, i + 1: i + num_steps + 1]
yield X, Y

总之我们知道不论随机还是相邻函数,其结果都是每次输出内含batch_size个字符串的X/Y矩阵(batch_size,num_steps)。