1.SVD簡介
SVD(Singular Value Decomposition),奇異值分解,也就是將一個矩陣進行分解,然後從分解後的矩陣上對數據進行分析。矩陣分解可以將原始矩陣表示成新的易於處理的形式,這種新的形式是兩個或者多個矩陣的乘積,如下形式:
如式1,將原始矩陣分解爲三個矩陣相乘的形式,其中中間的Σ只有對角元素。其他元素都爲0,並且對角元素是從大到小排列的,這些元素成爲奇異值,也就是原始數據矩陣的奇異值。我們可以利用這些奇異值對數據進行降維,從而用更小的數據集來表示原始數據,並且這些降維後的數據往往保留了80%~90%的原始信息,也就相當於用較小的數據量包含的大部分的信息,從而去掉了那些冗餘和噪聲信息。
2.奇異值與特徵值
特徵值分解和奇異值分解,兩者有着緊密的聯繫,其目的都是一樣的都是提取一個矩陣的最重要的特徵。
先說特徵值分解。特徵值分解我在另一篇文章《PCA算法來簡化數據》中有簡單的介紹,但是沒有提到分解的概念,只提到了降維的概念,其實目的都是一樣的:
如式2所示,我們將A分解爲三個矩陣的乘積的形式,其中Q是A對應的特徵向量組成的矩陣,Σ是對應的特徵值組成的對角矩陣。我們在對其進行特徵值分解的時候,都需要求解方陣A的特徵值和特徵向量,然後對特徵值以及對應的特徵向量進行排序,然後取出前幾個佔比比較大的維度(方向)對原始矩陣A進行降維,也就是提取這個矩陣最重要的特徵(此特徵非A中對應的特徵值的特徵,只是表示矩陣A變化的特徵,具體參考《PCA算法來簡化數據》)。
特徵值降維可以大大的降低數據的維度,但是它也有自己的侷限性,比如說它要求矩陣A必須是方陣。
下面說奇異值分解。我們看公式1,我們將原始矩陣分成了三個矩陣相乘的形式,但是此時的A不是方陣,所以說奇異值分解適用於任何矩陣。其中U是一個m×m的正交矩陣,裏面的向量成爲左奇異向量,V是一個n×n的正交矩陣,裏面的向量稱爲有奇異向量。但是這兩個正交矩陣是怎麼來的呢?
我們用矩陣A乘以矩陣A的轉置,得到一個m×m的方陣,然後對這個方陣進行特徵值分解:
這就又回到了特徵值和特徵向量的概念,λ是特徵值,u是特徵向量,也就是我們說的左奇異向量,然後所有的特徵向量組成了m×m的正交矩陣。
同理們用矩陣A的轉置乘以矩陣A,然後就得到了一個n×n的方陣,然後進行特徵值分解就得到了一個n×n的正交矩陣V,裏面的向量就是右奇異向量。
另外我們也可以得出下列公式:
其中σ就是奇異值,u是左奇異向量,v是右奇異向量。奇異值σ和特徵值類似,也是在Σ矩陣中從大到小的對角矩陣,而且σ的減少特別的快,在很多情況下,前10%甚至1%的奇異值的和就佔了全部的奇異值之和的99%以上了。也就是說,我們也可以用前r大的奇異值來近似描述矩陣,這裏定義一下部分奇異值分解:
我們將式1矩陣分解中的右側部分的n變成了r,這就是降維了,降維的過程和特徵值特徵向量的降維類似,我們這裏取的是奇異值的前r個,然後可能包含了大部分的信息,所以就只保留了前r個特徵來表示矩陣A,近似等於原矩陣。由於分解後的三個矩陣維度相對較小,所以我們就可以用保存更小的三個矩陣(U,Σ,V)近似替代原矩陣即可。如圖1所示:
圖1
3.SVD的應用
我們前面介紹了,SVD對數據進行降維就是先對矩陣進行矩陣分解,然後取主要的奇異值對矩陣進行降維,一是減少了數據量,並且還去除了噪聲和冗餘數據。其主要應用可以用於推薦系統和隱性語義檢索(LSI)。
我們先介紹這個推薦系統的,後面還會通過python代碼進行一個例子的說明。
推薦系統需要計算特徵值之間的相似度,相似度的計算主要有三種方式:
- 計算屬性之間的歐氏距離,即兩個點之間的距離公式。我們用“相似度=1/(1+距離)”這樣的方式垃圾算相似度。當距離爲0的時候,相似度爲1.0,當相似度非常大的時候,相似度也就趨近於0。
- 第二個是皮爾遜相關係數。皮爾遜相關係數的取值範圍從-1到+1,我們通過0.5+0.5*corrcoef()這個函數計算,並且將其取值範圍規劃到0到1之間。
- 餘弦相似度。其計算的是兩個向量夾角的餘弦值。如果夾角度數爲90度,則相似度爲0,如果兩個向量的方向相同,則相似度爲1.0。同皮爾遜相關係數一樣,餘弦相似度的取值範圍也在-1到+1之間,因此我們也將它歸一化到0和1之間。我們將兩個向量A和B的餘弦相似度的定義如下:
下面我們通過代碼來看相似度的計算
def ecludSim(inA,inB):
return 1.0/(1.0 + la.norm(inA - inB))
def pearsSim(inA,inB):
if len(inA) < 3 : return 1.0
return 0.5+0.5*corrcoef(inA, inB, rowvar = 0)[0][1]
def cosSim(inA,inB):
num = float(inA.T*inB)
denom = la.norm(inA)*la.norm(inB)
return 0.5+0.5*(num/denom)
函數ecludSim是通過歐式距離來計算相似度,pearsSim是通過皮爾遜相關係數計算相似度,cosSim是通過餘弦相似度來計算相似度。這三個函數都是計算兩個特徵之間的相似度,也就是說輸入參數都是兩個向量。
下面我們通過一個餐館纔要推薦的例子,來看一下推薦。
推薦系統的工作過程:給定一個用戶,系統會爲此用戶返回N個最好的推薦菜。爲了實現這一點,則需要我們做到:
- 尋找用戶沒有評級的菜餚,即在用戶和物品的矩陣中的0值。數據集如下:
[4, 4, 0, 2, 2],
[4, 0, 0, 3, 3],
[4, 0, 0, 1, 1],
[1, 1, 1, 0, 0],
[2, 2, 2, 0, 0],
[5, 5, 5, 0, 0],
[1, 1, 1, 0, 0]
每一行是每個用戶對每個菜品的評分。
- 用戶沒有經濟的所有物品中,對每個物品預計一個可能的評分數。這就是說,我們認爲用戶可能會對物品的打分(這就是相似度計算的初衷)。
- 對這些物品的評分從高到低,返回前N個物品。
下面通過代碼來分析這個過程:
'''
dataMat:數據矩陣
user:用戶編號
simMeas計算相似度的方法,就如上面的三種方法
item:商品編號
'''
def standEst(dataMat, user, simMeas, item):
n = shape(dataMat)[1] #數據的商品的個數
simTotal = 0.0; ratSimTotal = 0.0
for j in range(n):
userRating = dataMat[user,j] #用戶對第j個商品的評分
if userRating == 0: continue #如果評分爲0,說明沒有評分,跳過此次循環
overLap = nonzero(logical_and(dataMat[:,item].A>0, \
dataMat[:,j].A>0))[0] #挑出所有對商品item和j都有評價的用戶
if len(overLap) == 0: similarity = 0
else: similarity = simMeas(dataMat[overLap,item], \
dataMat[overLap,j]) #計算商品item和j的相似度
print 'the %d and %d similarity is: %f' % (item, j, similarity)
simTotal += similarity #商品的相似度之和
ratSimTotal += similarity * userRating #相似度乘以評分然後求和
if simTotal == 0: return 0
else: return ratSimTotal/simTotal
上面的函數是沒有進行降維的計算相似度計算的方法,下面是入口函數:
'''
dataMat:數據
user:用戶編號
N:返回的前N個推薦個數,默認爲3
simMeas:相似度計算方法,默認預先計算方法
estMethod:計算推薦度的方法
'''
def recommend(dataMat, user, N=3, simMeas=cosSim, estMethod=standEst):
unratedItems = nonzero(dataMat[user,:].A==0)[1]#查找用戶沒有評價過的商品
if len(unratedItems) == 0: return 'you rated everything'
itemScores = []
for item in unratedItems: #循環用戶未評價過的商品
estimatedScore = estMethod(dataMat, user, simMeas, item) #計算用戶未評價過的商品和其他評級過的商品之間的相似度信息
itemScores.append((item, estimatedScore))
return sorted(itemScores, key=lambda jj: jj[1], reverse=True)[:N] #對相似度信息進行排序,並返回前N個推薦值
總體來說這個方法計算某個用戶所有未評價過的商品和其他商品的相似度,然後返回相似度排名,取前N個商品推薦。函數recommend中計算推薦度相關的函數用的是standEst,這個函數名是可以作爲estMethod參數傳過去的。下面給出SVD降維的推薦方法,用的函數如下:
def svdEst(dataMat, user, simMeas, item):
n = shape(dataMat)[1]
simTotal = 0.0; ratSimTotal = 0.0
U,Sigma,VT = la.svd(dataMat) #分解矩陣 U:左奇異矩陣,Sigma:奇異值矩陣,VT:右奇異矩陣的轉置
Sig4 = mat(eye(4)*Sigma[:4]) # 我感覺這裏應該是取了前4個奇異值
xformedItems = dataMat.T * U[:,:4] * Sig4.I #對數據進行降維
for j in range(n): #這個循環就是計算商品item和每個其他評價過的商品相似度,但請注意這裏用到的額是降維後的數據
userRating = dataMat[user,j]
if userRating == 0 or j==item: continue
similarity = simMeas(xformedItems[item,:].T,\
xformedItems[j,:].T)
print 'the %d and %d similarity is: %f' % (item, j, similarity)
simTotal += similarity
ratSimTotal += similarity * userRating
if simTotal == 0: return 0
else: return ratSimTotal/simTotal
其這裏用到的矩陣的數據如下:
[0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 5],
[0, 0, 0, 3, 0, 4, 0, 0, 0, 0, 3],
[0, 0, 0, 0, 4, 0, 0, 1, 0, 4, 0],
[3, 3, 4, 0, 0, 0, 0, 2, 2, 0, 0],
[5, 4, 5, 0, 0, 0, 0, 5, 5, 0, 0],
[0, 0, 0, 0, 5, 0, 1, 0, 0, 5, 0],
[4, 3, 4, 0, 0, 0, 0, 5, 5, 0, 1],
[0, 0, 0, 4, 0, 4, 0, 0, 0, 0, 4],
[0, 0, 0, 2, 0, 2, 5, 0, 0, 1, 2],
[0, 0, 0, 0, 5, 0, 0, 0, 0, 4, 0],
[1, 0, 0, 0, 0, 0, 0, 1, 2, 0, 0]
每一行還是用戶對每一件商品的評價分數。我們給出svdEst函數中降維後的xformedItems數據形式可能,可能幫助理解:
[[-0.45137416 0.03084799 -0.00290108 0.01189185]
[-0.36239706 0.02584428 -0.00189127 0.01348796]
[-0.46879252 0.03296133 -0.00281253 0.01656192]
[-0.01007685 -0.34024331 -0.22728592 0.14546051]
[-0.01567036 -0.38750193 0.61197998 -0.17137451]
[-0.01664563 -0.52000097 -0.3608907 -0.14984063]
[-0.00474684 -0.18887149 -0.00924222 0.94228361]
[-0.46712774 0.00389831 0.03349951 -0.02080674]
[-0.47223188 0.02853952 -0.00504059 0.00160266]
[-0.01591788 -0.39205093 0.55707516 0.04356321]
[-0.0552444 -0.52034959 -0.36330956 -0.19023805]]
從代碼中來看,我們是取的每一行的數據計算兩行數據之間的相似度,所以降維後,屬性值並沒有減少,只是每個商品取得樣本數少了(個人理解)。
下面我們來看隱性語義檢索。這部分內容,我直接引用網上別人的一個例子,文章後面會給出相應的文章參考地址:
奇異值與潛在語義索引LSI:
潛在語義索引(Latent Semantic Indexing)與PCA不太一樣,至少不是實現了SVD就可以直接用的,不過LSI也是一個嚴重依賴於SVD的算法,之前吳軍老師在矩陣計算與文本處理中的分類問題中談到:
“三個矩陣有非常清楚的物理含義。第一個矩陣X中的每一行表示意思相關的一類詞,其中的每個非零元素表示這類詞中每個詞的重要性(或者說相關性),數值越大越相關。最後一個矩陣Y中的每一列表示同一主題一類文章,其中每個元素表示這類文章中每篇文章的相關性。中間的矩陣則表示類詞和文章雷之間的相關性。因此,我們只要對關聯矩陣A進行一次奇異值分解,w 我們就可以同時完成了近義詞分類和文章的分類。(同時得到每類文章和每類詞的相關性)。”
上面這段話可能不太容易理解,不過這就是LSI的精髓內容,我下面舉一個例子來說明一下,下面的例子來自LSA tutorial,具體的網址我將在最後的引用中給出:
這就是一個矩陣,不過不太一樣的是,這裏的一行表示一個詞在哪些title中出現了(一行就是之前說的一維feature),一列表示一個title中有哪些詞,(這個矩陣其實是我們之前說的那種一行是一個sample的形式的一種轉置,這個會使得我們的左右奇異向量的意義產生變化,但是不會影響我們計算的過程)。比如說T1這個title中就有guide、investing、market、stock四個詞,各出現了一次,我們將這個矩陣進行SVD,得到下面的矩陣:
左奇異向量表示詞的一些特性,右奇異向量表示文檔的一些特性,中間的奇異值矩陣表示左奇異向量的一行與右奇異向量的一列的重要程序,數字越大越重要。
繼續看這個矩陣還可以發現一些有意思的東西,首先,左奇異向量的第一列表示每一個詞的出現頻繁程度,雖然不是線性的,但是可以認爲是一個大概的描述,比如book是0.15對應文檔中出現的2次,investing是0.74對應了文檔中出現了9次,rich是0.36對應文檔中出現了3次;
其次,右奇異向量中一的第一行表示每一篇文檔中的出現詞的個數的近似,比如說,T6是0.49,出現了5個詞,T2是0.22,出現了2個詞。
然後我們反過頭來看,我們可以將左奇異向量和右奇異向量都取後2維(之前是3維的矩陣),投影到一個平面上,可以得到:
在圖上,每一個紅色的點,都表示一個詞,每一個藍色的點,都表示一篇文檔,這樣我們可以對這些詞和文檔進行聚類,比如說stock 和 market可以放在一類,因爲他們老是出現在一起,real和estate可以放在一類,dads,guide這種詞就看起來有點孤立了,我們就不對他們進行合併了。按這樣聚類出現的效果,可以提取文檔集合中的近義詞,這樣當用戶檢索文檔的時候,是用語義級別(近義詞集合)去檢索了,而不是之前的詞的級別。這樣一減少我們的檢索、存儲量,因爲這樣壓縮的文檔集合和PCA是異曲同工的,二可以提高我們的用戶體驗,用戶輸入一個詞,我們可以在這個詞的近義詞的集合中去找,這是傳統的索引無法做到的。
參考:
https://mp.weixin.qq.com/s/Dv51K8JETakIKe5dPBAPVg
隱性語義索引、部分圖及內容:
https://www.cnblogs.com/LeftNotEasy/archive/2011/01/19/svd-and-applications.html
代碼:
《機器學習實戰》第14章