機械学習のExampleから覚えるPython(class:継承)

今までPythonを感覚的に使っていたので、改めて文法を知ろうかなと。
その際にいま流行りの機械学習(深層学習)のExampleを例にすると
わかりやすいのかなと思ったので書いてみる。

※基本的には Python3.x系のつもりで記載してます

Example

https://www.tensorflow.org/tutorials/ に記載されている

import tensorflow as tf
mnist = tf.keras.datasets.mnist

(x_train, y_train),(x_test, y_test) = mnist.load_data()
x_train, x_test = x_train / 255.0, x_test / 255.0

model = tf.keras.models.Sequential([
  tf.keras.layers.Flatten(),
  tf.keras.layers.Dense(512, activation=tf.nn.relu),
  tf.keras.layers.Dropout(0.2),
  tf.keras.layers.Dense(10, activation=tf.nn.softmax)
])
model.compile(optimizer='adam',
              loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])

model.fit(x_train, y_train, epochs=5)
model.evaluate(x_test, y_test)

※本記事記載の時点のコードです。

class

model = tf.keras.models.Sequential([
  tf.keras.layers.Flatten(),
  tf.keras.layers.Dense(512, activation=tf.nn.relu),
  tf.keras.layers.Dropout(0.2),
  tf.keras.layers.Dense(10, activation=tf.nn.softmax)
])

tf.keras.models.Sequentialclassと呼ばれるもので定義されています。

実際のコードは こちらにあります。

@keras_export('keras.models.Sequential', 'keras.Sequential')
class Sequential(Model):

継承

class Sequential(Model):

このSequentialクラスは Modelクラスを継承しています。
Modelを「基底クラス」、Sequentialを「派生クラス」と言ったりします。

やっていることは、シンプルで Sequential = Model + Sequential独自 という形で
Modelの関数や変数を Sequentialでも使えます。

Exampleで記述している

model.compile(optimizer='adam',
              loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])

model.fit(x_train, y_train, epochs=5)
model.evaluate(x_test, y_test)

こちらの compilefitevaluate 関数は Sequentialのクラスコード内には記述がありません。
実際に記述されているのは Model にあります。

確認したい方は こちらを参照してください。

多重継承

基底クラスを複数宣言できるというお話です。

class DerivedClassName(Base1, Base2, Base3):
    <statement-1>
    .
    .
    .
    <statement-N>

参考

さて、あとはSequentialクラスをインスタンスしている部分(初期化)と
演算子(Exampleでは x_train / 255.0 )さえ理解すれば題目の
Exampleコードはすべて理解できたことになりますね!

機械学習のExampleから覚えるPython(関数の引数)

今までPythonを感覚的に使っていたので、改めて文法を知ろうかなと。
その際にいま流行りの機械学習(深層学習)のExampleを例にすると
わかりやすいのかなと思ったので書いてみる。

※基本的には Python3.x系のつもりで記載してます

Example

https://www.tensorflow.org/tutorials/ に記載されている

import tensorflow as tf
mnist = tf.keras.datasets.mnist

(x_train, y_train),(x_test, y_test) = mnist.load_data()
x_train, x_test = x_train / 255.0, x_test / 255.0

model = tf.keras.models.Sequential([
  tf.keras.layers.Flatten(),
  tf.keras.layers.Dense(512, activation=tf.nn.relu),
  tf.keras.layers.Dropout(0.2),
  tf.keras.layers.Dense(10, activation=tf.nn.softmax)
])
model.compile(optimizer='adam',
              loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])

model.fit(x_train, y_train, epochs=5)
model.evaluate(x_test, y_test)

※本記事記載の時点のコードです。

関数の引数

前回 機械学習のExampleから覚えるPython(関数呼び出し)にて、load_data 関数の引数 pathが呼び出し時に省略されていることが確認できました。

関数の引数については3つの指定ができます。

  • dafaultによる省略
  • 引数を入力(引数名無し)
  • 引数名を指定

defaultによる省略

関数定義側で、default値が設定している場合に
呼び出し側が default値で良い場合省略することができます。

  • 関数定義側
def load_data(path='mnist.npz'):
  • 呼び出し側
(x_train, y_train),(x_test, y_test) = mnist.load_data()

pathmnist.npzの値で処理されます。

引数を入力(引数名無し)

model.fit(x_train, y_train, epochs=5)

このコードを見てみます。
定義側はこのようになってます。

  def fit(self,
          x=None,
          y=None,
          batch_size=None,
          epochs=1,
          verbose=1,
          callbacks=None,
          validation_split=0.,
          validation_data=None,
          shuffle=True,
          class_weight=None,
          sample_weight=None,
          initial_epoch=0,
          steps_per_epoch=None,
          validation_steps=None,
          validation_freq=1,
          max_queue_size=10,
          workers=1,
          use_multiprocessing=False,
          **kwargs):

self は一旦無視していただいて、x_train, y_trainfit関数の
どの引数にあたるかというと、順番に設定しています。
つまり、以下のようになります。

#         x=x_train, y=y_train
model.fit(x_train, y_train, epochs=5)

引数名を指定

先ほどのコードの epochsの設定が該当します。

model.fit(x_train, y_train, epochs=5)

fit関数で定義されている epochsを指定して 5という値を入力しています。

可変長引数

関数の定義では、可変長引数 を定義することができます。
fit関数でも最後の引数に出てきていた **kwargsが該当します。

こちらのページが参考になると思いますので、リンク貼っときます。

機械学習のExampleから覚えるPython(関数呼び出し)

今までPythonを感覚的に使っていたので、改めて文法を知ろうかなと。
その際にいま流行りの機械学習(深層学習)のExampleを例にすると
わかりやすいのかなと思ったので書いてみる。

※基本的には Python3.x系のつもりで記載してます

Example

https://www.tensorflow.org/tutorials/ に記載されている

import tensorflow as tf
mnist = tf.keras.datasets.mnist

(x_train, y_train),(x_test, y_test) = mnist.load_data()
x_train, x_test = x_train / 255.0, x_test / 255.0

model = tf.keras.models.Sequential([
  tf.keras.layers.Flatten(),
  tf.keras.layers.Dense(512, activation=tf.nn.relu),
  tf.keras.layers.Dropout(0.2),
  tf.keras.layers.Dense(10, activation=tf.nn.softmax)
])
model.compile(optimizer='adam',
              loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])

model.fit(x_train, y_train, epochs=5)
model.evaluate(x_test, y_test)

※本記事記載の時点のコードです。

関数の呼び出し

mnist = tf.keras.datasets.mnist

(x_train, y_train),(x_test, y_test) = mnist.load_data()

tf.keras.datasets.mnistload_data関数を呼んでいます。
その返り値を x_train,y_train, x_test,y_testに代入しています。

load_data関数はこちらで定義されています。

@keras_export('keras.datasets.mnist.load_data')
def load_data(path='mnist.npz'):

「@」(デコレータ)は今後書こうと思います。
呼び出しの時に 引数pathは default : mnist.npz のままで良いということで省略してますね。

returnとしても以下で定義されているためそのまま格納ですね!

    return (x_train, y_train), (x_test, y_test)

返り値の受け取りについては後々...

機械学習のExampleから覚えるPython(import)

今までPythonを感覚的に使っていたので、改めて文法を知ろうかなと。
その際にいま流行りの機械学習(深層学習)のExampleを例にすると
わかりやすいのかなと思ったので書いてみる。

※基本的には Python3.x系のつもりで記載してます

Example

https://www.tensorflow.org/tutorials/ に記載されている

import tensorflow as tf
mnist = tf.keras.datasets.mnist

(x_train, y_train),(x_test, y_test) = mnist.load_data()
x_train, x_test = x_train / 255.0, x_test / 255.0

model = tf.keras.models.Sequential([
  tf.keras.layers.Flatten(),
  tf.keras.layers.Dense(512, activation=tf.nn.relu),
  tf.keras.layers.Dropout(0.2),
  tf.keras.layers.Dense(10, activation=tf.nn.softmax)
])
model.compile(optimizer='adam',
              loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])

model.fit(x_train, y_train, epochs=5)
model.evaluate(x_test, y_test)

※本記事記載の時点のコードです。

import

本コードに適用するパッケージを記述します。
パッケージとは __init__.py があるディレクトリ構想のものを示します。
Python 3.3以降では __init__.py がなくても良いみたいです。

import module as identifier

import tensorflow as tf

tensorflow パッケージを読みこみ、tfと定義する。

もし、

import tensorflow

だけであれば、その後のコードは

mnist = tensorflow.keras.datasets.mnist

と書かないとエラーになります。

from relative_module import identifier

https://www.tensorflow.org/tutorials/keras/basic_classification より

# TensorFlow and tf.keras
import tensorflow as tf
from tensorflow import keras

from を使った記述があります。 これは、tensorflow パッケージにある keras モジュールを呼び出してます。

その後のコードでは、kerasから記述することができます。

fashion_mnist = keras.datasets.fashion_mnist

最初の import - as で書くと

fashion_mnist = tf.keras.datasets.fashion_mnist

import時の検索範囲

その他

「,」区切り

from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau

とか書くことができます。 import xxxでも書けますが、Pythonのスタイルガイドでは非推奨らしいです。

「*」

from tensorflow import *

ですべてのモジュールをインポートすることができます。
ただ、個人的には自分で書いたライブラリ以外は必要分しかimport しないようにしてます。

追加(相対インポート)

コメントいただいたので、該当するリンクページ載せときます。

LazyAdamOptimizerを見てみた

見てみた

概要

Adam Optimizerの sparse更新をより効率化したもの。

既存のAdamアルゴリズムは2つの移動平均の各train variableに対して行います。 計算はすべてのStepで更新されます。 このクラスは sparse variableに対して勾配更新の緩やかな処理を行います。 すべてのインデックスのアキュムレータを更新するのではなく、 現在のバッチに含まれるスパース変数インデックスの移動平均アキュムレータのみを更新します。

オリジナルのAdamオプティマイザと比較すると、一部のアプリケーションではモデルトレーニングのスループットが大幅に向上しています。
ただし、元のAdamアルゴリズムとはわずかに異なるセマンティクスが提供されているため、異なる経験的結果が生じる可能性があります。

詳細

  • lazy_adam_optimizer.py
    では、Adam Optimizerから2つの関数のみoverrideしている。

  • _apple_sparse
    ※こちらを例に以後違いを説明します。

  • _resource_apply_sparse

_apple_sparse

Adam Optimizerでは以下のように関数を呼んでいる。

LazyAdam Optimizerでは*\_shared関数は呼んでいない

  def _apply_sparse(self, grad, var):
    beta1_power, beta2_power = self._get_beta_accumulators()
    beta1_power = math_ops.cast(beta1_power, var.dtype.base_dtype)
    beta2_power = math_ops.cast(beta2_power, var.dtype.base_dtype)
    lr_t = math_ops.cast(self._lr_t, var.dtype.base_dtype)
    beta1_t = math_ops.cast(self._beta1_t, var.dtype.base_dtype)
    beta2_t = math_ops.cast(self._beta2_t, var.dtype.base_dtype)
    epsilon_t = math_ops.cast(self._epsilon_t, var.dtype.base_dtype)
    lr = (lr_t * math_ops.sqrt(1 - beta2_power) / (1 - beta1_power))

    # \\(m := beta1 * m + (1 - beta1) * g_t\\)
    m = self.get_slot(var, "m")
    m_t = state_ops.scatter_update(m, grad.indices,
                                   beta1_t * array_ops.gather(m, grad.indices) +
                                   (1 - beta1_t) * grad.values,
                                   use_locking=self._use_locking)

    # \\(v := beta2 * v + (1 - beta2) * (g_t * g_t)\\)
    v = self.get_slot(var, "v")
    v_t = state_ops.scatter_update(v, grad.indices,
                                   beta2_t * array_ops.gather(v, grad.indices) +
                                   (1 - beta2_t) * math_ops.square(grad.values),
                                   use_locking=self._use_locking)

    # \\(variable -= learning_rate * m_t / (epsilon_t + sqrt(v_t))\\)
    m_t_slice = array_ops.gather(m_t, grad.indices)
    v_t_slice = array_ops.gather(v_t, grad.indices)
    denominator_slice = math_ops.sqrt(v_t_slice) + epsilon_t
    var_update = state_ops.scatter_sub(var, grad.indices,
                                       lr * m_t_slice / denominator_slice,
                                       use_locking=self._use_locking)
    return control_flow_ops.group(var_update, m_t, v_t)

 m_t := beta_1 * m_ {t-1} *+ (1 - beta_1) * g

の箇所だけを比較すると

  • Adam Optimizer
    # m_t = beta1 * m + (1 - beta1) * g_t
    m = self.get_slot(var, "m")
    m_scaled_g_values = grad * (1 - beta1_t)
    m_t = state_ops.assign(m, m * beta1_t,
                           use_locking=self._use_locking)
    with ops.control_dependencies([m_t]):
      m_t = scatter_add(m, indices, m_scaled_g_values)
  • LazyAdam Optimizer
    # \\(m := beta1 * m + (1 - beta1) * g_t\\)
    m = self.get_slot(var, "m")
    m_t = state_ops.scatter_update(m, grad.indices,
                                   beta1_t * array_ops.gather(m, grad.indices) +
                                   (1 - beta1_t) * grad.values,
                                   use_locking=self._use_locking)

となっている。

LazyAdamで使っている scatter_updateindicesの部分(sparse)のみ更新する関数

そのあと v_tも同じように計算し

    m_t_slice = array_ops.gather(m_t, grad.indices)
    v_t_slice = array_ops.gather(v_t, grad.indices)

切り取って

    denominator_slice = math_ops.sqrt(v_t_slice) + epsilon_t
    var_update = state_ops.scatter_sub(var, grad.indices,
                                       lr * m_t_slice / denominator_slice,
                                       use_locking=self._use_locking)

更新値を算出。

まとめ

Moving-Averageの計算において、部分な値にて計算しているだけみたい。
(一部 addから updateの変更も合っているけど)

参考