Press "Enter" to skip to content

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

 

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。
再对所有通道值做标准化。

# 自定义特征预处理层
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层做标准化。

 

注意,在tf模型中的所有对tensor的变换都需要用tf提供的方法,否则无法被编译为计算图,也就无法导出为SavedModel,同时也无法得到最优的执行速度。

 

另外,call方法的training参数是tf框架传入的,我们要把它透传给BatchNormalization,否则模型做预测的时候也会调整内部的权重。

 

构造模型网

 

有了预处理层,我们就可以构造整个网络结构了:

 

# 用函数式实现神经网络
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层后紧跟着预处理层。

 

编译模型

 

常规操作,给模型配置损失函数、优化函数、还有评估函数。

 

# 编译模型
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]
)

 

训练模型

 

训练样本未经处理,直接丢入模型,在模型计算过程中完成预处理:

 

# 训练模型
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层学习到的参数

 

print(model.variables)

 

在输出头部可以看见几个相关参数:

 

[<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数据直接丢入模型,完全不需要对其进行预处理,全部交给模型完成:

 

# 模型预测
print(model.evaluate(data_loader.test_data, data_loader.test_label))

 

得到精度97.33%:

 

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

Be First to Comment

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注