💨

Elixir microsoft/LightGBM クラス分類を試す

2022/12/02に公開約5,200字1件のコメント

この記事は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_iterationeval_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もモデルファイルを更新しているため、良さそうなパラメータが見つかった場合は、改めて見つかったパラメータでfit or refitが必要です。

(完)

Discussion

docker環境で準備する場合のメモです。

Dockerfile
# microsoft/LightGBM
WORKDIR /root
RUN git clone --recursive https://github.com/microsoft/LightGBM \
  && cd LightGBM \
  && mkdir build \
  && cd build \
  && cmake .. \
  && make -j4
ENV LIGHTGBM_DIR=/root/LightGBM
アプリケーションでの設定(config/config.exs等)
config :lgbm_ex_cli,
  lightgbm_cmd: Path.join(System.get_env("LIGHTGBM_DIR"), "lightgbm")
ログインするとコメントできます