Deep Learning Code Golfとは
Deep Learning Code Golfは、私(select766)が作成した言葉で、「深層学習のモデル定義をできるだけ短いソースコードで表現する」というゲームです。 このゲームを考えたきっかけは、深層学習の新しいモデル構造ConvMixerを提案する論文*1で、手法の簡潔さを主張するためのおまけとして280文字以下で定義できるという参考コード
def ConvMixr(h,d,k,p,n): S,C,A=Sequential,Conv2d,lambda x:S(x,GELU(),BatchNorm2d(h)) R=type('',(S,),{'forward':lambda s,x:s[0](x)+x}) return S(A(C(3,h,p,p)),*[S(R(A(C(h,h,k,groups=h,padding=k//2))),A(C(h,h,1))) for i in range(d)],AdaptiveAvgPool2d((1,1)),Flatten(),Linear(h,n))
がTwitter上で話題となっていたためです。論文自体はコードの長さ自体を短くすることを目標としているわけではありませんが、私はコードを短くすること自体をゲームにしたらどうなるか興味を持ったので考察することにしました。 Deep Learning Code Golf(以下DLCG)の元となったのは、以前より知られているCode Golf(コードゴルフ)というゲームです。ショートコードとも呼ばれます。Code Golfは、特定の課題を解くプログラムを、できるだけ短いソースコードで表現するゲームです。コードゴルフという名称は、スポーツのゴルフのように短い打数(文字数)でゴールすることからつけられたようです。コードゴルフで解く課題の例として有名なのは、FizzBuzz問題です。FizzBuzz問題とは、「1から100までの数を出力せよ。ただし、3の倍数の場合はFizz、5の倍数の場合はBuzz、3の倍数かつ5の倍数の場合はFizzBuzzと出力せよ」という問題です。この問題をPython言語で普通に解くコードは、
for i in range(1, 101): if i % 3 == 0 and i % 5 == 0: print("FizzBuzz") elif i % 3 == 0: print("Fizz") elif i % 5 == 0: print("Buzz") else: print(i)
のようになります。もちろん通常のプログラミングではこれで正解なのですが、Code Golfでは、このコードをできるだけ短くする(コード自体の文字数を少なくする)ことを目指します。例えばPythonではインデントが必須であるもののスペース1個で十分で、%
などの演算子の前後のスペースも不要ですので、元のコードと実行結果を変えずに
for i in range(1,101): if i%3==0 and i%5==0: print("FizzBuzz") elif i%3==0: print("Fizz") elif i%5==0: print("Buzz") else: print(i)
のように書き換えることができます。これにより、コードが196文字から143文字に減少します。文法レベルの書き換えだけでなく、「3の倍数かつ5の倍数」を「15の倍数」と言い換えてロジックを組みなおせば
for i in range(1,101): if i%15==0: print("FizzBuzz") elif i%3==0: print("Fizz") elif i%5==0: print("Buzz") else: print(i)
のように少し短くなります。さらに多くのテクニックを用いると、
for i in range(100):print(i%3//2*"Fizz"+i%5//4*"Buzz"or-~i)
のようなコードになります*2。ここまでくると、理解困難なコードとなるのが分かるでしょう。通常のプログラミングで重視される、コードの理解しやすさや変更のしやすさ、場合によっては実行速度を無視して、コードが短いということそれ自体を追求するのがCode Golfの目的です。 コードゴルフはプログラミング言語の仕様を最大限活用するゲームですので、言語によって使えるテクニックや実現できるコードの文字数が変わってきます。本稿では、Python言語(バージョン3系)を使用します。
従来のCode Golfのほとんどでは、入力が決まれば出力が一意に定まるような課題が扱われてきました。アルゴリズムが違ったとしても、結果自体が変化しないことが条件となります。しかし本稿で扱うDLCGでは、若干異なります。深層学習は機械学習の一種で、画像などを入力し、その中に含まれる文字や物体を認識するというモデルを生成することが課題です。分類正解率が100%であれば一意の出力となりますが、通常は100%にはならず、ある程度の誤りが含まれます。統計的に正解率が一定以上であればよいと考え、具体的に各入力画像に対してどのような出力をするかは一意に定まらないのが普通です。正解率を決める要因は課題自体の難しさのほか、モデルの構造や学習時の手法などに影響されます。モデルの構造に自由度を持たせることがDLCGのユニークな点で、「正解率**%以上のモデルを記述せよ」という課題になります。あまりに単純なモデルだと表現力が足りず正解率が低くなるという傾向がありますので、一定の正解率を担保しつつ、モデルの構造を簡略化したりコードレベルの実装方法を短くしたりすることに戦略性が生まれます。
ルール設計と実行環境
Deep Learning Code Golfがゲームとして成立するためのルールを考えました。
深層学習を用いて解くべき課題の設定
深層学習を用いて解く課題としては、画像を入力とし、10種類の物体のうちどれが写っているかを認識して分類する画像分類問題を扱うこととしました。 より具体的には、CIFAR-10データセットを用います。画像は32ピクセル四方のフルカラー(RGBの3チャンネル)で、分類すべき物体は10種類(飛行機、自動車、鳥…)です。学習用画像が5万枚、テスト用画像が1万枚あります。課題設定として重要なのは、要求されるモデルの複雑さと計算コストです。画像分類の入門課題としてよく用いられるMNISTデータセットは簡単すぎて、極めて単純な構造のモデルでも正解率99%が得られてしまい、差がつきません。逆に、実用に近い課題として用いられるImageNetデータセットは複雑すぎて、学習初期の段階では全く正解できず、学習を数時間進めないと差がつきません。解き方によって短時間で差がつくという基準をもとにデータセットを選定しました。 性質の異なる課題としては、ニュース記事(英単語の列)を入力して記事のジャンルのいずれに該当するかに分類するようなシーケンス処理問題なども考えられます。課題によって適切なモデルの構造が大きく異なるため、面白い課題を考えてみるのもよいでしょう。
次に、課題自体とは別に、ゲームとして成立させるための諸条件を考察します。
プレイヤーが記述するコードの範囲
深層学習を実行するには、モデルの定義だけでなく、学習データの前処理機構、オプティマイザなどいくつかの機構が必須となります。いずれの機構も正解率に影響を与えますし、様々な書き方が考えられます。ゲームとして注力すべき部分がブレないように、プレイヤーが記述すべき範囲と、課題として固定された機構を与える範囲を分ける必要があります。
コードの長さは、文字数で計測しました。アルファベットでも漢字でも1文字にカウントします。改行も1文字と数えます。コードの先頭・末尾の改行文字は無視します。
実行時間
書いたコードで生成されるモデルが所定の正解率を達成できるかどうか評価するには、実際に学習を行う必要があります。深層学習は計算コストが高く、高性能なGPUを搭載した計算機で何日もかかる場合もあります。計算時間がかかりすぎるとゲームとして遊べないため、数分以内に完了することが望ましいと考えられます。後述する実行環境において、合理的な複雑さのモデルが1分程度で学習できるようなデータセット及び学習ループ回数(イテレーション数)を目指しました。
これを満たす制約として、5エポック(学習用画像全部をモデルに投入して、モデルパラメータを更新するサイクルを5回行う)学習させるという条件としました。ただし、モデルの複雑さやパラメータ数自体に制約を設けていないので、極端に複雑なものを定義すればいくらでも計算時間が長くなり得ます。現実的には、表現力の高い複雑なモデルは短いエポック数ではあまり正解率が高くならないことが多く、有力な解答にはなりづらいと考えられます。
テスト用データに対する最適化
機械学習では本来、テスト用のデータは未知のデータであり、テスト用データでの正解率を最適化の指標として使うべきではありません。モデルの学習へフィードバックするための評価用データとテスト用データは分かれているのが正当です。しかしながらゲームとしてこれらの指標を明確に分離するのは難しいため、テスト用データでの正解率が高まるようにモデルのハイパーパラメータを最適化しても構わないものとします。
ルールのまとめ
結論として、以下のようなルールとしました。
- 以下の条件設定で学習させた際に、テスト用データでの正解率X%以上となるコードを記述せよ。
- X=10,20,...
- プレイヤーが記述するコードの条件
- 引数のない関数
m
を定義する。実行されるとPyTorchのモジュールオブジェクトを返す。 m
の内部で必要なモジュールは、コード内でインポートする。
- 引数のない関数
- 学習条件
データオーグメンテーション(学習用)
torchvision.transforms.Compose([ torchvision.transforms.RandomCrop(32, padding=4), torchvision.transforms.RandomHorizontalFlip(), torchvision.transforms.ToTensor(), pl_bolts.transforms.dataset_normalizations.cifar10_normalization(), ])
データオーグメンテーション(テスト用)
torchvision.transforms.Compose([ torchvision.transforms.ToTensor(), pl_bolts.transforms.dataset_normalizations.cifar10_normalization(), ])
実行環境
環境構築および起動に手間がかかるとゲームとして遊びにくいです。本稿では、深層学習向けに作られたクラウド上のインタラクティブなPython実行環境であるGoogle Colabを活用することとしました。Pythonの標準ライブラリだけで深層学習を行うのは事実上不可能なので、深層学習ライブラリとしてPyTorchを用いました。Google Colabでは初期状態でインストール済みです。さらに、学習ループの記述を簡略化するためPyTorch LightningおよびLightning Boltsを用いました。ただしこれらのライブラリの使用によってプレイヤーの書くべきコードに変化はありません。モデル定義のコードを文字列として評価用の関数に与えると、その文字数と学習結果得られたモデルの正解率を算出するような環境を整備しました。なお、深層学習ライブラリとしてTensorflowを用いれば、クラス名の長さに違いがあるため異なるモデル構造が最適になる可能性があります。
再現性について
深層学習では、モデルパラメータを初期化する乱数や、モデルに与えるデータの順序などにより学習結果に違いが出ます。これらの数学的な違い以外にも、GPU上での並列計算の実行順序に伴うハードウェアレベルの非決定的な挙動があり、完全な再現は難しいのが実情です。本稿の実行環境では以下のコードにより乱数の固定を試みました。同じハードウェア・ソフトウェアのバージョンで複数回実行したところ同じ結果が得られるようでした。一方で環境が違う場合までは保証できないため、賞金が出るようなコンペを実施するには難しい点となります。
pytorch_lightning.seed_everything(1, True) torch.backends.cudnn.deterministic = True torch.backends.cudnn.benchmark = False torch.use_deterministic_algorithms(True)
本稿執筆時点でのライブラリのバージョンを下表に示します。今後掲載するコード例では実験的機能も利用しているため、将来のバージョンでは同じコードが使えない可能性があります。
ライブラリ | バージョン |
---|---|
Python | 3.7.12 |
torch | 1.10.0+cu111 |
torchvision | 0.11.1+cu111 |
pytorch_lightning | 1.5.1 |
pl_bolts (lightning-bolts) | 0.4.0 |
実行環境のColab notebookは https://colab.research.google.com/drive/1KGZFdERNGVAofSCEBnEMfeRENOpWcCUA?usp=sharing にて配布しています。また、書いたコードを投稿できるサイトを試作しました。 https://dlcodegolf.web.app/ からアクセスできます。
次回は、このルールに沿って私が実装してみたコードを解説します。
2022-01-22修正: 「再現性について」で再現性をより向上するよう改良
*1:Patches Are All You Need? (執筆時点では査読中の論文で、匿名) https://openreview.net/forum?id=TVHS5Y4dNvM
*2:https://qiita.com/ymg_aq/items/b8e5d26035180bc8797e#python3-59-bytes