"受限波爾茲曼"這名字聽起來就霸氣,算法如其名,也挺難的。之所以難,是因為我們大部分人都沒學過概率圖模型,其實RBM是條件隨機場的變體,所以如果學習這個算法,建議先把CRF給熟悉了,那么學起來就會輕松很多。受限玻爾茲曼機是由Geoff Hinton發明,是一種用於降維、分類、回歸、協同過濾、特征學習和主題搭建的算法。RBM網絡作為一種無監督學習的方法,其目的是盡可能地表達輸入數據的的規則和特征。
一 網絡結構
我們首先介紹一下受限玻爾茲曼機這類神經網絡,因為它相對簡單具有重要的歷史意義。下文將以示意圖和通俗的語言解釋其運作原理。
RBM是有兩個層的淺層神經網絡,它是組成深度置信網絡的基礎部件。RBM的第一個層稱為可見層,又稱輸入層,由顯元 (visible units) 組成,用於輸入訓練數據。第二個層是隱藏層,相應地,由隱元 (hidden units) 組成,用作特征檢測器 (feature detectors)。
上圖中每個圓圈都是一個與神經元相似的單元,稱為節點,運算在節點中進行。一個層中的節點與另一層中的所有節點分別連接,但與同一層中的其它節點並不相連。
也就是說,層的內部不存在通信-這就是受限玻爾茲曼機被稱為受限的原因。每個節點對輸入進行處理和運算,判定是否繼續傳輸輸入的數據,而這種判定一開始是隨機的。(“隨機”(stochastic)一詞在此處指與輸入相乘的初始系數是隨機生成的。)
每個可見節點負責處理網絡需要學習的數據集中一個項目的一種低層次特征。舉例來說,如果處理的是一個灰度圖像的數據集,則每個可見節點將接收一張圖像中每個像素的像素值。(MNIST圖像有784個像素,所以處理這類圖像的神經網絡的一個可見層必須有784個輸入節點。)
接着來看單個像素值x如何通過這一雙層網絡。在隱藏層的節點1中x與一個權重相乘,再與所謂的偏差相加。這兩步運算的結果輸入激活函數,得到節點的輸出,即輸入為x時通過節點的信號強度。
輸出a = 激活函數f((權重w * 輸入x) + 偏差b )
下面來看一個隱藏節點如何整合多項輸入。每個x分別與各自的權重相乘,乘積之和再與偏差相加,其結果同樣經過激活函數運算得到節點的輸出值。
由於每個可見節點的輸入都被傳遞至所有的隱藏節點,所以也可將RBM定義為一種對稱二分圖。
對稱指每個可見節點都與所有的隱藏節點相連接(見下圖)。二分指有兩個部分或層,而這里的圖是指代節點網絡的數學名詞。
在每個隱藏節點中,每一個輸入x都會與其相對應的權重w相乘。也就是說,每個輸入x會對應三個權重,因此總共有12個權重(4個輸入節點 x 3個隱藏節點)。兩層之間的權重始終都是一個行數等於輸入節點數、列數等於輸出節點數的矩陣。
每個隱藏節點會接收四個與對應權重相乘后的輸入值。這些乘積之和與一個偏差值相加(至少能強制讓一部分節點激活),其結果再經過激活運算得到每個隱藏節點的輸出a。
如果這兩個層屬於一個深度神經網絡,那么第一隱藏層的輸出會成為第二隱藏層的輸入,隨后再通過任意數量的隱藏層,直至到達最終的分類層。(簡單的前饋動作僅能讓RBM節點實現自動編碼器的功能。)
二 重構
我們重點關注RBM如何在無監督情況系學習重構數據,在可見層和第一個隱藏層之間進行多次正向和反向傳播,而無需加大網絡的深度。
在重構階段,第一個隱藏層的激活值成為反向傳播中的輸入。這些輸入值與同樣的權重相乘,每兩個相連的節點之間各有一個權重,就像正向傳播中輸入x的加權運算一樣。這些乘積的和再與每個可見層的偏差相加,所得結果就是重構值,亦即原始輸入的近似值,這一過程可以用下圖來表示:
由於RBM的權重初始化是隨機的,重構值與原始輸入之間的差別通常很大。可以將r值與輸入之差視為重構誤差,此誤差值隨后經由反向傳播來修正RBM的權重,如此不斷的反復,直至誤差達到最小。
- RBM在正向傳遞中使用輸入值來預測節點的激活值,即輸入為x時輸出a的概率:p(a|x:w)。
- 但在反向傳播時,激活值成為輸入,而輸出的是對於原始數據的重構值,或者說猜測值,此時RBM則是在嘗試估計激活值為a時輸入為x的概率,激活值得加權系數與正向傳播中的權重相同。第二個階段可以表示為:p(x|a:w)。
上述兩種預測值相結合,可以得到輸入x和激活值a的聯合概率分布,即p(x,a)。
重構與回歸,分類運算不同,回歸運算根據需要輸入值估計一個連續值,分類運算時猜測應當為一個特定的輸入樣例添加哪種具體的標簽。而重構則是在猜測原始輸入的概率分布,即同時預測許多不同的點的值,這被稱為生成學習,必須和分類器所進行的判別學習區分開來,后者是將輸入值映射至標簽,用直線將數據划分為不同的組。
試想輸入數據和重構數據是形狀不同的常態曲線,兩者僅有部分重疊。RBM用Kullback Leibler來衡量預測的概率分布與輸入值的基准分布之間的距離。
KL散度衡量兩條曲線下方不重疊(即散度)的面積,而RBM的優化算法會嘗試將這些離散部分的面積最小化,使共用權重在與第一層的激活值相乘后,可以得到與原始輸入高度近似的結果。下圖左半邊是一組原始輸入的概率分布曲線ρ,與之並列的是重構值的概率分布曲線q,右半邊的圖則顯示了兩條曲線之間的差異。
RBM根據權重產生的誤差反復調整權重,以此學習估計原始數據的近似值。可以說權重會慢慢開始反映出輸入的結構,而這種結構被編碼為第一個隱藏層的激活值。整個學習過程看上去像是兩條概率分布曲線在逐步重合。
三 使用 RBM 的過程


- 首先,將每個隱藏節點的激勵值 (activation) 計算出來:
- 然后,將每個隱藏節點的激勵值都用 S 形函數進行標准化,變成它們處於開啟狀 (用 1 表示) 的概率值:


- 至此,每個隱藏節點hj開啟的概率被計算出來了。其處於關閉狀態 (用 0 表示) 的概率自然也就是

- 通常RBM中的神經元都是二值化的,也就是說只有開啟和不開啟兩種狀態,也就是0或者1。那么到底這個元開啟還是關閉,我們需要將開啟的概率與一個從 0, 1 均勻分布中抽取的隨機值

四 訓練 RBM
RBM 的訓練過程,實際上是求出一個最能產生訓練樣本的概率分布。也就是說,要求一個分布,在這個分布里,訓練樣本的概率最大。由於這個分布的決定性因素在於權值W ,所以我們訓練 RBM 的目標就是尋找最佳的權值。為了保持讀者的興趣,這里我們不給出最大化對數似然函數的推導過程,直接說明如何訓練 RBM。
五 實例代碼
# -*- coding: utf-8 -*- """ Created on Sat May 19 09:30:02 2018 @author: zy """ ''' 受限的玻爾茲曼機:https://blog.csdn.net/zc02051126/article/details/9668439 ''' import matplotlib.pylab as plt import numpy as np import random class RBM(object): ''' 定義一個RBM網絡類 ''' def __init__(self,n_visible,n_hidden,momentum=0.5,learning_rate=0.1,max_epoch=50,batch_size=128,penalty=0,weight=None,v_bias=None,h_bias=None): ''' RBM網絡初始化 使用動量的隨機梯度下降法訓練網絡 args: n_visible:可見層節點個數 n_hidden:隱藏層節點個數 momentum:動量參數 一般取值0.5,0.9,0.99 當取值0.9時,對應着最大速度1/(1-0.9)倍於梯度下降算法 learning_rate:學習率 max_epoch:最大訓練輪數 batch_size:小批量大小 penalty:規范化 權重衰減系數 一般設置為1e-4 默認不使用 weight:權重初始化參數,默認是n_hidden x n_visible v_bias:可見層偏置初始化 默認是 [n_visible] h_bias:隱藏層偏置初始化 默認是 [n_hidden] ''' #私有變量初始化 self.n_visible = n_visible self.n_hidden = n_hidden self.max_epoch = max_epoch self.batch_size = batch_size self.penalty = penalty self.learning_rate = learning_rate self.momentum = momentum if weight is None: self.weight = np.random.random((self.n_hidden,self.n_visible))*0.1 #用於生成一個0到0.1的隨機符點數 else: self.weight = weight if v_bias is None: self.v_bias = np.zeros(self.n_visible) #可見層偏置 else: self.v_bias = v_bias if h_bias is None: self.h_bias = np.zeros(self.n_hidden) #隱藏層偏置 else: self.h_bias = h_bias def sigmoid(self,z): ''' 定義s型函數 args: z:傳入元素or list 、nparray ''' return 1.0/(1.0+np.exp(-z)) def forword(self,inpt): ''' 正向傳播 args: inpt : 輸入數據(可見層) 大小為batch_size x n_visible ''' z = np.dot(inpt,self.weight.T) + self.h_bias #計算加權和 return self.sigmoid(z) def backward(self,inpt): ''' 反向重構 args: inpt : 輸入數據(隱藏層) 大小為batch_size x n_hidden ''' z = np.dot(inpt,self.weight) + self.v_bias #計算加權個 return self.sigmoid(z) def batch(self): ''' 把數據集打亂,按照batch_size分組 ''' #獲取樣本個數和特征個數 m,n = self.input_x.shape #生成打亂的隨機數 per = list(range(m)) random.shuffle(per) per = [per[k:k+self.batch_size] for k in range(0,m,self.batch_size)] batch_data = [] for group in per: batch_data.append(self.input_x[group]) return batch_data def fit(self,input_x): ''' 開始訓練網絡 args: input_x:輸入數據集 ''' self.input_x = input_x Winc = np.zeros_like(self.weight) binc = np.zeros_like(self.v_bias) cinc = np.zeros_like(self.h_bias) #開始每一輪訓練 for epoch in range(self.max_epoch): batch_data = self.batch() num_batchs = len(batch_data) #存放平均誤差 err_sum = 0.0 #隨着迭代次數增加 penalty減小 self.penalty = (1 - 0.9*epoch/self.max_epoch)*self.penalty #訓練每一批次數據集 for v0 in batch_data: ''' RBM網絡計算過程 ''' #前向傳播 計算h0 h0 = self.forword(v0) h0_states = np.zeros_like(h0) #從 0, 1 均勻分布中抽取的隨機值,盡然進行比較判斷是開啟一個隱藏節點,還是關閉一個隱藏節點 h0_states[h0 > np.random.random(h0.shape)] = 1 #print('h0',h0.shape) #反向重構 計算v1 v1 = self.backward(h0_states) v1_states = np.zeros_like(v1) v1_states[v1 > np.random.random(v1.shape)] = 1 #print('v1',v1.shape) #前向傳播 計算h1 h1 = self.forword(v1_states) h1_states = np.zeros_like(h1) h1_states[h1 > np.random.random(h1.shape)] = 1 #print('h1',h1.shape) '''更新參數 權重和偏置 使用棟梁的隨機梯度下降法''' #計算batch_size個樣本的梯度估計值 dW = np.dot(h0_states.T , v0) - np.dot(h1_states.T , v1) #沿着axis=0進行合並 db = np.sum(v0 - v1,axis=0).T dc = np.sum(h0 - h1,axis=0).T #計算速度更新 Winc = self.momentum * Winc + self.learning_rate * (dW - self.penalty * self.weight)/self.batch_size binc = self.momentum * binc + self.learning_rate * db / self.batch_size cinc = self.momentum * cinc + self.learning_rate * dc / self.batch_size #對於最大化對數似然函數 使用梯度下降法是加號 最小化是減號 開始更新 self.weight = self.weight + Winc self.v_bias = self.v_bias + binc self.h_bias = self.h_bias + cinc err_sum = err_sum + np.mean(np.sum((v0 - v1)**2,axis=1)) #計算平均誤差 err_sum = err_sum /num_batchs print('Epoch {0},err_sum {1}'.format(epoch, err_sum)) def predict(self,input_x): ''' 預測重構值 args: input_x:輸入數據 ''' #前向傳播 計算h0 h0 = self.forword(input_x) h0_states = np.zeros_like(h0) #從 0, 1 均勻分布中抽取的隨機值,盡然進行比較判斷是開啟一個隱藏節點,還是關閉一個隱藏節點 h0_states[h0 > np.random.random(h0.shape)] = 1 #反向重構 計算v1 v1 = self.backward(h0_states) return v1 def visualize(self, input_x): ''' 傳入 形狀為m xn的數據 即m表示圖片的個數 n表示圖像的像素個數 其中 m = row x row n = s x s args: input_x:形狀為 m x n的數據 ''' #獲取輸入樣本的個數和特征數 m, n = input_x.shape #獲取每張圖像的寬和高 默認寬=高 s = int(np.sqrt(n)) #把所有圖片以 row x row排列 row = int(np.ceil(np.sqrt(m))) #其中多出來的row + 1是用於繪制邊框的 data = np.zeros((row*s + row + 1, row * s + row + 1)) - 1.0 #圖像在x軸索引 x = 0 #圖像在y軸索引 y = 0 #遍歷每一張圖像 for i in range(m): z = input_x[i] z = np.reshape(z,(s,s)) #填充第i張圖像數據 data[x*s + x + 1 :(x+1)*s + x + 1 , y*s + y + 1 :(y+1)*s + y + 1] = z x = x + 1 #換行 if(x >= row): x = 0 y = y + 1 return data def read_data(path): ''' 加載數據集 數據按行分割,每一行表示一個樣本,每個特征使用空格分割 args: path:數據文件路徑 ''' data = [] for line in open(path, 'r'): ele = line.split(' ') tmp = [] for e in ele: if e != '': tmp.append(float(e.strip(' '))) data.append(tmp) return data if __name__ == '__main__': #加載MNIST數據集 總共有5000張圖像,每張圖像有784個像素點 MNIST數據集可以從網上下載 data = read_data('data.txt') data = np.array(data) print(data.shape) #(5000, 784) #創建RBM網絡 rbm = RBM(784, 100,max_epoch = 50,learning_rate=0.05) #開始訓練 rbm.fit(data) #顯示64張手寫數字 images = data[0:64] print(images.shape) a = rbm.visualize(images) fig = plt.figure(1,figsize=(8,8)) plt.imshow(a,cmap=plt.cm.gray) plt.title('original data') #顯示重構的圖像 rebuild_value = rbm.predict(images) b = rbm.visualize(rebuild_value) fig = plt.figure(2,figsize=(8,8)) plt.imshow(b,cmap=plt.cm.gray) plt.title('rebuild data') #顯示權重 w_value = rbm.weight c = rbm.visualize(w_value) fig = plt.figure(3,figsize=(8,8)) plt.imshow(c,cmap=plt.cm.gray) plt.title('weight value(w)') plt.show()
運行結果如下:
參考文獻
[1] Andrew Ng機器學習筆記(三)(拓展)深度學習與受限玻爾茲曼機(推薦)
[2]受限玻爾茲曼機基礎教程
[6]受限的玻爾茲曼機