Keras模型理解和调试

1. 基本概念

Keras模型都继承自keras.Model类,可以分为两种类型:

  • 顺序(Sequential)模型:单输入单输出,且层与层之间首尾顺序相连,对应于常见的逐层相连的神经网络,可以使用keras.models.Sequential并依次add所需要的层进行快速构建。
  • 使用函数式API的模型:对应于更加复杂的结构,如拥有多个输入或者输出,以及涉及到跨层连接,需要手动组织层之间的数据关系再调用keras.models.Model构建。

从形式和内容角度讲,其分为结构和权重两个内涵:

  • 结构本质是一个图,其中的节点是模型中的层,而节点间的连接边表示数据在层之间的流动。Keras支持通过Python API搭建模型结构,或者从JSON、yaml导入。
  • 权重的本质是一系列张量,模型结构中的层如果是具参数的都对应拥有其权重,但权重数据本身可以独立于结构存在。

另外,超参数的作用在于配置结构参数或者调节训练过程,其本身并不是模型的组成部分。

2. 张量与层

Keras库本身没有定义具体的张量类型,而是只具有张量这一概念,其实现类型则由后端负责。

因此,Keras也没有直接构造张量的API,但是可以间接的构造之。

2.1. 构造输入张量

使用keras.layers.Input函数可以构造一个张量,这些张量作为神经网络的输入,在训练时中需要指定feed_dict为之投喂数据。

import keras

input_tensor = keras.layers.Input(shape=(32,), name='input_tensor')
print(input_tensor)
Tensor("input_tensor:0", shape=(?, 32), dtype=float32)

这里使用的是TensorFlow后端,所以发现input_tensor的类型就是TensorFlow中的张量类。并且,Keras会自动在张量shape的首位添加一个不定大小的维度,对应于batch_size,所以就不在需要额外地考虑这个维度。

2.2. 使用层生成输出张量

或者构建一个层,将一个现有的张量当作函数参数传递给它,返回一个经过层处理后的输出张量

dense_layer = keras.layers.Dense(4, activation='relu', name='dense_layer')
output_tensor = dense_layer(input_tensor)
print(dense_layer.input)
print(dense_layer.output)
Tensor("input_tensor:0", shape=(?, 32), dtype=float32)
Tensor("dense_layer/Relu:0", shape=(?, 4), dtype=float32)

最值得注意的应当是Keras中的层类型除了可以被add进Sequential模型,还可以被当作函数进行调用,原因自然是其中重载了__call__方法。不过这种调用是有副作用的,Keras会将被调用的层与对应的输入输出张量绑定,并记录在对应的对象体内。

比如如果继续调用如下代码,

input_tensor2 = keras.layers.Input(shape=(16,), name='input_tensor2')
output_tensor2 = dense_layer(input_tensor2)

结果报错,

ValueError: Input 0 is incompatible with layer dense_layer: expected axis -1 of input shape to have value 32 but got shape (None, 16)

原因是densor_layer经过第一次调用,已经绑定了input_tensor的形状,再次调用会与input_tensor2的形状产生冲突,因此失败。

所以如果使用的是一致(不一定要相同)的维度,则可以继续进行添加,

input_tensor3 = keras.layers.Input(shape=(2, 32,), name='input_tensor3') 
output_tensor3 = dense_layer(input_tensor3)

成功,现在densor_layer已经绑定上了这两组输入输出,并为之依次标号0和1,可以调用get_input/output_at查看之。

for i in (0,1): 
print(dense_layer.get_input_at(i))
print(dense_layer.get_output_at(i))
Tensor("input_tensor:0", shape=(?, 32), dtype=float32)
Tensor("dense_layer/Relu:0", shape=(?, 4), dtype=float32)
Tensor("input_tensor3:0", shape=(?, 2, 32), dtype=float32)
Tensor("dense_layer_2/Relu:0", shape=(?, 2, 4), dtype=float32)    

3. 函数式API模型

有别于顺序模型通过不断添加Layer构建模型,函数式API模型的构建需要手动组织层间关系,因此也更加灵活。

如上一节所示,先构建层与张量,然后通过指定模型的输入/输出张量完成模型的构建。

可以指定单个输入输出构建简单的模型,

simple_model = keras.Model(inputs=input_tensor, outputs=output_tensor)
print(simple_model.input)
print(simple_model.output)
Tensor("input_tensor:0", shape=(?, 32), dtype=float32)
Tensor("dense_layer/Relu:0", shape=(?, 4), dtype=float32)
graph TB
input_tensor((input_tensor ?x32)) --> dense_layer[dense_layer Weight:32x4 bias:4]
dense_layer --> output_tensor((output_tensor ?x4))

或者以将输入输出指定为含有多个元素的列表,使模型拥有更加复杂的结构,

complex_model = keras.Model(inputs=(input_tensor, input_tensor3), outputs=(output_tensor, output_tensor3))
print(complex_model.input)
print(complex_model.output)
[<tf.Tensor 'input_tensor:0' shape=(?, 32) dtype=float32>, <tf.Tensor 'input_tensor3:0' shape=(?, 2, 32) dtype=float32>]
[<tf.Tensor 'dense_layer/Relu:0' shape=(?, 4) dtype=float32>, <tf.Tensor 'dense_layer_2/Relu:0' shape=(?, 2, 4) dtype=float32>]
graph TB
input_tensor((input_tensor ?x32)) --> dense_layer[dense_layer Weight:32x4 bias:4]
dense_layer --> output_tensor((output_tensor ?x4))
input_tensor3((input_tensor3 ?x2x32)) --> dense_layer
dense_layer --> output_tensor3((output_tensor3 ?x2x4))
print(complex_model.count_params())

输出为132(32×4+4),参数的数量也只有一个层的那么多,可见两组输入输出是公用的同一个全连接层。

编译模型,

complex_model.compile(loss='mean_squared_error', optimizer='adam')

训练模型时,也必须同时指定两组样本以及标签,

import numpy as np 

batch_size = 10
x = np.random.random((batch_size, 32)).astype(np.float32)
y = np.random.random((batch_size, 4)).astype(np.float32)
x3 = np.random.random((batch_size, 2, 32)).astype(np.float32)
y3 = np.random.random((batch_size, 2, 4)).astype(np.float32)

complex_model.fit([x, x3], [y, y3])

4. 模型调试

如果需要对模型进行白盒调试 ,所需要的操作可以分为两种类型:查看和编辑,也就是读和写。

4.1. 查看操作

查看操作较为简单,对于需要观察数据的断面,只需先找到的其对应的层,再通过获得层的input/output得到对应的张量,利用断面张量以及原模型整体输入等,使用函数式API模型便可以很方便地构建一个子模型。

不过就像前面所说,因为张量与层是互相绑定的,所以所得到的子模型将会与原模型共用内部的层对象,当然也就包括其中的参数。

4.2. 编辑操作

Keras的本意是用来构建模型的,而不是用来编辑,其中唯一与模型编辑有关的API便是keras.models.Sequential.pop,只能用来移除顺序模型的最后一个层,作用相当有限,所以对模型进行编辑也需要间接进行,这可以有两种方式。

  • 直接在原模型的基础上进行编辑

    有点类似查看操作中的方法,找到对应的张量,然后构建利用它再模型吗,如果有必要,可以建立新的层对张量进行进一步处理。这种方法的较为便捷,但是需要注意原模型和编辑模型虽然是两个不同的模型对象,但是它们却在内部共用了相同的层对象,因此对这些层的修改也会反馈到原模型中。
    其中一个值得注意的地方是,Keras模型虽然内部可以包含层,但其本身是层类型的子类,

    import inspect

    print(inspect.getmro(type(simple_model)))
    print(inspect.getmro(type(dense_layer)))
    (<class 'keras.engine.training.Model'>, <class 'keras.engine.network.Network'>, <class 'keras.engine.base_layer.Layer'>, <class 'object'>)
    (<class 'keras.layers.core.Dense'>, <class 'keras.engine.base_layer.Layer'>, <class 'object'>)

    所以,可以将整个模型作为一个层进行各种,置入新模型中,而原模型此时则成为了一个子模型。

  • 另起炉灶构建新的模型

    这种方法可以构建一个与原模型完全独立的新模型,二者之间可以做到互不影响,不过过程比较复杂。需要先获取原模型的结构信息,将其配置导出(比如导出为JSON格式等),然后在其基础上进行所需要的修改,利用配置建立新层并组装为新模型。

5. 后记

总的看来,Keras模型对象中包含了构建模型所需要的足够配置信息。因此,如果需要设计模型调试工具,在一定程度上,可以只利用原模型对象体来构建新的调试模型,而不需要过多的关注原始源文件中的各种构造语句。但是如果需要进一步地添加更多的调试功能,比如将原始模型的训练数据以及训练方式迁移到调试模型中,那么还是需要对源文件进行分析。