練)
上一篇我們把一段文本切成了 token并且給每個 token 按詞表順序編了個號。那會兒我就在評論區(qū)看到一批人卡在同一步編號嘛整數(shù)喂給模型不就完事了但實際寫代碼的時候你會發(fā)現(xiàn)模型接受的根本不是整數(shù)本身而是先過一個叫 Embedding 的層出來后每個編號都成了一串浮點數(shù)——比如tensor([0.0245, -0.0193, 0.0832, ...])。這就很反直覺一個 3怎么突然就有意思了我第一次在調(diào)試大模型中間張量的時候也對著屏幕愣了半天。干過傳統(tǒng)編程的人都知道id 就是 id數(shù)據(jù)庫里主鍵是 3 不代表比 2 大、比 4 小??傻搅松疃葘W(xué)習(xí)里編號查完表之后拿出來的向量竟然能用來算蘋果和香蕉的距離甚至能算詞和詞的類比關(guān)系。這篇文章就把這件事徹底講透Embedding 到底做了一步什么操作編號為什么能變成詞向量以及這個詞向量在大模型里又是怎么被繼續(xù)改造的。1. 先打破一個直覺編號進(jìn) Embedding 層本質(zhì)是查表1.1 前向傳播一個矩陣行抽取操作先看代碼。PyTorch 里最典型的 Embedding 層長得這樣import torch import torch.nn as nn # 詞表大小 10000向量維度 64 emb nn.Embedding(num_embeddings10000, embedding_dim64) # 假設(shè)三個 token id ids torch.tensor([3, 17, 892]) vectors emb(ids) print(vectors.shape) # torch.Size([3, 64]) print(vectors)看到?jīng)]輸入是[3, 17, 892]這三個整數(shù)輸出的形狀是(3, 64)也就是每個編號各自拿到了一條 64 維的浮點向量。關(guān)鍵點來了這個操作的前向計算沒有任何語義理解它就是一次查表。你可以把它完全等價成下面這行vectors emb.weight[ids]emb.weight的形狀是(10000, 64)也就是一張有 10000 行的表每行是一個 64 維向量。ids里面的編號 3、17、892 就當(dāng)成行號從表里把對應(yīng)的行抽出來。所以嚴(yán)格講編號本身并不是變成了詞向量而是編號作為一個索引幫你從 Embedding 矩陣?yán)锇褜?yīng)的那一行向量取了出來。這個類比就像你去學(xué)校檔案室學(xué)生編號本身是 3 號、17 號這些數(shù)字沒有任何性格、成績、興趣信息。但管理員拿著編號去柜子里抽檔案拿到的那份檔案才真正記錄了這個人是什么樣。Embedding 矩陣就是那個檔案柜每個 token 編號背后掛著一份向量檔案。1.2 真正改變向量語義的不是查表是反向傳播如果只是查表那你可能馬上要問表里存的那些浮點數(shù)從哪來一開始是什么答案是一開始是隨機(jī)數(shù)。nn.Embedding(10000, 64)在你創(chuàng)建它的瞬間權(quán)重矩陣?yán)锏乃兄刀际菑囊粋€隨機(jī)分布里采出來的噪聲。你用隨機(jī)初始化的權(quán)重去查表查出來的向量之間沒有任何語義關(guān)系——你把蘋果和火星的編號拿去查查到的那兩行向量跟扔骰子沒有區(qū)別。那后面為什么有語義了因為訓(xùn)練過程會反復(fù)修改這張表里每一行的數(shù)值。前向傳播只是取反向傳播才是改。每訓(xùn)練一步模型根據(jù)損失函數(shù)算出梯度然后沿著梯度方向微調(diào)整個 Embedding 矩陣?yán)锏膮?shù)。經(jīng)過海量文本的反復(fù)更新蘋果那一行的數(shù)值會慢慢變得跟香蕉水果等詞的向量相近跟火星的向量相去甚遠(yuǎn)。這里有個初學(xué)者常犯的誤區(qū)以為 Embedding 層本身帶有什么 NLP 魔法。其實它就是一個普通的可學(xué)習(xí)參數(shù)矩陣跟全連接層的權(quán)重矩陣沒有本質(zhì)區(qū)別。它的語義完全是被訓(xùn)練任務(wù)逼出來的。2. 為什么不能直接拿編號當(dāng)數(shù)字獨熱編碼差在哪2.1 直接喂編號的致命問題既然編號是整數(shù)為什么不直接把3、17、892當(dāng)成數(shù)值特征喂給模型這樣連 Embedding 層都省了。問題出在數(shù)值的序和大小關(guān)系上。編號 3 和編號 4 在詞表里相鄰但它倆的語義可能毫無關(guān)系。比如一個按字母排序的詞表里3 號是 about4 號是 above這倆語義接近還說得過去可如果 892 號是 apple、893 號是 abandon 呢中間沒有任何幾何意義。你要是直接把編號作為數(shù)值特征模型會忍不住學(xué)到數(shù)值越大代表某種信號越強(qiáng)這種關(guān)系。這是神經(jīng)網(wǎng)絡(luò)的天性它看到數(shù)字 100 和 200就會天然假設(shè) 200 的強(qiáng)度是 100 的兩倍。但 token id 之間根本不存在這種線性關(guān)系。喂原始編號等于給模型喂了一堆有誤導(dǎo)性的數(shù)值信號訓(xùn)練會很難收斂。2.2 分布式表示從工牌號到性格量表那不用編號用**獨熱編碼one-hot encoding**呢這也是很多人第一個想到的辦法。獨熱編碼的做法是詞表 10000 個詞就用 10000 維的向量。每個詞對應(yīng)一個維度這個詞出現(xiàn)時那個維度置 1其余全是 0。比如貓是 3 號向量就是[0, 0, 1, 0, ..., 0]。這個方案有兩個硬傷。第一個硬傷是維度爆炸。詞表越大向量就越長。現(xiàn)實中的大模型詞表動輒幾萬甚至十幾萬 token每次都要操作這么稀疏的高維向量存儲和計算都扛不住。第二個硬傷更致命任意兩個不同的獨熱向量在空間里都是完全正交的余弦相似度永遠(yuǎn)是 0。貓和狗的向量沒有一絲相似之處貓和火星的向量也沒有模型完全沒有辦法從向量本身推斷出任何語義親疏關(guān)系。稠密 Embedding 用的則是另一套思路不搞一個詞一個位置的開關(guān)式表示而是用一個固定長度的稠密實數(shù)向量來表示每個詞每個維度上都有數(shù)值。你可以把每個維度理解為模型自己從數(shù)據(jù)里發(fā)現(xiàn)的某種抽象特征——雖然這個特征具體代表什么人很難說清但多個維度組合起來就能構(gòu)成一個詞的多維畫像。打個比方獨熱編碼像工牌號每個員工一個編號編號只回答你是誰分布式表示像一份性格量表每個員工在幾個維度上打分比如外向程度、細(xì)心程度。有了量表就算你沒見過這兩個人也能算出來他們在性格空間里靠得近不近。2.3 獨熱編碼和稠密向量的定量對比我把兩種表示擺一起對比你感受會更直接對比項獨熱編碼稠密 Embedding維度等于詞表大小比如 50000自定常見 128~1024稀疏度極度稀疏99.99% 是 0稠密處處非零詞間相似度任意兩詞相似度恒為 0余弦相似度可比較可學(xué)習(xí)性不可學(xué)習(xí)固定編碼端到端訓(xùn)練更新存儲成本詞表 5 萬就要 5 萬維5 萬 x 512 參數(shù)遠(yuǎn)小于前者說到底Embedding 就是用可學(xué)習(xí)的稠密低維實數(shù)向量替代不可學(xué)習(xí)的稀疏高維獨熱向量。它保留了每個詞的獨立身份又給了模型表達(dá)詞與詞之間關(guān)系的手段。這個思路在深度學(xué)習(xí)里叫分布式表示distributed representation——語義不再由單個維度獨立承擔(dān)而是分布在一組維度的組合模式上。3. 語義是訓(xùn)練打磨出來的梯度如何重塑向量空間3.1 語言學(xué)假設(shè)相似語境等于相似語義現(xiàn)在到了最核心的問題Embedding 矩陣?yán)锬且粋€個浮點數(shù)是怎么從隨機(jī)噪聲變成能承載語義的結(jié)構(gòu)的這背后有一個語言學(xué)層面的假設(shè)圈內(nèi)叫分布式假設(shè)distributional hypothesis一個詞的意思由它經(jīng)常出現(xiàn)的上下文決定。說得再直白點如果兩個詞經(jīng)常出現(xiàn)在相同的語境里那它們的語義大概率是相近的。蘋果和香蕉都會出現(xiàn)在我吃了一個__這個__很甜水果店里的__這樣的上下文里所以它們應(yīng)該是近義詞或同類詞。蘋果和火星幾乎不會共享上下文所以它們的距離應(yīng)該很遠(yuǎn)。那怎么把這種直覺變成一個可計算的訓(xùn)練目標(biāo)最經(jīng)典的思路就是 word2vec 里的兩個任務(wù)Skip-gram 和 CBOW。Skip-gram給你一個中心詞讓你預(yù)測它周圍會出現(xiàn)哪些詞。CBOW給你周圍的上下文詞讓你預(yù)測中間那個詞是誰。不管哪種本質(zhì)上都是讓模型學(xué)會一件事利用詞之間的共現(xiàn)關(guān)系預(yù)測上下文。3.2 訓(xùn)練任務(wù)如何把相鄰詞的向量拉近以 Skip-gram 為例。假設(shè)語料里有一句話蘋果 是 一種 非常 好吃 的 水果。我們?nèi)≈行脑~好吃窗口大小設(shè)為 2那么模型的任務(wù)就是看到好吃的向量去預(yù)測它周圍可能出現(xiàn)蘋果一種非常的這些詞的概率。這個預(yù)測過程不是直接拿好吃的向量去比而是把中心詞向量跟上下文詞的向量分別做點積點積結(jié)果經(jīng)過 softmax 變成概率。模型希望真實上下文詞的概率盡可能高于是損失函數(shù)會引導(dǎo)梯度更新——本質(zhì)上是在做一件事把中心詞的向量往真實上下文詞向量的方向上推同時把那些沒有出現(xiàn)的隨機(jī)詞向量往反方向推一點。所以如果一個詞經(jīng)常和另一堆詞共同出現(xiàn)它們的向量就會被反復(fù)互相推近。訓(xùn)練幾十萬步之后蘋果和香蕉因為上下文高度重疊向量空間里自然靠在一起蘋果和火星因為上下文毫無交集被推得越來越遠(yuǎn)。這里我不想堆公式給你講個直覺想象你在空曠操場上擺了 5 萬張卡片每張卡片是一個詞。有一個訓(xùn)練信號反復(fù)告訴你每次出現(xiàn)蘋果時周圍應(yīng)該站著甜、水果、樹、汁、紅這些卡片每次看到這堆卡片時中心位置大概率是蘋果。你不斷根據(jù)這類信號挪動卡片位置最終所有卡片會自動聚類成水果類聚一堆、動物類聚一堆、動詞類聚一堆的布局。你甚至不需要別人告訴你蘋果是水果僅憑共現(xiàn)統(tǒng)計規(guī)律這個幾何結(jié)構(gòu)自己就冒出來了。這非常神奇但背后的機(jī)制并不神秘Embedding 向量是訓(xùn)練信號在參數(shù)空間里留下的痕跡。3.3 大模型依然沿用了這套底層邏輯說到大模型很多人會問GPT 這類模型又不專門訓(xùn)練 embedding那它的詞向量靠譜嗎靠譜。因為大模型的訓(xùn)練任務(wù)是預(yù)測下一個 token這個任務(wù)的底層邏輯跟 Skip-gram 是互通的。每次模型看到前文、要預(yù)測下一個 token 時它都在隱式地學(xué)習(xí) token 之間的共現(xiàn)規(guī)律。所有 token 的 embedding 參數(shù)都參與了反向傳播一樣會被海量語料反復(fù)打磨。區(qū)別在于大模型的輸入 Embedding 只是第一層后面還有幾十層 Transformer 會繼續(xù)改寫這些向量。但作為起點的 Embedding 層本身早就在訓(xùn)練中形成了非常豐富的靜態(tài)語義結(jié)構(gòu)。你可以把它理解為大模型的 Embedding 層是一本靜態(tài)詞典Transformer 層則在這本詞典的基礎(chǔ)上根據(jù)當(dāng)前上下文再做動態(tài)調(diào)節(jié)。4. 大模型里的 Embedding 遠(yuǎn)比查表復(fù)雜從靜態(tài)向量到上下文表征4.1 編號對應(yīng)的是 token 而不是詞先把概念捋一下在大模型里你分配編號的對象嚴(yán)格來說不是詞而是 token。token 可能是完整的詞比如 apple可能是詞根比如 un可能是標(biāo)點甚至可能是單個字符。所以更準(zhǔn)確的說法是大模型學(xué)到的叫token embedding不是傳統(tǒng)意義上的 word embedding。這帶來一個很實際的影響一個單詞可能被拆成好幾個 token每個 token 單獨編號、單獨有向量。比如 unbelievable 可能被切成 un、believ、able。那它的語義就不存在單一向量里而是由這幾個 token 組合表達(dá)。這就解釋了為什么大模型的詞表里經(jīng)常能看到奇怪的 ##able 這種條目——那是子詞 token不是完整詞。4.2 位置編碼和 Transformer 改造編號過了 Embedding 層拿到初始向量之后接著還要做一件事加位置信息。如果沒有位置編碼蘋果 吃 了 和 吃了 蘋果 這兩句的輸入向量序列是完全一樣的模型無法區(qū)分蘋果在第一個位置還是第三個位置。所以 Transformer 架構(gòu)會給每個位置的向量注入位置信息常見的有絕對位置編碼、相對位置編碼或者是旋轉(zhuǎn)位置編碼RoPE這類更現(xiàn)代的做法。做完這些之后向量序列才進(jìn)入 Transformer 層。每一層里每個 token 的向量都會經(jīng)過注意力機(jī)制去看序列里其他 token 的向量然后更新自己的值。注意這里有本質(zhì)變化Embedding 層查出來的初始向量是固定的、與上下文無關(guān)的但經(jīng)過 Transformer 之后每個 token 的最終向量是隨著上下文動態(tài)變化的。NLP 界有個經(jīng)典例子word2vec 學(xué)到的 bank 只有一個向量不管是河岸還是銀行都用它但在 BERT 里bank 出現(xiàn)在 river bank 和 central bank 兩條句子里時最終向量是完全不同的。這就是靜態(tài) embedding 和上下文 embeddingcontextualized embedding的區(qū)別。4.3 想檢查理解程度該看哪一層如果你要排查大模型經(jīng)常會遇到一個困惑到底該把哪一層的輸出叫embedding我的經(jīng)驗是分場景要看基礎(chǔ)的詞義結(jié)構(gòu)看輸入 Embedding 層的輸出就夠了。它反映的是靜態(tài)語義。要看模型在具體語境下的理解得看最后一層或倒數(shù)幾層的輸出。那里面包含了對整個句子的上下文建模結(jié)果。很多向量檢索系統(tǒng)、RAG 應(yīng)用里說的把文本編碼成向量通常指的不是第一層 Embedding而是模型輸出的語義向量比如 CLS 位置或最后一層的平均池化。兩者差距很大別搞混。還有一個高維向量經(jīng)常被誤解的點768 維甚至 4096 維的向量并不是這個詞有 768 種意思每維對應(yīng)一種。真實情況是模型學(xué)出來的每個維度本身很難用人能理解的方式解釋語義是散在整體方向上的。這就像你沒辦法單獨指著紅酒果香維度的某一個物理量說它代表什么但兩個酒的多維輪廓一比較相似度就出來了。5. 自己動手三種方式驗證 Embedding 里藏了什么理論講再多不如自己上手看一眼。你不需要完整訓(xùn)練一個大模型隨便拿一個預(yù)訓(xùn)練好的 embedding 文件或者訓(xùn)練一個小模型就能做這三件事。5.1 降維可視化看詞聚類第一步把高維向量降維到二維平面肉眼觀察詞的分布。常用工具是 t-SNE 或 UMAP。import numpy as np from sklearn.manifold import TSNE import matplotlib.pyplot as plt # 假設(shè) vectors 是詞向量矩陣shape(N, dim) # 先抽一批高頻詞別全量跑 indices np.arange(0, 1000) embedding_subset vectors[indices] tsne TSNE(n_components2, random_state42, perplexity30) coords tsne.fit_transform(embedding_subset) for i, (x, y) in enumerate(coords): plt.scatter(x, y, s5) plt.annotate(vocab[indices[i]], (x, y), fontsize8) plt.show()跑完你會發(fā)現(xiàn)非常直觀數(shù)字類聚在一邊顏色類聚在一邊食物類聚在一邊。如果訓(xùn)練語料足夠大、質(zhì)量足夠好甚至可以觀察到男女性別詞沿著同一方向分開排列。這個幾何結(jié)構(gòu)就是語義相似度的可視化證明。實操細(xì)節(jié)t-SNE 全量跑幾萬詞會非常慢先隨機(jī)抽 1000~2000 個詞子集perplexity 一般取 15~50太小會顯得聚成一團(tuán)太大結(jié)構(gòu)又會被打散。5.2 余弦相似度查最近鄰可視化適合宏觀觀察最近鄰檢索則適合具體驗證某個詞的朋友圈。import numpy as np # vecs: (N, dim) 已歸一化 # target_idx: 要查的詞 query vecs[target_idx] sims vecs query # 如果向量都是 L2 歸一化的點積等價余弦相似度 top_indices np.argsort(sims)[::-1][1:11] # 排除自己 for idx in top_indices: print(vocab[idx], sims[idx])我拿一個小語料訓(xùn)練過的 embedding 做過實驗查 dog前幾位往往是 cat, pet, puppy, leash查 beautiful出現(xiàn)的是 gorgeous, lovely, pretty, wonderful。這種結(jié)果真的會讓人有種模型是不是懂點事的錯覺。補(bǔ)充一個經(jīng)驗小語料幾 MB 文本訓(xùn)練出來的 embedding 一般不太干凈最近鄰里會混入很多噪聲。如果你看到 beautiful 的最近鄰是 page 這種完全不搭邊的詞別懷疑代碼就是訓(xùn)練數(shù)據(jù)太少、共現(xiàn)統(tǒng)計不夠。5.3 詞類比運算測試這是 word2vec 時期最有名的玩法vector(king) - vector(man) vector(woman) ≈ vector(queen)。原理不復(fù)雜因為訓(xùn)練好的向量空間里國王和王后的差異方向跟男人和女人的差異方向基本一致所以做一次向量加減法得到的結(jié)果應(yīng)該落在queen附近。result_vec vectors[king_idx] - vectors[man_idx] vectors[woman_idx] # 然后拿 result_vec 和全詞表做余弦相似度檢索 # 如果一切正常排名第一的應(yīng)該是 queen在大模型那種超大詞表里這個測試有時候會失效因為 token 切分和上下文建模會改變很多細(xì)節(jié)。但在中小規(guī)模 embedding 上進(jìn)行這個檢驗仍然是判斷語義質(zhì)量的好方法。如果連這類簡單的類比結(jié)構(gòu)都沒學(xué)出來你基本可以斷定語料量不夠或者訓(xùn)練不充分。6. 實操中容易踩的六個 Embedding 坑最后說說我在實際項目里積累的一些教訓(xùn)。這些坑教科書上很少寫但幾乎每個人都會踩一遍??右籩mbedding_dim 寧小勿大。向量維度不是越大越好。維度越高Embedding 層的參數(shù)量就越大詞表大小 × 維度在小數(shù)據(jù)集上特別容易過擬合。通用經(jīng)驗中小語料用 128~256大規(guī)模預(yù)訓(xùn)練模型用 768 起步。你要是拿 20 萬條數(shù)據(jù)去訓(xùn)一個維度 4096 的 embedding大概率訓(xùn)出一堆噪聲。坑二不固定隨機(jī)種子復(fù)現(xiàn)不出來。由于 Embedding 矩陣是隨機(jī)初始化的哪怕你數(shù)據(jù)、代碼完全一樣不設(shè)隨機(jī)種子跑兩次最終向量也有差異。調(diào)參階段務(wù)必設(shè)置random.seed、np.random.seed、torch.manual_seed有 GPU 還要額外設(shè)置torch.cuda.manual_seed_all。我見過有人兩天前跑出一個很好的結(jié)果隔天換了臺機(jī)器復(fù)現(xiàn)不出來最后發(fā)現(xiàn)是 embedding 初始化不同造成的。坑三微調(diào)時凍結(jié)還是更新 Embedding要想清楚。如果你用預(yù)訓(xùn)練 embedding 做下游任務(wù)、數(shù)據(jù)量又少建議凍結(jié) Embedding 層省顯存、防過擬合。如果任務(wù)領(lǐng)域跟預(yù)訓(xùn)練語料差異極大比如預(yù)訓(xùn)練語料是通用文本下游是一堆專業(yè)術(shù)語可以考慮解凍并微調(diào)讓模型吸收領(lǐng)域信息。折中方案用較低的學(xué)習(xí)率微調(diào) Embedding。坑四計算相似度前先歸一化。高維向量用歐氏距離容易受向量模長影響而模型學(xué)習(xí)到的模長往往跟詞頻有關(guān)系——高頻詞模長偏大低頻詞偏小。比較語義相似度優(yōu)先用余弦相似度計算前先對向量做 L2 歸一化。做向量檢索系統(tǒng)時同樣建議統(tǒng)一歸一化否則召回質(zhì)量波動很大。坑五OOV詞表外詞的處理。模型訓(xùn)練時詞表是封閉的推理時碰到完全沒見過的新詞分詞器會切成更小的 token 或子詞盡量繞開完全無向量可用的局面。如果你在維護(hù)自己的詞表遇到真正的新 token常見做法是隨機(jī)初始化一個向量或者取已有近義詞向量的平均值做初始化。直接全零初始化要謹(jǐn)慎零向量在空間里會變成所有詞的公共鄰居檢索時容易出奇怪結(jié)果??恿斎?embedding 和輸出層權(quán)重共享的問題。不少 Transformer 架構(gòu)里輸入 Embedding 矩陣和最后的輸出 softmax 投影是同一個權(quán)重矩陣這叫 weight tying。好處是省參數(shù)量、訓(xùn)練更穩(wěn)。但如果你手動微調(diào)模型改了輸入 Embedding 的權(quán)重輸出投影會同步變化反過來也一樣。調(diào)試時別以為它倆是兩個獨立副本否則會白排查很久。寫到這里Embedding 從編號到詞向量的最后一層窗戶紙也應(yīng)該捅破了。它的本質(zhì)就三句話前向是一次查表反向是不斷改寫表格內(nèi)容表格里的每一行最終被訓(xùn)練成了語義空間里的一個坐標(biāo)點。從我個人的實操體驗來說做模型調(diào)試時間長了你會發(fā)現(xiàn)幾乎所有跟語義沾邊的問題——任務(wù)遷移、檢索排序、模型泛化——追根溯源都會落到向量空間的結(jié)構(gòu)上。所以這一步基礎(chǔ)打牢后面玩大模型會順很多。