# FruitFly —— 果蝇玩俄罗斯方块 用 **MaleCNS 雄性果蝇全中枢神经系统连接组**(Janelia,166,700 个神经元、2,558 万个化学突触) 驱动一个闭环俄罗斯方块智能体。 棋盘 → 复眼光感受器 → 脉冲神经网络 → 下行神经元 → 动作 → 回灌游戏,每个仿真 tick 走一圈。 --- ## 先说结论(诚实版) **能跑通,但玩不好。** 这不是缺陷,是这个模型如实反映的结果。 ### 长局对照(400 块上限,3 局平均) | 智能体 | 均分 | 均消行 | 每块消行 | 顶出率 | |---|---|---|---|---| | Random(下界) | 0.0 | 0.0 | 0.000 | 100% | | Greedy(只算洞数+高度) | 64.7 | 47.3 | 0.279 | 100% | | **Dellacherie(强基线)** | **188.0** | **156.3** | **0.391** | **0%** | | 纯连接组(166,700 细胞,无学习) | 0.0 | 0.0 | 0.000 | 100% | | 固定接线 + 可塑读出 | 0.0 | 0.0 | 0.000 | 100% | ### 短局对照(60 块上限,5 局平均,正式实验配置) | 智能体 | 均分 | 均消行 | 每块消行 | 顶出率 | |---|---|---|---|---| | Random | 0.20 | 0.20 | 0.011 | 100% | | Greedy | 27.40 | 19.20 | 0.322 | 20% | | Dellacherie | 24.60 | 20.00 | 0.333 | 0% | | 纯连接组 | 0.00 | 0.00 | 0.000 | 100% | | 可塑读出 | 0.00 | 0.00 | 0.000 | 100% | > 短局里 Dellacherie 的**分数**低于 Greedy,是因为它偏向求稳、以单行消除为主 > (平均 1.23 分/行),而 Greedy 偶尔吃到多行消除(1.43 分/行)。拉长到 400 块后 > 差距就体现出来了:Dellacherie 消 156 行且从不顶出,Greedy 消 47 行且必定顶出。 > **短局数字是被方块上限截断的,不是这两个基线的真实水平。** ### 为什么神经线玩不好 **下行神经元的自发活动把视觉驱动淹没了**: | 条件 | 光感受器发放率 | 下行神经元发放率 | DN 每 tick 发放细胞 | |---|---|---|---| | 有视觉输入 | 0.0446 | 0.0652 | 85.66 / 1314 | | 无视觉输入 | 0.0373 | 0.0647 | **84.97 / 1314** | 加上视觉输入后光感受器发放率上升约 **20%**,但下行神经元的发放率只差 **0.8%**。 根因在动力学参数:`tonic_current = 0.180` 配 `decay = exp(-0.02/0.1) = 0.8187`, 稳态电位 `0.180 / (1 − 0.8187) ≈ 0.993`,**正好压在阈值 1.0 上**。 网络被设计成"自发活跃",整脑约 6.3% 的细胞每 tick 都因噪声和张力电流发放, 视觉输入只是一个小扰动。 这一点 Fly64 的作者在仓库里已经明确写过: > It is not a validated living fly or a trained Mario player. > Mario can walk into a wall and stay there. 本项目复现并**量化**了这个现象(发放率证据 + 可解码性证据)。要让它真的会玩, 需要的不只是"接上连接组"。 --- ## 快速开始 ```bash # 1. 装依赖(四个包,无深度学习框架) pip install -r requirements.txt # 2. 下载 MaleCNS 数据(约 1.1 GB) python scripts/fetch_connectome.py # 3. 构建连接组(约 30 秒,会缓存到 data/cache/) python scripts/build_connectome.py # 4. 跑实验(结果落到 artifacts/main/) python scripts/run_experiment.py --line all --episodes 3 --max-pieces 60 --out artifacts/main # 5. 信息流诊断(回答"棋盘信息传到了哪一层",约 30 分钟) # 51,200 次全脑步进,受内存带宽限制,无法显著加速 python scripts/diagnose_information.py # 5b.(可选)解码器定标:证明"信息在时解码器解得出来",用于给诊断结果做对照 python scripts/calibrate_decoder.py # 6. 生成仪表盘(读 artifacts/main/ 与 artifacts/information_flow.json) python scripts/build_dashboard.py # 7. 测试(--full 会跑依赖连接组的用例) python tests/test_pipeline.py --full ``` 跑完打开 `dashboard/index.html`。 上面 2–7 步可以直接一条命令串起来: ```bash python scripts/reproduce_all.py # 全流程 python scripts/reproduce_all.py --skip-data # 数据已下载时跳过下载 ``` **路径约定**:实验命令务必带 `--out artifacts/main`,否则默认会写到 `artifacts/` 根目录, 而 `build_dashboard.py` 默认读的是 `artifacts/main/`。诊断脚本的输出路径是固定的 `artifacts/information_flow.json`,不需要传参。 **性能量级**:全脑一步 LIF 约 26–29 ms(内存带宽受限,2,558 万条边就是物理下限), 约 0.7× 实时。一局 500 tick 大约 15 秒。 --- ## 仓库结构 ``` README.md 本文档 LICENSE MIT(含第三方数据与项目的声明) requirements.txt 四个依赖,无深度学习框架 scripts/ fetch_connectome.py 下载 MaleCNS 数据(约 1.1 GB) build_connectome.py 构建连接组并缓存 run_experiment.py 实验入口(pure / plastic / baseline) diagnose_information.py 信息流诊断(可解码性检验) calibrate_decoder.py 解码器定标(合成数据上的功效上界) build_dashboard.py 生成自包含 HTML 仪表盘 reproduce_all.py 一键串起全流程 probe_neuprint.py neuPrint API 探测(备用查询通道) eval_retina_mapping.py 复眼映射方案对比工具 tests/test_pipeline.py 快测 + --full(需连接组) fruitfly/ 神经侧代码 console.py 控制台 UTF-8 兼容(Windows 代码页不是 UTF-8 时的兜底) tetris/engine.py 游戏侧代码,零第三方依赖 .github/workflows/tests.yml CI:只跑快测,不下载数据集 data/raw/ 下载的数据集 ← 不入库 data/cache/ 构建好的连接组缓存 ← 不入库 artifacts/ 实验产物 ← 不入库 main/results.json 正式实验汇总 ← 例外:入库作证据 plastic_long/results.json 可塑长时训练汇总 ← 例外:入库作证据 dashboard/index.html 生成的仪表盘 ← 不入库 ``` `data/`、`artifacts/`、`dashboard/*.html` 都写在 `.gitignore` 里:数据集 1.1 GB 且版权属 Janelia,实验产物全部可由脚本重新生成。CI 只跑不依赖数据集的快测, 因为 GitHub Actions 上每次拉 1.1 GB 不划算 —— 需要连接组的用例(`--full`)请在本地跑。 例外是两次正式实验的汇总结果(`artifacts/main/`、`artifacts/plastic_long/` 下的 `results.json` / `run_manifest.json` / `episodes.csv`,合计约 50 KB):这些数字写在 本文档里,入库后审阅者不必先跑几十分钟仿真就能核对。 --- ## 架构 ``` tetris/engine.py 确定性俄罗斯方块,零第三方依赖 fruitfly/connectome.py 连接组构建、查询、缓存(feather → CSR) fruitfly/retina.py 棋盘 → 光感受器(逐行展开 + 高斯接受角) fruitfly/network.py 稀疏 LIF 脉冲网络 fruitfly/motor.py 下行神经元活动 → 动作(含阈值分位数标定) fruitfly/agent.py 闭环智能体 fruitfly/readout.py 三因子可塑读出(固定接线,只学输出端) fruitfly/baseline.py Dellacherie / Greedy / Random 对照组(带 BFS 规划) fruitfly/experiment.py 实验主循环与结果汇总 ``` ### 神经网络动力学 完全对齐 Fly64 公开的技术说明: ``` 每 20 ms: v ← exp(-dt/0.1)·v + 1.5·W·spikes + 0.180 + noise + retina 噪声是带种子的伯努利背景活动(1.2 Hz,幅度 0.22) 阈值 1 复位 0 视网膜电流只注入光感受器 ``` 这些是**工程标定值,不是实测生理参数**。 --- ## 数据 | 文件 | 说明 | |---|---| | `connectome-weights-*.feather` | 1.1 GB,分 2,318 个记录批次流式读取 | | `body-annotations-*.feather` | 类型 / 类别 / 侧向 / 神经递质 | | `optic-column-type-assignments-v1.0.xlsx` | 光列 (i, j) 与 L1/R7/R8 的 bodyId | 数据源:(免 token 直链下载)。 ### 两个关键的字段陷阱 1. **神经递质名是小写的**(`gaba` / `glutamate` / `histamine`)。写首字母大写会导致 抑制性细胞数恒为 0,整个网络变成纯兴奋回路。 2. **侧向标注要用 `somaSide` 而不是 `rootSide`**: | 字段 | 非空标注数 | |---|---| | `somaSide` | 150,726 | | `rootSide` | 17,939 | 用 `rootSide` 会让**全部下行神经元的侧向为空**,运动解码器无法按左右拆分转向池, "左右不对称 → 转向"这个唯一有解剖学依据的定向通道整个失效。 顺带一个生物学确认:光感受器是**组胺能**的,其输出权重全为负——这是对的, 果蝇光感受器通过组胺门控氯通道**抑制** L1/L2。 --- ## 三个必须讲清的工程问题 ### 1. 棋盘要铺到复眼上,但复眼不是矩形 官方光列表的 `(i, j)` 是**六边形轴向坐标**。判别方法:在 `x = i + j/2, y = j·√3/2` 变换下,全部 892 个右眼列的两两最近邻距离恒为 **1.0000,标准差 0.0000**。 但复眼在该空间里是一条**斜向平行四边形带**(长轴自左下至右上,宽度中部最大、 上下两端收窄)—— 因为眼面是曲面,`(i, j)` 是一套曲线坐标。 直接后果:任何"把矩形棋盘线性铺进包围盒"的做法都必然漏掉两个对角角区。 实测确认,早期实现下棋盘**右下角的响应光感受器数为 0**。 解决办法是**逐行展开**:每个 j 行把该行 `i` 方向的实际区间映射到棋盘整宽。 ``` 逐行展开后: 193/200 格有光感受器 再叠加高斯接受角: 200/200 格 ← 当前实现 ``` 已知偏差(写进报告):逐行展开假设每一背腹行都张满整个水平视野,而真实复眼中 背侧缘区(DRA)只张一个很窄的方位角范围。 ### 2. 光列坐标得靠四级回退链凑齐 | 来源 | 数量 | 占比 | 做法 | |---|---|---|---| | `direct` | 2,628 | 43.1% | 官方光列表直接命中(R7 / R8 / L1) | | `via_L1` | 3,249 | 53.3% | 经最强下游 L1 反查 | | `via_L3` | 29 | 0.5% | 经最强下游 L3 反查 | | `proxy` | 185 | 3.0% | 前三级都没命中,用确定性代理位置 | L3 不在官方表里,它的光列靠**柱传播**推断:L3 与 L1 同属一个小眼柱(同一批 R1–R6 同时投射到两者),所以"找与它共享最多 R1–R6 突触前细胞的 L1,继承其光列"。 这是数据驱动的推断,不是位置猜测。 **`proxy` 那 185 个光感受器的坐标不由数据决定,解读结果时必须计入。** 推理"最强下游"时必须用 `|W|` 而不是 `W`:光感受器权重恒为负, 用 `max(axis=1) > 0` 判断"是否有连接"会把 100% 判成无连接。 ### 3. 动作阈值必须按实测分布标定 手工照 Fly64 的马里奥通路定的阈值,用到俄罗斯方块的通路上完全不匹配:实测 前进池 EMA 的最大值只有 **0.091**,而原硬降阈值是 **0.20** —— 硬降这个动作 整局一次都没触发过。 现在启动时跑 400 tick 预热,按 `|ema_steer|` / `ema_burst` / `ema_forward` 的实测分布分位数定阈值。标定用的是随机棋盘输入,**不含任何游戏成绩信息**, 不会把表现泄漏进基线。 --- ## 信息流诊断 直接看下行神经元的发放率会被自发活动淹没,但发放率不变不等于信息不在 —— 信息也可能存在于发放**模式**里。所以做两项独立检验: 1. **发放率对比**:有/无视觉输入时 DN 发放率只差 0.8%(率编码里没有) 2. **可解码性**:用交叉验证的岭回归从各层群体活动解码棋盘占用状态 ### 解码器先定标,再解读 低 R² 有两种截然不同的解释:**信息不在**,或者**解码器无能**。二者必须分开, 否则结论不可用。`scripts/calibrate_decoder.py` 在已知信息必然存在的合成数据上 测出解码器的功效上界: | 解码目标 | n=160 | n=400 | n=1000 | |---|---|---|---| | 200 格(全盘) | 0.761 | 0.862 | 0.912 | | 25 维(5×5 降采样) | 0.880 | 0.926 | 0.951 | | 11 维(10 列高 + 占用率) | 0.956 | 0.985 | 0.990 | | 1 维(占用率) | 0.951 | 0.984 | 0.990 | 解码器在真实实验的样本量(n=160)下能把全盘 200 格解到 R²≈0.76。因此诊断里 若某层 R² 接近 0,结论只能是"该层线性可解码信息不足",而不是"方法不行"。 实测中内层交叉验证选出的 α 达到 oracle 最优值的 96%,说明 α 选择环节无系统偏差。 ### 关于 R² 的零点 目标(棋盘占用)**必须先在训练折内中心化**。否则模型要花容量去学均值,且 α→∞ 时预测塌向 0,R² 趋向 `1 − ΣY²/Σ(Y−Ȳ)²` —— 对"列高"这类均值远非 0 的 目标约为 −1.8,使"只会预测均值"这一零点严重偏离 0,R² 失去可比性。 中心化修正后,纯噪声输入与打乱标签对照的 R² 均落在 0 附近(实测 −0.023), 零点校准正确。 运行 `python scripts/diagnose_information.py` 得到各层的 R² 对照表。 --- ## 动作映射 | 下行神经元 | 动作 | 依据 | |---|---|---| | DNa02 / DNg13 左右差 | 左移 / 右移 | 与 Fly64 同构,是唯一有解剖学依据的定向通道 | | DNp01 / DNp10 爆发 | 旋转 | Fly64 里这一对驱动跳跃(爆发式事件),语义迁移 | | DNg100 前进活动 | 硬降 | "前进"在俄罗斯方块里只能落到"推进时间"上 | **这是工程重映射,不是果蝇行为学发现。** DNg100 驱动的"前进"在俄罗斯方块中 没有对应物,只能落到"推进时间"上。 --- ## 实验线 - `--line pure` 纯连接组,166,700 细胞,无任何学习或参数拟合 - `--line plastic` 固定接线 + 三因子可塑读出(pre × post × reward,等价于 带基线的 REINFORCE)。**接线本身一个突触都没动** - `--line baseline` 对照组 ### 关于奖励 报告分数**永远只用消行分**(0/1/3/5/8)。塑形项(洞数、高度)只用于给学习 提供梯度——否则"从不消行 ⇒ 奖励恒为 0 ⇒ 梯度消失 ⇒ 学不动"。 --- ## 来源与致谢 - **BetterGI** — 工程化范式参考:屏幕 → 视觉识别 → 输入自动化 → 可视化层 - **Fly64 / ornata/fly** — 方法论蓝本。LIF 动力学参数、光感受器注入约定、下行神经元到动作的映射思路 均来自其公开的技术说明 - **MaleCNS** — 数据源:Janelia 雄性果蝇全中枢神经系统连接组 v1.0 感谢源神,感谢 BetterGI 社区。 ### 一处明确的设计偏离:不做屏幕 CV BetterGI 走的是**纯图像视觉识别**路线——截屏、CV 识别 UI 元素、驱动键鼠。 它的价值在于对付**没有可编程接口的第三方游戏**。 本项目**不采用**这条路线,而是直接读取游戏内部状态(`TetrisGame.observation()` 返回 `(3, 20, 10)` 的干净矩阵)。理由: 1. **本项目的瓶颈在神经侧,不在感知侧。** 实测全脑一步要 26–29 ms, 而一次截屏 + CV 识别的开销会叠加在这个已经很紧的预算上。 2. **CV 会引入无法分离的误差源。** 如果棋盘识别本身有 3% 的格子判错, 就无法判断"果蝇玩不好"是神经回路的问题还是识别的问题。本项目要回答的 是前者,所以必须让输入是精确的。 3. **状态可精确读取是俄罗斯方块相对游戏截屏的根本优势**,主动放弃它没有收益。 如果将来要把这套东西接回真实游戏客户端(例如用神经网络去玩原神一类的 无接口程序),那时再叠加 BetterGI 式的 CV 感知层——届时应把 CV 层视为 **独立的、可单独测量误差的模块**,而不是藏在链路里。 --- ## 声明的工程近似(完整清单) 跑完实验后 `artifacts/main/run_manifest.json` 里会有完整清单,核心几条: 1. 俄罗斯方块**没有光学系统**。棋盘按逐行展开铺到复眼光列上,是工程手段, 不是测量到的 MaleCNS 光学参数 2. 小眼接受角用 σ=1.0 棋盘格的高斯核建模,是标定值而非实测光学 3. 3.0% 的光感受器坐标来自确定性代理映射,不由数据决定 4. LIF 参数对齐 Fly64 的工程标定值,不是实测生理参数 5. 抑制性递质判据(gaba/glutamate/histamine)是近似,不等于普适的递质-符号映射 6. 动作映射是工程重映射,不是行为学结论 7. "神经时间"换算(20 ms/tick,每 N tick 落一格)是工程选择 8. 传入权重按"入边绝对权和"归一化,是数值稳定手段,会改变各细胞的相对驱动强度 9. 可塑读出只改输出层,与真实果蝇的突触可塑性不是一回事 --- ## 已知局限与下一步 **为什么玩不好**(按可能性排序): 1. 网络处于自发活动主导的状态。`tonic_current=0.180` 配 `decay=0.8187` 的稳态 电位约 0.993,正好压在阈值 1.0 上——网络被设计成"自发活跃",视觉输入是一个 小扰动。可以考虑降低 tonic 让网络更依赖感觉输入 2. 4 个细胞的动作池太小,左右差被泊松噪声主导。可以扩大读出群体 3. 视觉前端只用亮度 + 时间对比度两路。Fly64 还用了绿通道拮抗 4. 可塑读出学的是 tick 级策略,要学出时序扩展行为需要远超 150 块的样本量 **尚未做**: - 光流 / 运动检测前端(果蝇的 T4/T5 通路专门做这个,本项目只是把亮度灌进光感受器) - 多巴胺能奖励通路建模(可塑规则里的奖励是全局标量,没有生物学结构) - 完整的行为学验证(本项目没有与真实果蝇行为数据对照)