この記事を読む前に、Part 1を読んでください。
今まで
今までのLivebookが3つの部分に分けています:
Notebook dependencies and setup
Mix.install([
{:exla, "~> 0.4"},
{:axon, "~> 0.3"},
{:explorer, "~> 0.4"},
{:kino, "~> 0.6"}
])
準備モジュール
alias Explorer.DataFrame, as: DF
alias Explorer.Series, as: S
require Explorer.DataFrame # For DF.mutate
defmodule Pre do
def count_missings(df) do
DF.names(df)
|> Enum.reduce(%{}, fn name, acc ->
count = DF.pull(df, name)
|> S.is_nil() |> S.sum()
if (count > 0), do: Map.put(acc, name, count), else: acc
end)
end
def separate(df, id, label) do
{
DF.pull(df, id) |> S.to_list(),
DF.pull(df, label)
|> S.cast(:float)
|> S.to_list
|> Enum.map(& [[&1]] |> Nx.tensor),
df |> DF.discard([id, label]) |> DF.to_rows
}
end
def drop(df, keys) do
keys
|> Enum.reduce(df, & DF.discard(&2, &1))
end
def make_dummies(df, key) do
DF.pull(df, key)
|>S.to_list
|> Enum.uniq
|> Enum.with_index(& {&1, String.to_integer("#{&2}")})
|> Enum.into(%{})
end
def to_dummies(df, keys) do
keys
|> Enum.reduce(df, fn key, acc ->
map = make_dummies(acc, key)
series = DF.pull(acc, key)
new_series = S.transform(series, & Map.get(map, &1))
DF.put(acc, key, new_series)
end)
end
def map_to_tensor(datas) do
datas
|> Enum.map(& [Map.values(&1)] |> Nx.tensor)
end
end
メイン・プログラム
train_df =
DF.from_csv!("data/titanic/train.csv")
|> DF.rename_with(& String.downcase(&1))
|> Pre.drop(~w(cabin name ticket))
|> DF.mutate(embarked: fill_missing(embarked, "S"))
|> DF.mutate(age: fill_missing(age, 0.0))
|> Pre.to_dummies(["embarked", "sex"])
{trains_csv_ids, train_csv_labels, train_csv_maps} =
train_df
|> Pre.separate("passengerid", "survived")
train_csv_datas = train_csv_maps
|> Pre.map_to_tensor
これまでできたら、@piacerexさんの「Eixirで機械学習に初挑戦④:データ処理に強いElixirでKaggle挑戦(前半)…「データ前処理」の基礎編 ※最新Livebook 0.8に対応」記事を「ⅳ)未知データに対する予測」から続きましょう。
知データに対する予測
①未知データのロードと学習データの列差異の確認
未知データをロードし、学習データとの列差異を確認します
- {test_csv_header, _} = Pre.header_and_csv_datas("test.csv")
- train_csv_header -- test_csv_header
+ test_df =
+ DF.from_csv!("data/titanic/test.csv")
+ |> DF.rename_with(& String.downcase(&1))
+
+ {test_csv_ids, _, test_csv_maps} =
+ test_df
+ |> Pre.separate("passengerid", nil)
メイン・プログラムに続いて、テストデータを読み込んで、ヘッダーを小文字に変換するとseparate
の処理しましょう。
survivedが存在しません … これは、未知データにはラベルが無いのが当たり前だからです
test_df
|> DF.names()
["passengerid", "pclass", "sex", "age", "sibsp", "parch", "fare", "embarked"]
これに対応するため、Pre.separateを以下のように改修します
defmodule Pre do
…
+ def separate(df, id, nil) do
+ {
+ DF.pull(df, id) |> S.to_list(),
+ nil,
+ df |> DF.discard(id) |> DF.to_rows
+ }
+ end
def separate(df, id, label) do
{
DF.pull(df, id) |> S.to_list(),
DF.pull(df, label)
|> S.cast(:float)
|> S.to_list
|> Enum.map(& [[&1]] |> Nx.tensor),
df |> DF.discard([id, label]) |> DF.to_rows
}
end
…
ラベルが無いCSVファイルのときは、ラベルとして
nil
を返せれるようになりました
label
があるかないかをチェックするより、ラベルがnil
と渡して、nil
がそのTupleの位置に返せれます。
{[892, 893, 894, 895, 896, 897, 898, 899, 900, 901, 902, 903, 904, 905, 906, 907, 908, 909, 910,
911, 912, 913, 914, 915, 916, 917, 918, 919, 920, 921, 922, 923, 924, 925, 926, 927, 928, 929,
930, 931, 932, 933, 934, 935, 936, 937, 938, 939, 940, ...], nil,
[
%{"age" => 34.5, "cabin" => nil, "embarked" => "Q", "fare" => 7.8292, "name" => "Kelly, Mr. James", "parch" => 0, "pclass" => 3, "sex" => "male", "sibsp" => 0, "ticket" => "330911"},
%{"age" => 21.0, "cabin" => nil, "embarked" => "S", "fare" => 8.6625, "name" => "Cacic, Miss. Manda", "parch" => 0, "pclass" => 3, "sex" => "female", "sibsp" => 0, ...},
%{"age" => 25.0, "cabin" => nil, "embarked" => "S", "fare" => 9.5, "name" => "Sap, Mr. Julius", "parch" => 0, "pclass" => 3, "sex" => "male", ...},
%{"age" => nil, "cabin" => nil, "embarked" => "S", "fare" => 56.4958, "name" => "Hee, Mr. Ling", "parch" => 0, "pclass" => 3, ...},
%{"age" => 39.0, "cabin" => nil, "embarked" => "C", "fare" => 13.4167, "name" => "Karun, Mr. Franz", "parch" => 1, ...},
%{"age" => nil, "cabin" => "D34", "embarked" => "S", "fare" => 26.55, "name" => "Franklin, Mr. Thomas Parham", ...},
%{"age" => 41.0, "cabin" => nil, "embarked" => "S", "fare" => 7.85, ...},
%{"age" => 30.0, "cabin" => nil, "embarked" => "S", ...},
%{"age" => 45.0, "cabin" => "D19", ...},
%{"age" => 25.0, ...},
%{...},
...
]}
②空白値の確認
未知データに学習データと異なる欠損値が無いかチェックします
- test_csv_maps
+ test_df
|> Pre.count_missings
すると、普通にありましたw … fareの欠損値補完は、現在の「データ前処理」に含まれていないため、改修が必要です
%{"age" => 86, "cabin" => 327, "fare" => 1}
ここではいったん、fareは「0」で補完します
test_df =
DF.from_csv!("data/titanic/test.csv")
|> DF.rename_with(& String.downcase(&1))
+ |> Pre.drop(~w(cabin name ticket))
+ |> DF.mutate(embarked: fill_missing(embarked, "S"))
+ |> DF.mutate(age: fill_missing(age, 0.0))
+ |> DF.mutate(fare: fill_missing(fare, 0.0))
{test_csv_ids, _, test_csv_maps} =
test_df
|> Pre.separate("passengerid", nil)
fareが「0」で補完されたことを確認できました
DF.filter(test_df, fare == 0.0)
#Explorer.DataFrame<
Polars[3 x 8]
passengerid integer [1044, 1158, 1264]
pclass integer [3, 1, 1]
sex string ["male", "male", "male"]
age float [60.5, 0.0, 49.0]
sibsp integer [0, 0, 0]
parch integer [0, 0, 0]
fare float [0.0, 0.0, 0.0]
embarked string ["S", "S", "S"]
>
③未知データに対する予測の実施
未知データの「データ前処理」も済んだので、予測を実施してみます
ここで、Pre.processの第二引数に、学習データであるtrain_csv_mapsを指定することがポイントで、これは前述した「to_dummiesの実施時、検証データや未知データではカテゴリ値が網羅されていないケースへの対策として、学習データからカテゴリ値を拾うため」となります
test_df =
DF.from_csv!("data/titanic/test.csv")
|> DF.rename_with(& String.downcase(&1))
|> Pre.drop(~w(cabin name ticket))
|> DF.mutate(embarked: fill_missing(embarked, "S"))
|> DF.mutate(age: fill_missing(age, 0.0))
|> DF.mutate(fare: fill_missing(fare, 0.0))
+ |> Pre.to_dummies(["embarked", "sex"])
{test_csv_ids, _, test_csv_maps} =
test_df
|> Pre.separate("passengerid", nil)
+ test_csv_datas = test_csv_maps
+ |> Pre.map_to_tensor
また、test_csv_idsを予測結果リストとEnum.zipで連結し、ヘッダーも付けて、提出用CSVを作成します
{test_csv_ids, _, test_csv_maps} = Pre.csv_file_to_datas("test.csv")
result = Pre.process(test_csv_maps, train_csv_maps)
|> Enum.map(& Axon.predict(model, trained_state, &1)
|> Nx.to_flat_list |> List.first |> round)
|> then(& Enum.zip(test_csv_ids, &1))
|> Enum.map(& [elem(&1, 0), Integer.to_string(elem(&1, 1))])
|> then(& [["PassengerId", "Survived"] | &1])
じゃ、Livebookに新しい「モデルの学習」というセクションをメイン・プログラムの下に作って、以下のコードを入力してください。
train_datas = Enum.zip(train_csv_datas, train_csv_labels)
model =
Axon.input("input", shape: {nil, 7})
|> Axon.dense(48, activation: :tanh)
|> Axon.dropout(rate: 0.2)
|> Axon.dense(48, activation: :tanh)
|> Axon.dense(1, activation: :sigmoid)
trained_state =
model
|> Axon.Loop.trainer(:mean_squared_error, Axon.Optimizers.adam(0.0005))
|> Axon.Loop.metric(:accuracy, "Accuracy")
|> Axon.Loop.run(train_datas, %{}, epochs: 20, compiler: EXLA)
それからresults
を計算しましょう:
- result = Pre.process(test_csv_maps, train_csv_maps)
+ survived = test_csv_datas
|> Enum.map(& Axon.predict(model, trained_state, &1)
|> Nx.to_flat_list |> List.first |> round)
- |> then(& Enum.zip(test_csv_ids, &1))
- |> Enum.map(& [elem(&1, 0), Integer.to_string(elem(&1, 1))])
- |> then(& [["PassengerId", "Survived"] | &1])
+ passenger_ids = DF.pull(test_df, "passengerid") |> S.to_list()
+ result = DF.new(PassengerId: passenger_ids, Survived: survived)
+
+ Kino.DataTable.new(result)
提出用CSVができました
Kaggleに提出(submit)してみる
Kaggleへの提出用CSVファイルをresult.csvで生成します
result
- |> CSV.encode
- |> Enum.to_list
- |> then(& File.write("result.csv", &1))
+ |> DF.to_csv!("data/titanic/result.csv")
result
がデータフレームに入れたので、簡単にCSVファイルを出力することができます。
result.csvをKaggleの「Submit Predictions」ボタンをクリックして、アップロードします
オリジナル記事でこのファイルを提出する方法が書いてあります。
まとめ
いろいろ勉強になりました。Axonのことだけではなくて、いろんな処理をするためにExplorerを今まで使ったことない関数をいっぱい使いました。
@piacerexさんに特別「ありがとうございました」と言いたいです。機械学習のシリーズの記事で使えるの自信が増えています。これからのデータ分析の新しいツールになってます。
では、これからも、よろしくお願いします。