Day29 - 人工智慧 X.

斜槓學習 – 零基礎成為 AI 解夢大師秘笈

本文作者:Ovien

系列文章簡介

自由團隊將從0到1 手把手教各位讀者學會(1)Python基礎語法、(2)Python Web 網頁開發框架 – Django 、(3)Python網頁爬蟲 – 周易解夢網、(4)Tensorflow AI語言模型基礎與訓練 – LSTM、(5)實際部屬AI解夢模型到Web框架上。

學習資源

AI . FREE Team 讀者專屬福利 → Python Basics 免費學習資源

LSTM 的起源

RNN 在長篇文章的預測中,經常會有前期資訊到後面對決策影響越小,也就是前面的權重在後期的影響會越來越小的梯度消失問題

而為了解決這個問題,1997 年 LSTM (Long Short-Trem Memory) 首次發表於論文上,其獨特的設計可以良好的處理間隔與時序較長的Task,而真正發揚光大是在 2009 年的 ICDAR 的手寫識別比賽,透過LSTM建立的模型取得冠軍。

架構與公式

LSTM 雖然看起來很複雜,但是在計算上其實與 RNN 是有相似之處,都是會透過前一個的輸出加上這次計算的輸入,不過LSTM 特別之處就在有額外3個門閘(gate),會決定 input 是否重要到能被記住及能不能被輸出到 output。

1.忘記階段 :

  • 這個階段主要是對上一個節點傳進來的輸入進行選擇性忘記。簡單來說就是“忘記不重要的,記住重要的”。

2.選擇記憶階段 :

3.輸出階段 :

4.最後

利用 LSTM 訓練解夢模型

  • 準備數據、前處理

這次的數據,我們就從靈狐算命網爬取資料

成功爬取數據後,接下來我們要逐一清理數據。

text_sentence = []

with open("clean_all.txt","r") as f:
  for i in f.readlines():   
    if i.strip() != '':
      text_sentence.append(i.strip())
      
text =  ''.join(text_sentence)
text[:50]

※ 這裡的 with open("clean_all.txt","r") as f: 路徑因人而異,大家放檔案的位置可能都不同,這裡使用的是相對路徑。

查看資料集的字數

n = len(text)
w = len(set(text))
print(f'這個資料集共有{n}個字')
print(f'這個資料集不重複的字有{w}個字')
  • 將文字轉成數字 (index)

當資料準備好也清理完後,我們要將文本轉成數字,因為電腦只看得懂數值而非文字;使用tensorflow.keras提供的tokenizer,來tokenize我們的文本,將文本中的每一個文字賦予一個唯一的數值,進而建立一個字典。

Minion※ 以上只是示意圖,實際上不會是這樣排的,要看 Python 預設的編碼進行排序。

接下來 import 兩個套件,一個是numpy提供了很好的矩陣運算 API,不論是在基礎運算或是高階函式庫,都是開發者做深度學習時最好的朋友;另一個則是tensorflow,google 實驗室開發的深度學習框架,提供的高階 API 可以加速我們的開發流程。

首先宣告一個tf.keras.preprocessing.text.Tokenizer,來作為 tokenizer。

import tensorflow as tf
import numpy as np

# 初始化一個以字為單位的 Tokenizer
tokenizer = tf.keras.preprocessing.text.Tokenizer(
        num_words=w,
        char_level=True,
        filters='')
將我們的整份文本丟進去,它會將整份文本看過,第二行 code 則是將每個字賦值 (indexing)
# 將文字轉成inttokenizer.fit_on_texts(text)
text_as_int = tokenizer.texts_to_sequences([text])[0]
print(text_as_int[:10])

len(text_as_int)

這裡,我們簡單看一下文本與字典之間的關聯,使用 tokenizer 的 function index_word 將數值轉回文字並檢查:

start_idx = 10
end_idx = 20
partial_indices = text_as_int[start_idx:end_idx]

partial_texts = [
    tokenizer.index_word[idx] for idx in partial_indices
]
print("原本的中文字序列:")
print()
print(partial_texts)
print()
print()
print("轉換後的索引序列:")
print()
print(partial_indices)

現在整份解夢文本,都已經被轉換成數值,就像摩斯密碼一樣...

準備可以丟進模型的文本

看到這裡大家一定會有疑問,該清理都清了、該轉數值也轉了,為什麼還不能丟進模型?

因為我們必須先定義好,要丟給模型的資料集要長什麼樣子,再對數據做適當的轉換,再去設計 AI 模型的結構,檢視目前的資料集:

_type = type(text_as_int)
n = len(text_as_int)
print(f"text_as_int 是一個 {_type}\n")
print(f"文本的序列長度: {n}\n")
print("前 5 索引:", text_as_int[:5])

給定一個字符或者一個字符序列,下一個最可能出現的字符是什麼? → 這就是我們訓練模型要執行的任務,每個步驟(time step)預測下一個字符是什麼。

print("實際丟給模型的數字序列:")
print(partial_indices[:-1])
print()
print("方便我們理解的文本序列:")
print(partial_texts[:-1])

而模型要給我們的理想輸出應該是原資料集向左位移一個字對應的結果,如下圖:

print("實際丟給模型的文本序列:")
print(partial_texts[:-1])
print()
print("模型預期輸出的文本序列:")
print(partial_texts[1:])

定義好問題之後,使用 tensorflow 的 API: tf.data.Dataset.from_tensor_slices,將轉換成數值的文本,轉成tensorflow 可以接受的 tensor

# 將list轉換成tensor
characters = tf.data.Dataset.from_tensor_slices(text_as_int)
characters

接續設定 dataset 裡的每筆資料長度,使用 batch 這個內建的 function。

# tensorflow 的 dataset
SEQ_LENGTH = 10  

# 數字序列長度
sequences = characters.batch(SEQ_LENGTH+1,drop_remainder=True)

for item in sequences.take(1):
  d = tokenizer.index_word
  print('dataset索引序列',[i.numpy() for i in item])
  print('dataset文本序列',[d[i.numpy()] for i in item])

宣告一個function build_seq_pairs 來幫我們建立成對的句子(輸入/輸出),這也是為什麼剛剛要 SEQ_LENGTH+1 ,因為我們丟入一個長度11的句子,他才能夠返回長度各為10的(輸入/輸出),並使用dataset的內建function map ,讓全部的文本都做過一輪。

def build_seq_pairs(chunk):
    input_text = chunk[:-1]
    target_text = chunk[1:]    
    return input_text, target_text
ds = sequences.map(build_seq_pairs)

for idx,item in enumerate(ds.take(2)):
  d = tokenizer.index_word
  print(f"第 {idx} 個輸入句子的索引序列")
  print([i for i in item[0].numpy()])
  print(f"第 {idx} 個輸入句子的文本序列:")
  print([d[i] for i in item[0].numpy()])
  print()
  print(f"第 {idx} 個輸出句子的索引序列:")
  print([i for i in item[1].numpy()])
  print(f"第 {idx} 個輸出句子的文本序列:")
  print([d[i] for i in item[1].numpy()])
  print('-'*100)

接下來我們要設定訓練時,批次量BATCH_SIZE 以及設定緩衝區大小 BUFFER_SIZE 重新排列數據集;BATCH_SIZE 決定的是模型一次同時看多少個的句子,使用 GPU 平行運算來提升運算效率,這也是為什麼我們要使用 tensortensorflow被設定為可以處理無限的序列,它並不會在內存嘗試不同的組合,因此需要宣告一個緩衝區也就是BUFFER_SIZE,它維持一個緩衝區,在緩衝區重新排列元素。

BATCH_SIZE = 64
BUFFER_SIZE = 10000 
ds = ds.shuffle(BUFFER_SIZE).batch(BATCH_SIZE, drop_remainder=True)

※開發小知識:batch_size 在訓練時會盡量設成2的n次方,因為電腦的記憶體配置是用二進制去做運算,因此會稍微加速(讀者們可以實驗看看)

定義模型與超參數

使用 keras 基於 tensorflow 的高級 API 來實作

  • 使用 tf.keras.Sequential 定義模型。

  • 使用了三個層來定義模型:

    • tf.keras.layers.Embedding:輸入層。一個可訓練的對照表,它會將每個字符的數字映射到一個 embedding_dim 維度的向量。

    • tf.keras.layers.LSTM:LSTM 模型,其大小由 units=UNITS 定義。

    • tf.keras.layers.Dense:輸出層,帶有 vocab_size 個輸出。

EMBEDDING_DIM = 512
UNITS = 1024
LEARNING_RATE = 0.001

model = tf.keras.Sequential()
model.add(tf.keras.layers.Embedding(input_dim=w, output_dim=EMBEDDING_DIM,batch_input_shape=[BATCH_SIZE, None]))
model.add(tf.keras.layers.LSTM(units=UNITS, return_sequences=True, stateful=True, recurrent_initializer='glorot_uniform'))
model.add(tf.keras.layers.Dense(w))

model.summary()

model.summary() 可以檢視已經建完的模型

Model: "sequential"_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
embedding (Embedding)        (128, None, 512)          2787840   
_________________________________________________________________
lstm (LSTM)                  (128, None, 1024)         6295552   
_________________________________________________________________
dense (Dense)                (128, None, 5445)         5581125   
=================================================================
Total params: 14,664,517
Trainable params: 14,664,517
Non-trainable params: 0
_________________________________________________________________

這裡的 output 指的是文本當中每個字的機率值,然後輸出會選出機率最大的那個字,作為預測的下一個字。

現在運行這個模型,看看它是否按我們對於模型的假設運做,首先檢查輸出的形狀(shape):

for input_example_batch, target_example_batch in ds.take(1):
  example_batch_predictions = model(input_example_batch)  
  print(example_batch_predictions.shape, "# (batch_size, sequence_length, w)")

看起來模型沒問題,接下來定義優化器 (optimizer) 和損失函數 (loss_function) 

宣告一個function來作為損失函數(loss_function),本文的問題可以被視為一個有 w 個分類(字)的問題,而要定義分類問題的損失相對簡單,使用 sparse_categorical_crossentropy 是個不錯的選擇!

def loss(y_true, y_pred):
    return tf.keras.losses.sparse_categorical_crossentropy(y_true, y_pred, from_logits=True)

而 優化器(optimizer) 使用不論在任何任務中,表現都不錯的 tf.keras.optimizers.Adam ,除了學習率(learning_rate)之外,其他採用默認參數,並使用 tf.keras.Model.compile 方法配置訓練步驟,以及損失函數:

model.compile(
    optimizer=tf.keras.optimizers.Adam(learning_rate=LEARNING_RATE), loss=loss
)

訓練模型

在一番複雜的前置作業後,終於到重頭戲:訓練的部分啦!我們使用 fit 來訓練,並且把資料 dsepochs,作為 fit 的參數:

EPOCHS = 30 
history = model.fit(
    ds,
    epochs=EPOCHS, 
)
跑完 30 個 epochs 訓練完成後,儲存模型:
# 儲存訓練完成的模型
model.save('/content/gdrive/My Drive/LAB/IT_post/model_01.h5')

keras 的 model 在訓練時,會有像 tqdm 的進度條,能讓開發看到模型訓練的進度。

Epoch 1/10
2668/2668 [==============================] - 694s 260ms/step - loss: 3.3912
Epoch 2/10
2668/2668 [==============================] - 695s 260ms/step - loss: 2.5633
Epoch 3/10
2668/2668 [==============================] - 699s 262ms/step - loss: 2.3310
Epoch 4/10
2668/2668 [==============================] - 696s 261ms/step - loss: 2.2017
Epoch 5/10
2668/2668 [==============================] - 693s 260ms/step - loss: 2.1163
Epoch 6/10
2668/2668 [==============================] - 690s 259ms/step - loss: 2.0567
Epoch 7/10
2668/2668 [==============================] - 689s 258ms/step - loss: 2.0134
Epoch 8/10
2668/2668 [==============================] - 690s 259ms/step - loss: 1.9830
Epoch 9/10
2668/2668 [==============================] - 692s 259ms/step - loss: 1.9613
Epoch 10/10
  73/2668 [..............................] - ETA: 11:07 - loss: 2.3482

建構生成的模型

模型訓練完成後,需要額外建立生成的模型,再將原本訓練完成的模型權重,導入到生成模型中 (跟訓練時一樣的超參數,只差在 BATCH_SIZE 為 1)

EMBEDDING_DIM = 512
RNN_UNITS = 1024
BATCH_SIZE = 1

# 定義生成的模型
infer_model = tf.keras.Sequential()
infer_model.add(tf.keras.layers.Embedding(input_dim=w, output_dim=EMBEDDING_DIM, batch_input_shape=[BATCH_SIZE, None]))
infer_model.add(tf.keras.layers.LSTM(units=RNN_UNITS, return_sequences=True, stateful=True))
infer_model.add(tf.keras.layers.Dense(w))

# 載入已儲存模型之權重
infer_model.load_weights('/content/gdrive/My Drive/LAB/IT_post/model_01.h5')
infer_model.build(tf.TensorShape([1, None]))

可以看到,我們的模型 batch_size 設成1,因為我們每一次只需要讓 AI 模型讀取一段前段文字去生成下個段落;但在模型訓練時,我們是為了要提升運算效率(平行運算),因此 batch_size 才會設大於1。

我們使用 load_weights 作為讀取權重;build 會重建模型架構,而 tf.TensorShape 會將 BATCH_SIZE (維度)固定為1。

※ 因為循環神經網路傳遞狀態的方式,一旦建好模型,BATCH_SIZE 就不能做變動了。但在實際生成文章時,我們需要修改讓 BATCH_SIZE 等於 1。

檢視模型

infer_model.summary()

生成文章

因為我們在訓練時是用數值表示,生成也會是數值,因此我們要寫一段程式去將數值轉換回文字。

text_generated = '夢見老鼠'

for i in range(100):

  dream = tokenizer.texts_to_sequences([text_generated])[0]
  
  # 增加 batch 維度丟入模型取得預測結果後,再降維,拿掉 batch 維度
  input = tf.expand_dims(dream, axis=0)
  predictions = infer_model(input)
  predictions = tf.squeeze(predictions, 0)
  
  # 取得這個時間點模型生成的中文字
  predicted_id = tf.random.categorical(predictions, num_samples=1)[-1,0].numpy()
  partial_texts = [tokenizer.index_word[predicted_id]]
  text_generated += partial_texts[0]  
  
  # 成功生成 解夢文字檔 → text_generated
  # 透過擷取解夢內容作呈現
  print(text_generated.split('\n')[0].split('。')[0])

恭喜各位讀者到這邊,已經成為 AI 解夢大師,接下來最後一篇教學,會引導讀者們如何實際部署自己的 AI 模型到網頁上!

想更深入認識 AI . FREE Team ?

自由團隊 官方網站:https://aifreeblog.herokuapp.com/
自由團隊 Github:https://github.com/AI-FREE-Team/
自由團隊 粉絲專頁:https://www.facebook.com/AI.Free.Team/
自由團隊 IG:https://www.instagram.com/aifreeteam/
自由團隊 Youtube:https://www.youtube.com/channel/UCjw6Kuw3kwM_il39NTBJVTg/

文章同步發布於:第十二屆 IT 挑戰賽部落格

(繼續閱讀下一篇教學...)