iT邦幫忙

2

C 語言裡的 LightGBM - 編譯與實做

  • 分享至 

  • xImage
  •  

前言

LightGBM 是相當受歡迎的一個機器學習套件,他的訓練速度快,推論速度也快,分類效果好,套件用法單純親民,模型大小結合適當的壓縮技術,可以達到非常輕量化的水準,很適合拿來在實務上應用。在網路上已經有非常廣泛的 LightGBM 訓練教學,官方文件在這部份也寫得非常詳細。但是在編譯 Shared Library 與 C 語言的推論範例上,相關討論似乎比較少一點。雖然在 LightGBM GitHub 底下的 tests 資料夾也有一些範例程式碼可以閱讀,但多數著重在訓練的環節。所以這篇文章主要會著重在編譯不同平台的 Shared Library 與不同的推論實做這兩個部份。

簡介

安裝

安裝 LightGBM 的 Python 套件:

pip install lightgbm

準備資料

為了精簡文章版面,特徵維度設定為 5 並使用 numpy 套件隨機生成:

import numpy as np
import lightgbm as lgb

data_size = 64
features = 5

data = np.random.rand(data_size, features)
label = np.random.randint(2, size=data_size)
train_data = lgb.Dataset(data, label=label)

更多資料集的設定方式,請參考官方文件

設定參數

訓練參數使用 dict 物件:

param = {
    "num_leaves": 31,
    "objective": "regression",
    "force_col_wise": True,
    "verbose": -1,
}

簡單說明一下這些參數的作用:

  • num_leaves: 決策樹的葉子數量,數量越大,決策樹越複雜,也越容易造成 Overfitting 發生。
  • objective: 訓練目標,例如 regression 代表回歸任務,binary 代表二分類任務,更多訓練目標種類請參考官方文件
  • force_col_wise: 強制 LGB 以 Column-Wise Histogram Building 的方式建立決策樹,根據官方文件的說明,這個選項主要用在特徵數量很多的時候,也有減少記憶體消耗的作用。
  • force_row_wise: 相對於 force_col_wise,這個選項會強制使用 Row-Wise Histogram Building 的方式建立決策樹,這個選項主要用於訓練資料量非常大的時候。
  • verbose: 訓練過程 Log 的詳細程度。
    • < 0: 只顯示 Fatal 訊息。
    • = 0: 只顯示 Error 以上的訊息。
    • = 1: 只顯示 Info 以上的訊息。
    • > 1: 顯示所有訊息。

因為我們的資料量很小而且是隨機生成,所以 Overfitting 一下就發生了,這邊降低 Verbose 減少訊息干擾。

訓練決策樹

參數準備好後,訓練只要一行:

bst = lgb.train(param, train_data, 32)

我們使用一筆固定的資料做測試,以確認接下來不同實做方式都能獲得一致的輸出:

x = [
    [0.37, 1.09, 0.77, 0.68, 0.50],
    [0.53, 0.06, 0.27, 0.56, 1.94],
    [0.15, 0.09, 0.84, 0.92, 0.33],
    [2.81, 1.21, 1.74, 1.24, 0.99],
    [1.56, 0.95, 0.55, 0.61, 0.28],
]
p = bst.predict(x)
for i, pp in enumerate(p):
    print(f"Pred[{i}]: {pp:.6f}")

得到類似以下的輸出:

Pred[0]: 0.295044
Pred[1]: 0.342804
Pred[2]: 0.224139
Pred[3]: 0.433419
Pred[4]: 0.190785

備註:經過筆者深入的測試後,發現浮點數的精度在不同實做上影響其實還蠻巨大的!像上面這樣純用 list 當作輸入,LightGBM 預設會轉成 np.float64 (aka. double)。如果對精度的要求沒有那麼強烈,建議使用 np.float32 (aka. float) 就好,在 Python 這邊則要改用 np.array(x, dtype=np.float32) 當作輸入。

最後,我們把模型存成檔案:

bst.save_model("model.txt")

LightGBM 的模型可以單純的存成文字檔,訓練量較大時,這個文字檔可能也會很大,可以透過 gzip 套件之類的進行壓縮,解省硬碟空間。

之後如果要讀取模型,可以這樣做:

import lightgbm as lgb

bst = lgb.Booster(model_file="model.txt")
x = [
    [0.37, 1.09, 0.77, 0.68, 0.50],
    [0.53, 0.06, 0.27, 0.56, 1.94],
    [0.15, 0.09, 0.84, 0.92, 0.33],
    [2.81, 1.21, 1.74, 1.24, 0.99],
    [1.56, 0.95, 0.55, 0.61, 0.28],
]
p = bst.predict(x)
for i, pp in enumerate(p):
    print(f"Pred[{i}]: {pp:.6f}")

透過以上程式碼,我們可以在接下來的步驟驗證我們的模型在任何類型的實做下都有一致的輸出。

編譯 LightGBM

筆者嘗試將 LightGBM 決策樹用在 C 語言裡面,第一個想法是透過 ONNX 格式來運行。但我發現 ONNX Runtime 讀取稍微大型 LightGBM 的決策樹時,速度非常慢。筆者手邊有個 40 MB 的 LightGBM 決策樹,用 ORT 讀取至少都要一秒以上。

後來看到 lleaves 這個專案,該專案宣稱能把 LightGBM 編譯成更快的格式,筆者嘗試一下,發現編譯大型模型要非常久的時間,而且編譯出來的推論速度似乎也沒有比較快,所以就放棄這個工具了。

事實上,LightGBM 套件除了提供 Python API 以外,也有 C API(還有 R API)可以用,實測後發現效率非常好,讀取速度與推論速度都遠遠超過 Python API 與 ORT 的版本。關於編譯 LightGBM 的 Shared Library 官方文件已經寫得很詳細了,但比較沒有提到編譯成 Android Library 的部份。後來發現方法其實非常簡單,純粹只是筆者太菜而已 QQ

以下紀錄建置 LightGBM Shared Library 的相關步驟。

LightGBM Shared Library For Linux

LightGBM 官方 GitHub 裡的 Release 頁面其實已經有編譯好的 Shared Library 可以用,一般來說只要下載那個 lib_lightgbm.so 來用即可。

所以以下的編譯步驟純粹是練習用,提醒一下,如果是用 WSL 的朋友,盡量在 WSL 自己的檔案系統裡面編譯,不要在 NTFS 底下編譯,會格外花時間。

  1. 將 LightGBM 的 Source Code 下載下來:
    • git clone --recursive --depth 1 https://github.com/microsoft/LightGBM .
    • 因為只要編譯而已,所以這邊只有 Shallow Clone 一層,比較節省時間,雖然 Clone 那些第三方 Library 也是要花很久的時間 QQ
    • 如果要編譯特定版本再 Clone 整份專案,然後 Checkout 到指定版本 Tag 即可。
  2. 使用 cmakemake 編譯:
    # 請先確認系統包含以下套件:
    # sudo apt install gcc g++ make libssl-dev
    cmake .
    make -j20 # 請根據自身電腦的核心數做調整
    
  3. (Optional) 如果想要重新編譯,可以使用這個指令:
    • git clean -df
    • 這個指令真是編譯工作的好夥伴!

編譯好之後 lib_lightgbm.so 就會出現在當前目錄底下。

使用 cmake 之前,可以先觀察 CMakeLists.txt 有哪些參數可以調整,例如 USE_DEBUG 可以編譯成 Debug 版,USE_GPU 可以啟用 GPU 等等。如果要編譯成 Debug 版,可以把 cmake 指令改成這樣:

cmake . -DUSE_DEBUG="ON"

筆者編譯出來 Release 版的 Shared Library 約 5.3 MB,而 Debug 版則高達 40 MB,想進一步縮減大小,可以使用以下指令:

strip -s -R .comment -R .gnu.version --strip-unneeded lib_lightgbm.so

縮減後的大小約 4.7 MB,算是不無小補。

LightGBM Shared Library For Android

在編譯 Android 版的 Shared Library 之前,請先確認系統環境已經有 NDK 編譯工具。

  1. 如果剛才有進行過編譯,請先把 .so 檔備份下來,然後使用 git clean -df 清理編譯環境。
  2. 使用以下指令進行編譯:
cmake . \
    -DANDROID_ABI="arm64-v8a" \
    -DANDROID_PLATFORM="android-24" \
    -DANDROID_NDK="/path/to/android/ndk/<version>" \
    -DCMAKE_TOOLCHAIN_FILE="/path/to/android/ndk/<version>/build/cmake/android.toolchain.cmake"

make -j20 # 請根據自身電腦的核心數做調整

其中 -DANDROID_ABI 可以是 arm64-v8a (ARM64) 或 armeabi-v7a (ARM32) 之類的,如果是給 x86 虛擬機使用,則可以選擇 x86x86_64 等,根據情況做選擇。

關於 -DANDROID_PLATFORM 的部份,筆者嘗試了一下,至少要在 android-24 以上才能通過編譯,這點是我前面一直編譯失敗的原因。

完成編譯後,可以用 file lib_lightgbm.so 檢視檔案屬性,會看到類似以下的訊息:

lib_lightgbm.so: ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked, BuildID[sha1]=cbb1eff03ccca7f5b2af33f0f425b9f9aa93cb55, with debug_info, not stripped

看到 ARM aarch64 就代表是 ARM64 版本的 Shared Library 了。

這邊注意到,雖然沒有設定 -DUSE_DEBUG="ON",但檔案依然包含 Debug Info,且大小約 40 MB 左右。若要針對大小進行優化,可以使用 -DCMAKE_BUILD_TYPE="MinSizeRel" 參數,重新編譯後大小降到 11 MB 左右。

若同樣想要用 strip 指令降低檔案大小,可以用 Docker 拉一個 arm64v8/gcc 的容器下來,參考以下指令:

docker run -it --rm -v $PWD:/home -w /home arm64v8/gcc strip -s -R .comment -R .gnu.version --strip-unneeded lib_lightgbm.so

瘦身過後,lib_lightgbm.so 降到 4.4 MB 左右,不過目前筆者還沒在 Android 系統對這些編譯出來的檔案進行過驗證,還不確定在 Android 系統上是否能正常運作。

註:arm64v8/gcc 也能拿來 Strip armeabi-v7a.so,所以不用特地另外拉個 arm32v7/gcc 下來做瘦身。

程式碼實做

接下來進入我們的熱血 Coding 環節,以下示範 Static Link 與 Dynamic Link 兩種 C 語言的做法,主要差在一個要用 dlopen 一個不用。實際部署到 Android 環境時,採用 dlopen 的方式比較常見。各位 JNI 大神如果有更好的方法也請不吝賜教 QQ

Static Link

LightGBM 主要會用到的 Header 為 include/LightGBM/c_api.h,編譯時需加上 -I/path/to/lightgbm-repo/include

首先,透過 LGBM_BoosterCreateFromModelfile 讀取模型:

#include <stdio.h>
#include "LightGBM/c_api.h"

int main() {
    // Load LightGBM Model
    const char* pszModelFileName = "model.txt";
    void* pLgbHandle = NULL;
    int nModelIter = 0;
    int nErr = LGBM_BoosterCreateFromModelfile(
        pszModelFileName, &nModelIter, &pLgbHandle);
    
    if (nErr != 0) {
        printf("LGBM_GetLastError(), Error: %s\n", LGBM_GetLastError());
        return nErr;
    }

如果使用 LightGBM C API 的過程中出現任何錯誤,都可以透過 LGBM_GetLastError 來得知錯誤訊息,例如找不到模型就會輸出 Could not open _model.txt 這樣的訊息。

接下來,把我們的輸入資料準備好:

#define FEATURES 5
#define DATASIZE 5

double lpfFeatures[DATASIZE][FEATURES] = {
    {0.37, 1.09, 0.77, 0.68, 0.50},
    {0.53, 0.06, 0.27, 0.56, 1.94},
    {0.15, 0.09, 0.84, 0.92, 0.33},
    {2.81, 1.21, 1.74, 1.24, 0.99},
    {1.56, 0.95, 0.55, 0.61, 0.28}};

這份輸入資料與前面介紹 LightGBM 時使用的資料是相同的,用來驗證模型的輸出是否一致。

備註:經過實測後發現,輸入資料必須位於連續記憶體空間內才會有正確的輸出。如果是用兩層 malloc 的話(例如:double **lpfFeatures),很容易出問題。建議用一層 malloc (例如:double *lpfFeatures)然後用 Pointer Offset 的方式去設定輸入資料。例如:

// 這樣的做法可能會發生記憶體錯誤
double **lpfFeatures = (double**)malloc(sizeof(double*) * DATASIZE);
for (int i = 0; i < DATASIZE; i++) {
    // 這樣分配的記憶體空間不連續,可能會發生錯誤
    lpfFeatures[i] = (double*)malloc(sizeof(double*) * FEATURES);
    for (int j = 0; j < FEATURES; j++) {
        // 設定每筆資料的特徵
    }
}

// 必須使用這樣的做法
// 分配一個連續的記憶體空間給 lpfFeatures
double *lpfFeatures = (double*)malloc(sizeof(double) * DATASIZE * FEATURES);
// 透過 lpfFeaturesPtr 去設定每筆資料的特徵
double *lpfFeaturesPtr = lpfFeatures;
for (int i = 0; i < DATASIZE; i++) {
    for (int j = 0; j < FEATURES; j++) {
        // 設定特徵值
        // e.g. lpfFeaturesPtr[j] = FEATURE_VALUE;
        // 也可以改成 lpfFeatures[i * FEATURES + j] = FEATURE_VALUE;
        // 這樣就不用 lpfFeaturesPtr
        // 取決於實際情況該怎麼用
    }
    lpfFeaturesPtr += FEATURES; // 移動 lpfFeaturesPtr 到下一筆資料
}

最後,使用 LGBM_BoosterPredictForMat 進行模型推論並輸出結果,然後釋放記憶體:

// Prepare Arguments
int64_t nOut = 0;
double plfResults[DATASIZE] = {0};
int bIsRowMajor = 1;
int nStartIter = 0;
int nNumIter = 0;
const char* pszParams = "n_jobs=0";

// Perform Inference
nErr = LGBM_BoosterPredictForMat(
    pLgbHandle, lpfFeatures, C_API_DTYPE_FLOAT64, DATASIZE, FEATURES,
    bIsRowMajor, C_API_PREDICT_NORMAL, nStartIter, nNumIter, pszParams, &nOut, plfResults);

printf("LGBM_BoosterPredictForMat(), nErr: %d\n", nErr);
if (nErr) printf("LGBM_GetLastError(), %s\n", LGBM_GetLastError());

// Print Outputs
printf("nOut: %ld\n", nOut);
for (int i = 0; i < nOut; i++)
    printf("plfResults[%d]: %lf\n", i, plfResults[i]);

// Release Memory
LGBM_BoosterFree(pLgbHandle);

LGBM_BoosterPredictForMat 用了非常多參數,關於這些參數的解釋可以參考官方文件。其中 pszParams 這個參數,如果沒有特別的需求,可以設定成空字串 "" 即可,但如果給 NULL 程式就會掛掉。

筆者的完整的程式碼如下:


#include <stdio.h>

#include "LightGBM/c_api.h"

#define FEATURES 5
#define DATASIZE 5

int main() {
    // Load LightGBM Model
    const char* pszModelFileName = "model.txt";
    void* pLgbHandle = NULL;
    int nModelIter = 0;
    int nErr = LGBM_BoosterCreateFromModelfile(
        pszModelFileName, &nModelIter, &pLgbHandle);

    printf("LGBM_BoosterCreateFromModelfile(), nErr: %d\n", nErr);
    printf("pLgbHandle: %p\n", pLgbHandle);
    printf("nModelIter: %d\n", nModelIter);

    if (nErr != 0) {
        printf("LGBM_GetLastError(), %s\n", LGBM_GetLastError());
        return nErr;
    }

    // Create Features
    double lpfFeatures[DATASIZE][FEATURES] = {
        {0.37, 1.09, 0.77, 0.68, 0.50},
        {0.53, 0.06, 0.27, 0.56, 1.94},
        {0.15, 0.09, 0.84, 0.92, 0.33},
        {2.81, 1.21, 1.74, 1.24, 0.99},
        {1.56, 0.95, 0.55, 0.61, 0.28}};

    // Inference
    int64_t nOut = 0;
    double plfResults[DATASIZE] = {0};
    int bIsRowMajor = 1;
    int nStartIter = 0;
    int nNumIter = 0;
    const char* pszParams = "n_jobs=0";

    nErr = LGBM_BoosterPredictForMat(
        pLgbHandle, lpfFeatures, C_API_DTYPE_FLOAT64, DATASIZE, FEATURES,
        bIsRowMajor, C_API_PREDICT_NORMAL, nStartIter, nNumIter, pszParams, &nOut, plfResults);

    printf("LGBM_BoosterPredictForMat(), nErr: %d\n", nErr);
    if (nErr) printf("LGBM_GetLastError(), %s\n", LGBM_GetLastError());

    printf("nOut: %ld\n", nOut);
    for (int i = 0; i < nOut; i++)
        printf("plfResults[%d]: %lf\n", i, plfResults[i]);

    // Release Memory
    nErr = LGBM_BoosterFree(pLgbHandle);
    printf("LGBM_BoosterFree(), nErr: %d\n", nErr);

    return 0;
}

編譯與執行的指令如下:

gcc main.c -o main -I /path/to/lightgbm-repo/include -L /path/to/lightgbm-repo -l_lightgbm
LD_LIBRARY_PATH=/path/to/lightgbm-repo ./main

假設你的 lib_lightgbm.so 放在 /home/user/LightGBM/lib_lightgbm.so,那 /path/to/lightgbm-repo 就是 /home/user/LightGBM

執行結果如下:

LGBM_BoosterCreateFromModelfile(), nErr: 0
pLgbHandle: 0x55fcc39e32f0
nModelIter: 32
LGBM_BoosterPredictForMat(), nErr: 0
nOut: 5
plfResults[0]: 0.295044
plfResults[1]: 0.342804
plfResults[2]: 0.224139
plfResults[3]: 0.433419
plfResults[4]: 0.190785
lgbBoosterFree(), nErr: 0

理論上最後的結果要與 Python 版的執行結果一致。

Dynamic Link

這部份主要是多了 dlopen 與 Function Pointer 的用法,完整程式碼如下:

#include <dlfcn.h>
#include <stdint.h>
#include <stdio.h>

typedef void* BoosterHandle;

#define C_API_DTYPE_FLOAT32 (0)
#define C_API_DTYPE_FLOAT64 (1)
#define C_API_DTYPE_INT32 (2)
#define C_API_DTYPE_INT64 (3)

#define C_API_PREDICT_NORMAL (0)
#define C_API_PREDICT_RAW_SCORE (1)
#define C_API_PREDICT_LEAF_INDEX (2)
#define C_API_PREDICT_CONTRIB (3)

typedef int (*LPFN_CreateFromFile)(const char*, int*, BoosterHandle*);
typedef const char* (*LPFN_GetLastError)();
typedef int (*LPFN_BoosterFree)(BoosterHandle);
typedef int (*LPFN_PredictForMat)(
    BoosterHandle, const void*, int, int32_t, int32_t, int, int, int, int,
    const char*, int64_t*, double*);

#define FEATURES 5
#define DATASIZE 5

int main(int argc, char* argv[]) {
    // Load LightGBM Library
    const char* pszLgbLibPath = "LightGBM/lib_lightgbm.so";
    void* pLgbLib = dlopen(pszLgbLibPath, RTLD_LAZY);

    printf("pLgbLib: %p\n", pLgbLib);

    LPFN_CreateFromFile lgbCreateFromFile = NULL;
    LPFN_PredictForMat lgbPredictForMat = NULL;
    LPFN_GetLastError lgbGetLastError = NULL;
    LPFN_BoosterFree lgbBoosterFree = NULL;

    lgbCreateFromFile = dlsym(pLgbLib, "LGBM_BoosterCreateFromModelfile");
    lgbPredictForMat = dlsym(pLgbLib, "LGBM_BoosterPredictForMat");
    lgbGetLastError = dlsym(pLgbLib, "LGBM_GetLastError");
    lgbBoosterFree = dlsym(pLgbLib, "LGBM_BoosterFree");

    printf("lgbCreateFromFile: %p\n", lgbCreateFromFile);
    printf("lgbPredictForMat: %p\n", lgbPredictForMat);
    printf("lgbGetLastError: %p\n", lgbGetLastError);
    printf("lgbBoosterFree: %p\n", lgbBoosterFree);

    // Load LightGBM Model
    const char* pszModelFileName = "model.txt";
    void* pLgbHandle = NULL;
    int nModelIter = 0;
    int nErr = lgbCreateFromFile(pszModelFileName, &nModelIter, &pLgbHandle);
    printf("lgbCreateFromFile(), nErr: %d\n", nErr);
    printf("pLgbHandle: %p\n", pLgbHandle);
    printf("nModelIter: %d\n", nModelIter);
    if (nErr != 0) return nErr;

    // Create Features
    double lpfFeatures[DATASIZE][FEATURES] = {
        {0.37, 1.09, 0.77, 0.68, 0.50},
        {0.53, 0.06, 0.27, 0.56, 1.94},
        {0.15, 0.09, 0.84, 0.92, 0.33},
        {2.81, 1.21, 1.74, 1.24, 0.99},
        {1.56, 0.95, 0.55, 0.61, 0.28}};

    // Inference
    int64_t lnOut = 0;
    double plfResults[DATASIZE] = {0};
    int bIsRowMajor = 1;

    nErr = lgbPredictForMat(
        pLgbHandle, lpfFeatures, C_API_DTYPE_FLOAT64, DATASIZE, FEATURES,
        bIsRowMajor, C_API_PREDICT_NORMAL, 0, 0, "", &lnOut, plfResults);

    printf("lgbPredictForMat(), nErr: %d\n", nErr);
    if (nErr) printf("lgbGetLastError(), Error: %s\n\n", lgbGetLastError());

    for (int i = 0; i < DATASIZE; i++)
        printf("plfResults[%d]: %lf\n", i, plfResults[i]);

    // Release Memory
    nErr = lgbBoosterFree(pLgbHandle);
    printf("lgbBoosterFree(), nErr: %d\n", nErr);
    dlclose(pLgbLib);

    return 0;
}

因為 c_api.h 實際上只有幾個常數會用到而已,所以我們直接把那些 Define 拿過來。

編譯與執行的指令如下:

gcc main.c -ldl -o main
./main

使用動態載入的編譯與執行指令都乾淨多了,只是寫 Function Pointer 真的蠻累的。

最後輸出如下,輸出一樣與 Python 版的實做一致:

pLgbLib: 0x56228ae4c2d0
lgbCreateFromFile: 0x7f56528345f0
lgbPredictForMat: 0x7f5652841f30
lgbGetLastError: 0x7f565282e270
lgbBoosterFree: 0x7f565282ef50
lgbCreateFromFile(), nErr: 0
pLgbHandle: 0x56228ae50610
nModelIter: 32
lgbPredictForMat(), nErr: 0
plfResults[0]: 0.295044
plfResults[1]: 0.342804
plfResults[2]: 0.224139
plfResults[3]: 0.433419
plfResults[4]: 0.190785
lgbBoosterFree(), nErr: 0

Implement In Python

最後一個瘋狂的應用:使用 Python 的 ctypes 套件,執行更 Native 的推論。這算是筆者第一次寫 ctypes 的 Python 程式,真的是個很有趣的經驗。

完整程式碼如下:

from ctypes import *


class LgbDefine:
    DTYPE_FLOAT32 = 0
    DTYPE_FLOAT64 = 1
    DTYPE_INT32 = 2
    DTYPE_INT64 = 3

    PREDICT_NORMAL = 0
    PREDICT_RAW_SCORE = 1
    PREDICT_LEAF_INDEX = 2
    PREDICT_CONTRIB = 3


def Main():
    # Load Library
    lgb_lib_path = "LightGBM/lib_lightgbm.so"
    lgb = cdll.LoadLibrary(lgb_lib_path)

    # Load Model
    fp = "model.txt"
    err, model, n_iter = LoadModel(lgb, fp)
    print(f"LoadModel(), Error: {err}, Iteration: {n_iter.value}")
    if err != 0:
        ShowError(lgb)
        exit(err)

    # Create Input Data
    data = [
        [0.37, 1.09, 0.77, 0.68, 0.50],
        [0.53, 0.06, 0.27, 0.56, 1.94],
        [0.15, 0.09, 0.84, 0.92, 0.33],
        [2.81, 1.21, 1.74, 1.24, 0.99],
        [1.56, 0.95, 0.55, 0.61, 0.28],
    ]
    data, data_size, features = ConvFeatures(data)
    # 這邊其實用 numpy.array() 轉換成 ctypes 速度上會比較快
    # ConvFeatures() 僅做練習用途

    # Perform Inference
    err, results = LgbInference(lgb, model, data, data_size, features)
    print(f"LgbInference(), Error: {err}")
    if err != 0:
        ShowError(lgb)
        exit(err)

    # Show Results
    for i, res in enumerate(results):
        print(f"Pred[{i}]: {res:.6f}")


def LoadModel(lib_lgb, fp: str):
    model_fn = c_char_p(fp.encode("UTF-8"))
    model_iter = c_int32(0)
    model_iter_p = pointer(model_iter)
    handle = c_void_p()
    handle_p = pointer(handle)
    err = lib_lgb.LGBM_BoosterCreateFromModelfile(model_fn, model_iter_p, handle_p)
    return err, handle, model_iter


def ShowError(lib_lgb):
    lib_lgb.LGBM_GetLastError.restype = c_char_p
    b_msg: bytes = lib_lgb.LGBM_GetLastError()
    s_msg = b_msg.decode("UTF-8")
    print(f"Error Message: {s_msg}")


def DisplayArray2D(arr):
    for row in arr:
        for n in row:
            print(n, end=" ")
        print()


def ConvFeatures(data_py):
    data_size = len(data_py)
    features = len(data_py[0])

    Row = c_double * features
    Data = Row * data_size

    data_cc = Data()

    for i, row_py in enumerate(data_py):
        assert len(row_py) == features
        for j, n_py in enumerate(row_py):
            data_cc[i][j] = c_double(n_py)

    return data_cc, data_size, features


def LgbInference(lib_lgb, model, cc_x, data_size, features):
    n_out = c_int64(0)
    Result = c_double * data_size
    results = Result()
    is_row_major = c_int32(1)
    start_it = c_int32(0)
    n_it = c_int32(0)
    params = b"num_threads=0"

    err = lib_lgb.LGBM_BoosterPredictForMat(
        model,
        cc_x,
        LgbDefine.DTYPE_FLOAT64,
        data_size,
        features,
        is_row_major,
        LgbDefine.PREDICT_NORMAL,
        start_it,
        n_it,
        params,
        pointer(n_out),
        results,
    )

    return err, results


if __name__ == "__main__":
    Main()

執行此 Python 程式,得到相同的執行結果:

LoadModel(), Error: 0, Iteration: 32
LgbInference(), Error: 0
Pred[0]: 0.295044
Pred[1]: 0.342804
Pred[2]: 0.224139
Pred[3]: 0.433419
Pred[4]: 0.190785

結論

LightGBM 是個輕量也相當強大的機器學習套件,擁有高度優化的訓練方法,也有非常快速的推論實做,純文字格式的模型檔案搭配壓縮技術,對硬碟空間相當友善。透過以上實做進行比較,使用 LightGBM C API 相較於 Python API 與 ONNX 都是更理想的選擇。但目前筆者僅比較過少量資料與小型決策樹,在讀取速度與推論速度上都確實比較快,但尚未進行較大型完整的速度比較,預定在接下來的幾天會著手進行這樣的實驗,再補上比較的章節。

參考


圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言