下面内容,算是一个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_stdself.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 xdef loss(self,x,t):#one-hoty = self.predict(x)loss = self.final_layer.forward(y,t)return lossdef accuracy(self,x,t):y = self.predict(x)y = np.argmax(axis=1)if t.ndim != 1:#one-hot to numt = 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 accdef 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 gradsdef gradient(self,x,t):#backpropagationgrads = {}loss = self.loss(x,t) # 也许这里是省略了,看最后怎么用吧dout = 1.0dout = self.final_layer.backward(dout)layers = list(self.layers.values())layers.reverse()#reverse in placefor layer in layers:dout = layer.backward(dout)grads['W1'] = self.layers['Affine1'].dWgrads['b1'] = self.layers['Affine1'].dbgrads['W2'] = self.layers['Affine2'].dWgrads['b2'] = self.layers['Affine2'].dbreturn grads
用书中代码的两层网络,一般校验结果在1e-6~1e-10的级别。发现自己的偶尔在1e-6左右,经常在0.01这个级别,两层网络,grad从1.0起,能到0.01差距,可以说非常大了,但是死活看不出哪有问题。
有些代码虽然写的不一样,但是看不出实际问题,比如他的softmax内部是转置做的,再转回来,我是直接做的,axis都能对应上。
跟踪数据,发现softmaxBP的梯度还相似,一到Affine层马上就不同了,跟起来发现变量太多,所以逐步排除随机性,保持一致性,查找原因。
seed固定,mnist中data固定,网络层参数定义顺序固定,batch_size改成1,便于观察
首先,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.Tx = 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_areturn y
def softmax_no_batch(a):#accords to bookmax_a = np.max(a)exp_a = np.exp(a - max_a)sum_exp_a = np.sum(exp_a)y = exp_a / sum_exp_areturn 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 ydef softmax_batch(a):#自己做一版直接的向量化,效率会geng好么?numpy,whateverif a.ndim == 1:return 0max_a = np.max(a,axis=1)max_a = max_a.reshape(max_a.size,1)#根据batch分别做# exp_a_input = a - max_a#for debugexp_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_areturn 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.0001grad = np.zeros_like(x)for idx in range(x.size):tmp_val = x[idx]x[idx] = float(tmp_val) + hfxh1 = f(x) # f(x+h)x[idx] = float(tmp_val) - hfxh2 = f(x) # f(x-h)grad[idx] = (fxh1 - fxh2) / (2 * h)x[idx] = tmp_val # 还原值return graddef 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还原,而我因为没碰到不匹配的情况,没实现这个操作。
验证完了,自然是可以用效率更高的反向传播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/