おわりに - なぜ機械学習はうさん臭く感じられるのか?

本講座では計8回にわたり、ディープニューラルネットワークの原理と実装について 説明してきた。ニューラルネットワークの原理は基本的には 勾配降下法であり、その基盤となっているのが関数の微分可能性である。 ニューラルネットワークにはさまざまな形態が存在するが、 画像処理・画像認識の場合は畳み込みニューラルネットワークが非常に 有効であることがわかっている。また、ニューラルネットワークの 出力形式や損失関数を変えることにより、ニューラルネットワークが 物体検出や奥行き推定など、さまざまなタスクに利用可能であることを紹介した。

さて、本講座は「真面目なプログラマのための」ディープラーニング入門、 と銘打っている。真面目なプログラマとは何か? 諸説いろいろあるだろうが、 多くのプログラマは、ソフトウェア開発において 仕様の明確さや、 システムの効率・堅牢性、そして 保守のしやすさといったものを 追求するであろう。このようなプログラマにとって、 ニューラルネットワーク (および機械学習全般) が とっつきにくいと感じる理由はいくつかある:

仕様のわかりにくさ
たいていのソフトウェアは 「正しい挙動は明確に定義可能である」という想定のもとに作られている。 間違ったコードはエラーを発生させたり、間違った結果を出力するので、 プログラマの多くは、バグは (十分な時間さえあれば) いずれ発見・修正が可能であろうと考えている。 しかし機械学習の場合、この想定は成り立たない。 なぜなら機械学習のモデルは完璧ではないため、 正しいコードがつねに正しい解を出すとは限らないからだ。 逆に間違ったコードが「正しそうな解」を出すこともある。 たとえば以下の例は backward() メソッドをわざと間違って書いてある (第3回で使われた数式を正確に実装していない)。 しかしこれを使って訓練しても損失は少しずつ低下し、 エラーも出ないため、すぐにはバグに気づかない:
# 間違ったレイヤーの実装 (でも、なんとなく動作する)
class BuggyLayer:
    ...

    def backward(self, delta):
        # self.y が計算されたときのシグモイド関数の微分を求める。
        ds = [ y1*(1-y1) for y1 in self.y ]
        # 各偏微分を計算する。
        for i in range(self.nout):
            for j in range(self.nin):
                self.dw[i][j] += delta[i] * ds[i]
        for i in range(self.nout):
            self.db[i] += delta[i] * ds[i]
        return self.x
仕様の流動性
上のようなバグはアルゴリズム (数式) の単純な実装ミスであり、まだ修正は比較的容易である。 しかし機械学習においてはこのアルゴリズム自体の正当性が曖昧なため、 たとえそれを正しく実装できたとしても、真の意味で「正しいアルゴリズム」かどうかは わからない。なぜなら、機械学習における「正しさ」とは、 せいぜい「実行してみて精度が高いもの」という程度の定義しかないからだ。 たとえば、あるニューラルネットワークで畳み込みレイヤーをいくつ重ねればよいのか? という疑問に対しては、決まった規則や答えがなく、 試行錯誤によって答えを見つけるしかない。 さらに、ニューラルネットワークの世界では、研究者にとっても 「なぜこれで精度が上がるのかよくわからない」方法や、 「どちらが本当にいい方法なのかよくわからない」テクニックが数多く存在する。 そしてこれらの意見は数年のスパンで変わることがある。 例をあげると: などである。
品質の悪い実装
以上のように、多くのニューラルネットワークの仕様は流動的なので、 開発者の多くは「何が正しいのか」よくわからないままに 開発することになる。すると、試行錯誤の跡がそのままコードに残されることが多い。 つまり無駄なコードや効率の悪いロジックが放置されることになる。 しかも文書化もおざなりで、十分なテストケースも提供されていないことが多い。 「正しい仕様」がはっきりしない状況では、 こうしたコードを適切にリファクタリングすることは難しく、 したがってシステムの効率化や信頼性の向上といった作業も難しくなる。
フレームワークの不安定さ
まさに上に述べたことを体現するかのように、 多くの機械学習フレームワークは「堅牢なソフトウェア」とは 言いがたいものが多い。たとえば PyTorch のモデルを正しく動かすためには、 以下の要素がすべて揃っている必要がある: よくできたフレームワークというものは、たいてい後方互換性が保たれるように 作られている。したがって、バージョンの差はそれほど問題とはならない。 しかし機械学習フレームワークは後方互換性が保たれない変更が されることがしばしばあり (最近は安定してきている)、 それぞれのフレームワークが「特定のバージョンのライブラリ」に 依存していることが多い。また、個々の構成要素 (PyTorch/Python/CUDA) は それぞれ勝手なタイミングで更新されるため、 うっかりバージョンアップしたら以前のモデルが 動かなくなった、ということが頻繁にある。 (本講座の最初で 「Python の最新バージョンを使わないように」と 警告したのはこのためである。)
標準化の欠如、コミュニティの分断
本講座では機械学習フレームワークとして PyTorch を使ってきたが、 もうひとつ有名なフレームワークとして TensorFlow がある。 本講座で PyTorch を使っているのは、ひとえに筆者の好みによるものである。 機械学習フレームワークは日々、新しいものが現れているが、 その多くは似たような機能をもっており、車輪の再発明が多い。 また、公開されているモデルや論文などの実装はたいてい ひとつのフレームワークでしか動作しないため、 「別フレームワークによる同一アルゴリズムの実装」を作るという 試みがさかんに行われている。さらに (本講座も含めて) 「学習のためにあえて自力で作製した」実装が非常に多く出回っており、 たとえば GitHub で PyTorch で実装した YOLO を検索してみると、1,000以上のリポジトリがヒットする (TensorFlow ではさらに多くの実装が存在するようだ)。 YOLO のような有名なアルゴリズムさえ「定番の実装」が存在しない状態では、 これを基盤としたシステムを構築することにはリスクを伴う。 ONNX形式のような試みはなされているが、 まだそれほど普及しているとはいえない。 このような標準化の欠如、およびそれによる コミュニティの分断は現在の機械学習における大きな課題である。

まとめると、機械学習 (および、それをとりまく環境) は、 真面目なプログラマの足を引っぱる要素が多いのである。 とくにニューラルネットワークは上記のように不確定な要素が多く、 率直にいって使わないに越したことはない。 結局のところ、本講座は「真面目なプログラマが、 ニューラルネットワークという不安定要因からシステムを守るための予備知識」 と言えるかもしれない。機械学習・ニューラルネットワークはたしかに 役に立つ技術といえそうだが、それは現代の巨大な 情報システムの中のごく一部にすぎない。ソフトウェア全体にわたる 総合的な知識なしには、機械学習が真の意味で世の中を良くすることは ありえないのだ。ソフトウェアに対して「真面目」でありつつ、 なおかつ機械学習について語れるエンジニアが今後必要とされていくであろうと思われる。


クリエイティブ・コモンズ・ライセンス
この作品は、クリエイティブ・コモンズ 表示 - 継承 4.0 国際 ライセンスの下に提供されています。
Yusuke Shinyama