从零开始自己搭建RNN【Pytorch文档】1

从零开始自己搭建RNN【Pytorch文档】1

先贴官方教程:https://pytorch.org/tutorials/intermediate/char_rnn_classification_tutorial.html
  参考Blog:https://blog.csdn.net/iteapoy/article/details/106478462

字母级RNN的分类任务

数据下载:https://download.pytorch.org/tutorial/data.zip

从零开始自己搭建RNN【Pytorch文档】1_第1张图片
这次我们只用到 /name 这个文件夹下的18个文件,每个文件以语言命名,格式为 [Language].txt 打开后,里面是该语言中常用的姓/名。

任务说明

输入一个姓名,根据它的拼写,用循环神经网络对它分类,判断它属于哪个语言里的姓名

理论基础

RNN

循环神经网络(Recurrent Neural Network)里面引入了循环体结构
从零开始自己搭建RNN【Pytorch文档】1_第2张图片
x t x_t xt是第 t 步循环时的输入, h t h_t ht是第 t 步循环时的输出,他们都是向量,把它按时序展平变成一般的神经网络那样的单向传播结构。展开后就是一个链状结构:
从零开始自己搭建RNN【Pytorch文档】1_第3张图片
每一个A块里的结构都是一样的,现在的问题是:变量到底应该怎么更新?输入的 x t x_t xt应该如何处理,才能输出 h t h_t ht,图里的 A 内部具体的更新结构如下:
从零开始自己搭建RNN【Pytorch文档】1_第4张图片
流程为:

  • 把上一步输出的 h t − 1 h_{t-1} ht1乘上权重矩阵 W h W_h Wh,变成 W h h t − 1 W_hh_{t-1} Whht1,h是hidden layer
  • 把这一步输入的 x t x_t xt也乘上权重矩阵 W i W_i Wi,变成 W i i t W_ii_{t} Wiit,i是input
  • 把两者相加,得到 W h h t − 1 + W i i t W_hh_{t-1}+W_ii_{t} Whht1+Wiit
  • 经过一个 t a n h tanh tanh函数,就得到这一步的输出: h t = t a n h ( W h h t − 1 + W i i t ) h_t=tanh(W_hh_{t-1}+W_ii_t) ht=tanh(Whht1+Wiit)

反复循环流程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((Whht1+bh)+(Wiit+bi))

这里进行了简化,即令所有的向量 b 都为0。另外,我们在初始化向量 h 0 h_0 h0 的时候,也会把它初始化成全为0的向量

RNN这样一个结构用来处理有前后关联的序列非常有效,因此在自然语言处理里也取得了不错的成绩。因为一句话可以看成是许多词组成的序列,这些词之前有前后文/上下文关系

LSTM

LSTM是长短期记忆网络(LSTM,Long Short-Term Memory),通过三个门(遗忘门、输入门、输出门)的控制,存储短期记忆或长期记忆。它的整体流程是这样:
从零开始自己搭建RNN【Pytorch文档】1_第5张图片
LSTM里的一个A内部的结构变成了这样子:
从零开始自己搭建RNN【Pytorch文档】1_第6张图片
李宏毅老师的简化版容易入门:
从零开始自己搭建RNN【Pytorch文档】1_第7张图片
对同一个输入 x t x_t xt,乘上不同的权重 W f , W i , W , W o W_f,W_i,W,W_o Wf,Wi,W,Wo,得到四个不同的值:

  • z f = W f x t z_f=W_fx_t zf=Wfxt,遗忘门(Forget Gate)的输入
  • z i = W i x t z_i=W_ix_t zi=Wixt,输入门(Input Gate)的输入
  • z = W x t z_=Wx_t z=Wxt,真正的输入
  • z o = W o x t z_o=W_ox_t zo=Woxt,输出门(Output Gate)的输入

从零开始自己搭建RNN【Pytorch文档】1_第8张图片
先来看红色方框圈出来的部分,输入和输入门的更新,它负责判断是否要接受新的输入
从零开始自己搭建RNN【Pytorch文档】1_第9张图片
这一步操作流程:

  • z i z_i zi乘权重 W i W_i Wi,加上偏置向量 b i b_i bi,经过输入门,得到 i t = σ ( W i z i + b i ) i_t=\sigma(W_iz_i+b_i) it=σ(Wizi+bi)
  • z z z乘权重 W c W_c Wc,加上偏置向量 b c b_c bc,经过 t a n h tanh tanh,得到 c t = σ ( W c z + b c ) c_t=\sigma(W_cz+b_c) ct=σ(Wcz+bc)
  • i t i_t it c t c_t ct按元素相乘(Hadamard乘积,运算符 ⊙ \odot ∗ \ast ),得到 i t ∗ c t i_t\ast c_t itct,输入cell

再看蓝色圈出来的部分,是遗忘门的更新,它负责判断是否要更新cell中的值,如果更新了,就要忘记之前的值,写入新的值:
从零开始自己搭建RNN【Pytorch文档】1_第10张图片

  • cell中存放了上一步的存储向量 c t − 1 c_{t-1} ct1
  • z f z_f zf乘以权重 W f W_f Wf,加上偏置向量 b f b_f bf,经过遗忘门,得到 f t = σ ( W f z f + b f ) f_t=\sigma(W_fz_f+b_f) ft=σ(Wfzf+bf)
  • 在cell中把 c t − 1 c_{t-1} ct1 f t f_t ft按元素相乘,然后和 i t ∗ c t i_t\ast c_t itct相加,就变成了新的存储向量
    c t = c t − 1 ∗ f t + i t ∗ c t c_t=c_{t-1}\ast f_t+i_t\ast c_t ct=ct1ft+itct

最后看橙色圈出来的部分,输出门的更新,它负责判断是否要输出最后的值:
从零开始自己搭建RNN【Pytorch文档】1_第11张图片

  • 新的 c t c_t ct通过 t a n h tanh tanh函数,得到 t a n h ( c t ) tanh(c_t) tanh(ct)
  • z o z_o zo乘以权重 W o W_o Wo,加上偏置向量 b o b_o bo,经过输出门,变成 o t = σ ( W o z o + b o ) o_t=\sigma(W_oz_o+b_o) ot=σ(Wozo+bo)
  • t a n h ( c t ) tanh(c_t) tanh(ct) o t o_t ot按元素相乘,得到新的隐藏层状态 h t = o t ∗ t a n h ( c t ) h_t=o_t\ast tanh(c_t) ht=ottanh(ct)
  • 隐藏层状态 h t h_t ht通过softmax函数,得到最后的输出 y t = s o f t m a x ( h t ) y_t=softmax(h_t) yt=softmax(ht)

以上,就是LSTM中某一步的状态更新情况。再回头看这张图:
从零开始自己搭建RNN【Pytorch文档】1_第12张图片
之前, 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} ht1拼在一起,变成一个新向量 [ x t , h t − 1 ] [x_t,h_{t-1}] [xt,ht1],更新公式为:
从零开始自己搭建RNN【Pytorch文档】1_第13张图片
从零开始自己搭建RNN【Pytorch文档】1_第14张图片
从零开始自己搭建RNN【Pytorch文档】1_第15张图片
从零开始自己搭建RNN【Pytorch文档】1_第16张图片
一般称循环神经网络的时候,其实都是在说LSTM

GRU

因为LSTM太复杂,而且容易过拟合,有一个简化版的LSTM,叫做GRU,它把遗忘门和输入门合并成了一个更新门,从三个门减少到了两个门,更新公式如下(省略偏置向量b):
从零开始自己搭建RNN【Pytorch文档】1_第17张图片

one-hot 编码

需要将输入 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 hti2o为输入 x t x_t xt到输出 o t o_t otsoftmax把输出 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} ht1拼接在一起变成[ x t x_t xt, h t − 1 h_{t-1} ht1]
  • hidden = self.i2h(combined):之前基础理论里,把输入[ x t x_t xt, h t − 1 h_{t-1} ht1]乘上权重 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,ht1],在此实际上是通过一个线性的全连接层i2h,它的输入大小为input_size + hidden_size,输出是hidden_size
  • output = self.i2o(combined):把输入的[ x t x_t xt, h t − 1 h_{t-1} ht1]乘上权重 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,ht1],通过一个线性的全连接层i2o,它的输入是input_size + hidden_size,输出是output_size
  • 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__()    #继承父类的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

在训练的每个循环中会执行以下过程:

  • 创建输入tensor和目标tensor
  • 初始化隐藏层状态 h 0 h_0 h0
  • 输入每个字母 x t x_t xt
  • 保存下一个字母需要的隐藏层状态 h t h_t ht
  • 将模型预测的输出 y t y_t yt与目标 y t ∗ y_t^* yt进行对比
  • 梯度反向传播
  • 返回输出和损失函数
#定义损失函数 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')

你可能感兴趣的:(Machine,Lee),python,神经网络,机器学习,人工智能)