都9102年了還不懂動態圖嗎?一文帶你瞭解飛槳動態圖

PaddlePaddle發表於2019-08-28

導讀:飛槳PaddlePaddle致力於讓深度學習技術的創新與應用更簡單。飛槳核心框架已提供了動態圖(DyGraph)相關的API和文件,並且還附有Language model、Sentiment Classification、OCR、ResNet等模型的動態圖版本官方實現。飛槳目前兼具了動態圖和靜態圖的優勢,同時具備靈活性和高效性。 


飛槳動態圖&靜態圖整體結構如下:

都9102年了還不懂動態圖嗎?一文帶你瞭解飛槳動態圖

1. 動態圖與靜態圖

目前深度學習框架主要有宣告式程式設計和指令式程式設計兩種程式設計方式。宣告式程式設計,程式碼先描述要做的事情但不立即執行,對深度學習任務建模,需要事先定義神經網路的結構,然後再執行整個圖結構,這一般稱為靜態圖模式。而指令式程式設計對應的動態圖模式,程式碼直接返回運算的結果,神經網路結構的定義和執行同步。通常來說,靜態圖模式能夠對整體性做編譯優化,更有利於效能的提升,而動態圖則非常便於使用者對程式進行除錯。

2. 飛槳動態圖的三大特色

飛槳的DyGraph模式是一種動態的圖執行機制。與靜態計算圖的執行機制不同,DyGraph模式下的操作可以立即獲得執行結果,而不必等待計算圖全部構建完成。這樣可以讓開發者更加直觀地構建深度學習任務並進行模型的除錯,同時還減少了大量用於構建靜態計算圖的程式碼,使得編寫、除錯網路的過程變得非常便捷。

飛槳DyGraph動態圖模式,主要有三大特色:

  • 靈活便捷的程式碼書寫方式:能夠使用Python的控制流(for,if…else..等)進行程式設計。

  • 便捷的除錯功能:直接使用Python的列印方法即時列印所需要的結果,從而檢查正在執行的模型結果便於除錯。

  • 和靜態執行圖通用的模型程式碼:對於沒有使用Python控制流的網路,動態圖的程式碼可以直接在靜態圖模式下執行,提升執行的效率。

3. 飛槳動態圖與靜態圖的直觀對比

讓我們通過一個實際例子,直觀地感受一下動態圖與靜態圖在使用過程中的差異。

想要實現如下的功能:

(1)  如果inp1各元素之和小於inp2各元素之和,那麼執行inp1與 inp2各元素對應相加。

(2)  如果inp1各元素之和大於等於inp2各元素之和,那麼執行inp1與 inp2各元素對應相減。

如果使用飛槳動態圖來實現的話,程式碼如下:

import paddle.fluid asfluid
import numpy as np
inp1 = np.random.rand(4, 3, 3)
inp2 = np.random.rand(4, 3, 3)
# dynamic graph
with fluid.dygraph.guard():    
if np.sum(inp1) <np.sum(inp2):       
 x =fluid.layers.elementwise_add(inp1, inp2)    else:        
x =fluid.layers.elementwise_sub(inp1, inp2)    dygraph_result = x.numpy()


在飛槳動態圖的模式下,可以靈活複用(if…else…)等Python控制流操作,關鍵程式碼只需要短短6行,非常簡單。

 

而如果換用靜態圖方式來實現的話,程式碼可就複雜多了。具體如下:

import paddle.fluid asfluid
import numpy as np
inp1 = np.random.rand(4, 3, 3)
inp2 = np.random.rand(4, 3, 3)
# static graph
with new_program_scope():    
inp_data1 =fluid.layers.data(name='inp1', shape=[3, 3], dtype=np.float32)    
inp_data2 =fluid.layers.data(name='inp2', shape=[3, 3], dtype=np.float32)     
a=fluid.layers.expand(fluid.layers.reshape(fluid.layers.reduce_sum(inp_data1),[1, 1]), [4, 1])    b=fluid.layers.expand(fluid.layers.reshape(fluid.layers.reduce_sum(inp_data2),[1, 1]), [4, 1])  
  cond =fluid.layers.less_than(x=a, y=b)
    
 ie =fluid.layers.IfElse(cond) 
   with ie.true_block():    
    d1 =ie.input(inp_data1)      
  d2 =ie.input(inp_data2)     
   d3 =fluid.layers.elementwise_add(d1, d2)       
 ie.output(d3)   
  with ie.false_block():      
  d1 =ie.input(inp_data1) 
       d2 =ie.input(inp_data2)    
    d3 =fluid.layers.elementwise_sub(d1, d2)     
   ie.output(d3)    out = ie()    
 exe =fluid.Executor(fluid.CPUPlace() if not core.is_compiled_with_cuda() elsefluid.CUDAPlace(0))   
 static_result =exe.run(fluid.default_main_program(),feed={'inp1': inp1,'inp2':inp2},fetch_list=out)[0]

怎麼樣?感受到差異了嗎?

直觀一點,直接看程式碼行數。

關鍵程式碼部分,靜態圖方式的程式碼行數有20行,而動態圖方式僅需要短短的6行程式碼。程式碼量減少到1/3,邏輯複雜程度也大大簡化。

這就是飛槳動態圖在Python控制流操作複用和程式碼簡潔性方面的優勢。

除此之外,飛槳動態圖還提供了非常便捷的除錯功能,直接使用Python的列印方法,就可以即時列印出所需要的結果,從而檢查正在執行的模型結果,非常方便除錯。

4. 飛槳動態圖的基本用法

飛槳動態圖具有如此多的優勢,下面講述最基本的一些用法。

(1)  動態圖與靜態圖的最大區別是採用了命令式的程式設計方式,任務不用在區分組網階段和執行階段。程式碼執行完成之後,可以立馬獲取結果。由於採用與我們書寫大部分Python和c++的方式是一致的指令式程式設計方式,程式的編寫和除錯會非常的容易。

(2)  同時動態圖能夠使用Python的控制流,例如for,if else, switch等,對於rnn等任務的支援更方便。

(3)  動態圖能夠與numpy更好的互動。

使用飛槳動態圖,首先需要將PaddlePaddle升級到最新的1.5.1版本,使用以下命令即可。

pip install -q --upgrade paddlepaddle==1.5.1import paddle.fluid as fluidwith fluid.dygraph.guard():

這樣就可以在fluid.dygraph.guard()上下文環境中使用動態圖DyGraph的模式執行網路了。DyGraph將改變以往靜態圖的執行方式,開始執行之後會立即執行,並且將計算結果返回給Python。

Dygraph非常適合和Numpy一起使用,使用fluid.dygraph.to_variable(x)將會將Numpy的ndarray轉換為fluid.Variable,而使用fluid.Variable.numpy()將可以把任意時刻獲取到的計算結果轉換為Numpy ndarray,舉例如下:

import paddle.fluid asfluidimport numpy as npx = np.ones([10, 2, 2], np.float32) with fluid.dygraph.guard():     inputs = []     seq_len = x.shape[0]     for i in range(seq_len):        inputs.append(fluid.dygraph.to_variable(x[i]))     ret =fluid.layers.sums(inputs)     print(ret.numpy())  

得到輸出:

   [[10. 10.]   [10. 10.]]           

以上程式碼根據輸入x的第0維的長度、將x拆分為多個ndarray的輸入,執行了一個sum操作之後,可以直接將執行的結果列印出來。然後通過呼叫reduce_sum後使用Variable.backward()方法執行反向,使用Variable.gradient()方法即可獲得反向網路執行完成後的梯度值的ndarray形式:

    loss =fluid.layers.reduce_sum(ret)    loss.backward()    print(loss.gradient())

得到輸出 :

   [1.]


 5. 飛槳動態圖的專案實戰


下面以“手寫數字識別”為例講解一個動態圖實戰案例,手寫體識別是一個非常經典的影像識別任務,任務中的圖片如下圖所示,根據一個28 * 28畫素的影像,識別圖片中的數字。


MNIST示例程式碼地址:

https://github.com/PaddlePaddle/models/tree/develop/dygraph/mnist


都9102年了還不懂動態圖嗎?一文帶你瞭解飛槳動態圖


介紹網路訓練的基本結構,也比較簡單,兩組conv2d和pool2d層,最後一個輸出的全連線層。

都9102年了還不懂動態圖嗎?一文帶你瞭解飛槳動態圖

飛槳動態圖模式下搭建網路並訓練模型的全過程主要包含以下內容:

5.1   資料準備

首先使用paddle.dataset.mnist作為訓練所需要的資料集:飛槳把一些公開的資料集進行了封裝,使用者可以通過dataset.mnist介面直接呼叫mnist資料集,train()返回訓練資料的reader,test()介面返回測試的資料的reader。

train_reader = paddle.batch(paddle.dataset.mnist.train(),batch_size=BATCH_SIZE, drop_last=True)

5.2  Layer定義

為了能夠支援更復雜的網路搭建,動態圖引入了Layer模組,每個Layer是一個獨立的模組,Layer之間又可以互相巢狀。

使用者需要關注的是,a)Layer儲存的狀態,包含一些隱層維度、需要學習的引數等;b)包含的sub Layer,為了方便大家使用,飛槳提供了一些定製好的Layer結構,如果Conv2D,Pool2D,FC等。c) 前向傳播的函式,這個函式中定義了圖的執行結構,這個函式與靜態圖的網路搭建是完全不一樣的概念,函式只是描述了執行結構,在函式被呼叫的時候程式碼才執行,靜態圖的網路搭建是程式碼真正在執行。

Conv2D是飛槳提供的卷積運算的Layer,Pool2D是池化操作的Layer。

1)定義SimpleImgConvPool 子Layer:SimpleImgConvPool把網路中迴圈使用的部分進行整合,其中包含包含了兩個子Layer,Conv2D和Pool2D,forward函式定義了前向執行時的結構。

class SimpleImgConvPool(fluid.dygraph.Layer)   
 def __init__(self,name_scope, num_filters, filter_size, pool_size, pool_stride, pool_padding=0, pool_type='max',global_pooling=False, conv_stride=1, conv_padding=0, conv_dilation=1, conv_groups=1,act=None, use_cudnn=False, param_attr=None, bias_attr=None):       
  super(SimpleImgConvPool,self).__init__(name_scope)      
   self._conv2d =fluid.dygraph.Conv2D(self.full_name(), num_filters=num_filters, filter_size=filter_size,stride=conv_stride,padding=conv_padding, dilation=conv_dilation, groups=conv_groups,aram_attr=None, bias_attr=None, act=act, use_cudnn=use_cudnn)       
  self._pool2d =fluid.dygraph.Pool2D(self.full_name(), pool_size=pool_size, pool_type=pool_type,pool_stride=pool_stride, pool_padding=pool_padding, global_pooling=global_pooling,use_cudnn=use_cudnn)  
 def forward(self,inputs):      
   x =self._conv2d(inputs)    
     x = self._pool2d(x)      
   return x 


2)構建MNIST Layer,MNIST Layes包含了兩個SimpleImgConvPool子Layer,以及一個FC(全連線層),forward函式定義瞭如圖2所示得網路結構

class MNIST(fluid.dygraph.Layer):    def __init__(self,name_scope):        super(MNIST,self).__init__(name_scope)        self._simple_img_conv_pool_1 = SimpleImgConvPool(self.full_name(), 20,5, 2, 2, act="relu")        self._simple_img_conv_pool_2 = SimpleImgConvPool(self.full_name(), 50,5, 2, 2, act="relu")        pool_2_shape = 50 *4 * 4        SIZE = 10        scale = (2.0 / (pool_2_shape**2 *SIZE))**0.5        self._fc =fluid.dygraph.FC(self.full_name(),10, param_attr=fluid.param_attr.ParamAttr(initializer=fluid.initializer.NormalInitializer(loc=0.0, scale=scale)),act="softmax")    def forward(self, inputs,label=None):        x =self._simple_img_conv_pool_1(inputs)        x =self._simple_img_conv_pool_2(x)        x = self._fc(x)        if label is notNone:            acc =fluid.layers.accuracy(input=x, label=label)            return x, acc        else:            return x


5.3  優化器定義

使用經典的Adam優化演算法:

adam =fluid.optimizer.AdamOptimizer(learning_rate=0.001)

 5.4   訓練


構建訓練迴圈,順序為:1).從reader讀取資料 2).呼叫MNIST Layer 前向網路3).利用cross_entropy計算loss 4)呼叫backward計算梯度 5)呼叫adam.minimize更新梯度,6) clear_gradients()將梯度設定為0(這種方案是為了支援backward of backward功能,如果系統自動將梯度置為0,則無法使用backward of backward功能)

with fluid.dygraph.guard(): 
   epoch_num = 5   
 BATCH_SIZE = 64    
 mnist =MNIST("mnist")  
  adam =fluid.optimizer.AdamOptimizer(learning_rate=0.001)   
 train_reader =paddle.batch(paddle.dataset.mnist.train(), batch_size= BATCH_SIZE,drop_last=True)     np.set_printoptions(precision=3,suppress=True)
    for epoch inrange(epoch_num):      
  for batch_id, data inenumerate(train_reader()):        
    dy_x_data = np.array(         
      [x[0].reshape(1, 28, 28)             
    for x indata]).astype('float32')    
        y_data =np.array(            
    [x[1] for xin data]).astype('int64').reshape(BATCH_SIZE, 1)      
       img =fluid.dygraph.to_variable(dy_x_data)            label =fluid.dygraph.to_variable(y_data)            label.stop_gradient = True         
  cost 
=mnist(img)    
       loss =fluid.layers.cross_entropy(cost, label)        
   avg_loss =fluid.layers.mean(loss)        
   dy_out =avg_loss.numpy()        
   avg_loss.backward()          
 adam.minimize(avg_loss)    
       mnist.clear_gradients()          
 dy_param_value ={}         
  for param inmnist.parameters():           
    dy_param_value[param.name] = param.numpy()     
      if batch_id % 20== 0:         
      print("Loss at step {}: {}".format(batch_id,avg_loss.numpy()))

 

5.5  預測

預測的目標是為了在訓練的同時,瞭解一下在開發集上模型的表現情況,由於動態圖的訓練和預測使用同一個Layer,有一些op(比如dropout)在訓練和預測時表現不一樣,使用者需要切換到預測的模式,通過 .eval()介面進行切換(注:訓練的時候需要切回到訓練的模式)

預測程式碼如下圖所示:

def test_mnist(reader, model, batch_size):    acc_set = []    avg_loss_set = []    for batch_id, data in enumerate(reader()):       dy_x_data = np.array([x[0].reshape(1, 28, 28) for x indata]).astype('float32')       y_data = np.array([x[1] for x indata]).astype('int64').reshape(batch_size, 1)       img = to_variable(dy_x_data)       label = to_variable(y_data)       label.stop_gradient = True       prediction, acc = model(img, label)       loss = fluid.layers.cross_entropy(input=prediction, label=label)       avg_loss = fluid.layers.mean(loss)       acc_set.append(float(acc.numpy()))       avg_loss_set.append(float(avg_loss.numpy()))       # get test acc and loss   acc_val_mean = np.array(acc_set).mean()   avg_loss_val_mean = np.array(avg_loss_set).mean()   return avg_loss_val_mean, acc_val_mean

最終可以通過列印資料自行繪製Loss曲線:

都9102年了還不懂動態圖嗎?一文帶你瞭解飛槳動態圖

5.6   除錯

除錯是我們在搭建網路時候非常重要的功能,動態圖由於是指令式程式設計,使用者可以直接利用python的print列印變數,通過print( tensor.numpy() ) 直接列印tensor的值。在執行了backward之後,使用者可以通過print(tensor.gradient) 列印反向的梯度值。

這樣,一個簡單的動態圖的例項就完成了,親愛的開發者們,你們學會了麼?

想與更多的深度學習開發者交流,請加入飛槳官方QQ群:432676488。

相關文章