0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

視覚野刺激パターン生成を実装してAWSで動かしてみた

0
Posted at

はじめに

本記事では、写像や標準偏差等、数学や統計学、物理学関連の用語がたくさん出てきます。
なるべく注釈をつけていますが、分かりづらいかもしれません

AIが作った画像を、モニターではなく自分の視覚として受け取るには、画像を電極の刺激パターンに変えることが必要です。
視覚野を電気刺激すると光の点(ホスフェン)が見えるので、画像に合わせてどの電極にどれだけ電流を流すかを決める部分です。

調査記事「モニター・キーボードがいらない世界に向けて」で、最初の試作は「疑似皮質視覚デモ」にすると書きました。

今回の実装では、画像を入れると電極ごとの電流を作り、その電流で見える像をシミュレーションして、ブラウザに映します。
電極数は100本、1,000本、10,000本を切り替えられます。

先に分かったことを書きます。

  • 電極数を増やすと、細部(線や文字の輪郭)の再現は上がります。輪郭相関は100本で0.274、10,000本で0.658でした
  • 精度を左右したのは、エンコーダーの学習だけではありません
    患者ごとの閾値の較正も、同じ程度に影響していました(1,000本の単純サンプリングで、較正なしの0.085が、較正ありで0.270)
  • 画像指標のSSIMだけでは、電極数の違いが見えませんでした。
    細部を見る指標を足して、ようやく差が出ました

実装した範囲は、シミュレーターの上で刺激パターンを作るところまでです。
実際の刺激は行っていません。

作ったもの

流れは次のとおりです。

  • 画像をエンコーダーに入れ、電極ごとに「どれくらいの明るさで見せたいか」を決めます
  • 明るさを、電極ごとの閾値を使って電流(µA)に変換します
    このとき、平均電流の上限も守ります
  • 電流をシミュレーターに入れ、見えるはずの像を計算します

作ったのは次の6つです。

  • 電極配置とホスフェンの大きさ(視覚野の対数極写像)
  • 微分可能なホスフェンシミュレーター
  • エンコーダー3種類(単純サンプリング、輪郭、学習版)
  • 閾値の較正(模擬患者への問いかけ)
  • 学習と評価のスクリプト
  • ブラウザUIとFastAPIのAPI

PyTorchで実装し、Mac上のCPUだけで学習と推論が動きます。テストは21件です。

電極をどこに置くか

視覚野には、視野が歪んで写っています。中心窩(視野の中心)が広い面積を占め、周辺は狭くなります。
この写像は複素対数で近似できることが知られていて(Schwartz、Vision Research 1980年)、今回はそれを参考にしました。

電極を皮質上の格子に置き、視野の位置に戻します。
すると、ホスフェンは中心で密に、周辺で疎に並びます。
ホスフェンの大きさは、拡大率の逆数にあたる「偏心度+a」に比例させました。
aの値(0.75度)は私が置いた値です。

z = np.exp(uu + 1j * vv) - A_DEG            # 右視野の位置(度)
keep = z.real >= 0.0                        # 中心窩付近で正中線を越える分を除く
sigma = size_scale * du * (ecc + A_DEG)     # ホスフェンの大きさ ∝ 偏心度

電極は左右の半視野に半分ずつ置き、ちょうど100本、1,000本、10,000本になるよう個数を調整しています。

電流から像を作るシミュレーター

電極iに電流 I を流したときの明るさは、閾値 thr を中心にしたシグモイドで決めます。

像は、各電極のガウス形の光を足し合わせ、飽和させたものです。

def brightness(self, current_ua):
    return torch.sigmoid((current_ua - self.thr) / (SLOPE * self.thr))

def forward(self, current_ua):
    b = self.brightness(current_ua)
    total = b @ self.basis          # basis: (電極数, 画素数) のガウス
    return 1 - torch.exp(-total)    # 飽和

閾値は電極ごとに違います。
Fernándezら(2021年)は、96電極のUtah arrayで、単電極の閾値が平均66.8、標準偏差36.5 µAだったと報告しています。

この平均と標準偏差を目標にして、対数正規分布で生成しました。
ただし15〜140 µAで切り詰めたため、生成した閾値は平均65.2、標準偏差31.2 µAで、標準偏差は文献値より小さくなっています。

Dynaphos(van der Grintenら、eLife 2024年)を参考にした最小実装で、移植ではありません。刺激の時間変化や、隣り合う電極の干渉は入れていません。

エンコーダーを3種類作る

比較のため、3種類を作りました。

  • 単純サンプリング:電極ごとに、そのホスフェンの範囲の画像の平均値を取ります
  • 輪郭:Sobelフィルターで輪郭を取ってから、同じ方法で電極に割り当てます
  • 学習版:小さなCNN(パラメータ107,017個)です。画像を3段階まで縮小しながら畳み込み、各電極のホスフェンの位置で特徴を取り出して、電極ごとの明るさを出します

学習版は、電極の数に依存しません。
入力に、電極の偏心度、ホスフェンの大きさ、閾値の推定値、電流上限の割合を並べます。同じ重みで、100本でも10,000本でも動きます。

明るさから電流への変換は、閾値の逆関数です。

def brightness_to_current(b, thr_hat):
    b = b.clamp(0.02, 0.98)
    logit = torch.log(b / (1 - b))
    return (thr_hat * (1 + SLOPE * logit)).clamp(0.0, I_MAX_UA)

平均電流の上限は、電流全体を同じ比率で下げて守ります。

患者ごとの閾値を較正する

実際の患者の閾値は分かりません。
そこで、電極ごとに「光が見えましたか」と電流を変えて尋ねる手順を、模擬患者で作りました。二分探索を7回行い、各回に3回尋ねて多数決を取ります。
1電極あたり21回の問いかけで、1,000電極なら21,000回です。

推定誤差は約6%でした。較正しないで、全電極に母集団の平均閾値を使うと、誤差は約51%になります。

学習のやり方

画像から像までを、微分可能なシミュレーター越しにつないで学習しました。

  • 学習データは、図形、線、文字を描いた合成シーン4,000枚です
  • 電極数は100本(配置4種)、1,000本(3種)、10,000本(1種)を混ぜます
  • 較正の状態も混ぜます。4割は較正なしで、残りは、閾値にばらつき(0〜35%)を加えたものを使います
  • 電流上限は「なし」と、40、25、15 µAを混ぜます
  • 損失は、像と平滑化した原画の平均二乗誤差、SSIM、平均電流のペナルティです

3,000ステップで、Mac上のCPUで約10分でした。

結果:電極数と較正で何が変わるか

合成シーン120枚で評価しました。使ったのは、学習に使っていない模擬患者(電極配置のジッターと閾値の乱数が異なる個体)と、別の乱数で作ったシーンです。指標は2つです。SSIMは平滑化した原画との一致で、大きな領域の再現を見ます。輪郭相関は、像と原画の輪郭マップの相関で、細部の再現を見ます。

電極数 単純サンプリング(較正なし) 単純サンプリング(較正あり) 学習版(較正あり)
100 輪郭相関 0.030 / SSIM 0.351 0.077 / 0.471 0.274 / 0.603
1,000 0.085 / 0.229 0.270 / 0.452 0.540 / 0.568
10,000 0.150 / 0.157 0.470 / 0.431 0.658 / 0.512

読み取れることは4つです。

  • 電極数を増やすと、輪郭相関は上がります。学習版・較正ありで、0.274、0.540、0.658です
  • 較正の影響は大きく、1,000本の単純サンプリングでは、輪郭相関が0.085から0.270に上がりました。学習版は、そこからさらに0.540まで上げています
  • 平均電流を25 µAに制限しても、1,000本の学習版は輪郭相関0.512を保ちました。単純サンプリングは0.307です
  • SSIMは、電極数を増やすとむしろ下がります(学習版で0.603、0.568、0.512)。
    大きな領域を見る指標のため、と考えていますが、確かめていません

SSIMと平均二乗誤差は学習の損失と同じ対象なので、学習版に有利です。
細部を見る輪郭相関は、学習で直接最適化していない指標です。
全21通りの結果は、リポジトリのREADMEにあります。

image-1791014032690.png

flow_ja.jpg

つまずいたところ

左右の電極が正中線で重なった

比較図を見ると、単純サンプリングの像の中央に、縦の明るい帯が出ていました。
対数写像が中心窩付近で正中線を越えるため、左右の半視野の電極が重なっていたのが原因です。正中線を越える電極を除き、個数を合わせ直して、再学習しました。

数値の表だけを見ていたら、気づけなかった問題です。ベースラインの像を目で見る作業は省けないと感じました。

SSIMだけでは解像度の差が出なかった

最初の評価では、SSIMが電極数を増やしても上がりませんでした。
比較図の見た目と食い違っていたため、細部の再現を見る輪郭相関を足しました。
指標が何を見ているかを確かめずに、結果を解釈しかけた例です。

EC2の初回起動でpipが失敗した

AWSに置いたEC2が、CloudFrontから504を返し続けました。
コンソール出力を見ると、起動スクリプトのpipが失敗していました。

ERROR: Could not find a version that satisfies the requirement flit_core<4,>=3.11 (from versions: none)

原因は、Amazon Linux 2023標準のpip(22.3.1)です。
typing_extensionsをソースからビルドしようとして、ビルド用の依存をPyTorchのCPU用インデックスから探し、見つからなかったのでした。
pipを先に更新し、CPU版のtorchをバージョン固定で入れる形に直しました。

ユーザーデータは、初回起動時にしか実行されません。
そこで、テンプレート内のインスタンスの論理IDを変えて、インスタンスごと入れ替えました。

動かし方

ローカルで動かす手順です。

uv venv --python 3.12 .venv
uv pip install --python .venv/bin/python torch numpy pillow fastapi "uvicorn[standard]" pytest httpx
.venv/bin/python -m pytest tests -q
.venv/bin/python -m cortical_stim.train --steps 3000 --pool 4000
.venv/bin/python -m cortical_stim.evaluate
ALLOW_NO_KEY=1 .venv/bin/uvicorn server.app:app --port 8791

ブラウザで http://127.0.0.1:8791/ を開くと、カメラかサンプル画像を入力にできます。
ALLOW_NO_KEY=1 は、アクセスキーなしで起動するための、ローカル専用の指定です。

ブラウザUIとAPI

APIは、画像を受け取って像を返すだけです。

  • POST /api/stimulate:JPEGかPNGを送ると、ホスフェン像のPNGが返ります
    電極数、エンコーダー、較正の有無、電流上限は、クエリで指定します
  • 返す統計:平均電流、閾値を超えた電極の割合、推論時間などをヘッダーに入れます

較正のことをキャリブレーションとも言います

UIは1枚のHTMLで、カメラかサンプル画像を入力にできます。
電極数やエンコーダーを切り替えると、すぐに像が変わります。

AWSにデプロイする

カメラはHTTPSでないと使えないため、CloudFrontの背後にEC2を置きました。

  • EC2:t3.large(CPU)です
    このアカウントはGPU系EC2のクォータが0でしたが、推論はCPUで足ります。サーバー側の推論時間は、数回の実測で、100本が13〜22ミリ秒、1,000本が19〜22ミリ秒、10,000本が80〜100ミリ秒でした
  • CloudFront:キャッシュ無効で、オリジンにはHTTPの8080番を指定します
  • S3:アプリと重みのアーカイブを、非公開のバケットに置きます。EC2は起動時に取得します
  • AWS Systems Manager:コードの更新を、SSHなしで実行します
  • AWS CloudFormation:上記をまとめて作成・削除します

公開して使うので、独立したレビューを2種類通し、次の点を直しました。

  • セキュリティグループは、CloudFrontのオリジン向けアドレスだけを許可します。
    ただし、このリストは全CloudFront共通のため、他人のディストリビューションからも到達できます
    そこで、CloudFrontが付けるオリジン認証ヘッダと、アクセスキーの2つをアプリ側で検証します
  • アクセスキーは、URLのフラグメントで渡し、ヘッダーで送ります
    サーバーにもアクセスログにも残りません
  • 画像は2 MB・400万画素までに制限します。小さなPNGに巨大な画像を詰めた攻撃を防ぐためです
  • サービスは一時ユーザー(DynamicUser)で動かし、ファイルシステムは読み取り専用にします
    キーが設定されていないと、サーバーは起動しません

CloudFrontからEC2までの区間は、HTTPで暗号化されていません。
カメラ映像(顔が映り得る)はこの区間を通るため、画面にもそう書きました。

architecture_ja.jpg

実カメラで確かめる

MacBookの内蔵カメラで、ローカル版を確認しました。
映像がそのまま像になりました。
電極数を上げると、像はよくなりました。AWS版でも同じでした。

スクリーンショット 2026-10-02 10.15.42.png

AWSのリソースは、確認後にすべて削除しました。

できていないこと

  • 評価に使ったのは合成シーンだけで、実カメラ映像の定量評価はしていません
  • 模擬患者は、学習時と同じシミュレーター・同じ閾値分布から作っています。
    未知の患者モデルへの汎化は確かめていません
  • ホスフェンは視線に固定されるため実際には視線追跡が必要ですが、扱っていません
  • 人間の知覚で確かめた結果ではなく、画像指標による比較です

おわりに

冒頭の問いに戻ります。画像を電極の刺激パターンに変える段階は、シミュレーターの上でなら、CPUだけで動かせました。
電極数を上げると、細部の再現は上がりました。
一方で、その差を支えたものは2つあります。
エンコーダーの学習と、患者ごとの閾値の較正です。

入力側(キーボードの代わり)は、先に投稿した記事「脳から文字へのデコーダー:公開データ・CPU・Amazon Bedrockで再現」で公開されている脳活動のデータを使って試しています。

最後まで読んでいただきありがとうございました。

参考

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?