今天继续读keras中的Layer源码。我推荐你打开源码和我同步阅读。
293-400
def assert_input_compatibility(self, inputs):
"""Checks compatibility between the layer and provided inputs.
This checks that the tensor(s) `input`
verify the input assumptions of the layer
(if any). If not, exceptions are raised.
# Arguments
inputs: input tensor or list of input tensors.
# Raises
ValueError: in case of mismatch between
the provided inputs and the expectations of the layer.
"""
inputs = to_list(inputs)
for x in inputs:
try:
K.is_keras_tensor(x)
except ValueError:
raise ValueError('Layer ' + self.name + ' was called with '
'an input that isn\'t a symbolic tensor. '
'Received type: ' +
str(type(x)) + '. Full input: ' +
str(inputs) + '. All inputs to the layer '
'should be tensors.')
if not self.input_spec:
return
if not isinstance(self.input_spec, (list, tuple)):
input_spec = to_list(self.input_spec)
else:
input_spec = self.input_spec
if len(inputs) != len(input_spec):
raise ValueError('Layer ' + self.name + ' expects ' +
str(len(input_spec)) + ' inputs, '
'but it received ' + str(len(inputs)) +
' input tensors. Input received: ' +
str(inputs))
for input_index, (x, spec) in enumerate(zip(inputs, input_spec)):
if spec is None:
continue
# Check ndim.
if spec.ndim is not None:
if K.ndim(x) != spec.ndim:
raise ValueError('Input ' + str(input_index) +
' is incompatible with layer ' +
self.name + ': expected ndim=' +
str(spec.ndim) + ', found ndim=' +
str(K.ndim(x)))
if spec.max_ndim is not None:
ndim = K.ndim(x)
if ndim is not None and ndim > spec.max_ndim:
raise ValueError('Input ' + str(input_index) +
' is incompatible with layer ' +
self.name + ': expected max_ndim=' +
str(spec.max_ndim) + ', found ndim=' +
str(K.ndim(x)))
if spec.min_ndim is not None:
ndim = K.ndim(x)
if ndim is not None and ndim < spec.min_ndim:
raise ValueError('Input ' + str(input_index) +
' is incompatible with layer ' +
self.name + ': expected min_ndim=' +
str(spec.min_ndim) + ', found ndim=' +
str(K.ndim(x)))
# Check dtype.
if spec.dtype is not None:
if K.dtype(x) != spec.dtype:
raise ValueError('Input ' + str(input_index) +
' is incompatible with layer ' +
self.name + ': expected dtype=' +
str(spec.dtype) + ', found dtype=' +
str(K.dtype(x)))
# Check specific shape axes.
if spec.axes:
try:
x_shape = K.int_shape(x)
except TypeError:
x_shape = None
if x_shape is not None:
for axis, value in spec.axes.items():
if (value is not None and
x_shape[int(axis)] not in {value, None}):
raise ValueError(
'Input ' + str(input_index) +
' is incompatible with layer ' +
self.name + ': expected axis ' +
str(axis) + ' of input shape to have '
'value ' + str(value) +
' but got shape ' + str(x_shape))
# Check shape.
if spec.shape is not None:
try:
x_shape = K.int_shape(x)
except TypeError:
x_shape = None
if x_shape is not None:
for spec_dim, dim in zip(spec.shape, x_shape):
if spec_dim is not None and dim is not None:
if spec_dim != dim:
raise ValueError(
'Input ' + str(input_index) +
' is incompatible with layer ' +
self.name + ': expected shape=' +
str(spec.shape) + ', found shape=' +
str(x_shape))
在assert_input_compatibility中,检查完tensor后,进入shape的检查。每一个tensor对应一个input_spec,如果对应不上,就会报错。
看到首先检查的是tensor的维度数,即ndim。其次是 max_ndim和min_ndim。
维度检查完毕,就开始检查数据类型。360-366
继续,检查每一个axis的维度数。368-383 这里的检查是比较宽松的,因为允许 维度是None。
紧接着的shape检查就会要求二者严格相等了。385-399
看起来,spec.shape和spec.axes更像是在版本迭代中重叠起来的。为了兼容,一直保留了下来。
401-412

这里是call方法,留给具体的Layer进行实现。其实就是模板设计模式。我们自定义自己的Layer时,通过重写这个方法实现自己的计算逻辑。
413-540
这里是call方法的包装方法,用来处理预处理一些keras的记录信息。

读一下注释,可以发现这个包装方法主要完成四项功能:
调用_add_inbound_node()方法
这里想进一步理解一下inbound_node这个概念。我们的layer创建好后,可以多次调用,如下所示:
myLayer = MyLayer()
o1 = myLayer(inputs1)
o2 = myLayer(inputs2)
inputs1和inputs2是input tensor的列表,里面的每个input都来自某一个上游的Layer的output。但是每个列表中的input未必来自同一个Layer的output。
在上面,由于我们调用了两次myLayer。所以,在myLayer的inbound_nodes数组中就会添加两个node,来分别记录这两次调用的信息。具体记录哪些信息,就在这个包装的__call__方法中。
如果layer还没有built,会进行build方法的调用
更新_keras_shape,这个需要结合代码进行理解
对每一个output tensor, 更新它的_keras_history。这个在上篇文章中已经讲过。每当产生一个新的tensor,及时确定它的三维坐标(layer,node_index,output_tensor_index)
这段注释,我觉得写的非常好。值得学习。提纲挈领地说明了这个方法的内容。这样,在下面的阅读中,就不会迷失在浩繁的细节中。

这里,首先将inputs进行复制。
然后加入了name_scope,因为可能需要调用build方法,添加weight,这样,每个weight就会在该layer的name_scope下了。
name_scope这个概念,我觉得直接理解成命名前缀就可以了。简单易懂。
调用built前,先检查了inputs的各个shape,这个方法我们刚学习过。
通过后,开始依次收集每个input的shape,用来进行build的调用。
获取shape的方法值得学习一下:

看到调用了build。然后将built置为True。
这里,build方法如下:

这也是一个模板方法,如果我们自定义的layer中有weight,就需要重写这个方法,在里面添加我们的weight。感觉这种模板设计特别清晰容易理解。
如果没有build方法,那么自定义layer时就可能在任何地方添加我们的weight,这样肯定会引发混乱,不利于大家交流代码。所以,这个build方法其实就是一个约定。
build完之后,初始化了weights,并再次检查了input shape,因为在build方法中,可能会创建input_spec。这些我们都比较熟悉了。
再往下,就是mask的处理了。这块儿,是比较容易搞乱的地方。需要认真研究一下。
475

看一下,_collect_previous_mask这个方法:

可以看到,首先获取了这个tensor的坐标,然后,找到了对应的node,在node的output_masks中根据tensor_index获取到mask,添加进结果list中。
这里,进一步可以看到Node的作用。存储了每个output tensor的mask。细心体会一下,就能感觉到Node这层抽象的必要性。因为像mask这种信息,显然不适合直接放到layer中。

这里看到,收集了mask信息后,添加到kwargs中,当然,如果你明确传入了mask,就不会用收集到的mask更新。前提是,你的call方法中还要有mask这个参数。这个参数表明了你的layer是support_mask的。
至此,就会实际调用我们自己定义的逻辑。

需要研究一下,是怎么计算mask的。

这里的mask就是简单传递一下。为了找到一个具体例子,我想起来了Embedding这个layer,找到里面的方法:

可以看到,计算是相当直接。就是将等于0的值进行mask。如果你想用其他的值进行mask,就可以继承这个layer,重写相关的方法。
今天内容不少了。先到这里吧。




