DL-TensorFlow入门

简介

TensorFlow 是由 Google 团队开发的深度学习框架,其初衷是以最简单的方式实现机器学习和深度学习的概念。该框架融合了计算代数的优化技术,极大地方便了复杂数学表达式的计算。

TensorFlow 深度学习框架的三大核心功能:

  • 加速计算。神经网络本质上由大量的矩阵相乘、矩阵相加等基本数学运算构成,TensorFlow 的重要功能就是利用 GPU 方便地实现并行计算加速功能。
  • 自动梯度TensorFlow 可以自动构建计算图,通过 TensorFlow 提供的自动求导的功能,不需要手动推导即可计算输出对网络参数的偏导数。
  • 常用神经网络接口TensorFlow 除了提供底层的矩阵相乘、相加等数学函数,还包含常用神经网络运算函数、常用网络层、网络训练、模型保存与加载、网络部署等一系列深度学习的功能。

简单示例:

1
2
3
4
5
6
7
import tensorflow as tf

a = tf.constant(2.0)
b = tf.constant(4.0)

print('a+b=', a+b) # a+b= tf.Tensor(6.0, shape=(), dtype=float32)
# 运算时同时创建计算图 𝑐=𝑎+𝑏 和数值结果 6.0=2.0+4.0 的方式叫做命令式编程,也称为动态图模式。

基础

数据类型

Tensorflow 中的数据类型包含数值类型字符串类型布尔类型

数值类型

按照维度区分为四种类型:标量向量矩阵张量

1
2
3
4
5
6
7
8
9
10
11
12
# 标量
a1 = 1
a2 = tf.constant(2)
# 向量
b1 = tf.constant([1, 2., 3.3])
b2 = b1.numpy() # tf 张量转换为 numpy 数组
# 矩阵
c1 = tf.constant([[1, 2], [3, 4]])
# 三维张量
d1 = tf.constant([[[1, 2], [3, 4]], [[5, 6], [7, 8]]])

a1, a2, b1, b2, c1, d1
字符串类型
1
2
3
s1 = tf.constant('Hello Tensorflow!')
s2 = tf.strings.lower(s1)
s1, s2
布尔类型
1
2
3
bool_1 = tf.constant(True)
bool_0 = tf.constant(False)
bool_1, bool_0

张量

  1. 从数组、列表创建张量

    1
    2
    # tf.convert_to_tensor() 函数可以将 list 对象或 numpy 中的对象倒入到新的 Tensor 中
    tf.convert_to_tensor([1, 2.]), tf.convert_to_tensor([[1, 2], [3, 4]])
  2. 创建全 01 张量

    1
    2
    3
    4
    5
    6
    7
    8
    # 标量
    tf.zeros([]), tf.ones([])
    # 向量
    tf.zeros([1]), tf.ones([1])
    # 矩阵
    tf.zeros([2, 2]), tf.ones([3, 2])
    # 张量
    tf.zeros([2, 2, 2]), tf.ones([3, 2, 2])
  3. 创建自定义值的张量

    1
    2
    # tf.fill(shape, value)
    tf.fill([2], -1), tf.fill([2, 3], -2)
  4. 创建已知分布的张量

    1
    2
    # tf.random.normal(shape, mean=0.0, stddev=1.0) 创建形状为 shape,均值为 mean,标准差为 stddev 的正态分布
    tf.random.normal([2, 3]), tf.random.normal([2, 2], 1, 2)
  5. 创建序列

    1
    2
    # tf.range(start=0, limit, delta=1) 函数创建一段连续的整数序列
    tf.range(10, delta=2), tf.range(2, 10, delta=3)
待优化张量

为了区分需要计算梯度信息的张量和不需要计算梯度信息的张量。Tensorflow 中增加了一种专门的数据类型来支持梯度信息的记录:tf.Variable()

1
2
3
v1 = tf.constant([-1, 0, 1, 2])
v2 = tf.Variable(v1)
v1, v2, v2.name, v2.trainable
应用

张量的典型应用现在说明可能会有点超时,不需要完全理解,有初步印象即可。

  1. 标量

    1
    2
    3
    4
    5
    6
    7
    # 就是一个简单的数字,维度数为 0,shape 为 []。常用在于误差值、各种测量指标等。
    out = tf.random.normal([4, 10])
    y = tf.constant([2, 3, 2, 0])
    y = tf.one_hot(y, depth=10)
    loss = tf.keras.losses.mse(y, out)
    loss = tf.reduce_mean(loss) # 平均 mse
    loss
  2. 向量

    1
    2
    3
    4
    5
    # 在 2 个输出节点的网络层,创建长度为 2 的偏置向量,并累加在每个输出节点上
    z = tf.random.normal([4, 2])
    b = tf.zeros([2])
    z = z + b
    z
  3. 矩阵

    1
    2
    3
    4
    5
    6
    x = tf.random.normal([2, 4])
    # 令连接层的输出节点数为 3,则权值张量 w 的shape [4, 3]
    w = tf.random.normal([4, 3])
    b = tf.zeros([3])
    out = x @ w + b
    out
  4. 三维张量

    1
    2
    3
    4
    5
    6
    7
    8
    # 三维张量的一个典型应用就是表示序列信号,其格式是 x = [b, sequence len, feature len]
    (x_train, y_train), (x_test, y_test) = keras.datasets.imdb.load_data(num_words=10000)
    x_train = tf.keras.preprocessing.sequence.pad_sequence(x_train, maxlen=80) # 填充句子,截断为等长 80 个单词的句子
    x_train.shape # (25000, 80)

    embedding = tf.layers.Embedding(10000, 100) # 创建词向量 Embedding 层类
    out = embedding(x_train) # 将数字编码的单词转换为词向量
    out.shape # ([25000, 80, 100])
  5. 四维张量

    1
    2
    # 四维张量在卷积神经网络中应用非常广泛,用于保存特征图数据,格式一般为 [b, h, w, c],b 表示输入样本的数量,h/w 表示特征图的高/宽,c表示特征图的通道数。
    x = tf.random.normal([4, 32, 32, 3]) # 创造 32x32 的图片输入

数值精度

常见数值精度: tf.int16tf.int32tf.int64tf.float16tf.float32tf.float64

1
2
3
4
5
6
7
8
9
10
11
12
d1 = tf.constant(123456789, tf.int16) # 精度不足发生溢出
d2 = tf.constant(123456789, tf.int32)
d1, d2

import numpy as np
d3 = tf.constant(np.pi, tf.float32)
d4 = tf.constant(np.pi, tf.float64) # 精度更大保存的数据更多,对应的内存占用更多。
d3, d4

d5 = tf.constant(np.pi, tf.float16)
d5 = tf.cast(d5, tf.float32) # 类型转换
d5

数值运算

加减乘除

加减乘除可以分别通过 tf.add()tf.subtract()tf.multiply()tf.divide() 函数实现,同时 Tnsorflow 也重载了 +, -, *, /, //, % 运算符。

1
2
3
a = tf.range(5)
b = tf.constant(2)
a//b, a%b
乘方运算

tf.pow(a, x)tf.square(x)tf.sqrt(x) 函数分别实现乘方运算、平方和平方根运算。

1
2
x = tf.range(4)
x**2, tf.pow(x, 2)
指数对数运算

tf.exp(a) 函数实现自然对数 e 运算。

1
tf.exp(1.)
矩阵运算

使用 @ 运算符实现矩阵相乘,而 tf.matmul(a, b) 函数也可以实现。其中 a 的倒数第一个维度长度(行)必须要 b 的倒数第二个维度长度(列)必须相等。

1
2
3
4
5
6
7
8
a = tf.random.normal([4, 3, 28, 32])
b = tf.random.normal([4, 3, 32, 2])
a@b

# 哈达马乘积,要求矩阵 a 和 b 必须具有相同的阶
a = tf.random.normal([4, 3])
b = tf.random.normal([4, 3])
a*b

索引和切片

通过索引和切片可以提取张量部分的数据,实践中使用频率很高。

索引
1
2
3
4
5
6
x = tf.random.normal([4, 32, 32, 3])
# 第一张图片的数据,第一张图片数据的第二行,第一张图片数据的第二行第三列,第一张图片数据的第二行第三列B通道
x[0], x[0][1], x[0][1][2], x[0][1][2][1]

# 等价于上述方式的索引
x[0, 1, 2, 1]
切片
1
2
3
# 通过 start:end:step 切片方式可以提取一段数据
x[0, ::] # :: 表示读取在行维度上的所有行
x[:, 0:28:2, 0:28:2, :] # :: 简写为 :

维度转换

维度变换是最核心的张量操作,算法的每个模块对于数据张量的格式有不同的逻辑需求,即现有的数据格式不能满足计算的要求,就需要通过维度变换将数据切换形式,满足不同场合的运算需求。
基本的维度变换操作函数有以下几种:改变视图 reshape()插入维度 expand_dims()删除维度 squeeze()交换维度 transpose()复制数据 tile()

改变视图
1
2
3
4
5
# 张量的存储 Stroage 和视图 View 概念,同一个存储,在不同的角度观察数据,即可以产生不同的视图。
x = tf.range(96) # 生成向量
x = tf.reshape(x, [2, 4, 4, 3]) # 改变 x 的视图获得 4D 张量
x
# 在通过 reshape 改变视图时,必须记住张量的存储顺序,新视图的维度不能与存储顺序相悖,否则需要通过【维度交换】将存储顺序调整。
增删视图

tf.expand_dims(x, axis) 在指定的 axis 轴前插入一个新的维度。
tf.squeeze(x, axis) 其中 axis 为待删除维度的索引号,该参数默认值会删除所有长度为 1 的维度。

1
2
3
4
5
6
7
8
9
10
# 增加维度,增加一个长度为 1 的维度相当于给原有的数据添加一个新维度的概念,维度长度为 1,故数据格式不需要改变,其仅仅是改变了数据结构的理解方式。
x = tf.random.uniform([28, 28])
x.shape # shape=(28, 28)
x = tf.expand_dims(x, axis=2)

# 删除维度,是增加维度的逆操作,删除维度只能删除维度为 1 的维度,也不会改变张量的存储
x = tf.squeeze(x, axis=0)
x.shape # TensorShape([28, 28, 1])
x = tf.squeeze(x, axis=2)
x.shape # TensorShape([28, 28])
交换维度

tf.transpose(x, perm) 其中 perm 表示新维度的顺序 list

1
2
3
x = tf.random.normal([2, 32, 32, 3])
x = tf.transpose(x, [0, 3, 1, 2]) # shape=(2, 3, 32, 32)
x.shape
复制数据

tf.tile(x, multiples) 完成数据在指定维度上的复制操作,multiples 表示每个维度上面的复制倍数,对应位置为 1 表示不复制,对应 2 表示复制一份。该函数会创建一个新的张量来保存复制后的张量,因为涉及较多的 IO 操作,计算代价较高。

1
2
3
4
b = tf.constant([1, 2])
b = tf.expand_dims(b, axis=0) # 插入新维度,变为矩阵
b = tf.tile(b, multiples=[2, 1]) # 样本维度上复制一份
b

Broadcasting

Broadcasting 称为广播机制,是一种轻量级的张量复制手段,在逻辑上扩展张量数据的形状,但只有实际使用时才会执行数据复制操作。
Broadcasting 核心设计思想是普适性,即同一份数据可以普遍适合于其他位置。

  • 验证普适性之前首先需要将 shape 靠右对齐,然后进行普适性判断;
  • 对于长度为 1 的维度,默认这个数据普遍适合于当前维度的其他位置。
  • 对于不存在的维度,则在增加维度后默认当前数据是普适于新维度的,从而可以扩展更多维度数、任意长度的张量形状。
1
2
3
4
5
6
7
x = tf.random.normal([2,4])
w = tf.random.normal([4,3])
b = tf.random.normal([3])

y = x @ w + b
# 上述运行未发生异常的原因在于自动调用 Broadcasting 函数 tf.broadcast_to(x, new_shape) 将两者的shape 扩张为相同的 [2,3]
x, w, b, y

进阶

合并与分割

合并

合并是指将多个张量在某个维度上合并为一个张量。张量的合并可以通过拼接(Concatenate堆叠(Stack来实现,其中拼接不会产生新的维度,而堆叠会创造新的维度。
拼接和堆叠的唯一约束在于非合并维度的长度必须一致。

1
2
3
4
5
6
7
8
9
10
# tf.contact(tensors, axis) 函数拼接张量,tensors 表示所有需要合并的张量 list,而 axis 表示拼接张量的维度索引
a = tf.random.normal([4, 35, 8])
b = tf.random.normal([6, 35, 8])
tf.concat([a, b], axis=0)

# tf.stack(tensors, axis) 函数采用堆叠方式合并多个张量,其中参数 axis 的用法与 expand_dims() 一致
a = tf.random.normal([35, 8])
b = tf.random.normal([35, 8])
x = tf.stack([a, b], axis=0)
x.shape
分割

合并的逆操作就是分割,即将一个张量分拆为多个张量。

1
2
3
4
5
# tf.split(x, num_or_size_splits, axis),其中参数 num_or_size_splits 指切割方案:单个数值时等长切割;列表时按列表内数值切分。
# tf.unstack(x, axis) 固定长度为 1 的方式切割。
x = tf.random.normal([10, 35, 8])
res = tf.split(x, 10)
len(res), res[0]

数据统计

在神经网络的计算中通常需要统计数据的各种属性,例如最值最值位置均值范数等。

向量范数

向量范数是表征向量“长度”的一种度量方法,其可以推广到张量上,常用于表示张量的权值大小、梯度大小等。
常用的向量范数:

  • L1 范数,定义为向量 x 的所有元素绝对值之和。
  • L2 范数,定义为向量 x 的所有元素的平方和,再开根号。
  • np.inf 范数,定义为向量 x 的所有元素绝对值得最大值。
1
2
x = tf.ones([2,2])
tf.norm(x, ord=1), tf.norm(x, ord=2), tf.norm(x, ord=np.inf) # 计算L1范数,计算L2范数,计算np.inf范数
最值、均值、和

tf.reduce_max(x,axis), tf.reduce_min(x,axis), tf.reduce_mean(x,axis), tf.reduce_sum(x,axis) 函数可以求解张量在某个维度或全局上的最大值、最小值、平均值、和。

1
2
x = tf.random.normal([4,10])
tf.reduce_max(x, axis=0), tf.reduce_min(x, axis=1), tf.reduce_mean(x, axis=0), tf.reduce_sum(x, axis=1)
`tf.argmax(x, axis)`, `tf.argmin(x, axis)` 可以获取 `axis` 轴上的最大值、最小值。
1
2
3
out = tf.random.normal([2,10])
out = tf.nn.softmax(out, axis=1) # 通过softmax() 函数转化为概率值
tf.argmax(out, axis=0), tf.argmin(out, axis=1)

张量比较

为了计算分类任务的准确率,一般需要将预测结果和真实标签比较,统计比较结果中的正确值来计算准确率。

1
2
3
out = tf.random.normal([100, 10])
out = tf.nn.softmax(out, axis=1) # 输出转化为概率
pred = tf.argmax(out, axis=1) # 计算预测值

tf.equal(a, b) 或者 tf.math.equal(a, b) 均可以实现比较两个张量是否相等,返回布尔类型的张量比较结果。
类似的比较函数还有 tf.math.greater(), tf.math.less(), tf.math.grater_equal(), tf.math.less_equal(), tf.math.not_equal(), tf.math.is_nan()

1
2
3
4
5
6
y = tf.random.uniform([100], dtype=tf.int64, maxval=10) # 构建真实值
x = tf.equal(pred, y) # 比较张量
# 统计比较张量结果中的True
x = tf.cast(x, dtype=tf.float32) # 布尔类型转 int 类型 False -> 0, True -> 1
correct = tf.reduce_sum(x) # 统计 True 的个数
correct # 准确率

填充与复制

填充

在需要长度的数据开始或结束处填充足够数量的特定数值,这些数值通常代表无意义。

1
2
3
4
5
6
7
8
9
# tf.pad(x, padding) padding 包含多个 [Left Padding, Right Padding] 嵌套方案 list
a = tf.constant([1,2,3,4,5,6])
b = tf.constant([7,8,1,6])
b = tf.pad(b, [[0,2]]) # 末尾填充两个0
b # [7,8,1,6,0,0]

a = tf.random.normal([4, 28, 28, 3])
a = tf.pad(a, [[0,0], [2,2], [2,2], [0,0]])
a.shape # (4,32,32,3)
复制
1
2
3
4
# tf.tile() 函数可以在任意维度将数据重复复制多份
a = tf.random.normal([4, 28, 28, 3])
a = tf.tile(a, [2, 3, 3, 1])
a.shape # (8, 84, 84, 3)

数据限幅

考虑如何实现非线性激活函数 ReLU 的问题,那么其可以通过数据限幅运算实现,限制元素的范围即可。

1
2
3
4
5
6
7
8
9
10
11
# tf.maximum(x, a) 函数实现限制数据的下限幅。tf.minimum(x, a) 函数实现限制数据的上限幅。
x = tf.range(10)
# 下限幅,上限幅
tf.maximum(x, 2), tf.minimum(x, 7)

# 基于 tf.maximum() 函数实现 ReLU 函数
def relu(x):
return tf.maximum(x, 0.)
# 组合使用 tf.maximum(), tf.minimum() 可以同时对数据的上下边界限幅。tf.clip_by_value(x, a, b) 函数也可以实现同时对上下边界限幅
x = tf.range(10)
tf.maximum(tf.minimum(x, 7), 2)

高级操作

  1. tf.gather
    根据索引号收集数据。

    1
    2
    x = tf.random.uniform([4,35,8], maxval=100, dtype=tf.int32)
    tf.gather(x, [0,1], axis=0)
  2. tf.gather_nd
    通过指定每次采样点的多维坐标来实现采样多个点的目的。

    1
    2
    x = tf.random.uniform([4,35,8], maxval=100, dtype=tf.int32)
    tf.gather_nd(x, [[1,1], [2,2], [3,3]])
  3. tf.boolean_mask
    除了上述索引号的方式采样,还可以通过给定掩码(Mask)的方式进行采样。

    1
    2
    x = tf.random.uniform([4,35,8], maxval=100, dtype=tf.int32)
    tf.boolean_mask(x, mask=[True, False, False, False], axis=0) # 掩码长度必须与维度长度一致
  4. tf.where
    通过 th.where(cond,a,b) 操作可以根据 cond 条件的真假从参数 A 或参数 B 中读取数据。

    1
    2
    3
    4
    a = tf.ones([3,3])
    b = tf.zeros([3,3])
    cond = tf.constant([[True,False,False], [False,True,False], [True,True,False]])
    tf.where(cond, a, b)
  5. tf.scatter_nd
    通过 tf.scatter_nd(indices, updates, shape) 函数可以高效地刷新张量的部分数据,但该函数只能在全 0 的白板张量上面执行刷新操作。

    1
    2
    3
    indices = tf.constant([[4], [3], [1], [7]])
    updates = tf.constant([4.4, 3.3, 1.1, 7.7])
    tf.scatter_nd(indices, updates, [8])
  6. tf.meshgrid
    通过 tf.meshgrid() 函数可以方便地生成二维网格的采样点坐标,方便可视化。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    x = tf.linspace(-8, 8, 100)
    y = tf.linspace(-8, 8, 100)
    x,y = tf.meshgrid(x, y)
    z = tf.sqrt(x**2 + y**2)
    z = tf.sin(z) / z

    import matplotlib.pyplot as plt
    from mpl_toolkits.mplot3d import Axes3D

    fig = plt.figure()
    ax = Axes3D(fig)
    ax.contour3D(x.numpy(), y.numpy(), z.numpy(), 50)
    plt.show()

    1


神经网络

神经网络属于机器学习的一个分支,特指利用多个神经元去参数化映射函数的模型。

感知机

感知机模型如下:
2

其接受长度为 n 的一维向量 x=[x1,x2,...,x],每个输入节点通过权值 w 的连接汇集为变量 z,即 z = w1*x1 + w2*x2 + ... + w*x + b

全连接层

感知机模型的不可导特征严重束缚其潜力,使得其只能解决简单的任务。在感知机的基础上,将不连续的阶跃激活函数更换成平滑连续可导的激活函数,并通过堆叠多个网络层来增强网络的表达能力。

通过替换感知机的激活函数,同时并行堆叠多个神经元来实现多输入、多输出的网络结构。

由于每个输出节点与全部的输入节点相连接,这种网络层被称为全连接层 Fully-connected layer 或者稠密连接层 Dense layer ,而 W 矩阵叫做全连接层的权值矩阵b 向量叫做全连接层的偏置向量

张量实现

TensorFlow 中,要实现全连接层,只需要定义好权值张量 W 和 偏执张量 B,并利用 TensorFlow 提供的批量矩阵相乘函数 tf.matmul() 即可完成网络层的计算。

1
2
3
4
5
6
x = tf.random.normal([2, 784])
w1 = tf.Variable(tf.random.truncated_normal([784, 256], stddev=0.1))
b1 = tf.Variable(tf.zeros([256]))
o1 = tf.matmul(x, w1) + b1 # 线性变换
o1 = tf.nn.relu(o1) # 激活函数
o1.shape
层实现

全连接层本质上是矩阵的相乘和相加运算,实现并不复杂。但 Tensorflow 中有更方便的实现:layers.Dense(units, activation),函数参数 units 指定输出节点数,activation 激活函数类型。

1
2
3
4
5
6
7
8
9
x = tf.random.normal([4, 28*28])

from tensorflow.keras import layers

fc = layers.Dense(512, activation=tf.nn.relu) # 创建全连接层,指定输出节点数和激活函数
h1 = fc(x)

# Dense 类的权值矩阵,Dense 类的偏置向量,待优化参数列表,所有参数列表
fc.kernel, fc.bias, fc.trainable_variables, fc.variables
全连接层梯度

在将单个感知机模型推广到全连接的网络时,输入层通过一个全连接层得到输出 o,其与真实结果 t 计算均方误差,均方误差可以表示为:
$$ L = {\frac {1} {2}} sum_{i=1}^K (o_i - t_i)^2 $$

2_1

由于 ${\frac {\delta L} {\delta w_{jk}}}$ 仅与节点 $o_k$ 有关系,因此上式中的求和符号可以省略,即 i==k
$$ {\frac {\delta L} {\delta w_{jk}}} = (o_k - t_k) * {\frac {\delta o_k} {\delta w_{jk}}} $$

考虑 Sigmoid 函数的导数 $\alpha^` = \alpha (1-\alpha)$,代入其中可得:
$$ {\frac {\delta L} {\delta w_{jk}}} = (o_k - t_k) * o_k * (1 - o_k) * {\frac {\delta o_k} {\delta w_{jk}}} $$

再将 ${\frac {\delta o_k} {\delta w_{jk}}} = x_j$ 替换,其最终可得:
$$ {\frac {\delta L} {\delta w_{jk}}} = (o_k - t_k) * o_k * (1 - o_k) * x_j $$

由此可得某条连接 $w_{jk}$ 上的偏导数,仅与当前连接的输出节点 $o_k$、对应真实值节点的结果 $t_k$ 和对应的输入节点 $x_j$ 有关。

链式法则

使用 TensorFlow 自动求导功能验证链式法则:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
x = tf.constant(1.)
w1 = tf.constant(2.)
b1 = tf.constant(1.)
w2 = tf.constant(2.)
b2 = tf.constant(1.)

# y2 = (x*w1 + b1)*w2 + b2 = (y1)*w2 + b2
with tf.GradientTape(persistent=True) as tape:
tape.watch([w1, b1, w2, b2])
y1 = x * w1 + b1
y2 = y1 * w2 + b2

dy2_dy1 = tape.gradient(y2, [y1])[0]
dy1_dw1 = tape.gradient(y1, [w1])[0]
dy2_dw1 = tape.gradient(y2, [w1])[0]

print(dy2_dy1 * dy1_dw1) # 2.0
print(dy2_dw1) # 2.0

通过以上代码,通过自动求导计算出 ${\frac {\delta y_2} {\delta y_1}}$、 ${\frac {\delta y_1} {\delta w_1}}$ 和 ${\frac {\delta y_2} {\delta w_1}}$ 三个值,借助链式法则可以发现:
$$ {\frac {\delta y_2} {\delta w_1}} = {\frac {\delta y_2} {\delta y_1}} * {\frac {\delta y_1} {\delta w_1}} $$

由此可以发现偏导数的传播是符合链式法则的。

神经网络

在设计全连接网络时,网络的结构配置等超参数可以按经验法则自由设置,只需要遵循少量的约束即可。
3

张量实现

网络模型实现如下:

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
# 隐藏层 1 张量
w1 = tf.Variable(tf.random.truncated_normal([784, 256]), name='w1')
b1 = tf.Variable(tf.zeros([256]), name='b1')
# 隐藏层 2 张量
w2 = tf.Variable(tf.random.truncated_normal([256, 128]), name='w2')
b2 = tf.Variable(tf.zeros([128]), name='b2')
# 隐藏层 3 张量
w3 = tf.Variable(tf.random.truncated_normal([128, 64]), name='w3')
b3 = tf.Variable(tf.zeros([64]), name='b3')
# 输出层张量
w4 = tf.Variable(tf.random.truncated_normal([64, 10]), name='w3')
b4 = tf.Variable(tf.zeros([10]), name='b3')

with tf.GradientTape() as tape:
# 隐藏层 1 前向计算,[b, 28*28] -> [b, 256]
h1 = x@w1 + tf.broadcast_to(b1, [x.shape[0], 256])
# 通过激活函数
h1 = tf.nn.relu(h1)
# 隐藏层 2 前向计算,[b, 256] -> [b, 128]
h2 = h1@w2 + b2
h2 = tf.nn.relu(h2)
# 隐藏层 3 前向计算,[b, 128] -> [b, 64]
h3 = h2@w3 + b3
h3 = tf.nn.relu(h3)
# 输出层计算,[b, 64] -> [b, 10]
h4 = h3@w4+b4

使用 TensorFlow 自动求导计算梯度时,需要将前向计算过程放置在 tf.GradientTape() 环境中,利用 GradientTape()`` 对象的 gradient()`` 函数自动求解参数的梯度,并利用 optimizer 对象更新参数。

层实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fc1 = layers.Dense(256, activation=tf.nn.relu)
fc2 = layers.Dense(128, activation=tf.nn.relu)
fc3 = layers.Dense(64, activation=tf.nn.relu)
fc4 = layers.Dense(10, activation=tf.nn.relu)

x = tf.random.normal([4, 28*28])
h1 = fc1(x)
h2 = fc2(h1)
h3 = fc3(h2)
h4 = fc4(h3)

# 当然也可以使用 Sequential 构建一个网络大类对象
model = Sequential([
layers.Dense(256, activation=tf.nn.relu),
layers.Dense(128, activation=tf.nn.relu)
layers.Dense(64, activation=tf.nn.relu)
layers.Dense(10, activation=tf.nn.relu)
])
# 前向计算只需要调用一次网络大类对象,就可以完成所有层的按序计算
out = model(x)

激活函数

激活函数与阶跃函数、符号函数不同,因为这些函数都是平滑可导的,适合用于于梯度下降算法。

Sigmoid

Sigmoid 函数也被称为 Logistic 函数,其定义为
$$ Sigmoid(x) = {\frac {1} {1 + e^{-x}}} $$
该函数最大的特性在于可以将 x 的输入压缩到 $x \in (0,1)$ 区间,这个区间的数值在机器学习中可以表示以下含义:

  • 概率分布 (0,1) 区间的输出和概率的分布范围 [0,1] 一致,可以通过 Sigmoid 函数将输出转译为概率输出。
  • 信号,可以将 0,1 理解为某种信号,如像素的颜色强度,1 表示当前通道颜色最强,0 则表示当前通道无颜色。
1
2
x = tf.linspace(-6.0, 6.0, 100)
y_sigmoid = tf.nn.sigmoid(x)

4

ReLU

在使用 Sigmoid 函数时,遇到输入值较大或较小时容易出现梯度为 0 的现象,该现象被称为梯度弥散现象,而在出现梯度弥散时,梯度长时间无法更新,会导致训练难以收敛或训练停止不动的现象发生。
为了解决上述问题 ReLU 函数被开始广泛使用,其函数定义如下:
$$ ReLU(x) = max(0, x) $$

1
2
x = tf.linspace(-6.0, 6.0, 100)
y_relu = tf.nn.relu(x)

ReLU 函数的设计来源于神经科学,其函数值和导数值的计算十分简单,同时有着优秀的梯度特性,是目前最广泛应用的激活函数之一。
5

LeakyReLU

ReLU 函数在遇到输入值 x<0 时也会出现梯度弥散现象,因此 LeakyReLU 函数被提出,其表达式如下:
$$ LeakyReLU =
\begin{cases}
x, & x >= 0 \\
px, & x < 0
\end{cases} $$
其中 p 为用户自行设置的较小参数的超参数。

1
2
x = tf.linspace(-6.0, 6.0, 100)
y_leakyrelu = tf.nn.leaky_relu(x, alpha=0.1) # alpha 参数代表 p

6

Tanh

Tanh 函数可以将 x 的输入压缩到 $x \in (-1,1)$ 区间,其定义为:
$$ tanh(x) = {\frac {e^x - e^{-x}} {e^x + e^{-x}}} = 2 * sigmoid(2x) - 1 $$
tanh 激活函数可通过 Sigmoid 函数缩放平移后实现。

1
2
x = tf.linspace(-6.0, 6.0, 100)
y_tanh = tf.nn.tanh(x)

7

误差函数

在搭建完模型结构后,接下来就是选择合适的误差函数来计算误差。常见的误差函数有均方交叉熵KL散度Hinge Loss 函数等。
其中均方差函数和交叉熵函数较为常见,均方差函数用于回归问题交叉熵函数用于分类问题

均方误差函数

均方差 MSE 函数将输出向量和真实向量映射到笛卡尔坐标系的两个点上,通过计算这两个点之间的欧式距离(准确来讲是欧式距离的平方)来衡量两个向量之间的差距。
MSE 误差函数的值总是大于等于 0,当 MSE 达到最小值 0 时,输出等于真实值,此时的神经网络的参数达到最优状态。

均方误差损失函数表达式为:
$$ L = {\frac {1} {2}} sum_{k=1}^K (y_k - o_k)^2 $$

其偏导数 ${\frac {\delta L} {\delta o_i}}$ 可以展开为:
$$ {\frac {\delta L} {\delta o_i}} = {\frac {1} {2}} sum_{k=1}^K {\frac {\delta} {\delta o_i}} (y_k - o_k)^2 $$

利用复合函数导数法则分解为:
$$ \begin{equation}
\begin{split}
{\frac {\delta L} {\delta o_i}} = {\frac {1} {2}} sum_{k=1}^K 2 (y_k - o_k) {\frac {\delta (y_k - o_k)} {\delta o_i}} \\
= sum_{k=1}^K (y_k - o_k) -1 {\frac {\delta o_k} {\delta o_i}} \\
= sum_{k=1}^K (o_k - y_k) {\frac {\delta o_k} {\delta o_i}}
\end{split}
\nonumber
\end{equation}$$

考虑 ${\frac {\delta o_k} {\delta o_i}}$ 仅当 k==i 时才为 1,也就是说偏导数 ${\frac {\delta L} {\delta o_i}}$ 只与第 i 号节点有关,与其他节点无关。均方误差函数的导数可以推导为:
$$ {\frac {\delta L} {\delta o_i}} = (o_i - y_i) $$

TensorFlow 中可以通过函数方式或层方式实现 MSE 误差计算。

1
2
3
4
5
6
7
8
9
10
11
# 通过函数方式计算
o = tf.random.normal([2,10]) # 构造网络层输出值
y_onehot = tf.constant([1,3]) # 构造真实值
y_onehot = tf.one_hot(y_onehot, depth=10)
loss = keras.losses.MSE(y_onehot, o) # 计算均方误差
print(loss)
print(tf.reduce_mean(loss)) # 计算 batch 均方误差

# 通过层方式实现
criteon = keras.losses.MeanSquaredError() # 创建 MSE 类
criteon(y_onehot, o) # 计算 batch 均方误差
交叉熵误差函数

熵用于衡量信息中的不确定度。熵越大代表不确定性越大,信息量也就越大。

基于熵引出交叉熵(Cross Entropy的定义:
$$ H(p||q) = - sum_i p(i) log_2 q(i) $$
通过变换,交叉熵可以分解为 p 的熵 $H(p)$ 和 pqKL 散度(Kullback-Leibler Divergence)的和:
$$ H(p||q) = H(p) + D_{KL}(p||q) $$
而其中 KL 定义为:
$$ D_{KL}(p||q) = sum_i p(i) log({\frac {p(i)} {q(i)}}) $$
KL 散度是用于衡量两个分布之间距离的指标。
根据 KL 散度定义推导分类问题中交叉熵的计算表达式:
$$ H(p||q) = H(p) + D_{KL}(p||q) = D_{KL}(p||q) = sum_j y_j log({\frac {y_j} {o_j}}) = 1*log{\frac {1} {o_i}} + sum_{j!=i} 0*log({\frac {0} {o_j}}) = - log o_i $$

二分类问题时 $H(p)=0$
最小化交叉熵损失函数的过程就是最大化正确类别的预测概率的过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
# 二分类交叉熵损失
y_true = tf.constant([0, 1, 0, 0])
y_pred = tf.constant([-18.6, 0.51, 2.94, -12.8])
bce = keras.losses.BinaryCrossentropy(from_logits=True)
loss = bce(y_true, y_pred)
loss.numpy() # 0.865458

# 多分类交叉熵损失
y_true = tf.constant([[0, 1, 0], [0, 0, 1], [1, 0, 0]])
y_pred = tf.constant([[0.05, 0.95, 0], [0.1, 0.8, 0.1], [0.8, 0.1, 0.1]])
cce = tf.keras.losses.CategoricalCrossentropy()
loss = cce(y_true, y_pred)
loss.numpy() # 0.85900736

过拟合与欠拟合

当模型的容量过大时,网络模型除了学习训练集的模态之外,还会把额外的观测误差也学习,这样就会导致学习的模型在训练集上表现较好,但在未知的样本上表现不佳,也就是模型泛化能力较弱,这种现象被称为过拟合(Overfitting
当模型的容量过小时,模型不能够很好地学习到训练集数据的模态,导致模型在训练集上表现不佳,同时在未知的样本上也表现不佳,这种现象被称为欠拟合(Underfitting

用一个例子来解释模型容量和数据分布之间的关系:

  • a:使用简单线性函数去学习时,会很难学习到一个较好的函数,从而出现训练集和测试集均表现不佳。
  • b:学习的模型和真实模型之间容量大致匹配时,模型才具有较好的泛化能力。
  • c:使用过于复杂的函数去学习时,学习到的函数会过度“拟合”训练集,从而导致在测试集上表现不佳。

8


Keras 高层接口

Keras 是一个由 Python 语言开发的开源神经网络计算库,其被设计为高度模块化和易扩展的高层神经网络接口,使用户可以不需要通过过多的专业知识就可以轻松、快速地完成模型的搭建和训练。

TensorFlowKeras 被实现在 tf.keras 子模块中。

常见功能模块

tf.keras 提供了一系列高层的神经网络相关类和函数,如经典数据集加载函数、网络层类、模型容器、损失函数类、优化器类、经典模型类等。

网络层

对于常见的神经网络层,可以直接使用张量方式的底层接口函数来实现,这些接口函数一般在 tf.nn 模块中。

tf.keras.layers 命名空间下提供了大量常见的网络层类,如全连接层激活函数层池化层卷积层循环神经网络层等。对于这些网络层类,只需要在创建时指定相关参数,并调用 __call__ 方法即可完成前向计算。在调用 __call__ 方法时,Keras 会自动触发每个层的前向传播逻辑,而这些逻辑也一般实现在 call 方法中。

1
2
3
4
5
6
7
8
x = tf.constant([2., 1., 0.1])
layer = layers.Softmax(axis=-1) # 创建 Softmax 层
out = layer(x)
out

# 另一种实现方式
out_2 = tf.nn.softmax(x) # 调用 softmax 函数完成前向计算
out_2
网络

通过 Keras 的网络容器 Sequential 可以将多个网络层封装成一个网络模型,只需要调用一次网络模型即可完成数据从第一层到最后一层的顺序传播运算。

1
2
3
4
5
6
7
8
9
10
model = Sequential([
layers.Dense(3, activation=None),
layers.ReLU(),
layers.Dense(2, activation=None),
layers.ReLU()
])
x = tf.random.normal([4, 3])
out = model(x)
out
# 还可以通过 add() 方法继续追加新的网络层,实现动态创建网络的功能。

模型装配、训练与测试

在训练网络模型时,一般的流程是通过前向计算获得网络的输出值,再通过损失函数计算网络误差,然后通过自动求导工具计算梯度并更新,同时间隔性地测试网络的性能。

模型装配

Keras 中有两个比较特殊的类:

  • keras.Layer:网络层的母类,其定义了网络层的一些常见功能,如添加权值、管理权值列表等。
  • keras.Model:网络的母类,除了具有 Layer 类的功能之外,还具有保存模型、加载模型、训练与测试模型等功能。Sequential 也是 Model 的子类。
1
2
3
4
5
6
7
8
9
network = Sequential([
layers.Dense(256, activation='relu'),
layers.Dense(128, activation='relu'),
layers.Dense(64, activation='relu'),
layers.Dense(32, activation='relu'),
layers.Dense(10)
]) # 构建网络层
network.build(input_shape=(4, 28*28))
network.summary()

创建网络之后,正常流程就是迭代数据集多个 epoch,每次按批产生训练数据集、前向计算,然后通过损失函数计算误差值,并反向传播自动计算梯度、更新网络参数。

1
2
3
4
5
6
7
8
from tensorflow.keras import optimizers, losses

# compile() 函数指定网络使用的优化器对象、损失函数类型、评价指标等参数。
network.compile(
optimizer=optimizers.Adam(learning_rate=0.001), # 采用 Adam 优化器,学习率设置为 0.001
loss=losses.CategoricalCrossentropy(from_logits=True), # 使用交叉熵损失函数,
metrics=['accuracy']
) # 模型装配
模型训练

模型装配完成后,通过 Model.fit() 函数传入待训练和测试的数据集,其会返回训练过程中的数据记录。

1
2
3
4
5
# train_db 为 tf.data.Dataset 对象;epochs 指定训练迭代的数量;validation_data 指定用于验证的数据集和验证的频率
history = network.fit(train_db, epochs=5, validation_data=val_db, validation_freq=2)

# 指标包含loss、测量指标等记录
history.history
模型测试

通过 Model.predict() 方法即可完成模型的预测。

1
2
3
4
5
6
x, y = next(iter(db_test))
out = network.predict(x) # 模型预测
out

# 如果仅测试模型的性能,通过 Model.evaulate() 循环测试数据集的所有样本,并打印性能指标
network.evaulate(db_test)

模型保存与加载

模型在训练完成后,需要将模型保存到文件系统上,从而方便后续的模型测试与部署工作。

Keras 中有三种常用的模型保存与加载方法:

  1. 张量方式
    网络的状态主要体现在网络结构以及网络层内部张量数据上,因此在拥有网络数据结构的前提下,直接保存网络张量参数是最轻量级的方式。通过 Model.save_weights(path) 将当前的网络参数保存到 path 文件上。

    1
    network.save_weights('path')

    在需要使用网络参数时可以通过 load_weights(path) 加载保存的张量数据,但其需要使用相同的网络结构才能够正确恢复网络状态。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    network = Sequential([
    layers.Dense(256, activation='relu'),
    layers.Dense(128, activation='relu'),
    layers.Dense(64, activation='relu'),
    layers.Dense(32, activation='relu'),
    layers.Dense(10)
    ]) # 构建网络层
    network.build(input_shape=(4, 28*28))
    network.summary()
    network.compile(
    optimizer=optimizers.Adam(learning_rate=0.001), # 采用 Adam 优化器,学习率设置为 0.001
    loss=losses.CategoricalCrossentropy(from_logits=True), # 使用交叉熵损失函数,
    metrics=['accuracy']
    ) # 模型装配
    network.load_weights('')
  2. 网络方式
    通过 Model.save(path) 即可将模型的结构和模型的参数保存到 path 文件中。
    通过 keras.models.load_model(path) 可以恢复网络结构和网络参数。

    1
    2
    network.save('path') # 保存模型结构与模型参数
    network = keras.models.load_model('path') # 从文件加载模型结构与模型参数
  3. SavedModel 方式
    tf.saved_model.save(network, path) 即可将模型以 SaveModel 的方式保存到 path 目录中,用户无需关心文件的保存格式。
    通过 tf.saved_model.load(path) 函数可以实现从文件中恢复模型对象。

    1
    2
    tf.saved_model.save(network, 'path') # 保存模型结构与模型参数到文件
    tf.saved_model.load('path') # 从文件中恢复模型对象

测量指标

Keras 提供了一些常用的指标测量工具,其位于 keras.metrics 模块中,专门用于统计训练过程中常用的指标数据。

Keras 的指标测量工具使用方法一般分为四个主要步骤:新建测量器写入数据读取统计数据清零测量器

  1. 新建测量器
    keras.metrics 模块中有常用的测量器类,例如平均值 Mean 类,准确率 Accuracy 类,余弦相似度 CosineSimilarity 类等。

    1
    2
    # 适合 Loss 数据
    loss_meter = metrics.Mean()
  2. 写入数据
    通过测量器的 update_state() 函数写入新的数据,测量器会根据自身逻辑记录并处理采样数据。

    1
    2
    # 记录采样数据,通过 float() 函数将张量转换为普通数值
    loss_meter.update_state(float(loss))
  3. 读取统计数据
    在多次采样数据后,可以在需要的地方调用测量器的 result() 函数来获取统计值。

    1
    2
    # 打印统计提前的平均 loss
    print('loss:', loss_meter.result())
  4. 清零测量器
    通过 reset_states() 函数即可实现清除状态功能。

    1
    2
    3
    if step % 100 == 0:
    print('loss:', loss_meter.result())
    loss_meter.reset_states() # 打印完成后,清零测量器
  5. 实战

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    # 利用准确率测量器 Accuracy 类来统计训练过程中的准确率

    # 创建准确率测量器
    acc_meter = metrics.Accuracy()
    # 将当前 batch 样本的标签和预测结果写入测量器
    out = network(x)
    pred = tf.argmax(out, axis=1)
    pred = tf.cast(pred, dtype=tf.int32)
    acc_meter.update_state(y, pred)
    # 输出统计的平均准确率
    print('acc', acc_meter.result().numpy())
    acc_meter.reset_states()

自定义网络

对于需要创建自定义逻辑的网络层,可以通过自定义类来实现。在创建自定义网络层类时,需要继承自 layers.Layer 基类;创建自定义网络类时,需要继承自 keras.Model 基类,这样建立的自定义类才能够方便地利用 Layer/Model 基类提供的参数管理等功能,同时也可以与其他标准网络层类交互使用。

自定义网络层

对于自定义网络层,至少需要实现初始化 __init__ 方法和前向传播逻辑 call 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 创建自定义类,并继承自 layers.Layer。创建初始化方法,并调用母类的初始化方法,由于是全连接层,因此需要设置两个参数:输入特征的长度和输出特征的长度。
class MyDense(layers.Layer):
def __init__(self, inp_dim, outp_dim):
super(MyDense, self).__init__()
# 创建权值张量,并添加到类管理列表中,设置为需要优化
self.kernel = self.add_variable('w', [inp_dim, outp_dim], trainable=True) # trainable 参数是否需要被优化
self.t = self.Variable(tf.random.normal([inp_dim, outp_dim]), trainable=False) # 通过 tf.Variable 定义的张量也会被纳入参数列表

def call(self, inputs, training=False):
# 实现自定义类的前向传播逻辑
out = inputs @ self.kernel # X@W
out = tf.nn.relu(out) # 执行激活函数运算
return out

net = MyDense(4, 3)
net.trainble_variables, net.variables # 查看自定义层的参数列表
自定义网络
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 自定义网络类,需要继承自 keras.Model
class MyModel(keras.Model):
# 完成网络内需要的网络层的创建工作
def __init__(self):
super(MyModel, self).__init__()
self.fc1 = MyDense(28*28, 256)
self.fc2 = MyDense(256, 128)
self.fc3 = MyDense(128, 64)
self.fc4 = MyDense(64, 32)
self.fc5 = MyDense(32, 10)

# 自定义前向传播逻辑
def call(self, inputs, training=None):
x = self.fc1(inputs)
x = self.fc2(x)
x = self.fc3(x)
x = self.fc4(x)
x = self.fc5(x)
return x

总结

之前在学习机器学习内容的时候,感觉好难啊,我怎么什么都不会,但自从开始看深度学习相关的内容,就发现好些东西突然醒悟,之前好多不理解的东西也能理解,哈哈哈哈哈哈哈。这就是学习新东西的魅力。
总之就是持续学习,继续进步。


引用


个人备注

此博客内容均为作者学习《TensorFlow深度学习》所做笔记,侵删!
若转作其他用途,请注明来源!