语言模型数据集(周杰伦专辑歌词)
上一节 循环网络RNN 中我们已经简要介绍了该网络的原理。本节将介绍如何预处理一个语言模型数据集,并将其转换成字符级循环神经网络所需要的输入格式。为此,我们收集了周杰伦从第一张专辑《Jay》到第十张专辑《跨时代》中的歌词,并在后面几节里应用循环神经网络来训练一个语言模型。当模型训练好后,我们就可以用这个模型来创作歌词。
读取数据集
首先读取这个数据集,看看前100个字符是什么样的。
import torch
import random
import zipfile
with zipfile.ZipFile('../data/jaychou_lyrics.txt.zip', 'r') as zin:
with zin.open('jaychou_lyrics.txt') as f:
corpus_chars = f.read().decode('utf-8')
print(corpus_chars[:100])
输出
想要有直升机
想要和你飞到宇宙去
想要和你融化在一起
融化在宇宙里
我每天每天每天在想想想想著你
这样的甜蜜
让我开始乡相信命运
感谢地心引力
让我碰到你
漂亮的让我面红的可爱女人
温柔的让我心疼的可
这个数据集有6万多个字符。为了打印方便,我们把换行符替换成空格,然后仅使用前1万个字符来训练模型。
corpus_chars = corpus_chars.replace('\n', ' ').replace('\r', ' ')
corpus_chars = corpus_chars[0:10000]
print(corpus_chars[:100])
print(f'数据集长度: {len(corpus_chars)}')
输出
想要有直升机 想要和你飞到宇宙去 想要和你融化在一起 融化在宇宙里 我每天每天每天在想想想想著你 这样的甜蜜 让我开始乡相信命运 感谢地心引力 让我碰到你 漂亮的让我面红的可爱女人 温柔的让我心疼的可
数据集长度: 10000
建立字符索引
我们将每个字符映射成一个从0开始的连续整数,又称索引,来方便之后的数据处理。为了得到索引,我们将数据集里所有不同字符取出来,然后将其逐一映射到索引来构造词典。接着,打印vocab_size,即词典中不同字符的个数,又称词典大小。
idx_to_char = list(set(corpus_chars))
char_to_idx = {char: i for i, char in enumerate(idx_to_char)}
vocab_size = len(char_to_idx)
print(f'词典大小: {vocab_size}')
输出
词典大小: 1027
对上面代码的解释
set() 是Python的内置函数,用于创建一个无序、不重复的集合。当把一个字符串传入 set() 时,它会自动提取出其中所有唯一的字符。将上一步得到的集合(set)转换回一个列表(list),因为列表更容易按索引操作。enumerate() 是一个Python内置函数,它会遍历一个序列(如列表),同时返回索引和对应的值。对于 idx_to_char = [’ ', ‘d’, ‘e’, ‘h’, ‘l’, ‘o’, ‘r’, ‘w’],enumerate 会产生:(0, ’ '), (1, ‘d’), (2, ‘e’), (3, ‘h’), (4, ‘l’), (5, ‘o’), (6, ‘r’), (7, ‘w’)。然后利用字典推导式创建一个新的字典。
之后,将训练数据集中每个字符转化为索引,并打印前20个字符及其对应的索引。
corpus_indices = [char_to_idx[char] for char in corpus_chars]
sample = corpus_indices[:20]
print('字符:', ''.join([idx_to_char[idx] for idx in sample]))
print('索引:', sample)
输出
字符: 想要有直升机 想要和你飞到宇宙去 想要和
索引: [477, 914, 211, 1016, 194, 253, 570, 477, 914, 982, 136, 311, 386, 64, 341, 362, 570, 477, 914, 982]
对上面代码的解释
利用列表推导式将循环和查询的结果组合成一个新的列表。
知识拓展(推导式)
是的,推导式是 Python 语言中一种非常独特、强大且简洁的特性。它允许我们用一种简洁的语法来快速创建列表、字典、集合等数据结构。
什么是推导式?
推导式是一种从一个序列构建另一个序列的简洁方法,它将循环和条件判断集成在一行代码中,使代码更加 Pythonic。
四种主要的推导式
从for开始分,for前是表达式,for后是循环
1. 列表推导式(中括号、方括号)
用途:快速创建列表
基本语法:
[expression for item in iterable if condition]
示例:
# 传统方式
squares = []
for i in range(5):
squares.append(i**2)
print(squares) # [0, 1, 4, 9, 16]
# 列表推导式方式
squares = [i**2 for i in range(5)]
print(squares) # [0, 1, 4, 9, 16]
更多例子:
# 带条件过滤
even_squares = [i**2 for i in range(10) if i % 2 == 0]
print(even_squares) # [0, 4, 16, 36, 64]
# 处理字符串
words = ['hello', 'world', 'python']
lengths = [len(word) for word in words]
print(lengths) # [5, 5, 6]
2. 字典推导式(大括号、花括号)
用途:快速创建字典
基本语法:
{key_expression: value_expression for item in iterable if condition}
示例:
# 传统方式
squares_dict = {}
for i in range(5):
squares_dict[i] = i**2
print(squares_dict) # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
# 字典推导式方式
squares_dict = {i: i**2 for i in range(5)}
print(squares_dict) # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
更多例子:
# 键值转换
original = {'a': 1, 'b': 2, 'c': 3}
reversed_dict = {value: key for key, value in original.items()}
print(reversed_dict) # {1: 'a', 2: 'b', 3: 'c'}
# 带条件过滤
numbers = [1, 2, 3, 4, 5, 6]
even_squares = {x: x**2 for x in numbers if x % 2 == 0}
print(even_squares) # {2: 4, 4: 16, 6: 36}
3. 集合推导式(大括号,花括号)
用途:快速创建集合(自动去重)
基本语法:
{expression for item in iterable if condition}
示例:
# 传统方式
unique_chars = set()
for char in "hello world":
unique_chars.add(char)
print(unique_chars) # {'h', 'e', 'l', 'o', ' ', 'w', 'r', 'd'}
# 集合推导式方式
unique_chars = {char for char in "hello world"}
print(unique_chars) # {'h', 'e', 'l', 'o', ' ', 'w', 'r', 'd'}
更多例子:
# 带计算的集合推导式
numbers = [1, 2, 2, 3, 4, 4, 5]
unique_squares = {x**2 for x in numbers}
print(unique_squares) # {1, 4, 9, 16, 25}
4. 生成器推导式(小括号)
用途:创建生成器(惰性求值,节省内存)
基本语法:
(expression for item in iterable if condition)
示例:
# 列表推导式(立即计算,占用内存)
squares_list = [i**2 for i in range(1000000)] # 立即创建100万个元素的列表
# 生成器推导式(惰性计算,节省内存)
squares_gen = (i**2 for i in range(1000000)) # 创建生成器对象,不立即计算
print(squares_gen) # <generator object <genexpr> at 0x...>
# 使用时逐个产生值
for i, value in enumerate(squares_gen):
if i < 5: # 只取前5个
print(value)
else:
break
# 输出: 0, 1, 4, 9, 16
时序数据的采样
在训练中我们需要每次随机读取小批量样本和标签。与之前章节的实验数据不同的是,时序数据的一个样本通常包含连续的字符。假设时间步数为5,样本序列为5个字符,即"想"“要”“有”“直”“升”。该样本的标签序列为这些字符分别在训练集中的下一个字符,即"要"“有”“直”“升”“机”。我们有两种方式对时序数据进行采样,分别是随机采样和相邻采样。
随机采样
下面的代码每次从数据里随机采样一个小批量。其中批量大小batch_size指每个小批量的样本数,num_steps为每个样本所包含的时间步数。在随机采样中,每个样本是原始序列上任意截取的一段序列。相邻的两个随机小批量在原始序列上的位置不一定相毗邻。因此,我们无法用一个小批量最终时间步的隐藏状态来初始化下一个小批量的隐藏状态。在训练模型时,每次随机采样前都需要重新初始化隐藏状态。
def data_iter_random(corpus_indices, batch_size, num_steps, device=None):
# 减1是因为输出的索引是相应输入的索引加1
num_examples = (len(corpus_indices) - 1) // 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]
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 torch.tensor(X, device=device), torch.tensor(Y, device=device)
让我们输入一个从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([[ 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]])
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]])
这里面Y始终比X大1,
比如0的预测结果应该是1,
0、1预测的正确结果应该是2,
0、1、2预测的正确结果应该是3
依次类推
相邻采样
除对原始序列做随机采样之外,我们还可以令相邻的两个随机小批量在原始序列上的位置相毗邻。这时候,我们就可以用一个小批量最终时间步的隐藏状态来初始化下一个小批量的隐藏状态,从而使下一个小批量的输出也取决于当前小批量的输入,并如此循环下去。这对实现循环神经网络造成了两方面影响:一方面,在训练模型时,我们只需在每一个迭代周期开始时初始化隐藏状态;另一方面,当多个相邻小批量通过传递隐藏状态串联起来时,模型参数的梯度计算将依赖所有串联起来的小批量序列。同一迭代周期中,随着迭代次数的增加,梯度的计算开销会越来越大。为了使模型参数的梯度计算只依赖一次迭代读取的小批量序列,我们可以在每次读取小批量前将隐藏状态从计算图中分离出来。
def data_iter_consecutive(corpus_indices, batch_size, num_steps, device=None):
corpus_indices = torch.tensor(corpus_indices, device=device)
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。相邻的两个随机小批量在原始序列上的位置相毗邻。
for X, Y in data_iter_consecutive(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]])
观察可以看到Y始终比X向后偏移一个位置,这是因为
对于语言模型(如GPT),训练目标是:
给定前文,预测下一个词
不是给定第一个词,预测后面所有词
例子:句子 “I love machine learning”
输入 “I” → 预测 “love”
输入 “I love” → 预测 “machine”
输入 “I love machine” → 预测 “learning”
知识扩展(下划线)
在Python中,在函数名前面加下划线(_) 是一种命名约定,有特定的含义。
_data 前面加下划线的含义
1. 表示“内部使用”的约定
- 单前导下划线:
_function_name - 含义:这是一个“内部”函数,仅在当前模块/类内部使用,不建议从外部直接调用
- 目的:告诉其他开发者:“这是一个实现细节,请不要直接使用,因为接口可能会改变”
2. 在这个具体代码中的原因
def data_iter_random(corpus_indices, batch_size, num_steps, device=None):
# ... 一些计算 ...
def _data(pos): # <-- 这个下划线表示:这只是data_iter_random的内部工具函数
return corpus_indices[pos: pos + num_steps]
for i in range(epoch_size):
# ...
X = [_data(j * num_steps) for j in batch_indices] # 仅在内部使用
Y = [_data(j * num_steps + 1) for j in batch_indices]
yield torch.tensor(X, device=device), torch.tensor(Y, device=device)
为什么这里要用 _data?
_data函数的作用很单一:只是根据位置pos截取一段序列- 它完全服务于
data_iter_random这个主函数 - 外部调用者不应该(也不需要)直接调用
_data - 如果未来修改了数据加载逻辑,
_data函数可能会被删除或修改,但不会影响外部调用者
1. 单前导下划线:_var
def public_function():
return "外部可以调用"
def _internal_function(): # 暗示:内部使用
return "不建议外部直接调用"
2. 单末尾下划线:var_
class_ = "My Class" # 避免与关键字class冲突
type_ = "string" # 避免与内置函数type冲突
3. 双前导下划线:__var(名称修饰)
class MyClass:
def __private_method(self): # Python会将其重命名为_MyClass__private_method
return "这是私有方法"
4. 双前导和末尾下划线:__var__
class MyClass:
def __init__(self): # 魔法方法/特殊方法
pass
def __str__(self): # 魔法方法
return "MyClass"
5. 单下划线:_
# 作为临时变量(表示这个变量不重要)
for _ in range(10):
do_something()
# 在交互模式中存储上一个结果
>>> 3 + 4
7
>>> _
7
小结
- 时序数据采样方式包括随机采样和相邻采样
- 随机采样中每个小批量样本来自原始序列的随机位置,训练时每次都需要重新初始化隐藏状态
- 相邻采样中相邻小批量在原始序列上位置毗邻,可以用前一小批量的隐藏状态初始化下一小批量的隐藏状态
- 使用这两种方式的循环神经网络训练在实现上略有不同
7004

被折叠的 条评论
为什么被折叠?



