百度深度學習框架飛槳PaddlePaddle設計思想與核心技術

異步社區 發佈 2020-03-02T09:27:29+00:00

name: "x" type { type: LOD_TENSOR lod_tensor { tensor { data_type: FP32 dims: -1 dims: 1

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會員卡,近千門電子書、專欄視頻課免費學

關鍵字: