tensorflow2.0 keras SavedModel模型特征预处理
神经网络和xgboost有一个很大的区别,就是xgboost树模型对每个特征的数值范围不敏感,因此基本不需要做特征预处理就可以达到不错的效果。
而神经网络对特征的数值范围敏感,如果不进行特征预处理,模型效果可能还不如xgboost。
神经网络的特征预处理
为了让神经网络正常工作,特征预处理主要是2类:
- one-hot
- 标准化(让每个特征均值0、方差1)
在scikit-learn里面,我们一般是先对全量训练数据进行预处理完成标准化,然后再输入到模型。
在tensorflow神经网络模型里并不是这样,我们的标准化动作属于模型中的一层(标准化层),它是随着batch训练自学习的,并不需要我们先把全部训练数据过一遍,只需要直接投入到1个batch接着1个batch的训练过程中即可。
这样非常方便,因为我们训练完成后可以直接把模型保存为SavedModel放到tensorflow serving中,在线服务只需要直接把原始特征传给tensorflow serving,由模型预测过程中自动帮我们完成标准化,这样在线服务代码就不需要重复去实现特征预处理的复杂过程了。
one-hot或者其他简单的线性变化原理都一样,就是作为一层写到模型里即可。
举个例子
这是来自我的视频教程《简单粗暴的tensorflow2.0》中的部分代码,对其略作改动,以便给大家演示如何在神经网络模型中引入一些数据预处理逻辑。
数据loader
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import tensorflow as tf import numpy as np class MNISTLoader(): def __init__(self): mnist = tf.keras.datasets.mnist (self.train_data, self.train_label), (self.test_data, self.test_label) = mnist.load_data() # MNIST中的图像默认为uint8(0-255的数字)。以下代码将其归一化到0-1之间的浮点数,并在最后增加一维作为颜色通道 self.train_data = np.expand_dims(self.train_data.astype(np.float32) / 255.0, axis=-1) # [60000, 28, 28, 1] self.test_data = np.expand_dims(self.test_data.astype(np.float32) / 255.0, axis=-1) # [10000, 28, 28, 1] self.train_label = self.train_label.astype(np.int32) # [60000] self.test_label = self.test_label.astype(np.int32) # [10000] self.num_train_data, self.num_test_data = self.train_data.shape[0], self.test_data.shape[0] def get_batch(self, batch_size): # 从数据集中随机取出batch_size个元素并返回 index = np.random.randint(0, np.shape(self.train_data)[0], batch_size) return self.train_data[index, :], self.train_label[index] |
注意,这里我刻意没有对输入的训练图片进行/255的缩放,目的就是把这一步实现在模型的层中,以便可以让特征预处理+模型融为一体。
自定义预处理层
在模型的第一层就是我们的预处理层,我这里自定义Layer,给大家演示2个动作:
- 所有通道值除以255。
- 再对所有通道值做标准化。
1 2 3 4 5 6 7 8 9 |
# 自定义特征预处理层 class PreprocessLayer(tf.keras.layers.Layer): def __init__(self): super().__init__() self.normal = tf.keras.layers.BatchNormalization() def call(self, inputs, training): # 记得接受training参数,tf训练时会传True,预测时传False x = tf.math.divide(inputs, 255) return self.normal(x, training) # training参数决定标准化是否要学习这个batch,我们预测时不需要学习,这个很重要! |
这里使用tf.match.divide方法对tensor的所有列除以255,然后使用BatchNormalization层做标准化(其实它就是根据1个batch内的数据进行归一化,在batch之间不停的调整)。
注意,在tf模型中的所有对tensor的变换都需要用tf提供的方法,否则无法被编译为计算图,也就无法导出为SavedModel,同时也无法得到最优的执行速度。
另外,call方法的training参数是tf框架传入的,我们要把它透传给BatchNormalization,否则模型做预测的时候也会调整内部的权重。
构造模型
有了预处理层,我们就可以构造整个网络结构了:
1 2 3 4 5 6 7 8 |
# 用函数式实现神经网络 inputs = tf.keras.Input(shape=(28, 28, 1)) x = PreprocessLayer()(inputs) x = tf.keras.layers.Flatten()(x) x = tf.keras.layers.Dense(units=100, activation=tf.nn.relu)(x) x = tf.keras.layers.Dense(units=10)(x) outputs = tf.keras.layers.Softmax()(x) model = tf.keras.Model(inputs=inputs, outputs=outputs) |
为了方便,我使用函数式构造方法,在Input层后紧跟着预处理层。
编译模型
常规操作,给模型配置损失函数、优化函数、还有评估函数。
1 2 3 4 5 6 |
# 编译模型 model.compile( optimizer=tf.keras.optimizers.Adam(learning_rate=0.001), loss=tf.keras.losses.sparse_categorical_crossentropy, metrics=[tf.keras.metrics.sparse_categorical_accuracy] ) |
训练模型
训练样本未经处理,直接丢入模型,在模型计算过程中完成预处理:
1 2 3 4 5 6 7 |
# 训练模型 num_epochs = 5 batch_size = 50 data_loader = MNISTLoader() model.fit(data_loader.train_data, data_loader.train_label, epochs=num_epochs, batch_size=batch_size) |
观察batchNormalization层学习到的参数
1 |
print(model.variables) |
在输出头部可以看见几个相关参数:
1 |
[<tf.Variable 'preprocess_layer_1/batch_normalization_1/gamma:0' shape=(1,) dtype=float32, numpy=array([1.3039484], dtype=float32)>, <tf.Variable 'preprocess_layer_1/batch_normalization_1/beta:0' shape=(1,) dtype=float32, numpy=array([0.4065588], dtype=float32)>, <tf.Variable 'preprocess_layer_1/batch_normalization_1/moving_mean:0' shape=(1,) dtype=float32, numpy=array([0.13061081], dtype=float32)>, <tf.Variable 'preprocess_layer_1/batch_normalization_1/moving_variance:0' shape=(1,) dtype=float32, numpy=array([0.09487393], dtype=float32)>, <tf.Variable 'dense_4/kernel:0' shape=(784, 100) dtype=float32, numpy= |
模型预测
现在,我们拿着test数据直接丢入模型,完全不需要对其进行预处理,全部交给模型完成:
1 2 |
# 模型预测 print(model.evaluate(data_loader.test_data, data_loader.test_label)) |
得到精度97.33%:
1 2 |
10000/10000 [==============================] - 0s 44us/sample - loss: 0.0921 - sparse_categorical_accuracy: 0.9733 [0.09212067870919127, 0.9733] |
最后
采用继承Model实现模型是一样的道理,只需要在模型计算过程中调用batchNormalization层或者普通的tf.math运算即可,但要记得调用batchNormalization时候也要透传traning参数。
模型正常导出即可用于tf serving,这样工程代码也就不需做特征预处理了,多么方便。
美团有一篇博客大概提到了特征预处理进模型的思路:https://tech.meituan.com/2018/10/11/tfserving-improve.html。
有任何问题欢迎留言交流。
如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~
