更新至 TensorFlow 2.0 alpha 版本
本教程使用評論文本對電影評論進行分類(正面或負面)。這是二分類的一個例子,一種重要且廣泛適用的機器學習問題。
我們將使用包含來自 IMDB 的 50,000 條電影評論文本的 IMDB 數據集。它們被分爲 25,000 條訓練評論和 25,000 條測試評論。訓練和測試集是平衡的,這意味着它們包含相同數量的正面和負面評論。
本教程使用 tf.keras,一個高級API,用於在 TensorFlow 中構建和訓練模型。有關使用 tf.keras
進行更高級的文本分類教程,請參閱 MLCC 文本分類指南。
!pip install -q tensorflow==2.0.0-alpha0
from __future__ import absolute_import, division, print_function
import tensorflow as tf
from tensorflow import keras
import numpy as np
print(tf.__version__)
2.0.0-alpha0
下載 IMDB 數據集
IMDB 數據集已包含在 TensorFlow 中,並且經過預處理,使得評論(單詞序列)已經被轉換爲整數序列,其中每個整數表示字典中的一個特定單詞。
以下代碼將 IMDB 數據集下載到你的計算機中(如果你已經下載了它,則使用緩存副本):
imdb = keras.datasets.imdb
(train_data, train_labels), (test_data, test_labels) = imdb.load_data(num_words=10000)
Downloading data from https://storage.googleapis.com/tensorflow/tf-keras-datasets/imdb.npz
17465344/17464789 [==============================] - 0s 0us/step
參數 num_words=10000
保留了訓練數據中最常出現的 10,000 個單詞。丟棄少見的單詞以保持數據大小的可管理性。
探索數據
讓我們花一點時間來了解數據的格式。數據集經過預處理:每個樣本都是一個整數數組,表示電影評論的單詞。每個標籤都是 0 或 1 的整數值,其中 0 表示負面評論,1 表示正面評論。
print("Training entries: {}, labels: {}".format(len(train_data), len(train_labels)))
Training entries: 25000, labels: 25000
評論文本已轉換爲整數,其中每個整數表示字典中的特定單詞。以下是第一篇評論的內容:
print(train_data[0])
[1, 14, 22, 16, 43, 530, 973, 1622, 1385, 65, 458, 4468, 66, 3941, 4, 173, 36, 256, 5, 25, 100, 43, 838, 112, 50, 670, 2, 9, 35, 480, 284, 5, 150, 4, 172, 112, 167, 2, 336, 385, 39, 4, 172, 4536, 1111, 17, 546, 38, 13, 447, 4, 192, 50, 16, 6, 147, 2025, 19, 14, 22, 4, 1920, 4613, 469, 4, 22, 71, 87, 12, 16, 43, 530, 38, 76, 15, 13, 1247, 4, 22, 17, 515, 17, 12, 16, 626, 18, 2, 5, 62, 386, 12, 8, 316, 8, 106, 5, 4, 2223, 5244, 16, 480, 66, 3785, 33, 4, 130, 12, 16, 38, 619, 5, 25, 124, 51, 36, 135, 48, 25, 1415, 33, 6, 22, 12, 215, 28, 77, 52, 5, 14, 407, 16, 82, 2, 8, 4, 107, 117, 5952, 15, 256, 4, 2, 7, 3766, 5, 723, 36, 71, 43, 530, 476, 26, 400, 317, 46, 7, 4, 2, 1029, 13, 104, 88, 4, 381, 15, 297, 98, 32, 2071, 56, 26, 141, 6, 194, 7486, 18, 4, 226, 22, 21, 134, 476, 26, 480, 5, 144, 30, 5535, 18, 51, 36, 28, 224, 92, 25, 104, 4, 226, 65, 16, 38, 1334, 88, 12, 16, 283, 5, 16, 4472, 113, 103, 32, 15, 16, 5345, 19, 178, 32]
電影評論的長度可能不同。以下代碼顯示了第一個和第二個評論中的字數。由於神經網絡的輸入必須是相同的長度,因此我們稍後需要解決此問題。
len(train_data[0]), len(train_data[1])
(218, 189)
將整數轉換回單詞
瞭解如何將整數轉換回文本可能很有用。這裏,我們將創建一個輔助函數來查詢包含整數到字符串映射的字典對象:
# 獲取單詞到整數索引的映射,格式爲字典
word_index = imdb.get_word_index()
# 保留第一個索引
word_index = {k:(v+3) for k,v in word_index.items()}
word_index["<PAD>"] = 0
word_index["<START>"] = 1
word_index["<UNK>"] = 2 # unknown
word_index["<UNUSED>"] = 3
reverse_word_index = dict([(value, key) for (key, value) in word_index.items()])
def decode_review(text):
return ' '.join([reverse_word_index.get(i, '?') for i in text])
Downloading data from https://storage.googleapis.com/tensorflow/tf-keras-datasets/imdb_word_index.json
1646592/1641221 [==============================] - 0s 0us/step
現在我們可以使用 decode_review
函數顯示第一個評論的文本:
decode_review(train_data[0])
"<START> this film was just brilliant casting location scenery story direction everyone's really suited the part they played and you could just imagine being there robert <UNK> is an amazing actor and now the same being director <UNK> father came from the same scottish island as myself so i loved the fact there was a real connection with this film the witty remarks throughout the film were great it was just brilliant so much that i bought the film as soon as it was released for <UNK> and would recommend it to everyone to watch and the fly fishing was amazing really cried at the end it was so sad and you know what they say if you cry at a film it must have been good and this definitely was also <UNK> to the two little boy's that played the <UNK> of norman and paul they were just brilliant children are often left out of the <UNK> list i think because the stars that play them all grown up are such a big profile for the whole film but these children are amazing and should be praised for what they have done don't you think the whole story was so lovely because it was true and was someone's life after all that was shared with us all"
準備數據
評論 —— 整數數組 —— 必須在輸入神經網絡之前轉換爲張量。這種轉換可以通過以下兩種方式完成:
- 對數組進行獨熱編碼,將其轉換爲 0 和 1 的向量。例如,序列 [3, 5] 將變成 10,000 維向量,除了索引 3 和 5(它們是1)之外全部爲零。然後,將其作爲我們網絡中的第一層 —— 一個可以處理浮點矢量數據的 Dense 層。然而,這種方法是存儲密集型的,需要
num_words * num_reviews
大小的矩陣。 - 或者,我們可以填充數組,使它們都具有相同的長度,然後創建一個整數張量,大小爲
max_length * num_reviews
。我們可以使用能夠處理這種大小的 Embedding 層作爲我們網絡中的第一層。
本教程中,我們將使用第二種方法。
由於電影評論的長度必須相同,我們將使用 pad_sequences 函數來標準化評論長度:
train_data = keras.preprocessing.sequence.pad_sequences(train_data,
value=word_index["<PAD>"],
padding='post',
maxlen=256)
test_data = keras.preprocessing.sequence.pad_sequences(test_data,
value=word_index["<PAD>"],
padding='post',
maxlen=256)
我們現在看一下樣本的長度:
len(train_data[0]), len(train_data[1])
(256, 256)
並檢查(填充後的)第一個評論:
print(train_data[0])
[ 1 14 22 16 43 530 973 1622 1385 65 458 4468 66 3941
4 173 36 256 5 25 100 43 838 112 50 670 2 9
35 480 284 5 150 4 172 112 167 2 336 385 39 4
172 4536 1111 17 546 38 13 447 4 192 50 16 6 147
2025 19 14 22 4 1920 4613 469 4 22 71 87 12 16
43 530 38 76 15 13 1247 4 22 17 515 17 12 16
626 18 2 5 62 386 12 8 316 8 106 5 4 2223
5244 16 480 66 3785 33 4 130 12 16 38 619 5 25
124 51 36 135 48 25 1415 33 6 22 12 215 28 77
52 5 14 407 16 82 2 8 4 107 117 5952 15 256
4 2 7 3766 5 723 36 71 43 530 476 26 400 317
46 7 4 2 1029 13 104 88 4 381 15 297 98 32
2071 56 26 141 6 194 7486 18 4 226 22 21 134 476
26 480 5 144 30 5535 18 51 36 28 224 92 25 104
4 226 65 16 38 1334 88 12 16 283 5 16 4472 113
103 32 15 16 5345 19 178 32 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0]
建立模型
神經網絡是通過堆疊網絡層創建的 —— 這需要兩個主要的架構決策:
- 模型中要使用多少層?
- 每層使用多少隱藏單元?
在此示例中,輸入數據由單詞索引數組組成。要預測的標籤是 0 或 1。讓我們爲這個問題建立一個模型:
# 輸入大小是電影評論的詞彙數量 (10,000 words)
vocab_size = 10000
model = keras.Sequential()
model.add(keras.layers.Embedding(vocab_size, 16))
model.add(keras.layers.GlobalAveragePooling1D())
model.add(keras.layers.Dense(16, activation='relu'))
model.add(keras.layers.Dense(1, activation='sigmoid'))
model.summary()
Model: "sequential"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
embedding (Embedding) (None, None, 16) 160000
_________________________________________________________________
global_average_pooling1d (Gl (None, 16) 0
_________________________________________________________________
dense (Dense) (None, 16) 272
_________________________________________________________________
dense_1 (Dense) (None, 1) 17
=================================================================
Total params: 160,289
Trainable params: 160,289
Non-trainable params: 0
_________________________________________________________________
這些層按順序堆疊以構建分類器:
- 第一層是
Embedding
層。該層採用整數編碼的詞彙表,並查找每個詞索引的嵌入向量。這些向量在模型訓練中進行學習,併爲輸出數組添加一個維度:(batch, sequence, embedding)
。 - 接下來,
GlobalAveragePooling1D
層通過對序列維度求平均,使每個樣本返回固定長度的輸出向量。這允許模型以最簡單的方式處理可變長度的輸入。 - 這個固定長度的輸出向量通過一個全連接的帶有 16 個隱藏單元的
Dense
層傳輸。 - 最後一層與單個輸出節點密集連接。使用
sigmoid
激活函數,輸出是介於 0 和 1 之間的浮點數,表示概率或置信度。
隱藏單元
上述模型在輸入和輸出之間有兩個中間層,或稱爲“隱藏”層。輸出的數量(單元,節點或神經元)是網絡層的表徵空間維度。換句話說,在學習內部表徵時允許網絡的自由度。
如果模型具有更多隱藏單元(更高維度的表徵空間),或者更多的網絡層,則網絡可以學習更復雜的表徵。但是,它使網絡的計算成本更高,並且可能導致學習到不需要的模式 —— 這些模式可以提高訓練數據的性能,但不會提高測試數據的性能。這稱爲過擬合,我們稍後會進行探討。
損失函數和優化器
模型需要一個損失函數和一個用於訓練的優化器。由於這是二分類問題,模型輸出的是概率(具有 sigmoid 激活的單個單元層),我們將使用 binary_crossentropy
損失函數。
這不是損失函數的唯一選擇,例如,你可以選擇 mean_squared_error
。但是,通常用 binary_crossentropy
處理概率更好 —— 它度量概率分佈之間的“距離”,在本例中,測量真實分佈和預測之間的“距離”。
稍後,當我們探索迴歸問題(比如預測房子的價格)時,我們將看到如何使用另一種稱爲均方誤差的損失函數。
現在,配置模型以使用優化器和損失函數:
model.compile(optimizer='adam',
loss='binary_crossentropy',
metrics=['accuracy'])
創建驗證集
在訓練時,我們想要檢查模型在以前沒有見過的數據上的準確性。通過從原始訓練數據中分離 10,000 個樣本來創建驗證集。(爲什麼不使用測試集?我們的目標是僅使用訓練數據開發和調整我們的模型,然後僅使用測試數據來評估我們的準確性)。
x_val = train_data[:10000]
partial_x_train = train_data[10000:]
y_val = train_labels[:10000]
partial_y_train = train_labels[10000:]
訓練模型
使用包含 512 個樣本的小批量訓練模型 40 個週期。x_train
和 y_train
張量中所有樣本需要進行 40 次迭代。在訓練期間,監控模型在驗證集中的 10,000 個樣本的損失和準確率:
history = model.fit(partial_x_train,
partial_y_train,
epochs=40,
batch_size=512,
validation_data=(x_val, y_val),
verbose=1)
Train on 15000 samples, validate on 10000 samples
Epoch 1/40
15000/15000 [==============================] - 1s 43us/step - loss: 0.6987 - acc: 0.5008 - val_loss: 0.6940 - val_acc: 0.4948
Epoch 2/40
15000/15000 [==============================] - 0s 27us/step - loss: 0.6912 - acc: 0.5518 - val_loss: 0.6901 - val_acc: 0.5675
...
Epoch 39/40
15000/15000 [==============================] - 0s 26us/step - loss: 0.1819 - acc: 0.9370 - val_loss: 0.2885 - val_acc: 0.8860
Epoch 40/40
15000/15000 [==============================] - 0s 27us/step - loss: 0.1773 - acc: 0.9395 - val_loss: 0.2876 - val_acc: 0.8854
評估模型
讓我們看看模型的表現,evaluate
方法將返回兩個值。損失(代表我們的誤差的值,值越低越好)和準確率。
results = model.evaluate(test_data, test_labels)
print(results)
25000/25000 [==============================] - 1s 45us/sample - loss: 0.3145 - accuracy: 0.8742
[0.3145076360368729, 0.87424]
這種相當簡單的方法可以達到約 87% 的準確度率,採用更高級的方法,模型應該可以接近 95%。
創建準確率和損失的圖表
model.fit()
返回一個包含字典的 History
對象,其中包括訓練期間產生的所有數據:
history_dict = history.history
history_dict.keys()
dict_keys(['val_accuracy', 'accuracy', 'loss', 'val_loss'])
有四個元素:每個元素對應在訓練和驗證期間受監控的指標。我們可以使用這些來繪製訓練和驗證損失以及訓練和驗證準確率,以進行比較:
import matplotlib.pyplot as plt
acc = history_dict['accuracy']
val_acc = history_dict['val_accuracy']
loss = history_dict['loss']
val_loss = history_dict['val_loss']
epochs = range(1, len(acc) + 1)
# "bo" 表示 "blue dot(藍點)"
plt.plot(epochs, loss, 'bo', label='Training loss')
# "b" 表示 "solid blue line(藍色實線)"
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend()
plt.show()
(原文缺少此圖像)
plt.clf() # 清除圖像
plt.plot(epochs, acc, 'bo', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc')
plt.title('Training and validation accuracy')
plt.xlabel('Epochs')
plt.ylabel('Accuracy')
plt.legend()
plt.show()
在上兩圖中,點表示訓練損失和準確率,實線表示驗證損失和準確率。
請注意,訓練損失隨着每個週期而減少,並且訓練準確率隨着每個週期而增加。這在使用梯度下降優化時是預料之中的 —— 它應該在每次迭代時最小化期望的數量。
這和驗證損失和驗證準確率不同 —— 它們似乎在大約二十個週期之後達到峯值。這是過擬合的一個例子:模型在訓練數據上的表現比在以前從未見過的數據上表現得更好。在此之後,模型過度優化並學習特定於訓練數據的表徵,這些表徵沒有泛化至測試數據。
對於這種特殊情況,我們可以通過在大約二十個週期之後停止訓練來防止過擬合。稍後,你將看到如何使用回調自動執行此操作。