python手写神经网络之手动微分与梯度校验——《深度学习入门——基于Python的理论与实现(第五章)》

23 篇文章 1 订阅
21 篇文章 1 订阅

下面内容,算是一个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/

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值