赞
踩
在前面的章节里,已经给大家介绍了什么是RNN网络的进阶型——LSTM网络的基本知识,如果不清楚的同学请移步到《Pytorch与深度学习 —— 10. 什么是长短期记忆网络》。在《Pytorch与深度学习 —— 9. 使用 RNNCell 做文字序列的转化之 RNN 入门篇》 这篇文章里,我提前做了一些简单的铺垫,例如独热向量等基础知识后,现在我们就正式开始回答在介绍RNN网络模型一开始便提到的姓名分类问题。
我们现在有这样的一组数据集,它是按照拉丁文字进行拼写的来自不同国家的常见姓氏,如果打开这个数据集,可以发现它大概是这样
Input | Output |
---|---|
Abbas | English |
Addams | English |
Brooks | English |
Muirchertach | Irish |
Neil | Irish |
Ha | Korean |
… | … |
数据集我已经放在了CSDN的下载里,如果有需要的同学也可以自己去下载。
在我们这个应用中,我们要考虑的是,当输入一个新的姓名后,比如 ‘Abbas’ 后,我们的程序能否判断出它是一个英语姓氏。
回顾问题后,现在我们要来做一个读取数据的简单程序,把在文本里的姓氏,按照所在 { 语言 : [姓氏] } 这种字典-列表的形式导入到程序里。
# ASCII codes all_letters = string.ascii_letters + " .,;'" def find_files(path): return glob.glob(path) 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 read_lines(filename): lines = open(filename, encoding='utf-8').read().strip().split('\n') return [unicode_to_ascii(line) for line in lines] def load_data(path): for filename in find_files(path): category = os.path.splitext(os.path.basename(filename))[0] language_list.append(category) lines = read_lines(filename) names_dictionary[category] = lines
我们主要依靠的是这几段代码,它们的作用就是从文本中依次读取每一个名字,然后把名字由UTF-8 转码成ASCII,然后以存储在前面我提到过的 {语言: [姓氏]} 这样的字典-列表结构中。
为了让程序能够理解数据集,我们需要对这些字符串数据进行一定程度的编码。One-Hot-Vector 我在前面的文章里已经解释过了,所以在这里不做过多的重复。
这里只做一些必要的补充性介绍。
我们已经通过如下的代码,创建出了一个新的用于编码的字符序列,这个序列包括一些特定的符号(在西班牙语、葡萄牙语等传统拉丁语族国家才有的重音符号)。
all_letters = string.ascii_letters + " .,;’"
比方说我们要编码一个名叫 ‘abc’ 的姓名,那么每一个字符对应一个长度为57的One-Hot向量。然后按照顺序进行输出后,结果应该是
tensor([[[1., 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., 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., 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., 0.]],
[[0., 0., 1., 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., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
0., 0., 0., 0., 0., 0.]]])
由 ‘abc’ 所转化成的用于网络输入的张量维度就是 (3, 1, 57),也就是我在前面章节介绍过的对于NLP方向来说,pytorch接受的数据默认维度即:
( L , N , H i n ) ≈ ( S e q u e n c e , B a t c h , F e a t u r e s ) (L, N, H_{in}) \approx (Sequence, Batch, Features) (L,N,Hin)≈(Sequence,Batch,Features)
Batch
一个Batch对应一个单词,比如上面提到的 ‘abc’;
Sequence
即这个单词包含有多少个字符,对于单词 ‘abc’ 来说,它包含3个字符;
Features
即每一个字符所对应的 One-Hot 编码。
需要注意的地方
这里存在一个新手很容易犯的问题,就是对于我们的数据来说,有单词 ‘Muirchertach’ 长度为12, 也有单词像 ‘Ha’ 这样只有长度为2的,对于可执行并行计算的神经网络来说,如果数据长度不统一,那么不仅导致网络处理效率会降低,而且我们实际处理起来也特别麻烦。所以我们需要对这些数据做一个类似 padding 的操作。
这个概念在很多方面都有提到或者广泛使用,如果是第一次学习计算机或者数据分析的朋友,填充简而言之就是下面这个列表表示的意思。
原始列表 | 填充后至8位长 |
---|---|
[‘a’, ‘b’, ‘c’] | [‘a’, ‘b’, ‘c’, 0, 0, 0, 0, 0] |
[‘h’, ‘e’, ‘l’, ‘l’, ‘o’] | [‘h’, ‘e’, ‘l’, ‘l’, ‘o’, 0, 0, 0] |
理解这些基本概念后,我们就可以使用代码实现这个过程。
def line_to_one_hot_tensor(line, max_padding=0):
"""
Turn a line into a one-hot based tensor (character, one-hot-vector)
"""
if max_padding >= len(line):
tensor = torch.zeros(max_padding, len(all_letters))
else:
tensor = torch.zeros(len(line), len(all_letters))
for idx, letter in enumerate(line):
tensor[idx][_letter_to_index(letter)] = 1
return tensor
这个代码片段会把比如 ‘abc’ 这样的单词转换为 (Sequence, Features) 这样结构的独热向量表示的张量。之所以没有直接转换为 (L, N, H)这样的结构,是因为我们在读取数据的时候可能一次性要读取很多个不同的单词,所以得到的单词组,比如 [‘Abbas’, ‘Addams’, …] 这样的数组,就可以通过下面这段代码,再转换成 (L、N、H)的结构了。
from torch.nn.utils.rnn import pad_sequence
def concatenate_tensors(tensor_list):
return pad_sequence(tensor_list)
def to_one_hot_based_tensor(surnames: list, padding=20):
tensors = []
for name in surnames:
tensor = line_to_one_hot_tensor(name, padding)
tensors.append(tensor)
return concatenate_tensors(tensors)
我们提到过,通过神经元网络输出的结果,其实是个概率。比如通过网络输入的Features是这样的一组 One-Hot 向量
I
n
p
u
t
→
=
{
(
1
,
0
,
0
,
0
)
(
1
,
0
,
0
,
0
)
(
0
,
1
,
0
,
0
)
(
0
,
0
,
1
,
0
)
\overrightarrow{Input} = \left\{
经过我们的网络处理后,输出的 Output 是对应的每一个标签的可能性:
O
u
t
p
u
t
→
=
{
l
a
b
e
l
.
1
0.15
l
a
b
e
l
.
2
0.35
l
a
b
e
l
.
3
0.1
l
a
b
e
l
.
4
0.4
\overrightarrow{Output} = \left\{
然后经过比如交叉熵进行比对的时候,我们告诉这个网络输出的值其实应该是 label 2, 你给出的 label 4 是错误的,所以网络会根据我们告诉它的情况,执行反馈计算的时候调整 label 2 和 label 4的权重。
因此,对于我们这个例子来说,我们就需要把 [‘English’, ‘Irish’, ‘French’, …] 这一类字符标签,转化成 [0, 1, 2, 3, 4, 5, …] 这样的形式,所以其实还是挺简单的。
def line_to_index(line: str, data_list: list):
"""
Turn a line into an index based from dataset
"""
return data_list.index(line)
def to_simple_tensor(languages):
indices = []
for lang in languages:
index = line_to_index(lang, languages)
indices.append(index)
return torch.tensor(indices)
所以我们用到了这样两段极为简单的代码,帮助我们转化标签。
数据加载这里,由于我们使用的是自己的数据,所以没法直接用 Pytorch 提供的 DataLoader,但是我们可以重载名为Dataset的类。
from torch.utils.data import Dataset class MyNameDataset(Dataset): def __init__(self, dict_data: dict): self.x_data = [] self.y_data = [] self.languages = [] for lang, names in dict_data.items(): for name in names: self.x_data.append(name) self.y_data.append(lang) self.languages.append(lang) def __len__(self): return len(self.x_data) def __getitem__(self, item): return self.x_data[item], self.y_data[item]
我们把 {‘语言’: [姓名]} 这个字典-列表类型输入这个重载类后,再加载到DataLoader里,就可以根据需要输出我们想要的 {标签:姓名} 姓名对了。
比如我们通过DataLoader,让它一次性抓取10条数据,输出的结果就是这样的
[(‘Durdin’, ‘Guliev’, ‘Palmer’, ‘Gerhard’, ‘Timpe’, ‘Jelvakov’, ‘Seighin’, ‘Neverov’, ‘Babayants’, ‘Robishaw’), (‘Russian’, ‘Russian’, ‘English’, ‘German’, ‘Czech’, ‘Russian’, ‘Irish’, ‘Russian’, ‘Russian’, ‘English’)]
我们的这个模型比较简单,用到了一层LSTM作为主要的数据处理,以及一层线性层做最终的输出。
class LSTMModel(torch.nn.Module): def __init__(self, input_size, hidden_size, output_size, batch_size, sequence_size, num_layers=1): super().__init__() self.input_size = input_size self.hidden_size = hidden_size self.num_layers = num_layers self.output_size = output_size self.batch_size = batch_size # lstm layer self.cell = torch.nn.LSTM( input_size=self.input_size, hidden_size=self.hidden_size, num_layers=num_layers) # linear layer for output self.linear = torch.nn.Linear(sequence_size * hidden_size, self.output_size) def forward(self, input_x): """ forward computation @param input_x, tensor of shape (L, N, H_in) @return tensor of shape (N, H_out) """ # get dimension from input_x _, batch, features = input_x.size() # hidden, tensor of shape (D * num_layers, N, H_hidden) hidden = self.init_zeros(batch) # cell, tensor of shape (D * num_layers, N, H_hidden) cell = self.init_zeros(batch) # output tensor (L, N, D * H_hidden) output, _ = self.cell(input_x, (hidden, cell)) # convert the shape of output to (N, L * H_hidden) hidden = convert_hidden_shape(output, batch) # (N, L * H_hidden) to (N, H_out) output = self.linear(hidden) return output def init_zeros(self, batch_size=0, hidden_size=0): if batch_size == 0: batch_size = self.batch_size if hidden_size == 0: hidden_size = self.hidden_size return torch.zeros(self.num_layers, batch_size, hidden_size)
这里需要注意的是经过LSTM计算后的网络,输出的Output维度是
( L , N , D ∗ H o u t ) (L, N, D * H_{out}) (L,N,D∗Hout)
由于我们使用的是单向,所以D=1,最终输出的维度是
( L , N , H o u t ) (L, N, H_{out}) (L,N,Hout)
但是线性层能接受的输入维度是
( N , H i n ) (N, H_{in}) (N,Hin)
这意味着我们要把LSTM网络输出的结构转化成线性层可接受的维度, 即
( L , N , H o u t ) → ( N , H i n ) = ( N , L ∗ H o u t ) (L, N, H_{out}) \rightarrow (N, H_{in}) = ( N, L * H_{out}) (L,N,Hout)→(N,Hin)=(N,L∗Hout)
这里我提供一个比较笨的转化方法,你可以在学会LSTM之后对这部分进行修改。
def convert_hidden_shape(hidden, batch_size):
tensor_list = []
for i in range(batch_size):
ts = hidden[:, i, :].reshape(1, -1)
tensor_list.append(ts)
ts = torch.cat(tensor_list)
return ts
另外就是需要注意下,维度转换的时候,一定要注意保证数据的正确性和完整性,否则会影响最终的输出。
这部分就是例行公事了,创建网络对象、选择合适的损失函数、合适的优化函数,然后制定训练和测试过程。
主要过程
# define a model model = LSTMModel( input_size=INPUT_SIZE, hidden_size=HIDDEN_SIZE, output_size=OUTPUT_SIZE, sequence_size=SEQUENCE_SIZE, batch_size=BATCH_SIZE) # loss function criterion = torch.nn.CrossEntropyLoss() # majorized function optimizer = torch.optim.Adam(model.parameters(), lr=0.001) # Training and testing process for epoch in range(10): # load dataset languages, language_idx, train_loader, test_loader = load_dataset() # training train(epoch, model, optimizer, criterion) # testing test(model)
这里稍微玩了点小花招,我们在每一次训练网络的过程中,都会重新随机的加载一次数据,这样保证每次训练集和测试集都有所不同,能够更好的评估和修正模型的准确度。
然后分别就是训练过程和测试过程的代码了
训练过程
def train(epoch, model, optimizer, criterion): running_loss = 0 for idx, data in enumerate(train_loader, 0): # convert data input_x, label_y = convert_data(data) # clear the gradients optimizer.zero_grad() # forward computation predicate_y = model(input_x) # loss computation loss = criterion(predicate_y, label_y) # backward propagation loss.backward() # update network parameters optimizer.step() # print loss running_loss += loss.item() if idx % 100 == 0: print('[%d, %5d] loss: %.3f' % (epoch, idx, running_loss / 100)) running_loss = 0
测试过程
def test(model): correct = 0 total = 0 with torch.no_grad(): for idx, data in enumerate(test_loader, 0): # convert data input_x, label_y = convert_data(data) # predicate predicate_y = model(input_x) # check output _, predicated = torch.max(predicate_y.data, dim=1) total += label_y.cpu().size(0) correct += (predicated == label_y).sum().item() print("Accuracy on test set: %d %%" % (100 * correct / total))
实验输出结果
如果程序一切正常,输出的结果就是这样的
[0, 0] loss: 0.029 [0, 100] loss: 2.041 [0, 200] loss: 1.928 [0, 300] loss: 1.969 [0, 400] loss: 1.946 [0, 500] loss: 1.885 [0, 600] loss: 1.895 [0, 700] loss: 1.858 [0, 800] loss: 1.898 [0, 900] loss: 1.960 [0, 1000] loss: 1.897 [0, 1100] loss: 1.880 [0, 1200] loss: 1.911 [0, 1300] loss: 1.816 [0, 1400] loss: 1.925 [0, 1500] loss: 1.913 Accuracy on test set: 97 % [1, 0] loss: 0.015 [1, 100] loss: 1.933 [1, 200] loss: 1.880 [1, 300] loss: 1.795 [1, 400] loss: 1.908 [1, 500] loss: 1.881 [1, 600] loss: 1.821 [1, 700] loss: 1.874 [1, 800] loss: 1.845 [1, 900] loss: 1.880 [1, 1000] loss: 1.806 [1, 1100] loss: 1.794 [1, 1200] loss: 1.820 [1, 1300] loss: 1.951 [1, 1400] loss: 1.852 [1, 1500] loss: 1.865 Accuracy on test set: 97 % [2, 0] loss: 0.012 [2, 100] loss: 1.798 [2, 200] loss: 1.799 [2, 300] loss: 1.864 [2, 400] loss: 1.848 [2, 500] loss: 1.878 [2, 600] loss: 1.766 [2, 700] loss: 1.816 [2, 800] loss: 1.857 [2, 900] loss: 1.821 [2, 1000] loss: 1.929 [2, 1100] loss: 1.905 [2, 1200] loss: 1.830 [2, 1300] loss: 1.870 [2, 1400] loss: 1.867 [2, 1500] loss: 1.897 Accuracy on test set: 97 % [3, 0] loss: 0.013 [3, 100] loss: 1.824 [3, 200] loss: 1.843 [3, 300] loss: 1.884 [3, 400] loss: 1.870 [3, 500] loss: 1.831 [3, 600] loss: 1.907 [3, 700] loss: 1.807 [3, 800] loss: 1.858 [3, 900] loss: 1.837 [3, 1000] loss: 1.809 [3, 1100] loss: 1.873 [3, 1200] loss: 1.904 [3, 1300] loss: 1.848 [3, 1400] loss: 1.886 [3, 1500] loss: 1.860 Accuracy on test set: 97 % [4, 0] loss: 0.017 [4, 100] loss: 1.834 [4, 200] loss: 1.847 [4, 300] loss: 1.870 [4, 400] loss: 1.779 [4, 500] loss: 1.773 [4, 600] loss: 1.900 [4, 700] loss: 1.862 [4, 800] loss: 1.828 [4, 900] loss: 1.831 [4, 1000] loss: 1.804 [4, 1100] loss: 1.846 [4, 1200] loss: 1.898 [4, 1300] loss: 1.883 [4, 1400] loss: 1.888 [4, 1500] loss: 1.820 Accuracy on test set: 97 % [5, 0] loss: 0.018 [5, 100] loss: 1.815 [5, 200] loss: 1.886 [5, 300] loss: 1.853 [5, 400] loss: 1.897 [5, 500] loss: 1.862 [5, 600] loss: 1.894 [5, 700] loss: 1.865 [5, 800] loss: 1.818 [5, 900] loss: 1.868 [5, 1000] loss: 1.790 [5, 1100] loss: 1.815 [5, 1200] loss: 1.813 [5, 1300] loss: 1.890 [5, 1400] loss: 1.784 [5, 1500] loss: 1.848 Accuracy on test set: 94 % [6, 0] loss: 0.023 [6, 100] loss: 1.870 [6, 200] loss: 1.861 [6, 300] loss: 1.850 [6, 400] loss: 1.868 [6, 500] loss: 1.892 [6, 600] loss: 1.866 [6, 700] loss: 1.853 [6, 800] loss: 1.803 [6, 900] loss: 1.805 [6, 1000] loss: 1.801 [6, 1100] loss: 1.895 [6, 1200] loss: 1.821 [6, 1300] loss: 1.808 [6, 1400] loss: 1.906 [6, 1500] loss: 1.864 Accuracy on test set: 97 % [7, 0] loss: 0.021 [7, 100] loss: 1.807 [7, 200] loss: 1.785 [7, 300] loss: 1.900 [7, 400] loss: 1.863 [7, 500] loss: 1.830 [7, 600] loss: 1.809 [7, 700] loss: 1.844 [7, 800] loss: 1.794 [7, 900] loss: 1.901 [7, 1000] loss: 1.892 [7, 1100] loss: 1.829 [7, 1200] loss: 1.875 [7, 1300] loss: 1.873 [7, 1400] loss: 1.825 [7, 1500] loss: 1.788 Accuracy on test set: 97 % [8, 0] loss: 0.015 [8, 100] loss: 1.850 [8, 200] loss: 1.792 [8, 300] loss: 1.860 [8, 400] loss: 1.863 [8, 500] loss: 1.835 [8, 600] loss: 1.776 [8, 700] loss: 1.865 [8, 800] loss: 1.780 [8, 900] loss: 1.851 [8, 1000] loss: 1.873 [8, 1100] loss: 1.819 [8, 1200] loss: 1.804 [8, 1300] loss: 1.855 [8, 1400] loss: 1.892 [8, 1500] loss: 1.869 Accuracy on test set: 97 % [9, 0] loss: 0.015 [9, 100] loss: 1.818 [9, 200] loss: 1.795 [9, 300] loss: 1.839 [9, 400] loss: 1.911 [9, 500] loss: 1.854 [9, 600] loss: 1.859 [9, 700] loss: 1.810 [9, 800] loss: 1.852 [9, 900] loss: 1.842 [9, 1000] loss: 1.797 [9, 1100] loss: 1.842 [9, 1200] loss: 1.777 [9, 1300] loss: 1.822 [9, 1400] loss: 1.783 [9, 1500] loss: 1.872 Accuracy on test set: 95 %
可以看到整体的实验结果还算满意,大概有96%左右的准确度。如果你希望把训练好的模型保存起来,并且期待能否把模型应用到一般的应用程序里,比如说C程序里,那么就可以把模型和参数都保存起来。
保存模型和数据
文件的后缀名没啥强制要求,我比较喜欢叫ptm,因为是 pytorch model 的简写,你也可以自己定义个喜欢的后缀名。
# finally save the model
torch.save(model, "LSTM_Surname_Classfication.ptm")
在下一章节里,我将给大家演示如何使用C程序加载训练好的模型。
欢迎关注我的博客~
Adios~~
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。