watch: TF2 - PKU 02 | NN Optimization Methods

2 神经网络优化

2.1 常用函数

  1. 在每个元素上执行条件语句,为真则返回A,为假则返回B:tf.where(条件语句,真返回A,假返回B)

    1
    2
    3
    4
    
    a = tf.constant([1, 2, 3, 1, 1])
    b = tf.constant([0, 1, 3, 4, 5])
    c = tf.where(tf.greater(a,b), a, b)	# 若a>b, 返回a对应位置的元素,否则返回b对应位置的元素
    print(c)	# tf.Tensor([1 2 3 4 5], shape=(5,), dtype=int32)
    
  2. 返回一个 [0,1) 之间的随机数:np.random.RandomState.rand(维度) 维度为空时返回标量

    1
    2
    3
    4
    5
    
    rdm = np.random.RandomState(seed=1)	# 设置随机种子每次生成的随机数相同
    a = rdm.rand()
    b = rdm.rand(2,3)
    print(a) # 0.4170220047
    print(b) # [[7.20324493e-01 1.14374817e-04 3.02332573e-01] [1.46755891e-01 9.23385948e-02 1.86260211e-01]]
    
  3. 将两个数组按垂直方向叠加 np.vstack(数组1,数组2)

    1
    2
    3
    4
    
    a = np.array([1, 2, 3])	# shape=(3,)
    b = np.array([4, 5, 6])
    c = np.vstack((a,b))
    print(c)	# [[1 2 3] [4 5 6]] shape=(2,3)
    
  4. 生成网格坐标

    np.mgrid[起始值:结束值:步长,起始值:结束值:步长,...] 生成若干维度的等差数组,不包括结束值

    x.ravel() 将多维数组x变为一维数组,“把变量拉直”

    np.c_[数组1,数组2,...] 数组对应位置元素配对

    1
    2
    3
    4
    5
    6
    7
    
    import numpy as np
    x, y = np.mgrid[1:3:1, 2:4:0.5] # x坐标+1递增,y坐标+0.5递增
    grid = np.c_[x.ravel(), y.ravel()] # 两个(8,) array配对
    
    print(x)  # [[1. 1. 1. 1.] [2. 2. 2. 2.]]  shape=(2,4)
    print(y)  # [[2. 2.5 3. 3.5] [2. 2.5 3. 3.5]]
    print(grid)  # [[1. 2.] [1. 2.5] [1. 3.] [1. 3.5] [2. 2.] [2. 2.5] [2. 3.] [2. 3.5]] shape=(8,2)
    

神经网络复杂度

多用网络层数和网络参数的个数表示:

空间复杂度:层数=隐藏层的层数+1个输出层;总参数=总w+总b

时间复杂度:乘加运算次数(多少次wx+b)

2.2 指数衰减学习率

先用较大的学习率,快速得到较优解,然后逐步减小学习率,使模型在训练后期稳定

指数衰减学习率 = 初始学习率 x 学习率衰减率 ^ (当前epoch(或batch) / 多少epoch(或batch)衰减一次)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
epoch = 40
LR_BASE = 0.2
LR_DECAY = 0.99
LR_STEP = 1

for epoch in range(epoch):
    lr = LR_BASE * LR_DECAY ** (epoch / LR_STEP)
    with tf.GradientTape() as tape:
        loss = tf.square(w+1)
    grads = tape.gradient(loss, w)
    
    w.assign_sub(lr * grads)
    print(f"After {epoch} epoch, w is {w.numpy()}, loss is {loss}, lr is {lr}")

2.3 激活函数

sigmoid 函数 作用:把无限变有限,把无穷归一,引入非线性,全域可导(导数在0-0.25之间);缺点:应用链式法则容易造成梯度消失;输入特征最好使均值为0的小数,但经过sigmoid后变为正数,导致收敛慢;其中有幂运算,计算时间长

tf.math.tanh(x) 激活后的输出的均值是0,导数在0-1之间,仍易造成梯度消失,两次幂运算,计算时间 更长

tf.nn.relu(x) f(x)=max(x,0) 优点:导数不是0就是1,在正区间内解决了梯度消失问题;只需判断输入是否大于0,计算速度快;收敛速度远快于sigmoid 和 tanh。缺点:输出不是以0为均值,收敛慢;Dead Relu问题,当输入特征是负数时,激活函数输出为0,反向传播时,梯度为0,导致相应的参数无法更新。可以改进随机初始化,避免过多的负数特征输入Relu函数;可以通过减小学习率,减小参数分布的巨大变化,避免训练中产生过多的负数特征

tf.nn.leaky_relu(x) f(x) = max(ax, x) 是为了解决Relu在负区间的导数为0 引起神经元死亡问题而设计的,它在负区间引入了固定的斜率a。理论上来讲,Leaky Relu 有 Relu 的所有优点,外加不会有 Dead Relu问题,但是在实际操作中,并没有完全证明 Leaky Relu 总好于Relu

对于初学者的建议:

  • 首选relu激活函数
  • 学习率设置较小值
  • 输入特征标准化,即让输入特征满足以0为均值,以1为标准差的正态分布
  • 初始参数中心化,即让随机生成的参数满足以0为均值,根号下当前层输入特征个数分之2 为标准差的正态分布。

2.4 损失函数

预测值y 与 已知答案 y_ 的差距

NN优化目标:loss最小,主流三种计算方法:mse,交叉熵,自定义

loss_mse = tf.reduce_mean(tf.square(y_ - y))

 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
import tensorflow as tf
import numpy as np

SEED = 23455

rdm = np.random.RandomState(seed=SEED)
x = rdm.rand(32, 2)	# 真实y = x1+x2
y_ = [[x1+x2 + (rdm.rand()/10.0 - 0.05)] for (x1, x2) in x] # 加上噪声[0,1)/10 = [0,0.1); [-0.05, 0.05)
x = tf.cast(x, dtype=tf.float32)

w1 = tf.Variable(tf.random.normal([2,1], stddev=1, seed=1)) # 初始化网络参数 2x1

epoch = 15000
lr = 0.002

for epoch in range(epoch):
    with tf.GradientTape() as tape:
        y = tf.matmul(x, w1)	# 前向
        loss_mse = tf.reduce_mean(tf.square(y_ - y))
    grads = tape.gradient(loss_mse, w1)	# 损失函数对参数求偏导
    w1.assign_sub(lr * grads)	# 更新参数
    
    if epoch % 500 == 0:	# 每迭代500轮,打印参数
        print(f"After {epoch} training setps, w1 is {w1.numpy()}")
print(f"final w1 is {w1.numpy()}")  # [[1.0009] [0.9977]]

。。。。

2.6 优化器

参数更新:下一时刻的参数 等于 当前时刻的参数 减去 η (梯度下降)。η等于学习率乘上(一阶动量除以根号下二阶动量)。

SGD中无动量,一阶动量等于loss对参数的偏导数,二阶动量=1,所以 η 是沿梯度方向改变的步长。

单层网络 w*x+b 应用 SGD:

1
2
w1.assign_sub(lr * grads[0])
b1.assign_sub(lr * grads[1])

SGDM 在SGD基础上增加了一阶动量,一阶动量等于上一时刻的一阶动量与当前时刻的梯度共同作用:mₜ = β⋅mₜ₋₁ + (1-β)⋅gₜ , β是接近1的系数(经验值0.9),所以上一时刻的动量占主导;二阶动量=1。 一阶动量是“指数滑动平均值”?过去一段时间的平均值?

1
2
3
4
5
6
7
8
m_w, m_b = 0, 0
beta = 0.9

# sgd-momentum
m_w = beta * m_w + (1-beta) * grads[0]	# grads是loss对各参数的偏导数
m_b = beta * m_b + (1-beta) * grads[1]
w1.assign_sub(lr * m_w)
b1.assign_sub(lr * m_b)

Adagrad 在SGD 基础上增加了二阶动量,可以对模型中的每个参数分配自适应学习率了。一阶动量与SGD设置相同,等于loss的梯度,二阶动量是从开始时刻到现在,梯度平方的累计和 $V_t = \sum_{τ=1}^t g_τ^2$

1
2
3
4
5
6
7
v_w, v_b = 0, 0	# 二阶动量零时刻初值为0

# 在 for 循环里,每batch更新一次:
v_w += tf.square(grads[0])	# 梯度平方的累计和
v_b += tf.square(grads[1])
w1.assign_sub(lr * grads[0] / tf.sqrt(v_w))
b1.assign_sub(lr * grads[1] / tf.sqrt(v_b))

RMSProp 在SGD基础上增加了二阶动量,一阶动量仍等于当前时刻loss的梯度,二阶动量 $V_t = β⋅V_{t-1} + (1-β)⋅g_t^2$

1
2
3
4
5
6
7
8
v_w, v_b = 0, 0	# 二阶动量零时刻初值为0
beta = 0.9

# 在 for 循环里,每batch更新一次:
v_w += beta*v_w + (1-beta) * tf.square(grads[0])	# 梯度平方 与 上一时刻的二阶梯度的加权和
v_b += beta*v_b + (1-beta) * tf.square(grads[1])
w1.assign_sub(lr * grads[0] / tf.sqrt(v_w))
b1.assign_sub(lr * grads[1] / tf.sqrt(v_b))

Adam 同时结合了SGDM的一阶动量和RMSProp的二阶动量,并分别增加修正系数:mₜ = β₁⋅mₜ₋₁ + (1-β₁)⋅gₜ ,$\hat{m_t} = \frac{m_t}{1-β_1^t}$ ,Vₜ = β₂⋅Vₜ₋₁ + (1-β₂)⋅gₜ²,$\hat{V_t}=\frac{V_t}{1-β_2^t}$ , t 是从开始到现在经历的所有batch数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
m_w, m_b = 0, 0
v_w, v_b = 0, 0
beta1, beta2 = 0.9, 0.999
delta_w, delta_b = 0, 0
global_step = 0

m_w = beta1 * m_w + (1 - beta1) * grads[0]	# 一阶动量=上一时刻与此刻梯度加权和
m_b = beta1 * m_b + (1 - beta1) * grads[1]
v_w = beta2 * v_w + (1-beta2) * tf.square(grads[0])	# 二阶动量=上一时刻与此刻梯度平方的加权和
v_b = beta2 * v_b + (1-beta2) * tf.square(grads[1])

# 修正
m_w_correction = m_w / (1-tf.pow(beta1, int(global_step)))
m_b_correction = m_b / (1-tf.pow(beta1, int(global_step)))
v_w_correction = v_w / (1-tf.pow(beta2, int(global_step)))
v_b_correction = v_b / (1-tf.pow(beta2, int(global_step)))

w1.assign_sub(lr * m_w_correction / tf.sqrt(v_w_correction))
b1.assign_sub(lr * m_b_correction / tf.sqrt(v_b_correction))

不同优化器训练速度不同