Paddle關鍵概念
正文共:6452?字?0?圖
預計閱讀時間:?17?分鐘
本文討論一下PaddlePaddle框架中幾個重要的概念,在使用Paddle進行開發時,弄清楚這幾個概念是使用Paddle進行開發的一個前提。
Tensor張量
與當前主流框架相同,Paddle同樣使用Tensor張量來表示數據,你可以將不同維度的Tensor理解成對應維度的矩陣,當然,兩者是有差異的。
在Fluid中所有的數據類型都為LoD-Tensor,對于不存在序列信息的數據(如此處的變量X),其lod_level=0。
在Paddle Fluid版本中,有3個比較重要的Tensor
可學習參數
神經網絡中的可學習參數(模型權重、偏置等)其生命周期與整個訓練周期一樣長,在訓練過程中,可學習參數會被修改,最常見的就是模型權重這一可學習參數會被梯度下降算法修改,在Fluid中可以使用 fluid.layers.create_parameter
來創建可學習參數
w = fluid.layers.create_parameter(name='w', shape=[1], dtype='float32')
在日常編寫模型中,該方法并不常用,因為Paddle已經為大部分常見的神經網絡基本計算模型進行了封裝,如創建全鏈接層,并不需要自己手動創建鏈接權重w與偏置b
y = fluid.layers.fc(input=x, size=128, bias_attr=True)
其實進入 fluid.layers.create_parameter()
方法的源碼和 fluid.layers.fc()
方法的源碼,可以發現兩者最終都是調用 helper.create_parameter()
方法來創建可學習參數的,其中helper是LayerHelper的實例。
輸入數據Tensor
Fluid使用 fluid.layers.data()
方法來接受輸入數據,與TensorFlow中的placeholder占位符作用是相同的,同樣,fluid.layers.data()方法需要定義好輸入Tensor的形狀,如果遇到無法確定形狀的維度,則設置為None。
x = fluid.layers.data(name='x', shape=[3, None], dtype='int64')
# Batch size 無需顯示指定, Paddle會自動填充
a = fluid.layers.data(name='a', shape=[3,6], dtype='int64')
# 輸入數據為寬、高不是固定的圖像, 主要圖像數據類型通常為float32
img = fluid.layers.data(name='img', shape=[3, None, None], dtype='float32')
上述代碼中,一個tick就是Batch size,無需自己顯示的定義。(通常訓練模型時,都以一Batch size大小的數據為一次訓練數據的量)
可以從源碼看出(如下),data()方法中使用appendbatchsize變量判斷是否要為shape自動加上batch size,但正是的創建輸入,使用的是 helper.create_global_variable()
方法。而helper依舊是LayerHelper的實例。
def data(name,
? ? ? ? shape,
? ? ? ? append_batch_size=True,
? ? ? ? dtype='float32',
? ? ? ? lod_level=0,
? ? ? ? type=core.VarDesc.VarType.LOD_TENSOR,
? ? ? ? stop_gradient=True):
? ?helper = LayerHelper('data', **locals())
? ?shape = list(shape)
? ?for i in six.moves.range(len(shape)):
? ? ? ?if shape[i] is None:
? ? ? ? ? ?shape[i] = -1
? ? ? ? ? ?append_batch_size = False
? ? ? ?elif shape[i] < 0:
? ? ? ? ? ?append_batch_size = False
? ?if append_batch_size:
? ? ? ?shape = [-1] + shape ?# append batch size as -1
? ?data_var = helper.create_global_variable(
? ? ? ?name=name,
? ? ? ?shape=shape,
? ? ? ?dtype=dtype,
? ? ? ?type=type,
? ? ? ?stop_gradient=stop_gradient,
? ? ? ?lod_level=lod_level,
? ? ? ?is_data=True)
? ?return data_var
LayerHelper以出現多次,可以看出LayerHelper在Paddle中起著關鍵作用,從Paddle的設計文檔中可以了解到LayerHelper,它的作用主要是在各個layers函數之間共享代碼,設計LayerHelper主要考慮到如果開發全局輔助函數有幾個缺點:
1.需要提供這些方法的命名空間,方便開發人員快速定位并使用
2.全局函數迫使圖層開發人員需要逐個傳遞參數
為了避免以上缺點,才定義出了LayerHelper,但它通常在layers開發中使用,對應創建模型結構的搭建不會使用到LayerHelper,關于LayerHelper的更多細節可以參考Design Doc:Python API,當然后面的文章我也打算講講Paddle設計層面以及底層點的東西,到時也會有所提及。
常量Tensor
Fluid通過 fluid.layers.fill_constant()
實現常量Tensor,常量即常量,在模型訓練過程中,其值不會改變。
data = fluid.layers.fill_constant(shape=[1], value=0, dtype='int64')
該方法的內部依舊是使用LayerHelper類來實現常量Tensor
helper = LayerHelper("fill_constant", **locals())
Paddle數據傳入
Fluid版本的Paddle支持兩種傳入數據的方式,分別是
Python Reader同步讀入數據:使用?fluid.layers.data()
定義輸入層,并在?fluid.Executor
或?fluid.ParallelExecutor
中使用?executor.run(feed=...)
來傳入數據,與TensorFlow類似,定義好Placeholder占位符,再在具體訓練時,將具體的數據喂養給模型
py_reader異步讀入數據:異步讀入數據,先需要使用?fluid.layers.py_reader()
配置異步數據輸入層,再使用?py_reader()
的?decorate_paddle_reader
或?decorate_tensor_provider
方法配置數據,配置完數據后,最總通過?fluid.layers.read_file
讀取數據。
對于常用模型,比較常見的事同步讀入數據的方式。Paddle的數據讀入會在后一章進行講解。
Operator表示對數據的操作
Fluid版本的Paddle中,所有的數據操作都由Operator表示,在python端,Operator操作被進一步封裝在 paddle.fluid.layers
與 paddle.fluid.nets
等模塊中,簡單而言,所謂構建神經網絡其實就是使用框架提供的各種Operator來操作數據,不必想的過于復雜。一個簡單的加法運算如下:
import paddle.fluid as fluid
#輸入層
a = fluid.layers.data(name='a', shape=[2], dtype='float32')
b = fluid.layers.data(name='b', shape=[2], dtype='float32')
result = fluid.layers.elementwise_add(a,b)
cpu = fluid.CPUPlace() # 定義運算場所
exe = fluid.Executor(cpu) # 創建執行器
exe.run(fluid.default_startup_program()) # 網絡參數初始化
# 準備數據
import numpy
x = numpy.array([1,2])
y = numpy.array([2,3])
#執行計算
outs = exe.run(
? ?feed={'a':x,'b':y},
? ?fetch_list=[a,b,result.name])
#查看輸出結果
print(outs)
一開始使用 fluid.layers.data()
定義數據輸入層,具體的數據通過 numpy.array()
生成,因為 fluid.layers.data()
定義了shape=[2],那么此時numpy.array()需要定義成[1,2],即第一維的形狀為2,如果shape=[1,2],那么第一維的形狀為1,第二維的形狀為2,即numpy.array()需要定義成[[1,2]]。
動態圖機制
Fluid使用的是動態圖機制,因為靜態圖機制讓使用者在編寫過程中喪失了對網絡結構修改的靈活性,所以很多優秀的框架都引入了動態圖這種設計(TensorFlow 2.0動態圖機制也大幅加強了)。
動態圖 vs 靜態圖
動態計算意味著程序將按照我們編寫命令的順序進行執行。這種機制將使得調試更加容易,并且也使得我們將大腦中的想法轉化為實際代碼變得更加容易。而靜態計算則意味著程序在編譯執行時將先生成神經網絡的結構,然后再執行相應操作。從理論上講,靜態計算這樣的機制允許編譯器進行更大程度的優化,但是這也意味著你所期望的程序與編譯器實際執行之間存在著更多的代溝。這也意味著,代碼中的錯誤將更加難以發現(比如,如果計算圖的結構出現問題,你可能只有在代碼執行到相應操作的時候才能發現它)。盡管理論上而言,靜態計算圖比動態計算圖具有更好的性能,但是在實踐中我們經常發現并不是這樣的。
來源:動態圖 vs 靜態圖
Program描述模型
Fluid版本的Paddle中使用Program的形式描述計算過程,開發者所有寫入在Program中的Operator操作都會自動轉為ProgramDesc描述語言。
Fluid通過提供順序、分支和循環三種執行結構的支持
其中順序執行,寫法與靜態圖的形式沒什么差別,如下:
# 順序執行的方式搭建網絡
x = fluid.layers.data(name='x', shape=[13], dtype='float32')
y_predict = fluid.layers.fc(input=x, size=1, act=None)
y = fluid.layers.data(name='y', shape=[1], dtype='float32')
cost = fluid.layers.square_error_cost(input=y_predict, label=y)
分支條件switch的使用如下:
# 條件分支——switch、if else
lr = fluid.layers.tensor.create_global_var(
? ?shape=[1],
? ?value=0.0,
? ?dtype='float32',
? ?persistable=True,
? ?name='learning_rate'
)
one_var = fluid.layers.fill_constant(
? ?shape=[1], dtype='float32', value=1.0
)
two_var = fluid.layers.fill_constant(
? ?shape=[1], dtype='float32', value=2.0
)
# switch
with fluid.layers.control_flow.Switch() as switch:
? ?with switch.case(global_step == one_var):
? ? ? ?fluid.layers.tensor.assign(input=one_var, output=lr)
? ? ? ?with switch.default():
? ? ? ? ? ?fluid.layers.tensor.assign(input=two_var, output=lr)
其中流程控制方面的內容都放在了 fluid.layers.control_flow
模塊下,里面包含了While、Block、Conditional、Switch、if、ifelse等跟中操作,這樣可以讓在編寫模型時,想編寫普通的python程序一樣。
Executor執行Program
Program相當于你模型的整體結構,Fluid中程序執行分為編譯與執行兩個階段,當你定義編寫完Program后,還需要定義Executor,Executor會接收Program,然后將其轉為FluidProgram,這一步稱為編譯,然后再使用C++編寫的后端去執行它,執行的過程也由Executor完成,相關代碼片段如下:
cpu = fluid.core.CPUPlace()
exe = fluid.Executor(cpu) #執行器
exe.run(fluid.default_startup_program()) #初始化Program
outs = exe.run(
? ?feed={'a':x, 'b':y},
? ?fetch_list=[result.name]
)
在Fluid中使用Executor.run來運行一段Program。
正式進行網絡訓練前,需先執行參數初始化。其中 defalut_startup_program
中定義了創建模型參數,輸入輸出,以及模型中可學習參數的初始化等各種操作。
由于傳入數據與傳出數據存在多列,因此 fluid 通過 feed 映射定義數據的傳輸數據,通過 fetch_list 取出期望結果
整體實踐
寫過簡單結構,預測一組數據,使用單個全連接層來實現預測,核心的邏輯就是喂養為模型一些訓練數據,模型輸出預測結果,該預測結果與真實結果直接會有個誤差,作為損失,通過平方差損失來定義這兩個損失,然后再最小化該損失,整體邏輯如下:
import paddle.fluid as fluid
import numpy as np
'''
真實數據
'''
#訓練數據
train_data = np.array([[1.0],[2.0],[3.0],[4.0]]).astype('float32')
#真實標簽
y_true = np.array([[2.0], [4.0], [6.0], [8.0]]).astype('float32')
# 輸入層,接受真實數據
x = fluid.layers.data(name='x', shape=[1], dtype='float32')
y = fluid.layers.data(name='y', shape=[1], dtype='float32')
# 模型結構,這里就一個簡單的全連接層
y_predict = fluid.layers.fc(input=x, size=1, act=None)
# 定義損失,真實標簽值與模型預測值之間的損失
cost = fluid.layers.square_error_cost(input=y_predict, label=y)
#取平均,因為通常有batch個數據,取平均值作為損失則可
avg_cost = fluid.layers.mean(cost)
# 定義執行設備,CUP或GPU
cpu = fluid.CPUPlace()
# Executor執行,該方法只能實現單設備執行
exe = fluid.Executor(cpu)
# 初始化整個結構中的節點
exe.run(fluid.default_startup_program())
for i in range(100):
? ?outs = exe.run(
? ? ? ?feed = {'x': train_data, 'y': y_true}, #將真實數據喂養給模型,注意傳入數據與輸入層的關系
? ? ? ?fetch_list=[y_predict.name, avg_cost.name] #獲取列表,即執行完后,outs會獲得的數據
? ?)
print(outs)