深入理解大語言模型(LLM)的資料處理流程對於模型訓練至關重要。本文首先介紹如何使用滑動視窗和不同步幅(stride)設定來建立資料載入器,並以批次大小為 1 和 8 的例子說明如何有效地取樣訓練資料。接著,文章詳細闡述瞭如何將文字資料轉換為數值向量表示,即嵌入向量。此過程包含兩個關鍵步驟:詞彙嵌入和位置嵌入。詞彙嵌入將每個詞彙對映到一個高維向量空間,而位置嵌入則為詞彙新增位置資訊,使模型能夠理解詞彙在序列中的順序。文章使用 PyTorch 框架示範瞭如何建立嵌入層、處理輸入資料、以及如何結合詞彙嵌入和位置嵌入生成最終的輸入嵌入向量,這些向量將作為 LLM 訓練的輸入。最後,文章簡要提及了注意力機制及其在捕捉長距離依賴關係中的作用,為後續深入理解 LLM 架構奠定基礎。
2.6 資料取樣與滑動視窗
為了更好地理解 GPTDatasetV1 類別(清單 2.5)和 create_dataloader_v1 函式(清單 2.6)如何協同工作,我們來測試一下批次大小為 1 且上下文大小為 4 的資料載入器(dataloader):
with open("the-verdict.txt", "r", encoding="utf-8") as f:
raw_text = f.read()
dataloader = create_dataloader_v1(
raw_text, batch_size=1, max_length=4, stride=1, shuffle=False)
data_iter = iter(dataloader)
first_batch = next(data_iter)
print(first_batch)
執行上述程式碼後,輸出如下:
[tensor([[ 40, 367, 2885, 1464]]), tensor([[ 367, 2885, 1464, 1807]])]
first_batch 變數包含兩個張量:第一個張量儲存輸入的 token ID,第二個張量儲存目標的 token ID。由於 max_length 設定為 4,因此兩個張量都包含四個 token ID。值得注意的是,輸入大小為 4 是為了簡化說明,通常訓練 LLM 時的輸入大小至少為 256。
內容解密:
max_length=4表示每次處理的 token 序列長度為 4。stride=1表示每次移動視窗時,輸入 token ID 的偏移量為 1,模擬滑動視窗的效果,如圖 2.14 所示。- 第一個張量
tensor([[ 40, 367, 2885, 1464]])是輸入的 token ID,第二個張量tensor([[ 367, 2885, 1464, 1807]])是對應的目標 token ID。
為了進一步理解 stride=1 的含義,我們再取樣另一個批次:
second_batch = next(data_iter)
print(second_batch)
輸出如下:
[tensor([[ 367, 2885, 1464, 1807]]), tensor([[2885, 1464, 1807, 3619]])]
比較第一個和第二個批次,可以發現第二個批次的 token ID 相對於第一個批次偏移了一個位置(例如,第一個批次輸入的第二個 ID 是 367,而第二個批次輸入的第一個 ID 就是 367)。這說明瞭 stride=1 的效果,即每次移動一個 token 的位置。
練習 2.2:使用不同的步幅和上下文大小測試資料載入器
為了更深入地理解資料載入器的工作原理,可以嘗試不同的設定,例如 max_length=2 和 stride=2,或者 max_length=8 和 stride=2。
使用大於 1 的批次大小
接下來,我們看看如何使用批次大小大於 1 的資料載入器:
dataloader = create_dataloader_v1(
raw_text, batch_size=8, max_length=4, stride=4,
shuffle=False
)
data_iter = iter(dataloader)
inputs, targets = next(data_iter)
print("Inputs:\n", inputs)
print("\nTargets:\n", targets)
輸出如下:
Inputs:
tensor([[ 40, 367, 2885, 1464],
[ 1807, 3619, 402, 271],
[10899, 2138, 257, 7026],
[15632, 438, 2016, 257],
[ 922, 5891, 1576, 438],
[ 568, 340, 373, 645],
[1049, 5975, 284, 502],
[284, 3285, 326,11]])
Targets:
tensor([[367,2885,1464,1807],
[3619,402 ,271 ,10899],
[2138 ,257 ,7026 ,15632],
[438 ,2016 ,257 ,922],
[5891 ,1576 ,438 ,568],
[340 ,373 ,645 ,1049],
[5975 ,284 ,502 ,284],
[3285 ,326 ,11 ,287]])
此例中,我們將 stride 設定為 max_length(均為4),以充分利用資料集,避免批次之間的重疊,因為過多的重疊可能導致過擬合。
圖表說明
圖2.14直觀展示了當從輸入資料集建立多個批次時,我們如何在文字上滑動輸入視窗。如果步幅設為1,則在建立下一個批次時,將輸入視窗移動一個位置;如果將步幅設為等於輸入視窗的大小,則可以防止批次之間的重疊。
圖表呈現
此圖示展示了從原始文字到嵌入向量的處理流程。
圖表解說:
- 輸入文字首先經過分詞處理,拆分成單個的詞元。
- 分詞後的結果被轉換成對應的 token ID。
- 最後,token ID 被轉換成嵌入向量,用於後續的模型訓練。
圖表呈現
圖表解說:
- 圖中展示了輸入ID如何對應到權重矩陣中的特定行,從而獲得其嵌入向量表示。
圖表呈現
圖表解說:
- 當輸入特定的token ID(如3)時,嵌入層會根據權重矩陣輸出對應的嵌入向量。
圖表呈現
圖表解說:
- 此序列圖描述了輸入ID透過嵌入層查詢並取得對應嵌入向量的過程。
圖表呈現
圖表解說:
- 詞彙大小和輸出維度共同決定了嵌入層的大小,並最終生成對應的權重矩陣。
圖表呈現
圖表解說:
- 當詞彙大小為6,輸出維度為3時,嵌入層的權重矩陣大小為6x3,用於儲存所有token的嵌入向量。
圖表呈現
圖表解說:
- 在訓練開始前,權重矩陣通常被隨機初始化,然後在訓練過程中逐步最佳化。
圖表呈現
圖表解說:
- 將token ID轉換成嵌入向量是LLM訓練的第一步,這些向量將作為模型的輸入。
圖表呈現
圖表解說:
- GPT-like LLM是一種根據深度神經網路架構的語言模型,使用反向傳播演算法進行訓練。
圖表呈現
圖表解說:
- 反向傳播演算法是神經網路訓練的核心,用於計算梯度並更新模型引數。
圖表呈現
@startuml
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle
title LLM資料處理與嵌入向量編碼技術
package "機器學習流程" {
package "資料處理" {
component [資料收集] as collect
component [資料清洗] as clean
component [特徵工程] as feature
}
package "模型訓練" {
component [模型選擇] as select
component [超參數調優] as tune
component [交叉驗證] as cv
}
package "評估部署" {
component [模型評估] as eval
component [模型部署] as deploy
component [監控維護] as monitor
}
}
collect --> clean : 原始資料
clean --> feature : 乾淨資料
feature --> select : 特徵向量
select --> tune : 基礎模型
tune --> cv : 最佳參數
cv --> eval : 訓練模型
eval --> deploy : 驗證模型
deploy --> monitor : 生產模型
note right of feature
特徵工程包含:
- 特徵選擇
- 特徵轉換
- 降維處理
end note
note right of eval
評估指標:
- 準確率/召回率
- F1 Score
- AUC-ROC
end note
@enduml
圖表解說:
- 在LLM訓練過程中,嵌入權重會隨著模型的最佳化而不斷更新,以更好地表示語義資訊。
2.8 編碼詞彙位置資訊
在前面的章節中,我們已經瞭解如何將單個詞彙ID轉換為三維嵌入向量。現在,我們將進一步探討如何對這些嵌入向量進行修改,以編碼詞彙在文字中的位置資訊。
為什麼需要位置資訊?
原則上,詞彙嵌入向量是大語言模型(LLM)的合適輸入。然而,LLM的自注意力機制(self-attention mechanism)並不具備對序列中詞彙位置或順序的概念。前述嵌入層的工作方式是,無論詞彙ID在輸入序列中的位置如何,相同的詞彙ID總是會被對映到相同的向量表示,如圖2.17所示。
位置感知嵌入
為了克服這一限制,我們可以使用兩大類別位置感知嵌入:相對位置嵌入(relative positional embeddings)和絕對位置嵌入(absolute positional embeddings)。
絕對位置嵌入
絕對位置嵌入直接與序列中的特定位置相關聯。對於輸入序列中的每個位置,都會新增一個獨特的嵌入向量到詞彙的嵌入向量中,以傳達其確切位置。例如,第一個詞彙將具有特定的位置嵌入,第二個詞彙具有另一個不同的嵌入,依此類別推,如圖2.18所示。
相對位置嵌入
相對位置嵌入的重點則是詞彙之間的相對位置或距離。這意味著模型學習的是“相距多遠”而不是“在什麼確切位置”的關係。這種方法的優勢在於,即使模型在訓練過程中未曾見過某些長度的序列,它也能更好地泛化到不同長度的序列。
如何選擇位置嵌入?
兩種型別的位置嵌入都旨在增強LLM理解詞彙之間順序和關係的能力,從而實作更準確和上下文感知的預測。選擇使用哪種方法通常取決於特定的應用和被處理資料的性質。
OpenAI的GPT模型
OpenAI的GPT模型使用的是在訓練過程中最佳化的絕對位置嵌入,而不是像原始變換器模型中的位置編碼那樣固定或預定義。這種最佳化過程是模型訓練的一部分。目前,讓我們建立初始的位置嵌入,以生成LLM的輸入。
程式碼範例:實作詞彙嵌入和位置嵌入
import torch
import torch.nn as nn
# 詞彙嵌入層
embedding_layer = nn.Embedding(num_embeddings=6, embedding_dim=3)
input_ids = torch.tensor([2, 3, 5, 1])
# 取得詞彙嵌入向量
token_embeddings = embedding_layer(input_ids)
print(token_embeddings)
# 建立位置嵌入層
class PositionalEncoding(nn.Module):
def __init__(self, d_model, max_len=5000):
super(PositionalEncoding, self).__init__()
self.encoding = torch.zeros(max_len, d_model)
self.encoding.requires_grad = False
pos = torch.arange(0, max_len, dtype=torch.float)
pos = pos.unsqueeze(1)
_2i = torch.arange(0, d_model, 2, dtype=torch.float)
self.encoding[:, 0::2] = torch.sin(pos / (10000 ** (_2i / d_model)))
self.encoding[:, 1::2] = torch.cos(pos / (10000 ** (_2i / d_model)))
def forward(self, x):
batch_size, seq_len, _ = x.size()
return x + self.encoding[:seq_len, :].unsqueeze(0)
# 初始化位置編碼層
pos_encoder = PositionalEncoding(d_model=3)
# 取得帶有位置資訊的嵌入向量
input_embeddings = pos_encoder(token_embeddings.unsqueeze(0)).squeeze(0)
print(input_embeddings)
內容解密:
- 詞彙嵌入層初始化:我們首先初始化了一個詞彙嵌入層
embedding_layer,它將詞彙ID對映到三維向量空間。 - 取得詞彙嵌入向量:透過將輸入ID
input_ids傳入embedding_layer,我們獲得了對應的詞彙嵌入向量token_embeddings。 - 建立位置編碼層:定義了一個
PositionalEncoding類別,用於生成位置嵌入。它根據正弦和餘弦函式,為不同位置的詞彙生成獨特的位置向量。 - 取得帶有位置資訊的嵌入向量:將
token_embeddings傳入pos_encoder,獲得最終的輸入嵌入向量input_embeddings,它包含了詞彙的位置資訊。
使用絕對嵌入方法的輸入嵌入處理
在大語言模型(LLM)的訓練過程中,輸入文字需要被轉換為數值向量,也就是所謂的嵌入表示。嵌入表示將離散資料(如單詞或影像)轉換為連續的向量空間,使其與神經網路操作相容。首先,原始文字被分解為標記(token),這些標記可以是單詞或字元。然後,這些標記被轉換為整數表示,即標記ID。
嵌入層的實作
為了實作輸入嵌入,我們首先定義一個嵌入層,用於將標記ID轉換為向量表示。假設我們的詞彙量大小(vocab_size)為50,257,輸出維度(output_dim)為256。
vocab_size = 50257
output_dim = 256
token_embedding_layer = torch.nn.Embedding(vocab_size, output_dim)
內容解密:
torch.nn.Embedding是 PyTorch 中用於建立嵌入層的類別,它本質上是一個查詢表,用於根據輸入的標記ID檢索相應的向量表示。vocab_size指定了詞彙量的大小,即模型能夠處理的不同標記的數量。output_dim指定了輸出向量的維度,這裡我們選擇256維。
處理輸入資料
接下來,我們例項化一個資料載入器(dataloader),用於載入和處理輸入資料。
max_length = 4
dataloader = create_dataloader_v1(
raw_text, batch_size=8, max_length=max_length,
stride=max_length, shuffle=False
)
data_iter = iter(dataloader)
inputs, targets = next(data_iter)
print("Token IDs:\n", inputs)
print("\nInputs shape:\n", inputs.shape)
內容解密:
create_dataloader_v1是一個自定義函式,用於建立資料載入器。它根據指定的批次大小(batch_size)、最大長度(max_length)等引數處理原始文字資料。inputs和targets分別代表輸入資料和目標資料。- 輸出的
inputs.shape表示輸入資料的形狀,例如torch.Size([8, 4]),表明有8個樣本,每個樣本包含4個標記。
嵌入標記ID
使用嵌入層將標記ID轉換為向量表示。
token_embeddings = token_embedding_layer(inputs)
print(token_embeddings.shape)
內容解密:
token_embedding_layer(inputs)將輸入的標記ID轉換為對應的向量表示。- 輸出的形狀
torch.Size([8, 4, 256])表示每個標記被嵌入到一個256維的向量中,因此對於8個樣本,每個樣本有4個標記,每個標記對應一個256維向量。
編碼詞位置
為了捕捉序列中標記的位置資訊,我們需要新增位置嵌入。
context_length = max_length
pos_embedding_layer = torch.nn.Embedding(context_length, output_dim)
pos_embeddings = pos_embedding_layer(torch.arange(context_length))
print(pos_embeddings.shape)
內容解密:
pos_embedding_layer是另一個嵌入層,用於將位置索引對映到向量表示。torch.arange(context_length)生成一個從0到context_length-1的序列,用於表示位置索引。- 輸出的形狀
torch.Size([4, 256])表示對於每個位置索引,有一個對應的256維向量。
合成輸入嵌入
將標記嵌入和位置嵌入相加以獲得最終的輸入嵌入。
input_embeddings = token_embeddings + pos_embeddings
print(input_embeddings.shape)
內容解密:
- PyTorch 的廣播機制允許將形狀為
(4, 256)的pos_embeddings加到形狀為(8, 4, 256)的token_embeddings上,從而得到形狀為(8, 4, 256)的input_embeddings。 - 最終的輸入嵌入包含了標記資訊和位置資訊,可以作為LLM的主體部分的輸入。
注意力機制簡介
在準備好輸入嵌入後,我們將探討LLM架構中的一個重要組成部分:注意力機制。注意力機制允許模型在處理序列資料時能夠動態地聚焦於不同的部分,從而更好地捕捉長距離依賴關係。