1. 程式人生 > >【程式碼閱讀記錄】Spiking-Neural-Network---Genre-Recognizer(脈衝神經網路的風格識別器)(1)

【程式碼閱讀記錄】Spiking-Neural-Network---Genre-Recognizer(脈衝神經網路的風格識別器)(1)

前言:

{

    之前從工作中瞭解到了脈衝神經網路(Spiking Neural Network,SNN)。SNN在1952年被首次提出,被譽為第三代神經網路[1]。不過這次不打算再讀論文了,我想直接找段程式碼[2]讀讀看。

    此篇記錄中的程式碼和圖全都來自[2]。

}

正文:

{

    程式碼比較少,一共不到700行,只分成了兩個.py檔案:FinalProject.py和FinalProjectNeuron.py。FinalProject.py應該是人口檔案。

    GenreClassifier是分類器的類。根據程式碼1,此分類器的網路結構很簡單,有1個輸入層,3個隱含層和1個輸出層,具體是138-10-20-10-1。可以看到輸出層的權值被單獨地初始化了。

#程式碼1
def __init__(self):
	self.timeStep = 2
	self.learningRate = 0.001
	self.spikingThreshold = 0.8			#Found through testing my specific neurons
	self.inputLayerSize = 138
	self.numSeconds = 2005						#Number of input neurons/pixels
	self.timeThreshold = 10  				#Time to simulate neuron for

	self.classifications = 2
	self.hiddenLayerNum = 3
	self.neuronPerLayer = [10, 20, 10]

	self.dataList = []
	self.isFirst = 0

	self.inputLayer = []
	for i in range(self.inputLayerSize):
		self.inputLayer.append(Neuron(self.timeThreshold,0))

	self.middleLayer = []
	currNumInputs = 138
	for i in range(self.hiddenLayerNum):
		currLayer = []
		for j in range(self.neuronPerLayer[i]):
			currLayer.append(Neuron(self.timeThreshold, currNumInputs))
		self.middleLayer.append(currLayer)
		currNumInputs = self.neuronPerLayer[i]

	self.outputLayer = Neuron(self.timeThreshold, 0)	

	weights = []
	for i in range(math.floor(currNumInputs/2)):
		self.outputLayer.weights.append(math.ceil(uniform(0,1000))/1000)
	for i in range(math.floor(currNumInputs/2), currNumInputs):
                self.outputLayer.weights.append(math.ceil(uniform(0,1000))/1000)

    作者還給出了結構圖,見圖1。

圖1(這個水印怎麼去掉?)

    Neuron的建構函式(C++中叫建構函式,一時間忘了這個在Python裡叫什麼)如程式碼2。

#程式碼2
def __init__(self, durationForSimulation, numInputs):
	self.T = durationForSimulation #Time to simulate, in ms
	self.dT = 0.1 #the dT time step in dV/dT
	self.VArray = zeros(int((self.T/self.dT) + 1)) #Array of membrane potentials, for plotting later
	self.Vt = 1 #Threshold, in V
	self.Vr = 0 #Reset potential, in mV
	self.initialV = 0 #Initial Membrane Potential = Formula is change in mV
	self.R = 1 #Membrane resistance, in kOhms
	self.C = 10 #Capacitance in uF
	self.tauM = self.R*self.C #Membrane time constant, in miliseconds
	self.firingRate = 10

	self.currentChangeInPotential = float(10.0) #Change in current membrane potential - Used in array

	self.numFired = 0
	self.notFired = 0
	self.fired = 0
	self.spikeRateForData = []
	self.totalSpikingRate = 0

	self.classificationRate = 0
	self.classificationActivity = 0

	self.weights = [];
	for i in range(numInputs):
            randomWeight = math.ceil(uniform(0,2000)-1000)/1000
            self.weights.append(randomWeight)

    可以看出,此分類器是全連線結構。但是作者對最後一層與前一層之間的權值單獨進行了定義,之後應該會涉及到原因。另外還定義了電阻和電容等引數,比之前的神經網路要真實。

    Neuron的反饋通過程式碼3得出。

#程式碼3
def runNeuron(self, inputCurrent):
	self.counter = 0.0
	I = inputCurrent #input current, in Amps - Given by parameter, plus minimum threshold to actually fire

	spikeSum = 0.
	self.VArray = zeros(int((self.T/self.dT) + 1)) #Array of membrane potentials, for plotting later
	self.currentChangeInPotential = 0

	for i in range(1, len(self.VArray)) :
		self.currentChangeInPotential = (-1*self.VArray[i-1] + self.R*I)
		self.VArray[i] = self.VArray[i-1] + self.currentChangeInPotential/self.tauM*self.dT
		if(self.VArray[i] >= self.Vt):
			self.VArray[i] = self.Vr
			spikeSum += 1

return (math.ceil((spikeSum/self.T)*1000))/1000

    可以看到,每個Neuron都有一個設定長度的VArray,其模擬一段時間(durationForSimulation)內此Neuron的狀態。當計算Neuron的輸出時,遍歷VArray,根據VArray中的上一個元素(電壓)、當前輸入電流和Neuron的電阻更新VArray中當前的元素,其中增加的電壓和上一個電壓成反比,和輸入電流成正比;並且在遍歷中如果增加後的電壓高於一個閾值,則產生一個脈衝(spike);最後輸出一個和脈衝數成正比的值。

    我的理解是,每更新一次相當於過濾durationForSimulation,Neuron的外環境對應地更新一次,但內環境的初始狀態會重置,而且之後會更新很多次。

    接下來是主要內容:訓練。訓練函式太大,比較複雜,而且不止一個,我就不在這放出來了。train函式負責更新隱含層的權值,其步驟大致如下:

  1. 陸續輸入輸入列表中的初始輸入資料到輸入層;
  2. 計算輸入層每個Neuron每次輸出有沒有超過一個閾值spikingThreshold,並且如果超過spikingThreshold的情況過半,則設其fired變數為1(當然也記錄了每次的輸出);
  3. 計算隨後的隱含層的每個Neuron(輸入資料變成了輸入層的輸出乘以權值)的輸出currentSpikeRate ,其中當初始輸入資料的第139維資料為0時(網路輸入的大小為138,第139維是多出來的一個維度),本次的輸入會*0.7;
  4. 如果currentSpikeRate 輸入落在了[spikingThreshold-0.1, spikingThreshold),或者某權值對應的輸入層Neuron的fired不為1,則跳過此權值的更新,否則計算此權值的更新量deltaW = (學習率*(1-此權值))/時間步,其中時間步是上述程式碼1中的self.timeStep;
  5. 如果currentSpikeRate 大於等於spikingThreshold且此權值和deltaW的和落在(-1, 1],則增加deltaW到此權值,但是如果currentSpikeRate小於等於spikingThreshold-0.1且此權值和deltaW的和大於等於-1,則從此權值減去deltaW,並且對於剩下的情況,同樣跳過此權值的更新;
  6. 對所有隱含層的其他權值也進行上述操作。

    看這段程式碼的時候我發現了一個問題,程式碼4中的兩個elif永遠執行不到,可能這兩個elif原本是無條件的else。

#程式碼4
if(currWeight+deltaW <= 1 and currWeight+deltaW>-1):
    neuron.weights[j] += deltaW
    # neuron.weights[j] = round(neuron.weights[j])
elif(currWeight+deltaW == 1):
    neuron.weights[j] = 1.000
#--------------------------------------------------------------------
if(currWeight+deltaW >= -1):
    neuron.weights[j] -= deltaW
    # neuron.weights[j] = round(neuron.weights[j])
elif(currWeight+deltaW == -1):
    neuron.weights[j] = -1.000

}

結語:

{

    本次就先更新這麼多,我先消化一下,剩下的下次再更。

    參考資料:

    {

    }

}