跳到正文
Morikatron · 游戏AI开发者博客· 一手来源· morika-ma2·· 6 天前精选AI 评分79

『がんばれ森川君2号』的AI拥有5个“大脑”

『がんばれ森川君2号』のAIは、5つの脳を持っていた

推荐理由

原作者复盘三十年前在初代PlayStation上实现五组神经网络的取舍,定点运算与学习循环嵌入游戏的做法仍有借鉴意义。

正文 · 原文

こんにちは、モリカトロンCTOの松原です。

先日古いストレージの整理整頓をしていたところ、30年近く前に発売されたPlayStation 用ソフト『がんばれ森川君2号』のソースコードや開発資料のアーカイブが出てまいりました。現モリカトロン社長の森川のアイデアをゲームとして実装したものですが、プログラム開発は私一人が担当しており(当時は複数プログラマによる共同開発がとても苦手だったので……)、ゲーム本編のコードのほかにも、モデルデータやサウンドデータの読み込み・整備・変換・ステージのエディットツール(これはmacで動いてた)など、すべてがコテコテの自作でした。当時はOpen SourceもAIコーダーもGoogle検索さえ存在しないのに、よくまあ一人で書いたものだと、あきれてしまいます。

さて、そんなプロジェクトのコード一式が出てきましたので、ちょっとどんなだったかな、と興味津々でのぞいてみました。

コードを書いた本人のはずなのに、忘れていたことが山ほどありました。本稿はその記録です。 結論を先に書くと、 いちばん面白かったのは学習アルゴリズムそのものではなく、 「うまく学習させないための設計」がコードと仕様書の両方に残っていたことでした。

相手は、主記憶2メガバイトで浮動小数点演算装置のない計算機

まず、動かす相手の性能を確認しておきます。記憶で書くと間違えるので、資料に当たり直しました。

項目 初代 PlayStation の仕様
主記憶 2メガバイト。うち先頭64キロバイトはカーネルが予約するので、ゲームが使えるのは約1.94メガバイト
中央演算処理装置 MIPS R3000A 系
浮動小数点演算装置 搭載されていない(COP1の枠は空のまま)
幾何演算コプロセッサ GTE。こちらも32ビットの固定小数点で動く
描画 毎秒30フレームないし60フレームを維持する必要がある

浮動小数点演算装置とは、小数の計算を専門に行う回路のことです。 いまのパーソナルコンピュータやスマートフォンには当然のように入っていますが、 初代 PlayStation の中央演算処理装置にはこれがありません。 コプロセッサ1番の枠は浮動小数点演算装置のために用意されていたのですが、そこは空のままで、 代わりにコプロセッサ2番へ幾何演算専用の回路(GTE)が載っていました。 その GTE も浮動小数点には対応せず、32ビットの固定小数点で動きます。

つまり、機械の側が固定小数点を前提に設計されていたわけです。

予約された64キロバイトには、アドレッシングモードで直接触れる「貴重な先頭32キロバイト」が 含まれています。最適化の観点では、いちばん惜しい場所を持っていかれている格好です。

ニューラルネットワークの計算は、実質的に小数の掛け算と足し算の塊です。 さらに活性化関数として使われるシグモイド関数には指数関数が含まれます。 普通ならここで諦める条件です。

脳は5つに分かれていた

読み直して最初に気づいたのは、ネットワークが1つではなかったことです。 役割の異なる5種類が独立して存在し、そのうち2種類は10個ずつ複製されていました。

規模は、起動時の検証コードから確定できました。 それぞれの脳の入力数と出力数が想定どおりかを確認し、 違っていたらエラーを表示する処理を入れてあったので、そこに期待値が残っていたのです。 動作確認用に書いたコードが、30年後にいちばん役に立ちました。

脳 入力 出力 個数 担当
第1 5 5 1 方針の決定(本能)
第3 7 3 10 対象物の印象。「食べたい」「快」「不快」を出す
第4 7 7 10 行動の選択
第5 3 3 1 本能による行動
第7 4 1 1 背景音楽の選択

すべて入力層・中間層・出力層の3層構造で、中間層は1つだけです。

第2と第6が欠番になっています。構造体に定義そのものがないので、 試作して捨てたのか、番号だけ確保していたのか、いまとなっては思い出せません。

全体はこう繋がっています。

  視界にある物体を1つずつ
         │
         ├─→ 第3の脳(10個から最も近いものを選ぶ)
         │        └─→ 食べたい / 快 / 不快
         │                    │
         │              全物体ぶん合計
         │                    ↓
         │            環境印象度(3つの値)
         │                    │
         │              + 体調(2つ)
         │                    ↓
         │              第1の脳 ──→ 方針(5つから選択)
         │
         ├─→ 第4の脳(10個から最も近いものを選ぶ)──→ 行動(7つから選択)
         │
         └─→ 第5の脳(入力は第3の出力)──→ 本能行動(3つ)──→ 実際のコマンド

  体調(2つ)+ 性格(2つ)──→ 第7の脳 ──→ 背景音楽の種類(4段階)

規模を見て「小さい」と思われるかもしれません。実際に小さいです。 しかし主記憶が2メガバイトしかなく、そこに映像も音もプログラム本体も載せる必要があります。 この規模は妥協ではなく、与えられた条件のもとでの設計判断、ということですね。 同条件ならば、いま設計しても同じような規模にせざるをえないと思います。

なお、それぞれの脳が何を担当していたかは、コードを追うだけでは分かりませんでした。 決め手になったのは、当時の自分がローマ字で書き残していたコメントです。 「本能を方針決定に使う」「本能による行動を得る」といった短い注記が、 そのまま役割の説明になっていました。 第7の脳にだけはコメントがなく、呼び出し箇所を探して前後を読んで、ようやく用途が分かりました。

入力は「感覚」として設計されていた

同じ場所から、当時の設計資料も出てきました。 「因子-印象リスト」という表計算ファイルで、 ネットワークの入力ユニットに何を割り当てるかを日本語で定義したものです。

こういう項目が並んでいます。

  • 視覚: グレー、光っている、点滅している、不規則な模様、規則的な模様、丸みがある、生き物的、デコボコで不定形
  • 嗅覚: 食べ物系、気持ちよい匂い、生臭い
  • 聴覚: 心地よい音、気に障る音、ひどく大きな音
  • 動き: 激しい、人工的、不規則、恐い動き、妙な動き
  • 温度: 非常に熱い、暖かい、涼しい、非常に冷たい
  • 作用力: 電磁気、とげ、毒など、不快な物理的刺激

別の資料では、1つの入力ユニットが5段階の強度で定義されていました。 音の軸なら「すごく心地よい音/心地よい音/気にならない音/気に障る音/すごく気に障る音」、 形の軸なら「丸っぽい/丸と三角の中間/三角っぽい/三角と四角の中間/四角っぽい」という具合です。

これは特徴量の設計そのものです。 現在の機械学習でも入力の特徴量をどう作るかが性能を左右しますが、 1990年代の家庭用ゲーム機の上で、 それを「生き物が世界をどう感じるか」という言葉で設計していたことになります。

環境の印象は、目に入る物すべての印象を足したものだった

第1の脳(方針決定)の入力になる「環境印象度」の作り方が、はっきり分かりました。

環境印象度を計算する:
    3つの合計値を 0 で初期化する

    視界にあるすべての物体について繰り返す:
        その物体の特徴(7つ)を組み立てる
        10個ある第3の脳から、その特徴にもっとも近いものを選ぶ
        選んだ脳で前向き計算をする
        得られた「食べたい」「快」「不快」を、それぞれ合計に足す

    3つの合計値が、いまいる場所の印象になる

個々の物への印象を足し合わせると、その場所全体の印象になる。 素直で、生き物の感じ方としても納得のいく設計です。

そしてこの3つの値に体調2つを加えた5つが第1の脳に入って、「方針」が決まります。 方針の出力は5つあり、選べない方針にはあらかじめ無効値を入れて候補から外し、 残ったものから最大値を選ぶ、という使い方をしていました。

浮動小数点演算装置がない機械で、どう計算したか

浮動小数点は「使えなかった」のではなく、測って捨てた

ここは書く前に確かめ直したところです。

ソニーが配っていた開発キットには、浮動小数点をサポートする数学ライブラリが入っていました。 三角関数、対数、指数関数、形式変換をひととおり提供するもので、 浮動小数点を使いたければ使う手段はあったのです。

ただし、ハードウェアに浮動小数点演算装置がない以上、実体はソフトウェアによる模倣です。 当時から「ソフトウェア浮動小数点は極端に遅く、コードも膨れるので、 何としても避けるべき」と言われていた類のものでした。

私の記憶では、固定小数点で書いたものと実測して比べたら速度差が桁で違ったので、 固定小数点版を採用したはずです。当時の測定記録は残っていないので、 そこは「たしかそうだった」という程度の話ですが、 選択肢がないから固定小数点にしたのではなく、両方あったうえで選んだというのが実際のところでした。

32ビットの固定小数点で、数値演算基盤から自作する

採用したのは 32ビットの固定小数点数でした。 上位16ビットを整数部、下位16ビットを小数部として扱う形式です。

32ビットの整数を1つ用意する
    上位16ビット = 整数部分
    下位16ビット = 小数部分(65536分の1が最小単位)

掛け算:
    2つの値を64ビットに拡張して掛け、結果を16ビット右にずらす
割り算:
    割られる値を16ビット左にずらしてから割る

この形式のための演算ライブラリ(掛け算、割り算、指数関数)を一式自作したうえで、 ニューラルネットワークをその上に載せていました。

シグモイド関数をテーブルで妥協していなかった

読み返して自分でも驚いたのが、ここです。

活性化関数はシグモイド関数、すなわち「1 ÷ (1 + eの-x乗)」です。指数関数が入っています。

処理速度が厳しい環境では、この関数をあらかじめ計算して表にしておき、 入力値から表を引いて近似するのが定石です。速度は劇的に速くなりますが、 表の刻み幅の分だけ精度が落ちます。

当時の私は、その定石を採っていませんでした。 指数関数を連分数展開で真面目に計算していました。

指数関数を計算する(連分数展開):
    入力の2乗を求める
    ある初期値から始めて、次の操作を数回繰り返す:
        作業値 = 入力の2乗 ÷ (作業値 + 定数)
        定数を4ずつ減らしていく
    最後に補正して返す

連分数展開は、割り算を入れ子にして関数を近似する手法です。 表引きより計算量は増えますが、少ない項数で精度が出ます。 速度が最優先のはずの家庭用ゲーム機で、精度のほうを取っていたわけです。

当時これをどこで覚えたかというと、たぶん書籍だと思います。 インターネットで論文もサンプルコードも引ける時代ではなかったので、 アルゴリズムの事典を1冊買ってきて、そこから起こしていたような記憶が、ちょっとだけあります。

学習の更新式は教科書どおり

学習は誤差逆伝播法で、慣性項つきでした。

重みの更新:
    今回の更新量 = 学習率 × 誤差信号 × 入力側の出力
                 + 慣性係数 × 前回の更新量
    重み = 重み + 今回の更新量

慣性項は前回の更新量の一部を今回に持ち越す仕組みで、 学習を安定させ、局所解にはまりにくくする効果があります。 固定小数点で実装すると桁あふれの管理が面倒になる部分ですが、省略していませんでした。えらいですね。

学習でゲームを止めない

実装として、もっとも実務的だったのがここです。

ニューラルネットワークの学習は、収束するまで何度も繰り返す処理です。 何回で終わるかは事前に分かりません。 これを毎秒30フレームで動いているゲームの中で素直に実行すると、 学習が長引いた瞬間に画面が固まります。

そこで、学習のループの内側からゲーム側の更新処理を呼んでいました。

学習を行う(対象の脳, 最大繰り返し回数, 目標誤差):
    繰り返し回数の上限まで、以下を繰り返す:

        ゲーム側を1フレーム進める      ← 学習ループの内側から呼ぶ

        保持しているすべての学習サンプルについて:
            1サンプルぶんの学習を行う

        すべてのサンプルの誤差を合計し、平均を求める
        平均誤差が目標を下回っていれば、そこで打ち切る

1エポックごとに描画と入力処理へ制御を戻しています。 学習に何秒かかっても、プレイヤーから見れば画面は動き続けます。

論文にも教科書にも書かれていない種類の工夫です。 「学習には時間がかかる」という性質を、 リアルタイムで動き続ける製品の上でどう飼い慣らすか、という問いへの回答になっています。

10個の脳から「いちばん近いもの」を選ぶ

第3と第4の脳は10個ずつ用意していました。 これは同じ計算を10回するという意味ではありません。 入力に対して、10個のうちもっとも近いものを選んで使う構造です。

もっとも近い脳を探す(脳の配列, 個数, 入力):
    それぞれの脳について:
        その脳が保持している学習サンプルと入力を比較し、差の合計を求める
    差の合計がもっとも小さかった脳を選ぶ
    選んだものを「直前に使った脳」として記録しておく

入力と完全に一致するサンプルを持つ脳があるかを判定する処理も、別に用意していました。

結果として、経験の種類ごとに別々の脳が育っていくことになります。 1つの大きなネットワークにすべてを詰め込むのではなく、 小さなネットワークを複数持たせて使い分ける方針です。 主記憶が2メガバイトの環境では、この分割は理にかなっています。

さらに、脳が「混乱している」かどうかを判定する処理もありました。 全サンプルの平均誤差を見て、学習が破綻していないかを確認するものです。

音楽もニューラルネットワークが選んでいた

今回読み直していちばん意外だったのが、第7の脳です。 正直なところ、こんなものを作ったことすら忘れていました。

背景音楽を選ぶ:
    入力[0] = 体調のパラメータ
    入力[1] = もう1つの体調のパラメータ(反転)
    入力[2] = 性格のパラメータ
    入力[3] = もう1つの性格のパラメータ

    第7の脳で前向き計算をする

    出力が 0.75 以上なら 音楽の種類A
    0.50 以上なら        音楽の種類B
    0.25 以上なら        音楽の種類C
    それ未満なら         音楽の種類D

体調と性格をニューラルネットワークに通して、出力ひとつの値で背景音楽を4種類に振り分けています。

つまり、育て方によって性格が変わると、流れる音楽も変わります。 プレイヤーに説明される仕様ではありませんが、 ロボットの内面が音楽という形で外に出ていたことになります。

本題 — 正しく学習させないための設計

ここまで実装の話をしてきましたが、このプロジェクトでいちばん重要な部分は、 おそらく技術的な工夫ではありません。

このゲームの人工知能には、通常の機械学習とは正反対の要求仕様がありました。

  • すぐに覚えてしまったら失敗
  • 誤解しなかったら失敗
  • 教えたとおりにしか覚えなかったら失敗

学習が速く、正確で、汎化性能が高いこと。 機械学習の文脈では、これらはすべて「良いこと」です。 ところが育成ゲームとしては、そのすべてが製品の失敗を意味します。 飼っているロボットが一度で完璧に言うことを聞くようになったら、 プレイヤーには何も残りません。

そもそもこの種の開発には、「正しく動いている」という基準が存在しません。 学習処理にはあらかじめ決まった正解がないので、 出力が想定と違っても、それがバグなのか学習の結果なのかを切り分けられないのです。

では、どうやって「ちょうどよく誤解する」状態を作ったのか。

学習アルゴリズムをいじるのではなく、世界の側を調整していました。 入力として与える感覚の値や、対象物が持つ性質のほうを動かして、 ロボットが適度に勘違いするように仕向けるという方法です。

いまの言葉でいえば、モデルではなく環境と報酬の設計にあたります。 強化学習で報酬関数の設計が本質的に重要であることは今では広く知られていますが、 1997年の育成ゲームの現場では、 製品として面白くするために、すでに同じところに手を入れていたことになります。

30年後に、自分の書いたコードを読み返して

読み直して分かったことが2つあります。

ひとつは、当時の判断が制約に対する答えとして筋が通っていたこと。 浮動小数点演算装置がないから固定小数点の演算基盤を自作し、 主記憶が足りないからネットワークを小さく分割し、 リアルタイム性を守るために学習ループからゲームループを呼ぶ。 どれも、その環境で製品を出すという条件から素直に導かれる判断でした。

もうひとつは、自分がいかに忘れているかということ。 5つの脳がそれぞれ何をしていたか、 特に第7が音楽を選んでいたことは、コードを見るまで完全に忘れていました。 30年前の自分が残したコメントに助けられて、ようやく思い出せた格好です。 将来の自分は他人だと思って書いておくものだと、あらためて思いました。

そして、いま読んで価値があると感じたのは、 「学習がうまくいきすぎるとゲームとしては失敗する」という価値基準の反転のほうでした。 これは計算機の性能とは無関係で、いまでもそのまま通用する問題です。

人工知能をゲームに載せるとき、私たちはつい「賢くする」ことを考えます。 しかし遊びとして成立させるには、どこで賢くならないようにするかを設計しなければならない。 30年前のソースコードは、そのことを実装の形で残していました。

来源:Morikatron · 游戏AI开发者博客 · tech.morikatron.ai