TensorFlow 2.0 笔记(一)神经网络计算过程及模型搭建

第一章 神经网络计算过程及模型搭建

1 人工智能三学派

我们常说的人工智能,就是让机器具备人的思维和意识。人工智能主要有三个学派,即行为主义、符号主义连接主义

行为主义:是基于控制论的,是在构建感知、动作的控制系统。单脚站立是行为主义一个典型例子,通过感知要摔倒的方向,控制两只手的动作,保持身体的平衡。这就构建了一个感知、动作的控制系统,是典型的行为主义。

符号主义:基于算数逻辑表达式。即在求解问题时,先把问题描述为表达式,再求解表达式。例如在求解某个问题时,利用 if case 等条件语句和若干计算公式描述出来,即使用了符号主义的方法,如专家系统。符号主义是能用公式描述的人工智能,它让计算机具备了理性思维。

连接主义:仿造人脑内的神经元连接关系,使人类不仅具备理性思维,还具备无法用公式描述的感性思维,如对某些知识产生记忆。

图 1.1 展示了人脑中的一根神经元,其中紫色部分为树突,其作为神经元的输入。黄色部分为轴突,其作为神经元的输出。人脑就是由 860 亿个这样的神经元首尾相接组成的网络。

image-20220418200339700

图1.1 神经元示意图

基于连接主义的神经网络模仿上图的神经元,使计算机具有感性思维。图1.2展示了从出生到成年,人脑中神经网络的变化。

image-20220418200403398

图1.2 人脑神经网络变化示意图

随着我们的成长,大量的数据通过视觉、听觉涌入大脑,使我们的神经网络连接,也就是这些神经元连接线上的权重发生了变化,有些线上的权重增强了,有些线上的权重减弱了。如图1.3所示。

image-20220418200419098

图1.3 神经网络权重变化示意图

2 神经网络设计过程

我们要用计算机模仿刚刚说到的神经网络连接关系,让计算机具备感性思维。

首先,需要准备数据,数据量越大越好,要构成特征和标签对。如要识别猫,就要有大量猫的图片和这个图片是猫的标签,构成特征标签对。

随后,搭建神经网络的网络结构,并通过反向传播,优化连线的权重,直到模型的识别准确率达到要求,得到最优的连线权重,把这个模型保存起来。

最后,用保存的模型,输入从未见过的新数据,它会通过前向传播,输出概率值,概率值最大的一个,就是分类或预测的结果。图2.1展示了搭建与使用神经网络模型的流程。

image-20220418200500161

图2.1 搭建与使用神经网络示意图

2.1 数据集介绍

本讲中采用鸢尾花数据集,此数据集包含鸢尾花花萼长、花萼宽、花瓣长、花瓣宽及对应的类别。其中前4个属性作为输入特征,类别作为标签,0代表狗尾草鸢尾,1代表杂色鸢尾,2代表弗吉尼亚鸢尾。人们通过对数据进行分析总结出了规律:通过测量花的花萼长、花萼宽、花瓣长、花瓣宽,可以得出鸢尾花的类别(如:花萼长>花萼宽且花瓣长/花瓣宽>2 ,则杂色鸢尾)。
由上述可知,可通过if与case语句构成专家系统,进行判别分类。在本讲中,采用搭建神经网络的办法对其进行分类,即将鸢尾花花萼长、花萼宽、花瓣长、花瓣宽四个输入属性喂入搭建好的神经网络,网络优化参数得到模型,输出分类结果。

2.2 网络搭建与训练

本讲中,我们搭建包含输入层与输出层的神经网络模型,通过对输入值乘权值,并于偏置值求和的方式得到输出值,图示如下。

image-20220418200636007

图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的狗尾草鸢尾。

image-20220418200800255

图2.3 鸢尾花神经网络展开模型

image-20220418200832361

图2.4 权重与偏置初始化矩阵

有了输入数据与线上权重等数据,即可按照y = x*w+b方式进行前向传播,计算过程如图2.5所示。

image-20220418201100095

图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使得损失函数最小。为达成这一目的,我们采用梯度下降的方法。损失函数的梯度表示损失函数

对各参数求偏导后的向量,损失函数梯度下降的方向,就是是损失函数减小的方向。梯度下降法即沿着损失函数梯度下降的方向,寻找损失函数的最小值,从而得到最优的参数。梯度下降法涉及的公式如下

image-20220418201456568

上式中,lr表示学习率,是一个超参数,表征梯度下降的速度。如学习率设置过小,参数更新会很慢,如果学习率设置过大,参数更新可能会跳过最小值。

上述梯度下降更新的过程为反向传播,下面通过例子感受反向传播。利用如下公式对参数w进行更新。

image-20220418201510438

设损失函数为(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理解。

image-20220418203132808

图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,故计算方式为image-20220418204552893

(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( )函数使前向传播的输出值符合概率分布,进而与独热码形式的标签作比较,其计算公式为image-20220418205043058

其中\(y_i\)是前向传播的输出。在前一部分,我们得到了前向传播的输出值,分别为1.01、2.01、-0.66,通过上述计算公式,可计算对应的概率值:

image-20220418205103178

上式中,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。

image-20220418210420111

图4.1 训练过程loss曲线

image-20220418210430502

图4.2 训练过程准确率曲线

posted @ 2022-04-19 15:32  王陸  阅读(403)  评论(0编辑  收藏  举报