Titanicから始めよう:ハイパーパラメーターチューニングのまねごとをしてみた僕たちのKaggle挑戦記

Ray Tuneというハイパーパラメーターチューニングパッケージを使って、隠れ層や学習率などをチューンしてみました。その結果は果たして?(やっちゃダメなこともやっちゃったよ)。

» 2022年01月24日 05時00分 公開
[かわさきしんじDeep Insider編集部]

この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。

「僕たちのKaggle挑戦記」のインデックス

連載目次

 前回は、特徴量エンジニアリングのまねごとをしてみようということで、Titanicコンペティションの元データを加工して、新たな特徴量を作成して、それらを使ってPyTorchで定義した線形回帰モデル/3層の全結合型DNNモデル/4層の全結合型DNNモデルの学習を行い、テストデータの生死予測をしてみたのでした。

 その結果はあまり芳しいものではありませんでした。スコアは0.77033。前々回にEDAのまねごとをしたときには、何かのモデルにデータを入力するのではなく、EDAで発見した「女性は全員生存→男性で旅客運賃が高い人は生存(そんな人はいなかった)→男性でチケットクラスが1、かつ若年層か高齢層の人は生存(1人だけ)→女性でチケットクラスが3、かつ乗船した港が'S'は死亡」という傾向に従って提出用のCSVファイルを操作しただけですが、そのスコア0.7790よりも低いものとなっていました。

 モデルによる推測の精度を上げるために、「EDAもやった。特徴量エンジニアリングもやった」となると次にくるのはハイパーパラメーターチューニングです。というわけで今回はハイパーパラメーターチューニングのまねごとをしてみることにしました。

でも、その前に

 とはいえ、実はこの原稿を書き始めるまでに筆者はかなりの試行錯誤をしていました。というのは、前回のデータのままでハイパーパラメーターをチューンしてもなかなかスコアが上がらなかったのです。これには幾つかの原因が考えられます。

  • モデルに問題がある
  • EDAや特徴量エンジニアリングが不十分

 前者については、この連載では線形回帰モデル/3層の全結合型DNN/4層の全結合型DNNの3種類をPyTorchのLinearクラスを使って定義したものを使っています。ハイパーパラメーターチューニングの結果がダメなら、scikit-learnなど、別のフレームワークを使うことを考慮した方がよさそうです。

 後者については、前回に元データを加工した結果のデータフレームはだいたい次のようなものでした(ヒートマップ)。

前回の特徴量エンジニアリングによって作成したデータ 前回の特徴量エンジニアリングによって作成したデータ

 これが悪かったのでは? ということで、今回はさらに(元のCSVファイルにある)Nameカラムから「Mr」「Ms」などの敬称(title)を抜き出して、それらをone-hot表現に変換するなどの処理を加えることにしました。その結果、得られたのは次のようなデータフレームです。実際にどんなことをしているかはこちらのノートブックを参照してください。

今回使用しているデータ 今回使用しているデータ

 このデータをCSVに保存してから、今回ハイパーパラメーターチューニングを行うためのノートブックに新たなデータセットとしてアップロードして使用するようにしています。

 上のヒートマップを見ると分かりますが、Title_MrカラムはSurvivedカラムと強い負の相関を持ち、Title_MrsカラムとTitle_MsカラムはSurvivedカラムと強い正の相関を持っています。また敬称と性別には関連があることからSex_maleカラムは不要だと判断し、以下の処理ではこれをドロップすることにしました(Title_anotherカラムも相関がそれほど強くなさそうだったのでこれもドロップすることにしました)。

 という話をしたところで、ハイパーパラメーターチューニングを行うコードについて見ていくことにしましょう


かわさき

 追加の処理はDataiTeam Titanic EDAなどのノートブックを参考にしています(ありがとうございます)。[かわさき]


ハイパーパラメーターチューニング

 ハイパーパラメーターチューニングを行うノートブックはこちらで公開しているので、興味のある方は参考にしてください。

 このノートブックの先頭では前回と同様、線形回帰モデル/3層の全結合型DNNモデル/4層の全結合型DNNモデルを作成し、それに上掲のデータフレームを(PyTorchのデータローダー経由で)入力して学習を行い、最後にテストデータから生死を推測しています。その結果は「0.76794」となりました。


かわさき

 あれ? なんか前回のスコア「0.7703」を下回っているようです……。ま、まあ、目標は、これを(できれば大きく)上回ることです。


 ハイパーパラメーターチューニングを行うフレームワークは多々ありますが、ここでは「pytorch hyperparameter tuning」として検索を行ったときに先頭に表示された「Ray Tune」を使うことにしました。


かわさき

 安直! と思いましたか? でも、PyTorchの公式サイト内にもチュートリアルがあるので、これはある意味、王道のアプローチなのです。なお、Rayの公式サイトには似た内容のチュートリアルがあり、実際にはそちらを参考にしました。



一色

 へぇ。自分は知らなかったですが、PyTorch公式でも取り扱っているなら押さえておきたいですね。[一色]


 Ray自体は分散アプリケーション構築用のAPIを提供するものですが、その一部としてハイパーパラメーターをチューンするRay Tuneが含まれているようです。Kaggleのノートブック環境には標準でインストールされているので、使用するには以下のimport文を実行します。

import ray
from ray import tune

Ray Tuneを使う準備

 Ray Tuneでは学習と検証を行う関数と、それを起動する関数の2段構えでハイパーパラメーターのチューンを行います(後者がmain関数となり、その中でチューン後のモデルとテストデータを使って推測を行う場合もあるでしょう。そうしたコードはチュートリアルを参照してください。ここでは3つのモデルのハイパーパラメーターをチューンするので、それぞれの学習と検証を行う関数と、それらを起動する関数を別個に定義しています)。

 このうち学習と検証を行う関数では、例えば、3層の全結合型DNNモデルのノード数や、学習率(learning rate)、エポック数などを変化させながら学習を行い、学習済みのモデルのステートを保存したり、検証した結果をRay Tuneフレームワークに報告したりするようになっています。

 それを起動する側の関数では、今述べたノード数などのハイパーパラメーターにはどんな値を設定するかを構成して、例えば、3層の全結合型DNNの隠れ層のノード数には4/8/16/32の4つの値を候補とするとか、バッチサイズも同じく4/8/16/32を候補とするとかといった構成を記述した上で、実際に学習と検証を行う関数を実行します。

 そして、さまざまなハイパーパラメーターの組み合わせ(全ての組み合わせを試すのではなく、その中から幾つかの組み合わせをランダムでピックアップします)で試行をし、試行が全て終わったら、その中から最良のモデル(とそのためのハイパーパラメーターの組み合わせ)が決定するので、今度はそのモデルを使ってテストデータから生死の推測を行うといった具合になるでしょう。


一色

 既に実行してしまった後ですが、ニューラルネットワークのエポック数をハイパーパラメーターチューニングするのは典型的な誤りで、すごくムダらしいですよ……。エポック数を増やしたり減らしたりして調整するよりも、早期停止(Early Stopping)する方が適切です。同様のことを自分の回でも書きましたが。理由としては、「なぜn_estimatorsやepochsをパラメータサーチしてはいけないのか - 天色グラフィティ」などが参考になるかと思います。次から気を付けよう。



かわさき

 ぎゃぁぁぁ……。これから気を付けます。ついでに早期停止だと、どんなコードになるかも可能なら次回にちょっとお知らせできるようにします。


学習と検証を行うコード

 まずは、学習と検証を行うhypartune_DNN1関数を示します。これは3層の全結合型DNNモデルを対象としたものです。


かわさき

 hypartune_DNN1関数と同様にDNN2モデルとLinearRegressionモデルのチューンを行う関数も定義していますが、これらについてはクラスのインスタンス生成を除けばほぼ同様なのでここでは説明は省略します。もちろん、本当はリファクタリングをしたいのですが、今回はあきらめました(関数名に含まれる「hypar」は「hyper」の「hy」と「parameters」の「par」を合わせたものです。分かりにくい名前ですみません)。


def hypartune_DNN1(config, checkpoint_dir=None):
    torch.manual_seed(2)
    random.seed(2)
    global INPUT_SIZE, OUTPUT_SIZE

    if checkpoint_dir:
        checkpoint = os.path.join(checkpoint_dir, "checkpoint")
        model_state, optimizer_state = torch.load(checkpoint)
        net.load_state_dict(model_state)
        optimizer.load_state_dict(optimizer_state)

    datasets = get_datasets()
    train_sets, val_sets = datasets[1# [0] for Regression, [2] for DNN2
    trainloader = DataLoader(train_sets, batch_size=config['batch_size'])
    valloader = DataLoader(val_sets, batch_size=config['batch_size'])

    net = DNN1(INPUT_SIZE, config['l1'], OUTPUT_SIZE)

    criterion = nn.BCELoss()
    optimizer = optim.Adam(net.parameters(), lr=config['lr'])

    for epoch in range(config['epochs']):
        running_loss = 0.0
        
        for cnt, (X, y) in enumerate(trainloader, 1):
            optimizer.zero_grad()

            pred = net(X.float())
            loss = criterion(pred.reshape(-1), y.float())
            loss.backward()
            optimizer.step()
            running_loss += loss.item()

        print(f'[{epoch:4d}] loss: {running_loss / cnt:.4f}')

        val_loss = 0.0
        predicts = []
        labels = []
        with torch.no_grad():
            for cnt, (val_X, val_y) in enumerate(valloader, 0):
                pred = net(val_X.float())
                loss = criterion(pred.reshape(-1), val_y.float())
                val_loss += loss.item()
                pred[pred >= 0.5] = 1
                pred[pred < 0.5] = 0
                predicts.extend(pred)
                labels.extend(val_y)

            result = [p == t for p, t in zip(predicts, labels)]
            accuracy = sum(result) / len(predicts)
            val_loss_avg = val_loss / cnt

        with tune.checkpoint_dir(step=epoch) as checkpoint_dir:  # using default dir
            path = os.path.join(checkpoint_dir, 'checkpoint')
            torch.save(
                (net.state_dict(), optimizer.state_dict()), path)

        tune.report(loss=val_loss_avg, accuracy=accuracy)

    print('Finished Training')

学習と検証を行うhypartune_DNN1関数

 長い関数ですが、最初はデータローダーの設定、次に学習を行うコード、そして検証を行うコード、最後にモデルの状態を保存したり、ログに記録する目的でRay Tuneに検証結果を報告したりするコードが含まれています。

 この関数のパラメーターはconfigとcheckpoint_dirの2つです。configには、ノード数や学習率など、チューンしたいハイパーパラメーターとその候補となる値が辞書形式で渡されます。例えば、隠れ層のノード数は、configから「config['l1']」のようにしてピックアップしていきます(上のコードでは、エポック数、学習率、バッチサイズに関して、このようにしてconfigから値を取得するようになっています)。この関数が呼び出されるたびに、この値が変化することで異なる構成で学習と検証が行われ、最適なハイパーパラメーターが決定されるようになります(そうした面倒なことを取り計らってくれるのがRay Tuneです)。

net = DNN1(INPUT_SIZE, config['l1'], OUTPUT_SIZE)

configパラメーターに渡された値の使用例

 checkpoint_dirは、チェックポイントの復元をする必要があるときに、Ray Tuneから値が渡されるようです。関数内では、ここにRay Tuneから値が渡されたときには、モデルとオプティマイザーの状態を復元するようになっています(そのためのコードはチュートリアルにあるコードをそのまま使用しています)。

 データローダーの作成では、get_datasetsという名前のヘルパーを用意しました。これを使ってこの関数内でPyTorchのデータセットクラスを作成して、それを3分割してK-fold交差検証を行えるように準備しています。

 その後はDNN1クラスのインスタンス生成、学習を行うループ、検証を行うループを実行していますが、上で述べたconfigからのハイパーパラメーターの取得を除けば、この辺のコードは既におなじみのものでしょう。

 最後の「with tune.checkpoint_dir(step=epoch) as checkpoint_dir:」で始まるブロックはエポックごとに学習後のモデルの状態とオプティマイザーの状態を保存するものです。最良の結果はここに保存された各種の情報を参照して決定されます。その後のtune.report関数呼び出しではRay Tuneがログに記録するメトリックを指定します。上のコードでは検証時に得られた損失と、検証時に得た推測結果の精度をログに記録するようにしています。

チューンで使用する学習用関数を呼び出すコード

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。