文本分類是指將給定文本按照其內容判別到一個或多個預先確定的文本類別中的過程。文本分類是一種典型的有知道的學習過程,根據已經被標記的文本集合,通過學習,得到一個文本特徵和文本類別之間的關係模型,然後利用這個關係模型對新文本進行類別判斷。文本分類計數用於識別文檔主題,並將之歸類到預先定義的主題或主題集合中。
需要注意的是,多類文本分類與多標籤分類並不同,其中多類分類區別於二分類問題,即在 $n (n>2)$ 個類別中互斥地選取一個作爲輸出;而多標籤分類,是在 n 個標籤中非互斥地選取 $m (m<n)$ 個標籤作爲輸出。本文介紹瞭如何基於 TensorFlow 2.0 的長短期記憶網絡進行多類文本分類,非常實用,希望對讀者有所啓迪。
對自然語言處理(Natural Language Processing,NLP)領域來說,很多創新之處都是關於如何在詞向量中加入上下文。常用的方法之一就是使用遞歸神經網絡(Recurrent Neural Networks,RNN)。下面是遞歸神經網絡的概念:
- 它們利用順序信息。
- 它們具備記憶能力,能夠記住到目前爲止計算過的內容,也就是說,我最後說的內容將影響我接下來要講的內容。
- 遞歸神經網絡是文本和語音分析的理想選擇。
- 最常用的遞歸神經網絡是長短期記憶網絡(Long-Short Term Memory,LSTM)。
上圖是遞歸神經網絡的架構。
- “A” 是前饋神經網絡(Feedforward neural network)的一層。
- 如果我們只看右邊的話,它會遞歸地遍歷每個序列的元素。
- 如果我們將左邊展開,它看起來將會跟右邊一模一樣。
譯註: 前饋神經網絡(Feedforward neural network),是最早發明、最簡單的人工神經網絡類型。在它內部,參數從輸入層向輸出層單向傳播。和遞歸神經網絡不通,它內部不會構成有向環。
假設我們正在解決新聞文章數據集的文檔分類問題。
- 我們輸入每個單詞,這些單詞以某種方式相互關聯。
- 當我們看到文章中所有的單詞時,我們會在文章末尾做出預測。
- 遞歸神經網絡通過傳遞上一次輸出的輸入,能夠保留信息,並能夠在最後利用所有信息進行預測。
- 這對於短句很有效,但當我們處理一篇長文章時,將會有一個長期依賴問題。
因此,我們通常不是用普通的遞歸神經網絡,而是使用長短期記憶網絡。長短期記憶網絡是一種遞歸神經網絡,可以解決這種長期依賴問題。
譯註: 長短期記憶網絡(Long Short-Term Memory,LSTM),是一種時間遞歸神經網絡,適合於處理和預測時間序列中間隔和延遲相對較長的重要事件。基於長短期記憶網絡的系統可以實現機器翻譯、視頻分析、文檔摘要、語音識別、圖像識別、手寫識別、控制聊天機器人、合成音樂等任務。
在我們的新聞文章文檔分類示例中,有這種多對一的關係。輸入是單詞序列,而輸出是單個類或標籤。
現在,我們將使用 TensorFlow 2.0 和 Keras,解決一個使用長短期記憶網絡的 BBC 新聞文檔分類問題。數據集可以點擊此鏈接來獲取。
- 首先,我們導入庫,並確保 TensorFlow 是正確的版本。
import csv
import tensorflow as tf
import numpy as np
from tensorflow.keras.preprocessing.text import Tokenizer
from tensorflow.keras.preprocessing.sequence import pad_sequences
from nltk.corpus import stopwords
STOPWORDS = set(stopwords.words('english'))
print(tf.__version__)
- 將超參數置於頂部,如下所示,便於進行更改和編輯。
- 屆時,我們將會講解每個超參數是如何工作的。
vocab_size = 5000
embedding_dim = 64
max_length = 200
trunc_type = 'post'
padding_type = 'post'
oov_tok = '<OOV>'
training_portion = .8
- 定義兩個包含文章和標籤的列表。同時,我們刪除了停用詞。
articles = []
labels = []
with open("bbc-text.csv", 'r') as csvfile:
reader = csv.reader(csvfile, delimiter=',')
next(reader)
for row in reader:
labels.append(row[0])
article = row[1]
for word in STOPWORDS:
token = ' ' + word + ' '
article = article.replace(token, ' ')
article = article.replace(' ', ' ')
articles.append(article)
print(len(labels))
print(len(articles))
數據中有 2225 篇新聞文章,我們將它們分爲訓練集和驗證集,根據我們之前設置的參數,80% 用於訓練,20% 用於驗證。
train_size = int(len(articles) * training_portion)
train_articles = articles[0: train_size]
train_labels = labels[0: train_size]
validation_articles = articles[train_size:]
validation_labels = labels[train_size:]
print(train_size)
print(len(train_articles))
print(len(train_labels))
print(len(validation_articles))
print(len(validation_labels))
詞法分析器(Tokenizer)爲我們承擔了所有繁重的工作。在我們的文章中,它將進行標記化,需要 5000 個最常見的單詞。oov_token
是在遇到不可見的單詞時放入一個特殊的值。這意味着我們希望 <OOV>
用於不在 word_index
中的單詞。fit_on_text
將遍歷所有文本,並創建如下詞典:
tokenizer = Tokenizer(num_words = vocab_size, oov_token=oov_tok)
tokenizer.fit_on_texts(train_articles)
word_index = tokenizer.word_index
dict(list(word_index.items())[0:10])
譯註: 詞法分析器(Tokenizer),是計算機科學中將字符串行轉換爲標記(token)串行的過程。進行詞法分析的進程或者函數叫作詞法分析器(lexical analyzer,簡稱lexer),也叫掃描器(scanner)。詞法分析器一般以函數的形式存在,供語法分析器調用。
我們可以看到,“
完成標記化之後,下一步就是將這些標記轉換爲序列列表。下面是已經轉換成序列的訓練數據中的第 11 篇文章。
train_sequences = tokenizer.texts_to_sequences(train_articles)print(train_sequences[10])
當我們爲自然語言處理訓練神經網絡時,我們需要相同大小的序列,這就是我們爲什麼使用填充的原因。如果你查看一下的話,就會發現,我們的 max_length
是 200,所以我們使用 pad_sequences
,將所有文章的長度都設置爲 200。結果,你會看到第一篇文章長度爲 426,變成了 200;第二篇是 192,也變成了 200。以此類推。
train_padded = pad_sequences(train_sequences, maxlen=max_length, padding=padding_type, truncating=trunc_type)
print(len(train_sequences[0]))
print(len(train_padded[0]))
print(len(train_sequences[1]))
print(len(train_padded[1]))
print(len(train_sequences[10]))
print(len(train_padded[10]))
此外,還有 padding_type
和 truncating_type
, 還有所有的 post
,例如,第 11 篇文章的長度是 186,我們需要填充到 200,我們就在結尾處開始填充,也就是說,填充了 14 個 0。
print(train_padded[10])
對於第一篇文章,它的長度爲 426,我們需要將其截斷到 200,我們就在結尾處截斷。
然後,我們對驗證序列執行同樣的操作。
validation_sequences = tokenizer.texts_to_sequences(validation_articles)
validation_padded = pad_sequences(validation_sequences, maxlen=max_length, padding=padding_type, truncating=trunc_type)
print(len(validation_sequences))
print(validation_padded.shape)
現在,我們來看一下標籤。因爲我們的標籤是文本,因此,我們將它們進行標記。在訓練時,標籤應該是 numpy 數組。所以,我們要將標籤列表轉換爲 numpy 數組,如下所示:
label_tokenizer = Tokenizer()
label_tokenizer.fit_on_texts(labels)
training_label_seq = np.array(label_tokenizer.texts_to_sequences(train_labels))
validation_label_seq = np.array(label_tokenizer.texts_to_sequences(validation_labels))print(training_label_seq[0])
print(training_label_seq[1])
print(training_label_seq[2])
print(training_label_seq.shape)
print(validation_label_seq[0])
print(validation_label_seq[1])
print(validation_label_seq[2])
print(validation_label_seq.shape)
在訓練深度神經網絡之前,我們應該探索一下我們的原始文章和填充後的文章是什麼樣子的。運行下面的代碼,我們瀏覽第 11 篇文章,可以看到,一些單詞變成了“
reverse_word_index = dict([(value, key) for (key, value) in word_index.items()])
def decode_article(text):
return ' '.join([reverse_word_index.get(i, '?') for i in text])
print(decode_article(train_padded[10]))
print('---')
print(train_articles[10])
現在,是實施長短期記憶網絡的時候了。
-
我們構建了一個
tf.keras.Sequential
模型,從嵌入層開始。嵌入層爲每個單詞存儲一個向量。調用時,它將單詞索引序列轉換爲向量序列。經過訓練後,具有相似意義的單詞,通常會具有相似的向量。 -
雙向包裝器(Bidirectional wrapper)與 LSTM 層一起使用,它通過 LSTM 層向前和向後傳播輸入,然後連接輸出。這有助於長短期記憶網絡學習長期依賴關係。然後我們將其擬合到密集神經網絡(Dense Neural Network)中進行分類。
-
我們使用
relu
代替than
函數,因爲這兩個函數能夠彼此很好地相互替代。 -
我們添加了 6 個單位和
softmax
激活的密集層(Dense Layer)。當我們有多個輸出時,softmax
將輸出層轉換爲概率分佈。
model = tf.keras.Sequential([
# Add an Embedding layer expecting input vocab of size 5000, and output embedding dimension of size 64 we set at the top
tf.keras.layers.Embedding(vocab_size, embedding_dim),
tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(embedding_dim)),
# tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(32)),
# use ReLU in place of tanh function since they are very good alternatives of each other.
tf.keras.layers.Dense(embedding_dim, activation='relu'),
# Add a Dense layer with 6 units and softmax activation.
# When we have multiple outputs, softmax convert outputs layers into a probability distribution.
tf.keras.layers.Dense(6, activation='softmax')
])
model.summary()
在我們的模型摘要中,我們有嵌入,雙向包含長短期記憶網絡,然後就是兩個密集層(Dense layer)。雙向的輸出爲 128,因爲它是我們在長短期記憶網絡中輸入的兩倍。我們也可以堆疊 LSTM 層,但我們發現,結果反而更糟。
print(set(labels))
我們總共有 5 個標籤,但因爲我們沒有對標籤進行獨熱編碼(One-hot encode),因此,我們不得不使用
sparse_categorical_crossentropy
作爲損失函數,它似乎認爲 0 也是一個可能的標籤,而詞法分析器對象是從整數 1 開始標記化,而不是整數 0。結果,儘管從未使用過 0,但最後一個密集層需要標籤 0、1、2、3、4、5 的輸出。
如果你希望最後一個密集層爲 5,那麼你就需要從訓練和驗證標籤中減去 1。我決定保持現狀。
我決定訓練 10 個輪數,正如你將看到的,這是很多輪數。
model.compile(loss='sparse_categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
num_epochs = 10
history = model.fit(train_padded, training_label_seq, epochs=num_epochs, validation_data=(validation_padded, validation_label_seq), verbose=2)
def plot_graphs(history, string):
plt.plot(history.history[string])
plt.plot(history.history['val_'+string])
plt.xlabel("Epochs")
plt.ylabel(string)
plt.legend([string, 'val_'+string])
plt.show()
plot_graphs(history, "accuracy")
plot_graphs(history, "loss")
我們可能只需 3 到 4 個輪數。在訓練結束時,我們可以發現有點過擬合。
在後續文章中,我們將致力於改進這一模型。
你可以在 Github 找到本文的 Jupyter notebook。
參考文獻:
- Coursera:Natural Language Processing in TensorFlow
- O’Reilly:Strata Data Conference 2019
作者介紹:
Susan Li,是加拿大多倫多的高級數據科學家。她的理想是,每次發表文章,就改變世界。
原文鏈接: