AI 学习之使用 RNN 和 LSTM 进行智能写作

智能写作

智能写作就是指人工智能模型自己能根据一定的条件或者是无任何条件下自由地生成内容。例如人工智能写小说,写新闻,写诗等等。智能写作其实已经被应用到商业领域了,很多大出版商都研发了智能写作系统。可能你每天阅读的新闻或文章就能人工智能写的,只是你察觉不到而已。

为了循序渐进地让大家弄懂智能写作程序的研发,我们先来实现一个智能起名程序,然后再来看一个智能写诗的程序。

本次实战编程中我们将实现一个为恐龙起名的程序,这个程序是一个字(母)级别的RNN模型,也就是RNN的每个时间步只输出一个字(母)。因为汉字太多,较复杂,而教学目的就是要简单易懂,所以我们选择实现英文版的,英文总共就26个字母。学会了英文字母版的后,如果感兴趣的话你也可自行实现汉字版的。生活中有些人会去算命先生那里为小孩起名,貌似现在还有专门起名的网站。

file

我们已经为大家收集了很多恐龙的名字,将它们保存在了dataset中,大家可以点击查看这个数据集。其实就是本文档同目录下的dinos.txt文件。你也可以用写字板或者word打开它(如果用记事本软件打开它,会出现无换行的情况)。最出名的恐龙名就是霸王龙了,想必不少人都看过《侏罗纪公园》,霸王龙相当地牛逼啊,它的英文是Tyrannosaurus,你可以在dinos.txt中搜索到Tyrannosaurus。迅猛龙的英文是Velociraptor,迅猛龙也是非常吓人的~ 下面我们将构建一个字母级别的语言模型,来学习dinos.txt中的恐龙名,找到为恐龙命名的规律,以获得生成出新恐龙名的能力。

首先我们加载一些系统库以及我们自定义的工具库utils,这个工具库里面已经为大家实现了很多函数,包括rnn_forwardrnn_backward等等,这些函数的实现我们在前一个实战编程中已经学习了,所以在此就不为它们浪费篇幅了。

import numpy as np
from utils import *
import random

1 - 数据预处理

下面的代码单元会读取出dinos.txt文件中所有的恐龙名,并且从中提取出一个信息。

data = open('dinos.txt', 'r').read() # 读取出所有恐龙名
data = data.lower() # 都转换成小写字母
chars = list(set(data))# 提取出这些名字中的字符种类(英文中总共就26个字母,加上文件中的换行符,就是27个字符) 
data_size, vocab_size = len(data), len(chars) # 提取出字符数量,以及字符种类数量
print('dinos.txt文件里面总共有%d个字符,%d个字符种类' % (data_size, vocab_size))

print("字符种类打印:\n", chars)
dinos.txt文件里面总共有19909个字符,27个字符种类
字符种类打印:
 ['a', 'y', 'p', 'l', 'g', 'e', 'v', 'h', 'i', 'd', 'u', 'f', 'c', '\n', 'z', 'j', 'o', 'k', 'b', 'q', 's', 'w', 'x', 'r', 'm', 't', 'n']

我们前面的文章中说过<EOS>可以表示句子的末尾,在本数据集中我们使用换行符来充当着<EOS>的作用,用它来表示一个恐龙名的末尾。换行符在编程中是用"\n"来表示的,程序员一般都知道。

下面的代码单元会生成两个python字典,字典中每个字符种类都对应了一个索引,索引范围是[0,26]。

char_to_ix = { ch:i for i,ch in enumerate(sorted(chars)) } # 这个字典用于将字符种类转化成索引
ix_to_char = { i:ch for i,ch in enumerate(sorted(chars)) } # 这个字典用于将索引转化成字符种类
print(ix_to_char) # 从下面的打印结果可知,索引0代表了换行符,字母z的索引是26...  
{0: '\n', 1: 'a', 2: 'b', 3: 'c', 4: 'd', 5: 'e', 6: 'f', 7: 'g', 8: 'h', 9: 'i', 10: 'j', 11: 'k', 12: 'l', 13: 'm', 14: 'n', 15: 'o', 16: 'p', 17: 'q', 18: 'r', 19: 's', 20: 't', 21: 'u', 22: 'v', 23: 'w', 24: 'x', 25: 'y', 26: 'z'}

2 - 实现2个功能函数

下面我将实现两个功能函数,它们将会在后面我们构建神经网络模型时起到大作用。

2.1 - 梯度值裁剪函数

后面我们将在模型中使用这个函数来避免RNN出现梯度爆炸。有很多种裁剪的方法,本文档中我们将使用一种简单的方法,那就是为梯度值设置一个最大值,如果实际梯度值超过了这个最大值,那么我们就将梯度值设为这个最大值,也就是说,绝对不会让梯度值超过这个最大值。假设我们将最大值设置为10,如果梯度值超过10,就将梯度值设置为10,如果梯度值小于-10,那么就将梯度值设置为-10.

我们知道如果出现梯度爆炸,那么会严重影响神经网络的学习效率。下面左图就是一个出现梯度爆炸的神经网络的学习路线图,可以看出神经网络一整乱撞,经常偏离代表最优解的中心点。而右图就是经过了梯度值裁剪的神经网络的学习路线图,不会到处乱跑,而是一步步地逼近最优解。

file

### 梯度值裁剪函数

def clip(gradients, maxValue):
    '''    
    参数:
    gradients -- 一个字典,包含了如下梯度值"dWaa", "dWax", "dWya", "db", "dby"
    maxValue -- 指定的最大值

    返回值: 
    gradients -- 一个字典,包含了被裁剪后的梯度值
    '''

    dWaa, dWax, dWya, db, dby = gradients['dWaa'], gradients['dWax'], gradients['dWya'], gradients['db'], gradients['dby']

    # 循环取出每组梯度值
    for gradient in [dWax, dWaa, dWya, db, dby]:
        np.clip(gradient, -maxValue, maxValue, out=gradient) # 裁剪这组梯度值

    # 将裁剪后的梯度值重新保存到gradients中
    gradients = {"dWaa": dWaa, "dWax": dWax, "dWya": dWya, "db": db, "dby": dby}

    return gradients
np.random.seed(3)
dWax = np.random.randn(5,3)*10
dWaa = np.random.randn(5,5)*10
dWya = np.random.randn(2,5)*10
db = np.random.randn(5,1)*10
dby = np.random.randn(2,1)*10
gradients = {"dWax": dWax, "dWaa": dWaa, "dWya": dWya, "db": db, "dby": dby}
gradients = clip(gradients, 10)
print("gradients[\"dWaa\"][1][2] =", gradients["dWaa"][1][2])
print("gradients[\"dWax\"][3][1] =", gradients["dWax"][3][1])
print("gradients[\"dWya\"][1][2] =", gradients["dWya"][1][2])
print("gradients[\"db\"][4] =", gradients["db"][4])
print("gradients[\"dby\"][1] =", gradients["dby"][1])
gradients["dWaa"][1][2] = 10.0
gradients["dWax"][3][1] = -10.0
gradients["dWya"][1][2] = 0.2971381536101662
gradients["db"][4] = [10.]
gradients["dby"][1] = [8.45833407]

2.2 - 采样函数

教程前面的文章中我们说过,语言模型的每一个时间步输出的预测值是词表中每个词的概率。如下图所示,在本文档中每个时间步输出的预测值就是每个字母的概率。例如第一个时间步输出的预测值中字母a的概率可能是10%,字母m的概率是2%等等,第二个时间步输出的预测值中字母a的概率可能是5%,字母m的概率是13%等等。一个模型训练好后,参数都固定了,那么只要输入是固定的,输出就也是固定的,例如在人脸识别中,输出张三的脸,那么神经网络的预测结果肯定是张三,不可能出现有时候预测成张三有时候会是李四的情况。那么问题就来了,当语言模型训练好了后,我们如何让它生成恐龙名字呢?如果每次都取每个时间步的最大概率的那个字母,那岂不是每次模型生成的都是同一个名字~ 所以我们需要采样函数,不是每次都取最大概率的字母,而是随机往预测值里面取出一个字母来,这个随机取的过程就叫做采样(sample)。在下图中,每一个时间步得出预测值后都会进行一次采样,然后将采样得到的字母作为输入传给下一个时间步。一般来说,在模型训练完成后,在使用模型时才加入采样的功能,也就是说在模型训练期间是不进行采样的。但是我们本次实战编程的目的在于教学,所以我们会一边训练一边采样,每训练2000次就采样生成7个名字,这样能让我们看到随着训练次数的增加,采样输出的结果越来越好的现象。

file

# 采样函数

def sample(parameters, char_to_ix, seed):
    """
    参数:
    parameters -- 训练好了的参数,该字典包含了Waa, Wax, Wya, by, b. 
    char_to_ix -- 用于将字符转化为相应索引的字典
    seed -- 随机种子

    返回值:
    indices -- 采样得到的一串字符的索引
    """

    Waa, Wax, Wya, by, b = parameters['Waa'], parameters['Wax'], parameters['Wya'], parameters['by'], parameters['b']
    # 获取词表的大小。因为by是与预测值关联的阈值,那么预测值有多少个元素就有多少个b,
    # 在本例中预测值包含了27个元素,也就是27个字符的概率。这27个字符就是我们的词表。
    # 所以vocab_size就等于27.
    vocab_size = by.shape[0] 
    # 获取RNN时间步单元中的神经元的个数。Waa是指用前一个时间步的激活值a作为输入来计算a时的参数。
    # 因为一个神经元对应一个激活值,所以利用Waa就可以知道时间步单元中的神经元的个数.
    n_a = Waa.shape[1]

    # 第一个时间步的输入x是一个0向量
    # 输入x是一个one-hot向量,就是向量里只有一个元素是1,其它元素都是0.
    # 这个one-hot向量的大小就是词表的大小,在本例中就是27。
    # 如果输入x是字母a的话,那么向量中只有第二个元素是1,其它元素都是0.
    x = np.zeros((vocab_size, 1))
    # 第一个时间步的输入a也是0向量
    a_prev = np.zeros((n_a, 1))

    indices = []

    idx = -1 

    counter = 0
    newline_character = char_to_ix['\n'] # 获取换行符对应的索引

    # 不停地执行时间步来生成字母,直到生成字母是换行符时或者生成了50个字母了才结束。
    # 对于一个训练有素的模型来说,不可能出现生成一个50个字母的恐龙名,名字哪有那么长,
    # 但是为了防止出现无限循环,我们还是加上50个字母的上限设置。
    while (idx != newline_character and counter != 50):

        # 执行一个时间步,得出预测值y,y里面保存了每个字母的概率
        a = np.tanh(np.dot(Wax, x) + np.dot(Waa, a_prev) + b)
        z = np.dot(Wya, a) + by
        y = softmax(z)

        # 为每个时间步的采样设置不同的随机种子
        np.random.seed(counter + seed) 

        # 从y中随机选出一个元素,返回这个元素在词表中的索引到idx中
        # choice的随机并不是纯粹地乱随机,它是根据y中每个元素的概率来选的。
        # 假如y中字母h的概率为30%,那么choice就有30%的概率选择返回h的索引到idx中。
        idx = np.random.choice(list(range(vocab_size)), p=y.ravel())

        # 添加本时间步采样到的这个字母的索引到indices中。
        indices.append(idx)

        # 将本时间步采样到的字母设置为下一个时间步的输入x
        x = np.zeros((vocab_size, 1))
        x[idx] = 1 # 因为x是一个one-hot向量,所以将idx对应的元素设置为1就可以让x代表本时间步采样到的字母了。

        # 将本时间步得到的激活值a设置为下一个时间步的输入a
        a_prev = a

        seed += 1
        counter +=1

    if (counter == 50):
        indices.append(char_to_ix['\n'])

    return indices
np.random.seed(2)
_, n_a = 20, 100
Wax, Waa, Wya = np.random.randn(n_a, vocab_size), np.random.randn(n_a, n_a), np.random.randn(vocab_size, n_a)
b, by = np.random.randn(n_a, 1), np.random.randn(vocab_size, 1)
parameters = {"Wax": Wax, "Waa": Waa, "Wya": Wya, "b": b, "by": by}

indices = sample(parameters, char_to_ix, 0)
print("Sampling:")
print("list of sampled indices:", indices)
print("list of sampled characters:", [ix_to_char[i] for i in indices])
Sampling:
list of sampled indices: [12, 17, 24, 14, 13, 9, 10, 22, 24, 6, 13, 11, 12, 6, 21, 15, 21, 14, 3, 2, 1, 21, 18, 24, 7, 25, 6, 25, 18, 10, 16, 2, 3, 8, 15, 12, 11, 7, 1, 12, 10, 2, 7, 3, 11, 9, 2, 12, 14, 24, 0]
list of sampled characters: ['l', 'q', 'x', 'n', 'm', 'i', 'j', 'v', 'x', 'f', 'm', 'k', 'l', 'f', 'u', 'o', 'u', 'n', 'c', 'b', 'a', 'u', 'r', 'x', 'g', 'y', 'f', 'y', 'r', 'j', 'p', 'b', 'c', 'h', 'o', 'l', 'k', 'g', 'a', 'l', 'j', 'b', 'g', 'c', 'k', 'i', 'b', 'l', 'n', 'x', '\n']

3 - 构建语言模型

下面我们来一步步地构建字母级别的语言模型

3.1 - 优化函数

下面我们将实现一个优化函数,在这个函数中我们会进行一次梯度下降,也就是执行一次前向传播,一次反向传播以及优化更新一次参数,这三个步骤我们之前已经学过了,所以为了节省篇幅我们在工具库里已经为大家实现了对应的函数,分别是rnn_forward,rnn_backward,update_parameters。本次我们使用的随机梯度下降,随机梯度下降的定义在《2.2.1 mini-batch》中——“如果你将一个样本当做一个子训练集的话,这就叫做随机梯度下降”。也就是说每次我们只输入一个训练样本,一个样本中只包含一个恐龙名。使用随机梯度下降时梯度值会乱跑,所以在优化函数中我们使用了裁剪函数。

# 优化函数

def optimize(X, Y, a_prev, parameters, learning_rate = 0.01):
    """
    执行一次梯度下降

    参数:
    X -- 包含了一个恐龙名的索引,例如可以是X = [None,3,5,11,22,3],
         对应于27个字符的词表就相当于X = [None,c,e,k,v,c]
    Y -- 真实标签Y也是这个恐龙名,只不过与X错了一下位,X = [3,5,11,22,3,0],最后一个0表示结尾。
         为什么要错位呢,因为当第一个时间步输入空None时,我们希望这个时间步的预测结果是3(即希望3的概率最大),
         当第二个时间步输入3时,我们希望这个时间步的预测结果是5.
    a_prev -- 上一次梯度下降得到的激活值
    parameters -- 参数字典:
                        Wax -- (n_a, n_x)
                        Waa -- (n_a, n_a)
                        Wya -- (n_y, n_a)
                        b --  (n_a, 1)
                        by -- (n_y, 1)

    返回值:
    loss -- 损失值
    gradients -- 梯度字典:
                        dWax -- (n_a, n_x)
                        dWaa -- (n_a, n_a)
                        dWya -- (n_y, n_a)
                        db -- (n_a, 1)
                        dby --(n_y, 1)
    a[len(X)-1] -- 本次梯度下降得到的激活值,维度是 (n_a, 1)
    """

    loss, cache = rnn_forward(X, Y, a_prev, parameters)

    gradients, a = rnn_backward(X, Y, parameters, cache)

    # 调用裁剪函数对梯度值进行裁剪
    gradients = clip(gradients, 5)

    # 用裁剪后的梯度来优化参数
    parameters = update_parameters(parameters, gradients, learning_rate)

    return loss, gradients, a[len(X)-1]
np.random.seed(1)
vocab_size, n_a = 27, 100
a_prev = np.random.randn(n_a, 1)
Wax, Waa, Wya = np.random.randn(n_a, vocab_size), np.random.randn(n_a, n_a), np.random.randn(vocab_size, n_a)
b, by = np.random.randn(n_a, 1), np.random.randn(vocab_size, 1)
parameters = {"Wax": Wax, "Waa": Waa, "Wya": Wya, "b": b, "by": by}
X = [None,3,5,11,22,3]
Y = [3,5,11,22,3,0]

loss, gradients, a_last = optimize(X, Y, a_prev, parameters, learning_rate = 0.01)
print("Loss =", loss)
print("gradients[\"dWaa\"][1][2] =", gradients["dWaa"][1][2])
print("np.argmax(gradients[\"dWax\"]) =", np.argmax(gradients["dWax"]))
print("gradients[\"dWya\"][1][2] =", gradients["dWya"][1][2])
print("gradients[\"db\"][4] =", gradients["db"][4])
print("gradients[\"dby\"][1] =", gradients["dby"][1])
print("a_last[4] =", a_last[4])
Loss = 142.21675878802185
gradients["dWaa"][1][2] = -2.4784176840522094
np.argmax(gradients["dWax"]) = 113
gradients["dWya"][1][2] = -0.9888264360547835
gradients["db"][4] = [5.]
gradients["dby"][1] = [0.98882644]
a_last[4] = [-0.99994779]

3.2 - 训练模型

def model(ix_to_char, char_to_ix, num_iterations = 35000, n_a = 50, dino_names = 7, vocab_size = 27):
    """
    训练模型,训练期间每训练2000次就采样生成7个名字

    参数:
    ix_to_char -- 索引转字母的字典
    char_to_ix -- 字母转索引的字典
    num_iterations -- 需要训练的次数
    n_a -- 设置RNN的神经元个数
    dino_names -- 每次需要产生多少个名字 
    vocab_size -- 词表大小,即字符种类数

    返回值:
    parameters -- 训练得到的最终参数
    """

    # 获取输入x和输出y向量的大小。
    # 因为输入x是one-hot向量,所以它的大小就等于词表的大小
    # 因为输出y是关于词表中每一个字母的概率,所以大小也与词表相同
    n_x, n_y = vocab_size, vocab_size

    # 根据神经元个数以及输入元素个数和输出元素个数来创建并初始化相应的Waa,by等等相关参数
    parameters = initialize_parameters(n_a, n_x, n_y)

    # 这个是用于在后面来使损失更加平滑的,不用太在意这个
    loss = get_initial_loss(vocab_size, dino_names)

    # 将数据集dinos.txt中的所有恐龙名读取处理
    with open("dinos.txt") as f:
        examples = f.readlines()
    examples = [x.lower().strip() for x in examples]

    # 打乱这些名字的顺序
    np.random.seed(0)
    np.random.shuffle(examples)

    a_prev = np.zeros((n_a, 1))

    # 进行训练
    for j in range(num_iterations):

        # 选出一个恐龙名来作为训练样本
        index = j % len(examples) # 除以j是为了防止索引超出名字数量
        X = [None] + [char_to_ix[ch] for ch in examples[index]] 
        Y = X[1:] + [char_to_ix["\n"]] # 除了第一个元素None外,将所有X元素赋值给Y,然后再加上个换行符

        # 进行一次训练
        curr_loss, gradients, a_prev = optimize(X, Y, a_prev, parameters)

        # 是损失更加平滑
        loss = smooth(loss, curr_loss)

        # 每训练2000次生成7个名字
        if j % 2000 == 0:

            print('Iteration: %d, Loss: %f' % (j, loss) + '\n')

            seed = 0
            # 循环7次,生成7个名字
            for name in range(dino_names): 

                # 在当前训练好的模型上进行采样,生成一个名字
                sampled_indices = sample(parameters, char_to_ix, seed)
                print_sample(sampled_indices, ix_to_char)

                seed += 1  # 增加采样的随机种子的值,避免每次生成的都是同样的名字

            print('\n')

    return parameters

执行下面的代码后,训练会跑起来。你注意观察结果,你会发现开始的名字都不像是名字,随着训练次数的增加,生成的字符串越来越像名字了。

parameters = model(ix_to_char, char_to_ix)
Iteration: 0, Loss: 23.087336

Nkzxwtdmfqoeyhsqwasjkjvu
Kneb
Kzxwtdmfqoeyhsqwasjkjvu
Neb
Zxwtdmfqoeyhsqwasjkjvu
Eb
Xwtdmfqoeyhsqwasjkjvu

Iteration: 2000, Loss: 27.884160

Liusskeomnolxeros
Hmdaairus
Hytroligoraurus
Lecalosapaus
Xusicikoraurus
Abalpsamantisaurus
Tpraneronxeros

Iteration: 4000, Loss: 25.901815

Mivrosaurus
Inee
Ivtroplisaurus
Mbaaisaurus
Wusichisaurus
Cabaselachus
Toraperlethosdarenitochusthiamamumamaon

Iteration: 6000, Loss: 24.608779

Onwusceomosaurus
Lieeaerosaurus
Lxussaurus
Oma
Xusteonosaurus
Eeahosaurus
Toreonosaurus

Iteration: 8000, Loss: 24.070350

Onxusichepriuon
Kilabersaurus
Lutrodon
Omaaerosaurus
Xutrcheps
Edaksoje
Trodiktonus

Iteration: 10000, Loss: 23.844446

Onyusaurus
Klecalosaurus
Lustodon
Ola
Xusodonia
Eeaeosaurus
Troceosaurus

Iteration: 12000, Loss: 23.291971

Onyxosaurus
Kica
Lustrepiosaurus
Olaagrraiansaurus
Yuspangosaurus
Eealosaurus
Trognesaurus

Iteration: 14000, Loss: 23.382339

Meutromodromurus
Inda
Iutroinatorsaurus
Maca
Yusteratoptititan
Ca
Troclosaurus

Iteration: 16000, Loss: 23.310540

Meuspsapcosaurus
Inda
Iuspsarciiasauruimphis
Macabosaurus
Yusociman
Caagosaurus
Trrasaurus

Iteration: 18000, Loss: 22.846780

Phytrohekosaurus
Mggaaeschachynthalus
Mxsstarasomus
Pegahosaurus
Yusidon
Ehantor
Troma

Iteration: 20000, Loss: 22.921921

Meutroinepheusaurus
Lola
Lytrogonosaurus
Macalosaurus
Ytrpangricrlosaurus
Elagosaurus
Trochiqkaurus

Iteration: 22000, Loss: 22.758659

Meutrodon
Lola
Mustodon
Necagpsancosaurus
Yuspengromus
Eiadrus
Trochepomushus

Iteration: 24000, Loss: 22.671906

Mitrseitan
Jogaacosaurus
Kurroelathateraterachus
Mecalosaurus
Yusicheosaurus
Eiaeosaurus
Trodon

Iteration: 26000, Loss: 22.628685

Niusaurus
Liceaitona
Lytrodon
Necagrona
Ytrodon
Ejaertalenthomenxtheychosaurus
Trochinictititanhimus

Iteration: 28000, Loss: 22.635401

Plytosaurus
Lmacaisaurus
Mustocephictesaurus
Pacaksela
Xtspanesaurus
Eiaestedantes
Trocenitaudosantenithamus

Iteration: 30000, Loss: 22.627572

Piutysaurus
Micaahus
Mustodongoptes
Pacagsicelosaurus
Wustapisaurus
Eg
Trochesaurus

Iteration: 32000, Loss: 22.250284

Mautosaurus
Kraballona
Lyusianasaurus
Macallona
Xustanarhasauruiraplos
Efaiosaurus
Trodondonsaurukusaurukusaurus

Iteration: 34000, Loss: 22.477635

Nivosaurus
Libaadosaurus
Lutosaurus
Nebahosaurus
Wrosaurus
Eiaeosaurus
Spidonosaurus

结论

虽然可能你英文不好,但是应该也能感觉到随着训练次数的增加,生成的字符串越来越像名字了。当然你还可以增加训练次数或者调整一些超参数,也许效果会更好。我们的数据集很小,所以能使用CPU来训练RNN。在商业项目中,数据集超大,将需要很多GPU来训练很长时间。

4 - 智能写诗

智能写诗其实和我们前面的智能起名其实大同小异。智能起名的数据集是恐龙的名字,而智能写诗的数据集是一些莎士比亚的诗歌"The Sonnets". 就是文档同目录下的shakespeare.txt文件。大家可能都知道莎士比亚,但是你们知道为什么称莎士比亚为老处男吗?因为它的名字取得不好——“啥是逼呀”。可能他的名字是由一个还没有训练好的模型给起的。

file

在智能写诗的模型中,我们使用了LSTM单元。之前因为名字很短,所以不需要RNN有很长的记忆,但是诗歌会很长,所以需要LSTM来增强RNN的记忆力。

由于大体步骤和上面的取名是差不多的,所以就不把代码贴出来了,模型已经为大家用Keras实现了,并且已经提前为大家训练了1000个epochs。因为如果在你们电脑上训练它,需要花很长时间。执行下面的代码就可以将训练好了的模型加载起来了。仅仅是加载这个训练好了的模型都要花好几分钟。

from __future__ import print_function
from keras.callbacks import LambdaCallback
from keras.models import Model, load_model, Sequential
from keras.layers import Dense, Activation, Dropout, Input, Masking
from keras.layers import LSTM
from keras.utils.data_utils import get_file
from keras.preprocessing.sequence import pad_sequences
from shakespeare_utils import *
import sys
import io
Using TensorFlow backend.
/usr/local/lib/python3.6/dist-packages/tensorflow/python/framework/dtypes.py:458: FutureWarning: Passing (type, 1) or '1type' as a synonym of type is deprecated; in a future version of numpy, it will be understood as (type, (1,)) / '(1,)type'.
  _np_qint8 = np.dtype([("qint8", np.int8, 1)])
/usr/local/lib/python3.6/dist-packages/tensorflow/python/framework/dtypes.py:459: FutureWarning: Passing (type, 1) or '1type' as a synonym of type is deprecated; in a future version of numpy, it will be understood as (type, (1,)) / '(1,)type'.
  _np_quint8 = np.dtype([("quint8", np.uint8, 1)])
/usr/local/lib/python3.6/dist-packages/tensorflow/python/framework/dtypes.py:460: FutureWarning: Passing (type, 1) or '1type' as a synonym of type is deprecated; in a future version of numpy, it will be understood as (type, (1,)) / '(1,)type'.
  _np_qint16 = np.dtype([("qint16", np.int16, 1)])
/usr/local/lib/python3.6/dist-packages/tensorflow/python/framework/dtypes.py:461: FutureWarning: Passing (type, 1) or '1type' as a synonym of type is deprecated; in a future version of numpy, it will be understood as (type, (1,)) / '(1,)type'.
  _np_quint16 = np.dtype([("quint16", np.uint16, 1)])
/usr/local/lib/python3.6/dist-packages/tensorflow/python/framework/dtypes.py:462: FutureWarning: Passing (type, 1) or '1type' as a synonym of type is deprecated; in a future version of numpy, it will be understood as (type, (1,)) / '(1,)type'.
  _np_qint32 = np.dtype([("qint32", np.int32, 1)])
/usr/local/lib/python3.6/dist-packages/tensorflow/python/framework/dtypes.py:465: FutureWarning: Passing (type, 1) or '1type' as a synonym of type is deprecated; in a future version of numpy, it will be understood as (type, (1,)) / '(1,)type'.
  np_resource = np.dtype([("resource", np.ubyte, 1)])

Loading text data...
Creating training set...
number of training examples: 31412
Vectorizing training set...
Loading model...

为了让大家感受一下,我们提供了下面这个代码单元,执行它可以让模型再被训练一个epoch,这个训练过程可能会花好几分钟。

print_callback = LambdaCallback(on_epoch_end=on_epoch_end)

model.fit(x, y, batch_size=128, epochs=1, callbacks=[print_callback])
Epoch 1/1
31412/31412 [==============================] - 177s - loss: 2.5650   

<keras.callbacks.History at 0x7f8c14069208>
# 这个工具函数会调用模型来为我们生成诗歌。运行这个代码后,下面会出现一个输入框,你在输入框里面输入一句英语,
# 然后按回车键,模型就会接着你的这句话继续写诗。例如我们在输入框里面写上“I love porn movie ”
generate_output()
Write the beginning of your poem, the Shakespeare machine will complete it. Your input is: Do you know?

Here is your poem: 

Do you know?
then i with thi hamare thy owh lecks in  hops,
his perponed thi dod' thas anmens extide;
still bethat my dealk me and eye's love,
and love mestay i be do everofer other,
before in my pill is forstling mefeswers.

 why should contay, though i senf not outere might,
when ail i defind cinde nended.

showed thei knows mest of the moret thou my braes,
on his weeter self kith my beluch deese.
 he th

当然,效果与商用产品比起来还差得很远,商用项目是需要很多时间人力计算力金钱的。

这个智能写诗的RNN与智能起名的RNN是很相似的,当然比智能起名的要高档了一点点,主要高档了下面3个方面:

  • 使用了LSTM
  • 使用了2层神经网络
  • 使用了Keras

如果你想了解智能写诗的代码,你可以去看Keras在GitHub上开源的智能写作代码: https://github.com/keras-team/keras/blob/master/examples/lstm_text_generation.py.

帮助方法

utils.py

import numpy as np

def softmax(x):
    e_x = np.exp(x - np.max(x))
    return e_x / e_x.sum(axis=0)

def smooth(loss, cur_loss):
    return loss * 0.999 + cur_loss * 0.001

def print_sample(sample_ix, ix_to_char):
    txt = ''.join(ix_to_char[ix] for ix in sample_ix)
    txt = txt[0].upper() + txt[1:]  # capitalize first character 
    print ('%s' % (txt, ), end='')

def get_initial_loss(vocab_size, seq_length):
    return -np.log(1.0/vocab_size)*seq_length

def softmax(x):
    e_x = np.exp(x - np.max(x))
    return e_x / e_x.sum(axis=0)

def initialize_parameters(n_a, n_x, n_y):
    """
    Initialize parameters with small random values

    Returns:
    parameters -- python dictionary containing:
                        Wax -- Weight matrix multiplying the input, numpy array of shape (n_a, n_x)
                        Waa -- Weight matrix multiplying the hidden state, numpy array of shape (n_a, n_a)
                        Wya -- Weight matrix relating the hidden-state to the output, numpy array of shape (n_y, n_a)
                        b --  Bias, numpy array of shape (n_a, 1)
                        by -- Bias relating the hidden-state to the output, numpy array of shape (n_y, 1)
    """
    np.random.seed(1)
    Wax = np.random.randn(n_a, n_x)*0.01 # input to hidden
    Waa = np.random.randn(n_a, n_a)*0.01 # hidden to hidden
    Wya = np.random.randn(n_y, n_a)*0.01 # hidden to output
    b = np.zeros((n_a, 1)) # hidden bias
    by = np.zeros((n_y, 1)) # output bias

    parameters = {"Wax": Wax, "Waa": Waa, "Wya": Wya, "b": b,"by": by}

    return parameters

def rnn_step_forward(parameters, a_prev, x):

    Waa, Wax, Wya, by, b = parameters['Waa'], parameters['Wax'], parameters['Wya'], parameters['by'], parameters['b']
    a_next = np.tanh(np.dot(Wax, x) + np.dot(Waa, a_prev) + b) # hidden state
    p_t = softmax(np.dot(Wya, a_next) + by) # unnormalized log probabilities for next chars # probabilities for next chars 

    return a_next, p_t

def rnn_step_backward(dy, gradients, parameters, x, a, a_prev):

    gradients['dWya'] += np.dot(dy, a.T)
    gradients['dby'] += dy
    da = np.dot(parameters['Wya'].T, dy) + gradients['da_next'] # backprop into h
    daraw = (1 - a * a) * da # backprop through tanh nonlinearity
    gradients['db'] += daraw
    gradients['dWax'] += np.dot(daraw, x.T)
    gradients['dWaa'] += np.dot(daraw, a_prev.T)
    gradients['da_next'] = np.dot(parameters['Waa'].T, daraw)
    return gradients

def update_parameters(parameters, gradients, lr):

    parameters['Wax'] += -lr * gradients['dWax']
    parameters['Waa'] += -lr * gradients['dWaa']
    parameters['Wya'] += -lr * gradients['dWya']
    parameters['b']  += -lr * gradients['db']
    parameters['by']  += -lr * gradients['dby']
    return parameters

def rnn_forward(X, Y, a0, parameters, vocab_size = 27):

    # Initialize x, a and y_hat as empty dictionaries
    x, a, y_hat = {}, {}, {}

    a[-1] = np.copy(a0)

    # initialize your loss to 0
    loss = 0

    for t in range(len(X)):

        # Set x[t] to be the one-hot vector representation of the t'th character in X.
        # if X[t] == None, we just have x[t]=0. This is used to set the input for the first timestep to the zero vector. 
        x[t] = np.zeros((vocab_size,1)) 
        if (X[t] != None):
            x[t][X[t]] = 1

        # Run one step forward of the RNN
        a[t], y_hat[t] = rnn_step_forward(parameters, a[t-1], x[t])

        # Update the loss by substracting the cross-entropy term of this time-step from it.
        loss -= np.log(y_hat[t][Y[t],0])

    cache = (y_hat, a, x)

    return loss, cache

def rnn_backward(X, Y, parameters, cache):
    # Initialize gradients as an empty dictionary
    gradients = {}

    # Retrieve from cache and parameters
    (y_hat, a, x) = cache
    Waa, Wax, Wya, by, b = parameters['Waa'], parameters['Wax'], parameters['Wya'], parameters['by'], parameters['b']

    # each one should be initialized to zeros of the same dimension as its corresponding parameter
    gradients['dWax'], gradients['dWaa'], gradients['dWya'] = np.zeros_like(Wax), np.zeros_like(Waa), np.zeros_like(Wya)
    gradients['db'], gradients['dby'] = np.zeros_like(b), np.zeros_like(by)
    gradients['da_next'] = np.zeros_like(a[0])

    ### START CODE HERE ###
    # Backpropagate through time
    for t in reversed(range(len(X))):
        dy = np.copy(y_hat[t])
        dy[Y[t]] -= 1
        gradients = rnn_step_backward(dy, gradients, parameters, x[t], a[t], a[t-1])
    ### END CODE HERE ###

    return gradients, a

为者常成,行者常至