摘要:目前實現(xiàn)了前向傳播和反向傳播以及梯度下降優(yōu)化器,并寫了個優(yōu)化線性模型的例子。下篇中將繼續(xù)總結(jié)計算圖節(jié)點計算梯度的方法以及反向傳播和梯度下降優(yōu)化器的實現(xiàn)。
前段時間因為課題需要使用了一段時間TensorFlow,感覺這種框架很有意思,除了可以搭建復雜的神經(jīng)網(wǎng)絡,也可以優(yōu)化其他自己需要的計算模型,所以一直想自己學習一下寫一個類似的圖計算框架。前幾天組會開完決定著手實現(xiàn)一個模仿TensorFlow接口的簡陋版本圖計算框架以學習計算圖程序的編寫以及前向傳播和反向傳播的實現(xiàn)。目前實現(xiàn)了前向傳播和反向傳播以及梯度下降優(yōu)化器,并寫了個優(yōu)化線性模型的例子。
代碼放在了GitHub上,取名SimpleFlow, 倉庫鏈接: https://github.com/PytLab/sim...
雖然前向傳播反向傳播這些原理了解起來并不是很復雜,但是真正著手寫起來才發(fā)現(xiàn),里面還是有很多細節(jié)需要學習和處理才能對實際的模型進行優(yōu)化(例如Loss函數(shù)對每個計算節(jié)點矩陣求導的處理)。其中SimpleFlow的代碼并沒有考慮太多的東西比如dtype和張量size的檢查等,因為只是為了實現(xiàn)主要圖計算功能并沒有考慮任何的優(yōu)化, 內(nèi)部張量運算使用的Numpy的接口(畢竟是學習和練手的目的嘛)。好久時間沒更新博客了,在接下來的幾篇里面我將把實現(xiàn)的過程的細節(jié)總結(jié)一下,希望可以給后面學習的童鞋做個參考。
正文本文主要介紹計算圖以及前向傳播的實現(xiàn), 主要涉及圖的構(gòu)建以及通過對構(gòu)建好的圖進行后序遍歷然后進行前向傳播計算得到具體節(jié)點上的輸出值。
先貼上一個簡單的實現(xiàn)效果吧:
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)
計算圖是計算代數(shù)中的一個基礎(chǔ)處理方法,我們可以通過一個有向圖來表示一個給定的數(shù)學表達式,并可以根據(jù)圖的特點快速方便對表達式中的變量進行求導。而神經(jīng)網(wǎng)絡的本質(zhì)就是一個多層復合函數(shù), 因此也可以通過一個圖來表示其表達式。
本部分主要總結(jié)計算圖的實現(xiàn),在計算圖這個有向圖中,每個節(jié)點代表著一種特定的運算例如求和,乘積,向量乘積,平方等等... 例如求和表達式$f(x, y) = x + y$使用有向圖表示為:
表達式$f(x, y, z) = z(x+y)$使用有向圖表示為:
與TensorFlow的實現(xiàn)不同,為了簡化,在SimpleFlow中我并沒有定義Tensor類來表示計算圖中節(jié)點之間的數(shù)據(jù)流動,而是直接定義節(jié)點的類型,其中主要定義了四種類型來表示圖中的節(jié)點:
Operation: 操作節(jié)點主要接受一個或者兩個輸入節(jié)點然后進行簡單的操作運算,例如上圖中的加法操作和乘法操作等。
Variable: 沒有輸入節(jié)點的節(jié)點,此節(jié)點包含的數(shù)據(jù)在運算過程中是可以變化的。
Constant: 類似Variable節(jié)點,也沒有輸入節(jié)點,此節(jié)點中的數(shù)據(jù)在圖的運算過程中不會發(fā)生變化
Placeholder: 同樣沒有輸入節(jié)點,此節(jié)點的數(shù)據(jù)是通過圖建立好以后通過用戶傳入的
其實圖中的所有節(jié)點都可以看成是某種操作,其中Variable, Constant, Placeholder都是一種特殊的操作,只是相對于普通的Operation而言,他們沒有輸入,但是都會有輸出(像上圖中的$x$, $y$節(jié)點,他們本身輸出自身的值到$+$節(jié)點中去),通常會輸出到Operation節(jié)點,進行進一步的計算。
下面我們主要介紹如何實現(xiàn)計算圖的基本組件: 節(jié)點和邊。
Operation節(jié)點節(jié)點表示操作,邊代表節(jié)點接收和輸出的數(shù)據(jù),操作節(jié)點需要含有以下屬性:
input_nodes: 輸入節(jié)點,里面存放與當前節(jié)點相連接的輸入節(jié)點的引用
output_nodes: 輸出節(jié)點, 存放以當前節(jié)點作為輸入的節(jié)點,也就是當前節(jié)點的去向
output_value: 存儲當前節(jié)點的數(shù)值, 如果是Add節(jié)點,此變量就存儲兩個輸入節(jié)點output_value的和
name: 當前節(jié)點的名稱
graph: 此節(jié)點所屬的圖
下面我們定義了Operation基類用于表示圖中的操作節(jié)點(詳見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
在初始化方法中除了定義上面提到的屬性外,還需要進行兩個操作:
將當前節(jié)點的引用添加到他輸入節(jié)點的output_nodes這樣可以在輸入節(jié)點中找到當前節(jié)點。
將當前節(jié)點的引用添加到圖中,方便后面對圖中的資源進行回收等操作
另外,每個操作節(jié)點還有兩個必須的方法: comput_output和compute_gradient. 他們分別負責根據(jù)輸入節(jié)點的值計算當前節(jié)點的輸出值和根據(jù)操作屬性和當前節(jié)點的值計算梯度。關(guān)于梯度的計算將在后續(xù)的文章中詳細介紹,本文只對節(jié)點輸出值的計算進行介紹。
下面我以求和操作為例來說明具體操作節(jié)點的實現(xiàn):
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
可見,計算當前節(jié)點output_value的值的前提條件就是他的輸入節(jié)點的值在此之前已經(jīng)計算得到了。
Variable節(jié)點與Operation節(jié)點類似,Variable節(jié)點也需要output_value, output_nodes等屬性,但是它沒有輸入節(jié)點,也就沒有input_nodes屬性了,而是需要在創(chuàng)建的時候確定一個初始值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_valueConstant節(jié)點和Placeholder節(jié)點
Constant和Placeholder節(jié)點與Variable節(jié)點類似,具體實現(xiàn)詳見: https://github.com/PytLab/sim...
計算圖對象在定義了圖中的節(jié)點后我們需要將定義好的節(jié)點放入到一個圖中統(tǒng)一保管,因此就需要定義一個Graph類來存放創(chuàng)建的節(jié)點,方便統(tǒng)一操作圖中節(jié)點的資源。
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模塊的時候創(chuàng)建一個全局變量來引用默認的圖:
from .graph import Graph # Create a default graph. import builtins DEFAULT_GRAPH = builtins.DEFAULT_GRAPH = Graph()
為了模仿TensorFlow的接口,我們給Graph添加上下文管理器協(xié)議方法使其成為一個上下文管理器, 同時也添加一個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代碼塊中的節(jié)點默認會添加到當前的圖中。最后退出with代碼塊時再對圖進行恢復即可。這樣我們可以按照TensorFlow的方式來在某個圖中創(chuàng)建節(jié)點.
Ok,根據(jù)上面的實現(xiàn)我們已經(jīng)可以創(chuàng)建一個計算圖了:
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)
實現(xiàn)了計算圖和圖中的節(jié)點,我們需要對計算圖進行計算, 本部分對計算圖的前向傳播的實現(xiàn)進行總結(jié)。
會話首先,我們需要實現(xiàn)一個Session來對一個已經(jīng)創(chuàng)建好的計算圖進行計算,因為當我們創(chuàng)建我們之前定義的節(jié)點的時候其實只是創(chuàng)建了一個空節(jié)點,節(jié)點中并沒有數(shù)值可以用來計算,也就是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.""" # ...計算某個節(jié)點的輸出值
上面我們已經(jīng)可以構(gòu)建出一個計算圖了,計算圖中的每個節(jié)點與其相鄰的節(jié)點有方向的聯(lián)系起來,現(xiàn)在我們需要根據(jù)圖中節(jié)點的關(guān)系來推算出某個節(jié)點的值。那么如何計算呢? 還是以我們剛才$f(x, y, z) = z(x + y)$的計算圖為例,
若我們需要計算橙色$ imes$運算節(jié)點的輸出值,我們需要計算與它相連的兩個輸入節(jié)點的輸出值,進而需要計算綠色$+$的輸入節(jié)點的輸出值。我們可以通過后序遍歷來獲取計算一個節(jié)點所需的所有節(jié)點的輸出值。為了方便實現(xiàn),后序遍歷我直接使用了遞歸的方式來實現(xiàn):
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
通過此函數(shù)我們可以獲取計算一個節(jié)點值所需要所有節(jié)點列表,再依次計算列表中節(jié)點的輸出值,最后便可以輕易的計算出當前節(jié)點的輸出值了。
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例子
上面我們實現(xiàn)了計算圖以及前向傳播,我們就可以創(chuàng)建計算圖計算表達式的值了, 如下:
$$ f = left[ egin{matrix} 1 & 2 & 3 3 & 4 & 5 end{matrix} ight] imes left[ egin{matrix} 9 & 8 7 & 6 10 & 11 end{matrix} ight] + 3 = left[ egin{matrix} 54 & 54 106 & 104 end{matrix} ight] $$
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.]])總結(jié)
本文使用Python實現(xiàn)了計算圖以及計算圖的前向傳播,并模仿TensorFlow的接口創(chuàng)建了Session以及Graph對象。下篇中將繼續(xù)總結(jié)計算圖節(jié)點計算梯度的方法以及反向傳播和梯度下降優(yōu)化器的實現(xiàn)。
最后再附上simpleflow項目的鏈接, 歡迎相互學習和交流: https://github.com/PytLab/sim...
參考Deep Learning From Scratch
https://en.wikipedia.org/wiki...
https://zhuanlan.zhihu.com/p/...
http://blog.csdn.net/magic_an...
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/41389.html
摘要:目前實現(xiàn)了前向傳播和反向傳播以及梯度下降優(yōu)化器,并寫了個優(yōu)化線性模型的例子。下篇中將繼續(xù)總結(jié)計算圖節(jié)點計算梯度的方法以及反向傳播和梯度下降優(yōu)化器的實現(xiàn)。 前段時間因為課題需要使用了一段時間TensorFlow,感覺這種框架很有意思,除了可以搭建復雜的神經(jīng)網(wǎng)絡,也可以優(yōu)化其他自己需要的計算模型,所以一直想自己學習一下寫一個類似的圖計算框架。前幾天組會開完決定著手實現(xiàn)一個模仿TensorF...
摘要:首先第一種當然是在年提出的,它奠定了整個卷積神經(jīng)網(wǎng)絡的基礎(chǔ)。其中局部感受野表示卷積核只關(guān)注圖像的局部特征,而權(quán)重共享表示一個卷積核在整張圖像上都使用相同的權(quán)值,最后的子采樣即我們常用的池化操作,它可以精煉抽取的特征。 近日,微軟亞洲研究院主辦了一場關(guān)于 CVPR 2018 中國論文分享會,機器之心在分享會中發(fā)現(xiàn)了一篇非常有意思的論文,它介紹了一種新型卷積網(wǎng)絡架構(gòu),并且相比于 DenseNet...
摘要:全卷積神經(jīng)網(wǎng)絡僅使用卷積層,這就使其成為全卷積神經(jīng)網(wǎng)絡。輸入圖像中包含了真值對象框中心的網(wǎng)格會作為負責預測對象的單元格。在圖像中,它是被標記為紅色的單元格,其中包含了真值框的中心被標記為黃色。在過去幾個月中,我一直在實驗室中研究提升目標檢測的方法。在這之中我獲得的較大啟發(fā)就是意識到:學習目標檢測的較佳方法就是自己動手實現(xiàn)這些算法,而這正是本教程引導你去做的。 在本教程中,我們將使用 P...
閱讀 3089·2021-11-25 09:43
閱讀 2266·2021-09-07 10:28
閱讀 3605·2021-08-11 11:14
閱讀 2788·2019-08-30 13:49
閱讀 3556·2019-08-29 18:41
閱讀 1174·2019-08-29 11:26
閱讀 1983·2019-08-26 13:23
閱讀 3382·2019-08-26 10:43