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

0 前言

来自官方教程,对于萌新学习用Pytorch做NLP任务有很大的帮助,就翻译过来,顺便自己Mark一下,因为打开官网有时候太慢了,还是看自己写的Blog比较快。另外,之前在做⭐ 李宏毅2020机器学习作业4-RNN:句子情感分类的时候,代码看起来有些难度。之前的几个作业都还能看懂,但是作业4实在跳跃度太大了,就先拿这几个练个手。

这是官方教程中同一个系列的文章,总共有3篇:

  • 第一篇,教你搭建一个字母级别(character-level)的RNN,对名字进行分类,是一个分类的任务。
  • 第二篇,教你搭建一个字母级别(character-level)的RNN,生成名字,是一个自然语言生成的任务。
  • 第三篇,教你搭建一个Seq2Seq的RNN,进行机器翻译,也是一个自然语言生成的任务。Seq2Seq是序列到序列的模型,类似于单词级别(word-level)的RNN。

博主更新完的本系列文章:

这是第二篇。关于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 表示所有字母的数量。这次多加了一个特殊符号 <EOS> 。因为是文本生成,所以需要有一个符号来结束文本生成的过程。我们设定,当生成 <EOS> 的时候,就结束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://stackoverflow.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> ,和 x t x_t xt h t − 1 h_{t-1} ht1 拼接到一起 [ c a t e g o r y , x t , h t − 1 ] [category, x_t, h_{t-1}] [category,xt,ht1]作为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_lettersn_letters 的多分类任务。比如:

  • (“A”, “B”) = ( [ 1 , 0 , 0 , 0 , … , 0 , 0 ] , 1 ) ( [1,0,0,0,\ldots,0,0], 1) ([1,0,0,0,,0,0],1)
  • (“B”, “C”) = ( [ 0 , 1 , 0 , 0 , … , 0 , 0 ] , 2 ) ( [0,1,0,0,\ldots,0,0], 2) ([0,1,0,0,,0,0],2)
  • (“C”, “D”) = ( [ 0 , 0 , 1 , 0 , … , 0 , 0 ] , 3 ) ( [0,0,1,0,\ldots,0,0], 3) ([0,0,1,0,,0,0],3)
  • (“D”, “<EOS>”) = ( [ 0 , 0 , 0 , 1 , … , 0 , 0 ] , 4 ) ( [0,0,0,1,\ldots,0,0], 4) ([0,0,0,1,,0,0],4)
import torch
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print(device)

# category 的 one-hot编码
def categoryTensor(category):
    li = all_categories.index(category)
    tensor = torch.zeros(1, n_categories)
    tensor[0][li] = 1
    return tensor.to(device)

# input 的 one-hot编码 ,从第一个字母到最后一个字母(不包含 EOS)
def inputTensor(line):
    tensor = torch.zeros(len(line), 1, n_letters)
    for li in range(len(line)):
        letter = line[li]
        tensor[li][0][all_letters.find(letter)] = 1
    return tensor.to(device)

# output 的 one-hot编码 ,从第二个字母到最后的 EOS 符
def targetTensor(line):
    letter_indexes = [all_letters.find(line[li])for li in range(1,len(line))]
    letter_indexes.append(n_letters - 1) # EOS的索引
    return torch.LongTensor(letter_indexes).to(device)

与第一篇一样,从训练集中随机采样。

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):拼接得到 [ c a t e g o r y , x t , h t − 1 ] [category, x_t, h_{t-1}] [category,xt,ht1]
  • hidden = self.i2h(input_combined) h t = W h [ c a t e g o r y , x t , h t − 1 ] h_t= W_h[category, x_t, h_{t-1}] ht=Wh[category,xt,ht1]
  • output = self.i2o(input_combined) o t = W o 1 [ c a t e g o r y , x t , h t − 1 ] o_t=W_{o1}[category, x_t, h_{t-1}] ot=Wo1[category,xt,ht1]
  • output_combined = torch.cat((hidden, output), 1) o t ′ = [ h t , o t ] o'_{t} = [h_t, o_t] ot=[ht,ot]
  • output = self.o2o(output_combined) o t = W o 2 o t ′ = W o 2 [ h t , o t ] o_t = W_{o2}o'_{t}=W_{o2}[h_t, o_t] ot=Wo2ot=Wo2[ht,ot]
  • output = self.dropout(output):用 dropout 防止过拟合
  • output = self.softmax(output):用 softmax 把 o t o_t ot 转化成预测字母的概率分布 y t y_t 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:

1m 28s (5000 5%) 2.8628
2m 48s (10000 10%) 3.2724
4m 6s (15000 15%) 1.9587
5m 24s (20000 20%) 2.3308
6m 44s (25000 25%) 2.3424
8m 1s (30000 30%) 3.5255
9m 19s (35000 35%) 1.2786
10m 37s (40000 40%) 1.8138
11m 56s (45000 45%) 2.5911
13m 14s (50000 50%) 2.0981
14m 33s (55000 55%) 3.1198
15m 51s (60000 60%) 2.3281
17m 9s (65000 65%) 2.2316
18m 29s (70000 70%) 3.0411
19m 48s (75000 75%) 3.0554
21m 7s (80000 80%) 2.0455
22m 25s (85000 85%) 2.3849
23m 43s (90000 90%) 2.3806
25m 0s (95000 95%) 1.4392
26m 23s (100000 100%) 2.6739

画图

画出损失函数随着训练的变化情况:

import matplotlib.pyplot as plt
import matplotlib.ticker as ticker

plt.figure()
plt.plot(all_losses)

在这里插入图片描述

预测

给定模型一个首字母,然后模型生成下一个字母,不断重复,直到遇到 “<EOS>”标记符,停止生成。

  • 创建输入类别category的tensor,开始字母的 tensor 和 初始化隐藏层状态 h 0 h_0 h0
  • 用首字母生成一个字符串 output_name
  • 在到达最大输出长度前:
    • 给模型输入当前的字母
    • 模型生成下一个字母,和下一个隐藏层状态
    • 如果生成的字母是 “<EOS>”标记符, 停止生成
    • 如果生成的字母是一个常规的字母,把它加入 output_name,并且继续生成
  • 返回最终生成的名字 output_name
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
  • 8
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

iteapoy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值