全连接层的作用_python构建计算图2——全连接层
(好久不更~)前文中,參照tensorflow的方式實現(xiàn)了簡單的自動求導。接下來要在自動求導的基底(模板)上搭建簡單的bp神經(jīng)網(wǎng)絡。
計算圖
前文曾多次提到計算圖,關于什么是計算圖,有很多種說法。既然它被稱為圖,便具有圖的基本元素:點和線。如下圖:
點:節(jié)點,用來儲存變量。比如輸入X,隱含層h,輸出y
線(箭頭):操作(算符),用來確定兩個節(jié)點之間的聯(lián)系,或者說由前一個節(jié)點經(jīng)過這個操作后可以得到后面的節(jié)點。
可以利用上一篇構(gòu)造復合函數(shù)的方式來理解它。構(gòu)造函數(shù)f(x),需要自變量x和一個映射f,這里的x是節(jié)點,映射f是操作。如果另y=f(x),那么y就是新的節(jié)點,映射f的箭頭是從x指向y。多個不同的映射可以組成一個大網(wǎng)絡,儲存更復雜的信息。
計算圖的好處在于,它是微分鏈式法則的直觀體現(xiàn),每個節(jié)點的梯度都可以看做上一個節(jié)點的梯度與本層導數(shù)的乘積。因此,計算圖網(wǎng)絡結(jié)構(gòu)建立的同時,每個節(jié)點的梯度也是確定了的。
全連接層
說到全連接層,就不得不提一下BP神經(jīng)網(wǎng)絡。神經(jīng)網(wǎng)絡是模方大腦神經(jīng)元的連接方式,通過建立神經(jīng)元之間的相互聯(lián)系來對某個問題進行建模的方法。BP神經(jīng)網(wǎng)絡是在眾多神經(jīng)網(wǎng)絡中一枝獨秀,利用梯度下降方法,可以最快最準的接近局部最優(yōu)值(缺點當然是容易陷入局部最優(yōu),因此也出現(xiàn)了很多改進算法)。
從最簡單的線性回歸說起。我們有兩組數(shù)據(jù),自變量:x,目標:t。對目標t的估計值為:
估計值和目標之間的均方誤差為:
可以做出loss關于a0和a1的三維曲面:
可以看到他是一個二次函數(shù)形式,必然存在最小值。我們對a0和a1分別求偏導:
最小二乘法是利用這兩個偏導數(shù)等于0求出取到最值時的a0和a1:
而梯度下降法則是將a0和a1按照上面算出的導數(shù),以某個速率向最值點靠近(以他們的導數(shù)乘某個常數(shù)為速率,以梯度的負數(shù)為方向,做爬坡運動)
對于線性回歸問題來說,loss是個拋物線,必然存在最值點:
但是對于更復雜的問題來說,就不一定了,loss不一定是拋物線,可能存在多個極值點,這樣的話最小二乘法便不再好用,但是梯度下降卻可以盡最大可能搜索最優(yōu)值(雖然不容易跳出局部最優(yōu),但還是比最小二乘法要好吧)
但是隨著問題復雜度的提升,自變量和因變量之間也不再是簡單的線性關系,這時就要加入一個激活函數(shù),但是光加入激活函數(shù)只能表示一些簡單的非線性模型,對于更加復雜的非線性模型也無能為力。
所以現(xiàn)在的問題是如何利用簡單的方程表示出復雜的方程。這一點在高等數(shù)學中其實已經(jīng)學到了。對于一個復雜的函數(shù),我們總可以把他展開成級數(shù)的形式,而且我們還可以證明,展開的級數(shù)就是泰勒級數(shù),這個展開也叫做泰勒展開。
我們可以利用最簡單的冪函數(shù)的疊加來趨近一個極其復雜的函數(shù)。那在這個問題上,也可以利用無數(shù)個簡單的非線性函數(shù)的疊加來擬合一個極其復雜的非線性函數(shù),所以就有了下圖:
把n個線性關系疊加起來再通過激活函數(shù)計算得到最終的估計值:
損失值loss(均方差)為:
要繼續(xù)利用梯度下降法,就需要計算loss關于每個w的導數(shù):
這樣就建立了梯度下降與鏈式法則之間聯(lián)系,和上一文《簡單實現(xiàn)自動求導》一模一樣。
這里可以體現(xiàn)出計算圖的好處。我們不需要再手動計算這些復雜的梯度值到底是多少,在計算圖建立的同時,它就已經(jīng)幫我們算好了。我們只需要給他一個學習的指令,他就可以按照我們要求的訓練方式學習。
全連接層,顧名思義,重在“全連接”,他將所有的輸入都和輸出相連,如下圖的兩層神經(jīng)網(wǎng)絡,一個輸入,一個隱含層,一個輸出。
在黑框中可以看到,所有的輸入X都和所有隱含層的節(jié)點相連,所有隱含層的節(jié)點都和輸出y相連。
或許會問,難道還有不全連接的神經(jīng)網(wǎng)絡嗎。當然有,先不說一些復雜的結(jié)果,最基本的卷積神經(jīng)網(wǎng)絡CNN就不是全連接的,它利用卷積核掃描圖像,而卷積核一般都是比圖像小的多的方形如3×3,5×5。
兩層的神經(jīng)網(wǎng)絡理論上可以擬合任意的非線性函數(shù)。在梯度計算方面與之前稍有不同:先看第一層,第j個輸出為:
要計算w的該變量,就需要計算前面那個鏈式表達式,它其中的一項:
所以:
對于很多層的網(wǎng)絡:
我們給定一個變量δ,它實際上是每一層神經(jīng)網(wǎng)絡的梯度,下式為輸出層的梯度:
那么權重改變量:
而上一層的梯度:
權重改變量:
因此,在網(wǎng)絡訓練過程中,先計算每一層的梯度和權重改變量,算完之后再對每一層的權重梯度下降。值得注意的是:不可以每計算一層的權重改變量,就對該層的權重做出改變。這樣會影響后面的計算。
寫到這兒,需要的理論知識已經(jīng)有了。接下來就是如何搭建靜態(tài)圖:
上一文有提到,我搭建的計算圖中只有兩個基本變量:Variable和placeholder。
placeholder作為需要傳入數(shù)據(jù)的變量,Variable則作為需要訓練的變量(這里用不到不需要訓練的變量,不然應該再給一個Constant更完整,我這里其實簡化了很多操作^_^)
與之前相同的是,Variable包括value和grad兩個變量。
不同的是,Variable包含了last,next,root和target四個指針(可以這么叫吧)。last指向前一個節(jié)點,next指向后一個節(jié)點,root指向輸入的placeholder(就是起始節(jié)點),target指向目標的placeholder(要計算loss,就必須要計算輸出和目標之間的差距),一條路徑上的每個節(jié)點的root和target都相同(這是為了方便對每個節(jié)點求值的時候都能從placeholder開始)。
placeholder在圖中只起到占位的作用,它用來確定計算內(nèi)存與復雜度,需要在后續(xù)求值的時候傳入數(shù)據(jù)。每個節(jié)點都有root指針,可以確保在求該節(jié)點值的時候,從它的root開始,先傳入數(shù)據(jù),然后利用next指針指向下一個節(jié)點,計算下一個節(jié)點的值和導數(shù)。
值得一提的是,這里我沒有使用一個全局變量來儲存整個圖結(jié)構(gòu),而是用上述的四個指針將整個計算圖建立起來。類似于{root,next1(last1),next2(last2)。。。target}。
如果不使用root指針,還有兩個方法:
1.儲存整個網(wǎng)絡的圖結(jié)構(gòu),這也是一般計算圖的方案,這樣就可以從圖的起始節(jié)點開始算起。
2.要求一個節(jié)點的值,就要先知道上一個節(jié)點的值,然后帶入上一個節(jié)點的激活函數(shù)才能得到這個節(jié)點的值。同理要知道前一個節(jié)點的值,就要求上上個節(jié)點的值,以此類推。而當上上一個節(jié)點是placeholder時,上上個節(jié)點值就是傳入的數(shù)據(jù),然后再計算上個節(jié)點的值。這不正是遞歸算法嘛,先往回走,走到初值時,再往前走。那用一個root指針豈不是取代了往回走的過程,我直接告訴它該從哪里開始往前走即可。
下面放入代碼:
先說明一下整個代碼的結(jié)構(gòu):
mytensor文件夾中包含五個py文件,__init__用于從tensor文件夾外部調(diào)用內(nèi)部的文件。mytensor(這里不是指文件夾,而是mytensor文件夾內(nèi)的py文件)用來定義基類Varibale,placeholder,一些基本函數(shù),還有不同的Loss。nn用于定義不同的神經(jīng)網(wǎng)絡。train用于定義不同的訓練方式。cnn_func包含卷積中的一些操作(這里不會提到)。
代碼文件mytensor.py
# -*- coding: utf-8 -*- import numpy as np #Variable基類 class Variable:def __init__(self, value=None):self.value = valueself.grad = Noneself.next = Noneself.last = Noneself.root = None self.target = Noneself.eval_func = Lineif isinstance(value, np.ndarray):self.size = value.shapeelse:self.size = None def __add__(self, other):res = Variable()res.root = self.rootself.next, other.next = res, resres.last = selfres.func = lambda x: x + other.valueres.func_grad = lambda x: 1return resdef func(self, X):return 1def func_grad(self, X):return 1def run(self, feed_dict, need_grad=False):#喂入數(shù)據(jù)root = self.root#找到rootroot.value = feed_dict[root]#給root喂入數(shù)據(jù)target = self.target#找到targetif target is not None:target.value = feed_dict[target]#給target喂入數(shù)據(jù)#求值過程while root.next is not self.next: root.next.value = root.next.func(root.value)if need_grad:#如果需要求導root.next.grad = root.grad * root.next.func_grad(root.value)root = root.nextreturn root.value__add__函數(shù)用來定義兩個Variable的相加算法。這里Variable多出了eval_func變量,雖然這里用不到,但后續(xù)有類繼承它時才會用到。
run方法便是在求該節(jié)點值得時候會用到,與之前所說相同,先找到節(jié)點的root,然后從root開始一步一步往前傳,如果需要自動求導的話,need_grad傳入True即可。
定義完基類,接下來就是繼承基類的子類了,首先是placeholder:
###############占位##################### class placeholder(Variable):def __init__(self, size):super().__init__(self)self.size = sizeself.root = selfself.grad = 1然后定義一些全連接層中可能用到的激活函數(shù):
class relu(Variable):def __init__(self, X):super().__init__()X.next = selfself.last = Xself.root = X.rootdef func(self, X):return np.maximum(0, X)def func_grad(self, X):res = np.zeros(X.shape)res[X >= 0] = 1return resclass Line(Variable):def __init__(self, X):super().__init__(self)self.last = Xself.root = X.rootdef func(self, X):return Xdef func_grad(self, X):return np.ones(X.shape)class softmax(Variable):def __init__(self, X):super().__init__(self)X.next = selfself.last = Xself.root = X.rootdef func(self, X):return np.exp(X) / np.sum(np.exp(X), axis = 1).reshape(X.shape[0], 1)def func_grad(self, X):return np.ones(X.shape) class sigmoid(Variable):def __init__(self, X):super().__init__(self)X.next = selfself.last = Xself.root = X.rootdef func(self, X):return 1 / (1 + np.exp(-X))def func_grad(self, X):return self.func(X) * (1 - self.func(X)) class square(Variable):def __init__(self, X):super().__init__(self)X.next = selfself.last = Xself.root = X.rootdef func(self, X):return np.square(X)def func_grad(self, X):return 2 * X寫法與上一文自動求導中定義初等函數(shù)的方法完全一致(多了一個next指針)。
然后定義損失函數(shù):
class MeanSquareLoss(Variable):def __init__(self, yhat, y):super().__init__(self)self.target = yself.last = yhatself.root = yhat.rootyhat.next = selfdef func(self, yhat):return np.mean(np.square(yhat - self.target.value)) / 2def func_grad(self, yhat, grad):return (self.target.value - yhat) * gradclass SoftmaxCrossEntropy(Variable):def __init__(self, yhat, y):super().__init__(self)self.target = yself.last = yhatself.root = yhat.rootyhat.next = selfdef func(self, yhat):return np.mean(-np.log(yhat) * self.target.value)def func_grad(self, yhat, grad): return (self.target.value - yhat)這里不定義損失函數(shù)也是可以的。但是還需要用定義初等函數(shù)的方法寫一下mean函數(shù)(square函數(shù)我倒是寫了)。然后在訓練的時候,可以把loss = MeanSquareLoss(yhat,y)寫為loss = mean(square(yhat - y))
然后是Session。其實我這里寫Session只是為了形式上好看點,失去了tensorflow中Session的意義。。
###############Session#################### class Session:def run(self, operator, feed_dict, need_grad = False):return operator.run(feed_dict, need_grad)上述代碼全部包含在mytensor.py中。
在nn.py文件中,就開始構(gòu)建真正的全連接層了:
# -*- coding: utf-8 -*- #導入一些要用到的函數(shù) from mytensor.mytensor import Variable import mytensor.mytensor as mt import numpy as np #全連接層 class FullConnection(Variable):def __init__(self, X, W, b=None, eval_func=None, need_trans=False):super().__init__(self)X.next = selfself.last = Xself.root = X.rootself.W = Wif b == None:self.b = Variable(np.zeros(W.size[1]))else:self.b = bself.eval_func = eval_funcself.need_trans = need_transdef func(self, X):if self.need_trans:N = X.shape[0]D = np.prod(X.shape[1:])X = np.reshape(X, (N, D))h = X.dot(self.W.value) + self.b.valueif self.eval_func is None:self.eval_func = mt.Lineh = self.eval_func(self.W).func(h)self.value = hreturn hdef func_grad(self, dout, first=False):if first:grad = self.eval_func(self.W).func_grad(self.value)return gradelse: dw = self.last.value.T.dot(dout)db = np.sum(dout, axis = 0)grad = self.last.eval_func(self.W).func_grad(self.last.value)dout = np.reshape(dout.dot(self.W.value.T), self.last.value.shape) * grad return dout, dw, db可以看到全連接層的構(gòu)造和之前的基本函數(shù)大的框架是一致的。構(gòu)造函數(shù)中要有父類的構(gòu)造函數(shù),然后重寫func和func_grad。
不同之處在于,全連接層的變量更多,有權重W,偏置b,激活函數(shù)eval_func。need_trans是針對卷積操作時判斷是否需要把輸入的矩陣轉(zhuǎn)化為一個長鏈的布爾值。
func函數(shù)用來計算節(jié)點的值,就是基本的f(Xw+b)。
而func_grad輸入上一層的梯度值,計算本層的梯度值以及權重和偏置的改變量,并返回。
不管是全連接層還是CNN,RNN。網(wǎng)絡構(gòu)造都是這個模式。構(gòu)造函數(shù)中儲存所有的變量。func中是前傳操作,func_grad中利用上一層的梯度,計算本層的權重,偏置該變量和本層的梯度。
這樣便可以將不同的網(wǎng)絡結(jié)構(gòu)統(tǒng)一起來,讓后續(xù)工作簡單化。
現(xiàn)在可以說是萬事俱備只欠東風了。該定義的都定義了,該搭建的也都搭建好了,我們已經(jīng)可以搭建出任意層數(shù)的神經(jīng)網(wǎng)絡,并且定義它的loss值。可以認為:前傳過程已經(jīng)沒有任何問題了。但是現(xiàn)在需要個一個東西讓他們跑起來——訓練。
train.py中只定義了GradientDecent訓練方式:
# -*- coding: utf-8 -*- #訓練方式 from mytensor.mytensor import Variable #梯度下降法 class GradientDescentOptimizer(Variable):def __init__(self, alpha, loss):super().__init__()self.alpha = alphaself.loss = lossdef run(self, feed_dict, end=False):now = self.lossnow.run(feed_dict)dnow = now.last now.grad = [now.func_grad(dnow.value, dnow.func_grad(None, True))]while dnow is not now.root:dnow.grad = dnow.func_grad(now.grad[0])now, dnow = dnow, dnow.last root = self.loss.rootwhile root.next is not self.loss:try:root.next.W.value += self.alpha * root.next.grad[1]root.next.b.value += self.alpha * root.next.grad[2]except:passroot = root.next它和別的類不同,沒有func和func_grad。但它卻重寫了run方法。畢竟它是優(yōu)化器,與之前的節(jié)點都不同(其實這里不需要繼承父類)。
這里首先要計算完每一層的梯度,每一層權重和偏置的改變量,然后才可以對權重和偏置做出改變,否則會影響每一層梯度的計算。本質(zhì)上這些計算得是并行的,但是由于本層的梯度必須要利用前一層的梯度,擁有串行的性質(zhì)。這就導致了我們必須得寫兩個循環(huán)。
使用try-except是因為,卷積中池化層沒有權重和偏置,為了程序的統(tǒng)一才用這個方法。
這樣所有的方法都已經(jīng)寫好,如果從mytensor文件夾外調(diào)用的就需要__init__.py了。
__init__.py中定義了接口的調(diào)用方式
# -*- coding: utf-8 -*- from .nn import FullConnection from .nn import Conv2D from .nn import MaxPool from .train import GradientDescentOptimizer from .mytensor import Variable, placeholder from .mytensor import matmul from .mytensor import exp, sin, cos, log from .mytensor import square, relu, softmax, sigmoid, Line from .mytensor import MeanSquareLoss, SoftmaxCrossEntropy from .mytensor import Session用mnist數(shù)據(jù)集測試:
# -*- coding: utf-8 -*- ''' mnist測試 ''' import numpy as np import mytensor as mt from sklearn.preprocessing import StandardScaler from sklearn.preprocessing import LabelBinarizer from sklearn import metricsdata = np.load("mnist.npz") def DataStandard(X):return StandardScaler().fit_transform(X) def DataTrans(y):return LabelBinarizer().fit_transform(y)X_train, y_train, X_val, y_val, X_test, y_test = data['X'], data['y'], data['X_val'], data['y_val'], data['X_test'], data['y_test'] X_train, X_val, X_test = DataStandard(X_train), DataStandard(X_val), DataStandard(X_test) y_train, y_val, y_test = DataTrans(y_train), DataTrans(y_val), DataTrans(y_test) BATCH_SIZE = 128 #權重和偏置 W1 = mt.Variable(np.random.uniform(-0.01, 0.01, (784, 128))) b1 = mt.Variable(np.random.uniform(-0.01, 0.01, (128))) W2 = mt.Variable(np.random.uniform(-0.01, 0.01, (128, 10))) b2 = mt.Variable(np.random.uniform(-0.01, 0.01, (10))) #占位符 xs = mt.placeholder((None, 784)) ys = mt.placeholder((None, 10)) #兩層神經(jīng)網(wǎng)絡 h1 = mt.nn.FullConnection(xs, W1, b1, mt.relu) h2 = mt.nn.FullConnection(h1, W2, b2, mt.softmax) #定義loss和train loss = mt.SoftmaxCrossEntropy(h2, ys) train = mt.train.GradientDescentOptimizer(1e-4, loss)sess = mt.Session()start = 0 for i in range(10000): end = start + BATCH_SIZEif end >= X_train.shape[0]:end = X_train.shape[0] - 1X_batch = X_train[start: end]y_batch = y_train[start: end]start = endif start == X_train.shape[0] - 1:start = 0sess.run(train, {xs: X_batch, ys: y_batch})if (i % 100 == 0):los = sess.run(loss, {xs: X_val, ys: y_val})output = sess.run(h2, {xs: X_val})y_pred = np.argmax(output, axis = 1)acc = metrics.accuracy_score(y_pred, np.argmax(y_val, axis = 1))print("times : {}, loss : {}, accuracy : {}".format(i, los, acc))los = sess.run(loss, {xs: X_test, ys: y_test}) output = sess.run(h2, {xs: X_test}) y_pred = np.argmax(output, axis = 1) acc = metrics.accuracy_score(y_pred, np.argmax(y_test, axis = 1)) print("test data: loss : {}, accuracy : {}".format(los, acc))。。。
最后得到93%正確率。其實這在全連接層還算是可以接受的結(jié)果。如果利用卷積池化來處理的話,準確率必然會大大提高。
后記:數(shù)模又雙叒叕參與獎了,懷著悲憤的心情才抽出時間寫下此文。
總結(jié)
以上是生活随笔為你收集整理的全连接层的作用_python构建计算图2——全连接层的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python发布_python网站发布
- 下一篇: 物流设计大赛优秀作品_中国外运杯第七届全