LightGBMをg++で使ってみた

# 概要
* LightGBM CLI のインストール
* LightGBM CLIをC++で実行

# はじめに
今回はLightGBMをマイクロソフトが提供しているLightGBMのCLI版を用いて、c++でコンパイルできる方法を紹介していきます。
処理の流れとしては

> 1. LigthGBM CLIを用いてモデルを学習
1. 学習したモデルファイルを用いて、C++のコードを作成
1. g++でコンパイル、実行ファイルの作成

という感じになっています。 
実験環境は

> * google colaboratory

です。
興味のある方はぜひ途中まででも大丈夫ですので、ご付き合いください(^^)

# メイン
## LightGBM CLI とは？
まず、「LightGBM CLIとはなんぞや？」となってる方に簡単に説明すると、LightGBM CLIという名の通り、LightGBMのCLI（CUI）バージョンです。通常であればLightGBMは`pip`、`scikit-learn`でインストールして来て`.py`に`import lightgbm as gbm`として使ったりしますよね。CLIバージョンでは、`./lightgbm`とCUIで入力して使います。
とまあ、脇道に逸れた話は置いといて、先に進みましょう。

## LightGBM CLIのインストール
それではCLIバージョンのLightGBMをインストールしていきましょう。
と言っても、そんなに難しいことはしなくて公式サイトのインストール手順の通りにインストールしていきます。
> [公式サイト](https://lightgbm.readthedocs.io/en/latest/Installation-Guide.html#)

今回はcolabで実験したので環境はLinuxで行ってます。実行コードは↓です。

```.bash
!git clone --recursive https://github.com/microsoft/LightGBM
%cd LightGBM
!mkdir build
%cd build
!cmake ..
!make -j4
!make -j4 install
```

上記のコードを実行するとLightGBMというディレクリの中に、lightgbmという実行ファイルが作成されます。

## LightGBM CLIの使い方
インストールが終わったので、次にCLIでLightGBMを使う方法を見ていきましょう。

今回使うデータは定番のirisデータセットを使っていきます。

### Datasetの準備
まずはirisデータセットを準備していきましょう。今回は`seaborn`の方のirisを使っています。

```.py
import seaborn as sns
iris = sns.load_dataset('iris')
import numpy as np
import pandas as pd

columns = ["sepal_length", "sepal_width", "petal_length", "petal_width"]
data = iris[columns]

conditions=[(iris["species"] == "setosa"),(iris["species"] == "versicolor"), (iris["species"] == "virginica")]
choice = [0,1,2]

target = pd.Series(np.select(conditions, choice))
```

データの整形後、trainとtestに分割していきます。
その後、CLIでtrainingするために、CSVの形でtrainとtestデータを保存しておきます。

```.py
from sklearn.model_selection import train_test_split

train_data, test_data, train_target, test_target = train_test_split(data, target, test_size=0.2, shuffle=True,random_state=42)

pd.concat([train_target, train_data], axis=1).to_csv("iris.train", header=False, index=False)
pd.concat([test_target, test_data],axis=1).to_csv("iris.test", header=False, index=False)
```

#### Train, Testファイルについて
`iris.train`と`iris.test`について軽く説明をしたいと思います。
まずファイル名ですが、エイリアスが設定されておりそれに合わせて名前を決めることができます。
>* [trainのエイリアス]( https://lightgbm.readthedocs.io/en/latest/Parameters.html#data)
>* [testのエイリアス]( https://lightgbm.readthedocs.io/en/latest/Parameters.html#valid)

次にtrainの目的変数と説明変数の順番ですが、何も設定をしなければデフォルトで一番最初の列が目的変数になります。
設定するパラメータは`label_column`で設定することができます。

>* [label_column](https://lightgbm.readthedocs.io/en/latest/Parameters.html#label_column)


### Train, Convert, Predictのファイルを作成
CLIでは、taskに合わせてconfig fileのようなものを作成する必要があります。

>* [task parameters](https://lightgbm.readthedocs.io/en/latest/Parameters.html#task)

ここでは、主に使用するであろう`train、predict、convert_model(convert)`について説明したいと思います。<font color="gray">（refit、save_binaryはよくわからないです）</font>

trainとpredictは名前の通りの役割なのでわかりやすいのかなと思うのですが、convertはわかりにくい感が満載です。
convertで何をするか簡単に説明しますと、

1. 学習したモデルを`.cpp`の形で保存
1. `build`、`make`で学習したタスクの新しいLightGBM実行ファイルを構築
1. 新しく構築したLightGBMを使ってpredictを行う

という感じです。（下記で参考サイト乗っけてます。）
個人的には、predict機能がCLIで元からできるのになんでconvertという機能があるのかなと感じました。
最初はconvertした`.cpp`を使って、c++でそのモデルを使えるようになるのかなと思ったのですが、これは僕のc++に対する理解不足と相まって、「おそらく使えないだろうけどわからないな」という結論になりました。<font color="gray">(もし分かる方がいたらご教授お願いします)</font>

とまあ機能の説明はここまでにして、まずはファイルの作り方を紹介していきます。
ファイルの作り方としては、設定したいパラメータを羅列して、ファイル名をaliasに合わせて保存するだけです。
以下のコードはパラメータの例です。パラメータはpythonでLightGBMを使うときと変わらないので、そんなに違和感なくて設定できると思います。

> * [LightGBM ドキュメント](https://lightgbm.readthedocs.io/en/latest/Parameters.html#core-parameters)
> * [lightGBM全パラメーター解説(途中)](https://qiita.com/cryptrader/items/de15e69d8435ea1684ac)

```.py
# train
train_conf = """
task = train
objective = multiclass
metric = multi_logloss
data = iris.train
num_class = 3
output_model = trained_model.txt
num_tree = 5
num_leaves = 3
"""

with open("train.conf", "w") as f:
    f.write(train_conf)

# convert
convert_conf = """
task = convert_model
input_model= trained_model.txt
convert_model_language = cpp
"""

with open("convert.conf", "w") as f:
    f.write(convert_conf)

# predict
predict_conf = """
task = predict
input_model = trained_model.txt
data = iris.test
"""

with open("predict.conf", "w") as f:
  f.write(predict_conf)
```

上記のコードを実行すると、`train.conf`,`convert.conf`,`predict.conf`の３つのファイルが作成できます。
それぞれの使い方は[公式サイト](https://lightgbm.readthedocs.io/en/latest/Quick-Start.html#run-lightgbm)の通りです。

#### Train

```.bash
! ./lightgbm config=train.conf
```

oupput_modelのパラメータで設定した`.txt`ファイルが作成されます。
今回であれば、trained_model.txtという名前でモデルのファイルが作成されます。
ちなみになんですが、このモデルファイルはpythonで作成したモデルファイルと中身は一緒です。
このコマンドですね → `model.save_model('model.txt')`


#### Convert

```.bash
! ./lightgbm config=convert.conf
```

ここではパラメータを設定してないので、デフォルトの`gbdt_prediction.cpp`が作成されます。
`convert_model`のパラメータで設定することができます。
ちなみにConvertで生成されたファイルの使い方はGithubのIssueのところにあったので参考にしてもらえればと思います。

>* [Loading a model from Python into C++ just for predictions](https://github.com/microsoft/LightGBM/issues/2397)

一応該当コードも添付しときます。

```.bash
cd $BUILD_DIRECTORY/tests/cpp_test && ../../lightgbm config=train.conf convert_model_language=cpp convert_model=../../src/boosting/gbdt_prediction.cpp && ../../lightgbm config=predict.conf output_result=origin.pred || exit -1 
cd $BUILD_DIRECTORY/build && make lightgbm -j4 || exit -1 
cd $BUILD_DIRECTORY/tests/cpp_test && ../../lightgbm config=predict.conf output_result=ifelse.pred && python test.py || exit -1 
```

#### Predict

```.bash
! ./lightgbm config=predict.conf
```

ここでもパラメータを設定してないので、デフォルトの`LightGBM_predict_result.txt`が作成されます。
`output_result`のパラメータで設定することができます。

## C++で使う
それではいよいよc++でコンパイルしていきます。ここで必要になってくるファイルはTrainで生成された`.txt`ファイルと、testデータセットです。
そしてLightGBMの`C API`というものを使用します。

>* [C API](https://lightgbm.readthedocs.io/en/latest/C-API.html)

c++に関してはあまり知識がないので、誰か海外ニキが試みてないのかなーと探してたら、あるにはあったんですがそれだけでは実行ができませんでした。ただ参考にはとてもなったので、そのサイトを以下に載せています。

>* [How to call lightgbm in C++ program](https://www.fatalerrors.org/a/how-to-call-lightgbm-in-c-program.html)

#### コード
まずc++のコードです。colabで実行しているので、`%%writefile skeleton_test.cpp`が記載してあります。

```.cpp
%%writefile skeleton_test.cpp

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

void predict()
{
    int temp;
    int p = 1;
    BoosterHandle handle;  
    
    // load model 
    temp = LGBM_BoosterCreateFromModelfile("trained_model.txt", &p, &handle);    
    std::cout <<"load result value is "<<temp <<std::endl;
    
    // predict file data
    const char* para = "None";
    int res = LGBM_BoosterPredictForFile(handle, "iris.test", 0, C_API_PREDICT_NORMAL, 0, 0, para, "result");
    std::cout << "file predict result is " << res << std::endl;
 
    // row data
    std::vector<float> row={6.1,2.8,4.7,1.2};
    void* in_p = static_cast<void*>(row.data());
    
    std::vector<double> out = (3, 0); //result用
    double* out_result = static_cast<double*>(out.data());
    
    int64_t *out_len = new int64_t();
    res = LGBM_BoosterPredictForMat(handle, in_p, C_API_DTYPE_FLOAT32, 1, 4, 1, C_API_PREDICT_NORMAL, 0, 2, "None", out_len, out_result);
    std::cout << "row predict return is " << res << std::endl;
    std::cout << "row predict result size is " << int(out.size()) << " value is " << out_result[0] << " "<< out_result[1] << " " << out_result[2] << std::endl;
 
    delete out_len;

}

int main(void) {
  predict(); 
   
  std::cout << "Ok complete!"<< std::endl;
  return 0;
}
```

このコードの主な点は、

* モデルをロード
* ファイルデータからpredictする
* 配列などのrowデータから予測する

といった感じです。
使ったC APIとしては

* [LGBM_BoosterCreateFromModelfile](https://lightgbm.readthedocs.io/en/latest/C-API.html#c.LGBM_BoosterCreateFromModelfile)
* [LGBM_BoosterPredictForFile](https://lightgbm.readthedocs.io/en/latest/C-API.html#c.LGBM_BoosterPredictForFile)
* [LGBM_BoosterPredictForMat](https://lightgbm.readthedocs.io/en/latest/C-API.html#c.LGBM_BoosterPredictForMat)

の３つです。

最後に以下のコマンドを実行することで完了です。

```.bash
!g++ -fpermissive  -o a.out -v skeleton_test.cpp lib_lightgbm.so -Wl,-rpath,/content/LightGBM
```

```.bash
./a.out
```

実行結果としては、まずファイルデータからpredictした場合はパラメータで指定してる`result`という結果ファイルが作成されます。
そしてrowデータから予測してる場合は、コマンドラインに結果が表示されます。
↓こんな感じです。

```.bash
load result value is 0
file predict result is 0
row predict return is 0
row predict result size is 3 value is 0.274069 0.45598 0.269951
Ok complete!
```

これにて完了！！


## 最後に
今回はLightGBMをC++で使えるようにしてみました。エッジデバイスとかで使うときに役に立つかもなくらいには考えています。C++も使えようになりたいなと思った今日このごろでした。