はじめに
以前の記事で、 Livebook 上で XGBoost を実行しました
@ta_to_jp さんのコメントでバージョンアップについてお知らせいただいたので、最新版で改めて実行してみました
最新版の場合、結構色々変わっていたため、記事の更新ではなく別記事を書くことにしました
また、パラメータ調整で少し改善しています
実装したノートブックはこちら
実行環境
今回はイテレーション数が多く、ローカルで実行すると時間がかかりすぎるため、以前紹介した手法を使い、 Google Colab 上で Livebook を実行します
当時からバージョン等が変わっているため、最新版はこちらのノートブックを参照してください
せっかくなので Colab Pro で T4 GPU 、ハイメモリを使いました
そこまでしなくても大丈夫だとは思います
セットアップ
以前と同じパッケージの最新版をインストールします
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")
データを表形式で表示します
train_data =
train_data_input
|> Kino.Input.read()
|> Map.get(:file_ref)
|> Kino.Input.file_path()
|> DataFrame.from_csv!()
Kino.DataTable.new(train_data)
test_data =
test_data_input
|> Kino.Input.read()
|> Map.get(:file_ref)
|> Kino.Input.file_path()
|> DataFrame.from_csv!()
Kino.DataTable.new(test_data)
前処理
前処理用のモジュールを定義します
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
提出結果のスコア: 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
提出結果のスコア: 0.75358
前回は 0.75119 だったので、こちらも 2 % ほど改善しています
まとめ
最新版を使い、学習率を下げることでスコアが微増しました
また、最初はローカルで実行してみましたが、あまりに遅いため Google Colab 上で実行することになりました
やはり Google Colab は強力なプラットフォームですね