將離散的文本轉(zhuǎn)化為連續(xù)的向量表示,即嵌入向量(Embedding Vector)。嵌入向量是大模型處理自然語言的起點,它將人類語言的符號轉(zhuǎn)化為機器可以理解的數(shù)學(xué)表示。
本文將以Transformer架構(gòu)為核心,深入探討嵌入向量的生成過程,剖析其背后的“魔法”,并通過代碼示例展示如何實現(xiàn)這一過程。
嵌入向量的簡介
從上一篇我們已經(jīng)了解了詞元和詞元ID的概念,最后我們生成了一個詞匯表(Vocabulary),并且知道詞匯表的大小通常在幾萬到幾十萬之間,具體大小取決于模型設(shè)計。
詞元ID是離散的整數(shù),無法直接用于神經(jīng)網(wǎng)絡(luò)的數(shù)學(xué)運算。因此,嵌入層(Embedding Layer)將詞元ID映射為連續(xù)的向量表示。嵌入層本質(zhì)上是一個可學(xué)習(xí)的查找表,存儲為一個形狀為 [vocab_size, embedding_dim] 的矩陣,其中:
vocab_size:詞匯表的大小。
embedding_dim:每個詞元的向量維度。
詞匯表的概念我們已經(jīng)了解,嵌入向量的概念可以簡單理解為:你用多少個數(shù)字來表示一個詞,維度越高,詞向量表達的語義就越豐富,但也更復(fù)雜。
我們要記住的是嵌入向量是模型最早期的“參數(shù)矩陣”,通常是隨機初始化的,然后在訓(xùn)練中慢慢學(xué)習(xí)。
我們先看一個例子,如下代碼:
import?torch
import?torch.nn as nn
# 設(shè)置打印選項
torch.set_printoptions(threshold=10000, precision=4, sci_mode=False)
# 參數(shù)定義
vocab_size?=?10000
embedding_dim?=?256
embedding_layer?= nn.Embedding(vocab_size, embedding_dim)
# 輸入 token id
token_ids?= torch.tensor([101,?102,?103,?104,?105,?106,?107])
# 獲取嵌入向量
embeddings?= embedding_layer(token_ids)
# 輸出嵌入矩陣
print("嵌入矩陣:")
print(embeddings)
執(zhí)行上面代碼后,我們看到程序會輸出如下信息:
嵌入矩陣:
tensor([[ ? ?-1.1887, ? ? -0.3787, ? ? -1.6036, ? ? ?1.2109, ? ? -1.5041,
? ? ? ? ? ? ? 0.5217, ? ? -0.0660, ? ? ?0.8761, ? ? -1.3062, ? ? -0.5456,
? ? ? ? ? ? ?-2.2370, ? ? -0.7596, ? ? ?0.6463, ? ? ?1.3679, ? ? -0.7995,
? ? ? ? ? ? ?-0.8499, ? ? -1.1883, ? ? -0.4964, ? ? -0.9248, ? ? ?1.3193,
? ? ? ? ? ? ?-0.3776, ? ? -1.6146, ? ? -0.2606, ? ? ?1.3084, ? ? ?1.5899,
? ? ? ? ? ? ?-0.3184, ? ? ?0.7106, ? ? ?0.4439, ? ? -1.0974, ? ? -0.0911,
? ? ? ? ? ? ? 0.0765, ? ? -1.1273, ? ? -2.0399, ? ? -0.7867, ? ? ?0.5819,
....中間信息省略
-0.6946, ? ? ?0.1002, ? ? -0.8110, ? ? -1.1093, ? ? ?0.4499,
? ? ? ? ? ? ?-0.5466, ? ? ?0.8090, ? ? ?1.3586, ? ? -0.4617, ? ? ?0.0936,
? ? ? ? ? ? ? 0.4514, ? ? -1.0935, ? ? ?1.1986, ? ? ?0.5158, ? ? ?0.7961,
? ? ? ? ? ? ? 0.1658, ? ? ?0.9241, ? ? -0.2872, ? ? -1.5406, ? ? ?0.6301,
? ? ? ? ? ? ? 1.3381, ? ? -1.6376, ? ? ?0.5164, ? ? -1.1603, ? ? -1.0949,
? ? ? ? ? ? ? 0.7568, ? ? -0.8883, ? ? -0.0534, ? ? -1.1359, ? ? -0.1575,
? ? ? ? ? ? ?-0.7413]], grad_fn=<EmbeddingBackward0>)
這段代碼到底做了什么事情?我們接下來進行詳解:
定義嵌入矩陣的大?。?/p>
vocab_size = 10000
表示你有一個詞匯表(vocabulary),大小是 10,000,意思是你有 10,000 個獨立的詞(或子詞、token),詞匯表的概念可以參照上篇文章的介紹。
embedding_dim = 256
表示每個詞要被映射為一個256維的向量。這就是“嵌入維度”,你可以理解為:
把每個離散的 token 映射到一個連續(xù)空間中,變成一個可學(xué)習(xí)的向量(表示它的“意義”或“語義”)
初始化嵌入層:
embedding_layer = nn.Embedding(vocab_size, embedding_dim)
nn.Embedding(vocab_size, embedding_dim) 是 PyTorch 提供的嵌入層。
它的作用是創(chuàng)建一個大小為 [vocab_size, embedding_dim] 的查找表,每行對應(yīng)一個 token 的向量。
換句話說,它是一個形狀為 [10000, 256] 的矩陣。每一行是一個詞的向量:
token_id = 0 → [0.1234, -0.5321, ..., 0.0012] ?# 長度為256
token_id = 1 → [0.3332, -0.8349, ..., -0.2176]
...
token_id = 9999 → [...]
這個矩陣的參數(shù)是可訓(xùn)練的,會隨著模型訓(xùn)練不斷優(yōu)化,使得語義相近的 token 向量距離也更近。
定義 token id:
token_ids = torch.tensor([101, 102, 103, 104, 105, 106, 107])
這里創(chuàng)建了一個 tensor,內(nèi)容是 [101, 102, 103, 104, 105, 106, 107],它代表你輸入的 7 個詞/子詞的索引(ID)。
每個數(shù)字表示詞表中的一個詞,例如:
101 → “寫”
102 → “代”
103 → “碼”
104 → “的”
105 → “中”
106 → “年”
107 → “人”
(這里只是舉例,真實情況看 tokenizer)
變?yōu)榍度胂蛄浚?/p>
embeddings = embedding_layer(token_ids)
把 token_ids [101, 102, 103, 104, 105, 106, 107] 送進嵌入層后,會從嵌入矩陣中取出它們對應(yīng)的向量,得到:
embeddings.shape == [7, 256]
每個詞變成了一個 256 維的向量,這些向量是浮點數(shù),比如:
embeddings[0] = tensor([ 0.1371, -0.0208, ..., 0.0415]) ?# token 101 的嵌入
embeddings[1] = tensor([-0.0817, 0.2991, ..., 0.0034]) ?# token 102 的嵌入
...
輸出的向量是啥?
這些 256 維向量就是詞的語義向量表示(Word Embedding):
它們是模型可訓(xùn)練參數(shù);
它們的數(shù)值是隨機初始化的(除非你加載了預(yù)訓(xùn)練模型);
它們的作用是:把 token 編碼成模型能處理的“連續(xù)表示”;
在模型訓(xùn)練過程中,這些向量會逐步學(xué)習(xí)到語義,比如 “我” 和 “我們” 的向量距離會比 “我” 和 “電腦” 更近。如何訓(xùn)練我們后續(xù)再講,這里只要明白它們是怎么初始化的和有什么作用就行。
最終經(jīng)過大量語料訓(xùn)練之后,每個 token 的 embedding都是模型學(xué)習(xí)到的語義表示,它不再“隨機”,而是能捕捉詞義的相似性。
大概的流程為:
原始輸入文本 → tokenizer → token_id → embedding向量 → 加入位置編碼 → 輸入Transformer
位置編碼簡介
位置編碼(Positional Encoding)是 Transformer 架構(gòu)的關(guān)鍵組件之一,在Transformer架構(gòu)中,模型主要依賴自注意力機制來處理輸入序列。然而,自注意力機制本身是無序的,即它不考慮輸入序列中詞或標(biāo)記(token)的相對位置或絕對位置信息。這會導(dǎo)致模型無法區(qū)分序列中不同位置的詞,即使它們的語義完全相同。為了解決這個問題,引入了位置編碼(Positional Encoding),其作用是:
提供位置信息:為序列中的每個位置賦予一個獨特的表示,使模型能夠感知詞的順序和相對位置。
保持序列順序的語義:通過位置編碼,Transformer可以理解序列中詞的排列順序?qū)φZ義的影響。
支持并行計算:位置編碼是預(yù)先計算或固定的(不像RNN那樣依賴序列處理),因此不會影響Transformer的并行化優(yōu)勢。
常見的位置編碼方法:
位置編碼是在 進入 Transformer 架構(gòu)的第一層之前添加的,通常在模型的輸入端(即嵌入層之后)。
對于標(biāo)準(zhǔn) Transformer(如 GPT 或 BERT),位置編碼是直接加到詞嵌入上,作為整個模型的初始輸入。
對于某些變體(如使用 RoPE 的模型),位置信息可能在注意力機制內(nèi)部通過旋轉(zhuǎn)矩陣應(yīng)用,但這仍然發(fā)生在 Transformer 層處理之前或作為注意力計算的一部分。
接著上面的嵌入向量代碼,我們先使用正弦/余弦編碼來實現(xiàn)一個位置編碼:
import?torch
import?torch.nn as nn
import?math
# 設(shè)置打印選項
torch.set_printoptions(threshold=10000, precision=4, sci_mode=False)
# 定義位置編碼類
class?PositionalEncoding(nn.Module):
? ??def?__init__(self, d_model, max_len=5000):
? ? ? ??super(PositionalEncoding, self).__init__()
? ? ? ??pe?= torch.zeros(max_len, d_model)
? ? ? ??position?= torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
? ? ? ??div_term?= torch.exp(torch.arange(0, d_model,?2).float() * (-math.log(10000.0) / d_model))
? ? ? ??pe[:,?0::2] = torch.sin(position * div_term)
? ? ? ??pe[:,?1::2] = torch.cos(position * div_term)
? ? ? ??pe?= pe.unsqueeze(0) ?# Shape: (1, max_len, d_model)
? ? ? ??self.register_buffer('pe', pe)
? ??def?forward(self, x):
? ? ? ??# x: (batch_size, seq_len, d_model)
? ? ? ??x?= x + self.pe[:, :x.size(1), :] ?# Add positional encoding
? ? ? ??return?x
# 參數(shù)定義
vocab_size?=?10000
embedding_dim?=?256
embedding_layer?= nn.Embedding(vocab_size, embedding_dim)
pos_encoder?= PositionalEncoding(d_model=embedding_dim, max_len=5000)
# 輸入 token id
token_ids?= torch.tensor([101,?102,?103,?104,?105,?106,?107])
# 獲取嵌入向量
embeddings?= embedding_layer(token_ids)
# 輸出嵌入矩陣
print("嵌入矩陣:")
print(embeddings)
# 添加位置編碼
embeddings_with_pe?= pos_encoder(embeddings.unsqueeze(0)).squeeze(0) ?# Add batch dimension and remove it
# 輸出添加位置編碼后的矩陣
print("n添加位置編碼后的嵌入矩陣:")
print(embeddings_with_pe)
RoPE 旋轉(zhuǎn)位置編碼:(RoPE 只作用在自注意力中的 Query 和 Key 上,不是 Value,也不是 Embedding 本身,下面代碼只是示例。)
import?torch
import?torch.nn?as?nn
import?math
# 設(shè)置打印選項(便于查看向量)
torch.set_printoptions(threshold=10000, precision=4, sci_mode=False)
# ========================
# 旋轉(zhuǎn)位置編碼(RoPE)模塊
# ========================
class?RotaryPositionalEncoding(nn.Module):
? ??def?__init__(self, dim, max_len=5000, base=10000):
? ? ? ??super(RotaryPositionalEncoding, self).__init__()
? ? ? ??assert?dim %?2?==?0,?"RoPE要求維度必須是偶數(shù)。"
? ? ? ? self.dim = dim
? ? ? ? self.max_len = max_len
? ? ? ? self.base = base
? ? ? ? self._build_cache()
? ??def?_build_cache(self):
? ? ? ? half_dim = self.dim //?2
? ? ? ? inv_freq =?1.0?/ (self.base ** (torch.arange(0, half_dim).float() / half_dim)) ?# [dim/2]
? ? ? ? pos = torch.arange(self.max_len).float() ?# [max_len]
? ? ? ? sinusoid = torch.einsum('i,j->ij', pos, inv_freq) ?# [max_len, dim/2]
? ? ? ? self.register_buffer('sin', torch.sin(sinusoid)) ?# [max_len, dim/2]
? ? ? ? self.register_buffer('cos', torch.cos(sinusoid)) ?# [max_len, dim/2]
? ??def?forward(self, x):
? ? ? ??"""
? ? ? ? 輸入:
? ? ? ? ? ? x: Tensor, shape (batch, seq_len, dim)
? ? ? ? 輸出:
? ? ? ? ? ? Tensor, shape (batch, seq_len, dim),應(yīng)用RoPE后
? ? ? ? """
? ? ? ? batch_size, seq_len, dim = x.size()
? ? ? ? sin = self.sin[:seq_len].unsqueeze(0).to(x.device) ?# [1, seq_len, dim/2]
? ? ? ? cos = self.cos[:seq_len].unsqueeze(0).to(x.device)
? ? ? ? x1 = x[...,?0::2]
? ? ? ? x2 = x[...,?1::2]
? ? ? ? x_rotated = torch.cat([x1 * cos - x2 * sin, x1 * sin + x2 * cos], dim=-1)
? ? ? ??return?x_rotated
# ========================
# 主程序:嵌入 + RoPE 演示
# ========================
# 參數(shù)定義
vocab_size =?10000
embedding_dim =?256
embedding_layer = nn.Embedding(vocab_size, embedding_dim)
rope_encoder = RotaryPositionalEncoding(dim=embedding_dim, max_len=5000)
# 輸入 token ids(假設(shè)是一個樣本)
token_ids = torch.tensor([101,?102,?103,?104,?105,?106,?107]) ?# [seq_len]
embeddings = embedding_layer(token_ids).unsqueeze(0) ?# [1, seq_len, dim]
# 應(yīng)用 RoPE 位置編碼
rope_embeddings = rope_encoder(embeddings).squeeze(0) ?# [seq_len, dim]
# 打印結(jié)果
print("原始嵌入向量:")
print(embeddings.squeeze(0))
print("n應(yīng)用 RoPE 后的嵌入向量:")
print(rope_embeddings)
結(jié)尾語
在大模型的世界里,嵌入向量和位置編碼就像是兩把開啟理解語言奧秘的鑰匙:前者將離散的語言符號映射到連續(xù)的語義空間,后者則幫助模型理解“誰先誰后”、“誰靠誰近”。我們從嵌入矩陣的初始化講起,了解了這些向量是如何從隨機開始,逐步在訓(xùn)練中學(xué)會“懂語言”的;然后走進了位置編碼的演化史,從經(jīng)典的正弦余弦到如今主流的旋轉(zhuǎn)位置編碼(RoPE),我們看到了模型如何用巧妙的方式“感知順序”,并最終在注意力機制中扮演關(guān)鍵角色。
值得強調(diào)的是,RoPE 并不是一種加法編碼,而是一種乘法思維,它精準(zhǔn)地嵌入在自注意力中的 Query 和 Key 上,為模型引入位置的相對關(guān)系感。這種設(shè)計既數(shù)學(xué)優(yōu)雅,又計算高效,成為當(dāng)前大語言模型如 LLaMA、ChatGLM 的標(biāo)配。
理解這些底層機制,不僅有助于我們更好地使用大模型,更是在 AI 工程實踐中邁出的堅實一步,也是為我們親自訓(xùn)練一個基礎(chǔ)模型,必須打通的一道關(guān)卡。