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 |
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') |
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') |
结果报错,
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') |
成功,现在densor_layer已经绑定上了这两组输入输出,并为之依次标号0和1,可以调用get_input/output_at
查看之。
for i in (0,1): |
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) |
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)) |
[<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 |
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模型对象中包含了构建模型所需要的足够配置信息。因此,如果需要设计模型调试工具,在一定程度上,可以只利用原模型对象体来构建新的调试模型,而不需要过多的关注原始源文件中的各种构造语句。但是如果需要进一步地添加更多的调试功能,比如将原始模型的训练数据以及训练方式迁移到调试模型中,那么还是需要对源文件进行分析。