第3章〓深度学习框架 深度学习框架提供了一整套封装完善且使用方便、简单的API,可以供人们快速地实现各种算法模型,同时它也提供了算法模型训练、评价、调优的方法,是深度学习领域工作、研究的神兵利器。 目前市场占有率最高的框架主要有TensorFlow和PyTorch,它们的核心都是基于张量和计算图进行操作的。TensorFlow各个版本的API管理相对烦琐,学习较困难,而PyTorch要容易得多,但是作为一个深度学习工程师,无论TensorFlow还是PyTorch都需要掌握,因为两者都可能会在工作中被用到。尤其TensorFlow在工业界中占有非常重要的地位,这是无法避开的,因此本章将重点梳理TensorFlow的API,同时也将介绍PyTorch,通过阅读本章可以实现TensorFlow和PyTorch相关API的无缝衔接,帮助读者开启实战深度学习之旅! 3.1基本概念 在深度学习框架中有4种数据类型,分别如下: (1) 标量,也就是常量。 (2) 向量,一维数据。 (3) 矩阵,二维数组。 (4) 张量,三维及以上的数据。 但是在广义上它们都被称为张量,又被称为Tensor。无论是TensorFlow还是PyTorch或者其他框架的核心都是基于张量的数据操作。 图31计算图 另一个比较重要的概念是计算图,即将算法模型的计算过程图形化地展示出来,可以方便地查看各个变量之间的关系及张量的流向,如图31所示。 a、b表示数据的输入,add表示数据的运算方法,又称为算子,其实现的计算为y=(a×θ1+b×θ2)×θ3-c×θ4。深度学习框架会自动生成计算图,如图32所示。 图32自动生成计算图 TensorFlow 2.x中提供了动态图和静态图,而PyTorch提供的是动态图。所谓静态图是指先构建计算图,然后进行运算,虽然运行效率高,但是调试不方便,也不够灵活。动态图则是运算与计算图的构建同时进行,相对灵活且容易调试,但是运行效率有所下降。 例如静态图中需要先定义规则y=(a×θ1+b×θ2)×θ3-c×θ4,然后将结构存储起来,然后把θ1=1,θ2=2,θ3=3,θ4=4,放到计算图中计算。动态图则是y=(a×1+b×2)×3-c×4,同时构建计算规则并填充数据,其区别可以类比为,建造房子时先搭建整个房子的框架,再砌砖,这就是静态图。边搭框架边砌砖,这就是动态图。 总结 TensorFlow 2.x中提供了动态图和静态图,而PyTorch提供的是动态图。 练习 运行代码“第3章/TensorFlowAPI/计算图的生成.py”在TensorBoard中查看计算图的结构。 3.2环境搭建 深度学习框架的运行环境分为CPU和GPU(显卡)版本,安装GPU的TensorFlow运行环境,需要满足以下条件: (1) 显卡支持CUDA架构。CUDA是一种由NVIDIA推出的通用并行计算架构,该架构使GPU能够解决复杂的计算问题。 本机是否支持CUDA架构,可以在设备管理器中查看显卡的型号,如图33所示。 图33设备管理器 CUDA支持的显卡型号及算力,可以在网站https://developer.nvidia.com/zhcn/cudagpus#compute中可看,如图34所示。 图34CUDA支持显卡型号 一般来说集成显卡都不支持,独立显卡最少要在1GB以上才支持GPU的深度学习框架的运行。 (2) CUDA Toolkit的版本与本机计算机的显卡驱动应保持一致。CUDA Toolkit是CUDA开发工具包,主要包含了CUDAC、CUDAC++的编译器、科学库、示例程序等。 通常在安装显卡驱动时会默认安装CUDA Driver,但是不会安装CUDA Toolkit。因为只安装CUDA Driver就可以正常办公了,而CUDA Toolkit通常是为了开发工作的需要才会被安装。 检查CUDA Toolkit与显卡驱动版本是否匹配,可以在网站https://docs.nvidia.com/cuda/cudatoolkitreleasenotes/index.html#titleresolvedissues中进行,如图35所示。 图35显卡驱动与CUDA Toolkit版本对应 在计算机中打开NVIDIA控制面板可以查看当前显卡驱动版本,如图36所示。 图36本机显卡驱动版本 如果驱动版本太低,则可以在网站https://www.nvidia.com/download/index.aspx?lang=enus中下载驱动并进行升级,如图37所示。 图37选择显卡型号并下载驱动 (3) CUDA Toolkit、cuDNN版本应保持一致。cuDNN是NVIDIA针对深度神经网络中的基础操作而设计的基于GPU的加速库,其依赖CUDA Toolkit工具包。 其依赖关系可以查看网站https://developer.nvidia.com/rdp/cudnnarchive,如图38所示。 图38cuDNN与CUDA Toolkit的对应关系 (4) Python、TensorFlow、CUDA Toolkit、cuDNN版本应保持一致。根据cuDNN、CUDA的版本就可以在网站中选择Python、TensorFlow的版本进行安装,网站的网址为https://tensorflow.google.cn/install/source_windows?hl=zhcn#gpu,如图39所示。 图39选择TensorFlow、Python版本进行安装 手动配置TensorFlow的GPU版本比较复杂,推荐使用conda命令进行安装,conda会根据本机的GPU型号和驱动版本自动安装CUDA Toolkit和cuDNN。进入“1.10 包的管理”节由conda命令创建的StudyDNN环境后,执行命令conda install tensorflowgpu=2.6.0,如图310所示。 图310安装TensorFlow 2.6.0 然后在命令行中执行以下代码测试是否成功地安装了GPU版本,如图311所示。 图311TensorFlow 2.6.0安装成功GPU版本 注意: 如果你的计算机是第1次安装深度学习框架GPU版本,则应先升级显卡驱动; conda命令可以对相关依赖包进行下载并安装,而pip默认下载最新版本,pip安装的库有可能出现不兼容问题,因此推荐使用conda命令安装。 创建一个PyTorch的GPU运行环境,需要先创建一个conda环境,执行的命令如图312所示。 图312conda创建环境 然后进入网站https://pytorch.org/getstarted/previousversions/选择一个版本对应的命令来执行,如图313所示。 图313conda创建PyTorch的GPU环境 同样conda会自动安装CUDA Toolkit,如图314所示。 图314conda安装PyTorch的CUDA库 然后在命令行中执行以下代码测试是否成功地安装了GPU版本,如图315所示。 图315PyTorch安装成功GPU环境 注意: 不要将TensorFlow和PyTorch安装在同一环境中,否则会冲突; 笔者当前计算机显卡驱动版本为512.36,对应cudatoolkit=11.3,所以只能安装PyTorch 11.2,读者应根据自己的计算机选择相应版本。 总结 CUDA环境的安装可使用conda命令一键安装。安装的版本需要跟自己的显卡型号相匹配。 练习 在自己的计算机中搭建TensorFlow和PyTorch运行环境。 3.3TensorFlow基础函数 3.3.1TensorFlow初始类型 TensorFlow主要的数据类型有两种,一种是ResourceVariable变量类型,其初始后的值可以通过assign()函数进行修改; 另一种是EagerTensor类型,其值域定义后不可修改,代码如下: #第3章/TensorFlowAPI/基本函数.py import tensorflow as tf import numpy as np #(1) 创建标量 scalar = tf.constant([[1., 2]], dtype=tf.float32) print('类型:', type(scalar)) #(2) variable是一个初始的变量值,随着代码的推进会变化 theta = tf.Variable([[1.], [2.]], dtype=tf.float32) print('类型:', type(theta)) #(3) 将NumPy转换为Tensor rnd_numpy = np.random.randn(4, 3) np_tensor = tf.convert_to_tensor(rnd_numpy) print("将其他对象转换成Tensor:", type(np_tensor)) #(4) 将张量转换成其他数据类型 type_conversion = tf.cast(theta, dtype=tf.int8) print("Tensor数据类型转换: ", type_conversion) #EagerTensor对象不能修改,而ResourceVariable对象可以修改 theta = theta.assign([[5.], [6.]]) print('ResourceVariable对象修改后的值:', theta) #以下语句会报错,因为EagerTensor对象不能修改 #EagerTensor' object has no attribute 'assign' scalar.assign([[11., 22]]) 运行结果如下: 类型: <class 'Tensorflow.python.framework.ops.EagerTensor'> 类型: <class 'Tensorflow.python.ops.resource_variable_ops.ResourceVariable'> 将其他对象转换成Tensor: <class 'Tensorflow.python.framework.ops.EagerTensor'> Tensor数据类型转换: tf.Tensor( [[1] [2]], shape=(2, 1), dtype=int8) ResourceVariable对象修改后的值: <tf.Variable 'UnreadVariable' shape=(2, 1) dtype=float32, NumPy= array([[5.], [6.]], dtype=float32)> Traceback (most recent call last): File "D:/DLAI/TensorFlowAPI/基本函数.py", line 24, in <module> scalar.assign([[11., 22]]) File "C:\Users\qwen\anaconda32\envs\py38_tf26\lib\site-packages\TensorFlow\python\framework\ops.py", line 401, in __getattr__ self.__getattribute__(name) AttributeError: 'Tensorflow.python.framework.ops.EagerTensor' object has no attribute 'assign' tf.Variable()创建的是ResourceVariable类型,tf.constant()、tf.convert_to_tensor()、tf.cast()创建的是EagerTensor类型,scalar.assign([[11.,22]])意图对EagerTensor类型进行修改,所以会报'Tensorflow.python.framework.ops.EagerTensor' object has no attribute 'assign'错误。 tf.constant()用于创建标量; tf.convert_to_tensor()用于将其他数据类型(例如NDArray)转换为EagerTensor; tf.cast()用于将默认的float32位数据类型转换为int类型。 3.3.2TensorFlow指定设备 运行TensorFlow可以指定CPU或者GPU运行(GPU需要显卡支持),其语句主要用tf.device(device_name)来指定,一般来说如果安装的是TensorFlowgpu,则会默认用GPU来执行,如果GPU不存在,则会调CPU来运行,代码如下: #第3章/TensorFlowAPI/指定运行设备.py import tensorflow as tf import time #在此cpu作用域语句下面计算执行时间 with tf.device("cpu"): t1 = time.time() cpu = tf.constant(1) print(time.time() - t1) #在此gpu作用域语句下面计算执行时间 with tf.device('gpu'): t2 = time.time() gpu = tf.constant(1) print(time.time() - t1) print(cpu.device, gpu.device, sep='\n') 运行结果如下: 0.0 0.03126859664916992 /job:localhost/replica:0/task:0/device:CPU:0 /job:localhost/replica:0/task:0/device:GPU:0 3.3.3TensorFlow数学运算 TensorFlow已封装好一些基本数学运算函数,例如加、减、乘、除、内积、点乘等,其运算过程与NumPy中保持一致,代码如下: #第3章/TensorFlowAPI/数学运算.py import tensorflow as tf data1 = tf.constant([[2., 2.]], dtype=tf.float32) data2 = tf.constant([[4., 4.]], dtype=tf.float32) #加法 print(tf.add(data1, data2)) #减法 print(tf.subtract(data1, data2)) #点乘 print(tf.multiply(data1, data2)) #除法 print(tf.divide(data1, data2)) #内积 print(tf.matmul(data1, tf.transpose(data2))) #调用tf.math下面的函数 print(tf.math.sqrt(data1)) 运行结果如下: tf.Tensor([[6. 6.]], shape=(1, 2), dtype=float32) tf.Tensor([[-2. -2.]], shape=(1, 2), dtype=float32) tf.Tensor([[8. 8.]], shape=(1, 2), dtype=float32) tf.Tensor([[0.5 0.5]], shape=(1, 2), dtype=float32) tf.Tensor([[16.]], shape=(1, 1), dtype=float32) tf.Tensor([[1.4142135 1.4142135]], shape=(1, 2), dtype=float32) tf.math类下提供了众多常用的数学函数,如图316所示。 图316TensorFlow常见数学函数 根据 axis轴的位置进行与统计函数相关的操作,代码如下: #第3章/TensorFlowAPI/数学运算.py data3 = tf.convert_to_tensor( [[1.1228833, -0.94511896, -1.1491745], [0.50430834, -0.1323103, -0.509263]] ) #1轴维度最小值 print(tf.reduce_min(data3, axis=1)) #0轴维度平均值 print(tf.reduce_mean(data3, axis=0)) #0轴维度求和 print(tf.reduce_sum(data3, axis=0)) #1轴维度最大 print(tf.reduce_max(data3, axis=1)) 运行结果如下: tf.Tensor([-1.1491745 -0.509263 ], shape=(2,), dtype=float32) tf.Tensor([ 0.81359583 -0.53871465 -0.82921875], shape=(3,), dtype=float32) tf.Tensor([ 1.6271917 -1.0774293 -1.6584375], shape=(3,), dtype=float32) tf.Tensor([1.12288330.50430834], shape=(2,), dtype=float32) 代码中计算的维度参照axis的位置,由外到内是从0轴开始,由于data3是二维数据,所以当axis=0时竖着算,当axis=1时算最内层,其计算顺序如图317所示。 图317TensorFlow中axis的计算顺序 另一个常用操作就是根据axis获取最小值、最大值、排序后的索引位置,代码如下: #第3章/TensorFlowAPI/数学运算.py data3 = tf.convert_to_tensor( [[1.1228833, -0.94511896, -1.1491745], [0.50430834, -0.1323103, -0.509263]] ) #最大值的下标 print(tf.argmax(data3, axis=0)) #最小值的下标 print(tf.argmin(data3, axis=0)) #排序的下标 print(tf.argsort(data3, axis=1)) 运行结果如下: tf.Tensor([0 1 1], shape=(3,), dtype=int64) tf.Tensor([1 0 0], shape=(3,), dtype=int64) tf.Tensor( [[2 1 0] [2 1 0]], shape=(2, 3), dtype=int32) tf.argmax得到的是索引位置,如果axis不同,则其索引值也不同,其计算过程如图318所示。 图318TensorFlow中根据axis得到索引值 3.3.4TensorFlow维度变化 维度的操作主要包括维度转换、升维、降维、维度交换等,主要使用tf.reshape()、tf.expand_dims()、tf.squeeze()、tf.transpose()等函数,无论张量的维度如何变化,其size(总数)应该保持不变,代码如下: #第3章/TensorFlowAPI/维度变换.py import tensorflow as tf #随机生成4维数据 rnd_shape4 = tf.random.normal([4, 32, 32, 3]) print("获取维度:", rnd_shape4.shape) #数据维度升维、降维 #在0轴升一维,变成(1,4, 32, 32, 3) shape5 = tf.expand_dims(rnd_shape4, axis=0) print('升维:', shape5.shape) #在0轴降一维,变成(4, 32, 32, 3) shape5_to4 = tf.squeeze(shape5, axis=0) print('降维:', shape5_to4.shape) #在1轴升一维,变成(4, 1, 32, 32, 3) shape6 = tf.expand_dims(rnd_shape4, axis=1) print('升维:', shape6.shape) #在0轴降一维,变成(4, 32, 32, 3) shape6_1 = tf.squeeze(shape6, axis=1) print('降维:', shape6_1.shape) #改变维度 to_shape = tf.reshape(shape6_1, [1, 1, 4, 32, 32, 3]) print("改变维度:", to_shape.shape) transpose_num = tf.random.normal([32, 28, 3]) #按指定的顺序转置,得到(28, 32, 3) print("按指定的顺序交换: ", tf.transpose(transpose_num, perm=[1, 0, 2]).shape) #以下语句会报错,因为tf.squeeze只能对axis=0位置是1的数组进行操作 error_shape = tf.squeeze(rnd_shape4, axis=0) 运行结果如下: 获取维度: (4, 32, 32, 3) 升维: (1, 4, 32, 32, 3) 降维: (4, 32, 32, 3) 升维: (4, 1, 32, 32, 3) 降维: (4, 32, 32, 3) 改变维度: (1, 1, 4, 32, 32, 3) 按指定的顺序交换: (28, 32, 3) Tensorflow.python.framework.errors_impl.InvalidArgumentError: Can not squeeze dim[0], expected a dimension of 1, got 4 [Op:Squeeze] 属性shape为多维数组的形状,在进行张量操作时,需要特别注意张量的shape变化。代码中tf.random.normal([4,32,32,3])初始了一个shape为4×32×32×3的张量,tf.expand_dims(rnd_shape4,axis=0)即在最外层插入1个维度就变成shape为1×4×32×32×3的张量,其size总数不变; tf.squeeze(shape5,axis=0)将最外层的维度去除,就变成了4×32×32×3的张量,其size总数也不变。tf.expand_dims(rnd_shape4,axis=1)即在1轴插入1个维度,所以变成了4×1×32×32×3,总结规律可以发现,axis=?就在该位置插入1个维度,而tf.squeeze就减少1个维度。 tf.reshape()可以实现任意维度变化的操作,可以代替tf.expand_dims()、tf.squeeze()实现维度的变化。tf.transpose()按指定的axis进行交换,交换后size保持不变,但是shape发生了变化。 注意: tf.squeeze只能对axis所在位置维度为1的数组进行降维,所以error_shape变量时会报InvalidArgumentError错误。 3.3.5TensorFlow切片取值 TensorFlow支持如NumPy一样的下标取值方法,代码如下: #第3章/TensorFlowAPI/下标、切片操作.py #初始张量 a = tf.ones([1, 5, 5, 3]) #先取axis=0的内容,得到5*5*3;从5*5*3中再得到axis=0下标为2的内容,即5*3;从5*3 #中再得到axis=0下标为2的内容,即shape=(3,) print(a[0][2][2].shape) 运行结果如下: (3,) 代码a[0],即从1个5×5×3中得到第0个内容,所以shape为5×5×3; a[0] [2]即从5个5×3中得到下标为2的内容,即5×3;a[0] [2] [2]即从5个(3,)中得到下标为2的内容,即shape=(3,)。 上面的代码a[0][2][2]可以被修改成a[0,2,2]的表达方式,代码如下: #第3章/TensorFlowAPI/下标、切片操作.py import tensorflow as tf #初始张量 a = tf.ones([1, 5, 5, 3]) print(a[0].shape) #[5,5,3] 由外向内 print(a[0, 2].shape) #[5,3] 第0轴,第1轴里的第3个 print(a[0, 2, 2].shape) #[3,] 运行结果如下: (5, 5, 3) (5, 3) (3,) 根据结果可知,a[0][2][2]和a[0,2,2]的内容是一样的。从a[0,2,2]的表达式中可知每个axis可以用逗号隔开,有多少个逗号就有多少个维度。 多维度的切片与NumPy保持一致,代码如下: #第3章/TensorFlowAPI/下标、切片操作.py import tensorflow as tf #初始张量 a = tf.ones([1, 5, 5, 3]) print("多维度的切片:", a[0, :, :, :].shape) #得到5*5*3 print("多维度的切片:", a[:, :, :, 2].shape) #得到1*5*5 print("多维度间隔取值:", a[:, 0:4:2, 0:4:2, :].shape) #得到1*2*2*3 print("...省略:", a[..., 2].shape) #得到1*5*5 运行结果如下: 多维度的切片: (5, 5, 3) 多维度的切片: (1, 5, 5) 多维度间隔取值: (1, 2, 2, 3) ...省略: (1, 5, 5) 在每个axis中都可以使用切片的语法,a[0,:,:,:]表示axis=0中下标为0的所有的内容; a[:,:,:,2]表示axis=3中下标为2的内容,前面axis的内容保留,所以其shape为1×5×5; a[:,0:4:2,0:4:2,:]表示axis=0中所有的内容,axis=1每2取1个,所以axis=1时取下标0、2的内容,axis=2也一样,axis=3中所有的内容,所以其shape为1×2×2×3; a[...,0]中的...表示前面axis=0,1,2的内容全部保留,与a[:,:,:,2]相同。 TensorFlow中下标或者切片只能取值而不能进行修改,但是NumPy都可以,代码如下: #第3章/TensorFlowAPI/下标、切片操作.py import tensorflow as tf import numpy as np NumPy_a = np.ones([1, 5, 5, 3]) tensor_a = tf.ones([1, 5, 5, 3]) #NumPy修改值 NumPy_a[..., 0] = 0 #TensorFlow中不支持下标修改 tensor_a[..., 0] = 0 运行结果如下: tensor_a[..., 0] = 0 TypeError: 'Tensorflow.python.framework.ops.EagerTensor' object does not support item assignment 3.3.6TensorFlow中gather取值 函数gather(params,indices,axis=None)提供了更灵活、强大的取值方法。params要求传入张量,indices可以传入多种形式的索引号的值,axis为指定的轴位置,代码如下: #第3章/TensorFlowAPI/gather取值.py import tensorflow as tf params = tf.constant([10., 11., 12., 13., 14., 15.]) #取params索引下标为[2, 0, 2, 5]的内容 print(tf.gather(params, indices=[2, 0, 2, 5])) #先取params索引下标[2, 0]的值,组成1个维度,然后取索引下标[2, 5]的值,再组成1个维度 #然后将两个维度合并成一个新的维度,变成2*2的张量 print(tf.gather(params, [[2, 0], [2, 5]])) 运行结果如下: tf.Tensor([12. 10. 12. 15.], shape=(4,), dtype=float32) tf.Tensor( [[12. 10.] [12. 15.]], shape=(2, 2), dtype=float32) 代码中params只有一个维度,所以axis=0。将params的内容变为4×3,其gather取值的代码如下: #第3章/TensorFlowAPI/gather取值.py import tensorflow as tf params = tf.constant([[0, 1.0, 2.0], [10.0, 11.0, 12.0], [20.0, 21.0, 22.0], [30.0, 31.0, 32.0]]) #没有指定axis,默认为0轴,即下标索引为3和1的内容 print(tf.gather(params, indices=[3, 1])) #axis=1,则取axis=1中下标索引为2和1的内容 print(tf.gather(params, indices=[2, 1], axis=1)) 运行结果如下: tf.Tensor( [[30. 31. 32.] [10. 11. 12.]], shape=(2, 3), dtype=float32) tf.Tensor( [[ 2.1.] [12. 11.] [22. 21.] [32. 31.]], shape=(4, 2), dtype=float32) 代码中axis=1时会将索引下标为[2,1]的内容全部取走,所以得到的shape=(4,2),其取值过程如图319所示。 图319TensorFlow中gather取值 同样indices可以为多维张量,代码如下: #第3章/TensorFlowAPI/gather取值.py import tensorflow as tf params = tf.constant([[0, 1.0, 2.0], [10.0, 11.0, 12.0], [20.0, 21.0, 22.0], [30.0, 31.0, 32.0]]) #indices是一个多维张量 indices = tf.constant([ [2, 4], [0, 4]]) #当indices中的4超过params的索引时会自动补0 #因为axis=0,所以0轴补3个0 print(tf.gather(params, indices, axis=0)) print('#'*20) #因为axis=1,所以1轴补1个0 print(tf.gather(params, indices, axis=1)) 运行结果如下: tf.Tensor( [[[20. 21. 22.] [ 0.0.0.]] [[ 0.1.2.] [ 0.0.0.]]], shape=(2, 2, 3), dtype=float32) #################### tf.Tensor( [[[ 2.0.] [ 0.0.]] [[12.0.] [10.0.]] [[22.0.] [20.0.]] [[32.0.] [30.0.]]], shape=(4, 2, 2), dtype=float32) 多维张量gather取值与此类似,代码如下: #第3章/TensorFlowAPI/gather取值.py import tensorflow as tf params = tf.random.normal([4, 35, 8]) #axis=0就是第1个维度的变化 #[7, 9, 16]的下标不存在,会补0,所以是[5,35,8] result0 = tf.gather(params, axis=0, indices=[2, 3, 7, 9, 16]) #axis=1就是第2个维度的变化,所以是[4,5,8] result1 = tf.gather(params, axis=1, indices=[2, 3, 7, 9, 16]) #axis=2就是最里面的维度,所以是[4,35,3] result2 = tf.gather(params, axis=2, indices=[2, 3, 7]) #axis=2就是第3个维度的变化,所以[4,35]会保留,另外[[2, 3, 7], [0, 1, 2]]即对[8]取了 #两次,所以应该变成[2,3],结合后变成[4,35,2,3] result3 = tf.gather(params, axis=2, indices=[[2, 3, 7], [0, 1, 2]]) print(result0.shape, result1.shape, result2.shape, result3.shape) 运行结果如下: (5, 35, 8) (4, 5, 8) (4, 35, 3) (4, 35, 2, 3) 代码tf.gather(params, axis=0, indices=[2, 3, 7, 9, 16])中indices的数量超过了4,TensorFlow会自动补0,如图320所示。 图320TensorFlow中gather补0 3.3.7TensorFlow中布尔取值 TensorFlow提供了两种方式的布尔取值,一种是类似NumPy的风格,代码如下: #第3章/TensorFlowAPI/布尔取值.py import tensorflow as tf import numpy as np data = np.array([[1, 2], [3, 4], [5, 6]]) #获取data中>2的数 mask = data > 2 print('mask:', mask) print("输出满足条件的数: ", data[mask], data[data > 2]) 运行结果如下: mask: [[False False] [ TrueTrue] [ TrueTrue]] 输出满足条件的数: [3 4 5 6] [3 4 5 6] 与NumPy一样data[data>2]中的条件只能为简写表达式,其作用是将data中为True的内容取出来。 另一种方式是使用tf.boolean_mask()函数来取值,代码如下: #第3章/TensorFlowAPI/布尔取值.py import tensorflow as tf import numpy as np tensor = tf.random.normal([4, 28, 28, 3]) mask = np.array([True, True, False, False]) #由于4维中axis=0,1为True,所以输出是(2,28,28,3) print('多维度布尔取值: ', tf.boolean_mask(tensor, mask).shape) old = tf.random.normal([2, 3, 4]) print("原值: ", old.shape) #因为axis=0时,其shape为2,所以mask需要两个数组的内容,正好对应 #当axis=0时,0的下标为True,所以这里是2 * 4 result1 = tf.boolean_mask(old, mask=[[True, False, False], [True, False, False]], axis=0) #当axis=1时,其shape为3,所以需要有3个值 #当axis=1时,0和2的下标都为True,所以这里是2*2*4 result2 = tf.boolean_mask(old, mask=[True, False, True], axis=1) #当axis=2时,其shape为4,所以需要有4个值 #当axis=2时,下标0、2都为True,所以为2,则加外层,所以是2*3*2 result3 = tf.boolean_mask(old, mask=[True, False, True, False], axis=2) print('布尔多维取值axis=0: ', result1.shape) print('布尔多维取值axis=1: ', result2.shape) print('布尔多维取值axis=2: ', result3.shape) 运行结果如下: 多维度布尔取值: (2, 28, 28, 3) 原值: (2, 3, 4) 布尔多维取值axis=0: (2, 4) 布尔多维取值axis=1: (2, 2, 4) 布尔多维取值axis=2: (2, 3, 2) 代码中布尔值列表的长度需要跟axis中的shape数相等。例如result1中的mask=[[True, False, False],[True, False, False]]跟old张量shape=[2, 3, 4]中的2相同。 3.3.8TensorFlow张量合并 张量合并,即将多个张量合并成一个新张量,但是除指定的axis的shape可以不相等,其他shape必须相等,代码如下: #第3章/TensorFlowAPI/张量合并.py import tensorflow as tf #axis所在shape可以不相等,但是其他shape必须相等 #从0外围的维度合并Tensor,就是[6,35,8] print(tf.concat([tf.ones([4, 35, 8]), tf.ones([2, 35, 8])], axis=0).shape) #从1这个维度合并Tensor,就是[4,35,8] print(tf.concat([tf.ones([4, 32, 8]), tf.ones([4, 3, 8])], axis=1).shape) #不能合并的情况,axis=1这个维度没有对齐 try: df1 = tf.ones([4, 35, 8]) df2 = tf.ones([3, 33, 8]) print(tf.concat([df1, df2], axis=0).shape) except Exception as e: print(e) 运行结果如下: (6, 35, 8) (4, 35, 8) ConcatOp : Dimensions of inputs should match: shape[0] = [4,35,8] vs. shape[1] = [3,33,8] [Op:ConcatV2] name: concat 3.3.9TensorFlow网格坐标 生成网格点坐标张量的方法tf.meshgrid(),代码如下: #第3章/TensorFlowAPI/meshgrid生成网格.py import tensorflow as tf from matplotlib import pyplot as plt x = tf.cast([[0, 1, 2], [0, 1, 2]], dtype=tf.float32) y = tf.cast([[0, 0, 0], [1, 1, 1]], dtype=tf.float32) #生成网络坐标点,即[0,0],[1,0],[2,0],[0,1],[1,1],[2,1]共6个坐标 points_x, points_y = tf.meshgrid(x, y) #将坐标点画到图上 plt.plot(points_x, points_y, color='red', markersize=15, marker='.', linestyle='') #将坐标值画到图上 for i in range(x.shape[0]): for j in range(y.shape[1]): xl = x[i, j].NumPy() yl = y[i, j].NumPy() plt.text(xl, yl + 0.01, f"{xl},{yl}") plt.grid(True) plt.xlabel('X') plt.ylabel('Y') plt.title("tf.meshgrid生成网格坐标张量") plt.show() 运行结果如图321所示。 图321TensorFlow中生成网格坐标 3.3.10TensorFlow自动求梯度 TensorFlow提供了自动求梯度的功能,代码如下: #第3章/TensorFlowAPI/自动求导.py import tensorflow as tf def g(z): return 1 / (1 + tf.exp(-z)) #输入x的值 ga = tf.convert_to_tensor([[2.], [3.], [1.]]) #初始权重值 init_w = { 'w1': tf.Variable([[0.03, 0.02, 0.01], [0.013, 0.012, 0.001]]), 'w2': tf.Variable([[0.03, 0.02, 0.01], [0.013, 0.012, 0.001]]) } y = tf.convert_to_tensor([[1.], [0.]]) with tf.GradientTape() as tape: #存储每次反向传播的梯度 lw = [] lg = [] #进行两次前向传播 for i in range(1, 3): #获取权重 w = init_w['w%s' % i] #线性计算 z = w @ ga #非线性计算 out = g(z) #增加偏置项 ga = tf.concat([out, tf.cast([[1.0]], dtype=tf.float32)], axis=0) lw.append(w) lg.append(out) #损失函数 d = out - y loss = tf.reduce_sum(0.5 * tf.square(d)) #自动求梯度 gradients = tape.gradient(loss, lw) print('误差: ', d) #print('激活: ', lg) print('梯度: ', gradients) #梯度下降 for layer in range(len(gradients)): gradients[layer] = gradients[layer] - 0.1 * gradients[layer] #更新权重 init_w['w1'].assign(gradients[-1]) init_w['w2'].assign(gradients[0]) 运行结果如下: 误差: tf.Tensor( [[-0.4909289] [ 0.5035277]], shape=(2, 1), dtype=float32) 梯度: [<tf.Tensor: shape=(2, 3), dtype=float32, NumPy= array([[-0.00101757, -0.00152636, -0.00050879], [-0.00047112, -0.00070667, -0.00023556]], dtype=float32)>, <tf.Tensor: shape=(2, 3), dtype=float32, NumPy= array([[-0.06529391, -0.06325722, -0.12268066], [ 0.0669831 ,0.06489372,0.12585449]], dtype=float32)>] 自动求梯度只需在with tf.GradientTape()as tape作用域下,并使用tape.gradient(loss,lw)就可以根据loss求梯度。相对于“2.6.2反向传播算法”节中实现的梯度计算,使用自动求梯度功能非常方便。 总结 TensorFlow的基本API主要有维度变换、切片取值、自动求梯度等功能,其中自动求梯度功能对于网络的学习提供了极大的便利。 练习 调试本节所有代码,观察多维数组状态下张量的操作功能; 本节相关API在TensorFlow模型搭建中有重要作用。 3.4TensorFlow中的Keras模型搭建 3.4.1tf.keras简介 Keras是一个广泛受欢迎的神经网络框架,用Python编写而成,能够在TensorFlow、PyTorch中运行,其特点是支持快速神经网络模型搭建,同时网络模型具有很强的层次性,可以在CPU和GPU上运行。 TensorFlow 2.x版本中的Keras主要在tensorflow.keras模块下,包含Models、Layers、Optimizers等子模块,每个子模块又封装了子方法,如图322所示。 图322tf.keras结构概要图 使用tensorflow.keras可以方便、快速地构建神经网络模型,是深度学习代码实战中的重要组成部分。 3.4.2基于tf.keras.Sequential模型搭建 Sequential的参数是一个列表,列表里面存放着layers层下的各个算子,将各个算子按线型结构组装起来,模型就搭建完毕了。这个过程可以形象地类比成美食“串串”,Sequential就是那一根“竹签”,将各种“美食”(算子)串联起来,就可以“涮串串”了,代码如下: #第3章/TensorFlowAPI/基于tf.keras.Sequential模型搭建.py import tensorflow as tf from tensorflow.keras import Sequential, layers, Input #输入数据的shape input_shape = (28, 28, 1) model = Sequential( [ Input(shape=input_shape),#输入数据的维度需要指定 layers.Flatten(), #将多维数据打平成一维数据 layers.Dense(784, activation='sigmoid'), #784个神经元 layers.Dense(128, activation='sigmoid'), layers.Dense(10, activation='softmax') #10个类别 ] ) #描述模型的结构 model.summary() 运行结果如下: Model: "sequential" _________________________________________________________________ Layer (type) Output ShapeParam # ================================================================= flatten (Flatten)(None, 784) 0 _________________________________________________________________ dense (Dense)(None, 784) 615440 _________________________________________________________________ dense_1 (Dense)(None, 128) 100480 _________________________________________________________________ dense_2 (Dense)(None, 10)1290 ================================================================= Total params: 717,210 Trainable params: 717,210 Non-trainable params: 0 _________________________________________________________________ Sequential提供了网络模型组装方法,layers模块下提供了各个算子(例如Dense类)设置神经元的个数(Dense又称全连接层),其主要参数如下: def __init__(self, units,#神经元的个数 activation=None,#激活函数 use_bias=True,#是否支持偏置项,即wx+b中的b kernel_initializer='glorot_uniform', **kwargs): model.summary()会将各个网络层的shape及param参数自动计算出来。Input(shape=input_shape)用来表示输入数据的shape。 在model.summary()处断点,查看model对象的构成会发现input、output分别用于描述网络层的输入与输出,layers属性用来描述神经网络层中的结构,trainable_variables、trainable_weights属性会自动化初始神经网络权重中的θ值,如图323所示。 图323model对象结构 3.4.3继承tf.keras.Model类模型搭建 创建神经网络模型也可以通过自定义类来继承tf.keras.Model类,然后在__init__()构造方法中初始各个网络层,并在call()构造函数中实现前向传播的逻辑,代码如下: #第3章/TensorFlowAPI/继承tf.keras.Modle类模型搭建.py import tensorflow as tf from tensorflow.keras import layers class MyModle(tf.keras.Model): def __init__(self): #继承Model类中的属性 super(MyModel, self).__init__() #全连接 self.flat = layers.Flatten() self.dense1 = layers.Dense(784, activation='sigmoid') self.dense2 = layers.Dense(128, activation='sigmoid') #输出 self.out = layers.Dense(10, activation='softmax') #call()为类的实例方法,可以使类的实例对象成为可调用对象 def call(self, inputs): #在call()中实现前向传播 x = self.flat(inputs) x = self.dense1(x) x = self.dense2(x) #返回预测结果 return self.out(x) def get_config(self): #重载get_config方法 base_config = super(MyModel, self).get_config() return dict(list(base_config.items())) if __name__ == "__main__": model = MyModel() #继承Model类,需要通过build编译一下 model.build(input_shape=(28, 28, 1)) model.summary() 继承Model的方法,需要调用model.build(input_shape)指明输入的shape。 3.4.4函数式模型搭建 另一种方法是直接描述输入,然后将返回值再传给下一个算子的输入,直到最后一个输出,然后将输入与输出放入Model类,代码如下: #第3章/TensorFlowAPI/函数式模型搭建.py from tensorflow.keras import Input, layers, Model from tensorflow.keras.activations import sigmoid, softmax #描述输入 inputs = Input(shape=(28, 28, 1)) #描述中间过程 x = layers.Flatten()(inputs) x = layers.Dense(784, activation=sigmoid)(x) x = layers.Dense(128, activation=sigmoid)(x) outputs = layers.Dense(10, activation=softmax)(x) #把输入和输出装载在Model这个类里面 #inputs和outpus可以为列表,用来表示多输入或者多输出 model = Model(inputs=inputs, outputs=outputs) model.summary() 激活函数不仅可以传字符串,也可以使用tensorflow.keras.activations模块下定义好的方法。只要描述了inputs和outputs模型model便会自动地找到相关层。 总结 TensorFlow搭建模型可以使用Sequential,也可以继承Model类,同时还支持函数式搭建。 练习 分别使用3种搭建模型的方式完成拥有3个隐藏层的模型搭建,并调试代码以观察每行变量中对象的变化。 3.5TensorFlow中模型的训练方法 3.5.1使用model.fit训练模型 在TensorFlow中训练模型可以使用model.fit()方法,其参数如下: model.fit( x=None, #训练x的数据 y=None, #训练y的数据 batch_size=None, #每次训练的样本数 epochs=1, #迭代次数 verbose=1, #日志等级;0:为不在标准输出流输出日志信息; #1:显示进度条;2:每个epoch输出一行记录 callbacks=None, #回调函数,即在训练过程中将被调用执行的函数 validation_split=0.0, #从x和y中设置验证集的比例 validation_data=None, #验证集的数据,validation_data设置后会覆盖 #validation_split shuffle=True, #是否打乱数据,通常为True class_weight=None, #不重要,一般默认 sample_weight=None, #不重要,一般默认 initial_epoch=0, #开始的epoch次数设置 steps_per_epoch=None, #不重要,一般默认 validation_steps=None, #不重要,一般默认 validation_freq=1, #不重要,一般默认 max_queue_size=10, #不重要,一般默认 workers=1, #进程数,Linux可以设置多个,Windows为1 use_multiprocessing=False #是否多进程 ) 一般设置只需传入x、y、batch_size、epochs、callbacks参数,代码如下: #第3章/TensorFlowAPI/使用model.fit训练模型.py import tensorflow as tf from tensorflow.keras import Input, layers, Model from tensorflow.keras.activations import sigmoid, softmax import numpy as np #第1步: 读取数据 ######################## num_classes = 10 #类别数 input_shape = (28, 28, 1) #自动下载手写数字识别的数据集 (x_train, y_train), (x_test, y_test) = tf.keras.datasets.mnist.load_data() #归一化处理,将值放在0~1 x_train = x_train.astype('float32') / 255. x_test = x_test.astype('float32') / 255. #增维,变成[batch,height,width,channel] x_train = np.expand_dims(x_train, -1) x_test = np.expand_dims(x_test, -1) #将y转换为one_hot编码 y_train = tf.keras.utils.to_categorical(y_train, num_classes) y_test = tf.keras.utils.to_categorical(y_test, num_classes) #第2步: 构建模型 ######################## #描述输入 inputs = Input(shape=input_shape) #描述中间过程 x = layers.Flatten()(inputs) x = layers.Dense(784, activation=sigmoid)(x) x = layers.Dense(128, activation=sigmoid)(x) outputs = layers.Dense(10, activation=softmax)(x) #把输入和输出装载在Model这个类里面 #inputs和outpus可以为列表,用来表示多输入或者多输出 model = Model(inputs=inputs, outputs=outputs) model.summary() #第3步: 设置回调函数、学习率和损失函数 callbacks = [ tf.keras.callbacks.ModelCheckpoint( filepath='mnist_weights', save_best_only=True, monitor='val_loss', ), ] #设置学习率和损失函数 model.compile( optimizer=tf.keras.optimizers.Adam(learning_rate=0.001), loss=tf.keras.losses.CategoricalCrossentropy(from_logits=False), metrics=['accuracy'], #评价指定,此处为准确率 ) #第4步: 训练 ######################## model.fit( x=x_train, y=y_train, batch_size=2, epochs=10, validation_split=0.1, callbacks=callbacks ) 执行后就会开始进行训练,控制台会展示每个epoch训练后的损失、准确率的结果,如图324所示。 图324model.fit训练结果 训练数据用的是手写数字识别数据集,其主要任务是能够识别手写[0,9]的数字,共计10个分类,都是灰度图,其尺寸为28×28。tf.keras.datasets.mnist.load_data()会自动进行下载,共计6万张灰度图,如图325所示。 图325手写数字识别中的数据 因为一张图片,其值域在[0,255],这里通过x_train.astype('float32')/255将其值域限定在[0,1],可以在一定程度上缓解梯度爆炸。 由于mnist.load_data()的数据shape是60000×28×28,但是图片应该还有一个通道维度,所以需要通过np.expand_dims(x_train,-1)变成60000×28×28×1。 输出值y通过tf.keras.utils.to_categorical(y_test,num_classes)变成[0. 0. 0. 0. 1. 0. 0. 0. 0. 0.]的独热编码,这里表示数字4。共计10个分类,当前图片为哪个分类,那么其位置中的值为1,其他为0。 回调函数callbacks是一个列表,tf.keras.callbacks.ModelCheckpoint()用来设置模型和权重参数保存等信息,主要参数如下: keras.callbacks.ModelCheckpoint( filepath, #保存权重文件的位置 monitor='val_loss',#需要监视的值,val_accuracy、val_loss或者accuracy verbose=0, #信息展示方式: 0或者1 save_best_only=True, #True表示当前epoch的monitor比上一次好时保存 save_weights_only=False,#True只存储权重,下次想用还得搭建模型。 #False会将权重和模型一起保存 mode='auto',#save_best_only=True、monitor=val_accuracy, #mode=max;val_loss时为min;auto自动推断 period=1#隔几个epoch进行保存 ) 图326模型和权重文件的目录 在本例中设置save_weights_only=False,训练完成后存在的文件如图326所示。 图326中assets目录用于存储计算图; variables目录包含一个标准训练检查点的变量; saved_model.pb文件用于存储模型; keras_metadata.pb为Keras格式的权重。 当设置save_weights_only=True时,callbacks.ModelCheckpoint()中的filepath文件后缀名要为.h5文件,再次训练得到如图327所示的内容。 图327只存储权重参数文件 可在model.compile()函数中设置学习率和损失函数,其主要参数如下: def compile(self, optimizer='rmsprop',#梯度下降学习方法,俗称优化器,在 #tf.keras.optimizers模块下 loss=None, #损失函数设置,tf.keras.losses模块下有多种 #损失函数的封装。当然也可以自定义损失函数 metrics=None, #评价标准,自带accuracy和mse run_eagerly=None, #是否支持调试模型 ) 学习率在tf.keras.optimizers.Adam()中的learning_rate设置,CategoricalCrossentropy()为交叉熵损失。最后调用model.fit()函数TensorFlow会自动根据反向传播算法进行训练、学习。 神经网络的学习训练的过程如下: (1) 读取并处理数据。 (2) 构建神经网络模型。 (3) 回调函数、学习率、损失函数的设置。 (4) 调用model.fit()进行训练。 注意: model.fit()中的epochs的大小与训练计算机的显存有关系,如果设置得太大,则会报OOM错误;调用model.fit()后会自动进行反向传播算法的学习,但是这个过程在代码中是不可见的。 3.5.2使用model.train_on_batch训练模型 训练方式使用model.fit()虽然非常方便,但是不能在训练过程中调试代码,也不能在训练过程中进行自定义控制,因此TensorFlow还提供了model.train_on_batch的方法进行训练,代码如下: #第3章/TensorFlowAPI/使用model_train_on_batch训练模型.py #第4步: 训练 ######################## BATCH_SIZE = 6 epochs = 10 #按某个batch_size切割数据,并进行缓存 #训练集 ds_train = tf.data.Dataset.from_tensor_slices( (x_train, y_train) ).shuffle(buffer_size=1000).batch(BATCH_SIZE).prefetch( tf.data.experimental.AUTOTUNE ).cache() #验证集 ds_val = tf.data.Dataset.from_tensor_slices( (x_test, y_test) ).shuffle(buffer_size=1000).batch(BATCH_SIZE).prefetch( tf.data.experimental.AUTOTUNE ).cache() for epoch in tf.range(1, epochs + 1): #将模型的评价内容置为初始状态 model.reset_metrics() #获取当前学习率 new_lr = model.optimizer.lr #获取数据以进行训练 for x, y in ds_train: #训练 train_result = model.train_on_batch(x, y) #获取验证数据进行训练 for val_x, val_y in ds_val: valid_result = model.train_on_batch(val_x, val_y) #操作每5个epoch学习率变为2xnew_lr if epoch % 5 == 0: model.optimizer.lr.assign(new_lr * 2) tf.print("当前学习率:", new_lr.NumPy()) #每隔10个epoch保存一次权重文件 if epoch % 10 == 0: model.save(f'train_on_batch{epoch}.h5') #打印训练中的结果 tf.print("train:", dict(zip(model.metrics_names, train_result))) tf.print("valid:", dict(zip(model.metrics_names, valid_result))) 在3.4.5节的基础上更改了训练的方式,通过自定义for epoch in tf.range(1,epochs+1)来控制学习率每5个epoch变为2xnew_lr,每10个epoch保存一次权重文件。model.train_on_batch(x,y)每次循环的单个数据,同样该函数也自动进行反向传播并进行训练、学习,学习的过程并没有用代码来体现。 运行后可以看到目录中存在train_on_batch10.h5这个权重文件。观察控制台的输出会发现当前学习率已从0.001变为0.004,如图328所示。 图328train_on_batch训练模型 如果model.save()不指定后缀名,则默认保存为TensorFlow的格式,如图329所示。 图329save保存为tf格式的模型和参数 3.5.3自定义模型训练 训练方式model.fit()完全是一个黑箱操作,而model.train_on_batch虽然能够在一定程度上对训练过程进行控制,但是与反向传播学习的原理并不对应,同时其灵活程度有时不够,此时就可以使用完全自定义模型进行训练、学习,代码如下: #第3章/TensorFlowAPI/完全自定义模型训练.py #第4步: 训练 ######################## def train_step(model, x, y): #model:模型 #features:训练集x #labels:训练集y #tf.GradientTape()自动求梯度的作用域语句 with tf.GradientTape() as tape: #前向传播 predictions = model(x, training=True) #使用损失函数 loss = loss_func(y, predictions) #根据损失函数自动求梯度 gradients = tape.gradient(loss, model.trainable_variables) #将梯度更新到model.trainable_variables属性中,然后由optimizers进行指定优化器 #的梯度下降 optimizer.apply_gradients(zip(gradients, model.trainable_variables)) #更新评价指标 train_loss.update_state(loss) train_metric.update_state(y, predictions) #验证集 def valid_step(model, features, labels): #验证集不进行梯度下降更新学习 predictions = model(features) batch_loss = loss_func(labels, predictions) valid_loss.update_state(batch_loss) valid_metric.update_state(labels, predictions) BATCH_SIZE = 6 epochs = 10 #按某个batch_size切割数据,并进行缓存 #训练集 ds_train = tf.data.Dataset.from_tensor_slices( (x_train, y_train) ).shuffle(buffer_size=1000).batch(BATCH_SIZE).prefetch( tf.data.experimental.AUTOTUNE ).cache() #验证集 ds_val = tf.data.Dataset.from_tensor_slices( (x_test, y_test) ).shuffle(buffer_size=1000).batch(BATCH_SIZE).prefetch( tf.data.experimental.AUTOTUNE ).cache() for epoch in tf.range(1, epochs + 1): #训练集 for x, y in ds_train: train_step(model, x, y) #验证集 for val_x, val_y in ds_val: valid_step(model, val_x, val_y) logs = f'Epoch={epoch},' \ f'Loss:{train_loss.result()},' \ f'Accuracy:{train_metric.result()},' \ f'Valid Loss:{valid_loss.result()},' \ f'Valid Accuracy:{valid_metric.result()}' #每隔10个epoch保存一次权重文件 if epoch % 5 == 0: model.save(f'完全自定义训练{epoch}') tf.print(logs) #将评价指标重置为0 train_loss.reset_states() valid_loss.reset_states() train_metric.reset_states() valid_metric.reset_states() 运行结果如图330所示。 图330自定义训练结果 在函数train_step(model,x,y)中使用tf.GradientTape()来控制model(x,training=True)进行前向传播,并定义损失函数loss_func(y,predictions),然后根据损失loss对象结合tape.gradient(loss,model.trainable_variables)求梯度。 然后根据优化器的选择optimizer.apply_gradients(zip(gradients,model.trainable_variables))更新梯度下降的权重参数。 函数valid_step(model,features,labels)是验证集,并不需要进行梯度下降的更新。整个训练、学习过程与神经网络反向传播算法的原理相对应,相对model.fit()更容易理解,但构造过程稍复杂一些。 总结 在TensorFlow中搭建模型可使用model.fit、model.train_on_batch和自定义模型训练的方式,其形式灵活多变。 练习 分别使用3种搭建模型的方式及3种不同的训练方式进行组合,完成手写数字识别的训练。 3.6TensorFlow中Metrics评价指标 3.6.1准确率 准确率是分类问题中最为原始的评价指标,准确率的定义是预测正确的结果占总样本的百分比,其公式如下: Accuracy=TP+TNTP+TN+FP+FN(31) 公式中各评价指标的含义如下。 (1) TP(True Positive): 真正例,被模型预测为正的正样本。 (2) FP(False Positive): 假正例,被模型预测为正的负样本。 (3) FN(False Negative): 假负例,被模型预测为负的正样本。 (4) TN(True Negative): 真负例,被模型预测为负的负样本。 在kearas.metrics模块下框架已自带正例、负例的统计,只需将训练的代码修改如下: #第3章/TensorFlowAPI/神经网络的评价指标.py #设置学习率和损失函数 METRICS = [ tf.keras.metrics.TruePositives(name='TP'), tf.keras.metrics.FalsePositives(name='FP'), tf.keras.metrics.TrueNegatives(name='TN'), tf.keras.metrics.FalseNegatives(name='FN'), ] model.compile( optimizer=tf.keras.optimizers.Adam(learning_rate=0.001), loss=tf.keras.losses.CategoricalCrossentropy(from_logits=False), metrics=METRICS, #评价指定,此处为准确率 run_eagerly=True ) def plt_accuracy(history): h = history.history #history.history得到的是字典 #但是v不是一个list,转换成NDarray格式以方便计算 h = {k: np.array(v) for k, v in h.items()} train_accuracy = (h['TP'] + h['FP']) / (h['TP'] + h['FP'] + h['TN'] + h['FN']) val_accuracy = (h['val_TP'] + h['val_FP']) / (h['val_TP'] + h['val_FP'] + h['val_TN'] + h['val_FN']) plt.plot(history.epoch, train_accuracy, label='训练准确率') plt.plot(history.epoch, val_accuracy, label='验证准确率') plt.xlabel('迭代次数') plt.ylabel('准确率') plt.title('迭代次数与准确率的变化') plt.legend(loc='best') plt.show() #第4步: 训练 ######################## history = model.fit( x=x_train, y=y_train, batch_size=6, epochs=3, validation_split=0.1, callbacks=callbacks ) #对训练结果进行绘图 plt_accuracy(history) 训练结束history中将自带METRICS的结果,plt_accuracy()通过解析history中的内容,通过准确率公式计算后得到结果,如图331所示。 图331准确率的变化 准确率在类别数据量不平衡时,并不能客观地评价算法的优劣,例如测试集中有100个样本,其中99个是负样本,1个是正样本,假设算法模型都能正确地预测出负样本,那么模型的准确率就是99%,从数值来看效果不错,但事实上这个算法没有任何预测能力(因为正样本没有预测成功,模型偏向于预测负样本)。 3.6.2精确率 精确率又叫查准率,在所有被预测为正的样本中实际为正样本的概率,其公式如下: Precision=TPTP+FP (32) 精确率和准确率看上去有些类似,但它们是两个不同的概念。精确率代表对正样本结果的预测准确程度,而准确率代表整体的预测准确程度,包括正样本和负样本,代码如下: #第3章/TensorFlowAPI/神经网络的评价指标.py #精准率 def plt_precision(history): h = history.history #history.history得到的是个字典 #但是v不是一个list,转换成NDarray格式以方便计算 h = {k: np.array(v) for k, v in h.items()} train_accuracy = (h['TP']) / (h['TP'] + h['FP']) val_accuracy = (h['val_TP']) / (h['val_TP'] + h['val_FP']) plt.plot(history.epoch, train_accuracy, label='训练精准率') plt.plot(history.epoch, val_accuracy, label='验证精准率') plt.xlabel('迭代次数') plt.ylabel('精准率') plt.legend(loc='best') plt.title('迭代次数与精准率的变化') plt.show() 调用执行后的结果如图332所示。 图332精确率的变化 3.6.3召回率 召回率又叫查全率,它的含义是在实际为正的样本中被预测为正样本的概率,公式如下: Recall=TPTP+FN (33) 代码如下: #第3章/TensorFlowAPI/神经网络的评价指标.py #召回率 def plt_recall(history): h = history.history #history.history得到的是个字典 #但是v不是一个list,转换成NDarray格式以方便计算 h = {k: np.array(v) for k, v in h.items()} train_accuracy = (h['TP']) / (h['TP'] + h['FN']) val_accuracy = (h['val_TP']) / (h['val_TP'] + h['val_FN']) plt.plot(history.epoch, train_accuracy, label='训练召回率') plt.plot(history.epoch, val_accuracy, label='验证召回率') plt.xlabel('迭代次数') plt.ylabel('召回率') plt.legend(loc='best') plt.title('迭代次数与召回率的变化') plt.show() 调用执行后得到的结果如图333所示。 图333召回率的变化 3.6.4PR曲线 在不同的应用场景中关注的指标不同。例如,在预测股票时更关心精准率,人们更关心那些升值的股票里真正升值的有多少,而在医疗病患中更关心的是召回率,即真的病患里我们预测错的情况应该越小越好。 精准率和召回率是一对此消彼长的关系。例如,在推荐系统中,我们想推送的内容应尽可能地让用户感兴趣,那只能推送我们把握较高的内容,这样就漏掉了一些用户感兴趣的内容,召回率就低了; 如果想让用户感兴趣的内容都被推送,就只能推送全部内容,但是这样精准率就低了。在实际工程中,往往需要结合两个指标的结果,从而寻找到一个平衡点,使算法的性能达到最大化,这就是PR曲线,代码如下: #第3章/TensorFlowAPI/神经网络的评价指标.py def plt_PR(history): h = history.history #history.history得到的是个字典 #但是v不是一个list,转换成NDarray格式以方便计算 h = {k: np.array(v) for k, v in h.items()} #精准率 train_precision = (h['TP']) / (h['TP'] + h['FP']) val_precision = (h['val_TP']) / (h['val_TP'] + h['val_FP']) #召回率 train_recall = (h['TP']) / (h['TP'] + h['FN']) val_recall = (h['val_TP']) / (h['val_TP'] + h['val_FN']) plt.plot(train_recall, train_precision, label='训练P-R曲线') plt.plot(val_recall, val_precision, label='验证P-R曲线') plt.xlabel('ReCall') plt.ylabel('Precision') plt.legend(loc='best') plt.title('P-R曲线') plt.show() 调用执行后得到的结果如图334所示。 图334PR曲线变化 模型的精准率和召回率互相制约,PR曲线越向右上凸出,表示模型的性能越好;由于精准率和召回率更关注正样本的情况,当负样本比较多时PR曲线的效果一般,此时使用ROC曲线更合适。 3.6.5F1Score 平衡精准率和召回率的另一个指标是F1Score,其公式如下: F1=2×P×RP+R(34) F1值越高,代表模型的性能越好,代码如下: #第3章/TensorFlowAPI/神经网络的评价指标.py def plt_F1_Score(history): h = history.history #history.history得到的是个字典 #但是v不是一个list,转换成NDarray格式以方便计算 h = {k: np.array(v) for k, v in h.items()} #精准率 train_precision = (h['TP']) / (h['TP'] + h['FP']) val_precision = (h['val_TP']) / (h['val_TP'] + h['val_FP']) #召回率 train_recall = (h['TP']) / (h['TP'] + h['FN']) val_recall = (h['val_TP']) / (h['val_TP'] + h['val_FN']) f1_train = (2 * train_precision * train_recall) / (train_precision + train_recall) f1_val = (2 * val_precision * val_recall) / (val_precision + val_recall) plt.plot(history.epoch, f1_train, label='训练集F1-Score') plt.plot(history.epoch, f1_val, label='验证集F1-Score') plt.xlabel('迭代次数') plt.ylabel('F1-Score') plt.legend(loc='best') plt.title('迭代次数F1-Score的变化') plt.show() 调用执行后得到的结果如图335所示。 图335F1Score曲线变化 3.6.6ROC曲线 在实际数据集中经常会出现类别不平衡现象,即负样本比正样本多(或者相反),而测试数据中的正负样本的分布也可能随着时间的变化而变化,ROC曲线和AUC曲线可以很好地消除样本类别不平衡对评估指标结果的影响。 ROC曲线可以无视样本的不平衡,主要取决于灵敏度(sensitivity)和特异度(specificity),又称为真正率(TPR)和假正率(FPR)。 (1) 真正率(True Positive Rate,TPR),又称为灵敏度。 (2) 假负率(False Negative Rate,FNR)。 (3) 假正率(False Positive Rate,FPR)。 (4) 真负率(True Negative Rate,TNR),又称为特异度。 对应公式为 TPR=TPTP+FN FNR=FNTP+FN FPR=FPTN+FP TNR=TNTN+FP (35) 从式(35)可知,灵敏度TPR与召回率一样,是正样本的召回率,特异度TNR是负样本的召回率,而假负率FNR=1-TPR,假正率FPR=1-TNR,这4个维度都是针对单一类别的预测结果而言的,所以对整体样本是否均衡并不敏感。例如,假设总样本中,90%是正样本,10%是负样本,在这种情况下如果使用准确率进行评价,则是不科学的,但是TPR和TNR是可以的,因为TPR只关注90%正样本中有多少是被预测正确的,而与那10%的负样本没有关系,同样FPR只注意10%负样本中有多少是被预测错误的,也与90%的正样本无关,这样就避免了样本不平衡的问题。 ROC曲线主要由灵敏度TPR和假正率FPR构成,其中横坐标为假正率FPR、纵坐标为灵敏度TPR,代码如下: #第3章/TensorFlowAPI/神经网络的评价指标.py def plt_ROC(history): h = history.history #history.history得到的是个字典 #但是v不是一个list,转换成NDarray格式以方便计算 h = {k: np.array(v) for k, v in h.items()} train_FPR = (h['FP']) / (h['TN'] + h['FP']) train_TPR = (h['TP']) / (h['TP'] + h['FN']) val_FPR = (h['val_FP']) / (h['val_TN'] + h['val_FP']) val_TPR = (h['val_TP']) / (h['val_TP'] + h['val_FN']) plt.plot(train_FPR, train_TPR, label='训练集ROC') plt.plot(val_FPR, val_TPR, label='验证集ROC') plt.xlabel('False Positive Rate') plt.ylabel('True Positive Rate') plt.legend(loc='best') plt.title('ROC曲线') plt.show() 调用执行后得到的结果如图336所示。 图336ROC曲线变化 ROC曲线中如果FPR越低TPR越高,则代表性能越高。 3.6.7AUC曲线 AUC(Area Under Curve)又称曲线下面积,是处于ROC曲线下方的面积的大小,ROC曲线下方面积越大说明模型性能越好,因此AUC同理。如果是完美模型,则它的AUC=1,说明所有正例排在负例的前面,模型达到最优状态,代码如下: #第3章/TensorFlowAPI/神经网络的评价指标.py #增加评价指标 METRICS = [ tf.keras.metrics.AUC(name='AUC') ] def plt_AUC(history): h = history.history #history.history得到的是个字典 #但是v不是一个list,转换成NDArray格式以方便计算 h = {k: np.array(v) for k, v in h.items()} train_auc = h['AUC'] val_auc = h['val_AUC'] plt.plot(history.epoch, train_auc, label='训练集AUC') plt.plot(history.epoch, val_auc, label='验证集AUC') plt.xlabel('Epoch') plt.ylabel('AUC') plt.legend(loc='best') plt.title('Epoch与AUC的变化') plt.show() 调用执行后得到的结果如图337所示。 图337AUC曲线变化 3.6.8混淆矩阵 混淆矩阵(Confusion Matrix)又被称为错误矩阵,通过它可以直观地观察到算法的分类效果,它的每列表示真值样本的分类概率,每行表示预测样本的分类概率,它反映了分类结果的混淆程度,代码如下: #第3章/TensorFlowAPI/神经网络的评价指标.py from collections import deque #自己实现的混淆矩阵函数,只保存最后1个epoch的结果 val_ll = deque([], maxlen=1) def confusion_matrix_acc(y_true, y_pred): #取预测值下标的最大值的位置 pre_label = tf.argmax(y_pred, axis=-1) #取真实值下标的最大值的位置 true_lable = tf.argmax(y_true, axis=-1) #调用混淆矩阵函数 matrix_mat = tf.math.confusion_matrix(true_lable, pre_label) #只保存最后一次预测与真实值的矩阵值 val_ll.append(matrix_mat) #这个返回值没有使用,语法要求要有返回值,metrics默认求平均 return matrix_mat #在compile中的metrics增加自定义评价函数confusion_matrix_acc model.compile( optimizer=tf.keras.optimizers.Adam(learning_rate=0.001), loss=tf.keras.losses.CategoricalCrossentropy(from_logits=False), metrics=[METRICS, confusion_matrix_acc], #评价指定,此处为准确率 run_eagerly=True ) #将矩阵中的数据转换成浮点数 def getMatrix(matrix): #求和 per_sum = matrix.sum(axis=1) #得到每个单元格的概率 ll = [matrix[i, :] / per_sum[i] for i in range(10)] #保留两位小数 matrix = np.around(np.array(ll), 3) #如果有异常值,则赋为0 matrix[np.isnan(matrix)] = 0 return matrix #绘制混淆矩阵 def plt_confusion_matrix(class_num=10): #设置类别名称 kind = [f"数字_{x}" for x in range(class_num)] #将分类概率转换为百分比 data = getMatrix(val_ll[0].NumPy()) plt.imshow(data, cmap=plt.cm.Blues) plt.title("手写数字识别混淆矩阵") #title plt.xlabel("预测值") plt.ylabel("真实值") plt.yticks(range(class_num), kind) #y轴标签 plt.xticks(range(class_num), kind, rotation=45) #x轴标签 #数值处理 for x in range(class_num): for y in range(class_num): value = float(data[y, x]) color = 'red' if value else 'green' plt.text(x, y, value, verticalalignment='center', horizontalalignment='center', color=color) #自动调整子图参数,使之填充整个图像区域 plt.tight_layout() plt.colorbar() plt.savefig("手写数字识别的混淆矩阵.jpg") plt.show() 调用执行后得到的结果如图338所示。 图338混淆矩阵 如图338中,当预测“数字_0”时,真实值“数字_0”的概率为0.948,预测值为“数字_3”的概率是0.02,也就是说有0.052的概率当前模型将“数字_0”预测为其他数字。代码中主要使用了tf.math.confusion_matrix(true_lable,pre_label)来统计验证集中预测为正样本的数量,因为其返回的是一个整数,所以getMatrix(matrix)将其转换成了概率。deque([],maxlen=1)是一个队列,因为model.fit()在训练时,metrics不能从confusion_matrix_acc()函数得到返回值matrix_mat(metrics本身没有混淆矩阵这个指标),所以这里使用队列来更新matrix_mat的值。 总结 评估模型的指标有准确率、精确率、召回率、PR曲线、F1Score、混淆矩阵等指标。 练习 调试并重写评价指标的代码,观察模型评估指标与代码实现的关系。 3.7TensorFlow中的推理预测 推理预测调用load_model()函数加载保存的权重文件,然后使用cv2.imread()读入灰度图,并进行维度的变化,最后由tf.argmax取最大下标对应的类别,得到推理结果,代码如下: #第3章/TensorFlowAPI/预测推理.py import tensorflow as tf import cv2 from tensorflow.keras.models import load_model if __name__ == "__main__": model = load_model('完全自定义训练400') height,width = 28, 28 #读预测图片 img = cv2.imread('test.png', cv2.IMREAD_GRAYSCALE) #转换为模型输入的shape img = cv2.resize(img, [height,width]) #转换为torch.tensor()类型 img_tensor = tf.cast(img, dtype=tf.float32) #因为模型输入要为[1,28,28,1],所以升维 img_tensor = tf.reshape(img_tensor, [1, height,width, 1]) output = model.predict(img_tensor) #取预测结果最大值的下标 index = tf.argmax(output, -1).NumPy() #预测类型 kind = [f"数字_{x}" for x in range(10)] print(f"预测={kind[int(index+1)]},概率={round(float(output[:, int(index)]), 2)}") 运行结果如图339所示。 图339TensorFlow预测结果 总结 模型搭建、模型训练、模型保存、模型推理预测是神经网络的4大阶段,推理阶段也是实际软件应用的主要内容。 练习 完成手写数字识别模型的推理代码。 3.8PyTorch搭建神经网络 3.8.1PyTorch中将数据转换为张量 PyTorch中将数据转换为张量的函数常见的有torch.tensor()、torch.from_numpy()、torch.as_tenor()等,代码如下: #第3章/PyTorchAPI/转换为张量.py import torch import numpy as np from torch.autograd import Variable #(1) 创建标量 scalar = torch.tensor([[1, 2], [3, 4]], dtype=torch.float32) scalar[0, :] = 0 print(scalar) #(2) 从NumPy转换为张量 np_tensor = torch.from_numpy(np.random.rand(2, 2)) print(np_tensor) #(3) torch随机生成指定维度的数据 rnd = torch.randn([2, 2]) print(rnd) print('#'*30) 运行结果如下: tensor([[0., 0.], [3., 4.]]) tensor([[0.5540, 0.9942], [0.3511, 0.1088]], dtype=torch.float64) tensor([[ 1.6299, -2.0948], [-0.1387, -0.4610]]) 代码scalar[0, :]=0会运行成功,而TensorFlow则无法运行。PyTorch中的张量可以通过索引操作改变值的内容。 如果想定义一个变量,即专门用来存储权重参数并能够进行自动求导的张量,则需要使用Variable()类,代码如下: #第3章/PyTorchAPI/转换为张量.py #(4) 创建权重参数变量 var_tensor = Variable(scalar, requires_grad=True) #(5) 损失函数 v_out = torch.mean(var_tensor * var_tensor) #(6) 反向传播 v_out.backward() #(7) 获取梯度值 print(var_tensor.grad) 运行结果如下: tensor([[0.0000, 0.0000], [1.5000, 2.0000]]) 3.8.2PyTorch指定设备 指定设备使用torch.device()传入cpu或者cuda可指定硬件来运行,代码如下: #第3章/PyTorchAPI/指定设备.py import torch tensor1 = torch.tensor([[1, 2], [3, 4]]) tensor2 = torch.tensor([[3, 3], [5, 5]]) device = torch.device("cuda" if torch.cuda.is_available() else "cpu") if device: print(tensor1 + tensor2) 运行结果如下: tensor([[4, 5], [8, 9]]) 3.8.3PyTorch数学运算 常见的数学操作已被PyTorch封装,代码如下: #第3章/PyTorchAPI/数学运算.py import torch tensor1 = torch.tensor([[1., 2.], [3., 4.]]) tensor2 = torch.tensor([[3., 3.], [5., 5.]]) #矩阵内积 print(torch.matmul(tensor1, tensor2)) #矩阵点乘 print(torch.mul(tensor1, tensor2)) #矩阵相加 print(torch.add(tensor1, tensor2)) #矩阵相减 print(torch.sub(tensor1, tensor2)) #矩阵平方根 print(torch.sqrt(tensor1)) #矩阵求平均 print(torch.mean(tensor1)) 运行结果如下: tensor([[13., 13.], [29., 29.]]) tensor([[ 3., 6.], [15., 20.]]) tensor([[4., 5.], [8., 9.]]) tensor([[-2., -1.], [-2., -1.]]) tensor([[1.0000, 1.4142], [1.7321, 2.0000]]) tensor(2.5000) 3.8.4PyTorch维度变化 维度的变化主要使用torch.reshape()、torch.unsqueeze()、torch.squeeze()、torch.transpose()函数,代码如下: #第3章/PyTorchAPI/维度变化.py import torch tensor1 = torch.tensor([[1., 2.], [3., 4.]]) #由原来的2*2,变换为[1,2,2] print(torch.reshape(tensor1, [1, 2, 2]).shape) #在原来的2*2的axis=0处增加1这个维度 new_tensor = torch.unsqueeze(tensor1, 0) print(new_tensor.shape) #在原来的1*2*2的axis=0处,减去1这个维度 print(torch.squeeze(new_tensor, 0).shape) #使用transpose变换维度,按axis进行变换,由原来的1*2*2变成2*1*2 print(torch.transpose(new_tensor, 1, 0).shape) 运行结果如下: torch.Size([1, 2, 2]) torch.Size([1, 2, 2]) torch.Size([2, 2]) torch.Size([2, 1, 2]) 3.8.5PyTorch切片取值 其切片取值的语法与TensorFlow保持一致,需要注意的是PyTorch中通过切片取值可以进行修改,代码如下: #第3章/PyTorchAPI/切片取值.py import torch a = torch.randn([4, 28, 28, 3]) print(a[0, ...].shape) #第1个[28,28,3] print(a[0, 1, :, :].shape) #[28,3] print(a[:, :, :, 2].shape) #[4,28,28],因为它取的是前面所有的值,而最里层的是 #第0个,所以是[4,28,28] print(a[..., 2].shape) #[4,28,28] print(a[:, 0, :, :].shape) #[4,28,3] 运行结果如下: torch.Size([28, 28, 3]) torch.Size([28, 3]) torch.Size([4, 28, 28]) torch.Size([4, 28, 28]) torch.Size([4, 28, 3]) 3.8.6PyTorch中gather取值 PyTorch中的gather取值与TensorFlow类似,代码如下: #第3章/PyTorchAPI/gather取值.py import torch params = torch.tensor([[1, 2], [3, 4]]) #沿着1轴取值: [0,0]表示第1行取下标为0,0的值; [3,4]表示第2行取下标为1,1的值 print(torch.gather(params, 1, index=torch.tensor([[0, 0], [1, 1]]))) #沿着0轴取值 print(torch.gather(params, 0, index=torch.tensor([[1, 1], [0, 0]]))) 运行结果如下: tensor([[1, 1], [4, 4]]) tensor([[3, 4], [1, 2]]) 3.8.7PyTorch中布尔取值 在PyTorch中通常使用切片的语法来过滤数据,当然也可以使用torch.masked_select()函数,代码如下: #第3章/PyTorchAPI/布尔取值.py import torch data = torch.tensor([[1, 2], [3, 4], [5, 6]]) #获取data中>2的数 mask = data > 2 print('mask:', mask) #通常直接使用data[data > 2]来表示 print("输出满足条件的数: ", data[mask], data[data > 2]) #按布尔掩码选择元素 print("输出满足条件的数: ", torch.masked_select(data, mask, out=None)) 运行结果如下: mask: tensor([[False, False], [ True, True], [ True, True]]) 输出满足条件的数: tensor([3, 4, 5, 6]) tensor([3, 4, 5, 6]) 输出满足条件的数: tensor([3, 4, 5, 6]) 3.8.8PyTorch张量合并 张量合并使用torch.cat()函数来完成,代码如下: #第3章/TensorFlowAPI/张量合并.py import torch t1 = torch.randn([4, 28, 28]) t2 = torch.randn([2, 28, 28]) #按axis=0进行合并 print(torch.cat([t1, t2], dim=0).shape) 运行结果如下: torch.Size([6, 28, 28]) 3.8.9PyTorch模型搭建 PyTorch搭建神经网络模型主要集中在torch.optim模块和torch.nn模块下,其模块下的概要结构如图340所示。 图340PyTorch模型搭建主要模块 PyTorch模型创建继承自torch.nn.Module类,可以在__init__()初始构造函数中描述属性,然后在forward()方法中实现网络的前向传播,其描述方法与3.4.3节类似,代码如下: #第3章/PyTorchAPI/手写数字识别.py import torch #pip install thop from thop import profile #搭建模型 class MyModel(torch.nn.Module): def __init__(self): #继承Model类中的属性 super(MyModel, self).__init__() #全连接 self.flat = torch.nn.Flatten() #Linear(input_channel,output_channel) self.fc = torch.nn.Sequential( torch.nn.Linear(784, 784), torch.nn.Linear(784, 128) ) #输出 self.out = torch.nn.Linear(128, 10) def forward(self, input_x): x = self.flat(input_x) #第1个网络层的计算和输出 x = torch.sigmoid(self.fc(x)) return torch.sigmoid(self.out(x)) if __name__ == "__main__": model = MyModel() print("输出网络结构: ", model) #PyTorch输入的维度是batch_size,channel,height,width input = torch.randn(1, 1, 28, 28) flops, params = profile(model, inputs=(input,)) print('浮点计算量: ', flops) print('参数量: ', params) 运行结果如下: 输出网络结构: MyModel( (flat): Flatten(start_dim=1, end_dim=-1) (fc): Sequential( (0): Linear(in_features=784, out_features=784, bias=True) (1): Linear(in_features=784, out_features=128, bias=True) ) (out): Linear(in_features=128, out_features=10, bias=True) ) [INFO] Register count_linear() for <class 'torch.nn.modules.linear.Linear'>. [INFO] Register zero_ops() for <class 'torch.nn.modules.container.Sequential'>. 浮点计算量: 716288.0 参数量: 717210.0 torch.nn.Sequential()与tf.keras.Sequential()一样,将神经网络各层“串起来”。获取模型的参数量使用了thop这个库。 注意: PyTorch的输入维度是[batch_size,channel,height,width],而TensorFlow的输入维度是[batch_size,height,width,channel]。 3.8.10PyTorch模型自定义训练 PyTorch的自定义训练与3.5.3节类似,主要步骤如下: (1) 读取训练数据和验证数据。 (2) 设置优化器、学习率等。 (3) 设置损失函数。 (4) 对训练集进行学习。 (5) 对验证集进行学习。 (6) 保存模型和参数。 使用手写数字识别模型MyModel进行训练,代码如下: #第3章/PyTorchAPI/手写数字识别自定义训练.py if __name__ == "__main__": model = MyModel() print("输出网络结构: ", model) #训练过程 #(1)加载数据集 batch_size = 60 learning_rate = 1e-5 epochs = 10 #手写数字识别的数据集会被自动下载到当前./data目录 #训练集 train_loader = torch.utils.data.DataLoader( torchvision.datasets.MNIST( './data/', train=True, transform=torchvision.transforms.ToTensor(), download=True ), batch_size=batch_size, shuffle=True, ) #验证集 val_loader = torch.utils.data.DataLoader( torchvision.datasets.MNIST( './data/', train=False, download=True, transform=torchvision.transforms.ToTensor(), ), batch_size=batch_size, shuffle=True) #(2)设置优化器、学习率 optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate) #(3)设置损失函数 criterion = torch.nn.CrossEntropyLoss() #(4)训练 def model_train(data, label): #清空模型的梯度 model.zero_grad() #前向传播 outputs = model(data) #计算损失 loss = criterion(outputs, label) #计算acc pred = torch.argmax(outputs, dim=1) accuracy = torch.sum(pred == label) / label.shape[0] #反向传播 loss.backward() #权重更新 optimizer.step() #进度条显示loss和acc desc = "train.[{}/{}] loss: {:.4f}, Acc: {:.2f}".format( epoch, epochs, loss.item(), accuracy.item() ) return loss, accuracy, desc #验证 @torch.no_grad() def model_val(val_data, val_label): #前向传播 val_outputs = model(val_data) #计算损失 loss = criterion(val_outputs, val_label) #计算前向传播结果与标签一致的数量 val_pred = torch.argmax(val_outputs, dim=1) num_correct = torch.sum(val_pred == val_label) return loss, num_correct #根据指定的epoch进行学习 for epoch in range(1, epochs + 1): #进度条 process_bar = tqdm(train_loader, unit='step') #开始训练模式 model.train(True) #因为train_loader是按batch_size划分的,所以都要进行学习 for step, (data, label) in enumerate(process_bar): #调用训练函数 loss, accuracy, desc = model_train(data, label) #输出日志 process_bar.set_description(desc) #在训练的最后1组数据后面进行验证 if step == len(process_bar) - 1: #用来计算总数 total_loss, correct = 0, 0 #根据验证集进行验证 model.eval() for _, (val_data, val_label) in enumerate(val_loader): #验证集前向传播 loss, num_correct = model_val(val_data, val_label) total_loss += loss correct += num_correct #计算总测试的平均acc val_acc = correct / (batch_size * len(val_loader)) #计算总测试的平均loss val_Loss = total_loss / len(val_loader) #验证集的日志 var_desc = " val.[{}/{}]loss: {:.4f}, Acc: {:.2f}".format( epoch, epochs, val_Loss.item(), val_acc.item() ) #显示训练集和验证集的日志 process_bar.set_description(desc + var_desc) #进度条结束 process_bar.close() #保存模型和权重 torch.save(model, './torch_mnist.pt') 运行结果如图341所示。 图341PyTorch训练手写数字识别结果 仔细观察代码会发现与TensorFlow的训练代码类似,例如训练数据处理通过torch.utils.data.DataLoader()来控制,优化器通过torch.optim.Adam()设置,损失函数的设置通过torch.nn.CrossEntropyLoss()实现。在模型训练函数model_train(data,label)中,由outputs=model(data)进行前向传播,由loss=criterion(outputs,label)进行损失函数的计算,由loss.backward()进行反向传播,由optimizer.step()进行梯度下降后的权重更新,而当训练完毕时调用torch.save(model,'./torch_mnist.pt')保存模型和权重。 3.8.11PyTorch调用Keras训练 PyTorch也支持调用Keras模块进行训练,首先调用torchkeras库中的KerasModel()类,然后使用model.fit()就能进行训练了,代码如下: #第3章/PyTorchAPI/手写数字识别Keras风格训练.py #需要安装的包 #pip install torchkeras,wandb,accelerate from torchkeras import KerasModel if __name__ == "__main__": #(1)加载数据集 batch_size = 60 learning_rate = 1e-5 epochs = 10 #手写数字识别的数据集会被自动下载到当前./data目录 #训练集 train_loader = torch.utils.data.DataLoader( torchvision.datasets.MNIST( './data/', train=True, transform=torchvision.transforms.ToTensor(), download=True ), batch_size=batch_size, shuffle=True, ) #验证集 val_loader = torch.utils.data.DataLoader( torchvision.datasets.MNIST( './data/', train=False, download=True, transform=torchvision.transforms.ToTensor(), ), batch_size=batch_size, shuffle=True) #(2) 调用集成函数,设置优化器、学习率等 model = MyModel() model = KerasModel( net=model, loss_fn=torch.nn.CrossEntropyLoss(), optimizer=torch.optim.Adam(model.parameters(), lr=1e-5) ) print(model) #使用model.fit()函数进行训练 history = model.fit( train_data=train_loader, val_data=val_loader, epochs=10, patience=3, #每隔3个epoch保存权重 ckpt_path='./torch_mnist_keras.pt', ) 运行结果如图342所示。 图342PyTorch调用Keras训练手写数字识别结果 3.8.12PyTorch调用TorchMetrics评价指标 评价指标库TorchMetrics在分类模型中支持20种指标,如图343所示。 图343TorchMetrics评价指标 使用方法,代码如下: #第3章/PyTorchAPI/手写数字识别评价指标.py from torchmetrics import MetricCollection, Accuracy, Precision, Recall, ConfusionMatrix #绘制混淆矩阵 def plt_confusion_matrix(data, class_num=10): #设置类别名称 kind = [f"数字_{x}" for x in range(class_num)] data = np.around(data.NumPy(), 2) plt.imshow(data, cmap=plt.cm.Blues) plt.title("手写数字识别混淆矩阵") #title plt.xlabel("预测值") plt.ylabel("真实值") plt.yticks(range(class_num), kind) #y轴标签 plt.xticks(range(class_num), kind, rotation=45) #x轴标签 #数值处理 for x in range(class_num): for y in range(class_num): value = data[y, x] color = 'red' if value else None plt.text(x, y, value, verticalalignment='center', horizontalalignment='center', color=color) #自动调整子图参数,使之填充整个图像区域 plt.tight_layout() plt.colorbar() plt.savefig("手写数字识别的混淆矩阵.jpg") plt.show() if __name__ == "__main__": model = MyModel() #训练过程 #(1)加载数据集 batch_size = 60 learning_rate = 1e-5 epochs = 10 #训练集 train_loader = torch.utils.data.DataLoader( torchvision.datasets.MNIST( './data/', train=True, transform=torchvision.transforms.ToTensor(), download=True ), batch_size=batch_size, shuffle=True, ) #验证集 val_loader = torch.utils.data.DataLoader( torchvision.datasets.MNIST( './data/', train=False, download=True, transform=torchvision.transforms.ToTensor(), ), batch_size=batch_size, shuffle=True) #(2)设置优化器、学习率 optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate) #(3)设置损失函数 criterion = torch.nn.CrossEntropyLoss() #定义评价指标 metrics = { 'acc': Accuracy(num_classes=10, task='multiclass'), 'prec': Precision(num_classes=10, task='multiclass', average='macro'), 'recall': Recall(num_classes=10, task='multiclass', average='macro'), 'matrix': ConfusionMatrix(num_classes=10, task='multiclass', normalize='true') } train_collection = MetricCollection(metrics) val_collection = MetricCollection(metrics) #(4)训练 def model_train(data, label): #清空模型的梯度 model.zero_grad() #前向传播 outputs = model(data) #计算损失 loss = criterion(outputs, label) train_collection.forward(outputs, label) #反向传播 loss.backward() #权重更新 optimizer.step() return loss #验证 @torch.no_grad() def model_val(val_data, val_label): #前向传播 val_outputs = model(val_data) #计算损失 loss = criterion(val_outputs, val_label) val_collection.forward(val_outputs, val_label) return loss #根据指定的epoch进行学习 for epoch in range(1, epochs + 1): #进度条 process_bar = tqdm(train_loader, unit='step') #开始训练模式 model.train(True) #因为train_loader是按batch_size划分的,所以都要进行学习 for step, (data, label) in enumerate(process_bar): #调用训练函数 loss = model_train(data, label) #在训练的最后1组数据后面进行验证 if step == len(process_bar) - 1: #根据验证集进行验证 model.eval() for _, (val_data, val_label) in enumerate(val_loader): #验证集前向传播 loss = model_val(val_data, val_label) train_metrics = train_collection.compute() val_metrics = val_collection.compute() train_desc = f"train:acc={train_metrics['acc']},precision={train_metrics['prec']},recall=={train_metrics['recall']}" val_desc = f" val:acc={val_metrics['acc']},precision={val_metrics['prec']},recall=={val_metrics['recall']}" process_bar.close() #保存模型和权重 torch.save(model, './torch_mnist.pt') #展示最后1次epoch的混淆矩阵图 plt_confusion_matrix(val_metrics['matrix']) 运行结果如图344所示。 图344增加TorchMetrics评价指标运行结果 代码中通过from torchmetrics import MetricCollection, Accuracy来引入模型评价指标,然后由train_collection=MetricCollection(metrics)来收集每次训练和验证中的指标值,而在model_train(data,label)训练函数中只需增加train_collection.forward(outputs,label)就可以完成指标的收集工作,训练完成后由train_collection.compute()完成所有数据的计算并得到相关指标的值,然后就可以根据需要对数据进行可视化操作,例如代码中混淆矩阵的可视化。 3.8.13PyTorch中推理预测 预测推理代码只需由torch.load()导入模型权重文件,然后由cv2.imread()读取图片,转换为模型后输入shape,因为模型的输入维度为[batch_size,channel,height,width],所以需要由img_tensor.unsqueeze(0)进行升维,然后由model(img_tensor)进行前向传播,再根据输出的概率通过torch.max(output,1)排序,得到概率最大值,代码如下: #第3章/PyTorchAPI/预测推理.py import torch import cv2 if __name__ == "__main__": #加载模型文件 model = torch.load("torch_mnist.pt") #读预测图片 img = cv2.imread('test.png', cv2.IMREAD_GRAYSCALE) #转换为模型输入的shape img = cv2.resize(img, [28, 28]) #转换为torch.tensor()类型 img_tensor = torch.tensor(img, dtype=torch.float32) #因为模型输入为[1,28,28,1],所以要升维 img_tensor = img_tensor.unsqueeze(0) #前向传播 output = model(img_tensor) #取预测结果最大值的下标 max_value, pre = torch.max(output, 1) #预测类型 kind = [f"数字_{x}" for x in range(10)] print(f"预测={kind[pre+1]},概率={round(max_value.item(), 2)}") 运行结果如图345所示。 图345PyTorch手写数字推理结果 总结 PyTorch的主要API也由张量操作、模型搭建、模型训练、TorchMetrics模型评估、模型推理构成,相对TensorFlow来讲API管理更加规范,调试起来较为方便。 练习 完成由PyTorch构建手写数字识别的模型,并进行训练、推理工作。