激活函数

g(z)=g(z)(1g(z))g'(z)=g(z)(1-g(z)) sigmoid

tanh:g(z)=tanh(z)=a=ezezez+ezg(z)=tanh(z)=a=\frac{e^z-e^{-z}}{e^z+e^{-z}}, g(z)=(1g2(z))g'(z)=(1-g^2(z)) tanh

ReLU: g(z)=a=max(0,z)g(z)=a=max(0,z) ReLU

leaky ReLU: a=max(0.01z,z)a=max(0.01z,z) leaky ReLU

损失函数

常见的损失函数有均方误差和交叉熵误差

均方误差(MSE)

公式E=12k(yktk)2E = \frac{1}{2} \sum_{k}(y_k - t_k)^2,其中 yky_k 表示神经网络的输出,tkt_k 表示监督数据,k 表示数据的维数。

python 实现

1
2
def mean_squared_error(y, t):
return 0.5 * np.sum((y-t)**2)

交叉熵误差

公式E=ktklogykE = -\sum_{k} t_k \log y_k

python 实现:

1
2
3
def cross_entropy_error(y, t):
delta = 1e-7
return -np.sum(t * np.log(y + delta))

这里,参数 y 和 t 是 NumPy 数组。函数内部在计算 np.log 时,加上了一个微小值 delta。这是因为,当出现 np.log(0)时,np.log(0)会变为负无限大的-inf,这样一来就会导致后续计算无法进行。作为保护性对策,添加一个微小值可以防止负无限大的发生。

one-hot 编码

将正确解标签表示为 1,其他标签表示为 0 的表示方法称为 one-hot 编码

mini-batch

神经网络的学习也是从训练数据中选出一批数据,然后对每个 mini-batch 进行学习。比如,从 60000 个训练数据中随机选择 100 笔,再用这 100 笔数据进行学习。这种学习方式称为 mini-batch 学习,同时,mini-batch 的损失函数也是利用 一部分样本数据来近似地计算整体

mini-batch 版交叉熵误差的实现

1
2
3
4
5
6
def cross_entropy_error(y, t):
if y.ndim == 1:
t = t.reshape(1, t.size)
y = y.reshape(1, y.size)
batch_size = y.shape[0]
return -np.sum(t * np.log(y + 1e-7)) / batch_size

当监督数据是标签形式(非 one-hot 表示,而是像“2”“ 7”这样的标签)时,交叉熵误差可通过如下代码实现。

1
2
3
4
5
6
def cross_entropy_error(y, t):
if y.ndim == 1:
t = t.reshape(1, t.size)
y = y.reshape(1, y.size)
batch_size = y.shape[0]
return -np.sum(np.log(y[np.arange(batch_size), t] + 1e-7)) / batch_size

实现的要点是,由于 one-hot 表示中 t 为 0 的元素的交叉熵误差也为 0,因 此针对这些元素的计算可以忽略。换言之,如果可以获得神经网络在正确解标签处的输出,就可以计算交叉熵误差。因此,t 为 one-hot 表示时通过 t * np.log(y) 计算的地方,在 t 为标签形式时,可用 np.log( y [np.arange (batch_size), t] )实现相同的处理

梯度法

通过使用梯度来寻找函数最小值(或者尽可能小的值)的方法,即通过不断地沿梯度方向前进, 逐渐减小函数值的过程,常见的参数更新公式 x0=x0ηfx0x_0 = x_0 - \eta \frac{\partial f}{\partial x_0}x1=x1ηfx1x_1 = x_1 - \eta \frac{\partial f}{\partial x_1} 就来自于此

通过梯度下降法更新参数,并且当使用的数据是随机选择的 mini batch 数据时,称该方法为随机梯度下降法(SGD)

神经网络的学习过程

前提

神经网络存在合适的权重和偏置,调整权重和偏置以便拟合训练数据的过程称为“学习”。神经网络的学习分成下面 4 个步骤。

步骤 1(mini-batch)

从训练数据中随机选出一部分数据,这部分数据称为 mini-batch。我们的目标是减小 mini-batch 的损失函数的值。

步骤 2(计算梯度)

为了减小 mini-batch 的损失函数的值,需要求出各个权重参数的梯度。 梯度表示损失函数的值减小最多的方向。

步骤 3(更新参数)

将权重参数沿梯度方向进行微小更新。

步骤 4(重复)

重复步骤 1、步骤 2、步骤 3。

Logistic Regression cost function

y^=σ(wTx+b),where σ(z)=11+ez\hat{y}=\sigma(w^Tx+b), where\ \sigma(z)=\frac{1}{1+e^{-z}}

Loss function: 通常采用 L(y^,y)=12(y^y)2L(\hat{y},y)=\frac{1}{2}(\hat{y}-y)^2 作为 loss function,在逻辑回归中不这么做,因为其对梯度下降法很可能找不到全局最优值

我们采用的是对数损失函数 L(y^,y)=(ylogy^+(1y)log(1y^))L(\hat{y},y)=-(ylog\hat{y}+(1-y)log(1-\hat{y})),它是一个凸函数

Cost function: J(w,b)=1mi=1mL(y^(i),y(i))J(w,b)=\frac{1}{m}\sum{_{i=1}^mL(\hat{y}^{(i)},y^{(i)})}

层的思想在神经网络中的应用

乘法层:正向传播计算乘积。反向传播会将上游传来的导数乘以“翻转值”(即正向传播时的另一个输入值)。需要保存正向传播时的输入变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MulLayer:
def __init__(self):
self.x = None
self.y = None

def forward(self, x, y):
self.x = x
self.y = y
return x * y

def backward(self, dout):
# 翻转 x 和 y
dx = dout * self.y
dy = dout * self.x
return dx, dy

加法层:正向传播计算和。反向传播将上游导数原封不动地传递给下游(乘以 1)。不需要保存输入变量。

1
2
3
4
5
6
7
8
class AddLayer:
def forward(self, x, y):
return x + y

def backward(self, dout):
dx = dout * 1
dy = dout * 1
return dx, dy

激活函数层

ReLU:

1
2
3
4
5
6
7
8
9
10
11
12
class Relu:
def __init__(self):
self.mask = None
def forward(self, x):
self.mask = (x <= 0)
out = x.copy()
out[self.mask] = 0
return out
def backward(self, dout):
dout[self.mask] = 0
dx = dout
return dx

Sigmoid:

1
2
3
4
5
6
7
8
9
10
11
12
class Sigmoid:
def __init__(self):
self.out = None

def forward(self, x):
out = 1 / (1 + np.exp(-x))
self.out = out
return out

def backward(self, dout):
dx = dout * (1.0 - self.out) * self.out
return dx

Affine:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Affine:
def __init__(self, W, b):
self.W = W
self.b = b
self.x = None
self.dW = None
self.db = None

def forward(self, x):
self.x = x
out = np.dot(x, self.W) + self.b
return out

def backward(self, dout):
dx = np.dot(dout, self.W.T)
self.dW = np.dot(self.x.T, dout)
self.db = np.sum(dout, axis=0)
return dx

Softmax-with-Loss:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class SoftmaxWithLoss:
def __init__(self):
self.loss = None # 损失
self.y = None # softmax 的输出
self.t = None # 监督数据(one-hot vector)
def forward(self, x, t):
self.t = t
self.y = softmax(x)
self.loss = cross_entropy_error(self.y, self.t)
return self.loss
def backward(self, dout=1):
batch_size = self.t.shape[0]
dx = (self.y - self.t) / batch_size
return dx

应用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import sys, os
sys.path.append(os.pardir)
import numpy as np
from common.layers import *
from common.gradient import numerical_gradient
from collections import OrderedDict
class TwoLayerNet:
def __init__(self, input_size, hidden_size, output_size,
weight_init_std=0.01):
#
初始化权重
self.params = {}
self.params['W1'] = weight_init_std * \
np.random.randn(input_size, hidden_size)
self.params['b1'] = np.zeros(hidden_size)
self.params['W2'] = weight_init_std * \
np.random.randn(hidden_size, output_size)
self.params['b2'] = np.zeros(output_size)
#
生成层
self.layers = OrderedDict()
self.layers['Affine1'] = \
Affine(self.params['W1'], self.params['b1'])
self.layers['Relu1'] = Relu()
self.layers['Affine2'] = \
Affine(self.params['W2'], self.params['b2'])
self.lastLayer = SoftmaxWithLoss()
def predict(self, x):
for layer in self.layers.values():
x = layer.forward(x)
return x
# x: 输入数据, t: 监督数据
def loss(self, x, t):
y = self.predict(x)
return self.lastLayer.forward(y, t)
def accuracy(self, x, t):
y = self.predict(x)
y = np.argmax(y, axis=1)
if t.ndim != 1 : t = np.argmax(t, axis=1)
accuracy = np.sum(y == t) / float(x.shape[0])
return accuracy
# x: 输入数据, t: 监督数据
def numerical_gradient(self, x, t):
loss_W = lambda W: self.loss(x, t)
grads = {}
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 grads
def gradient(self, x, t):
# forward
self.loss(x, t)
# backward
dout = 1
dout = self.lastLayer.backward(dout)
layers = list(self.layers.values())
layers.reverse()
for layer in layers:
dout = layer.backward(dout)
# 设定
grads = {}
grads['W1'] = self.layers['Affine1'].dW
grads['b1'] = self.layers['Affine1'].db
grads['W2'] = self.layers['Affine2'].dW
grads['b2'] = self.layers['Affine2'].db
return grads

与学习相关的技巧

参数的更新

神经网络学习的目的是找到使损失函数的值尽可能小的参数。这是寻找最优参数的问题,解决这个问题的过程称为最优化

SGD

使用参数的梯度,沿梯度方向更新参数,重复这个步骤多次,从而靠近最优参数,这个过程称为随机梯度下降法(SGD)

WWηLW\boldsymbol{W} \leftarrow \boldsymbol{W} - \eta \frac{\partial L}{\partial \boldsymbol{W}}

1
2
3
4
5
6
class SGD:
def __init__(self, lr=0.01):
self.lr = lr
def update(self, params, grads):
for key in params.keys():
params[key] -= self.lr * grads[key]

它的缺点是呈“之”字形朝最小值(0,0)移动,效率低

Momentum

更新过程原理类似于物理中的动量,它的更新路径就像小球在碗中滚动。这里新出现了一个变量vv,对应物理上的速度。

vαvηLW\boldsymbol{v} \leftarrow \alpha \boldsymbol{v} - \eta \frac{\partial L}{\partial \boldsymbol{W}}

WW+v\boldsymbol{W} \leftarrow \boldsymbol{W} + \boldsymbol{v}

AdaGrad

在有关学习率的技巧中,有一种称为学习率衰减的方法,即随着学习的进行,学习率逐渐减小。这个想法相当于“全体”参数的学习率值一起降低。AdaGrad进一步发展了这个想法,针对一个一个的参数,赋予其定制的值

hh+LWLW\boldsymbol{h} \leftarrow \boldsymbol{h} + \frac{\partial L}{\partial \boldsymbol{W}} \odot \frac{\partial L}{\partial \boldsymbol{W}}

WWη1hLW\boldsymbol{W} \leftarrow \boldsymbol{W} - \eta \frac{1}{\sqrt{\boldsymbol{h}}} \frac{\partial L}{\partial \boldsymbol{W}}

这里出现了新变量hh,它保存了以前的所有梯度值的平方和(\odot为对应矩阵元素的乘法)。然后在更新参数时通过乘1h\frac{1}{\sqrt{h}},就可以调整学习率的尺度,这意味着参数的元素中变动较大的元素学习率将变小

AdaGrad会记录所有梯度的平方和,因此学习越深入更新的幅度就越小,如果无止境地学习,更新量就有可能变为0.为了解决这个问题可以使用RMSProm方法,RMSProp方法并不是将过去所有的梯度一视同仁地相加,而是逐渐地遗忘过去的梯度,在做加法运算时将新梯度的信息更多地反映出来。

Adam

Adam综合了Momentum和AdaGrad两种方法

下图为四种方式在梯度下降时的对比最优化方法的比较:SGD、Momentum、AdaGrad、Adam

目前并不存在能在所有问题中都表现良好的方法。这4种方法各有各的特点,都有各自擅长解决的问题和不擅长解决的问题

权重的初始值

1
w = np.random.randn(node_num, node_num) * 0.01

在使用高斯分布随机生成权重时,通常会乘一个小值,是为了让各层激活值分布均匀。下图为使用标准差为1的高斯分布作为权重初始值时的各层激活值的分布

使用标准差为1的高斯分布作为权重初始值时的各层激活值的分布

当数据分布具有偏向性时,如果使用了S型函数,会出现梯度消失的情况。为此我们通常使用标准差为一个小值的高斯分布使用标准差为0.01的高斯分布作为权重初始值时的各层激活值的分布

这次不会发生梯度消失的问题。但是,激活值的分布有所偏向,说明在表现力上会有很大问题。比如,如果100个神经元都输出几乎相同的值,那么也可以由1个神经元来表达基本相同的事情

综上,我们需要一个合理的初始值。

Xavier初始值

Xavier初始值:与前一层有n个节点连接时,初始值使用标准差为1n\frac{1}{\sqrt{n}}的分布。下图为实现效果

1
w = np.random.randn(node_num, node_num) / np.sqrt(node_num)

使用Xavier初始值作为权重初始值时的各层激活值的分布

后面的层呈稍微歪斜的形状,如果用tanh代替sigmoid,这个问题就会得到解决

He初始值

Xavier初始值是以激活函数是线性函数为前提而推导出来的。因为 sigmoid函数和tanh函数左右对称,且中央附近可以视作线性函数,所以适合使用Xavier初始值。但当激活函数使用ReLU时,一般推荐使用ReLU专用的初始值,即He初始值。

He初始值:与前一层有n个节点连接时,初始值使用标准差为2n\frac{2}{\sqrt{n}}的分布。

下为激活函数为ReLU时三种初始值的对比激活函数使用ReLU时,不同权重初始值的激活值分布的变化

总结一下,当激活函数使用ReLU时,权重初始值使用He初始值,当激活函数为sigmoid或tanh等S型曲线函数时,初始值使用Xavier初始值。

Batch Normalization

为了使各层拥有适当的广度,尝试“强制性”地调整激活值的分布,Batch Normalization方法是基于这个想法而产生的。Batch Norm的思路是调整各层的激活值分布使其拥有适当的广度使用了Batch Normalization的神经网络的例子(Batch Norm层的背景为灰色)

Batch Norm以进行学习时的mini-batch为单位,按mini batch进行正规化。具体而言,就是进行使数据分布的均值为0、方差为1的正规化。

μB1mi=1mxi\mu_B \leftarrow \frac{1}{m} \sum_{i=1}^m x_i

σB21mi=1m(xiμB)2\sigma_B^2 \leftarrow \frac{1}{m} \sum_{i=1}^m (x_i - \mu_B)^2

x^ixiμBσB2+ε\hat{x}_i \leftarrow \frac{x_i - \mu_B}{\sqrt{\sigma_B^2 + \varepsilon}}

式中的ε是一个微小值(比如,10e-7等),它是为了防止出现除以0的情况。该式所做的是将mini-batch的输入数据变换为均值为0、方差为1的数据,通过将这个处理插入到激活函数的前面(或者后面),可以减小数据分布的偏向。接着,Batch Norm层会对正规化后的数据进行缩放和平移的变换

yiγx^i+βy_i \leftarrow \gamma \hat{x}_i + \beta

一开始γ\gamma=1,β\beta=0,然后再通过学习调整到合适的值

几乎所有的情况下都是使用Batch Norm时学习进行得更快。它可以推动学习的进行。并对权重初始值变得健壮,不那么依赖初始值

正则化

训练中会出现过拟合的情况,为了尽可能减小这种情况出现的可能性,我们有以下对策

权值衰减

权值衰减:通过在学习的过程中对大的权重进行惩罚,来抑制过拟合。很多过拟合是由于权重参数取值过大才发生的。它通过为损失函数加上L1范数或L2范数(12λW2\frac{1}{2}\lambda W^2)来抑制权重变大,以L2范数为例,对于所有权重,权值衰减方法都会为损失函数加上12λW2\frac{1}{2}\lambda W^2。因此也要在求权重梯度的计算中,要为之前的误差反向传播法的结果加上正则化项的导数λW\lambda W

Dropout

如果网络的模型变得很复杂,只用权值衰减就难以应对了。Dropout是一种在学习的过程中随机删除神经元的方法。训练时,随机选出隐藏层的神经元,然后将其删除。被删除的神经元不再进行信号的传递

image-20260222231931808

如图所示。训练时,每传递一次数据,就会随机选择要删除的神经元。然后,测试时,虽然会传递所有的神经元信号,但是对于各个神经元的输出, 要乘上训练时的删除比例后再输出。