基与字符级RNN(Char-RNN)的人名生成

译者:@jianchengss

作者: Sean Robertson

上一个教程 里我们使用RNN把名字分类到它所属的语言中, 这次我们改变一下来学习从语言中生成名字.

  1. > python sample.py Russian RUS
  2. Rovakov
  3. Uantov
  4. Shavakov
  5. > python sample.py German GER
  6. Gerren
  7. Ereng
  8. Rosher
  9. > python sample.py Spanish SPA
  10. Salla
  11. Parer
  12. Allan
  13. > python sample.py Chinese CHI
  14. Chan
  15. Hang
  16. Iun

我们仍然手工搭建一个包含几个线性层的小的RNN. 这次的最大的不同是输入一个类别, 每次输出一个字母, 而不是读入所有名字的字母来预测一个类别. 循环的预测每一个字母来构成语言(也可以用文 字或者其他更高级的结构完成), 通常被称为“语言模型”.

推荐阅读:

假设你至少安装了PyTorch, 熟悉Python, 理解Tensors:

下面这些对了解 RNNs 和其工作原理也是很有用的:

还建议上一个教程: 用字符级RNN分类名称

数据准备

注解:

这里 下载数据, 并解压到当前目录.

更多的细节参考上一个教程, 总之, 数据含有一批纯文本文件: data/names/[Language].txt 每一行一个人名. 将行分割成数组, 并把 Unicode 转换成 ASCII 编码, 最后放进一个字典里 {language: [names ...]}.

  1. from __future__ import unicode_literals, print_function, division
  2. from io import open
  3. import glob
  4. import unicodedata
  5. import string
  6. all_letters = string.ascii_letters + " .,;'-"
  7. n_letters = len(all_letters) + 1 # 添加 EOS 标记
  8. def findFiles(path): return glob.glob(path)
  9. # 将 Unicode 字符串转换为纯 ASCII 编码, 感谢 http://stackoverflow.com/a/518232/2809427
  10. def unicodeToAscii(s):
  11. return ''.join(
  12. c for c in unicodedata.normalize('NFD', s)
  13. if unicodedata.category(c) != 'Mn'
  14. and c in all_letters
  15. )
  16. # 读取文件并分割成行
  17. def readLines(filename):
  18. lines = open(filename, encoding='utf-8').read().strip().split('\n')
  19. return [unicodeToAscii(line) for line in lines]
  20. # 构建映射字典 category_lines , 每个类别是由很多个行组成的list
  21. category_lines = {}
  22. all_categories = []
  23. for filename in findFiles('data/names/*.txt'):
  24. category = filename.split('/')[-1].split('.')[0]
  25. all_categories.append(category)
  26. lines = readLines(filename)
  27. category_lines[category] = lines
  28. n_categories = len(all_categories)
  29. print('# categories:', n_categories, all_categories)
  30. print(unicodeToAscii("O'Néàl"))

创建网络

这个网络扩展了 上一个教程的RNN , 为类别张量添加了一个额外的参数, 并和其他的参数串联在一起. 类别张量

和字母的输入一样是 one-hot 向量.

我们将输出解释成为下一个字母的概率, 采样的时候, 最有可能的输出被当做下一个输入.

为了让网络更加有效工作, 我添加了第二个线性层 o2o (在合并了隐藏层和输出层的后面). 还有一个 Dropout 层, 使输入的部分值以给定的概率值随机的变成 0 (这里概率取0.1), 这样做通常是为了模糊输入以防止过拟合. 这里我们在网络的最末端使用它, 从而故意添加一些混乱和增加采样的多样化.

基与字符级RNN(Char-RNN)的人名生成 - 图1

  1. import torch
  2. import torch.nn as nn
  3. from torch.autograd import Variable
  4. class RNN(nn.Module):
  5. def __init__(self, input_size, hidden_size, output_size):
  6. super(RNN, self).__init__()
  7. self.hidden_size = hidden_size
  8. self.i2h = nn.Linear(n_categories + input_size + hidden_size, hidden_size)
  9. self.i2o = nn.Linear(n_categories + input_size + hidden_size, output_size)
  10. self.o2o = nn.Linear(hidden_size + output_size, output_size)
  11. self.dropout = nn.Dropout(0.1)
  12. self.softmax = nn.LogSoftmax(dim=1)
  13. def forward(self, category, input, hidden):
  14. input_combined = torch.cat((category, input, hidden), 1)
  15. hidden = self.i2h(input_combined)
  16. output = self.i2o(input_combined)
  17. output_combined = torch.cat((hidden, output), 1)
  18. output = self.o2o(output_combined)
  19. output = self.dropout(output)
  20. output = self.softmax(output)
  21. return output, hidden
  22. def initHidden(self):
  23. return Variable(torch.zeros(1, self.hidden_size))

训练

训练前的准备

首先, 利用辅助函数产生随机的(category, line)对:

  1. import random
  2. # 从list中随机选取项
  3. def randomChoice(l):
  4. return l[random.randint(0, len(l) - 1)]
  5. # 获取随机的类别和该类别中随机的行
  6. def randomTrainingPair():
  7. category = randomChoice(all_categories)
  8. line = randomChoice(category_lines[category])
  9. return category, line

对每一个时间点(也就是说在训练集中词的每个字母)网络的输入是 (类别, 当前字母, 隐藏层状态) , 输出是 (下一个字母, 下一个隐藏层状态) . 对于每一个训练集, 我们需要的是类别、输入的字母集、输出/目标字母集.

因为在每一步, 我们从当前的字母预测下一个字母, 这样的字母对是在原有行中连续字母的集合, 例如, 对于 "ABCD<EOS>" 将会产生 (“A”, “B”), (“B”, “C”), (“C”, “D”), (“D”, “EOS”).

基与字符级RNN(Char-RNN)的人名生成 - 图2

类别张量是一个大小为 <1 x n_categories>one-hot tensor 张量, 在训练的每一个时间点把它喂给网络 —— 这是一个设计的选择, 它可以被当作为初始隐藏状或其他策略的一部分.

  1. # 类别的 one-hot 向量
  2. def categoryTensor(category):
  3. li = all_categories.index(category)
  4. tensor = torch.zeros(1, n_categories)
  5. tensor[0][li] = 1
  6. return tensor
  7. # 输入串从第一个字母到最后一个字母(不包括 EOS )的 one-hot 矩阵
  8. def inputTensor(line):
  9. tensor = torch.zeros(len(line), 1, n_letters)
  10. for li in range(len(line)):
  11. letter = line[li]
  12. tensor[li][0][all_letters.find(letter)] = 1
  13. return tensor
  14. # 目标的第二个字母到结尾(EOS)的 LongTensor
  15. def targetTensor(line):
  16. letter_indexes = [all_letters.find(line[li]) for li in range(1, len(line))]
  17. letter_indexes.append(n_letters - 1) # EOS
  18. return torch.LongTensor(letter_indexes)

为了训练过程的便利, 添加一个 randomTrainingExample 函数, 获取随机的 (category, line) 对, 并把他们转换成需要的 (category, input, target) 张量.

  1. # 从随机的(category, line)对中生成 category, input, and target 张量
  2. def randomTrainingExample():
  3. category, line = randomTrainingPair()
  4. category_tensor = Variable(categoryTensor(category))
  5. input_line_tensor = Variable(inputTensor(line))
  6. target_line_tensor = Variable(targetTensor(line))
  7. return category_tensor, input_line_tensor, target_line_tensor

网络的训练

与分类相比, 分类只用到了最后的输出, 而这里每个步都会产生一个预测, 所以我们需要计算每一步的损失.

自动求导(autograd)的魔力就在于, 它允许将每一步的损失简单的加和, 并在最后调用 backward

  1. criterion = nn.NLLLoss()
  2. learning_rate = 0.0005
  3. def train(category_tensor, input_line_tensor, target_line_tensor):
  4. hidden = rnn.initHidden()
  5. rnn.zero_grad()
  6. loss = 0
  7. for i in range(input_line_tensor.size()[0]):
  8. output, hidden = rnn(category_tensor, input_line_tensor[i], hidden)
  9. loss += criterion(output, target_line_tensor[i])
  10. loss.backward()
  11. for p in rnn.parameters():
  12. p.data.add_(-learning_rate, p.grad.data)
  13. return output, loss.data[0] / input_line_tensor.size()[0]

为了跟踪训练花费了多长时间, 这里添加一个 timeSince(timestamp) 函数, 返回一个人们易读的字符串:

  1. import time
  2. import math
  3. def timeSince(since):
  4. now = time.time()
  5. s = now - since
  6. m = math.floor(s / 60)
  7. s -= m * 60
  8. return '%dm %ds' % (m, s)

训练和往常一样, 不停的调用 train 并等待一会, 打印当前时间, 每隔 print_every 个例子打印 loss, 将每 plot_every 个例子的平均损失保存在 all_losses 中以便后面画图.

  1. rnn = RNN(n_letters, 128, n_letters)
  2. n_iters = 100000
  3. print_every = 5000
  4. plot_every = 500
  5. all_losses = []
  6. total_loss = 0 # 每 plot_every 次迭代需要重置
  7. start = time.time()
  8. for iter in range(1, n_iters + 1):
  9. output, loss = train(*randomTrainingExample())
  10. total_loss += loss
  11. if iter % print_every == 0:
  12. print('%s (%d %d%%) %.4f' % (timeSince(start), iter, iter / n_iters * 100, loss))
  13. if iter % plot_every == 0:
  14. all_losses.append(total_loss / plot_every)
  15. total_loss = 0

绘制损失

从 all_losses 中绘制历史损失, 以展现网络的学习过程

  1. import matplotlib.pyplot as plt
  2. import matplotlib.ticker as ticker
  3. plt.figure()
  4. plt.plot(all_losses)

网络采样

为了采样, 我们给网络一个字母并问下一个字母是什么, 重复这个过程直到 EOS 标记.

  • 创建输入类别、起始字母和隐藏层状态的张量
  • 创建一个带有起始字母的 output_name
  • 直到最大的输出长度,
    • 当前字母喂给网络
    • 从最高的输出获取下一个字母和下一个隐藏层状态
    • 如果输出字母是 EOS, 算法结束
    • 如果输出是常规字母, 将其加入到 output_name 并继续
  • 返回最终的名字

注解:

与给定起始字母不同的是, 有其他的策略是在训练的时候包含一个“串起始”标记, 让网络选择属于自己的起始字母.

  1. max_length = 20
  2. # 从类别和起始字母采样
  3. def sample(category, start_letter='A'):
  4. category_tensor = Variable(categoryTensor(category))
  5. input = Variable(inputTensor(start_letter))
  6. hidden = rnn.initHidden()
  7. output_name = start_letter
  8. for i in range(max_length):
  9. output, hidden = rnn(category_tensor, input[0], hidden)
  10. topv, topi = output.data.topk(1)
  11. topi = topi[0][0]
  12. if topi == n_letters - 1:
  13. break
  14. else:
  15. letter = all_letters[topi]
  16. output_name += letter
  17. input = Variable(inputTensor(letter))
  18. return output_name
  19. # 给定一个类别和多个起始字母 获取个采样结果
  20. def samples(category, start_letters='ABC'):
  21. for start_letter in start_letters:
  22. print(sample(category, start_letter))
  23. samples('Russian', 'RUS')
  24. samples('German', 'GER')
  25. samples('Spanish', 'SPA')
  26. samples('Chinese', 'CHI')

练习

  • 尝试使用不同 类别->行 数据集, 例如:
    • 小说系列 -> 角色名字
    • 词性 -> 词语
    • 国家 -> 城市
  • 使用“串起始”标记, 使采样的时候不用给定起始字母
  • 使用更大和/或更好的网络结构获取更好的结果
    • 尝试一下 nn.LSTM 和 nn.GRU 层
    • 将这些 RNNs 组合成更高级的网络