基本文字分類

在 TensorFlow.org 上檢視 在 Google Colab 中執行 在 GitHub 上檢視原始碼 下載筆記本

本教學課程示範從磁碟上儲存的純文字檔案開始進行文字分類。您將訓練二元分類器,以便對 IMDB 資料集執行情感分析。在本筆記本的最後,有一個練習供您嘗試,您將在其中訓練多類別分類器,以預測 Stack Overflow 上程式設計問題的標籤。

import matplotlib.pyplot as plt
import os
import re
import shutil
import string
import tensorflow as tf

from tensorflow.keras import layers
from tensorflow.keras import losses
print(tf.__version__)

情感分析

本筆記本訓練情感分析模型,根據評論文字將電影評論分類為正面負面。這是二元 (或雙類別) 分類的範例,二元分類是重要且廣泛適用的機器學習問題類型。

您將使用大型電影評論資料集,其中包含來自 網際網路電影資料庫的 50,000 篇電影評論文字。這些評論分為 25,000 篇用於訓練,25,000 篇用於測試。訓練和測試集是平衡的,表示它們包含相同數量的正面和負面評論。

下載並探索 IMDB 資料集

讓我們下載並解壓縮資料集,然後探索目錄結構。

url = "https://ai.stanford.edu/~amaas/data/sentiment/aclImdb_v1.tar.gz"

dataset = tf.keras.utils.get_file("aclImdb_v1", url,
                                    untar=True, cache_dir='.',
                                    cache_subdir='')

dataset_dir = os.path.join(os.path.dirname(dataset), 'aclImdb')
os.listdir(dataset_dir)
train_dir = os.path.join(dataset_dir, 'train')
os.listdir(train_dir)

aclImdb/train/posaclImdb/train/neg 目錄包含許多文字檔,每個檔案都是單一篇電影評論。讓我們看看其中一篇。

sample_file = os.path.join(train_dir, 'pos/1181_9.txt')
with open(sample_file) as f:
  print(f.read())

載入資料集

接下來,您將從磁碟載入資料,並將其準備為適合訓練的格式。為此,您將使用實用的 text_dataset_from_directory 公用程式,該公用程式需要以下目錄結構。

main_directory/
...class_a/
......a_text_1.txt
......a_text_2.txt
...class_b/
......b_text_1.txt
......b_text_2.txt

若要準備用於二元分類的資料集,您需要在磁碟上有兩個資料夾,分別對應於 class_aclass_b。這些將是正面和負面電影評論,可以在 aclImdb/train/posaclImdb/train/neg 中找到。由於 IMDB 資料集包含其他資料夾,因此您將在使用此公用程式之前移除它們。

remove_dir = os.path.join(train_dir, 'unsup')
shutil.rmtree(remove_dir)

接下來,您將使用 text_dataset_from_directory 公用程式來建立已加上標籤的 tf.data.Datasettf.data 是一組功能強大的工具,可用於處理資料。

執行機器學習實驗時,最佳做法是將資料集分成三個部分:訓練集驗證集測試集

IMDB 資料集已分為訓練集和測試集,但缺少驗證集。讓我們使用訓練資料的 80:20 分割,透過使用下方的 validation_split 引數來建立驗證集。

batch_size = 32
seed = 42

raw_train_ds = tf.keras.utils.text_dataset_from_directory(
    'aclImdb/train',
    batch_size=batch_size,
    validation_split=0.2,
    subset='training',
    seed=seed)

如您在上方看到的,訓練資料夾中有 25,000 個範例,您將使用其中的 80% (或 20,000 個) 進行訓練。您稍後會看到,您可以透過將資料集直接傳遞至 model.fit 來訓練模型。如果您是 tf.data 新手,您也可以逐一查看資料集,並列印出一些範例如下所示。

for text_batch, label_batch in raw_train_ds.take(1):
  for i in range(3):
    print("Review", text_batch.numpy()[i])
    print("Label", label_batch.numpy()[i])

請注意,評論包含原始文字 (帶有標點符號和偶爾的 HTML 標記,例如 <br/>)。您將在以下章節中說明如何處理這些標記。

標籤是 0 或 1。若要查看這些標籤中的哪一個對應於正面和負面電影評論,您可以檢查資料集上的 class_names 屬性。

print("Label 0 corresponds to", raw_train_ds.class_names[0])
print("Label 1 corresponds to", raw_train_ds.class_names[1])

接下來,您將建立驗證和測試資料集。您將使用訓練集中剩餘的 5,000 篇評論進行驗證。

raw_val_ds = tf.keras.utils.text_dataset_from_directory(
    'aclImdb/train',
    batch_size=batch_size,
    validation_split=0.2,
    subset='validation',
    seed=seed)
raw_test_ds = tf.keras.utils.text_dataset_from_directory(
    'aclImdb/test',
    batch_size=batch_size)

準備用於訓練的資料集

接下來,您將使用實用的 tf.keras.layers.TextVectorization 層來標準化、權杖化和向量化資料。

標準化是指預先處理文字,通常是移除標點符號或 HTML 元素,以簡化資料集。權杖化是指將字串分割成權杖 (例如,透過在空白字元上分割,將句子分割成個別單字)。向量化是指將權杖轉換為數字,以便可以將其饋送至神經網路。所有這些任務都可以透過此層完成。

如您在上方看到的,評論包含各種 HTML 標記,例如 <br />。這些標記不會由 TextVectorization 層中的預設標準化工具移除 (預設會將文字轉換為小寫並移除標點符號,但不移除 HTML)。您將編寫自訂標準化函式來移除 HTML。

def custom_standardization(input_data):
  lowercase = tf.strings.lower(input_data)
  stripped_html = tf.strings.regex_replace(lowercase, '<br />', ' ')
  return tf.strings.regex_replace(stripped_html,
                                  '[%s]' % re.escape(string.punctuation),
                                  '')

接下來,您將建立 TextVectorization 層。您將使用此層來標準化、權杖化和向量化我們的資料。您將 output_mode 設定為 int,以便為每個權杖建立唯一的整數索引。

請注意,您使用的是預設分割函式,以及您在上方定義的自訂標準化函式。您也將為模型定義一些常數,例如明確的最大 sequence_length,這會導致該層將序列填補或截斷為完全 sequence_length 個值。

max_features = 10000
sequence_length = 250

vectorize_layer = layers.TextVectorization(
    standardize=custom_standardization,
    max_tokens=max_features,
    output_mode='int',
    output_sequence_length=sequence_length)

接下來,您將呼叫 adapt,以將預先處理層的狀態調整為資料集。這會導致模型建立字串到整數的索引。

# Make a text-only dataset (without labels), then call adapt
train_text = raw_train_ds.map(lambda x, y: x)
vectorize_layer.adapt(train_text)

讓我們建立一個函式,以查看使用此層來預先處理某些資料的結果。

def vectorize_text(text, label):
  text = tf.expand_dims(text, -1)
  return vectorize_layer(text), label
# retrieve a batch (of 32 reviews and labels) from the dataset
text_batch, label_batch = next(iter(raw_train_ds))
first_review, first_label = text_batch[0], label_batch[0]
print("Review", first_review)
print("Label", raw_train_ds.class_names[first_label])
print("Vectorized review", vectorize_text(first_review, first_label))

如您在上方看到的,每個權杖都已替換為整數。您可以透過在該層上呼叫 .get_vocabulary() 來查閱每個整數對應的權杖 (字串)。

print("1287 ---> ",vectorize_layer.get_vocabulary()[1287])
print(" 313 ---> ",vectorize_layer.get_vocabulary()[313])
print('Vocabulary size: {}'.format(len(vectorize_layer.get_vocabulary())))

您幾乎已準備好訓練模型。作為最後一個預先處理步驟,您將將您先前建立的 TextVectorization 層套用至訓練、驗證和測試資料集。

train_ds = raw_train_ds.map(vectorize_text)
val_ds = raw_val_ds.map(vectorize_text)
test_ds = raw_test_ds.map(vectorize_text)

設定資料集以提升效能

以下是您在載入資料時應使用的兩種重要方法,以確保 I/O 不會變成封鎖。

.cache() 會在資料從磁碟載入後將其保留在記憶體中。這將確保資料集在訓練模型時不會成為瓶頸。如果您的資料集太大而無法放入記憶體中,您也可以使用此方法來建立高效能的磁碟快取,其讀取效率高於許多小型檔案。

.prefetch() 會在訓練時重疊資料預先處理和模型執行。

您可以在資料效能指南中深入瞭解這兩種方法,以及如何將資料快取至磁碟。

AUTOTUNE = tf.data.AUTOTUNE

train_ds = train_ds.cache().prefetch(buffer_size=AUTOTUNE)
val_ds = val_ds.cache().prefetch(buffer_size=AUTOTUNE)
test_ds = test_ds.cache().prefetch(buffer_size=AUTOTUNE)

建立模型

現在是時候建立您的神經網路了

embedding_dim = 16
model = tf.keras.Sequential([
  layers.Embedding(max_features, embedding_dim),
  layers.Dropout(0.2),
  layers.GlobalAveragePooling1D(),
  layers.Dropout(0.2),
  layers.Dense(1, activation='sigmoid')])

model.summary()

這些層會依序堆疊以建構分類器

  1. 第一層是 Embedding 層。此層會採用整數編碼的評論,並查閱每個單字索引的嵌入向量。這些向量會在模型訓練時學習。向量會為輸出陣列新增維度。產生的維度為:(批次、序列、嵌入)。若要深入瞭解嵌入,請查看單字嵌入教學課程。
  2. 接下來,GlobalAveragePooling1D 層會透過平均序列維度,傳回每個範例的固定長度輸出向量。這可讓模型以最簡單的方式處理可變長度的輸入。
  3. 最後一層是密集連接的單一輸出節點。

損失函數和最佳化工具

模型需要損失函數和最佳化工具才能進行訓練。由於這是二元分類問題,且模型會輸出機率 (具有 Sigmoid 啟動的單元層),因此您將使用 losses.BinaryCrossentropy 損失函數。

現在,設定模型以使用最佳化工具和損失函數

model.compile(loss=losses.BinaryCrossentropy(),
              optimizer='adam',
              metrics=[tf.metrics.BinaryAccuracy(threshold=0.5)])

訓練模型

您將透過將 dataset 物件傳遞至 fit 方法來訓練模型。

epochs = 10
history = model.fit(
    train_ds,
    validation_data=val_ds,
    epochs=epochs)

評估模型

讓我們看看模型的效能。將傳回兩個值。損失 (代表我們錯誤的數字,值越低越好) 和準確度。

loss, accuracy = model.evaluate(test_ds)

print("Loss: ", loss)
print("Accuracy: ", accuracy)

這種相當簡單的方法可達到約 86% 的準確度。

建立準確度和損失隨時間變化的圖表

model.fit() 傳回 History 物件,其中包含一個字典,其中包含訓練期間發生的所有事件

history_dict = history.history
history_dict.keys()

有四個項目:訓練和驗證期間針對每個監控指標各有一個項目。您可以使用這些項目繪製訓練和驗證損失的圖表以進行比較,以及訓練和驗證準確度

acc = history_dict['binary_accuracy']
val_acc = history_dict['val_binary_accuracy']
loss = history_dict['loss']
val_loss = history_dict['val_loss']

epochs = range(1, len(acc) + 1)

# "bo" is for "blue dot"
plt.plot(epochs, loss, 'bo', label='Training loss')
# b is for "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.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(loc='lower right')

plt.show()

在此圖表中,點代表訓練損失和準確度,實線是驗證損失和準確度。

請注意,訓練損失會隨著每個週期減少,而訓練準確度會隨著每個週期增加。這是使用梯度下降最佳化時的預期情況,它應該在每次迭代時盡可能減少所需的數量。

驗證損失和準確度並非如此,它們似乎在訓練準確度之前達到峰值。這是過度配適的範例:模型在訓練資料上的效能優於在從未見過的資料上的效能。在此之後,模型會過度最佳化並學習特定於訓練資料的表示法,這些表示法無法概括到測試資料。

針對此特定情況,您可以透過在驗證準確度不再增加時停止訓練來防止過度配適。其中一種方法是使用 tf.keras.callbacks.EarlyStopping 回呼。

匯出模型

在上述程式碼中,您在將文字饋送至模型之前,將 TextVectorization 層套用至資料集。如果您希望模型能夠處理原始字串 (例如,為了簡化部署),您可以將 TextVectorization 層包含在模型中。若要執行此操作,您可以使用剛訓練的權重建立新模型。

export_model = tf.keras.Sequential([
  vectorize_layer,
  model,
  layers.Activation('sigmoid')
])

export_model.compile(
    loss=losses.BinaryCrossentropy(from_logits=False), optimizer="adam", metrics=['accuracy']
)

# Test it with `raw_test_ds`, which yields raw strings
loss, accuracy = export_model.evaluate(raw_test_ds)
print(accuracy)

對新資料進行推論

若要取得新範例的預測,您可以直接呼叫 model.predict()

examples = tf.constant([
  "The movie was great!",
  "The movie was okay.",
  "The movie was terrible..."
])

export_model.predict(examples)

將文字預先處理邏輯包含在模型中,可讓您匯出模型以進行生產,從而簡化部署,並降低 訓練/測試傾斜 的可能性。

在選擇在何處套用 TextVectorization 層時,需要記住效能差異。在模型外部使用它可以讓您在 GPU 上訓練時,對資料執行非同步 CPU 處理和緩衝處理。因此,如果您要在 GPU 上訓練模型,您可能會想要選擇此選項,以便在開發模型時獲得最佳效能,然後在您準備好進行部署時,切換為將 TextVectorization 層包含在模型中。

請造訪此教學課程,深入瞭解如何儲存模型。

練習:Stack Overflow 問題的多類別分類

本教學課程說明如何從頭開始在 IMDB 資料集上訓練二元分類器。作為練習,您可以修改此筆記本,以訓練多類別分類器,來預測 Stack Overflow 上程式設計問題的標籤。

已為您準備好一個 資料集 供您使用,其中包含發佈至 Stack Overflow 的數千個程式設計問題的主體 (例如,「如何在 Python 中依值排序字典?」)。這些問題各標記有一個標籤 (Python、CSharp、JavaScript 或 Java)。您的任務是以問題作為輸入,並預測適當的標籤,在本例中為 Python。

您將使用的資料集包含從 BigQuery 上更大的公開 Stack Overflow 資料集中擷取的數千個問題,其中包含超過 1,700 萬篇文章。

下載資料集後,您會發現它的目錄結構與您先前使用的 IMDB 資料集類似

train/
...python/
......0.txt
......1.txt
...javascript/
......0.txt
......1.txt
...csharp/
......0.txt
......1.txt
...java/
......0.txt
......1.txt

若要完成此練習,您應該修改此筆記本以使用 Stack Overflow 資料集,方法是進行下列修改

  1. 在筆記本的頂端,更新下載 IMDB 資料集的程式碼,以使用程式碼下載已準備好的 Stack Overflow 資料集。由於 Stack Overflow 資料集具有類似的目錄結構,因此您不需要進行太多修改。

  2. 將模型的最後一層修改為 Dense(4),因為現在有四個輸出類別。

  3. 在編譯模型時,將損失變更為 tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)。這是用於多類別分類問題的正確損失函數,當每個類別的標籤是整數時 (在本例中,它們可以是 0、123)。此外,將指標變更為 metrics=['accuracy'],因為這是多類別分類問題 (tf.metrics.BinaryAccuracy 僅用於二元分類器)。

  4. 在繪製準確度隨時間變化的圖表時,將 binary_accuracyval_binary_accuracy 分別變更為 accuracyval_accuracy

  5. 完成這些變更後,您將能夠訓練多類別分類器。

深入瞭解

本教學課程從頭開始介紹文字分類。若要深入瞭解一般的文字分類工作流程,請查看 Google Developers 提供的文字分類指南

# MIT License
#
# Copyright (c) 2017 François Chollet
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.