LoginSignup
6
1

More than 1 year has passed since last update.

「Elixirで機械学習に初挑戦」をやってみた(中編)

Last updated at Posted at 2023-03-21

はじめに

@piacerex さんの「Elixirで機械学習に初挑戦」シリーズを自分なりにやってみるシリーズです

  • 前編:

  • 後編:

今回も Livebook を使います

Elixirで機械学習に初挑戦④:データ処理に強いElixirでKaggle挑戦(前半)…「データ前処理」の基礎編

今回は Kaggle のタイタニックデータを使って、乗客の生き残りを学習・予測します

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

アカウントの作成

Kaggle のアカウントを作成します

トップページの右上「Register」をクリックし、指示に従っていけば簡単に作成できます

データのダウンロード

以下のページ右下「Download All」をクリックし、 taitanic.zip をダウンロードします

tainanic.zip を展開して出てくる以下の CSV ファイルを使用します

  • train.csv: 学習データ
  • test.csv: テストデータ

セットアップ

Livebook で新しいノートブックを開き、以下のコードを実行します

Mix.install([
  {:csv, "~> 3.0"},
  {:exla, "~> 0.5"},
  {:axon, "~> 0.5"},
  {:kino, "~> 0.9"},
  {:kino_vega_lite, "~> 0.1"}
])

データの選択

Kino.Input.file を使ってファイル選択の UI を作ります

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

ファイル選択で「train.csv」を選択し、アップロードします

同じく、「test.csv」をアップロードします

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

前処理

@piacerex さんは List of Map の形で処理していたので、私は Map of List の形で処理していきます

また、ファイル入出力には Kino の力を活用します

CSV の読込

アップロードした学習データを読み込みます

先頭行はヘッダー、それ以外はデータ行です

[header_org | rows_org] =
  train_data_input
  |> Kino.Input.read()
  |> Map.get(:file_ref)
  |> Kino.Input.file_path()
  |> File.stream!()
  |> CSV.decode!()
  |> Enum.to_list()

2023/04/02

Kino の更新により、ファイル読込の処理が変わったので更新しました

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

[
  ["PassengerId", "Survived", "Pclass", "Name", "Sex", "Age", "SibSp", "Parch", "Ticket", "Fare",
   "Cabin", "Embarked"],
  ["1", "0", "3", "Braund, Mr. Owen Harris", "male", "22", "1", "0", "A/5 21171", "7.25", "", "S"],
  ["2", "1", "1", "Cumings, Mrs. John Bradley (Florence Briggs Thayer)", "female", "38", "1", "0",
   "PC 17599", "71.2833", "C85", "C"],
  ["3", "1", "3", "Heikkinen, Miss. Laina", "female", "26", "0", "0", "STON/O2. 3101282", "7.925",
   "", "S"],
  ...
]

ヘッダーを Atom に変換します

header =
  header_org
  |> Enum.map(fn column_name ->
    column_name
    |> String.downcase()
    |> String.to_atom()
  end)

結果は以下の通り

[:passengerid, :survived, :pclass, :name, :sex, :age, :sibsp, :parch, :ticket, :fare, :cabin,
 :embarked]
train_data =
  header
  |> Enum.with_index()
  |> Enum.into(%{}, fn {key, index} ->
    {
      key,
      Enum.map(rows_org, &Enum.at(&1, index))
    }
  end)

各列毎に List 化します

以下のような形式になります

%{
  age: ["22", "38", "26", "35", "35", "", "54", "2", "27", "14", "4", "58", "20", "39", "14", "55",
   "2", "", "31", "", "35", "34", "15", "28", "8", "38", "", "19", "", "", "40", "", "", "66", "28",
   "42", "", "21", "18", "14", "40", "27", "", "3", "19", "", "", "", "", ...],
  cabin: ["", "C85", "", "C123", "", "", "E46", "", "", "", "G6", "C103", "", "", "", "", "", "",
   "", "", "", "D56", "", "A6", "", "", "", "C23 C25 C27", "", "", "", "B78", "", "", "", "", "",
   "", "", "", "", "", "", "", "", "", "", "", ...],
  embarked: ["S", "C", "S", "S", "S", "Q", "S", "S", "S", "C", "S", "S", "S", "S", "S", "S", "Q",
   "S", "S", "C", "S", "S", "Q", "S", "S", "S", "C", "S", "Q", "S", "C", "C", "Q", "S", "C", "S",
   "C", "S", "S", "C", "S", "S", "C", "C", "Q", "S", "Q", ...],
  ...
}

テーブル表示してみましょう

Kino.DataTable.new(train_data)

スクリーンショット 2023-03-21 21.32.29.png

タイタニック号の乗客毎に以下の項目を持ったデータになっています

  • age: 年齢
  • cabin: 客室番号
  • embarked: 搭乗港(C: シェルブール, Q: クイーンズタウン, S: サウサンプトン)、
  • fare: 乗船料金
  • name: 氏名
  • parch: 一緒に乗っていた親・子どもの数
  • passengerid: 乗客ID
  • pclass: チケット階級(1: 一等級、2: 二等級、3: 三等級)
  • sex: 性別
  • sibsp: 一緒に乗っていた兄弟・姉妹・配偶者の数
  • survided: 生き残ったか(0: いいえ、1: はい)
  • ticket: チケット番号

ID 一覧取得

passengerid が乗客を識別するための ID なので、これを ID 一覧として取得します

id_list = train_data.passengerid

結果は以下の通り

["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18",
 "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30", "31", "32", "33", "34",
 "35", "36", "37", "38", "39", "40", "41", "42", "43", "44", "45", "46", "47", "48", "49", "50",
 ...]

ラベル一覧取得

survived が学習・推論する対象なので、これをラベル一覧として取得します

学習時、 Float 型のテンソルにしたいので Nx.tensor(type: :f32) で変換します

label_tensor =
  train_data.survived
  |> Enum.map(fn datum ->
    datum
    |> String.to_integer()
    |> Nx.tensor(type: :f32)
    |> Nx.new_axis(-1)
    |> Nx.new_axis(-1)
  end)

結果は以下の通り

[
  #Nx.Tensor<
    f32[1][1]
    [
      [0.0]
    ]
  >,
  #Nx.Tensor<
    f32[1][1]
    [
      [1.0]
    ]
  >,
  ...
]

入力データの取得

推論時の入力(予測の元にする)データを取得します

対象は passengerid と survived 以外です(ひとまず)

inputs = Map.drop(train_data, [:passengerid, :survived])

不要な項目の排除

生き残ったかどうかにあまり関係しなさそうな項目を排除します

dropped_inputs = Map.drop(inputs, [:cabin, :name, :ticket])

欠損値処理

テーブルを見ても分かりますが、いくつかの項目が空になっているデータがあります

例えば年齢が空になっているデータの件数を取得してみます

train_data.age
|> Enum.filter(& &1=="")
|> Enum.count()

結果は 177 です

これを全項目について行い、空(欠損値)を持つ項目を洗い出します

count_missings = fn data ->
  data
  |> Enum.into(%{}, fn {key, list} ->
    {
      key,
      list
      |> Enum.filter(& &1=="")
      |> Enum.count()
    }
  end)
  |> Enum.filter(fn {_, count} -> count > 0 end)
end
count_missings.(train_data)

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

[age: 177, cabin: 687, embarked: 2]

つまり年齢、客室番号、搭乗港には欠損値がある、ということです

これらの項目について、空のとき別の値で埋めます(cabin は既に排除しているので何もしません)

  • age: 空のとき「0」
  • embarked: 空のとき「S」
filled_inputs =
  dropped_inputs
  |> Map.put(
    :age,
    dropped_inputs.age
    |> Enum.map(fn
      "" -> "0"
      others -> others
    end)
  )
  |> Map.put(
    :embarked,
    dropped_inputs.embarked
    |> Enum.map(fn
      "" -> "S"
      others -> others
    end)
  )

Kino.DataTable.new(filled_inputs)

スクリーンショット 2023-03-21 22.01.28.png

数値化

搭乗港や性別は文字列なので、これを数値に変換します

まず、性別を一意の数値に変換するためのマップを用意します

sex_map =
  filled_inputs.sex
  |> Enum.uniq()
  |> Enum.sort()
  |> Enum.with_index()
  |> Enum.into(%{})

結果はこうなります

%{"female" => 1, "male" => 0}

また、搭乗港の場合も同様にします

embarked_map =
  filled_inputs.embarked
  |> Enum.uniq()
  |> Enum.sort()
  |> Enum.with_index()
  |> Enum.into(%{})

搭乗港の変換マップは以下の通りになります

%{"C" => 0, "Q" => 1, "S" => 2}

準備した変換マップを使って、各項目を文字列から数値に変換します

categorised_inputs =
  filled_inputs
  |> Map.put(
    :sex,
    filled_inputs.sex
    |> Enum.map(&sex_map[&1])
  )
  |> Map.put(
    :embarked,
    filled_inputs.embarked
    |> Enum.map(&embarked_map[&1])
  )

その他の項目も数字から数値に変換します

parsed_inputs =
  [:age, :fare, :parch, :pclass, :sibsp]
  |> Enum.reduce(categorised_inputs, fn column_name, acc ->
    acc
    |> Map.put(
      column_name,
      acc
      |> Map.get(column_name)
      |> Enum.map(fn value ->
        value
        |> Float.parse()
        |> elem(0)
      end)
    )
  end)

Kino.DataTable.new(parsed_inputs)

スクリーンショット 2023-03-21 22.13.37.png

これで全データの全項目が欠損のない数値になりました

テンソル化

各列の値をテンソルにした後、行単位のデータにするため転置(transpose)してから List に分割します

input_tensor_list =
  parsed_inputs
  |> Map.values()
  |> Nx.tensor()
  |> Nx.transpose()
  |> Nx.to_batched(1)
  |> Enum.to_list()

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

[
  #Nx.Tensor<
    f32[1][7]
    [
      [22.0, 0.0, 7.25, 0.0, 3.0, 0.0, 1.0]
    ]
  >,
  #Nx.Tensor<
    f32[1][7]
    [
      [38.0, 1.0, 71.2833023071289, 0.0, 1.0, 1.0, 1.0]
    ]
  >,
  ...
]

モジュール化

これまでの一連の操作を繰り返し実行できるようにモジュール化します

テストデータの場合は生き残ったかの正解を持っていないため、ラベルは nil が返るようにしています

defmodule PreProcess do
  def load_csv(kino_input) do
    [header_org | rows_org] =
      kino_input
      |> Kino.Input.read()
      |> Map.get(:file_ref)
      |> Kino.Input.file_path()
      |> File.stream!()
      |> CSV.decode!()
      |> Enum.to_list()

    header =
      header_org
      |> Enum.map(fn column_name ->
        column_name
        |> String.downcase()
        |> String.to_atom()
      end)

    header
    |> Enum.with_index()
    |> Enum.into(%{}, fn {key, index} ->
      {
        key,
        Enum.map(rows_org, &Enum.at(&1, index))
      }
    end)
  end

  def fill_empty(data, fill_map) do
    fill_map
    |> Enum.reduce(data, fn {column_name, fill_value}, acc ->
      acc
      |> Map.put(
        column_name,
        data
        |> Map.get(column_name)
        |> Enum.map(fn
          "" -> fill_value
          others -> others
        end)
      )
    end)
  end

  def to_number(data, columns_names) do
    columns_names
    |> Enum.reduce(data, fn column_name, acc ->
      conversion_map =
        data
        |> Map.get(column_name)
        |> Enum.uniq()
        |> Enum.sort()
        |> Enum.with_index()
        |> Enum.into(%{})

      acc
      |> Map.put(
        column_name,
        data
        |> Map.get(column_name)
        |> Enum.map(&conversion_map[&1])
      )
    end)
  end

  def to_tensor(data, columns_names) do
    columns_names
    |> Enum.reduce(data, fn column_name, acc ->
      acc
      |> Map.put(
        column_name,
        acc
        |> Map.get(column_name)
        |> Enum.map(fn value ->
          value
          |> Float.parse()
          |> elem(0)
        end)
      )
    end)
    |> Map.values()
    |> Nx.tensor()
    |> Nx.transpose()
    |> Nx.to_batched(1)
    |> Enum.to_list()
  end

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

    id_list = Map.get(data_org, id_key)

    label_list =
      if Map.has_key?(data_org, label_key) do
        data_org
        |> Map.get(label_key)
        |> Enum.map(fn datum ->
          datum
          |> String.to_integer()
          |> Nx.tensor(type: :f32)
          |> Nx.new_axis(-1)
          |> Nx.new_axis(-1)
        end)
      else
        nil
      end

    inputs =
      data_org
      |> Map.drop([id_key, label_key, :cabin, :name, :ticket])
      |> fill_empty(%{age: "0", embarked: "S", fare: "0"})
      |> to_number([:sex, :embarked])
      |> to_tensor([:age, :fare, :parch, :pclass, :sibsp])

    {id_list, label_list, inputs}
  end
end

改めて学習データをID、ラベル、入力値に変換します

{
  train_id_list,
  train_label_list,
  train_inputs
} = PreProcess.process(train_data_input, :passengerid, :survived)

また、テスト用データも同様に読み込んでおきます

{
  test_id_list,
  test_label_list,
  test_inputs
} = PreProcess.process(test_data_input, :passengerid, :survived)

モデルの定義

学習、推論するためのモデルを準備します

入力の項目すうが 7 なので、入力層の形を {nil, 7} にしています

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)

学習

学習データとして入力値とラベルのセットを作ります

train_data = Enum.zip(train_inputs, train_label_list)

進捗状況を見るためのグラフを準備します

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

acc_plot =
  VegaLite.new(width: 300)
  |> VegaLite.mark(:line)
  |> VegaLite.encode_field(:x, "step", type: :quantitative)
  |> VegaLite.encode_field(:y, "accuracy", type: :quantitative)
  |> Kino.VegaLite.new()

Kino.Layout.grid([loss_plot, acc_plot], columns: 2)

学習を実行します(エポック数は多めに 50 にします)

trained_state =
  model
  |> Axon.Loop.trainer(:mean_squared_error, Axon.Optimizers.adam(0.0005))
  |> Axon.Loop.metric(:accuracy, "accuracy")
  |> Axon.Loop.kino_vega_lite_plot(loss_plot, "loss", event: :epoch_completed)
  |> Axon.Loop.kino_vega_lite_plot(acc_plot, "accuracy", event: :epoch_completed)
  |> Axon.Loop.run(train_data, %{}, epochs: 50, compiler: EXLA)

学習を実行すると、 loss が下がって accuracy が上がる様子が見えます

train.gif

最終的には以下のようなグラフになります

titanic_loss.png

titanic_acc.png

loss も accuracy も傾きが小さくなり、これ以上学習してもあまり改善が見込めないような状態になっているのが分かります

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

Axon.predict でテストデータの最初の一件だけ推論してみます

test_inputs
|> List.first()
|> then(&Axon.predict(model, trained_state, &1))

結果は以下のようにテンソルで返ってきます

#Nx.Tensor<
  f32[1][1]
  EXLA.Backend<host:0, 0.3107249500.3383099400.171576>
  [
    [0.10238001495599747]
  ]
>

0.5 より小さいので、この乗客は生き残っていない、と予測したようです

全件に対して実行してみます

results =
  test_inputs
  |> Nx.concatenate()
  |> then(&Axon.predict(model, trained_state, &1))
  |> Nx.to_flat_list()
  |> Enum.map(&round(&1))

結果は以下のように 0 と 1 の List になります

[0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0,
 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, ...]

推論結果のダウンロード

推論結果を ID と合わせて CSV に変換します

results_csv =
  results
  |> then(&[test_id_list, &1])
  |> Enum.zip()
  |> Enum.map(fn {id, label} ->
    [id, "#{label}"]
  end)
  |> then(& [["PassengerId", "Survived"] | &1])
  |> CSV.encode()
  |> Enum.to_list()
  |> Enum.join()

CSV ファイルをダウンロードします

Kino.Download.new(fn -> results_csv end, filename: "result.csv")

dl_button.png

推論結果の Kaggle へのアップロード

Kaggle のタイタニックデータのページから「Submit Predictions」のボタンをクリックし、推論結果の CSV ファイルをアップロードします

スクリーンショット 2023-03-21 22.46.00.png

一覧にアップロードしたファイルが追加され、右端に正解率が表示されます

「Leaderboard」タブをクリックすると、順位表が表示されます

スクリーンショット 2023-03-21 22.48.20.png

正解率は 77.751 % でした

「Jump to your leaderboard position」で自分の順位が確認できます

スクリーンショット 2023-03-21 22.48.29.png

今回の順位は 3,969 位でした

Explorer を使った場合

ここまでの操作をより簡単にするため、 Explorer を使ってみます

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

Explorer を使う場合のセットアップ

セットアップ時に CSV ではなく Explorer をインストールします

Mix.install([
  {:exla, "~> 0.5"},
  {:axon, "~> 0.5"},
  {:kino, "~> 0.9"},
  {:kino_vega_lite, "~> 0.1"},
  {:explorer, "~> 0.5"}
])

エイリアス等を準備します

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")

Explorer による CSV 読込

Explorer では、 DataFrame.from_csv だけで簡単に 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)

スクリーンショット 2023-03-21 22.54.10.png

Explorer による項目抽出

ID 一覧の取得はかなり単純です

id_list = Series.to_list(train_data["PassengerId"])

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

[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27,
 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, ...]

ラベルはテンソルの配列にするため少し長くなります

label_tensor =
  train_data["Survived"]
  |> Series.to_tensor(backend: EXLA.Backend)
  |> Nx.as_type(:f32)
  |> Nx.new_axis(1)
  |> Nx.to_batched(1)
  |> Enum.to_list()

DataFrame.discard で不要な列を排除します

inputs = DataFrame.discard(train_data, ["PassengerId", "Survived"])

Kino.DataTable.new(inputs)
dropped_inputs = DataFrame.discard(inputs, ["Cabin", "Name", "Ticket"])

Kino.DataTable.new(dropped_inputs)

Explorer による欠損値補完

Series.fill_missing を使って欠損値を補完します

年齢について 0 歳で補完していたのは乱暴だったので、平均値で補完するようにしています

filled_inputs =
  dropped_inputs
  |> DataFrame.put("Age", Series.fill_missing(train_data["Age"], :mean))
  |> DataFrame.put("Embarked", Series.fill_missing(train_data["Embarked"], "S"))

Kino.DataTable.new(filled_inputs)

Explorer によるワンホットエンコーディング

性別と搭乗港について、 DataFrame.dummies でワンホット(One-Hot)エンコーディングします

dummied_inputs =
  filled_inputs
  |> DataFrame.dummies(["Sex", "Embarked"])
  |> DataFrame.concat_columns(DataFrame.discard(filled_inputs, ["Sex", "Embarked"]))

Kino.DataTable.new(dummied_inputs)

スクリーンショット 2023-03-21 23.01.55.png

ワンホットエンコーディングにより、列が値の種類の分だけ増殖しています

  • Sex
    • Sex-male
    • Sex-female
  • Embarked
    • Embarked_C
    • Embarked_Q
    • Embarked_S

それぞれ、各値に該当する場合は 1 、そうでない場合は 0 になります

性別、搭乗港については値の大小に意味がない(意味を持たせるべきではない)ため、値毎に別の項目として扱うためです

データフレームからのテンソル化

最後にテンソル化します

input_tensor_list =
  dummied_inputs
  |> DataFrame.to_columns()
  |> Map.values()
  |> Nx.tensor(backend: EXLA.Backend)
  |> Nx.transpose()
  |> Nx.to_batched(1)
  |> Enum.to_list()

Explorer による前処理のモジュール化

ここまでの一連の処理をモジュール化します

  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 ->
      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(backend: EXLA.Backend)
    |> Nx.transpose()
    |> Nx.to_batched(1)
    |> Enum.to_list()
  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)

    label_list =
      if has_label_key do
        data_org[label_key]
        |> Series.to_tensor(backend: EXLA.Backend)
        |> Nx.as_type(:f32)
        |> Nx.new_axis(1)
        |> Nx.to_batched(1)
        |> Enum.to_list()
      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.discard(["Cabin", "Name", "Ticket"])
      |> fill_empty(%{"Age" => :mean, "Embarked" => "S", "Fare" => :mean})
      |> replace_dummy(["Sex", "Embarked"])
      |> to_tensor()

    {id_list, label_list, inputs}
  end
end

改めて学習データをID、ラベル、入力値に変換します

{
  train_id_list,
  train_label_list,
  train_inputs
} = PreProcess.process(train_data_input, "PassengerId", "Survived")

また、テスト用データも同様に読み込んでおきます

{
  test_id_list,
  test_label_list,
  test_inputs
} = PreProcess.process(test_data_input, "PassengerId", "Survived")

Explorer を使った場合の学習

モデルを定義します

ワンホットエンコーディングにより列が増えたため、入力層の形が {nil, 10} になります

model =
  Axon.input("input", shape: {nil, 10})
  |> Axon.dense(48, activation: :tanh)
  |> Axon.dropout(rate: 0.2)
  |> Axon.dense(48, activation: :tanh)
  |> Axon.dense(1, activation: :sigmoid)

学習データとして入力値とラベルのセットを作ります

train_data = Enum.zip(train_inputs, train_label_list)

進捗状況を見るためのグラフを準備します

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

acc_plot =
  VegaLite.new(width: 300)
  |> VegaLite.mark(:line)
  |> VegaLite.encode_field(:x, "step", type: :quantitative)
  |> VegaLite.encode_field(:y, "accuracy", type: :quantitative)
  |> Kino.VegaLite.new()

Kino.Layout.grid([loss_plot, acc_plot], columns: 2)

学習を実行します(Explorer を使わない場合と同じです)

trained_state =
  model
  |> Axon.Loop.trainer(:mean_squared_error, Axon.Optimizers.adam(0.0005))
  |> Axon.Loop.metric(:accuracy, "accuracy")
  |> Axon.Loop.kino_vega_lite_plot(loss_plot, "loss", event: :epoch_completed)
  |> Axon.Loop.kino_vega_lite_plot(acc_plot, "accuracy", event: :epoch_completed)
  |> Axon.Loop.run(train_data, %{}, epochs: 50, compiler: EXLA)

学習時の推移は以下のようになります

titanic_loss_ex.png

titanic_acc_ex.png

ワンホットエンコードや欠損値の平均による補完のため、 loss が低く、 accuracy が高くなっています

Explorer を使った場合の推論

推論結果をデータフレームに格納してテーブル表示します

results =
  test_inputs
  |> Nx.concatenate()
  |> then(&Axon.predict(model, trained_state, &1))
  |> Nx.to_flat_list()
  |> Enum.map(&round(&1))
  |> then(&%{
    "PassengerId" => test_id_list,
    "Survived" => &1
  })
  |> DataFrame.new()

Kino.DataTable.new(results)

スクリーンショット 2023-03-21 23.21.36.png

Explorer による推論結果の CSV 出力

データフレームを CSV 形式で出力し、ダウンロードします

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

Explorer を使った場合の正解率

Explorer を使った場合の正解率は 77.511 でした

残念ながら、Explorer を使わない場合より少し低くなっていますが、この差はテストデータ 1 件分なので、ほとんど誤差と言えるでしょう

まとめ

Kaggle のタイタニックデータを使って、それなりの精度のモデルが構築できました

Explorer を使ったからと言って精度に改善は見られませんでしたが、コードの可読性は著しく上がったと思います

次回、更にデータ分析によって前処理を改善しました

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