第5章 CHAPTER 5 TensorFlow进阶 人工智能将是谷歌的最终版本。它将成为终极搜索引擎,可以理解网络上的一切信息。它会准确地理解你想要什么,给你需要的东西。——拉里·佩奇 在介绍完张量的基本操作后,进一步学习张量的进阶操作,如张量的合并与分割、范数统计、张量填充、数据限幅等,并通过MNIST数据集的测试实战,来加深用户对TensorFlow张量操作的理解。 5.1合并与分割 5.1.1合并 合并是指将多个张量在某个维度上合并为一个张量。以某学校班级成绩册数据为例,设张量A保存了某学校1~4号班级的成绩册,每个班级35个学生,共8门科目成绩,则张量A的shape为[4,35,8]; 同样的方式,张量B保存了其他6个班级的成绩册,shape为[6,35,8]。通过合并这2份成绩册,便可得到学校所有班级的成绩册,记为张量C,shape应为[10,35,8],其中,10代表10个班级,35代表35个学生,8代表8门科目。这就是张量合并的意义所在。 张量的合并可以使用拼接(Concatenate)和堆叠(Stack)操作实现,拼接操作并不会产生新的维度,仅在现有的维度上合并,而堆叠会创建新维度。选择使用拼接还是堆叠操作来合并张量,取决于具体的场景是否需要创建新维度。 (1) 拼接: 在TensorFlow中,可以通过tf.concat(tensors,axis)函数拼接张量,其中参数tensors保存了所有需要合并的张量List,axis参数指定需要合并的维度索引。回到上面的例子,在班级维度上合并成绩册,这里班级维度索引号为0,即axis=0,合并张量A和B的代码如下: In [1]: a = tf.random.normal([4,35,8]) # 模拟成绩册A b = tf.random.normal([6,35,8]) # 模拟成绩册B tf.concat([a,b],axis=0) # 拼接合并成绩册 Out[1]: In [14]: tf.norm(x,ord=2) # 计算L2范数 Out[14]: In [15]: import numpy as np tf.norm(x,ord=np.inf) # 计算∞范数 Out[15]: 5.2.2最值、均值、和 通过tf.reduce_max、tf.reduce_min、tf.reduce_mean、tf.reduce_sum函数可以求解张量在某个维度上的最大、最小、均值、和,也可以求全局最大、最小、均值和信息。 考虑shape为4,10的张量,其中,第一个维度代表样本数量,第二个维度代表当前样本分别属于10个类别的概率,需要求出每个样本的概率最大值为,可以通过tf.reduce_max函数实现: In [16]: x = tf.random.normal([4,10]) # 模型生成概率 tf.reduce_max(x,axis=1) # 统计概率维度上的最大值 Out[16]: 返回长度为4的向量,分别代表每个样本的最大概率值。同样求出每个样本概率的最小值,实现如下: In [17]: tf.reduce_min(x,axis=1) # 统计概率维度上的最小值 Out[17]: 求出每个样本概率的均值,实现如下: In [18]: tf.reduce_mean(x,axis=1) # 统计概率维度上的均值 Out[18]: 当不指定axis参数时,tf.reduce_*函数会求解出全局元素的最大、最小、均值、和等数据,例如: In [19]:x = tf.random.normal([4,10]) # 统计全局的最大、最小、均值、和,返回的张量均为标量 tf.reduce_max(x),tf.reduce_min(x),tf.reduce_mean(x) Out [19]: (, , ) 在求解误差函数时,通过TensorFlow的MSE误差函数可以求得每个样本的误差,需要计算样本的平均误差,此时可以通过tf.reduce_mean在样本数维度上计算均值,实现如下: In [20]: out = tf.random.normal([4,10]) # 模拟网络预测输出 y = tf.constant([1,2,2,0]) # 模拟真实标签 y = tf.one_hot(y,depth=10) # One-hot编码 loss = keras.losses.mse(y,out) # 计算每个样本的误差 loss = tf.reduce_mean(loss) # 平均误差,在样本数维度上取均值 loss # 误差标量 Out[20]: 与均值函数相似的是求和函数tf.reduce_sum(x, axis),它可以求解张量在axis轴上所有特征的和: In [21]:out = tf.random.normal([4,10]) tf.reduce_sum(out,axis=-1) # 求最后一个维度的和 Out[21]: 除了希望获取张量的最值信息,还希望获得最值所在的位置索引号,例如分类任务的标签预测,就需要知道概率最大值所在的位置索引号,一般把这个位置索引号作为预测类别。考虑10分类问题,得到神经网络的输出张量out,shape为2,10,代表了2个样本属于10个类别的概率,由于元素的位置索引代表了当前样本属于此类别的概率,预测时往往会选择概率值最大的元素所在的索引号作为样本类别的预测值,例如: In [22]:out = tf.random.normal([2,10]) out = tf.nn.softmax(out,axis=1) # 通过softmax函数转换为概率值 out Out[22]: 以第一个样本为例,可以看到,它概率最大的索引为i=0,最大概率值为0.1877。由于每个索引号上的概率值代表了样本属于此索引号的类别的概率,因此第一个样本属于0类的概率最大,在预测时考虑第一个样本应该最有可能属于类别0。这就是需要求解最大值的索引号的一个典型应用。 通过tf.argmax(x, axis)和tf.argmin(x, axis)可以求解在axis轴上,x的最大值、最小值所在的索引号,例如: In [23]:pred = tf.argmax(out,axis=1) # 选取概率最大的位置 pred Out[23]: 可以看到,这2个样本概率最大值都出现在索引0上,因此最有可能都是类别0,可以将类别0作为这2个样本的预测类别。 5.3张量比较 为了计算分类任务的准确率等指标,一般需要将预测结果和真实标签比较,统计比较结果中正确的数量来计算准确率。考虑100个样本的预测结果,通过tf.argmax获取预测类别,实现如下: In [24]:out = tf.random.normal([100,10]) out = tf.nn.softmax(out,axis=1) # 输出转换为概率 pred = tf.argmax(out,axis=1) # 计算预测值 Out[24]: 可以看到,随机产生的预测数据中预测正确的个数是12,因此它的准确度是 accuracy=12100=12% 这也是随机预测模型的正常水平。 除了比较相等的tf.equal(a, b)函数,其他的比较函数用法类似,如表5.1所示。 表5.1常用比较函数总结 函数比 较 逻 辑函数比 较 逻 辑 tf.math.greatera>btf.math.less_equala≤b tf.math.lessa 填充后句子张量形状一致,再将这两个句子stack在一起,代码如下: In [29]:tf.stack([a,b],axis=0) # 堆叠合并,创建句子数维度 Out[29]: 在自然语言处理中,需要加载不同句子长度的数据集,有些句子长度较小,如仅10个单词,部分句子长度较长,如超过100个单词。为了能够保存在同一张量中,一般会选取能够覆盖大部分句子长度的阈值,如80个单词。对小于80个单词的句子,在末尾填充相应数量的0; 对大于80个单词的句子,截断超过规定长度的部分单词。以IMDB数据集的加载为例,来演示如何将不等长的句子变换为等长结构,代码如下: In [30]:total_words = 10000 # 设定词汇量大小 max_review_len = 80 # 最大句子长度 embedding_len = 100 # 词向量长度 # 加载IMDB数据集 (x_train,y_train),(x_test,y_test) = keras.datasets.imdb.load_data(num_words=total_words) # 将句子填充或截断到相同长度,设置为末尾填充和末尾截断方式 x_train = keras.preprocessing.sequence.pad_sequences(x_train,maxlen=max_review_len,truncating ='post',padding='post') x_test = keras.preprocessing.sequence.pad_sequences(x_test,maxlen=max_review_len,truncating ='post',padding='post') print(x_train.shape,x_test.shape) # 打印等长的句子张量形状 Out[30]: (25000,80) (25000,80) 上述代码中,将句子的最大长度max_review_len设置为80个单词,通过keras.preprocessing.sequence.pad_sequences函数可以快速完成句子的填充和截断工作,以其中某个句子为例,观察其变换后的向量内容: [17781287412630163154176679821051232 8515645401481391216646651010 13611734 749216 38048422665124312724210 100000000000000 00000000000000 0000000000] 可以看到在句子末尾填充了若干数量的0,使得句子的长度刚好为80。实际上,也可以选择当句子长度不够时,在句子前面填充0; 句子长度过长时,截断句首的单词。经过处理后,所有的句子长度都变为80,从而训练集可以统一保存在shape为25000,80的张量中,测试集可以保存在shape为25000,80的张量中。 下面介绍同时在多个维度进行填充的例子。考虑对图片的高宽维度进行填充,以28×28大小的图片数据为例,如果网络层所接受的数据高宽为32×32,则必须将28×28大小填充到32×32,可以选择在图片矩阵的上、下、左、右方向各填充2个单元,如图5.2所示。 图5.2图片填充 上述填充方案可以表达为[[0,0],[2,2],[2,2],[0,0]],实现如下: In [31]: x = tf.random.normal([4,28,28,1]) # 图片上下、左右各填充2个单元 tf.pad(x,[[0,0],[2,2],[2,2],[0,0]]) Out[31]: In [34]:tf.minimum(x,7) # 上限幅到7 Out[34]: 基于tf.maximum函数,可以实现ReLU函数如下: def relu(x): # ReLU函数 return tf.maximum(x,0.) # 下限幅为0即可 通过组合tf.maximum(x, a)和tf.minimum(x, b)可以实现同时对数据的上下边界限幅,即x∈[a,b],例如: In [35]:x = tf.range(9) tf.minimum(tf.maximum(x,2),7) # 限幅为2~7 Out[35]: 更方便地,可以使用tf.clip_by_value函数实现上下限幅: In [36]:x = tf.range(9) tf.clip_by_value(x,2,7) # 限幅为2~7 Out[36]: 5.6高级操作 上述介绍的操作函数大部分都是常用并且容易理解的,接下来将介绍部分常用,但是稍复杂的功能函数。 5.6.1tf.gather tf.gather可以实现根据索引号收集数据的目的。考虑班级成绩册的例子,假设共有4个班级,每个班级35个学生,8门科目,保存成绩册的张量shape为4,35,8。 x = tf.random.uniform([4,35,8],maxval=100,dtype=tf.int32)# 成绩册张量 现在需要收集第1~2个班级的成绩册,可以给定需要收集班级的索引号0,1,并指定班级的维度axis=0,通过tf.gather函数收集数据,代码如下: In [38]:tf.gather(x,[0,1],axis=0) # 在班级维度收集第1~2号班级成绩册 Out[38]: In [42]:tf.gather(a,[3,1,0,2],axis=0) # 收集第4,2,1,3号元素 Out[42]: 将问题变得稍微复杂一点。如果希望抽查第2,3班级的第3,4,6,27号同学的科目成绩,则可以通过组合多个tf.gather实现。首先抽出第2,3班级,实现如下: In [43]: students=tf.gather(x,[1,2],axis=0) # 收集第2,3号班级 Out[43]: 再串行提取第二个采样点的数据x[2,2],以及第三个采样点的数据x[3,3],最后通过stack方式合并采样结果,实现如下: In [46]: tf.stack([x[1,1],x[2,2],x[3,3]],axis=0) Out[46]: 这种方法也能正确地得到shape为3,8的结果,其中3表示采样点的个数,8表示每个采样点的特征数据的长度。但是它最大的问题在于用手动串行方式地执行采样,计算效率极低。 可以采用tf.gather_nd来实现。 5.6.2tf.gather_nd 通过tf.gather_nd函数,可以指定每次采样点的多维坐标来实现采样多个点的目的。回到上面的挑战,希望抽查第2个班级的第2个同学的所有科目,第3个班级的第3个同学的所有科目,第4个班级的第4个同学的所有科目。那么这3个采样点的索引坐标可以记为1,1、2,2、3,3,将这个采样方案合并为一个List参数,即[[1,1],[2,2],[3,3]],通过tf.gather_nd函数即可实现如下: In [47]: # 根据多维坐标收集数据 tf.gather_nd(x,[[1,1],[2,2],[3,3]]) Out[47]: 可以看到,结果与串行采样方式完全一致,实现更加简洁,计算效率大大提升。 一般地,在使用tf.gather_nd采样多个样本时,例如希望采样i号班级,j个学生,k门科目的成绩,则可以表达为[…,[i,j,k],…],外层的括号长度为采样样本的个数,内层列表包含了每个采样点的索引坐标,例如: In [48]: # 根据多维度坐标收集数据 tf.gather_nd(x,[[1,1,2],[2,2,3],[3,3,4]]) Out[48]: 上述代码中,抽出了班级1的学生1的科目2、班级2的学生2的科目3、班级3的学生3的科目4的成绩,共有3个成绩数据,结果汇总为一个shape为3的张量。 5.6.3tf.boolean_mask 除了可以通过给定索引号的方式采样,还可以通过给定掩码(Mask)的方式进行采样。继续以shape为4,35,8的成绩册张量为例,这次以掩码方式进行数据提取。 考虑在班级维度上进行采样,对这4个班级的采样方案的掩码为: mask=[True,False,False,True] 即采样第1和第4个班级的数据,通过tf.boolean_mask(x, mask, axis)可以在axis轴上根据mask方案进行采样,实现为: In [49]: # 根据掩码方式采样班级,给出掩码和维度索引 tf.boolean_mask(x,mask=[True,False,False,True],axis=0) Out[49]: 共采样4个学生的成绩,shape为4,8。 如果用掩码方式,可以表达为如表5.2所示,行为每个班级,列为每个学生,表中数据表达了对应位置的采样情况。 表5.2成绩册掩码采样方案 学生0 学生1 学生2 班级0 True True False 班级1 False True True 因此,通过这张表,就能很好地表征利用掩码方式的采样方案,代码实现如下: In [52]: # 多维掩码采样 tf.boolean_mask(x,[[True,True,False],[False,True,True]]) Out[52]: 采样结果与tf.gather_nd完全一致。可见tf.boolean_mask既可以实现tf.gather方式的一维掩码采样,又可以实现tf.gather_nd方式的多维掩码采样。 上面的3个操作比较常用,尤其是tf.gather和tf.gather_nd出现的频率较高,必须掌握。下面再补充3个高阶操作。 5.6.4tf.where 通过tf.where(cond, a, b)操作可以根据cond条件的真假从参数A或B中读取数据,条件判定规则如下: oi=aicondi为True bicondi为False 其中i为张量的元素索引,返回的张量大小与A和B一致,当对应位置的condi为True,oi从ai中复制数据; 当对应位置的condi为False,oi从bi中复制数据。考虑从2个全1和全0的3×3大小的张量A和B中提取数据,其中condi为True的位置从A中对应位置提取元素1,condi为False的位置从B中对应位置提取元素0,代码如下: In [53]: a = tf.ones([3,3]) # 构造a为全1矩阵 b = tf.zeros([3,3]) # 构造b为全0矩阵 # 构造采样条件 cond = tf.constant([[True,False,False],[False,True,False],[True,True,False]]) tf.where(cond,a,b) # 根据条件从a,b中采样 Out[53]: 可以看到,返回的张量中为1的位置全部来自张量a,返回的张量中为0的位置全部来自张量b。 当参数a=b=None时,即a和b参数不指定,tf.where会返回cond张量中所有True的元素的索引坐标。考虑如下cond张量: In [54]: cond # 构造的cond张量 Out[54]: 其中True共出现4次,每个True元素位置处的索引分别为0,0、1,1、2,0、2,1,可以直接通过tf.where(cond)形式来获得这些元素的索引坐标,代码如下: In [55]:tf.where(cond) # 获取cond中为True的元素索引 Out[55]: 那么这有什么用途?考虑一个场景,需要提取张量中所有正数的数据和索引。首先构造张量a,并通过比较运算得到所有正数的位置掩码: In [56]:x = tf.random.normal([3,3]) # 构造a Out[56]: 通过比较运算,得到所有正数的掩码: In [57]:mask=x>0 # 比较操作,等同于tf.math.greater() mask Out[57]: 通过tf.where提取此掩码处True元素的索引坐标: In [58]:indices=tf.where(mask) # 提取所有大于0的元素索引 Out[58]: 拿到索引后,通过tf.gather_nd即可恢复出所有正数的元素: In [59]:tf.gather_nd(x,indices) # 提取正数的元素值 Out[59]: 实际上,当得到掩码mask之后,也可以直接通过tf.boolean_mask获取所有正数的元素向量: In [60]:tf.boolean_mask(x,mask) # 通过掩码提取正数的元素值 Out[60]: 结果也是一致的。 通过上述一系列的比较、索引号收集和掩码收集的操作组合,能够比较直观地感受到这个功能是有很大实际应用的,并且深刻地理解它们的本质有利于更加灵活地选用简便高效的方式实现我们的目的。 5.6.5scatter_nd 通过tf.scatter_nd(indices, updates, shape)函数可以高效地刷新张量的部分数据,但是这个函数只能在全0的白板张量上面执行刷新操作,因此可能需要结合其他操作来实现现有张量的数据刷新功能。 如图5.3所示,演示了一维张量白板的刷新运算原理。白板的形状通过shape参数表示,需要刷新的数据索引号通过indices表示,新数据为updates。根据indices给出的索引位置将updates中新的数据依次写入白板中,并返回更新后的结果张量。 图5.3scatter_nd更新数据 实现一个图5.3中向量的刷新实例,代码如下: In [61]: # 构造需要刷新数据的位置参数,即为4、3、1和7号位置 indices = tf.constant([[4],[3],[1],[7]]) # 构造需要写入的数据,4号位写入4.4,3号位写入3.3,以此类推 updates = tf.constant([4.4,3.3,1.1,7.7]) # 在长度为8的全0向量上根据indices写入updates数据 tf.scatter_nd(indices,updates,[8]) Out[61]: 可以看到,在长度为8的白板上,写入了对应位置的数据,4个位置的数据被刷新。 考虑三维张量的刷新例子,如图5.4所示,白板张量的shape为[4,4,4],共有4个通道的特征图,每个通道大小为4×4,现有2个通道的新数据updates为[2,4,4],需要写入索引为1,3的通道上。 图5.4三维张量更新 将新的特征图写入现有白板张量,实现如下: In [62]: # 构造写入位置,即2个位置 indices = tf.constant([[1],[3]]) updates = tf.constant([# 构造写入数据,即2个矩阵 [[5,5,5,5],[6,6,6,6],[7,7,7,7],[8,8,8,8]], [[1,1,1,1],[2,2,2,2],[3,3,3,3],[4,4,4,4]] ]) # 在shape为[4,4,4]白板上根据indices写入updates tf.scatter_nd(indices,updates,[4,4,4]) Out[62]: 可以看到,数据被刷新到第2和第4个通道特征图上。 5.6.6meshgrid 通过tf.meshgrid函数可以方便地生成二维网格的采样点坐标,方便可视化等应用场合。考虑2个自变量x和y的sinc函数表达式为: z=sincx2+y2x2+y2 如果需要绘制在x∈[-8,8],y∈[-8,8]区间的sinc函数的3D曲面,如图5.5所示,则首先需要生成x和y轴的网格点坐标集合{(x,y)},这样才能通过sinc函数的表达式计算函数在每个(x,y)位置的输出值z。可以通过如下方式生成10000个坐标采样点: points = [] # 保存所有点的坐标列表 for x in range(-8,8,100): # 循环生成x坐标,100个采样点 for y in range(-8,8,100): # 循环生成y坐标,100个采样点 z = sinc(x,y) # 计算每个点(x,y)处的sinc函数值 points.append([x,y,z]) # 保存采样点 很明显这种串行采样方式效率极低,那么有没有通过tf.meshgrid函数即可以简洁、高效的方式生成网格坐标。 图5.5 sinc函数 通过在x轴上进行采样100个数据点,y轴上采样100个数据点,然后利用tf.meshgrid(x, y)即可返回这10000个数据点的张量数据,保存在shape为100,100,2的张量中。为了方便计算,tf.meshgrid会返回在axis=2维度切割后的2个张量A和B,其中张量A包含了所有点的x坐标,B包含了所有点的y坐标,shape都为100,100,实现如下: In [63]: x = tf.linspace(-8.,8,100) # 设置x轴的采样点 y = tf.linspace(-8.,8,100) # 设置y轴的采样点 x,y = tf.meshgrid(x,y) # 生成网格点,并内部拆分后返回 x.shape,y.shape # 打印拆分后的所有点的x,y坐标张量shape Out[63]: (TensorShape([100,100]),TensorShape([100,100])) 利用生成的网格点坐标张量A和B,sinc函数在TensorFlow中实现如下: z = tf.sqrt(x**2+y**2) z = tf.sin(z)/z # sinc函数实现 通过matplotlib库即可绘制出函数在x∈[-8,8],y∈[-8,8]区间的三维(3D)曲面,如图5.5所示。代码如下: import matplotlib from matplotlib import pyplot as plt # 导入三维坐标轴支持 from mpl_toolkits.mplot3d import Axes3D fig = plt.figure() ax = Axes3D(fig) # 设置三维坐标轴 # 根据网格点绘制sinc函数三维曲面 ax.contour3D(x.numpy(),y.numpy(),z.numpy(),50) plt.show() 5.7经典数据集加载 到目前为止,已经学完张量的常用操作方法,已具备实现大部分深度网络的技术储备。最后将以一个完整的张量方式实现的分类网络模型实战收尾本章。在进入实战之前,先正式介绍对于常用的经典数据集,如何利用TensorFlow提供的工具便捷地加载数据集。对于自定义的数据集的加载,会在后续章节中介绍。 在TensorFlow中,keras.datasets模块提供了常用经典数据集的自动下载、管理、加载与转换功能,并且提供了tf.data.Dataset数据集对象,方便实现多线程(Multithreading)、预处理(Preprocessing)、随机打散(Shuffle)和批训练(Training on Batch)等常用数据集的功能。 对于常用的经典数据集,介绍如下: Boston Housing: 波士顿房价趋势数据集,用于回归模型训练与测试。 CIFAR10/100: 真实图片数据集,用于图片分类任务。 MNIST/Fashion_MNIST: 手写数字图片数据集,用于图片分类任务。 IMDB: 情感分类任务数据集,用于文本分类任务。 这些数据集在机器学习或深度学习的研究和学习中使用非常频繁。对于新提出的算法,一般优先在经典的数据集上面测试,再尝试迁移到更大规模、更复杂的数据集上。 通过datasets.xxx.load_data()函数即可实现经典数据集的自动加载,其中xxx代表具体的数据集名称,如CIFAR10、MNIST。TensorFlow会默认将数据缓存在用户目录下的.keras/datasets文件夹中,如图5.6所示,用户不需要关心数据集是如何保存的。如果当前数据集不在缓存中,则会自动从网络下载、解压和加载数据集; 如果已经在缓存中,则自动完成加载。例如,自动加载MNIST数据集: In [66]: import tensorflow as tf from tensorflow import keras from tensorflow.keras import datasets # 导入经典数据集加载模块 # 加载MNIST数据集 (x,y),(x_test,y_test) = datasets.mnist.load_data() print('x:',x.shape,'y:',y.shape,'x test:',x_test.shape,'y test:',y_test) Out [66]: # 返回数组的形状 x: (60000,28,28) y: (60000,) x test: (10000,28,28) y test: [7 2 1 ...4 5 6] 通过load_data()函数会返回相应格式的数据,对于图片数据集MNIST、CIFAR10等,会返回2个tuple,第1个tuple保存了用于训练的数据x和y训练集对象; 第2个tuple则保存了用于测试的数据x_test和y_test测试集对象,所有的数据都用Numpy数组容器保存。 图5.6TensorFlow缓存经典数据集的位置 数据加载进入内存后,需要转换成Dataset对象,才能利用TensorFlow提供的各种便捷功能。通过Dataset.from_tensor_slices可以将训练部分的数据图片x和标签y都转换成Dataset对象: train_db = tf.data.Dataset.from_tensor_slices((x,y)) # 构建Dataset对象 将数据转换成Dataset对象后,一般需要再添加一系列的数据集标准处理步骤,如随机打散、预处理、批训练等。 5.7.1随机打散 通过Dataset.shuffle(buffer_size)工具可以设置Dataset对象随机打散数据之间的顺序,防止每次训练时数据按固定顺序产生,从而使得模型尝试“记忆”住标签信息,代码实现如下: train_db = train_db.shuffle(10000) # 随机打散样本,不会打乱样本与标签映射关系 其中,buffer_size参数指定缓冲池的大小,一般设置为一个较大的常数即可。调用Dataset提供的这些工具函数会返回新的Dataset对象,可以通过 db=db.step1().step2().step3.() 方式按序完成所有的数据处理步骤,实现起来非常方便。 5.7.2批训练 为了利用显卡的并行计算能力,一般在网络的计算过程中会同时计算多个样本,把这种训练方式称为批训练,其中一个批中样本的数量称为Batch Size。为了一次能够从Dataset中产生Batch Size数量的样本,需要设置Dataset为批训练方式,实现如下: train_db = train_db.batch(128) # 设置批训练,batch size为128 其中128为Batch Size参数,即一次并行计算128个样本的数据。Batch Size一般根据用户的GPU显存资源来设置,当显存不足时,可以适量减少Batch Size来减少算法的显存使用量。 5.7.3预处理 从keras.datasets中加载的数据集的格式大部分情况都不能直接满足模型的输入要求,因此需要根据用户的逻辑自行实现预处理步骤。Dataset对象通过提供map(func)工具函数,可以非常方便地调用用户自定义的预处理逻辑,它实现在func函数中。例如,下面代码调用名为preprocess的函数完成每个样本的预处理。 # 预处理函数实现在preprocess函数中,传入函数名即可 train_db = train_db.map(preprocess) 考虑MNIST手写数字图片,从keras.datasets中经batch()后加载的图片x shape为[b,28,28],像素使用0~255的整型表示; 标签shape为b,即采样数字编码方式。实际的神经网络输入,一般需要将图片数据标准化到0,1或[-1,1]等0附近区间,同时根据网络的设置,需要将shape为28,28的输入视图调整为合法的格式; 对于标签信息,可以选择在预处理时进行Onehot编码,也可以在计算误差时进行Onehot编码。 根据5.8节的实战设定,将MNIST图片数据映射到x∈[0,1]区间,视图调整为[b,28×28]; 对于标签数据,选择在预处理函数中进行Onehot编码。preprocess函数实现如下: def preprocess(x,y): # 自定义的预处理函数 # 调用此函数时会自动传入x,y对象,shape为[b,28×28],[b] # 标准化到0~1 x = tf.cast(x,dtype=tf.float32) / 255. x = tf.reshape(x,[-1,28*28]) # 打平 y = tf.cast(y,dtype=tf.int32) # 转成整型张量 y = tf.one_hot(y,depth=10) # One-hot编码 # 返回的x,y将替换传入的x,y参数,从而实现数据的预处理功能 return x,y 5.7.4循环训练 对于Dataset对象,在使用时可以通过 for step,(x,y) in enumerate(train_db): # 迭代数据集对象,带step参数 或 for x,y in train_db: # 迭代数据集对象 方式进行迭代,每次返回的x和y对象即为批量样本和标签。当对train_db的所有样本完成一次迭代后,for循环终止退出。这样完成一个Batch的数据训练,称为一个step; 通过多个step来完成整个训练集的一次迭代,称为一个Epoch。在实际训练时,通常需要对数据集迭代多个Epoch才能取得较好地训练效果。例如,固定训练20个Epoch,实现如下: for epoch in range(20): # 训练Epoch数 for step,(x,y) in enumerate(train_db): # 迭代step数 # training... 此外,也可以通过设置Dataset对象,使得数据集对象内部遍历多次才会退出,实现如下: train_db = train_db.repeat(20) # 数据集迭代20遍才终止 上述代码使得for x,y in train_db循环迭代20个Epoch才会退出。不管使用上述哪种方式,都能取得一样的效果。由于第4章已经完成了前向计算实战,此处略过。 5.8MNIST测试实战 前面已经介绍并实现了前向传播和数据集的加载部分,现在来完成剩下的分类任务逻辑。在训练的过程中,通过间隔数个Step后打印误差数据,可以有效监督模型的训练进度,代码如下: # 间隔100个step打印一次训练误差 if step % 100 == 0: print(step,'loss:',float(loss)) 由于loss为TensorFlow的张量类型,因此可以通过float()函数将标量转换为标准的Python浮点数。在若干个Step或者若干个Epoch训练后,可以进行一次测试(验证),以获得模型的当前性能,例如: if step % 500 == 0: # 每500个batch后进行一次测试(验证) # evaluate/test 现在利用学习到的TensorFlow张量操作函数,完成准确度的计算实战。首先考虑一个Batch的样本x,通过前向计算可以获得网络的预测值,代码如下: for x,y in test_db: # 对测验集迭代一遍 h1 = x @ w1 + b1 # 第一层 h1 = tf.nn.relu(h1) # 激活函数 h2 = h1 @ w2 + b2 # 第二层 h2 = tf.nn.relu(h2) # 激活函数 out = h2 @ w3 + b3 # 输出层 预测值out的shape为[b,10],分别代表了样本属于每个类别的概率,根据tf.argmax函数选出概率最大值出现的索引号,即样本最有可能的类别号: pred = tf.argmax(out,axis=1) # 选取概率最大的类别 由于标注y已经在预处理中完成了Onehot编码,这在测试时其实是不需要的,因此通过tf.argmax可以得到数字编码的标注y: y = tf.argmax(y,axis=1) # One-hot编码逆过程 通过tf.equal可以比较这两者的结果是否相等: correct = tf.equal(pred,y) # 比较预测值与真实值 并求和比较结果中所有True(转换为1)的数量,即为预测正确的数量: total_correct += tf.reduce_sum(tf.cast(correct,dtype =tf.int32)).numpy() # 统计预测正确的样本个数 预测正确的数量除以总测试数量即可得到准确率,并打印出来,实现如下: # 计算准确率 print(step,'Evaluate Acc:',total_correct/total) 通过简单的3层神经网络,训练固定的20个Epoch后,在测试集上获得了87.25%的准确率。如果使用复杂的神经网络模型,增加数据增强环节,精调网络超参数等技巧,可以获得更高的模型性能。模型的训练误差曲线如图5.7所示,测试准确率曲线如图5.8所示。 图5.7MNIST训练误差曲线 图5.8MNIST测试准确率曲线