0
点赞
收藏
分享

微信扫一扫

【2-神经网络优化】北京大学TensorFlow2.0

课程地址:【北京大学】Tensorflow2.0_哔哩哔哩_bilibili

Python3.7和TensorFlow2.1

六讲:

  1. 神经网络计算:神经网络的计算过程,搭建第一个神经网络模型

  1. 神经网络优化:神经网络的优化方法,掌握学习率、激活函数、损失函数和正则化的使用,用Python语言写出SGD、Momentum、Adagrad、RMSProp、Adam五种反向传播优化器

  1. 神经网络八股:神经网络搭建八股,六步法写出手写数字识别训练模型

  1. 网络八股扩展:神经网络八股扩展,增加自制数据集、数据增强、断点续训、参数提取和acc/loss可视化,实现给图识物的应用程序

  1. 卷积神经网络:用基础CNN、LeNet、AlexNet、VGGNet、InceptionNet和ResNet实现图像识别

  1. 循环神经网络:用基础RNN、LSTM、GRU实现股票预测


预备知识-几个函数

tf.where(条件语句,真返回A,假返回B):条件语句真返回A,条件语句假返回B

tf.where(tf.greater(a,b),a,b)   # 若a>b,返回a对应位置的元素,否则返回b对应位置的元素

np.random.RandomState.rand(维度):返回一个[0,1)之间的随机数。若维度为空,返回标量

import numpy as np
rdm = np.random.RandomState(seed=1)  # seed=常数,每次生成随机数相同
a = rdm.rand()   # 返回一个随机标量
b = rdm.rand(2,3)   # 返回维度为2行3列的随机数矩阵

np.vstack(数组1,数组2):将两个数组按垂直方向叠加

import numpy as np
a = np.array([1,2,3])
b = np.array([4,5,6])
c = np.vstack((a,b))  # 注意要加括号,因为该函数的输入参数只有1个
# c:
# [[1 2 3]
# [4 5 6]]

以下三个函数经常一起使用,生成网格坐标点:

np.mgrid[起始值:结束值:步长, 起始值:结束值:步长, ...]:返回若干组维度相同的等差数组,且 [起始值 结束值) 为左闭右开。第一个“起始值:结束值:步长”决定了二维数组的行,第二个“起始值:结束值:步长”决定了二维数组的列

为了产生坐标对,把x(网格交点的x坐标)和y(网格交点的y坐标)组合起来,是一个二维有序数组,且对应位置就是它们的坐标

import numpy as np
x,y = np.mgrid[1:3:1,2:4:0.5]

x.ravel():将x变为一维数组,把.前的变量拉直

np.c_[数组1, 数组2, ...]:使返回的间隔数值点配对

grid = np.c_[x.ravel(),y.ravel()]


神经网络复杂度

分为时间复杂度和空间复杂度

时间复杂度

即模型的运算次数,用乘加运算次数衡量。每个具有计算能力的神经元小球都要收集前一层的每一个输入特征,乘以各自线上的权重,再加上这个神经元的偏置项b,有几条权重线就有几次乘加运算

空间复杂度

用神经网络层数和神经网络中待优化的参数个数表示。计算神经网络层数时,只统计具有运算能力的层:输入层仅把数据传输过来,没有运算,故在统计神经网络层数时,不算输入层;输入层和输出层之间的所有层都叫隐藏层


学习率lr

表征了参数每次更新的幅度

指数衰减学习率

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

指数衰减学习率,根据当前迭代次数,动态改变学习率的值,一般写在for循环中

绿色的为超参数

  • 初始学习率:最初的学习率

  • 学习率衰减率:每次学习率按照这个比例指数衰减

  • 当前轮数:变量,是个计数器,可以用当前迭代了多少次数据集,即epoch的数值表示;也可以用当前一共迭代了多少次batch,即global_step表示

  • 多少轮衰减一次:迭代多少次数据集,或迭代多少次batch更新一次学习率,决定了学习率更新的频率

框起来的4行是补充的4行

import tensorflow as tf

w = tf.Variable(tf.constant(5, dtype=tf.float32))

epoch = 40
LR_BASE = 0.2  # 最初学习率
LR_DECAY = 0.99  # 学习率衰减率
LR_STEP = 1  # 喂入多少轮BATCH_SIZE后,更新一次学习率

for epoch in range(epoch):  # for epoch 定义顶层循环,表示对数据集循环epoch次,此例数据集数据仅有1个w,初始化时constant赋值为5,循环40次迭代
    lr = LR_BASE * LR_DECAY ** (epoch / LR_STEP)
    with tf.GradientTape() as tape:  # with结构到grads框起了梯度的计算过程
        loss = tf.square(w + 1)
    grads = tape.gradient(loss, w)  # .gradient函数告知谁对谁求导

    w.assign_sub(lr * grads)  # .assign_sub 对变量做自减 即:w -= lr*grads 即 w = w - lr*grads
    print("After %s epoch,w is %f,loss is %f,lr is %f" % (epoch, w.numpy(), loss, lr))


激活函数

对于线性函数,即使有多层神经元首尾相接,构成深层神经网络,依旧是线性组合,模型的表达力不够。激活函数是用来加入非线性因素,因为线性模型的表达能力不够。引入非线性激活函数,可使深层神经网络的表达能力更加强大

优秀的激活函数应满足:

  1. 非线性:激活函数非线性时,多层神经网络可逼近所有函数。只有当激活函数是非线性时,才不会被单层网络替代,使多层网络有了意义

  1. 可微性:优化器大多用梯度下降更新参数,若不可微,就无法更新参数了

  1. 单调性:当激活函数是单调的,能保证单层网络的损失函数是凸函数,更容易收敛

  1. 近似恒等性:f(x)≈x,当参数初始化为随机小值时,神经网络更稳定

激活函数输出值的范围:

  1. 为有限值时:权重对特征的影响会更显著,基于梯度的优化方法(梯度下降法更新参数)更稳定

  1. 为无限值时:参数的初始值对模型的影响非常大,建议调小学习率

sigmoid tf.nn.sigmoid(x)

sigmoid函数把输入值变换到0-1之间输出,若输入值是非常大的负数,输出的就是0;若输入值是非常大的正数,输出的就是1,相当于对输入进行了归一化

缺点:

  1. 易造成梯度消失:深层网络更新参数时,需要从输出层到输入层逐层进行链式求导,而sigmoid的导数输出是0-0.25间的小数,链式求导需要多层导数连续相乘,会出现多个0-0.25之间的连续相乘,结果将趋于0,产生梯度消失,使得参数无法继续更新

  1. 输出非0均值,收敛慢:希望输入每层神经网络的特征是以0为均值的小数值,但是过sigmoid激活函数后的数据都是正数,会使收敛变慢

  1. 幂运算复杂,训练时间长:sigmoid函数存在幂运算,计算复杂度大,训练时间长

sigmoid函数可应用在训练过程中

  • sigmoid函数只能处理两个类(多分类问题用softmax)

  • softmax函数大都运用在神经网络中的最后一层,使值在(0,1)之间

tanh tf.math.tanh(x)

该激活函数的输出值为零均值,但依旧存在梯度消失幂运算(复杂,训练时间长)的问题

比sigmoid函数收敛速度快

ReLU tf.nn.relu(x)

分段函数,符合“激活函数具备近似恒等性”

优点:

  1. 在正区间内解决了梯度消失问题

  1. 使用时只需要判断输入是否>0,计算速度快

  1. 训练参数时的收敛速度比sigmoid和tanh快

缺点:

  • 输出非0均值,收敛慢

  • dead relu问题:某些神经元可能永远不会被激活,导致相应的参数永远不能被更新

送入激活函数的输入特征是负数时,激活函数输出是0,反向传播得到的梯度是0,导致参数无法更新,造成神经元死亡。造成神经元死亡的根本原因是经过relu函数的负数特征过多导致。可以:

  • 改进随机初始化,避免过多的负数特征送入relu函数;

  • 通过设置更小的学习率,减少参数分布的巨大变化,避免训练中产生过多负数特征进入relu函数

Leaky ReLU tf.nn.leaky_relu(x)

为解决relu负区间为0,引起神经元死亡问题而设计的。Leaky ReLU负区间引入了一个固定的斜率α,使其负区间不再恒等于0

虽然Leaky ReLU比relu效果更好,但实际使用中选择relu作激活函数的网络会更多

softmax tf.nn.softmax(x)

对神经网络全连接层输出进行变换,使其服从概率分布,即每个值都位于[0,1],且和为1

总结


损失函数loss

前向传播计算出的结果y与已知标准答案y_的差距

神经网络的优化目标就是找到某套参数,使得计算出的结果y和已知标准答案y_无限接近

均方误差MSE(Mean Squared Error)

回归问题最常用的损失函数。回归问题解决的是对具体数值的预测,如房价预测、销量预测,这些问题需要预测的不是一个事先定义好的类别,而是一个任意实数

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

例:预测酸奶日销量y(即产量),影响日销量的因素为x1、x2,销量y_(即已知答案,最佳情况是产量=销量)

随机生成x1和x2,拟造数据集y_=x1+x2,添加-0.05~0.05的噪声。拟合可以预测销量的函数

# 自制数据集,构建一层神经网络,预测酸奶日销量
import tensorflow as tf
import numpy as np

SEED = 23455   # 随机种子,保证每次生成的数据集一样(实际应用中不写SEED)

rdm = np.random.RandomState(seed=SEED)  # 生成[0,1)之间的随机数
x = rdm.rand(32, 2)  # 生成32行2列的输入特征x,包含了32组0-1之间的随机数x1和x2

# .rand()生成[0,1)之间的随机数
# 生成标准答案y_
y_ = [[x1 + x2 + (rdm.rand() / 10.0 - 0.05)] for (x1, x2) in x]  # 生成噪声[0,1)/10=[0,0.1); [0,0.1)-0.05=[-0.05,0.05)
x = tf.cast(x, dtype=tf.float32)   # x转变数据类型

w1 = tf.Variable(tf.random.normal([2, 1], stddev=1, seed=1))  # 随机初始化参数w1,初始化为两行一列

epoch = 15000   # 数据集迭代次数
lr = 0.002

for epoch in range(epoch):
    with tf.GradientTape() as tape:
        y = tf.matmul(x, w1)   # for循环中用with结构,求前向传播计算结果y
        loss_mse = tf.reduce_mean(tf.square(y_ - y))   # 求均方误差损失函数loss_mse
    grads = tape.gradient(loss_mse, w1)   # 损失函数对待训练参数w1求偏导
    w1.assign_sub(lr * grads)   # 更新参数w1

    if epoch % 500 == 0:   # 每迭代500轮数据打印当前参数w1
        print("After %d training steps,w1 is " % (epoch))
        print(w1.numpy(), "\n")
print("Final w1 is: ", w1.numpy())

拟合结果为y=1.000972x1+0.9977485x2,与构造数据集的y=x1+x2一致,说明拟合正确

使用均方误差作为损失函数,默认认为销量预测的多了或者少了,损失是一样的,但真实情况并非如此

自定义损失函数

根据具体任务和目的,可设计不同的损失函数。损失函数的定义能极大影响模型预测效果,好的损失函数设计对于模型训练能够起到良好的引导作用

预测商品销量,预测多了损失成本,预测少了损失利润,而利润往往≠成本,这种情况下使用均方误差计算loss,无法使利益最大化

loss = tf.reduce_sum(tf.where(tf.greater(y, y_), (y - y_) * COST, (y_ - y) * PROFIT))

例:预测酸奶销量,假设酸奶成本COST=1元,酸奶利润PROFIT=99元。预测少了损失利润99元,预测多了损失成本1元,显然预测少了损失大,希望拟合的函数往多了预测

# 希望拟合的函数尽量往多了预测,用自定义损失函数拟合出来的预测酸奶日销量会不会智能地往多了预测?
# 以下代码相比上面,仅改动了loss函数
import tensorflow as tf
import numpy as np

SEED = 23455
COST = 1
PROFIT = 99

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

w1 = tf.Variable(tf.random.normal([2, 1], stddev=1, seed=1))

epoch = 10000
lr = 0.002

for epoch in range(epoch):
    with tf.GradientTape() as tape:
        y = tf.matmul(x, w1)
        # 只改动了损失函数:当预测的y多了时损失成本,当预测的y少了时损失利润
        loss = tf.reduce_sum(tf.where(tf.greater(y, y_), (y - y_) * COST, (y_ - y) * PROFIT))

    grads = tape.gradient(loss, w1)
    w1.assign_sub(lr * grads)

    if epoch % 500 == 0:
        print("After %d training steps,w1 is " % (epoch))
        print(w1.numpy(), "\n")
print("Final w1 is: ", w1.numpy())

# 自定义损失函数
# 酸奶成本1元,酸奶利润99元
# 成本很低,利润很高,人们希望多预测些,生成模型系数大于1,往多了预测

可见拟合出的参数均>1,偏大,模型的确在尽量往多了预测

交叉熵损失函数CE(Cross Entropy)

表征两个概率分布之间的距离。通过交叉熵的值可以判断哪个预测结果与标准答案更接近,交叉熵越小说明两者分布越接近。是分类问题中常用的损失函数

tf.losses.categorical_crossentropy(y_,y)
# 传入的y_和y为列表或array

softmax与交叉熵结合

在执行分类问题时,通常先用softmax函数让输出结果符合概率分布,再求y和y_的交叉熵损失函数

# TensorFlow给出了一个可同时计算概率分布和交叉熵的函数
tf.nn.softmax_cross_entropy_with_logits(y_,y)
# 等同于下面两句:
# y_pro = tf.nn.softmax(y)
# tf.losses.categorical_crossentropy(y_,y_pro)

欠拟合与过拟合

欠拟合

模型不能有效拟合数据集,对现有数据集学习的不够彻底

解决方法:

  • 增加输入特征项:给网络更多维度的输入特征

  • 增加网络参数:扩展网络规模,增加网络深度,提升模型表达力

  • 减少正则化参数

过拟合

模型对当前数据拟合的太好了,但对从未见过的新数据却难以做出正确的判断,缺乏泛化力

解决方法:

  • 数据清洗:减少数据集中的噪声,使数据集更纯净

  • 增大训练集:让模型见到更多数据

  • 采用正则化:一种通用的、有效的方法

  • 增大正则化参数

正则化缓解过拟合

正则化在损失函数中引入模型复杂度指标,利用给参数w加权值,弱化了训练数据的噪声(一般只对w使用正则化,不正则化偏置b)

正则化的选择:

  1. L1正则化大概率会使很多参数变为0,因此该方法可通过稀疏参数,即减少参数的数量,降低复杂度

  1. L2正则化会使参数很接近0但不为0,因此该方法可通过减小参数值的大小,降低复杂度,可以有效缓解数据集中因噪声引起的过拟合

例:

dot.csv

未加L2正则化的代码:

# 导入所需模块
import tensorflow as tf
from matplotlib import pyplot as plt
import numpy as np
import pandas as pd

# 读入数据/标签,生成x_train y_train
df = pd.read_csv('dot.csv')
x_data = np.array(df[['x1', 'x2']])  # 取出x1和x2两列,并转成二维数组
y_data = np.array(df['y_c'])  # 取出y_c列,并转成列表

# np.vstack(数组1,数组2):将两个数组按垂直方向叠加
x_train = np.vstack(x_data).reshape(-1, 2)
y_train = np.vstack(y_data).reshape(-1, 1)

Y_c = [['red' if y else 'blue'] for y in y_train]   # 1的话红点,0的话蓝点
# Y_c打印出来是一个二维数组,里面是['red']['blue']这样的元素

# 转换x的数据类型,否则后面矩阵相乘时会因数据类型问题报错
x_train = tf.cast(x_train, tf.float32)
y_train = tf.cast(y_train, tf.float32)

# from_tensor_slices函数切分传入的张量的第一个维度,生成相应的数据集,使输入特征和标签值一一对应
train_db = tf.data.Dataset.from_tensor_slices((x_train, y_train)).batch(32)

# 生成两层神经网络的参数,输入层为2个神经元,隐藏层为11个神经元(随便选的),1层隐藏层,输出层为1个神经元
# 用tf.Variable()保证参数可训练
w1 = tf.Variable(tf.random.normal([2, 11]), dtype=tf.float32)   # 2个输入特征
b1 = tf.Variable(tf.constant(0.01, shape=[11]))

w2 = tf.Variable(tf.random.normal([11, 1]), dtype=tf.float32)  # 第二层网络输入特征的个数11与第一层网络的输出个数11要一致
b2 = tf.Variable(tf.constant(0.01, shape=[1]))   # 整个神经网络的输出节点个数要和标签一样,数据集中每组x1、x2对应一个标签

# 定义超参数
lr = 0.005  # 学习率
epoch = 800  # 循环轮数

# 训练部分
# for循环嵌套,反向传播求梯度,更新参数
for epoch in range(epoch):   # epoch是对数据集的循环计数
    for step, (x_train, y_train) in enumerate(train_db):   # step是对batch的循环计数
        # 在with结构中完成前向传播计算出y
        with tf.GradientTape() as tape:  # 记录梯度信息

            h1 = tf.matmul(x_train, w1) + b1  # 记录神经网络乘加运算
            h1 = tf.nn.relu(h1)
            y = tf.matmul(h1, w2) + b2

            # 采用均方误差损失函数mse = mean(sum(y-out)^2)
            loss = tf.reduce_mean(tf.square(y_train - y))

        # 计算loss对各个参数的梯度
        variables = [w1, b1, w2, b2]
        grads = tape.gradient(loss, variables)  # 损失函数loss分别对w1/b1/w2/b2求偏导数

        # 实现梯度更新,分别更新w1/b1/w2/b2
        # w1 = w1 - lr * w1_grad
        # tape.gradient是自动求导结果与[w1, b1, w2, b2]  索引为0,1,2,3
        w1.assign_sub(lr * grads[0])
        b1.assign_sub(lr * grads[1])
        w2.assign_sub(lr * grads[2])
        b2.assign_sub(lr * grads[3])

    # 每20个epoch,打印loss信息
    if epoch % 20 == 0:
        print('epoch:', epoch, 'loss:', float(loss))

# 预测部分
print("*******predict*******")

# xx在-3到3之间以步长为0.1,yy在-3到3之间以步长0.1,生成间隔数值点
xx, yy = np.mgrid[-3:3:.1, -3:3:.1]   # 生成网格坐标点,密度是0.1
'''
xx:
[[-3.  -3.  -3.  ... -3.  -3.  -3. ]
 [-2.9 -2.9 -2.9 ... -2.9 -2.9 -2.9]
 [-2.8 -2.8 -2.8 ... -2.8 -2.8 -2.8]
 ...
 [ 2.7  2.7  2.7 ...  2.7  2.7  2.7]
 [ 2.8  2.8  2.8 ...  2.8  2.8  2.8]
 [ 2.9  2.9  2.9 ...  2.9  2.9  2.9]]
'''
# 将xx , yy拉直,并合并配对为二维张量,生成二维坐标点
grid = np.c_[xx.ravel(), yy.ravel()]
'''
xx.ravel():
[-3.  -3.  -3.  ...  2.9  2.9  2.9]

grid:
[[-3.  -3. ]
 [-3.  -2.9]
 [-3.  -2.8]
 ...
 [ 2.9  2.7]
 [ 2.9  2.8]
 [ 2.9  2.9]]
'''

grid = tf.cast(grid, tf.float32)
'''
grid:
tf.Tensor(
[[-3.  -3. ]
 [-3.  -2.9]
 [-3.  -2.8]
 ...
 [ 2.9  2.7]
 [ 2.9  2.8]
 [ 2.9  2.9]], shape=(3600, 2), dtype=float32)
'''

# 将网格坐标点喂入训练好的神经网络,神经网络会为每个坐标输出一个预测值,probs为输出
# 要区分输出偏向1还是0,可以把预测结果为0.5的线标出颜色,即为0和1的区分线
probs = []
for x_test in grid:
    # 使用训练好的参数进行预测
    h1 = tf.matmul([x_test], w1) + b1
    h1 = tf.nn.relu(h1)
    y = tf.matmul(h1, w2) + b2   # y为预测结果
    probs.append(y)

# 取第0列给x1,取第1列给x2
x1 = x_data[:, 0]
x2 = x_data[:, 1]

# probs的shape调整成xx的样子
probs = np.array(probs).reshape(xx.shape)

# 画出x1和x2的散点
plt.scatter(x1, x2, color=np.squeeze(Y_c))  # squeeze去掉纬度是1的纬度,相当于去掉[['red'],['blue']]内层括号,变为['red','blue']

# 把坐标xx yy和对应的值probs放入contour函数,给probs值为0.5的所有点上色  plt.show()后 显示的是红蓝点的分界线
'''
plt.contour(X, Y, Z, [levels])  作用:绘制轮廓线,类似于等高线
X和Y就是坐标位置  Z代表每个坐标对应的高度值,是一个二维数组
levels传入一个包含高度值的一维数组,这样便会画出传入的高度值对应的等高线
'''
plt.contour(xx, yy, probs, levels=[.5])   # 画出预测值y=0.5的曲线
plt.show()

# 读入红蓝点,画出分割线,不包含正则化   结果:轮廓不够平滑,存在过拟合现象
# 不清楚的数据,建议print出来查看

轮廓不够平滑,存在过拟合现象

加了L2正则化的代码:

# 相比p29_regularizationfree.py,仅在with结构加入了L2正则化

        with tf.GradientTape() as tape:  # 记录梯度信息

            h1 = tf.matmul(x_train, w1) + b1  # 记录神经网络乘加运算
            h1 = tf.nn.relu(h1)
            y = tf.matmul(h1, w2) + b2

            # 采用均方误差损失函数mse = mean(sum(y-out)^2)
            loss_mse = tf.reduce_mean(tf.square(y_train - y))
           
            # 添加l2正则化
            loss_regularization = []
            # tf.nn.l2_loss(w)=sum(w ** 2) / 2
            loss_regularization.append(tf.nn.l2_loss(w1))
            loss_regularization.append(tf.nn.l2_loss(w2))
            # 求和
            # 例:x = tf.constant(([1,1,1],[1,1,1]))
            #    tf.reduce_sum(x)
            # >>>6
            loss_regularization = tf.reduce_sum(loss_regularization)
            # 总loss = 衡量预测值与标准答案差距的均方误差loss_mse + 各个参数w权重的正则化loss_regularization
            loss = loss_mse + 0.03 * loss_regularization  # REGULARIZER = 0.03

加了L2正则化后的曲线更平缓,有效缓解了过拟合


优化器更新网络参数

用python编写反向传播优化器,实现参数更新。主要有以下几类优化器:

  • SGD(with No Momentum)

  • SGD(with Momentum)

  • AdaGrad

  • RMSProp

  • Adam

未完待续...

举报

相关推荐

0 条评论