知覚・学習・コネクショニズム ― ビット・マップから3次元モデル、弁別ネットワーク、並列分散処理まで
人工知能シリーズの最終回として、記号を操作する枠組みの外側にある3つの領域を扱います。コンピュータによる視覚では、ビット・マップから原始スケッチ・2.5次元スケッチ・3次元モデル表現へと段階を踏む考え方と、ガウスの畳み込み法による境界検出を実装します。学習機械では、実行部分と学習部分という構造、単一表現のトリック、信頼度割り当て問題を整理し、EPAMの弁別ネットワークとバージョン空間の候補排除アルゴリズムを動かします。コネクショニズムでは、パーセプトロンの限界から誤差伝播学習への展開をたどり、綴りから音素への変換、側抑制とネッカーキューブ、コネクション・マシンのデータ並列プログラミングをSwiftで確かめます。最後に、1990年前後のこれらの構想が、その後の30年で何になったのかを見ます。
目次
- はじめに
- 1. 知覚という逆問題
- 2. ビット・マップから3次元モデルへ
- 3. 境界検出とガウスの畳み込み法
- 4. 原始スケッチと2.5次元スケッチ
- 5. 学習機械の構造
- 6. 単一表現のトリックと反復学習
- 7. 信頼度割り当て問題
- 8. 概念の学習と特性ベクトル
- 9. EPAMモデルと弁別ネットワーク
- 10. バージョン空間と候補排除アルゴリズム
- 11. コネクショニズム ― 並列分散処理
- 12. パーセプトロンと線形分離
- 13. 誤差伝播学習と前進供給ネットワーク
- 14. 音声合成への応用 ― 綴りから音素へ
- 15. 並列処理による知覚 ― 側抑制とネッカーキューブ
- 16. 逐次処理の問題点
- 17. コネクション・マシンとデータ並列
- 18. その後の30年
- まとめ
- 注釈
- 参考資料
はじめに
このシリーズでは、知識をどう表現するか、規則からどう推論するか、可能性の空間をどう探索するか、という問題を扱ってきました。どれも記号を操作する枠組みの中にあります。意味ネットワークのノード、production ruleの条件部、ゲームツリーのノード。すべて、人間が読んで意味の分かる記号でできていました。
この記号主義の枠組みには、うまく扱えない領域が2つ残ります。
1つは知覚です。カメラが返してくるのは記号ではなく、数百万個の明るさの数値です。「これは椅子である」という記号にたどり着くまでに、そもそも「ここに物体の輪郭がある」ことを見つけ出さなければなりません。この最初の一歩を、規則で書き下すことが誰にもできませんでした。
もう1つは学習です。エキスパートシステムは、専門家から規則を聞き出して人手で書き込む必要がありました。この作業(知識獲得のボトルネック)が、実用化の最大の障害でした。規則を経験から自分で作れないだろうか、という問いが残ります。
この記事では、この2つに取り組んだ研究を扱います。そして、それらを追いかけていくと、記号を単位としないコネクショニズムという別の枠組みに行き着きます。1990年前後、これは記号主義への挑戦として登場しました。その後どうなったかは、最後の節で見ます。
1. 知覚という逆問題
視覚を計算の問題として定式化すると、その難しさの正体が見えます。
3次元の世界にある物体が、光を反射し、レンズを通って、2次元の平面に投影される。カメラがするのはここまでです。視覚がしなければならないのは、この過程を逆にたどることです。
世界(3次元) ──投影──→ 画像(2次元) ← カメラがすること
世界(3次元) ←──復元── 画像(2次元) ← 視覚がすること
投影は情報を捨てます。奥行きが失われ、隠れた面が消え、照明と反射率の区別がつかなくなります。捨てられた情報を復元しようとするのですから、答えは一意に決まりません。同じ画像を作る3次元の世界は無数にあります。これを**不良設定問題(ill-posed problem)**と呼びます。
にもかかわらず人間は、ほとんどの場合ただ1つの解釈にたどり着きます。なぜかといえば、世界についての前提を暗黙に使っているからです。
| 前提 | 内容 |
|---|---|
| 物体は連続している | 隣り合う点は、たいてい同じ面に属する |
| 面はなめらか | 奥行きは急に飛ばない(物体の縁を除いて) |
| 光源は上にある | 陰影から凹凸を読むときの手がかり |
| 見ている角度は特別ではない | たまたま一直線に見える配置は、実際に一直線である |
これらの前提を計算に埋め込むことが、コンピュータによる視覚の設計そのものになります。「知覚は無意識の推論である」というヘルムホルツの主張(19世紀)が、そのまま計算の言葉に翻訳されたと見ることができます(注1)。
2. ビット・マップから3次元モデルへ
視覚を一気に解こうとせず、段階的な表現の系列として組み立てる。この方針を明確にしたのが David Marr です(注2)。
出発点はビット・マップ(bitmap)、つまり明るさの数値が縦横に並んだだけの配列です。ここには「物体」も「輪郭」も書かれていません。あるのは数値だけです。
イメージ(明るさの配列)
↓ どこで明るさが急に変わるか
原始スケッチ(primal sketch)
↓ 各点までの距離と面の向き
2.5次元スケッチ(2.5-D sketch)
↓ 見る位置に依存しない形の記述
3次元モデル表現(3-D model representation)
| 段階 | 何が表現されているか | 座標系 |
|---|---|---|
| イメージ | 各画素の明るさ | 画像 |
| 原始スケッチ | 明るさの変化(境界点、線分、点、それらの並び) | 画像 |
| 2.5次元スケッチ | 見えている面までの距離と、面の向き | 見る人を中心とした座標 |
| 3次元モデル表現 | 物体の形そのもの。部分と部分の関係 | 物体を中心とした座標 |
この分け方で重要なのは、2.5次元スケッチと3次元モデル表現の間にある断絶です。
2.5次元スケッチは「見えているもの」の記述です。手前にある面までの距離は分かりますが、裏側は分かりません。座標の原点は見ている人にあります。首を動かせば、すべての数値が変わります。
3次元モデル表現は「物のかたち」の記述です。原点は物体そのものにあります。首を動かしても変わりません。だからこそ、記憶にある形と照合できます。「2.5次元」という中途半端な名前は、視点に縛られた、面までの距離の地図という、この段階の性質を表しています。
そして、この系列の最初の一歩、イメージから原始スケッチへの変換が、境界検出です。
3. 境界検出とガウスの畳み込み法
**境界(edge, 輪郭)**とは、明るさが急に変わる場所です。物体の縁、面の向きの変わり目、影の境目などが、画像の上では明るさの急変として現れます。
素朴に考えれば、隣り合う画素の差を取ればよさそうです。ところが実際の画像にはノイズが乗っています。差を取ると、ノイズも同じように「急変」として拾われてしまいます。
そこで先に平滑化してから変化を調べます。平滑化に使うのがガウス関数です。
G(x) = exp( −x² / (2σ²) ) (全体の和が1になるように正規化する)
この重みで近傍の画素を混ぜ合わせることを**ガウスの畳み込み法(Gaussian convolution)と呼びます。σ(標準偏差)が、どれくらいの広さで混ぜるかを決めます。このような、近傍の重み付き和を取る処理を一般にフィルタ(filter)**と呼びます。ガウスの畳み込みは平滑化フィルタの代表であり、後で使うラプラシアンは変化を取り出すフィルタです。
2次元のガウス関数は縦横に分離できるので、横方向に1回、縦方向に1回の1次元畳み込みで済みます。計算量が O(r²) から O(r) に落ちる、実装上の要点です。
struct Image {
let width: Int, height: Int
var pixels: [Double]
init(width: Int, height: Int, value: Double = 0) {
self.width = width; self.height = height
self.pixels = [Double](repeating: value, count: width * height)
}
subscript(x: Int, y: Int) -> Double {
get { pixels[y * width + x] }
set { pixels[y * width + x] = newValue }
}
// 端は最も近い画素で埋める
func clamped(_ x: Int, _ y: Int) -> Double {
let cx = min(max(x, 0), width - 1)
let cy = min(max(y, 0), height - 1)
return self[cx, cy]
}
}
// 1次元のガウス核
func gaussianKernel(sigma: Double) -> [Double] {
let radius = max(Int(ceil(3 * sigma)), 1)
var k = [Double]()
for i in -radius...radius {
let x = Double(i)
k.append(exp(-(x * x) / (2 * sigma * sigma)))
}
let sum = k.reduce(0, +)
return k.map { $0 / sum }
}
func convolve(_ img: Image, sigma: Double) -> Image {
let k = gaussianKernel(sigma: sigma)
let r = k.count / 2
var tmp = Image(width: img.width, height: img.height)
for y in 0..<img.height { // 横方向
for x in 0..<img.width {
var acc = 0.0
for (i, w) in k.enumerated() { acc += w * img.clamped(x + i - r, y) }
tmp[x, y] = acc
}
}
var out = Image(width: img.width, height: img.height)
for y in 0..<img.height { // 縦方向
for x in 0..<img.width {
var acc = 0.0
for (i, w) in k.enumerated() { acc += w * tmp.clamped(x, y + i - r) }
out[x, y] = acc
}
}
return out
}
境界点をどう定義するか
平滑化した画像で、明るさの変化が最も急な場所を見つけます。1次微分の極大を探す方法もありますが、ここでは2次微分がゼロになる場所を使います。明るさが急変する中心では、2次微分が正から負(または負から正)へ符号を変えるからです。この符号の変わり目をゼロ交差(zero crossing)と呼び、その位置が境界点になります。
2次微分にあたるのがラプラシアンです。
func laplacian(_ img: Image) -> Image {
var out = Image(width: img.width, height: img.height)
for y in 0..<img.height {
for x in 0..<img.width {
out[x, y] = img.clamped(x - 1, y) + img.clamped(x + 1, y)
+ img.clamped(x, y - 1) + img.clamped(x, y + 1)
- 4 * img.clamped(x, y)
}
}
return out
}
// 符号が変わる場所が境界点。傾きが小さすぎるものは捨てる。
// ラプラシアンの応答は平滑化を強めるほど小さくなるので、
// しきい値はその場面の最大応答に対する割合で決める
func zeroCrossings(_ lap: Image, ratio: Double) -> Image {
let scale = lap.pixels.map { abs($0) }.max() ?? 1
let threshold = ratio * scale
var out = Image(width: lap.width, height: lap.height)
for y in 0..<lap.height {
for x in 0..<lap.width {
let c = lap.clamped(x, y)
let r = lap.clamped(x + 1, y)
let d = lap.clamped(x, y + 1)
let crossH = (c < 0) != (r < 0) && abs(c - r) > threshold
let crossV = (c < 0) != (d < 0) && abs(c - d) > threshold
out[x, y] = (crossH || crossV) ? 1.0 : 0.0
}
}
return out
}
「ガウスで平滑化してからラプラシアンを取り、ゼロ交差を境界とする」という手順は、Marr と Ellen Hildreth が定式化したもので、**ガウシアンのラプラシアン(Laplacian of Gaussian)**と呼ばれます(注3)。
動かしてみる
明るい四角形と円を置いた場面に、強いノイズを載せて入力とします。
=== ノイズのない場面 ===
@@@@@@@@@@@@
@@@@@@@@@@@@
@@@@@@@@@@@@ #
@@@@@@@@@@@@ #######
@@@@@@@@@@@@ #########
@@@@@@@@@@@@ ###########
@@@@@@@@@@@@ ###########
@@@@@@@@@@@@ ###########
#############
###########
###########
#########
#######
#
これにノイズを載せたものが、実際にプログラムへ与えられるビット・マップです。
=== ノイズを載せた入力イメージ ===
.. :.. . : :.. :.:...: ... : .
. ::.. : :. . ... :. . .: :.. .
: . :. . : :: :. .:. .. .... : : .: .
: ... : :::: ::... ..: .: . .. ..
..: %@@%#%@#%##@:. . .. ::: : ... ..
: :##@#%@#@@#@%: ..... .::..: .
:@%%@#%@##%#%: : ... .. . :.: .:
:. @@%%#@@%%@%# :. . :..: :.. . ..
... %@%%%###%@@@..: . ..:...# .:.:.. :
%@#%%#@%%@%@. .:. ::. .##*##%% . .
.: %%%%@%%##%@@..::: . .%#*%####* : :
... @@#@%%@%#%%#. : .: .:#*%+####%##....
. %@#@@@@#@#@% .. :.: #+#%#*#*#%*:.:
. :%#@@%@#@@%%% . : . :#%%###%*+%* :
.:.::.:: :. : :: .:: ..***%##%#***#*.:.
σ を変えて境界検出をかけると、次のようになります。
--- 平滑化なし:境界点 897 個 ---
################ ###### ###############
## ### ######### # ## ####### #########
##### ####### #### ### ######## ######
################# # ####### #### # #
#### # # # # ####### # ####### ## ##
(以下、全面が境界点で埋まる)
平滑化せずにゼロ交差を取ると、画面のほとんどが境界点になります。ノイズによる微小な符号変化を、すべて境界として拾っているからです。σ を上げていくと、
--- σ = 1.0:境界点 507 個 ---
--- σ = 2.0:境界点 126 個 ---
--- σ = 3.0:境界点 90 個 ---
###########
#
# #
# #
# #
# # #######
# # # #
# # # ##
# # # #
# # # #
# # # #
# # # #
########### # #
# #
# #
# #
## #
### ##
#####
四角形と円の輪郭が、ノイズの中から復元されました。 境界点は 897 個から 90 個に減っています(ノイズのない同じ場面を σ=1.0 で処理したときの境界点は 86 個なので、ほぼ妥当な数です)。
σ の選択という問題
この実験には、そのまま重要な論点が含まれています。σ をいくつにすればよいかは、原理的に決まりません。
- σ が小さい:細かい変化を拾える。ただしノイズも拾う
- σ が大きい:ノイズに強い。ただし細かい構造が消え、境界の位置もずれる
「正しい σ」は存在しません。見たい対象の大きさによって変わるからです。木の葉の輪郭を見たいのか、森の輪郭を見たいのかで、答えが違います。
この事情への対処が、複数の σ で同時に処理し、結果を統合するという方針です。これを**スケール空間(scale space)**と呼びます。異なる σ で得られた境界のうち、どの σ でも現れるものは実在する境界である可能性が高い、という考え方です。「1つの正解を求める」のではなく「複数の解像度で見て突き合わせる」というこの発想は、後で見るとおり、現代の視覚システムにもそのまま生き残っています。
4. 原始スケッチと2.5次元スケッチ
境界点が求まっても、まだ点の集まりでしかありません。原始スケッチは、これを構造化していきます。
| 段階 | 内容 |
|---|---|
| 生の原始スケッチ | 境界点、線分の切れ端、点、それらの向きと明るさの差 |
| 完全な原始スケッチ | それらをまとめた線・曲線・境界・領域、そして繰り返しの構造 |
まとめ上げには、近さ・向きの揃い方・つながりやすさといった手がかりが使われます。これは20世紀初頭のゲシュタルト心理学が「群化の法則」として記述した現象を、計算の手続きに置き換えたものです。
原始スケッチから2.5次元スケッチへ進むには、奥行きの情報が要ります。ここで使われる手がかりが、いわゆる「◯◯からの形(shape from X)」です。
| 手がかり | 何を使うか |
|---|---|
| 両眼視差 | 左右の目に映る位置のずれ |
| 運動 | 視点が動いたときの像の流れ方 |
| 陰影 | 明るさの分布から面の向きを読む |
| きめ(テクスチャ) | 模様の細かさの変化から傾きを読む |
| 輪郭 | 輪郭線の形そのものから面の向きを推定する |
いずれの手がかりも、単独では答えが定まりません。陰影からの形は光源の位置を仮定しなければ解けませんし、きめからの形は元の模様が一様であることを仮定します。だから複数の手がかりを組み合わせ、互いに矛盾しない解を選ぶ必要があります。
個々の手がかりは曖昧だが、多数の制約を同時に満たす解はほぼ1つに定まる。 この考え方を制約充足と呼び、視覚だけでなく知覚全般を貫く原理になっています。後の第15節で扱うネッカーキューブは、この原理がむき出しになった例です。
5. 学習機械の構造
ここから学習の話に移ります。
学習するプログラムは、機能の異なる2つの部分に分けて考えると整理できます。
| 部分 | 役割 |
|---|---|
| 実行部分(performance element, PE) | 実際に課題をこなす。チェスを指す、病気を診断する、文字を読む |
| 学習部分(learning element, LE) | 実行部分の振る舞いを見て、その中身を書き換える |
環境(問題、事例、正解、報酬)
│
┌───────────┴───────────┐
↓ ↓
┌─────────┐ 知識の書き換え ┌─────────┐
│ 学習部分 │ ─────────────→ │ 実行部分 │ ──→ 出力
│ LE │ ←───────────── │ PE │
└─────────┘ 振る舞いと結果 └─────────┘
この分け方が有効なのは、学習部分にとって実行部分は「書き換える対象のデータ」であるという関係がはっきりするからです。実行部分がプロダクション規則の集まりなら、学習部分の仕事は規則の追加・削除・修正になります。実行部分が評価関数なら、学習部分の仕事は重みの調整になります。
前回のゲームの記事で見た Arthur Samuel のチェッカープログラムは、この構造の初期の実例でした。実行部分はα-β探索と静的評価関数、学習部分は評価関数の重みを対局結果から調整する仕組みです。
学習部分を設計するとき、決めなければならないことが4つあります。
- 実行部分のどの部分を書き換えるか(規則か、重みか、ネットワークの形か)
- その部分をどう表現するか
- どんな情報が手に入るか(正解つきの事例か、勝敗だけか、まったく手がかりがないか)
- 手に入った情報を、どの書き換えに結びつけるか
3番目は、学習の種類そのものを決めます。1つ1つの事例に正解がついてくるなら教師あり学習、行動の結果として良し悪しだけが返るなら強化学習、正解も評価もなく構造だけを見つけるなら教師なし学習です。そして4番目が、次に述べる信頼度割り当て問題です。
6. 単一表現のトリックと反復学習
学習部分が「事例」と「仮説」を突き合わせるとき、両者の表現が違っていると、比較のための変換が必要になります。この変換自体が難しい問題になりかねません。
そこで使われるのが**単一表現のトリック(single-representation trick)**です。事例と仮説を、同じ表現の言葉で書く。 そうすれば、両者を直接照合できます。
具体的には、事例を「すべての属性が具体的な値で埋まった仮説」とみなします。すると、仮説と事例の関係は「一般と特殊」という1本の軸の上に並びます。
〈?, ?, ?〉 ← 最も一般的な仮説(何にでも当てはまる)
〈?, 赤, ?〉
〈?, 赤, 大〉
〈円, 赤, 大〉 ← 事例そのもの(最も特殊な仮説)
第10節のバージョン空間は、このトリックの上に成り立っています。学習とは「この軸のどこに線を引くか」を決める作業になります。
もう1つ、学習の基本的な形が**反復学習(incremental learning, 逐次学習)**です。事例をまとめて与えて一度に処理するのではなく、1つずつ与えて、そのつど仮説を修正するやり方です。
| まとめて学習 | 反復学習 | |
|---|---|---|
| 事例の与え方 | 全部そろえてから | 1つずつ |
| 記憶する量 | 全事例を保持 | 現在の仮説だけでよい |
| 途中で使えるか | 使えない | いつでも現在の仮説が使える |
| 事例の順序 | 影響しない | 結果が変わることがある |
反復学習は、記憶量が少なくて済み、学習しながら働けるという利点があります。一方、提示された順序に結果が左右されるという性質を持ちます。第9節のEPAMで、この性質を実際に確認します。
7. 信頼度割り当て問題
学習の中心にある難問が、**信頼度割り当て問題(credit-assignment problem)**です。Marvin Minsky が1961年に定式化しました(注4)。
問題はこうです。結果が良かった(あるいは悪かった)とき、その手柄(あるいは責任)を、どの判断に割り当てればよいか。
チェッカーで負けたとします。60手指しました。どの手が悪かったのでしょうか。最後の一手ではないかもしれません。20手目の何気ない一手が敗因だったかもしれません。負けたという1ビットの情報を、60個の判断にどう配分すればよいのか。
この問題は2つの方向に分かれます。
| 種類 | 内容 |
|---|---|
| 時間方向の割り当て | 一連の行動のうち、どの時点の行動が結果に効いたか |
| 構造方向の割り当て | システムを構成する多数の要素のうち、どの要素が結果に効いたか |
時間方向の問題への古典的な解決が、Samuel の使った考え方です。最終結果を待たず、次の局面の評価値を「暫定的な正解」として今の評価値を直す。 評価は最終的な勝敗から徐々に手前へ染み出していきます。この考え方は後に **TD学習(時間差分学習)**として定式化され、強化学習の基礎になりました(注5)。
構造方向の問題への解決が、第13節で扱う誤差伝播学習です。出力の誤差を、各単位が誤差にどれだけ寄与したかに応じて逆向きに配分します。微分の連鎖律が、そのまま信頼度の配分規則になるという発見でした。
信頼度割り当ては、この記事のあと3つの節に繰り返し現れます。学習という営みの中心にある問題だからです。
8. 概念の学習と特性ベクトル
学習の中でも最も基本的な形が概念の学習(concept learning)です。「鳥とは何か」「この病気の患者とは何か」というカテゴリの境界を、事例から見つけ出します。
対象を表現する最も単純な形が**特性ベクトル(feature vector, 属性ベクトル)**です。あらかじめ決めた属性の並びに、値を入れたものです。
属性: 〈 形, 色, 大きさ 〉
事例1: 〈 円, 赤, 大 〉 ← この概念に属する(正例)
事例2: 〈 四角, 赤, 大 〉 ← 正例
事例3: 〈 円, 青, 大 〉 ← 属さない(負例)
特性ベクトルによる表現には、はっきりした限界があります。属性の間の関係を書けません。 「A の上に B がある」という関係は、決まった数の属性の並びでは表せません。このシリーズの知識表現の記事で扱った意味ネットワークやフレームは、まさにこの限界を越えるために作られたものでした。
とはいえ、限界と引き換えに得られるものがあります。特性ベクトルの空間には構造が入ります。 具体的な値を ?(何でもよい)に置き換えていくと、仮説は自然に一般と特殊の順序で並びます。学習を「この順序づけられた空間の探索」として定式化できるのです。
概念学習を探索とみなすと、このシリーズで扱った探索の枠組みがそのまま使えます。ステートは仮説、演算子は「一般化する」「特殊化する」、目標は「すべての正例を覆い、どの負例も覆わない仮説」です。
ここで、決定的に重要な事実を1つ確認しておきます。事例だけからは、正しい概念は決まりません。
正例と負例をすべて説明する仮説は、たいてい複数あります。どれを選ぶかは、事例が教えてくれるものではなく、学習する側があらかじめ持っている偏りが決めます。これを**帰納バイアス(inductive bias)**と呼びます。
特性ベクトルの連言(AND で結んだ形)しか仮説として認めない、という制限も1つのバイアスです。この制限があるから探索が有限で済み、収束もします。バイアスのない学習は、一般化ができない。 これは技術上の都合ではなく、原理的な事実です(注6)。
9. EPAMモデルと弁別ネットワーク
**EPAM(Elementary Perceiver and Memorizer)**は、Edward Feigenbaum と Herbert Simon による、人間の知覚と記憶を説明するための計算モデルです(注7)。名前のとおり「知覚と記憶の基本要素」を扱うモデルで、人間が無意味つづりを覚える実験の結果を再現することが目的でした。
EPAMの中心にあるのが**弁別ネットワーク(discrimination network)**です。木構造で、
- 内部ノードは1つの属性を調べる分岐点
- 葉は記憶された対象(イメージ)
対象を提示されると、根から属性を調べながら枝をたどり、葉に到達します。これが**弁別(sorting)**です。
indirect enum Net {
case empty
case image(Object) // 記憶されたイメージ
case test(attribute: Int, branches: [String: Net])
}
func sort(_ net: Net, _ features: [String], path: [String] = []) -> (Net, [String]) {
switch net {
case .empty, .image:
return (net, path)
case .test(let attr, let branches):
let v = features[attr]
let step = "\(attrNames[attr])=\(v)"
if let sub = branches[v] {
return sort(sub, features, path: path + [step])
}
return (.empty, path + [step + "(未知の枝)"]) // その値の枝がまだない
}
}
学習の仕組みが、EPAMの特徴的なところです。ネットワークは、区別に失敗したときだけ成長します。
func learn(_ net: Net, _ obj: Object) -> Net {
switch net {
case .empty:
return .image(obj) // 空いている葉に覚える
case .image(let stored):
if stored.name == obj.name { return net } // すでに覚えている
// 混同が起きた。2つを区別できる属性を1つ選んで分岐点を作る
guard let attr = (0..<attrNames.count).first(where: {
stored.features[$0] != obj.features[$0]
}) else {
return net // 特性ベクトルが同一なら区別できない
}
return .test(attribute: attr, branches: [
stored.features[attr]: .image(stored),
obj.features[attr]: .image(obj),
])
case .test(let attr, var branches):
let v = obj.features[attr]
branches[v] = learn(branches[v] ?? .empty, obj)
return .test(attribute: attr, branches: branches)
}
}
6種類の動物を順に提示して育てた結果です。
=== ネットワークを1つずつ育てる ===
犬 を提示:(根) 空の葉に到達 → そのまま記憶する
猫 を提示:(根) 「犬」と混同 → 分岐点を作る
鳥 を提示:大きさ=小 「猫」と混同 → 分岐点を作る
魚 を提示:大きさ=小 → 脚の数=0(未知の枝) 空の葉に到達 → そのまま記憶する
馬 を提示:大きさ=大(未知の枝) 空の葉に到達 → そのまま記憶する
蛇 を提示:大きさ=中 「犬」と混同 → 分岐点を作る
=== できあがった弁別ネットワーク ===
[大きさ は?]
中:
[脚の数 は?]
0: → 蛇
4: → 犬
大:
→ 馬
小:
[脚の数 は?]
0: → 魚
2: → 鳥
4: → 猫
覚えた対象 6 個 / 分岐点 3 個
属性は4つ(大きさ・脚の数・体表・住む場所)ありますが、使われた分岐点は3つだけです。「体表」も「住む場所」も一度も調べられていません。EPAMは対象を丸ごと記憶しているのではなく、他と区別するのに必要な最小限の情報だけを蓄えています。
このことは、未知の対象を与えると分かりやすく現れます。
=== 未知の対象を提示すると、既知のどれかに引き寄せられる ===
狼:大きさ=中 → 脚の数=4 → 「犬」と判定される
鮫:大きさ=大 → 「馬」と判定される
鷲:大きさ=中 → 脚の数=2(未知の枝) → 未知として新しく覚えられる
鮫が馬と判定されました。 ネットワークが知っている「大きい」ものは馬だけなので、大きさを見た時点で馬だと結論してしまいます。これはバグではなく、EPAMが人間の記憶について主張していることそのものです。学習が進んでいない段階では、粗い特徴だけで判断してしまい、専門家になるほど細かい分岐が増えていく。チェスの熟達者の研究などで観察された現象と対応しています。
そして、第6節で予告した反復学習の性質も確認できます。
=== 提示の順序を変えると、別の形のネットワークになる ===
[脚の数 は?]
0:
[大きさ は?] 中: → 蛇 小: → 魚
2:
→ 鳥
4:
[大きさ は?] 中: → 犬 大: → 馬 小: → 猫
覚えた対象 6 個 / 分岐点 3 個
並びは違うが、6種すべてを正しく弁別できるか: できる
犬と蛇(大きさが同じで脚の数が違う)を先に提示すると、根の分岐点が「大きさ」から「脚の数」に変わります。同じ6種類を覚えても、覚えた順序が違えば内部構造が違う。何を学んだかだけでなく、どの順に経験したかが、記憶の形を決める。 EPAMが認知モデルとして主張していたのは、この点でした。
10. バージョン空間と候補排除アルゴリズム
EPAMは1つの仮説(ネットワーク)を持ち、失敗するたびに修正しました。これに対して、事例と矛盾しない仮説をすべて同時に保持するという方針があります。Tom Mitchell のバージョン空間(version space)モデルです(注8)。
バージョン空間とは、与えられた事例をすべて説明する仮説の集合です。素朴に考えると、全部を列挙して保持するのは非現実的です。ここで第6節の単一表現のトリックが効いてきます。仮説は一般と特殊の順序で並んでいるので、集合を「境界」だけで表現できます。
| 記号 | 内容 |
|---|---|
| S(specific boundary) | バージョン空間の中で最も特殊な仮説の集合 |
| G(general boundary) | バージョン空間の中で最も一般的な仮説の集合 |
S と G の間に挟まれる仮説が、すべてバージョン空間の要素です。2つの境界だけで、集合全体を表せます。
enum Constraint: Equatable {
case value(String) // 具体的な値
case any // 何でもよい(?)
case none // 何にも当てはまらない(⊥)
}
// 単一表現のトリック:例も仮説も同じ形なので、そのまま照合できる
func matches(_ h: Hypothesis, _ e: Example) -> Bool {
for (c, v) in zip(h, e) {
switch c {
case .any: continue
case .value(let hv): if hv != v { return false }
case .none: return false
}
}
return true
}
**候補排除アルゴリズム(candidate-elimination algorithm)**は、事例が来るたびに S と G を動かします。
- 正例が来たら:それに合わない G の要素を捨て、S を「その正例を覆うまで」最小限に一般化する
- 負例が来たら:それに合ってしまう S の要素を捨て、G を「その負例を除くまで」最小限に特殊化する
mutating func update(example e: Example, isPositive: Bool) {
if isPositive {
G.removeAll { !matches($0, e) }
var newS: [Hypothesis] = []
for s in S {
if matches(s, e) { newS.append(s); continue }
let g = minimalGeneralization(s, toCover: e)
if G.contains(where: { moreGeneralOrEqual($0, g) }) { newS.append(g) }
}
S = newS.filter { s in !newS.contains { strictlyMoreGeneral(s, $0) } }
} else {
S.removeAll { matches($0, e) }
var newG: [Hypothesis] = []
for g in G {
if !matches(g, e) { newG.append(g); continue }
for h in minimalSpecializations(g, toExclude: e) {
if S.contains(where: { moreGeneralOrEqual(h, $0) }) { newG.append(h) }
}
}
G = newG.filter { g in !newG.contains { strictlyMoreGeneral($0, g) } }
}
}
「赤くて大きいもの(形は関係ない)」という概念を教えた結果です。
初期状態
S = 〈⊥, ⊥, ⊥〉
G = 〈?, ?, ?〉
バージョン空間の大きさ: 48
例1: 〈円, 赤, 大〉 +(正例)
S = 〈円, 赤, 大〉 G = 〈?, ?, ?〉 バージョン空間の大きさ: 8
例2: 〈四角, 赤, 大〉 +(正例)
S = 〈?, 赤, 大〉 G = 〈?, ?, ?〉 バージョン空間の大きさ: 4
例3: 〈円, 青, 大〉 -(負例)
S = 〈?, 赤, 大〉 G = 〈?, 赤, ?〉 バージョン空間の大きさ: 2
例4: 〈三角, 赤, 小〉 -(負例)
S = 〈?, 赤, 大〉 G = 〈?, 赤, 大〉 バージョン空間の大きさ: 1
収束:学習された概念は 〈?, 赤, 大〉
48個あった候補が、4つの事例で1個に絞られました。 S と G が出会った時点で学習は完了です。
この方式には、他の学習法にない性質があります。
第一に、いつ学習が終わったかが分かります。 S と G が一致すれば収束、まだ隙間があれば情報が足りない、と自分で判定できます。
第二に、「まだ分からない」と言えます。
学習した概念で未知の事例を判定する:
〈四角, 赤, 小〉 → 概念に属さない
〈三角, 緑, 大〉 → 概念に属さない
〈円, 赤, 大〉 → 概念に属する
収束前であれば、S のすべてに合えば「属する」、G のどれにも合わなければ「属さない」、その中間なら「判定できない」と答えられます。バージョン空間に残っている仮説の間で意見が割れているという状態を、そのまま表現できるのです。
第三に、次に何を尋ねればよいかが分かります。 バージョン空間をちょうど半分に割るような事例を選んで問い合わせれば、最も効率よく候補を減らせます。これは現在の**能動学習(active learning)**の考え方そのものです。
一方で、限界もはっきりしています。事例にノイズがあると破綻します。 誤ってラベルのついた事例が1つ混じるだけで、正しい仮説が候補から消され、バージョン空間が空になって二度と回復しません。「すべての事例と完全に矛盾しないこと」を要求する設計の、当然の帰結です。現実のデータは必ず汚れているので、この点が候補排除アルゴリズムを実用から遠ざけました。統計的な機械学習が、誤りを許容する枠組みとして後に主流になっていきます。
11. コネクショニズム ― 並列分散処理
ここまでの学習は、すべて記号を単位にしていました。属性、値、仮説、規則。どれも人間が読める記号です。
**コネクショニズム(connectionism)**は、この前提を捨てます。単純な処理単位を多数つなぎ、**つながりの強さ(重み)**に知識を持たせます。記号は、どこにも書かれていません。
この立場は**並列分散処理(parallel distributed processing, PDP)**とも呼ばれます(注9)。名前の3語が、それぞれ主張を表しています。
| 語 | 主張 |
|---|---|
| 並列(parallel) | 多数の単位が同時に動く。逐次的な手順ではない |
| 分散(distributed) | 1つの概念が多数の単位にまたがって表される。単位と概念が1対1に対応しない |
| 処理(processing) | 記憶と処理が分かれていない。重みが知識であり、同時に計算装置でもある |
計算の中身は単純です。各単位は、入ってくる信号に**重み付きリンク(weighted link)**の重みを掛けて足し合わせ、その和を関数に通して出力します。
出力 = f( Σ wᵢ xᵢ + b )
記号主義との違いは、対比するとはっきりします。
| 記号主義 | コネクショニズム | |
|---|---|---|
| 知識の単位 | 記号(規則、フレーム、命題) | 重み(数値) |
| 知識のありか | 特定の場所に局在 | 全体に分散 |
| 処理の進み方 | 逐次(一度に1つの規則) | 並列(全単位が同時に) |
| 知識の獲得 | 人が書く | 事例から学習する |
| 一部が壊れたら | その知識だけ失われる | 全体の性能が少しずつ落ちる |
| 中身の説明 | 読める | 読めない |
最後の2行が、この立場の性格をよく表しています。優雅な劣化(graceful degradation)――一部が壊れても急には壊れない――は、脳の性質としても、実用システムの性質としても魅力的でした。一方で中身が読めないことは、エキスパートシステムが備えていた「なぜそう判断したか説明できる」という利点を失うことを意味します。この取引は、現在もそのまま続いています。
12. パーセプトロンと線形分離
コネクショニズムの出発点が、Frank Rosenblatt のパーセプトロン(perceptron)(1958年)です(注10)。重み付きリンクを1層だけ持ち、重み付き和がしきい値を超えたら発火します。
struct Perceptron {
var weights: [Double]
var bias: Double
// 重み付き和がしきい値を超えたら発火する
func output(_ x: [Double]) -> Double {
let sum = zip(weights, x).reduce(bias) { $0 + $1.0 * $1.1 }
return sum > 0 ? 1 : 0
}
// パーセプトロン学習規則: w ← w + η(t − y)x
mutating func train(_ x: [Double], target: Double, rate: Double) -> Bool {
let y = output(x)
let error = target - y
if error == 0 { return true } // 正解なら何もしない
for i in 0..<weights.count { weights[i] += rate * error * x[i] }
bias += rate * error
return false
}
}
学習規則は驚くほど単純です。間違えたときだけ、正しい方向へ重みを少し動かす。 それだけで、次の定理が成り立ちます。
パーセプトロン収束定理:解が存在するなら、この規則は有限回で必ずそこに到達する(注10)。
実際に動かすと、そのとおりになります。
=== 線形分離できる問題 ===
AND:6 回の反復で収束 重み [0.20, 0.10] しきい値 -0.20
OR:4 回の反復で収束 重み [0.10, 0.10] しきい値 0.00
問題は「解が存在するなら」という条件です。パーセプトロンが表現できるのは、入力空間を1枚の平面で2つに分ける分類だけです。これを**線形分離可能(linearly separable)**といいます。
=== 線形分離できない問題 ===
XOR:200 回まで反復しても収束しない 重み [-0.10, 0.00] しきい値 0.10
入力 [0, 0] → 出力 1(正解 0)✗
入力 [0, 1] → 出力 1(正解 1)✓
入力 [1, 0] → 出力 0(正解 1)✗
入力 [1, 1] → 出力 0(正解 0)✓
XOR(排他的論理和)は線形分離できません。図に描けば理由は明らかです。
x₂
1 | ●(0,1)=1 ○(1,1)=0
|
0 | ○(0,0)=0 ●(1,0)=1
+───────────────────────── x₁
0 1
● と ○ を1本の直線で分けることはできない
「収束しなかった」のは、反復回数が足りなかったからではありません。念のため、重みとしきい値の組み合わせを総当たりで確かめました。
=== XOR を分ける直線は本当に存在しないのか ===
重みとしきい値の組み合わせを 68921 通り試した結果:XOR を分けられる直線は1つも見つからない
この限界を体系的に示したのが、Marvin Minsky と Seymour Papert の『パーセプトロン』(1969年)です(注11)。彼らは XOR のような例だけでなく、「図形がつながっているか」といった性質も1層のパーセプトロンでは判定できないことを証明しました。
隠れ層を加えれば表現力が上がることは、当時から分かっていました。分かっていなかったのは、隠れ層の重みをどう学習させるかです。 出力層の重みなら「正解との差」が直接分かりますが、隠れ層の単位には正解がありません。第7節の信頼度割り当て問題です。
この本の影響で神経回路網への研究資金は細り、1970年代から80年代前半にかけて、この分野は長い冬の時代に入ります。その空白を破ったのが、次に見る誤差伝播学習でした。
13. 誤差伝播学習と前進供給ネットワーク
**前進供給ネットワーク(feedforward network, 順伝播型ネットワーク)**は、単位を層に分け、信号が入力層から出力層へ一方向にだけ流れるように結線したものです。同じ層の中を横につなぐリンクは置きません(横方向へのリンク禁止)。後戻りするリンクも横につなぐリンクもないため、信号の流れに循環がなく、計算は必ず有限回で終わります。
入力層 隠れ層 出力層
○ ──┬──────→ ○ ──┬──────→ ○
○ ──┼──────→ ○ ──┤
○ ──┴──────→ ○ ──┘
w1 w2
(同じ層の中を横につなぐリンクはない)
**誤差伝播学習(error propagation learning, 誤差逆伝播法・backpropagation)**は、この構造で隠れ層の重みを学習する方法です。要点は次の1文に尽きます。
出力の誤差を、各単位が誤差にどれだけ寄与したかに応じて、逆向きに配分する。
各単位の寄与の度合いは、微分の連鎖律で計算できます。信頼度割り当て問題が、微分によって解かれたわけです。
// 前進供給:入力から出力へ一方向に信号が流れる
func forward(_ x: [Double]) -> (hidden: [Double], output: [Double]) {
let h = zip(w1, b1).map { w, b in
sigmoid(zip(w, x).reduce(b) { $0 + $1.0 * $1.1 })
}
let o = zip(w2, b2).map { w, b in
sigmoid(zip(w, h).reduce(b) { $0 + $1.0 * $1.1 })
}
return (h, o)
}
// 誤差伝播:出力の誤差を隠れ層へ逆向きに配分し、重みを直す
@discardableResult
func train(_ x: [Double], target: [Double], rate: Double) -> Double {
let (h, o) = forward(x)
// 出力層の誤差信号
var dOut = [Double](repeating: 0, count: o.count)
for i in 0..<o.count {
let diff: Double = o[i] - target[i]
dOut[i] = diff * o[i] * (1 - o[i])
}
// 隠れ層へ誤差を伝播させる(信頼度割り当て問題を微分で解く)
var dHid = [Double](repeating: 0, count: h.count)
for j in 0..<h.count {
var acc = 0.0
for i in 0..<o.count { acc += dOut[i] * w2[i][j] }
dHid[j] = acc * h[j] * (1 - h[j])
}
// 重みの更新
for i in 0..<o.count {
for j in 0..<h.count { w2[i][j] -= rate * dOut[i] * h[j] }
b2[i] -= rate * dOut[i]
}
for j in 0..<h.count {
for k in 0..<x.count where x[k] != 0 { w1[j][k] -= rate * dHid[j] * x[k] }
b1[j] -= rate * dHid[j]
}
return zip(o, target).reduce(0) { $0 + ($1.0 - $1.1) * ($1.0 - $1.1) }
}
しきい値関数をなめらかなシグモイド関数に取り替えている点が重要です。階段状の関数は微分できないため、連鎖律が使えません。微分可能にすることが、学習を可能にした。 これが誤差伝播学習の技術的な核心です。
パーセプトロンに解けなかった XOR を、隠れ層4単位のネットワークに解かせます。
=== XOR を隠れ層つきのネットワークで学習する ===
5000 回:二乗誤差の合計 0.003182
10000 回:二乗誤差の合計 0.001213
15000 回:二乗誤差の合計 0.000735
20000 回:二乗誤差の合計 0.000524
入力 [0, 0] → 出力 0.010(正解 0) ✓
入力 [0, 1] → 出力 0.987(正解 1) ✓
入力 [1, 0] → 出力 0.993(正解 1) ✓
入力 [1, 1] → 出力 0.014(正解 0) ✓
隠れ層が1つ加わるだけで、線形分離できない問題が解けるようになりました。
この方法は複数の研究者が独立に到達しています。Seppo Linnainmaa の逆モードの自動微分(1970年)、Paul Werbos の博士論文(1974年)などが先行し、1986年の David Rumelhart・Geoffrey Hinton・Ronald Williams による論文が広く知られる契機になりました(注12)。
14. 音声合成への応用 ― 綴りから音素へ
誤差伝播学習の実力を世に示した応用が、NETtalk(Terrence Sejnowski と Charles Rosenberg、1987年)です(注13)。英語の綴りを読み上げるために、各文字に対応する**音素(phoneme)**を出力するネットワークです。得られた音素の列は市販の音声合成器に送られて音声になります。ネットワークが受け持つのは綴りから音素への変換、つまり読み上げの中で最も不規則な部分でした。
英語の綴りと発音の対応は不規則で有名です。しかも、同じ文字が文脈によって違う音になります。
cat の c → /k/ city の c → /s/
cool の c → /k/ cent の c → /s/
規則を書くことはできますが、例外が多く、規則の集合は膨れ上がります。NETtalk の設計は明快でした。周囲の文字を一緒に入力に含める。
入力:注目する文字を中央に置いた7文字の窓
_ _ c i t y _
└─┬─┘ ↑ └─┬─┘
左3 中央 右3
出力:中央の文字に対応する音素
7つの位置それぞれを、アルファベット26文字+語の外側を表す記号の27通りで符号化します。入力単位は 7 × 27 = 189 個です。
let alphabet = Array("abcdefghijklmnopqrstuvwxyz_") // '_' は語の外側
let windowSize = 7 // 前後3文字ずつ+中央
let half = windowSize / 2
func encode(_ word: String, at index: Int) -> [Double] {
let letters = Array(word)
var v = [Double](repeating: 0, count: windowSize * alphabet.count)
for k in 0..<windowSize {
let pos = index - half + k
let ch: Character = (pos >= 0 && pos < letters.count) ? letters[pos] : "_"
if let a = alphabet.firstIndex(of: ch) { v[k * alphabet.count + a] = 1 }
}
return v
}
40語(文字単位で152事例)を学習させ、学習に使っていない6語で確かめました。
=== 綴りから音素への変換を学習する ===
入力:前後3文字を含む 7 文字の窓(7 × 27 = 189 個の入力単位)
出力:18 種類の音素 [-abdefiklmnoprstuz]
学習語 40 語、文字単位の事例 152 個
150 回:誤差 1.600 学習語の正解率 151/152 未知語の正解率 24/25
300 回:誤差 0.386 学習語の正解率 152/152 未知語の正解率 24/25
600 回:誤差 0.098 学習語の正解率 152/152 未知語の正解率 24/25
=== 学習に使っていない語を読ませる ===
cast → /kast/ 正解 /kast/ ✓
crib → /krib/ 正解 /krib/ ✓
cyst → /sist/ 正解 /sist/ ✓
cede → /sed-/ 正解 /sid-/ ✗
mace → /mes-/ 正解 /mes-/ ✓
tunic → /tunik/ 正解 /tunik/ ✓
そして、肝心の c の読み分けです。
=== 文字 c の読みは、後ろの文字で決まるか ===
e・i・y の前の c(/s/ と読む):18/18 正解
それ以外の c(/k/ と読む) :30/30 正解
48か所すべてで正しく読み分けました。 未知語の cast(c→/k/)、cyst(c→/s/)を含みます。ネットワークには「e・i・y の前では /s/」という規則を一度も教えていません。事例だけから、重みの中にこの規則に相当するものが形成されたわけです。
一方、cede を /sid-/ ではなく /sed-/ と読み違えています。c は正しく /s/ と読めており、間違えたのは母音 e のほうです。 学習語の中に「e を /i/ と読む」例が1つもないので、これは知らないことを問われた結果です。ネットワークが学べるのは、あくまで与えられた事例に現れた規則性の範囲にとどまります。
誤り方が人間に似る
NETtalk が注目された理由は、正解率そのものよりも、学習の過程と誤り方が人間に似ていたことにあります。実際のNETtalkの学習音声は、最初は区別のない音の連なりを発し、次第に語らしい音の塊が現れ、やがて単語として聞き取れるようになる、という経過をたどりました。
言語心理学の側でも、話し言葉の言い間違いには規則性のあることが知られていました。
| 現象 | 内容 |
|---|---|
| レキシカル・バイアス(lexical bias) | 音の入れ替わりによる言い間違いは、意味のある単語になる場合のほうが、でたらめな音の並びになる場合より起きやすい |
| 音素反復(phoneme repetition) | 近くに同じ音素を含む語があると、言い間違いが起きやすくなる |
どちらも、記号を1つずつ順に処理するモデルでは説明しにくい現象です。音素を選ぶ処理が語の情報から独立しているなら、間違えた結果がたまたま実在の語になる確率が上がる理由がありません。
これらを、活性が語のレベルと音素のレベルの間を双方向に広がるネットワークで説明したのが Gary Dell のモデル(1986年)です(注14)。音素のレベルへ広がった活性が語のレベルへ戻り、実在する語を後押しするため、結果として実在語になる誤りが増える。近くに同じ音素があれば、その音素の活性が高まって取り違えやすくなる。正しい振る舞いだけでなく、誤り方まで再現できるという点が、コネクショニズムを認知モデルとして支持する有力な論拠になりました。
15. 並列処理による知覚 ― 側抑制とネッカーキューブ
第13節の前進供給ネットワークでは、同じ層の中を横につなぐリンクを禁じていました。知覚のモデルでは、逆にこの横方向のリンクが主役になります。
側抑制
側抑制(lateral inhibition)は、各受容器が隣の受容器の応答を差し引く仕組みです。カブトガニの複眼で Haldan Hartline と Floyd Ratliff が見出しました(注15)。
// 各受容器は、自分の入力から隣の受容器の応答を差し引く。
// すべての単位が同じ規則で同時に更新されるので、本質的に並列な計算になる。
func lateralInhibition(_ input: [Double], strength k: Double, steps: Int) -> [Double] {
var r = input
for _ in 0..<steps {
var next = [Double](repeating: 0, count: r.count)
for i in 0..<r.count { // ← この繰り返しは並列に実行できる
let left = i > 0 ? r[i - 1] : r[i]
let right = i < r.count - 1 ? r[i + 1] : r[i]
next[i] = max(0, input[i] - k * (left + right))
}
r = next
}
return r
}
左半分が暗く右半分が明るいだけの、境界がどこにも書かれていない入力を与えます。
入力(左半分 0.30、右半分 0.70 の一様な面)
8 ############ 0.300
9 ############ 0.300
10 ############################ 0.700
11 ############################ 0.700
側抑制のあとの応答
8 ######### 0.226
9 #### 0.102 ← 段差の直前でくぼむ
10 ###################### 0.564 ← 直後に盛り上がる
11 ################## 0.441
面の内部はほぼ平坦なまま(左 0.200 付近で凹凸 0.0089、右 0.474 付近で凹凸 0.0089)
段差の手前が暗く、直後が明るく強調されました。これは人間の視覚でも観察される現象で、**マッハ・バンド(Mach bands)**と呼ばれます(注16)。
注目すべきは、どの受容器も隣としか話していないことです。全体を見渡して「ここが境界だ」と判断する中枢はありません。にもかかわらず、境界だけが浮かび上がる。第3節でラプラシアンを使って計算したのと同じことを、局所的で並列な処理だけで実現しているわけです。
ネッカーキューブ
**ネッカーキューブ(Necker cube)**は、線画で描かれた立方体が、2通りの立体として見える図形です(注17)。どちらの面が手前かが決まらず、見ているうちに反転します。
┌───────┐
╱│ ╱│
┌───────┐ │ どちらの面が手前か?
│ └─────│─┘ 見ているうちに反転する
│╱ │╱
└───────┘
この現象をネットワークで表します。8つの頂点それぞれに「手前」「奥」の2通りの解釈があるとして、16個の単位を用意します。
- 同じ解釈に属する単位どうしは支持し合う(横方向のリンク、興奮性)
- 同じ頂点の別解釈どうしは打ち消し合う(抑制性)
func weight(from j: Int, to i: Int) -> Double {
if i == j { return 0 }
let V = NeckerNetwork.vertices
if i % V == j % V { return -1.5 } // 同じ頂点の別解釈は排他
if (i < V) == (j < V) {
return useLateralLinks ? 0.15 : 0.0 // 横方向のリンク
}
return 0
}
mutating func relax(steps: Int) {
for _ in 0..<steps {
var next = activation
for i in 0..<activation.count { // ← すべての単位を同時に更新
var net = 0.0
for j in 0..<activation.count { net += weight(from: j, to: i) * activation[j] }
// 興奮は上限へ、抑制は下限へ向かって効く。加えて常に減衰する
let push = net > 0 ? net * (1 - activation[i]) : net * (activation[i] + 0.2)
let v = activation[i] + 0.1 * (push - 0.1 * activation[i])
next[i] = min(max(v, -0.2), 1.0)
}
activation = next
}
}
ほぼ互角の状態(すべての単位が 0 付近、ごくわずかな揺らぎだけ)から出発させます。
横方向のリンクあり(頂点どうしが解釈を支持し合う):
初期の揺らぎ 12345 → A=-0.19 B=0.93 Aを選んだ頂点 0/8 → 解釈Bで統一
初期の揺らぎ 99999 → A=0.93 B=-0.19 Aを選んだ頂点 8/8 → 解釈Aで統一
初期の揺らぎ 4242 → A=-0.19 B=0.93 Aを選んだ頂点 0/8 → 解釈Bで統一
初期の揺らぎ 777 → A=0.93 B=-0.19 Aを選んだ頂点 8/8 → 解釈Aで統一
8頂点の意見が必ず揃います。 そして、どちらに揃うかは初期のごくわずかな揺らぎで決まります。「半分は手前、半分は奥」という中途半端な状態には留まりません。人間が多義図形を見たとき、必ずどちらか一方に見え、両方が混ざった見え方をしないことと対応しています。
横方向のリンクを外すと、何が起きるでしょうか。
横方向のリンクなし(頂点どうしが連絡しない):
初期の揺らぎ 12345 → Aを選んだ頂点 2/8 → ばらばら
初期の揺らぎ 99999 → Aを選んだ頂点 7/8 → ばらばら
初期の揺らぎ 4242 → Aを選んだ頂点 3/8 → ばらばら
初期の揺らぎ 777 → Aを選んだ頂点 5/8 → ばらばら
各頂点が単独で決めるため、全体として辻褄の合う解釈になりません。抑制は残っているので各頂点は「手前か奥か」を決めますが、頂点どうしがばらばらに決めるので、立方体として成立しない状態になります。
「見え」をまとめているのは、層の中を横に走るリンクである。 第4節で述べた制約充足が、ここでは重みの形で直接実装されています。前進供給ネットワークが横方向のリンクを禁じるのは学習を可能にするためですが、知覚のモデルではその禁止こそが外されるわけです。
16. 逐次処理の問題点
コネクショニズムが提起した論点のうち、実装に直結するものが更新の順序です。
同じ結線、同じ規則のネットワークを、2通りの方法で動かしてみます。
- 同期更新(並列):すべての単位が、更新前の値だけを見て、同時に新しい値を計算する
- 逐次更新:単位を1つずつ順番に更新する。更新した値は、その場で次の単位の計算に使われる
func ring(size: Int, syncUpdate: Bool, steps: Int) -> [Double] {
var a = [Double](repeating: 0, count: size)
a[0] = 1.0 // 1か所だけ興奮させる
for _ in 0..<steps {
if syncUpdate {
let snapshot = a // 更新前の値だけを見る
for i in 0..<size {
let left = snapshot[(i + size - 1) % size]
a[i] = max(snapshot[i], left * 0.9)
}
} else {
for i in 0..<size {
let left = a[(i + size - 1) % size]
a[i] = max(a[i], left * 0.9) // 更新した値がその場で次に使われる
}
}
}
return a
}
=== 同時に更新するか、順番に更新するか ===
1 ステップ後
同期更新(並列): 1.00 0.90 0.00 0.00 0.00 0.00 0.00 0.00
逐次更新 : 1.00 0.90 0.81 0.73 0.66 0.59 0.53 0.48
2 ステップ後
同期更新(並列): 1.00 0.90 0.81 0.00 0.00 0.00 0.00 0.00
逐次更新 : 1.00 0.90 0.81 0.73 0.66 0.59 0.53 0.48
3 ステップ後
同期更新(並列): 1.00 0.90 0.81 0.73 0.00 0.00 0.00 0.00
逐次更新 : 1.00 0.90 0.81 0.73 0.66 0.59 0.53 0.48
同期更新では興奮が1ステップに1個ずつ進みます。逐次更新では、同じ1ステップで端まで届いてしまいます。
これは実装の細部ではありません。同じ結線・同じ規則でも、更新の順序を決めた瞬間に、モデルの意味が変わってしまうということです。神経系の振る舞いを模したいのであれば、「1ステップで信号が全体に伝わる」という逐次更新の振る舞いは明らかに間違っています。伝わるのに時間がかかることこそが、モデル化したい対象の性質だからです。
逐次的な計算機で並列なモデルを走らせるとき、この食い違いは必ず問題になります。ここから2つの帰結が出てきます。
第一に、逐次機の上では「更新前の値のコピー」を持たなければなりません。 上のコードの snapshot がそれです。並列機なら本来不要な、逐次実行の都合だけで生じる余分な作業です。
第二に、そもそも並列に動く機械を作るべきではないか、という発想が出てきます。次節の主題です。
17. コネクション・マシンとデータ並列
**コネクション・マシン(Connection Machine)**は、Danny Hillis が構想し、1980年代に Thinking Machines 社が製品化した並列計算機です(注18)。初代 CM-1 は 65,536 個の1ビット処理装置を持ち、超立方体状の通信網でつながっていました。
設計思想は明快です。データの要素1つに処理装置を1台割り当てる。 画素が100万個あれば処理装置も100万台、ネットワークの単位が10万個あれば10万台。全員が同じ命令を、それぞれ自分の担当データに対して同時に実行します。この方式を**データ並列(data parallel)**と呼びます。
プログラムの書き方も変わります。「配列を先頭から順に処理する」のではなく、「配列全体に対する操作」を並べていきます。第15節の側抑制は、こう書き直せます。
func shiftRight(_ a: [Double]) -> [Double] { [a[0]] + a.dropLast() }
func shiftLeft(_ a: [Double]) -> [Double] { Array(a.dropFirst()) + [a[a.count - 1]] }
func inhibitStep(input: [Double], response: [Double], k: Double) -> [Double] {
let left = shiftRight(response) // 隣(左)の応答を全員が同時に受け取る
let right = shiftLeft(response) // 隣(右)の応答を全員が同時に受け取る
// 以下の1行は、全要素に対して同時に適用される
return zip(zip(input, left), right).map { max(0, $0.0 - k * ($0.1 + $1)) }
}
ループ変数も、要素の順序も出てきません。 あるのは「全体を左にずらす」「全体を右にずらす」「要素ごとに計算する」という3つの全体操作だけです。この形で書かれたプログラムは、処理装置が何台あっても同じように動きます。
段数と演算数
データ並列で本当に得をしているのは何か、を数えてみます。**段数(並列に実行したときの時間ステップ数)と演算数(実際に行われる演算の総数)**を分けて数えます。
func parallelSum(_ a: [Double]) -> (Double, Cost) {
var cost = Cost()
var v = a
while v.count > 1 {
var next = [Double]()
var i = 0
while i + 1 < v.count { next.append(v[i] + v[i + 1]); cost.ops += 1; i += 2 }
if i < v.count { next.append(v[i]) }
v = next
cost.depth += 1 // 段が1つ進む
}
return (v[0], cost)
}
=== 総和:同じ答えを何段で出せるか ===
要素数 | 逐次の段数 | 並列の段数 | 逐次の演算数 | 並列の演算数
8 | 8 | 3 | 8 | 7
64 | 64 | 6 | 64 | 63
1024 | 1024 | 10 | 1024 | 1023
65536 | 65536 | 16 | 65536 | 65535
65,536 要素の総和が、65,536 段から 16 段になりました。 演算の総数はほとんど変わっていません。減っているのは段数、つまり時間のほうです。
もう1つの基本操作が**累積和(スキャン、prefix sum)**です。各位置に、そこまでの合計を入れます。逐次では前から順に足すしかなく要素の数に比例した段数がかかりますが、並列版なら log n 段で済みます。
func parallelScan(_ a: [Double]) -> ([Double], Cost) {
var cost = Cost()
var v = a
var offset = 1
while offset < v.count {
var next = v
for i in 0..<v.count where i >= offset { // ← 全要素が同時に動く
next[i] = v[i] + v[i - offset]
cost.ops += 1
}
v = next
cost.depth += 1
offset *= 2
}
return (v, cost)
}
累積和の例:入力 [3, 1, 4, 1, 5, 9, 2, 6]
逐次: [3, 4, 8, 9, 14, 23, 25, 31] 7 段 / 7 演算
並列: [3, 4, 8, 9, 14, 23, 25, 31] 3 段 / 17 演算
答えは一致するか: true
ここで並列版のほうが演算数は多い(7 対 17)ことに注目してください。データ並列は「計算を減らす」技術ではありません。余分な計算をしてでも段数を減らす技術です。処理装置が潤沢にあるなら、この取引は割に合います。
この「段数と演算数を分けて考える」見方と、スキャンを基本操作として組み立てる作法は、コネクション・マシンの研究から生まれ、現在の並列計算の標準的な語彙になりました(注19)。
18. その後の30年
ここまで扱ってきたのは、おおむね1990年前後までの構想です。その後どうなったかを、領域ごとに見ます。
視覚 ― パイプラインは消え、畳み込みは残った
第2節で見た「イメージ → 原始スケッチ → 2.5次元スケッチ → 3次元モデル」という段階的な設計は、そのままの形では実現しませんでした。 各段階を人手で設計する方針は、1990年代を通じて期待した性能に届きませんでした。境界検出の精度を上げても、その先の段階が思うように積み上がらなかったのです。
決定的な転換は2012年です。AlexNet が画像分類コンテストで、それまでの手法を大差で上回りました(注20)。中身は、生の画素から分類結果までを1つのネットワークで学習する**畳み込みニューラルネットワーク(CNN)**でした。人手で設計した境界検出も、原始スケッチも、途中の表現の定義も、いっさい使いません。
ただし、消えたのは手で設計した部分であって、考え方まで消えたわけではありません。
| 1990年の構想 | 現在 |
|---|---|
| ガウスの畳み込み | 畳み込みは残った。 重みが手作りから学習済みに変わった |
| 境界検出のフィルタ | CNNの浅い層が、学習の結果として境界検出器に似たものを作る |
| スケール空間(複数のσ) | 特徴ピラミッド、マルチスケール処理として標準的に使われる |
| 2.5次元スケッチ | 単眼深度推定として復活。画像1枚から距離の地図を出す |
| 3次元モデル表現 | NeRF や3Dガウシアン・スプラッティングなど、別の形で追求されている |
とりわけ興味深いのは、学習したCNNの浅い層を可視化すると、特定の向きの境界に反応するフィルタが現れることです。Marr たちが手で設計しようとしたものに近いものが、事例から自動的に生まれてきます。何を計算すべきかについての分析は正しかった。間違っていたのは、それを人が書き下せるという前提のほうだったと見ることができます。
学習 ― 記号から統計へ、そして規模へ
候補排除アルゴリズムは、ノイズに弱いという弱点のため実用から退きました。その後の流れは次のとおりです。
| 時期 | 中心 |
|---|---|
| 1980年代 | バージョン空間、決定木の学習(ID3、後に C4.5) |
| 1990年代 | 統計的な学習。サポートベクターマシン、確率モデル。誤りを許容する枠組みへ |
| 2000年代 | アンサンブル学習。データ量の増加 |
| 2010年代以降 | 深層学習。規模の拡大が性能を押し上げる |
一方で、第8節で見た帰納バイアスという概念は、いっそう中心的になりました。「バイアスのない学習は一般化できない」という主張は、現在も変わらず成り立ちます。CNNが画像に強いのは、「近くの画素は関係が深い」「位置がずれても同じものは同じ」というバイアスが構造に埋め込まれているからです。どんなバイアスを設計に埋め込むかが、現在のモデル設計の中心にあります。
信頼度割り当て問題も生き続けています。時間方向の割り当ては強化学習の中心課題であり続け、構造方向の割り当ては誤差伝播学習として、現在のあらゆる学習の土台にあります。現在の大規模モデルの学習も、第13節で書いた連鎖律の適用そのものです。 規模と工学が変わっただけで、原理は1986年から変わっていません。
コネクショニズム ― 2度目の冬と、その後
1986年の誤差伝播学習の後、コネクショニズムは再び停滞します。層を深くすると誤差信号が薄れて学習が進まない(勾配消失)、計算資源が足りない、データが足りない、といった事情が重なりました。1990年代後半から2000年代前半は、サポートベクターマシンなど別の手法が主流でした。
流れが変わったのは2006年以降、そして2012年に決定的になります。変わったのは原理ではなく、条件のほうでした。
| 要因 | 内容 |
|---|---|
| 計算資源 | GPUによる大幅な高速化 |
| データ | インターネットの普及による大規模なデータセット |
| 技術的な改良 | 活性化関数、初期化、正規化など、深い層を学習させるための工夫の蓄積 |
2017年に登場した Transformer は、現在の大規模言語モデルの基礎になりました(注21)。第14節の NETtalk が「周囲7文字の窓」で文脈を捉えようとしたのに対し、Transformer は文脈のどこを見るかを学習で決めます。文脈を固定幅の窓で切る代わりに、必要な場所へ注意を向ける。 課題の設定は地続きです。
音声の側でも、NETtalk の課題設定そのものが変わりました。かつては「綴り → 音素 → 音声波形」と段階を踏むのが当然でしたが、2016年の WaveNet 以降、テキストから波形までを直接学習する方式が主流になりました(注22)。視覚で起きたのと同じ、中間表現の消滅がここでも起きたわけです。
コネクション・マシン ― 機械は消え、モデルは残った
Thinking Machines 社は1994年に破綻しました。専用の並列機という商売は、汎用の計算機の性能向上に追いつかれたのです。
しかし、プログラミングのモデルのほうは完全に生き残りました。 現在のGPUは、まさに「データの要素1つに処理装置1台」という発想の機械です。数千個の演算器が同じ命令を別々のデータに対して同時に実行します。第17節で書いたスキャンや畳み込みは、GPU向けライブラリの基本部品としてそのまま使われています。
深層学習がGPUの上で花開いたのは偶然ではありません。ニューラルネットワークの計算は、もともとデータ並列の形をしています。 第15節の「すべての単位を同時に更新する」は、そのままGPUの得意な処理です。1980年代にコネクショニズムのために構想された計算のモデルが、20年後に別の形の機械として実現し、その上でコネクショニズムが復活した、という順序になります。
決着していない論点
最後に、決着していないことを1つ挙げておきます。
コネクショニズムが台頭した1988年、Jerry Fodor と Zenon Pylyshyn は、記号を持たないネットワークでは思考の体系性を説明できないと批判しました(注23)。「太郎が花子を愛する」を理解できる者は必ず「花子が太郎を愛する」も理解できる。この体系性は、構成的な記号構造を前提にしなければ説明できない、という主張です。
現在の大規模言語モデルは、この批判に対する明快な答えにはなっていません。文法的な構成性をかなりの程度扱えるように見える一方で、規則を確実に適用すること、多段の推論を誤りなく行うこと、学習した範囲を外れた組み合わせに対応することでは、依然として不安定さを見せます。記号処理を外部の道具として呼び出す設計(計算や検索を外部に任せる方式)が広く使われているのは、この弱点への実務的な対処と見ることができます。
記号とネットワークのどちらか一方が勝った、という決着はついていません。 このシリーズが扱ってきた記号主義の道具立て――知識の明示的な表現、規則による推論、空間の探索――は、姿を変えて現在のシステムの中に残り続けています。
まとめ
この記事では、記号主義AIが正面から扱えなかった2つの領域を見てきました。
知覚については、視覚が投影の逆をたどる不良設定問題であり、世界についての前提を持ち込まなければ解けないことを確認しました。ビット・マップから原始スケッチ、2.5次元スケッチ、3次元モデル表現へと段階を踏む構想は、各段階の表現を明確に定義した点で今も価値があります。実装では、ガウスの畳み込みによる平滑化がノイズと本当の境界を分けるうえで決定的であること、そして σ の選択に唯一の正解がないことを、実測で確かめました。
学習については、実行部分と学習部分という分け方、事例と仮説を同じ言葉で書く単一表現のトリック、そして学習の中心にある信頼度割り当て問題を整理しました。EPAMの弁別ネットワークは、必要なだけ区別を覚えるという方針と、提示された順序が記憶の形を左右するという性質を示しました。候補排除アルゴリズムは、48個の候補を4つの事例で1個に絞り込み、「まだ分からない」と言えることや「次に何を尋ねるべきか」が分かることを実演しました。同時に、ノイズに弱いという弱点も明らかになりました。
コネクショニズムについては、パーセプトロンが線形分離できる問題しか扱えないことを総当たりで確かめ、隠れ層と誤差伝播学習がその壁を越えることを見ました。綴りから音素への変換では、規則を一度も教えていないネットワークが、事例だけから c の読み分けを48か所すべてで正しく行いました。側抑制とネッカーキューブでは、局所的で並列な処理だけから境界の強調と一貫した解釈が生まれることを、そして層の中を横に走るリンクがそれを支えていることを確認しました。
そして、この3つを貫く主題が並列性でした。更新の順序を決めた瞬間にモデルの意味が変わるという事実は、逐次計算機の上で並列なモデルを扱うことの難しさを示しています。コネクション・マシンは、その難しさに機械の側から答えようとした試みでした。会社は消えましたが、データ並列というモデルはGPUとして生き残り、その上で深層学習が花開きました。
シリーズ全体を振り返ると、扱ってきた道具立ては、知識表現、自然言語処理、エキスパートシステム、ヒューリスティック探索、ゲーム木の探索、そして今回の知覚と学習でした。1990年前後に描かれたこれらの構想の多くは、その形のままでは実現しませんでした。人が知識を書き下す方式は知識獲得のボトルネックに阻まれ、人が中間表現を設計する方式は学習に置き換えられました。
一方で、問題の立て方のほうは、驚くほど残っています。 探索の枠組みはゲームAIの中核であり続け、信頼度割り当ては学習の中心課題であり続け、帰納バイアスの必要性は原理として変わらず、畳み込みは形を変えて生き残りました。「探索を減らすのは知識である」という主題も、「バイアスのない学習は一般化できない」という主題も、結局は同じことを別の角度から言っています。何もないところからは何も出てこない。どこかで前提を持ち込まなければならない。 変わったのは、その前提を人が書くのか、事例から獲得させるのか、という一点でした。
古い教科書を読む価値は、そこにあります。答えの多くは古びますが、問いの立て方は古びません。
注釈
- Hermann von Helmholtz, "Handbuch der physiologischen Optik"(1867年)。知覚を、感覚入力から原因を推定する「無意識の推論(unbewusster Schluss)」として説明した。この考え方は、現在のベイズ的な知覚理論にも受け継がれている。
- David Marr, "Vision: A Computational Investigation into the Human Representation and Processing of Visual Information"(1982年、著者の没後に刊行)。原始スケッチ・2.5次元スケッチ・3次元モデル表現という表現の系列を提示した。なお Marr は別に、情報処理システムを「計算理論」「表現とアルゴリズム」「ハードウェアによる実現」の3つの水準で分析すべきだと論じており、こちらの「3つの水準」と、本文で扱った「3段階の表現」は別の話である。
- David Marr・Ellen C. Hildreth, "Theory of Edge Detection"(Proceedings of the Royal Society of London B, 1980年)。ガウス関数で平滑化してからラプラシアンを取り、そのゼロ交差を境界とする方式を示した。1次微分にもとづく手法としては John Canny, "A Computational Approach to Edge Detection"(IEEE TPAMI, 1986年)が広く使われている。
- Marvin Minsky, "Steps Toward Artificial Intelligence"(Proceedings of the IRE, 1961年)。信頼度割り当て問題を、学習する機械が直面する基本的な困難として定式化した。
- Richard S. Sutton, "Learning to Predict by the Methods of Temporal Differences"(Machine Learning, 1988年)。時間差分学習を定式化した。Gerald Tesauro の TD-Gammon(1992年、1995年)は、この方法と神経回路網を組み合わせてバックギャモンで世界最高水準に達した初期の例。
- Tom M. Mitchell, "The Need for Biases in Learning Generalizations"(技術報告、1980年)。事前の偏りなしに帰納的な一般化はできないことを論じた。
- Edward A. Feigenbaum・Herbert A. Simon, "EPAM-like Models of Recognition and Learning"(Cognitive Science, 1984年)。EPAM の初出は Feigenbaum の博士論文(1959年)および "The Simulation of Verbal Learning Behavior"(1961年)。弁別ネットワークを成長させることで、人間の言語学習実験の結果を再現した。
- Tom M. Mitchell, "Generalization as Search"(Artificial Intelligence, 1982年)。バージョン空間と候補排除アルゴリズムを定式化し、概念学習を仮説空間の探索として整理した。
- David E. Rumelhart・James L. McClelland・PDP Research Group, "Parallel Distributed Processing: Explorations in the Microstructure of Cognition"(全2巻、1986年)。コネクショニズムの立場を体系的に提示し、この分野の再興の中心になった書物。
- Frank Rosenblatt, "The Perceptron: A Probabilistic Model for Information Storage and Organization in the Brain"(Psychological Review, 1958年)。収束定理は同著者の "Principles of Neurodynamics"(1962年)などで扱われている。
- Marvin Minsky・Seymour Papert, "Perceptrons: An Introduction to Computational Geometry"(1969年)。1層のパーセプトロンで計算できる関数の範囲を厳密に分析した。
- David E. Rumelhart・Geoffrey E. Hinton・Ronald J. Williams, "Learning representations by back-propagating errors"(Nature, 1986年)。同等の手法は Seppo Linnainmaa(1970年、逆モードの自動微分)や Paul Werbos(1974年、博士論文)が先行して示している。
- Terrence J. Sejnowski・Charles R. Rosenberg, "Parallel Networks that Learn to Pronounce English Text"(Complex Systems, 1987年)。7文字の窓を入力とし、中央の文字の音素を出力するネットワークで英語の読み上げを学習した。学習の経過を音声にした記録が広く知られている。
- Gary S. Dell, "A Spreading-Activation Theory of Retrieval in Sentence Production"(Psychological Review, 1986年)。語のレベルと音素のレベルの間を活性が双方向に広がるモデルによって、レキシカル・バイアスや音素反復といった言い間違いの規則性を説明した。
- Haldan K. Hartline・Floyd Ratliff らによるカブトガニ(Limulus)の複眼の研究(1950年代〜1960年代)。側抑制の定量的なモデルを与えた。Hartline は1967年のノーベル生理学・医学賞を受賞している。
- Ernst Mach が1865年に報告した明るさの錯視。明るさが変化する境界の暗い側がより暗く、明るい側がより明るく見える。側抑制によって説明される。
- Louis Albert Necker が1832年に報告した多義図形。奥行きの手がかりが欠けているため、2通りの立体解釈が両立し、知覚が反転する。多義図形を相互抑制つきのネットワークで扱う考え方は、注9の文献などで扱われている。
- W. Daniel Hillis, "The Connection Machine"(1985年、MIT の博士論文にもとづく)。初代 CM-1 は 65,536 個の1ビット処理装置を超立方体状の通信網で結んだ。*Lisp や C* といったデータ並列向けの言語が用意された。Thinking Machines 社は1994年に破綻している。
- W. Daniel Hillis・Guy L. Steele Jr., "Data Parallel Algorithms"(Communications of the ACM, 1986年)。本文の並列スキャンはここで示された方式にあたる。Guy E. Blelloch, "Prefix Sums and Their Applications"(技術報告、1990年)が、スキャンを並列計算の基本操作として体系化した。
- Alex Krizhevsky・Ilya Sutskever・Geoffrey E. Hinton, "ImageNet Classification with Deep Convolutional Neural Networks"(NIPS, 2012年)。大規模な画像分類で、それまでの手法を大きく上回る結果を示した。畳み込みニューラルネットワーク自体は Yann LeCun らの LeNet(1989年〜)に遡る。
- Ashish Vaswani ほか, "Attention Is All You Need"(NIPS, 2017年)。注意機構だけで構成した Transformer を提示し、現在の大規模言語モデルの基礎になった。
- Aäron van den Oord ほか, "WaveNet: A Generative Model for Raw Audio"(2016年)。音声波形を直接生成する方式を示し、それまでの段階的な音声合成を置き換えていった。
- Jerry A. Fodor・Zenon W. Pylyshyn, "Connectionism and Cognitive Architecture: A Critical Analysis"(Cognition, 1988年)。思考の体系性・生産性・構成性を根拠に、記号構造を持たないネットワークだけでは認知を説明できないと論じた。
- 本記事のサンプルはすべて実際にコンパイル・実行して確かめた。掲載した境界点の個数、バージョン空間の大きさ、正解率、活性値、段数と演算数は、いずれも実測値である。テスト画像・動物の特性ベクトル・綴りと音素の対応表は、いずれも本記事のために用意したもの。
参考資料
- Computer vision - Wikipedia
- David Marr (neuroscientist) - Wikipedia
- Marr–Hildreth algorithm - Wikipedia
- Edge detection - Wikipedia
- Gaussian blur - Wikipedia
- Scale space - Wikipedia
- Inverse problem - Wikipedia
- Machine learning - Wikipedia
- Credit assignment problem - Wikipedia
- Concept learning - Wikipedia
- Version space learning - Wikipedia
- Inductive bias - Wikipedia
- EPAM - Wikipedia
- Connectionism - Wikipedia
- Perceptron - Wikipedia
- Perceptrons (book) - Wikipedia
- Backpropagation - Wikipedia
- Feedforward neural network - Wikipedia
- NETtalk (artificial neural network) - Wikipedia
- Lateral inhibition - Wikipedia
- Mach bands - Wikipedia
- Necker cube - Wikipedia
- Connection Machine - Wikipedia
- Data parallelism - Wikipedia
- Prefix sum - Wikipedia
- AlexNet - Wikipedia
- Convolutional neural network - Wikipedia
- Transformer (deep learning architecture) - Wikipedia
- WaveNet - Wikipedia
元記事(Bitz Notebook): 知覚・学習・コネクショニズム ― ビット・マップから3次元モデル、弁別ネットワーク、並列分散処理まで