LSTM 生成文章

隨着深度學習的迅猛發展,人工智能的強大能力已經超出了模仿人類的簡單動作,例如識別物體,如今已經能發展到自動駕駛,而且車開的比人都好的地步。目前深度學習進化出的一大功能是能夠進行藝術創作,前幾年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的效果是最好的。

更詳細的講解和代碼調試演示過程,請點擊鏈接

更多技術信息,包括操作系統,編譯器,面試算法,機器學習,人工智能,請關照我的公衆號:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章