序言:重新訓練人工智慧大型模型是一項複雜且高成本的任務,尤其對於當前的LLM(大型語言模型)來說,全球99.99%的企業難以承擔。這是因為模型訓練需要巨大的資源投入、複雜的技術流程以及大量的人力支援。因此,無論在科學研究還是實際應用中,人們通常依賴開源的預訓練模型及其已經學習到的各種特徵資訊,就像使用開源的Linux一樣。本節將講解如何利用這些預訓練模型中的“嵌入”資訊來解決實際問題。
使用預訓練嵌入與RNN
在之前的所有示例中,我們收集了訓練集中要使用的完整單詞集,然後用它們訓練了嵌入。這些嵌入最初是聚合在一起的,然後輸入到密集網路中,而在最近的章節中,我們探討了如何使用RNN來改進結果。在此過程中,我們被限制在資料集中已經存在的單詞,以及如何使用該資料集中的標籤來學習它們的嵌入。回想一下在前面有一章,我們討論了遷移學習。如果,您可以不自己學習嵌入,而是使用已經預先學習的嵌入,研究人員已經完成了將單詞轉化為向量的艱苦工作,並且這些向量是經過驗證的呢?其中一個例子是Stanford大學的Jeffrey Pennington、Richard Socher和Christopher Manning開發的GloVe(Global Vectors for Word Representation)模型。
在這種情況下,研究人員分享了他們為各種資料集預訓練的單詞向量:
• 一個包含60億個標記、40萬個單詞的詞彙集,維度有50、100、200和300,單詞來自維基百科和Gigaword
• 一個包含420億個標記、190萬個單詞的詞彙集,維度為300,來自通用爬蟲
• 一個包含8400億個標記、220萬個單詞的詞彙集,維度為300,來自通用爬蟲
• 一個包含270億個標記、120萬個單詞的詞彙集,維度為25、50、100和200,來自對20億條推文的Twitter爬蟲
考慮到這些向量已經預訓練,我們可以輕鬆地在TensorFlow程式碼中重複使用它們,而不必從頭開始學習。首先,我們需要下載GloVe資料。這裡選擇使用Twitter資料集,包含270億個標記和120萬個單詞的詞彙集。下載的是一個包含25、50、100和200維度的歸檔檔案。
為了讓整個過程稍微方便一些,我已經託管了25維版本,您可以像這樣將其下載到Colab筆記本中:
!wget --no-check-certificate \
https://storage.googleapis.com/laurencemoroney-blog.appspot.com/glove.twitter.27B.25d.zip \
-O /tmp/glove.zip
這是一個ZIP檔案,您可以像這樣解壓縮,得到一個名為glove.twitter.27B.25d.txt的檔案:
解壓GloVe嵌入
import os
import zipfile
local_zip = '/tmp/glove.zip'
zip_ref = zipfile.ZipFile(local_zip, 'r')
zip_ref.extractall('/tmp/glove')
zip_ref.close()
檔案中的每一行都是一個單詞,後面跟著為其學習到的維度係數。最簡單的使用方式是建立一個字典,其中鍵是單詞,值是嵌入。您可以這樣設定這個字典:
glove_embeddings = dict()
f = open('/tmp/glove/glove.twitter.27B.25d.txt')
for line in f:
values = line.split()
word = values[0]
coefs = np.asarray(values[1:], dtype='float32')
glove_embeddings[word] = coefs
f.close()
此時,您可以簡單地透過使用單詞作為鍵來查詢任何單詞的係數集。例如,要檢視“frog”的嵌入,您可以使用:
glove_embeddings['frog']
有了這個資源,您可以像以前一樣使用分詞器獲取語料庫的單詞索引——但現在,您可以建立一個新的矩陣,我稱之為嵌入矩陣。這個矩陣將使用GloVe集中的嵌入(從glove_embeddings獲取)作為其值。因此,如果您檢查資料集中單詞索引中的單詞,如下所示:
{'
那麼嵌入矩陣的第一行應該是GloVe中“
您可以使用以下程式碼建立該矩陣:
embedding_matrix = np.zeros((vocab_size, embedding_dim))
for word, index in tokenizer.word_index.items():
if index > vocab_size - 1:
break
else:
embedding_vector = glove_embeddings.get(word)
if embedding_vector is not None:
embedding_matrix[index] = embedding_vector
這只是建立了一個矩陣,矩陣的維度是您所需的詞彙大小和嵌入維度。然後,對於分詞器的每個詞彙索引項,您會查詢GloVe中的係數(從glove_embeddings中獲取),並將這些值新增到矩陣中。
接著,您需要修改嵌入層,使用預訓練的嵌入,透過設定weights引數,並指定不希望該層被訓練,透過設定trainable=False:
model = tf.keras.Sequential([
tf.keras.layers.Embedding(vocab_size, embedding_dim,
weights=[embedding_matrix], trainable=False),
tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(embedding_dim, return_sequences=True)),
tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(embedding_dim)),
tf.keras.layers.Dense(24, activation='relu'),
tf.keras.layers.Dense(1, activation='sigmoid')
])
現在,您可以像之前一樣進行訓練。然而,您需要考慮您的詞彙大小。在上一章中,您為了避免過擬合,做了一些最佳化,目的是防止嵌入過多地學習低頻單詞;您透過使用更小的詞彙表、僅包含常用單詞來避免過擬合。在這種情況下,由於單詞嵌入已經透過GloVe為您學習過,您可以擴充套件詞彙表——但擴充套件多少呢?
首先要探索的是,您的語料庫中有多少單詞實際上在GloVe集中。GloVe有120萬個單詞,但不能保證它包含您的所有單詞。所以,這裡有一些程式碼,可以快速對比,讓您探索您的詞彙表應該多大。
首先,整理資料。建立一個包含Xs和Ys的列表,其中X是詞彙索引,Y=1表示該單詞在嵌入中,0則表示不在。此外,您可以建立一個累計集,在每個時間步計算單詞的比例。例如,索引為0的單詞“OOV”不在GloVe中,所以它的累計Y值為0。下一個索引的單詞“new”在GloVe中,所以它的累計Y值為0.5(即,到目前為止看到的單詞中有一半在GloVe中),然後您會繼續這樣計算整個資料集:
xs = []
ys = []
cumulative_x = []
cumulative_y = []
total_y = 0
for word, index in tokenizer.word_index.items():
xs.append(index)
cumulative_x.append(index)
if glove_embeddings.get(word) is not None:
total_y = total_y + 1
ys.append(1)
else:
ys.append(0)
cumulative_y.append(total_y / index)
然後,您可以使用以下程式碼繪製Xs與Ys的關係圖:
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(12, 2))
ax.spines['top'].set_visible(False)
plt.margins(x=0, y=None, tight=True)
plt.axis([13000, 14000, 0, 1])
plt.fill(ys)
這將給您一個單詞頻率圖,看起來像圖7-17。
圖7-17. 單詞頻率圖
如圖表所示,密度在10,000到15,000之間發生變化。這讓您直觀地看到,大約在13,000標記的位置,未在GloVe嵌入中的單詞的頻率開始超過那些已經在GloVe嵌入中的單詞。
如果您再繪製累計的cumulative_x與cumulative_y的關係,您將能更好地理解這個變化。以下是程式碼:
import matplotlib.pyplot as plt
plt.plot(cumulative_x, cumulative_y)
plt.axis([0, 25000, .915, .985])
您可以看到圖7-18中的結果。
圖7-18. 繪製單詞索引頻率與GloVe的關係
現在,您可以調整plt.axis中的引數,放大檢視拐點,看看未出現在GloVe中的單詞是如何開始超過那些在GloVe中的單詞的。這是設定詞彙大小的一個不錯起點。
使用這種方法,我選擇了一個詞彙大小為13,200(而不是之前為了避免過擬合而使用的2,000),並使用了以下模型架構,其中embedding_dim是25,因為我使用的是GloVe集:
model = tf.keras.Sequential([
tf.keras.layers.Embedding(vocab_size, embedding_dim,
weights=[embedding_matrix], trainable=False),
tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(embedding_dim, return_sequences=True)),
tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(embedding_dim)),
tf.keras.layers.Dense(24, activation='relu'),
tf.keras.layers.Dense(1, activation='sigmoid')
])
然後,使用Adam最佳化器:
adam = tf.keras.optimizers.Adam(learning_rate=0.00001, beta_1=0.9, beta_2=0.999, amsgrad=False)
model.compile(loss='binary_crossentropy', optimizer=adam, metrics=['accuracy'])
訓練30個epoch後,得到了很好的結果。準確率如圖7-19所示。驗證準確率與訓練準確率非常接近,表明我們不再過擬合。
圖7-19. 使用GloVe嵌入的堆疊LSTM準確率
這一點透過損失曲線得到進一步驗證,如圖7-20所示。驗證損失不再發散,這表明儘管我們的準確率只有大約73%,我們可以有信心認為模型的準確性達到了這個程度。
圖7-20. 使用GloVe嵌入的堆疊LSTM損失
訓練模型更長時間會得到非常相似的結果,並且表明,儘管大約在第80個epoch左右開始出現過擬合,模型仍然非常穩定。
準確率指標(圖7-21)顯示模型訓練得很好。
損失指標(圖7-22)顯示大約在第80個epoch左右開始出現發散,但模型仍然擬合得很好。
圖7-21. 使用GloVe的堆疊LSTM在150個epoch上的準確率
圖7-22. 使用GloVe的堆疊LSTM在150個epoch上的損失
這告訴我們,這個模型是早停的好候選者,您只需要訓練它75到80個epoch,就能得到最佳結果。
我用來自《洋蔥報》的標題(《洋蔥報》是諷刺性標題的來源,也是諷刺資料集的來源),與其他句子進行了測試,測試程式碼如下:
test_sentences = [
"It Was, For, Uh, Medical Reasons, Says Doctor To Boris Johnson, Explaining Why They Had To Give Him Haircut",
"It's a beautiful sunny day",
"I lived in Ireland, so in high school they made me learn to speak and write in Gaelic",
"Census Foot Soldiers Swarm Neighborhoods, Kick Down Doors To Tally Household Sizes"
]
這些標題的結果如下——記住,接近50%(0.5)的值被認為是中立的,接近0的是非諷刺的,接近1的是諷刺的:
[[0.8170955 ]
[0.08711044]
[0.61809343]
[0.8015281 ]]
來自《洋蔥報》的第一句和第四句顯示了80%以上的諷刺機率。關於天氣的陳述則顯得非常非諷刺(9%),而關於在愛爾蘭上高中這句話被認為可能是諷刺的,但信心不高(62%)。
總結
本節中我們介紹了迴圈(遞迴)神經網路(RNN),它們在設計中使用面向序列的邏輯,可以幫助您理解句子的情感,不僅基於其中的單詞,還基於它們出現的順序。瞭解了基本的RNN如何工作,以及LSTM如何在此基礎上改進,保留長期上下文。您使用這些技術改進了您一直在做的情感分析模型。接著,您研究了RNN的過擬合問題以及改善它們的技術,包括使用從預訓練嵌入中進行遷移學習。在接下來的章節中,我們將使用前面全部所學內容探索如何預測單詞,進而建立一個能夠生成文字的模型,甚至為您寫詩!