無監(jiān)督語義匹配之BERT-Whitening
大家好,我是DASOU;
因為業(yè)務(wù)場景常常用到無監(jiān)督語義匹配,所以一直在關(guān)注這方面的進展;
現(xiàn)在大家都比較熟知的就是:BERT-Whitening和SimCSE;
之前梳理了一下BERT-Whitening的理論和代碼,分享給大家,希望有幫助;
文章大體脈絡(luò)如下:
BERT-Whitening 公式推導(dǎo)+注解 PCA和SVD簡單梳理 協(xié)方差矩陣的幾何意義 對BERT-Whitening 代碼的簡單梳理
1. BERT-Whitening 解讀
BERT的輸出向量在計算無監(jiān)督相似度的時候效果很差是一個共識了,具體原因這里不多說,去看我之前這個文章;
然后一個改進措施就是想要把BERT的輸出向量變成高斯分布,也就是讓輸出向量滿足各向同性;
什么是各向同性呢?就是向量矩陣的協(xié)方差矩陣是一個單位矩陣乘以一個常數(shù),換句話說在每個向量維度上方差是一樣的;
現(xiàn)在大家比較熟知的是兩種方式:
一個是bert-flow模型,采用了基于流的生成模型來做這個數(shù)據(jù)分布的轉(zhuǎn)變;
第二個是bert-whitening。
這個文章重點聊一下BERT的白化,也就是第二種。
它做的事情就是直接將bert的輸出向量矩陣變成均值為0,協(xié)方差矩陣為單位矩陣;
補充兩個知識點,方便后續(xù)大家理解;
第一個是,協(xié)方差矩陣是單位矩陣,說明數(shù)據(jù)分布是在一個二維的圓上,三維的球上。
第二個是對于取值確定的矩陣A,經(jīng)過W=AX變換后,協(xié)方差矩陣將變換為
公式推導(dǎo)如下:
我們原始的向量矩陣是,變化之后的矩陣是;
我們執(zhí)行的變化是:
上面這個操作,是我們想讓的均值為0,協(xié)方差矩陣為單位陣;
我們知道:
那么就可以推導(dǎo)出:
進而可以推導(dǎo)出:
對做SVD奇異值分解,有:
因為是一個實對稱矩陣,有:
也就是有:
求解W就好了:
2. 簡單梳理PCA和SVD
先總體說一下我的覺得最重要的一個知識點:
SVD是直接對原始矩陣進行奇異值分解,得到左奇異向量,奇異值和右奇異向量;
PCA是對矩陣的協(xié)方差矩陣進行特征分解,得到對應(yīng)的特征向量和特征值,其中特征向量和SVD中的右奇異值是一個東西(如果我沒記錯的話~~);
2.1 特征分解
先說一下特征值分解:
一個方陣A,一般來說可以被對角化為如下式子:
X是A特征向量構(gòu)造的矩陣, 是一個對角陣,也就是只有對角線上有值,同時這個值是A的特征值;
如果說這個A除了是方陣,還是一個對稱陣,那么式子中的X就變成了正交矩陣,我們使用M來表示,可以對角化如下式子:
有兩個變化,一個是變成了正交矩陣,一個是最后面的是轉(zhuǎn)置矩陣符號,而不是逆矩陣符號;
2.2 PCA
PCA分為兩個步驟:
第一個步驟是找到一組新的正交基去表示數(shù)據(jù),比如我原來是使用n個正交基來表示數(shù)據(jù)中的向量,我現(xiàn)在找到另外的新的n個正交基來重新表示向量;這n個新的正交基是怎么找到呢?一個比較形象的表述是第一個新坐標軸選擇是原始數(shù)據(jù)中方差最大的方向,第二個新坐標軸選取是與第一個坐標軸正交的平面中使得方差最大的,第三個軸是與第1,2個軸正交的平面中方差最大的。依次類推,可以得到n個這樣的坐標軸。
第二個步驟經(jīng)過上面這個過程,我們會發(fā)現(xiàn),越到后面的基,方差越小,幾乎接近于0,也就是說這些后面的基沒啥作用。于是,我們可以忽略余下的坐標軸,只保留前面k個含有絕大部分方差的坐標軸。這個步驟就是在降維;
在這里想要說一個細節(jié)點,就是經(jīng)過第一個步驟之后,并不進行第二個步驟,從公式角度就是,而不是;那么得到的的協(xié)方差矩陣是一個對角化矩陣,以三維為例子,在空間上的分布是一個橢圓球體;
這個時候如果協(xié)方差矩陣想要變成單位矩陣,就是對向量矩陣做一個標準化就可以了;
現(xiàn)在有一個問題,上面我們是形象化的描述如何找到這些基,那么從實際出發(fā),如果找到呢?
我們是這么做的:通過計算數(shù)據(jù)矩陣的協(xié)方差矩陣,然后得到協(xié)方差矩陣的特征值特征向量,選擇特征值最大(即方差最大)的k個特征所對應(yīng)的特征向量組成的矩陣。這樣就可以將數(shù)據(jù)矩陣轉(zhuǎn)換到新的空間當(dāng)中,實現(xiàn)數(shù)據(jù)特征的降維。
在這里,需要注意的特征值最大,代表的就是在這個特征值對應(yīng)的特征向量方向方差最大;
PCA大體流程:

我自己簡單的總結(jié)就是,首先對數(shù)據(jù)進行中心化,然后計算協(xié)方差矩陣,然后計算對應(yīng)的特征值和特征向量等等;
需要注意的是,第一個步驟之后,如果我們不想去降低維度,那么這個全部的特征向量也可以使用,簡單說就是; 這個操作就是對原始數(shù)據(jù)做了一個旋轉(zhuǎn)變化,協(xié)方差矩陣會變成對角矩陣;
2.3 SVD分解:
奇異值分解是一個能適用于任意矩陣的一種分解的方法,對于任意矩陣A總是存在一個奇異值分解:
假設(shè)A是一個的矩陣,那么得到的U是一個的方陣,U里面的正交向量被稱為左奇異向量。Σ是一個的矩陣,Σ除了對角線其它元素都為0,對角線上的元素稱為奇異值。
是v的轉(zhuǎn)置矩陣,是一個n*n的矩陣,它里面的正交向量被稱為右奇異值向量。而且一般來講,我們會將Σ上的值按從大到小的順序排列。
在這里有幾個點想要強調(diào)一下:U這里對應(yīng)的是 對應(yīng)的特征向量;
V這里對應(yīng)的是對應(yīng)的特征向量,也就是A矩陣的協(xié)方差矩陣對應(yīng)的特征向量;這一點比較重要,我們在使用PCA降低維度的時候,想要拿到的那個變化矩陣就是這個V(注解,挑選前K個),也就是變化之后為;
通過,我們也可以得到這樣一個結(jié)果
所以在降低維度的時候我們這兩種都可以;
2.4 協(xié)方差矩陣的幾何意義:
協(xié)方差矩陣是一個單位矩陣,數(shù)據(jù)是分布在一個圓上;
協(xié)方差矩陣是一個對角化矩陣,我們可以將原始數(shù)據(jù)標準化,這樣對應(yīng)的數(shù)據(jù)的協(xié)方差矩陣就會變成單位矩陣,還可以對原始數(shù)據(jù)進行平移,移動到原點附近的圓上;
如果協(xié)方差矩陣是一個普通的矩陣,我們可以做PCA【不降低維度的那種】,將其轉(zhuǎn)化為對角化矩陣,之后做標準化,這樣協(xié)方差矩陣變成了單位矩陣,然后對數(shù)據(jù)做平移,移動到原點附近;
3. 梳理BERT白化代碼:
有兩個版本的代碼,
一個是蘇劍林的Keras版本:https://github.com/bojone/BERT-whitening
一個是Pytorch版本:https://github.com/autoliuweijie/BERT-whitening-pytorch
我看了一遍Pytorch版本,主要的細節(jié)點我羅列在下面;
首先就是下載數(shù)據(jù)和下載一些英文預(yù)訓(xùn)練模型。
之后就是跑代碼,分為三種方向:
第一種就是不使用白化的方式,直接在任務(wù)中使用BERT的輸出向量;
第二種是在任務(wù)中數(shù)據(jù)中使用白化方式,也就是在任務(wù)數(shù)據(jù)中計算kernel和bias,然后在任務(wù)數(shù)據(jù)中使用此參數(shù),
去對BERT系列預(yù)訓(xùn)練模型的輸出向量做轉(zhuǎn)化;
第三種是在大數(shù)據(jù)中,在這里也就是NLI數(shù)據(jù),計算相應(yīng)的kernel和bias,然后在任務(wù)數(shù)據(jù)中使用這個參數(shù),去做對應(yīng)的轉(zhuǎn)化;
第三種方式很方便,如果實際工作真的使用bert白化,肯定是我在訓(xùn)練數(shù)據(jù)中計算出來參數(shù),然后在測試數(shù)據(jù)中使用這個參數(shù)直接去做轉(zhuǎn)化,這樣效率最高。
把測試數(shù)據(jù)補充進來然后再重新計算對應(yīng)的參數(shù),感覺總是多了一個步驟,效率不高;
這就要求我們在大數(shù)據(jù)中計算參數(shù)的時候,確保大數(shù)據(jù)具有普適性,能夠很好的適配任務(wù)數(shù)據(jù);這樣計算出來的參數(shù)才有使用的可能;
在實際運行代碼的時候,我只是使用了SICKRelatednessCosin這個任務(wù),整體代碼寫的相當(dāng)?shù)牟诲e,大致的過一遍也就可以了;
最核心的代碼是這個:
def compute_kernel_bias(vecs, n_components):
"""計算kernel和bias
最后的變換:y = (x + bias).dot(kernel)
"""
vecs = np.concatenate(vecs, axis=0)
mu = vecs.mean(axis=0, keepdims=True)
cov = np.cov(vecs.T)
u, s, vh = np.linalg.svd(cov)
W = np.dot(u, np.diag(s**0.5))
W = np.linalg.inv(W.T)
W = W[:, :n_components]
return W, -mu
def transform_and_normalize(vecs, kernel, bias):
"""應(yīng)用變換,然后標準化
"""
if not (kernel is None or bias is None):
vecs = (vecs + bias).dot(kernel)
return vecs / (vecs**2).sum(axis=1, keepdims=True)**0.5