大家好,我是Kay,小白一个。以下是我完成斯坦福 cs231n-assignment2-FullyConnectedNets 这份作业的做题过程、思路、踩到的哪些坑、还有一些得到的启发和心得。希望下面的文字能对所有像我这样的小白有所帮助。
在之前我们有训练过一个两层的全连接网络,但是我们把求损失函数和求梯度放在同一个 function 里了,这不够“模块化”,等以后遇到更大型的网络这样的操作效率不高。在这份练习里,我们即将对每一层网络都分开构建前播和后播函数,再组装成一个 fully-connected nets。优化的事就交给接下来的几份训练吧。
(结尾有我个人对“神经网络”的学习心得!)
然而,首先一上来我就遇到了报错:
run the following from the cs231n directory and try again:
python setup.py build_ext --inplace
You may also need to restart your iPython kernel
后来发现核心的 bug 是:
running build_ext
building 'im2col_cython' extension
error: Unable to find vcvarsall.bat
这个错误怎么解决自行百度吧。哎,搞配置是一件特别令人沮丧的事情。
【思路】前播思路类似求 score 函数,后播就是 back propagation。四段代码见下,成功通过。
affine_forward:
x_re = np.reshape(x, (x.shape[0], -1))
out = np.dot(x_re, w) + b
affine_backward:
db = np.sum(dout, axis = 0)
x_re = np.reshape(x, (x.shape[0], -1))
dw = np.dot(x_re.T, dout)
dx = np.reshape(np.dot(dout, w.T), x.shape)
relu_forward:
out = np.maximum(0, x)
relu_backward:
dx = (x > 0) * dout
【思路】其实还是一样的,先定义参数,然后求 scores,然后求 loss,然后求 gradient。
【开始Debug】这里和之前的作业不一样的是,W,b 和 reg 都是“全局变量”,要写成诸如 self.params['W1'] 的形式才能通过!
init:
self.params['W1'] = weight_scale * np.random.randn(input_dim, hidden_dim)
self.params['b1'] = np.zeros(hidden_dim)
self.params['W2'] = weight_scale * np.random.randn(hidden_dim, num_classes)
self.params['b2'] = np.zeros(num_classes)
scores:
hid, cache = affine_relu_forward(X, self.params['W1'], self.params['b1'])
scores, fc_cache = affine_forward(hid, self.params['W2'], self.params['b2'])
loss:
loss, dout = softmax_loss(scores, y)
loss += 0.5 * self.reg * (np.sum(self.params['W1']**2) + np.sum(self.params['W2']**2))
grads:
d_hid, grads['W2'], grads['b2'] = affine_backward(dout, fc_cache)
dx, grads['W1'], grads['b1'] = affine_relu_backward(d_hid, cache)
#add reg
grads['W1'] += self.reg * self.params['W1']
grads['W2'] += self.reg * self.params['W2']
【思路】solver 相当于提供了一个封装好的 API 集,输入一定的数据,就能直接得到整个数据训练后的结果。而完成全连接网络,这里有几个注意点就是:
1.如果有 n 层 hid,那么就会有 n+1 层+若干输入;
2.要处理好每个“第 i 层”的形状大小,还要处理列表和字典使用的下标的关系要对应好(i+=1)。
3. cnt 是隐藏层数、亦是 reLU 需要循环的次数;n 是总层数,亦是参数需要的下标数;在回传时,还要有个 r (= n - 当前层)。
所以还是四步走:init, scores, loss, grads.
init:
in_dim = input_dim #in_dim for starting loop
lays_dims = hidden_dims + [self.num_classes]
for i, out_dim in enumerate(lays_dims, start = 1):
self.params["W%d" % i] = weight_scale * np.random.randn(in_dim, out_dim)
self.params["b%d" % i] = np.zeros(out_dim)
in_dim = out_dim
scores:
n = self.num_layers
cnt = n - 1 #loop reLU n-1 times
cache = {}
hid = X
for i in range(cnt):
i += 1
hid, cache['%d'%i] = affine_relu_forward(hid, self.params['W%d'%i], self.params['b%d'%i])
scores, fc_cache = affine_forward(hid, self.params['W%d' % n], self.params['b%d' % n])
loss & grads:
loss, dout = softmax_loss(scores, y)
d_hid, grads['W%d'%n], grads['b%d'%n] = affine_backward(dout, fc_cache)
for i in range(cnt):
r = cnt-i #reverse the parameter cnt to backpropagate the grads
d_hid, grads['W%d'%r], grads['b%d'%r] = affine_relu_backward(d_hid, cache['%d'%r])
#add the reg!
for i in range(n):
i += 1
loss += 0.5 * self.reg * np.sum(self.params['W%d' % i]**2)
grads['W%d' % i] += self.reg * self.params['W%d' % i]
【思路】
简单来说,SGD容易陷入局部最优解,而采用动量的形式进行更新,我们可以轻易地“滑”过局部解。
v = config['momentum'] * v - config['learning_rate'] * dw
next_w = w + v
RMSProp则是从另一个角度来解决这个问题的,它是在AdaGrad之上发展而来。AdaGrad是将SGD除以一个“梯度累计值“,但是顾名思义该值是”累增“的,所以会导致一个步长越来越短甚至瘫痪掉的问题。RMSProp就是用来改善AdaGrad的这个瘫痪问题,它为”梯度累计值“增加了一个衰减功能,使其不会那么快瘫痪掉。
config['cache']=config['decay_rate']*config['cache']+(1-config['decay_rate'])*dw**2
next_w=w-config['learning_rate']*dw/(np.sqrt(config['cache'])+config['epsilon'])
而Adam是将上方两种方法结合了在一起:它是对动量v进行带衰减的”梯度累计“。但是做到这里还不够,存在有一个小小的瑕疵:由于v是0,那么初始的几次SGD去除这个累计值可能导致步伐迈太大的问题,所以又要引入一个新的参数来解决它。
config['m']= config['beta1']*config['m'] + (1-config['beta1'])*dw
config['v']=config['beta2']*config['v']+(1-config['beta2'])*(dw**2)
mt = config['m'] / (1 - config['beta1']**config['t'])
vt = config['v'] / (1-config['beta2']**config['t'])
next_w=w - config['learning_rate']*mt/(np.sqrt(vt)+config['epsilon'])
【开始 Debug】
这里Adam代码老是出错,百度了一下别人的代码,原来要再加一句:
config['t'] += 1 #why?
【思考提升】我推测是用在for循环时,下标是从0开始数起,所以还要加一。
【思路】
BatchNormalization改变了输入的形状,使之更容易得到妥善的训练:减去均值是为了把数据都移至中心,使其对我们的分类器边缘变动不那么敏感;除以方差是为了使其正态化。简单来说就是要zero mean and unit variance。而值得注意的是,由于训练过程中的io是会改变的,所以BatchNormalization要持续每个层都做一次。
Dropout的话,则是随机丢掉某几个neurons,使整个训练过程不会过拟合。
具体操作的话,那是Q2Q3的事了,这里就不展开了。
【思路】这里以最重要的一个超参数:learning rate为例,我们来讨论一下几个的调参技巧。
首先,一个核心的设计搜索思路,是持续地设定各种随机的数值,然后在整个训练过程中,要分阶段记录这个数值的各种表现情况(比如loss、validation accuracy)。这样做的意义是,很直观地看到我们这个数值的训练情况。
之后,让我们来瞧瞧这段代码:learning_rate = 10 ** uniform(-6, 1)。它有三个优点:范围广、随机、指数。
范围广的优点就不说了,要注意的点是必须大到把整个训练范围都包含住,所以要检查边缘值的表现情况。
随机的话是因为比起均匀地加减固定的值,会帮助我们更容易“踩中”更好的值。
而用指数而不是均匀搜索是因为这个超参数有乘法效应,当值是0.001时,改变0.1就会造成很大的“影响”,但是当值是10的时候改变0.1影响就会很小了。
然后,我要开始总结套路了,调参的具体做法是,跟着我四步走:建立各个超参数list,建立一个model和solver(具体需要的API在文件里有),输入solver.train(),然后比较该份acc和best_acc。
model = FullyConnectedNet([100,100], reg=0.05, weight_scale=5e-3,
normalization='batchnorm', dtype=np.float32, dropout=0.2)
solver = Solver(model, data,
num_epochs=50, batch_size=100,
update_rule='adam',
optim_config={
'learning_rate': 0.00075
},
print_every=100,
lr_decay=0.95,
verbose=True)
solver.train()
best_model = model
到这里,我们算是大致上学完了“神经网络”的基础了。我们学习了: