查看原文
其他

起名字这个技术活,终于用Pytorch找到解决办法了!

2017-10-13 集智小仙女 集智AI学园

上节课我们使用深度学习技术创造了一个会写嘻哈说唱词的 AI,这节课我们使用类似的方法,再创建一个 AI国际起名大师

AI 起名大师专起外国名,你只需告诉他你想要个哪国的名字,AI 起名大师分分钟给你想出一大堆名字出来!

下面都是是用 AI 起名大师生成的名字,有了它你还会为了起一个好的外国名而发愁吗

Russian

Rovakov Uantov Shavakov


German

Gerren Ereng Rosher


Spanish

Salla Parer Allan


资源:

集智 AI 学园公众号回复“AI起名大师”,获取本节课的 Jupyter Notebook 文档!

准备数据

做深度学习的第一步是把数据准备好。像之前一样,我们先准备数据。

这次的数据是18个文本文件,每个文件以“国家名字”命名,其中存储了这个国家的不同人名。

在读取这些数据前,为了简化神经网络的输入参数规模,我们把各国各语言人名都转化成用26个英文字母来表示,下面就是转换的方法。

import glob

import unicodedata

import string


# all_letters 即课支持打印的字符+标点符号

all_letters = string.ascii_letters + " .,;'-"

# Plus EOS marker

n_letters = len(all_letters) + 1

EOS = n_letters - 1


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("O'Néàl"))

O'Neal

可以看到 "O'Néàl" 被转化成了以普通ASCII字符表示的 O'Neal。

在上面的代码中,还要注意这么几个变量。

print('all_letters: ', all_letters)

print('n_letters: ', n_letters)

print('EOS: ', EOS)

all_letters: abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ .,;'-

n_letters: 59

EOS: 58

其中 all_letters 包含了我们数据集中所有可能出现的字符,也就是“字符表”。n_letters 是字符表的长度,在本例中长度为59。EOS 的索引号为58,它在字符表中没有对应的字符,仅代表结束。

读取数据

准备好处理数据的方法,下面就可以放心的读取数据了。

我们建立一个列表 all_categories 用于存储所有的国家名字。

建立一个字典 category_lines,以读取的国名作为字典的索引,国名下存储对应国别的名字。

# 按行读取出文件中的名字,并返回包含所有名字的列表

def read_lines(filename):

    lines = open(filename).read().strip().split('\n')

    return [unicode_to_ascii(line) for line in lines]



# category_lines是一个字典

# 其中索引是国家名字,内容是从文件读取出的这个国家的所有名字

category_lines = {}

# all_categories是一个列表

# 其中包含了所有的国家名字

all_categories = []

# 循环所有文件

for filename in glob.glob('../data/names/*.txt'):

    # 从文件名中切割出国家名字

    category = filename.split('/')[-1].split('.')[0]

    # 将国家名字添加到列表中

    all_categories.append(category)

    # 读取对应国别文件中所有的名字

    lines = read_lines(filename)

    # 将所有名字存储在字典中对应的国别下

    category_lines[category] = lines


# 共有的国别数

n_categories = len(all_categories)


print('# categories: ', n_categories, all_categories)

print()

print('# names: ', category_lines['Russian'][:10])

# categories: 18 ['Arabic', 'Chinese', 'Czech', 'Dutch', 'English', 'French', 'German', 'Greek', 'Irish', 'Italian', 'Japanese', 'Korean', 'Polish', 'Portuguese', 'Russian', 'Scottish', 'Spanish', 'Vietnamese']


# names: ['Ababko', 'Abaev', 'Abagyan', 'Abaidulin', 'Abaidullin', 'Abaimoff', 'Abaimov', 'Abakeliya', 'Abakovsky', 'Abakshin']

现在我们的数据准备好了,可以搭建神经网络了!

搭建神经网络

这次使用的 RNN 神经网络整体结构上与之前相似,细节上增加了一部分内容。

在输入层增加了 category,即名字的国别。这个 category 是以独热编码向量(one-hot vector)的方式输入的,再啰嗦一遍就是,作为长度为18的向量,代表自己国别的位置为1, 其它位置都为0。

我们搭建的神经网络的目的是生成“名字”,而“名字”就是一序列的字符。所以神经网络的输出(output)代表的是字符表中的每个字符,能成为“下一个字符”的概率,概率最大的那个字符,即作为“下一个字符”。

另外,这次还加入了“第二层”神经网络,即 o2o 层,以增强神经网络的预测性能。在 o2o 中还包括一层“dropout”,它会将输入“softmax”之前的数据随机置0(在本例中为随机置0.1)。“dropout”常被用来缓解“过拟合(overfitting)”的问题,因为它可以增加“混乱(chaos)”并提高采样的多样性。

建立神经网络的代码比较简单,并且网络的各个部分都在上图中标识出来了。

import torch

import torch.nn as nn

from torch.autograd import Variable


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


        self.i2h = nn.Linear(n_categories + input_size + hidden_size, hidden_size)

        self.i2o = nn.Linear(n_categories + input_size + hidden_size, output_size)

        self.o2o = nn.Linear(hidden_size + output_size, output_size)

        self.softmax = nn.LogSoftmax()


    def forward(self, category, input, hidden):

        input_combined = torch.cat((category, input, hidden), 1)

        hidden = self.i2h(input_combined)

        output = self.i2o(input_combined)

        output_combined = torch.cat((hidden, output), 1)

        output = self.o2o(output_combined)

        return output, hidden


    def init_hidden(self):

        return Variable(torch.zeros(1, self.hidden_size))

准备训练

首先建立一个可以随机选择数据对 (category, line) 的方法,以方便训练时调用。

import random


def random_training_pair():

    # 随机选择一个国别名

    category = random.choice(all_categories)

    # 读取这个国别名下的所有人名

    line = random.choice(category_lines[category])

    return category, line

对于训练过程中的每一步,或者说对于训练数据中每个名字的每个字符来说,神经网络的输入是 (category, current letter, hidden state),输出是 (next letter, next hidden state)。所以在每 批次 的训练中,我们都需要“一个国别”、“对应国别的一批名字”、“以及要预测的下一个字符”。

与上节课一样,神经网络还是依据“当前的字符”预测“下一个字符”。比如对于“Kasparov”这个名字,创建的数据对是 ("K", "a"), ("a", "s"), ("s", "p"), ("p", "a"), ("a", "r"), ("r", "o"), ("o", "v"), ("v", "EOS")。

国别名(category)也是以独热编码向量的形式输入神经网络的,它是一个 <1 x n_categories> 的向量。在输入的时候我们将它“并”在字符向量里,再“并”上隐藏层状态,作为神经网络的输入。

# 将名字所属的国家名转化为“独热向量”

def make_category_input(category):

    li = all_categories.index(category)

    tensor = torch.zeros(1, n_categories)

    tensor[0][li] = 1

    return Variable(tensor)


# 将一个名字转化成矩阵Tensor

# 矩阵的每行为名字中每个字符的独热编码向量

def make_chars_input(chars):

    tensor = torch.zeros(len(chars), n_letters)

    # 遍历每个名字中的每个字符

    for ci in range(len(chars)):

        char = chars[ci]

        # 独热编码

        tensor[ci][all_letters.find(char)] = 1

    # 增加一个维度

    tensor = tensor.view(-1, 1, n_letters)

    return Variable(tensor)


# 将“目标”,也就是“下一个字符”转化为Tensor

# 注意这里最后以 EOS 作为结束标志

def make_target(line):

    # 从第2个字符开始,取出每个字符的索引

    letter_indexes = [all_letters.find(line[li]) for li in range(1, len(line))]

    # 在最后加上 EOS 的索引

    letter_indexes.append(n_letters - 1) # EOS

    # 转化成 LongTensor

    tensor = torch.LongTensor(letter_indexes)

    return Variable(tensor)

同样为了训练时方便使用,我们建立一个 random_training_set 函数,以随机选择出数据集 (category, line) 并转化成训练需要的 Tensor: (category, input, target)。

def random_training_set():

    # 随机选择数据集

    category, line = random_training_pair()

    # 转化成对应 Tensor

    category_input = make_category_input(category)

    line_input = make_chars_input(line)

    line_target = make_target(line)

    return category_input, line_input, line_target

开始训练!

与之前处理得分类问题不同,在分类问题中只有最后的输出被使用。而在当前的 生成 名字的任务中,神经网络在每一步都会做预测,所以我们需要在每一步计算损失值。

PyTorch 非常易用,它允许我们只是简单的把每一步计算的损失加起来,并在最后进行反向传播。

def train(category_tensor, input_line_tensor, target_line_tensor):

    hidden = rnn.init_hidden()

    optimizer.zero_grad()

    loss = 0


    for i in range(input_line_tensor.size()[0]):

        output, hidden = rnn(category_tensor, input_line_tensor[i], hidden)

        loss += criterion(output, target_line_tensor[i])


    loss.backward()

    optimizer.step()


    return output, loss.data[0] / input_line_tensor.size()[0]

我们定义 time_since 函数,它可以打印出训练持续的时间。

import time

import math


def time_since(t):

    now = time.time()

    s = now - t

    m = math.floor(/ 60)

    s -= m * 60

    return '%dm %ds' % (m, s)

训练的过程与我们前几节课一样,就是调用训练函数,再等上几分钟就好啦吼吼!

通过 plot_every 控制打印日志的频率,通过 all_losses 控制记录绘图数据的频率,已经都是老套路啦!

n_epochs = 100000

print_every = 5000

plot_every = 500

all_losses = []

loss_avg = 0 # Zero every plot_every epochs to keep a running average

learning_rate = 0.0005


rnn = RNN(n_letters, 128, n_letters)

optimizer = torch.optim.Adam(rnn.parameters(), lr=learning_rate)

criterion = nn.CrossEntropyLoss()


start = time.time()


for epoch in range(1, n_epochs + 1):

    output, loss = train(*random_training_set())

    loss_avg += loss


    if epoch % print_every == 0:

        print('%s (%d %d%%) %.4f' % (time_since(start), epoch, epoch / n_epochs * 100, loss))


    if epoch % plot_every == 0:

        all_losses.append(loss_avg / plot_every)

        loss_avg = 0

0m 20s (5000 5%) 1.7640

0m 43s (10000 10%) 2.0930

1m 9s (15000 15%) 2.0813

1m 32s (20000 20%) 2.3847

1m 53s (25000 25%) 2.3773

2m 16s (30000 30%) 1.0473

2m 38s (35000 35%) 2.4157

2m 59s (40000 40%) 1.4604

3m 21s (45000 45%) 3.6522

3m 43s (50000 50%) 1.4112

4m 5s (55000 55%) 1.9039

4m 24s (60000 60%) 2.0797

4m 44s (65000 65%) 2.6220

5m 3s (70000 70%) 1.7693

5m 22s (75000 75%) 1.4734

5m 41s (80000 80%) 1.1522

6m 1s (85000 85%) 2.6431

6m 22s (90000 90%) 1.7703

6m 41s (95000 95%) 1.9069

7m 0s (100000 100%) 2.2563

绘制观察损失曲线

让我们将训练过程中记录的损失绘制成一条曲线,观察下神经网络学习的效果。

import matplotlib.pyplot as plt

import matplotlib.ticker as ticker

%matplotlib inline


plt.figure()

plt.plot(all_losses)

[<matplotlib.lines.Line2D at 0x114e8ec50>]

正如所有的优秀的神经网络一样^_^,损失值逐步降低并稳定在一个范围中。

测试使用神经网络

既然神经网络训练好了,那也就是说,我们喂给它第一个字符,他就能生成第二个字符,喂给它第二个字符,它就会生成第三个,这样一直持续下去,直至生成 EOS 才结束。

那下面我们编写 generate_one 函数以方便的使用神经网络生成我们想要的名字字符串,在这个函数里我们定义以下内容:

  • 建立输入国别,开始字符,初始隐藏层状态的 Tensor

  • 创建 output_str 变量,创建时其中只包含“开始字符”

  • 定义生成名字的长度最大不超过 max_length

    • 将当前字符传入神经网络

    • 在输出中选出预测的概率最大的下一个字符,同时取出当前的隐藏层状态

    • 如果字符是 EOS,则生成结束

    • 如果是常规字符,则加入到 output_str 中并继续下一个流程

  • 返回最终生成的名字字符串

max_length = 20


# 通过指定国别名 category

# 以及开始字符 start_char

# 还有混乱度 temperature 来生成一个名字

def generate_one(category, start_char='A', temperature=0.5):

    category_input = make_category_input(category)

    chars_input = make_chars_input(start_char)

    hidden = rnn.init_hidden()


    output_str = start_char


    for i in range(max_length):

        output, hidden = rnn(category_input, chars_input[0], hidden)


        # 这里是将输出转化为一个多项式分布

        output_dist = output.data.view(-1).div(temperature).exp()

        # 从而可以根据混乱度 temperature 来选择下一个字符

        # 混乱度低,则趋向于选择网络预测最大概率的那个字符

        # 混乱度高,则趋向于随机选择字符

        top_i = torch.multinomial(output_dist, 1)[0]


        # 生成字符是 EOS,则生成结束

        if top_i == EOS:

            break

        else:

            char = all_letters[top_i]

            output_str += char

            chars_input = make_chars_input(char)


    return output_str


# 再定义一个函数,方便每次生成多个名字

def generate(category, start_chars='ABC'):

    for start_char in start_chars:

        print(generate_one(category, start_char))

generate('Russian', 'RUS')

Raine

Urlamov

Sakhgany

generate('German', 'GER')

Gros

Echer

Rober

generate('Spanish', 'SPA')

Salvan

Pare

Alen

看起来还不错哈!不过这个模型还是有点简单,我们以后还可以对它进行改进,发明出更多的玩法!

更多玩法和改进

本文中给出的是通过指定“国家名”生成“人名”,我们还可以通过改变训练数据集,搞出其它的玩法,比如:

  • 通过小说 生成 小说中人物名字

  • 通过演讲 生成 演讲词汇

  • 通过国家 生成 城市名

如果要考虑到改进:

  • 可以尝试增加神经网络的规模,来提升预测的效果

  • 尝试把中文字符加进来,让神经网络可以生成中文的名字!

  • 如果要使用中文,注意要控制输入层的规模,可以试试嵌入


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

课程表



推荐阅读

教程 | 如何从TensorFlow转入PyTorch

AI 有嘻哈 | 使用 PyTorch 搭建一个会嘻哈的深度学习模型

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

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存