SOM神經網絡、LVQ神經網絡、CPN神經網絡與Python實現

競爭型學習神經網絡

簡述

最近對神經網絡比較感興趣,因此花了兩三天時間對整個領域進行了簡單的調研,梳理和學習。其中深度學習,尤其以我們熟悉的如今大火的深度網絡模型,如CNN, RNN, GAN和AE及它們的子類等;是在本世紀初,硬件性能達到了一個新的高度後纔能有如此巨大的發展的。事實上在21世紀之前,各種各樣的神經網絡模型已經被髮明並應用。那時候由於硬件性能的限制,以及由通信網絡不發達導致的數據量匱乏,並沒有人敢去思考如何用巨大的算力去挖掘大數據。那時的網絡一般是藉助設計網絡結構的技巧取勝(比如很多網絡不借助編程,而是直接用硬件實現),其中很多網絡在今天,其設計理念也是值得借鑑的。
爲此,我在兩三天的時間裏,學習了一些從上世紀60年代,到本世紀初出現的一些經典的神經網絡模型,並在此整理並給出代碼。

競爭層

我們本篇主要介紹競爭型的神經網絡,即在網絡中出現競爭層的網絡。競爭層比起我們熟悉的全連接層要樸素很多;
首先,競爭層會接受一個向量樣本,每個競爭層中的神經元都自帶一個和該樣本維度相同的向量,稱作"權向量"。然後,用每個權向量和輸入向量按照一定規則進行比較,相似程度最高的那個稱作獲勝神經元。
然後在輸出時,只有獲勝神經元會輸出1,其他神經元輸出0,也就是勝者通喫的法則。這一特點也就決定了,在使用反向傳播算法訓練這種網絡時,只有獲勝神經元會受到訓練。
講到這裏大家可能會發現,這種訓練方法有點類似聚類中的K-means算法。通過比較中心向量和樣本的相似程度,調整中心向量。的確,兩者理念是相同的。而這一特點也就決定了競爭型網絡既可以用於監督學習,也可以用於無監督學習。

SOM神經網絡簡述

首先,我們來介紹Self-Organizing feature Map,自組織特徵映射網絡,som是一種自組織的網絡結構,常用於數據的聚類和降維。網絡的結構非常簡單,只有一層競爭層,接受輸入向量並進行比較後,直接輸出結果。
SOM
其主要特點是網絡內部的拓撲結構,爲什麼它叫"自組織"網絡?因爲SOM在執行基本的競爭層工作的同時,不止是調整獲勝神經元,還會調整獲勝神經元附近的神經元。爲什麼要進行這樣的操作呢?我們可以想象,在我們的訓練過程中,相似的樣本會在向量空間中相對接近,形成一個獨立的簇狀。如果讓相鄰的神經元也去靠近同樣的樣本,則很大概率會讓訓練出的網絡的相鄰神經元,在向量空間中也越接近,表徵這相同的類別。WIKI上的一張圖就很形象。
WIKIPIDIA
其訓練分爲:

  1. 初始化每個節點的權重。權重設置爲標準化小型隨機值。
  2. 從訓練集中隨機選擇一個向量並呈現給網格。
  3. 檢查每個節點來計算哪一個的權重最像輸入向量。這會讓你獲得最佳匹配單元(BMU)。我們通過迭代所有節點並計算每個節點的權重與當前輸入向量之間的歐幾里德距離來計算BMU。權重向量和輸入向量最接近的節點則被標記爲BMU。
  4. 計算BMU鄰域的半徑。在半徑範圍內找到的節點則認爲處於BMU鄰域內。
  5. 調整在步驟4中發現的節點的權重,讓它們更像輸入向量。節點和BMU越近,權重調整的越多。重複步驟2,迭代N次。
    評估相似度可以使用單純的向量歐拉距離,也可以藉助度量學習或者歸一化等方法幫助訓練。

這種方法是一種聚類方法,而又有不同於聚類的地方;因爲網絡能自組織地學習特徵向量空間中的分佈,並把相似的分佈烙印在自己的網絡拓撲結構上,它同時還具備了特徵間聚類和可視化的能力。
貧困數據
如圖,在特徵上接近的數據會被聚集在網絡拓撲結構相同的區域內。

SOM神經網絡實戰

TIPS:有關訓練的小技巧
第一,SOM和BP網不同,它的學習速度很大程度取決於樣本,而不取決於網絡當前的學習情況。所以如果想讓網絡有效訓練,一種方法是使用隨迭代次數下降的學習率。
第二:在進行自組織訓練時,調動周邊神經元的一個方法是先計算神經元在網絡拓撲上的距離,再根據該距離計算出一個衰減因子。常用的衰減函數有高斯分佈、門函數等。
前面說了,SOM網絡能用作聚類、降維等工作上。這裏我們就分別體驗一下它的功能。
考慮這樣的一項任務,對一個色彩比較多樣的圖片,我們想要讓它進行簡單壓縮,讓每個像素點的色彩種類限制在幾種。即在圖片所有像素組成的三維向量空間內尋找到最能表徵圖片的n種顏色。
這個任務很適合SOM,因爲SOM進行的就是向量的競爭,而且最後得到的權值向量就可以直接被用於表徵圖片的顏色。

首先我們調一下包,常用的numpy和matplotlib等

import numpy as np
import random
from copy import deepcopy
import matplotlib
from matplotlib import pyplot as plt

然後我們設計一個SOM類,以及配套的距離估算函數等。

def gaussian(x, sigma):
    """Returns a Gaussian centered in c."""
    d = (2*np.pi)**0.5*sigma
    return np.exp(-(x**2)/2/(sigma**2))/d

def Manhattan(x1,x2):
    return abs(x1[0]-x2[0])+abs(x1[1]-x2[1])
     

   
class node:
    def __init__(self,input_len,weight):
        self.w = np.ones(input_len)*weight
        self.x = None
        self.y = None
        
    def forward(self, x):
        self.x = x
        vec = (x-self.w).reshape(1,-1)
        self.y = np.dot(vec,vec.T)
        return self.y
    
    def backward(self):
        return (self.x-self.w)
        

class SOM:
    def __init__(self,x,y,input_len,sigma,weight,lr=0.2):
        '''
        輸入參數
        x,y:輸出層的長寬
        input_len:輸入向量的size
        sigma:高斯衰落函數的標準差
        '''
        self.x = x;self.y = y;
        self.nodes = [[node(input_len,weight) for _ in range(y)] for _ in range(x)]
        self.sigma = sigma
        self.lr = lr
        
    def fit(self, data, iter_num=10000):
        for t in range(iter_num):
            lr = self.lr*np.exp(-t/iter_num)
            x = random.choice(data)
            winner = (0,0)
            dist = float("inf")
            for i in range(self.x):
                for j in range(self.y):
                    node = self.nodes[i][j]
                    d = node.forward(x)
                    if d<dist:
                        winner = (i,j)
                        dist = d
            
            i,j = winner
            dw = self.nodes[i][j].backward()
            for m in range(self.x):
                for n in range(self.y):
                    node = self.nodes[m][n]
                    manh = Manhattan((m,n),(i,j))
                    node.w += lr*gaussian(manh,self.sigma)*dw
        
    def predict(self, x):
        res = None
        dist = float("inf")
        for i in range(self.x):
            for j in range(self.y):
                node = self.nodes[i][j]
                d = node.forward(x)
                if d<dist:
                    res = node.w
                    dist = d
        return res
    
    
    def weights(self):
        return [[self.nodes[i][j].w for j in range(self.y)] 
                for i in range(self.x)]

導入一張圖片用作訓練,這裏我用的是我的微信頭像’kokoro’

img = plt.imread('kokoro.jpg')
pixels = img.reshape(-1,3).copy()
plt.imshow(pixels.reshape(img.shape))

秦心
然後就可以初始化一個網絡並且訓練了,我們設計的是2*2的矩陣型拓撲結構,可以看到訓練後得到的幾個權向量如下

som = SOM(2,2,3,0.1,155,0.2)
som.fit(pixels,10000)
som.weights()

w
我們把SOM的predict方法用在原圖片上,就可以看見被4色化的圖片了

zipped = np.zeros(pixels.shape).astype("uint8")
for i in range(len(pixels)):
    x = som.predict(pixels[i])
    zipped[i] = x.astype("uint8")

img_zip = zipped.reshape(img.shape)
plt.subplot(121)
plt.imshow(img_zip)
plt.subplot(122)
plt.imshow(img)

比較
保真度還是相當高的
我們再來體驗一下基於SOM的聚類,理論上SOM的聚類效果不應該很好。一是因爲它是根據輸入向量和權向量間距離直接分類的,效果上肯定比不上高斯混合聚類這種概率模型。二是我這裏使用的是簡單的歐拉距離,並不能很好的表徵一般的特徵距離。但我們還是姑且一試

首先導入經典的聚類數據集鳶尾花

from sklearn.datasets import load_iris
iris = load_iris()
descr = iris['DESCR']
data = iris['data']
feature_names = iris['feature_names']
target = iris['target']
target_names = iris['target_names']

初始化網絡並訓練

som = SOM(2,2,4,0.1,3,0.2)
som.fit(data,10000)
labels = np.zeros(len(data))
dic = {}
cnt = 0
for i in range(len(data)):
    vec = tuple(som.predict(data[i]))
    if not dic.get(vec):
        dic[vec]=cnt
        cnt+=1
    labels[i] = dic[vec]

plt.figure(figsize=(10,4), dpi=80)
plt.subplot(1,2,1)
plt.scatter(data[:,2].reshape(-1), data[:,3].reshape(-1),edgecolors='black',
            c=target)
plt.xlabel('petal length (cm)')
plt.ylabel('petal width (cm)')
plt.title('Iris original distribution')
plt.subplot(1,2,2)
plt.scatter(data[:,2].reshape(-1), data[:,3].reshape(-1),edgecolors='black',
            c=labels)
plt.xlabel('petal length (cm)')
plt.ylabel('petal width (cm)')
plt.title('Distribution by SOM network')

在兩個特徵上的2Dplot如下,可以看見效果的確一般。
在這裏插入圖片描述

LVQ神經網絡簡述

下一個,我們來介紹LVQ網絡。LVQ學習向量量化算法是一種有監督的聚類算法,目標是在數據分佈中找到一組能表徵幾種數據簇的原型向量。
學習向量量化的算法很簡單,就是隨機採樣一個數據點,並比較該數據點的標籤和原型向量自己的標籤是否一致。一致則把原型向量向該數據點的方向拉,不一致則把原型向量推走。
網絡使用了LVQ的思想,但是在此之上又添加了更多細節。
在這裏插入圖片描述
1、使用one-hot的輸出形式,每次網絡會進行競爭讓某個神經元獲勝,和同爲one-hot的label對比計算誤差,如果分類錯誤就能一次更新兩個神經元的權值。
2、對競爭層進行分組,比如競爭層有3M個神經元,而輸出層只有M個神經元,每3個神經元以1的連接權重和一個輸出層神經元相連接。這樣最終結果只有M個分類。儘管整個競爭層只有一個神經元會獲勝,但這樣的結構保證了一個分類對應的不只是一個向量中心,提高了分類的可靠性。
容易理解的是,網絡的思想是很樸素的。就是使用LVQ的思想學習爲一個類別學習出幾個原型向量,並用這些原型向量分類其他數據。一般來說,它的學習效率和魯棒性都更好。

LVQ神經網絡實戰

我們來解決一個純粹的向量模型分類問題
在這裏插入圖片描述
首先還是先設計一個網絡類

class LVQnet:
    def __init__(self, input_sz, output_sz, groups):
        '''
        初始化,給出輸入向量的維度和輸出的種類數
        groups是競爭層的分組狀況,如[1,2,3,2]
        意爲競爭層共有8個神經元,4組輸出
        '''
        assert len(groups)==output_sz
        self.groups = groups
        self.hidden_sz = sum(groups)
        #隨機初始化神經元的原型向量
        self.prototype = np.random.rand(self.hidden_sz,input_sz)*0.01
        self.hidden2out = np.zeros((output_sz,self.hidden_sz))
        cnt = 0
        for i in range(len(groups)):
            for j in range(groups[i]):
                self.hidden2out[i][cnt] = 1
                cnt+=1
        
    def fit(self, X, Y, lr = 0.5, iterations = 1000):
        N = len(X)
        for t in range(iterations):
            gamma = lr*(1-t/iterations)
            idx = random.randint(0,N-1)
            x = X[idx]
            out = self.predict(x)
            y = Y[idx]
            delta = abs(out-y)
            sign = int(np.sum(delta)==0)*2-1
            #根據delta修正獲勝神經元的原型向量
            self.prototype[self.winner] += gamma*sign*self.v[self.winner]
    
    def predict(self,x):
        x = np.tile(x,(self.hidden_sz,1))
        v = x-self.prototype
        self.v = v
        distance = np.sum(v**2,axis = 1).reshape(-1)
        winner = np.argmin(distance)
        self.winner = winner
        out = np.zeros((self.hidden_sz,1))
        out[winner][0] = 1
        out = self.hidden2out.dot(out).reshape(-1)
        return out

不需要太多花裏胡哨,就是簡單的競爭邏輯和原型向量更新規則。
我們把上述的向量丟進網絡裏訓練。

X = np.array(
[
    [-6,0],
    [-4,2],
    [-2,-2],
    [0,1],
    [0,2],
    [0,-2],
    [0,1],
    [2,2],
    [4,-2],
    [6,0]
])
Y = np.array(
[
    [1,0],
    [1,0],
    [1,0],
    [0,1],
    [0,1],
    [0,1],
    [0,1],
    [1,0],
    [1,0],
    [1,0]
])#one-hot形式的編碼
network = LVQnet(2,2,[4,2])
network.fit(X,Y,lr = 0.1, iterations=2000)
x = [0 for _ in range(10)]
y = [0 for _ in range(10)]
u = list(X[:,0].reshape(-1))
v = list(X[:,1].reshape(-1))
for i in range(10):
    if np.argmax(Y[i])==0:
        plt.quiver(x[i],y[i],u[i],v[i],color='r',angles='xy', scale_units='xy', scale=1)
    else:
        plt.quiver(x[i],y[i],u[i],v[i],color='g',angles='xy', scale_units='xy', scale=1)

prototype = network.prototype
for i in range(len(prototype)):
    plt.quiver(x[i],y[i],prototype[i][0],prototype[i][1],color='yellow',angles='xy', scale_units='xy', scale=1)
plt.xlim([-6,6])
# Set the y-axis limits
plt.ylim([-6,6])
# Show the plot
plt.show()

給出的向量樣本如圖所示
在這裏插入圖片描述
分類得到的原型向量如下圖的黃色箭頭所示
在這裏插入圖片描述
表徵兩個類需要6個原型向量,如果歸一化向量,則可以使用更少。

X = X/np.sqrt((np.tile(np.sum(X**2,axis=1),(2,1)).T))
u = list(X[:,0].reshape(-1))
v = list(X[:,1].reshape(-1))
for i in range(10):
    if np.argmax(Y[i])==0:
        plt.quiver(x[i],y[i],u[i],v[i],color='r',angles='xy', scale_units='xy', scale=1)
    else:
        plt.quiver(x[i],y[i],u[i],v[i],color='g',angles='xy', scale_units='xy', scale=1)
plt.xlim([-1.5,1.5])
# Set the y-axis limits
plt.ylim([-1.5,1.5])
# Show the plot
plt.show()

network = LVQnet(2,2,[2,2])
network.fit(X,Y,lr = 0.1, iterations=2000)

for i in range(10):
    if np.argmax(Y[i])==0:
        plt.quiver(x[i],y[i],u[i],v[i],color='r',angles='xy', scale_units='xy', scale=1)
    else:
        plt.quiver(x[i],y[i],u[i],v[i],color='g',angles='xy', scale_units='xy', scale=1)

prototype = network.prototype
for i in range(len(prototype)):
    plt.quiver(x[i],y[i],prototype[i][0],prototype[i][1],color='yellow',angles='xy', scale_units='xy', scale=1)
plt.xlim([-1.5,1.5])
# Set the y-axis limits
plt.ylim([-1.5,1.5])
# Show the plot
plt.show()

在這裏插入圖片描述
在這裏插入圖片描述

CPN神經網絡簡述

CPN網絡是一種拓撲結構類似三層前饋網絡的網絡,但是它的訓練和使用更爲簡單。實際上CPN是由一層競爭層和一層全連接層組合而成。在這裏插入圖片描述
它是一種混合了SOM網絡和BP網絡的監督聚類方法,首先設置一層競爭層進行簡單聚類,這裏一般不需要進行自組織。然後再通過全連接層把獲勝神經元的y_i=1用權值矩陣傳到輸出層,並用label反向傳播調整權值矩陣。隨着算法的演變,權值矩陣將被編碼爲期望輸出的形式。
訓練的過程應該不用太多解釋,主要是要把訓練分爲兩部分

  1. 訓練競爭層,讓競爭層的權向量收斂到數據分佈的聚類中心。訓練方法參考上面的SOM的訓練,不過一般不用讓網絡自組織。
  2. 訓練全連接層,使用簡單的反向傳播算法。每次輸入一個向量後,競爭層有一個神經元被激活,它的輸出爲1,其他輸出爲0.這樣,整個網絡的輸出也就是該神經元對應的全連接層的連接權。然後通過對比該連接權和真實輸出,確定權向量的正確取值。

競爭層的部分有時需要進行向量的歸一化處理。很容易看出,CPN比起SOM就是多做了一個編碼的工作。讓聚類的無標籤形式變成監督情形的有標籤形式。
CPN的缺點依然很明顯,如果競爭層的聚類效果不好,就有很大概率在進行權值矩陣編碼時發生震盪,致使網絡不收斂。
在這裏插入圖片描述

CPN神經網絡實戰

我們來實踐一下,解決下面的這個模式分類問題
在這裏插入圖片描述
定義網絡

class CPNnet:
    def __init__(self, input_sz, hidden_sz, output_sz):
        '''
        初始化,給出輸入向量的維度和輸出的種類數
        此外還需要給出隱層數目,也就是中心向量的數目
        '''
        self.values = np.random.rand(hidden_sz,input_sz)
        self.sz = hidden_sz
        self.W = np.zeros((hidden_sz,output_sz))
        
        
    def fit(self, X, Y, lr = 1, iterations = 1000):
        #首先把value向量調整到適應X的情況
        #rand生成的是 [0,1]的均勻分佈,計算X各維度的均值並做歸一化
        means = np.tile(np.mean(X,axis=0),(self.sz,1))
        self.values *= 2*means
        N = len(X)
        #階段1,對隱層向量進行訓練
        for t in range(iterations):
            gamma = lr*(1-t/iterations)
            idx = random.randint(0,N-1)
            x = X[idx]
            out = self.predict(x)
            self.values[self.winner] += gamma*(x-self.values[self.winner])
        #階段2,對權值矩陣進行訓練
        for t in range(iterations//10):
            gamma = lr*(1-t/iterations)
            idx = random.randint(0,N-1)
            x = X[idx]
            y = Y[idx]
            out = self.predict(x)
            delta = out-y
            self.W[self.winner] -= gamma*delta
    
    def predict(self,x0):
        x = deepcopy(x0)
        minus = self.values-np.tile(x,(len(self.values),1))
        dist = np.sum(minus**2,axis=1).reshape(-1)
        winner = np.argmin(dist)
        self.winner = winner
        out = self.W[winner]
        return out

首先我們來看看網絡能否收斂到目標的幾個向量上

X0 = np.array([
    [0.0,0.0],
    [0.5,0.0],
    [0.0,0.5],
    [1.0,1.0],
    [0.5,1.0],
    [1.0,0.5]
])
Y0 = np.array([
    [1,0,0,0,0],
    [1,0,0,0,0],
    [0,1,0,0,0],
    [0,0,1,0,0],
    [0,0,0,1,0],
    [0,0,0,0,1]
])
X = deepcopy(X0)
Y = deepcopy(Y0)
cpn = CPNnet(2,6,5)
cpn.fit(X,Y)
for x in X:
    print(cpn.predict(x))

在這裏插入圖片描述
可以看見,網絡找到了對應的權向量,而且對它們進行了正確編碼。
我們嘗試一下預測任務

x = np.array([0.2,1])
print(cpn.predict(x))
x = np.array([1,0.2])
print(cpn.predict(x))
x = np.array([0.7,0.6])
print(cpn.predict(x))

在這裏插入圖片描述
再來嘗試一下鳶尾花聚類任務

iris = load_iris()
descr = iris['DESCR']
data = iris['data']
feature_names = iris['feature_names']
target = iris['target']
target_names = iris['target_names']

X = deepcopy(data)
# 把target進行獨熱編碼
Y = np.zeros((len(target),3))
for i in range(len(target)):
    Y[i][target[i]] = 1
cpn = CPNnet(4,10,3)
cpn.fit(X,Y)
labels = [np.argmax(cpn.predict(x)) for x in X]
plt.figure(figsize=(10,4), dpi=80)
plt.subplot(1,2,1)
plt.scatter(data[:,0].reshape(-1), data[:,1].reshape(-1),edgecolors='black',
            c=target)
plt.xlabel('sepal length (cm)')
plt.ylabel('sepal width (cm)')
plt.title('Iris original distribution')
plt.subplot(1,2,2)
plt.scatter(data[:,0].reshape(-1), data[:,1].reshape(-1),edgecolors='black',
            c=labels)
plt.xlabel('sepal length (cm)')
plt.ylabel('sepal width (cm)')
plt.title('Distribution by CPN')

在這裏插入圖片描述
網絡爲數據打上了正確的標籤,但是仍然沒能克服競爭層存在的泛化能力差的固有問題。

小結

簡單總結下,競爭型網絡一般用於聚類和數據降維任務,可以有監督學習,也可以無監督學習。由於競爭層只會激活一個神經元,在訓練時也只需要修改該神經元對應的一組權值,因此訓練十分高效。但是競爭層也有其固有的缺點,即分類正確率比較低,在有各種強大的非線性分類模型和機器學習算法的當今,把它用於分類任務並不明智。不過這種網絡結構在計算資源匱乏的20世紀仍然是具有創造性的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章