【前沿重器】

全新栏目,本栏目主要和大家一起讨论近期自己学习的心得和体会,与大家一起成长。具体介绍:仓颉专项:飞机大炮我都会,利器心法我还有。

往期回顾

  • 前沿重器[1] | 微软小冰-多轮和情感机器人的先行者
  • 前沿重器[2] | 美团搜索理解和召回
  • 前沿重器[3] | 平安智能问答系统
  • 心法利器[1] | NLP知识蒸馏思考
  • 心法利器[2] | 统计语言模型使用反思

说起来自己还挺菜的,tensorflow很久之前就已经把keras合并到自己的生态里,虽然早就会了keras,但是因为排期的原因自己一直没有更新这块的知识,这次系统地学习一下,并记录下来。

当然,我不想写成一个教程,也不想整成一个API,更希望这是一个导学,能给大家一些学习的思路,学这个之前,希望大家还是要对深度学习和tensorflow有些了解。下面讲的实质性知识的东西,全部来源于tensorflow的源码和官网版本的API文档。给个链接吧:

  • tensorflow源码:https://github.com/tensorflow
  • tensorflow1.15.0文档:https://tensorflow.google.cn/versions/r1.15/api_docs/python/tf/keras/wrappers
  • tensorflow keras文档:https://tensorflow.google.cn/guide/keras

我的目标是学的1.14,但是1.14文档因为一些原因我好像上不去,所以我主要参考的是1.15的文档,在更新公告上没有对keras进行太大幅度的更新,因此不用太担心。

tf.keras生态

keras本身是一套tensorflow老版本的优化方案,由于其便捷性一直受到大家欢迎,因此google将其收编也是大势所趋,合并的具体版本并不清楚,但是在1.14版本已经成型,因此我以这个版本为基础进行自己的学习,有API更新的我后续也会跟进。

接着说tf.keras,最简单的了解一个包,其实就是用help了,我们来看看help(tensorflow.keras)会发生什么,内容比较多,这里我只想把这玩意下属的包给列举出来,特殊的我加点解释:

PACKAGE CONTENTS
    activations (package) # 激活函数
    applications (package) # 应用,预装的一些固定结构,例如mobilenet
    backend (package) # 后端,底层函数,类似绝对值、点积之类的都有
    callbacks (package) # 回调函数接口
    constraints (package) # 训练过程中对函数的约束,如MaxNorm
    datasets (package) # 数据集操作
    estimator (package) # 基于keras构造的estimator类
    experimental (package) # 一些实验模块会放在这里,例如学习率变化策略
    initializers (package) # 初始化工具
    layers (package) # 各种深度学习的层
    losses (package) # 各种损失函数定义
    metrics (package) # 各种评估指标,可用于训练过程监测
    mixed_precision (package)
    models (package) # 就是keras里的model类,组合各种层形成模型
    optimizers (package) # 优化器
    preprocessing (package) # 预处理工具
    regularizers (package) # 正则化
    utils (package) # 各种工具函数
    wrappers (package) # 实现多个工具共通,目前实现了sklearn的

可见tf.keras实现了大量的功能,形成了相对完备的深度学习生态,这个完整的框架能让我们轻松实现深度学习。

当然看API学习本身缺少系统性,API更适合学完之后的深入学习或者是平时的词典查阅,学习还是要系统性的。

建模框架

如何写模型应该是初学者最关心的问题,keras建模主要有两种模式,分别是序列式和函数式。

序列式建模

链接:https://tensorflow.google.cn/guide/keras/sequential_model

序列式建模是keras最基本的结构,简单的理解就是和搭积木一样一个接着一个的堆叠就好了,来看看例子:

# Define Sequential model with 3 layers
model = keras.Sequential(
    [
        layers.Dense(2, activation="relu", name="layer1"),
        layers.Dense(3, activation="relu", name="layer2"),
        layers.Dense(4, name="layer3"),
    ]
)# Call model on a test input
x = tf.ones((3, 3))
y = model(x)

通过keras.Sequential将每一个层堆叠起来,这个堆叠的实际上就是keras.layers的对象,如上图所示就是3个全连接层,这样子堆叠相比古老的tensorflow.nn就避免了计算每一层输入输出的维数的问题,很方便。

当然,除了直接构造一个keras.layers对象向量直接放入Sequential之外,还可以用add的方式进行放入。

model = keras.Sequential()
model.add(layers.Dense(2, activation="relu"))
model.add(layers.Dense(3, activation="relu"))
model.add(layers.Dense(4))

构造完之后,如果想看看自己的建模内容,可以用``model.summary`查看并汇总,上面的模型执行后的效果如下:

Model: "sequential_3"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense_7 (Dense)              (1, 2)                    10        
_________________________________________________________________
dense_8 (Dense)              (1, 3)                    9         
_________________________________________________________________
dense_9 (Dense)              (1, 4)                    16        
=================================================================
Total params: 35
Trainable params: 35
Non-trainable params: 0

函数式建模

函数式建模是keras有一种建模框架,文档:https://tensorflow.google.cn/guide/keras/functional。

函数式是一种更为灵活的模式,对于更复杂的网络,就可以用它来整。来看一个完整的例子:

inputs = keras.Input(shape=(784,))
dense = layers.Dense(64, activation="relu")
x = dense(inputs)
x = layers.Dense(64, activation="relu")(x)
outputs = layers.Dense(10)(x)
model = keras.Model(inputs=inputs, outputs=outputs, name="mnist_model")

首先初始化了一个输入点,然后后面构造的dense对象,这个对象是可调用的(内部其实就是有一个call函数),调用了这个inputs,这就代表了在inputs后接了一个dense层,一个接着一个,就能实现整体建模,我们用``model.summary`看看:

Model: "mnist_model"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_1 (InputLayer)         [(None, 784)]             0         
_________________________________________________________________
dense (Dense)                (None, 64)                50240     
_________________________________________________________________
dense_1 (Dense)              (None, 64)                4160      
_________________________________________________________________
dense_2 (Dense)              (None, 10)                650       
=================================================================
Total params: 55,050
Trainable params: 55,050
Non-trainable params: 0
_________________________________________________________________

如我们所料就是一个完整网络。

有了网络,就可以开始训练和预测了,整个过程也不复杂:

# 加载数据
(x_train, y_train), (x_test, y_test) = keras.datasets.mnist.load_data()# 数据预处理
x_train = x_train.reshape(60000, 784).astype("float32") / 255
x_test = x_test.reshape(10000, 784).astype("float32") / 255# 模型编译
model.compile(
    loss=keras.losses.SparseCategoricalCrossentropy(from_logits=True),
    optimizer=keras.optimizers.RMSprop(),
    metrics=["accuracy"],
)# 训练模型
history = model.fit(x_train, y_train, batch_size=64, epochs=2, validation_split=0.2)# 模型效果评估
test_scores = model.evaluate(x_test, y_test, verbose=2)
print("Test loss:", test_scores[0])
print("Test accuracy:", test_scores[1])

刚才说到,函数式能构造更为复杂的模型,我们来看一个例子,我们先看一张模型的结构图:

keras 与tensorflow版本匹配_建模

可以清楚看到,模型有多个输入和多个输出,这种模型结构就只能用函数式来处理了。

num_tags = 12  # Number of unique issue tags
num_words = 10000  # Size of vocabulary obtained when preprocessing text data
num_departments = 4  # Number of departments for predictions# 模型的输入部分,一共3个输入
title_input = keras.Input(
    shape=(None,), name="title"
)  # Variable-length sequence of ints
body_input = keras.Input(shape=(None,), name="body")  # Variable-length sequence of ints
tags_input = keras.Input(
    shape=(num_tags,), name="tags"
)  # Binary vectors of size `num_tags`# 对模型的输入分别进行处理# Embed each word in the title into a 64-dimensional vector
title_features = layers.Embedding(num_words, 64)(title_input)# Embed each word in the text into a 64-dimensional vector
body_features = layers.Embedding(num_words, 64)(body_input)# Reduce sequence of embedded words in the title into a single 128-dimensional vector
title_features = layers.LSTM(128)(title_features)# Reduce sequence of embedded words in the body into a single 32-dimensional vector
body_features = layers.LSTM(32)(body_features)# 处理以后将他们拼接起来# Merge all available features into a single large vector via concatenation
x = layers.concatenate([title_features, body_features, tags_input])# Stick a logistic regression for priority prediction on top of the features
priority_pred = layers.Dense(1, name="priority")(x)# Stick a department classifier on top of the features
department_pred = layers.Dense(num_departments, name="department")(x)# 整理结果,完成输出# Instantiate an end-to-end model predicting both priority and department
model = keras.Model(
    inputs=[title_input, body_input, tags_input],
    outputs=[priority_pred, department_pred],
)

当然的,构造好模型以后,就需要编译模型,定义好训练方式,针对这种多目标的问题还要设计好对应的权重,一般的有两种方式:

# 向量形式的损失函数定制
model.compile(
    optimizer=keras.optimizers.RMSprop(1e-3),
    loss=[
        keras.losses.BinaryCrossentropy(from_logits=True),
        keras.losses.CategoricalCrossentropy(from_logits=True),
    ],
    loss_weights=[1.0, 0.2],
)# 字典形式的损失函数定制
model.compile(
    optimizer=keras.optimizers.RMSprop(1e-3),
    loss={"priority": keras.losses.BinaryCrossentropy(from_logits=True),"department": keras.losses.CategoricalCrossentropy(from_logits=True),
    },
    loss_weights=[1.0, 0.2],
)

并在训练过程中按照要求灌入数据,完成训练:

# Dummy input data
title_data = np.random.randint(num_words, size=(1280, 10))
body_data = np.random.randint(num_words, size=(1280, 100))
tags_data = np.random.randint(2, size=(1280, num_tags)).astype("float32")# Dummy target data
priority_targets = np.random.random(size=(1280, 1))
dept_targets = np.random.randint(2, size=(1280, num_departments))
model.fit(
    {"title": title_data, "body": body_data, "tags": tags_data},
    {"priority": priority_targets, "department": dept_targets},
    epochs=2,
    batch_size=32,
)

当然,还有一些诸如共享层、权重提取、重用之类的场景,keras都有实现,详情可以看这个链接:https://tensorflow.google.cn/guide/keras/functional。

自定义层

类似transformer之类的,都是对模型的创新,这时候我们需要自己去写了,只需要按照keras给定的API去写就行:

class CustomDense(layers.Layer):def __init__(self, units=32):
        super(CustomDense, self).__init__()
        self.units = unitsdef build(self, input_shape):
        self.w = self.add_weight(
            shape=(input_shape[-1], self.units),
            initializer="random_normal",
            trainable=True,
        )
        self.b = self.add_weight(
            shape=(self.units,), initializer="random_normal", trainable=True
        )def call(self, inputs):return tf.matmul(inputs, self.w) + self.bdef get_config(self):return {"units": self.units}
inputs = keras.Input((4,))
outputs = CustomDense(10)(inputs)
model = keras.Model(inputs, outputs)
config = model.get_config()
new_model = keras.Model.from_config(config, custom_objects={"CustomDense": CustomDense})

类里面有几个需要注意的点:

  • 继承layers,并定义好对应的构造函数__init__
  • build中可以进一步对参数进行定义,在第一次使用该层的时候调用该部分代码,在这里创建变量可以使得变量的形状自适应输入的形状,并且给里面的参数进行初始化,可以看到里面有个initializer
  • call,在函数式下,执行这一层的时候会调用这个函数进行前向计算。像上面的例子就是做了一个线性变换。
  • 如果是需要支持序列化,则需要用get_config或者是from_config的方式把必要的超参返回出来。

有关自定义新层的方法,详见:https://tensorflow.google.cn/guide/keras/custom_layers_and_models

训练、评估和预测

上面讲的都是模型的建立,现在我们就要谈谈怎么怎么折腾这个模型了。

首先是训练,在进行建模和编译以后,就可以开始训练了:

# 模型编译
model.compile(
    optimizer=keras.optimizers.RMSprop(),  # Optimizer# Loss function to minimize
    loss=keras.losses.SparseCategoricalCrossentropy(),# List of metrics to monitor
    metrics=[keras.metrics.SparseCategoricalAccuracy()],
)# 开始训练
history = model.fit(
    x_train,
    y_train,
    batch_size=64,
    epochs=2,# We pass some validation for# monitoring validation loss and metrics# at the end of each epoch
    validation_data=(x_val, y_val),
)

在进行训练以后就可以进行评估和预测了:

results = model.evaluate(x_test, y_test, batch_size=128)
predictions = model.predict(x_test[:3])

注意,这里的predict其实只是把输出层的预测结果给出,如果是分类问题,可能还要做类似筛选最大之类的后处理。

保存模型

保存模型一共分3种:

  • 保存和加载整个模型
  • 保存架构
  • 保存和加载模型的权重

这3种模式都有很明确的使用场景,例如最后一个权重的,就是在模型预测、迁移学习时使用。

保存和加载整个模型

对于这种模式的保存,是最完整的,按照官方说法,一共覆盖着4种东西:

  • 模型的架构和配置
  • 模型权重
  • 编译信息(compile)
  • 优化器状态,最强的是可以继续训练

API也非常简单:

  • model.save()tf.keras.models.save_model()
  • tf.keras.models.load_model()

保存架构

保存架构是只保存模型的结构,而不保存权重。

  • get_config()from_config()
  • tf.keras.models.model_to_json()tf.keras.models.model_from_json()

仅保存权重

  • get_config()from_config()
  • tf.keras.models.model_to_json()tf.keras.models.model_from_json()

到此,整个模型的核心流程已经完成,下面我们看一下keras还为我们提供了什么有意思的功能供我们使用。

值得关注的API

经过上面的详细学习,其实大家已经对tensorflow.keras有足够的了解,剩下就是在实践过程中逐步学习成长,但这里面,也有很多指的阅读的API和源码,有兴趣的大家可以好好看看。

layers

这应该是建模的关键,keras目前已经支持几乎所有主流模型,全连接、卷积、池化、RNN等。有兴趣的多去看看,包括里面涉及的变量,都多了解,对后续自己写自定义层有很大好处。

losses

各种损失函数,常见的如BinaryCrossentropyMeanSquaredError,也有一些比较有意思的HingePoisson等。

activations

激活函数,可以看看有些啥,炼丹的时候可以都尝试尝试,softmaxtanhrelu,也有elu之类的新东西。

preprocessing

预处理,这里分成了三个模块,imagesequencetext,很多的预处理工作其实在这里面都有实现。

metrics

评估指标是评价模型好坏的核心,keras里面也内置了大量的已经写好的函数,AUCAccuracy等。

backend

backend里面有大量非常有用的基础函数,熟悉一下,可以大大减轻自己手写代码的压力。来看看几个例子:

  • argmaxargmindot,各种简单算子。
  • gradients,返回梯度。
  • manual_variable_initialization,人工特征初始化。

小结

至今仍然感觉,看API、看源码、看文档是对一个工具了解的必经之路,看看博客之类的远远无法真正了解他们,即使是我们已经熟练使用了,回头看看文档,也会发现里面有很多好东西,总之,学无止境吧。