2018年12月21日星期五

Keras源码分析(7):Layer

文件:/keras/engine/base_layer.py

在前面第5节我们已经接触到一个简单而特别的层:InputLayer,它继承自基类:Layer。尽管简单,但关于层的基本逻辑倒也清楚明白。

在Node一节中,我们也说了许多与Node相关的Layer的内容。接下来我们仍需对这个基类Layer作进一步讨论。

class Layer(object):

这是Layer的主要处理逻辑所在,给定输入,产生输出。因为各种layer作用的不同,所以在些基类中无法做具体实现,它们的具体功能留待各种子类去实现。
    def call(self, inputs, **kwargs):
        return inputs

这个函数对call进行了进一步的包装。下面的分析中,我们仅保留了代码的主要部分,而省略了一些次要的代码,目的是让它的逻辑显得更加清晰。
    def __call__(self, inputs, **kwargs):

根据self.built标志确定是否需要build。如果需要,则首先要从inputs中获取input_shapes,这是build函数所唯一需要的,然后,把收集到的input_shapes传给build,执行build操作。build的作用在下面有解释,在此基类中build实际是个空操作。
        if not self.built:
            input_shapes = []
            for x_elem in to_list(inputs):
                if hasattr(x_elem, '_keras_shape'):
                    input_shapes.append(x_elem._keras_shape)
                elif hasattr(K, 'int_shape'):
                    input_shapes.append(K.int_shape(x_elem))
                else:
                    raise ValueError('......')

            self.build(unpack_singleton(input_shapes))

此处调用call,实现本层的主要逻辑,获得output。在此基类中call函数仅仅是将inputs原样奉还。
        output = self.call(inputs, **kwargs)
        ......

        # 如果call未对inputs进行修改,为避免inputs元数据的丢失,需要对它进行复制
        output_ls = to_list(output)
        inputs_ls = to_list(inputs)
        output_ls_copy = []
        for x in output_ls:
            if x in inputs_ls:
                x = K.identity(x)
            output_ls_copy.append(x)
        output = unpack_singleton(output_ls_copy)
        ......
 
        # 调用_add_inbound_node创建层间连接并保存历史
        self._add_inbound_node(input_tensors=inputs,
                               output_tensors=output,
                               input_masks=previous_mask,
                               output_masks=output_mask,
                               input_shapes=input_shape,
                               output_shapes=output_shape,
                               arguments=user_kwargs)
        ......

        #返回本层输出
        return output


这是一个内部方法,用来创建输入方向的node
    def _add_inbound_node(self, input_tensors, output_tensors,
                          input_masks, output_masks,
                          input_shapes, output_shapes, arguments=None):
        input_tensors = to_list(input_tensors)
        output_tensors = to_list(output_tensors)
        input_masks = to_list(input_masks)
        output_masks = to_list(output_masks)
        input_shapes = to_list(input_shapes)
        output_shapes = to_list(output_shapes)

遍历input_tensors,从每个input_tensor._keras_history中得到input_layer,node_index和tensor_index,把它们分别放进inbound_layers,node_indices和tensor_indices中。
        inbound_layers = []
        node_indices = []
        tensor_indices = []
        for x in input_tensors:
            if hasattr(x, '_keras_history'):
                inbound_layer, node_index, tensor_index = x._keras_history
                inbound_layers.append(inbound_layer)
                node_indices.append(node_index)
                tensor_indices.append(tensor_index)
            else:
                inbound_layers.append(None)
                node_indices.append(None)
                tensor_indices.append(None)

从前面Node一节我们已经知道,这个新的Node对象将把自己加到当前layer的_inbound_nodes列表中,同时也加到所有inbound_layer的_outbound_nodes列表中。
        Node(
            self,
            inbound_layers=inbound_layers,
            node_indices=node_indices,
            tensor_indices=tensor_indices,
            input_tensors=input_tensors,
            output_tensors=output_tensors,
            input_masks=input_masks,
            output_masks=output_masks,
            input_shapes=input_shapes,
            output_shapes=output_shapes,
            arguments=arguments
        )

更新输出output_tensors的history, _keras_shape和_uses_learning_phase。
        for i in range(len(output_tensors)):
            output_tensors[i]._keras_shape = output_shapes[i]
            uses_lp = any(
                [getattr(x, '_uses_learning_phase', False)
                 for x in input_tensors])
            uses_lp = getattr(self, 'uses_learning_phase', False) or uses_lp
            output_tensors[i]._uses_learning_phase = getattr(
                output_tensors[i], '_uses_learning_phase', False) or uses_lp
            output_tensors[i]._keras_history = (self, len(self._inbound_nodes) - 1, i)

对于上面的代码,我们把解释的重点放在最后一句上,因为它让我们清楚地知道,在output tensor的_keras_history中到底放进了什么。
(1)self:输出该第i个tensor的layer对象
(2)len(self._inbound_nodes) - 1:与第i个tensor相关的node对象在当前layer._inbound_nodes中的位置,即node_index
(3)i: 当然是第i个tensor在output_tensors中的位置,即:tensor_index

根据input_shape,创建该层的权重weights。不同的层可能有不同的创建方法,故留待子类去实现
    def build(self, input_shape):
        self.built = True

获取layer与给定node相关的输出tensor(s)
    def get_output_at(self, node_index):
        return self._get_node_attribute_at_index(node_index,
                                                 'output_tensors',
                                                 'output')

获取在layer._inbound_nodes[node_index]位置的node中由attr指定属性的值values
    def _get_node_attribute_at_index(self, node_index, attr, attr_name):
        ......
        values = getattr(self._inbound_nodes[node_index], attr)
        return unpack_singleton(values)

2018年12月20日星期四

Keras源码分析(6):Node

文件:/keras/engine/base_layer.py

在上一节,我们提到了Node,它的作用是用来联结两个层。这一节我们具体来看一看Node是如何联结两个层的。

对于Node这个命名,一开始会让人觉提很纳闷:为什么叫Node,难道它是一个神经元节点吗?它到底是什么,我们还是来看看源码吧。
class Node(object):
    def __init__(self, outbound_layer,
                 inbound_layers, node_indices, tensor_indices,
                 input_tensors, output_tensors,
                 input_masks, output_masks,
                 input_shapes, output_shapes,
                 arguments=None):
从Node对象的成员变量,我们来看看Node对象中到底保存了什么:
(1)首先是输出层,也是该Node对象被new出来的那个层,该层可以看作是我们分析Node的立足点或参照点
        self.outbound_layer = outbound_layer
(2)输入层列表,因为可能有多个输入,所以是个列表
        self.inbound_layers = inbound_layers
(3)输入和输出张量列表,因为可能有多个输入输出,且它们一一对应
        self.input_tensors = input_tensors
        self.output_tensors = output_tensors
(4)输入和输出mask张量列表,它们也是和输入输出一一对应
        self.input_masks = input_masks
        self.output_masks = output_masks
(5)输入和输出张量shape列表,它们也是和输入输出一一对应
        self.input_shapes = input_shapes
        self.output_shapes = output_shapes
(6)要解释清楚下面的变量,需要多一些口舌,解释我们放在后面。
        self.node_indices = node_indices
        self.tensor_indices = tensor_indices
这就是说,Node对象要保存的信息都是从外部传进来的,所以要想真正全面理解Node,我们还需要深入探寻创建和使用它的代码的上下文。

下面是Node对象的关键处理:它把自己分别追加到outbound_layer._inbound_nodes和inbound_layers中的每个layer._outbound_nodes的列表中。这样做的目的,显然是建立起从输入层到输出层关联。现在你可以画一个流程图,左边是几个输入层,右边是一个输出层,中间再画一个圆圈并标上Node,用线把它与左右两边的层连起来,此时,你看这个Node像节点吗?
        for layer in inbound_layers:
            if layer is not None:
                layer._outbound_nodes.append(self)
        outbound_layer._inbound_nodes.append(self)

现在我们回头来解释上面的第(6)条:

首先说说Node对象是在什么时候产生的。每当我们要建立一个网络时,我们需要new一些层,当传入tensors给它们的时候,就会调用这些层的__call__方法,这些层的__call__方法每调用一次,就会产生一个新的Node对象,并把当前的layer作为outbound_layer传给该Node对象,接下来该Node对象把自己分别放到outbound_layer._inbound_nodes列表和每一个inbound_layers._outbound_nodes的列表中,这一点我们在上面已经看到了。对于单输入输出,这种映射关系很清楚,到此一切都OK,但是对于有多个输入源的层,则问题就复杂了点。例如:
   a = Input(shape=(280, 256))
   b = Input(shape=(280, 256))
   lstm = LSTM(32)
   encoded_a = lstm(a)
   encoded_b = lstm(b)
当第一次调用lstm(a)的时候,它创建了一个node,并把这个node放到lstm的_inbound_nodes的第0个位置,第二次调用lstm(b)的时候,它又创建了一个node,并把这个node放到lstm的_inbound_nodes的第1个位置。当我们要想获取它们的输出时,我们可以这样做:
    encoded_a = lstm.get_output_at(0) 
    encoded_a = lstm.get_output_at(1)
这就是说,我们想要得到lstm的某个输出,不是简单地用lstm.output,而是用lstm.get_output_at(index)。

像上面这种情况,一个layer的输入可能是多个tensor的列表,进而产生的输出也是多个输出tensor的列表,所以,由这样的层作为inbound_layer的时候,我们需要知道input_tensor在inbound_layer的输出tensor列表中的位置,即:tensor_index。

对于像这样一个layer对象的输入和输出可能是一个列表的情况,我们要想从output_tensors中得到某个输出,则必须指定它在这个输出列表中的索引(tensor_index)。

从例子中我们把思路拉回来,考虑当前layer(outbound_layer)的所有输入,把与它们相关的node_index和tensor_index收集起来,分别依次放进两个node_indices和tensor_indices中,然后传递给该新建的node,并保存在它相应的成员变量中。这样,结合tensor._keras_history,我们把输出和输入的关系也就建立起来了。

尽管只是简单几行代码,解释起来并不简单。总之,通过许多Node对象把层和层,输入和输出连成了一个有向图,让它们彼此可以相互追溯。现在你可以再画一个图,左边是输入,右边是输出,中间再画一个圆圈并标上Node,用线把它与左右两边连起来,现在你再看这个Node像节点吗?

2018年12月16日星期日

Keras源码分析(5):Tensor、Input 和 InputLayer

文件:/keras/engine/input_layer.py

Tensor,翻译过来就是张量。

TensorFlow支持两种类型的Tensor,一种是立即执行的(eager execution),一种是图执行的(graph execution)。图执行的Tensor一般只是一个空壳(placeholder),没有值(tensor_content),它只定义了维度(shape)和数据类型(dtype),当然还有部分定义好的方法,待未来填值计算。TensorFlow将那些待计算的tensor组织成有向图(graph),后面tensor的计算依赖它前面的tensor的计算结果,因此,只要位于图入口处的那些tensor填入了值,就可执行图计算,就能输出结果。

Keras是建立在Tensorflow,CNTK或Theano之上的是一个高级神经网络库。Keras中的Tensor对底层TensorFlow或Theano的张量进行了扩展,加入了如下两个属性:
_Keras_history: 保存最近作用于这个tensor上的Layer对象及有关元数据。
_keras_shape: 保存(batch_size, input_dim,)与输入数据有关的维度大小的元组

张量是深度学习框架中的一个非常核心的概念,模型(内部)的计算都是基于张量进行的。

例子:
from keras.layers import Input, Dense
from keras.models import Model

inputs = Input(shape=(784,))

x = Dense(64, activation='relu')(inputs)
x = Dense(64, activation='relu')(x)
predictions = Dense(10, activation='softmax')(x)

model = Model(inputs=inputs, outputs=predictions)
model.compile(optimizer='rmsprop',
              loss='categorical_crossentropy',
              metrics=['accuracy'])
model.fit(data, labels)
上述的inputs和predictions都是tensor。显然,这里的tensor是一个图执行的tensor,由model.fit填入数据,然后执行计算。

下面来看Input是如何定义的:
def Input(shape=None, batch_shape=None,
          name=None, dtype=None, sparse=False,
          tensor=None):
    ......
    if shape is not None and not batch_shape:
        batch_shape = (None,) + tuple(shape)
    if not dtype:
        dtype = K.floatx()
    input_layer = InputLayer(batch_input_shape=batch_shape,
                             name=name, dtype=dtype,
                             sparse=sparse,
                             input_tensor=tensor)
    outputs = input_layer._inbound_nodes[0].output_tensors
    return unpack_singleton(outputs)
代码中引入了一个InputLayer的对象,它是整个网络的起始输入层,它实际做的只是对原始数据的原样输出。从这里可知这个Input实际上在内部定义了一个输入层,并把该输入层的输出作为自己的输出返回, 也即返回这里的input_tensor,如果它不是None;否则返回下面代码中的K.placeholder对象。
unpack_singleton(outputs):如果outputs只有一个元素,则返回outputs[0],否则返回outputs。

下面我们来看InputLayer的源码:
class InputLayer(Layer):

    @interfaces.legacy_input_support
    def __init__(self, input_shape=None, batch_size=None,
                 batch_input_shape=None,
                 dtype=None, input_tensor=None, sparse=False, name=None):
        ......
        self.trainable = False
        self.built = True
        self.sparse = sparse
        self.supports_masking = True
        ......
        self.batch_input_shape = batch_input_shape
        self.dtype = dtype

        if input_tensor is None:
            self.is_placeholder = True
            input_tensor = K.placeholder(shape=batch_input_shape,
                                         dtype=dtype,
                                         sparse=self.sparse,
                                         name=self.name)
        else:
            self.is_placeholder = False
            input_tensor._keras_shape = batch_input_shape

        input_tensor._uses_learning_phase = False
        input_tensor._keras_history = (self, 0, 0)
        Node(self,
             inbound_layers=[],
             node_indices=[],
             tensor_indices=[],
             input_tensors=[input_tensor],
             output_tensors=[input_tensor],
             input_masks=[None],
             output_masks=[None],
             input_shapes=[batch_input_shape],
             output_shapes=[batch_input_shape])

其中省略了一些对函数参数处理的代码。 对于NN中的每一层,不外乎三个部分:输入、处理(data的正向传播和error的反向传播)和输出。 在InputLayer的这段init的代码中,首先是对输入input_tensor的处理,如果是None, 则产生一个placeholder作为input tensor。从这里可看到所谓的tensor的占位符的特性。
接下来是一个逻辑处理和理解上的一个关键部分:new一个输入节点(Node)对象。Node对象的作用是用来联结两个层,我们将另辟章节对它进行分析,在这里我们只粗略地讨论一下传给它的参数。
Node的参数有4个主要部分:
(1)layers,包括outbound_layer和inbound_layers,这里outbound_layer接受的是这个InputLayer对象本身(self),inbound_layers=[],因为InputLayer是第一个输入层,所以它的inbound_layers是空。
(2)tensors,包括输入张量和输出张量,它们都等于[input_tensor],即:input_tensors=[input_tensor],output_tensors=[input_tensor],所以说这一层其实什么也没干。
(3)shapes,很自然地,输入和输出的维度参数(包括batch_size)都是[batch_input_shape],即:input_shapes=[batch_input_shape], output_shapes=[batch_input_shape]
(4)indices,这是一个很难理解的东东,需要在后面展开来讨论。 这里且注意这样一个细节就是output tensor(这里是input_tensor)的_keras_history,它的值的形式是(layer, node_index, tensor_index)这样的3元组,这里赋的值是(self, 0, 0)。