はじめに
2024年2月版を書きました
XGBoost は機械学習アルゴリズムの一つで、 Kaggle でも上位を取っている手法です
Elixir でも XGBoost 用のモジュール EXGBoost が作られています
そこで、今回は以前 CNN で学習したタイタニックの生存者予測を XGBoost で学習してみます
先駆者 @piacerex さん
また、今回も Livebook で実装していきます
データの準備
Kaggle からタイタニックのデータをダウンロードします
アカウントの作成
Kaggle のアカウントを作成します
トップページの右上「Register」をクリックし、指示に従っていけば簡単に作成できます
データのダウンロード
以下のページ右下「Download All」をクリックし、 taitanic.zip をダウンロードします
tainanic.zip を展開して出てくる以下の CSV ファイルを使用します
- train.csv: 学習データ
- test.csv: テストデータ
XGBoost によるトレーニングの実装
まず、『「Elixirで機械学習に初挑戦」をやってみた(中編)』でやったのと同じ前処理で実装してみます
実装したノートブックはこちら
セットアップ
依存モジュールをインストールします
Mix.install([
{:exgboost, "~> 0.3"},
{:explorer, "~> 0.6"},
{:nx, "~> 0.6"},
{:kino, "~> 0.10"},
{:kino_vega_lite, "~> 0.1"}
])
- EXGBoost: XGBoost
- Explorer: データ解析
- Nx: 行列演算
- Kino: Livebook の UI/UX
- KinoVegaLite: グラフ描画
エイリアス等の設定をします
alias Explorer.DataFrame
alias Explorer.Series
require Explorer.DataFrame
データのロード
以下のコードを実行すると、ファイルアップロード用のフォームが表示されます
train_data_input = Kino.Input.file("train data")
ここに Kaggle からダウンロードした train.csv をドラッグ&ドロップしてアップロードします
同様に test.csv についてもアップロードします
test_data_input = Kino.Input.file("test data")
train.csv をロードしてテーブル表示します
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.csv もロードしてテーブル表示します
前処理
これによって欠損値補完や、目的変数、説明変数の抽出を行っています
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")
train_id_list
が各行(乗客)の ID 、 train_labels が目的変数 = 生存フラグ、 train_inputs が説明変数 = 年齢等の値です
{
test_id_list,
nil,
test_inputs
} = Preprocess.process(test_data_input, "PassengerId", "Survived")
テストデータに対しても同様の前処理を施します
タイタニックのテストデータは正解のラベルを含んでいないので、目的変数は nil
にしています
トレーニング
XGBoost によるトレーニングを実行します
CNN のときと同様、トレーニングの状況をグラフで確認できるようにしてみます
トレーニングが進行すれば回帰残差はどんどん小さくなるはずなので、これをプロットするための枠を用意します
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()
トレーニングの反復毎に呼び出されるコールバック関数として、グラフを描画する関数を用意します
毎回描画すると処理が重くなりすぎるため、 20 回に 1 回だけ実行します
step_callback =
EXGBoost.Training.Callback.new(:after_iteration, fn state ->
if rem(state.iteration, 20) == 0 do
Kino.VegaLite.push(
loss_plot,
%{"step" => state.iteration, "mlogloss" => state.metrics["training"]["mlogloss"]}
)
end
{:cont, state}
end)
EXGBoost.train
で XGBoost を実行します
booster =
EXGBoost.train(train_inputs, train_labels,
num_class: 2,
objective: :multi_softprob,
num_boost_rounds: 10000,
early_stopping_rounds: 10,
evals: [{train_inputs, train_labels, "training"}],
callbacks: [step_callback]
)
early_stopping_rounds: 10
によって、10回連続で残差が改善されなかった場合に早期終了するよう指定します
また、 callbacks: [step_callback]
によってグラフ描画用関数を指定します
実行すると、用意しておいたグラフ用の枠の中に、残差が瞬く間に小さくなる様子が表示されます
また、実行結果には最終的な反復回数と、最小の残差が表示されます
今回は早期終了せず、最後までトレーニングされました
推論
EXGBoost.predict
にトレーニング済みのパラメータとテストデータの説明変数を渡すことで、テストデータの目的変数の値を推論します
EXGBoost.predict
の実行結果は乗客毎に [生存しなかった確率, 生存した確率]
の形で返ってくるため、これを Nx.argmax
により、生存したかどうかのフラグに変換します
preds = EXGBoost.predict(booster, test_inputs) |> Nx.argmax(axis: -1)
実行結果は以下のようになります
#Nx.Tensor<
s64[418]
[0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, ...]
>
結果を乗客の ID と合わせてデータフレーム化します
results =
preds
|> Nx.to_flat_list()
|> Enum.map(&round(&1))
|> then(
&%{
"PassengerId" => test_id_list,
"Survived" => &1
}
)
|> DataFrame.new()
Kino.DataTable.new(results)
データフレームを CSV としてダウンロードします
results
|> DataFrame.dump_csv!()
|> then(&Kino.Download.new(fn -> &1 end, filename: "result.csv"))
Kaggle への提出
ダウンロードした CSV を Kaggle に提出します
結果は 0.72248 でした
同じ前処理をしたときの CNN が 0.77511 なので、今回の例では CNN より精度が低い結果となりました
前処理を改善した XGBoost の実行
次に『「Elixirで機械学習に初挑戦」をやってみた(後編)』でやったのと同じように、前処理を改善した上で XGBoost を実行してみます
実装したノートブックはこちら
セットアップ、データロード
セットアップ、データロードは同じです
改善した前処理
データ分析の結果、改善した前処理の実装です
チケット番号から同乗者数などの項目を付加しています
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
full_data =
train_data
|> DataFrame.discard("Survived")
|> DataFrame.concat_rows(test_data)
Kino.DataTable.new(full_data)
followers_df =
full_data["Ticket"]
|> Series.frequencies()
|> DataFrame.rename(["Ticket", "followers"])
|> DataFrame.mutate(followers: followers - 1)
|> DataFrame.filter(col("Ticket") != "LINE")
Kino.DataTable.new(followers_df)
{
train_id_list,
train_labels,
train_inputs
} = PreProcess.process(train_data_input, "PassengerId", "Survived", followers_df)
{
test_id_list,
nil,
test_inputs
} = PreProcess.process(test_data_input, "PassengerId", "Survived", followers_df)
前処理改善時のトレーニング
トレーニングは同じように実行します
実行結果は以下のようになりました
%EXGBoost.Booster{
ref: #Reference<0.355326591.2837577729.195022>,
best_iteration: 4205,
best_score: 0.26485201133872904
}
今回は途中で早期終了が発生しています
前処理改善時の推論結果
Kaggle に提出した結果、精度は 0.75119 になりました
同じ前処理で CNN を使ったときは 0.78947 だったので、こちらも CNN に軍配が上がりました
まとめ
コールバック関数を使うことで、 CNN のときと同様にグラフ出力することができました
また、早期終了も設定できるので、過学習対策も可能です
今回のデータ、前処理、設定では CNN の方が XGBoost よりも優れていましたが、場合によって変わると思うので、他にも様々なデータで使ってみたいと思います