Sklearn、TensorFlow 与 Keras 机器学习实用指南第三版(五)(1)

简介: Sklearn、TensorFlow 与 Keras 机器学习实用指南第三版(五)

第十二章:使用 TensorFlow 进行自定义模型和训练

到目前为止,我们只使用了 TensorFlow 的高级 API,Keras,但它已经让我们走得很远:我们构建了各种神经网络架构,包括回归和分类网络,Wide & Deep 网络,自正则化网络,使用各种技术,如批量归一化,dropout 和学习率调度。事实上,您将遇到的 95%用例不需要除了 Keras(和 tf.data)之外的任何东西(请参见第十三章)。但现在是时候深入研究 TensorFlow,看看它的低级Python API。当您需要额外控制以编写自定义损失函数,自定义指标,层,模型,初始化程序,正则化器,权重约束等时,这将非常有用。您甚至可能需要完全控制训练循环本身;例如,应用特殊的转换或约束到梯度(超出仅仅剪切它们)或为网络的不同部分使用多个优化器。我们将在本章中涵盖所有这些情况,并且还将看看如何使用 TensorFlow 的自动生成图功能来提升您的自定义模型和训练算法。但首先,让我们快速浏览一下 TensorFlow。

TensorFlow 的快速浏览

正如您所知,TensorFlow 是一个强大的用于数值计算的库,特别适用于大规模机器学习(但您也可以用它来进行需要大量计算的任何其他任务)。它由 Google Brain 团队开发,驱动了谷歌许多大规模服务,如 Google Cloud Speech,Google Photos 和 Google Search。它于 2015 年 11 月开源,现在是业界最广泛使用的深度学习库:无数项目使用 TensorFlow 进行各种机器学习任务,如图像分类,自然语言处理,推荐系统和时间序列预测。

那么 TensorFlow 提供了什么?以下是一个摘要:

  • 它的核心与 NumPy 非常相似,但支持 GPU。
  • 它支持分布式计算(跨多个设备和服务器)。
  • 它包括一种即时(JIT)编译器,允许它优化计算以提高速度和内存使用。它通过从 Python 函数中提取计算图,优化它(例如通过修剪未使用的节点),并有效地运行它(例如通过自动并行运行独立操作)来工作。
  • 计算图可以导出为可移植格式,因此您可以在一个环境中训练 TensorFlow 模型(例如在 Linux 上使用 Python),并在另一个环境中运行它(例如在 Android 设备上使用 Java)。
  • 它实现了反向模式自动微分(请参见第十章和附录 B)并提供了一些优秀的优化器,如 RMSProp 和 Nadam(请参见第十一章),因此您可以轻松最小化各种损失函数。

TensorFlow 提供了许多建立在这些核心功能之上的功能:最重要的当然是 Keras,但它还有数据加载和预处理操作(tf.data,tf.io 等),图像处理操作(tf.image),信号处理操作(tf.signal)等等(请参见图 12-1 以获取 TensorFlow 的 Python API 概述)。

提示

我们将涵盖 TensorFlow API 的许多包和函数,但不可能覆盖所有内容,因此您应该花些时间浏览 API;您会发现它非常丰富且有很好的文档。

在最低级别上,每个 TensorFlow 操作(简称 op)都是使用高效的 C++代码实现的。许多操作有多个称为内核的实现:每个内核专门用于特定设备类型,如 CPU、GPU,甚至 TPU(张量处理单元)。正如您可能知道的,GPU 可以通过将计算分成许多较小的块并在许多 GPU 线程上并行运行来显着加快计算速度。TPU 速度更快:它们是专门用于深度学习操作的定制 ASIC 芯片(我们将在第十九章讨论如何使用 GPU 或 TPU 与 TensorFlow)。


图 12-1. TensorFlow 的 Python API

TensorFlow 的架构如图 12-2 所示。大部分时间,您的代码将使用高级 API(特别是 Keras 和 tf.data),但当您需要更灵活性时,您将使用较低级别的 Python API,直接处理张量。无论如何,TensorFlow 的执行引擎将有效地运行操作,即使跨多个设备和机器,如果您告诉它的话。

TensorFlow 不仅可以在 Windows、Linux 和 macOS 上运行,还可以在移动设备上运行(使用 TensorFlow Lite),包括 iOS 和 Android(请参阅第十九章)。请注意,如果您不想使用 Python API,还可以使用其他语言的 API:有 C++、Java 和 Swift 的 API。甚至还有一个名为 TensorFlow.js 的 JavaScript 实现,可以直接在浏览器中运行您的模型。


图 12-2. TensorFlow 的架构

TensorFlow 不仅仅是一个库。TensorFlow 是一个庞大生态系统中心。首先,有用于可视化的 TensorBoard(请参阅第十章)。接下来,有由 Google 构建的用于将 TensorFlow 项目投入生产的一套库,称为TensorFlow Extended (TFX):它包括用于数据验证、预处理、模型分析和服务的工具(使用 TF Serving;请参阅第十九章)。Google 的 TensorFlow Hub 提供了一种轻松下载和重复使用预训练神经网络的方式。您还可以在 TensorFlow 的model garden中获得许多神经网络架构,其中一些是预训练的。查看TensorFlow 资源https://github.com/jtoy/awesome-tensorflow以获取更多基于 TensorFlow 的项目。您可以在 GitHub 上找到数百个 TensorFlow 项目,因此通常很容易找到您正在尝试做的任何事情的现有代码。

提示

越来越多的机器学习论文随着它们的实现发布,有时甚至附带预训练模型。请查看https://paperswithcode.com以轻松找到它们。

最后但并非最不重要的是,TensorFlow 拥有一支充满激情和乐于助人的开发团队,以及一个庞大的社区为其改进做出贡献。要提出技术问题,您应该使用https://stackoverflow.com,并在问题中标记tensorflowpython。您可以通过GitHub提交错误和功能请求。要进行一般讨论,请加入TensorFlow 论坛

好了,现在是开始编码的时候了!

像 NumPy 一样使用 TensorFlow

TensorFlow 的 API 围绕着张量展开,这些张量从操作流向操作,因此得名 TensorFlow。张量与 NumPy 的ndarray非常相似:通常是一个多维数组,但也可以保存标量(例如42)。当我们创建自定义成本函数、自定义指标、自定义层等时,这些张量将非常重要,让我们看看如何创建和操作它们。

张量和操作

您可以使用tf.constant()创建一个张量。例如,这里是一个表示具有两行三列浮点数的矩阵的张量:

>>> import tensorflow as tf
>>> t = tf.constant([[1., 2., 3.], [4., 5., 6.]])  # matrix
>>> t
<tf.Tensor: shape=(2, 3), dtype=float32, numpy=
array([[1., 2., 3.],
 [4., 5., 6.]], dtype=float32)>

就像ndarray一样,tf.Tensor有一个形状和一个数据类型(dtype):

>>> t.shape
TensorShape([2, 3])
>>> t.dtype
tf.float32

索引工作方式与 NumPy 类似:

>>> t[:, 1:]
<tf.Tensor: shape=(2, 2), dtype=float32, numpy=
array([[2., 3.],
 [5., 6.]], dtype=float32)>
>>> t[..., 1, tf.newaxis]
<tf.Tensor: shape=(2, 1), dtype=float32, numpy=
array([[2.],
 [5.]], dtype=float32)>

最重要的是,各种张量操作都是可用的:

>>> t + 10
<tf.Tensor: shape=(2, 3), dtype=float32, numpy=
array([[11., 12., 13.],
 [14., 15., 16.]], dtype=float32)>
>>> tf.square(t)
<tf.Tensor: shape=(2, 3), dtype=float32, numpy=
array([[ 1.,  4.,  9.],
 [16., 25., 36.]], dtype=float32)>
>>> t @ tf.transpose(t)
<tf.Tensor: shape=(2, 2), dtype=float32, numpy=
array([[14., 32.],
 [32., 77.]], dtype=float32)>

请注意,编写t + 10等同于调用tf.add(t, 10)(实际上,Python 调用了魔术方法t.__add__(10),它只是调用了tf.add(t, 10))。其他运算符,如-*,也受支持。@运算符在 Python 3.5 中添加,用于矩阵乘法:它等同于调用tf.matmul()函数。

注意

许多函数和类都有别名。例如,tf.add()tf.math.add()是相同的函数。这使得 TensorFlow 可以为最常见的操作保留简洁的名称,同时保持良好组织的包。

张量也可以保存标量值。在这种情况下,形状为空:

>>> tf.constant(42)
<tf.Tensor: shape=(), dtype=int32, numpy=42>
注意

Keras API 有自己的低级 API,位于tf.keras.backend中。这个包通常被导入为K,以简洁为主。它曾经包括函数如K.square()K.exp()K.sqrt(),您可能在现有代码中遇到:这在 Keras 支持多个后端时编写可移植代码很有用,但现在 Keras 只支持 TensorFlow,您应该直接调用 TensorFlow 的低级 API(例如,使用tf.square()而不是K.square())。从技术上讲,K.square()及其相关函数仍然存在以保持向后兼容性,但tf.keras.backend包的文档只列出了一些实用函数,例如clear_session()(在第十章中提到)。

您将找到所有您需要的基本数学运算(tf.add()tf.multiply()tf.square()tf.exp()tf.sqrt()等)以及大多数您可以在 NumPy 中找到的操作(例如tf.reshape()tf.squeeze()tf.tile())。一些函数的名称与 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()函数与 NumPy 的T属性并不完全相同:在 TensorFlow 中,将创建一个具有其自己的转置数据副本的新张量,而在 NumPy 中,t.T只是相同数据的一个转置视图。同样,tf.reduce_sum()操作之所以被命名为这样,是因为其 GPU 核心(即 GPU 实现)使用的减少算法不保证元素添加的顺序:因为 32 位浮点数的精度有限,每次调用此操作时结果可能会发生微小变化。tf.reduce_mean()也是如此(当然tf.reduce_max()是确定性的)。

张量和 NumPy

张量与 NumPy 兼容:您可以从 NumPy 数组创建张量,反之亦然。您甚至可以将 TensorFlow 操作应用于 NumPy 数组,将 NumPy 操作应用于张量:

>>> import numpy as np
>>> a = np.array([2., 4., 5.])
>>> tf.constant(a)
<tf.Tensor: id=111, shape=(3,), dtype=float64, numpy=array([2., 4., 5.])>
>>> t.numpy()  # or np.array(t)
array([[1., 2., 3.],
 [4., 5., 6.]], dtype=float32)
>>> tf.square(a)
<tf.Tensor: id=116, shape=(3,), dtype=float64, numpy=array([4., 16., 25.])>
>>> np.square(t)
array([[ 1.,  4.,  9.],
 [16., 25., 36.]], dtype=float32)
警告

请注意,NumPy 默认使用 64 位精度,而 TensorFlow 使用 32 位。这是因为 32 位精度通常对神经网络来说足够了,而且运行速度更快,使用的内存更少。因此,当您从 NumPy 数组创建张量时,请确保设置dtype=tf.float32

类型转换

类型转换可能会严重影响性能,并且当它们自动完成时很容易被忽略。为了避免这种情况,TensorFlow 不会自动执行任何类型转换:如果您尝试在具有不兼容类型的张量上执行操作,它只会引发异常。例如,您不能将浮点张量和整数张量相加,甚至不能将 32 位浮点数和 64 位浮点数相加:

>>> tf.constant(2.) + tf.constant(40)
[...] InvalidArgumentError: [...] expected to be a float tensor [...]
>>> tf.constant(2.) + tf.constant(40., dtype=tf.float64)
[...] InvalidArgumentError: [...] expected to be a float tensor [...]

这可能一开始有点烦人,但请记住这是为了一个好的目的!当然,当您真正需要转换类型时,您可以使用tf.cast()

>>> t2 = tf.constant(40., dtype=tf.float64)
>>> tf.constant(2.0) + tf.cast(t2, tf.float32)
<tf.Tensor: id=136, shape=(), dtype=float32, numpy=42.0>

变量

到目前为止,我们看到的tf.Tensor值是不可变的:我们无法修改它们。这意味着我们不能使用常规张量来实现神经网络中的权重,因为它们需要通过反向传播进行调整。此外,其他参数可能也需要随时间变化(例如,动量优化器会跟踪过去的梯度)。我们需要的是tf.Variable

>>> v = tf.Variable([[1., 2., 3.], [4., 5., 6.]])
>>> v
<tf.Variable 'Variable:0' shape=(2, 3) dtype=float32, numpy=
array([[1., 2., 3.],
 [4., 5., 6.]], dtype=float32)>

tf.Variable的行为很像tf.Tensor:您可以执行相同的操作,它与 NumPy 很好地配合,对类型也一样挑剔。但是它也可以使用assign()方法(或assign_add()assign_sub(),它们会增加或减少给定值来就地修改变量)。您还可以使用单个单元格(或切片)的assign()方法或使用scatter_update()scatter_nd_update()方法来修改单个单元格(或切片):

v.assign(2 * v)           # v now equals [[2., 4., 6.], [8., 10., 12.]]
v[0, 1].assign(42)        # v now equals [[2., 42., 6.], [8., 10., 12.]]
v[:, 2].assign([0., 1.])  # v now equals [[2., 42., 0.], [8., 10., 1.]]
v.scatter_nd_update(      # v now equals [[100., 42., 0.], [8., 10., 200.]]
    indices=[[0, 0], [1, 2]], updates=[100., 200.])

直接赋值不起作用:

>>> v[1] = [7., 8., 9.]
[...] TypeError: 'ResourceVariable' object does not support item assignment
注意

在实践中,您很少需要手动创建变量;Keras 提供了一个add_weight()方法,它会为您处理,您将看到。此外,模型参数通常会直接由优化器更新,因此您很少需要手动更新变量。

其他数据结构

TensorFlow 支持几种其他数据结构,包括以下内容(请参阅本章笔记本中的“其他数据结构”部分或附录 C 了解更多详细信息):

稀疏张量(tf.SparseTensor

高效地表示大部分为零的张量。tf.sparse包含了稀疏张量的操作。

张量数组(tf.TensorArray

是张量列表。它们默认具有固定长度,但可以选择性地扩展。它们包含的所有张量必须具有相同的形状和数据类型。

不规则张量(tf.RaggedTensor

表示张量列表,所有张量的秩和数据类型相同,但大小不同。张量大小变化的维度称为不规则维度tf.ragged包含了不规则张量的操作。

字符串张量

是类型为tf.string的常规张量。这些表示字节字符串,而不是 Unicode 字符串,因此如果您使用 Unicode 字符串(例如,像"café"这样的常规 Python 3 字符串)创建字符串张量,那么它将自编码为 UTF-8(例如,b"caf\xc3\xa9")。或者,您可以使用类型为tf.int32的张量来表示 Unicode 字符串,其中每个项目表示一个 Unicode 代码点(例如,[99, 97, 102, 233])。tf.strings包(带有s)包含用于字节字符串和 Unicode 字符串的操作(以及将一个转换为另一个的操作)。重要的是要注意tf.string是原子的,这意味着其长度不会出现在张量的形状中。一旦您将其转换为 Unicode 张量(即,一个包含 Unicode 代码点的tf.int32类型的张量),长度将出现在形状中。

集合

表示为常规张量(或稀疏张量)。例如,tf.constant([[1, 2], [3, 4]])表示两个集合{1, 2}和{3, 4}。更一般地,每个集合由张量的最后一个轴中的向量表示。您可以使用tf.sets包中的操作来操作集合。

队列

在多个步骤中存储张量。TensorFlow 提供各种类型的队列:基本的先进先出(FIFO)队列(FIFOQueue),以及可以优先处理某些项目的队列(PriorityQueue),对其项目进行洗牌的队列(RandomShuffleQueue),以及通过填充来批处理不同形状的项目的队列(PaddingFIFOQueue)。这些类都在tf.queue包中。

有了张量、操作、变量和各种数据结构,你现在可以定制你的模型和训练算法了!

自定义模型和训练算法

你将首先创建一个自定义损失函数,这是一个简单而常见的用例。

自定义损失函数

假设你想训练一个回归模型,但你的训练集有点嘈杂。当然,你首先尝试通过删除或修复异常值来清理数据集,但结果还不够好;数据集仍然很嘈杂。你应该使用哪种损失函数?均方误差可能会过分惩罚大误差,导致模型不够精确。平均绝对误差不会像惩罚异常值那样严重,但训练可能需要一段时间才能收敛,训练出的模型可能不够精确。这可能是使用 Huber 损失的好时机(在第十章介绍)。Huber 损失在 Keras 中是可用的(只需使用tf.keras.losses.Huber类的实例),但让我们假装它不存在。要实现它,只需创建一个函数,该函数将标签和模型预测作为参数,并使用 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)
警告

为了获得更好的性能,你应该使用矢量化的实现,就像这个例子一样。此外,如果你想要从 TensorFlow 的图优化功能中受益,你应该只使用 TensorFlow 操作。

也可以返回平均损失而不是单个样本损失,但这不推荐,因为这样做会使在需要时无法使用类权重或样本权重(参见第十章)。

现在你可以在编译 Keras 模型时使用这个 Huber 损失函数,然后像往常一样训练你的模型:

model.compile(loss=huber_fn, optimizer="nadam")
model.fit(X_train, y_train, [...])

就是这样!在训练期间的每个批次中,Keras 将调用huber_fn()函数来计算损失,然后使用反向模式自动微分来计算损失相对于所有模型参数的梯度,最后执行梯度下降步骤(在这个例子中使用 Nadam 优化器)。此外,它将跟踪自从 epoch 开始以来的总损失,并显示平均损失。

但是当你保存模型时,这个自定义损失会发生什么?

保存和加载包含自定义组件的模型

保存包含自定义损失函数的模型可以正常工作,但是当你加载它时,你需要提供一个将函数名称映射到实际函数的字典。更一般地,当你加载包含自定义对象的模型时,你需要将名称映射到对象:

model = tf.keras.models.load_model("my_model_with_a_custom_loss",
                                   custom_objects={"huber_fn": huber_fn})
提示

如果你用@keras.utils.reg⁠ister_keras_serializable()装饰huber_fn()函数,它将自动可用于load_model()函数:不需要将其包含在custom_objects字典中。

使用当前的实现,任何在-1 和 1 之间的错误都被认为是“小”。但是如果你想要一个不同的阈值呢?一个解决方案是创建一个函数来创建一个配置好的损失函数:

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.compile(loss=create_huber(2.0), optimizer="nadam")

不幸的是,当你保存模型时,threshold不会被保存。这意味着在加载模型时你将需要指定threshold的值(注意要使用的名称是"huber_fn",这是你给 Keras 的函数的名称,而不是创建它的函数的名称):

model = tf.keras.models.load_model(
    "my_model_with_a_custom_loss_threshold_2",
    custom_objects={"huber_fn": create_huber(2.0)}
)

你可以通过创建tf.keras.losses.Loss类的子类,然后实现它的get_config()方法来解决这个问题:

class HuberLoss(tf.keras.losses.Loss):
    def __init__(self, threshold=1.0, **kwargs):
        self.threshold = threshold
        super().__init__(**kwargs)
    def call(self, y_true, y_pred):
        error = y_true - y_pred
        is_small_error = tf.abs(error) < self.threshold
        squared_loss = tf.square(error) / 2
        linear_loss  = self.threshold * tf.abs(error) - self.threshold**2 / 2
        return tf.where(is_small_error, squared_loss, linear_loss)
    def get_config(self):
        base_config = super().get_config()
        return {**base_config, "threshold": self.threshold}

让我们来看看这段代码:

  • 构造函数接受**kwargs并将它们传递给父构造函数,父构造函数处理标准超参数:损失的name和用于聚合单个实例损失的reduction算法。默认情况下,这是"AUTO",等同于"SUM_OVER_BATCH_SIZE":损失将是实例损失的总和,加权后再除以批量大小(而不是加权平均)。其他可能的值是"SUM""NONE"
  • call()方法接受标签和预测值,计算所有实例损失,并返回它们。
  • get_config()方法返回一个字典,将每个超参数名称映射到其值。它首先调用父类的get_config()方法,然后将新的超参数添加到此字典中。

然后您可以在编译模型时使用此类的任何实例:

model.compile(loss=HuberLoss(2.), optimizer="nadam")

当您保存模型时,阈值将与模型一起保存;当您加载模型时,您只需要将类名映射到类本身:

model = tf.keras.models.load_model("my_model_with_a_custom_loss_class",
                                   custom_objects={"HuberLoss": HuberLoss})

当您保存模型时,Keras 会调用损失实例的get_config()方法,并以 SavedModel 格式保存配置。当您加载模型时,它会在HuberLoss类上调用from_config()类方法:这个方法由基类(Loss)实现,并创建一个类的实例,将**config传递给构造函数。

损失就是这样了!正如您现在将看到的,自定义激活函数、初始化器、正则化器和约束并没有太大不同。

自定义激活函数、初始化器、正则化器和约束

大多数 Keras 功能,如损失、正则化器、约束、初始化器、指标、激活函数、层,甚至完整模型,都可以以类似的方式进行自定义。大多数情况下,您只需要编写一个带有适当输入和输出的简单函数。这里有一个自定义激活函数的示例(相当于tf.keras.activations.softplus()tf.nn.softplus())、一个自定义 Glorot 初始化器的示例(相当于tf.keras.initializers.glorot_normal())、一个自定义ℓ[1]正则化器的示例(相当于tf.keras.regularizers.l1(0.01))以及一个确保权重都为正的自定义约束的示例(相当于tf.keras.con⁠straints.nonneg()tf.nn.relu()):

def my_softplus(z):
    return tf.math.log(1.0 + tf.exp(z))
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 value is just tf.nn.relu(weights)
    return tf.where(weights < 0., tf.zeros_like(weights), weights)

正如您所看到的,参数取决于自定义函数的类型。然后可以像这里展示的那样正常使用这些自定义函数:

layer = tf.keras.layers.Dense(1, activation=my_softplus,
                              kernel_initializer=my_glorot_initializer,
                              kernel_regularizer=my_l1_regularizer,
                              kernel_constraint=my_positive_weights)

激活函数将应用于此Dense层的输出,并将其结果传递给下一层。层的权重将使用初始化器返回的值进行初始化。在每个训练步骤中,权重将传递给正则化函数以计算正则化损失,然后将其添加到主损失中以获得用于训练的最终损失。最后,在每个训练步骤之后,将调用约束函数,并将层的权重替换为受约束的权重。

如果一个函数有需要与模型一起保存的超参数,那么您将希望子类化适当的类,比如tf.keras.regu⁠larizers.Reg⁠⁠ularizertf.keras.constraints.Constrainttf.keras.initializers.Ini⁠tializertf.keras.layers.Layer(适用于任何层,包括激活函数)。就像您为自定义损失所做的那样,这里是一个简单的ℓ[1]正则化类,它保存了其factor超参数(这次您不需要调用父构造函数或get_config()方法,因为它们不是由父类定义的):

class MyL1Regularizer(tf.keras.regularizers.Regularizer):
    def __init__(self, factor):
        self.factor = factor
    def __call__(self, weights):
        return tf.reduce_sum(tf.abs(self.factor * weights))
    def get_config(self):
        return {"factor": self.factor}

请注意,您必须为损失、层(包括激活函数)和模型实现call()方法,或者为正则化器、初始化器和约束实现__call__()方法。对于指标,情况有些不同,您将立即看到。

自定义指标

损失和指标在概念上并不相同:损失(例如,交叉熵)被梯度下降用来训练模型,因此它们必须是可微的(至少在评估它们的点上),它们的梯度不应该在任何地方都为零。此外,如果它们不容易被人类解释也是可以的。相反,指标(例如,准确率)用于评估模型:它们必须更容易被解释,可以是不可微的或者在任何地方梯度为零。

也就是说,在大多数情况下,定义一个自定义指标函数与定义一个自定义损失函数完全相同。实际上,我们甚至可以使用我们之前创建的 Huber 损失函数作为指标;它会工作得很好(在这种情况下,持久性也会以相同的方式工作,只保存函数的名称"huber_fn",而不是阈值):

model.compile(loss="mse", optimizer="nadam", metrics=[create_huber(2.0)])

在训练期间的每个批次,Keras 将计算这个指标并跟踪自开始时的平均值。大多数情况下,这正是你想要的。但并非总是如此!例如,考虑一个二元分类器的精度。正如你在第三章中看到的,精度是真正例的数量除以正例的预测数量(包括真正例和假正例)。假设模型在第一个批次中做出了五个正面预测,其中四个是正确的:这是 80%的精度。然后假设模型在第二个批次中做出了三个正面预测,但它们全部是错误的:这是第二个批次的 0%精度。如果你只计算这两个精度的平均值,你会得到 40%。但等一下——这不是这两个批次的模型精度!事实上,总共有四个真正例(4 + 0)中的八个正面预测(5 + 3),所以总体精度是 50%,而不是 40%。我们需要的是一个对象,它可以跟踪真正例的数量和假正例的数量,并且可以在需要时基于这些数字计算精度。这正是tf.keras.metrics.Precision类所做的:

>>> precision = tf.keras.metrics.Precision()
>>> precision([0, 1, 1, 1, 0, 1, 0, 1], [1, 1, 0, 1, 0, 1, 0, 1])
<tf.Tensor: shape=(), dtype=float32, numpy=0.8>
>>> precision([0, 1, 0, 0, 1, 0, 1, 1], [1, 0, 1, 1, 0, 0, 0, 0])
<tf.Tensor: shape=(), dtype=float32, numpy=0.5>

在这个例子中,我们创建了一个Precision对象,然后像一个函数一样使用它,为第一个批次传递标签和预测,然后为第二个批次(如果需要,还可以传递样本权重)。我们使用了与刚才讨论的示例中相同数量的真正例和假正例。在第一个批次之后,它返回 80%的精度;然后在第二个批次之后,它返回 50%(这是到目前为止的总体精度,而不是第二个批次的精度)。这被称为流式指标(或有状态指标),因为它逐渐更新,批次之后。

在任何时候,我们可以调用result()方法来获取指标的当前值。我们还可以通过使用variables属性查看其变量(跟踪真正例和假正例的数量),并可以使用reset_states()方法重置这些变量:

>>> precision.result()
<tf.Tensor: shape=(), dtype=float32, numpy=0.5>
>>> precision.variables
[<tf.Variable 'true_positives:0' [...], numpy=array([4.], dtype=float32)>,
 <tf.Variable 'false_positives:0' [...], numpy=array([4.], dtype=float32)>]
>>> precision.reset_states()  # both variables get reset to 0.0

如果需要定义自己的自定义流式指标,创建tf.keras.metrics.Metric类的子类。这里是一个基本示例,它跟踪总 Huber 损失和迄今为止看到的实例数量。当要求结果时,它返回比率,这只是平均 Huber 损失:

class HuberMetric(tf.keras.metrics.Metric):
    def __init__(self, threshold=1.0, **kwargs):
        super().__init__(**kwargs)  # handles base args (e.g., dtype)
        self.threshold = threshold
        self.huber_fn = create_huber(threshold)
        self.total = self.add_weight("total", initializer="zeros")
        self.count = self.add_weight("count", initializer="zeros")
    def update_state(self, y_true, y_pred, sample_weight=None):
        sample_metrics = self.huber_fn(y_true, y_pred)
        self.total.assign_add(tf.reduce_sum(sample_metrics))
        self.count.assign_add(tf.cast(tf.size(y_true), tf.float32))
    def result(self):
        return self.total / self.count
    def get_config(self):
        base_config = super().get_config()
        return {**base_config, "threshold": self.threshold}

让我们走一遍这段代码:

  • 构造函数使用add_weight()方法创建需要在多个批次中跟踪指标状态的变量——在这种情况下,所有 Huber 损失的总和(total)和迄今为止看到的实例数量(count)。如果愿意,你也可以手动创建变量。Keras 跟踪任何设置为属性的tf.Variable(更一般地,任何“可跟踪”的对象,如层或模型)。
  • 当你将这个类的实例用作函数时(就像我们用Precision对象做的那样),update_state()方法会被调用。它根据一个批次的标签和预测更新变量(以及样本权重,但在这种情况下我们忽略它们)。
  • result()方法计算并返回最终结果,在这种情况下是所有实例上的平均 Huber 指标。当你将指标用作函数时,首先调用update_state()方法,然后调用result()方法,并返回其输出。
  • 我们还实现了get_config()方法,以确保threshold与模型一起保存。
  • reset_states()方法的默认实现将所有变量重置为 0.0(但如果需要,你可以覆盖它)。
注意

Keras 会无缝处理变量持久性;不需要任何操作。

当你使用简单函数定义指标时,Keras 会自动为每个批次调用它,并在每个时期期间跟踪平均值,就像我们手动做的那样。因此,我们的HuberMetric类的唯一好处是threshold将被保存。但当然,有些指标,比如精度,不能简单地在批次上进行平均:在这些情况下,除了实现流式指标之外别无选择。

现在你已经构建了一个流式指标,构建一个自定义层将会变得轻而易举!


Sklearn、TensorFlow 与 Keras 机器学习实用指南第三版(五)(2)https://developer.aliyun.com/article/1482432

相关实践学习
基于阿里云DeepGPU实例,用AI画唯美国风少女
本实验基于阿里云DeepGPU实例,使用aiacctorch加速stable-diffusion-webui,用AI画唯美国风少女,可提升性能至高至原性能的2.6倍。
相关文章
|
2天前
|
机器学习/深度学习 算法 TensorFlow
TensorFlow 2keras开发深度学习模型实例:多层感知器(MLP),卷积神经网络(CNN)和递归神经网络(RNN)
TensorFlow 2keras开发深度学习模型实例:多层感知器(MLP),卷积神经网络(CNN)和递归神经网络(RNN)
|
4天前
|
机器学习/深度学习 PyTorch TensorFlow
TensorFlow、Keras 和 Python 构建神经网络分析鸢尾花iris数据集|代码数据分享
TensorFlow、Keras 和 Python 构建神经网络分析鸢尾花iris数据集|代码数据分享
15 0
|
13天前
|
机器学习/深度学习 运维 监控
TensorFlow分布式训练:加速深度学习模型训练
【4月更文挑战第17天】TensorFlow分布式训练加速深度学习模型训练,通过数据并行和模型并行利用多机器资源,减少训练时间。优化策略包括配置计算资源、优化数据划分和减少通信开销。实际应用需关注调试监控、系统稳定性和容错性,以应对分布式训练挑战。
|
15天前
|
机器学习/深度学习 人工智能 算法框架/工具
Sklearn、TensorFlow 与 Keras 机器学习实用指南第三版(八)(4)
Sklearn、TensorFlow 与 Keras 机器学习实用指南第三版(八)
30 0
Sklearn、TensorFlow 与 Keras 机器学习实用指南第三版(八)(4)
|
15天前
|
机器学习/深度学习 算法框架/工具 TensorFlow
Sklearn、TensorFlow 与 Keras 机器学习实用指南第三版(七)(4)
Sklearn、TensorFlow 与 Keras 机器学习实用指南第三版(七)
46 0
Sklearn、TensorFlow 与 Keras 机器学习实用指南第三版(七)(4)
|
4月前
|
机器学习/深度学习 人工智能 API
TensorFlow Lite,ML Kit 和 Flutter 移动深度学习:1~5
TensorFlow Lite,ML Kit 和 Flutter 移动深度学习:1~5
73 0
|
4月前
|
机器学习/深度学习 存储 人工智能
TensorFlow Lite,ML Kit 和 Flutter 移动深度学习:6~11(3)
TensorFlow Lite,ML Kit 和 Flutter 移动深度学习:6~11(3)
81 0
|
4月前
|
机器学习/深度学习 Dart TensorFlow
TensorFlow Lite,ML Kit 和 Flutter 移动深度学习:6~11(5)
TensorFlow Lite,ML Kit 和 Flutter 移动深度学习:6~11(5)
73 0
|
3月前
|
机器学习/深度学习 PyTorch TensorFlow
Python中的深度学习:TensorFlow与PyTorch的选择与使用
Python中的深度学习:TensorFlow与PyTorch的选择与使用
|
3月前
|
机器学习/深度学习 数据可视化 TensorFlow
基于tensorflow深度学习的猫狗分类识别
基于tensorflow深度学习的猫狗分类识别
65 1