先贴官方教程:https://pytorch.org/tutorials/intermediate/char_rnn_classification_tutorial.html
参考Blog:https://blog.csdn.net/iteapoy/article/details/106478462
数据下载:https://download.pytorch.org/tutorial/data.zip
这次我们只用到 /name
这个文件夹下的18个文件,每个文件以语言命名,格式为 [Language].txt
打开后,里面是该语言中常用的姓/名。
输入一个姓名,根据它的拼写,用循环神经网络对它分类,判断它属于哪个语言里的姓名
循环神经网络(Recurrent Neural Network)里面引入了循环体结构
x t x_t xt是第 t 步循环时的输入, h t h_t ht是第 t 步循环时的输出,他们都是向量,把它按时序展平变成一般的神经网络那样的单向传播结构。展开后就是一个链状结构:
每一个A
块里的结构都是一样的,现在的问题是:变量到底应该怎么更新?输入的 x t x_t xt应该如何处理,才能输出 h t h_t ht,图里的 A 内部具体的更新结构如下:
流程为:
反复循环流程1-4,就是一个最简单的RNN
在这个简单RNN中, W h , W i W_h,W_i Wh,Wi并没有与时间步相关的下标,在训练过程中,这两个矩阵中的数字会发生变化,因为它们是模型要学习的“参数”始终只有这两个矩阵。
另外,你可能会看到一种带有偏置向量 b 的更新方式:
h t = t a n h ( ( W h h t − 1 + b h ) + ( W i i t + b i ) ) h_t=tanh((W_hh_{t-1}+b_h)+(W_ii_t+b_i)) ht=tanh((Whht−1+bh)+(Wiit+bi))
这里进行了简化,即令所有的向量 b 都为0。另外,我们在初始化向量 h 0 h_0 h0 的时候,也会把它初始化成全为0的向量
RNN这样一个结构用来处理有前后关联的序列非常有效,因此在自然语言处理里也取得了不错的成绩。因为一句话可以看成是许多词组成的序列,这些词之前有前后文/上下文关系
LSTM是长短期记忆网络(LSTM,Long Short-Term Memory),通过三个门(遗忘门、输入门、输出门)的控制,存储短期记忆或长期记忆。它的整体流程是这样:
LSTM里的一个A
内部的结构变成了这样子:
李宏毅老师的简化版容易入门:
对同一个输入 x t x_t xt,乘上不同的权重 W f , W i , W , W o W_f,W_i,W,W_o Wf,Wi,W,Wo,得到四个不同的值:
先来看红色方框圈出来的部分,输入和输入门的更新,它负责判断是否要接受新的输入
这一步操作流程:
再看蓝色圈出来的部分,是遗忘门的更新,它负责判断是否要更新cell中的值,如果更新了,就要忘记之前的值,写入新的值:
最后看橙色圈出来的部分,输出门的更新,它负责判断是否要输出最后的值:
以上,就是LSTM中某一步的状态更新情况。再回头看这张图:
之前, z f , z i , z , z o z_f,z_i,z,z_o zf,zi,z,zo都是用 x t x_t xt乘以不同的权重得到的,但是光凭 x t x_t xt 还不能传递足够多的信息,于是把 x t x_t xt和上一步输出的隐藏层状态 h t − 1 h_{t-1} ht−1拼在一起,变成一个新向量 [ x t , h t − 1 ] [x_t,h_{t-1}] [xt,ht−1],更新公式为:
一般称循环神经网络的时候,其实都是在说LSTM
因为LSTM太复杂,而且容易过拟合,有一个简化版的LSTM,叫做GRU,它把遗忘门和输入门合并成了一个更新门,从三个门减少到了两个门,更新公式如下(省略偏置向量b):
需要将输入 x t x_t xt转化为一个特征向量, x t x_t xt可以是字母的特征向量,或者单词的特征向量,或者句子的特征向量
one-hot 编码是一个长度为 n 的向量,只有 1 个数字是1,其它的 n − 1 个数字都是0。one-hot 编码使得每个字母在它们各自的维度上,与其它字母是独立的
比如一个单词 “apple”,就分别对a、p、p、l、e 编码,作为输入,LSTM需要循环5次
而单词级(word-level)RNN就是把整个单词 “apple” 编码成一个向量。通常,对单词的编码用于Seq2Seq模型,即处理的是一个序列 “An apple a day keeps the doctor away”
首先把所有的/name/[language].txt
文件读取出来
n_letters
表示所有字母的数量
unicodeToAscii()
函数将输入转换为英文字母
from __future__ import unicode_literals, print_function, division
from io import open
import glob
import os
import unicodedata
import string
def findFiles(path): return glob.glob(path) #glob(path)查询目录下文件
def unicodeToAscii(str): #记下即可
return ''.join(
c for c in unicodedata.normalize('NFD', str)
if unicodedata.category(c) != 'Mn'
and c in all_letters
)
print(findFiles('MLdata/data/names/*.txt')) #查找以.txt结尾的文件
all_letters = string.ascii_letters + '.,;' #ascii_letters生成所有字母
n_letters = len(all_letters)
print(unicodeToAscii('Ślusàrski'))
out
[‘MLdata/data/names\Arabic.txt’, ‘MLdata/data/names\Chinese.txt’, ‘MLdata/data/names\Czech.txt’, ‘MLdata/data/names\Dutch.txt’, ‘MLdata/data/names\English.txt’, ‘MLdata/data/names\French.txt’, ‘MLdata/data/names\German.txt’, ‘MLdata/data/names\Greek.txt’, ‘MLdata/data/names\Irish.txt’, ‘MLdata/data/names\Italian.txt’, ‘MLdata/data/names\Japanese.txt’, ‘MLdata/data/names\Korean.txt’, ‘MLdata/data/names\Polish.txt’, ‘MLdata/data/names\Portuguese.txt’, ‘MLdata/data/names\Russian.txt’, ‘MLdata/data/names\Scottish.txt’, ‘MLdata/data/names\Spanish.txt’, ‘MLdata/data/names\Vietnamese.txt’]
Slusarski
文件命名[language].txt
中,language
是类别category,把每个文件打开,存入一个数组lines = [name1,...]
,建立一个词典category_lines = {language: lines}
#文件命名`[language].txt`中,`language`是类别category,把每个文件打开,
#存入一个数组`lines = [name1,...]`,建立一个词典`category_lines = {language: lines}`
category_lines = {
}
all_categories = []
def readLines(filename):
lines = open(filename, encoding = 'utf-8').read().strip().split('\n') #先read读取,strip去除首尾空格,split以'\n'分隔
return [unicodeToAscii(line) for line in lines] #将lines中的name读出放入字母标准化函数并输出标准的name
for filename in findFiles('MLdata/data/names/*.txt'):
category = os.path.splitext(os.path.basename(filename))[0] #os.path.basename(path)返回path最后的文件名,os.path.splitext() 将文件名和扩展名分开
all_categories.append(category)
lines = readLines(filename) #readLines一次性读取整个文件;自动将文件内容分析成一个行的列表
category_lines[category] = lines #把language与名字组合放在词典里
n_categories = len(all_categories)
print(all_categories)
print(category_lines['Italian'])
out
[‘Arabic’, ‘Chinese’, ‘Czech’, ‘Dutch’, ‘English’, ‘French’, ‘German’, ‘Greek’, ‘Irish’, ‘Italian’, ‘Japanese’, ‘Korean’, ‘Polish’, ‘Portuguese’, ‘Russian’, ‘Scottish’, ‘Spanish’, ‘Vietnamese’]
[‘Abandonato’, ‘Abatangelo’, ‘Abatantuono’, ‘Abate’, ‘Abategiovanni’, ‘Abatescianni’, ‘Abba’, ‘Abbadelli’, ‘Abbascia’, ‘Abbatangelo’, ‘Abbatantuono’, ‘Abbate’, ‘Abbatelli’, ‘Abbaticchio’, ‘Abbiati’, ‘Abbracciabene’, ‘Abbracciabeni’, ‘Abelli’, ‘Abello’, ‘Abrami’, ‘Abramo’, ‘Acardi’, ‘Accardi’, ‘Accardo’, ‘Acciai’, ‘Acciaio’, ‘Acciaioli’, ‘Acconci’, ‘Acconcio’, ‘Accorsi’, ‘Accorso’, ‘Accosi’, ‘Accursio’, ‘Acerbi’, …
接下来,就是要对字母进行one-hot编码,转成 tensor
假设字母表中的字母数量为n_letters
,一个字母的向量就是<1 x n_letters>
维,只有其中1维是1,其余都是0
一个长度为line_length
的单词,它的向量维度为
维
按照batch来训练,设定一个单词为一组batch的话,单词的向量维度就是
import torch
device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
#返回字母letter的索引 index
def letterToIndex(letter):
return all_letters.find(letter)
#把一个字母编码成tensor
def letterToTensor(letter):
tensor = torch.zeros(1, n_letters) #创建 1 x n_letters 的张量
#把字母letter 的索引定为1,其他为0
tensor[0][letterToIndex(letter)] = 1
return tensor.to(device)
#把一个单词编码为tensor
def lineToTensor(line):
tensor = torch.zeros(len(line), 1, n_letters)
#遍历单词中所有字母,对每个字母letter的索引设为1,其他为0
for li, letter in enumerate(line):
tensor[li][0][letterToIndex(letter)] = 1
return tensor.to(device)
print(letterToTensor('J'))
print(lineToTensor('Jones').size())
out
tensor([[0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1.,
0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
0.]], device=‘cuda:0’)
torch.Size([5, 1, 55])
一个简单的RNN,设计为两层的结构,i2h
( i n p u t t o h i d d e n input to hidden input to hidden)为输入 x t x_t xt到隐藏层 h t h_t ht,i2o
为输入 x t x_t xt到输出 o t o_t ot,softmax
把输出 o t o_t ot变成预测值 y t y_t yt,在此用的是LogSoftmax
函数,对应的损失函数为NLLLoss()
,若是一般的Softmax
函数,对应的损失函数就是交叉熵损失CrossEntropy()=Log(NLLLoss())
在此设定隐藏层的向量维度为128维
模型真正运行步骤在forward()
函数中,它的输入为 x t x_t xt,隐藏层为 h t h_t ht:
combined = torch.cat((input, hidden), 1)
:把 x t x_t xt和上一步的 h t − 1 h_{t-1} ht−1拼接在一起变成[ x t x_t xt, h t − 1 h_{t-1} ht−1]hidden = self.i2h(combined)
:之前基础理论里,把输入[ x t x_t xt, h t − 1 h_{t-1} ht−1]乘上权重 W h W_h Wh变成新的隐藏层 h t = W h [ x t , h t − 1 ] h_t=W_h[x_t,h_{t-1}] ht=Wh[xt,ht−1],在此实际上是通过一个线性的全连接层i2h
,它的输入大小为input_size + hidden_size
,输出是hidden_size
output = self.i2o(combined)
:把输入的[ x t x_t xt, h t − 1 h_{t-1} ht−1]乘上权重 W o W_o Wo,得到输出 o t = W o [ x t , h t − 1 ] o_t=W_o[x_t,h_{t-1}] ot=Wo[xt,ht−1],通过一个线性的全连接层i2o
,它的输入是input_size + hidden_size
,输出是output_size
output = self.softmax(output)
:通过softmax函数,把 o t o_t ot转换为预测值 y t y_t ytimport torch.nn as nn
class RNN(nn.Module):
#初始化定义每一层的输入大小和输出大小
def __init__(self, input_size, hidden_size, output_size):
super(RNN, self).__init__() #继承父类的init方法
self.hidden_size = hidden_size
self.i2h = nn.Linear(input_size + hidden_size, hidden_size) #设置网络中的全连接层
self.i2o = nn.Linear(input_size + hidden_size, output_size)
self.softmax = nn.LogSoftmax(dim = 1)
#前向传播过程
def forward(self, input, hidden):
combined = torch.cat((input, hidden), 1) #按维数1拼接(横着拼)
hidden = self.i2h(combined)
output = self.i2o(combined)
output = self.softmax(output)
return output, hidden
#初始化隐藏层状态 h0
def initHidden(self):
return torch.zeros(1, self.hidden_size).to(device)
n_hidden = 128
rnn = RNN(n_letters, n_hidden, n_categories)
rnn = rnn.to(device)
#输入字母A测试
input = letterToTensor('A')
hidden = torch.zeros(1, n_hidden).to(device)
output, next_hidden = rnn(input, hidden)
print(output)
#输入名字Albert的第一个字母A测试
input = lineToTensor('Albert')
hidden = torch.zeros(1, n_hidden).to(device)
output, next_hidden = rnn(input[0], hidden)
print(output)
定义一个函数categoryFromOutput()
可以把 y t y_t yt转换为对应的类别,用Tensor.topk
选出概率最大的那个概率的下标category_i
,就是 y t y_t yt的类别
def categoryFromOutput(output):
top_n, top_i = output.topk(1)
category_i = top_i[0].item()
return all_categories[category_i], category_i
print(categoryFromOutput(output))
接下来训练模型,随机采样一部分数据进行训练
用randomChoice()
从全部数据中随机采样,先采样得到类别category
,再从类别中随机采样,得到姓名line
randomTrainingExample()
将采样得到的category-line
对变成tensor
#训练
import random
def randomChoice(l):
return l[random.randint(0, len(l)-1)]
def randomTrainingExample():
category = randomChoice(all_categories) #采样得到category
line = randomChoice(category_lines[category]) #从category中采样得到line
category_tensor = torch.tensor([all_categories.index(category)], dtype = torch.long).to(device)
line_tensor = lineToTensor(line)
return category, line, category_tensor, line_tensor
for i in range(10):
category, line, category_tensor, line_tensor = randomTrainingExample()
print('category = ', category, '/ line = ', line)
定义损失函数为NLLLoss()
,学习率0.005
在训练的每个循环中会执行以下过程:
#定义损失函数 NLLLoss
criterion = nn.NLLLoss()
learning_rate = 0.005
def train(category_tensor, line_tensor):
hidden = rnn.initHidden()
rnn.zero_grad()
#RNN的循环
for i in range(line_tensor.size()[0]):
output, hidden = rnn(line_tensor[i], hidden)
loss = criterion(output, category_tensor)
loss.backward()
#更新参数
for p in rnn.parameter():
p.data.add_(p.grad.data, alpha =- learning_rate)
return output, loss.item()
下面正式开始训练模型
timeSince()
计算出训练时间, 总共训练n_iters
次,每次用1个样本进行训练,每print_every
次打印当前的训练损失,每plot_every
次把损失保存到all_losses
数值中,方便画图
import time
import math
n_iters = 100000
print_every = 5000
plot_every = 1000
current_loss = 0
all_loss = []
def timeSince(since):
now = time.time()
s = now - since
return '%dm %ds' % (s//60, s%60)
start = time.time()
for iter in range(1, n_iters + 1):
category, line, category_tensor, line_tesor = randomTrainingExample()
output, loss = train(category_tensor, line_tensor)
current_loss += loss
if iter % print_every == 0:
guess, guess_i = categoryFromOutput(output)
current = '√' if guess == category else 'x(%s)' % category
print('%d %d%% (%s) %.4f %s / %s %s' % (iter, iter/n_iters*100, timeSince(start), loss, line, guess, correct))
if iter % plot_every == 0:
all_losses.append(current_loss/plot_every)
current_loss = 0
#画图
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
plt.figure()
plt.plot(all_losses)
为了看模型在各个分类上的预测情况,画出18国语言的混淆矩阵,每一行都是真实语言,每一列都是预测语言,用函数evaluate()
来计算混淆矩阵,evaluate()
和train()
非常相似,但是不需要反向传播
#绘制混淆矩阵
confusion = torch.zeros(n_categories, n_categories)
n_confusion = 10000
def evaluate(line_tensor):
hidden = rnn.initHidden()
for i in range(line_tensor.size()[0]):
output, hidden = rnn(line_tensor[i], hidden)
return output
for i in range(n_confusion):
category, line, category_tensor, line_tensor = randomTrainExample()
output = evaluate(line_tensor)
guess, guess_i = categoryFromOutput(output)
category_i = all_categories.index(category)
confusion[category_i][guess_i] += 1
for i in range(n_categories):
confusion[i] = confusion[i] / confusion[i].sum()
fig = plt.figure()
ax = fig.add_subplot(111)
cax = ax.matshow(confusion.numpy())
fig.colorbar(cax)
ax.set_xticklabels([''] + all_categories, rotation = 90)
ax.set_yticklabels([''] + all_categories)
ax.xaxis.set_major_locator(ticker.MultipleLocator(1))
ax.yaxis.set_major_locator(ticker.MultipleLocator(1))
plt.show
两种语言连线处的正方形颜色越偏暖色,表示两种语言的姓名越相似
对于每个名字input_line
,每次预测n_predictions = 3
种最有可能的类别,并输出他们对应的概率
#预测
def predict(input_line, n_predictions = 3):
print('\n > %s' % input_line)
with torch.no_grad():
output = evaluate(lineToTensor(input_line))
topv, topi = output.topk(n_predictions, 1, True)
predictions = []
for i in range(n_predictions):
value = topv[0][i].item()
category_index = topi[0][i].item()
print('(%.2f) %s' % (value, all_catrgories[category_index]))
predictions.append([value, all_categories[category_index]])
print('Dovesky')
print('Jackson')
print('Satoshi')