第一章 神经网络计算过程及模型搭建
1 人工智能三学派
我们常说的人工智能,就是让机器具备人的思维和意识。人工智能主要有三个学派,即行为主义、符号主义和连接主义。
行为主义:是基于控制论的,是在构建感知、动作的控制系统。单脚站立是行为主义一个典型例子,通过感知要摔倒的方向,控制两只手的动作,保持身体的平衡。这就构建了一个感知、动作的控制系统,是典型的行为主义。
符号主义:基于算数逻辑表达式。即在求解问题时,先把问题描述为表达式,再求解表达式。例如在求解某个问题时,利用 if case 等条件语句和若干计算公式描述出来,即使用了符号主义的方法,如专家系统。符号主义是能用公式描述的人工智能,它让计算机具备了理性思维。
连接主义:仿造人脑内的神经元连接关系,使人类不仅具备理性思维,还具备无法用公式描述的感性思维,如对某些知识产生记忆。
图 1.1 展示了人脑中的一根神经元,其中紫色部分为树突,其作为神经元的输入。黄色部分为轴突,其作为神经元的输出。人脑就是由 860 亿个这样的神经元首尾相接组成的网络。
图1.1 神经元示意图
基于连接主义的神经网络模仿上图的神经元,使计算机具有感性思维。图1.2展示了从出生到成年,人脑中神经网络的变化。
图1.2 人脑神经网络变化示意图
随着我们的成长,大量的数据通过视觉、听觉涌入大脑,使我们的神经网络连接,也就是这些神经元连接线上的权重发生了变化,有些线上的权重增强了,有些线上的权重减弱了。如图1.3所示。
图1.3 神经网络权重变化示意图
2 神经网络设计过程
我们要用计算机模仿刚刚说到的神经网络连接关系,让计算机具备感性思维。
首先,需要准备数据,数据量越大越好,要构成特征和标签对。如要识别猫,就要有大量猫的图片和这个图片是猫的标签,构成特征标签对。
随后,搭建神经网络的网络结构,并通过反向传播,优化连线的权重,直到模型的识别准确率达到要求,得到最优的连线权重,把这个模型保存起来。
最后,用保存的模型,输入从未见过的新数据,它会通过前向传播,输出概率值,概率值最大的一个,就是分类或预测的结果。图2.1展示了搭建与使用神经网络模型的流程。
图2.1 搭建与使用神经网络示意图
2.1 数据集介绍
本讲中采用鸢尾花数据集,此数据集包含鸢尾花花萼长、花萼宽、花瓣长、花瓣宽及对应的类别。其中前4个属性作为输入特征,类别作为标签,0代表狗尾草鸢尾,1代表杂色鸢尾,2代表弗吉尼亚鸢尾。人们通过对数据进行分析总结出了规律:通过测量花的花萼长、花萼宽、花瓣长、花瓣宽,可以得出鸢尾花的类别(如:花萼长>花萼宽且花瓣长/花瓣宽>2 ,则杂色鸢尾)。
由上述可知,可通过if与case语句构成专家系统,进行判别分类。在本讲中,采用搭建神经网络的办法对其进行分类,即将鸢尾花花萼长、花萼宽、花瓣长、花瓣宽四个输入属性喂入搭建好的神经网络,网络优化参数得到模型,输出分类结果。
2.2 网络搭建与训练
本讲中,我们搭建包含输入层与输出层的神经网络模型,通过对输入值乘权值,并于偏置值求和的方式得到输出值,图示如下。
图2.2 鸢尾花神经网络简要模型
由图2.2可知输出y = x*w+b,即所有的输入x乘以各自线上的权重w求和加上偏置项b得到输出y。由2.1部分对数据集的介绍可知,输入特征x形状应为(1,4)即1行4列,输出y形状应为(1,3)即1行3列,w形状应为(4,3)即4行3列,b形状应为(3, )即有3个偏置项。
搭建好基本网络后,需要输入特征数据,并对线上权重w与偏置b进行初始化。搭建的神经网络如图2.3所示,w,b初始化矩阵如图2.4所示。在这里,我们输入标签为0的狗尾草鸢尾。
图2.3 鸢尾花神经网络展开模型
图2.4 权重与偏置初始化矩阵
有了输入数据与线上权重等数据,即可按照y = x*w+b方式进行前向传播,计算过程如图2.5所示。
图2.5 前向传播计算过程
图2.5中输出y中,1.01代表0类鸢尾得分,2.01代表1类鸢尾得分,-0.66代表2类鸢尾得分。通过输出y可以看出数值最大(可能性最高)的是1类鸢尾,而不是标签0类鸢尾。这是由于最初的参数w和b是随机产生的,现在输出的结果是蒙的。
为了修正这一结果,我们用损失函数,定义预测值y和标准答案(标签) y_的差距,损失函数可以定量的判断当前这组参数w和b的优劣,当损失函数最小时,即可得到最优w的值和b的值。
损失函数的定义有多种方法,均方误差就是一种常用的损失函数,它计算每个前向传播输出y和标准答案_y的差求平方再求和再除以n求平均值,表征了网络前向传播推理结果和标准答案之间的差距。
通过上述对损失函数的介绍,其目的是寻找一组参数w和b使得损失函数最小。为达成这一目的,我们采用梯度下降的方法。损失函数的梯度表示损失函数
对各参数求偏导后的向量,损失函数梯度下降的方向,就是是损失函数减小的方向。梯度下降法即沿着损失函数梯度下降的方向,寻找损失函数的最小值,从而得到最优的参数。梯度下降法涉及的公式如下
上式中,lr表示学习率,是一个超参数,表征梯度下降的速度。如学习率设置过小,参数更新会很慢,如果学习率设置过大,参数更新可能会跳过最小值。
上述梯度下降更新的过程为反向传播,下面通过例子感受反向传播。利用如下公式对参数w进行更新。
设损失函数为(w+1)^2,则其对w的偏导数为2w+2。设w在初始化时被随机初始化为5,学习率设置为0.2。则我们可按上述公式对w进行更新:
第一次参数为5,按上式计算即5-0.2×(2×5+2)=2.6。
同理第二次计算得到参数为1.16,第三次计算得到参数为0.296……
画出损失函数(w+1)^2的图像,可知w=-1时损失函数最小,我们反向传播优化参数的目的即为找到这个使损失函数最小的w=-1值。
3 TensorFlow2.X 基本概念与常见函数
3.1 基本概念
TensorFlow中的Tensor表示张量,是多维数组、多维列表,用阶表示张量的维数。0阶张量叫做标量,表示的是一个单独的数,如123;1阶张量叫作向量,表示的是一个一维数组如[1,2,3];2阶张量叫作矩阵,表示的是一个二维数组,它可以有i行j列个元素,每个元素用它的行号和列号共同索引到,如在[[1,2,3],[4,5,6],[7,8,9]]中,2的索引即为第0行第1列。张量的阶数与方括号的数量相同,0个方括号即为0阶张量,1个方括号即为1阶张量。故张量可以表示0阶到n阶的数组。也可通过reshape的方式得到更高维度数组,举例如下:
c = np.arange(24).reshape(2,4,3)
print(c)
输出结果:
[[[ 0 1 2] [ 3 4 5] [ 6 7 8] [ 9 10 11]]
[[12 13 14] [15 16 17] [18 19 20] [21 22 23]]]
TensorFlow中数据类型包括32位整型(tf.int32)、32位浮点(tf.float32)、64位浮点(tf.float64)、布尔型(tf.bool)、字符串型(tf.string)
创建张量有若干种不同的方法:
(1) 利用tf.constant(张量内容,dtype=数据类型(可选)),第一个参数表示张量内容,第二个参数表示张量的数据类型。举例如下:
a = tf.constant([1, 5], dtype=tf.int64)
print("a:", a)
print("a.dtype:", a.dtype)
print("a.shape:", a.shape)
输出结果为:
a: tf.Tensor([1 5], shape=(2,), dtype=int64)
a.dtype: <dtype: 'int64'>
a.shape: (2,)
即会输出张量内容、形状与数据类型,shape中数字为2,表示一维张量里有2个元素。
注:去掉dtype项,不同电脑环境不同导致默认值不同,可能导致后续程序bug
(2) 很多时候数据是由numpy格式给出的,此时可以通过如下函数将numpy格式化为Tensor格式:tf. convert_to_tensor(数据名,dtype=数据类型(可选))。举例如下:
import tensorflow as tf
import numpy as np
a = np.arange(0, 5)
b = tf.convert_to_tensor(a, dtype=tf.int64)
print("a:", a)
print("b:", b)
输出结果为:
a: [0 1 2 3 4]
b: tf.Tensor([0 1 2 3 4], shape=(5,), dtype=int64)
(3) 可采用不同函数创建不同值的张量。如用tf. zeros(维度)创建全为0的张量,tf.ones(维度)创建全为1的张量,tf. fill(维度,指定值)创建全为指定值的张量。其中维度参数部分,如一维则直接写个数,二维用[行,列]表示,多维用[n,m,j..]表示。举例如下:
a = tf.zeros([2, 3])
b = tf.ones(4)
c = tf.fill([2, 2], 9)
print("a:", a)
print("b:", b)
print("c:", c)
输出结果:
a: tf.Tensor([[0. 0. 0.] [0. 0. 0.]], shape=(2, 3), dtype=float32)
b: tf.Tensor([1. 1. 1. 1.], shape=(4,), dtype=float32)
c: tf.Tensor([[9 9] [9 9]], shape=(2, 2), dtype=int32)
可见,tf.zeros([2,3])创建了一个二维张量,第一个维度有两个元素,第二个维度有三个元素,元素的内容全是0;tf.ones(4)创建了一个一维张量,里边有4个元素,内容全是1;tf.fill([2,2],9)创建了一个两行两列的二维张量,第一个维度有两个元素,第二个维度也有两个元素,内容都是9。
(4) 可采用不同函数创建符合不同分布的张量。如用tf. random.normal (维度,mean=均值,stddev=标准差)生成正态分布的随机数,默认均值为0,标准差为1;用tf. random.truncated_normal (维度,mean=均值,stddev=标准差)生成截断式正态分布的随机数,能使生成的这些随机数更集中一些,如果随机生成数据的取值在(μ-2σ,μ+2σ),之外则重新进行生成,保证了生成值在均值附近;利用tf. random. uniform(维度,minval=最小值,maxval=最大值),生成指定维度的均匀分布随机数,用minval给定随机数的最小值,用maxval给定随机数的最大值,最小、最大值是前闭后开区间。举例如下:
d = tf.random.normal([2, 2], mean=0.5, stddev=1)
print("d:", d)
e = tf.random.truncated_normal([2, 2], mean=0.5, stddev=1)
print("e:", e)
f = tf.random.uniform([2, 2], minval=0, maxval=1)
print("f:", f)
d: tf.Tensor([[ 2.8951766 0.34715778] [-0.88545835 0.1141389 ]], shape=(2, 2), dtype=float32)
e: tf.Tensor([[ 1.3894703 1.226865 ] [ 0.6791599 -0.03111815]], shape=(2, 2), dtype=float32)
f: tf.Tensor([[1.3811994e-01 8.8101661e-01] [1.9073486e-04 9.7648060e-01]], shape=(2, 2), dtype=float32)
3.2 常用函数
(1)利用tf.cast (张量名,dtype=数据类型)强制将Tensor转换为该数据类型;利用tf.reduce_min (张量名)计算张量维度上元素的最小值;利用tf.reduce_max (张量名)计算张量维度上元素的最大值。举例如下:
x1 = tf.constant([1., 2., 3.], dtype=tf.float64)
print("x1:", x1)
x2 = tf.cast(x1, tf.int32)
print("x2", x2)
print("minimum of x2:", tf.reduce_min(x2))
print("maxmum of x2:", tf.reduce_max(x2))
输出结果:
x1: tf.Tensor([1. 2. 3.], shape=(3,), dtype=float64)
x2 tf.Tensor([1 2 3], shape=(3,), dtype=int32)
minimum of x2: tf.Tensor(1, shape=(), dtype=int32)
maxmum of x2: tf.Tensor(3, shape=(), dtype=int32)
(2) 可用tf.reduce_mean (张量名,axis=操作轴)计算张量沿着指定维度的平均值;可用f.reduce_sum (张量名,axis=操作轴)计算张量沿着指定维度的和,如不指定axis,则表示对所有元素进行操作。其中维度可按图3.1理解。
图3.1 维度定义
由上图可知对于一个二维张量,如果axis=0表示纵向操作(沿经度方向) ,axis=1 表示横向操作(沿纬度方向)。举例如下:
x = tf.constant([[1, 2, 3], [2, 2, 3]])
print("x:", x)
print("mean of x:", tf.reduce_mean(x)) # 求x中所有数的均值
print("sum of x:", tf.reduce_sum(x, axis=1)) # 求每一行的和
输出结果:
x: tf.Tensor(
[[1 2 3]
[2 2 3]], shape=(2, 3), dtype=int32)
mean of x: tf.Tensor(2, shape=(), dtype=int32)
sum of x: tf.Tensor([6 7], shape=(2,), dtype=int32)
(3) 可利用tf.Variable(initial_value,trainable,validate_shape,name)函数可以将变量标记为“可训练”的,被它标记了的变量,会在反向传播中记录自己的梯度信息。其中initial_value默认为None,可以搭配tensorflow随机生成函数来初始化参数;trainable默认为True,表示可以后期被算法优化的,如果不想该变量被优化,即改为False;validate_shape默认为True,形状不接受更改,如果需要更改,validate_shape=False;name默认为None,给变量确定名称。举例如下:
w = tf.Variable(tf.random.normal([2, 2], mean=0, stddev=1)),表示首先随机生成正态分布随机数,再给生成的随机数标记为可训练,这样在反向传播中就可以通过梯度下降更新参数w了。
(4) 利用TensorFlow中函数对张量进行四则运算。利用tf.add (张量1,张量2)实现两个张量的对应元素相加;利用tf.subtract (张量1,张量2)实现两个张量的对应元素相减;利用tf.multiply (张量1,张量2)实现两个张量的对应元素相乘;利用tf.divide (张量1,张量2)实现两个张量的对应元素相除。注:只有维度相同的张量才可以做四则运算,举例如下:
a = tf.ones([1, 3])
b = tf.fill([1, 3], 3.)
print("a:", a)
print("b:", b)
print("a+b:", tf.add(a, b))
print("a-b:", tf.subtract(a, b))
print("a*b:", tf.multiply(a, b))
print("b/a:", tf.divide(b, a))
输出结果:
a: tf.Tensor([[1. 1. 1.]], shape=(1, 3), dtype=float32)
b: tf.Tensor([[3. 3. 3.]], shape=(1, 3), dtype=float32)
a+b: tf.Tensor([[4. 4. 4.]], shape=(1, 3), dtype=float32)
a-b: tf.Tensor([[-2. -2. -2.]], shape=(1, 3), dtype=float32)
a*b: tf.Tensor([[3. 3. 3.]], shape=(1, 3), dtype=float32)
b/a: tf.Tensor([[3. 3. 3.]], shape=(1, 3), dtype=float32)
(5) 利用TensorFlow中函数对张量进行幂次运算。可用tf.square (张量名)计算某个张量的平方;利用tf.pow (张量名,n次方数)计算某个张量的n次方;利用tf.sqrt (张量名)计算某个张量的开方。举例如下:
a = tf.fill([1, 2], 3.)
print("a:", a)
print("a的平方:", tf.pow(a, 3))
print("a的平方:", tf.square(a))
print("a的开方:", tf.sqrt(a))
输出结果:
a: tf.Tensor([[3. 3.]], shape=(1, 2), dtype=float32)
a的平方: tf.Tensor([[27. 27.]], shape=(1, 2), dtype=float32)
a的平方: tf.Tensor([[9. 9.]], shape=(1, 2), dtype=float32)
a的开方: tf.Tensor([[1.7320508 1.7320508]], shape=(1, 2), dtype=float32)
(6) 可利用tf.matmul(矩阵1,矩阵2)实现两个矩阵的相乘。举例如下:
a = tf.ones([3, 2])
b = tf.fill([2, 3], 3.)
print("a*b:", tf.matmul(a, b))
输出结果:tf.Tensor([[6. 6. 6.] [6. 6. 6.] [6. 6. 6.]], shape=(3, 3), dtype=float32),即a为一个3行2列的全1矩阵,b为2行3列的全3矩阵,二者进行矩阵相乘。
(7) 可利用tf.data.Dataset.from_tensor_slices((输入特征, 标签))切分传入张量的第一维度,生成输入特征/标签对,构建数据集,此函数对Tensor格式与Numpy格式均适用,其切分的是第一维度,表征数据集中数据的数量,之后切分batch等操作都以第一维为基础。举例如下:
features = tf.constant([12, 23, 10, 17])
labels = tf.constant([0, 1, 1, 0])
dataset = tf.data.Dataset.from_tensor_slices((features, labels))
for element in dataset:
print(element)
(<tf.Tensor: shape=(), dtype=int32, numpy=12>, <tf.Tensor: shape=(), dtype=int32, numpy=0>)
(<tf.Tensor: shape=(), dtype=int32, numpy=23>, <tf.Tensor: shape=(), dtype=int32, numpy=1>)
(<tf.Tensor: shape=(), dtype=int32, numpy=10>, <tf.Tensor: shape=(), dtype=int32, numpy=1>)
(<tf.Tensor: shape=(), dtype=int32, numpy=17>, <tf.Tensor: shape=(), dtype=int32, numpy=0>)
即将输入特征12和标签0对应,产生配对;将输入特征23和标签1对应,产生配对……
(8) 可利用tf.GradientTape( )函数搭配with结构计算损失函数在某一张量处的梯度,举例如下:
with tf.GradientTape() as tape:
x = tf.Variable(tf.constant(3.0))
y = tf.pow(x, 2)
grad = tape.gradient(y, x)
print(grad)
输出结果:tf.Tensor(6.0, shape=(), dtype=float32) 在上例中,损失函数为\(w^2\),\(w\)当前取值为3,故计算方式为
(9) 可利用enumerate(列表名)函数枚举出每一个元素,并在元素前配上对应的索引号,常在for循环中使用。举例如下:
seq = ['one', 'two', 'three']
for i, element in enumerate(seq):
print(i, element)
输出结果:
0 one
1 two
2 three
(10)可用tf.one_hot(待转换数据,depth=几分类)函数实现用独热码表示标签,在分类问题中很常见。标记类别为为1和0,其中1表示是,0表示非。如在鸢尾花分类任务中,如果标签是1,表示分类结果是1杂色鸢尾,其用把它用独热码表示就是0,1,0,这样可以表示出每个分类的概率:也就是百分之0的可能是0狗尾草鸢尾,百分百的可能是1杂色鸢尾,百分之0的可能是弗吉尼亚鸢尾。举例如下:
classes = 3
labels = tf.constant([1, 0, 2]) # 输入的元素值最小为0,最大为2
output = tf.one_hot(labels, depth=classes)
print("result of labels1:", output)
输出结果:
result of labels1: tf.Tensor(
[[0. 1. 0.]
[1. 0. 0.]
[0. 0. 1.]], shape=(3, 3), dtype=float32)
索引从0开始,待转换数据中各元素值应小于depth,若带转换元素值大于等于depth,则该元素输出编码为[0, 0 … 0, 0]。即depth确定列数,待转换元素的个数确定行数。举例如下:
classes = 3
labels = tf.constant([1, 0, 4]) #输入的元素值4超出depth-1
output = tf.one_hot(labels, depth=classes)
print("result of labels1:", output)
输出结果:
result of labels1: tf.Tensor(
[[0. 1. 0.]
[1. 0. 0.]
[0. 0. 0.]], shape=(3, 3), dtype=float32)
即元素4对应的输出编码为[0. 0. 0.]。
(11)可利用tf.nn.softmax( )函数使前向传播的输出值符合概率分布,进而与独热码形式的标签作比较,其计算公式为
其中\(y_i\)是前向传播的输出。在前一部分,我们得到了前向传播的输出值,分别为1.01、2.01、-0.66,通过上述计算公式,可计算对应的概率值:
上式中,0.256表示为0类鸢尾的概率是25.6%,0.695表示为1类鸢尾的概率是69.5%,0.048表示为2类鸢尾的概率是4.8%。程序实现如下:
y = tf.constant([1.01, 2.01, -0.66])
y_pro = tf.nn.softmax(y)
print("After softmax, y_pro is:", y_pro) # y_pro 符合概率分布
输出结果:
After softmax, y_pro is: tf.Tensor([0.25598174 0.6958304 0.0481878 ], shape=(3,), dtype=float32)
与上述计算结果相同。
(12)可利用assign_sub对参数实现自更新。使用此函数前需利用tf.Variable定义变量w为可训练(可自更新),举例如下:
x = tf.Variable(4)
x.assign_sub(1)
print("x:", x) # 4-1=3
输出结果:x: <tf.Variable 'Variable:0' shape=() dtype=int32, numpy=3>
即实现了参数w自减1。注:直接调用tf.assign_sub会报错,要用w.assign_sub。
(13)可利用tf.argmax (张量名,axis=操作轴)返回张量沿指定维度最大值的索引,维度定义与图3.1一致。举例如下:
test = np.array([[1, 2, 3], [2, 3, 4], [5, 4, 3], [8, 7, 2]])
print("test:\n", test)
print("每一列的最大值的索引:", tf.argmax(test, axis=0)) # 返回每一列最大值的索引
print("每一行的最大值的索引", tf.argmax(test, axis=1)) # 返回每一行最大值的索引
输出结果:
test:
[[1 2 3]
[2 3 4]
[5 4 3]
[8 7 2]]
每一列的最大值的索引: tf.Tensor([3 3 1], shape=(3,), dtype=int64)
每一行的最大值的索引 tf.Tensor([2 2 0 0], shape=(4,), dtype=int64)
4 程序实现鸢尾花数据集分类
4.1 数据集回顾
先回顾鸢尾花数据集,其提供了150组鸢尾花数据,每组包括鸢尾花的花萼长、花萼宽、花瓣长、花瓣宽4个输入特征,同时还给出了这一组特征对应的鸢尾花类别。类别包括狗尾鸢尾、杂色鸢尾、弗吉尼亚鸢尾三类, 分别用数字0、1、2表示。使用此数据集代码如下:
from sklearn import datasets
# 导入数据,分别为输入特征和标签
x_data = datasets.load_iris().data
y_data = datasets.load_iris().target
即从sklearn包中导出数据集,将输入特征赋值给x_data变量,将对应标签赋值给y_data变量。
4.2 程序实现
我们用神经网络实现鸢尾花分类仅需要三步:
(1)准备数据,包括数据集读入、数据集乱序,把训练集和测试集中的数据配成输入特征和标签对,生成train和test即永不相见的训练集和测试集;
(2)搭建网络,定义神经网络中的所有可训练参数;
(3)优化这些可训练的参数,利用嵌套循环在with结构中求得损失函数loss对每个可训练参数的偏导数,更改这些可训练参数,为了查看效果,程序中可以加入每遍历一次数据集显示当前准确率,还可以画出准确率acc和损失函数loss的变化曲线图。以上部分的完整代码与解析如下:
(1) 数据集读入:
from sklearn import datasets
# 导入数据,分别为输入特征和标签
x_data = datasets.load_iris().data
y_data = datasets.load_iris().target
(2) 数据集乱序:
# 随机打乱数据(因为原始数据是顺序的,顺序不打乱会影响准确率)
# seed: 随机数种子,是一个整数,当设置之后,每次生成的随机数都一样(为方便教学,以保每位同学结果一致)
np.random.seed(116) # 使用相同的seed,保证输入特征和标签一一对应
np.random.shuffle(x_data)
np.random.seed(116)
np.random.shuffle(y_data)
tf.random.set_seed(116)
(3) 数据集分割成永不相见的训练集和测试集:
# 将打乱后的数据集分割为训练集和测试集,训练集为前120行,测试集为后30行
x_train = x_data[:-30]
y_train = y_data[:-30]
x_test = x_data[-30:]
y_test = y_data[-30:]
(4) 配成[输入特征,标签]对,每次喂入一小撮(batch):
# from_tensor_slices函数使输入特征和标签值一一对应。(把数据集分批次,每个批次batch组数据)
train_db = tf.data.Dataset.from_tensor_slices((x_train, y_train)).batch(32)
test_db = tf.data.Dataset.from_tensor_slices((x_test, y_test)).batch(32)
上述四小部分代码实现了数据集读入、数据集乱序、将数据集分割成永不相见的训练集和测试集、将数据配成[输入特征,标签]对。人类在认识这个世界的时候信息是没有规律的,杂乱无章的涌入大脑的,所以喂入神经网络的数据集也需要被打乱顺序。(2)部分实现了让数据集乱序,因为使用了同样的随机种子,所以打乱顺序后输入特征和标签仍然是一一对应的。(3)部分将打乱后的前120个数据取出来作为训练集,后30个数据作为测试集,为了公正评判神经网络的效果,训练集和测试集没有交集。(4)部分使用from_tensor_slices把训练集的输入特征和标签配对打包,将每32组输入特征标签对打包为一个batch,在喂入神经网络时会以batch为单位喂入。
(5) 定义神经网路中所有可训练参数:
# 生成神经网络的参数,4个输入特征故,输入层为4个输入节点;因为3分类,故输出层为3个神经元
# 用tf.Variable()标记参数可训练
# 使用seed使每次生成的随机数相同(方便教学,使大家结果都一致,在现实使用时不写seed)
w1 = tf.Variable(tf.random.truncated_normal([4, 3], stddev=0.1, seed=1))
b1 = tf.Variable(tf.random.truncated_normal([3], stddev=0.1, seed=1))
(6) 嵌套循环迭代,with结构更新参数,显示当前loss:
lr = 0.1 # 学习率为0.1
train_loss_results = [] # 将每轮的loss记录在此列表中,为后续画loss曲线提供数据
test_acc = [] # 将每轮的acc记录在此列表中,为后续画acc曲线提供数据
epoch = 500 # 循环500轮
loss_all = 0 # 每轮分4个step,loss_all记录四个step生成的4个loss的和
# 训练部分
for epoch in range(epoch): #数据集级别的循环,每个epoch循环一次数据集
for step, (x_train, y_train) in enumerate(train_db): #batch级别的循环 ,每个step循环一个batch
with tf.GradientTape() as tape: # with结构记录梯度信息
y = tf.matmul(x_train, w1) + b1 # 神经网络乘加运算
y = tf.nn.softmax(y) # 使输出y符合概率分布(此操作后与独热码同量级,可相减求loss)
y_ = tf.one_hot(y_train, depth=3) # 将标签值转换为独热码格式,方便计算loss和accuracy
loss = tf.reduce_mean(tf.square(y_ - y)) # 采用均方误差损失函数mse = mean(sum(y-out)^2)
loss_all += loss.numpy() # 将每个step计算出的loss累加,为后续求loss平均值提供数据,这样计算的loss更准确
# 计算loss对各个参数的梯度
grads = tape.gradient(loss, [w1, b1])
# 实现梯度更新 w1 = w1 - lr * w1_grad b = b - lr * b_grad
w1.assign_sub(lr * grads[0]) # 参数w1自更新
b1.assign_sub(lr * grads[1]) # 参数b自更新
# 每个epoch,打印loss信息
print("Epoch {}, loss: {}".format(epoch, loss_all/4))
train_loss_results.append(loss_all / 4) # 将4个step的loss求平均记录在此变量中
loss_all = 0 # loss_all归零,为记录下一个epoch的loss做准备
# 测试部分
# total_correct为预测对的样本个数, total_number为测试的总样本数,将这两个变量都初始化为0
total_correct, total_number = 0, 0
for x_test, y_test in test_db:
# 使用更新后的参数进行预测
y = tf.matmul(x_test, w1) + b1
y = tf.nn.softmax(y)
pred = tf.argmax(y, axis=1) # 返回y中最大值的索引,即预测的分类
# 将pred转换为y_test的数据类型
pred = tf.cast(pred, dtype=y_test.dtype)
# 若分类正确,则correct=1,否则为0,将bool型的结果转换为int型
correct = tf.cast(tf.equal(pred, y_test), dtype=tf.int32)
# 将每个batch的correct数加起来
correct = tf.reduce_sum(correct)
# 将所有batch中的correct数加起来
total_correct += int(correct)
# total_number为测试的总样本数,也就是x_test的行数,shape[0]返回变量的行数
total_number += x_test.shape[0]
# 总的准确率等于total_correct/total_number
acc = total_correct / total_number
test_acc.append(acc)
print("Test_acc:", acc)
print("--------------------------")
(7) 计算当前参数前向传播后的准确率,显示当前准确率acc:
# 测试部分
# total_correct为预测对的样本个数, total_number为测试的总样本数,将这两个变量都初始化为0
total_correct, total_number = 0, 0
for x_test, y_test in test_db:
# 使用更新后的参数进行预测
y = tf.matmul(x_test, w1) + b1
y = tf.nn.softmax(y)
pred = tf.argmax(y, axis=1) # 返回y中最大值的索引,即预测的分类
# 将pred转换为y_test的数据类型
pred = tf.cast(pred, dtype=y_test.dtype)
# 若分类正确,则correct=1,否则为0,将bool型的结果转换为int型
correct = tf.cast(tf.equal(pred, y_test), dtype=tf.int32)
# 将每个batch的correct数加起来
correct = tf.reduce_sum(correct)
# 将所有batch中的correct数加起来
total_correct += int(correct)
# total_number为测试的总样本数,也就是x_test的行数,shape[0]返回变量的行数
total_number += x_test.shape[0]
# 总的准确率等于total_correct/total_number
acc = total_correct / total_number
test_acc.append(acc)
print("Test_acc:", acc)
print("--------------------------")
(8) acc / loss可视化:
# 绘制 loss 曲线
plt.title('Loss Function Curve') # 图片标题
plt.xlabel('Epoch') # x轴变量名称
plt.ylabel('Loss') # y轴变量名称
plt.plot(train_loss_results, label="$Loss$") # 逐点画出trian_loss_results值并连线,连线图标是Loss
plt.legend() # 画出曲线图标
plt.show() # 画出图像
# 绘制 Accuracy 曲线
plt.title('Acc Curve') # 图片标题
plt.xlabel('Epoch') # x轴变量名称
plt.ylabel('Acc') # y轴变量名称
plt.plot(test_acc, label="$Accuracy$") # 逐点画出test_acc值并连线,连线图标是Accuracy
plt.legend()
plt.show()
上述两部分完成了对准确率的计算并可视化准确率与loss。(7)部分前向传播计算出y,使其符合概率分布并找到最大的概率值对应的索引号,调整数据类型与标签一致,如果预测值和标签相等则correct变量自加一,准确率即预测对了的数量除以测试集中的数据总数。(9)部分可将计算出的准确率画成曲线图,通过设置图标题、设置x轴名称、设置y轴名称,标出每个epoch时的准确率并画出曲线,可用同样方法画出loss曲线。结果图如图4.1与4.2。
图4.1 训练过程loss曲线
图4.2 训练过程准确率曲线