人工智慧--自然語言處理簡介

果冻人工智能發表於2024-11-08

上一篇:《人工智慧模型訓練中的資料之美——探索TFRecord》

序言:自然語言處理(NLP)是人工智慧中的一種技術,專注於理解基於人類語言的內容。它包含了程式設計技術,用於建立可以理解語言、分類內容,甚至生成和創作人類語言的新作品的模型。在接下來的幾章中,我們將會探討這些技術。此外,現在有許多利用 NLP 的服務來建立應用程式,比如聊天機器人(它們屬於應用,屬於Agent應用開發),但這些內容不在知識的範圍之內——我們將專注於 NLP 的基礎知識(實現原理),以及如何進行語言建模,使您可以訓練神經網路,教導電腦去理解和分類文字。

我們將從本節開始,先了解如何將語言分解成數字,以及這些數字如何用於神經網路,所謂‘分解’其實就給用一個數字代替語言句子中的字詞或者詞根,因為計算機只能處理數字;人們把語言轉換成數字交由電腦處理後,再重新轉回語言文字就可以被人類識別並知道電腦做了什麼了。

將語言編碼為數字

有多種方法可以將語言編碼成數字。最常見的是透過字母進行編碼,就像字串在程式中儲存時的自然形式一樣。不過,在記憶體中,您儲存的不是字母本身,而是它的編碼——可能是 ASCII、Unicode 值,或者其他形式。例如,考慮單詞“listen”。用 ASCII 編碼的話,這個單詞可以被表示為數字 76、73、83、84、69 和 78。這種編碼方式的好處是,您現在可以用數字來表示這個單詞。但如果考慮“silent”這個詞,它是“listen”的一個字母異位詞。儘管這兩個單詞的編碼數字相同,但順序不同,這可能會讓建立一個理解文字的模型變得有些困難。

一個“反義詞異構詞”是指一個單詞的字母順序顛倒後形成的另一個單詞,且二者具有相反的含義。例如,“united”和“untied”就是一對反義詞異構詞,另外還有“restful”和“fluster”,“Santa”和“Satan”,“forty-five”和“over fifty”。我之前的職位名稱是“Developer Evangelist”,後來改成了“Developer Advocate”——這是個好事,因為“Evangelist”就是“Evil’s Agent”(邪惡代理人)的反義詞異構詞!

一種更好的替代方法可能是用數字來編碼整個單詞,而不是逐個字母編碼。在這種情況下,“silent”可以用數字x表示,“listen”可以用數字y表示,它們彼此不會重疊。

使用這種技術,考慮一個句子比如“I love my dog.”您可以將它編碼為數字 [1, 2, 3, 4]。如果您想要編碼“I love my cat.”,可以是 [1, 2, 3, 5]。您已經可以看出這些句子在數值上相似——[1, 2, 3, 4] 看起來很像 [1, 2, 3, 5],因此可以推測它們的含義相似。

這個過程叫做“分詞”,接下來您將探索如何在程式碼中實現它。

分詞入門

TensorFlow Keras 包含一個稱為“preprocessing”的庫,它提供了許多非常實用的工具來為機器學習準備資料。其中之一是“Tokenizer”,它可以將單詞轉化為令牌。讓我們透過一個簡單的示例來看它的實際操作:

import tensorflow as tf

from tensorflow import keras

from tensorflow.keras.preprocessing.text import Tokenizer

sentences = [

'Today is a sunny day',

'Today is a rainy day'

]

tokenizer = Tokenizer(num_words=100)

tokenizer.fit_on_texts(sentences)

word_index = tokenizer.word_index

print(word_index)

在這個例子中,我們建立了一個 Tokenizer 物件,並指定了它可以分詞的單詞數量。這將是從詞庫中生成的最大令牌數。我們這裡的詞庫非常小,只包含六個獨特的單詞,所以遠小於所指定的一百個。

一旦我們有了一個分詞器,呼叫 fit_on_texts 就會建立出令牌化的單詞索引。列印出來會顯示詞庫中的鍵/值對集合,類似於這樣:

{'today': 1, 'is': 2, 'a': 3, 'day': 4, 'sunny': 5, 'rainy': 6}

這個分詞器非常靈活。例如,如果我們將語料庫擴充套件,新增另一個包含單詞“today”且帶有問號的句子,結果會顯示它足夠智慧,可以將“today?”過濾成“today”:

sentences = [

'Today is a sunny day',

'Today is a rainy day',

'Is it sunny today?'

]

輸出結果為:{'today': 1, 'is': 2, 'a': 3, 'sunny': 4, 'day': 5, 'rainy': 6, 'it': 7}

這種行為是由分詞器的filters引數控制的,預設情況下會移除除撇號外的所有標點符號。因此,例如,“Today is a sunny day”將根據之前的編碼變成一個包含 [1, 2, 3, 4, 5] 的序列,而“Is it sunny today?”將變成 [2, 7, 4, 1]。當您已將句子中的單詞分詞後,下一步就是將句子轉換為數字列表,其中數字是單詞在詞典中的鍵值對所對應的值。

將句子轉換為序列

現在您已經瞭解瞭如何將單詞分詞並轉化為數字,接下來的一步是將句子編碼為數字序列。分詞器有一個名為text_to_sequences的方法,您只需傳遞句子的列表,它就會返回序列的列表。例如,如果您修改之前的程式碼如下:

sentences = [

'Today is a sunny day',

'Today is a rainy day',

'Is it sunny today?'

]

tokenizer = Tokenizer(num_words=100)

tokenizer.fit_on_texts(sentences)

word_index = tokenizer.word_index

sequences = tokenizer.texts_to_sequences(sentences)

print(sequences)

您將得到表示這三句話的序列。回想一下詞彙索引是這樣的:

{'today': 1, 'is': 2, 'a': 3, 'sunny': 4, 'day': 5, 'rainy': 6, 'it': 7}

輸出結果將如下所示:

[[1, 2, 3, 4, 5], [1, 2, 3, 6, 5], [2, 7, 4, 1]]

然後,您可以將數字替換成單詞,這樣句子就會變得有意義了。

現在考慮一下,當您用一組資料訓練神經網路時會發生什麼。通常的模式是,您有一組用於訓練的資料,但您知道它無法涵蓋所有的需求,只能儘量覆蓋多一些。在 NLP 的情況下,您的訓練資料中可能包含成千上萬個單詞,出現在不同的上下文中,但您不可能在所有的上下文中涵蓋所有可能的單詞。所以,當您向神經網路展示一些新的、之前未見過的文字,包含未見過的單詞時,會發生什麼呢?您猜對了——它會感到困惑,因為它完全沒有那些單詞的上下文,結果它的預測就會出錯。

使用“詞彙表外”令牌

處理這些情況的一個工具是“詞彙表外”(OOV)令牌。它可以幫助您的神經網路理解包含未見過的文字的資料上下文。例如,假設您有以下的小型語料庫,希望處理這樣的句子:

test_data = [

'Today is a snowy day',

'Will it be rainy tomorrow?'

]

請記住,您並沒有將這些輸入新增到已有的文字語料庫中(可以視作您的訓練資料),而是考慮預訓練網路如何處理這些文字。如果您使用已有的詞彙和分詞器來分詞這些句子,如下所示:

test_sequences = tokenizer.texts_to_sequences(test_data)

print(word_index)

print(test_sequences)

輸出結果如下:

{'today': 1, 'is': 2, 'a': 3, 'sunny': 4, 'day': 5, 'rainy': 6, 'it': 7}

[[1, 2, 3, 5], [7, 6]]

那麼新的句子,在將令牌換回單詞後,變成了“today is a day”和“it rainy”。

正如您所見,幾乎完全失去了上下文和意義。這裡可以用“詞彙表外”令牌來幫助,您可以在分詞器中指定它。只需新增一個名為 oov_token 的引數,您可以將其設定為任意字串,但確保它不會出現在您的語料庫中:

tokenizer = Tokenizer(num_words=100, oov_token="")

tokenizer.fit_on_texts(sentences)

word_index = tokenizer.word_index

sequences = tokenizer.texts_to_sequences(sentences)

test_sequences = tokenizer.texts_to_sequences(test_data)

print(word_index)

print(test_sequences)

您會看到輸出有了一些改進:

{'': 1, 'today': 2, 'is': 3, 'a': 4, 'sunny': 5, 'day': 6, 'rainy': 7, 'it': 8}

[[2, 3, 4, 1, 6], [1, 8, 1, 7, 1]]

您的令牌列表中多了一個新的項“”,並且您的測試句子保持了它們的長度。現在反向編碼後得到的是“today is a day”和“ it rainy ”。

前者更加接近原始含義,而後者由於大部分單詞不在語料庫中,仍然缺乏上下文,但這算是朝正確方向邁出了一步。

理解填充(padding)

在訓練神經網路時,通常需要所有資料的形狀一致。回憶一下之前章節中提到的,訓練影像時需要將影像格式化為相同的寬度和高度。在文字處理中也面臨相似的問題——一旦您將單詞分詞並將句子轉換為序列後,它們的長度可能會各不相同。為了使它們的大小和形狀一致,可以使用填充(padding)。

為了探索填充,讓我們在語料庫中再新增一個更長的句子:

sentences = [

'Today is a sunny day',

'Today is a rainy day',

'Is it sunny today?',

'I really enjoyed walking in the snow today'

]

當您將它們轉換為序列時,您會看到數字列表的長度不同:

[

[2, 3, 4, 5, 6],

[2, 3, 4, 7, 6],

[3, 8, 5, 2],

[9, 10, 11, 12, 13, 14, 15, 2]

]

(當您列印這些序列時,它們會顯示在一行上,為了清晰起見,我在這裡分成了多行。)

如果您想讓這些序列的長度一致,可以使用 pad_sequences API。首先,您需要匯入它:

from tensorflow.keras.preprocessing.sequence import pad_sequences

使用這個 API 非常簡單。要將您的(未填充的)序列轉換為填充後的集合,只需呼叫 pad_sequences,如下所示:

padded = pad_sequences(sequences)

print(padded)

您會得到一個格式整齊的序列集合。它們會在單獨的行上,像這樣:

[[ 0 0 0 2 3 4 5 6]

[ 0 0 0 2 3 4 7 6]

[ 0 0 0 0 3 8 5 2]

[ 9 10 11 12 13 14 15 2]]

這些序列被填充了 0,而 0 並不是我們單詞列表中的令牌。如果您曾疑惑為什麼令牌列表從 1 開始而不是 0,現在您知道原因了!

現在,您得到了一個形狀一致的陣列,可以用於訓練。不過在此之前,讓我們進一步探索這個 API,因為它提供了許多可以最佳化資料的選項。

首先,您可能注意到在較短的句子中,為了使它們與最長的句子形狀一致,必要數量的 0 被新增到了開頭。這被稱為“前填充”,它是預設行為。您可以透過 padding 引數來更改它。例如,如果您希望序列在末尾填充 0,可以使用:

padded = pad_sequences(sequences, padding='post')

其輸出如下:

[[ 2 3 4 5 6 0 0 0]

[ 2 3 4 7 6 0 0 0]

[ 3 8 5 2 0 0 0 0]

[ 9 10 11 12 13 14 15 2]]

現在您可以看到單詞在填充序列的開頭,而 0 位於末尾。

另一個預設行為是,所有句子都被填充到與最長句子相同的長度。這是一個合理的預設設定,因為這樣您不會丟失任何資料。權衡之處在於您會得到大量填充。如果不想這樣做,比如因為某個句子太長導致填充過多,您可以使用 maxlen 引數來指定所需的最大長度,如下所示:

padded = pad_sequences(sequences, padding='post', maxlen=6)

其輸出如下:

[[ 2 3 4 5 6 0]

[ 2 3 4 7 6 0]

[ 3 8 5 2 0 0]

[11 12 13 14 15 2]]

現在您的填充序列長度一致,且填充量不多。不過,您會發現最長句子的一些單詞被截斷了,它們是從開頭截斷的。如果您不想丟失開頭的單詞,而是希望從句子末尾截斷,可以透過 truncating 引數來覆蓋預設行為,如下所示:

padded = pad_sequences(sequences, padding='post', maxlen=6, truncating='post')

結果顯示最長的句子現在從末尾截斷,而不是開頭:

[[ 2 3 4 5 6 0]

[ 2 3 4 7 6 0]

[ 3 8 5 2 0 0]

[ 9 10 11 12 13 14]]

TensorFlow 支援使用“稀疏”(形狀不同的)張量進行訓練,這非常適合 NLP 的需求。使用它們比本書的內容稍微進階一些,但在您完成接下來幾章提供的 NLP 入門後,可以進一步查閱文件瞭解更多。

移除停用詞和清理文字

在接下來的章節中,我們會看一些真實的文字資料集,並發現資料中經常有不想要的文字內容。你可能需要過濾掉一些所謂的“停用詞”,這些詞過於常見,不帶任何實際意義,比如“the”,“and”和“but”。你也可能會遇到很多HTML標籤,去除它們可以使文字更加乾淨。此外,其他需要過濾的內容還包括粗話、標點符號或人名。稍後我們會探索一個推文的資料集,其中經常包含使用者的ID,我們也會想要去除這些內容。

雖然每個任務會因文字內容的不同而有所差異,但通常有三種主要的方法可以程式設計地清理文字。第一步是去除HTML標籤。幸運的是,有一個名叫BeautifulSoup的庫可以讓這項任務變得簡單。例如,如果你的句子包含HTML標籤(比如
),以下程式碼可以將它們移除:

from bs4 import BeautifulSoup

soup = BeautifulSoup(sentence)

sentence = soup.get_text()

一種常見的去除停用詞方法是建立一個停用詞列表,然後預處理句子,移除其中的停用詞。以下是一個簡化的例子:

stopwords = ["a", "about", "above", ... "yours", "yourself", "yourselves"]

一個完整的停用詞列表可以在本章的一些線上示例中找到。然後,當你遍歷句子時,可以使用如下程式碼來移除句子中的停用詞:

words = sentence.split()

filtered_sentence = ""

for word in words:

if word not in stopwords:

filtered_sentence = filtered_sentence + word + " "

sentences.append(filtered_sentence)

另一件可以考慮的事情是去除標點符號,它可能會干擾停用詞的移除。上面展示的程式碼是尋找被空格包圍的詞語,因此如果停用詞後緊跟一個句號或逗號,它將不會被識別出來。

Python的string庫提供的翻譯功能可以輕鬆解決這個問題。它還帶有一個常量string.punctuation,其中包含了常見的標點符號列表,因此可以使用如下程式碼將其從單詞中移除:

import string

table = str.maketrans('', '', string.punctuation)

words = sentence.split()

filtered_sentence = ""

for word in words:

word = word.translate(table)

if word not in stopwords:

filtered_sentence = filtered_sentence + word + " "

sentences.append(filtered_sentence)

在這裡,每個句子在過濾停用詞之前,單詞中的標點符號已經被移除。因此,如果將句子拆分後得到“it;”,它會被轉換為“it”,然後作為停用詞被過濾掉。不過,注意當這樣處理時,你可能需要更新停用詞列表。通常,這些列表中會包含一些縮略詞和縮寫形式,比如“you’ll”。翻譯器會將“you’ll”轉換為“youll”,如果想要將它過濾掉,就需要在停用詞列表中新增它。

遵循這三個步驟後,你將獲得一組更加乾淨的文字資料。但當然,每個資料集都有其獨特之處,你需要根據具體情況進行調整

本節總結,本節介紹了自然語言處理(NLP)的基礎概念,包括文字編碼、分詞、去停用詞和清理文字等技術。首先,探討了如何將語言轉為數字以便於計算機處理,並透過編碼方法將單詞分解為數值。接著,介紹了分詞工具(如Tokenizer)在文字預處理中分配和管理單詞索引。還討論了處理未見過的詞彙(OOV)以減少模型誤差的策略。在清理文字方面,使用BeautifulSoup庫去除HTML標籤,並利用停用詞列表和標點符號過濾功能對資料集進一步清理。此外,為確保資料一致性,介紹了填充(padding)技術以使資料形狀一致,適用於模型訓練。這些步驟為文字清理和建模提供了堅實的基礎,但在實際應用中應靈活調整以應對不同資料集的需求。

相關文章