💨
Elixir microsoft/LightGBM クラス分類を試す
この記事はElixir Advent Calendar 2022の1つです。カレンダーも是非ご覧ください!
Elixirでmicrosoft/LightGBMを使って、クラス分類を行う流れの紹介です。
下記2つのライブラリを使います。
- 
https://github.com/tato-gh/lgbm_ex_cli
- LightGBM CLIを内部で利用しています
 - モデル構築と大規模データの予測に利用します
 - CLI単体で使う流れはこちらの記事をご覧ください
 
 - 
https://github.com/tato-gh/lgbm_ex_capi
- LightGBM C-APIを内部で利用しています
 - データ予測に利用します
 - 大規模データになるとCLIの方が早いです(おそらく)
 
 - どちらもNot stableですので、もしお手元で試される場合はご注意ください
 
クラス分類の対象データとして、Iris Data Setを取り上げます(ElixirのScidataパッケージで簡単に取得できます)。
- ここではクラス分類を取り上げていますが、回帰も同様の流れで可能です。
 
以下、Livebookを想定して進めていきます
インストール
新しいノートブックを開いたら、Notebook dependencies and setup 欄でインストールします。
lightgbm_cmd = File.cwd!() <> "/LightGBM/lightgbm"
if System.find_executable(lightgbm_cmd) do
  "already existing"
else
  System.shell("git clone --recursive https://github.com/microsoft/LightGBM")
  System.shell("cd LightGBM && mkdir build && cd build && cmake .. && make -j4")
end
Mix.install(
  [
    {:scidata, "~> 0.1.9"},
    {:lgbm_ex_cli, "0.1.0", git: "https://github.com/tato-gh/lgbm_ex_cli"},
    {:lgbm_ex_capi, "0.1.0", git: "https://github.com/tato-gh/lgbm_ex_capi"},
  ],
  config: [
    lgbm_ex_cli: [lightgbm_cmd: lightgbm_cmd]
  ],
  system_env: %{
    "LIGHTGBM_DIR" => Path.join(File.cwd!(), "LightGBM")
  }
)
# 見た目のためにalias
alias LGBMExCli, as: LightGBM_CLI
alias LGBMExCapi, as: LightGBM_API
:ok
- lgbm_ex_cliアプリケーションにlightgbmコマンドのパスを設定しています
 - lgbm_ex_capiライブラリのインストール時にmakeが必要になるため、LIGHTGBM_DIRを環境変数に設定しています
 
(1)モデル作成
モデルはCLIで作成します。モデルや訓練データはファイル保存されるため、適当な格納先フォルダを作っています。
{features, labels} = Scidata.Iris.download()
workdir = Path.join(File.cwd!(), "iris")
File.mkdir_p(workdir)
{:ok, model_file} =
  LightGBM_CLI.fit(workdir, features, labels, [
    objective: "multiclass",
    metric: "multi_logloss",
    num_class: 3,
    num_iterations: 10,
    num_leaves: 5,
    min_data_in_leaf: 1
  ])
(2-A)データ予測 CLI編
CLIでデータ予測を行う場合は、モデル作成時に取得したモデルファイルのパスと、予測対象データを渡します。
results = LightGBM_CLI.predict(model_file, features)
- 
model_fileがあるフォルダには、結果が格納されたTSVファイルも作成されます - 
predictが返すresultsはそれを読んだものです 
(2-B)データ予測 C-API編
C-APIでデータ予測を行う場合は、モデル作成時に取得したモデルファイルからモデルをロードした後で、予測対象データを渡します。
{:ok, nif_reference} = LightGBM_API.load(model_file)
results = LightGBM_API.predict(nif_reference, features)
- 
nif_referenceは、ElixirからCのリソースを参照するためのリファレンスです。上記ではロードしたモデルを特定するために使っています - まとまったデータに対する予測はCLIを使う方が早いですが、都度都度予測するようなケースにおいては、C-APIの方が早いです(CLIを使うと予測対象データのファイル書き込みが都度発生するためです)
 
(3)補足:C-APIを使ったモデル情報の取得
C-APIを使うと、作成した(ロードした)モデルのクラス数などを取得できます。
num_classes = LightGBM_API.get_num_classes(nif_reference)
num_iterations = LightGBM_API.get_num_iterations(nif_reference)
num_features = 4
feature_importance = LightGBM_API.get_feature_importance(nif_reference, num_features)
{
  num_iterations,
  num_classes,
  feature_importance
}
(4)補足:アーリーストッピングを使ったモデル構築
アーリーストッピングを使う場合には、fitに検証用データを渡します。
# 訓練用と検証用の分離(記事都合上の準備)
{features, labels} = Scidata.Iris.download()
data_size = Enum.count(labels)
validation_size = div(data_size, 10)
shuffled_indexes = Enum.shuffle(0..(data_size - 1))
[train_indexes, validation_indexes] = Enum.chunk_every(shuffled_indexes, data_size - validation_size)
{train_features, train_labels} =
  Enum.reduce(train_indexes, {[], []}, fn index, {acc_x, acc_y} ->
    x = Enum.at(features, index)
    y = Enum.at(labels, index)
    {acc_x ++ [x], acc_y ++ [y]}
  end)
{validation_features, validation_labels} =
  Enum.reduce(validation_indexes, {[], []}, fn index, {acc_x, acc_y} ->
    x = Enum.at(features, index)
    y = Enum.at(labels, index)
    {acc_x ++ [x], acc_y ++ [y]}
  end)
# アーリーストッピングを使ったモデル構築
{:ok, model_file, num_iteration, eval_value} =
  LightGBM_CLI.fit(workdir, {train_features, validation_features}, {train_labels, validation_labels}, [
    objective: "multiclass",
    metric: "multi_logloss",
    num_class: 3,
    num_iterations: 1000,
    num_leaves: 5,
    min_data_in_leaf: 1,
    early_stopping_round: 2
  ])
- アーリーストッピングを使った場合は、モデルファイルパスのほかに、
num_iterationとeval_valueが返ってきます- // (ライブラリ内部の話ですが)CLIの出力ログをパースするしか方法がなさそうでした...
 
 
(5)補足:パラメータ探索
LightGBMにはパラメータがたくさんありますので、適当なパラメータを探索するケースがあります。そのような際にはrefitを使って、すでに作成済みの訓練データに対して、違うパラメータでモデルを再構築できます。
下記は、max_depthとmin_data_in_leafを探索している例です。
base_params = [
  objective: "multiclass",
  metric: "multi_logloss",
  num_class: 3,
  num_iterations: 1000,
  early_stopping_round: 2,
  num_leaves: 5,
  min_data_in_leaf: 1
]
max_depth_list = [3, 5, 7]
min_data_in_leaf_list = [1, 2, 3]
for max_depth <- max_depth_list, min_data_in_leaf <- min_data_in_leaf_list do
  params = Keyword.merge(
    base_params,
    max_depth: max_depth,
    min_data_ln_leaf: min_data_in_leaf
  )
  {:ok, _model_file, num_iteration, eval_value} = LightGBM_CLI.refit(workdir, params)
  {max_depth, min_data_in_leaf, {:ok, num_iteration, eval_value}}
end
- 
refitもモデルファイルを更新しているため、良さそうなパラメータが見つかった場合は、改めて見つかったパラメータでfitorrefitが必要です。 
(完)
Discussion
docker環境で準備する場合のメモです。