Adamによる最適化

Adam最適化によるメリットとその他手法との比較

AdamはAdaGradやRMSProp,SGDなどと同様にニューラルネットーワクの学習において、よく使われる最適化手法の一つです。最適化の手法は世の中にたくさん提案がされており、ニューラルネットワークのパラメータの最適化に関しては入力変数と出力変数の間に起こりうるパターンを見つけ出し、予測精度が最大になるような重みパラメータをどう見つけるかという問題になります。その際に学習率は実は現れる説明変数の登場回数が大きく異なる場合などにおいて重要な役割を果たすことが知られています。例えばあまり多くは現れないが、重要なパラメータについては一度の更新で大きく最適な方向へ更新を行いたい、もしくはそれほど重要ではないノイズのようなパラメータが多く現れる場合にはそのパラメータの方向にあまり大きく変化してほしくないケースも存在します。そこでAdaGradは初めにSGDが学習率を全てのパラメータで同じに扱っているという欠点に気づき、それを勾配の二乗で減らしていき、パラメータ毎に学習率を更新する手法を提案しました。しかし、それの意味するところは全てのパラメータは更新されればされるほどだんだん更新されなくなる手法です。つまりAdaGradはパラメータ毎に学習率を調整する、たまにしか現れないパラメータに関する更新手法としては都合の良い手法でしたが、すぐに更新されなくなってしまうような問題も抱えていました。そこで次に現れた手法としてRMSPropがあります。この手法は最近の勾配の値を頼りにして、過去の平均的な勾配との比較を行いながら学習率の調整を行う手法になります。AdaGradとそれほど大きく変わる手法ではありませんが、RMSPropはこれまでに現れた勾配値の二乗平均と比べて大きい変化ならそのまま勾配を進め、小さくなってきたら、変化を小さくしていき、極小点に向かう手法になっています。AdaGradに比べて過去の勾配の二乗値をそのまま加算していくのではなく、最近のアップデートの影響を強く反映させることで、AdaGradは過去に一度大きな勾配が来た場合は、その後ほとんど更新されなくなる現象がありましたが、その現象の影響を抑えつつ、最小化することができます。実際にこの最適化手法は良い働きを示しますが、Adamは更にAdaGradとRMSPropの短所に気づき、勾配の更新単位が考慮されていないこと、つまりパラメータによって一回で更新するべき単位が異なるために、これまでの手法だと学習率だけに対して調整を行うような手法であったものをAdamは勾配の二乗平均と平均を1次モーメントと2次モーメントとして考慮することで、パラメータ毎に適切なスケールで重みが更新されることを可能にしました。まとめると以下のような効果がAdamで得られると考えています。

  • Sparseness of the parameter(パラメータの更新頻度は現れる説明変数などによって異なる場合があるため、学習ステップを重みによって変えることができる)

  • Momentum効果(直前の勾配が最も現在の勾配に関連している)

  • Uncertainty of gradients update direction based on the annealing method( 例えばパラメータが鞍点など、小さな極小点にはまって動けないとき、勾配の符号は頻繁に変わることが想定されます。つまり、勾配をどちらの方向に更新するのか、確定できていない状況です。一方で勾配の符号がほとんど変わらず、ただ最小値を目指していくような場合にはAdaGradのように少しずつlearning rateを下げていくような効果を加えることでうまく最小値付近で収束させることができるのではないかと考えることもできます。

つまり、焼きなまし法と似たような形で小さな極小値にトラップされることなく最小値を目指すためのアプローチであり、更にパラメータ毎に最近の勾配の値に比べて、大きかったのか、小さかったのかを考慮したパラメータ更新を行うことで、パラメータ毎に適切な更新ができる最適化手法になっており、AdaGradやRMSPropの長所を活かし、更にそれらの欠点を発見して克服した手法と言えます多くの文献では実験的にAdamが最も良い分類性能を示したなどと書かれることが多いですが、それらの理由は以上の理由に集約できると考えています。これらの理由から、入力される説明変数の分布の偏りや、様々な複雑な形を取り得るニューラルネットワークの構造に対して頑健に学習ができるのではないかと考えています。

Required Libraries

  • numpy 1.21.1
  • scikit-learn 0.18.1
In [1]:
import numpy as np
from sklearn.preprocessing import LabelBinarizer
from sklearn.model_selection import train_test_split
import renom as rm
from renom.cuda.cuda import set_cuda_active
set_cuda_active(False)

Make label data

データのリファレンスは以下のようになります。

ISOLET Data Set, Ron Cole and Mark Fanty. Department of Computer Science and Engineering,
Oregon Graduate Institute, Beaverton, OR 97006.
In [2]:
filename = "./isolet1+2+3+4.data"
labels = []
X = []
y = []

def make_label_idx(filename):
    labels = []
    for line in open(filename, "r"):
        line = line.rstrip()
        label = line.split(",")[-1]
        labels.append(label)
    labels = list(set(labels))
    return list(set(labels))

labels = make_label_idx(filename)
labels = sorted(labels, key=lambda d:int(d.replace(".","").replace(" ","")))

データの読み込み

In [3]:
for line in open(filename,"r"):
    line = line.rstrip()
    label = labels.index(line.split(",")[-1])
    features = list(map(float,line.split(",")[:-1]))
    X.append(features)
    y.append(label)

X = np.array(X)
y = np.array(y)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
print("X_train:{}, y_train:{}, X_test:{}, y_test:{}".format(X_train.shape, y_train.shape,
                                                            X_test.shape, y_test.shape))
lb = LabelBinarizer().fit(y)
labels_train = lb.transform(y_train)
labels_test = lb.transform(y_test)
print("labels_train:{}, labels_test:{}".format(labels_train.shape, labels_test.shape))
X_train:(4990, 617), y_train:(4990,), X_test:(1248, 617), y_test:(1248,)
labels_train:(4990, 26), labels_test:(1248, 26)

ネットワークの定義とパラメータの初期化

In [4]:
output_size = len(labels)
sequential = rm.Sequential([
    rm.Dense(100),
    rm.Relu(),
    rm.Dense(50),
    rm.Relu(),
    rm.Dense(output_size)
])

学習ループ

In [5]:
epoch = 20
batch_size = 128
N = len(X_train)
optimizer = rm.Adam(lr=0.01)
for i in range(epoch):
    perm = np.random.permutation(N)
    loss = 0
    for j in range(0, N//batch_size):
        train_batch = X_train[perm[j*batch_size : (j+1)*batch_size]]
        response_batch = labels_train[perm[j*batch_size : (j+1)*batch_size]]
        with sequential.train():
            l = rm.softmax_cross_entropy(sequential(train_batch), response_batch)
        grad = l.grad()
        grad.update(optimizer)
        loss += l.as_ndarray()
    train_loss = loss / (N//batch_size)
    test_loss = rm.softmax_cross_entropy(sequential(X_test), labels_test).as_ndarray()
    print("epoch:{:03d}, train_loss:{:.4f}, test_loss:{:.4f}".format(i, float(train_loss), float(test_loss)))
epoch:000, train_loss:1.3367, test_loss:0.4439
epoch:001, train_loss:0.3355, test_loss:0.2605
epoch:002, train_loss:0.2238, test_loss:0.2537
epoch:003, train_loss:0.1772, test_loss:0.3013
epoch:004, train_loss:0.1396, test_loss:0.1789
epoch:005, train_loss:0.1350, test_loss:0.2375
epoch:006, train_loss:0.0864, test_loss:0.2309
epoch:007, train_loss:0.0829, test_loss:0.1849
epoch:008, train_loss:0.0616, test_loss:0.2061
epoch:009, train_loss:0.0471, test_loss:0.2114
epoch:010, train_loss:0.0792, test_loss:0.1997
epoch:011, train_loss:0.0520, test_loss:0.2532
epoch:012, train_loss:0.0572, test_loss:0.3516
epoch:013, train_loss:0.0803, test_loss:0.3358
epoch:014, train_loss:0.0720, test_loss:0.2725
epoch:015, train_loss:0.0859, test_loss:0.2785
epoch:016, train_loss:0.0384, test_loss:0.2300
epoch:017, train_loss:0.0311, test_loss:0.2861
epoch:018, train_loss:0.0172, test_loss:0.2492
epoch:019, train_loss:0.0461, test_loss:0.3394