4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【2024年2月版】Elixir で XGBoost = 勾配ブースティング回帰木を実行する EXGBoost

Last updated at Posted at 2024-02-13

はじめに

以前の記事で、 Livebook 上で XGBoost を実行しました

@ta_to_jp さんのコメントでバージョンアップについてお知らせいただいたので、最新版で改めて実行してみました

最新版の場合、結構色々変わっていたため、記事の更新ではなく別記事を書くことにしました
また、パラメータ調整で少し改善しています

実装したノートブックはこちら

実行環境

今回はイテレーション数が多く、ローカルで実行すると時間がかかりすぎるため、以前紹介した手法を使い、 Google Colab 上で Livebook を実行します

当時からバージョン等が変わっているため、最新版はこちらのノートブックを参照してください

せっかくなので Colab Pro で T4 GPU 、ハイメモリを使いました

スクリーンショット 2024-02-11 17.40.56.png

そこまでしなくても大丈夫だとは思います

セットアップ

以前と同じパッケージの最新版をインストールします

Mix.install([
  {:exgboost, "~> 0.5"},
  {:explorer, "~> 0.8"},
  {:nx, "~> 0.6"},
  {:kino, "~> 0.12"},
  {:kino_vega_lite, "~> 0.1"}
])

エイリアスとマクロの設定をします

alias Explorer.DataFrame
alias Explorer.Series
require Explorer.DataFrame

データ入力

トレーニング用データ、テスト用データのファイルをアップロードします

train_data_input = Kino.Input.file("train data")
test_data_input = Kino.Input.file("test data")

スクリーンショット 2024-02-11 17.46.17.png

データを表形式で表示します

train_data =
  train_data_input
  |> Kino.Input.read()
  |> Map.get(:file_ref)
  |> Kino.Input.file_path()
  |> DataFrame.from_csv!()

Kino.DataTable.new(train_data)

スクリーンショット 2024-02-11 17.47.03.png

test_data =
  test_data_input
  |> Kino.Input.read()
  |> Map.get(:file_ref)
  |> Kino.Input.file_path()
  |> DataFrame.from_csv!()

Kino.DataTable.new(test_data)

スクリーンショット 2024-02-11 17.50.13.png

前処理

前処理用のモジュールを定義します

defmodule Preprocess do
  defp load_csv(kino_input) do
    kino_input
    |> Kino.Input.read()
    |> Map.get(:file_ref)
    |> Kino.Input.file_path()
    |> DataFrame.from_csv!()
  end

  defp fill_empty(data, fill_map) do
    fill_map
    |> Enum.reduce(data, fn {column_name, fill_value}, acc ->
      DataFrame.put(
        acc,
        column_name,
        Series.fill_missing(data[column_name], fill_value)
      )
    end)
  end

  defp replace_dummy(data, columns_names) do
    data
    |> DataFrame.dummies(columns_names)
    |> DataFrame.concat_columns(DataFrame.discard(data, columns_names))
  end

  defp to_tensor(data) do
    data
    |> DataFrame.to_columns()
    |> Map.values()
    |> Nx.tensor()
    |> Nx.transpose()
  end

  def process(kino_input, id_key, label_key) do
    data_org = load_csv(kino_input)

    id_list = Series.to_list(data_org[id_key])

    has_label_key =
      data_org
      |> DataFrame.names()
      |> Enum.member?(label_key)

    labels =
      if has_label_key do
        Series.to_tensor(data_org[label_key])
      else
        nil
      end

    inputs =
      if has_label_key do
        DataFrame.discard(data_org, [label_key])
      else
        data_org
      end
      |> DataFrame.discard([id_key, "Cabin", "Name", "Ticket"])
      |> fill_empty(%{"Age" => 0.0, "Embarked" => "S", "Fare" => 0.0})
      |> replace_dummy(["Sex", "Embarked"])
      |> to_tensor()

    {id_list, labels, inputs}
  end
end

各データに前処理を施します

{
  train_id_list,
  train_labels,
  train_inputs
} = Preprocess.process(train_data_input, "PassengerId", "Survived")
{
  test_id_list,
  nil,
  test_inputs
} = Preprocess.process(test_data_input, "PassengerId", "Survived")

トレーニング

トレーニング状況を見るためのグラフを用意します
(この時点では空)

loss_plot =
  VegaLite.new(width: 700)
  |> VegaLite.mark(:line)
  |> VegaLite.encode_field(:x, "step", type: :quantitative)
  |> VegaLite.encode_field(:y, "mlogloss", type: :quantitative)
  |> Kino.VegaLite.new()

各イテレーションで実行するコールバックを用意します

step_callback =
  EXGBoost.Training.Callback.new(
    :after_iteration,
    fn state ->
      if rem(state.iteration, 1000) == 0 do
        Kino.VegaLite.push(
          loss_plot,
          %{"step" => state.iteration, "mlogloss" => state.metrics["training"]["mlogloss"]}
        )
      end

      state
    end,
    :loss_plot
  )

EXGBoost v0.3 のときと比べて、以下の点を変更しています

  • EXGBoost.Training.Callback.new の第3引数 name (コールバックを識別するための名前) に :loss_plot を指定
  • if rem(state.iteration, 1000) == 0 do のグラフにプロットする感覚を 20 から 1000 に増加(イテレーション数が増えるため)

トレーニングを実行しますトレーニングが 10,000 回で終わらないため、最大 200,000 回まで実行するようにしています
また、 learning_rates(学習率)を 0.1 で固定します(デフォルトは 0.3)

末尾に Kino.nothing() を追加して、トレーニングの結果の回帰木を表示しないようにしています
バグかスペック不足のため、表示しようとするとエラーになるためです

booster =
  EXGBoost.train(train_inputs, train_labels,
    num_class: 2,
    objective: :multi_softprob,
    num_boost_rounds: 200000,
    learning_rates: fn _ -> 0.1 end,
    max_depth: 6,
    early_stopping_rounds: 10,
    evals: [{train_inputs, train_labels, "training"}],
    callbacks: [step_callback]
  )

Kino.nothing()

テストデータに対する推論

学習した回帰木を使って、テストデータを推論します

preds = EXGBoost.predict(booster, test_inputs) |> Nx.argmax(axis: -1)

推論結果を提出用に整形し、表形式で表示します

results =
  preds
  |> Nx.to_flat_list()
  |> Enum.map(&round(&1))
  |> then(
    &%{
      "PassengerId" => test_id_list,
      "Survived" => &1
    }
  )
  |> DataFrame.new()

Kino.DataTable.new(results)

推論結果をダウンロードします

results
|> DataFrame.dump_csv!()
|> then(&Kino.Download.new(fn -> &1 end, filename: "result.csv"))

結果は以下のようになりました

イテレーション数: 56,216

トレーニング中のロス推移:
visualization (3).png

提出結果のスコア: 0.74162

前回は 0.72248 だったので、 2 % ほど改善しています

学習率を下げたことで、微改善したのだと思われます

前処理の改善

前処理を以下のように変更します

変更内容は前回の記事を参照してください

defmodule PreProcess do
  def load_csv(kino_input) do
    kino_input
    |> Kino.Input.read()
    |> Map.get(:file_ref)
    |> Kino.Input.file_path()
    |> DataFrame.from_csv!()
  end

  def fill_empty(data, fill_map) do
    fill_map
    |> Enum.reduce(data, fn {column_name, fill_value}, acc ->
      fill_value =
        if fill_value == :median do
          Series.median(data[column_name])
        else
          fill_value
        end

      DataFrame.put(
        acc,
        column_name,
        Series.fill_missing(data[column_name], fill_value)
      )
    end)
  end

  def replace_dummy(data, columns_names) do
    data
    |> DataFrame.dummies(columns_names)
    |> DataFrame.concat_columns(DataFrame.discard(data, columns_names))
  end

  def to_tensor(data) do
    data
    |> DataFrame.to_columns()
    |> Map.values()
    |> Nx.tensor()
    |> Nx.transpose()
  end

  def process(kino_input, id_key, label_key, followers_df) do
    data_org = load_csv(kino_input)

    id_list = Series.to_list(data_org[id_key])

    has_label_key =
      data_org
      |> DataFrame.names()
      |> Enum.member?(label_key)

    labels =
      if has_label_key do
        Series.to_tensor(data_org[label_key])
      else
        nil
      end

    inputs =
      if has_label_key do
        DataFrame.discard(data_org, [id_key, label_key])
      else
        DataFrame.discard(data_org, [id_key])
      end
      |> DataFrame.mutate(
        prob_child:
          col("Name") |> contains("Master") or
            (col("Name") |> contains("Miss") and
               col("Parch") > 0)
      )

    filled_age =
      [
        Series.to_list(inputs["Age"]),
        Series.to_list(inputs["prob_child"])
      ]
      |> Enum.zip()
      |> Enum.map(fn
        {nil, true} ->
          9

        {nil, false} ->
          30

        {age, _prob_child} ->
          age
      end)
      |> Series.from_list()

    inputs =
      inputs
      |> DataFrame.put("Age", filled_age)
      |> DataFrame.join(followers_df, how: :left)
      |> fill_empty(%{"followers" => 0, "Embarked" => "S", "Fare" => :median})
      |> replace_dummy(["Embarked", "Pclass"])
      |> DataFrame.mutate(is_man: col("Sex") == "male")
      |> DataFrame.mutate(fare_group: (col("Fare") / 50) |> floor())
      |> DataFrame.mutate(age_group: (col("Age") / 10) |> floor())
      |> DataFrame.discard(["Cabin", "Name", "Ticket", "Sex", "Fare", "Age", "SibSp", "Parch"])
      |> to_tensor()

    {id_list, labels, inputs}
  end
end

トレーニング時のハイパーパラメータを以下のように変更します

booster =
  EXGBoost.train(train_inputs, train_labels,
    num_class: 2,
    objective: :multi_softprob,
    num_boost_rounds: 100000,
    learning_rates: fn _ -> 0.1 end,
    max_depth: 6,
    early_stopping_rounds: 10,
    evals: [{train_inputs, train_labels, "training"}],
    callbacks: [step_callback]
  )

Kino.nothing()

結果は以下のようになりました

イテレーション数: 41,602

トレーニング中のロス推移:
visualization (3).png

提出結果のスコア: 0.75358

前回は 0.75119 だったので、こちらも 2 % ほど改善しています

まとめ

最新版を使い、学習率を下げることでスコアが微増しました

また、最初はローカルで実行してみましたが、あまりに遅いため Google Colab 上で実行することになりました

やはり Google Colab は強力なプラットフォームですね

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?