前段時間由於課題須要使用了一段時間TensorFlow,感受這種框架頗有意思,除了能夠搭建複雜的神經網絡,也能夠優化其餘本身須要的計算模型,因此一直想本身學習一下寫一個相似的圖計算框架。前幾天組會開完決定着手實現一個模仿TensorFlow接口的簡陋版本圖計算框架以學習計算圖程序的編寫以及前向傳播和反向傳播的實現。目前實現了前向傳播和反向傳播以及梯度降低優化器,並寫了個優化線性模型的例子。node
代碼放在了GitHub上,取名SimpleFlow, 倉庫連接: https://github.com/PytLab/sim...python
<!-- more -->git
雖然前向傳播反向傳播這些原理了解起來並非很複雜,可是真正着手寫起來才發現,裏面仍是有不少細節須要學習和處理才能對實際的模型進行優化(例如Loss函數對每一個計算節點矩陣求導的處理)。其中SimpleFlow的代碼並無考慮太多的東西好比dtype
和張量size
的檢查等,由於只是爲了實現主要圖計算功能並無考慮任何的優化, 內部張量運算使用的Numpy的接口(畢竟是學習和練手的目的嘛)。很久時間沒更新博客了,在接下來的幾篇裏面我將把實現的過程的細節總結一下,但願能夠給後面學習的童鞋作個參考。github
本文主要介紹計算圖以及前向傳播的實現, 主要涉及圖的構建以及經過對構建好的圖進行後序遍歷而後進行前向傳播計算獲得具體節點上的輸出值。網絡
先貼上一個簡單的實現效果吧:session
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))
計算圖是計算代數中的一個基礎處理方法,咱們能夠經過一個有向圖來表示一個給定的數學表達式,並能夠根據圖的特色快速方便對錶達式中的變量進行求導。而神經網絡的本質就是一個多層複合函數, 所以也能夠經過一個圖來表示其表達式。app
本部分主要總結計算圖的實現,在計算圖這個有向圖中,每一個節點表明着一種特定的運算例如求和,乘積,向量乘積,平方等等... 例如求和表達式$f(x, y) = x + y$使用有向圖表示爲:框架
表達式$f(x, y, z) = z(x+y)$使用有向圖表示爲:ide
與TensorFlow的實現不一樣,爲了簡化,在SimpleFlow中我並無定義Tensor
類來表示計算圖中節點之間的數據流動,而是直接定義節點的類型,其中主要定義了四種類型來表示圖中的節點:函數
Operation
: 操做節點主要接受一個或者兩個輸入節點而後進行簡單的操做運算,例如上圖中的加法操做和乘法操做等。Variable
: 沒有輸入節點的節點,此節點包含的數據在運算過程當中是能夠變化的。Constant
: 相似Variable
節點,也沒有輸入節點,此節點中的數據在圖的運算過程當中不會發生變化Placeholder
: 一樣沒有輸入節點,此節點的數據是經過圖創建好之後經過用戶傳入的其實圖中的全部節點均可以當作是某種操做,其中Variable
, Constant
, Placeholder
都是一種特殊的操做,只是相對於普通的Operation
而言,他們沒有輸入,可是都會有輸出(像上圖中的$x$, $y$節點,他們自己輸出自身的值到$+$節點中去),一般會輸出到Operation
節點,進行進一步的計算。
下面咱們主要介紹如何實現計算圖的基本組件: 節點和邊。
Operation
節點節點表示操做,邊表明節點接收和輸出的數據,操做節點須要含有如下屬性:
input_nodes
: 輸入節點,裏面存放與當前節點相鏈接的輸入節點的引用output_nodes
: 輸出節點, 存放以當前節點做爲輸入的節點,也就是當前節點的去向output_value
: 存儲當前節點的數值, 若是是Add
節點,此變量就存儲兩個輸入節點output_value
的和name
: 當前節點的名稱graph
: 此節點所屬的圖下面咱們定義了Operation
基類用於表示圖中的操做節點(詳見https://github.com/PytLab/sim...:
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
在初始化方法中除了定義上面提到的屬性外,還須要進行兩個操做:
output_nodes
這樣能夠在輸入節點中找到當前節點。另外,每一個操做節點還有兩個必須的方法: comput_output
和compute_gradient
. 他們分別負責根據輸入節點的值計算當前節點的輸出值和根據操做屬性和當前節點的值計算梯度。關於梯度的計算將在後續的文章中詳細介紹,本文只對節點輸出值的計算進行介紹。
下面我以求和操做爲例來講明具體操做節點的實現:
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_value
, output_nodes
等屬性,可是它沒有輸入節點,也就沒有input_nodes
屬性了,而是須要在建立的時候肯定一個初始值initial_value
:
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
節點Constant
和Placeholder
節點與Variable
節點相似,具體實現詳見: https://github.com/PytLab/sim...
在定義了圖中的節點後咱們須要將定義好的節點放入到一個圖中統一保管,所以就須要定義一個Graph
類來存放建立的節點,方便統一操做圖中節點的資源。
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模塊的時候建立一個全局變量來引用默認的圖:
from .graph import Graph # Create a default graph. import builtins DEFAULT_GRAPH = builtins.DEFAULT_GRAPH = Graph()
爲了模仿TensorFlow的接口,咱們給Graph
添加上下文管理器協議方法使其成爲一個上下文管理器, 同時也添加一個as_default
方法:
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,根據上面的實現咱們已經能夠建立一個計算圖了:
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
實現了計算圖和圖中的節點,咱們須要對計算圖進行計算, 本部分對計算圖的前向傳播的實現進行總結。
首先,咱們須要實現一個Session
來對一個已經建立好的計算圖進行計算,由於當咱們建立咱們以前定義的節點的時候其實只是建立了一個空節點,節點中並無數值能夠用來計算,也就是output_value
是空的。爲了模仿TensorFlow的接口,咱們在這裏也把session定義成一個上下文管理器:
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)$的計算圖爲例,
若咱們須要計算橙色$\\times$運算節點的輸出值,咱們須要計算與它相連的兩個輸入節點的輸出值,進而須要計算綠色$+$的輸入節點的輸出值。咱們能夠經過後序遍從來獲取計算一個節點所需的全部節點的輸出值。爲了方便實現,後序遍歷我直接使用了遞歸的方式來實現:
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
經過此函數咱們能夠獲取計算一個節點值所須要全部節點列表,再依次計算列表中節點的輸出值,最後即可以輕易的計算出當前節點的輸出值了。
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
上面咱們實現了計算圖以及前向傳播,咱們就能夠建立計算圖計算表達式的值了, 以下:
$$ f = \left[ \begin{matrix} 1 & 2 & 3 \\ 3 & 4 & 5 \\ \end{matrix} \right] \times \left[ \begin{matrix} 9 & 8 \\ 7 & 6 \\ 10 & 11 \\ \end{matrix} \right] + 3 = \left[ \begin{matrix} 54 & 54 \\ 106 & 104 \\ \end{matrix} \right] $$
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))
輸出值:
array([[ 54., 54.], [ 106., 104.]])
本文使用Python實現了計算圖以及計算圖的前向傳播,並模仿TensorFlow的接口建立了Session
以及Graph
對象。下篇中將繼續總結計算圖節點計算梯度的方法以及反向傳播和梯度降低優化器的實現。
最後再附上simpleflow項目的連接, 歡迎相互學習和交流: https://github.com/PytLab/sim...