集智专栏
资源加载中,请稍后...
集智专栏

零基础深度学习Part I:计算图

原文:Deep Learning From Scratch I: Computational Graphs

翻译:iMoe

审校:Kaiser


这是本系列教程的第一章。本章将带领你入门深度神经网络的数学和算法基础。然后我们将效仿 TensorFlow API,自己动手用 Python 实现一个神经网络库。

  • Part I: Computational Graphs
  • Part II: Perceptrons
  • Part III: Training criterion
  • Part IV: Gradient Descent and Backpropagation
  • Part V: Multi-Layer Perceptrons
  • Part VI: TensorFlow

学习本章不需要任何机器学习或者神经网络的基础。但是,对于本科生级别的微积分、线性代数、基础算法和概率学,需要有一定的基础。如果学习过程中遇到困难,请写在评论里。

在本章结束时,你将会深入理解神经网络背后的数学知识,以及深度学习库在其背后所起的作用。

我会让代码尽可能的简单明了,相比于运行效率,这样更加易于理解。由于我们的 API 是效仿 TensorFlow 的,你在完成本章的学习后,自然会知道如何使用 TensorFlow 的 API,以及 TensorFlow 背后的运行机理(而不是花时间去学习某个全能、最高效的 API)。




计算图 Computational graphs

我们从计算图(computational graph)的理论开始,因为神经网络本身是计算图的一个特殊形式。

Computational graph 是有向图,其中的节点都对应着 操作(Operation) 或者 变量(Variable)。

Variable 可以把自己的值递送给 Operation,而 Operation 可以把自己的输出递送给其他的 Operation。这样的话,计算图中的每一个节点都定义了 graph 中的 Variable 的一个函数(本句意义可以参照“函数”的定义,大意为一种输入对应一种输出)。

递送入节点的、从节点中传出的值,被称为 tensor,这是个用于多维数组的词。因此,它包括标量、矢量、矩阵,也包括高阶的张量(tensor)。

下例中的 computational graph 把两个输入 x 和 y 相加,计算得总和 z。

本例中,x 和 y 是 z 的输入节点,z 是 x 和 y 的消耗者。z 因此定义了一个函数,即:

$ z:R^2 -> R $ where $z(x,y) = x + y$


当计算变得越来越复杂时,computational graph 的概念就越显得重要了。比如,下面的 computational graph 定义了一个仿射变换:

$$ z(A, x, b) = Ax + b $$




操作 Operations

每一个 Operation 有三项特征:

  • 一个计算函数:用于计算对于给定的输入,应当输出的值
  • 输入节点(node):可有多个,可以是 Variable 或者其他 Operation
  • consumer:可有多个,将 Operation 的输出作为它们的输入

实现代码:

程序说明
Operation
示例代码
class Operation: """Represents a graph node that performs a computation. An `Operation` is a node in a `Graph` that takes zero or more objects as input, and produces zero or more objects as output. """ def __init__(self, input_nodes=[]): """Construct Operation """ self.input_nodes = input_nodes # Initialize list of consumers (i.e. nodes that receive this operation's output as input) self.consumers = [] # Append this operation to the list of consumers of all input nodes for input_node in input_nodes: input_node.consumers.append(self) # Append this operation to the list of operations in the currently active default graph _default_graph.operations.append(self) def compute(self): """Computes the output of this operation. "" Must be implemented by the particular operation. """ pass
程序验证过程
True


一些简单的 Operation

为了熟悉操作类(日后会需要),我们来实现一些简单的 Operation。在这两个Operation中,我们假定所有tensor都是NumPy数组,这样的话,元素加法和矩阵乘法(.点号)就不需要我们自己实现了。

加法

程序说明
Addition
示例代码
class add(Operation): """Returns x + y element-wise. """ def __init__(self, x, y): """Construct add Args: x: First summand node y: Second summand node """ super().__init__([x, y]) def compute(self, x_value, y_value): """Compute the output of the add operation Args: x_value: First summand value y_value: Second summand value """ return x_value + y_value
程序验证过程
True


矩阵乘法

程序说明
Matrix multiplication
示例代码
class matmul(Operation): """Multiplies matrix a by matrix b, producing a * b. """ def __init__(self, a, b): """Construct matmul Args: a: First matrix b: Second matrix """ super().__init__([a, b]) def compute(self, a_value, b_value): """Compute the output of the matmul operation Args: a_value: First matrix value b_value: Second matrix value """ return a_value.dot(b_value)
程序验证过程
True




占位符 Placeholders

在计算图中,并非所有节点都是Operation,比如在仿射变化的graph 中,$A$, $x$ 和 $b$ 都不是 Operation。相对地,它们是graph的输入,而且,如果我们想要计算 graph 的输出,就必须为它们各提供一个值。为了提供这样的值,我们引入 placeholder。

程序说明
Placeholder
示例代码
class placeholder: """Represents a placeholder node that has to be provided with a value when computing the output of a computational graph """ def __init__(self): """Construct placeholder """ self.consumers = [] # Append this placeholder to the list of placeholders in the currently active default graph _default_graph.placeholders.append(self)
程序验证过程
True




变量 Variables

在仿射变换的 graph 中,$x$ 与 $A$ 和 $b$ 有本质的不同。x 是 operation 的输入,而 A 和 b 是 operation 的参数,即它们是 graph 本身固有的。我们把 A 和 b 这样的参数称为 variable。

程序说明
Variables
示例代码
class Variable: """Represents a variable (i.e. an intrinsic, changeable parameter of a computational graph). """ def __init__(self, initial_value=None): """Construct Variable Args: initial_value: The initial value of this variable """ self.value = initial_value self.consumers = [] # Append this variable to the list of variables in the currently active default graph _default_graph.variables.append(self)
程序验证过程
True




Graph类

最后,我们需要一个把所有 operation, placeholdervariable 包含在一起的类。创建一个新的 graph 时,可以通过调用 as_default 方法来设置它的 _defaultgraph

通过这个方式,我们不用每次都传入一个 graph 的引用,就可以创建 operation, placeholder 和 variable。

程序说明
Graph
示例代码
class Graph: """Represents a computational graph """ def __init__(self): """Construct Graph""" self.operations = [] self.placeholders = [] self.variables = [] def as_default(self): global _default_graph _default_graph = self
程序验证过程
True




举例

现在我们来用上面列举的类,创建一个仿射变换的 computational graph:

程序说明
Example
示例代码
# Create a new graph Graph().as_default() # Create variables A = Variable([[1, 0], [0, -1]]) b = Variable([1, 1]) # Create placeholder x = placeholder() # Create hidden node y y = matmul(A, x) # Create output node z z = add(y, b)
程序验证过程
True




计算操作输出

既然已经学会了怎么创建计算图,我们就该考虑怎么计算 operation 的输出了。

创建一个 会话(Session) 类,用来包括一个 operation 的执行。我们希望能够对 session 的实例调用 run 方法,能够传入需要计算的 operation,以及一个包含所有 placeholder 所需要的值的字典。

session = Session()
output = session.run(z, {
    x: [1, 2]
})

这里计算过程是这样的:

为了计算 operation 所代表的函数,我们需要按正确的顺序进行计算。比如,如果中间结果 y 还没计算出来,我们就不能先计算 z。因此我们必须确保 operation 执行顺序正确,只有这样才能确保在计算某个 operation之前,它所需要的输入节点的值都已经计算好了。这点可以通过 post-order traversal 实现。

程序说明
Session
示例代码
import numpy as np class Session: """Represents a particular execution of a computational graph. """ def run(self, operation, feed_dict={}): """Computes the output of an operation Args: operation: The operation whose output we'd like to compute. feed_dict: A dictionary that maps placeholders to values for this session """ # Perform a post-order traversal of the graph to bring the nodes into the right order nodes_postorder = traverse_postorder(operation) # Iterate all nodes to determine their value for node in nodes_postorder: if type(node) == placeholder: # Set the node value to the placeholder value from feed_dict node.output = feed_dict[node] elif type(node) == Variable: # Set the node value to the variable's value attribute node.output = node.value else: # Operation # Get the input values for this operation from node_values node.inputs = [input_node.output for input_node in node.input_nodes] # Compute the output of this operation node.output = node.compute(*node.inputs) # Convert lists to numpy arrays if type(node.output) == list: node.output = np.array(node.output) # Return the requested node value return operation.output def traverse_postorder(operation): """Performs a post-order traversal, returning a list of nodes in the order in which they have to be computed Args: operation: The operation to start traversal at """ nodes_postorder = [] def recurse(node): if isinstance(node, Operation): for input_node in node.input_nodes: recurse(input_node) nodes_postorder.append(node) recurse(operation) return nodes_postorder
程序验证过程
True


测试一下上例里头我们写的类:

程序说明
Example
示例代码
session = Session() output = session.run(z, { x: [1, 2] }) print(output)
程序验证过程
True

您也许喜欢这些文章

9行Python代码搭建神经网络

发表至趣味项目
该系列的上一篇文章中,Milo阐述了人工智能与脑神经科学的关系,并给自己定下了两个小目标:1.学习层次隐式马尔可夫模型 2. 用Python搭建神经网络。本文中他用极少的Python代码实现了非常简单的神经网络。

是直是弯?爆照判断

发表至业界新闻
长久以来,“性取向”都是一个争议性的话题,尤其是在“政治正确”大行其道的如今。最近,斯坦福大学的一项研究提供了不得了的说法。该研究分析了逾35000张脸部图片,并建立了准确度极高的的算法,来判断人的性取向。

动态图:TensorFlow的命令式、运行定义式界面

发表至业界新闻
TensorFlow的新功能:动态图机制(Eager execution)支持命令式的编程风格(就像NumPy),当eager execution模式开启之后,TensorFlow会立即执行操作,而不用预先定义在Session.run()里面。 本站已在计算后台升级了TensorFlow,可以让读者第一时间体验。

文章评论(1)

HKW 发表于 9天前 回复
给iMoe打♂尻