ご挨拶
こんにちは、森と申します。
業務で FPGA と Spking Neural Network(以下SNNと省略します) を触れることになったので、理解を深める目的で本記事を書きました。
本記事の内容
本記事は「FPGAを使ってSNNの人工ニューロンの動きを可視化してみる」、という内容です。
FPGAとは?
FPGA とは、Field Programmable Gate Arrayの文字通り、設計者がフィールド(現場)で論理回路の構成をプログラムできるゲート(論理回路)を集積したデバイスです。製造後は回路構成を変更できないLSI(集積回路)に対し、プログラムにより内部の回路構成、つまり処理内容を変更可能であることからこのように呼ばれます。近年はADコンバータやマイコンを内蔵したFPGAもあります。デバイスの見た目はLSIと何ら変わりなく、製造プロセスも同じですが、デバイス内部の処理内容を設計者が変更できる点が異なります。
SNNとは?
Spiking Neural Networkの略で、人間の脳の神経細胞(ニューロン)の仕組みを模倣したANNのことです※1。人工ニューロンの出力値が今流行りのANNでは連続値なのに対し、SNNでは離散のスパイク値なのが大きな違いです。今流行りのANNもニューロンの仕組みを模倣してますが、より生物学的に模倣しているのがSNNという位置づけです。
※1:「狭義のANN≃今流行りのANN」の定義もあり、SNNがANNと対比して扱われることもあります。
事前準備
まずはFPGAボードがないと始まりません。選択肢が多く(どの企業のどの種類のFPGAにするか?、どの企業のどの種類のボードにするのか?)、かつ結構良い値段するのでとても悩みましたが、約3万円のFPGAボード(DE10-Lite Board)を購入しました。このボードはAlteraのMAX 10というFPGAを搭載しています。
(ボードの種類はLLMに相談して決めました。)

図1:DE10-Lite Boardのレイアウト(引用元)
何をするかを整理する
今回やりたいことは、FPGA上でSNN(LIF)の人工ニューロンの動きを可視化させることです。LIFの仕組みを理解し、FPGAでどう可視化させるかを整理します。
なぜLIF?
仕組みがシンプルだからです。複雑なSNNモデルの人工ニューロンを実装しようとしたら、仕組みを理解しきれず断念してしまう気がしたからです。
LIFとは?
Leaky Integrate and Fire(漏れ積分発火)の略で、SNNモデルの1つです。SNNモデルにはHodgkin-HuxleyモデルやIzhikevichモデルなどいくつか種類がありますが、LIFモデルが特にシンプルな構造をしています。神経科学などの分野では複雑なHodgkin-Huxleyモデルが使われたりすることが多いですが、AIの分野で利用する場合はシンプルなLIFモデルが人気です。
SNN(LIF)
LIFニューロンの大まかな処理フローを図2に、図2に出てくるパラメータの説明とFPGA実装時に用いた設定値を表1に示しています。
※LIFニューロンには「不応期」という、ニューロン発火後、入力(刺激)がきても膜電位が変化しない時間を設定するパラメータがありますが、今回は省略しています。
図2:LIFニューロンの大まかな処理フロー
表1:図2に出てくるパラメータの説明と今回設定する値
| 記号 | 名称 | 役割と説明 | 設定値 |
|---|---|---|---|
| $V$ | 膜電位 (Membrane Potential) | ニューロンの現在の電位。入力電流を時間積分した結果として常に変化する主要な状態変数。 | だいたい0~100の範囲 |
| $V_{\text{rest}}$ | 静止膜電位 (Resting Potential) | 入力電流がない場合に膜電位が最終的に落ち着く電位。 | 20 |
| $V_{\text{th}}$ | 閾値電位 (Threshold Potential) | 膜電位 $V$ がこの値に達したとき、ニューロンはスパイク(発火)を生成する。 | 70 |
| $V_{\text{reset}}$ | リセット電位 (Reset Potential) | 発火(スパイク)直後に膜電位が強制的に戻される電位。 | 0 |
| $V_{\text{peak}}$ | ピーク電位 (Peak Potential) | 発火時に到達する活動電位の最大値。モデルの計算式には直接現れないことが多い。 | 100 |
| $\tau$ | 時定数 (Time Constant) | 膜電位が静止電位に戻る速さ(リークの速さ)を示す値。$\tau = RC$ で定義されることが多い。 | 100 |
| $R$ | 抵抗値 (Resistance) | 細胞膜の抵抗(リーク抵抗)を示す。$\tau$ を用いてモデル化される場合は省略されることがある。 | 10 |
| $\Delta t$ | 更新間隔 (Time Step) | シミュレーションにおいて、膜電位 $V$ を更新する時間の間隔。LIFモデルの離散化に使用される。 | 10 (ms) |
FPGAでどう可視化させるか
FPGAボードの「Button」「LED」「7-Segment Display」を以下の用途で使います。
- Button:押すとシナプス(ニューロン)に電流が流れるようにする
- LED:ニューロンの膜電位の状態を視覚化する(点灯しているLEDが多いほど膜電位が大きい)
- 7-Segment Display:スパイクが発生した回数を視覚化する
HDLで処理を書いていく
HDLはVHDLを使います。実はHDL書くのは2回目で、CQ出版社さんのセミナ(実習・VHDLによるFPGA開発・設計入門[FPGAボード使用])で触ったのが初めてでした。なのでHDL大初心者です。m(_ _)m
設計ソフトウェアはQuartus Prime Lite Edtitonを使いました。
HDLとは?
Hardware Description Language (HDL) is a programming language that is used to describe the structure, behaviour and timing of electronic circuits, and most commonly, digital logic circuits. HDLs are used for designing processors, motherboards, CPUs and various other Digital circuits. In addition to their use in circuit design, HDLs serve the purpose of simulating the circuit and verifying its response. Many HDLs are available, but the most popular HDLs so far are Verilog and VHDL.
--Geminiによる翻訳--
Hardware Description Language (HDL) は、電子回路、特にデジタル論理回路の構造、動作、タイミングを記述するために使用されるプログラミング言語です。HDLは、プロセッサ、マザーボード、CPU、その他様々なデジタル回路の設計に用いられます。回路設計での使用に加え、HDLは回路のシミュレーションやその応答の検証という目的も果たします。多くのHDLが存在しますが、現時点(so far)で最も人気のあるHDLは、Verilog と VHDL です。
最終的なソースコードとピン配置は以下です。(ソースコードはGeminiに頼りまくってました。かなり汚いソースになっている気がしています。)
ModelSimなどのシミュレーションソフトは利用せず、実機に書き込んでデバッグしました。
lif.vhd
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.ALL;
-- トップモジュール (LIFニューロンモデル)
entity lif is
Port (
CLK : in STD_LOGIC;
RST : in STD_LOGIC;
BTN_IN : in STD_LOGIC;
V_LED_OUT : out STD_LOGIC_VECTOR(9 downto 0);
SPIKE_7SEG : out STD_LOGIC_VECTOR(6 downto 0)
);
end lif;
architecture Behavioral of lif is
------------------------------------------------------------------
-- 1. パラメータ定義
------------------------------------------------------------------
constant K_TAU : integer := 100; -- 時定数 τ
constant K_R : integer := 10; -- 抵抗 R
constant K_VREST : signed(7 downto 0) := to_signed(20, 8); -- 静止膜電位 V_rest
constant K_VTH : signed(7 downto 0) := to_signed(70, 8); -- 閾値電位 V_th
constant K_VPEAK : signed(7 downto 0) := to_signed(100, 8); -- ピーク電位 V_peak
constant K_VRESET : signed(7 downto 0) := to_signed(0, 8); -- リセット電位 V_reset
constant K_DT : integer := 10; -- 時間ステップ(ms) Δt
-- 50MHzクロックから100Hzを生成
constant CLK_FREQ_HZ : integer := 50000000;
constant TARGET_FREQ : integer := 100;
constant CLK_DIV_MAX : integer := CLK_FREQ_HZ / TARGET_FREQ;
------------------------------------------------------------------
-- 2. 内部信号
------------------------------------------------------------------
signal V_current : signed(7 downto 0) := K_VREST;
signal I : signed(7 downto 0);
signal V_next : signed(7 downto 0);
signal spike_counter : unsigned(3 downto 0) := (others => '0');
signal spike : STD_LOGIC := '0';
-- クロック分周用
signal clk_div_counter : integer range 0 to CLK_DIV_MAX-1 := 0;
signal slow_clk_enable : STD_LOGIC := '0'; -- LIFロジック実行エッジを生成
-- LIF計算用の一時的な拡張信号
signal Driving_Force : signed(15 downto 0); -- V項 (-V + V_rest + R*I)
signal Delta_V : signed(15 downto 0); -- ΔV項 (Δt * Driving_Force / τ)
begin
------------------------------------------------------------------
-- A. 入力処理 (I_in)
------------------------------------------------------------------
-- ボタン押下時 (アクティブロー: BTN_IN='0') に I=10 を流す
I <= to_signed(10, 8) when BTN_IN = '0' else to_signed(0, 8);
------------------------------------------------------------------
-- B. クロック分周器 (slow_clk_enable生成)
------------------------------------------------------------------
process(CLK, RST)
begin
-- RST='0' (アクティブロー) でリセット
if RST = '0' then
clk_div_counter <= 0;
slow_clk_enable <= '0';
elsif rising_edge(CLK) then
if clk_div_counter = CLK_DIV_MAX - 1 then
clk_div_counter <= 0;
slow_clk_enable <= '1'; -- 1サイクルだけ '1' に
else
clk_div_counter <= clk_div_counter + 1;
slow_clk_enable <= '0';
end if;
end if;
end process;
------------------------------------------------------------------
-- C. LIFニューロンのメインロジック
------------------------------------------------------------------
-- Driving_ForceとDelta_Vを計算
Driving_Force <= resize((-V_current + K_VREST), 16) + resize((K_R * I), 16);
-- 符号拡張後の除算
Delta_V <= resize(to_signed(K_DT, 16) * Driving_Force / to_signed(K_TAU, 16), Delta_V'length);
process(CLK, RST)
begin
if RST = '0' then
V_current <= K_VREST;
spike_counter <= (others => '0');
elsif rising_edge(CLK) then
-- slow_clk_enable='1' のエッジでのみLIFを更新
if slow_clk_enable = '1' then
-- 次の膜電位を計算
V_next <= V_current + Delta_V(7 downto 0); -- 8ビットに縮小して加算
-- スパイク判定
if V_next >= K_VTH then
-- 閾値越え: Vをリセット電位に設定
spike <= '1';
V_current <= K_VPEAK;
V_current <= K_VRESET;
if spike_counter < 9 then
spike_counter <= spike_counter + 1;
end if;
else
-- 通常ステップ: V_currentをV_nextで更新
spike <= '0';
V_current <= V_next;
end if;
end if;
end if;
end process;
------------------------------------------------------------------
-- D. 出力処理 (LEDと7セグ)
------------------------------------------------------------------
-- V_LED_OUT (RST='0'で消灯を最優先し、高い値からチェック)
V_LED_OUT <= (others => '0') when RST = '0' else
"1111111111" when V_current >= to_signed(90, 8) else
"0111111111" when V_current >= to_signed(80, 8) else
"0011111111" when V_current >= to_signed(70, 8) else
"0001111111" when V_current >= to_signed(60, 8) else
"0000111111" when V_current >= to_signed(50, 8) else
"0000011111" when V_current >= to_signed(40, 8) else
"0000001111" when V_current >= to_signed(30, 8) else
"0000000111" when V_current >= to_signed(20, 8) else
"0000000011" when V_current >= to_signed(10, 8) else
"0000000001" when V_current >= to_signed(0, 8) else
(others => '0');
-- SPIKE_7SEG
with std_logic_vector(spike_counter) select
SPIKE_7SEG <= "1000000" when "0000", -- 0
"1111001" when "0001", -- 1
"0100100" when "0010", -- 2
"0110000" when "0011", -- 3
"0011001" when "0100", -- 4
"0010010" when "0101", -- 5
"0000010" when "0110", -- 6
"1111000" when "0111", -- 7
"0000000" when "1000", -- 8
"0011000" when "1001", -- 9
"0111110" when others; -- -
end Behavioral;
pins.csv
To,Direction,Location,I/O Bank,VREF Group,Fitter Location,I/O Standard,Reserved,Current Strength,Slew Rate,Differential Pair,Strict Preservation
BTN_IN,Input,PIN_A7,7,B7_N0,PIN_A7,2.5 V,,,,,
CLK,Input,PIN_N5,2,B2_N0,PIN_N5,2.5 V,,,,,
RST,Input,PIN_B8,7,B7_N0,PIN_B8,2.5 V,,,,,
SPIKE_7SEG[0],Output,PIN_C14,7,B7_N0,PIN_C14,2.5 V,,,,,
SPIKE_7SEG[1],Output,PIN_E15,7,B7_N0,PIN_E15,2.5 V,,,,,
SPIKE_7SEG[2],Output,PIN_C15,7,B7_N0,PIN_C15,2.5 V,,,,,
SPIKE_7SEG[3],Output,PIN_C16,7,B7_N0,PIN_C16,2.5 V,,,,,
SPIKE_7SEG[4],Output,PIN_E16,7,B7_N0,PIN_E16,2.5 V,,,,,
SPIKE_7SEG[5],Output,PIN_D17,7,B7_N0,PIN_D17,2.5 V,,,,,
SPIKE_7SEG[6],Output,PIN_C17,7,B7_N0,PIN_C17,2.5 V,,,,,
V_LED_OUT[0],Output,PIN_A8,7,B7_N0,PIN_A8,2.5 V,,,,,
V_LED_OUT[1],Output,PIN_A9,7,B7_N0,PIN_A9,2.5 V,,,,,
V_LED_OUT[2],Output,PIN_A10,7,B7_N0,PIN_A10,2.5 V,,,,,
V_LED_OUT[3],Output,PIN_B10,7,B7_N0,PIN_B10,2.5 V,,,,,
V_LED_OUT[4],Output,PIN_D13,7,B7_N0,PIN_D13,2.5 V,,,,,
V_LED_OUT[5],Output,PIN_C13,7,B7_N0,PIN_C13,2.5 V,,,,,
V_LED_OUT[6],Output,PIN_E14,7,B7_N0,PIN_E14,2.5 V,,,,,
V_LED_OUT[7],Output,PIN_D14,7,B7_N0,PIN_D14,2.5 V,,,,,
V_LED_OUT[8],Output,PIN_A11,7,B7_N0,PIN_A11,2.5 V,,,,,
V_LED_OUT[9],Output,PIN_B11,7,B7_N0,PIN_B11,2.5 V,,,,,
動作確認
GIF1が動作確認している様子です。それっぽくなっています。
① ボタン押下時のLED点灯
観察:ボタンを押すと、LEDが徐々に点灯していきます。
モデル上の解釈:ボタン押下により入力電流が流れ、膜電位が時間積分されて上昇しています。
生理学的な対応:これはニューロンが刺激を受けて膜電位が上昇する「脱分極」に相当します。
② 閾値到達時の発火とリセット
観察:LEDが7つ点灯したタイミングでスパイク回数が増加し、同時にLEDが消灯します。
モデル上の解釈:膜電位が閾値を超えたことでスパイクが発生し、直後に膜電位がリセット電位へ戻されています。
生理学的な対応:これは発火後に膜電位が急激に低下する「再分極」を表しています。
補足:本来は発火時にピーク電位(LED全点灯)を表示したかったのですが、実装上の問題により実現できていません。また、スパイクカウントの挙動にもタイミング起因と思われる違和感があります。
③ ボタン未入力時の電位回復(過分極から静止電位へ)
観察:LEDが一度消灯した後、ボタンを押していないにもかかわらず、再び徐々に点灯していきます。
モデル上の解釈:リセット後に膜電位が静止膜電位よりも低い状態(過分極)となり、リーク項によって静止膜電位へ戻っていきます。
生理学的な対応:これは過分極後の膜電位回復を表しています。
④ 入力電流がない場合のリーク動作
観察:閾値に達しない程度までボタンを押した後に手を離すと、時間経過とともにLEDが消灯します。
モデル上の解釈:入力電流がないため、膜電位はリークによって静止膜電位へ向かって減衰しています。
生理学的な対応:刺激がない場合に膜電位が自然に低下する挙動を表しています。
最後に
LIFモデルの理解度が大分深まりました。本記事では触れてませんが、LIFモデルを理解するにあたり、仕組みを深ぼりに深ぼっていて、そのためには化学(イオン)と物理学(電気回路)の知識が必要で、そこで沼ってました。本当に複雑なSNNモデルを扱わなくて良かったと感じています。(断念していたに違いない)
FPGAはなんちゃってコーディングしかしてないので、次は複雑な回路を書いてみたなという気持ちになりました。
最後まで読んでくださり、ありがとうございました。

