手寫識別優化
前言
之前實現的神經網絡還有很多可以優化的地方。本文主要正對其進行優化。
初始化
在訓練神經網絡之前參數初始化。這裏的參數就是w,b。
w,b的初始化。
權重w初始化基於正態分佈模型(高斯分佈):
以0爲均值,1爲方差然後除以當前神經元的個數的算數平方根。
偏執b初始化基於高斯模型:
以0爲均值,1爲方差。
公式爲:
爲什麼?
首先觀察一下高斯分佈的函數圖像
符合高斯分佈的數據也就是說,數據的大部分點在中間“山峯”的區域。高斯分佈就是說兩端的很少,中間的很多。這跟人類社會的2 8法則一項。頂尖的人很少,智障也很少。大家都是中庸的。包括看客和我。
在這裏w和b 會對神經元的輸出產生影響。w稱爲權重,我們也可以理解爲神經元的重要性(因爲w越大輸出越大,w越小輸出越小)。
因爲我們在沒有訓練模型之前最好是對每個神經元都不“偏心”。那麼我們乾脆就認爲大家都差不多,差不多的那就是中庸的。那就符合高斯分佈好啦。有因爲權值不能過大,大了收斂很慢。而且難算,那麼幹脆除一個定值(當前層的節點個數)好了。
import random
import numpy as np
class Network(object):
def __init__(self, sizes):
"""
基本參數初始化
:param sizes:
"""
# 網絡層數
self.num_layers = len(sizes)
# 網絡每層神經元個數
self.sizes = sizes
#調用權重初始化函數
self.default_weight_initializer()
def default_weight_initializer(self):
"""
權重和偏置的初始化
:return:
"""
# 初始化每層的偏置
self.biases = [np.random.randn(y, 1) for y in self.sizes[1:]]
# 初始化每層的權重
self.weights = [np.random.randn(y, x)/np.sqrt(x)
for x, y in zip(self.sizes[:-1], self.sizes[1:])]
正則化
正則化的目的就是爲了提高模型的泛化能力,一定基礎上防止過擬合。
L1正則化
在前面的博文中我們定義了LOSS函數,也就是COST函數。
這個應該很熟悉了,就是每個神經元真實標籤值和預測值之間的gap。(這裏只是討論待定係數,還是忽略激勵函數的存在)。
我們姑且把這個損失函數成爲C0
加了正則項之後損失函數變形:
我們很容易看出損失函數其實就是多了後面一項。
其中的amda不是學習率是我們手動設置的一個參數。
還是老規矩,我們對這個COST求偏w
其中sgn(x)是一個 函數。也稱做符號函數sign函數。就是改變輸入的符號。正號變成負號,負號變成正號。
函數表達式爲:
函數圖像爲:
那我們的權重更新也發生了變化。
式子中的前面兩項已經很熟悉了,就是梯度下降。關鍵是後面加了一個東西。這個作用是什麼樣子的呢?
還記得一元凸函數的樣子
我們最終目的就是要讓cost滾到谷底。
第一種情況:
當w是個負數的時候,並且COST在山谷的左邊的時候!w應該往右邊滾動。
由梯度下降的知識可以知道前面兩項就是可以讓其向右邊挪動。
因爲第二項偏導數會變成一個負數,正數減去一個負數自然會增大!!!
我們看後面那一項:
因爲w是一個負數,sgn(w)變成了-1。
那麼這裏就是減去一個負數。相當於w往下滾的越快了。
這是怎麼回事,那麼會不會滾來滾去導致無法收斂呢?
別急當w在這個點的時候。
w也會向右邊滾動(同理),但是我們現在sgn(x)就是一個正數了。那麼現在往右邊挪就不那麼順暢了,這個第三項那個正數會狠狠的拉它一把。讓它滾向右邊的步伐縮小。
還不明白?看第二種情況!
第二種情況:
當w在山谷的右邊的時候,應該往左邊滾動。
由梯度下降的知識就知道,因爲偏導數是個正數自然會向左邊挪動。這裏的sgn(x)也是個正數。
那麼w會加速往左邊滾。
總結一下這個sgn(x)就是爲了讓w爲負數的時候迅速增大,w爲正數的時候迅速見效。讓w收斂的同時儘可能的等於0。
以前梯度下降很單純往谷底走就好了
現在 谷底和原點就兩個磁鐵一樣吸引着我們的w
w會找到一個比較合適的中間值停下來,從而在一定程度上面避免過擬合!!!
以上是一元凸函數而且只有一個參數,我們上升一下維度(盜個圖)
其中彩色的波浪叫做損失等高線(藉助地理的概念)。就是說在w在一個圈上面產生的cost是一樣的。那麼我們的w應該儘量靠近紫色的圈圈。這個時候損失會最小。
但是這裏加了一個正方形也就是w的絕對值圍城的正放形。同樣的它想要w儘可能的靠近它的圓心。於是就出現了上述的情況。w爲了滿足這兩個條件就在黑點的地方停下來了。正則項成功的限制住了w。讓其一定程度上避免過擬合的發生。
L2正則
與L1正則是一樣的。也是爲了避免過擬合。
看公式
後面不再是絕對值。而是w的平方。前面的2n純屬爲了求導方便。
我的理解就是
L2正則就是爲了方便計算
其餘的跟L1正則沒有任何區別!!!
那就看看計算
1、求偏導
2、設置更新函數
是不是基本一毛一樣!大家可以根據L1正則和梯度下降的法則分析一下過程。其實L2正則也是限制W的。(又偷個圖)
這一次再是個圓。其餘一樣的!!
代碼部分
說了這麼其實多代碼其實超級簡單,在我們之前實現的代碼基礎上加一行即可,就是是w的更新公式改改!!!
class Network(object):
def __init__(self, sizes):
# 網絡層數
self.num_layers = len(sizes)
# 網絡每層神經元個數
self.sizes = sizes
self.default_weight_initializer()
def default_weight_initializer(self):
# 初始化每層的偏置
self.biases = [np.random.randn(y, 1) for y in self.sizes[1:]]
# 初始化每層的權重
self.weights = [np.random.randn(y, x)/np.sqrt(x)
for x, y in zip(self.sizes[:-1], self.sizes[1:])]
# 隨機梯度下降
def SGD(self, training_data, epochs, mini_batch_size, eta,lmbda=0.0,
test_data=None):
if test_data: n_test = len(test_data)
# 訓練數據總個數
n = len(training_data)
# 開始訓練 循環每一個epochs
for j in xrange(epochs):
# 洗牌 打亂訓練數據
random.shuffle(training_data)
# mini_batch
mini_batches = [training_data[k:k + mini_batch_size]
for k in range(0, n, mini_batch_size)]
# 訓練mini_batch
for mini_batch in mini_batches:
self.update_mini_batch(mini_batch, eta,lmbda,n)
if test_data:
print "Epoch {0}: {1} / {2}".format(
j, self.evaluate(test_data), n_test)
print "Epoch {0} complete".format(j)
# 更新mini_batch
def update_mini_batch(self, mini_batch, eta,lmbda,n):
# 保存每層偏倒
nabla_b = [np.zeros(b.shape) for b in self.biases]
nabla_w = [np.zeros(w.shape) for w in self.weights]
# 訓練每一個mini_batch
for x, y in mini_batch:
delta_nable_b, delta_nabla_w = self.update(x, y)
# 保存一次訓練網絡中每層的偏倒
nabla_b = [nb + dnb for nb, dnb in zip(nabla_b, delta_nable_b)]
nabla_w = [nw + dnw for nw, dnw in zip(nabla_w, delta_nabla_w)]
# 更新權重和偏置 Wn+1 = wn - eta * nw 我們只改了這裏
self.weights = [w -eta*(lmbda/n)*w-(eta / len(mini_batch)) * nw
for w, nw in zip(self.weights, nabla_w)]
self.biases = [b - (eta / len(mini_batch)) * nb
for b, nb in zip(self.biases, nabla_b)]
# 前向傳播
def update(self, x, y):
# 保存每層偏倒
nabla_b = [np.zeros(b.shape) for b in self.biases]
nabla_w = [np.zeros(w.shape) for w in self.weights]
activation = x
# 保存每一層的激勵值a=sigmoid(z)
activations = [x]
# 保存每一層的z=wx+b
zs = []
# 前向傳播
for b, w in zip(self.biases, self.weights):
# 計算每層的z
z = np.dot(w, activation) + b
# 保存每層的z
zs.append(z)
# 計算每層的a
activation = sigmoid(z)
# 保存每一層的a
activations.append(activation)
# 反向更新了
# 計算最後一層的誤差
delta = self.cost_derivative(activations[-1], y) * sigmoid_prime(zs[-1])
# 最後一層權重和偏置的倒數
nabla_b[-1] = delta
nabla_w[-1] = np.dot(delta, activations[-2].transpose())
# 倒數第二層一直到第一層 權重和偏置的倒數
for l in range(2, self.num_layers):
z = zs[-l]
sp = sigmoid_prime(z)
# 當前層的誤差
delta = np.dot(self.weights[-l+1].transpose(), delta) * sp
# 當前層偏置和權重的倒數
nabla_b[-l] = delta
nabla_w[-l] = np.dot(delta, activations[-l - 1].transpose())
return (nabla_b, nabla_w)
注意!這代碼是不能允許的啊。只是結合之前的做一下優化,看一下修改的地方即可!