モデルの訓練とは、損失関数が返す値を小さくするようにパラメータを調整する作業にほかならない。損失関数が「どれだけ間違っているか」を測り、オプティマイザが「どう直すか」を決める。この 2 つはモデルの学習能力を左右する重要な要素であり、タスクに合った組み合わせを選ぶことが精度向上の第一歩になる。
損失関数の役割
損失関数はモデルの予測値と正解ラベルの「ズレ」を 1 つの数値にまとめる関数だ。この数値が小さいほど予測が正解に近いことを意味する。訓練ではこの値を最小化する方向にパラメータが更新されていく。
モデルが予測を出力
損失関数が予測と正解のズレを計算
オプティマイザがズレを小さくする方向に重みを更新
損失関数の選択を誤ると、モデルがまったく学習しなかったり、見当違いの方向に最適化されたりする。タスクの種類に応じた正しい選択が不可欠だ。
分類タスクの損失関数
分類タスクでは交差エントロピー(cross entropy)系の損失関数を使う。正解クラスに割り当てられた確率が高いほど損失は小さくなり、低いほど大きくなるという仕組みだ。
2 クラス分類用。出力層のユニット数は 1、活性化関数は sigmoid を使う。メールのスパム判定や病気の陽性 / 陰性判定など、答えが 2 択の場合に選ぶ。
多クラス分類用で、ラベルが整数(0, 1, 2, ...)の場合に使う。MNIST の手書き数字(0〜9)のように、ラベルがそのまま整数で与えられる場面で便利だ。
多クラス分類用で、ラベルが one-hot ベクトル([0, 0, 1, 0] など)の場合に使う。sparse 版と数学的には等価だが、ラベルの形式だけが異なる。
sparse と通常版のどちらを使うかは、ラベルの形式で機械的に決まる。整数ラベルなら sparse、one-hot ラベルなら通常版を選べばよい。
2 クラス分類の例
スパム判定のような 2 クラス分類では、出力層に sigmoid を置き、損失関数に binary_crossentropy を指定する。
model = keras.Sequential([ layers.Dense(64, activation="relu", input_shape=(100,)), layers.Dense(32, activation="relu"), layers.Dense(1, activation="sigmoid"), ]) model.compile( optimizer="adam", loss="binary_crossentropy", metrics=["accuracy"], )
sigmoid は出力を 0〜1 の範囲に押し込む関数で、「クラス 1 に属する確率」と解釈できる。0.5 を閾値にして 0 か 1 に分類するのが一般的な使い方だ。
多クラス分類の例
出力層に softmax を置くと、各クラスの確率分布が得られる。すべての出力の合計は 1 になる。
model = keras.Sequential([ layers.Dense(128, activation="relu", input_shape=(784,)), layers.Dense(64, activation="relu"), layers.Dense(10, activation="softmax"), ]) model.compile( optimizer="adam", loss="sparse_categorical_crossentropy", metrics=["accuracy"], )
出力は 1 つの確率値(0〜1)。2 クラス分類専用。
出力はクラス数ぶんの確率分布。多クラス分類に対応。
回帰タスクの損失関数
回帰タスクでは、予測値と正解値の数値的な距離を損失として計算する。
| 損失関数 | 計算方法 | 特徴 |
|---|---|---|
| mse | 誤差の 2 乗の平均 | 外れ値に敏感 |
| mae | 誤差の絶対値の平均 | 外れ値に頑健 |
| huber | 小さい誤差は mse、大きい誤差は mae | 両者の中間的な性質 |
住宅価格の予測を例にとると、次のようになる。
model = keras.Sequential([ layers.Dense(64, activation="relu", input_shape=(13,)), layers.Dense(32, activation="relu"), layers.Dense(1), ]) model.compile( optimizer="adam", loss="mse", metrics=["mae"], )
回帰の出力層には活性化関数を指定しない。値の範囲に制約を設けず、任意の実数を出力させるためだ。
オプティマイザの役割
損失関数が「現在の誤差」を教えてくれるのに対し、オプティマイザは「パラメータをどの方向にどれだけ動かすか」を決定する。すべてのオプティマイザは勾配降下法を基盤としており、損失関数の勾配(傾き)を手がかりに重みを更新していく。
勾配降下法の基本的な更新式は で表される。 は重み、 は学習率、 は損失の勾配だ。
1 回の更新でパラメータをどれだけ動かすかを制御するハイパーパラメータ。大きすぎると発散し、小さすぎると収束が遅い。
主要なオプティマイザ
もっとも基本的なオプティマイザ。勾配の方向にそのまま重みを更新する。momentum を加えると過去の勾配を考慮し、振動を抑えながら収束を速められる。シンプルなぶん挙動が予測しやすく、学習率のスケジューリングと組み合わせると高い最終精度を出すことがある。
勾配の平均(1 次モーメント)と勾配の 2 乗の平均(2 次モーメント)を使い、パラメータごとに学習率を自動調整する。デフォルト設定のまま幅広いタスクで安定した結果を出すため、最初に試すオプティマイザとして定番になっている。
勾配の 2 乗の移動平均で学習率を調整する。Adam から 1 次モーメントの補正を除いたような構造をしており、RNN 系のタスクで特に有効とされている。
オプティマイザの使い分け
文字列で指定するとデフォルト設定が適用される。ハイパーパラメータを細かく調整したい場合はオブジェクトで渡す。
# 文字列で指定(デフォルト設定) model.compile(optimizer="adam", loss="mse") # オブジェクトで指定(学習率を変更) model.compile( optimizer=keras.optimizers.Adam(learning_rate=0.0001), loss="mse", ) # SGD + momentum model.compile( optimizer=keras.optimizers.SGD(learning_rate=0.01, momentum=0.9), loss="mse", )
Adam のデフォルト学習率は 0.001 で、多くのケースではこの値で十分に機能する。精度が伸び悩んだときに 0.0001 や 0.0005 に下げてみるのは有効なアプローチだ。
学習率の影響
学習率はオプティマイザの中でもっとも重要なハイパーパラメータだ。値によって訓練の挙動は大きく変わる。
損失が振動したり発散したりして、モデルが収束しない。最適解の周囲を飛び越えてしまう。
損失は安定して下がるが、収束に非常に時間がかかる。局所最適解に捕まりやすくもなる。
学習率を訓練の途中で動的に変化させるスケジューリングという手法もある。たとえば最初は大きめの学習率で素早く収束に近づき、後半で小さくして精密に調整するやり方だ。
lr_schedule = keras.optimizers.schedules.ExponentialDecay( initial_learning_rate=0.01, decay_steps=1000, decay_rate=0.9, ) model.compile( optimizer=keras.optimizers.Adam(learning_rate=lr_schedule), loss="sparse_categorical_crossentropy", metrics=["accuracy"], )
ExponentialDecay は指定したステップごとに学習率を指数的に減衰させる。1000 ステップごとに学習率が 0.9 倍になるため、訓練の後半では自動的に細かい調整に切り替わる。
タスク別の推奨設定
最後に、タスクの種類と損失関数・出力層・オプティマイザの典型的な組み合わせをまとめておく。
| タスク | 損失関数 | 出力層 |
|---|---|---|
| 2 クラス分類 | binary_crossentropy | sigmoid(1 ユニット) |
| 多クラス分類 | sparse_categorical_crossentropy | softmax(クラス数) |
| 回帰 | mse または mae | 活性化なし(1 ユニット) |
オプティマイザは迷ったら Adam から始め、精度が頭打ちになったら学習率の調整や SGD + momentum への切り替えを検討する。損失関数とオプティマイザの正しい選択は、モデルの構造設計と同じくらい訓練の成否を左右する要素だ。
ラベルが整数(0〜9)で与えられる 10 クラス分類で、適切な損失関数はどれか?
- binary_crossentropy
- mse
- sparse_categorical_crossentropy
- huber