Fluid程序代碼可以分為編譯時代碼與運行時代碼。
編譯時的代碼可以理解為:用戶在編寫一段PaddlePaddle程序時,描述模型前向計算的代碼。它包括以下幾個方面的內容。
- 創建變量與描述變量。
- 創建算子與描述算子。
- 創建算子的屬性。
- 推斷變量的類型和形狀,進行靜態檢查。
- 規劃變量的內存復用。
- 定義反向計算。
- 添加與優化相關的算子。
- 添加多機多卡相關的算子,生成在多機多卡上運行的程序。
要定義反向計算,代碼如下。
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)
avg_cost = fluid.layers.mean(x=cost)
要添加優化算法,代碼如下。
learning_rate = 0.01
sgd_optimizer = fluid.optimizer.SGD(learning_rate)
sgd_optimizer.minimize(avg_cost)
編譯時的代碼可以理解為計算並控制運行時計算的代碼,它包括以下幾個方面的內容:
- 創建框架執行器(executor)。
- 為將要執行的計算過程創建變量操作域。
- 創建塊,並依次執行塊。
要讀入數據,代碼如下。
train_reader = paddle.batch(
paddle.reader.shuffle(paddle.dataset.uci_housing.train(), buf_size=500),
batch_size=20)
feeder = fluid.DataFeeder(place=place, feed_list=[x, y])
要定義執行程序的設備,代碼如下。
place = fluid.CPUPlace()
feeder = fluid.DataFeeder(place=place,feed_list=[x, y])
要創建執行器,並執行default_startup_program,代碼如下。
exe = fluid.Executor(place)
exe.run(fluid.default_startup_program())
要執行訓練程序default_main_program,代碼如下。
PASS_NUM = 100
for pass_id in range(PASS_NUM):
fluid.io.save_persistables(exe, "./fit_a_line.model/")
fluid.io.load_persistables(exe, "./fit_a_line.model/")
for data in train_reader():
avg_loss_value, = exe.run(fluid.default_main_program(),
feed=feeder.feed(data),
fetch_list=[avg_cost])
print(avg_loss_value)
1.2 Fluid內部執行流程
Fluid使用一種編譯器式的執行流程,分為編譯時和運行時兩個部分,具體包括編譯器定義Program、創建框架執行器和運行Program。
本地訓練任務執行流程如圖1-1所示。
圖1-1
(1)在編譯時,用戶編寫一段Python程序,通過調用Fluid提供的算子,向一段Program中添加變量(在Fluid中用戶輸入的變量通常以張量表示)以及對變量的操作(算子或者層)。用戶只需要描述核心的前向計算,不需要關心反向計算以及分布式環境下和異構設備下是如何計算的。
(2)原始的Program在平台內部轉換為中間描述語言ProgramDesc。
(3)在編譯期間重要的一個功能模塊是編譯轉換器。編譯轉換器接受一段 ProgramDesc,輸出一段變化後的 ProgramDesc,作為後端框架執行器最終需要執行的是Fluid Program。
(4)後端框架執行器接受編譯轉換器輸出的這段Program,依次執行其中的算子(可以類比為程序語言中的指令),在執行過程中會為算子創建所需的輸入/輸出並進行管理。
1.3 Program設計簡介
PaddlePaddle使用了一個Program的概念,那麼什麼是Program?
Program本質是對多個數據(通常以變量表示)和操作邏輯的描述。一個深度學習任務中的訓練或者預測都可以被描述為一段Program。
多個變量和一系列的算子不僅可以組合為Program,還可以組合成Block,而多個Block可以相互嵌套、並列,最終組合為Program。
用戶完成網絡定義後,1個Fluid程序中通常存在兩段Program。
- fluid.default_startup_program:定義了創建模型參數、輸入/輸出,以及模型中可學習參數的初始化等各種操作。default_startup_program可以由框架自動生成,使用時無須手動創建。如果調用修改了參數的默認初始化方式,框架會自動將相關的修改加入default_startup_program。
- fluid.default_main_program:也由框架自動生成,負責存儲定義的神經網絡模型、前向/反向計算,以及優化算法對網絡中可學習參數的更新。如果在指定程序操作時未指定在哪個Program中操作,Fluid默認自動在fluid.default_main_program操作。
使用Fluid的核心就是構建default_main_program。
可通過圖1-2所示的代碼,輸出ProgramDesc中的內容。
圖1-2
上面框出的兩行代碼的輸出內容如圖1-3所示。
圖1-3
1.4 Block簡介
Block的概念與通用程序一致,例如,下面這段C++代碼包含3個Block。
int main(){ //Block 0
int i = 0;
if (i<10){ //Block 1
for (int j=0;j<10;j++){ //Block 2
}
}
return 0;
}
類似地,下列Fluid的Program包含3個Block。
import paddle.fluid as fluid #Block 0
limit = fluid.layers.fill_constant_batch_size_like(
input=label, dtype='int64', shape=[1], value=5.0)
cond = fluid.layers.less_than(x=label, y=limit)
ie = fluid.layers.IfElse(cond)
with ie.true_block(): #Block 1
true_image = ie.input(image)
hidden = fluid.layers.fc(input=true_image, size=100, act='tanh')
prob = fluid.layers.fc(input=hidden, size=10, act='softmax')
ie.output(prob)
with ie.false_block(): #Block 2
false_image = ie.input(image)
hidden = fluid.layers.fc(
input=false_image, size=200, act='tanh')
prob = fluid.layers.fc(input=hidden, size=10, act='softmax')
ie.output(prob)
prob = ie()
1.5 Block和Program的設計細節
用戶描述的Block與Program信息在Fluid中以Protobuf 格式保存,所有的Protobuf信息定義在framework.proto中,在Fluid中稱為BlockDesc和ProgramDesc。ProgramDesc和BlockDesc的概念類似於一個抽象語法樹。
BlockDesc中包含一系列本地變量(在代碼中用vars表示)的定義和一系列的算子(在代碼中用ops表示)。
message BlockDesc {
required int32 parent = 1;
repeated VarDesc vars = 2;
repeated OpDesc ops = 3;
}
parent表示父級Block,因此Block中的操作符既可以引用本地定義的變量,也可以引用祖先塊中定義的變量。
Program中的每個Block都被壓平並存儲在數組中。Block ID是這個數組中Block的索引。
message ProgramDesc {
repeated BlockDesc blocks = 1;
}
在1.4節介紹的例子中,IfElse這個算子包含了兩個Block——true分支和false分支。
下述OpDesc的定義過程描述了一個算子可以包含哪些屬性。
message OpDesc {
AttrDesc attrs = 1;
...
}
屬性可以是Block的類型,實際上就是上面描述的Block ID。
message AttrDesc {
required string name = 1;
enum AttrType {
INT = 1,
STRING = 2,
...
BLOCK = ...
}
required AttrType type = 2;
optional int32 block = 10; // 當type == BLOCK時
...
}
1.6 框架執行器設計思想
在編譯時,用戶首先在Python前端對網絡進行表述,這個表述按照Program、Block的層級關係進行定義。轉換器在接收到用戶的表述後,將Program編譯成ProgramDesc以備運行時使用。在運行時,框架執行器將接受一個ProgramDesc、一個block_id和一個變量操作域。執行以下操作。
(1)框架執行器為每一個Block創建一個變量操作域,Block是可嵌套的,因此變量操作域也是可嵌套的。
(2)創建變量操作域中的所有變量。
(3)按順序創建並執行所有算子。
編譯、執行的具體過程如圖1-4表示。
圖1-4
1.6.1 代碼示例
框架執行器的C++實現代碼如下。
class Executor{
public:
void Run(const ProgramDesc& pdesc,
Scope* scope,
int block_id) {
auto& block = pdesc.Block(block_id);
//創建所有變量
for (auto& var : block.AllVars())
scope->Var(Var->Name());
}
//創建算子並按順序執行
for (auto& op_desc : block.AllOps()){
auto op = CreateOp(*op_desc);
op->Run(*local_scope, place_);
}
}
};
1.6.2 創建框架執行器
Fluid中使用fluid.Executor(place)創建框架執行器,place屬性由用戶定義,代表程序將在哪裡執行。
下例代碼表示創建一個框架執行器,它在CPU內運行。
cpu=fluid.CPUPlace()
exe = fluid.Executor(cpu)
1.6.3 運行框架執行器
Fluid使用Executor.run來運行程序。定義中通過feed映射獲取數據,通過fetch_list獲取結果。
...
x = numpy.random.random(size=(10, 1)).astype('float32')
outs = exe.run(
feed={'X': x},
fetch_list=[loss.name])
1.7 示例
線性回歸是最簡單的模型之一,可以把它作為一個優化問題來研究。該問題可通過最小化均方誤差求解。
本節通過簡單的線性回歸例子,介紹上述內容如何在代碼中實現。
1.7.1 定義Program
用戶可以隨意定義自己的數據和網絡結構,定義的結果都將作為一段Program被Fluid接收,Program的基本結構是Block,本節的Program僅包含Block 0。
#加載函數庫
import paddle.fluid as fluid #Block0
import numpy
#定義數據
train_data=numpy.array([[1.0],[2.0],[3.0],[4.0]]).astype('float32')
y_true = numpy.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)
avg_cost = fluid.layers.mean(cost)
#定義優化方法
sgd_optimizer = fluid.optimizer.SGD(learning_rate=0.01)
sgd_optimizer.minimize(avg_cost)
完成上述定義,也就是完成了fluid.default_main_program的構建過程,fluid.default_main_program負責神經網絡模型、前向/反向計算,以及優化算法對網絡中可學習參數的更新。
此時可以輸出這段 Program,觀察定義好的網絡形態。
print(fluid.default_main_program().to_string(True))
完整的ProgramDesc可以在本地查看,這裡僅節選前3個變量。
blocks {
idx: 0
parent_idx: -1
vars {
name: "mean_1.tmp_0"
type {
type: LOD_TENSOR
lod_tensor {
tensor {
data_type: FP32
dims: 1
}
}
}
persistable: false
}
vars {
name: "square_error_cost_1.tmp_1"
type {
type: LOD_TENSOR
lod_tensor {
tensor {
data_type: FP32
dims: -1
dims: 1
}
lod_level: 0
}
}
persistable: false
}
vars {
name: "square_error_cost_1.tmp_0"
type {
type: LOD_TENSOR
lod_tensor {
tensor {
data_type: FP32
dims: -1
dims: 1
}
lod_level: 0
}
}
persistable: false
...
從輸出結果中可以看到,整個定義過程在框架內部轉化為了一段ProgramDesc,以Block idx為索引。本次線性回歸模型中僅有1個Block,ProgramDesc中也僅有Block 0這一段BlockDesc。
BlockDesc中包含定義的變量和一系列的算子,以輸入x為例,在Python代碼中定義x是一個數據類型為「float 32」的一維數據。
x = fluid.layers.data(name="x",shape=[1],dtype='float32')
在BlockDesc中,變量x被描述為:
vars {
name: "x"
type {
type: LOD_TENSOR
lod_tensor {
tensor {
data_type: FP32
dims: -1
dims: 1
}
lod_level: 0
}
}
persistable: false
在Fluid中所有的數據類型都為LoD Tensor,對於不存在序列信息的數據(如此處的變量x),其lod_level=0。
dims表示數據的維度,這裡表示x的維度為[−1,1],其中−1是批的維度,在無法確定具體數值時,Fluid自動用−1表示。
參數persistable表示該變量在整個訓練過程中是否為持久化變量。
1.7.2 創建框架執行器
Fluid使用框架執行器來執行網絡訓練。作為使用者,實際上不必了解內部機制。
創建框架執行器只需要調用 fluid.Executor(place) 即可,在此之前請依據訓練場所定義place變量。
#在CPU內執行訓練
cpu = fluid.CPUPlace()
#創建框架執行器
exe = fluid.Executor(cpu)
1.7.3 運行框架執行器
Fluid使用Executor.run來運行一段Program。
在正式進行網絡訓練前,要先進行參數初始化。其中 default_startup_program 中定義了創建模型參數、輸入/輸出,以及模型中可學習參數的初始化等操作。
#參數初始化
exe.run(fluid.default_startup_program())
由於傳入的數據與傳出的數據存在多列,因此 Fluid 通過 feed 映射定義數據的傳輸數據,通過 fetch_list 取出期望結果:
#開始訓練
outs = exe.run(
feed={'x':train_data,'y':y_true},
fetch_list=[y_predict.name,avg_cost.name])
上述代碼段中定義了train_data用於傳入x變量,y_true用於傳入y變量,輸出y的預測值和最後一輪樣本中cost的均值。
輸出結果如下。
[array([[1.5248038],
[3.0496075],
[4.5744114],
[6.099215 ]], dtype=float32), array([1.6935859], dtype=float32)]
-END-
喜歡的朋友,歡迎轉發到朋友圈
關注【異步圖書】微信號,回復「VIP」獲取異步社區VIP會員卡,近千門電子書、專欄視頻課免費學