大数据,机器学习,人工智能人工智能/模式识别/机器学习精华专题

有了LSTM网络,我再也不怕老师让我写作文了

2019-02-01  本文已影响19人  望月从良

随着深度学习的迅猛发展,人工智能的强大能力已经超出了模仿人类的简单动作,例如识别物体,如今已经能发展到自动驾驶,而且车开的比人都好的地步。目前深度学习进化出的一大功能是能够进行艺术创作,前几年google开发的DeepDream算法能够自己绘制出犹如毕加索抽象画般的艺术作品,而现在使用LSTM网络甚至可以开发出自动作曲程序,据说现在很多曲调都是由深度学习网络创作的。

很多艺术创作其实是通过序列号数据构成的,例如文章其实是一个个单词前后相邻构成,音乐是一个个音符前后相邻构成,甚至绘画也是笔触前后相邻构成,因此艺术创作从数学上看其实是时间序列数据,而LSTM忘了是最擅长处理时间序列数据的,因此只要我们训练网络识别相应艺术创作的时间序列中的数据规律,我们就可以利用网络进行相应的创作。

我们要创建的网络具有的功能是自动写作。我们把含有N个单词的句子输入网络,让网络预测第N+1个单词,然后把预测结果重新输入网络,让网络预测第N+2个单词,这种自我循环能让网络创作出跟人写出来几乎一模一样的句子。例如我们有句子"hello Tom, how are you",我们把"hello Tom, how"输入网络后网络预测下个单词是"are",然后我们继续把"hello Tom, how are"输入网络,网络预测下一个单词是"you",网络运行的基本流程如下图:

1.png

上图中数据采样很重要,通常我们会从下一个可能单词的概率分布中,选择概率最大的那个单词,但是这么做会导致生成的句子不流畅,看起来不像人写得。通用做法是在可能性最高的若干个单词集合中进行一定随机选择。例如网络预测某个词的概率是30%,那么我们引入一种随机方法,使得该词被选中的概率是30%。

我们引入的随机方法,它的随机性必须要有所控制。如果随机性为0,那么最终网络创作的句子就没有一点创意,如果随机性太高,那么得到的句子在逻辑上可能就比较离谱,因此我们要把随机性控制在某个程度。于是我们引入一个控制随机性的参数叫temperature,也就是温度的意思。

在前面章节我们多次看到,当网络要给出概率时,最后输出层时softmax,它会输出一个向量,向量中每个分量的值是0到1间的小数,所有分量加总得1.我们假设这个向量用original_distributin表示,那么我们用下面的方法引入新的随机性:

def  reweight_distribution(original_distribution, temperature=0.5):
  distribution = np.log(original_distribution) / temperature
  distribution = np.exp(distribution)
  return distribution / np.sum(distribution)

上面代码会把网络softmax层输出的结果重新打乱,打乱的程度由tenperature来控制,它的值越大,打乱的程度就越高。接下来我们做一个LSTM网络,它预测的下一个元素是字符而不是我们前面所说的单词。

深度学习网络进行文章创作时,与用于输入它的文本数据相关。如果你用莎士比亚的作品作为训练数据,网络创作的文章与莎士比亚就很像,如果我们在上面函数中引入随机性,那么网络创作结果就会有一部分像莎士比亚,有一部分又不像,而不像的那部分就是网络创作的艺术性所在,下面我们用德国超人哲学创始人尼采的文章训练网络,让我们通过深度学习再造一个新的哲学家,首先我们要加载训练数据:

import  keras
import  numpy as np

path = keras.utils.get_file('nietzche.txt', 
                            origin = 'https://s3.amazonaws.com/text-datasets/nietzsche.txt')
text = open(path).read().lower()
print('Corpus length: ', len(text))

上面代码运行后,我们会下载单词量为600893的文本数据。接着我们以60个字符为一个句子,第61个字符作为预测字符,也就是告诉网络看到这60个字符后你应该预测第61个字符,同时前后两个采样句子之间的间隔是3个字符:

maxlen = 60
step = 3
setences = []
#next_chars 对应下一个字符,以便用于训练网
next_chars = []

for i in range(0, len(text) - maxlen, step):
  setences.append(text[i : i + maxlen])
  next_chars.append(text[i + maxlen])
  
print('Number of sequentence: ', len(setences))

chars = sorted(list(set(text)))
print('Unique characters: ', len(chars))
#为每个字符做编号
char_indices = dict((char, chars.index(char)) for char in chars)
print('Vectorization....')
'''
整个文本中不同字符的个数为chars, 对于当个字符我们对他进行one-hot编码,
也就是构造一个含有chars个元素的向量,根据字符对于的编号,我们把向量的对应元素设置为1,
一个句子含有60个字符,因此一行句子对应一个二维句子(maxlen, chars),矩阵的行数是maxlen,列数
是chars
'''
x = np.zeros((len(setences), maxlen, len(chars)), dtype = np.bool)
y = np.zeros((len(setences), len(chars)), dtype = np.bool)

for i, setence in enumerate(setences):
  for t, char in enumerate(setence):
    x[i, t, char_indices[char]] = 1
  y[i, char_indices[next_chars[i]]] = 1

上面代码中构造的x就是输入数据,当输入句子是x时,我们要调教网络去预测下一个字符是y。代码先统计文本资料总共有多少个不同的字符,这些字符包含标点符号,根据运行结果显示,文本总共有57个不同字符,同时我们将不同字符进行编号。

然后构造含有57个元素的向量,当句子中某个字符出现时,我们就把向量中下标对应字符编号的元素设置为1,我们这些向量输入到网络进行训练:

from keras import layers

model = keras.models.Sequential()
model.add(layers.LSTM(128, input_shape(maxlen, len(chars))))
model.add(layers.Dense(len(chars), activation = 'softmax'))
optimizer = leras.optimizers.RMSprop(lr = 0.01)
model.compile(loss = 'categorical_crossentropy', optimizer = optimizer)

网络输出结果对应一个含有57个元素的向量,每个元素对应相应编号的字符,元素的值表示下一个字符是对应字符的概率。我们按照前面说过的方法对网络给出的概率分布引入随机性,然后选出下一个字符,把选出的字符添加到输入句子中形成新的输入句子传入到网络,让网络以同样的方法判断下一个字符:

def  sample(preds, temperature = 1.0):
  preds = np.asarray(preds).astype('float64')
  preds = np.log(preds) / temperature
  exp_preds = np.exp(preds)
  preds = exp_preds / np.sum(exp_preds)
  '''
  由于preds含有57个元素,每个元素表示对应字符出现的概率,我们可以把这57个元素看成一个含有57面的骰子,
  骰子第i面出现的概率由preds[i]决定,然后我们模拟丢一次这个57面骰子,看看出现哪一面,这一面对应的字符作为
  网络预测的下一个字符
  '''
  probas = np.random.multinomial(1, preds, 1)
  return np.argmax(probas)

接着我们启动训练流程:

import random
import sys

for epoch in range(1, 60):
  print('epoch:', epoch)
  model.fit(x, y, batch_size = 128, epochs = 1)
  start_index = random.randint(0, len(text) - maxlen - 1)
  generated_text = text[start_index: start_index + maxlen]
  print('---Generating with seed:"' + generated_text + '"')
  
  for temperature in [0.2, 0.5, 1.0, 1.2]:
    print('---temperature:', temperature)
    #先输出一段原文
    sys.stdout.write(generated_text)
    '''
    根据原文,我们让网络创作接着原文后面的400个字符组合成的段子
    '''
    for i in range(400):
      sampled = np.zeros((1, maxlen, len(chars)))
      for t, char in enumerate(generated_text):
        sampled[0, t, char_indices[char]] = 1.
        
      #让网络根据当前输入字符预测下一个字符
      preds = model.predict(sampled, verbose = 0)[0]
      next_index = sample(preds, temperature)
      next_char = chars[next_index]
      
      generated_text += next_char
      generated_text = generated_text[1:]
      
      sys.stdout.write(next_char)
      sys.stdout.flush()
    
    print()

上面代码将尼采的作品输入到网络进行训练,训练后网络生成的段子就会带上明显的尼采风格,代码最好通过科学上网的方式,通过谷歌的colab,运行到GPU上,如果在CPU上运行,它训练的速度会非常慢。

我们看看经过20多次循环训练后,网络生成文章的效果如下:


屏幕快照 2019-01-31 下午4.11.37.png

输出中,Generating with seed 后面的语句是我们从原文任意位置摘出的60个字符。接下来的文字是网络自动生成的段子。当temperature值越小,网络生成的段子与原文就越相似,值越大,网络生成的段子与原文差异就越大,随着epoch数量越大,也就是网络训练次数越多,它生成的段子就越通顺,而且表达的内容也越有创意。

注意到随着temperature值越大,网络合成的词语错误也越多,有些单词甚至是几个字符的随机组合。从观察上来看,temperature取值0.5的效果是最好的。

更详细的讲解和代码调试演示过程,请点击链接

更多技术信息,包括操作系统,编译器,面试算法,机器学习,人工智能,请关照我的公众号:


这里写图片描述
上一篇下一篇

猜你喜欢

热点阅读