tensorflow计算图_通过从头开始模仿其API来了解TensorFlow
TensorFlow是一個非常強大的開源機器學習庫,用于實現和部署機器學習模型。這使其非常適合研究和生產。多年來,它已成為最受歡迎的深度學習庫之一。
這篇文章的目標是建立一種直覺和理解深度學習庫的工作原理,特別是TensorFlow。為了實現這一目標,我們將模仿其API并從頭開始實施其核心構建塊。最終,你將能夠自信地使用TensorFlow,因為你將對其內部工作原理有深刻的概念性理解。
理論
TensorFlow是一個由兩個核心構建塊組成的框架 - 用于定義計算圖的庫和用于在各種不同硬件上執行此類圖的運行時。
計算圖
簡而言之,計算圖是一種將計算描述為有向圖的抽象方法。有向圖是由節點(頂點)和邊組成的數據結構。它是一組頂點通過有向邊成對連接。
這是一個非常簡單的例子:
有向無環圖的簡單示例
圖有許多形狀和大小,用于解決許多現實問題,例如代表網絡,包括電話網絡,電路網絡,道路網絡甚至社交網絡。它們也常用于計算機科學中以描述依賴性,用于調度或在編譯器中用于表示直線代碼(沒有循環和條件分支的語句序列)。使用后者的圖表允許編譯器有效地消除公共子表達式。
TensorFlow在內部使用有向圖來表示計算,他們稱之為data flow graphs(或計算圖)。
雖然有向圖中的節點可以是任何節點,但計算圖中的節點主要表示操作(operations),變量(variables)或占位符(placeholders)。
操作根據特定規則創建或操作數據。在TensorFlow中,這些規則稱為Ops,是操作的縮寫。另一方面,變量表示可以通過對這些變量運行Ops來操縱的共享持久狀態。
這些邊對應于流經不同操作的數據或多維數組(即所謂的張量)。換句話說,邊將信息從一個節點傳遞到另一個節點。一個操作(一個節點)的輸出成為另一個操作的輸入,連接兩個節點的邊攜帶該值。
下面是一個非常簡單的程序的例子:
為了從這個程序中創建一個計算圖,我們為程序中的每個操作創建節點,以及輸入變量a和b。事實上,a和b可以是常數如果它們不變的話。如果一個節點被用作另一個操作的輸入,我們畫一個從一個節點到另一個節點的有向箭頭。
該程序的計算圖可能如下所示:
表示我們簡單程序及其數據流的計算圖
上面的計算圖表示了不同的計算步驟,我們需要執行這些步驟才能得到最終的結果。首先,我們創建兩個常數a和b。然后我們把它們相乘,取它們的和,然后用這兩個運算的結果一個除以另一個。最后,我們打印出結果。
這并不難,但問題是為什么我們需要一個計算圖呢?將計算組織成有向圖的優點是什么呢?
首先,計算圖是描述計算機程序及其計算的一種更為抽象的方法。在最基本的層次上,大多數計算機程序主要由兩部分組成——基本操作和按順序逐行執行這些操作的順序。這意味著我們首先要把a和b相乘,只有當這個表達式被求值時我們才會取它們的和。因此,程序指定了執行的順序,但是計算圖只指定了跨操作的依賴關系。換句話說,這些操作的輸出如何從一個操作流向另一個操作。
這允許并行性或依賴性驅動調度。如果我們查看我們的計算圖,我們會看到我們可以并行執行乘法和加法。那是因為這兩個操作并不相互依賴。因此,我們可以使用圖形的拓撲來驅動操作的調度并以最有效的方式執行它們,例如在一臺機器上使用多個GPU,甚至在多臺機器上分布執行。TensorFlow正是這樣做的,它可以將不依賴于彼此的操作分配給不同的內核,只需要構造一個有向圖,就可以從實際編寫程序的人那里獲得最少的輸入。
另一個關鍵優勢是可移植性。圖是代碼的獨立于語言的表示。因此,我們可以用Python構建圖形,保存模型,并使用另一種語言(比如c++)恢復模型。
TensorFlow基礎知識
TensorFlow中的計算圖包含幾個部分:
- 變量:將TensorFlow變量視為計算機程序中的常規變量。變量可以在任何時間點修改,但不同之處在于它們必須在會話中運行圖形之前進行初始化。它們代表圖中的可變參數。變量的一個很好的例子是神經網絡中的權重或偏差。
- 占位符:占位符允許我們從外部將數據提供到圖中,而不像變量那樣它們不需要初始化。占位符只是定義形狀和數據類型。我們可以將占位符視為圖中的空節點,稍后會提供該值。它們通常用于輸入和標簽。
- 常量:無法更改的參數。
- 操作:操作表示圖形中執行Tensors計算的節點。
- 圖:圖就像一個中心樞紐,它將所有變量,占位符,常量連接到操作。
- 會話:會話創建一個運行時,在該運行時執行操作并評估Tensors。它還分配內存并保存中間結果和變量的值。
記得從一開始我們說TensorFlow由兩部分組成,一個用于定義計算圖的庫和一個用于執行這些圖形的運行時嗎?這就是圖和會話。Graph類用于構造計算圖,Session用于執行和計算所有或部分節點。延遲執行的主要優點是在計算圖的定義期間,我們可以構造非常復雜的表達式,而無需直接評估它們并在所需的內存中分配空間。
例如,如果我們使用NumPy來定義一個大的矩陣,我們會立即得到一個內存不足的錯誤。在TensorFlow中,我們將定義一個Tensor,它是多維數組的描述。它可能具有形狀和數據類型,但它沒有實際值。
在上面的Python代碼片段中,我們使用tf.zeros和np.zeros創建一個矩陣,所有元素都設置為零。NumPyNumPy將立即實例化1萬億x1萬億的矩陣所需要的內存量(值為0),但TensorFlow只會聲明形狀和數據類型,但在圖的這一部分執行之前不會分配內存。
聲明和執行之間的核心區別非常重要,因為這是允許TensorFlow在連接到不同機器的不同設備(CPU,GPU,TPU)上分配計算負載的原因。
有了這些核心構建塊,讓我們將簡單程序轉換為TensorFlow程序。一般來說,這可以分為兩個階段:
以下是我們的簡單程序在TensorFlow中的樣子:
我們從導入tensorflow開始。接下來,我們在with語句中創建一個對象Session。這樣做的好處是,在塊執行后會話自動關閉,我們不必自己調用sess.close()。
現在,在with-block內部,我們可以開始構建新的TensorFlow操作(節點),從而定義邊(Tensors)。例如:
a = tf.constant(15, name="a")這就創建了一個名為a的Constant張量,它會生成值15。該名稱是可選的,但是當您想要查看生成的圖時,這個名稱很有用,我們稍后會看到。
但現在的問題是,我們的圖在哪里呢?我的意思是,我們還沒有創建圖,但我們已經添加了這些操作。這是因為TensorFlow為當前線程提供了一個默認圖,它是同一上下文中所有API函數的隱式參數。一般來說,僅僅依靠默認圖就足夠了。但是,對于高級用例,我們還可以創建多個圖。
好了,現在我們可以創建另一個Constant b,也定義了我們基本的算術運算,如multiply,add和divide。所有這些操作都會自動添加到默認圖中。
現在是時候計算結果了。到目前為止,還沒有求值,也沒有給這些張量分配任何實際的數值。我們要做的是運行會話,顯式地告訴TensorFlow執行圖。
我們已經創建了一個會話對象,我們所要做的就是調用ses .run(res)并傳遞一個想求值的操作(這里是res)。這將只運行計算res值所需的計算圖。這意味著為了計算res,我們必須計算prod和sum以及a和b。最后,我們可以打印結果,這就是run()返回的張量。
讓我們導出圖并使用TensorBoard將其可視化:
生成的圖由TensorBoard可視化
順便說一句,TensorBoard不僅非常適合可視化學習,而且還可以查看和調試您的計算圖,所以一定要查看它。
從頭開始實現TensorFlow的API
我們的目標是模仿TensorFlow的基本操作,以便用我們自己的API鏡像我們的簡單程序,就像我們剛才用TensorFlow做的那樣。
我們已經了解了一些核心構建模塊,如Variable,Operation或Graph。這些是我們想要從頭開始實現的構建塊,所以讓我們開始吧。
圖
第一個缺失的部分是圖。A Graph包含一組Operation對象,表示計算單位。此外,圖包含一組Placeholder和Variable對象,它們表示在操作之間流動的數據單位。
對于我們的實現,我們基本上需要三個列表來存儲所有這些對象 此外,我們的圖需要一個調用的方法as_default,我們可以調用它來創建一個用于存儲當前圖實例的全局 變量。這樣,在創建操作,占位符或變量時,我們不必傳遞對圖的引用。
class Graph(): def __init__(self): self.operations = [] self.placeholders = [] self.variables = [] self.constants = [] def as_default(self): global _default_graph _default_graph = self操作
下一個缺失的部分是操作。要回想一下,操作是計算圖中的節點,并在Tensors上執行計算。大多數操作將零或多個張量作為輸入,并產生零個或多個Tensors對象作為輸出。
簡而言之,操作的特征如下:
因此,每個節點只知道它周圍的環境,這意味著它知道輸入的本地輸入和直接傳遞給下一個使用它的節點的輸出。
輸入節點是進入此操作的Tensors(≥0)列表。
這兩個forward和backward只有占位符方法,它們必須通過每一個具體的操作來實現。在我們的實現中,在forward pass(或forward-propagation)期間調用forward,其計算操作的輸出,而backward是在backward pass(或backpropagation)期間調用的,在此過程中,我們計算操作相對于每個輸入變量的梯度。這并不是TensorFlow的工作方式但是我發現如果一個操作是完全自治的,這就更容易推理了,這意味著它知道如何計算輸出和每個輸入變量的局部梯度。
請注意,在這篇文章中我們將只實現forward pass,這意味著我們可以將 backward函數留空并且現在不用擔心它。
每個操作都在默認圖中注冊也很重要。當您想要使用多個圖時,這會派上用場。
讓我們一步一步,首先實現基類:
class Operation(): def __init__(self, input_nodes=None): self.input_nodes = input_nodes self.output = None # Append operation to the list of operations of the default graph _default_graph.operations.append(self) def forward(self): pass def backward(self): pass我們可以使用這個基類來實現各種操作。但是我們馬上要實現的運算都是只有兩個參數a和b的運算。為了使我們的工作更簡單,并避免不必要的代碼重復,讓我們創建一個BinaryOperation,它只負責將a和b初始化為輸入節點。
class BinaryOperation(Operation): def __init__(self, a, b): super().__init__([a, b])現在,我們可以使用BinaryOperation并實現一些更具體的操作,例如add,multiply,divide或matmul(用于兩個矩陣的乘法)。對于所有操作,我們假設輸入是簡單的標量或NumPy數組。這使得我們的操作實現變得簡單,因為NumPy已經為我們實現了它們,尤其是更復雜的操作,例如兩個矩陣之間的點積。后者使我們能夠很容易地在一批樣本上對圖進行評估,并為這批樣本中的每個觀察值計算輸出。
class add(BinaryOperation): """ Computes a + b, element-wise """ def forward(self, a, b): return a + b def backward(self, upstream_grad): raise NotImplementedErrorclass multiply(BinaryOperation): """ Computes a * b, element-wise """ def forward(self, a, b): return a * b def backward(self, upstream_grad): raise NotImplementedErrorclass divide(BinaryOperation): """ Returns the true division of the inputs, element-wise """ def forward(self, a, b): return np.true_divide(a, b) def backward(self, upstream_grad): raise NotImplementedErrorclass matmul(BinaryOperation): """ Multiplies matrix a by matrix b, producing a * b """ def forward(self, a, b): return a.dot(b) def backward(self, upstream_grad): raise NotImplementedError占位符
當我們查看我們的簡單程序及其計算圖時,我們可以注意到并非所有的節點都是操作,尤其是a和b。相反,它們是在會話中計算圖的輸出時必須提供的圖的輸入。
在TensorFlow中,有不同的方法為圖提供輸入值,例如Placeholder,Variable或Constant。
class Placeholder(): def __init__(self): self.value = None _default_graph.placeholders.append(self)我們可以看到,實現Placeholder非常簡單。它沒有使用值(即名稱)初始化,只將自己附加到默認圖中。占位符的值是使用Session.run()的feed_dict可選參數提供的,但在實現會話時將對此進行更多介紹。
常量
我們要實現的下一個構建塊是常量。常量與變量完全相反,因為初始化后它們無法更改。另一方面,變量表示我們的計算圖中的可變參數。例如,神經網絡中的權重和偏差。
使用占位符作為輸入和標簽而不是變量是絕對有意義的,因為它們總是在每次迭代時更改。此外,區別非常重要,因為變量在backward pass 期間被優化,而常量和占位符則不是。所以我們不能簡單地用一個變量來輸入常數。占位符可以工作,但感覺有點被濫用了。為了提供這種特性,我們引入常量。
class Constant(): def __init__(self, value=None): self.__value = value _default_graph.constants.append(self) @property def value(self): return self.__value @value.setter def value(self, value): raise ValueError("Cannot reassign value.")Python中的下劃線有特定的含義。有些實際上只是約定,有些則由Python解釋器強制執行。用單下劃線_它的大部分是按慣例。因此,如果我們有一個名為_foo的變量,那么這通常被看作是一個暗示,即一個名稱將被開發人員視為私有的。但這并不是解釋器強制執行的,也就是說,Python在私有變量和公共變量之間沒有這些明顯的區別。
但是還有雙下劃線__,也叫“dunder”。解釋器對dunder的處理是不同的,它不僅僅是一種約定。它實際上應用了命名混淆。看看我們的實現,我們可以看到我們在類構造函數中定義了一個屬性__value。由于屬性名中有雙下劃線,Python將在內部將屬性重命名為類似于_Constant__value的名稱,因此它使用類名作為屬性的前綴。這個特性實際上是為了在處理繼承時防止命名沖突。但是,我們可以將此行為與getter結合使用來創建一些私有屬性。
我們所做的是創建一個dunder屬性__value,通過另一個“publicly”可用屬性值公開該值,并在有人試圖設置該值時引發ValueError。這樣,API的用戶就不能簡單地重新分配值,除非他們愿意投入更多的工作,并且發現我們在內部使用dunder。它不是一個真正的常量,更像是JavaScript中的const,但對于我們的目的,它是完全可以的。這至少可以防止值被輕易地重新分配。
變量
計算圖的輸入與正在調整和優化的“內部”參數之間存在質的差異。舉個例子,拿一個計算的簡單感知器y = w * x + b,x表示輸入數據,w和b是可訓練的參數,即計算圖中的變量。在TensorFlow中,變量在調用Session.run()時保持圖中的狀態,而不是每次調用run()時都必須提供占位符。
實現變量很容易。它們需要初始值并將其自身附加到默認圖。
class Variable(): def __init__(self, initial_value=None): self.value = initial_value _default_graph.variables.append(self)會話
在這一點上,我說我們對構建計算圖非常有信心,我們已經實現了最重要的構建塊來鏡像TensorFlow的API并使用我們自己的API重寫我們的簡單程序。我們必須建立一個最后一個缺失的部分 - 那就是Session。
因此,我們必須開始考慮如何計算操作的輸出。
從TensorFlow我們知道一個會話有一個run方法,當然還有其他幾個方法,但是我們只對這個特別的方法感興趣。
最后,我們希望能夠使用我們的會話如下:
session = Session()output = session.run(some_operation, { X: train_X # [1,2,...,n_features]})因此run需要兩個參數,一個operation要執行的參數和一個feed_dict將圖元素映射到值的字典。此字典用于為圖中的占位符提供值。提供的操作是我們要為其計算輸出的圖元素。
為了計算給定操作的輸出,我們必須在拓撲上對圖中的所有節點進行排序,以確保我們以正確的順序執行它們。這意味著我們不能在計算常數a和b之前計算加法。
拓撲排序可以定義為有向無環圖(DAG)中節點的排序,其中對于從節點a到節點B的每條有向邊,節點B在排序中出現在a之前。
該算法非常簡單:
以下是針對我們特定計算圖的算法的動畫插圖:
我們的計算圖的拓撲排序
當我們從拓撲上對以Div開頭的計算圖進行排序時,我們得到的順序是先計算常量,然后是運算Mul和Add,最后是Div。順序也可以是5、15、Add、Mul、Div,這實際上取決于我們處理input_nodes的順序。
讓我們創建一個微小的實用工具方法,在拓撲上從給定節點開始對計算圖進行排序。
def topology_sort(operation): ordering = [] visited_nodes = set() def recursive_helper(node): if isinstance(node, Operation): for input_node in node.input_nodes: if input_node not in visited_nodes: recursive_helper(input_node) visited_nodes.add(node) ordering.append(node) # start recursive depth-first search recursive_helper(operation) return ordering既然我們可以對計算圖進行排序并確保節點的順序正確,那么我們就可以開始研究實際的Session類。這意味著創建類并實現該run方法。
我們要做的是以下幾點:
按照這些步驟,我們最終得到一個可能是這樣的實現:
class Session(): def run(self, operation, feed_dict={}): nodes_sorted = topology_sort(operation) for node in nodes_sorted: if type(node) == Placeholder: node.output = feed_dict[node] elif type(node) == Variable or type(node) == Constant: node.output = node.value else: inputs = [node.output for node in node.input_nodes] node.output = node.forward(*inputs) return operation.output區分不同類型的節點非常重要,因為每個節點的輸出可能以不同的方式計算。請記住,在執行會話時,我們只有變量和常量的實際值,但占位符仍然在等待它們的值。因此,在計算占位符的輸出時,我們必須在feed_dict中查找作為參數提供的值。對于變量和常量,我們可以簡單地使用它們的值作為輸出,對于操作,我們必須收集每個input_node的輸出并調用該操作的forward。
至少我們已經實現了鏡像我們簡單的TensorFlow程序所需的所有部件。讓我們看看它是否真的有用。
為此,讓我們將API的所有代碼放在一個名為tf_api.py的單獨模塊中?,F在我們可以導入這個模塊,并開始使用我們實現的模塊。
import tf_api as tf# create default graphtf.Graph().as_default()# construct computational graph by creating some nodesa = tf.Constant(15)b = tf.Constant(5)prod = tf.multiply(a, b)sum = tf.add(a, b)res = tf.divide(prod, sum)# create a session objectsession = tf.Session()# run computational graph to compute the output for 'res'out = session.run(res)print(out)當我們運行此代碼時,假設到目前為止我們已經完成了所有操作,它將正確地打印3.75到控制臺。這正是我們希望看到的輸出。
這看起來和我們用TensorFlow做的很相似,對吧?唯一的區別是大寫,但這是故意的。而在TensorFlow中,實際上所有東西都是一個操作——即使是占位符和變量——我們沒有將它們實現為操作。為了區分它們,我決定使用小寫操作并大寫其余部分。
結論
我希望這篇文章對你有所幫助,TensorFlow現在有點不那么令人生畏了。
總結
以上是生活随笔為你收集整理的tensorflow计算图_通过从头开始模仿其API来了解TensorFlow的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 贪心算法之分发饼干
- 下一篇: bat脚本交互输入_Shell脚本的应用