Python Tensorflow1.x升级到2.x低阶API实践

Posted 肖永威

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python Tensorflow1.x升级到2.x低阶API实践相关的知识,希望对你有一定的参考价值。

1. 常用低阶API的升级

这份文档适用于使用低阶 TensorFlow1.x API升级到Tensorflow 2.x API 的开发者。如果您正在使用高阶 API (tf.keras),可能无需或仅需对您的代码执行很少操作,便可以让代码完全兼容 TensorFlow 2.x。

在 TensorFlow 2.x 中,1.X 的代码不经修改也许还可运行(除了contrib):

import tensorflow.compat.v1 as tf
tf.disable_v2_behavior()

但是,这样做无法发挥 TensorFlow 2.x 的许多优化改进。这里作者从低阶API代码视角实践Tensorflow1.x升级到2.x低阶API。

升级一些常见的改写经验,先从挥手告别难于理解的占位符开始,包括tf.placeholder,tf.InteractiveSession(),tf.Session()等,如下代码tf.placeholder所示。

    #定义输入变量
    x = tf.placeholder(dtype=tf.float32,shape=[None,in_units],name='x')

其他的部分常用API及其变化如下表所示。

功能Tensorflow 1.XTensorflow 2.X
梯度优化tf.train.AdamOptimizertf.optimizers.Adam
占位符tf.placeholder
会话tf.Session()
数据分布tf.truncated_normaltf.random.truncated_normal
数学计算logtf.logtf.math.log
模型保存tf.train.Saver.savetf.saved_model.save
模型加载tf.train.Saver.restoretf.saved_model.load

其中,tensorflow 1.x版本中的placeholder,在tf2中已经被取消,在tf2中,可以用tf.keras.Inputs代替。示例:

# tf1中
input_ids = tf.placeholder(dtype=tf.int32, shape=[None])
#tf2中,改写为:
input_ids = tf.keras.Input(dtype=tf.int32, shape=[None])

2. 关于计算图

在tensorflow 1.x中,由于是基于静态图机制(Graph Execution),需要先构造图,然后才真正运行,因此需要用显示调用Session后,才会真正触发计算,对调试代码非常不利,使用不方便。

在tensorflow 2.x中,默认是基于动态图机制(Eager Execution),就像常规函数一样,调用时就触发计算。对调试代码非常方便。

所以,tf1中session部分代码,可以全部去掉。示例:

# tf1中
sess = tf.InteractiveSession()
sess.run(tf.global_variables_initializer())

3. 全连接神经网络升级实践

3.1. 使用公开数据集

使用scikit-learn 提供的简单手写字符识别数据集DIGITS,每行数据宽度为64。

import numpy as np
import tensorflow as tf
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split

digits = load_digits()  # DIGITS 数据集是 scikit-learn 提供的简单手写字符识别数据集。
digits_y = np.eye(10)[digits.target]  # 标签独热编码
# 独热编码是一种稀疏向量,其中:一个元素设为 1,所有其他元素均设为 0。
# 切分数据集,测试集占 20%
X_train, X_test, y_train, y_test = train_test_split(digits.data, digits_y,test_size=0.2, random_state=1)

3.2. 定义全连接神经网络

class DNNModel(tf.Module):
    def __init__(self, layer=[64,128,256,10], keep_prob = 0.2, name=None):
        super(DNNModel, self).__init__(name=name)
        self.layer=layer
        self.keep_prob = keep_prob
        self.num = len(self.layer)-1   # 网络层数
        self.w = []
        self.b = []
        h_in = self.layer[0]
        h_out = self.layer[1]
        
        for i in range(self.num):
            # tf.truncated_normal由tf.random.truncated_normal替换
            w0 = tf.Variable(tf.random.truncated_normal([h_in,h_out],stddev=0.1),name='weights' +str(i+1) )
            b0 = tf.Variable(tf.zeros([h_out],name='biases' + str(i+1)))  
            self.w.append(w0)
            self.b.append(b0)
            if i < self.num -1:
                h_in = self.layer[i + 1]
                h_out = self.layer[i + 2]
        
        self.var = []
        for i in range(len(self.w)):
            self.var.append(self.w[i])
            self.var.append(self.b[i])
    
    @tf.function(input_signature=[tf.TensorSpec([None,64])])    
    def __call__(self, x):
        x = tf.cast(x, tf.float32)  # 转换输入数据类型
        h1 = x
        for i in range(self.num):
            #定义前向传播过程
            if i < self.num -1:
                #h0 = tf.nn.relu(x@self.w[i] + self.b[i], name='layer' +str(i+1))             
                h0 = tf.nn.relu(tf.matmul(h1, self.w[i]) + self.b[i], name='layer' +str(i+1))  # 或x@w,以及tf.add
                #使用dropout
                if i == 0:
                    h1 = h0
                else:
                    h1 = tf.nn.dropout(h0, rate = 1 - self.keep_prob) 
            else:
                h1 = tf.matmul(h1, self.w[i]) + self.b[i]

        # 定义输出层
        #self.y_conv = tf.nn.softmax(h1,name='y_conv') # 自行编写交叉熵损失,或tf.losses.categorical_crossentropy
        self.y_conv = h1 # 直接使用softmax_cross_entropy_with_logits
        
        return self.y_conv

注意:

  • python中一切皆对象,函数也是对象,同时也是可调用对象(callable),一个类实例要变成一个可调用对象,只需要实现一个特殊方法__call__()。但是在TensorFlow中,对于自定义模型的时候,经常用call()函数,区别在于__call__()用于class继承tf.Module,call()用于class继承tf.keras.Module。

3.3. 模型训练

layer=[64,128,128,10]
EPOCHS = 100  # 迭代此时
LEARNING_RATE = 0.01  # 学习率
model = DNNModel(layer = layer, keep_prob = 0.2)  # 实例化模型类

for epoch in range(EPOCHS):
    with tf.GradientTape() as tape:  # 追踪梯度
        preds = model(X_train)
        loss = tf.reduce_sum(tf.nn.softmax_cross_entropy_with_logits(logits=preds, labels=y_train))
        
        #loss = tf.reduce_sum(tf.losses.categorical_crossentropy(y_train, preds))

    trainable_variables = model.var # 需优化参数列表
    grads = tape.gradient(loss, trainable_variables)  # 计算梯度,求导

    optimizer = tf.optimizers.Adam(learning_rate=LEARNING_RATE)  # Adam 优化器
    optimizer.apply_gradients(zip(grads, trainable_variables))  # 更新梯度
    # 计算准确度
    preds = tf.argmax(model(X_test), axis=1)  # 取值最大的索引,正好对应字符标签
    labels = tf.argmax(y_test, axis=1)
    accuracy = tf.reduce_mean(tf.cast(tf.equal(preds, labels), tf.float32))

    # 输出各项指标
    if (epoch + 1) % 10 == 0:
        print(f'Epoch [epoch+1/EPOCHS], Train loss: loss, Test accuracy: accuracy')

3.4. 模型持久化保存

对于tensorflow2.x版本生成的saved_model模型,没有像1.x版本使用SavedModelBuilder API自定义签名(输入输出的数据类型+方法),2.x版本模型输入的数据类型可以通过@tf.function中的input_signature参数指定,方法目前来看是写死在源码中的,只有signature_constants.PREDICT_METHOD_NAME一种。很多时候模型的输入输出对我们来说是黑盒的,而在调用服务接口的时候我们需要知道模型的输入输出以及签名的key,我们可以使用saved_model_cli show --dir model/test/1 –all来查看我们需要的参数。

tf.saved_model.save(model,'DNNmodel_base')

3.5. 模型加载与使用

restored_model = tf.saved_model.load('DNNmodel_base')
f = restored_model.signatures["serving_default"]
X_test = tf.cast(X_test, tf.float32) 

preds = f(X_test)
preds = tf.constant(preds['output_0'])
preds = tf.argmax(preds, axis=1)
labels = tf.argmax(y_test, axis=1)
accuracy = tf.reduce_mean(tf.cast(tf.equal(preds, labels), tf.float32))
print(f'Test accuracy: accuracy')

4. 升级中遇到的问题

4.1. 关于tensorflow-cpu

针对Tensorflow 1.x的GPU安装版本,Tensorflow 2.x不再区分是否gpu,默认直接安装Tensorflow 。当系统检测到gpu并安装cuda后,则自动调用gpu。

如果,我们不需要或没有gpu时,gpu适配对这部分群体是浪费的(占用不必要的资源),于是有了Tensorflow-cpu,我们可以理解其为cpu only版本。

综上,也可以理解为:tensorflow == 1.x对应tensorflow-cpu == 2.x,tensorflow-gpu == 1.x 对应 tensorflow == 2.x。

4.2. 遇到问题

Tensorflow 2.0 使用AdamOptimizer模型报错。
原因:keras与tensorflow版本不匹配
ImportError: cannot import name ‘dtensor’ from ‘tensorflow.compat.v2.experimental’
环境 windows10 、tensorflow2.6版本(Keras已由2.10降至2.6)

5. 关于Tensorflow API简单说明

TensorFlow API一共可以分为三个层次,即低阶API、中阶API、高阶API:

第一层为Python实现的操作符,主要包括各种张量操作算子、计算图、自动微分;
第二层为Python实现的模型组件,对低级API进行了函数封装,主要包括各种模型层,损失函数,优化器,数据管道,特征列等等;
第三层为Python实现的模型成品,一般为按照OOP方式封装的高级API,主要为tf.keras.models提供的模型的类接口。

参考:

[1]. 谷歌开发者. 官方教程 手把手教你如何将 TensorFlow 1 升级到 TensorFlow 2(上). 知乎. 2020.09
[2]. 不要清汤锅. tensorflow / tensorflow-gpu / tensorflow-cpu区别?. CSDN博客. 2020.12
[3].凡心curry. TensorFlow 2 ——神经网络模型. CSDN博客. 2021.04
[4]. Doit. TensorFlow2.0教程-使用低级api训练(非tf.keras). 知乎. 2019.07
[5]. yunfeather. tensorflow中Adam优化器运用. CSDN博客. 2020.05
[6]. 丙吉. keras报错ImportError: cannot import name ‘dtensor’ from ‘tensorflow.compat.v2.experimental’. 简书. 2022.09
[7]. 然后就去远行吧. TensorFlow2.0 —— 模型保存与加载. CSDN博客. 2021.04
[8]. Tester_muller. Tensorflow 2.x 模型-部署与实践. CSDN博客. 2022.08
[9]. 然后就去远行吧. TensorFlow 2.0简介. CSDN博客. 2020.08
[10]. 肖永威. Tensorflow BP神经网络多输出模型在生产管理中应用实践. CSDN博客. 2020.09

以上是关于Python Tensorflow1.x升级到2.x低阶API实践的主要内容,如果未能解决你的问题,请参考以下文章

Python Tensorflow1.x升级到2.x低阶API实践

基于python的图像识别

使用Keras的模型类将Tensorflow 1.x代码迁移到Tensorflow 2.x

TensorFlow1.x 代码实战系列:MNIST手写数字识别

不降级解决No module named ‘tensorflow.contrib‘

yolov3_tiny+车牌识别 tensorflow1.x实现(适合新手,全程教学)