前段时间因为课题需要使用了一段时间TensorFlow,感觉这种框架很有意思,除了可以搭建复杂的神经网络,也可以优化其他自己需要的计算模型,所以一直想自己学习一下写一个类似的图计算框架。前几天组会开完决定着手实现一个模仿TensorFlow接口的简陋版本图计算框架以学习计算图程序的编写以及前向传播和反向传播的实现。目前实现了前向传播和反向传播以及梯度下降优化器,并写了个优化线性模型的例子。

代码放在了GitHub上,取名SimpleFlow, 仓库链接: https://github.com/PytLab/simpleflow

虽然前向传播反向传播这些原理了解起来并不是很复杂,但是真正着手写起来才发现,里面还是有很多细节需要学习和处理才能对实际的模型进行优化(例如Loss函数对每个计算节点矩阵求导的处理)。其中SimpleFlow的代码并没有考虑太多的东西比如dtype和张量size的检查等,因为只是为了实现主要图计算功能并没有考虑任何的优化, 内部张量运算使用的Numpy的接口(毕竟是学习和练手的目的嘛)。好久时间没更新博客了,在接下来的几篇里面我将把实现的过程的细节总结一下,希望可以给后面学习的童鞋做个参考。

正文

本文主要介绍计算图以及前向传播的实现, 主要涉及图的构建以及通过对构建好的图进行后序遍历然后进行前向传播计算得到具体节点上的输出值。

先贴上一个简单的实现效果吧:

 
 
 
 
 

Python

 
1
2
3
4
5
6
7
8
9
import simpleflow as sf
# Create a graph
with sf.Graph().as_default():
    a = sf.constant(1.0, name='a')
    b = sf.constant(2.0, name='b')
    result = sf.add(a, b, name='result')
    # Create a session to compute
    with tf.Session() as sess:
        print(sess.run(result))

计算图(Computational Graph)

计算图是计算代数中的一个基础处理方法,我们可以通过一个有向图来表示一个给定的数学表达式,并可以根据图的特点快速方便对表达式中的变量进行求导。而神经网络的本质就是一个多层复合函数, 因此也可以通过一个图来表示其表达式。

本部分主要总结计算图的实现,在计算图这个有向图中,每个节点代表着一种特定的运算例如求和,乘积,向量乘积,平方等等… 例如求和表达式$f(x,y)=x+y$使用有向图表示为:

表达式$f(x,y,z)=z(x+y)$使用有向图表示为:

与TensorFlow的实现不同,为了简化,在SimpleFlow中我并没有定义Tensor类来表示计算图中节点之间的数据流动,而是直接定义节点的类型,其中主要定义了四种类型来表示图中的节点:

  1. Operation: 操作节点主要接受一个或者两个输入节点然后进行简单的操作运算,例如上图中的加法操作和乘法操作等。
  2. Variable: 没有输入节点的节点,此节点包含的数据在运算过程中是可以变化的。
  3. Constant: 类似Variable节点,也没有输入节点,此节点中的数据在图的运算过程中不会发生变化
  4. Placeholder: 同样没有输入节点,此节点的数据是通过图建立好以后通过用户传入的

其实图中的所有节点都可以看成是某种操作,其中VariableConstantPlaceholder都是一种特殊的操作,只是相对于普通的Operation而言,他们没有输入,但是都会有输出(像上图中的xx, yy节点,他们本身输出自身的值到++节点中去),通常会输出到Operation节点,进行进一步的计算。

下面我们主要介绍如何实现计算图的基本组件: 节点和边。

Operation节点

节点表示操作,边代表节点接收和输出的数据,操作节点需要含有以下属性:

  1. input_nodes: 输入节点,里面存放与当前节点相连接的输入节点的引用
  2. output_nodes: 输出节点, 存放以当前节点作为输入的节点,也就是当前节点的去向
  3. output_value: 存储当前节点的数值, 如果是Add节点,此变量就存储两个输入节点output_value的和
  4. name: 当前节点的名称
  5. graph: 此节点所属的图

下面我们定义了Operation基类用于表示图中的操作节点(详见https://github.com/PytLab/simpleflow/blob/master/simpleflow/operations.py):

 
 
 
 
 
 

Python

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Operation(object):
    ''' Base class for all operations in simpleflow.
    An operation is a node in computational graph receiving zero or more nodes
    as input and produce zero or more nodes as output. Vertices could be an
    operation, variable or placeholder.
    '''
    def __init__(self, *input_nodes, name=None):
        ''' Operation constructor.
        :param input_nodes: Input nodes for the operation node.
        :type input_nodes: Objects of `Operation`, `Variable` or `Placeholder`.
        :param name: The operation name.
        :type name: str.
        '''
        # Nodes received by this operation.
        self.input_nodes = input_nodes
        # Nodes that receive this operation node as input.
        self.output_nodes = []
        # Output value of this operation in session execution.
        self.output_value = None
        # Operation name.
        self.name = name
        # Graph the operation belongs to.
        self.graph = DEFAULT_GRAPH
        # Add this operation node to destination lists in its input nodes.
        for node in input_nodes:
            node.output_nodes.append(self)
        # Add this operation to default graph.
        self.graph.operations.append(self)
    def compute_output(self):
        ''' Compute and return the output value of the operation.
        '''
        raise NotImplementedError
    def compute_gradient(self, grad=None):
        ''' Compute and return the gradient of the operation wrt inputs.
        '''
        raise NotImplementedError

在初始化方法中除了定义上面提到的属性外,还需要进行两个操作:

  1. 将当前节点的引用添加到他输入节点的output_nodes这样可以在输入节点中找到当前节点。
  2. 将当前节点的引用添加到图中,方便后面对图中的资源进行回收等操作

另外,每个操作节点还有两个必须的方法: comput_outputcompute_gradient. 他们分别负责根据输入节点的值计算当前节点的输出值和根据操作属性和当前节点的值计算梯度。关于梯度的计算将在后续的文章中详细介绍,本文只对节点输出值的计算进行介绍。

下面我以求和操作为例来说明具体操作节点的实现:

 
 
 
 
 

Python

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Add(Operation):
    ''' An addition operation.
    '''
    def __init__(self, x, y, name=None):
        ''' Addition constructor.
        :param x: The first input node.
        :type x: Object of `Operation`, `Variable` or `Placeholder`.
        :param y: The second input node.
        :type y: Object of `Operation`, `Variable` or `Placeholder`.
        :param name: The operation name.
        :type name: str.
        '''
        super(self.__class__, self).__init__(x, y, name=name)
    def compute_output(self):
        ''' Compute and return the value of addition operation.
        '''
        x, y = self.input_nodes
        self.output_value = np.add(x.output_value, y.output_value)
        return self.output_value

可见,计算当前节点output_value的值的前提条件就是他的输入节点的值在此之前已经计算得到了

Variable节点

Operation节点类似,Variable节点也需要output_valueoutput_nodes等属性,但是它没有输入节点,也就没有input_nodes属性了,而是需要在创建的时候确定一个初始值initial_value:

 
 
 
 
 

Python

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Variable(object):
    ''' Variable node in computational graph.
    '''
    def __init__(self, initial_value=None, name=None, trainable=True):
        ''' Variable constructor.
        :param initial_value: The initial value of the variable.
        :type initial_value: number or a ndarray.
        :param name: Name of the variable.
        :type name: str.
        '''
        # Variable initial value.
        self.initial_value = initial_value
        # Output value of this operation in session execution.
        self.output_value = None
        # Nodes that receive this variable node as input.
        self.output_nodes = []
        # Variable name.
        self.name = name
        # Graph the variable belongs to.
        self.graph = DEFAULT_GRAPH
        # Add to the currently active default graph.
        self.graph.variables.append(self)
        if trainable:
            self.graph.trainable_variables.append(self)
    def compute_output(self):
        ''' Compute and return the variable value.
        '''
        if self.output_value is None:
            self.output_value = self.initial_value
        return self.output_value

Constant节点和Placeholder节点

ConstantPlaceholder节点与Variable节点类似,具体实现详见: https://github.com/PytLab/simpleflow/blob/master/simpleflow/operations.py

计算图对象

在定义了图中的节点后我们需要将定义好的节点放入到一个图中统一保管,因此就需要定义一个Graph类来存放创建的节点,方便统一操作图中节点的资源。

 
 
 
 
 

Python

 
1
2
3
4
5
6
7
8
class Graph(object):
    ''' Graph containing all computing nodes.
    '''
    def __init__(self):
        ''' Graph constructor.
        '''
        self.operations, self.constants, self.placeholders = [], [], []
        self.variables, self.trainable_variables = [], []

为了提供一个默认的图,在导入simpleflow模块的时候创建一个全局变量来引用默认的图:

 
 
 
 
 

Python

 
1
2
3
4
from .graph import Graph
# Create a default graph.
import builtins
DEFAULT_GRAPH = builtins.DEFAULT_GRAPH = Graph()

为了模仿TensorFlow的接口,我们给Graph添加上下文管理器协议方法使其成为一个上下文管理器, 同时也添加一个as_default方法:

 
 
 
 
 

Python

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Graph(object):
    #...
    def __enter__(self):
        ''' Reset default graph.
        '''
        global DEFAULT_GRAPH
        self.old_graph = DEFAULT_GRAPH
        DEFAULT_GRAPH = self
        return self
    def __exit__(self, exc_type, exc_value, exc_tb):
        ''' Recover default graph.
        '''
        global DEFAULT_GRAPH
        DEFAULT_GRAPH = self.old_graph
    def as_default(self):
        ''' Set this graph as global default graph.
        '''
        return self

这样在进入with代码块之前先保存旧的默认图对象然后将当前图赋值给全局图对象,这样with代码块中的节点默认会添加到当前的图中。最后退出with代码块时再对图进行恢复即可。这样我们可以按照TensorFlow的方式来在某个图中创建节点.

Ok,根据上面的实现我们已经可以创建一个计算图了:

 
 
 
 
 

Python

 
1
2
3
4
5
import simpleflow as sf
with sf.Graph().as_default():
    a = sf.constant([1.0, 2.0], name='a')
    b = sf.constant(2.0, name='b')
    c = a * b

前向传播(Feedforward)

实现了计算图和图中的节点,我们需要对计算图进行计算, 本部分对计算图的前向传播的实现进行总结。

会话

首先,我们需要实现一个Session来对一个已经创建好的计算图进行计算,因为当我们创建我们之前定义的节点的时候其实只是创建了一个空节点,节点中并没有数值可以用来计算,也就是output_value是空的。为了模仿TensorFlow的接口,我们在这里也把session定义成一个上下文管理器:

 
 
 
 
 

Python

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Session(object):
    ''' A session to compute a particular graph.
    '''
    def __init__(self):
        ''' Session constructor.
        '''
        # Graph the session computes for.
        self.graph = DEFAULT_GRAPH
    def __enter__(self):
        ''' Context management protocal method called before `with-block`.
        '''
        return self
    def __exit__(self, exc_type, exc_value, exc_tb):
        ''' Context management protocal method called after `with-block`.
        '''
        self.close()
    def close(self):
        ''' Free all output values in nodes.
        '''
        all_nodes = (self.graph.constants + self.graph.variables +
                     self.graph.placeholders + self.graph.operations +
                     self.graph.trainable_variables)
        for node in all_nodes:
            node.output_value = None
    def run(self, operation, feed_dict=None):
        ''' Compute the output of an operation.'''
        # ...

计算某个节点的输出值

上面我们已经可以构建出一个计算图了,计算图中的每个节点与其相邻的节点有方向的联系起来,现在我们需要根据图中节点的关系来推算出某个节点的值。那么如何计算呢? 还是以我们刚才¥f(x,y,z)=z(x+y)$的计算图为例,

若我们需要计算橙色××运算节点的输出值,我们需要计算与它相连的两个输入节点的输出值,进而需要计算绿色++的输入节点的输出值。我们可以通过后序遍历来获取计算一个节点所需的所有节点的输出值。为了方便实现,后序遍历我直接使用了递归的方式来实现:

 
 
 
 
 
 

Python

 
1
2
3
4
5
6
7
8
9
10
11
12
def _get_prerequisite(operation):
    ''' Perform a post-order traversal to get a list of nodes to be computed in order.
    '''
    postorder_nodes = []
    # Collection nodes recursively.
    def postorder_traverse(operation):
        if isinstance(operation, Operation):
            for input_node in operation.input_nodes:
                postorder_traverse(input_node)
        postorder_nodes.append(operation)
    postorder_traverse(operation)
    return postorder_nodes

通过此函数我们可以获取计算一个节点值所需要所有节点列表,再依次计算列表中节点的输出值,最后便可以轻易的计算出当前节点的输出值了。

 
 
 
 
 
 

Python

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Session(object):
    # ...
    def run(self, operation, feed_dict=None):
        ''' Compute the output of an operation.
        :param operation: A specific operation to be computed.
        :type operation: object of `Operation`, `Variable` or `Placeholder`.
        :param feed_dict: A mapping between placeholder and its actual value for the session.
        :type feed_dict: dict.
        '''
        # Get all prerequisite nodes using postorder traversal.
        postorder_nodes = _get_prerequisite(operation)
        for node in postorder_nodes:
            if type(node) is Placeholder:
                node.output_value = feed_dict[node]
            else:  # Operation and variable
                node.compute_output()
        return operation.output_value

例子

上面我们实现了计算图以及前向传播,我们就可以创建计算图计算表达式的值了, 如下:厦门二手叉车租赁

 
 
 
 
 

Python

 
1
2
3
4
5
6
7
8
9
10
import simpleflow as sf
# Create a graph
with sf.Graph().as_default():
    w = sf.constant([[1, 2, 3], [3, 4, 5]], name='w')
    x = sf.constant([[9, 8], [7, 6], [10, 11]], name='x')
    b = sf.constant(1.0, 'b')
    result = sf.matmul(w, x) + b
    # Create a session to compute
    with sf.Session() as sess:
        print(sess.run(result))

输出值:

 
 
 
 
 

Python

 
1
2
array([[  54.,   54.],
       [ 106.,  104.]])

总结

本文使用Python实现了计算图以及计算图的前向传播,并模仿TensorFlow的接口创建了Session以及Graph对象。下篇中将继续总结计算图节点计算梯度的方法以及反向传播和梯度下降优化器的实现。

实现属于自己的TensorFlow(一) - 计算图与前向传播的更多相关文章

  1. Tensorflow实现神经网络的前向传播

    我们构想有一个神经网络,输入为两个input,中间有一个hidden layer,这个hiddenlayer当中有三个神经元,最后有一个output. 图例如下: 在实现这个神经网络的前向传播之前,我 ...

  2. 实现属于自己的TensorFlow(二) - 梯度计算与反向传播

    前言 上一篇中介绍了计算图以及前向传播的实现,本文中将主要介绍对于模型优化非常重要的反向传播算法以及反向传播算法中梯度计算的实现.因为在计算梯度的时候需要涉及到矩阵梯度的计算,本文针对几种常用操作的梯 ...

  3. TensorFlow笔记-04-神经网络的实现过程,前向传播

    TensorFlow笔记-04-神经网络的实现过程,前向传播 基于TensorFlow的NN:用张量表示数据,用计算图搭建神经网络,用会话执行计算图,优化线上的权重(参数),得到模型 张量(tenso ...

  4. tensorflow学习笔记(1)-基本语法和前向传播

    tensorflow学习笔记(1) (1)tf中的图 图中就是一个计算图,一个计算过程.                                       图中的constant是个常量 计 ...

  5. tensorflow 模型前向传播 保存ckpt tensorbard查看 ckpt转pb pb 转snpe dlc 实例

    参考: TensorFlow 自定义模型导出:将 .ckpt 格式转化为 .pb 格式 TensorFlow 模型保存与恢复 snpe tensorflow 模型前向传播 保存ckpt  tensor ...

  6. Tensorflow笔记——神经网络图像识别(一)前反向传播,神经网络八股

      第一讲:人工智能概述       第三讲:Tensorflow框架         前向传播: 反向传播: 总的代码: #coding:utf-8 #1.导入模块,生成模拟数据集 import t ...

  7. 120、TensorFlow创建计算图(二)

    #创建一个计算流图 #大多数tensorflow程序开始于dataflow graph 的构造函数 #在这个命令中你执行了tensorflow api的函数 #创建了新的操作tf.Operation ...

  8. tensorflow简单实现卷积前向过程

    卷积,说白了就是对应位置相乘再求和,卷积操作用广泛应用于图像识别,在自然语言处理中也开始应用,用作文本分类问题. 卷积操作最重要的部分就是卷积核或者说过滤器 1.常用过滤器尺寸为3*3或者5*5 2. ...

  9. 利用tensorflow实现前向传播

    import tensorflow as tf w1 = tf.Variable(tf.random_normal((2, 3), stddev=1, seed=1))w2 = tf.Variable ...

随机推荐

  1. oracle查询用户的权限

    DBA_* 描述的是数据库中的所有对象 ALL_* 描述的是当前用户有访问权限的所有对象 USER_* 描述的是当前用户所拥有的所有对象 查看所有用户:  select * from dba_user ...

  2. Oracle 表空间、段、区和块简述

    数据块(Block) 数据块Block是Oracle存储数据信息的最小单位.注意,这里说的是Oracle环境下的最小单位.Oracle也就是通过数据块来屏蔽不同操作系统存储结构的差异.无论是Windo ...

  3. Vue聊天框默认滚动到底部

    功能场景 在开发中,我们总能遇到某些场景需要运用到聊天框,比如客服对话.如果你不是一名开发人员,可能你在使用QQ或者聊天工具的时候并没有注意到,当你发出一条消息的时候,窗体会默认滚动到最底部,让用户可 ...

  4. py基础__socket编程

    目录 Python基础__socket编程 1.计算机网络 2.socke编程 3.socketserver模块 4.思考 Python基础__socket编程 1.计算机网络 1.OSI网络七层模型 ...

  5. 【Linux】Linux文件跟目录管理

    熟悉Linux的大家都知道,在Linux中,一切皆文件,可能在有些人的理解中,Linux跟我们的Windows差不多,是都具有图形操作界面的一种操作系统,但是更深入的来说,Linux更偏向于用命令操作 ...

  6. 大数据分析系统Hadoop的13个开源工具

    Hadoop是由Apache基金会开发的一个大数据分布式系统基础架构,最早版本是2003年原Yahoo!DougCutting根据Google发布的学术论文研究而来. 用户可以在不了解分布式底层细节的 ...

  7. MAC下绕开百度网盘限速下载的方法,三步操作永久生效

    第一步:下载所需工具:(①②步我放在同一个文件夹,可一起下载,链接失效请留言) 工具地址:链接: https://pan.baidu.com/s/1raicYzM 密码: ve3n ①下载Aria2G ...

  8. Go语言中多字节字符的处理

    1 概述 Go语言的字符串是使用 UTF-8 编码的.UTF-8 是 Unicode 的实现方式之一.本文内容包括:UTF-8 和 Unicode 的关系,Go语言提供的 unicode 包和 uni ...

  9. 记账APP(4)

    依旧是一个表格类型的增删改查,但是呢,在用节点做,有点懵,明天加油

  10. SVG中嵌入HTML元素

    <?xml version="1.0" standalone="yes"?> <style> .clsfont{ border:1px ...