前言
有好几个月没搞神经网络代码了,期间也就是回顾了两边之前的文字。
不料,对nn,cnn的理解反而更深入了-_-!。
修改
《零基础学习人工智能—Python—Pytorch学习(四)》,关于Linear的理解是错误的,已修改。
RNN
RNN是Recurrent Neural Network的缩写。中文叫循环神经网络。
RNN是在NN的基础上的扩展。
我们知道在训练NN的时候,要传递一个待训练的tensor。RNN简单来说就是在传递tensor的同时,再传递一个hidden。
这个hidden是哪来的?它是上一轮训练时,返回y_predict的同时,返回的(我们之前写NN只会返回y_predict,现在要多返回一个hidden)。
如果没有上一轮,那hidden就等于空的tensor。
代码实现
定义RNN
class RNN(nn.Module): # implement RNN from scratch rather than using nn.RNN def __init__(self, input_size, hidden_size, output_size): super(RNN, self).__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_tensor, hidden_tensor): combined = torch.cat((input_tensor, hidden_tensor), 1) hidden = self.i2h(combined) #input to hidden=i2h output = self.i2o(combined) #input to output=i2o output = self.softmax(output) return output, hidden def init_hidden(self): return torch.zeros(1, self.hidden_size)
代码分析
init
init函数增加了一个参数hidden_size。我们知道init函数是接受x的列和y_predict的列的,x的列是input,y_predict的列是输出。
现在我们增加了hidden_size,他是hidden层的tensor的列。
我们在使用linear的时候,会把input_size和hidden_size相加,因为,我们后面再前向传播的时候,也会把x的tensor和hidden的tensor组合成一个向量。
forward
forward函数,我们先看return。
return多返回了一个对象——hidden,hidden是一个tensor,是执行linear(i2h)时的output,他也是根据x进行线性变换后的tensor,因为不是最后输出的tensor,所以他叫做hidden,而最后输出的output的tensor叫做y_predict。
我们在看forward的第一行,combined = torch.cat((input_tensor, hidden_tensor), 1)。
这是把tensor变量input_tensor和hidden_tensor给合并成了一个tensor,使用的是torch.cat函数。参数1是指按列的方向合并。
即input_tensor如果是(3,5),hidden_tensor是(3,2),我们将得到一个(3,7),相当与把hidden的每一行的值取出来,添加进相同行的input_tensor里。
所以我们上面的init里,初始化linear的第一个参数(x的列)是input_size + hidden_size。
定义工具函数
import io import os import unicodedata import string import glob import torch import random # alphabet small + capital letters + " .,;'" ALL_LETTERS = string.ascii_letters + " .,;'" N_LETTERS = len(ALL_LETTERS) # Turn a Unicode string to plain ASCII, thanks to https://stackoverflow.com/a/518232/2809427 def unicode_to_ascii(s): return ''.join( c for c in unicodedata.normalize('NFD', s) if unicodedata.category(c) != 'Mn' and c in ALL_LETTERS ) def load_data(): # Build the category_lines dictionary, a list of names per language category_lines = {} all_categories = [] def find_files(path): return glob.glob(path) # Read a file and split into lines def read_lines(filename): lines = io.open(filename, encoding='utf-8').read().strip().split('n') return [unicode_to_ascii(line) for line in lines] for filename in find_files('data/names/*.txt'): category = os.path.splitext(os.path.basename(filename))[0] all_categories.append(category) lines = read_lines(filename) category_lines[category] = lines return category_lines, all_categories """ To represent a single letter, we use a “one-hot vector” of size <1 x n_letters>. A one-hot vector is filled with 0s except for a 1 at index of the current letter, e.g. "b" = <0 1 0 0 0 ...>. To make a word we join a bunch of those into a 2D matrix <line_length x 1 x n_letters>. That extra 1 dimension is because PyTorch assumes everything is in batches - we’re just using a batch size of 1 here. """ # Find letter index from all_letters, e.g. "a" = 0 def letter_to_index(letter): return ALL_LETTERS.find(letter) # Just for demonstration, turn a letter into a <1 x n_letters> Tensor def letter_to_tensor(letter): tensor = torch.zeros(1, N_LETTERS) tensor[0][letter_to_index(letter)] = 1 return tensor # Turn a line into a <line_length x 1 x n_letters>, # or an array of one-hot letter vectors def line_to_tensor(line): tensor = torch.zeros(len(line), 1, N_LETTERS) for i, letter in enumerate(line): tensor[i][0][letter_to_index(letter)] = 1 return tensor def random_training_example(category_lines, all_categories): def random_choice(a): random_idx = random.randint(0, len(a) - 1) return a[random_idx] category = random_choice(all_categories) line = random_choice(category_lines[category]) category_tensor = torch.tensor([all_categories.index(category)], dtype=torch.long) line_tensor = line_to_tensor(line) return category, line, category_tensor, line_tensor if __name__ == '__main__': print(ALL_LETTERS) print(unicode_to_ascii('Ślusàrski')) category_lines, all_categories = load_data() print(category_lines['Italian'][:5]) print(letter_to_tensor('J')) # [1, 57] print(line_to_tensor('Jones').size()) # [5, 1, 57]
代码分析
string.ascii_letters 是一个字符串常量,它包含了所有 ASCII 字母,即小写字母 'abcdefghijklmnopqrstuvwxyz' 和大写字母 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 的组合
letter_to_tensor函数:是把入参的字母转成tensor,tensor的shape是(1,57),后面我们使用该向量做神经网络的输入。
one-hot 向量
letter_to_tensor函数输出是tensor被称为one-hot向量,直译就是一个热点向量,即向量里就一个值是有意义的值。
【one-hot向量】就是一行N列的向量,列的值为1时,该列的位置,就是字母在字符串的位置。
如果我们有以下字母表,共27个字符(26个字母 + 空格):
ALL_LETTERS = "abcdefghijklmnopqrstuvwxyz "
转成one-hot 向量,就会如下图模式分配。
load_data函数:查询一个文件夹下的全部文件,然后把文件名全部取出来,组成集合,然后把文件的内容取出来,结合文件的名字,组成字典。
glob.glob:系统函数,查询指定目录下的指定文件。
data.names文件夹如下:
RNN前向传播
category_lines, all_categories = load_data() n_categories = len(all_categories) n_hidden = 128 print("N_LETTERS",N_LETTERS) rnn = RNN(N_LETTERS, n_hidden, n_categories) # RNN(57,128,18) 18是18个国家即18个类别 input_tensor = letter_to_tensor('A') # 把A转成tensor(1,57) hidden_tensor = rnn.init_hidden() output, next_hidden = rnn(input_tensor, hidden_tensor) input_tensor = line_to_tensor('Albert') hidden_tensor = rnn.init_hidden() output, next_hidden = rnn(input_tensor[0], hidden_tensor) def category_from_output(output): category_idx = torch.argmax(output).item() return all_categories[category_idx] print(category_from_output(output))
代码分析
torch.argmax(output).item()是取出tensor里的最大值,其最大值的索引。
这里,我们通过整个RNN的前向传播,得到了一个output,即通过线性转换,我们得到y_predict,然后我们找到y_predict里最大的值的索引,我们认为,我们这个索引对应国家集合里对应索引的国家名,就是我们预测的我们输入的名字(实际输入的是字母)的所属国家。
完整代码:
import threading import torch import torch.nn as nn from utils import ALL_LETTERS, N_LETTERS from utils import load_data, letter_to_tensor, line_to_tensor, random_training_example def category_from_output(output): category_idx = torch.argmax(output).item() return all_categories[category_idx] class RNN(nn.Module): # implement RNN from scratch rather than using nn.RNN def __init__(self, input_size, hidden_size, output_size): super(RNN, self).__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_tensor, hidden_tensor): combined = torch.cat((input_tensor, hidden_tensor), 1) hidden = self.i2h(combined) #input to hidden=i2h output = self.i2o(combined) #input to output=i2o output = self.softmax(output) return output, hidden def init_hidden(self): return torch.zeros(1, self.hidden_size) category_lines, all_categories = load_data() n_categories = len(all_categories) n_hidden = 128 print("N_LETTERS",N_LETTERS) rnn = RNN(N_LETTERS, n_hidden, n_categories) # RNN(57,128,18) 18是18个国家即18个类别 input_tensor = letter_to_tensor('A') # 把A转成tensor(1,57) hidden_tensor = rnn.init_hidden() output, next_hidden = rnn(input_tensor, hidden_tensor) print(category_from_output(output)) input_tensor = line_to_tensor('Albert') hidden_tensor = rnn.init_hidden() output, next_hidden = rnn(input_tensor[0], hidden_tensor) print(category_from_output(output))
使用RNN训练及预测
定义训练函数
def train(line_tensor, category_tensor): hidden = rnn.init_hidden() for i in range(line_tensor.size()[0]): output, hidden = rnn(line_tensor[i], hidden) loss = criterion(output, category_tensor) optimizer.zero_grad() loss.backward() optimizer.step() return output, loss.item()
代码分析
入参line_tensor是一个tesnor,shape是(n,1,57),即,是一个字符串转成n个one-hot向量。他是一个名字的字符串。
入参category_tensor是真实的y值,这里他的shape是tensor(1),即,这个名字的真实国家的索引。
然后我们通过循环执行output, hidden = rnn(line_tensor[i], hidden),把每个字母都在rnn里进行线性变换,这里每次都会把hidden返回,然后下次调用rnn,会把hideen传递过去,形成循环网络。
最后输出的output是个1行18列的tensor,1行是因为我们输出的x是1行,18列是因为我们创建rnn时,指定了18列的输出。
然后我们执行损失函数,loss = criterion(output, category_tensor),计算损失值,损失函数有很多种。(我们可以简单这样理解损失函数的运行逻辑,上面通过线性计算把X转成了tensor(1,18),这个tensor是y_predict,现在我们计算y_predicd与真y的差,在把结果平方,在求平均数,反正就是计算出一个数,这个数如果是2,3,那就代表我们的模型错的很离谱)
损失值求完,我们就可以使用损失值进行逆向传播了loss.backward()。这里loss是触发逆向传播的,也就是计算梯度,也就是求偏导数,之所以是loss触发,是因为loss本身是通过函数计算出来的,而我们求梯度,是对这个求loss的函数偏导,但是loss值,并不参与求偏导,所以loss是触发逆向传播的启点。
完整代码
import threading import torch import torch.nn as nn import matplotlib.pyplot as plt from utils import ALL_LETTERS, N_LETTERS from utils import load_data, letter_to_tensor, line_to_tensor, random_training_example def category_from_output(output): category_idx = torch.argmax(output).item() return all_categories[category_idx] class RNN(nn.Module): # implement RNN from scratch rather than using nn.RNN def __init__(self, input_size, hidden_size, output_size): super(RNN, self).__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_tensor, hidden_tensor): combined = torch.cat((input_tensor, hidden_tensor), 1) hidden = self.i2h(combined) #input to hidden=i2h output = self.i2o(combined) #input to output=i2o output = self.softmax(output) return output, hidden def init_hidden(self): return torch.zeros(1, self.hidden_size) category_lines, all_categories = load_data() # print("category_lines",category_lines) # print("all_categories",all_categories) n_categories = len(all_categories) n_hidden = 128 rnn = RNN(N_LETTERS, n_hidden, n_categories) criterion = nn.NLLLoss() learning_rate = 0.005 optimizer = torch.optim.SGD(rnn.parameters(), lr=learning_rate) def train(line_tensor, category_tensor): hidden = rnn.init_hidden() for i in range(line_tensor.size()[0]): output, hidden = rnn(line_tensor[i], hidden) loss = criterion(output, category_tensor) optimizer.zero_grad() loss.backward() optimizer.step() return output, loss.item() current_loss = 0 all_losses = [] plot_steps, print_steps = 1000, 5000 n_iters = 100000 for i in range(n_iters): category, line, category_tensor, line_tensor = random_training_example(category_lines, all_categories) # category=国家名 line=这个国家下的随机一个名字 category_tensor=国家名索引值转换成的tensor(1) line_tensor=名字转换成的tensor(n,1,57) output, loss = train(line_tensor, category_tensor) current_loss += loss if (i+1) % plot_steps == 0: all_losses.append(current_loss / plot_steps) current_loss = 0 if (i+1) % print_steps == 0: guess = category_from_output(output) correct = "CORRECT" if guess == category else f"WRONG ({category})" print(f"{i+1} {(i+1)/n_iters*100} {loss:.4f} {line} / {guess} {correct}") def show(): plt.figure() plt.plot(all_losses) plt.show() show() def predict(input_line): print(f"n> {input_line}") with torch.no_grad(): line_tensor = line_to_tensor(input_line) hidden = rnn.init_hidden() for i in range(line_tensor.size()[0]): output, hidden = rnn(line_tensor[i], hidden) guess = category_from_output(output) print("guess:",guess) while True: sentence = input("Input:") if sentence == "quit": break predict(sentence)
到此,Python—Pytorch学习-RNN(一)就介绍完了。
参考网站:https://github.com/patrickloeber/pytorch-examples/tree/master/rnn-name-classification
注:此文章为原创,任何形式的转载都请联系作者获得授权并注明出处!
若您觉得这篇文章还不错,请点击下方的【推荐】,非常感谢!