のんびりしているエンジニアの日記

ソフトウェアなどのエンジニア的な何かを書きます。

Google Cloud Storage(GCS)でうっかり30万以上溶かした話

皆さんこんにちは。
コンペで頑張ったので疲れました。

さて、Google Landmark 2021が終了し、Retrieval5位(金)、Recognition12位(銀)となりました。
本日は自戒と反省により、クラウドで30万円消失した話を
記録として書こうと思います。皆さん私を見て反面教師にしてください。

事象

9月入ってからLandmark2021に参加し、Google Cloud Platform、通称GCPを利用していた。
主な利用はGoogle Cloud Storageのみで、ほぼ容量課金だろうと高をくくっており、課金請求の上限など入れ忘れてました。

すると9/18に久々に請求額を確認すると32万ほどの請求額がありました。
さすがに目玉が飛び出て、調査にあたったといったものになります。

課金内容を確認したら原因はすぐにわかり、チームで対策を打ちました。(私が慌てて学習にストップかけた)

f:id:tereka:20210919012424p:plain
請求額

理由

課金の表面的な事象はGCSの大陸間通信です。
GCSはどこから利用するかにより、通信量に応じて課金がされるといった体系になっており、
この部分を完全にみおとしました。

今回、2人でLandmarkを取り組んでおり、TPUを中心に実施していました。
TPUで行う必要があったのはLandmarkが巨大なデータであったため、そして、GCSを利用したのはTFRecordをローカルに落とす必要がなかったからです。
相方のGCPインスタンスがヨーロッパ、私が格納していたデータのGCSのリージョンが北米にあったことにより、大陸間通信が膨大となり、課金されてしまったということです。

正直、チームマージして取り組む場合に大陸間通信のコストなどが影響あることを完全に見落とし、
そのあたりの共通認識を取らなかったのは大きな反省点です。
逆に言えば、チーム組む皆さんはきちんと認識を取りましょう。AWSのS3でも似たような課金体系だったはずです。

対策

1. 課金額のアラートをしかける。
シンプルですが、どんなにしょぼいものでも必ず請求アラートをしかけましょう。
何が起こるかわからないので、仕掛けておいて損はありません。
僕は油断して目玉が飛び出ました。

2. チーム内のリージョンを確認する。
データセットの共有などはストレージを利用することは多いと思います。
特にTPU関連を利用する際、GCP/GCSを利用することになるので、通信コストがかからないよう、
チームメンバーでのリージョン統一などは事前に行っておきましょう。

最後に

そもそもTPUを真面目に運用しない限り起こり得ない事象ではありますが、
私を反面教師にして、うっかりを発生せず、正しいクラウドライフをお送りください。

分散深層強化学習ライブラリHandyRLをコンペで使ってみた。

皆さんこんにちは
お元気ですか。ブログ書きながら、当チームのガチョウを見守っています。

最近までHungryGeeseに参加しており、このコンペでHandyRLライブラリには大変お世話になりました。
このコンペでHandyRLを改造して使ったので、そのポイントを記録として残しておきます。

HandyRLとは

一言で言えば、PyTorchで利用できる軽量な深層分散強化学習用のフレームワークです。
実際に使ってみた感想としても、これまでの強化学習のFWより直感的に理解しやすいものでした。

github.com

分散深層強化学習ではこの2つの処理を同時に行っています。

1. エージェントを自己対戦させてエピソードを生成する処理
2. 生成したエピソードを使ってモデルを学習させる処理

これを自前で実装するのは非常に骨が折れるものですが、このあたりをきれいに実装されており、使いやすいものになってました。

HandyRLの使い方

基本編

基本はチュートリアルを読んだほうが早いです。
基本的な流れとしてこのあたりを抑えておきましょう。たった3Stepでできます。

  1. ゲームごとにEnviroment Classを作成する。(チュートリアルだと、TicTacToe)
  2. 設定(config.yaml)を変更する。
  3. 学習する。

github.com

※気が向いたら別途記事書くかも・・・

Tips

コンペで勝つには、上記の基本だけだと対応しづらい部分があります。
そのため、他コンペでも利用できる改造ポイントをTipsを書いておきます。

自己対戦以外でエージェント作成

HandyRLのデフォルトエージェントは、自己対戦になります。
しかし、多様性の観点からある程度、様々なエージェントと対戦させたほうが良いです。
自己対戦以外のエージェントを利用する場合、train.pyとworker.pyを変更する必要があります。
今回は変更の例とその解説を簡単に行います。

train.pyのL587付近

                            for p in self.env.players():
                                if random.random() > 0.5:
                                    args['model_id'][p] = -1
                                else:
                                    args['model_id'][p] = self.model_era
                                    is_self_model = True
                                    players.append(p) # 自己対戦リスト

                            if is_self_model is False:
                                p = random.choice(self.env.players())
                                args['model_id'][p] = self.model_era
                                players.append(p)

入力の値が-1であれば、事前に学習済のエージェントを使ってエピソードを生成する、
また、self.model_era(=学習回数,Epoch)であれば、自己対戦用エージェントを利用する。といった方式を想定して入力しています。
入力されたデータによってどのモデルかは、worker.pyで選択するようにします。

worker.py

class Worker:
    def __init__(self, args, conn, wid):
        # 省略、以下を追加、モデルをPoolしておく処理、get_modelsでエージェントとなるモデルの一覧を取得する。
        self.trained_models = [ModelWrapper(model, ts=1) for model in get_models()] + [ModelWrapper(model, ts=2) for
                                                                 model in get_2ts_models()] 

    def _gather_models(self, model_ids, role="g"):
        model_pool = {}
        for model_id in model_ids:
            if model_id not in model_pool:
                if model_id < 0:
                    if role == "g":
                        model_pool[model_id] = self.trained_models # Poolしたモデルを利用する。
                elif model_id == self.latest_model[0]:
                    model_pool[model_id] = self.latest_model[1]
                else:
                    model_pool[model_id] = ModelWrapper(pickle.loads(send_recv(self.conn, ('model', model_id))), ts=-1)
                    if model_id > self.latest_model[0]:
                        self.latest_model = model_id, model_pool[model_id]
        return model_pool

    def run(self):
        while True:
            args = send_recv(self.conn, ('args', None))
            role = args['role']

            models = {}
            if 'model_id' in args:
                model_ids = list(args['model_id'].values())
                model_pool = self._gather_models(model_ids, role=role)

                # make dict of models
                for p, model_id in args['model_id'].items():
                    if model_id < 0:
                        models[p] = deepcopy(random.choice(model_pool[model_id])) # Poolの中からランダムに選択する。
                    else:
                        models[p] = model_pool[model_id]

学習済モデルを利用する実装です。いわばPoolのようなもので、学習済モデルをこの中から選択する方式です。
上記コードでは次のことを行っています。

・学習済モデルをコンストラクタで初期化
・_gather_modelsで、エピソード生成時にmodel_idが-1の場合にモデルPoolを選択
・runでPoolの中から一つモデルを選択し、対戦させる。

様々なエージェントで評価

初期値だとランダム動作での評価になる(HungryGeeseだけ?)ので、正直ある程度学習させると弱すぎてほぼ勝利し意味のある評価ができなくなります。
そのため、多様性がありつつも、ある程度強いエージェントで評価が必要です。

こちらもtrain.py/worker.pyを修正する必要があります。

train.pyのL596付近

                        elif args['role'] == 'e':
                            # evaluation configuration
                            args['player'] = [self.env.players()[self.num_results % len(self.env.players())]]
                            for p in self.env.players():
                                if p in args['player']:
                                    args['model_id'][p] = self.model_era
                                else:
                                    args['model_id'][p] = -1
                            self.num_results += 1

train.pyでは、一つ必ず、学習しているモデルを選択し、残りを評価として選択するためのモデルとして設定します。

worker.py

class Worker:
    def __init__(self, args, conn, wid):
        self.evaluate_models = [ModelWrapper(model, ts=1) for model in get_models()] + [ModelWrapper(model, ts=2) for
                                                                                        model in get_2ts_models()]

    def _gather_models(self, model_ids, role="g"):
        model_pool = {}
        for model_id in model_ids:
            if model_id not in model_pool:
                if model_id < 0:
                    if role == "g":
                        # 略
                    else:
                        model_pool[model_id] = self.evaluate_models

roleが"g"であれば、エピソード生成、"e"であれば、評価になります。
そのため、評価をする場合は定義しておいたものからランダムに選択する実装にしています。(選択部は学習と同じ選択仕様)

自己対戦モデルを一定期間保存する

一定期間のEpisodeを保存しておき、そのエージェントと対戦させたいといったケースが考えられます。
これにより学習を進めた結果、今までのモデルに負けるといったことを防ぐ取り組みです。
例えば、AlphaStarでは過去学習させたエージェントも保存しておき、対戦するようなこともされました。

この方式もworker.pyを変更することで実現しました。

class Worker:
    def __init__(self, args, conn, wid):
        # 省略、以下を追加、自己対戦モデル用のPool
        self.self_fight_models = []

    def _gather_models(self, model_ids, role="g"):
        model_pool = {}
        for model_id in model_ids:
            if model_id not in model_pool:
                if model_id < 0:
                    if role == "g":
                        if len(self.self_fight_models) == 0:
                            model_pool[model_id] = self.trained_models
                        else:
                            model_pool[model_id] = \
                                random.choices([self.self_fight_models, self.trained_models], k=1, weights=[1, 1])[0] # 自己対戦モデル or 学習済エージェント
                    else:
                        model_pool[model_id] = self.evaluate_models
                elif model_id == self.latest_model[0]:
                    # 略
                else:
                    model_pool[model_id] = ModelWrapper(pickle.loads(send_recv(self.conn, ('model', model_id))), ts=-1)
                    if model_id > self.latest_model[0]:
                        self.latest_model = model_id, model_pool[model_id]
                        if model_id % 300 == 0: # 300学習につき一つ対戦用のPoolに追加する。
                            self.self_fight_models.append(model_pool[model_id])
                            if len(self.self_fight_models) > 100: # 100件以上作られれば最初のを捨てる。
                                self.self_fight_models = self.self_fight_models[1:]
学習済モデルの実行

事前に学習したモデルを使いたいといった場面があります。
例えば、Kaggleでは、上位LBなど他エージェントのエピソードを獲得できます。
それを用いて勝利モデルの行動を事前に模倣学習することで、ある一定の強さのエージェントから学習を開始でき、学習の高速化を見込めます。
ソースをほぼ変更しなくとも具体的には次の方式で実現可能です。

1. modelsディレクトリを作成し、モデルを「1.pth」して配置する。
2. 設定の「config.yaml」の「restart_epoch」を1にする。
3. 学習を実行する。

HandyRLはepisodeの設定を1以上でmodels配下のモデルを利用する方式を採用しています。
また、デフォルトの読み込み(pytorchのload_state_dict)のStrict=Falseとなっているため、若干の構造変化には対応できます。

決定的動作で動かす

HandyRLのエピソード生成でのエージェントのアクションはモデルの出力の分布からサンプリングになります。
しかし、一番確度が高そうな行動を決定的に動かしたほうがエージェントとしては(おそらく)強くなります。

train.pyのL596付近

                            for p in self.env.players():
                                if random.random() > 0.5:
                                    args['model_id'][p] = -1
                                else:
                                    args['model_id'][p] = self.model_era
                                    is_self_model = True
                                    players.append(p)

                                if random.random() < 0.05:
                                    determistics.append(True)
                                else:
                                    determistics.append(False)

                            args['determistics'] = determistics

これによりdetermisticsに各プレイヤーが決定的に動くかどうかを入力しています。
(実装例は5%の確率で決定的に動作するエージェントになる)

また、generation.pyのL53の次の箇所を修正します。

                        if args['determistics'][player]:
                            action = legal_actions[np.argmax(p[legal_actions])]
                        else:
                            action = random.choices(legal_actions, weights=softmax(p[legal_actions]))[0]

これにより、determisticsフラグがTrueであれば、決定的に動作するようにしています。

感想

今まで強化学習を行うとなれば、複雑なライブラリや実装を読み解く必要があり、少し改造するにも大変だと感じていました。
しかし、HandyRLは非常にシンプルな構成で実装も読みやすく、扱いやすかったです。
シュミレーションコンペなどで強化学習を行う場合はこのライブラリを積極的に使うのが今後良さそうに思えます。

最後にこれを利用したのはHandyRLチームが積極的にHungryGeeseで学習の条件やNotebookを公開いただき、シュミレーションコンペと強化学習への障壁を下げてもらったからでした。
今回、HungryGeeseでフルに活用させていただき、最新の強化学習のキャッチアップ含め、勉強になりました。ありがとうございました。

新しくKaggle用のマシンを調達しました(2020年ver)

皆さんこんにちは
tereka114です。本日はKaggle Advent Calendar24日目の記事です。
今回はKaggle用マシンで購入したものとパーツ選びの基準をご紹介します。

qiita.com

Kaggle用マシン

Kaggle用マシンがなぜ必要か

Kaggleをするためにはノートパソコン一台でも十分です。
しかし、勝つには、最近の巨大データ、画像、NLPコンペの増加に伴いハイスペックPCが必要になってきました。
特に画像やNLPでは、CNN、BERTと呼ばれるニューラルネットワークの手法がソリューションを締めており、
普通の特徴量抽出の手法では勝てなくなってきています。

そのため、それなりの人たちの多くはGPUがあるマシンを持っていることが多くなりました。

クラウドがあるのではないか?といった疑問を持たれた方もいると思いますが、
はっきりいってオンプレ運用より高くなるので緊急時以外おすすめしません。

どのような要件のマシンが必要か

高性能なCPU、メモリ、GPUです。といっても元も子もないので、
どのようなところに影響するのかはパーツの選定の箇所で説明します。

パーツの選定

ここでは実際に組み上げたPCのパーツの紹介をしつつ、選定基準を記載していきます。

基本方針

CPU、GPU、メモリは良いものを選定するのは基本です。
しかし、意外に見落としがちなのはマザボと電源です。
この2パーツはとにかく、故障したときに原因がわかりにくいもので相当曲者です。
ここで失敗すると精神衛生が悲惨です。

各々のパーツ

CPU

lightgbmとテーブル加工の処理、画像の前処理で利用します。
画像処理のあるあるですが、良いGPUだけを準備してもCPUがボトルネックになれば高速化されないので、GPUに合わせてCPUも良くする必要があります。
可能な限りスペックをよくしたいなーと思っていたので、次のCPUを選択しました。
10コア20スレッドです。

メモリ

Kaggleでメモリが必要になるケースは概ね2パタンです。

  • 巨大なテーブルデータの場合、ほとんどのデータをメモリに載せる必要があるので
  • 高速化のためのキャッシュ、画像コンペだとすれば画像を読む、加工、廃棄をすることになりますが、廃棄せずメモリに持てると性能が向上します。

GPU

RTX3090です。私が参加するコンペの多くは画像、もしくは、ニューラルネットワーク
勝つソリューションが多いので、ここは譲れないところでした。

MSI GeForce RTX 3090 GAMING X TRIO 24G グラフィックスボード VD7347

MSI GeForce RTX 3090 GAMING X TRIO 24G グラフィックスボード VD7347

  • 発売日: 2020/09/24
  • メディア: Personal Computers

GPUの選定ですが、基本は性能とメモリです。
Kaggle界ではメモリの方が重要視されている気がします。

巨大モデルである程度のBatchsizeを担保しながら動かさないといけないので、11GBでは足りなくなってきていると感じています(MixedPrecisionも使っていますが厳しい)
そのため、24GBある3090を首を長くしてお待ちしていました。

ちなみに、普通の機械学習の勉強とかであれば、ColaboratoryにもGPUあるので、そういったところにGPUはおまかせするのも一つの手だと思います。

SSD

あまり考えずに選定していますが、最近のデータセットの規模から2TBは最低あっても良いと思います。
少ないとストレスなので、可能であれば大きめのものを買うと良いでしょう。

電源

先程簡単に説明しましたが、電源とマザボは最重要パーツの一つです。
というのも、電源とマザボが故障すると原因の調査が難しいものの一つで、問題が起こると、
一度総解体する必要も出てくるぐらい厄介なパーツです。

電源において重要なポイントとしては消費電力のギリギリのW数の電源を購入するのはいけません。
電源自体が劣化すると出力も下がり、運用していて落ちるようになります。
後々の平穏のためにも決してケチってはいけません。

そのため、200-300Wほどは余裕を持っておくことをおすすめします。

マザーボード

電源の箇所で紹介しましたが、最重要パーツの一つです。
可能な限り高めのもの(1万超え-3万程度)のものを購入するようにしています。
私のマザボ購入時のチェックはCPUの型番に対応しているか(これ対応していないと購入し直しです)。
メモリのスロットが何枚あるか、PCI-Eのスロットはどの程度あるかは確認しています。

私はよくASUSのものを購入しています。電源がついているかどうかなどわかりやすいので、気に入っています。

ASUS INTEL Z490 搭載 LGA1200 対応 ROG STRIX Z490-E GAMING 【 ATX 】

ASUS INTEL Z490 搭載 LGA1200 対応 ROG STRIX Z490-E GAMING 【 ATX 】

  • 発売日: 2020/05/21
  • メディア: Personal Computers

PCケース

タワー、ミドルタワーそしてフルタワーの種類があります。
GPU指す場合はフルタワーのものをおすすめです。でかいので余裕があります。

ファン

Intelの標準装備はあまり性能が良くないので必ず外部で購入するようにしています。
巨大ですが、それなりにコスパが良いものを使っています。
私は虎徹を購入するのが標準的なので今回もそれを買いました。

サイズ オリジナルCPUクーラー 虎徹 Mark II

サイズ オリジナルCPUクーラー 虎徹 Mark II

  • 発売日: 2017/06/02
  • メディア: 付属品

最後に

Kaggleを真面目にやりたいと思っている人は購入されることをおすすめします。
多分完成品を購入するよりも自作の方が安いです。

Kaggle Notebookで使えるトリックを紹介します

皆さんこんにちは。
お元気ですか。私はもう小麦そのものは食べる専門です。
本日はKaggle Notebookに関するトリックを紹介しようと思います。

Kaggle Notebook

Kaggle NotebookはKaggle上で動作する実行環境みたいなものです。
Kaggle上で動いているJupyter Notebookと解釈しても差し支えないでしょう。

昨年からこのKaggle Notebookで推論のみ提出コンペが増加しています。
推論のみ提出の嬉しいこととして、実行環境による差分がでないことがあります。
画像や自然言語処理は無限のモデルを生成するためのマシンリソースで殴ると勝ちやすいことがありますが、この場合、アンサンブルのみではなくある程度は工夫が必要になります。
反対に提出物作るのが格段に面倒になります。(ファイルのアップロード、推論コードの修正、コミット、サブミットを手動で行う)

黒魔術紹介

ここからは私が使っているトリックを紹介していきます。

  • コードを呼び出すほうがリソース管理が楽
  • 外部パッケージインストール
  • 例外
  • 小数点以下の結果に注意
  • コミットまでの速度を早くする

コードを呼び出すほうがリソース管理が楽

Kaggle KernelからはJupyter Notebookと同様の方式で実装を呼び出せます。
実装上はただの「!python predict.py」です。
通常のJupyter Notebookだと、コードを書いていくのが一番オーソドックスであり、本来の使い方だと思います。

近年のコンペだとチームマージした場合に問題が起こりやすいと思います。
よくあるのは、Keras/PyTorchの実装が混合するケースです。
その場合、PyTorchやTensorFlowのGPUメモリ開放の方法を実装せねばなりません。
また、GPUの稀に開放されないこともあるので面倒になるため、プロセスの終了をトリガーとすると開放もれがなくなるので便利です。
デバッグや更新が面倒なため、GPUメモリがシビアなコンペで利用します。

※ただし、実装の更新は面倒なので、コマンドライン引数の設定の工夫が必要です。

外部パッケージインストール

コンペによっては外部パッケージをインストールしたほうが良いものもあります。
私がKernelコンペで利用したもののひとつとしてapexがあります。
例えばapexを利用する場合のインストールコマンドは次のとおりです。
その後再起動もいらないので、長時間実行するコンペだと利用可能でしょう。

!cd ../input/apexpytorch && pip install --no-cache-dir --global-option="--cpp_ext" --global-option="--cuda_ext" . && cd -

例外

Kaggle Kernelの提出時に例外が出るケースがあります。
しかし、例外の内容がさっぱりわからないので、解析が難しいです。
例外が見えすぎてもコンペ参加者がTestを例外を使ってハックするという事が起こるため、どこまで参加者に見せるかは悩ましい問題です。
私が見つけたものだと、このあたりです。

Exception 説明
Notebook Exceeded Allowed Compute メモリのリークなどが原因でプロセスが落ちた場合
Submission Scoring Error 提出形式に不備があった場合、スコアが計算できなくて例外が発生
Notebook Timeout 計算時間オーバー
Notebook Threw Exception 例外発生時の問題(よくわからない)

小数点以下の結果に注意

Kaggle Kernelを利用して提出する場合、基本例外が発生してもよくわかりません。
Google QAのコンペ(Spearman's rank correlation coefficient)では、出力結果によりかなりの例外が発生することを確認していました。
数式的には均等な値(1しかない)でなければ例外が発生しうる要因はなかったので、疑問に思っていました。

調べたら小数点の桁をうまく調整すると回避できたので、それが原因でないかと推測し、対処できました。
殆ど使う機会はないのかなとは思っていますが、頭の片隅に残しておくと役に立つ日が来るでしょう。

www.kaggle.com

コミットまでの速度を早くする

週利用の制限時間が42hしかないため、実行時間の無駄使いはコンペとしては致命傷になりうります。
そのため、一部の処理をスキップしたり早めることで、コミット完了までの時間を早められます。

提出前後(submit)でそれはデータセットの長さに変化があります。
つまり、データセットの長さで変化するような実装を入れておけば短くできるということです。
次の例では、sample_submission.csvを使っていますが、この長さが変わらないこともあるようです。
その場合は別の方法でデータ数を取得するのが望ましいでしょう。

例えば、こんなコードを書けば良いです。

import pandas as pd
submission_df = pd.read_csv("sample_submission.csv")
if len(submission_df) < 11: # データ数で判断する。他の方式でも可能
     submission_df.to_csv("submission.csv", index=False)
     exit()
# 以降後続の処理を記載する。

最後に

Kaggle Notebookは不便なところも多いため、工夫して使わないと厳しいです。
ただ、工夫しがいはあるので、個人的には好きなコンペの開催形式の一つです。
懲りずに参加しようかと思います。

種類が豊富な画像モデルライブラリpytorch-image-models(timm)の紹介

皆さんこんにちは
お元気ですか。私は小麦を暫く食べるだけにしたいです。(畑で見たくない‥)

さて、本日は最近勢いのあるモデルのライブラリ(pytorch-image-models)を紹介します。

pytorch-image-modelsとは

通称timmと呼ばれるパッケージです。
PyTorchの画像系のモデルや最適化手法(NAdamなど)が実装されています。
Kagglerもこのパッケージを利用することが増えています。

github.com

従来まで利用していたpretrained-models.pytorchは更新が止まっており、最新のモデルに追従できていないところがありました。
このモデルは例えば、EfficientNetやResNeStなどの実装もあります
モデルの検証も豊富でImageNetの様々なパタンで行われているのでこの中で最適なものを選択すると良いでしょう。詳しくはこちらへ。

github.com

インストールはpipから可能です。

pip install timm

モデルの利用方法

モデルの利用方法は簡単です。
timmモジュールのcreate_modelを用いることで、指定されたモデルを取得できます。
各モデルには、次のメソッドが実装されています。

forward・・・モデル全体の出力を獲得する
forward_features・・・特徴量出力部分(Pooling前)の出力を獲得する

どのようなモデルが実装されているかは、先述のResultsのページをご確認ください。

import timm
import torch
m = timm.create_model('efficientnet_b3', pretrained=True)
m.eval()
matrix = torch.rand(2, 3,224,224)

m(matrix).size() # (2, 1000)
m.forward_features(matrix).size() # (2, 1536, 7, 7)

最後に

非常にモデルの種類が豊富で使いやすいです。
これを利用すればほんの数行でResNeStなど新しいモデルを試すことができるので、これから使っていこうと思っています。