受限玻爾茲曼機(restricted Boltzmann machine, RBM)是一種可經過輸入數據集學習機率分佈的隨機生成神經網絡,在降維、分類、協同過濾、特徵學習和主題建模等領域中有着普遍應用。python
在Netflix Prize後半程,有選手將RBM應用在該預測電影評分問題上並取得了不錯的效果。後來Edwin Chen的文章《Introduction to Restricted Boltzmann Machines》使用詳細而易懂的方式(沒什麼數學公式與推導)描述了RBM的運做機理,並使用Python的numpy寫了一個簡易實現。git
這篇文章經過逐行閱讀並運行Edwin Chen的開源代碼,觀看其中用到的數據結構、值的變化來展示RBM的運做原理及實現技巧。github
這裏仍然把背景放到音樂這裏來,使用六首歌曲來訓練RBM,其中三首爲Disco歌曲:ABBA的Dancing Queen,Bee Gees的Stayin' Alive,新褲子的別再問我什麼是迪斯科,另外三首是吉他英雄的solo:Dire Straits的Sultans Of Swing,Yngwie Malmsteen的Black Star和桶哥Buckethead的Thorne Room。網絡
做爲一個神經網絡,RBM有可見層和隱藏層兩層,其中可見層每一個節點對應一首歌曲,而隱藏層的每一個節點咱們則但願它對應於一種音樂類型,故對應上述歌曲的特色在說明中使隱藏層爲兩個節點。同時再加一個bias unit來控制太過熱門的item對該模型形成的影響,此神經網絡各節點的鏈接狀況是這樣的:數據結構
可見層的每一個節點與全部隱藏層的節點相連,bias unit與兩層全部的節點相連,每一個鏈接對應一個weight,首先使用矩陣來表示可見層與隱藏層節點的全部weight,好比該矩陣的第一行的第一列的數據對應於可見層節點Dancing Queen與hidden unit1相連的weight,而後在此基礎上,插入bias unit的weight,由於它與可見層、隱藏層皆相連,故此矩陣的行與列各加1。給兩層節點之間的全部weight賦予一個範圍內的隨機值以初始化該矩陣,與bias unit相連的節點暫全置爲0。dom
import numpy as np
num_hidden = 2
num_visible = 6
np_rng = np.random.RandomState(1234)
weights = np.asarray(np_rng.uniform(
low=-0.1 * np.sqrt(6. / (num_hidden + num_visible)),
high=0.1 * np.sqrt(6. / (num_hidden + num_visible)),
size=(num_visible, num_hidden)))
weights
複製代碼
array([[-0.0534304 , 0.02114986],
[-0.01078587, 0.04942556],
[ 0.04849323, -0.03938812],
[-0.03871753, 0.05228579],
[ 0.07935206, 0.06511344],
[-0.02462677, 0.00017236]])
複製代碼
# 加入bias unit.
weights = np.insert(weights, 0, 0, axis=0)
weights = np.insert(weights, 0, 0, axis=1)
weights
複製代碼
array([[ 0. , 0. , 0. ],
[ 0. , -0.0534304 , 0.02114986],
[ 0. , -0.01078587, 0.04942556],
[ 0. , 0.04849323, -0.03938812],
[ 0. , -0.03871753, 0.05228579],
[ 0. , 0.07935206, 0.06511344],
[ 0. , -0.02462677, 0.00017236]])
複製代碼
接下來構造一些訓練樣本,一個樣本是關於這六首歌的收聽狀況的list(按照上圖從上到下的順序對應list中的index),填值1表示該user聽過此歌,填值0表示該user未聽過此歌。這裏構造6個樣本做爲樣本集合,同時考慮到weight矩陣增長了bias unit,爲了後續線性代數運算的對應性,須要在樣本集合造成的矩陣中再插入一列,值均置爲1。函數
data = np.array([[1,1,1,0,0,0],[1,0,1,0,0,0],[1,1,1,0,0,0],[0,0,1,1,1,0], [0,0,1,1,0,0],[0,0,1,1,1,0]])
num_examples = data.shape[0]
data = np.insert(data, 0, 1, axis=1)
data
複製代碼
array([[1, 1, 1, 1, 0, 0, 0],
[1, 1, 0, 1, 0, 0, 0],
[1, 1, 1, 1, 0, 0, 0],
[1, 0, 0, 1, 1, 1, 0],
[1, 0, 0, 1, 1, 0, 0],
[1, 0, 0, 1, 1, 1, 0]])
複製代碼
有了輸入矩陣與weight矩陣,對於一個隱藏節點,計算它的值是1仍是0,首先要將全部與它相連的節點的取值各自乘以相應的weight再作加和,好比對於靠上的隱藏節點,當輸入第一個樣本時,可見層的取值爲[1, 1, 1, 1, 0, 0, 0]
,相應的權重爲[0, -0.0534304, -0.01078587, 0.04849323, -0.03871753, 0.07935206, -0.02462677]
,兩向量作點乘正好對應了上面的過程,注意兩向量第一個值對應於bias unit,而bias unit的weight爲0,對結果並未產生影響。學習
對於全部樣本和全部的隱藏節點都是同樣的處理,那麼可將這麼許屢次點乘化爲矩陣相乘,根據矩陣相乘的規則(行向量點乘列向量結果放在相應的位置)能夠看出將data與weight矩陣相乘恰好表示對每一個樣本和每一個隱藏節點將上述處理作了一次,會產生出一個新的形狀爲(6*3)
的矩陣,第一列對應bias unit值全爲0,第二列的第一個元素便對應了Dancing Queen在hidden unit1處生成的值。使用矩陣相乘,效率要比對樣本和隱藏單元進行迭代快得多。優化
pos_hidden_activations = np.dot(data, weights)
pos_hidden_activations
複製代碼
array([[ 0. , -0.01572304, 0.0311873 ],
[ 0. , -0.00493717, -0.01823826],
[ 0. , -0.01572304, 0.0311873 ],
[ 0. , 0.08912777, 0.07801112],
[ 0. , 0.00977571, 0.01289768],
[ 0. , 0.08912777, 0.07801112]])
複製代碼
算出激活值後,衆所周知,神經網絡的節點每每包含一個激活函數,這裏使用Sigmond函數,將上一步算出的激活值控制到0-1之間,來表明此節點被激活的可能性,當爲某節點計算出的激活機率越接近1,則其被激活的可能性越大,這裏利用numpy的廣播功能,對上述生成的矩陣中的全部節點都施加一個Sigmond函數。而後將bias unit對應的那一列全改成1,這表示bias unit老是被激活的,具體緣由見下文。ui
def logistic(x):
return 1.0 / (1 + np.exp(-x))
pos_hidden_probs = logistic(pos_hidden_activations)
pos_hidden_probs[:, 0] = 1
pos_hidden_probs
複製代碼
array([[1. , 0.49606932, 0.50779619],
[1. , 0.49876571, 0.49544056],
[1. , 0.49606932, 0.50779619],
[1. , 0.5222672 , 0.5194929 ],
[1. , 0.50244391, 0.50322437],
[1. , 0.5222672 , 0.5194929 ]])
複製代碼
rand函數會隨機生成一個處於0到1之間的數,將上述算出的節點激活機率與一個這樣的隨機數比較大小來決定是否激活此節點,這意味着即便此時某次訓練中某隱藏結點的激活機率爲0.99,也是有可能不被激活的。
pos_hidden_states = pos_hidden_probs > np.random.rand(num_examples, num_hidden + 1)
pos_hidden_states
複製代碼
array([[ True, True, True],
[ True, False, False],
[ True, False, False],
[ True, True, True],
[ True, False, True],
[ True, False, True]])
複製代碼
這個矩陣的含義舉例爲其第一行第二列的值表示Dancing Queen是否激活了hidden unit1。
如上述進行過了一次全部樣本對隱藏層激活狀況的計算,能夠得出Dancing Queen與hidden unit1同時亮起的相關性,記爲,看上述矩陣
data
與pos_hidden_probs
,第一個樣本在Dancing Queen節點取值爲1,對hidden unit1激活機率爲0.49606932,將二者相乘獲得一個值,對全部樣本如此計算獲得的值的加和即爲這兩個節點的相關性,這個過程一樣可使用矩陣相乘來表示以下:
pos_associations = np.dot(data.T, pos_hidden_probs)
pos_associations
複製代碼
array([[6. , 3.03788267, 3.05324311],
[3. , 1.49090435, 1.51103295],
[2. , 0.99213864, 1.01559239],
[6. , 3.03788267, 3.05324311],
[3. , 1.54697831, 1.54221017],
[2. , 1.04453441, 1.03898579],
[0. , 0. , 0. ]])
複製代碼
如此,一次從可見層到隱藏層的計算便結束了。以後反過來,從隱藏層到可見層,將pos_hidden_states
做爲樣本集合輸入隱藏層,作一遍與上述過程徹底相同的計算,一樣能夠計算出兩相連節點之間的相關性,此次記爲。因爲是隨機取的初始weight,Positive與Negative之間應該會有不小的差異,而RBM的優化目標即是經過多個epoch的訓練,使其差異儘量小。
上述一正一反便算完成了一個epoch,根據式子算出新的weight值(其中L爲學習速率須要鍊金而得),來開始下一個epoch的計算,如此會使得二者之間的差值愈來愈小,從而獲得一個訓練好的RBM模型。
比較巧妙的仍是bias unit,能夠看到上述有些可見節點與bias unit的關聯值達到6,而在下一次循環中,又會對bias unit整個從新賦值,這個處理能夠將那些熱門的item對隱藏節點是否激活的影響引向這個bias unit,來稀釋這種影響,儘可能防止「the Beatles現象」的出現。同時,因爲它與兩層每一個節點都相連,在從可見層到隱藏層的計算過程當中,它實際上是做爲一個隱藏層節點來一同參與計算的,而在反向時,它又做爲一個可見層節點來發揮做用,真是妙啊。
使用上述過程的完整版代碼(見文末參考連接),來看一下結果:
r = RBM(num_visible = 6, num_hidden = 2)
training_data = np.array([[1,1,1,0,0,0],[1,0,1,0,0,0],[1,1,1,0,0,0],[0,0,1,1,1,0], [0,0,1,1,0,0],[0,0,1,1,1,0]])
r.train(training_data, max_epochs = 5000)
print(r.weights[1:, 1:])
複製代碼
[[-8.09650002 3.95552071]
[-5.45512759 1.42845858]
[ 1.74474585 4.06127352]
[ 7.74906751 -3.54062571]
[ 3.18686136 -7.33215302]
[-2.46868951 -2.60826581]]
複製代碼
從訓練好的weight中能夠看出,hidden unit1傾向於對應rock guitar hero的音樂,而hidden unit2則傾向於對應disco。
聯繫推薦系統,顯然該模型能夠對item作降維處理,與Word2vec同樣,使用weight組成的向量表示便可,好比Dancing Queen可表示爲[-8.09650002, 3.95552071]
。
而要爲user推薦item,則須要將其收聽歷史向量[1, 1, 1, 0, 0, 0]
輸入訓練好的模型,激活一些隱藏節點,再將表示隱藏層節點被激活狀況的向量反向輸入模型,可爲每一個item獲得一個被激活的機率,去掉用戶已經聽過的item,再對機率進行從大到小排序選取K個便可作出TopK推薦。該處理只有簡單的向量計算很是迅速,可用於在線實時生成推薦結果。
對於顯示反饋,好比Netflix Prize的狀況,Ruslan Salakhutdinov等人對RBM提出了改進,可見層使用Softmax神經元來表示打分狀況,對於沒有被評分過的item則使用特殊的神經元表示,不與隱藏層相連避免無謂的計算;而條件RBM能夠在處理顯示反饋時將用戶瀏覽過哪些物品這樣的隱式反饋的影響同時考慮進去。這些改進都涉及到對本文計算過程與數學公式的改進,具體能夠參考論文。
echen/restricted-boltzmann-machines
Introduction to Restricted Boltzmann Machines
Restricted Boltzmann Machines for Collaborative Filtering