python手寫神經網絡之手動微分與梯度校驗——《深度學習入門——基於Python的理論與實現(第五章)》

下面內容,算是一個debug過程,但是也算是一個學習過程,瞭解梯度校驗的過程和影響微分梯度計算的因素。

 

下邊是根據書本模仿的兩層網絡,並非抄原代碼,所以有所不同,但是我主觀覺得差不多(有幾個接口暫不列出),但是代碼不是敲出來不報錯就行了,這個既然是兩種梯度,那就肯定是用來梯度校驗的,既然是梯度校驗,那當然得真校驗一把纔行啊。

兩層網絡(784,50,10)。網絡全隨機,不訓練,從mnist拿一個batch,做一次analytic grad,一次numerical grad,對比絕對值差距,校驗反向傳播。結果,一試,果然不正常了。

class TwoLayerNet():
    def __init__(self,input_size,hidden_size,output_size,weight_init_std=0.01):
        self.params = {}
        self.params['W1'] = np.random.randn(input_size,hidden_size) * weight_init_std
        self.params['b1'] = np.random.randn(hidden_size)
        self.params['W2'] = np.random.randn(hidden_size,output_size)
        self.params['b2'] = np.random.randn(output_size)

        self.layers = OrderedDict()
        self.layers['Affine1'] = Affine(self.params['W1'],self.params['b1'])
        self.layers['relu1'] = ReluLayer()
        self.layers['Affine2'] = Affine(self.params['W2'],self.params['b2'])

        self.final_layer = SoftmaxWithLoss()
    def predict(self,x):
        for layer in self.layers.values():
            x = layer.forward(x)
        return x
    def loss(self,x,t):#one-hot
        y = self.predict(x)
        loss = self.final_layer.forward(y,t)
        return loss

    def accuracy(self,x,t):
        y = self.predict(x)
        y = np.argmax(axis=1)
        if t.ndim != 1:#one-hot to num
            t = np.argmax(t,axis=1)
        acc = np.sum(y==t) / y.shape[0]#there s no need to cast to float    float(y.shape[0])
        return acc

    def numerical_gradient(self,x,t):
        grads = {}
        # f = lambda x:self.loss(x,t)#這樣寫,求的是對x的導數
        loss_W = lambda W:self.loss(x,t)#
        grads['W1'] = numerical_gradient(loss_W,self.params['W1'])
        grads['b1'] = numerical_gradient(loss_W,self.params['b1'])
        grads['W2'] = numerical_gradient(loss_W,self.params['W2'])
        grads['b2'] = numerical_gradient(loss_W,self.params['b2'])
        return grads
    def gradient(self,x,t):#backpropagation
        grads = {}


        loss = self.loss(x,t) # 也許這裏是省略了,看最後怎麼用吧

        dout = 1.0
        dout = self.final_layer.backward(dout)

        layers = list(self.layers.values())
        layers.reverse()#reverse in place

        for layer in layers:
            dout = layer.backward(dout)
        grads['W1'] = self.layers['Affine1'].dW
        grads['b1'] = self.layers['Affine1'].db
        grads['W2'] = self.layers['Affine2'].dW
        grads['b2'] = self.layers['Affine2'].db

        return grads

 

用書中代碼的兩層網絡,一般校驗結果在1e-6~1e-10的級別。發現自己的偶爾在1e-6左右,經常在0.01這個級別,兩層網絡,grad從1.0起,能到0.01差距,可以說非常大了,但是死活看不出哪有問題。

有些代碼雖然寫的不一樣,但是看不出實際問題,比如他的softmax內部是轉置做的,再轉回來,我是直接做的,axis都能對應上。

跟蹤數據,發現softmaxBP的梯度還相似,一到Affine層馬上就不同了,跟起來發現變量太多,所以逐步排除隨機性,保持一致性,查找原因。

 

排除隨機變量:

seed固定,mnist中data固定,網絡層參數定義順序固定,batch_size改成1,便於觀察

 

不同點——loss:

首先,FP的loss就不同,雖然BP是初始化1.0,但是還是先排查一下原因,首先,網絡未經訓練,所有層輸出概率接近0.1,其次,使用softmax,那麼-tlog(y)就是-log(0.1),也就是2.3,利用書本代碼,是可以得到2.3的loss的,但是我的代碼沒得到。

CE的實現

前者把one-hot轉成了下標,後者我的實現繼續用one-hot,但是t的其他下標都是0,所以應該結果相同。

-np.sum(np.log(y[np.arange(batch_size), t] + 1e-7)) / batch_size
return -np.sum(t*np.log(y+1e-7)) / float(batch_size)

 

 

他的代碼中,softmax之後,y是平均的,而我的y直接就不對了,[1]特別大,84%的概率

 

書上給出的向量化操作

def softmax(x):
    if x.ndim == 2:
        x = x.T
        x = x - np.max(x, axis=0)
        y = np.exp(x) / np.sum(np.exp(x), axis=0)
        return y.T 

    x = x - np.max(x) # 溢出對策
    return np.exp(x) / np.sum(np.exp(x))

 

自己實現的softmax,因爲當時的章節沒提到向量化,所以自己做了幾個版本的改進,速度倒是上去了,但是不明白爲什麼會有差別

def softmax_old(a):
    exp_a = np.exp(a)
    sum_exp_a = np.sum(exp_a)
    y = exp_a / sum_exp_a
    return y
def softmax_no_batch(a):#accords to book
    max_a = np.max(a)
    exp_a = np.exp(a - max_a)
    sum_exp_a = np.sum(exp_a)
    y = exp_a / sum_exp_a
    return y
def softmax(a):#書上沒有給出向量化的代碼,這是自己簡單寫的,原來代碼是錯的,不針對batch數據
    if a.ndim == 1:
        return softmax_no_batch(a)
    y = np.zeros_like(a,dtype=np.float64)
    for i in range(a.shape[0]):
        y_i = softmax_no_batch(a[i])
        y[i] += np.array(y_i)
    return y

def softmax_batch(a):#自己做一版直接的向量化,效率會geng好麼?numpy,whatever
    if a.ndim == 1:
        return 0
    max_a = np.max(a,axis=1)
    max_a = max_a.reshape(max_a.size,1)#根據batch分別做
    # exp_a_input = a - max_a#for debug
    exp_a = np.exp(a - max_a)#
    sum_exp_a = np.sum(exp_a,axis=1)
    sum_exp_a = sum_exp_a.reshape(sum_exp_a.size,1)#應該還有一個直接增維的
    y = exp_a / sum_exp_a
    return y

我目前用的softmax_batch版本,肉眼區別除了我分步驟,主要就是他先轉換了軸,盲猜是向量化的減法默認的軸錯了?但是實際看了一下,在softmax之前,應該已經不正常了,因爲有一個分類特別高,佔比84%,或者說,其他都是負數,他是8.04,所以問題應該出在前向傳播,affine層和relu層。

但是隱藏層最不好跟,因爲邏輯是不透明的,也不可能肉眼比數據,一層50,一層10,看不過來。直接就到結果,結果就是每個類的得分不平均。

但是affine層和relu層是最不容易出錯的,relu層就是一個mask和max操作,affine層就是一個乘法,最後發現,我的W2在初始化的時候,沒有*weight_init_std,所以是我的W2的scale大了?

做如下糾正

可見,量級是糾正了,不會有一個類直接0.84的那種貧富差距了,但是和概率預期不一樣,應該全接近0.1,尤其是,同樣的seed下,書本代碼就能接近0.1

下標5對應label,cross entropy應該是-log(0.026),

所以說,至少cross entropy,我的實現沒錯。

目前的問題仍然是,隱藏層輸出的結果,不均勻,而同樣初始化下,書本代碼是均勻的!

然後又試了一個土法,肉眼觀察,發現了大問題:我的b是np.random.randn()產生的,更可怕的是,b沒有乘以weight_init_std,也就是說b的初始化量級比w還大得多,書的原碼其實是用的zeros!

找到問題所在,下面打算循序漸進,逐步縮放b,看一下softmax是否會逐漸變得平均。

b先縮小10倍,已經是肉眼可見的有變均勻的趨勢了

縮小100倍,同w,已經足夠平均了

改成0初始化,確實更均勻了點

總結:侷限性

這是訓練前的網絡,這樣的結果只是一個數學期望的不同,並不是說那樣的初始化就一定不行(但是從邏輯上,確實很不行)。只是爲了解決網絡差別,我的核心問題是兩個網絡的梯度校驗結果偏差很大。

 

回到正題:我是做梯度校驗的

現在,解決了b的初始化問題,直接做一遍梯度校驗,貌似就沒問題了,1e-10的量級。成功了。

{'W1': 4.646074148956723e-10, 'b1': 3.3745217775269993e-09, 'W2': 7.674316464682633e-09, 'b2': 1.7980201277717489e-07}

 

這本書我個人認爲有很多不嚴謹的地方或者叫照顧不到的地方,他的宏觀的東西一般都不會錯,但是微觀的東西,你卻未必真的能理解,順序本身沒錯,只是有很多細節,需要你自己想到!(比如微分計算,我提過https://blog.csdn.net/huqinweI987/article/details/102858397),這裏也是,這不是網絡訓練後的樣子,一點初始化的小差別(也許是本來合理的操作,比如b的不同初始化,但是卻會直接引起梯度校驗結果不符合預期,但是b的不同本身也不算錯誤,網絡訓練後是可能糾正的,那時候運行同樣的代碼,可能結果就符合預期了,是不是很神奇(迷茫)?)

不過這個順序也有他的道理,大規模網絡訓練很慢,梯度校驗本來就是驗證你的反向傳播是否正確的,如果你不確定反向傳播是否正確,卻拿它來訓練網絡,那怎麼能得到一個正確的網絡,從而讓你再去校驗呢?一來邏輯悖論,二來本末倒置,本來就是應該預先檢查!

 

 

回顧&反思:

讓b維持randn初始化,不用zeros,發現偏差也比最初小了(因爲排查問題的時候,我先解決W2的標準化,後解決概率期望問題,所以忽略了W2帶來的改善,其實這是最核心的問題),所以問題分兩方面,一方面,b的初始化確實影響精度,另一方面,我前邊能初始1e-2~1e-5的量級,還是代碼錯了,核心問題就是W2的標準化(*0.01)。

上:randn初始化b;中:zeros初始化b;下:去掉W2的標準化

{'W1': 2.3271566726132363e-09, 'b1': 1.6902964365753116e-08, 'W2': 3.4771239819206885e-07, 'b2': 7.405936823164441e-07}
{'W1': 4.646074148956723e-10, 'b1': 3.3745217775269993e-09, 'W2': 7.674316464682633e-09, 'b2': 1.7980201277717489e-07}
{'W1': 0.0030875882040971845, 'b1': 0.02242550602948008, 'W2': 0.002463212042766619, 'b2': 0.006945896231676416}

 

額外的,爲什麼b影響梯度準確性?(當然,這不影響實質,這個量級已經足夠驗證梯度了)因爲無論b是什麼,導數都不會變,但是微分呢,如果b的scale比較大,那麼可以預見,n維的向量,未經標準化(相對來說,畢竟初始化的也是標準分佈,但是不夠小,相對來說不夠標準化)(說人話,同樣h=1e-7,可能一個維度tmp_val接近0以至於損失了精度,一個維度tmp_val非常大)卻用着同樣大小的h,每個維度都會因爲沒有標準化和計算精度損失而逐步放大偏差。但是這不是出現前邊錯誤的根本,錯誤的根本還是w的初始化問題,因爲b隻影響一條曲線在縱軸的“高低”,而w是影響斜率,從而更大幅度的影響橫軸在某個點時,y在縱軸的“高低”。

 

下邊是微分代碼,按維度改變每x向量的每一個值,把每個維度的微分都記錄下來,就是最終的結果。而analytic gradient梯度,直接用人的經驗去寫反向傳播代碼完成。

def _numerical_gradient_no_batch(f, x):
    h = 1e-4  # 0.0001
    grad = np.zeros_like(x)

    for idx in range(x.size):
        tmp_val = x[idx]
        x[idx] = float(tmp_val) + h
        fxh1 = f(x)  # f(x+h)

        x[idx] = float(tmp_val) - h
        fxh2 = f(x)  # f(x-h)
        grad[idx] = (fxh1 - fxh2) / (2 * h)

        x[idx] = tmp_val  # 還原值

    return grad


def numerical_gradient(f, X):
    if X.ndim == 1:
        return _numerical_gradient_no_batch(f, X)
    else:
        grad = np.zeros_like(X)

        for idx, x in enumerate(X):
            grad[idx] = _numerical_gradient_no_batch(f, x)

        return grad

 

所以,還是要自己多動手,尤其不要照抄代碼,要手動實現,看看你想的方法對不對,錯哪了,不然就成了背代碼了,還很難背,你以後實際運用還會出錯。之前都在說w怎麼初始化,b怎麼初始化,但是到底有多少影響,不自己跟蹤過輸出,很難有一個宏觀概念,可能大家都是隨便一初始化,然後用框架產生梯度,也不用校驗,然後w和b經過訓練也收斂了,所以就認識不到這一步。況且,這已經是最簡單的網絡了,複雜的網絡想跟蹤問題會更麻煩,有可能需要逐層跟蹤輸出分佈,反向傳播也一樣。

 

 

其他不同點,但是暫時也沒影響:

比如softmax內部的實現,比如affine層他保留了original shape,反向傳播時要拿這個shape還原,而我因爲沒碰到不匹配的情況,沒實現這個操作。

 

後續:根據驗證結果使用反向傳播,loss和前邊的呼應

驗證完了,自然是可以用效率更高的反向傳播gradient(),而摒棄numerical_gradient()了。

訓練過程就是用上前邊的gradient方法直接反向傳播,然後更新參數

loss_history = []
accuracy_history = []

#iterate train
for i in range(iterations):
    mask = np.random.choice(x_train.shape[0],batch_size)
    x_batch = x_train[mask]
    t_batch = t_train[mask]
    grads = net.gradient(x_batch,t_batch)
    for k in net.params:
        net.params[k] -= lr * grads[k]
    if i % print_iterations == 0:
        loss = net.loss(x_batch,t_batch)#暫時用batch來打印一下
        loss_history.append(loss)
        accuracy = net.accuracy(x_batch,t_batch)
        accuracy_history.append(accuracy)

 

因爲我的cross entropy是除以batch size的,所以很容易通過曲線看到,batch的loss,初始值,和前邊的計算是一致的-log(0.1)=2.3。

 

 

本文代碼

https://github.com/huqinwei/python_deep_learning_introduction/blob/master/chap05_NN-2layer_grad_check.py

https://github.com/huqinwei/python_deep_learning_introduction/blob/master/chap05_NN_2layer_train.py

更多本書相關代碼

https://github.com/huqinwei/python_deep_learning_introduction/

 

 

 

 

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