读取数据集

首先读取这个数据集

import torch
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')

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

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

建立字符索引

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

idx_to_char = list(set(corpus_chars))
char_to_idx = dict([(char, i) for i, char in enumerate(idx_to_char)])
vocab_size = len(char_to_idx)
vocab_size # 1027

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

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)

输出:

chars: 想要有直升机 想要和你飞到宇宙去 想要和
indices: [250, 164, 576, 421, 674, 653, 357, 250, 164, 850, 217, 910, 1012, 261, 275, 366, 357, 250, 164, 850]

时序数据的采样

在训练中需要每次随机读取小批量样本和标签。时序数据的一个样本通常包含连续的字符。样本的标签序列为这些字符分别在训练集中的下一个字符。有两种方式对时序数据进行采样,分别是随机采样和相邻采样。

随机采样

下面的代码每次从数据里随机采样一个小批量。其中批量大小batch_size指每个小批量的样本数,num_steps为每个样本所包含的时间步数。
在随机采样中,每个样本是原始序列上任意截取的一段序列。相邻的两个随机小批量在原始序列上的位置不一定相毗邻。因此,无法用一个小批量最终时间步的隐藏状态来初始化下一个小批量的隐藏状态。在训练模型时,每次随机采样前都需要重新初始化隐藏状态。

def data_iter_random(corpus_indices, batch_size, num_steps, device=None): # corpus_indices: 字符索引序列,batch_size: 批量大小,num_steps: 时间步数
    # 减1是因为输出的索引x是相应输入的索引y加1
    num_examples = (len(corpus_indices) - 1) // num_steps # 先获取语料库的总长度,再将语料库分割成多个长度为 num_steps 的子序列,计算可以得到多少个这样的完整子序列。
    epoch_size = num_examples // 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] # 返回从 pos 开始的长为 num_steps 的序列
    if device is None:
        device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') # 如果没有指定设备,就使用 GPU

    for i in range(epoch_size):
        # 每次读取batch_size个随机样本
        i = i * batch_size # 从 i * batch_size 开始
        batch_indices = example_indices[i: i + batch_size] # 从随机打乱的索引列表 example_indices 中取出当前批次的索引
        X = [_data(j * num_steps) for j in batch_indices] # 获取当前批次每一个样本的输入序列
        Y = [_data(j * num_steps + 1) for j in batch_indices] # 获取当前批次每一个样本的标签,即每一个样本的输入的下一个字符

        # 将输入序列 X 和目标序列 Y 转换为 PyTorch 张量。
        # 数据类型设置为 float32,并移动到指定的设备(CPU 或 GPU)。
        # 使用 yield 返回当前批次的数据,形成一个生成器。
        yield torch.tensor(X, dtype=torch.float32, device=device), torch.tensor(Y, dtype=torch.float32, device=device) 

相邻采样

除对原始序列做随机采样之外,还可以令相邻的两个随机小批量在原始序列上的位置相毗邻。这时候,就可以用一个小批量最终时间步的隐藏状态来初始化下一个小批量的隐藏状态,从而使下一个小批量的输出也取决于当前小批量的输入,并如此循环下去。这对实现循环神经网络造成了两方面影响:一方面,
在训练模型时,我们只需在每一个迭代周期开始时初始化隐藏状态;另一方面,当多个相邻小批量通过传递隐藏状态串联起来时,模型参数的梯度计算将依赖所有串联起来的小批量序列。同一迭代周期中,随着迭代次数的增加,梯度的计算开销会越来越大。
为了使模型参数的梯度计算只依赖一次迭代读取的小批量序列,可以在每次读取小批量前将隐藏状态从计算图中分离出来。

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') # 如果没有指定设备,就使用 GPU
    corpus_indices = torch.tensor(corpus_indices, dtype=torch.float32, device=device) # 将语料库转换为张量
    data_len = len(corpus_indices) # 获取语料库的总长度
    batch_len = data_len // batch_size # 计算每个批次中的子序列长度

    # corpus_indices[0: batch_size * batch_len] 截取语料库的前 batch_size * batch_len 个元素,确保数据可以被均匀分割。
    # view(batch_size, batch_len) 将张量的形状改为 batch_size 行,batch_len 列。
    indices = corpus_indices[0: batch_size*batch_len].view(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] # 选中每一行的第 i 到 i + num_steps 个字符作为输入
        Y = indices[:, i + 1: i + num_steps + 1] # 选中每一行的第 i + 1 到 i + num_steps + 1 个字符作为标签
        yield X, Y

循之际,如星夜般的幻想。