---
# System prepended metadata

title: LSTM簡易問答機器人
tags: [TensorFlow, ChatBot, Python]

---

# LSTM簡易問答機器人
紀錄使用 LSTM 製作一個簡單的問答系統，可以根據問題的輸入輸出預定的答案。

**特點**
- 文本預處理： 輸入和輸出文本通過分詞器轉換成序列，並填充字數讓所有長度一致
- Embedding 層： 將詞彙索引映射到密集向量中，這有助於捕捉詞彙之間的語義關係和上下文信息。
- 雙向 LSTM 層： 兩個 LSTM 層被包裹在 Bidirectional 層中，能讓模型能夠在每個時間步中同時考慮過去和未來的序列信息。
- Dense 層： 用於預測詞彙的機率。

## Step 1: 數據準備
在建立一個簡單的問答系統時，第一步是準備數據。這個數據集包含了問題和答案，讓模型可以學習如何根據問題生成正確的答案。
```python=
data = [
    ("你好", "你好!"),
    ("你叫什麼名字?", "我是機器人"),
    ("你平常喜歡做什麼?", "不知道"),
    ("早安", "早安!")
]
```

### 解釋：
- `data` 是一個包含問題和對應答案的列表，這些問題和答案將成為我們模型的訓練數據。
- 每個元素包含兩個字串：問題（`input_texts`）和對應的答案（`output_texts`）。

## Step 2: 文本預處理
在建立模型之前，需要對文本進行預處理。要將文本轉換為數字序列，並確保它們的長度一致。
```python=
input_texts = [x[0] for x in data]
output_texts = [x[1] for x in data]

tokenizers = Tokenizer()
tokenizers.fit_on_texts(input_texts + output_texts)
input_seq = tokenizers.texts_to_sequences(input_texts)
output_seq = tokenizers.texts_to_sequences(output_texts)
```

### 解釋：
- `input_texts` 和 `output_texts` 分別從 `data` 中提取出所有的問題和答案。
- `Tokenizer` 是 Keras 提供的文本處理工具，它可以將文本轉換成數字序列，方便後續的模型處理。`fit_on_texts` 用來建立一個詞彙表，並為每個單詞分配一個唯一的索引。
- `texts_to_sequences` 將每個文本轉換成對應的數字序列。

## Step 3: 填充序列
由於每個問題和答案的長度可能不同，為了讓它們可以批次處理，需要將所有序列的長度填充為相同的值。
```python=
max_seq_len = max(max([len(seq) for seq in input_seq]), max([len(seq) for seq in output_seq]))
input_seq = pad_sequences(input_seq, maxlen=max_seq_len, padding='post')
output_seq = pad_sequences(output_seq, maxlen=max_seq_len, padding='post')
```

### 解釋：
- `max_seq_len` 計算了所有問題和答案中最大的長度，這樣我們可以為所有序列設置相同的長度。
- `pad_sequences` 會將短於最大長度的序列補充零，這樣所有序列的長度就會一致。`padding='post'` 表示補充的零會加在序列的尾部。

## Step 4: 構建LSTM模型
構建LSTM模型來訓練問答系統。
```python=
vocab_size = len(tokenizers.word_index) + 1
output_seq = to_categorical(output_seq, num_classes=vocab_size)

model = Sequential()
model.add(Embedding(vocab_size, 128, input_length=max_seq_len))
model.add(Bidirectional(LSTM(128, return_sequences=True)))
model.add(Bidirectional(LSTM(128)))
model.add(Dense(vocab_size, activation='softmax'))
```

### 解釋：
- `vocab_size` 是詞彙表的大小（詞彙表中包含的所有單詞數量），它用來設定模型的輸入和輸出層。
- `to_categorical` 函數將答案序列轉換為 one-hot 編碼的形式，這有助於計算交叉熵損失。
- `Sequential()` 是 Keras 中的一個順序模型，表示模型層會一層一層地堆疊。
- `Embedding` 層將單詞索引轉換為嵌入向量，這些向量有助於捕捉詞彙間的語義關係。
- `Bidirectional(LSTM)` 層是雙向LSTM，它可以讓模型同時考慮過去和未來的上下文信息。
- `Dense` 層使用 softmax 激活函數，將模型的輸出轉換為概率分佈，用來預測每個單詞的機率。

## Step 5: 編譯並訓練模型
編譯模型並開始訓練。
```python=
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
model.fit(input_seq, output_seq, epochs=100, batch_size=64)
```

### 解釋：
- `compile` 用來設置模型的損失函數（`categorical_crossentropy`）、優化器（`adam`）和評估指標（`accuracy`）。這些是訓練過程中必須指定的參數。
- `fit` 函數用來開始訓練模型。訓練過程中，模型將根據訓練數據調整權重。`epochs=100` 表示訓練100個回合，每次回合會處理所有的訓練數據；`batch_size=64` 表示每次訓練中處理64個樣本。

## Step 6: 預測回答
定義一個預測函數，根據用戶輸入的問題來預測答案。
```python
def predict(input_text):
    input_seq = tokenizers.texts_to_sequences([input_text])
    input_seq = pad_sequences(input_seq, maxlen=max_seq_len, padding='post')
    prediction = model.predict(input_seq)
    predicted_seq = np.argmax(prediction, axis=-1)
    return tokenizers.sequences_to_texts([predicted_seq])[0]
```

### 解釋：
- `predict` 函數接受用戶的輸入文本，並將其轉換為數字序列。
- `pad_sequences` 用來確保輸入序列的長度與訓練數據一致。
- `model.predict` 用來生成模型的預測結果，返回的是每個單詞的概率分佈。
- `np.argmax(prediction, axis=-1)` 選擇概率最高的單詞索引，這就是模型預測的答案。
- 最後，`tokenizers.sequences_to_texts` 將預測的數字序列轉換回文本格式，並返回模型生成的答案。




## 完整程式碼
```python=
import numpy as np
import tensorflow as tf
from tensorflow.keras.preprocessing.text import Tokenizer
from tensorflow.keras.preprocessing.sequence import pad_sequences
from tensorflow.keras.utils import to_categorical
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, LSTM, Bidirectional, Embedding

data = [
    ("你好", "你好!"),
    ("你叫什麼名字?", "我是機器人"),
    ("你平常喜歡做什麼?", "不知道"),
    ("早安", "早安!")
]

input_texts = [x[0] for x in data]
output_texts = [x[1] for x in data]


tokenizers = Tokenizer()
tokenizers.fit_on_texts(input_texts + output_texts)
input_seq = tokenizers.texts_to_sequences(input_texts)
output_seq = tokenizers.texts_to_sequences(output_texts)


max_seq_len = max(max([len(seq) for seq in input_seq]), max([len(seq) for seq in output_seq]))

input_seq = pad_sequences(input_seq, maxlen=max_seq_len, padding='post')
output_seq = pad_sequences(output_seq, maxlen=max_seq_len, padding='post')


vocab_size = len(tokenizers.word_index) + 1

output_seq = to_categorical(output_seq, num_classes=vocab_size)

model = Sequential()
model.add(Embedding(vocab_size, 128, input_length=max_seq_len))
model.add(Bidirectional(LSTM(128, return_sequences=True)))
model.add(Bidirectional(LSTM(128)))
model.add(Dense(vocab_size, activation='softmax'))

model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])

model.fit(input_seq, output_seq, epochs=100, batch_size=64)

def predict(input_text):
    input_seq = tokenizers.texts_to_sequences([input_text])
    input_seq = pad_sequences(input_seq, maxlen=max_seq_len, padding='post')
    prediction = model.predict(input_seq)
    predicted_seq = np.argmax(prediction, axis=-1)
    return tokenizers.sequences_to_texts([predicted_seq])[0]

print(predict("你叫什麼名字?"))
print(predict("你平常喜歡做什麼?"))
```