本文介绍怎么把TensorFlow 1.X代码迁移到2.X。
目录
本文的迁移指南主要针对使用TensorFlow底层API的用户。如果使用的是高层的API(比如tf.keras),那么你的代码可能不需要做什么修改就能在2.X里运行。你只需要关注一些后面介绍的optimizer默认学习率的变更以及log的metric的名称的变化就行了。
在TensorFlow 2.X里还是可以运行1.X的代码(contrib包除外),你只需要从compat导入v1并且禁用2.X的特性:
import tensorflow.compat.v1 as tf
tf.disable_v2_behavior()
但是这样做的话你就不能利用2.X好的特性。本文会帮助你怎么升级你的遗留代码,把它重构的更加简单和高效并且易于维护。
升级脚本
迁移的第一步是使用升级脚本把1.X的代码”升级”到2.X,这个脚本只是把1.X的包用2.X的tf.compat.v1里等价的包替换。我们的代码风格还是1.X的,比如需要自己构建计算图,定义Place Holder等等。
重要的变更
如果我们在2.X里通过tf.compat.v1.disable_v2_behavior()来”模拟”1.X,那么需要注意如下的重要变更,这些变更可能会让你的1.X的代码出问题。
Eager执行
v1.enable_eager_execution()之后,任何隐式使用Graph的操作都会失败,必须显式的放到Graph的context里,比如:
import tensorflow.compat.v1 as tf
tf.disable_v2_behavior()
tf.enable_eager_execution()
x = tf.placeholder(dtype=tf.float32, shape=(2))
如果开启Eager执行后,我们定义tf.placeholder会失败,系统会抛出异常”RuntimeError: tf.placeholder() is not compatible with eager execution.”。我们必须把它放到Graph的context里:
import tensorflow.compat.v1 as tf
tf.disable_v2_behavior()
tf.enable_eager_execution()
with tf.Graph().as_default():
x = tf.placeholder(dtype=tf.float32, shape=(2))
Resource Variable
ResourceVariable是TensorFlow 2.X用来替代1.X的Variable的,如果开启了Eager执行,则默认会创建ResourceVariable。比如:
import tensorflow.compat.v1 as tf
x = tf.Variable([2,3.])
print(type(x))
输出为:<class 'tensorflow.python.ops.resource_variable_ops.ResourceVariable'>
如果使用禁用v2的行为:
import tensorflow.compat.v1 as tf
tf.disable_v2_behavior()
x = tf.Variable([2,3.])
print(type(x))
输出为:<class 'tensorflow.python.ops.variables.RefVariable'>
ResourceVariable会在数据的同步时增加锁,但是它和原来的RefVariable并不完全等价,这可能在某些极端情况下导致执行的结果和1.X不同。如果我们的1.X的代码调用了tf.enable_resource_variables(),则可以在创建变量时传入use_resource=False让它创建RefVariable。
TensorShape
在1.X里要得到Tensor的某个维度,需要t.shape[0].value,而在2.X里只需要t.shape[0]就可以了。
控制流
如果我们在1.X的代码里开启了v2的控制流,也就是通过tf.enable_control_flow_v2(),则1.X的某些代码也可能fail。
用2.X的特性来实现1.X的逻辑
下面我们通过一些例子来展示怎么把1.X的代码重新用2.X的特性来实现。
去掉Session.run
每一次Session.run都应该用一个Python函数来替代:
- feed_dict和v1.placeholders应该变成函数的参数
- fetches变成函数的返回值
- 调试时建议使用Eager执行,这样可以用标准的Python调试器(比如pdb)调试
如果调试没有问题后,可以给这个函数加上tf.function的装饰来提高速度。
注意:
- 和Session.run不同,tf.function的输入和输出是固定的。
- tf.control_dependencies不再需要。因为tf.function里的代码会按照顺序执行,被AutoGraph编译后的计算图也会保证这一点。
使用Python对象来跟踪TensorFlow变量和loss
在TensorFlow 2.x里强烈不建议通过变量名来跟踪变量(这里的变量名指的是Variable在计算图里的名字)。好的做法是直接用Python对象来跟踪变量。因此不要再使用tf.get_variable和各种variable scope了,直接用tf.Variable()创建变量。每一个variable_scope都应该用一个对象来替代,我们通常可以用如下对象:
- tf.keras.layers.Layer
- tf.keras.Model
- tf.Module
如果需要一组相关的变量(比如tf.Graph.get_collection(tf.GraphKeys.VARIABLES)),使用Layer或者Model对象的.variables和.trainable_variables属性。
Layer和Model类会内部保存一些属性来替代全局集合。它们的.losses属性可以用来替代tf.GraphKeys.LOSSES这个集合。更多介绍请参考Keras文档。
升级循环代码
尽量用高层的API。如果可能的话尽量用tf.keras.Model.fit来替代你自己的训练循环。这些高层的API会帮你处理很多细节问题。比如,他们会自动的收集正则化loss,并且在训练时会设置training=True(对于dropout层来说非常重要,如果是我们用底层的tf.nn.dropout,则我们必须设置keep_prob为PalceHolder,然后训练的时候feed进小于1的值,而预测的时候feed进1)。
升级数据处理pipeline
使用tf.data的dataset来处理输入。它的实现非常高效,并且和TensorFlow集成的很好。dataset可以直接传给tf.keras.Model.fit函数:
model.fit(dataset, epochs=5)
我们也可以直接迭代它的每一个batch:
for example_batch, label_batch in dataset:
break
去掉compat.v1
tf.compat.v1完全包括了TensorFlow 1.x的API,保留了原有的语义。
如果能够找到与之完全对应的2.X的版本,升级脚本会把v1的符号直接替换成对应的符号(比如v1.arg_max会直接变成tf.argmax)。但是还会有很多符号找不到对应的版本,这些需要我们自己来收到升级,用2.X的方法重写。
重写模型
导入
下面的示例需要导入tensorflow和tensorflow_datasets:
import tensorflow as tf
import tensorflow_datasets as tfds
注意:TensorFlow Datasets是独立于TensorFlow 2.X的一个包,用于提供常见的机器学习任务的数据。需要单独安装:
pip install tensorflow-datasets
另外,因为TensorFlow Datasets的很多数据都是放到google的服务器上,所以可能需要设置HTTP(s)代理,这可以通过设置环境变量HTTP_PROXY和HTTPS_PROXY。
底层API
需要重新的底层的API包括:
- 使用variable scope来控制变量的重用(resue)
- 使用v1.get_variable()创建变量
- 显式的访问集合(collection)
- 隐式的访问集合,比如:
- v1.global_variables
- v1.losses.get_regularization_loss
- v1.placeholder
- Session.run
- 变量的收到初始化
重写前
下面的代码是TensorFlow 1.X的典型写法:
in_a = tf.placeholder(dtype=tf.float32, shape=(2))
in_b = tf.placeholder(dtype=tf.float32, shape=(2))
def forward(x):
with tf.variable_scope("matmul", reuse=tf.AUTO_REUSE):
W = tf.get_variable("W", initializer=tf.ones(shape=(2,2)),
regularizer=tf.contrib.layers.l2_regularizer(0.04))
b = tf.get_variable("b", initializer=tf.zeros(shape=(2)))
return W * x + b
out_a = forward(in_a)
out_b = forward(in_b)
reg_loss=tf.losses.get_regularization_loss(scope="matmul")
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
outs = sess.run([out_a, out_b, reg_loss],
feed_dict={in_a: [1, 0], in_b: [0, 1]})
重写后
我们需要这样重写代码:
- variable变成Python局部对象
- forward函数还是类似的定义前向计算过程
- Session.run被直接调用forward替代
- 为了提高速度可以用tf.function装饰函数从而用AutoGraph把它编译成计算图(JIT的编译后执行,而不是Eager执行的解释执行)
- 正则的loss是自动处理的,不需要全局的集合
- 去掉Session和PlaceHolder
W = tf.Variable(tf.ones(shape=(2,2)), name="W")
b = tf.Variable(tf.zeros(shape=(2)), name="b")
@tf.function
def forward(x):
return W * x + b
out_a = forward([1,0])
print(out_a)
不需要Session,直接就可以调用forward函数,结果为:
tf.Tensor(
[[1. 0.]
[1. 0.]], shape=(2, 2), dtype=float32)
我们也可以直接定义tf.keras.regularizers.l2并根据参数计算L2正则loss:
out_b = forward([0,1])
regularizer = tf.keras.regularizers.l2(0.04)
reg_loss=regularizer(W)
tf.layers的重写
在TensorFlow 2.X里只保留了Keras的高层API(Estimator被有限的保留,原因是某些功能Keras还没有实现,但是迟早要被抛弃的),所以如果我们在1.X里使用了tf.layers,那么就需要用tf.keras.layers里等价的API实现。tf.layers是严重依赖variable scope和全局集合的:
def model(x, training, scope='model'):
with tf.variable_scope(scope, reuse=tf.AUTO_REUSE):
x = tf.layers.conv2d(x, 32, 3, activation=tf.nn.relu,
kernel_regularizer=tf.contrib.layers.l2_regularizer(0.04))
x = tf.layers.max_pooling2d(x, (2, 2), 1)
x = tf.layers.flatten(x)
x = tf.layers.dropout(x, 0.1, training=training)
x = tf.layers.dense(x, 64, activation=tf.nn.relu)
x = tf.layers.batch_normalization(x, training=training)
x = tf.layers.dense(x, 10)
return x
train_out = model(train_data, training=True)
test_out = model(test_data, training=False)
上面的函数是1.X里非常常见的写法,我们把模型(层)的定义封装成一个函数,通过使用tf.variable_scope来控制重用。如果两次调用传入的是相同的scope,则返回的是同一个模型(参数共享)。
我们可以这样实现tf.layers的重写:
- 对于简单的层的堆叠(stack)可以用tf.keras.Sequential替代(更加复杂的模型可以用keras的自定义Layer和/或Model实现,也可以用Keras的函数API实现)
- 模型自动来跟踪变量和正则loss
- tf.layers里的层通常可以找到tf.keras.layers里对应的API
大部分tf.keras.layers的参数是和tf.layers一样的,但是需要注意如下的区别:
- training参数(是否训练阶段,这可能影响Dropout等层的行为)变成运行(call)模型时传入(而不是以前的构造层的时候作为PlaceHolder传入)
- 1.X的函数通常需要传入输入x,而2.X不需要。2.X的输入是在call模型时直接传入,不需要像1.X那样先定义PlaceHolder然后feed进去。
另外还需要主要:
- 如果你使用了tf.contrib包里的API,那么它的参数可能和Keras差别很大
- 2.X的函数(包括Keras)不会再偷偷的把变量放到全局的集合里,因此如果你的代码还是按照以前的习惯去全局集合里找变量,那么通常会出问题。
重写后的代码为:
model = tf.keras.Sequential([
tf.keras.layers.Conv2D(32, 3, activation='relu',
kernel_regularizer=tf.keras.regularizers.l2(0.04),
input_shape=(28, 28, 1)),
tf.keras.layers.MaxPooling2D(),
tf.keras.layers.Flatten(),
tf.keras.layers.Dropout(0.1),
tf.keras.layers.Dense(64, activation='relu'),
tf.keras.layers.BatchNormalization(),
tf.keras.layers.Dense(10)
])
train_data = tf.ones(shape=(1, 28, 28, 1))
test_data = tf.ones(shape=(1, 28, 28, 1))
train_out = model(train_data, training=True)
print(train_out)
输出为:
tf.Tensor([[0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]], shape=(1, 10), dtype=float32)
下面是测试预测阶段的行为:
test_out = model(test_data, training=False)
print(test_out)
输出为:
tf.Tensor(
[[ 0.12883385 -0.14702433 -0.10418132 0.16400227 0.07311949 0.08754075
-0.21958953 0.00542981 -0.28473938 0.10698275]], shape=(1, 10), dtype=float32)
因为Dropout和BatchNormalization在训练和预测阶段的行为是不一样的,所以我们看到结果也是不同的。
如果我们想得到Keras帮我们创建和维护的Variable或者loss,也非常简单:
print([v.name for v in model.trainable_variables])
print(model.losses)
输出为:
['conv2d/kernel:0', 'conv2d/bias:0', 'dense/kernel:0', 'dense/bias:0', 'batch_normalization/gamma:0', 'batch_normalization/beta:0', 'dense_1/kernel:0', 'dense_1/bias:0']
[<tf.Tensor: shape=(), dtype=float32, numpy=0.07643835>]
这里我们不用操心变量的名字了,如果要创建一个新的模型,直接再构造调用一次:
model2 = tf.keras.Sequential([
tf.keras.layers.Conv2D(32, 3, activation='relu',
kernel_regularizer=tf.keras.regularizers.l2(0.04),
input_shape=(28, 28, 1)),
tf.keras.layers.MaxPooling2D(),
tf.keras.layers.Flatten(),
#tf.keras.layers.Dropout(0.1),
tf.keras.layers.Dense(64, activation='relu'),
tf.keras.layers.BatchNormalization(),
tf.keras.layers.Dense(10)
])
print([v.name for v in model2.trainable_variables])
print(model2.losses)
输出为:
['conv2d_1/kernel:0', 'conv2d_1/bias:0', 'dense_2/kernel:0', 'dense_2/bias:0', 'batch_normalization_1/gamma:0', 'batch_normalization_1/beta:0', 'dense_3/kernel:0', 'dense_3/bias:0']
[<tf.Tensor: shape=(), dtype=float32, numpy=0.07693285>]
变量的名字是不会重复的。如果我们要共享参数呢?那也很简单:直接把model或者model的某一层给要用的人就行:
print(model2.layers[0])
输出:
<tensorflow.python.keras.layers.convolutional.Conv2D object at 0x7f7c9879ab00>
混合了variable和layers的代码重写
比如下面的函数,既使用了tf.layers的API,也自己通过get_variable创建了变量:
def model(x, training, scope='model'):
with tf.variable_scope(scope, reuse=tf.AUTO_REUSE):
W = tf.get_variable(
"W", dtype=tf.float32,
initializer=tf.ones(shape=x.shape),
regularizer=tf.contrib.layers.l2_regularizer(0.04),
trainable=True)
if training:
x = x + W
else:
x = x + W * 0.5
x = tf.layers.conv2d(x, 32, 3, activation=tf.nn.relu)
x = tf.layers.max_pooling2d(x, (2, 2), 1)
x = tf.layers.flatten(x)
return x
train_out = model(train_data, training=True)
test_out = model(test_data, training=False)
重写的方法:
- 在构造函数__init__里收集Layer的参数
- 在build函数里创建变量
- 在call函数里实现计算并且返回结果
用v1.variable_scope定义的变量其实就是一个层,因此用tf.kears.layers.Layer来实现。我们首先基础Keras的Layer来实现W的逻辑:
# Create a custom layer for part of the model
class CustomLayer(tf.keras.layers.Layer):
def __init__(self, *args, **kwargs):
super(CustomLayer, self).__init__(*args, **kwargs)
def build(self, input_shape):
self.w = self.add_weight(
shape=input_shape[1:],
dtype=tf.float32,
initializer=tf.keras.initializers.ones(),
regularizer=tf.keras.regularizers.l2(0.02),
trainable=True)
@tf.function
def call(self, inputs, training=None):
if training:
return inputs + self.w
else:
return inputs + self.w * 0.5
注意:这里w是在build是才创建出来的,而build又是在第一次调用call是被调用的,因此w的shape取决于第一次call传入的x。
mylayer=CustomLayer()
input=tf.ones((2,3))
out=mylayer(input)
out=mylayer(input, training=True)
print(out)
# 下面的代码会抛出异常!!!
input2=tf.ones((2,4))
out2=mylayer(input2)
print(out2)
接下来就可以用Keras的Sequential把所有的Layer都stack起来构建模型了:
train_data = tf.ones(shape=(1, 28, 28, 1))
test_data = tf.ones(shape=(1, 28, 28, 1))
# Build the model including the custom layer
model = tf.keras.Sequential([
CustomLayer(input_shape=(28, 28, 1)),
tf.keras.layers.Conv2D(32, 3, activation='relu'),
tf.keras.layers.MaxPooling2D(),
tf.keras.layers.Flatten(),
])
train_out = model(train_data, training=True)
test_out = model(test_data, training=False)
下面是一些注意事项:
- 我们继承的Keras的Model或者Layer需要既支持Eager执行也需要支持1.X的计算图方式。
- 我们可以把call函数用tf.function装饰从而支持计算图的方式
-
call函数如果训练和预测行为不同,那么要有一个training参数来区分训练还是预测阶段
- 在构造函数或者build函数里使用self.add_weight()来创建变量
- build会传入输入的shape,这个时候可以根据不同的输入创建不同shape的变量;如果在构造函数里创建,那么通常是一个固定的shape,那么就需要调用者较早知道输入的shape,这对于库的开发者来说通常是不太可能知道的。
- 使用Layer.add_weight添加变量,这样Keras会帮我们跟踪变量以及loss。
- 不要在你的对象里保存tf.Tensor
- 因为你的对象既可能是Eager执行,也可能是tf.function里,这些Tensor在这两种模式里的行为是不一致的
- 使用tf.Variable来保存状态,它在两种模式下是一致的
- tf.Tensor只能用于保存临时的计算结果
tf.slim
很多TensorFlow 1.x使用了Slim库,在1.X里它是放在tf.contrib.layers里。在2.X里,即使tf.compat.v1里也没有了Slim库。要把使用Slim的代码升级到2.X会比tf.layers更加困难。更好的做法是:首先用v1.layers的API来重写Slim的代码,然后再用Keras的API重写v1.layers。如果读者没有用过Slim库并且也没有把某个第三方的依赖Slim库的代码升级到2.X的需求的话,可以跳过本节。
下面是一些注意事项:
- 删除所有的arg_scope
- 把normalizer_fn和activation_fn放到自己的Layer里
- Slim和v1.layers的参数名(比如都是卷积层)和默认参数都不相同
- 某些参数的尺度(scale)可能不同
- 如果你使用Slim的预训练模型,尝试用tf.keras.applicatoins包里的预训练模型或者TF Hub从Slim导出的SavedModel对象。
某些tf.contrib的API可能被移出TensorFlow的core,一般会被移到add-ons包里。
训练
有很多方法给tf.keras模型feed数据,包括Numpy数组和Python生成器(generator)。推荐的feed数据方法是使用tf.data包,这个包里包含了高效处理数据的类。tf.queue还是存在,但是它只是作为一种数据结构而不是输入的pipeline了。
使用Datasets
TensorFlow Datasets(tfds)包用于读取常见公开数据集,它会返回tf.data.Dataset对象。比如,我们可以用tfds加载MNIST数据集。
datasets, info = tfds.load(data_dir='/tmp/mnist', name='mnist', with_info=True, as_supervised=True)
mnist_train, mnist_test = datasets['train'], datasets['test']
第一次运行的时候会下载,类似如下的输出:
2020-09-21 17:02:57.997690: W tensorflow/core/platform/cloud/google_auth_provider.cc:184] All attempts to get a Google authentication bearer token failed, returning an empty token. Retrieving token from files failed with "Not found: Could not locate the credentials file.". Retrieving token from GCE failed with "Failed precondition: Error executing an HTTP request: libcurl code 6 meaning 'Couldn't resolve host name', error details: Couldn't resolve host 'metadata'".
Downloading and preparing dataset mnist/3.0.1 (download: 11.06 MiB, generated: 21.00 MiB, total: 32.06 MiB) to /tmp/mnist/mnist/3.0.1...
WARNING:absl:Dataset mnist is hosted on GCS. It will automatically be downloaded to your
local data directory. If you'd instead prefer to read directly from our public
GCS bucket (recommended if you're running on GCP), you can instead pass
`try_gcs=True` to `tfds.load` or set `data_dir=gs://tfds-data/datasets`.
Dl Completed...: 100%|██████████| 4/4 [00:24<00:00, 6.09s/ file]
Dataset mnist downloaded and prepared to /tmp/mnist/mnist/3.0.1. Subsequent calls will reuse this data.
前面的警告不用理睬,那是GCE的认证失败,因为我们不需要登录(除非我们需要上传模型)。后面的日志显示数据被下载到了本地。注意:如果不能访问Google的服务的话需要设置能访问的代理(HTTP_PROXY和HTTPS_PROXY)。
接下来是处理数据:
- 缩放图片
- 打乱顺序
- 收集一个batch的图片和标签
BUFFER_SIZE = 10 # Use a much larger value for real code.
BATCH_SIZE = 64
NUM_EPOCHS = 5
def scale(image, label):
image = tf.cast(image, tf.float32)
image /= 255
return image, label
为了调试简单,我们的数据集用take只返回5个batch:
train_data = mnist_train.map(scale).shuffle(BUFFER_SIZE).batch(BATCH_SIZE)
test_data = mnist_test.map(scale).batch(BATCH_SIZE)
STEPS_PER_EPOCH = 5
train_data = train_data.take(STEPS_PER_EPOCH)
test_data = test_data.take(STEPS_PER_EPOCH)
这样得到的Datasets是可迭代的,如果要把它变成迭代器,可以把它传给iter,然后就可以next来遍历了:
image_batch, label_batch = next(iter(train_data))
使用Keras来训练
上面的代码演示了我们自己来遍历一个Dataset的方法,但是更加推荐的方法是Keras Model的fit()、evaluate()和predict()方法。这些方法会自动遍历Dataset,它们相比手动遍历的好处为:
- 同时支持Numpy数组、Python生成器和tf.data.Dataset三种输入方式
- 自动处理正则化和激活的loss
- 支持在多个设备上用tf.distribute进行分布式计算
- 支持回调来计算loss或者各种metric
- 支持like tf.keras.callbacks.TensorBoard和自定义回调
- 非常高效,自动转换成TensorFlow的计算图
代码示例如下:
model = tf.keras.Sequential([
tf.keras.layers.Conv2D(32, 3, activation='relu',
kernel_regularizer=tf.keras.regularizers.l2(0.02),
input_shape=(28, 28, 1)),
tf.keras.layers.MaxPooling2D(),
tf.keras.layers.Flatten(),
tf.keras.layers.Dropout(0.1),
tf.keras.layers.Dense(64, activation='relu'),
tf.keras.layers.BatchNormalization(),
tf.keras.layers.Dense(10)
])
# Model is the full model w/o custom layers
model.compile(optimizer='adam',
loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
metrics=['accuracy'])
model.fit(train_data, epochs=NUM_EPOCHS)
loss, acc = model.evaluate(test_data)
print("Loss {}, Accuracy {}".format(loss, acc))
结果为:
Epoch 1/5
1/5 [=====>........................] - ETA: 0s - loss: 2.4843 - accuracy: 0.1719
5/5 [==============================] - 0s 5ms/step - loss: 1.4902 - accuracy: 0.5188
Epoch 2/5
1/5 [=====>........................] - ETA: 0s - loss: 0.4867 - accuracy: 0.8750
5/5 [==============================] - 0s 4ms/step - loss: 0.4048 - accuracy: 0.9062
Epoch 3/5
1/5 [=====>........................] - ETA: 0s - loss: 0.2883 - accuracy: 0.9375
5/5 [==============================] - 0s 4ms/step - loss: 0.2427 - accuracy: 0.9656
Epoch 4/5
1/5 [=====>........................] - ETA: 0s - loss: 0.1828 - accuracy: 1.0000
5/5 [==============================] - 0s 5ms/step - loss: 0.1660 - accuracy: 0.9906
Epoch 5/5
1/5 [=====>........................] - ETA: 0s - loss: 0.1239 - accuracy: 1.0000
5/5 [==============================] - 0s 4ms/step - loss: 0.1326 - accuracy: 0.9906
1/5 [=====>........................] - ETA: 0s - loss: 1.5845 - accuracy: 0.7969
5/5 [==============================] - 0s 2ms/step - loss: 1.6352 - accuracy: 0.7469
Loss 1.6351888179779053, Accuracy 0.746874988079071
自定义数据遍历循环
如果Keras的一步训练能够满足我们的要求,但是我们需要每一步之外有更多控制,那么我们可以自己循环遍历数据,然后使用tf.keras.Model.train_on_batch。在考虑这样做之前记得提醒自己:我们可以用tf.keras.callbacks.Callback做很多事情,如果可以用Callback就能做到,就没有必要自己遍历数据。
在训练的过程中也可以使用tf.keras.Model.test_on_batch或者tf.keras.Model.evaluate来检查模型的效果。
注意: train_on_batch和test_on_batch默认会返回单个batch的loss和指标(metric)。如果传入reset_metrics=False,则会累计loss和指标,那么你就需要记得在合适的时机reset它们。另外一些指标比如AUC可能要求reset_metrics为False才能计算。
继续上面的例子:
model.compile(optimizer='adam',
loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
metrics=['accuracy'])
for epoch in range(NUM_EPOCHS):
model.reset_metrics()
for image_batch, label_batch in train_data:
result = model.train_on_batch(image_batch, label_batch)
metrics_names = model.metrics_names
print("train: ",
"{}: {:.3f}".format(metrics_names[0], result[0]),
"{}: {:.3f}".format(metrics_names[1], result[1]))
for image_batch, label_batch in test_data:
result = model.test_on_batch(image_batch, label_batch,
# 返回累计的指标
reset_metrics=False)
metrics_names = model.metrics_names
print("\neval: ",
"{}: {:.3f}".format(metrics_names[0], result[0]),
"{}: {:.3f}".format(metrics_names[1], result[1]))
自定义训练步骤
如果想更加灵活,你可以自己实现训练步骤。这需要三步:
- 迭代Python的generator或者tf.data.Dataset来得到一个batch的训练数据
- 使用tf.GradientTape计算梯度
- 使用tf.keras.optimizers包下的某个Optimizer更新模型参数
注意:
- 在调用Layer或者Model的时候(call),一定要加一个training参数区分是训练阶段还是预测阶段
- 正确的设置训练的参数
- 模型的参数可能会在第一次call时创建
- 你需要自己除了正则化loss等
和1.X相比:
- 你不需要运行变量初始化的operator。变量在创建的时候就初始化好了。
- 你不需要手动管理依赖关系。即使是tf.function也和Eager模式一样按照定义的顺序执行。
model = tf.keras.Sequential([
tf.keras.layers.Conv2D(32, 3, activation='relu',
kernel_regularizer=tf.keras.regularizers.l2(0.02),
input_shape=(28, 28, 1)),
tf.keras.layers.MaxPooling2D(),
tf.keras.layers.Flatten(),
tf.keras.layers.Dropout(0.1),
tf.keras.layers.Dense(64, activation='relu'),
tf.keras.layers.BatchNormalization(),
tf.keras.layers.Dense(10)
])
optimizer = tf.keras.optimizers.Adam(0.001)
loss_fn = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)
@tf.function
def train_step(inputs, labels):
with tf.GradientTape() as tape:
predictions = model(inputs, training=True)
regularization_loss=tf.math.add_n(model.losses)
pred_loss=loss_fn(labels, predictions)
total_loss=pred_loss + regularization_loss
gradients = tape.gradient(total_loss, model.trainable_variables)
optimizer.apply_gradients(zip(gradients, model.trainable_variables))
for epoch in range(NUM_EPOCHS):
for inputs, labels in train_data:
train_step(inputs, labels)
print("Finished epoch", epoch)
tf.GradientTape()提供一个context,我们需要在这里进行前向计算loss。它会记录下这个context的变量,从而计算梯度的时候知道应该怎么做。然后用tf.gradient函数计算loss对参数的梯度,最后用optimizer.apply_gradient更新参数。
新风格的metric和loss
在TensorFlow 2.X里,metric和loss都是对象。他们在Eager和tf.function两种模式下都可以使用。一个loss对象是一个callable,调用时需要y_true和y_pred两个参数:
cce = tf.keras.losses.CategoricalCrossentropy(from_logits=True)
cce([[1, 0]], [[-1.0,3.0]]).numpy()
4.01815
一个metric对象有如下方法:
- Metric.update_state() — 用新的观察数据更新统计状态
- Metric.result() — 获得到目前位置观察数据的最新统计结果
- Metric.reset_states() — 清除状态,抛弃之前的观察数据重新开始统计。
Metric对象也是callable。使用update_state方法传入新的观察后会返回最新的结果(相对于先调用update_state更新状态然后再调用result返回结果)。我们不需要收到初始化Metric,因为TensorFlow 2.X是自动处理依赖控制。
下面的代码使用Metric来在自定义的训练循环中跟踪平均loss。
# Create the metrics
loss_metric = tf.keras.metrics.Mean(name='train_loss')
accuracy_metric = tf.keras.metrics.SparseCategoricalAccuracy(name='train_accuracy')
@tf.function
def train_step(inputs, labels):
with tf.GradientTape() as tape:
predictions = model(inputs, training=True)
regularization_loss=tf.math.add_n(model.losses)
pred_loss=loss_fn(labels, predictions)
total_loss=pred_loss + regularization_loss
gradients = tape.gradient(total_loss, model.trainable_variables)
optimizer.apply_gradients(zip(gradients, model.trainable_variables))
# Update the metrics
loss_metric.update_state(total_loss)
accuracy_metric.update_state(labels, predictions)
for epoch in range(NUM_EPOCHS):
# Reset the metrics
loss_metric.reset_states()
accuracy_metric.reset_states()
for inputs, labels in train_data:
train_step(inputs, labels)
# Get the metric results
mean_loss=loss_metric.result()
mean_accuracy = accuracy_metric.result()
print('Epoch: ', epoch)
print(' loss: {:.3f}'.format(mean_loss))
print(' accuracy: {:.3f}'.format(mean_accuracy))
下面是执行结果:
Epoch: 0
loss: 0.207
accuracy: 0.991
Epoch: 1
loss: 0.167
accuracy: 0.994
Epoch: 2
loss: 0.147
accuracy: 0.997
Epoch: 3
loss: 0.123
accuracy: 0.997
Epoch: 4
loss: 0.109
accuracy: 0.997
Keras的命名
在TensorFlow 2.X里,Keras的模型里关于Metric的命名更加一致。
调用compile会传入metrics参数一个列表,如果这个列表里是字符串,那么这个字符串会被用作metric的名字。这个名字在model.fit返回的history对象里可见,并且在keras.callback回传的logs里也能用这个名字。
model.compile(
optimizer = tf.keras.optimizers.Adam(0.001),
loss = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
metrics = ['acc', 'accuracy', tf.keras.metrics.SparseCategoricalAccuracy(name="my_accuracy")])
history = model.fit(train_data)
上面的代码会输出类似下面的log,这里的Metric的名字就是传入的字符串:
5/5 [==============================] - 0s 6ms/step - loss: 0.1233 - acc: 0.9937 - accuracy: 0.9937 - my_accuracy: 0.9937
我们也可以在history里看它们:
print(history.history.keys())
输出:
dict_keys(['loss', 'acc', 'accuracy', 'my_accuracy'])
如果是1.X的版本,则我们传入accuracy,它也会变成acc。也就是说不管我们传入accuracy还是acc,它都知道我们要的是SparseCategoricalAccuracy对象,但是名字都会变成acc,而在2.X里我们传入什么它就叫什么。
Keras的Optimizer
v1.train里的优化器比如v1.train.AdamOptimizer以及v1.train.GradientDescentOptimizer在tf.keras.optimizers里都有等价的实现。
把v1.train用keras.optimizers改写
下面是我们在改写optimizer时需要注意的事项:
- 更新optimizer可能无法使用老的checkpoint
- epsilon的默认值从1e-8变成了1e-7(大部分情况下影响很小)
- v1.train.GradientDescentOptimizer可以直接用tf.keras.optimizers.SGD替代
- v1.train.MomentumOptimizer可以用tf.keras.optimizers.SGD(…, momentum=…)替代
- v1.train.AdamOptimizer可以替换成tf.keras.optimizers.Adam,参数beta1和beta2需要改成beta_1和beta_2
- v1.train.RMSPropOptimizer可以替换成tf.keras.optimizers.RMSprop。参数decay需要改成rho
- v1.train.AdadeltaOptimizer可以直接替换成tf.keras.optimizers.Adadelta
- tf.train.AdagradOptimizer可以直接替换成tf.keras.optimizers.Adagrad
- tf.train.FtrlOptimizer可以直接替换成tf.keras.optimizers.Ftrl。参数accum_name和linear_name不再需要了。
- The tf.contrib.AdamaxOptimizer和tf.contrib.NadamOptimizer可以直接替换成tf.keras.optimizers.Adamax和tf.keras.optimizers.Nadam。但是参数beta1和beta2需要改成beta_1和beta_2
某些tf.keras.optimizer默认值的变化
optimizers.SGD、optimizers.Adam和optimizers.RMSprop的默认值没有变化。
下面是默认learning rate变化的类:
- optimizers.Adagrad从0.01变成了0.001
- optimizers.Adadelta从1.0变成了0.001
- optimizers.Adamax从0.002变成了0.001
- optimizers.Nadam从0.002变成了0.001
TensorBoard
TensorFlow 2.X在TensorBoard里摘要数据的tf.summary API进行了大量的重构。对于新版的教程可以参考这里。关于从TensorFlow 1.X向2.X迁移可以参考这里。
保存和加载
checkpoint兼容性
TensorFlow 2.X使用面向对象的checkpoint。
如果你足够小心的话,原来的基于名字的checkpoint还是可以被加载进来。代码转换过程可能导致变量名字的变化,不过有一些变通的解决方法。
最简单的方法是让新模型的名字和checkpoint能对上:
- 变量仍然可以设置名字。
- Keras模型也可以接受名字参数,这个名字会作为里面变量的前缀。
- v1.name_scope函数可以用来设置变量名的前缀。它和tf.variable_scope不同。它只影响变量的命名,不涉及变量的重用。
如果上面的方法还是无法让你加载老的checkpoint,那么试试v1.train.init_from_checkpoint函数。它有一个assignment_map的参数,可以指定新老名称的映射关系。
注意:面向对象的checkpoint可能会延迟加载,而基于名字的checkpoint需要变量都初始化好了才行。某些模型会在调用build或者传入第一个batch数据时才真正创建变量。
TensorFlow Estimator的repo包括一个转换工具来把1.X的Estimator的checkpoint升级成新版本的checkpoint。它可以作为一个怎么实现转换工具的参考示例。
saved_model兼容性
saved_model在兼容性方面并没有太多的变化:
- saved_model在TensorFlow 2.X里仍然可以工作
- TensorFlow 2.X通过saved_model保存的模型也可以被1.X加载——前提是这些op是1.X就有的而不是2.X新加的。
Graph.pb和Graph.pbtxt
没有直接的办法能够把一个Graph.pb文件直接转换成2.X能用的工具。你只能修改代码用2.X的方式保存模型。
但是如果你有一个冻结的(Frozen)图(一个tf.Graph的所有变量都转换成了常量,因此就无法训练),那就可以通过v1.wrap_function把它转换成一个concrete_function:
def wrap_frozen_graph(graph_def, inputs, outputs):
def _imports_graph_def():
tf.compat.v1.import_graph_def(graph_def, name="")
wrapped_import = tf.compat.v1.wrap_function(_imports_graph_def, [])
import_graph = wrapped_import.graph
return wrapped_import.prune(
tf.nest.map_structure(import_graph.as_graph_element, inputs),
tf.nest.map_structure(import_graph.as_graph_element, outputs))
比如下面是2016年Inception v1的一个冻结图:
path = tf.keras.utils.get_file(
'inception_v1_2016_08_28_frozen.pb',
'http://storage.googleapis.com/download.tensorflow.org/models/inception_v1_2016_08_28_frozen.pb.tar.gz',
untar=True)
加载这个tf.GraphDef:
graph_def = tf.compat.v1.GraphDef()
loaded = graph_def.ParseFromString(open(path,'rb').read())
把它封装成concrete_function:
inception_func = wrap_frozen_graph(
graph_def, inputs='input:0',
outputs='InceptionV1/InceptionV1/Mixed_3b/Branch_1/Conv2d_0a_1x1/Relu:0')
接下来就可以进行预测:
input_img = tf.ones([1,224,224,3], dtype=tf.float32)
inception_func(input_img).shape
TensorShape([1, 28, 28, 96])
Estimator
Estimator的训练
TensorFlow 2.X还会支持Estimator。(注:作者感觉迟早要放弃)
如果用Estimator,我们还是像1.X一样使用input_fn(),tf.estimator.TrainSpec和tf.estimator.EvalSpec等类。
下面是使用input_fn配合训练和验证的spec的例子。 首先是创建input_fn和训练/验证的Spec:
# Define the estimator's input_fn
def input_fn():
datasets, info = tfds.load(name='mnist', with_info=True, as_supervised=True)
mnist_train, mnist_test = datasets['train'], datasets['test']
BUFFER_SIZE = 10000
BATCH_SIZE = 64
def scale(image, label):
image = tf.cast(image, tf.float32)
image /= 255
return image, label[..., tf.newaxis]
train_data = mnist_train.map(scale).shuffle(BUFFER_SIZE).batch(BATCH_SIZE)
return train_data.repeat()
# Define train & eval specs
train_spec = tf.estimator.TrainSpec(input_fn=input_fn,
max_steps=STEPS_PER_EPOCH * NUM_EPOCHS)
eval_spec = tf.estimator.EvalSpec(input_fn=input_fn,
steps=STEPS_PER_EPOCH)
使用Keras定义模型然后转换成Estimator
在TensorFlow 2.X里构造Estimator的方法稍有不同。我们建议使用Keras来构建模型,然后使用tf.keras.estimator.model_to_estimator来把Keras模型转换成Estimator。读者可能会问:既然我都用Keras构建模型了,干嘛不直接用Keras的fit来训练和预测,还折腾成Estimator干什么呢?其实主要的原因就是有些功能只有Estimator有而Keras还没有实现。所以等哪天Keras把这些都实现了,Estimator也该寿终正寝了。
下面的代码展示了怎么利用model_to_estimator来实现Estimator的训练:
def make_model():
return tf.keras.Sequential([
tf.keras.layers.Conv2D(32, 3, activation='relu',
kernel_regularizer=tf.keras.regularizers.l2(0.02),
input_shape=(28, 28, 1)),
tf.keras.layers.MaxPooling2D(),
tf.keras.layers.Flatten(),
tf.keras.layers.Dropout(0.1),
tf.keras.layers.Dense(64, activation='relu'),
tf.keras.layers.BatchNormalization(),
tf.keras.layers.Dense(10)
])
model = make_model()
model.compile(optimizer='adam',
loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
metrics=['accuracy'])
estimator = tf.keras.estimator.model_to_estimator(
keras_model = model
)
tf.estimator.train_and_evaluate(estimator, train_spec, eval_spec)
注意:目前不支持在Keras里创建加权的(weighted)metric然后用model_to_estimator转换,你必须在Estimator的Spec里直接创建这些Metric。
使用model_fn
如果你有一个自定义的model_fn,你可以用Keras的API重写。然而为了兼容,自定义的model_fn会仍然以1.X的计算图的模式执行。这就意味着没有Eager执行,也没有自动的依赖控制。
注意:从长远来看,你应该尽量避免使用tf.estimator尤其是自定义的model_fn。可以替代的方案是tf.keras和tf.distribute。如果由于某些原因让你一定要用Estimator,那也最好用Keras构造Model,然后用tf.keras.estimator.model_to_estimator把它转换成Estimator。
尽量少的修改自定义model_fn的方法
为了让你自定义的model_fn能够在2.X里运行,如果你不想大动干戈,tf.compat.v1可以做到尽可能的兼容。 在自定义的model_fn里使用Keras模型的方法和自定义训练循环里很类似:
- 根据mode参数设置合适的训练阶段
- 显式的把模型的可训练参数传递给Optimizer
但是也有很重要的区别:
- 不能用Model.losses,必须用Model.get_losses_for
- 需要使用Model.get_updates_for来得到模型参数的更新(梯度)
注意:更新是每次batch后应用到模型的修改。这不仅仅模型的参数更新,还包括layers.BatchNormalization里的均值和方程的滑动平均(这并不是传统意义上的模型的参数)。
下面的代码展示了怎么在自定义的model_fn里创建Estmator:
def my_model_fn(features, labels, mode):
model = make_model()
optimizer = tf.compat.v1.train.AdamOptimizer()
loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)
training = (mode == tf.estimator.ModeKeys.TRAIN)
predictions = model(features, training=training)
if mode == tf.estimator.ModeKeys.PREDICT:
return tf.estimator.EstimatorSpec(mode=mode, predictions=predictions)
reg_losses = model.get_losses_for(None) + model.get_losses_for(features)
total_loss=loss_fn(labels, predictions) + tf.math.add_n(reg_losses)
accuracy = tf.compat.v1.metrics.accuracy(labels=labels,
predictions=tf.math.argmax(predictions, axis=1),
name='acc_op')
update_ops = model.get_updates_for(None) + model.get_updates_for(features)
minimize_op = optimizer.minimize(
total_loss,
var_list=model.trainable_variables,
global_step=tf.compat.v1.train.get_or_create_global_step())
train_op = tf.group(minimize_op, update_ops)
return tf.estimator.EstimatorSpec(
mode=mode,
predictions=predictions,
loss=total_loss,
train_op=train_op, eval_metric_ops={'accuracy': accuracy})
# Create the Estimator & Train
estimator = tf.estimator.Estimator(model_fn=my_model_fn)
tf.estimator.train_and_evaluate(estimator, train_spec, eval_spec)
使用2.X来自定义model_fn
如果逆向想完全抛弃1.X而用2.X来实现自定义的model_fn,则你需要更新optimizer和metric为tf.keras.optimizers和tf.keras.metrics。
对于自定义model_fn,除了上述的变化,还有一些需要升级的地方:
- 使用tf.kears.optimizers替代v1.train.Optimizer
- 显示的把模型的可训练变量传给tf.keras.optimizers
- 为了计算train_op/minimize_op:
- 如果loss是一个标量,使用Optimizer.get_updates()。返回list的第一个元素就是需要的train_op/minimize_op
- 如果loss是一个callable,使用Optimizer.minimize()来获得train_op/minimize_op
- 对于验证,使用tf.keras.metrics替代tf.compat.v1.metrics
比如上述例子里的my_model_fn,下面是迁移到2.X的方法:
def my_model_fn(features, labels, mode):
model = make_model()
training = (mode == tf.estimator.ModeKeys.TRAIN)
loss_obj = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)
predictions = model(features, training=training)
# 得到无条件的(unconditional)losses
# 得到和输入(features)相关的losses
reg_losses = model.get_losses_for(None) + model.get_losses_for(features)
total_loss=loss_obj(labels, predictions) + tf.math.add_n(reg_losses)
# 升级到tf.keras.metrics.
accuracy_obj = tf.keras.metrics.Accuracy(name='acc_obj')
accuracy = accuracy_obj.update_state(
y_true=labels, y_pred=tf.math.argmax(predictions, axis=1))
train_op = None
if training:
# 升级到tf.keras.optimizers.
optimizer = tf.keras.optimizers.Adam()
# 手动设置optimizer.iterations为tf.compat.v1.global_step
# 这个复制是必须的,因为tf.train.SessionRunHook等依赖与global step
optimizer.iterations = tf.compat.v1.train.get_or_create_global_step()
# 得到无条件的更新和输入相关的更新
update_ops = model.get_updates_for(None) + model.get_updates_for(features)
# 计算minimize_op.
minimize_op = optimizer.get_updates(
total_loss,
model.trainable_variables)[0]
train_op = tf.group(minimize_op, *update_ops)
return tf.estimator.EstimatorSpec(
mode=mode,
predictions=predictions,
loss=total_loss,
train_op=train_op,
eval_metric_ops={'Accuracy': accuracy_obj})
# Create the Estimator & Train.
estimator = tf.estimator.Estimator(model_fn=my_model_fn)
tf.estimator.train_and_evaluate(estimator, train_spec, eval_spec)
预定义Estimator
Premade Estimators in the family of tf.estimator.DNN, tf.estimator.Linear and tf.estimator.DNNLinearCombined* are still supported in the TensorFlow 2.0 API, however, some arguments have changed:
预定义Estimator家族中的tf.estimator.DNN、tf.estimator.Linear和tf.estimator.DNNLinearCombined*在TensorFlow 2.X里依然可以使用,但是有一些增强:
- input_layer_partitioner:在2.X里被移除
-
loss_reduction:把tf.compat.v1.losses.Reduction升级为tf.keras.losses.Reduction。它的默认值也从tf.compat.v1.losses.Reduction.SUM变成了tf.keras.losses.Reduction.SUM_OVER_BATCH_SIZE
- optimizer、dnn_optimizer和linear_optimizer: this参数从tf.compat.v1.train.Optimizer改为tf.keras.optimizers
为了适应上述改变:
- input_layer_partitioner不需要任何改变,因为2.X里的Distribution Strategy会自动处理
- 对于Floss_reduction,请查看tf.keras.losses.Reduction来获取帮助
- 对于optimizer参数,如果你没有传入optimizer、dnn_optimizer或者linear_optimizer arg,或者你是使用字符串的optimizer名,那么不需要任何改变。否则你需要把tf.compat.v1.train.Optimizer改成tf.keras.optimizers里对应的对象。
Checkpoint Converter
升级到keras.optimizer后就不能读取原来1.X生成的checkpoint,因为tf.keras.optimizers生成的变量和原来格式不同。为了让老的checkpoint可用,可用尝试下面的checkpoint转换工具:
curl -O https://raw.githubusercontent.com/tensorflow/estimator/master/tensorflow_estimator/python/estimator/tools/checkpoint_converter.py
这个工具的帮助如下:
python checkpoint_converter.py -h
2020-10-15 01:27:47.423752: I tensorflow/stream_executor/platform/default/dso_loader.cc:48] Successfully opened dynamic library libcudart.so.10.1
usage: checkpoint_converter.py [-h]
{dnn,linear,combined} source_checkpoint
source_graph target_checkpoint
positional arguments:
{dnn,linear,combined}
The type of estimator to be converted. So far, the
checkpoint converter only supports Canned Estimator.
So the allowed types include linear, dnn and combined.
source_checkpoint Path to source checkpoint file to be read in.
source_graph Path to source graph file to be read in.
target_checkpoint Path to checkpoint file to be written out.
optional arguments:
-h, --help show this help message and exit
TensorShape
这个类只是简单的保存整数,而不是原来的tf.compat.v1.Dimension对象。这样就不需要再调用.value()来得到整数值了。
下面的代码展示了TensorFlow 1.X和2.X的差别。
# Create a shape and choose an index
i = 0
shape = tf.TensorShape([16, None, 256])
shape
TensorShape([16, None, 256])
如果是是1.X,需要这样:
value = shape[i].value
而2.X变得简单:
value = shape[i]
如果你有类似下面的1.X的代码:
for dim in shape:
value = dim.value
print(value)
那么2.X可以这样写:
for value in shape:
print(value)
这是1.X的写法:
dim = shape[i]
dim.assert_is_compatible_with(other_dim)
这是2.X的写法:
other_dim = 16
Dimension = tf.compat.v1.Dimension
if shape.rank is None:
dim = Dimension(None)
else:
dim = shape.dims[i]
dim.is_compatible_with(other_dim) # or any other dimension method
shape = tf.TensorShape(None)
if shape:
dim = shape.dims[i]
dim.is_compatible_with(other_dim) # or any other dimension method
tf.TensorShape转换成bool的规则:如果rank已知则为True,否则False
print(bool(tf.TensorShape([]))) # Scalar
print(bool(tf.TensorShape([0]))) # 0-length vector
print(bool(tf.TensorShape([1]))) # 1-length vector
print(bool(tf.TensorShape([None]))) # Unknown-length vector
print(bool(tf.TensorShape([1, 10, 100]))) # 3D tensor
print(bool(tf.TensorShape([None, None, None]))) # 3D tensor with no known dimensions
print()
print(bool(tf.TensorShape(None))) # A tensor with unknown rank.
True
True
True
True
True
True
False
其它更改
- 移除了tf.colocate_with:TensorFlow的设备放置(device placement)算法有了很大的提升,不再需要了。
- 把v1.ConfigProto替换成tf.config里的类
结论
升级过程为:
- 运行升级脚本
- 改写contrib包里的API
- 把模型定义用Keras实现
- 使用tf.keras或者tf.estimator来进行训练和验证
- 或者使用自定义的循环,但是避免session和collection
要升级到2.X需要一些工作,但是好处是:
- 代码更少
- 简单明了
- 调试更容易
- 显示Disqus评论(需要科学上网)