5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Elixirで機械学習に初挑戦: Explorerでデータ処理 (Part 2)

Last updated at Posted at 2023-01-04

この記事を読む前に、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])

ちょっと待って下さい!

modeltrained_stateがまだ作成していませんよ!

あっ、記事の「本講義回の最終的なコード」の「*ⅱ)モデルの学習」にあります。でも、何も説明がありません。

じゃ、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ができました

image.png

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」ボタンをクリックして、アップロードします

オリジナル記事でこのファイルを提出する方法が書いてあります。

リンク: Kaggleに提出(submit)してみる

まとめ

いろいろ勉強になりました。Axonのことだけではなくて、いろんな処理をするためにExplorerを今まで使ったことない関数をいっぱい使いました。

@piacerexさんに特別「ありがとうございました」と言いたいです。機械学習のシリーズの記事で使えるの自信が増えています。これからのデータ分析の新しいツールになってます。

では、これからも、よろしくお願いします。

5
1
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
5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?