使用TensorFlow 自定义模型和训练
TensorFlow 速览
TensorFlow 是一个强大的数值计算库,特别适合做和微调大规模机器学习(但也可以用来做其它的重型计算)。TensorFlow 是谷歌大脑团队开发的,支持了谷歌的许多大规模服务,包括谷歌云对话、谷歌图片和谷歌搜索。TensorFlow 是 2015 年 11 月开源的,(按文章引用、公司采用、GitHub 星数)是目前最流行的深度学习库。无数的项目是用 TensorFlow 来做各种机器学习任务,包括图片分类、自然语言处理、推荐系统和时间序列预测。
TensorFlow 提供的功能如下:
-
TensorFlow 的核心与 NumPy 很像,但 TensorFlow 支持 GPU ; -
TensorFlow 支持分布式计算; -
TensorFlow 使用了即时JIT 编译器对计算速度和内存使用优化。编译器的工作是从 Python 函数提取出计算图,然后对计算图优化(比如剪切无用的节点),最后高效运行(比如自动并行运行独立任务); -
计算图可以导出为迁移形式,因此可以在一个环境中训练一个 TensorFlow 模型(比如使用 Python 或 Linux),然后在另一个环境中运行(比如在安卓设备上用 Java 运行); -
TensorFlow 实现了自动微分,并提供了一些高效的优化器,比如 RMSProp 和 NAdam ,因此可以容易的最小化各种损失函数。 -
TensorFlow 还提供了许多其他功能:最重要的是 tf.keras ,还有数据加载和预处理操作(tf.data ,tf.io 等等),图片处理操作(tf.image ),信号处理操作(tf.signal ),等等(如图:总结了 TensorFlow 的 Python API ) -
-
TensorFlow 的底层都是用高效的 C++实现的。许多操作有多个实现,称为核:每个核对应一个具体的设备型号,比如 CPU、GPU 甚至 TPU (张量处理单元)。GPU 通过将任务分成小块,在多个 GPU 线程中并行运行,可以极大提高提高计算的速度。TPU 更快:TPU 是自定义的 ASIC 芯片,专门用来做深度学习运算的。 -
TensorFlow 的架构见图 -
-
TensorFlow 不仅可以运行在 Windows、Linux 和 macOS 上,也可以运行在移动设备上(使用 TensorFlow Lite ),包括 iOS 和安卓。如果不想使用 Python API ,还可以使用 C++、Java、Go 和 Swift 的 API 。甚至还有 JavaScript 的实现 TensorFlow.js ,它可以直接在浏览器中运行。 -
TensorFlow 处于一套可扩展的生态系统库的核心位置。
- 首先,
TensorBoard 可以用来可视化。 - 其次,
TensorFlow Extended(TFX) ,是谷歌推出的用来生产化的库,包括:数据确认、预处理、模型分析和服务(使用 TF Serving )。 - 谷歌的
TensorFlow Hub 上可以方便下载和复用预训练好的神经网络。 - 可以从
TensorFlow 的 model garden (https://github.com/tensorflow/models/)获取许多神经网络架构,其中一些是预训练好的。 TensorFlow Resources 和 https://github.com/jtoy/awesome-tensorflow上有更多的资源。你可以在 GitHub 上找到数百个 TensorFlow 项目,无论干什么都可以方便地找到现成的代码。- 提示:越来越多的 ML 论文都附带了实现过程,一些甚至带有预训练模型。可以在https://paperswithcode.com/找到。
- 最后,
TensorFlow 有一支热忱满满的开发者团队,也有庞大的社区。要是想问技术问题,可以去http://stackoverflow.com/,问题上打上 tensorflow 和 python 标签。还可以在GitHub 上提 bug 和新功能。
像 NumPy 一样使用 TensorFlow
张量和运算
使用tf.constant() 创建张量
t=tf.constant([[1., 2., 3.], [4., 5., 6.]])
s=tf.constant(42)
t.shape
t.dtype
t + 10
tf.square(t)
t @ tf.transpose(t)
可以在 TensorFlow 中找到所有基本的数学运算(tf.add()、tf.multiply()、tf.square()、tf.exp()、tf.sqrt() ),以及 NumPy 中的大部分运算(比如 tf.reshape()、tf.squeeze()、tf.tile() )。
一些 TensorFlow 中的函数与 NumPy 中不同,例如,tf.reduce_mean()、tf.reduce_sum()、tf.reduce_max()、tf.math.log() 等同于 np.mean()、np.sum()、np.max()和np.log() 。
当函数名不同时,通常都是有原因的。例如,TensorFlow 中必须使用tf.transpose(t) ,不能像 NumPy 中那样使用t.T 。原因是函数tf.transpose(t) 所做的和NumPy 的属性T 并不完全相同:在 TensorFlow 中,是使用转置数据的复制来生成张量的,而在NumPy 中,t.T 是数据的转置视图。
相似的,tf.reduce_sum() 操作之所以这么命名,是因为它的 GPU 核(即 GPU 实现)所采用的 reduce 算法不能保证元素相加的顺序,因为 32 位的浮点数精度有限,每次调用的结果可能会有细微的不同。tf.reduce_mean() 也是这样(tf.reduce_max() 结果是确定的)。
许多函数和类都有假名。比如,tf.add() 和 tf.math.add() 是相同的。这可以让 TensorFlow 对于最常用的操作有简洁的名字,同时包可以有序安置。
Keras 的低级 API Keras API 有自己的低级 API ,位于keras.backend ,包括:函数square()、exp()、sqrt() 。在tf.keras 中,这些函数通常通常只是调用对应的TensorFlow 操作。如果你想写一些可以迁移到其它 Keras 实现上,就应该使用这些 Keras 函数。但是这些函数不多,
from tensorflow import keras
K = keras.backend
K.square(K.transpose(t)) + 10
张量和 NumPy
张量和 NumPy 融合地非常好:使用 NumPy 数组可以创建张量,张量也可以创建 NumPy 数组。可以在 NumPy 数组上运行 TensorFlow 运算,也可以在张量上运行NumPy 运算
a = np.array([2., 4., 5.])
t=tf.constant(a)
st =tf.square(a)
t.numpy()
n=np.array(t)
sn=np.square(t)
类型转换
类型转换对性能的影响非常大,并且如果类型转换是自动完成的,不容易被注意到。为了避免这样,TensorFlow 不会自动做任何类型转换:只是如果用不兼容的类型执行了张量运算,TensorFlow 就会报异常。例如,不能用浮点型张量与整数型张量相加,也不能将 32 位张量与 64 位张量相加:
try:
tf.constant(2.0) + tf.constant(40)
except tf.errors.InvalidArgumentError as ex:
print(ex)
try:
tf.constant(2.0) + tf.constant(40., dtype=tf.float64)
except tf.errors.InvalidArgumentError as ex:
print(ex)
t2 = tf.constant(40., dtype=tf.float64)
tf.constant(2.0) + tf.cast(t2, tf.float32)
使用tf.Variable() 创建可修改的张量:变量
v = tf.Variable([[1., 2., 3.], [4., 5., 6.]])
v.assign(2 * v)
v[0, 1].assign(42)
v[:, 2].assign([0., 1.])
v.scatter_nd_update(indices=[[0, 0], [1, 2]],
updates=[100., 200.])
自定义模型和训练算法
自定义损失函数
举例:
训练一个回归模型,但训练集有噪音::
- 通过清除或修正异常值来清理数据集
- 数据集还是有噪音,该用什么损失函数呢?
- 均方差可能对大误差惩罚过重,导致模型不准确。
- 均绝对值误差不会对异常值惩罚过重,但训练可能要很长时间才能收敛,训练模型也可能不准确。
此时使用 Huber 损失就比 MSE 好多了,只需创建一个函数,参数是标签和预测值,使用 TensorFlow 运算计算每个实例的损失
def huber_fn(y_true, y_pred):
error = y_true - y_pred
is_small_error = tf.abs(error) < 1
squared_loss = tf.square(error) / 2
linear_loss = tf.abs(error) - 0.5
return tf.where(is_small_error, squared_loss, linear_loss)
保存、加载包含自定义组件的模型
input_shape = X_train.shape[1:]
model = keras.models.Sequential([
keras.layers.Dense(30, activation="selu", kernel_initializer="lecun_normal",
input_shape=input_shape),
keras.layers.Dense(1),
])
model.compile(loss=huber_fn, optimizer="nadam", metrics=["mae"])
model.fit(X_train_scaled, y_train, epochs=20,
validation_data=(X_valid_scaled, y_valid))
model.save("my_model_with_a_custom_loss.h5")
model = keras.models.load_model("my_model_with_a_custom_loss.h5",
custom_objects={"huber_fn": huber_fn})
自定义激活函数、初始化器、正则器和约束
Keras 的大多数功能,比如损失、正则器、约束、初始化器、指标、激活函数、层,甚至是完整的模型,都可以用相同或相似的方式方法做自定义。
实例代码:
def my_softplus(z):
return tf.math.log(tf.exp(z) + 1.0)
def my_glorot_initializer(shape, dtype=tf.float32):
stddev = tf.sqrt(2. / (shape[0] + shape[1]))
return tf.random.normal(shape, stddev=stddev, dtype=dtype)
def my_l1_regularizer(weights):
return tf.reduce_sum(tf.abs(0.01 * weights))
def my_positive_weights(weights):
return tf.where(weights < 0., tf.zeros_like(weights), weights)
如下图,softplus 可以看作是ReLu 的平滑。根据神经科学家的相关研究,softplus 和 ReLu 与脑神经元激活频率函数有神似的地方。也就是说,相比于早期的激活函数,softplus 和 ReLu 更加接近脑神经元的激活模型,而神经网络正是基于脑神经科学发展而来,这两个激活函数的应用促成了神经网络研究的新浪潮。
model = keras.models.Sequential([
keras.layers.Dense(30, activation="selu", kernel_initializer="lecun_normal",
input_shape=input_shape),
keras.layers.Dense(1, activation=my_softplus,
kernel_regularizer=my_l1_regularizer,
kernel_constraint=my_positive_weights,
kernel_initializer=my_glorot_initializer),
])
model.compile(loss="mse", optimizer="nadam", metrics=["mae"])
model.fit(X_train_scaled, y_train, epochs=2,
validation_data=(X_valid_scaled, y_valid))
此例:
- 激活函数会应用到这个Dense层的输出上,结果会传递到下一层。
- 层的权重会使用初始化器的返回值。
- 在每个训练步骤中,权重会传递给正则化函数以计算正则化损失,这个损失会与主损失相加,得到训练的最终损失。
- 最后,会在每个训练步骤结束后调用约束函数,经过约束的权重会替换层的权重。
自定义指标
损失和指标的概念不同:
梯度下降使用损失(比如交叉熵损失)来训练模型,因此损失必须是可微分的(至少是在评估点可微分),梯度不能在所有地方都是 0。另外,就算损失比较难解释也没有关系。
指标(比如准确率)是用来评估模型的:指标的解释性一定要好,可以是不可微分的,或者可以在任何地方的梯度都是 0。但,在多数情况下,定义一个自定义指标函数和定义一个自定义损失函数是完全一样的。
示例代码:
def create_huber(threshold=1.0):
def huber_fn(y_true, y_pred):
error = y_true - y_pred
is_small_error = tf.abs(error) < threshold
squared_loss = tf.square(error) / 2
linear_loss = threshold * tf.abs(error) - threshold**2 / 2
return tf.where(is_small_error, squared_loss, linear_loss)
return huber_fn
model = keras.models.Sequential([
keras.layers.Dense(30, activation="selu", kernel_initializer="lecun_normal",
input_shape=input_shape),
keras.layers.Dense(1),
])
model.compile(loss="mse", optimizer="nadam", metrics=[create_huber(2.0)])
model.fit(X_train_scaled, y_train, epochs=2)
自定义层
如果模型的层顺序是 A、B、C、A、B、C、A、B、C,则完全可以创建一个包含 A、B、C 的自定义层 D,模型就可以简化为 D、D、D。
创建没有权重的自定义层:无状态层
最简单的方法是编写一个函数,将其包装进keras.layers.Lambda 层
实例代码:
exponential_layer = keras.layers.Lambda(lambda x: tf.exp(x))
model = keras.models.Sequential([
keras.layers.Dense(30, activation="relu", input_shape=input_shape),
keras.layers.Dense(1),
exponential_layer
])
model.compile(loss="mse", optimizer="sgd")
model.fit(X_train_scaled, y_train, epochs=5,
validation_data=(X_valid_scaled, y_valid))
model.evaluate(X_test_scaled, y_test)
要创建自定义状态层(有权重的层)
需要创建 keras.layers.Layer 类的子类。
class MyDense(keras.layers.Layer):
def __init__(self, units, activation=None, **kwargs):
super().__init__(**kwargs)
self.units = units
self.activation = keras.activations.get(activation)
def build(self, batch_input_shape):
self.kernel = self.add_weight(
name="kernel", shape=[batch_input_shape[-1], self.units],
initializer="glorot_normal")
self.bias = self.add_weight(
name="bias", shape=[self.units], initializer="zeros")
super().build(batch_input_shape)
def call(self, X):
return self.activation(X @ self.kernel + self.bias)
def compute_output_shape(self, batch_input_shape):
return tf.TensorShape(batch_input_shape.as_list()[:-1] + [self.units])
def get_config(self):
base_config = super().get_config()
return {**base_config, "units": self.units,
"activation": keras.activations.serialize(self.activation)}
model = keras.models.Sequential([
MyDense(30, activation="relu", input_shape=input_shape),
MyDense(1)
])
model.compile(loss="mse", optimizer="nadam")
model.fit(X_train_scaled, y_train, epochs=2,
validation_data=(X_valid_scaled, y_valid))
model.evaluate(X_test_scaled, y_test)
此例作为示例:定义了全连接层的简化版本。
自定义模型
创建keras.Model 类的子类,创建层和变量,用call() 方法完成模型想做的任何事
class ResidualBlock(keras.layers.Layer):
def __init__(self, n_layers, n_neurons, **kwargs):
super().__init__(**kwargs)
self.hidden = [keras.layers.Dense(n_neurons, activation="elu",
kernel_initializer="he_normal")
for _ in range(n_layers)]
def call(self, inputs):
Z = inputs
for layer in self.hidden:
Z = layer(Z)
return inputs + Z
class ResidualRegressor(keras.models.Model):
def __init__(self, output_dim, **kwargs):
super().__init__(**kwargs)
self.hidden1 = keras.layers.Dense(30, activation="elu",
kernel_initializer="he_normal")
self.block1 = ResidualBlock(2, 30)
self.block2 = ResidualBlock(2, 30)
self.out = keras.layers.Dense(output_dim)
def call(self, inputs):
Z = self.hidden1(inputs)
for _ in range(1 + 3):
Z = self.block1(Z)
Z = self.block2(Z)
return self.out(Z)
此例仅为如上图的创建自定义模型的示例,
使用自动微分计算梯度
用程序求偏导
f
(
w
1
,
w
2
)
=
3
w
1
2
+
2
w
1
w
2
f(w_1,w_2)=3 w_1^2+2 w_1 w_2
f(w1?,w2?)=3w12?+2w1?w2?
def f(w1, w2):
return 3 * w1 ** 2 + 2 * w1 * w2
w1, w2 = 5, 3
eps = 1e-6
dw1=(f(w1 + eps, w2) - f(w1, w2)) / eps
dw2=(f(w1, w2 + eps) - f(w1, w2)) / eps
这种方法很容易实现,但只是大概。重要的是,需要对每个参数至少要调用一次
f
(
?
)
f(~)
f(?),对于大神经网络,就不怎么可控。所以,应该使用自动微分。TensorFlow 的实现很简单:不仅结果是正确的(准确度只受浮点误差限制)
w1, w2 = tf.Variable(5.), tf.Variable(3.)
with tf.GradientTape() as tape:
z = f(w1, w2)
gradients = tape.gradient(z, [w1, w2])
先定义了两个变量w1 和w2 ,然后创建了一个tf.GradientTape 上下文,它能自动记录变量的每个操作,最后使用它算出结果 z 关于两个变量 [w1, w2] 的梯度
TensorFlow 函数和图
TensorFlow 是如何生成计算图的呢?
首先分析了 Python 函数源码,找到所有的控制语句,比如: for,while,if ,break、continue、return 。这个第一步被称为自动图(AutoGraph )
然后,调用函数for_stmt() 会形成运算tf.while_loop() ,但没有向其传递参数,而是传递一个符号张量(symbolic tensor),如图,一个没有任何真实值的张量,只有名字、数据类型和形状。
最后的图是跟踪中生成的。节点表示运算,箭头表示张量。
TF 函数规则
大多数时候,将 Python 函数转换为 TF 函数:要用@tf.function 装饰,或让Keras 来处理,但有一些规则要遵守:
- 如果调用任何外部库,包括
NumPy ,甚至是标准库,调用只会在跟踪中运行,不会是图的一部分。 - 可以调用其它 Python 函数或
TF 函数,但是它们要遵守相同的规则,因为 TensorFlow 会在计算图中记录它们的运算。注意,其它函数不需要用@tf.function 装饰。 - 最好在
TF 函数的外部创建变量。如果你想将一个新值赋值给变量,要确保调用它的assign() 方法,而不是使用= 。 - Python 的源码应该可以被
TensorFlow 使用 TensorFlow 只能捕获在张量或数据集上迭代的 for 循环。因此要确保使用for i in tf.range(x) ,而不是for i in range(x) ,否则循环不能在图中捕获,反而会在追踪中运行。
==
|