1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

前回、ショウジョウバエの脳の配線図をまるごとダウンロードして、13万9255個のニューロンをそのとおりに繋いで計算し、机の上のロボットを手から逃げさせました。
demo_c_light.gif

ただし前回のロボットには、一つだけ足りないものがありました。
何度やっても、まったく同じように逃げる。
同じ手を100回近づけても、反応が1ミリも変わりません。シナプスの強さが固定値だからです。

今回はそこを動くようにしました。結果はこうなりました。

  元気なとき   秒速45センチの手に、86ミリ手前で逃げる
  疲れたあと   秒速21センチまで引きつけて、48ミリまで近づけてから逃げる
  2分休むと    元に戻る

robot_full.gif

繰り返し脅かされると、だんだん反応しなくなる。
そして休ませると戻る。生き物が「慣れる」というのは、たぶんこういうことです。

robot_red.jpg

写真は実際に逃げた瞬間です。画面右下に LC4→DNp01 STRENGTH 11% と出ています。
シナプスが11%まで弱った状態でも、ゆっくり近づければまだ逃げる。
この記事は、そこに辿り着くまでの話です。


どこを弱らせるか

「慣れ」を作るには、脳のどこかのシナプスを、使うほど弱くなるようにします。問題はどこを弱らせるかです。
自作のコネクトームモニタ

いま使っている経路は、目(LC4 と LPLC2 という視覚の細胞)から DNp01 という巨大な神経へ、という一本道です。DNp01 はハエの逃避を指揮する細胞で、叩こうとして逃げられるときに働いているのがこれです。

私は最初、ここを避けるべきだと考えました。この経路は「見えた、逃げろ」だけの高速な反射で、学習する場所という印象がない。ハエで学習といえばキノコ体という部位が有名で、そちらなら実装例もあります。

そこでまず、目からキノコ体へ信号が届くのか、配線を調べました。

  直接つながっているか     0本
  2ホップで届くか         5177個のうち197個に、ごくわずか
  ランダムな細胞と比べて   1000分の1以下

まったく繋がっていませんでした。
適当に選んだ314個の細胞のほうが、まだキノコ体に影響を持っています。

念のため、測り方が壊れていないかを確かめました。視覚からキノコ体への入力自体は、ちゃんと見つかります。

キノコ体 γd(295細胞)への入力 キノコ体 γ main(2189細胞)への入力
aMe12(視覚) 6.4% DC1_adPN(嗅覚) 1.7%
MTe32(視覚) 3.5% DL1_adPN(嗅覚) 1.6%
MTe30(視覚) 2.9% DA1_lPN(嗅覚) 1.5%

片方には視覚、もう片方には匂いが入っている。測定は動いています。ただしその視覚入力を運んでいるのは aMe12 や MTe32 という別の細胞で、私が使っている LC4 / LPLC2 ではありませんでした。

ここで論文を読みました。Engel & Wu が1996年に、まさにこの逃避反応を生きたハエで慣れさせています。抄録にこうありました。

Although habituation is localized to afferents to the giant fiber ...

「慣れは巨大神経の入り口側で起きている」。私が「学習する場所ではない」と言って避けようとしていた、まさにそこでした。

判断の理由そのもの——仕組みが分かっている場所を選ぶ——は間違っていませんでした。ただ、自分がいま使っている回路について書かれた論文を、まだ読んでいませんでした。

場所はさらに絞れました。Gaitanidis らが2025年に、LC4 という細胞のカリウムチャネルを壊すと慣れが消え、LPLC2 では大して変わらないと報告しています。弱らせるのは LC4 から DNp01 への繋ぎ目だけ。これで、どこを触るかは自分の勘ではなく論文が決めたことになりました。

配線は CSR(圧縮行格納)で持っているので、「LC4 から DNp01 への興奮性の枝」を抜き出すのはこれだけです。1509万本の中から104本が残ります。

m = np.zeros(n, bool)
m[gf] = True                                     # gf は DNp01 の2細胞
e = np.flatnonzero(m[targets])                   # DNp01 に入る枝のid
src = np.repeat(np.arange(n), np.diff(indptr))[e]  # その枝の出どころ
keep = (signed[e] > 0) & np.array([p in lc4 for p in src])
return e[keep]                                   # -> 104本

どう弱らせるか

弱らせ方も論文が決めてくれます。この慣れは細胞内の cAMP という物質に依存していて(それを作れない変異体では慣れが遅く、分解できない変異体では速い)、入り口側で起きている。だから「信号が来たら少し弱る、時間が経つと戻る」という、手前側だけの単純な形になります。

  信号が1発来るたび   強さ ← 強さ × (1 − U)
  何も来ない間        強さ → ゆっくり1.0へ戻る(時定数 τ)

実装はこれだけです。pos はそのステップで信号が通った枝のid、D が枝ごとの強さ。全部で1509万本ある枝のうち104本だけを触るので、毎ステップ全部を走査するのではなく、ソート済みの104本に対して二分探索して当たったものだけ更新します。

def gate(self, pos, w):
    k = np.searchsorted(self.edges, pos)
    np.clip(k, 0, self.edges.size - 1, out=k)
    hit = self.edges[k] == pos          # 弱らせる対象の枝か
    if not hit.any():
        return w
    kh = k[hit]
    w = w.copy()
    w[hit] *= self.D[kh]                # 弱った分を反映して届ける
    self.D[kh] *= (1.0 - self.u)        # そして少し弱る
    return w

0.1ミリ秒ごとに呼ばれるので、ここが重いと全体が遅くなります。実測では、この処理を入れても計算時間はほぼ変わりませんでした。

自分で決める数字は U と τ の2つだけで、どちらも論文の値から逆算できます。

  • τ は「2分休ませると完全に戻る」から40秒
  • U は「5 Hzで叩くと平均85回目で反応しなくなる」から二分探索

U を「1発あたりに cAMP がどれだけ効くか」と読むと、おまけが付きました。cAMP が少ない変異体は U が小さくて慣れが遅い、多い変異体は U が大きくて速い。論文が報告している変異体の順番が、新しい数字を足さずにそのまま出ます。

弱らせ方には別の案もありました。「出口側の細胞が発火したときだけ弱らせる」という、より学習らしい形です。これは退けました。判定の基準が「5回続けて反応しなかったら慣れたとみなす」なので、出口側の発火を条件にすると、1回目の無反応で弱らせるのが止まってしまい、残り4回に進めません。基準の一歩手前で自分を止める形になります。

較正していて、予想していなかったことが起きました。

  U=0.0291 のとき
    Gaitanidis の「100回で65%減衰」    →  実測 60%
    Engel & Wu の「85回で反応停止」    →  実測 86回

別々の研究室の、別々の測り方の数字が、1つの値で同時に合いました。
片方に合わせたら、もう片方が勝手に合った。狙ってできることではないので、これは嬉しい誤算でした。

戻り方も確認しました。

  休息   シナプスの強さ   反応の回復
    0秒        0.56            0%
   15秒        0.70           40%
   60秒        0.90           87%
  120秒        0.98          107%

ひとつ白状しておくと、120秒の行は半分やらせです。「完全に戻る=時定数の3倍」と読んで τ を40秒に決めたので、120秒で戻るのは仕込み済みです。やらせでないのは途中の形(15秒で40%、60秒で87%)のほうで、そこは何にも合わせていません。

時計が2つある問題

実装で一番悩んだのはここです。

この脳は実時間の12倍遅い。
ロボットは0.3秒ごとに判断しますが、その1回で脳の中は0.025秒しか進みません。残りの0.275秒は、脳にとって存在しない時間です。

シナプスの回復を脳の時計に任せると、ハエなら2分で戻るものが、ロボットでは24分かかります。

幸い、このモデルには自発的な発火がありません。誰も何も喋っていない時間は、シナプスの強さが単純な式に従うだけです。そこで、脳が止まっている間の実時間ぶんを、式で進めてやることにしました。

wall = time.monotonic()
if dep is not None and self._last_wall is not None:
    gap = (wall - self._last_wall) - self.chunk_s   # 脳が居なかった時間
    if gap > 0:
        dep.recover_seconds(gap)
self._last_wall = wall

recover_seconds は、シミュレーションを回さずに式だけで進めます。誰も発火していない区間なら、これは近似ではなく厳密解です。

def recover_seconds(self, seconds):
    f = float(np.exp(-seconds / self.tau_rec))
    self.D += (1.0 - self.D) * (1.0 - f)

これのおかげで、「2分休ませる」実験を実際に2分待たずに測れます。まともに回すと、脳が12倍遅いので24分かかります。

これでモデルは意図的にちぐはぐになります。シナプスの回復だけが実時間で、他は脳の遅い時計のまま。後から読む人が必ず引っかかるので、理由ごと書き残しました。


測ってみたら、いったん外した

さて、慣れは入りました。効いているでしょうか。

予測はこうです。LC4 は「近づいてくる速さ」を、LPLC2 は「見かけの大きさ」を担当している。弱らせたのは LC4 のほうだけ。だから慣れると「速い手に早めに反応する」性質が消えて、「近づかれたら反応する」だけが残るはず。

結果を見る前に、合格ラインを決めて書き出しました。後から都合よく解釈しないためです。ログの先頭に印字してから測定を始めるので、後で書き換えられません。

say("HIT  : S_after <  S_before - 2*sd(S)  AND  |dI| <= 2*sd(I)")
say("MISS : S_after >= S_before - 2*sd(S)  OR   I_after < I_before - 2*sd(I)")
...
before = measure()      # ここから先が測定

当たり — 速い接近で逃げる距離が縮み、遅い接近は変わらない
外れ — 速い接近で変わらない、または遅い接近も同じだけ縮む

測った結果がこれです。

   手の速さ    慣れ前   慣れ後    変化
   0.05 m/s      180     180     0 mm
   0.20 m/s      180     180     0 mm
   0.50 m/s      220     180    40 mm
   0.80 m/s      280     180   100 mm
   測定のばらつき 40 mm

速い方は両方縮み、遅い方は動いていない。予測どおりの形です。それでも判定は「外れ」でした。40 > 40 が成り立たないからです。

そして、これは運が悪かったのではありません。測定の刻みが40ミリなので、ばらつきも最小で40ミリになります。つまり「40ミリぶんの効果」は、この合格ラインでは絶対に通れません。ものさしの目盛りと合格ラインを同じ大きさにしていたという、設計のミスでした。

ここで「実質当たりでしょう」と書きたくなりますが、それをやると合格ラインを先に決めた意味が消えます。測り方のほうを直しました。


測り方を変えたら、はっきり出た

逃げる距離ではなく、「手の速さを上げたとき、DNp01 の反応がどれだけ増えるか」を測ることにしました。その増え方が LC4 の担当ぶん、速さゼロでも残る底上げが LPLC2 の担当ぶんになります。連続した量なので、目盛りに潰されません。

                速さへの反応         速さゼロでも残る分
  慣れる前        91.0 ± 8.2            424.3 ± 8.2
  慣れたあと     -20.4 ± 4.7            416.8 ± 7.7
  2分休んだ後     93.1 ± 2.5            421.2 ± 11.9

速さへの反応が消え、底上げのほうは動かず、休むと完全に戻りました。
合格ラインに対して7倍の余裕です。左右の DNp01 を別々に見ても同じ向きで、右側の底上げは 229.6 → 230.0 とほとんど動きません。

予測していなかったのは、速さへの反応がマイナスまで行ったことです(91 → −20)。慣れたあとは、速い手のほうがわずかに反応が小さい。

理由の見当は付いています。LC4 は DNp01 を直接押すだけでなく、DNp01 を抑える側の細胞も動かしています。弱らせたのは押す側だけなので、抑える側が残る。配線を数えると、DNp01 が受けている入力の約半分は抑える側で、PVLP024 という細胞は自分の入力の45〜56%を LC4 / LPLC2 から直接もらっています。

ただしこれは推測で、確かめていません。慣れの前後で PVLP024 の発火を測れば分かるはずですが、まだやっていません。


実機で動かす

ここからが本番です。

最初に動かしたとき、何も変わらないように見えました。画面上のシナプスの強さは10%を切っているのに、手を近づけるとロボットは普通に下がる。

調べたら、下がっていたのは別の細胞でした。

  5分間の記録
    DNp01 による逃避      1 回
    MDN による後退      213 回

MDN(moonwalker descending neuron)は、ハエを後ろ歩きさせる細胞です。逃避とは役割が違って、じわじわ下がる。机の向こうから見ると、2つの後退は見分けがつきません。

しかも配線を数えたら、そもそも届かない経路でした。

  MDN   が受けている興奮性入力 6,191   うち LC4 から 0、LPLC2 から 0
  DNp01 が受けている興奮性入力 4,752   うち LC4 から 805、LPLC2 から 1080

MDN は LC4 からも LPLC2 からも直接もらっていません。だから LC4 をいくら弱らせても、MDN には届かない。

MDN を切れば慣れは見えます。でも MDN は MDN で仕事をしているので、切ると慣れたロボットが完全な置物になります。

そこで、切らずに見分けられるようにしました。ロボットの LED を、DNp01 の逃避なら赤+短いビープ音、MDN の後退なら青+無音、何もなければ消灯。

これで初めて、逃避のほうは完全に止まっていたことが見えました。

見るために計算を変えてしまわないこと

表示を足すとき、一つ気をつけたことがあります。
画面に出す処理が、計算結果を1スパイクも変えないこと。

送信は投げっぱなしの UDP で、ソケットは非ブロッキング、例外は全部握り潰します。受け手が居なくても、遅くても、固まっていても、シミュレーションは待ちません。そして、それを検査するテストを書きました。

base,   _ = run(feed_port=None)        # 送信なし
quiet,  _ = run(feed_port=8791)        # 送信あり・受け手なし
wedged, _ = run(feed_port=8792)        # 送信あり・受け手が固まっている

assert np.array_equal(base_times, quiet_times)
assert np.array_equal(base_times, wedged_times)

3番目が本番です。受信バッファを小さくして絶対に読まないプロセスを立てておく。ここでブロックしていると、
「脳が遅い」ように見えて原因に辿り着けません。
36,909スパイクが3通りとも完全に一致することを確認しています。


慣れるロボット

最後にもう一つ、数字をひねる必要がありました。

このモデルは「見かけの大きさ」だけで逃避の閾値を越えてしまいます。LC4 を疲れさせても、LPLC2 だけで逃げてしまう。そこで、大きさの入力を弱めることにしました。

動かしてよいのはこの1つだけ、と決めました。配線図も、細胞の計算式も、慣れの強さも回復の速さも触りません。距離を刺激の強さに変換する部分は、もともと私が勝手に決めた部分なので、そこだけ。

値も目で選ばず、条件を先に書いてから探しました。

  1  元気なとき、速い手   逃げること
  2  元気なとき、遅い手   逃げること
  3  疲れたとき、速い手   逃げないこと

条件2は入力を強くしたがり、条件3は弱くしたがります。同じつまみに正反対を要求しているので、通る範囲は狭く、0.20〜0.30 しかありませんでした。真ん中の 0.25 を採りました。

そして実機で2時間25分、38回の逃避を記録しました。

   シナプスの強さ   逃げた回数   そのときの手の速さ   逃げた距離
   50%以上              3回        0.45 m/s            86 mm
   20〜50%             10回        0.43 m/s            72 mm
   20%未満             25回        0.21 m/s            48 mm

慣れるほど、より近く、よりゆっくりの相手にしか反応しなくなります。
元気なときは秒速45センチの手に9センチ手前で逃げていたのが、疲れると秒速21センチまで引きつけて、5センチまで近づけてから逃げる。

そしてシナプスが20%を切った状態でも、秒速7〜14センチのゆっくりした手に10回逃げています。
冒頭の写真がその1回です。

戻ることも実機で確認しました。手を出さずに放っておいた区間からシナプスの回復速度を逆算すると、40.3秒。設定した40秒とほぼ完全に一致します。

robot_full.gif


何を測って、何を推測したか

前回と同じ節を置きます。ここを飛ばすと、ただの誇大広告になるので。

測ったもの。 慣れによって速さへの反応が 91 → −20 に落ち、底上げの分は動かず、2分で戻ること。実機で逃避距離が 86 → 48 ミリに、反応する手の速さが 0.45 → 0.21 m/s に下がること。回復の時定数が実機で40.3秒であること。LC4 / LPLC2 からキノコ体へ繋がっていないこと。MDN が LC4 / LPLC2 から直接入力を受けていないこと。

推測であって、確かめていないもの。 慣れたあとに速さへの反応がマイナスまで行くのは、抑える側の細胞が残るからだということ。本物のハエなら大きさへの反応がもっと控えめで、最後のつまみ調整は要らなかっただろうということ。後者は、このモデルの LPLC2 が「動きの向き」を見る仕組みを持っていないことに由来すると考えていますが、その因果自体を確かめてはいません。

そして、どちらに属するかは固定ではありません。「大きさの入力は近づいているときだけ与える」も「慣れは LC4 だけに置く」も、最初は私の勘で、論文を読んだら論文由来に移りました。逆も起こりえます。


やってみて

慣れるロボットができました。繰り返し脅かすと反応が鈍り、近くまで引きつけるようになり、休ませると戻る。生き物らしい振る舞いのうち、前回できなかったものが一つ増えました。

面白かったのは、「どこを弱らせるか」を自分の勘で決めようとして外したことです。学習といえばキノコ体、という連想は間違っていませんが、自分が使っている回路の論文を読めば答えが書いてありました。配線を数えたら、そもそもキノコ体には繋がっていなかった。

同じ動きを2つの細胞がやっていたのも、しばらく気づけませんでした。LED を赤と青に分けるまで、逃避が完全に止まっていることが見えなかった。見分けられるようにするだけで、分からなかったことが分かるという、わりと普遍的な話だと思います。

そして、合格ラインを先に決めたのは正解でした。
1回目は外れが出て、しかもそれが測り方の欠陥だと分かった。後から基準を決めていたら、同じデータを見て「当たった」と書いていたはずです。


今回書いたもの

flywire/model/plasticity.py              シナプスが弱る仕組み
flywire/scripts/habituation_plastic.py   論文の数字への較正
robot/habituation_slope.py               合格ラインを先に決めた検証
robot/demo_gain.py                       条件つきのつまみ探索
robot/check_size_gate.py                 「近づいているときだけ」の回帰テスト
flywire/scripts/check_monitor.py         画面表示が計算結果を変えないことの検査

判断の記録は flywire/ASSUMPTIONS.md にあります。外れた推測も、外れたまま残してあります。

引用

Engel, J.E., Wu, C.-F. Altered habituation of an identified escape circuit in Drosophila memory mutants. J Neurosci 16(10), 3486–3499 (1996).

Gaitanidis, A. et al. The Drosophila escape motor circuit shows differential vulnerability to aging linked to functional decay. PLOS Biology (2025). https://doi.org/10.1371/journal.pbio.3003553

Klapoetke, N.C. et al. Ultra-selective looming detection from radial motion opponency. Nature 551, 237–241 (2017).

Ache, J.M. et al. Neural basis for looming size and velocity encoding in the Drosophila giant fiber escape pathway. Current Biology 29, 1073–1081 (2019).

Shiu, P.K. et al. A leaky integrate-and-fire computational model based on the connectome of the entire adult Drosophila brain reveals insights into sensorimotor processing. Nature (2024). https://doi.org/10.1038/s41586-024-07763-9

FlyWire のデータは CC BY-NC 4.0、非商用限定です。取得手順と謝辞は前回の記事にまとめてあります。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?