打开网易新闻 查看更多图片

小仙女说

在我的印象中,韩国一直在不遗余力地重申自己历史文化的悠久,煞有让中华文化都起源于韩国的势头。不管韩国再怎么吹嘘自己的文化起源,他们的文化本身不会说谎。而文化的载体就是语言,姓氏又是语言中特别的标志。所以,今天我们通过使用 PyTorch 平台搭建深度神经网络的方式,分析来自18种语言的不同姓氏,来看看韩国的姓氏文化是起源于哪里。

本期导航

  • 理解循环神经网络

  • 处理训练数据

  • 建立姓氏张量

  • 建立模型

每种语言中姓名的都有其特点,比如俄语中的“xx斯基、xx托夫”,中文的“赵钱孙李周吴郑王”。所以我们要使用 PyTorch 搭建一个神经网络模型,让这个模型去学习各种语言中姓氏的“特点”。最终这个模型可以预测出一个姓氏是属于哪种语言的,同时我们要使用这个模型可以分析出哪些语言的姓氏“特点”是相似的。

具体来说,我们将通过用几千个来自18种起源语言的姓氏进行训练,并最终根据拼写来预测姓氏来自哪种语言。

predict Hinton
(-0.47) Scottish
(-1.52) English
(-3.57) Irish

predict.py Schmidhuber
(-0.19) German
(-2.48) Czech
(-2.68) Dutch
理解循环神经网络

RNN 就是是循环神经网络(RNN, Recurrent Neural Networks)

如果你还不了解 RNN,可以先花几分钟看一下莫烦老师的介绍:

与普通的前馈神经网络不同,RNN 有先后的时间概念,因此它可以学习到先输入的数据和后输入数据之间的关系。

我们将包含中国及韩国在内的18种语言的姓氏全部转化成 ASCII 字符,让 RNN 学习出不同语言中姓名字符间排列的特征。

打开网易新闻 查看更多图片

【福利区】

在集智AI学园公众号回复“韩国人的祖先”,获取本课的 Jupyter Notebook 及训练用数据包。

打开网易新闻 查看更多图片

处理训练数据

在我们提供的数据文件中,包含在 data/names 目录下的是18个命名规则为"[Language].txt"的文本文件,每个文件都包含一些姓氏,每个姓氏占一行。

左右滑动查看代码

import glob

all_filenames = glob.glob('./data/names/*.txt')
print(all_filenames)
['./data/names/Arabic.txt', './data/names/Chinese.txt', './data/names/Czech.txt', './data/names/Dutch.txt', './data/names/English.txt', './data/names/French.txt', './data/names/German.txt', './data/names/Greek.txt', './data/names/Irish.txt', './data/names/Italian.txt', './data/names/Japanese.txt', './data/names/Korean.txt', './data/names/Polish.txt', './data/names/Portuguese.txt', './data/names/Russian.txt', './data/names/Scottish.txt', './data/names/Spanish.txt', './data/names/Vietnamese.txt']

现在先让我们解决这个问题:

在我们收集的18种语言的姓氏中,中文和韩文姓氏都已经转化为音译的字母。但是有些语言的姓氏并不能用普通的ASCII英文字符来表示,比如“lusàrski”,这些不一样的字母会增加神经网络的“困惑”,影响其训练效果。所以我们得首先把这些特别的字母转换成普通的ASCII字符(即26个英文字母)。

import unicodedata
import string

# 使用26个英文字母大小写再加上.,;这三个字符
# 建立字母表,并取其长度
all_letters = string.ascii_letters + " .,;'"
n_letters = len(all_letters)


# 将Unicode字符串转换为纯ASCII
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
)

print(unicode_to_ascii('lusàrski'))
print('all_letters:', all_letters)
print('all_letters:', len(all_letters))
Slusarski
all_letters: abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ .,;'
all_letters: 57

然后再建立 readLines 方法,用于从文件中一行一行地将姓氏读取出来。

以18种语言为索引,将读取出的姓氏各自存储在名为 category_lines 的字典中。

# 构建category_lines字典,名字和每种语言对应的列表
category_lines = {}
all_categories = []

# 按行读取出姓氏并转换成纯ASCII
def readLines(filename):
lines = open(filename).read().strip().split('\\n')
return [unicode_to_ascii(line) for line in lines]

for filename in all_filenames:
# 取出每个文件的名字(语言名)
category = filename.split('/')[-1].split('.')[0]
# 将语言名加入到all_categories列表
all_categories.append(category)
# 取出所有的姓氏lines
lines = readLines(filename)
# 将所有姓氏以语言为索引,加入到字典中
category_lines[category] = lines

n_categories = len(all_categories)

print('all_categories:', all_categories)
print('n_categories =', n_categories)
all_categories: ['Arabic', 'Chinese', 'Czech', 'Dutch', 'English', 'French', 'German', 'Greek', 'Irish', 'Italian', 'Japanese', 'Korean', 'Polish', 'Portuguese', 'Russian', 'Scottish', 'Spanish', 'Vietnamese']
n_categories = 18

all_categories 中包含18种语言的名字。

category_lines 中以18种语言为索引,存储了所有的姓氏。

print(category_lines['Italian'][:5])
['Abandonato', 'Abatangelo', 'Abatantuono', 'Abate', 'Abategiovanni']
建立姓氏张量

我们在第一期中已经介绍了张量(Tensor)的概念。本次我们需要将姓氏转化为张量(Tensor)才能输入到神经网络中。

在我们这个模型中,一个姓氏张量由若干个字符张量组成,所以我们先编写建立字符张量的方法。

我们首先对字符进行独热编码(one-hot-encoding)。还记得在上节课在“词袋模型”里讲到的“单词表”吗?

我们这次使用“字母表”对单个字符进行独热编码,就是将每个字符映射成一个“字母表向量”。

在前面的程序中我们已经建立好了一个字母表 all_letters,它包含52个大小写的英文字符以及5个符号,它的长度为 n_letters。

我们使用 <1 x n_letters> 维度的独热编码向量(one-hot-vector)来表示每一个字符。一个独热编码向量是指除了当前字母索引所在位置是1,其余均为0的向量,例如“b”= <0 1 0 0 0 ...>。

一个字符的张量维度是:<1 x n_letters>。一个姓氏包括若干个字符,设姓氏的长度为 line_length,那么姓氏张量的维度是:<line_length x 1 x n_letters>。

import torch

# 将一个字符转化为<1 x n_letters>的字符张量
def letter_to_tensor(letter):
# 创建一个初值全为0的张量
tensor = torch.zeros(1, n_letters)
# 在字母表中找到字符的位置
letter_index = all_letters.find(letter)
# 将张量中代表字符的相应位置置为1
tensor[0][letter_index] = 1
return tensor


# 将一个姓氏转化为<line_length x 1 x n_letters>的姓氏张量
# line_length就是一个姓氏的长度
def line_to_tensor(line):
tensor = torch.zeros(len(line), 1, n_letters)
# 这里将姓氏转化为枚举类型进行循环
# li是索引,即0,1,2,3...
# letter就是对应位置的字符
for li, letter in enumerate(line):
# 当前字符在字母表中的位置
letter_index = all_letters.find(letter)

# 通过li索引到的是姓氏张量中当前的字符张量
# letter_index索引到的是字符张量对应的位置
# 设定对应位置为1
tensor[li][0][letter_index] = 1
return tensor

我们打印一个字符张量来观察一下:

print(letter_to_tensor('J'))
Columns 0 to 12
0 0 0 0 0 0 0 0 0 0 0 0 0

Columns 13 to 25
0 0 0 0 0 0 0 0 0 0 0 0 0

Columns 26 to 38
0 0 0 0 0 0 0 0 0 1 0 0 0

Columns 39 to 51
0 0 0 0 0 0 0 0 0 0 0 0 0

Columns 52 to 56
0 0 0 0 0
[torch.FloatTensor of size 1x57]

可以看到字符张量与字母表等长(57),在张量中对应字母表‘J’的位置被置为了1。

我们再来打印一个姓氏张量来观察一下:

print(line_to_tensor('Jones').size())
torch.Size([5, 1, 57])

可以看到这个姓氏张量包含了5个字符张量。

搭建模型:

我们此次搭建的是一个特别“寻常”的 RNN 神经网络。

我们可以从下面的图观察到,它在结构上几乎和我们之前讲过的前馈神经网络差不多,只是在输入层将输入数据和隐藏层状态结合在了一起。

对于这个隐藏层状态,我们之前讲到 RNN 它是有“先后顺序”概念的,这一时刻使用的隐藏层状态就是在上一时刻产生的而已。

那网络刚运行时没有“上一时刻”怎么办?

初始化为0即可。

图中的 i2o i2h 代表 input to output 和 input to hidden 。

输出结果会经过一个logSoftmax层再进行输出。

import torch.nn as nn
from torch.autograd import Variable

# 网络组件继承自 nn.Module
class RNN(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super(RNN, self).__init__()

self.input_size = input_size
self.hidden_size = hidden_size
self.output_size = output_size

# 建立 i2h 和 i2o 的仿射映射(线性映射)
self.i2h = nn.Linear(input_size + hidden_size, hidden_size)
self.i2o = nn.Linear(input_size + hidden_size, output_size)

self.softmax = nn.LogSoftmax()

def forward(self, input, hidden):
# 合并输入数据和隐藏层状态
combined = torch.cat((input, hidden), 1)
# 线性层
hidden = self.i2h(combined)
output = self.i2o(combined)
# logSoftmax非线性计算输出
output = self.softmax(output)
# 返回当前的输出,以及用于下一时刻的隐藏层状态
return output, hidden

def init_hidden(self):
# 将隐藏层状态初始化为0
return Variable(torch.zeros(1, self.hidden_size))
观察模型

现在我们将上面建立的 RNN 网络实例化:

n_hidden = 128
rnn = RNN(n_letters, n_hidden, n_categories)

为了运行这个神经网络,我们需要传递一个输入(即当前字符的张量),以及一个先前的隐藏状态(在第一次运行时,我们先初始化为零)。然后网络会产生一个输出(属于某种语言的概率)以及一个隐藏状态(将被用在下一次的周期中)。

PyTorch 模块是在变量(Variable)上运行的,而不是直接在张量(Tensor)上运行的,所以我们需要先为输入建立变量。

我们先输入一个字符“A”到网络中,观察网络的输出

input = Variable(letter_to_tensor('A'))
# 第一次使用
# 将隐藏状态初始化为0
hidden = rnn.init_hidden()

# 生成预测结果
# 以及用于下一周期的隐藏状态
output, next_hidden = rnn(input, hidden)
# 打印出预测结果的长度(对18中语言的预测概率)
print('output.size =', output.size())
output.size = torch.Size([1, 18])

上面使用 RNN 网络对单个字符张量进行了预测,输出了字符 A 属于18种语言的概率。

下面我们调用 line_to_tensor 方法,操作姓氏张量,尝试使用未训练的网络对“Albert”进行预测。

# 注意在这里用的是 line_to_tensor
input = Variable(line_to_tensor('Albert'))
# 将隐藏状态初始化为0
# 效果与rnn.init_hidden()相同
hidden = Variable(torch.zeros(1, n_hidden))

# 预测名字中的第一个字符“A”
output, next_hidden = rnn(input[0], hidden)
print(output)
Variable containing:

Columns 0 to 9
-2.8384 -2.9227 -2.8450 -2.9126 -2.9272 -2.8526 -2.8668 -2.9104 -2.8237 -2.8771

Columns 10 to 17
-2.9886 -2.9789 -2.7978 -2.9731 -2.8218 -2.8537 -3.0130 -2.8579
[torch.FloatTensor of size 1x18]

输出是一个 <1 x n_categories> 的张量,代表这个姓氏属于18种语言的概率。

到这里我们用来预测分析韩国姓氏起源的神经网络就搭建完成了,但是目前这个网络还发挥不了预测的功能,因为我们还没对它进行训练!

在下一章中我会带领大家着手去训练这个神经网络模型,训练后的网络模型不仅能预测一个姓氏是属于哪种语言的,还能帮我们找出韩国的姓氏文化到底起源于哪里。

下章将在本周内推出,敬请期待……

【本节资源】

在集智AI学园公众号回复“韩国人的祖先”,获取本课的 Jupyter Notebook 及训练用数据包。

打开网易新闻 查看更多图片

PyTorch圣殿 | 传奇NLP攻城狮成长之路

课程表

  • 第一期:PyTorch概要简析

  • 第二期:小试牛刀:编写一个词袋分类器

  • 本期:韩国人的祖先来自中国吗?——来自人工智能的启示

  • 敬请期待:韩国人的祖先来自中国吗?——来自人工智能的启示 大结局

  • 第四期:AI编剧:使用RNN创作莎士比亚剧本

  • 第五期:起名大师:使用RNN生成个好名字

  • 第六期:AI翻译官:采用注意力机制的翻译系统

  • 第七期:探索词向量世界

  • 第八期:词向量高级:单词语义编码器

  • 第九期:长短记忆神经网络(LSTM)序列建模

  • 第十期:体验PyTorch动态编程,双向LSTM+CRF

更多职位内推点击阅读原文

如果您有任何关于Pytorch方面的问题,欢迎进【集智—清华】火炬群与大家交流探讨,添加集智小助手微信 swarmaAI,备注( pytorch交流群),小助手会拉你入群。

推荐阅读

PyTorch圣殿 | 传奇NLP攻城狮成长之路(二)

AI公司职位内推 | 自动驾驶、智慧医疗、智能家居

传奇NLP攻城狮成长之路(一)

教程 | Windows用户指南:如何用Floyd跑PyTorch

《深入浅出GAN-原理与应用》学习笔记

课程推荐 | 进击的强化学习

AI江湖|筑基篇——人工神经网络

深度 | 人工智能让我们失业?不,这取决于我们自己

学员原创 | 人工智能产品经理的新起点(200页PPT下载)

基于神经机器翻译技术的英文修改器养成记 | 集智原创发明

吐血推荐:超级好用的深度学习云平台Floyd | 集智AI学园

关注集智AI学园公众号

获取更多更有趣的AI教程吧!

搜索微信公众号:swarmAI

集智AI学园QQ群:426390994

学园网站:campus.swarma.org

商务合作|zhangqian@swarma.org

投稿转载|wangjiannan@swarma.org


查看更多内推职位!