【Pytorch官方教程】从零开始自己搭建RNN1 - 字母级RNN的生成任务

文章目录

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)

【Pytorch官方教程】从零开始自己搭建RNN1 - 字母级RNN的生成任务

预测

给定模型一个首字母,然后模型生成下一个字母,不断重复,直到遇到 “<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

上一篇:React哲学(精读React官方文档—12)


下一篇:java数据类型