flydnet
Use the real Drosophila connectome (FlyWire v783) as neural-network layers, and ask whether the wiring
actually matters. Pick any set of neurons by annotation (mushroom body, visual system, whole brain), wire a layer
exactly as the fly's synapse map, and train it with backprop or dopamine-like associative learning. fd.compare
trains the same model on the real connectome and on nested null models over paired seeds, runs exact sign-flip
tests, warns about pitfalls, and reads the nested controls to say which structure matters.
Own autograd engine on NumPy (CPU) / CuPy (GPU); PyTorch optional. Docs in Korean.
Alpha (0.1). API가 바뀔 수 있다. 연구용 도구이며, 정확도 향상을 기대할 도구는 아니다 (아래 "결과 요약").
초파리 커넥톰의 실제 배선을 신경망 층으로 쓰고, **"이 배선이 정말 중요한가"**를 통계로 묻는 라이브러리.
설치
pip install flydnet # CPU. 필요한 것은 numpy·pandas뿐 (scipy·pyarrow·torch 없음). CuPy가 이미 있으면 GPU도 자동
python -m flydnet doctor # 설치 진단: GPU 드라이버 CUDA·CuPy·torch, 어떤 옵션을 쓸지 알려 줌
python -m flydnet download # FlyWire v783 연결·주석 + DoOR 냄새 데이터 (약 130 MB, 버전 고정·SHA-256 확인)
FlyWire 연결 파일(parquet)은 받은 뒤 한 번 numpy 형식(Connectivity_783.npz)으로 바꿔 두고 그것을 읽는다.
바꾸는 데만 pyarrow가 필요하다: pip install "flydnet[parquet]" 후 python -m flydnet download flywire (그 뒤에는 필요 없음,
다른 컴퓨터에서 만든 npz를 같은 폴더에 복사해도 됨). 이미 받아 두었고 pyarrow가 있으면 처음 회로를 만들 때 자동으로 바뀐다.
선택 설치 (필요한 기능만):
| 옵션 | 무엇 | 언제 |
|---|---|---|
[parquet] |
pyarrow | 연결 parquet → npz 변환 (한 번) |
[scipy] |
scipy | Circuit.from_scipy·to_scipy, C 커널이 없는 플랫폼의 CPU 희소 연산 |
[graph] |
networkx | Circuit.to_networkx |
[torch] |
torch | fd.torch 연동 |
[gpu-cuda13]·[gpu-cuda12] |
CuPy + CUDA 런타임 | GPU (아래) |
GPU: CuPy가 없을 때만 드라이버 CUDA 버전(nvidia-smi 오른쪽 위)에 맞는 옵션 하나를 쓴다.
pip install "flydnet[gpu-cuda13]" # CUDA 13.x 드라이버 (CuPy + CUDA 런타임)
pip install "flydnet[gpu-cuda12]" # CUDA 12.x 드라이버
pip install "flydnet[torch]" # torch 연동 (flydnet.torch)이 필요할 때
Colab처럼 CuPy가 이미 깔린 곳에 GPU 옵션을 붙이면 CuPy가 두 개가 되고 CUDA 라이브러리가 바뀌어 torch까지
망가질 수 있다 (doctor가 잡아 줌. Colab이면 런타임을 삭제하고 옵션 없이 다시 설치).
드라이버를 업데이트해도 설치한 CuPy는 그대로 돈다 (드라이버는 하위 호환). CUDA·CuPy를 바꾼 뒤에는 doctor를 한 번 돌릴 것.
데이터 위치는 ~/.flydnet/data (fd.set_data_dir(...)로 바꿈). FLYDNET_DEVICE=cpu로 CPU를 강제할 수 있다.
CPU 희소 연산
CPU에서 커넥톰 전달(희소 행렬 x 배치)은 직접 작성한 C 커널이 한다 (flydnet/ganglion/csr.py, _csr.c).
Linux (x86_64·aarch64)·macOS (x86_64·arm64)·Windows (x86_64·arm64)용이 설치 파일 하나에 모두 들어 있고, 파이썬 스레드로
행을 나눠 계산한다 (OpenMP를 쓰지 않아 런타임 설치가 필요 없고, 스레드 수와 상관없이 결과 비트가 같음).
처음 열 때 작은 행렬로 numpy 결과와 비교해, 다르면 쓰지 않는다. 쓸 수 있는 경로를 이 순서로 고른다:
- C 커널 (위 6개 플랫폼)
- scipy (설치되어 있으면)
- numpy (느림 - 처음 한 번 경고)
세 경로 모두 행마다 연결 순서대로 더해 같은 입력이면 같은 비트를 낸다 (tests/test_golden.py가 세 경로에서 확인).
| 시각계 전체 (연결 429만, 배치 32, float32) | 시간 |
|---|---|
| C 커널, 8 스레드 | 2.8 ms |
| C 커널, 2 스레드 | 6.6 ms |
| C 커널, 1 스레드 | 13.0 ms |
| scipy (1 스레드) | 17.5 ms |
| numpy | 489 ms |
FLYDNET_SPARSE=c|scipy|numpy로 경로를, FLYDNET_THREADS=n으로 스레드 수(기본 CPU 수, 최대 8)를 정한다.
비교: python scripts/bench_sparse.py. GPU는 이 경로를 쓰지 않는다 (전용 CUDA 커널).
빠른 시작
가장 짧게 (PyTorch의 nn.Sequential처럼) - 입력을 발화율로 바꾸고, 연결 세기를 데이터에 맞게 자동 보정하고,
출력 뉴런을 클래스로 바꾸는 층까지 붙인 모델:
import flydnet as fd
Xtr, ytr, Xte, yte = fd.door_task(n_odors=12) # DoOR 실제 냄새 (사구체 반응)
model = fd.MushroomBody(n_in=Xtr.shape[1], n_classes=12) # 오른쪽 버섯체 PN → KC 2,597개
model.fit(Xtr, ytr, val=(Xte, yte)) # 처음 데이터로 보정한 뒤 학습 (fd.train)
model.score(Xte, yte) # 0.976 (찍기 0.083), GPU 약 25초
어떤 회로·그래프든 fd.ConnectomeModel(circuit, "in", "out", n_in, n_classes). 세부는 model.encoder·model.layer·
model.head, 또는 아래처럼 직접 조립. KC 출력이 MBON 출력(48개뿐)보다 정확도가 훨씬 높음 (같은 과제 0.97 대 0.36).
직접 조립:
import numpy as np, flydnet as fd
mb = fd.flywire() # 오른쪽 버섯체: PN 344, KC 2597, APL 1, MBON 48
model = fd.Pathway(
fd.Projection(784, 344), # 축삭 투사: 픽셀 → PN (모두 연결)
fd.Neuropil(mb, "PN", "KC"), # 실제 PN→KC 배선 (연결 13,485개만, 부호 유지 학습)
fd.Inhibition(frac=0.05), # APL 억제: KC 5%만
fd.Projection(2597, 10),
)
rule = fd.Adaptive(model.named_synapses(), rate=1e-3, clip=1.0) # clip: 기울기 크기 제한
loss = fd.surprise(model(x), y) # 놀람 = 교차 엔트로피
rule.clear(); loss.retrograde(); rule.step() # 역행성 신호(자동 미분) → 가소성
MNIST 2에폭 96.2% (GPU 약 6초, examples/ganglion_mnist.py).
학습 루프를 한 줄로 (fd.train: 배치·섞기·코사인 학습률·기울기 제한·평가), 실제 냄새 과제도 한 줄로 (fd.door_task),
가중치는 자동 보정 (calibrate: 그룹마다 목표 발화율이 되도록 들어오는 연결 배율을 맞춤):
Xtr, ytr, Xte, yte = fd.door_task(n_odors=12) # DoOR 실제 냄새
enc = fd.Glomeruli(mb)
layer = fd.Connectome(mb, "PN", "KC", t_ms=50, dt=0.5, input_mode="regular", trainable=["PN>KC"])
layer.calibrate(enc(Xtr[::3]), {"KC": 5}) # KC 평균 5 Hz가 되도록 배율 자동 조정
model = fd.Pathway(enc, layer, fd.Homeostasis(), fd.Projection(2597, 12))
hist = fd.train(model, Xtr, ytr, val=(Xte, yte), epochs=12) # 평가 정확도 0.96 (찍기 0.083), examples/quickstart.py
출력이 0 Hz일 때 (신호가 출력까지 가지 않음 - 기본 세기로는 버섯체 MBON의 96%, 합성 그래프는 전부 조용함):
R = fd.Glomeruli(mb)(Xtr[::3]) # 대표 입력 (Hz)
layer = fd.Connectome(mb, "PN", "MBON")
print(layer.reach(R)) # 그룹마다 입력(·activate 자극)에서의 홉 수·발화율, 신호가 끊기는 곳
layer.calibrate(R, {"MBON": 20}) # 출력만 목표로 줘도 경로 위 중간 그룹(KC)까지 함께 깨움 (억제 뉴런 APL은 제외)
처음 순전파에서 입력이 있는데 출력이 모두 0이면 경고가 나온다 (입력 뉴런조차 발화하지 않았으면 입력 쪽 문제로 알려 줌).
층을 만들 때 입력에서 경로가 없는 출력 그룹, 시냅스 지연보다 짧은 t_ms도 경고한다.
| 같은 과제, seed 4개 | 정확도 |
|---|---|
| MBON 48개에서 읽기 (배율 3 손으로) | 0.57 |
| KC 2,597개에서 읽기, 배율 3 손으로, 학습률 1e-2 | 0.938 ± 0.013 |
KC에서 읽기, 자동 보정, 학습률 3e-3 (fd.train 기본) |
0.962 ± 0.004 |
MBON은 48개뿐이라 병목. 자동 보정의 정확도 이득은 잡음 안 (+0.8%p) - 이점은 배율을 손으로 맞출 필요가 없다는 것.
한 줄 학습기: fd.Learner
커넥톰은 고정하고, 데이터는 한 번 훑어 배우고, 새 데이터는 이어서 배운다. 입력 변환·연결 세기 보정은 처음 데이터로 자동. 학습률·에폭을 고를 필요가 없고, 새 클래스를 배워도 앞에서 배운 것은 그대로다.
learner = fd.Learner() # 오른쪽 버섯체 PN → KC (fd.Learner(circuit, "in", "out")도)
learner.learn(X_a, y_a) # 처음: 입력 변환·보정 자동, 한 번 훑기
learner.learn(X_b, y_b) # 새 클래스 이어서 - 앞의 클래스 평균은 그대로
learner.learn(X_img, y_img, source="image") # 다른 종류(특징 수가 다른) 데이터: 입력 변환만 새로, 기억은 공유
learner.predict(X), learner.score(X, y) # 라벨은 문자열 등 아무 값이나
learner.save("me.npz"); fd.Learner.load("me.npz") # 불러온 뒤에도 계속 배움
| rule | 방식 | 냄새 24개, 6개씩 4번 차례로 | 한 번에 전부 |
|---|---|---|---|
"lda" (기본) |
흐름 선형 판별 (Hayes & Kanan 2020): 클래스 평균 + 공유 공분산을 누적, 한 번 훑기 | 0.859 | 0.865 |
"assoc" |
도파민 연합 학습: 정답 클래스 원형만 강화, 한 번 훑기 (생물에 가장 가까운 국소 규칙) | 0.744 | 0.712 |
"backprop" |
망각을 줄인 역전파 (아래), 기본 5에폭 | 0.708 | 0.841 |
DoOR 냄새 24개 (잡음 1.2, 찍기 4.2%), seed 3개 평균. 커넥톰까지 학습하는 fd.MushroomBody·fd.train(0.89 수준)에 가깝고,
차례로 배워도 한 번에 배운 것과 거의 같다 (잊지 않음). lda는 KC끼리 겹치는 반응을 공분산으로 걸러 (백색화 - 측면 억제의
탈상관과 같은 효과) 평균만 쓰는 것보다 훨씬 잘 구별한다. 공분산 축소는 자동 (shrink="auto" = max(OAS 추정, 특징 수 / 본 시료 수)).
같은 특징에서 class-incremental, 재생 없음:
| 특징 | lda | assoc | 망각을 줄인 역전파 | 역전파 그대로 |
|---|---|---|---|---|
| MNIST KC (5과제) | 0.896 | 0.728 | 0.656 | 0.419 |
| CIFAR-100 resnet18 (10과제) | 0.640 | 0.539 | 0.585 | 0.247 |
CIFAR의 0.640은 한 번에 전부 학습한 선형 분류(0.658)에 가깝다. 단 lda는 출력 뉴런 수의 제곱만큼 메모리를 쓴다
(KC 2,597개 → 54 MB, 12,000개 넘으면 오류 - rule="assoc").
역전파의 망각 줄이기 (rule="backprop"): 세 가지를 함께 쓴다.
- 새 클래스 가중치를 그 클래스 평균 특징으로 시작한다 (연합 규칙으로 초기화한 뒤 역전파로 다듬기).
- 이번
learn()에 나온 클래스끼리만 경쟁한다 (도파민이 그 출력에만 오는 것과 같음 - 다른 클래스 가중치는 안 바뀜). - 코사인 점수를 쓴다 (특징·가중치 크기 정규화). 특징 중심은 처음 데이터로 정해 고정한다.
class-incremental, 과제당 1에폭, 재생 버퍼 없음, 본 클래스 전체 정확도 (seed 3개):
| 특징 | 역전파 그대로 | ②만 | ②+③ | ①+②+③ | 연합 (k=1) |
|---|---|---|---|---|---|
| CIFAR-100 resnet18 (10과제) | 24.7% | 47.0% | 54.7% | 58.5% | 53.9% |
| MNIST 픽셀 (5과제) | 41.1% | 53.4% | 64.3% | 72.9% | 82.2% |
| MNIST KC (5과제) | 41.9% | 56.9% | 63.7% | 65.6% | 72.8% |
특징 중심을 빼면 효과가 사라진다 (①+②+③, CIFAR 26.3%). 중심을 첫 과제 데이터로만 정해도 거의 같다 (57.4%).
대조 실험: fd.compare
같은 학습 절차를 실제 배선과 대조군 배선에 seed마다 짝지어 돌리고 비교한다.
def run(circuit, seed): # 회로 하나로 모델을 만들고 학습해서 점수 하나 (seed를 학습 난수에)
...
return accuracy
report = fd.compare(run, mb, controls=["shuffled", "randomized", "shuffled_weights"], seeds=6, chance=1/30)
print(report)
조건 평균 95% CI 실제 - 대조 d p 실제 우세
real (실제 배선) 0.7622 [0.7449, 0.7795]
shuffled 0.7678 [0.7481, 0.7875] -0.005556 -0.30 0.688 2/6
randomized 0.6336 [0.6158, 0.6514] +0.1286 5.36 0.031 6/6
shuffled_weights 0.7678 [0.7537, 0.7819] -0.005556 -0.36 0.406 3/6
해석 (대조군 포함 관계):
→ 실제 배선이 randomized는 이기고 shuffled와는 차이가 없음 → 중요한 구조: 뉴런별 연결 수 분포 (차수·허브)
(스파이킹 버섯체, 합성 냄새 30클래스, examples/compare_odor.py --model lif, 약 20초)
| 대조군 | 유지하는 구조 | 묻는 것 |
|---|---|---|
"randomized" |
그룹 쌍별 연결 수 | 연결 수 분포(허브·차수)가 중요한가 |
"shuffled" |
+ 뉴런별 연결 수 | 누가 누구와 연결되는가 |
fd.controls.Local(xy, r) |
+ 시야 위치 대응 | 큰 공간 구조 말고 세부 배선까지 |
"shuffled_weights" |
배선 그대로, 세기만 섞음 | 시냅스 세기 분포 |
- 같은 seed끼리 짝지어 학습 잡음을 상쇄하고, 부호 뒤집기 순열 검정(분포 가정 없음, seed ≤ 14면 정확한 p)을 쓴다.
- 대조군 포함 관계(randomized ⊂ shuffled ⊂ local ⊂ 실제)로 어떤 구조가 중요한지 해석한다.
- 함정을 경고한다: seed가 적어 p가 0.05 아래로 내려갈 수 없음, 상한 근처라 차이가 가려짐, 찍기 수준, 같은 seed인데 결과가 다름, 큰 구조 때문에만 이김, 효과는 큰데 비유의.
가상 유전학: fd.genetics
초파리 실험실의 방법 그대로: 드라이버로 뉴런 집단을 고르고, 효과기로 끄고·켜고·막는다.
brain = fd.brain() # 138,639개 뉴런 (Shiu et al. 2024 모델과 같은 순서)
G = fd.genetics
sugar = G.driver(brain, cell_sub_class="sugar") # GAL4: 주석 조건으로 (root_ids=, group=도 가능)
mn9 = G.driver(brain, root_ids=[720575940660219265])
layer = fd.Connectome(brain, inputs=None, outputs="motor", t_ms=1000)
with G.activate(layer, sugar, hz=100): # CsChrimson: 포아송 자극
r = layer(None, batch=30, return_all=True) # 30번 시행, 모든 뉴런 발화율
| 효과기 | 실험실 도구 | 효과 |
|---|---|---|
activate(layer, line, hz) |
CsChrimson, P2X2 | 포아송 자극 (연속값 뉴런은 level로 고정) |
silence(layer, line) |
Kir2.1 | 발화 없음 |
block(layer, line) |
Shibire-ts | 발화는 하지만 시냅스 전달 차단 |
ablate(circuit, line) |
세포 제거 | 연결을 뺀 새 회로 (fd.compare로 학습 비교) |
- 드라이버 조합:
a & b(split-GAL4),a | b,a - b.lines(circuit, by="cell_type")는 세포 유형마다 드라이버. screen(measure, layer, lines): 집단마다 효과기를 발현해 측정값 변화와 짝지은 p값을 표로 (유전자 스크린).- 효과기는
with블록 안에서만 (또는.remove()까지), 학습 중에도 쓸 수 있다 (역전파 됨).
검증 - 원본 Shiu et al. 2024 Brian2 모델과 비교 (같은 뉴런 ID·매개변수, 1초 x 30시행, validation/shiu2024/):
| MN9 발화율 (Hz) | 무자극 | 단맛 25 | 단맛 50 | 단맛 100 | 단맛 200 | 쓴맛 100 | 단맛 + 쓴맛 |
|---|---|---|---|---|---|---|---|
| 원본 Brian2 | 0 | 0 | 12.6 | 61.8 | 88.8 | 0 | 2.2 |
| flydnet | 0 | 0.1 | 13.9 | 61.4 | 90.6 | 0 | 1.8 |
모든 조건에서 차이는 시행 간 잡음 안 (p > 0.17). 반응한 뉴런 329개의 발화율 상관 0.9985. 같은 입력 스파이크면 스파이크 시각까지 같다 (테스트로 고정). 시행당 약 17배 빠름 (GPU).
회로 기여도: fd.explain
학습된 모델이 답을 낼 때 어떤 세포 유형·경로에 기대는지 실제 이름으로, 그리고 실제로 꺼서 확인한다.
def score(layer, seed): # 설명할 값: 예) 정답 로짓의 합
return readout(layer(enc(X), seed=seed))[np.arange(len(y)), y].sum()
rep = fd.explain(score, layer, by="cell_type", pathways=True, verify=5)
print(rep) # 유형별·경로별 기여 + 실제로 끈 결과와의 순위 상관
- 방법: 뉴런·연결마다 배율(=1) 탐침을 두고 기울기를 구한다 = "끄면 얼마나 줄어드는가"의 1차 예측.
fd.genetics.silence와 같은 조작이라,verify로 예측을 실제 손상과 바로 비교한다. - 실제 냄새(DoOR) 8개를 구분하는 스파이킹 버섯체 모델 (
examples/explain_odor.py, 정확도 98.8%):- 기여가 큰 유형: KCγ, KCαβ, 그다음 사구체별 PN. APL(억제)은 음수 (끄면 정답 로짓이 오름)
- 실제로 꺼서 확인: KC 유형은 예측 크기까지 맞음 (5,960 대 5,730). 억제 뉴런 APL은 방향이 틀림 (예측 -1,050, 실제 +6,500) → 순위 상관 0.44, APL은 "방향을 틀린 유형"으로 자동 표시. 되먹임 억제처럼 큰 손상의 효과는 1차 예측으로 맞지 않으므로 결론은 실제로 끈 값으로
- 냄새마다 모델이 기대는 사구체와 그 냄새에 실제로 반응하는 사구체: 8개 모두 양의 상관 (평균 0.71)
세포 유형 드롭아웃: fd.genetics.mosaic
학습 중에만, 시료마다 세포 유형을 통째로 무작위로 끈다 (유전 모자이크). by="neuron"이면 보통 드롭아웃.
fd.genetics.mosaic(layer, p=0.2, by="cell_type", within=fd.genetics.driver(mb, group="PN"))
실제 냄새 20개, 스파이킹 버섯체, seed 6개 짝지음 (examples/mosaic_odor.py, 약 6분). 평가 = 냄새 구분에 중요한
사구체 10개를 하나씩 없앴을 때 (후각 수용체 결손 모사):
| 학습 | 깨끗 | 사구체 결손 평균 | 최악의 결손 |
|---|---|---|---|
| 드롭아웃 없음 | 0.934 | 0.885 | 0.840 |
| PN 뉴런 드롭아웃 (p 0.2) | 0.973 | 0.932 | 0.867 |
| PN 세포 유형 드롭아웃 (p 0.2) | 0.971 | 0.950 | 0.923 |
세포 유형 드롭아웃은 보통 드롭아웃과 깨끗한 정확도는 같고 (p = 0.75), 결손에는 더 강하다 (+1.7%p, 6/6 seed, p = 0.031, 최악의 결손 +5.6%p). 학습 때 끄는 단위와 평가 때 잃는 단위가 같은 것(사구체)이 이유일 것이므로, 손상이 세포 유형 단위로 일어나는 상황에서 쓸모 있다.
역전파 없는 학습: fd.ThreeFactor
뇌에서 가능한 정보만으로 커넥톰 연결을 학습하는 3요소 규칙 (e-prop 방식): 시냅스 변화 = 학습 신호(오차) x 시냅스 후 민감도 x 시냅스 전 흔적. 시간을 거슬러 가지 않아 메모리가 시뮬레이션 길이와 무관하다.
tf = fd.ThreeFactor(layer, feedback="connectome") # 오차를 실제 연결을 따라 (또는 "random", "none")
out = tf(x, seed=s)
loss = fd.surprise(readout(out), y); rule.clear(); loss.retrograde() # 리드아웃까지만
tf.assign(out); rule.step() # 흔적 x 신호 → 커넥톰 연결
- 출력 뉴런으로 들어오는 연결의 기울기는 시간 역전파와 같다 (테스트로 고정). 숨은 연결은 피드백으로 받은 오차를 쓴다.
- GPU 메모리 (버섯체, 배치 32): 시간 역전파 50 ms 261 MB → 800 ms 4.1 GB, 3요소 규칙은 길이와 무관하게 41 MB.
- 실제 냄새 12개, 버섯체 + 도파민 뉴런, PN>KC·KC>MBON 학습, seed 6개 (
examples/threefactor_odor.py, 약 9분):
| 학습 | 정확도 |
|---|---|
| 리드아웃만 (커넥톰 고정) | 0.302 |
| 시간 역전파 | 0.741 |
| 3요소, 피드백 없음 (KC>MBON만) | 0.551 |
| 3요소, 무작위 피드백 | 0.649 |
| 3요소, 실제 연결 피드백 (MBON → KC, MBON → DAN → KC) | 0.575 |
역전파 없이 리드아웃만보다 +35%p (p = 0.031, 6/6), 역전파와의 차이는 9%p. 숨은 연결 학습은 무작위 피드백으로 +10%p (p = 0.031). 실제 피드백 경로는 피드백 없음과 차이가 뚜렷하지 않고(+2%p, 4/6, p = 0.34) 무작위 피드백보다 못했다 (-7%p, 0/6, p = 0.031) - 이 설정에서는 실제 MBON → DAN → KC 경로가 오차를 그대로 전달하지 않는다.
어떤 그래프든: 다른 커넥톰과 합성 그래프
초파리용으로 만든 도구(ConnectomeLayer, compare, explain, genetics, ThreeFactor)가 어떤 방향 그래프에서도 동작한다.
c = fd.Circuit.from_edges(pre, post, weight, groups={"in": [...], "out": [...]}) # 번호 또는 이름
c = fd.Circuit.from_scipy(A) # A[i, j] = i → j (또는 orientation="post_pre")
c = fd.Circuit.from_networkx(G) # 노드 속성 → 주석, "group" 속성 → 그룹
worm = fd.worm() # 예쁜꼬마선충 (Cook et al. 2019), python -m flydnet download worm
g = fd.graphs.watts_strogatz(400, 12, 0.1, groups={"in": range(20), "out": range(360, 400)})
# erdos_renyi / watts_strogatz / barabasi_albert / stochastic_block / layered (억제 비율·데일의 법칙)
c.to_scipy(), c.to_networkx(), c.regroup({...}), c.check() # scipy·networkx는 선택 설치 ([scipy]·[graph])
- 주석이 없는 그래프에서는
explain·mosaic·genetics.lines가 그룹 단위로 동작한다 (주석이 있으면cell_type). - 세기는 시냅스 수처럼
w_syn(0.275 mV)을 곱해 쓴다. 기본 세기로 출력이 조용하면layer.calibrate(X, {"out": 10})(중간 층까지 자동으로 맞춤, 예:layered([20, 100, 100, 10])출력 0 → 11 Hz). examples/any_graph.py:- 예쁜꼬마선충 감각 뉴런 24개 → 체벽 근육 95개로 패턴 구분 +
fd.compare: 실제 배선의 이점은 보이지 않고, 시냅스 세기만 섞은 대조군이 오히려 좋음 (+9.7%p, 6/6, p = 0.031). 임의의 패턴 구분은 이 회로가 하는 일이 아니므로 흔한 결과 - 그래프 종류 비교 (노드 400, 연결 약 4,700, 그래프마다 세기를 골라 출력 그룹 발화율 약 18 Hz로 맞춤 - 예전엔 전체 평균으로 맞춰 블록 구조의 출력이 꺼진 채로 비교됐음): 무작위 0.64 > 척도 없음 0.60 > 작은 세상 0.37 > 블록 0.20 (작은 세상·블록은 6/6, p = 0.031. 척도 없음은 무작위와 차이 없음 - 0.1.18에서 척도 없음 그래프 생성기가 대상 m개를 서로 다르게 고르도록 고친 뒤, 예전 0.53). 단 입력·출력 노드의 위치 (고리 위 거리, 다른 모듈)에 크게 좌우되므로, 그래프 구조 자체의 결론이 아니라 이런 질문을 하는 방법의 예
- 예쁜꼬마선충 감각 뉴런 24개 → 체벽 근육 95개로 패턴 구분 +
플러그인: 뉴런 모델·학습 규칙을 직접
뉴런 모델 - 한 스텝만 정의하면 시뮬레이션(지연·입력·체크포인팅), 역전파(자동 미분), genetics, explain,
torch 연결 장치, 저장·불러오기가 따라온다.
@fd.neurons.register
class MyNeuron(fd.neurons.NeuronModel):
state = ("v",)
def init(self, ctx): return {"v": ctx.full(-65.0)}
def step(self, st, I_syn, I_ext, ctx): # Signal 연산만 → 역전파 자동
v = st["v"] + ctx.dt * (-(st["v"] + 65.0) / 10.0) + I_syn + I_ext
spk = fd.ganglion.fire(v, -50.0, slope=1.0)
return {"v": fd.ganglion.where(spk.data > 0, -65.0, v)}, spk
layer = fd.Connectome(circuit, "in", "out", neuron=MyNeuron(), checkpoint_every=50)
- 내장:
fd.neurons.LIF(내장 LIF와 스파이크가 스텝까지 같고 기울기 차이 4e-5 이내 - 플러그인 틀의 검증),fd.neurons.Izhikevich. 예제examples/custom_neuron.py는 AdEx를 직접 정의한다 - 비용: 내장 LIF의 약 1.4배 시간, 중간값을 모두 저장하므로 학습할 때는
checkpoint_every
학습 규칙 - 순전파를 스텝마다 지켜보는 관찰자 규격 (begin(info), step(s, spikes, **extra)).
fd.ThreeFactor와 fd.STDP(쌍 기반 STDP, 곱셈형)가 이 규격을 따른다.
stdp = fd.STDP(layer); stdp(x, seed=0); stdp.assign(); rule.step() # 원인 → 결과 순서면 강화, 반대면 약화
스파이킹 역전파 기울기 확인: fd.gradcheck
대리 기울기는 되먹임 회로를 돌며 곱해져서, 감쇠 없이 길게 돌리면 방향이 무작위이거나 반대가 되고 크기가 수억 배로 부푼다 (0.1.15까지의 기울기: 초파리 버섯체 1,000스텝에서 cos -0.75, 16억 배). 0.1.16부터:
surrogate_damp="auto"(기본): 시뮬레이션 동안 신호가 시냅스를 건널 수 있는 횟수(홉 수)로 감쇠를 정함 - 짧으면 감쇠 없음, 길수록 강하게. 초파리 버섯체·전체 뇌에서 가장 잘 맞은 값을 맞추는 경험 규칙noise=막전위 잡음 (선택): 대리 기울기가 '잡음 있는 뉴런의 발화 확률의 기울기'에 가까워짐truncate=구간 절단 역전파 (시냅스 지연보다 길어야 함)
print(fd.gradcheck(score, layer)) # 방향 일치(cos)·크기 비율·기준 신뢰도, 연결 종류별
print(fd.gradcheck(score, layer, seeds=16)) # 포아송 입력이면 여러 seed 평균 출력의 기울기로 (기준이 불안정할 때)
fd.tune(score, layer) # 후보 중 자기 손실에 가장 잘 맞는 감쇠를 골라 적용
| 초파리, 방향 일치 cos | 감쇠 없음 | auto (기본) |
|---|---|---|
| 버섯체 100스텝 (dt 0.5) | 0.01 | ≈0.87 (0.29) |
| 버섯체 1,000스텝 | -0.75 (16억 배) | ≈0.91 (0.09, 크기 1.4배) |
| 전체 뇌 200스텝 | 0.985 | 0.985 (0.985) |
gradcheck는 비교 기준(유한 차분)이 성립하는지도 잰다: 무작위성 없이 길게 돌린 회로는 출력이 연결 세기에 대해 울퉁불퉁해 기준이 흔들린다 (전체 뇌 1,000스텝 규칙 입력: 판단 불가) → 포아송 입력과seeds=로- 대리 기울기가 부분적으로만 맞는 회로가 있다 (예쁜꼬마선충: 깨끗한 기준으로 cos 약 0.5).
fd.ThreeFactor도 같은 대리 기울기라 이 부정확함은 그대로 (메모리·긴 시간 문제만 해결) explain은 기울기로 모든 유형을 한 번에 훑어 후보를 고르고, 결론은 실제로 끈 값(verify)으로. 1차 예측이 방향까지 틀린 유형(되먹임 억제 뉴런 APL 등)은 확인 결과에 표시된다- 표와 재현:
validation/gradients/
실험적 기능: 추가 연결 (구조적 가소성, growth)
실험적 기능 (0.1.18) - 인터페이스·기본값이 바뀌거나 없어질 수 있음. 효과는 조건부: 손상된 회로의 회복에서는 뚜렷하지만, 멀쩡한 회로와 MNIST에서는 이득이 없거나 무작위 연결보다 못한 경우가 있음 (아래 결과). 연구 용도로 쓰고, 결과는 늘 무작위 연결(
rule="random")과 짝지어 비교할 것
실제 배선은 그대로 두고, 학습으로 생기고 없어지는 시냅스를 따로 얹는다 (타고난 회로 + 경험으로 덧붙는 시냅스). 커넥톰 층의 "연결이 정해져 있다"는 한계를 풀면서, 실제 배선은 늘 기준으로 남는다.
from flydnet import lab # 실험실 - 엔진에는 없고, 층에 붙는 부품 (layer.attach)
layer = fd.Connectome(mb, "PN", "KC", trainable=["PN>KC"])
g = lab.Growth(layer, allow=["PN>KC"], budget=5000) # 층에 붙음 (layer.growth로도), 저장·불러오기 때 같이
g.grow(2000) # 무작위 (허용된 그룹 쌍 안에서)
g.grow(2000, rule="coactive", rates=R) # 헤브: 함께 많이 발화한 PN·KC 쌍
g.grow(2000, rule="gradient", loss=lambda L: fd.surprise(head(L(x)), y)) # 손실이 가장 줄 자리
lab.grow_contrastive(g, 2000, inputs=X, encoder=enc) # 정답 없이: 시료끼리 구별되게 (대조 학습)
g.prune(0.2) # 추가 연결 중 약한 20% (실제 배선은 절대 안 건드림)
g.extra_edges() # 표: pre, post, 경로, 세기(시냅스 수), 부호
- 생물학적 제약: 허용된 그룹 쌍 안에서만, 부호는 보내는 뉴런의 실제 부호 (데일의 법칙), 상한
budget·per_neuron - 받는 뉴런마다 상한
per_neuron(기본"auto"= 올림(budget ÷ 받을 수 있는 뉴런 수), 버섯체 KC 2,597개에 5,400개면 3개): 기울기·대조 학습 규칙이 효과가 커 보이는 소수 뉴런에 연결을 몰아, 그 뉴런들이 반응을 독차지하는 것을 막음 (MNIST에서 상한이 없으면 KC 상위 10%가 새 연결의 51%를 가져감). 실제 KC마다 PN 입력 수가 비슷한 것, SRigL(뉴런마다 입력 수 일정)과 같은 생각- 최고 성능이 필요하고 데이터가 고르게 퍼진다면
per_neuron=None: 냄새처럼 상한이 없어도 연결이 여러 뉴런에 퍼지는 데이터에서는 상한이 약간 손해 (냄새 손상 회복 5,400개: 상한 0.894, 없음 0.901). 퍼지는지는np.bincount(g.extra_edges().post)로 볼 수 있음 - 소수 뉴런에 몰려 있으면 상한을 켤 것
- 최고 성능이 필요하고 데이터가 고르게 퍼진다면
- 새 연결은 그 경로 실제 연결 하나 크기(시냅스 수 중앙값, 버섯체 PN→KC 10개,
init_syn)로 생겨 역전파로 커지거나 작아짐 (그 경로의 배율gains도 같이). prune·grow 뒤에도 살아남은 연결의 학습 상태(Adam 관성)는 유지 - 추가 연결이 없으면 계산이 전과 같다. 같은 지연·효과기(genetics)·저장·GPU. 예:
lab/growth_odor.py
결과 (0.1.18 기본값: 받는 뉴런 상한 auto, 처음 세기 = 경로 중앙값, seed 6개, 짝지은 부호 뒤집기 검정)
멀쩡한 회로에서는 이득 없음 (lab/growth_odor.py: 냄새 24개, PN→KC 4,000개 추가 = 실제의 30%, 추가 뒤 KC 5 Hz로
다시 보정): 실제 배선만 0.881, 무작위 0.883, 헤브 0.874, 기울기 0.890, 대조 학습 0.884 - 모두 차이 없음 (p ≥ 0.16).
상한이 없을 때 헤브 규칙은 해로웠음 (0.803, 6/6, p = 0.031 - 활발한 쌍만 이어 소수 KC가 반응을 독차지) → 상한으로 사라짐.
손상 회복 - 냄새에서는 정답 없이도 됨 (lab/growth_lesion.py): PN→KC 연결 13,485개 중 80%(10,788개)를 끊고, 남은
회로에 추가 연결을 얹음. 냄새 48개를 번갈아 두 묶음으로 나눠 B(24개)로 학습·평가. 모든 조건은 연결을 정한 뒤 KC 5 Hz로 보정
| 조건 (추가 연결 수) | 고르는 기준 | 정확도 | 회복 | 무작위 대비 |
|---|---|---|---|---|
| 손상 없음 | 0.877 | 100% | ||
| 80% 손상 | 0.823 | 0% | ||
| 무작위 1,350 / 5,400 | 없음 | 0.841 / 0.856 | 33% / 61% | |
| 항상성 1,350 / 5,400 | 활동 (정답 없음) | 0.833 / 0.853 | 17% / 55% | 못함 |
| 기울기 1,350 | 과제 B 정답 | 0.865 | 76% | |
| 기울기 1,350 / 5,400 | 다른 과제 A 정답 | 0.870 / 0.891 | 86% / 126% | |
| 대조 학습 1,350 / 5,400 | 정답 없음 | 0.873 / 0.894 | 91% / 130% | +0.032 / +0.037 (6/6, p = 0.031) |
| 대조 학습, SCARF 보기 1,350 / 5,400 | 정답 없음 | 0.867 / 0.893 | 81% / 128% | +0.026 (6/6, p = 0.031) / +0.036 |
| 대조 학습, 상한 없음 5,400 | 정답 없음 | 0.901 | 144% |
대조 학습 = 같은 시료에 잡음을 두 번 따로 넣어 KC 반응 두 벌을 만들고, 각 시료가 자기 짝을 다른 시료들 사이에서 찾도록
하는 목표(InfoNCE, SimCLR과 같은 생각)의 기울기로 연결 자리를 고름. 라벨을 전혀 쓰지 않음.
lab.grow_contrastive(g, n, inputs=X, encoder=enc, augment=0.3) - augment는 곱 잡음 세기(숫자) 또는 함수
(lab.corrupt(0.3) = SCARF: 특징 30%를 다른 시료의 값으로). 효과가 조건부라 핵심 엔진이 아니라 flydnet.lab에 둠
- 정답 없이 회복: 대조 학습 1,350개(잃은 연결의 1/8)로 손상 전과 차이 없음 (−0.005, p = 0.47)
- 순환 아님: 데이터를 만든 잡음(곱 로그 정규)과 같은 보기 만들기를 쓰면 '답을 아는' 셈이 될 수 있어, 무관한 SCARF로도 확인 - 같은 결과
- 손상 없는 회로에 대조 학습 5,400개를 얹으면 0.899 - 손상 회로 + 5,400 (PN→KC 8,097개)이 손상 없는 회로 + 5,400 (18,885개)과 비슷 (0.894 대 0.899)
- 정답 없이 활동만 보는 항상성 규칙(조용해진 KC에 입력)은 무작위보다 못함 - 희소 부호에서는 대부분의 KC가 원래 조용해서, 조용함이 입력 부족을 뜻하지 않음
MNIST에서는 대조 학습의 이득 없음 (lab/growth_lesion_mnist.py, 픽셀 → 무작위 투영 → PN → KC, 학습 3,000장,
보기 만들기 = 최대 2픽셀 이동 + 밝기 잡음): 손상 없음 0.862, 80% 손상 0.812
| 조건 | 1,350 | 5,400 |
|---|---|---|
| 무작위 | 0.847 | 0.862 (회복 100%) |
| 대조 학습 (상한 auto) | 0.849 | 0.851 (무작위 대비 −0.011, 1/6, p = 0.062) |
| 대조 학습, SCARF 보기 | 0.853 | |
| 대조 학습, 상한 없음 | 0.825 (무작위보다 확실히 못함, 0/6, p = 0.031) | |
| 손상 없는 회로 + 대조 학습 | 0.871 |
여기서는 무작위 연결만으로 회복되고 (배선이 병목이 아님), 대조 학습은 무작위를 넘지 못함. 상한이 없으면 대조 학습이 KC
상위 10%에 새 연결의 51%를 몰아 크게 나빠졌는데, 자동 상한으로 그 손해는 사라짐. 진단 (--conds): 이동을 빼고 밝기 잡음만,
온도 0.5, 시료 256개, 숫자마다 한 장(같은 숫자끼리 밀어내지 않게)은 모두 몰림을 고치지 못했고, 받는 뉴런 상한만 효과가 있었음.
→ 대조 학습 규칙은 입력 채널에 뜻이 있는 데이터(냄새의 사구체)에서 통했고, 무작위로 섞인 입력(MNIST 픽셀 → 무작위 투영)
에서는 통하지 않음. 80% 손상 회로는 반응이 가팔라 보정이 진동하기 쉬움 → calibrate가 진동하는 그룹의 보폭을 줄여 맞춤
짧은 이름
긴 이름도 그대로 동작한다.
| 짧은 이름 | 원래 이름 |
|---|---|
fd.flywire(), fd.brain(), fd.worm() |
Circuit.from_flywire(), Circuit.whole_brain(), Circuit.celegans() |
fd.Connectome |
fd.ConnectomeLayer (인자 damp = surrogate_damp, ckpt = checkpoint_every) |
fd.Adaptive, fd.Glomeruli, fd.Inhibition, fd.MBON |
AdaptivePlasticity, GlomerularEncoder, LateralInhibition, MushroomBodyOutput |
fd.tune |
fd.tune_surrogate |
구성 요소
| 무엇 | 이름 |
|---|---|
| 회로 고르기·대조군 | Circuit.from_flywire(groups, side), .shuffled(), .randomized(), .shuffled_weights(), .subset() |
| 커넥톰 배선 층 (시간 없음) | Neuropil (학습: "edge" / "pair" / "free" / 고정), LateralInhibition, AxonHillock |
| 시간 시뮬레이션 | ConnectomeLayer — 스파이킹 LIF(Shiu et al. 2024 Brian2 모델과 같은 한 스텝)·연속값 뉴런, 시간 역전파, 체크포인팅 |
| 역전파 없는 학습 | MushroomBodyOutput, AssocReadout, DopamineReadout (도파민 국소 규칙) |
| 인코더·리드아웃 | RateEncoder, GlomerularEncoder, KCExpansion, extract, train_linear |
| 데이터 | synthetic_odors, door_odors (DoOR 2.0), biconditional_mixtures |
| 시각계 | visual_circuit, column_map, drifting_grating, direction_offsets, MOTION_PATHWAY |
| 대조 실험 | compare, controls.Shuffled / Randomized / ShuffledWeights / Local / Custom |
| torch 연동 | fd.torch.bridge — 자체 엔진 구조물을 torch.nn 모델 안에서 (계산은 자체 엔진) |
전체 뇌(13.9만 뉴런, 연결 1,509만)도 일반 GPU에서 학습된다. scripts/bench.py (RTX 5070 12 GB, 감각 → 하행 뉴런,
연결 1,509만 개 모두 학습, 20 ms = 200스텝): 학습 1스텝 배치 8에 1.1초 (GPU 3.3 GB), 배치 32에 2.6초 (8.6 GB).
시뮬레이션만은 1초 시행 30개에 약 26초 (원본 Brian2 CPU는 1시행 약 15초).
자체 엔진 이름 (torch 대응)
| torch | flydnet | torch | flydnet | |
|---|---|---|---|---|
Tensor |
Signal |
nn.Module |
Tissue |
|
requires_grad |
plastic |
nn.Sequential |
Pathway |
|
backward() / .grad |
retrograde() / .retro |
nn.Linear |
Projection |
|
no_grad() |
quiescent() |
optim.SGD / Adam |
Plasticity / AdaptivePlasticity |
|
nn.Parameter |
Synapse |
F.cross_entropy |
surprise |
|
utils.checkpoint |
checkpoint |
"cuda" |
"gpu" |
이름은 같은 일을 하는 생물 구조에서 땄다 (역행성 신호, 시냅스, 조직, 신경 경로, 가소성…).
엔진 검증 (python scripts/verify.py): 연산 55종의 값·기울기를 torch autograd와 (꺾이는 점·동점 포함), 최적화기를
torch.optim과 한 스텝씩, 직접 작성한 LIF CUDA 커널을 일반 연산으로 짠 LIF와 (스파이크 같음, 기울기 float64 1e-7),
원본 Brian2와, CPU↔GPU를 비교 (tests/ref_*.py, 약 1분) + 테스트·예제 전체.
torch 연동: fd.torch.bridge
torch 모델 안에서 flydnet 구조물을 쓴다. 계산은 자체 엔진 (원본 모델과 같은 계산, 직접 작성한 CUDA 커널),
겉은 nn.Module. 복사 없이 메모리를 공유하고(GPU는 DLPack), 역전파가 torch 쪽으로 이어진다.
layer = fd.Connectome(mb, "PN", "MBON", trainable=True)
model = torch.nn.Sequential(encoder, fd.torch.bridge(layer, seed=0), torch.nn.Linear(48, 10))
opt = torch.optim.Adam(model.parameters()) # 커넥톰 학습 값도 torch가 갱신 (자체 엔진과 같은 메모리)
- 자체 엔진만 쓸 때와 출력·기울기가 같다 (테스트). 연결 장치 자체의 비용은 거의 없음 (전체 뇌 학습 1스텝 배치 8에 1.1초)
examples/torch_bridge.py, 시각계 예제 (examples/visual_motion.py: torch 학습 루프 + 자체 엔진 층)
0.1.17까지 있던 0.1의 torch판 복사본(fd.torch.ConnectomeLayer 등)은 0.1.18에서 뺐다. 같은 이름이 fd.에 있다
(fd.torch.RateEncoder → fd.RateEncoder). torch 학습 루프를 그대로 쓰려면 층을 연결 장치로 감쌀 것.
결과 요약
12개 실험 (재현: examples/의 각 스크립트):
| 질문 | 답 |
|---|---|
| 실제 배선이 정확도를 높이나 | 대체로 아니다. 차수와 큰 구조(위치 대응)를 유지한 무작위 배선과는 MNIST·냄새·시각 과제에서 차이 없음 |
| 그럼 배선의 무엇이 중요했나 | 큰 구조: 시각계의 위치 대응(없으면 학습 불가), 스파이킹 버섯체의 KC 연결 수 분포(무작위면 −13%p) |
| 도파민 연합 학습은 | 연속 학습에서 쓸모 있음: CIFAR-100 10과제 57.5% (재생 버퍼 역전파 51.4%), 역전파 없이 한 번 보기. 이득은 KC 배선이 아니라 학습 규칙에서 나옴 (같은 규칙을 원래 특징에 쓰면 57.5%, KC 실제 배선 위에서는 48.9%) |
| 역전파도 덜 잊게 할 수 있나 | 연합 규칙의 생각 세 가지(새 클래스는 평균에서 시작, 나온 클래스끼리만 경쟁, 코사인 점수)로 CIFAR-100 24.7% → 58.5% (재생 없음). fd.Learner(rule="backprop") |
| KC 확장 층은 | 선형 리드아웃일 때만 도움 (XOR형 과제 +19%p) |
개발
python -m venv .venv
.venv\Scripts\pip install --no-cache-dir -e ".[examples,dev,gpu-cuda13]"
.venv\Scripts\python scripts\build_csr.py --all # _csr.c를 고쳤을 때만: 6개 플랫폼 C 커널 (zig, 시험용)
.venv\Scripts\python -m pytest -q # 테스트 (tests/test_golden.py = 엔진 정리 전 결과와 비교)
.venv\Scripts\python scripts\release.py bump patch # 버전 올리기 + CHANGELOG 틀
.venv\Scripts\python scripts\release.py check --upload # 커밋·CHANGELOG·PyPI 중복·테스트·빌드·설치 확인 후 업로드
배포는 순수 파이썬 휠 하나 (6개 플랫폼 C 커널 포함). release.py check가 커널이 지금 _csr.c로 빌드된 것인지 확인한다.
배포 전 플랫폼 확인: GitHub Actions → wheels → Run workflow (5~10분, Linux·macOS·Windows에서 골든 시험).
데이터 출처: FlyWire v783 연결(Shiu et al. 2024, MIT), 세포 주석(Schlegel et al. 2024), DoOR 2.0(CC BY-SA 4.0). MIT 라이선스.
Metadata
Release files for flydnet 0.1.18
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| flydnet-0.1.18.tar.gz | 359.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| flydnet-0.1.18-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 626.4 kB
Release files / flydnet-0.1.18.tar.gz
| Download URL | flydnet-0.1.18.tar.gz |
|---|---|
| Size | 359.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e1f3cfc6b9500d30118a36b6d34f2110b2e2092c65f7d387f7dcd2984664a362
|
|
BLAKE2b-256 checksum How to use checksums |
e39940ab77fbdab505f265537d6614e9b1560c55f38bb0905367788dd29ca683
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.9
|
Release files / flydnet-0.1.18-py3-none-any.whl
| Download URL | flydnet-0.1.18-py3-none-any.whl |
|---|---|
| Size | 267.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ec61287161eaba3bb30f3ea7444e3e6e9c9c79b980c51fb6d81234d2939e9276
|
|
BLAKE2b-256 checksum How to use checksums |
49aa7aefb6f00cd7d2aff1bc0246216bfcc90aaf07882ddda43bfcd5404e5a68
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.9
|