全连接神经网络

先挖个坑。。总算™调出来了。。 明天上午再更
纪念我的跨年调参。。

比之前双层网络更进一步的是,全连接网络需要实现模块化的正向与反向传播,为了实现此目的,每层需要额外加一个cache来存储当前的数据。不像之前的双层网络一样,可以较为方便的手推每一层。
当网络的层数变多的时候,调超参也变得更加困难,因此我们需要一种方法来帮我们更方便地调参。

传播函数

仿射函数的正向传播

就是一种模块化的思想,我们只关心前一层的输出,当层的W、b即可。
代码与之前的类似,算是容易想到的。

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

仿射函数的反向传播

看上去就是和之前写的双层差不多。。除了名字变了变。

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

ReLU激活函数的正向传播

out = np.maximum(x, 0)

ReLU激活函数的反向传播

dx = dout
dx[x <= 0] = 0

可以看到,模块化的设计对于每个模块的实现都比较简单,而且组合也更为方便。

问题1

We’ve only asked you to implement ReLU, but there are a number of different activation functions that one could use in neural networks, each with its pros and cons. In particular, an issue commonly seen with activation functions is getting zero (or close to zero) gradient flow during backpropagation. Which of the following activation functions have this problem? If you consider these functions in the one dimensional case, what types of input would lead to this behaviour?

  1. Sigmoid
  2. ReLU
  3. Leaky ReLU

1会造成梯度消失问题,2、3不会。可以想象图像,2、3在0+附近也是线性的,而sigmoid函数在0+附近非常平缓。
当我们给一个较小的输入的时候(不足以激活sigmoid),就会导致梯度消失。

“三明治”夹层

我们可以把两个模块组合起来形成一个更大的模块。比如,先对输入进行一次仿射,然后再ReLU激活。这是一个比较常见的方法。

使用svm或softmax算法作为损失函数

由于我们之前写过了,这里就直接给我们了。。

双层网络

和之前写的类似,不需要太注释什么了。。

构造函数

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)

得分计算

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

layer1Out, layer1Cache = affine_relu_forward(X, W1, b1)
layer2Out, layer2Cache = affine_forward(layer1Out, W2, b2)
scores = layer2Out

损失及梯度计算

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

dh1, dW2, db2 = affine_backward(dscores, layer2Cache)
dX, dW1, db1 = affine_relu_backward(dh1, layer1Cache)

dW2 += self.reg * W2
dW1 += self.reg * W1

grads["W1"], grads["b1"] = dW1, db1
grads["W2"], grads["b2"] = dW2, db2

Solver

用一个Solver来帮助训练(预先给定好各种超参),需要先熟悉api之后使用。

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()

训练损失图

这里有必要说一下一个好的图应该是什么样子。
trainingLoss
这是与学习速率的关系。
另外一个比较有影响的超参是初始的规模weight_scale,那个超参如果不在合适的位置,一般会导致损失函数并无太大变化,或者变化比较杂乱(个人感觉)。。

实现全连接网络

现在去实现一个任意层数,层任意维数的网络。dropout等规则可以先不涉及。
前N-1层采用仿射+ReLU,最后一层采用仿射。

构造函数

layerDims = [input_dim] + hidden_dims + [num_classes]
for i in range(self.num_layers):
    self.params['W' + str(i + 1)] = weight_scale * np.random.randn(layerDims[i], layerDims[i + 1])
    self.params['b' + str(i + 1)] = np.zeros((1, layerDims[i + 1]))

正向传播计算得分

cache = {} # cache range from [1, N]
inputLayer = X
N = self.num_layers
for i in range(1, N): # 这里只有N-1层
    inputLayer, cache[i] = affine_relu_forward(inputLayer, self.params['W' + str(i)], self.params['b' + str(i)])
outputLayer, cache[N] = affine_forward(inputLayer, self.params['W' + str(N)], self.params['b' + str(N)])
scores = outputLayer

损失和梯度计算

loss, dscores = softmax_loss(scores, y)
loss += 0.5 * self.reg * np.sum(self.params['W' + str(N)] * self.params['W' + str(N)])
dout, dw, db = affine_backward(dscores, cache[N])
grads['W' + str(N)] = dw + self.reg * self.params['W' + str(N)]
grads['b' + str(N)] = db
for i in range(N - 1, 0, -1):
    loss += 0.5 * self.reg * np.sum(self.params['W' + str(i)] * self.params['W' + str(i)])

    dout, dw, db = affine_relu_backward(dout, cache[i])
    grads['W' + str(i)] = dw + self.reg * self.params['W' + str(i)]
    grads['b' + str(i)] = db

测试全连接网络

在实现网络之后,首先先给了我们几个小的demo,看网络有没有能力拟合(甚至于过拟合)这些数据。

3层网络

这是实例代码,当然需要调超参来达到100%的训练集准确率

num_train = 50
small_data = {
  'X_train': data['X_train'][:num_train],
  'y_train': data['y_train'][:num_train],
  'X_val': data['X_val'],
  'y_val': data['y_val'],
}

weight_scale = 1e-2
learning_rate = 9e-3
model = FullyConnectedNet([100, 100],
              weight_scale=weight_scale, dtype=np.float64)
solver = Solver(model, small_data,
                print_every=10, num_epochs=20, batch_size=25,
                update_rule='sgd',
                optim_config={
                  'learning_rate': learning_rate,
                }
         )
solver.train()

5层网络

超参更难调一些。。

learning_rate = 2e-2
weight_scale = 4e-2
model = FullyConnectedNet([100, 100, 100, 100],
                weight_scale=weight_scale, dtype=np.float64)
solver = Solver(model, small_data,
                print_every=10, num_epochs=20, batch_size=25,
                update_rule='sgd',
                optim_config={
                  'learning_rate': learning_rate,
                }
         )
solver.train()

问题2

Did you notice anything about the comparative difficulty of training the three-layer net vs training the five layer net? In particular, based on your experience, which network seemed more sensitive to the initialization scale? Why do you think that is the case?

相对来说,五层更难调,五层对于weight_scale更加敏感。 在weight_scale = 1e-2附近,无论怎么调learning_rate,学习曲线仍是较平缓的直线,因此weight_scale更能影响,也更难调。
造成这一现象的原因是,层数越多,weight_scale会越来越小。

更新规则

除了朴素的SGD以外,还有许多其它的更新当前位置的规则,可以更快/更优地到达最低点。
原理在notes里都讲了,稍微迁移一下即可

带动量的SGD

这里的momentum,更多上是一种动摩擦系数mu的感觉,即对系统造成一定的能量损失(否则停不下来)。

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

与朴素的SGD比较,带动量的SGD收敛更快、准确率更高。

Adagrad

笔记里并没有要求实现Adagrad,因为这个增长太aggressive了。。

# Assume the gradient dx and parameter vector x
cache += dx**2
x += - learning_rate * dx / (np.sqrt(cache) + eps)

RMSProp

比起上一个,这个更加平和。。

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

笔记上说Adam通常效果更好一些。

config["t"] += 1
config["m"] = config["beta1"] * config["m"] + (1 - config["beta1"]) * dw
# 偏差消除机制,因为初始化的几次m和v都是0,很难有一些较大的改变。
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"])

效果图

这里可以直观地感受一下四种算法的差别(看上去确实adam更好一些)。
updateRule

问题3

AdaGrad, like Adam, is a per-parameter optimization method that uses the following update rule:

cache += dw**2
w += - learning_rate * dw / (np.sqrt(cache) + eps)

John notices that when he was training a network with AdaGrad that the updates became very small, and that his network was learning slowly. Using your knowledge of the AdaGrad update rule, why do you think the updates would become very small? Would Adam have the same issue?

首先看一下Adam的公式

m = beta1*m + (1-beta1)*dx
v = beta2*v + (1-beta2)*(dx**2)
x += - learning_rate * m / (np.sqrt(v) + eps)

经过比较可以发现,AdaGrad算法随着迭代次数增加,cache变得越来越大,因此分母变得越来越大,导致w变化很小。
而Adam算法就不会出现此问题,是一个比较均衡的过程。

用所学知识训练一个模型吧!

现在已经学会了模块化的正向与反向传播,学习了如何去调超参,也学习了几种更新规则。之后就发挥自己的想象力来调超参吧!。。。。。
个人感觉核心的一点是:主要是要跟着loss图的感觉来。
首先先把batch_size调大一些,训练CIFAR肯定不能只有100。
循环次数改成5,不需要训练太多次。
更新规则改为adam。
一小段时间之后扩大隐藏层每层的规模,感觉和batch_size一样比较好吧。。
再调了一段时间后,一直没有一个理想的效果,看到曲线一直是平的,感觉是weight_scale不够合理,于是开始尝试更改学习速率和初始规模。
快乐调参。。。
这是最后版本:

learning_rate = 1.5e-3
weight_scale = 1.5e-3
model = FullyConnectedNet([200, 200, 200, 200],
                weight_scale=weight_scale, dtype=np.float64)
solver = Solver(model, data,
                print_every=100, num_epochs=5, batch_size=200,
                update_rule='adam',
                optim_config={
                  'learning_rate': learning_rate,
                }
         )
solver.train()

plt.plot(solver.loss_history, 'o')
plt.title('Training loss history')
plt.xlabel('Iteration')
plt.ylabel('Training loss')
plt.show()

best_model = model

这是一些比较好的learning_rate 和 weight_scale的参数。。
1e-3 1e-3 50.3%
1.5e-3 1.5e-3 50.5%
1.5e-3 1.25e-3 49.7%
1.5e-3 1.75e-3 51.1%

最后验证集的结果有51.4%,算是达标了吧。