1. 程式人生 > 實用技巧 >使用百度飛槳AI平臺預測波士頓房價程式碼

使用百度飛槳AI平臺預測波士頓房價程式碼

import paddle
import paddle.fluid as fluid
import paddle.fluid.dygraph as dygraph
from paddle.fluid.dygraph import Linear
import numpy as np
import os
import random

程式碼中引數含義如下:

paddle/fluid:飛槳的主庫,目前大部分的實用函式均在paddle.fluid包內。
dygraph:動態圖的類庫。
Linear:神經網路的全連線層函式,即包含所有輸入權重相加和啟用函式的基本神經元結構。在房價預測任務中,使用只有一層的神經網路(全連線層)來實現線性迴歸模型。

說明:

飛槳支援兩種深度學習建模編寫方式,更方便除錯的動態圖模式和效能更好並便於部署的靜態圖模式。

靜態圖模式(宣告式程式設計正規化,類比C++):先編譯後執行的方式。使用者需預先定義完整的網路結構,再對網路結構進行編譯優化後,才能執行獲得計算結果。
動態圖模式(指令式程式設計正規化,類比Python):解析式的執行方式。使用者無需預先定義完整的網路結構,每寫一行網路程式碼,即可同時獲得計算結果。
為了學習模型和除錯的方便,本教程均使用動態圖模式編寫模型。在後續的資深教程中,會詳細介紹靜態圖以及將動態圖模型轉成靜態圖的方法。僅在部分場景下需要模型轉換,並且是相對容易的。

資料處理

def load_data():
    # 從檔案匯入資料
    datafile = './work/housing.data'
    data = np.fromfile(datafile, sep=' ')

    # 每條資料包括14項,其中前面13項是影響因素,第14項是相應的房屋價格中位數
    feature_names = [ 'CRIM', 'ZN', 'INDUS', 'CHAS', 'NOX', 'RM', 'AGE', \
                      'DIS', 'RAD', 'TAX', 'PTRATIO', 'B', 'LSTAT', 'MEDV' ]
    feature_num = len(feature_names)

    # 將原始資料進行Reshape,變成[N, 14]這樣的形狀
    data = data.reshape([data.shape[0] // feature_num, feature_num])

    # 將原資料集拆分成訓練集和測試集
    # 這裡使用80%的資料做訓練,20%的資料做測試
    # 測試集和訓練集必須是沒有交集的
    ratio = 0.8
    offset = int(data.shape[0] * ratio)
    training_data = data[:offset]

    # 計算train資料集的最大值,最小值,平均值
    maximums, minimums, avgs = training_data.max(axis=0), training_data.min(axis=0), \
                                 training_data.sum(axis=0) / training_data.shape[0]
    
    # 記錄資料的歸一化引數,在預測時對資料做歸一化
    global max_values
    global min_values
    global avg_values
    max_values = maximums
    min_values = minimums
    avg_values = avgs

    # 對資料進行歸一化處理
    for i in range(feature_num):
        #print(maximums[i], minimums[i], avgs[i])
        data[:, i] = (data[:, i] - avgs[i]) / (maximums[i] - minimums[i])

    # 訓練集和測試集的劃分比例
    #ratio = 0.8
    #offset = int(data.shape[0] * ratio)
    training_data = data[:offset]
    test_data = data[offset:]
    return training_data, test_data

模型設計

模型定義的實質是定義線性迴歸的網路結構,飛槳建議通過建立Python類的方式完成模型網路的定義,即定義init函式和forward函式。forward函式是框架指定實現前向計算邏輯的函式,程式在呼叫模型例項時會自動執行forward方法。在forward函式中使用的網路層需要在init函式中宣告。

實現過程分如下兩步:

定義init函式:在類的初始化函式中宣告每一層網路的實現函式。在房價預測模型中,只需要定義一層全連線層,模型結構和《使用Python和Numpy構建神經網路模型》章節模型保持一致。
定義forward函式:構建神經網路結構,實現前向計算過程,並返回預測結果,在本任務中返回的是房價預測結果。

class Regressor(fluid.dygraph.Layer):
    def __init__(self):
        super(Regressor, self).__init__()
        
        # 定義一層全連線層,輸出維度是1,啟用函式為None,即不使用啟用函式
        self.fc = Linear(input_dim=13, output_dim=1, act=None)
    
    # 網路的前向計算函式
    def forward(self, inputs):
        x = self.fc(inputs)
        return x

訓練配置

訓練配置過程包含四步

以guard函式指定執行訓練的機器資源,表明在with作用域下的程式均執行在本機的CPU資源上。dygraph.guard表示在with作用域下的程式會以飛槳動態圖的模式執行(實時執行)。
宣告定義好的迴歸模型Regressor例項,並將模型的狀態設定為訓練。
使用load_data函式載入訓練資料和測試資料。
設定優化演算法和學習率,優化演算法採用隨機梯度下降SGD,學習率設定為0.01。
訓練配置程式碼如下所示:

# 定義飛槳動態圖的工作環境
with fluid.dygraph.guard():
    # 宣告定義好的線性迴歸模型
    model = Regressor()
    # 開啟模型訓練模式
    model.train()
    # 載入資料
    training_data, test_data = load_data()
    # 定義優化演算法,這裡使用隨機梯度下降-SGD
    # 學習率設定為0.01
    opt = fluid.optimizer.SGD(learning_rate=0.01, parameter_list=model.parameters())

說明:

預設本案例執行在讀者的筆記本上,因此模型訓練的機器資源為CPU。
模型例項有兩種狀態:訓練狀態.train()和預測狀態.eval()。訓練時要執行正向計算和反向傳播梯度兩個過程,而預測時只需要執行正向計算。為模型指定執行狀態,有兩點原因:
(1)部分高階的運算元(例如Drop out和Batch Normalization,在計算機視覺的章節會詳細介紹)在兩個狀態執行的邏輯不同。

(2)從效能和儲存空間的考慮,預測狀態時更節省記憶體,效能更好。

在上述程式碼中可以發現宣告模型、定義優化器等操作都在with建立的 fluid.dygraph.guard()上下文環境中進行,可以理解為with fluid.dygraph.guard()建立了飛槳動態圖的工作環境,在該環境下完成模型宣告、資料轉換及模型訓練等操作。

在基於Python實現神經網路模型的案例中,我們為實現梯度下降編寫了大量程式碼,而使用飛槳框架只需要定義SGD就可以實現優化器設定,大大簡化了這個過程。

訓練過程

訓練過程採用二層迴圈巢狀方式:

內層迴圈: 負責整個資料集的一次遍歷,採用分批次方式(batch)。假設資料集樣本數量為1000,一個批次有10個樣本,則遍歷一次資料集的批次數量是1000/10=100,即內層迴圈需要執行100次。
for iter_id, mini_batch in enumerate(mini_batches):

外層迴圈: 定義遍歷資料集的次數,通過引數EPOCH_NUM設定。
for epoch_id in range(EPOCH_NUM):

說明:
batch的取值會影響模型訓練效果。batch過大,會增大記憶體消耗和計算時間,且效果並不會明顯提升;batch過小,每個batch的樣本資料將沒有統計意義。由於房價預測模型的訓練資料集較小,我們將batch為設定10。

每次內層迴圈都需要執行如下四個步驟

  1. 資料準備:將一個批次的資料轉變成np.array和內建格式。
  2. 前向計算:將一個批次的樣本資料灌入網路中,計算輸出結果。
  3. 計算損失函式:以前向計算結果和真實房價作為輸入,通過損失函式square_error_cost計算出損失函式值(Loss)。飛槳所有的API介面都有完整的說明和使用案例,在後續的資深教程中我們會詳細介紹API的查閱方法。
  4. 反向傳播:執行梯度反向傳播backward函式,即從後到前逐層計算每一層的梯度,並根據設定的優化演算法更新引數opt.minimize。
with dygraph.guard(fluid.CPUPlace()):
    EPOCH_NUM = 10   # 設定外層迴圈次數
    BATCH_SIZE = 10  # 設定batch大小
    
    # 定義外層迴圈
    for epoch_id in range(EPOCH_NUM):
        # 在每輪迭代開始之前,將訓練資料的順序隨機的打亂
        np.random.shuffle(training_data)
        # 將訓練資料進行拆分,每個batch包含10條資料
        mini_batches = [training_data[k:k+BATCH_SIZE] for k in range(0, len(training_data), BATCH_SIZE)]
        # 定義內層迴圈
        for iter_id, mini_batch in enumerate(mini_batches):
            x = np.array(mini_batch[:, :-1]).astype('float32') # 獲得當前批次訓練資料
            y = np.array(mini_batch[:, -1:]).astype('float32') # 獲得當前批次訓練標籤(真實房價)
            # 將numpy資料轉為飛槳動態圖variable形式
            house_features = dygraph.to_variable(x)
            prices = dygraph.to_variable(y)
            
            # 前向計算
            predicts = model(house_features)
            
            # 計算損失
            loss = fluid.layers.square_error_cost(predicts, label=prices)
            avg_loss = fluid.layers.mean(loss)
            if iter_id%20==0:
                print("epoch: {}, iter: {}, loss is: {}".format(epoch_id, iter_id, avg_loss.numpy()))
            
            # 反向傳播
            avg_loss.backward()
            # 最小化loss,更新引數
            opt.minimize(avg_loss)
            # 清除梯度
            model.clear_gradients()
    # 儲存模型
    fluid.save_dygraph(model.state_dict(), 'LR_model')
epoch: 0, iter: 0, loss is: [0.22913733]
epoch: 0, iter: 20, loss is: [0.20844102]
epoch: 0, iter: 40, loss is: [0.04228407]
epoch: 1, iter: 0, loss is: [0.3518763]
epoch: 1, iter: 20, loss is: [0.23596768]
epoch: 1, iter: 40, loss is: [0.08234452]
epoch: 2, iter: 0, loss is: [0.10328938]
epoch: 2, iter: 20, loss is: [0.28633732]
epoch: 2, iter: 40, loss is: [0.06373774]
epoch: 3, iter: 0, loss is: [0.21967945]
epoch: 3, iter: 20, loss is: [0.08163998]
epoch: 3, iter: 40, loss is: [0.0068926]
epoch: 4, iter: 0, loss is: [0.07897817]
epoch: 4, iter: 20, loss is: [0.07491197]
epoch: 4, iter: 40, loss is: [0.11172725]
epoch: 5, iter: 0, loss is: [0.01574697]
epoch: 5, iter: 20, loss is: [0.10722847]
epoch: 5, iter: 40, loss is: [0.0330084]
epoch: 6, iter: 0, loss is: [0.04944484]
epoch: 6, iter: 20, loss is: [0.02068931]
epoch: 6, iter: 40, loss is: [0.00653498]
epoch: 7, iter: 0, loss is: [0.02126039]
epoch: 7, iter: 20, loss is: [0.03431994]
epoch: 7, iter: 40, loss is: [0.02446936]
epoch: 8, iter: 0, loss is: [0.08141419]
epoch: 8, iter: 20, loss is: [0.01587333]
epoch: 8, iter: 40, loss is: [0.0253534]
epoch: 9, iter: 0, loss is: [0.02588719]
epoch: 9, iter: 20, loss is: [0.07267632]
epoch: 9, iter: 40, loss is: [0.04521535]

儲存並測試模型

儲存模型

將模型當前的引數資料model.state_dict()儲存到檔案中(通過引數指定儲存的檔名 LR_model),以備預測或校驗的程式呼叫,程式碼如下所示。

# 定義飛槳動態圖工作環境
with fluid.dygraph.guard():
    # 儲存模型引數,檔名為LR_model
    fluid.save_dygraph(model.state_dict(), 'LR_model')
    print("模型儲存成功,模型引數儲存在LR_model中")
模型儲存成功,模型引數儲存在LR_model中

理論而言,直接使用模型例項即可完成預測,而本教程中預測的方式為什麼是先儲存模型,再載入模型呢?這是因為在實際應用中,訓練模型和使用模型往往是不同的場景。模型訓練通常使用大量的線下伺服器(不對外向企業的客戶/使用者提供線上服務),而模型預測則通常使用線上提供預測服務的伺服器,或者將已經完成的預測模型嵌入手機或其他終端裝置中使用。因此本教程的講解方式更貼合真實場景的使用方法。

回顧下基於飛槳實現的房價預測模型,實現效果與之前基於Python實現的模型沒有區別,但兩者的實現成本有天壤之別。飛槳的願景是使用者只需要瞭解模型的邏輯概念,不需要關心實現細節,就能搭建強大的模型。

測試模型

下面我們選擇一條資料樣本,測試下模型的預測效果。測試過程和在應用場景中使用模型的過程一致,主要可分成如下三個步驟:

配置模型預測的機器資源。本案例預設使用本機,因此無需寫程式碼指定。
將訓練好的模型引數載入到模型例項中。由兩個語句完成,第一句是從檔案中讀取模型引數;第二句是將引數內容載入到模型。載入完畢後,需要將模型的狀態調整為eval()(校驗)。上文中提到,訓練狀態的模型需要同時支援前向計算和反向傳導梯度,模型的實現較為臃腫,而校驗和預測狀態的模型只需要支援前向計算,模型的實現更加簡單,效能更好。
將待預測的樣本特徵輸入到模型中,列印輸出的預測結果。
通過load_one_example函式實現從資料集中抽一條樣本作為測試樣本,具體實現程式碼如下所示。

def load_one_example(data_dir):
    f = open(data_dir, 'r')
    datas = f.readlines()
    # 選擇倒數第10條資料用於測試
    tmp = datas[-10]
    tmp = tmp.strip().split()
    one_data = [float(v) for v in tmp]

    # 對資料進行歸一化處理
    for i in range(len(one_data)-1):
        one_data[i] = (one_data[i] - avg_values[i]) / (max_values[i] - min_values[i])

    data = np.reshape(np.array(one_data[:-1]), [1, -1]).astype(np.float32)
    label = one_data[-1]
    return data, label
with dygraph.guard():
    # 引數為儲存模型引數的檔案地址
    model_dict, _ = fluid.load_dygraph('LR_model')
    model.load_dict(model_dict)
    model.eval()

    # 引數為資料集的檔案地址
    test_data, label = load_one_example('./work/housing.data')
    # 將資料轉為動態圖的variable格式
    test_data = dygraph.to_variable(test_data)
    results = model(test_data)

    # 對結果做反歸一化處理
    results = results * (max_values[-1] - min_values[-1]) + avg_values[-1]
    print("Inference result is {}, the corresponding label is {}".format(results.numpy(), label))
Inference result is [[20.245665]], the corresponding label is 19.7

通過比較“模型預測值”和“真實房價”可見,模型的預測效果與真實房價接近。房價預測僅是一個最簡單的模型,使用飛槳編寫均可事半功倍。那麼對於工業實踐中更復雜的模型,使用飛槳節約的成本是不可估量的。同時飛槳針對很多應用場景和機器資源做了效能優化,在功能和效能上遠強於自行編寫的模型。

從下一章開始,我們將通過“手寫數字識別”的案例,完整掌握使用飛槳編寫模型的方方面面。