PaddleFluid和TensorFlow基本使用概念對比

PaperWeekly發表於2018-06-20

深度學習平臺的演化

時至今日,深度學習已成為事實上最流行的機器學習技術。學術界多年研究加上工業界的長期實踐提出了若干有效的基本建模單元:全連線,卷積,迴圈神經網路等;設計各類訓練技巧:初始化方法,跨層連線,各類 norm 技術等;發明了各種新的最佳化演算法:Adadelta,Adam 等;各類固定的網路結構:highway, residual, attention 等紛紛湧現,不勝列舉。學術界工業界多年的付出共同促成了深度學習方法今日的影響力。 

學術研究和生產實踐中積累了大量的知識,能夠很好的解釋神經網路中基本模組各自獨的學習能力和特性。基本模組和訓練技術的組合能夠搭建出千變萬化的神經網路模型。基本模組和訓練技術是有限的,但他們的組合卻是千變萬化,這是深度學習方法的魅力所在,也是難度所在。

正是這樣高度的模組化特性,研究者和工程師們都在努力避免重複造輪子以提高研究和生產的效率,又進一步催生了深度學習平臺技術的發展,深度學習框架已演變成為 AI 基礎設施中重要的一部分。從 Theano,到 DistBelief,到 TensorFlow;從 Caffe 到 Caffe2;從 Torch 到 PyTorch;從 PaddlePaddle 到 PaddleFluid,深度學習平臺技術也經歷了兩代的演化,並向著第三代平臺技術邁進。 

站在歷史發展的今天,當我們準備切換嘗試使用一個新的深度學習平臺作為支援自己學習和研究的工具時,平臺技術都發生了哪些演化,能夠為我們的帶來什麼便利呢?先讓我們來看看深度學習框架解決的三大問題: 

  • 如何描述計算以支援未來潛在會出現的新模型? 

  • 如何高效利用異構裝置最大化算力? 

  • 如何利用網路中的計算機進行分散式計算來處理千萬億級別的資料? 

以上三個問題中的第一個和使用者研究者最為密切相關。這篇文章我們透過分析 PaddleFluid 和 TensorFlow 的不同設計理念,來了解一個深度學習框架如何抽象深度學習模型,來看看我們的使用經驗如何在不同深度學習平臺之間過度和遷移。

如何描述計算

讓我們首先來看看 PaddleFluid 和 TensorFlow 在“如何描述機器學習模型”這一問題上各自的選擇。

TensorFlow之 Computation Graph 

TensorFlow 使用資料流圖(Dataflow Graph)來描述機器學習模型中的涉及到的所有計算(computation)和狀態(state)。一個 TensorFlow 模型只有一個計算圖,計算圖中包括數學運算和運算的物件(引數),甚至也包括:引數的初始化、最佳化演算法部分(對可學習引數的更新規則),以及資料預處理等。 

這樣的一個計算圖可以更進一步解釋: 

  • 一個機器學習模型,在 TensorFlow 中用一個有向無環圖表示;

  • 圖中的結點對應了機器學習模型中的某個具體運算,在 TensorFlow 中稱之為:Operation;

  • 圖中的邊是 Operation 之間的輸入輸出資料流動。

在 TenorFlow 中,Operation 的輸入輸出統一用 Tensor 表示,這裡可以簡單地理解為Tensor 構成了計算圖中的邊。 

總結之: 

1. 在這一篇中,我們暫不考慮 TensorFlow 在分散式、異構計算方面的設計,TensorFlow 使用計算圖(一個有向無環圖)來描述機器學習模型,任何模型的定義和最佳化過程將被轉換為一個計算圖。計算圖中的結點是 Operation,表示如何計算;計算圖中的邊是 Operation 之間的輸入輸出資料流動。在 TensorFlow 中用 Tensor 表示資料; 

2. TensorFlow 的計算圖遵循:先定義再執行的原則(deferred execution),也就是一個計算圖(這裡可以理解為代表了神經網路的網路拓撲)需要預先宣告,一旦宣告,執行時無法改變其結構。

PaddleFluid之 Program 

如何描述計算很大程度決定了一個神經網路框架計算功能的完備性。深度學習模型和方法歷經二十多年的發展:“依次執行一組計算的前向,再以和前向計算相反的順序執行反向計算,中間無分支無互動”,這樣的模型結構已經無法滿足研究者和千千萬萬框架使用者的想象力。

從 PaddleFluid 的設計目標 [1] 來看,在如何描述機器學習模型這一核心問題上,PaddleFluid 的目標是:創造一種新的計算描述方式,不但能夠描述至今為止人們已知的主流神經網路模型,並且能夠支援未來會出現的任意模型

PaddleFluid 是如何做到支援未來出現的新模型這一目標呢?PaddleFluid 的設計選擇是:對使用者來說,用一段 Program (在 PaddleFluid 內部會被轉化為一種叫作 ProgramDesc 的描述語言),而不是用計算圖來描述機器學習模型。 Program 用符合使用者使用直覺的方式,提供一種新的描述語言能夠描述任意複雜的機器學習模型。

對所有計算機專業同學學習程式語言的第一課一定是建立對“程式語言的三種執行結構:順序執行,條件選擇和迴圈執行”的認識。計算機世界的所有可計算邏輯都是由這三種執行結構表示,用這三種結構描述的邏輯是可計算的。那麼同樣道理,對一個神經網路框架來說,如果可以和程式語言一樣提供對這三種執行結構的支援,那麼將可以描述任意複雜的,可被計算機計算的,機器學習模型。PaddleFluid透過提供對這三種執行結構的支援,來做到對任意複雜模型的描述。

具體來說: 

1. Fluid 的核心設計理念都可以類比到程式語言,如果已經有寫程式的經驗,那麼使用 Fluid 構建神經網路模型的體驗,將非常接近寫程式;

2. 在 PaddleFluid 中,使用者不會顯示地感知“計算圖”這樣的概念,一個機器學習模型被描述為一個 Fluid Program (Fluid 內部稱之為 ProgramDesc );

  • 一個 Fluid Program 由一組巢狀的 Block 構成。 Block 的概念可以類比到 C++ 或是 Java 中的一對大括號,或是 Python 語言中的一個縮排快;

  •  Block 中的計算由順序執行、條件選擇或者迴圈執行三種方式組合,構成複雜的計算邏輯

3. Fluid Program 中包含對計算和計算物件的描述。計算的描述稱之為 Operator;計算作用的物件(或者說 Operator 的輸入和輸出)被統一為 Tensor。 

  • 在描述計算和計算的作用物件這一問題上,各個深度學習框架的選擇是相同的,如果有一個平臺的使用經驗,那麼將非常容易在各個平臺之間進行遷移。

總結

下面的表格總結了 TensorFlow 和 PaddleFluid 在描述機器學習模型上的不同設計選擇。可以看到,Operator和Tensor這些構成模型的基礎元素在兩個平臺中是相似的。如果有任一個平臺的使用經驗,可以非常快速的將這些概念在不同平臺之間類比推廣。

PaddleFluid和TensorFlow基本使用概念對比

核心使用概念

下面的小節,我們將更詳細地瞭解核心使用概念在兩個平臺的使用方法。 

資料表示和計算的物件:Tensor 

 Tensor 是向量矩陣概念的擴充套件,是神經網路模型計算操作的基本物件。這在是今天所有主流深度學習平臺的共同選擇。 

可以簡單地將 Tensor 理解為一個 N 維向量,它可以有任意多的維度。一個 Tensor 具有兩個基本特徵: 

1. 資料型別:每個 Tensor 的所有元素具有同樣的、已知的資料型別;

2. 大小(或者說形狀):即維度的個數(rank,階)以及各維度的長度。 

  • Tensor 某些維度的長度在定義模型階段可能是未知的,在實際演算法執行時才能確定。例如一個 mini-batch 中包含的樣本數目(batch size),或者是一個 mini-batch 中序列的最大長度。

TensorFlow中的Tensor

TensorFlow 的內部實現中, Tensor 的儲存方式就是一個 N 維陣列,其中每個元素都是一個具體的數值,例如整形、浮點等。如“TensorFlow”這個名字所表達的, Tensor 就是TensorFlow 中“被運算”的物件。在一個演算法的執行中,Operation 輸入是 Tensor,經過運算的中間結果是 Tensor,最終結果也是 Tensor

TensorFlow 中,有一些特殊的 Tensor,其中比較常見的有:

1.  tf.Variable (變數): Variable 用於表示機器學習演算法中的引數,具有全域性可見性。和一般的 Tensor 相比, Variable 不受 Session (Session 的概念下文會詳細解釋)的約束,因此在分散式計算的情況下,多個計算單元可以看到同一個 Varible ;

2.  tf.placeholder : placeholder 型別的 Tensor 在執行時必須接入具體的資料,通常用於引入輸入資料;

3.  tf.constant :常量 Tensor,用於生成常用的常量資料,例如全零、全 1 等。

PaddleFluid中的Tensor

PaddleFluid 中也使用 Tensor 作為神經網路中輸入輸出資料的統一表示。Tensor 的概念在今天主流的深度學習平臺中都是完全相同,可以在各個深度學習框架之間直接無縫遷移。

在 Fluid 中也同樣存在三種特殊的 Tensor

1. 模型中的可學習引數

模型中的可學習引數生存期和整個訓練任務一樣長,會接受最佳化演算法的更新。在 PaddleFluid 中同樣以 Variable 表示;

使用者在絕大多數情況下都不需要自己來建立網路中的可學習引數,Fluid 為幾乎常見的神經網路基本計算模組都提供了封裝。以最簡單的全連線模型為例,下面的程式碼片段會直接為全連線層建立連線權值 WW 和偏置( bias )兩個可學習引數,無需顯示地呼叫 variable 相關介面建立可學習引數

import paddle.fluid as fluid

y = fluid.layers.fc(input=x, size=128, bias_attr=True)

2. 輸入輸出Tensor

整個神經網路的輸入資料也是一個特殊的 Tensor,在這個 Tensor 中,一些維度的大小在定義模型時無法確定(通常包括:batch size;如過 mini-batch 之間,資料可變,也會包括序列的最大長度,圖片的寬度和高度等),在定義模型時需要佔位;

PaddleFluid 中使用 fluid.layers.data 來接入輸入資料, fluid.layer.data 需要提供輸入 Tensor 的 形狀資訊,當遇到無法確定的維度 時, 相應維度指定為 None ,如下面的程式碼片段所示: 

import paddle.fluid as fluid

x = fluid.layers.data(name="x", shape=[2, None, 3], dtype="int64")

3. 常量 Tensor 在 PaddleFluid 中需要透過組合 Tensor 和 fluid.layers.assign 來實現。

總結 

1. 在 TensorFlow 和 PaddleFluid 中都統一使用 Tensor 描述神經網路的輸入輸出以及中間結算結果;

2. 對可學習引數這一類特殊的 Tensor: 

TensorFlow 中,可學習引數用 tf.Variable (假設這裡已經執行 import tensorflow as tf)表示;

在 Fluid 中可學習引數使用 fluid.Variable (假設這裡已經執行 import paddle.fluid as fluid )表示;

不論是使用 TensorFlow 還是 PaddleFluid,通常都可以直接使用較高層次的 API,其中已經封裝了幾乎所有常見神經網路單元,例如全連線、LSTM、CNN 等,這些封裝中都已經為使用者正確的建立了該模組所需的可學習引數。通常不需要自己來建立可學習引數

3. 對輸入這一類特殊的 Tensor: 

TensorFlow 中用 tf.placeholder 完成佔位功能;

對使用者來說,邏輯上可認為等價於 PaddleFluid 中的 fluid.layers.data ;

但需注意,框架內部的實現機制不盡相同。 tf.placeholder 是一個 Tensor,而 pd.layers.data 建立輸出 Tensor 的同時,還建立了 Feed 資料相關的 operator。

計算原語:Operation/Operator

Tensor 是今天所有主流深度學習框架的統一資料表示(輸入、輸出、中間計算結果、模型的可學習引數都是 Tensor)。另一方面,對資料的操作,在主流深度學習框架中也高度統一為:Operator/Operation。在中文中,通常我們會習慣將其稱之為運算元。

注:在 TensorFlow 的官方文件中,使用 Operation 來稱呼對 Tensor 的操作和變化,而在 PaddleFluid 中使用 Operator 稱呼對 Tensor 的操作,這兩者沒有本質區別。下文將交叉使用兩者,但他們實際上是同一概念。

Operation/Operator 接受多個 Tensor 作為輸入,輸出若干個 Tensor,表示了從輸入到輸出的變化。

TensorFlow中的Operation 

一個 Operation,接受若干個 Tensor 作為輸入,輸出若干個 Tensor 。可以看出,Operator 作為圖的結點,從進入該結點的邊(tensor)獲得資料並完成計算,然後結果的Tensor 作為從該結點出發的邊。一個典型的 Operator 是 tf.matmul ,它接受兩個 Tensor輸入,將二者相乘,並輸出一個 Tensor 作為結果。TensorFlow 提供的所有運算元,可以在 API 幫助文件 [2] 中檢視。 

PaddleFluid中的Operator 

PaddleFluid 中的 Operator 完全等價於 TensorFlow 中的 operation。PaddleFluid 支援的所有運算元,可以在 API 幫助文件 [3] 中檢視。 

為了便於使用者使用,在 Python 端,Fluid 中的 Operator 被進一步封裝入paddle.fluid.layers , paddle.fluid.networks 等模組。這是因為:一些常見的對Tensor的操作可能是有更多基礎操作構成,例如:l2 norm 內部由 reduce、elementwise_add,scale 等多個 Operator 組合計算邏輯完成,為了提高使用的便利性,框架內部對基礎 Operator 進行了一些封裝,包括建立 Operator 依賴可學習引數,可學習引數的初始化細節等,減少使用者重複開發的成本。 

對所有深度學習框架都面臨同樣的封裝,在絕大多數情況下,使用者很少會直接與框架底層的 Operator 直接打交道,而是使用框架提供的 layers,networks 等模組,降低開發的程式碼量。不論是什麼樣的概念,他們在各個礦建之間的本質和作用都是相同的:對 Tensor 的變換。 

總結 

不論叫作 Operation、Operator 還是 layers,他們在各深度學習平臺中的含義和作用都是相同的:對 Tensor 的變換。是一個深度學習平臺提供的基礎計算能力。可以在每個平臺各自的 API 幫助文件中查到。 

在各個深度學習平臺都已加入 ONNX 專案的今天,每個深度學習平臺提供給大家的基本運算元都已趨同,與此同時,每個平臺也各有其特點,會提供一些獨特的運算元,方便某一類任務的開發。

構建模型並執行 

至此,我們看到了構成模型的基礎要素:Tensor 和 Operator 在兩個框架之間能夠直接遷移。最後一步,我們 看看在兩個平臺之上,整個訓練任務是如何執行起來的。 

TensorFlow中的Graph和Session 

1. TensorFlow 以計算圖描述機器學習模型,圖中的結點是 Operation,邊是 Tensor。在 TensorFlow 中,tf.Graph 維護了整個圖的拓撲資訊;

  • 對於 graph,需要額外注意一點:TensorFlow 的一個計算圖,會有一個 collection 的概念與之對應。 collection 是以圖為上下文的 key-value 表,用於維護圖級別的(也就是全域性的)資料。例如 一個 variable 可以設定為是全域性可見,此時這個 variable 相關的所有資訊會在計算圖對應的 collection 中進行維護。

2. 在圖“之上”,TensorFlow 利用 session 機制來實際執行模型。 

  • 對於一個定義好的 TensorFlow 的 Graph (也就是一個定義好的神經網路模型),為它建立一個 tf.Session 就可以對這個模型執行初始化、執行等流程層面的操作;

  • 更精確地說, TensorFlow 的 Session 連線了使用者程式和後端 Runtime,這裡所說的“使用者程式”就是 TensorFlow 的使用者對機器學習模型的定義和設定等,而後端 Runtime 是實際完成演算法訓練、測試等真實計算任務的程式。這種“連線”也是一種“隔離”,將使用者和真實計算時涉及的分散式計算等細節隔離,便於使用。

Fluid中的Program和Executor 

1. PaddleFluid 使用 Program 描述神經網路模型,對使用者來說,並沒有計算圖的概念。使用者定義的所有 Tensor 以及對 Tensor 的操作:Operator 都會被加入一段 Program 中;

  • 一段 Program 由巢狀的 Block 構成,但使用者無需顯示地建立 Block 或是顯示地注意到Block 的存在;

  • 在 PaddleFluid 程式中, Block 是在呼叫 while_op , if_op , parallel_do 等特殊 Operator 時,由這些 Operator 來建立;

  • 對使用者使用來說,只需要知道自己正在向一段 Fluid Program 中新增變數(Tensor)和操作(Operator)即可。

2. PaddleFluid 利用 Executor 來執行一段 Fluid Program。 

  • 為進一步理解 Fluid 中 Executor 的作用,需要先解釋一下 Fluid 程式的執行流程。 下圖展示單機上,Fluid 程式的執行流程:

PaddleFluid和TensorFlow基本使用概念對比

▲ Fig. Fluid本地訓練任務執行流程圖

1. Fluid 設計思想和靈感非常類似於程式設計語言,和高階編譯語言 C++/Java 編寫程式的過程非常類似,Fluid 程式執行分為兩個重要階段:編譯時和執行時;

2. 編譯期,使用者透過呼叫 Fluid 提供的運算元,向一段 Program 中新增變數(Tensor)以及對變數的操作(Operators 或者 Layers)。使用者只需要描述核心的前向計算,不需要關心反向計算,分散式下,異構裝置下如何計算;

3. 原始的 Program 在平臺內部轉換為中間描述語言: ProgramDesc ;

4. 編譯期最重要的一個功能模組是 Transpiler。Transpiler 接受一段 ProgramDesc ,輸出一段變化後的 ProgramDesc ,作為後端 Executor 最終需要執行的 Fluid Program ;

最為常用的 Transipler 包括:

1. 記憶體最佳化 Transipler:透過對變數讀寫依賴關係分析,插入記憶體回收 Operator 以維持執行過程中較小的記憶體開銷; 

2. 分散式環境下的 Transpiler:接受使用者定義的 local Program ,生成 Parameter Client 和 Parameter Server 執行的兩段 Program 。 

5. 後端 Executor 接受 Transpiler 輸出的這段 Program ,依次執行其中的 Operator(可以類比為程式語言中的指令),在執行過程中會為 Operator 建立所需的輸入輸出並進行管理。 

從上面的過程中可以看到,Fluid 程式的執行過程分為:編譯器的定義 Program ,和建立Executor 執行 Program 。 Executor 執行一段 Program 的過程是不可互動和不可中斷的。在 PaddleFluid 中,可以建立多餘一段 Program 。預設情況,一個 PaddleFluid 程式中存在 2 段 Program:

1. fluid.framework.default_startup_program :其中定義了建立模型引數,輸入輸出,以及模型中可學習引數的初始化等各種操作;

  •  default_startup_program 可以由框架自動生成,使用時無需顯示地建立;

  • 如果呼叫修改了引數的預設初始化方式,框架會自動的將相關的修改加入default_startup_program 。

2.  fluid.framework.default_main_program :定義了神經網路模型,前向反向計算,以及最佳化演算法對網路中可學習引數的更新;

  • 使用 Fluid 的核心就是構建起 default_main_program 。

3. PaddleFluid 中的 Scope 類似於 TensorFlow 中的 collection 這一概念,但在 Fluid 中Scope 是框架後端概念,使用者無法直接操作。因此,在使用框架時無需關心。

總結 

對使用框架的使用者來說,可以認為 TensorFlow 中的 Graph 等價於 PaddleFluid 中的 Program,他們在框架中的作用完全相同:完成了對模型的定義。 

TensorFlow 中的 Session 在使用邏輯上非常類似於 PaddleFluid 中的 Executor。 

TensorFlow 透過 Session 來完成計算圖上的初始化,計算等執行邏輯,連線了 TensorFlow 的前端和後端;

PaddleFluid 中透過 Executor 來執行一段使用者定義的 Fluid Program 。

1. Executor 連線了 PaddleFluid 的前端和後端;

2. Executor 接受使用者定義的原始模型(一段 Program ),透過呼叫系統中不同功能更的Transpiler 完成對原始 Program 的變化,進行最佳化。

完整例項:如何完成一個機器學習模型的訓練

這一節,我們以 MNIST 手寫數字識別問題 —— 機器學習任務的“Hello World”問題和資料,為例,透過一個可以執行的完整例項,來學習上文介紹的概念如何在 TensorFlow 和 PaddleFluid 平臺下如何使用和互相遷移。 

TensorFlow例項 

以下使用 Tensorflow 定義一個基本的 MLP(單隱層的神經網路)對該問題建模,以說明 Tensorflow 的基本使用方法。 

步驟1:定義資料

# 資料和標籤定義
x = tf.placeholder(tf.float32, shape=[None, 784])
y_ = tf.placeholder(tf.int32, shape=[None,])

如前所述, tf.placeholder 是用於引入資料的特殊 Tensor,這裡分別用 x 和 y_ 代表資料的特徵和標籤。

步驟2:定義模型

# opeartion的計算邏輯定義
y = tf.layers.dense(inputs=x, units=10)

# operation的loss計算方式指定
cross_entropy = tf.losses.sparse_softmax_cross_entropy(labels=y_, logits=y)

# operation最佳化方式指定
train_op = tf.train.AdamOptimizer().minimize(cross_entropy)

這段程式分為三個部分: 

1. 引數定義:一個單隱層的 MLP,按照 Tensorflow 的計算圖抽象方式,即對於輸入 xx,經過 y=\omega x+by=ωx+b 這步計算,得到輸出 yy。其中 xx、yy 是輸入和輸出 tensor,\omegaω 和 bb 是引數 tensor。 

Tensorflow 的 tf.layers 提供了常用的 operation 計算邏輯,這裡用到的 tf.layers.dense 即神經網路中全連線層的計算。 

2. loss計算方式定義:loss 在模型訓練的過程中,用於衡量當前模型產出的結果和目標之間的差距,也是最佳化演算法迭代模型的依據。 tf.losses 中定義了常用的 loss,這裡使用交叉熵(cross entropy),一種多分類情況下常用的 loss。 這裡引數中 y_ 指的是目標標籤,在上面資料引入的部分已經定義。

3. operation構建:在上面已經確定的引數和 loss 之外,還需要指定迭代時需要使用的最佳化演算法。同樣的 operation 可以在執行時使用不同的最佳化演算法。 

步驟3:引數初始化

init = tf.global_variables_initializer()
sess = tf.Session()
sess.run(init)

模型的訓練過程,由 tf.Session 管理, tf.Session.run() 以初始化後引數後的 Graph 為輸入引數,做好模型訓練的準備工作。 

這裡使用的是預設的引數初始化。實際上,神經網路訓練的引數初始化是有多種選擇的,這超出了本篇的覆蓋範圍,暫不贅述將在後面章節詳細討論。 

步驟4:資料輸入 + 執行模型訓練

train_reader = data_iterator()
test_lbl, test_img = load_MNIST("testing")
for step in range(100):
    images_batch, labels_batch = next(train_reader)
    _, loss_val = sess.run(
        [train_op, cross_entropy],
        feed_dict={
            x: images_batch,
            y_: labels_batch.astype("int32")
        })
    print("Cur Cost : %f" % loss_val)

所謂模型迭代,通常是以 batch 為單位向模型給入資料,之後根據指定的 loss 和最佳化方法更新模型引數。核心的函式是對 tf.Session.run() 的呼叫,其中包括之前定義好的 operation、最佳化方法以及給入的資料。

其中,給入資料的來源,是以下函式:

def data_iterator(dataset="training", path="data", batch_size=128):
    batch_idx = 0
    lbl, img = load_MNIST(dataset, path)
    while True:
        # shuffle labels and features
        idxs = np.arange(0, len(lbl))
        np.random.shuffle(idxs)
        shuf_features = img[idxs]
        shuf_labels = lbl[idxs]
        for batch_idx in range(0, len(lbl), batch_size):
            images_batch = shuf_features[batch_idx:
                                         batch_idx + batch_size] / 255.
            images_batch = images_batch.astype("float32")
            labels_batch = shuf_labels[batch_idx:
                                       batch_idx + batch_size].astype("int32")
            yield images_batch, labels_batch

本段程式中用到的 tf_load_MNIST 是從檔案中讀取資料。本段程式的作用是對資料做 shuffle,之後以 batch_size 為長度組織每個 batch 的資料。

步驟5:觀察模型效果

以上步驟已經構建了完整的 Tensorflow 模型訓練程式,每個 batch 觀察一次 loss,可以直觀看到模型的迭代效果:

PaddleFluid和TensorFlow基本使用概念對比

▲ Fig. TensorFlow MNIST手寫數字識別任務代價下降曲線

附:完整程式碼

import numpy as np
import tensorflow as tf

from tf_load_MNIST import load_MNIST


def data_iterator(dataset="training", path="data", batch_size=128):
    batch_idx = 0
    lbl, img = load_MNIST(dataset, path)
    while True:
        # shuffle labels and features
        idxs = np.arange(0, len(lbl))
        np.random.shuffle(idxs)
        shuf_features = img[idxs]
        shuf_labels = lbl[idxs]
        for batch_idx in range(0, len(lbl), batch_size):
            images_batch = shuf_features[batch_idx:
                                         batch_idx + batch_size] / 255.
            images_batch = images_batch.astype("float32")
            labels_batch = shuf_labels[batch_idx:
                                       batch_idx + batch_size].astype("int32")
            yield images_batch, labels_batch


def main():
    # define the network topology.
    x = tf.placeholder(tf.float32, shape=[None, 784])
    y_ = tf.placeholder(
        tf.int32, shape=[
            None,
        ])

    y = tf.layers.dense(inputs=x, units=10)
    cross_entropy = tf.losses.sparse_softmax_cross_entropy(labels=y_, logits=y)
    train_op = tf.train.AdamOptimizer().minimize(cross_entropy)

    # define the initializer.
    init = tf.global_variables_initializer()

    sess = tf.Session()
    sess.run(init)

    train_reader = data_iterator()
    for step in range(100):
        images_batch, labels_batch = next(train_reader)
        _, loss_val = sess.run(
            [train_op, cross_entropy],
            feed_dict={
                x: images_batch,
                y_: labels_batch.astype("int32")
            })
        print("Cur Cost : %f" % loss_val)


if __name__ == "__main__":
    main()

 tf_load_MNIST.py 完整程式碼:

import os
import struct
import numpy as np

def load_MNIST(dataset="training", path="."):
    """
    Python function for importing the MNIST data set.  It returns an iterator
    of 2-tuples with the first element being the label and the second element
    being a numpy.uint8 2D array of pixel data for the given image.
    """
    path = os.path.join(os.path.abspath('.'), "data")

    if dataset is "training":
        fname_img = os.path.join(path, "train-images.idx3-ubyte")
        fname_lbl = os.path.join(path, "train-labels.idx1-ubyte")
    elif dataset is "testing":
        fname_img = os.path.join(path, "t10k-images.idx3-ubyte")
        fname_lbl = os.path.join(path, "t10k-labels.idx1-ubyte")
    else:
        raise ValueError("dataset must be 'testing' or 'training'")

    # Load everything in some numpy arrays
    with open(fname_lbl, "rb") as flbl:
        magic, num = struct.unpack(">II", flbl.read(8))
        lbl = np.fromfile(flbl, dtype=np.int8)

    with open(fname_img, "rb") as fimg:
        magic, num, rows, cols = struct.unpack(">IIII", fimg.read(16))
        img = np.fromfile(fimg, dtype=np.uint8).reshape(len(lbl), rows * cols)

    return lbl, img

PaddleFluid例項

步驟1:定義資料

PaddleFluid 中以 fluid.layers.data 來接收輸入資料。

import numpy as np

import paddle.fluid as fluid
import paddle.v2 as paddle

# define the input layers for the network.
x = fluid.layers.data(name="img", shape=[1, 28, 28], dtype="float32")
y_ = fluid.layers.data(name="label", shape=[1], dtype="int64")

Fluid 中 Tensor 的第 0 維度固定為 batch size。在上面程式碼段中,影像輸入 x 的形狀為:[1, 28, 28]。這三個維度的含義分別是:channel 數目,影像的高度和寬度。 

實際上 Fluid 框架內部,一幅影像輸入是一個 4-D Tensor,所有 Tensor 的第 0 維固定為 batch size。框架內部會自動為batch size進行填充佔位。無需對batch size指定填充佔位。 

如果除去 batch size(第 0 維度)外,如果 Tensor 某一維度的大小隻能在執行時確定,可以在該位置上直接指定 None 進行佔位。

步驟2:定義模型 

透過呼叫 Fluid 提供的運算元定義含有一個隱層的神經網路。Fluid 模型的分為模型結構和最佳化方法兩部分。這一點與 TensorFlow 程式十分相似似,使用概念可以直接對應進行遷移。

# define the network topology.
y = fluid.layers.fc(input=x, size=10, act="softmax")
loss = fluid.layers.cross_entropy(input=y, label=y_)
avg_loss = fluid.layers.mean(loss)

# define the optimization algorithm.
optimizer = fluid.optimizer.Adam(learning_rate=1e-3)
optimizer.minimize(avg_loss)

Fluid 使用 Program 而不是計算圖描述模型,一般情況下,使用者無需關心 Program 的細節,當呼叫以上 layers 時,會向一個全域性的 Program:fluid.framework.default_main_program 中插入變數(Tensor)和對變數的操作(上述程式碼段中的 layers 和 optimzier)。 

步驟3:引數初始化 

如上文介紹,Fluid 程式中的 Executor 是連線 Fluid 前端和後端的介面。 

預設一個Fluid模型存在至少兩段 Program。用於初始化網路中的可學習引數的那一段Program 叫作 fluid.default_startup_program() 。

只有執行器 executor 可以執行 Fluid Program,因此,在初始化網路中的可學習引數之前,需要首先建立一個 Fluid executor。

# define the executor.
place = fluid.CPUPlace()
exe = fluid.Executor(place)
exe.run(fluid.default_startup_program())

在以上程式碼段中, place 用於告訴 executor 一段 Fluid Program 在何種裝置上執行,常見的有 fluid.CPUPlace() 和 fluid.CUDAPlace() 。 

步驟4:資料輸入 + 執行模型訓練 

我們在步驟 2 中定義的神經網路模型最終被插入一段叫做fluid.framework.default_main_program 的 Fluid Program 中。

網路可學習引數初始化之後,可以透過讓執行器 Executor 執行這段fluid.framework.default_main_program 來進行訓練。

train_reader = paddle.batch(
        paddle.reader.shuffle(paddle.dataset.mnist.train(), buf_size=5000),
        batch_size=BATCH_SIZE)
feeder = fluid.DataFeeder(place=place, feed_list=[x, y_])

for pass_id in range(100):
    for batch_id, data in enumerate(train_reader()):
        loss = exe.run(
            fluid.framework.default_main_program(),
            feed=feeder.feed(data),
            fetch_list=[avg_loss])
        print("Cur Cost : %f" % (np.array(loss[0])[0]))

從上面的程式碼片段中可以看到,Fluid 程式的訓練過程和 TensorFlow 程式的訓練過程非常接近,都放在一個 for 迴圈中,迴圈讀取一個 mini-batch 資料,呼叫執行器執行 Fluiddefault_main_program :接收 mini-batch 輸入,在其上進行前向,反向和引數更新計算。 

注:上面程式使用了 Fluid 內建的 MNIST 資料,和我們提供給 TensorFlow 示例程式的 MNIST 資料完全一樣。 

步驟5:觀察模型效果 

以上步驟已經構成了完整的 Tensorflow 模型訓練程式,每個 batch 觀察一次 loss,可以直觀看到模型的迭代效果:

PaddleFluid和TensorFlow基本使用概念對比

▲ Fig. Fluid MNIST手寫數字識別任務代價下降曲線

附:完整程式碼

import numpy as np

import paddle.fluid as fluid
import paddle.v2 as paddle


def main():
    BATCH_SIZE = 128

    # define the input layers for the network.
    x = fluid.layers.data(name="img", shape=[1, 28, 28], dtype="float32")
    y_ = fluid.layers.data(name="label", shape=[1], dtype="int64")

    # define the network topology.
    y = fluid.layers.fc(input=x, size=10, act="softmax")
    loss = fluid.layers.cross_entropy(input=y, label=y_)
    avg_loss = fluid.layers.mean(loss)

    optimizer = fluid.optimizer.Adam(learning_rate=5e-3)
    optimizer.minimize(avg_loss)

    # define the executor.
    place = fluid.CPUPlace()
    exe = fluid.Executor(place)
    exe.run(fluid.default_startup_program())

    train_reader = paddle.batch(
        paddle.reader.shuffle(paddle.dataset.mnist.train(), buf_size=5000),
        batch_size=BATCH_SIZE)
    feeder = fluid.DataFeeder(place=place, feed_list=[x, y_])

    for pass_id in range(100):
        for batch_id, data in enumerate(train_reader()):
            loss = exe.run(
                fluid.framework.default_main_program(),
                feed=feeder.feed(data),
                fetch_list=[avg_loss])
            print("Cur Cost : %f" % (np.array(loss[0])[0]))

if __name__ == "__main__":
    main()

總結

在這一節中,基於手寫數字識別資料集 MNIST,我們透過一個完整可執行的例子,展示了使用 TensorFlow 和 PaddleFluid 實現了同樣一個含有單隱層的全連線神經網路。透過這個例子展示主流深度學習框架核心概念、使用者介面、使用體驗的設計選擇。

可以看到儘管內部實現有著非常大的差異,但是對使用者來講,深度學習模型的核心概念,包括:Tensor、Operation、Optimzier、網路初始化等,在各個主流深度學習框架中都有著對應的實現。如果有著一個框架的使用經驗,這種使用經驗將非常容易遷移到其它深度學習框架下。

從迭代效果看,這一篇中這個簡單的模型依照預期擬合住了訓練資料,但是效果並不驚豔。原因在於:輸入資料是圖片畫素值,這裡的神經網路模型十分簡單,擬合能力有限。在後面的篇幅,我們將會使用更加複雜和實用的例子,進一步對比如何不同深度學習平臺如何訓練同一個神經網路,我們的使用經驗如何在不同框架之間進行切換和推廣,幫助我們選擇最適合的工具提高研究和生產的效率。

相關連結

[1]. PaddleFluid的設計目標

https://github.com/PaddlePaddle/Paddle/blob/develop/doc/fluid/design/motivation/fluid.md

[2]. TensorFlow API幫助文件

https://www.tensorflow.org/api_docs/python/?hl=zh-cn

[3]. PaddleFluid API幫助文件

http://www.paddlepaddle.org/docs/develop/api/en/fluid/layers.html

相關文章