文章目录
1 数据与说明
数据下载
数据下载链接:点击下载
数据是一个data.zip压缩包,解压后的目录树如下所示:
D:.
│ eng-fra.txt
│
└─names
Arabic.txt
Chinese.txt
Czech.txt
Dutch.txt
English.txt
French.txt
German.txt
Greek.txt
Irish.txt
Italian.txt
Japanese.txt
Korean.txt
Polish.txt
Portuguese.txt
Russian.txt
Scottish.txt
Spanish.txt
Vietnamese.txt
eng-fra.txt 是第三篇翻译任务中要用到的,这次我们只用到 /name 这个文件夹下的18个文件,每个文件以语言命名,格式为:[Language].txt。打开后,里面是该语言中常用的姓/名。
比如:打开我们最熟悉的 Chinese.txt,可以看到每一行是一个姓或者名(有一些姓/名确实有点点奇怪,但整体来说问题不大)。
Ang
Au-Yong
Bai
Ban
Bao
Bei
Bian
Bui
Cai
Cao
Cen
……
任务说明
这次任务的目标是:输入一个国家的语言名,和名字的首字母缩写,模型自动生成名字。
比如:
> python sample.py Russian RUS
Rovakov
Uantov
Shavakov
> python sample.py German GER
Gerren
Ereng
Rosher
> python sample.py Spanish SPA
Salla
Parer
Allan
> python sample.py Chinese CHI
Chan
Hang
Iun
这次,我们仍然要自己搭建一个RNN,由一些线性的全连接层组成。和第一篇预测类别不同之处在于,这次我们要输入一个类别,然后每次输出一个字母。这样一个循环预测下一个字母,生成一种语言的模型通常叫做语言模型(language model)。
2 代码
与第一篇相同,首先是数据预处理。这次仍然是字母级别的RNN,因此是对字母进行one-hot编码。
把所有的 /name/[Language].txt 文件读进来。
n_letters 表示所有字母的数量。这次多加了一个特殊符号 。因为是文本生成,所以需要有一个符号来结束文本生成的过程。我们设定,当生成 的时候,就结束RNN的循环。
因为某些语言的字母和常见的英文字母不太一样,所以我们需要把它转化成普普通通的英文字母,用到了 unicodeToAscii() 函数。
from io import open
import glob
import os
import unicodedata
import string
all_letters = string.ascii_letters + " .,;'-"
n_letters = len(all_letters) + 1 # 加上一个 EOS 标记
def findFiles(path): return glob.glob(path)
# Turn a Unicode string to plain ASCII, thanks to https://*.com/a/518232/2809427
def unicodeToAscii(s):
return ''.join(
c for c in unicodedata.normalize('NFD', s)
if unicodedata.category(c) != 'Mn'
and c in all_letters
)
# 读入文件 filename, 分行
def readLines(filename):
lines = open(filename, encoding='utf-8').read().strip().split('\n')
return [unicodeToAscii(line) for line in lines]
# 建立一个词典 category_lines = {category: lines} , lines = [names...]
category_lines = {}
all_categories = []
for filename in findFiles('data/names/*.txt'):
category = os.path.splitext(os.path.basename(filename))[0]
all_categories.append(category)
lines = readLines(filename)
category_lines[category] = lines
n_categories = len(all_categories)
if n_categories == 0 :
raise RuntimeError('Data not found. Make sure that you downloaded data '
'from https://download.pytorch.org/tutorial/data.zip and extract it to '
'the current directory.')
print('# categories:', n_categories, all_categories)
print(unicodeToAscii("O'Néàl"))
Out:
# categories: 18 ['Greek', 'Dutch', 'Irish', 'Arabic', 'Korean', 'French', 'Spanish', 'German', 'Portuguese', 'Italian', 'Vietnamese', 'Russian', 'Scottish', 'Chinese', 'English', 'Japanese', 'Czech', 'Polish']
O'Neal
和第一篇一样,需要把所有的值变成 Tensor :
-
inputTensor()函数:对输入的单词 line 进行one-hot 编码,大小为 < line_length × 1 × n_letters >
-
categoryTensor()函数:对类别进行 one-hot 编码,大小为 <1 x n_categories> ,和xt、ht-1拼接到一起[category,xt,ht-1]作为RNN的输入
-
targetTensor()函数:把目标值转换成Tensor,目标值不是 one-hot 编码,只是一个存储索引的序列
文本生成的过程:每一步,根据当前输入的字母,预测下一步输出的字母。在这里,预测得到的字母就是生成的字母。
根据训练集,我们需要创建样本,组成 input - target 对。比如,训练集中的一个词是 “ABCD”,首先,我们给它加上结束标记 “<EOS>” ,变成 “ABCD<EOS>”。然后,前一个词是input,后一个词是target,就可以创建成 (“A”, “B”), (“B”, “C”), (“C”, “D”), (“D”, “<EOS>”) 的样本对。 input 是one-hot 编码,target 则是普通的索引,可以看成是一个从 n_letters 到 n_letters 的多分类任务。比如:
- (“A”, “B”) = ( [ 1 , 0 , 0 , 0 , … , 0 , 0 ] , 1 )
- (“B”, “C”) = ( [ 0 , 1 , 0 , 0 , … , 0 , 0 ] , 2 )
- (“C”, “D”) = ( [ 0 , 0 , 1 , 0 , … , 0 , 0 ] , 3 )
- (“D”, “<EOS>”) = ( [ 0 , 0 , 0 , 1 , … , 0 , 0 ],4 )
与第一篇一样,从训练集中随机采样。
import random
# 从数组 l 中随机选一个元素
def randomChoice(l):
return l[random.randint(0,len(l)-1)]
# 随机采样一个 category,从该 category 中随机采样一个姓名line
def randomTrainingPair():
category = randomChoice(all_categories)
line = randomChoice(category_lines[category])
return category, line
# 从一个随机采样的 category-line 对中构建训练样本,
# 包含 category 的tensor, input 的 tensor, 和 target 的 tensors
def randomTrainingExample():
category, line = randomTrainingPair()
category_tensor = categoryTensor(category)
input_line_tensor = inputTensor(line)
target_line_tensor = targetTensor(line)
return category_tensor, input_line_tensor, target_line_tensor
模型
搭建本次任务的RNN模型,与第一篇不同的是,这次多了一个 o2o 层,并且用一个 dropout 层来防止过拟合。
-
input_combined = torch.cat((category, input, hidden), 1)
:拼接到[category, xt, ht-1] -
hidden = self.i2h(input_combined)
:ht = Wh[category, xt, ht-1] -
output = self.i2o(input_combined)
:ot = Wo1[category, xt, ht-1] -
output_combined = torch.cat((hidden, output), 1)
:o’t = [ht, ot] -
output = self.o2o(output_combined)
:ot = Wo2o’t = Wo2[ht, ot] -
output = self.dropout(output)
:用dropout防止过拟合 -
output = self.softmax(output)
:用softmax把ot转化成预测字母的概率分布yt
import torch.nn as nn
class RNN(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super(RNN,self).__init__()
self.hidden_size = hidden_size
self.i2h = nn.Linear(n_categories + input_size + hidden_size, hidden_size)
self.i2o = nn.Linear(n_categories + input_size + hidden_size, output_size)
self.o2o = nn.Linear(hidden_size + output_size, output_size)
self.dropout = nn.Dropout(0.1)
self.softmax = nn.LogSoftmax(dim=1)
def forward(self, category, input, hidden):
input_combined = torch.cat((category, input, hidden),1)
hidden = self.i2h(input_combined)
output = self.i2o(input_combined)
output_combined = torch.cat((hidden, output), 1)
output = self.o2o(output_combined)
output = self.dropout(output)
output = self.softmax(output)
return output, hidden
def initHidden(self):
return torch.zeros(1,self.hidden_size).to(device)
训练
在分类任务中,我们只用到了最后一步的 output ,但是在本次的文本生成任务中,要用到每一步的 output ,所以,我们会在每一步都计算损失loss.
因为 output 最后一层经过了 LogSoftmax,所以对应的损失函数依然是NLLLoss(),学习率设置为0.0005
criterion = nn.NLLLoss()
learning_rate = 0.0005
def train(category_tensor, input_line_tensor, target_line_tensor):
target_line_tensor.unsqueeze_(-1)
hidden = rnn.initHidden()
rnn.zero_grad()
loss = 0
for i in range(input_line_tensor.size(0)):
output, hidden = rnn(category_tensor, input_line_tensor[i], hidden)
loss += criterion(output, target_line_tensor[i])
loss.backward()
for p in rnn.parameters():
p.data.add_(p.grad.data, alpha=-learning_rate)
return output, loss.item() / input_line_tensor.size(0)
下面正式开始训练模型。
timeSince()
可以计算出训练时间。总共训练n_iters
次,每次用1个样本作为训练。每 print_every
次打印当前的训练损失,每 plot_every
次把损失保存到 all_losses
数组中,便于之后画图。
import time
def timeSince(since):
now = time.time()
s = now-since
return '%dm %ds'%(s//60,s%60)
n_iters = 100000
print_every = 5000
plot_every = 500
all_losses = []
total_loss = 0
n_hidden = 128
rnn = RNN(n_letters, n_hidden, n_letters)
rnn = rnn.to(device)
start = time.time()
for iter in range(1, n_iters + 1):
output, loss = train(*randomTrainingExample())
total_loss += loss
if iter % print_every == 0:
print('%s (%d %d%%) %.4f' %
(timeSince(start),iter, iter/n_iters*100,loss))
if iter % plot_every == 0:
all_losses.append(total_loss/plot_every)
total_loss = 0
Out:
0m 23s (5000 5%) 2.5188
0m 45s (10000 10%) 3.0014
1m 8s (15000 15%) 2.3518
1m 33s (20000 20%) 2.8295
2m 0s (25000 25%) 3.4643
2m 24s (30000 30%) 2.1880
2m 52s (35000 35%) 2.6564
3m 19s (40000 40%) 2.5555
3m 47s (45000 45%) 2.3225
4m 15s (50000 50%) 2.5692
4m 40s (55000 55%) 2.6630
5m 4s (60000 60%) 2.9264
5m 28s (65000 65%) 2.2237
5m 52s (70000 70%) 2.5134
6m 16s (75000 75%) 1.9850
6m 40s (80000 80%) 1.5491
7m 4s (85000 85%) 2.4384
7m 27s (90000 90%) 2.4045
7m 50s (95000 95%) 1.7542
8m 15s (100000 100%) 2.1884
画图
画出损失函数随着训练的变化情况:
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
plt.figure()
plt.plot(all_losses)
预测
给定模型一个首字母,然后模型生成下一个字母,不断重复,直到遇到 “<EOS>”标记符,停止生成。
- 创建输入类别category的tensor,开始字母的 tensor 和 初始化隐藏层状态 h0
- 用首字母生成一个字符串
output_name
- 在到达最大输出长度前:
- 给模型输入当前的字母
- 模型生成下一个字母,和下一个隐藏层状态
- 如果生成的字母是 “<EOS>”标记符, 停止生成
- 如果生成的字母是一个常规的字母,把它加入
output_name
,并且继续生成
- 返回最终生成的名字
output_nam
max_length = 20
def sample(category, start_letter = 'A'):
with torch.no_grad():
category_tensor = categoryTensor(category)
input = inputTensor(start_letter)
hidden = rnn.initHidden()
output_name = start_letter
for i in range(max_length):
output, hidden = rnn(category_tensor,input[0],hidden)
topv, topi = output.topk(1)
topi = topi[0][0]
if topi == n_letters - 1 :
break
else:
letter = all_letters[topi]
output_name += letter
input = inputTensor(letter)
return output_name
def samples(category, start_letters='ABC'):
for start_letter in start_letters:
print(sample(category,start_letter))
samples('Russian', 'RUS')
samples('German', 'GER')
samples('Spanish', 'SPA')
samples('Chinese', 'CHI')
Out:
Rovakov
Uantonov
Shalovev
Garterr
Eerter
Roure
Santan
Parer
Alanan
Chan
Han
Iun