教師あり学習(ORの予測)
Mix.install([
  {:nx, "~> 0.4"},
  {:axon, "~> 0.3"},
  {:exla, "~> 0.4"},
  {:table_rex, "~> 3.1"},
  {:kino_vega_lite, "~> 0.1.7"}
])
概要
- 教師あり学習(ORの予測)
 - 二値分類
 - 非線形分類によって二値分類が実現していることを理解する
 - 学習データの可視化
 - 学習過程のアニメーション化
 - 予測の可視化
 - 学習回数や学習率による精度の変化
 - 学習グラフの読み方
 
資料
- Eixirで機械学習に初挑戦①:基礎知識とLivebook+Nx+Axonによる機械学習入門 by piacerex
 - Eixirで機械学習に初挑戦②:機械学習コードの解説と「学習データの可視化」「学習過程のアニメ化」 by piacerex
 - Eixirで機械学習に初挑戦③:「予測」の可視化と「精度」に変化を与える要因、「学習過程グラフ」の読み方 ※最新Livebook 0.8に対応 by piacerex
 - Elixir生誕10周年祭■第3弾:Elixir/Livebook+NxでPythonっぽくAI・ML by piacerex
 
Nxの練習
行列をNx.tensorで作り、各値を3で除算するというNxのコード
Nx.tensor([[1, 2], [3, 4]])
|> Nx.divide(3)
Nx.tensor(for _ <- 1..32, do: [Enum.random(0..1)])
教師あり学習を実践:ORの予測
- ビット演算などで使う「OR」を教師あり学習させて予測させる
 - ラベルは、入力1と入力2のOR演算を行えば計算できるため、教師あり学習のためのラベルの準備を手作業で行わなくて済む
 
ORのビット演算
| input1 | input2 | output | 
|---|---|---|
| 0 | 0 | 0 | 
| 0 | 1 | 1 | 
| 1 | 0 | 1 | 
| 1 | 1 | 1 | 
  
ⅰ)学習/テストデータとラベルの準備
準備するもの
- 学習に使うデータ
 - テストに使うデータ
 - データが入力されたときに期待する正解である「ラベル」
 
ラベル生成
- 通常は、機械的なラベル生成はできないため、人が学習/テストデータを見て、ラベルを付与
 
## 学習データとラベルのセットを1000件生成
train_datas =
  Stream.repeatedly(fn ->
    input1 = Nx.tensor(for _ <- 1..32, do: [Enum.random(0..1)])
    input2 = Nx.tensor(for _ <- 1..32, do: [Enum.random(0..1)])
    data = %{"input1" => input1, "input2" => input2}
    label = Nx.logical_or(input1, input2)
    # 学習データとラベルをセットにする
    {data, label}
  end)
  |> Enum.take(1000)
学習データ1セットの可視化
- 「Smart」Cellで気軽にグラフを描画できる
 - 「Smart」Cellはコード化することができる
 
## グラフ描画可能なデータを準備
# 4点が全て重なると、どれだけ生成されたかが分かりにくいので、各値を適度に揺らす
gen_random = fn -> Enum.random(0..1) + Enum.random(0..5) / 100 end
input1 = Nx.tensor(for _ <- 1..32, do: [gen_random.()])
input2 = Nx.tensor(for _ <- 1..32, do: [gen_random.()])
datas =
  Enum.zip([Nx.to_flat_list(input1), Nx.to_flat_list(input2)])
  |> Enum.map(fn {input1, input2} ->
    %{input1: input1, input2: input2}
  end)
## グラフを描画
VegaLite.new(width: 600, height: 400)
|> VegaLite.data_from_values(datas, only: ["input1", "input2"])
|> VegaLite.mark(:point)
|> VegaLite.encode_field(:x, "input1", type: :quantitative)
|> VegaLite.encode_field(:y, "input2", type: :quantitative)
ⅱ)モデルの学習
- ⅰ)で作った学習データを入力したとき、そのラベルが出力となるよう、モデルに「学習」をさせる
 - 「学習」とは、「活性化関数」による通過/非活性の度合い(「重み」)を調整することを指す
 - Axon.Loopモジュールにある学習機能を使って、モデルの学習を行う
 
シグモイド関数
- 二値分類、つまり0/1を分類するための関数
 
活性化関数(Activation function)
- 入力されたデータを、次の層に通過させるかを決定するための関数
 - 入力値を別の数値に変換して出力
 - 活性化関数(Activation function)とは?
 
ReLU(Rectified Linear Unit)
- 入力値が0以下の場合には出力値が常に0、入力値が0より上の場合には出力値が入力値と同じ値となる関数
 - [活性化関数]ReLU(Rectified Linear Unit)/ランプ関数とは?
 
## ニューラルネットワーク(モデル)の構築
require Axon
# 入力層:学習(もしくは予測)で入力されるデータの形を定義
input1 = Axon.input("input1", shape: {nil, 1})
input2 = Axon.input("input2", shape: {nil, 1})
# 構築したモデル
model =
  Axon.concatenate(input1, input2)
  # 中間層:活性化関数で通過させるか決定
  |> Axon.dense(8, activation: :relu)
  # 出力層:シグモイド関数で二値分類
  |> Axon.dense(1, activation: :sigmoid)
# モデルの入出力を表示
Axon.Display.as_table(model, Nx.template({1, 1}, :s64))
|> IO.puts()
正解率(Accuracy)
- 正しく予測されたサンプルの割合
 - 【初心者向け】 機械学習におけるクラス分類の評価指標の解説
 
損失率(loss)
- 損失関数によってによって計算される
 - 分類タスクではクロスエントロピーがよく用いられる
 - 損失関数(Loss function)とは? 誤差関数/コスト関数/目的関数との違い
 
バイナリクロスエントロピー
- 結果が「1」である確率を求める関数
 - 結果が「0.5」以上なら「1」、「0.5」未満なら「0」とみなす
 - Cross-Entropy LossとBinary Cross-Entropy Lossの式と違いについて(クラス分類)
 
最適化アルゴリズム
- 「重み」を調整する量をコントロールする関数
 - 精度に影響する
 - [AI入門] ディープラーニングの仕組み ~その4:最適化アルゴリズムを比較してみた~
 
確率的勾配降下法(stochastic gradient descent)
- ランダムに取り出した学習データの一群から学習
 - 「イイ感じに重みを調整してくれる」
 - 
「重み」の更新
*「学習率(Learning Rate)」というパラメータによって調整可能    
- 更新完了までの速さ/遅さ(同時に雑さ/緻密さ)が変わる
 
 - 確率的勾配降下法の大雑把な意味
 - 確率的勾配降下法とは何か、をPythonで動かして解説する
 - See Axon.Optimizers.sgd/2
 
エポック数
- 通常であれば10回くらい必要
 - エポック(epoch)数とは【機械学習 / Deep Learning】
 
epochs_input = Kino.Input.number("Epochs", default: 3)
learning_rate_input = Kino.Input.number("Learning rate", default: 0.01)
## モデルの学習
# 学習結果状態
trained_state =
  model
  |> Axon.Loop.trainer(
    :binary_cross_entropy,
    Axon.Optimizers.sgd(Kino.Input.read(learning_rate_input))
  )
  |> Axon.Loop.metric(:accuracy, "Accuracy")
  |> Axon.Loop.run(train_datas, %{},
    epochs: Kino.Input.read(epochs_input),
    iterations: 1000,
    compiler: EXLA
  )
学習過程のアニメーション化
「学習」を可視化向けに関数化
fit = fn model, datas ->
  model
  |> Axon.Loop.trainer(:binary_cross_entropy, Axon.Optimizers.sgd(0.05))
  |> Axon.Loop.metric(:accuracy, "Accuracy")
  |> Axon.Loop.run(datas, %{}, compiler: EXLA)
end
「学習データ2つ(input1、input2)」と「学習データによる分類(x、y)」の2系統のレイヤーをグラフ化
# 「学習データ2つ(input1、input2)」と「学習データによる分類(x、y)」の2系統のレイヤーをグラフ化
graph =
  VegaLite.new(width: 600, height: 400)
  |> VegaLite.layers([
    VegaLite.new()
    |> VegaLite.mark(:point, tooltip: true)
    |> VegaLite.encode_field(:x, "input1", type: :quantitative)
    |> VegaLite.encode_field(:y, "input2", type: :quantitative),
    VegaLite.new()
    |> VegaLite.mark(:line)
    |> VegaLite.encode_field(:x, "x", type: :quantitative)
    |> VegaLite.encode_field(:y, "y", type: :quantitative)
  ])
  |> Kino.VegaLite.new()
  |> Kino.render()
# 「学習データ2つ(input1、input2)」と「学習データによる分類(x、y)」をグラフに流し込む処理
plot = fn model, datas, model_state ->
  input1 = datas |> Enum.map(&(elem(&1, 0)["input1"] |> Nx.to_flat_list())) |> List.flatten()
  input2 = datas |> Enum.map(&(elem(&1, 0)["input2"] |> Nx.to_flat_list())) |> List.flatten()
  x =
    for(i <- 0..99, do: i / 100)
    |> Nx.tensor()
    |> Nx.new_axis(0)
    |> Nx.transpose()
  y = Axon.predict(model, model_state, %{"input1" => x, "input2" => x})
  points =
    Enum.zip([input1, input2, Nx.to_flat_list(x), Nx.to_flat_list(y)])
    |> Enum.map(fn {input1, input2, x, y} ->
      %{input1: input1, input2: input2, x: x, y: y}
    end)
  Kino.VegaLite.clear(graph)
  Kino.VegaLite.push_many(graph, points)
end
for _ <- 1..15 do
  model_state = fit.(model, train_datas)
  plot.(model, train_datas, model_state)
end
ⅲ)テストデータによる評価
- ⅰ)で作ったテストデータを学習済みモデルに入力した結果が、いかにラベルと一致するかを評価
 - テストが不要なほどカンタンな例のため、今回は割愛
 
ⅳ)未知データによる予測
- ⅲ)で精度が充分な状態になっていることが前提
 - 
学習データには存在しないデータで、期待するデータが出力(予測、識別)されることを確認    
- 期待する精度が出ないケースを捕捉
 - 
期待以下が頻出する場合        
- モデルの見直し
 - 精度が出ないケースをサポート対象外とする
 
 
 - 精度を定期的にチェック
 - このタスクは、開発した学習済みモデルを本番運用に回した後も必要
 
Axon.predict(model, trained_state, %{
  "input1" => Nx.tensor([[0]]),
  "input2" => Nx.tensor([[0]])
})
Axon.predict(model, trained_state, %{
  "input1" => Nx.tensor([[0]]),
  "input2" => Nx.tensor([[1]])
})
Axon.predict(model, trained_state, %{
  "input1" => Nx.tensor([[1]]),
  "input2" => Nx.tensor([[0]])
})
Axon.predict(model, trained_state, %{
  "input1" => Nx.tensor([[1]]),
  "input2" => Nx.tensor([[1]])
})
予測の可視化
epochs_input = Kino.Input.number("Epochs", default: 3)
learning_rate_input = Kino.Input.number("Learning rate", default: 0.01)
predicts =
  1..5
  |> Enum.map(fn _ ->
    trained_state =
      model
      |> Axon.Loop.trainer(
        :binary_cross_entropy,
        Axon.Optimizers.sgd(Kino.Input.read(learning_rate_input))
      )
      |> Axon.Loop.metric(:accuracy, "Accuracy")
      |> Axon.Loop.run(train_datas, %{},
        epochs: Kino.Input.read(epochs_input),
        iteration: 1000,
        compiler: EXLA
      )
    [
      %{
        x: 0 + Enum.random(0..3) / 100,
        y:
          Axon.predict(model, trained_state, %{
            "input1" => Nx.tensor([[0]]),
            "input2" => Nx.tensor([[0]])
          })
          |> Nx.to_flat_list()
          |> List.first()
      },
      %{
        x: 0.1 + Enum.random(0..3) / 100,
        y:
          Axon.predict(model, trained_state, %{
            "input1" => Nx.tensor([[0]]),
            "input2" => Nx.tensor([[1]])
          })
          |> Nx.to_flat_list()
          |> List.first()
      },
      %{
        x: 0.9 - Enum.random(0..3) / 100,
        y:
          Axon.predict(model, trained_state, %{
            "input1" => Nx.tensor([[1]]),
            "input2" => Nx.tensor([[0]])
          })
          |> Nx.to_flat_list()
          |> List.first()
      },
      %{
        x: 1 - Enum.random(0..3) / 100,
        y:
          Axon.predict(model, trained_state, %{
            "input1" => Nx.tensor([[1]]),
            "input2" => Nx.tensor([[1]])
          })
          |> Nx.to_flat_list()
          |> List.first()
      }
    ]
  end)
  |> List.flatten()
VegaLite.new(width: 600, height: 400)
|> VegaLite.data_from_values(predicts, only: ["x", "y"])
|> VegaLite.mark(:point)
|> VegaLite.encode_field(:x, "x", type: :quantitative)
|> VegaLite.encode_field(:y, "y", type: :quantitative)