iris数据集是常用的分类实验数据集,由Fisher于1936年收集整理。iris也称鸢尾花卉数据集,是一类多重变量分析的数据集,包含150个数据集,可分为3类,每类50个数据,每个数据包含4个属性。可通过花萼长度、花萼宽度、花瓣长度、花瓣宽度4个属性预测鸢尾花卉属于Setosa、Versicolour、Virginica这3个种类中的哪一类。鸢尾花的样子如图3.2所示。
图3.2 鸢尾花
不需要下载鸢尾花数据集,一般常用的机器学习工具都自带iris数据集,导入iris数据集的代码如下:
from sklearn.datasets import load_iris data = load_iris()
这里导入的是sklearn数据库中的iris数据集,直接载入即可。其中的数据是以key-value的形式对应存放的,key值如下:
由于本例中需要iris的特征与分类目标,因此只需要获取data和target,代码如下:
from sklearn.datasets import load_iris data = load_iris() iris_target = data.target iris_data = np.float32(data.data) #将其转化为float类型的列表(list)
数据打印输出的结果如图3.3所示。
图3.3 数据打印输出的结果
这里分别打印了前5组数据。可以看到iris数据集分成了4个不同特征进行数据记录的,而每个特征又对应于一个分类表示。
下面就是数据处理部分,对特征的表示不需要变动。而对于分类表示的结果,全部打印输出的结果如图3.4所示。
图3.4 数据处理
这里按数字分成了3类,0、1和2分别代表3种类型。按照直接计算的思路可以将数据结果向固定的数字进行拟合。这样做就是一个回归问题,即通过回归曲线去拟合出最终结果。但是本例实际上是一个分类任务,因此需要对其进行分类处理。
分类处理中一个非常简单的方法就是进行独热编码(one-hot)处理,也就是将一个序列化数据分到不同的数据空间进行表示,如图3.5所示。
图3.5 one-hot处理
具体在程序处理上,读者可以自行实现one-hot的代码,也可以使用Keras自带的分散化工具对数据进行处理。代码如下:
iris_target = np.float32(tf.keras.utils.to_categorical(iris_target, num_classes=3))
这里的num_classes分成了3类,由一行三列来表示每个类别。
交叉熵函数与分散化表示的方法超出了本书的内容,因而不做过多介绍。读者只需要知道交叉熵函数需要和softmax配合,从分布上向离散空间靠拢即可。
iris_data = tf.data.Dataset.from_tensor_slices(iris_data).batch(50) iris_target = tf.data.Dataset.from_tensor_slices(iris_target).batch(50)
当生成的数据读取到内存中,并准备以批量形式打印时使用的是tf.data.Dataset.from_tensor_slices函数,可以根据具体情况对批量数据进行设置。tf.data.Dataset函数更多的细节和用法在后面的章节中会专门介绍。
梯度更新函数是根据误差的幅度对数据进行更新的方法,代码如下:
grads = tape.gradient(loss_value, model.trainable_variables) opt.apply_gradients(zip(grads, model.trainable_variables))
与前面线性回归例子的差别是,使用的模型直接获取参数的方式对数据进行不断更新而非人为指定。至于人为指定和排除某些参数的方法属于高级程序设计,在后面的章节会提到。
【程序3-1】
该程序的运行结果如图3.6所示。可以看到损失值在符合要求的范围内不断降低,最终达到预期目标。
图3.6 程序3-1的运行结果
对于有编程经验的程序设计人员来说,顺序编程过于抽象,同时缺乏过多的自由度,因此在较为高级的程序设计中达不到程序设计的目标。
Keras函数式编程是定义复杂模型(如多输出模型、有向无环图,或具有共享层的模型)的方法。下面从一个简单的例子开始。程序3-1建立模型的方法是使用顺序编程,即通过逐级添加的方式将数据“add”到模型中。这种方式在较低级的编程上可以较好地减轻编程的难度,但是在自由度方面会有非常大的影响。例如,当需要对输入的数据进行重新计算时,顺序编程方法就不适用了。
函数式编程方法类似于传统的编程。只需要建立模型导入输入和输出“形式参数”即可。有TensorFlow 1.x编程基础的读者可以将其看作是一种新的格式的“占位符”。示例代码如下:
inputs = tf.keras.layers.Input(shape=(4)) # 层的实例是可调用的,它以张量为参数,并且返回一个张量 x = tf.keras.layers.Dense(32, activation='relu')(inputs) x = tf.keras.layers.Dense(64, activation='relu')(x) predictions = tf.keras.layers.Dense(3, activation='softmax')(x) # 这部分创建了一个包含输入层和三个全连接层的模型 model = tf.keras.Model(inputs=inputs, outputs=predictions)
下面开始对其进行分析。
首先是input的形参:
inputs = tf.keras.layers.Input(shape=(4))
需要从源码上来看,代码如下:
tf.keras.Input( shape=None, batch_size=None, name=None, dtype=None, sparse=False, tensor=None, **kwargs )
Input函数用于实例化Keras张量。Keras张量是来自底层后端输入的张量对象,其中增加了某些属性,能够通过了解模型的输入和输出来构建Keras模型。
参数解析:
上面是官方的解释,可以看到,这里的input函数就是根据设定的维度大小生成一个可供存放对象的张量空间,维度就是shape中设定的维度。
与传统的TensorFlow不同的是,这里的batch大小并不显式地定义在输入shape中。
举例来说,在一个后续的学习中会遇到MNIST数据集,即一个手写图片分类的数据集,每张图片的大小用四维来表示,比如[1,28,28,1]。第1个数字是每个批次的大小,第2和3个数字是图片的尺寸,第4个数字是图片通道的个数。因此输入到input中的数据为:
#举例说明,这里四维变成三维,不设置batch的值 inputs = tf.keras.layers.Input(shape=(28,28,1))
下面每个层的写法与使用顺序模式也是不同的:
x = tf.keras.layers.Dense(32, activation='relu')(inputs)
在这里每个类被直接定义,之后将值作为类实例化以后的输入值进行计算。
x = tf.keras.layers.Dense(32, activation='relu')(inputs) x = tf.keras.layers.Dense(64, activation='relu')(x) predictions = tf.keras.layers.Dense(3, activation='softmax')(x)
可以看到这里与顺序最大的区别就在于实例化类以后有对应的输入端,较为符合一般程序编写习惯。
不需要额外的表示,直接将计算的最后一个层作为输出端即可:
predictions = tf.keras.layers.Dense(3, activation='softmax')(x)
直接将输入端和输出端在模型类中显式地注明,Keras即可在后台将各个层级通过输入和输出对应的关系连接在一起。
model = tf.keras.Model(inputs=inputs, outputs=predictions)
完整的代码如下所示。
【程序3-2】
程序3-2的基本架构仿照前面没有多少变化,损失函数和梯度更新方法是固定的写法,最大的不同点在于,使用模型自带的saver函数保存数据。在TensorFlow 2.0中,数据的保存由Keras完成,即将图(Graph)和对应的参数完整地保存在h5格式中。
前面已经说过,对于保存的文件,Keras是将所有的信息都保存在h5文件中,这里包含所有模型结构信息和训练过的参数信息。
new_model = tf.keras.models.load_model('./saver/the_save_model.h5')
tf.keras.models.load_model函数从给定的地址中载入h5文件中保存的模型,而后根据保存的模型自动建立一个新的模型。
模型的复用直接调用模型predict函数:
new_prediction = new_model.predict(iris_data)
这里直接将iris数据作为预测数据进行输入,全部代码如下所示。
【程序3-3】
该程序的运行结果如图3.7所示,最终的计算结果被完整地打印出来。
图3.7 程序3-3的运行结果
在程序3-1中,笔者使用符合传统TensorFlow习惯的梯度更新方式对参数进行了更新。然而这种看起来符合编程习惯的梯度计算和更新方法可能并不符合大多数有机器学习使用经验的读者使用。下面就以修改后的iris分类为例来讲解标准化TensorFlow 2.0的编译方法。
实际上大多数机器学习的程序设计人员往往习惯使用fit函数和compile函数进行数据载入和参数分析。代码如下(先运行,后面会有更为细节的运行分析)。
【程序3-4】
import tensorflow as tf import numpy as np from sklearn.datasets import load_iris data = load_iris() iris_data = np.float32(data.data) iris_target = (data.target) iris_target = np.float32(tf.keras.utils.to_categorical(iris_target,num_classes=3)) train_data = tf.data.Dataset.from_tensor_slices((iris_data,iris_target)).batch(128) input_xs = tf.keras.Input(shape=(4), name='input_xs') out = tf.keras.layers.Dense(32, activation='relu', name='dense_1')(input_xs) out = tf.keras.layers.Dense(64, activation='relu', name='dense_2')(out) logits = tf.keras.layers.Dense(3, activation="softmax",name='predictions')(out) model = tf.keras.Model(inputs=input_xs, outputs=logits) opt = tf.optimizers.Adam(1e-3) model.compile(optimizer=tf.optimizers.Adam(1e-3), loss=tf.losses.categorical_crossentropy, metrics = ['accuracy']) model.fit(train_data, epochs=500) score = model.evaluate(iris_data, iris_target) print("last score:",score)
本例还是使用sklearn中的iris数据集作为数据来源,之后将target转化成one-hot的形式进行存储。顺便提一句,TensorFlow本身也带有one-hot函数,即tf.one_hot,有兴趣的读者可以自行学习。
读取之后的处理在后文介绍,现在请读者继续按顺序阅读。
这里笔者不准备采用新模型的建立方法,对于读者来说,熟悉函数化编程就能应对大多数深度学习模型的建立。在后面章节中笔者会教读者自定义某些层的方法。
对于梯度的更新,目前为止都是采用类似回传调用等方式对参数进行更新。这是由程序设计者手动完成的,然而TensorFlow 2.0自带了(并且作为推荐使用)梯度更新方法。代码如下:
model.compile(optimizer=tf.optimizers.Adam(1e-3), loss=tf.losses.categorical_crossentropy,metrics = ['accuracy']) model.fit(train_data, epochs=500)
compile函数是模型适配损失函数和选择优化器的专用函数,而fit函数的作用是把训练参数加载到模型中。下面分别对其进行讲解。
compile函数是TensorFlow 2.0中用于配置训练模型的专用编译函数。源码如下:
compile(optimizer, loss=None, metrics=None, loss_weights=None, sample_weight_mode=None, weighted_metrics=None, target_tensors=None)
这里笔者主要介绍其中最重要的3个参数:optimizer、loss和metrics。
可以看到,优化器(Optimizer)被传入了选定的优化器函数,loss是损失函数,这里也被传入选定的多分类crossentropy函数。metrics是用来评估模型的标准,一般用准确率来表示。
实际上,compile编译函数是一个多重回调函数的集合,对于所有的参数实际上就是根据对应函数的“地址”回调对应的函数,并将参数传入。
举个例子,在上面的编译器中传递的是一个TensorFlow 2.0自带的损失函数,而实际上往往针对不同的计算和误差需要使用不同的损失函数,在这里自定义一个均方差(MSE)损失函数,代码如下:
def my_MSE(y_true , y_pred): my_loss = tf.reduce_mean(tf.square(y_true - y_pred)) return my_loss
损失函数中接收2个参数,分别是y_true和y_pred,即预测值和真实值的形式参数。之后根据需要计算出真实值和预测值之间的误差。
损失函数名作为地址传递给compile后即可作为自定义的损失函数在模型中进行编译。代码如下:
opt = tf.optimizers.Adam(1e-3) def my_MSE(y_true , y_pred): my_loss = tf.reduce_mean(tf.square(y_true - y_pred)) return my_loss model.compile(optimizer=tf.optimizers.Adam(1e-3), loss=my_MSE,metrics = ['accuracy'])
优化器的自定义实际上也是可以的,但是一般情况下优化器的编写需要比较高的编程技巧以及对模型的理解。这里使用TensorFlow 2.0自带的优化器即可。
fit函数的作用以给定数量的轮次(数据集上的迭代)训练模型,主要参数有如下4个:
fit函数的主要作用就是对输入的数据进行修改。如果已经成功运行了程序3-4,那么换一种略微修改后的代码,重写运行iris数据集。
【程序3-5】
import tensorflow as tf import numpy as np from sklearn.datasets import load_iris data = load_iris() #数据的形式 iris_data = np.float32(data.data) #数据读取 iris_target = (data.target) iris_target = np.float32(tf.keras.utils.to_categorical(iris_target,num_classes=3)) input_xs = tf.keras.Input(shape=(4), name='input_xs') out = tf.keras.layers.Dense(32, activation='relu', name='dense_1')(input_xs) out = tf.keras.layers.Dense(64, activation='relu', name='dense_2')(out) logits = tf.keras.layers.Dense(3, activation="softmax",name='predictions')(out) model = tf.keras.Model(inputs=input_xs, outputs=logits) opt = tf.optimizers.Adam(1e-3) model.compile(optimizer=tf.optimizers.Adam(1e-3), loss=tf.losses.categorical_crossentropy,metrics = ['accuracy']) #fit函数载入数据 model.fit(x=iris_data,y=iris_target,batch_size=128, epochs=500) score = model.evaluate(iris_data, iris_target) print("last score:",score)
程序3-4和程序3-5最大的不同在于数据读取方式的变化。在程序3-4中,数据的读取方式和fit函数的载入方式如下:
iris_data = np.float32(data.data) iris_target = (data.target) iris_target = np.float32(tf.keras.utils.to_categorical(iris_target,num_classes=3)) train_data = tf.data.Dataset.from_tensor_slices((iris_data,iris_target)).batch(128) …… model.fit(train_data, epochs=500)
iris的数据读取被分成2个部分,分别是数据特征部分和标注(Label)部分。标注部分使用Keras自带的工具进行离散化处理。而离散化后处理的部分又被tf.data.Dataset API整合成一个新的数据集,并且按batch被切分成多个部分。
此时fit的处理对象是一个被tf.data.Dataset API处理后的Tensor类型数据,并且在切分的时候按照整合的内容被依次读取,由于读取的是一个Tensor类型的数据,因此fit内部的batch_size划分不起作用,而要使用生成数据tf中数据生成器的batch_size进行划分。如果读者对其还是不能够理解,则可使用如下代码段打印重新整合后的train_data中的数据:
for iris_data,iris_target in train_data
回到程序3-5中,取出对应于数据读取和载入的部分:
可以看到数据在读取和载入的过程中没有变化,将处理后的数据直接输入到fit函数中供模型使用。此时由于是直接对数据进行操作,因此对数据的划分由fit函数负责,并且batch_size被设定为128。