1. 前言

本文是《Python深度学习(第2版)》一书第五、六、七章的学习笔记。

2. 机器学习基础

机器学习的核心难题是“过拟合”。在进行机器学习时,准确的模型评估,以及训练与泛化之间的平衡非常重要。

2.1 泛化:机器学习的目标

机器学习的根本问题在于优化和泛化之间的矛盾。优化是调节模型使其在训练集上得到最佳性能的过程,泛化是指训练好的模型在新数据上的性能。拟合得不好就是欠拟合,拟合得过好就是过拟合。我们需要的是稳健拟合:

image-20230715174158323

嘈杂的训练数据、模糊的特征、罕见特征或虚假相关性等数据上的因素都可能会让训练过程更容易出现过拟合现象。我们通常可以借助在训练前对每个特征计算有用性分数来进行特征选择,从而过滤那些噪声特征。

另外,只要模型具有足够的表示能力,它就可以被训练来拟合任何数据,即使这个数据集是杂乱无章、没有现实意义的。深度学习泛化的本质与模型本身关系不大,而是与现实世界中的信息结构密切相关。“流形假说”认为,所有自然数据都位于高维空间中的一个低维流形(manifold)中,该高维空间是数据编码空间。例如,手写数字在28*28 uint8数组的可能性空间中构成了一个流形——有效手写数字的子空间是连续的,所有有效样本由穿过子空间的光滑路径“连接”。

模型只需在输入空间内拟合相对简单、低维、高度结构化的子空间。在一个流形中,我们总是可以在两个输入之间插值(interpolate),即通过一条连续路径将一个输入变形为另一个输入。这是理解深度学习泛化的关键。然而,插值能实现的是局部泛化(直觉与模式识别),人类大脑则能够进行极端泛化(理性,reason)。

深度学习模型的本质是一条光滑连续的可微的(否则无法进行梯度下降)高维曲线,我们通过调节其参数使其平滑地拟合数据,从而接近数据的自然流形。

为了使模型表现良好,需要在输入空间的“密集采样”上训练模型。模型的性能由“模型架构预设”和“模型训练数据”决定。改进模型的最佳方法是用更多、更好的数据训练模型;次优的方法是调节模型允许存储的信息量或对模型曲线平滑度添加约束。例如,我们可以通过正则化(regularization)来降低过拟合。

2.2 评估机器学习模型

评估模型的重点是将可用数据划分为三部分:训练集、验证集和测试集。

开发模型时,我们需要调节模型的超参数,这个调节过程需要使用模型在验证数据上的表现作为反馈信号。然而,多次重复这个调节过程可能会将验证集的信息逐渐泄露到模型中,从而导致模型在验证集上过拟合。

无论如何,不能在训练和验证期间读取与测试集有关的任何信息。

在验证环节,三种经典评估方法是:

  1. 简单的留出验证。其缺点在于,如果可用数据少,验证集可能无法在统计学上代表数据。
  2. K折交叉验证。对于不同的训练集、测试集划分,如果模型性能变化很大,这种方法很有用。
  3. 带有打乱数据的重复K折交叉验证。多次使用K折交叉验证,每次划分为K个分区前都将数据打乱。如果可用数据较少,需要尽可能精确地评估模型,可以采用这种方案。

对于验证指标而言,我们还需要有一个基于常识的基准来判断。这个基准可以是随机分类器的性能,也可以是最简单的非机器学习方法的性能。

评估模型时还需要注意以下三点:

  1. 数据代表性。训练集和测试集都应该具有代表性。在划分它们之前,应该随机打乱数据。
  2. 时间箭头。如果数据有时间顺序,那么在划分数据前不应该随机打乱,否则可能造成时间泄露。要始终确保测试集中数据的时间晚于训练数据。
  3. 数据冗余。一定要确保训练集和验证集之间没有交集。

2.3 改进模型拟合

为了实现完美拟合,需要先实现过拟合,然后通过降低过拟合来提高泛化能力。

训练过程中我们常常会遇到三个问题:

  1. 训练损失不随时间推移而减小。
  2. 模型无法超越基于常识的基准。
  3. 模型可以超越基准,但是始终无法过拟合。

相应的解决方法有:

  • 调节关键的梯度下降参数(如学习率、批量大小等)。
  • 利用更好的架构预设(参考前人经验)。
  • 提高模型容量(如添加更多的层、使用更大的层等)。

2.4 提高泛化能力

如果模型能够表现出一定的泛化能力,并且能够过拟合,接下来我们应该专注于将泛化能力最大化。相关方法有:

  1. 数据集管理。常见做法有:确保拥有足够的数据,尽量减少标签错误,清理数据并处理损失值,进行特征选择……
  2. 特征工程。用更简单的方式表述问题。虽然深度学习能够自动提取有用的特征,但是良好的特征仍然有助于问题的解决,尤其是在样本数量少的时候。
  3. 提前终止。在Keras中,我们可以使用EarlyStopping回掉函数来实现提前终止训练过程,它会在验证指标停止改善时立即中断训练,并记录最佳模型状态。
  4. 模型正则化。常见做法有:缩减模型容量、添加权重正则化(包括L1正则化和L2正则化,分别对应在损失函数中添加权重系数的绝对值和平方,L2正则化也叫做权重衰减)、添加dropout(在训练过程中随机舍弃某一层的部分输出特征,将其设置为0)。

在Keras中,添加权重正则化的方法是向层中传入权重正则化项实例:

from tensorflow.keras import regularizers
model = keras.Sequential([
    layers.Dense(16,
                 kernel_regularizer=regularizers.l2(0.002),
                 activation="relu"),
    layers.Dense(16,
                 kernel_regularizer=regularizers.l2(0.002),
                 activation="relu"),
    layers.Dense(1, activation="sigmoid")
])

添加dropout的同时,我们需要将训练时该层的输出按dropout比率放大,或者将测试时该层的输出按照同样比率缩小,因为测试时没有单元被舍弃:

# dropout (in training)
layer_output *= np.random.randint(0, high=2, size=layer_output.shape)
# scaling (in training)
layer_output /= 0.5

Dropout的核心思想是在层的输出值中引入噪声,从而打破不重要的偶然模式。

在Keras中,可以通过Dropout层来引入dropout:

model = keras.Sequential([
    layers.Dense(16, activation="relu"),
    layers.Dropout(0.5),
    layers.Dense(16, activation="relu"),
    layers.Dropout(0.5),
    layers.Dense(1, activation="sigmoid")
])

3. 机器学习的通用工作流程

前面的练习中,我们都是直接使用TensorFlow自带的数据集。然而,在现实中,我们通常没有准备好的数据集,而是往往从待解决的问题开始。机器学习的通用工作流程包括定义任务、开发模型、部署模型三步。下面我们依次来研究一下这些步骤。

3.1 定义任务

首先要准确地定义问题,我们此时需要重点关注以下内容:

  • 输入数据是什么?要预测什么?
  • 面对的是什么类型的机器学习任务?分类?回归?生成式学习?强化学习?……
  • 现有的解决方案是什么?
  • 是否需要处理一些特殊限制?如端到端加密?在终端上运行?……

要继续这个机器学习任务,意味着我们做出了以下假设:

  • 假设可以根据输入对目标进行预测。
  • 假设现有数据及后续数据包含的信息足以用来学习输入和目标之间的关系。

接着,我们就要开始收集数据集。这一步通常是最费力、费时、费钱的。数据可能比算法更重要。此时,我们可能要进行(或者外包)数据标注任务,同时提防非代表性数据。另外,数据存在“概念漂移现象”——如果数据属性随时间变化,模型精度可能会逐渐下降。机器学习是在过去的数据上学习预测未来,这里有一个假设,即未来的规律与过去相同,但事实有时并非如此。

完成数据收集任务后,我们要尝试理解数据,将其可视化,观察它,尝试发现它的特点。

我们还要选择衡量模型成功的指标。对于平衡分类(每个类别比例相同)问题,精度和受试者操作特征曲线下面积(ROC AUC)是两个常用指标;对于类别不平衡的问题、排序问题和多标签分类问题,我们可以使用准确率和召回率,也可以使用精度或ROC AUC的加权形式。当然,我们也可以自定义指标。

3.2 开发模型

3.2.1 准备数据

开发模型的第一步是准备数据,我们需要将收集到的数据进行向量化规范化。输入数据应具有取值较小(通常在0~1范围内)和同质性(所有特征取值大致在同一范围内)两个特征。

另外,我们也可以考虑将特征规范化,使其均值为0、标准差为1:

x -= x.mean(axis=0)
x /= x.std(axis=0)

我们还要处理数据中的缺失值。如果缺失的是分类特征的值,我们可以创建一个新类别“此值缺失”来填充;如果缺失的是数值特征,我们可以考虑用数据集中该特征的均值或中位值代替。我们甚至可以训练一个模型去根据其他特征预测该特征的值。

3.2.2 选择评估方法

从简单留出验证、K折交叉验证和重复K折交叉验证中选择一种方法即可。

3.2.3 超越基准

此阶段,我们需要关注以下三件事情:

  1. 特征工程。过滤对于待解决问题来说没有信息量的特征;开发可能有用的新特征。
  2. 选择正确的架构预设。
  3. 选择足够好的训练配置(损失函数、批量大小、学习率等)。

下面列出了常见问题类型的最后一层激活函数和损失函数:

问题类型 最后一层激活函数 损失函数
二分类问题 sigmoid binary_crossentropy
多分类、单标签问题 softmax categorical_crossentropy
多分类、多标签问题 sigmoid binary_crossentropy

3.2.4 扩大模型规模:过拟合

很简单:增加层数、让每一层变得更大、训练更多轮。

3.2.5 模型正则化与调节超参数

此阶段,我们需要不断训练、调试模型、在验证数据上评估模型。可以做的有:尝试不同架构、添加dropout正则化、添加L1或L2正则化、尝试不同的超参数等。另外,KerasTuner等超参数自动调节软件可以将一些调节工作自动化。

3.3 部署模型

完成模型开发后,我们就可以考虑部署了。常见的部署模式有以下三种:

  1. 以RESTful API形式部署模型。如果应用程序能可靠地访问互联网、没有严格的延迟要求、输入数据不是高度敏感的,可以采取这种方式。
  2. 在设备上部署模型。这种情况通常对模型有严格的延迟限制,或者输入数据比较敏感,不应在远程服务器上解密。另外,如果模型可以做得足够小,也可以考虑部署在设备上。
  3. 在浏览器中部署模型。这样可以让用户分担计算开销,且能够满足数据敏感要求和严格的延迟要求。如果完成缓存,应用程序也可以在断网状态下运行。

最后,我们可以使用权重剪枝、权重量化等方法来优化最终的部署模型。模型部署完成之后,我们还需要持续监控模型在真实环境中的性能,并定期维护、更新模型。

4. 深入Keras

完成经验总结之后,我们来深入探索一下Keras,为以后面对的高级机器学习任务打基础。Keras API的设计原则是渐进式呈现复杂性,没有唯一正确的使用方式,而是提供了一系列繁简不一的工作流程。这些工作流程都基于Layer、Model等共享API,因此不同工作流程的组件都可以互相通信。

4.1 构建Keras模型的不同方法

Keras提供了三种构建Keras模型的不同方法:序贯模型(sequential)、函数式API和模型子类化:

image-20230717155217419

4.1.1 序贯模型

序贯模型就是使用Sequential类进行层的简单堆叠,不再赘述。注意,只有在数据上调用模型,或者调用模型的build()方法并给定输入形状后,序贯模型才有权重。我们还可以调用summary()方法来打印模型的概述信息。

序贯模型的局限性在于只能用于表示具有单一输入和单一输出的模型,并按顺序逐层处理。

4.1.2 函数式API

函数式API专注于类似图的模型结构。我们在理解Keras的函数式API时可以将它与Python中函数是第一类对象这个事实结合起来。下面是利用函数式API构建模型的一个例子:

inputs = keras.Input(shape=(3,), name="my_input")
features = layers.Dense(64, activation="relu")(inputs)
outputs = layers.Dense(10, activation="softmax")(features)
model = keras.Model(inputs=inputs, outputs=outputs)

inputs这样的对象叫做符号张量——它不包含任何数据,只是编码了调用模型时实际数据张量的详细信息。上面代码构建的模型如下所示:

>>> model.summary()
Model: "model_1"
_________________________________________________________________
 Layer (type)                Output Shape              Param #
=================================================================
 my_input (InputLayer)       [(None, 3)]               0

 dense_4 (Dense)             (None, 64)                256

 dense_5 (Dense)             (None, 10)                650

=================================================================
Total params: 906 (3.54 KB)
Trainable params: 906 (3.54 KB)
Non-trainable params: 0 (0.00 Byte)
_________________________________________________________________

我们还可以基于函数式API构建更复杂的多输入、多输出模型:

vocabulary_size = 10000
num_tags = 100
num_departments = 4

title = keras.Input(shape=(vocabulary_size,), name="title")
text_body = keras.Input(shape=(vocabulary_size,), name="text_body")
tags = keras.Input(shape=(num_tags,), name="tags")

features = layers.Concatenate()([title, text_body, tags])
features = layers.Dense(64, activation="relu")(features)

priority = layers.Dense(1, activation="sigmoid", name="priority")(features)
department = layers.Dense(num_departments, activation="softmax", name="department")(features)

model = keras.Model(inputs=[title, text_body, tags], outputs=[priority, department])

这种模型的训练过程与序贯模型相同,只是在数据传入上有一些细微差异:

model.compile(optimizer="rmsprop",
              loss={"priority": "mean_squared_error", "department": "categorical_crossentropy"},
              metrics={"priority": ["mean_absolute_error"], "department": ["accuracy"]})
model.fit({"title": title_data, "text_body": text_body_data, "tags": tags_data},
          {"priority": priority_data, "department": department_data},
          epochs=1)
model.evaluate({"title": title_data, "text_body": text_body_data, "tags": tags_data},
               {"priority": priority_data, "department": department_data})
priority_preds, department_preds = model.predict(
    {"title": title_data, "text_body": text_body_data, "tags": tags_data})

函数式模型是一种图数据结构,便于可视化和特征提取。我们将上述模型进行可视化来观察一下:

keras.utils.plot_model(model, "ticket_classifier.png", show_shapes=True)

这对于模型调试非常有帮助:

Unknown

我们还可以进行特征提取,复用模型的中间特征来创建新模型,例如:

features = model.layers[4].output
difficulty = layers.Dense(3, activation="softmax", name="difficulty")(features)
new_model = keras.Model(
    inputs=[title, text_body, tags],
    outputs=[priority, department, difficulty])

这个模型比之前的模型多了一个输出:

Unknown

4.1.3 模型子类化

模型子类化与我们学过的Layer类子类化非常相似,包含以下几个步骤:

  • 在构造函数中定义模型将使用的层。
  • call()方法中定义前向传播,重复使用之前创建的层。
  • 将子类实例化,在数据上调用,从而创建权重。

下面我们将前面的函数式API定义的模型改为子类化模型:

class CustomerTicketModel(keras.Model):
    def __init__(self, num_departments):
        super().__init__()
        self.concat_layer = layers.Concatenate()
        self.mixing_layer = layers.Dense(64, activation="relu")
        self.priority_scorer = layers.Dense(1, activation="sigmoid")
        self.department_classifier = layers.Dense(
            num_departments, activation="softmax")

    def call(self, inputs):
        title = inputs["title"]
        text_body = inputs["text_body"]
        tags = inputs["tags"]
        features = self.concat_layer([title, text_body, tags])
        features = self.mixing_layer(features)
        priority = self.priority_scorer(features)
        department = self.department_classifier(features)
        return priority, department

可以发现,子类化模型与函数式API定义的模型有共同之处——在子类化模型的call()方法中,我们还是使用了函数式API去定义、连接不同的层。子类化模型的优势在于更加灵活,你可以在call()方法里使用for循环或者递归,创建更加复杂的模型。

模型实例化后,训练过程就与之前定义的模型大同小异了:

model = CustomerTicketModel(num_departments=4)
priority, department = model({"title": title_data, "text_body": text_body_data, "tags": tags_data})

model.compile(optimizer="rmsprop",
              loss=["mean_squared_error", "categorical_crossentropy"],
              metrics=[["mean_absolute_error"], ["accuracy"]])
model.fit({"title": title_data,
           "text_body": text_body_data,
           "tags": tags_data},
          [priority_data, department_data],
          epochs=1)
model.evaluate({"title": title_data,
                "text_body": text_body_data,
                "tags": tags_data},
               [priority_data, department_data])
priority_preds, department_preds = model.predict({"title": title_data,
                                                  "text_body": text_body_data,
                                                  "tags": tags_data})

函数式模型的本质是一种数据结构,而子类化模型是一个类,包含字节码。因此,我们无法调用summary()plot_model()去观察模型的内部结构。子类化模型的前向传播是一个黑盒子。

事实上,序贯模型、函数式API和模型子类化可以交互和混用。这也是Keras的灵活之处。总体而言,函数式API在灵活性和易用性之间做了很好的平衡,建议优先使用。

4.2 使用内置的训练和评估循环

我们之前用过最多的工作流程是直接调用模型的内置方法:

compile() -> fit() -> evaluate() -> predict()

如果想要自定义这个工作流程,我们可以编写自定义指标,或者向fit()方法传入回调函数

4.2.1 编写自定义指标

常用的分类和回归指标都在keras.metrics模块中,指标都是keras.metrics.Metric类的子类。它们与层相似,都是存储在TensorFlow变量中的内部状态;它们又与层不同,指标无法通过反向传播更新,我们必须自己编写状体更新逻辑,在update_state()方法中。

下面的代码实现了一个简单的均方根误差(RMSE)指标类:

class RootMeanSquaredError(keras.metrics.Metric):
    def __init__(self, name="rmse", **kwargs):
        super().__init__(name=name, **kwargs)
        self.mse_sum = self.add_weight(name="mse_sum", initializer="zeros")
        self.total_samples = self.add_weight(
            name="total_samples", initializer="zeros", dtype="int32")

    def update_state(self, y_true, y_pred, sample_weight=None):
        y_true = tf.one_hot(y_true, depth=tf.shape(y_pred)[1])
        mse = tf.reduce_sum(tf.square(y_true - y_pred))
        self.mse_sum.assign_add(mse)
        num_samples = tf.shape(y_pred)[0]
        self.total_samples.assign_add(num_samples)

    def result(self):
        return tf.sqrt(self.mse_sum / tf.cast(self.total_samples, tf.float32))

    def reset_state(self):
        self.mse_sum.assign(0.)
        self.total_samples.assign(0)

其中,result()方法用于返回指标的当前值,reset_state()方法用于重置指标状态。

4.2.2 使用回调函数

回调函数是实现了特定方法的类实例,能够访问模型状态与性能的所有可用数据,并执行中断训练、保存模型、加载不同权重或改变模型状态等动作。Keras中常见的回调函数如下:

  • 模型检查点(keras.callbacks.ModelCheckpoint)
  • 提前终止(keras.callbacks.EarlyStopping)
  • 训练时动态调节参数值(如keras.callbacks.LearningRateScheduler和keras.callbacks.ReduceLROnPlateau)
  • 记录训练过程(keras.callbacks.CSVLogger)

下面是回调函数的使用示例:

callbacks_list = [
    keras.callbacks.EarlyStopping(
        monitor="val_accuracy",
        patience=2,
    ),
    keras.callbacks.ModelCheckpoint(
        filepath="checkpoint_path.keras",
        monitor="val_loss",
        save_best_only=True,
    )
]
model = get_mnist_model()
model.compile(optimizer="rmsprop",
              loss="sparse_categorical_crossentropy",
              metrics=["accuracy"])
model.fit(train_images, train_labels,
          epochs=10,
          callbacks=callbacks_list,
          validation_data=(val_images, val_labels))

# load stored model
model = keras.models.load_model("checkpoint_path.keras")

如果想要编写自定义回调函数,我们只需要将keras.callbacks.Callback类子类化,然后按需实现形如on_*_*()的六个方法即可,第一个星号处分别是epoch、batch和train,第二个星号处分别是begin和end。一个示例如下:

from matplotlib import pyplot as plt

class LossHistory(keras.callbacks.Callback):
    def on_train_begin(self, logs):
        self.per_batch_losses = []

    def on_batch_end(self, batch, logs):
        self.per_batch_losses.append(logs.get("loss"))

    def on_epoch_end(self, epoch, logs):
        plt.clf()
        plt.plot(range(len(self.per_batch_losses)), self.per_batch_losses,
                 label="Training loss for each batch")
        plt.xlabel(f"Batch (epoch {epoch})")
        plt.ylabel("Loss")
        plt.legend()
        plt.savefig(f"plot_at_epoch_{epoch}")
        self.per_batch_losses = []

最后,我们可以使用TensorBoard来对实验过程进行监控和可视化。

4.3 编写自定义的训练和评估循环

除了使用内置的训练和评估循环,我们还可以编写自定义的训练和评估逻辑。例如,内置的fit()只适用于监督学习,对于其他的机器学习任务(如生成式学习、自监督学习、强化学习等),我们可能需要自定义这个过程。

典型的训练循环包含前向传播、梯度计算、权重更新三个步骤。这些步骤对多个批量重复进行,大体上就是fit()在后台的工作。下面我们将其分解研究一下。

4.3.1 训练与推断

前面的线性分类器例子中,我们通过调用模型实例和使用梯度带完成前向传播、梯度计算两个步骤。事实上,在前向传播中调用Keras模型时,我们还需要传入training=True,这是因为某些Keras层(如Dropout层)在训练和推断过程中具有不同的行为。

此外,在检索权重梯度时,应传入model.trainable_weights,也就是只检索“可训练权重”的梯度。Keras中的BatchNormalization层会用到不可训练权重,用于记录一些信息,从而实时进行特征规范化。

4.3.2 指标的低阶用法

对于指标来说,我们可以使用update_state()方法更新指标,使用result()查询当前值,使用reset_state()方法重置指标。

4.3.3 完整的训练和评估循环

下面的示例代码实现了一个完整的训练步骤函数:

model = get_mnist_model()
loss_fn = keras.losses.SparseCategoricalCrossentropy()
optimizer = keras.optimizers.RMSprop()
metrics = [keras.metrics.SparseCategoricalAccuracy()]
loss_tracking_metric = keras.metrics.Mean()

def train_step(inputs, targets):
    with tf.GradientTape() as tape:
        # forward propagation
        predictions = model(inputs, training=True)
        # gradients calculation
        loss = loss_fn(targets, predictions)
        gradients = tape.gradient(loss, model.trainable_weights)
        # update weights
        optimizer.apply_gradients(zip(gradients, model.trainable_weights))
	# track metrics
    logs = {}
    for metric in metrics:
        metric.update_state(targets, predictions)
        logs[metric.name] = metric.result()
	# track loss
    loss_tracking_metric.update_state(loss)
    logs["loss"] = loss_tracking_metric.result()
    return logs

在每轮训练开始时和评估前,我们都需要重置指标状态:

def reset_metrics():
    for metric in metrics:
        metric.reset_state()
    loss_tracking_metric.reset_state()

一个完整的训练循环如下所示:

training_dataset = tf.data.Dataset.from_tensor_slices((train_images, train_labels))
training_dataset = training_dataset.batch(32)
epochs = 3
for epoch in range(epochs):
    reset_metrics()
    for inputs_batch, targets_batch in training_dataset:
        logs = train_step(inputs_batch, targets_batch)
    print(f"Results at the end of epoch {epoch}")
    for key, value in logs.items():
        print(f"...{key}: {value:.4f}")

结果如下所示:

Results at the end of epoch 0
...sparse_categorical_accuracy: 0.9143
...loss: 0.2908
Results at the end of epoch 1
...sparse_categorical_accuracy: 0.9545
...loss: 0.1597
Results at the end of epoch 2
...sparse_categorical_accuracy: 0.9626
...loss: 0.1278

下面是评估循环,注意其中传入了training=False

def test_step(inputs, targets):
    predictions = model(inputs, training=False)
    loss = loss_fn(targets, predictions)
    logs = {}
    for metric in metrics:
        metric.update_state(targets, predictions)
        logs["val_" + metric.name] = metric.result()
    loss_tracking_metric.update_state(loss)
    logs["val_loss"] = loss_tracking_metric.result()
    return logs

val_dataset = tf.data.Dataset.from_tensor_slices((val_images, val_labels))
val_dataset = val_dataset.batch(32)
reset_metrics()
for inputs_batch, targets_batch in val_dataset:
    logs = test_step(inputs_batch, targets_batch)
print("Evaluation results:")
for key, value in logs.items():
    print(f"...{key}: {value:.4f}")

4.3.4 利用tf.function加快运行速度

我们实现的训练和评估过程比TensorFlow自带的慢很多,这是因为它是“急切执行”的,方便调试。更高效的做法是将其编译为计算图,TensorFlow将对计算图进行全局优化。我们只需要在每个需要在执行前编译的函数头加上@tf.function装饰器即可。

4.3.5 在fit()中使用自定义训练循环

除了编写自定义训练循环外,我们也可以通过覆盖Model类的train_step()方法来自定义训练算法,然后让fit()调用我们的方法:

loss_fn = keras.losses.SparseCategoricalCrossentropy()
loss_tracker = keras.metrics.Mean(name="loss")

class CustomModel(keras.Model):
    def train_step(self, data):
        inputs, targets = data
        with tf.GradientTape() as tape:
            predictions = self(inputs, training=True)
            loss = self.compiled_loss(targets, predictions)
        gradients = tape.gradient(loss, self.trainable_weights)
        self.optimizer.apply_gradients(zip(gradients, self.trainable_weights))
        self.compiled_metrics.update_state(targets, predictions)
        return {m.name: m.result() for m in self.metrics}

训练过程与之前基本相同:

inputs = keras.Input(shape=(28 * 28,))
features = layers.Dense(512, activation="relu")(inputs)
features = layers.Dropout(0.5)(features)
outputs = layers.Dense(10, activation="softmax")(features)
model = CustomModel(inputs, outputs)
model.compile(optimizer=keras.optimizers.RMSprop(),
              loss=keras.losses.SparseCategoricalCrossentropy(),
              metrics=[keras.metrics.SparseCategoricalAccuracy()])
model.fit(train_images, train_labels, epochs=3)

在上面的代码中,我们使用了函数式API去构建模型。另外,我们无须在覆盖实现的train_step()上添加@tf.function装饰器,TensorFlow将自动添加。

5. 总结与思考

本文是原书第五、六、七章的笔记。我们总结了机器学习工作中的经验和通用工作流程,深入研究了三种构建Keras模型的不同方法,还学习了如何对内置的训练评估循环进行自定义、如何编写自定义的训练评估循环等。

文行至此,跑一下题。昨日(7月17日),吴恩达来到NUS做了一场AI相关的科普演讲,对接下来三年AI的发展作了展望。他对于AI的一种看法是将其视作一个工具集,其中包括监督学习、无监督学习、强化学习和生成式AI等。在接下来的三年里,这些工具的价值增长不一:监督学习价值基数大,增长也多;无监督学习基数适中,增长一般;生成式AI基数小,增长多;强化学习基数最小,从吴恩达的PPT上也很难看出其增长量。