『がんばれ森川君2号』的AI拥有5个“大脑”
『がんばれ森川君2号』のAIは、5つの脳を持っていた
原作者复盘三十年前在初代PlayStation上实现五组神经网络的取舍,定点运算与学习循环嵌入游戏的做法仍有借鉴意义。
大家好,我是 Morikatron 的 CTO 松原。
前几天整理旧存储时,我翻出了近 30 年前发售的 PlayStation 软件《がんばれ森川君2号》的源代码和开发资料存档。这是把现任 Morikatron 社长森川的想法实现成游戏的作品,但程序开发由我一人负责(当时我非常不擅长多名程序员共同开发……),除了游戏本篇的代码之外,模型数据和声音数据的读取、整理、转换、关卡编辑工具(这个是在 mac 上运行的)等等,全都是地地道道的自制。当时既没有 Open Source,也没有 AI 编码器,甚至连 Google 搜索都不存在,我居然一个人写了这么多,真让人惊讶。
那么,这个项目的整套代码既然出现了,我就兴致勃勃地瞧了瞧,想看看它到底是什么样子。
明明我应该是写代码的本人,却有一大堆事情已经忘了。本文就是这些记录。 先说结论, 最有趣的地方不是学习算法本身, 而是“为了不让它好好学习的设计”在代码和规格书两边都保留了下来。
对象是一台主内存 2MB、没有浮点运算单元的计算机
首先,我先确认一下要运行它的那台机器的性能。凭记忆写会出错,所以我重新查了资料。
| 項目 | 初代 PlayStation 的规格 |
|---|---|
| 主記憶 | 2兆字节。其中前64千字节由内核保留,因此,游戏可用的约为1.94兆字节 |
| 中央演算処理装置 | MIPS R3000A 系列 |
| 浮動小数点演算装置 | 未搭载(COP1 的槽位保持为空) |
| 几何运算协处理器 | GTE。它同样以32位定点数运行 |
| 描画 | 需要维持每秒30帧至60帧 |
浮点运算装置是指专门进行小数计算的电路。如今的个人电脑和智能手机里理所当然地配有它,但初代 PlayStation 的中央处理器却没有。协处理器1号的位置本是为浮点运算装置准备的,但那里一直空着,取而代之的是在协处理器2号上搭载了几何运算专用电路(GTE)。这个 GTE 也不支持浮点,而是以32位定点数运行。
也就是说,机器一侧是以定点数为前提设计的。
在被预留的64千字节中,包含了可通过寻址模式直接访问的“宝贵的前32千字节”。从优化的角度来看,这相当于最可惜的地方被占走了。
神经网络的运算实质上是小数乘法和加法的集合。此外,作为激活函数使用的 sigmoid 函数包含指数函数。通常情况下,这已经是让人放弃的条件。
大脑被分成了五个部分
重新读了一遍后,我首先注意到的是:网络并不只有一个。 存在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层结构,中间层只有一个。
第2和第6是缺号的。因为结构体里根本没有定义,是试作之后丢弃了,还是只预留了编号,现在已经想不起来了。
整体是这样连接起来的。
視界にある物体を1つずつ
│
├─→ 第3の脳(10個から最も近いものを選ぶ)
│ └─→ 食べたい / 快 / 不快
│ │
│ 全物体ぶん合計
│ ↓
│ 環境印象度(3つの値)
│ │
│ + 体調(2つ)
│ ↓
│ 第1の脳 ──→ 方針(5つから選択)
│
├─→ 第4の脳(10個から最も近いものを選ぶ)──→ 行動(7つから選択)
│
└─→ 第5の脳(入力は第3の出力)──→ 本能行動(3つ)──→ 実際のコマンド
体調(2つ)+ 性格(2つ)──→ 第7の脳 ──→ 背景音楽の種類(4段階)从规模上看,或许会让人觉得“很小”。实际上确实很小。但主存只有2MB,而且视频、音频和程序本体都必须放在里面。这个规模并非妥协,而是在给定条件下做出的设计判断。如果在同等条件下,即使现在来设计,也不得不做到差不多的规模。
另外,每一个“脑”负责什么,光靠追代码是看不出来的。起决定作用的是当时的自己用罗马字写下的注释。“把本能用于方针决策”“获得基于本能的行动”这类简短的注释,直接就成了角色的说明。只有第7个脑没有注释,我找到调用位置、读了前后文,才终于弄明白它的用途。
输入是被设计成「感觉」的
从同一个地方,还找出了当时的设计资料。 那是一个名为「因子-印象列表」的电子表格文件, 用日语定义了要给网络的输入单元分配什么。
里面排列着这样的项目。
- 视觉:灰色、发着光、正在闪烁、不规则的图案、规则的图案、有圆润感、像生物、凹凸不平且形状不定
- 嗅觉:食物类、好闻的气味、腥味
- 听觉:悦耳的声音、令人不快的声音、极其巨大的声音
- 动作:剧烈、人工、不规则、可怕的动作、奇怪的动作
- 温度:非常热、温暖、凉爽、非常冷
- 作用力:电磁、刺、毒等令人不适的物理刺激
另一份资料则把一个输入单元定义为5个阶段的强度。 以声音轴为例,就是“非常舒服的声音/舒服的声音/不介意的声音/刺耳的声音/非常刺耳的声音”, 以形状轴为例,就是“偏圆/圆与三角之间/偏三角/三角与四角之间/偏四角”这样的划分。
这就是特征量的设计本身。 即使在现在的机器学习中,如何构造输入的特征量也会左右性能, 而在1990年代的家用游戏机上, 这竟是以“生物如何感受世界”这样的语言来设计的。
环境的印象,是眼中所见一切事物的印象之和
作为第1个大脑(方针决定)输入的“环境印象度”的生成方法,已经弄清楚了。
環境印象度を計算する:
3つの合計値を 0 で初期化する
視界にあるすべての物体について繰り返す:
その物体の特徴(7つ)を組み立てる
10個ある第3の脳から、その特徴にもっとも近いものを選ぶ
選んだ脳で前向き計算をする
得られた「食べたい」「快」「不快」を、それぞれ合計に足す
3つの合計値が、いまいる場所の印象になる把对各个物品的印象相加,就会成为整个场所的印象。 这是一种直截了当、从生物的感知方式来看也合乎情理的设计。
然后,这3个值再加上2个体调值,共5个输入第1个大脑,“方针”就决定了。 方针的输出有5个,对于无法选择的方针,预先填入无效值并将其排除出候选, 再从剩下的当中选择最大值,当时是这么用的。
在没有浮点运算单元的机器上,是如何计算的
浮点数并非“不能用”,而是测量后舍弃了
这里是写之前重新确认过的地方。
Sony 所提供的开发套件中,包含了一个支持浮点数的数学库。 它提供了三角函数、对数、指数函数以及格式转换等一整套功能, 也就是说,如果想用浮点数,是有办法用的。
不过,既然硬件上没有浮点运算装置,其实体就是由软件进行的模拟。 从当时起,人们就说“软件浮点运算极端缓慢,代码也会膨胀, 所以无论如何都应该避免”之类的话。
在我的记忆里,与用定点数写成的版本进行实测比较后,速度差达到了数量级的不同, 所以应该采用了定点数版本。由于当时的测量记录没有留存下来, 这一点只能说是“确实好像是那样”的程度, 但并不是因为没有其他选择才采用定点数,而是在两者都存在的情况下做出的选择,这才是实际情况。
用 32 位定点数,从数值运算基础开始自行构建
采用的是 32 位定点数。 这是一种将高 16 位作为整数部分、低 16 位作为小数部分来处理的形式。
32ビットの整数を1つ用意する
上位16ビット = 整数部分
下位16ビット = 小数部分(65536分の1が最小単位)
掛け算:
2つの値を64ビットに拡張して掛け、結果を16ビット右にずらす
割り算:
割られる値を16ビット左にずらしてから割る在为此形式自行构建了一整套运算库(乘法、除法、指数函数)之后, 又在其上搭载了神经网络。
没有在 sigmoid 函数上用查表法妥协
重读一遍后连自己都感到惊讶的,就是这里。
激活函数是 sigmoid 函数,也就是「1 ÷ (1 + e 的 -x 次方)」。其中包含指数函数。
在处理速度要求严苛的环境中,预先计算该函数并做成表, 再根据输入值查表进行近似是常规做法。速度会大幅提升, 但精度会因表的步长而相应下降。
当时的我并没有采用那个常规做法。 而是用连分数展开认真地计算指数函数。
指数関数を計算する(連分数展開):
入力の2乗を求める
ある初期値から始めて、次の操作を数回繰り返す:
作業値 = 入力の2乗 ÷ (作業値 + 定数)
定数を4ずつ減らしていく
最後に補正して返す连分数展开是一种将除法嵌套起来以近似函数的方法。 相比查表计算量更大,但用较少的项数就能获得精度。 在本应以速度为最优先的家用游戏机上,却选择了精度。
当时是在哪里学到这个的,我想大概是书籍吧。 那时还不是一个能在互联网上查到论文和示例代码的时代, 所以隐约记得,自己买了一本算法事典,然后是从那里把它写出来的。
学习更新公式与教科书一致
学习采用误差反向传播法,并带有惯性项。
重みの更新:
今回の更新量 = 学習率 × 誤差信号 × 入力側の出力
+ 慣性係数 × 前回の更新量
重み = 重み + 今回の更新量惯性项是一种把上一次更新量的一部分延续到本次的机制, 具有稳定学习、使其不易陷入局部解的效果。 在定点数实现时,这部分会导致溢出管理变得麻烦,但并没有省略。真了不起啊。
不会因为学习而停止游戏
作为实现,最具实务意义的正是这里。
神经网络的学习是一种反复执行直到收敛的处理。 至于要重复多少次才会结束,事先并不知道。 如果在以每秒30帧运行的游戏中直接照这样执行, 一旦学习时间拖长,画面就会卡住。
因此,我们从学习循环的内部调用游戏侧的更新处理。
学習を行う(対象の脳, 最大繰り返し回数, 目標誤差):
繰り返し回数の上限まで、以下を繰り返す:
ゲーム側を1フレーム進める ← 学習ループの内側から呼ぶ
保持しているすべての学習サンプルについて:
1サンプルぶんの学習を行う
すべてのサンプルの誤差を合計し、平均を求める
平均誤差が目標を下回っていれば、そこで打ち切る每完成1个epoch,就把控制权交还给绘制和输入处理。 无论学习花上几秒,从玩家的角度来看,画面都在持续运行。
这是论文和教科书里都没有写过的一类巧思。 它回应的是这样一个问题: 如何把“学习需要时间”这一性质,驯服在持续实时运行的产品之上。
从10个大脑中选出“最接近的一个”
第3个和第4个大脑各准备了10个。 这并不意味着把同样的计算做10遍。 而是针对输入,从10个中选出最接近的一个来使用的结构。
もっとも近い脳を探す(脳の配列, 個数, 入力):
それぞれの脳について:
その脳が保持している学習サンプルと入力を比較し、差の合計を求める
差の合計がもっとも小さかった脳を選ぶ
選んだものを「直前に使った脳」として記録しておく另外还准备了这样一项处理:判断是否存在拥有与输入完全一致的样本的脑。
结果就是,每种经验类型都会各自培育出不同的脑。 并不是把所有东西都塞进一个巨大的网络, 而是让系统拥有多个小网络并分别使用,这是我们的方针。 在主内存只有2MB的环境下,这种分割是合理的。
此外,还有判断脑是否处于「混乱」状态的处理。 它通过查看所有样本的平均误差,来确认学习是否已经崩溃。
音乐也是由神经网络选择的
这次重读时最让我意外的是第七个脑。 说实话,我甚至忘了自己做过这样的东西。
背景音楽を選ぶ:
入力[0] = 体調のパラメータ
入力[1] = もう1つの体調のパラメータ(反転)
入力[2] = 性格のパラメータ
入力[3] = もう1つの性格のパラメータ
第7の脳で前向き計算をする
出力が 0.75 以上なら 音楽の種類A
0.50 以上なら 音楽の種類B
0.25 以上なら 音楽の種類C
それ未満なら 音楽の種類D把身体状况和性格输入神经网络,用单一输出值将背景音乐分配为4种。
也就是说,如果培养方式不同导致性格发生变化,流淌的音乐也会改变。 这并不是会向玩家说明的规格, 但机器人的内心就以音乐的形式表现到了外部。
正题——为了不让它正确学习的设计
到这里为止谈的都是实现方面的话题,但这个项目中最关键的部分, 恐怕并不是技术上的巧思。
这个游戏的人工智能有着与通常的机器学习完全相反的需求规格。
- 如果很快就记住了,就是失败
- 如果没有产生误解,就是失败
- 如果只记住教过的东西就会失败
学习速度快、准确、泛化性能高。 在机器学习的语境下,这些都是「好事」。 然而作为养成游戏,这一切都意味着产品的失败。 如果养着的机器人一次就完美地听话了, 玩家就什么都不剩了。
说到底,这类开发中并不存在「运行正确」这一基准。 因为学习处理没有预先确定的正解, 所以即使输出与设想不同,也无法区分那到底是 bug 还是学习的结果。
那么,是如何打造出「恰到好处地误解」的状态的呢。
不是去改动学习算法,而是调整世界这一侧。 通过改动作为输入给予的感觉数值,或是对象物所具有的性质, 来诱导机器人产生适度的误会,就是这样的方法。
用现在的话来说,这相当于不是设计模型,而是设计环境与奖励。 在强化学习中奖励函数的设计本质上很重要,这一点如今已广为人知, 但在 1997 年的养成游戏现场, 为了让产品有趣,其实已经在同样的地方动过手了。
30年后,重新阅读自己写下的代码
重新读过后,我明白了两件事。
其一是,当时的判断作为对制约条件的回答是合乎逻辑的。 因为没有浮点运算装置,所以自行搭建了定点运算基础; 因为主存不足,所以把网络分割得更小; 为了守住实时性,从学习循环中调用游戏循环。 这些都是在那个环境下推出产品这一条件下自然推导出的判断。
另一件是,我究竟有多健忘。 五个“大脑”各自在做什么, 尤其是第7个在挑选音乐这件事,在看到代码之前我完全忘了。 多亏30年前的自己留下的注释,才终于想起来。 我再次觉得,应当把将来的自己当成别人来写下来。
而如今读来觉得有价值的, 是“学习进行得太顺利,作为游戏反而会失败”这一价值标准的反转。 这与计算机性能无关,是至今仍然适用的问题。
在把人工智能装进游戏时,我们总会不自觉地去想“让它更聪明”。 但要让其作为游戏成立,就必须设计在哪些地方不要让它变聪明。 30年前的源代码,以实现的形式留下了这一点。
来源:Morikatron · 游戏AI开发者博客 · tech.morikatron.ai