【学习笔记】cs231n-assignment2-FullyConnectedNets

    前言

大家好,我是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
    这个错误怎么解决自行百度吧。哎,搞配置是一件特别令人沮丧的事情。

 

    TODO1:完成全连接层和 ReLUs 的前播和后播函数

    【思路】前播思路类似求 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


 
    TODO2:利用"模块化"的函数,重构两层神经网络

    【思路】其实还是一样的,先定义参数,然后求 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']


 
    TODO3:阅读 solver,完成 FullyConnectedNet 

    【思路】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]

 

 

 

    TODO4:更好的优化——SGD+momentum、RMSProp & Adam

    【思路】
简单来说,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开始数起,所以还要加一。


    TODO5:更多的优化方案——BatchNormalization、Dropout

    【思路】
BatchNormalization改变了输入的形状,使之更容易得到妥善的训练:减去均值是为了把数据都移至中心,使其对我们的分类器边缘变动不那么敏感;除以方差是为了使其正态化。简单来说就是要zero mean and unit variance。而值得注意的是,由于训练过程中的io是会改变的,所以BatchNormalization要持续每个层都做一次。

Dropout的话,则是随机丢掉某几个neurons,使整个训练过程不会过拟合。

具体操作的话,那是Q2Q3的事了,这里就不展开了。


    TODO6:调节超参数

    【思路】这里以最重要的一个超参数:learning rate为例,我们来讨论一下几个的调参技巧。
首先,一个核心的设计搜索思路,是持续地设定各种随机的数值,然后在整个训练过程中,要分阶段记录这个数值的各种表现情况(比如loss、validation accuracy)。这样做的意义是,很直观地看到我们这个数值的训练情况。
之后,让我们来瞧瞧这段代码:learning_rate = 10 ** uniform(-6, 1)。它有三个优点:范围广、随机、指数。
范围广的优点就不说了,要注意的点是必须大到把整个训练范围都包含住,所以要检查边缘值的表现情况。
随机的话是因为比起均匀地加减固定的值,会帮助我们更容易“踩中”更好的值。
而用指数而不是均匀搜索是因为这个超参数有乘法效应,当值是0.001时,改变0.1就会造成很大的“影响”,但是当值是10的时候改变0.1影响就会很小了。

然后,我要开始总结套路了,调参的具体做法是,跟着我四步走:建立各个超参数list,建立一个modelsolver(具体需要的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


    总结

到这里,我们算是大致上学完了“神经网络”的基础了。我们学习了:

  • 一个神经网络应该有三大基础东西 - 损失函数、梯度、权值(模板)
  • 神经网络将一个函数封装成一个独立的神经元,然后将神经元分层连接,使之高效地传递信息
  • 我们认识了各种激活函数(推荐使用ReLU)
  • 我们可以对初始数据做预处理(推荐使用“减均值+除标准差”)
  • 我们还可以对隐藏层也做类似处理(Batch Normalization)
  • 我们应该对权进行初始化(推荐使用“随机数+除标准差”)
  • 如果train/val之间有gap,就是过拟合了(推荐使用“R2+Dropout”)
  • 在训练中,要对各种参数进行更新(推荐使用Adam)
  • 最后,还有对超参数进行调优的方法(推荐使用四步走)

你可能感兴趣的:(CS231n)