CS231n Spring 2019 Assignment 2—Fully-Connected Neural Nets(全连接神经网络)


还是纪念一下今天早上电脑卡机按了一下强制关机就开不起来的经历吧,结果是供电线路出问题了,╮(╯▽╰)╭,该修还得修,没有电脑没法过日子啊。


Fully-Connected Neural Nets(全连接神经网络)

这一次是作业2中的Fully-Connected Neural Nets(全连接神经网络),具体要完成的就是FullyConnectedNets.ipynb,里面要完成的内容较多,我是花了3天时间才完整做完的。与之前最大不同就是:这次全是通过模块化设计,在一些网络结构里面不再把训练部分也加到函数里面,而是单独拿出来成为一块,通过前后向函数的堆叠,完成网络的设计,封装得更好了。这里还是按作业的顺序来介绍:

Affine layer: foward/backward

这里自己将它:仿射层,其实就是一个线性变换,在layers.py里面(这里面都是一些最小单位的层)编写前后向的仿射变换,虽然输入的x不是二维的,但是可以经过reshape/view来变成二维的,跟之前的作业很像:
affine_forward(x, w, b) --->return out, cache

# *****START OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****

out = np.dot(x.reshape(x.shape[0], -1), w) + b

# *****END OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****

反向传播这里也写得模块化多了,传入的参数就是上游来的梯度dout和前向传播时保存的用来反向传播时用的缓存cache。代码不难:
affine_backward(dout, cache)--->return dx, dw, db

# *****START OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****

dx = np.dot(dout, w.T)
dx = dx.reshape(x.shape)
dw = np.dot(x.reshape(x.shape[0], -1).T, dout)
db = np.sum(dout, axis=0)

# *****END OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****

ReLU activation: forward/backward

以前我们是用

np.maximum(0, x)

来实现ReLU()函数的,现在把它封装一下当做一个层来操作,实际操作都是一样的:relu_forward(x)--->return out, cache

# *****START OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****

out = np.maximum(0, x)

# *****END OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****

ReLU()函数的反向传播其实就是对矩阵中的每一个元素进行一个max gate的反向传播,就是只让正向时大于0的位置得到上游梯度,这里有一个mask的思想:
relu_backward(dout, cache)--->return dx

# *****START OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****

dx = dout * (x > 0)

# *****END OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****

"Sandwich" layers

顾名思义,就是像三明治一样进行各层的堆叠,将之前的Affine foward/backward layer和ReLU forward/backward activation layer进行再一次的组合封装,在layer_utils.py里面就都是简单层的复合。这里面基本上就是调用layers.py里面已经定义好的层就行,反向的时候记得按着逆序一步一步来就行:
affine_relu_forward(x, w, b)--->return out, cache

def affine_relu_forward(x, w, b):
    """
    Convenience layer that perorms an affine transform followed by a ReLU

    Inputs:
    - x: Input to the affine layer
    - w, b: Weights for the affine layer

    Returns a tuple of:
    - out: Output from the ReLU
    - cache: Object to give to the backward pass
    """
    a, fc_cache = affine_forward(x, w, b)
    out, relu_cache = relu_forward(a)
    cache = (fc_cache, relu_cache)
    return out, cache

affine_relu_backward(dout, cache)--->return dx, dw, db

def affine_relu_backward(dout, cache):
    """
    Backward pass for the affine-relu convenience layer
    """
    fc_cache, relu_cache = cache
    da = relu_backward(dout, relu_cache)
    dx, dw, db = affine_backward(da, fc_cache)
    return dx, dw, db

之后就是一个loss层,有softmax loss的,也有svm loss的,因为这在之前已经写过了,教程中在layers.py的最后也直接给出了,但还得看一遍熟悉一下传入返回参数啥的:

svm_loss(x, y)--->return loss, dx
softmax_loss(x, y)--->return loss, dx

Two-layer network

之前在作业1的时候也完成过一次两层全连接网络的编写two_layer_net.ipynb,但这次得implement modular versions。在fc_net.py完成TwoLayerNet类的编写,主要是一个参数的初始化还有loss和grad的计算,这两层网络的结构就是affine - relu - affine - softmax,可以考虑调用前面定义好的affine_relu_forwardaffine_forwardsoftmax_loss函数拼接而成,初始化的np.random.randn()函数返回的是指定shape的标准正太分布的numpy数组,代码如下:

# *****START OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****

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)

# *****END OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****


# *****START OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****

W1, b1 = self.params['W1'], self.params['b1']
W2, b2 = self.params['W2'], self.params['b2']

# affine_relu_layer
out1, cache1 = affine_relu_forward(X, W1, b1)
# affine_layer
out2, cache2 = affine_forward(out1, W2, b2)
scores = out2

# *****END OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****


# *****START OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****

# compute loss
data_loss, dscores = softmax_loss(scores, y)
reg_loss = 0.5 * self.reg * (np.sum(W1 * W1) + np.sum(W2 * W2))
loss = data_loss + reg_loss

# compute grads
dhidden, grads['W2'], grads['b2'] = affine_backward(dscores, cache2)
grads['W2'] += self.reg * W2

dX, grads['W1'], grads['b1'] = affine_relu_backward(dhidden, cache1)
grads['W1'] += self.reg * W1

# *****END OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****

可以看到在TwoLayerNet类里面没有了train函数,这次也是通过在solver.py定义Solver类来进行模型的训练优化。它的类结构是这样的:

|——class Solver(object)
   |—— __init__():初始化一些参数
   |—— _reset():设置一些用来记录的量
   |—— _step():用优化方法更新一步
   |—— _save_checkpoint():保存模型用,可选
   |—— check_accuracy():在提供的数据上测试准确率
   |—— train():训练模型,会调用前面的_step()函数

额,这个类也不需要我们自己写,只需要会部署就好了,下面的代码里面展示了我训练的参数,在10个epoch之后在验证集上达到了51.2%的准确率,达到教程50%的要求:

# *****START OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****

solver = Solver(model, data, update_rule='sgd', 
                optim_config={'learning_rate':1e-3,}, 
                lr_decay=0.95, num_epochs=10, 
                batch_size=100, print_every=100) #实例化一个对象
solver.train() # 调用Solver类方法train()进行训练

# *****END OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****

Multilayer network

接下来难度进一步加大,上面是定义两层的全连接神经网络,这里是要定义任意多层的神经网络,不过得益于之前的模块化设计,这里的多层网络设计起来也并不困难。跟TwoLayerNet很像,这里的FullyConnectedNet主要也是在fc_net.py中完成初始化和loss、grad的计算,其中就涉及到前向和反向传播。网络架构为{affine - [batch/layer norm] - relu - [dropout]} x (L - 1) - affine - softmax,可以考虑用affine_forwardsoftmax_lossrelu_forward来搭建,至于batch/layer norm和dropout可以参见下次的博文,这里可以先不写,但可以用pass来进行占位,提示下次该补全的地方。因为我是全部写好后来写的博客,我就已经写好了。结果太长,这里就不放了,具体可见fc_net.py

Update rules

这次作业的一大重点就是优化方法的编写,主要有SGD+Momentum、RMSProp和 Adam,这一部分主要出现在

网上相关好文:

SGD+Momentum

原始的SGD(mini-batch)就是x_{t+1}=x_{t}-\alpha \nabla f(x),非常朴素的梯度下降,但是这会有几个问题:

  1. 在梯度平缓的维度下降非常慢,在梯度险峻的维度容易抖动,见下图
  2. 容易陷入局部极小值或鞍点,在高维空间中,鞍点比局部极小值更容易出现
  3. 选择一个合适的学习率可能是困难的,更新方向完全依赖于当前batch计算出的梯度,因而易不稳定

随机梯度下降可能的问题
之后就有了SGD+Momentum也就是加上了动量的方法(我没有查到到底谁先提出了这个算法,好像momentum运用到网络优化中已经有些年份了,可以参读:On the momentum term in gradient descent learning algorithmsOn the importance of initialization and momentum in deep learning),Momentum算法借用了物理中的动量概念,它模拟的是物体运动时的惯性,有以下几个特点:

  1. 更新的时候在一定程度上保留之前更新的方向,同时利用当前batch的梯度微调最终的更新方向(见下图)
  2. 对于在梯度点处具有相同的方向的维度,其动量项增大,对于在梯度点处改变方向的维度,其动量项减小。因此,我们可以得到更快的收敛速度,同时可以减少摇摆

具体的公式有多种形式(在代码中用了公式(2)中的):
v_{t+1}= \rho v_{t}+\nabla f(x_{t}) \\x_{t+1}=x_{t}- \alpha v_{t+1} \tag{1}
v_{t+1}= \rho v_{t}-\alpha \nabla f(x_{t}) \\x_{t+1}=x_{t}+v_{t+1}\tag{2}

带有动量的SGD方法,左图是经典的动量,右图是Nesterov动量
代码:sgd_momentum(w, dw, config=None)--->return next_w, config

# *****START OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****

v = config['momentum'] * v - config['learning_rate'] * dw
next_w = w + v

# *****END OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****

RMSProp

RMSprop是一个未被发表的自适应学习率的算法,该算法由Geoff Hinton在其Coursera课堂的课程6e中提出,RMSprop其实解决了Adagrad的极速递减的学习率问题,从下图可以看出两者的不同(Adagrad会累加之前所有的梯度平方,而RMSprop仅仅是计算平方梯度的滑动平均值):
E[g^2]_t = 0.9 E[g^2]_{t-1} + 0.1 g^2_t \\ \theta_{t+1} = \theta_{t} - \dfrac{\eta}{\sqrt{E[g^2]_t + \epsilon}} g_{t}

RMSProp与AdaGrad的比较
代码:rmsprop(w, dw, config=None)--->return next_w, config

# *****START OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****

cache = config.get('cache')
cache = config['decay_rate'] * cache + (1 - config['decay_rate']) * (dw ** 2)
next_w = w + -config['learning_rate'] * dw / (np.sqrt(cache) + config['epsilon'])
config['cache'] = cache

# *****END OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****

Adam

自适应矩估计(Adaptive Moment Estimation,Adam)是另一种自适应学习率的算法,Adam对每一个参数都计算自适应的学习率:

  1. 除了像RMSprop一样存储一个滑动平均的历史平方梯度
  2. Adam同时还保存一个历史梯度的滑动平均值,类似于动量,可以说结合了Momentum和RMSprop的特点

完整版的Adam形式
代码:adam(w, dw, config=None)--->return next_w, config

# *****START OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****

config['t'] += 1
config['m'] = config['beta1'] * config['m'] + (1 - config['beta1']) * dw
mt = config['m'] / (1 - config['beta1'] ** config['t'])
config['v'] = config['beta2'] * config['v'] + (1 - config['beta2']) * (dw ** 2)
vt = config['v'] / (1 - config['beta2'] ** config['t'])
next_w = w - config['learning_rate'] * mt / (np.sqrt(vt) + config['epsilon'])

# *****END OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****

Train a good model!

这里是通过FullyConnectedNet构建全连接网络模型,在cifar10上进行训练,要求是在验证集上能达到50%的准确率(现在还没有使用)BatchNormalizationDropout,可能会低一点,你可以在我的基础上细调(包括优化方法,学习率等;我加入了一点计算训练时间的代码),我在验证集上达到了52.9%的准确率,在测试集上达到了52.8%的准确率:

# *****START OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****
# Check train and val accuracy on the first iteration, the last
# iteration, and at the end of each epoch.
# Append loss(not train or val loss) per iteration
import time
tic = time.time()

X_val= data['X_val']
y_val= data['y_val']
X_test= data['X_test']
y_test= data['y_test']

regularization_strength = 1e-2
# weight_scales = [2.5e-2, 5e-2]
# learning_rates = [1e-3, 3e-3, 1e-2]
# batch_sizes = [100]
weight_scales = [5e-2]
learning_rates = [1e-3]
batch_sizes = [200]
for ws in weight_scales:
    for lr in learning_rates:
        for bs in batch_sizes:
            model = FullyConnectedNet([600, 500, 400, 300, 200, 100],
                                     weight_scale=ws,
                                     reg = regularization_strength, dtype=np.float64,
                                     dropout=1, normalization='batchnorm')
            
            solver = Solver(model, data,
                print_every=500, num_epochs=30, batch_size=bs,
                update_rule='adam',
                optim_config={
                  'learning_rate': lr,
                },
                lr_decay=0.95) # decay learning rate when epoch increase 1
            print('weight_scales = %f, learning_rates = %f, batch_sizes = %f' % (ws, lr, bs))
            solver.train()
            y_val_pred = np.argmax(model.loss(X_val), axis=1)
            val_acc = (y_val_pred == y_val).mean()
            print('val_acc = %f' % (val_acc))
            print('=========================================================================')
            if val_acc > best_val:
                best_val = val_acc
                best_model = model

toc = time.time()
total_seconds = int(toc - tic)
hours = total_seconds // 3600
minutes = (total_seconds - hours * 3600) // 60
seconds = total_seconds - hours * 3600 - minutes * 60
print('Training took %d hours %d minutes %d seconds' % (hours, minutes, seconds))
# *****END OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****

形象的动图比较

opt1.gif

opt2.gif

来源:Alec Radford

结果

具体可见FullyConnectedNets.ipynb

链接

前后面的作业博文请见:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,036评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,046评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,411评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,622评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,661评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,521评论 1 304
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,288评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,200评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,644评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,837评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,953评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,673评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,281评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,889评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,011评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,119评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,901评论 2 355

推荐阅读更多精彩内容