はじめに
p-bit(確率的ビット) という素子をソフトウェアでシミュレートし、ノイズで崩れた数字の画像を元に戻して分類するエンジンを作りました。C++23 の標準ライブラリだけで動きます。
リポジトリ: https://github.com/purplecatman2005-dev/pbit-engine
この記事について
コードと記事は、AI(Claude)と対話しながら作りました。数値はすべて手元で実行した結果です。
左が元の「8」、真ん中が画素の 25% をランダムに反転させたもの、右が p-bit ネットワークで復元した結果です。
p-bit とは
量子ビットは 0 と 1 の「重ね合わせ」を持ちますが、p-bit は 0 と 1 の間をランダムに揺らぎ続ける普通のビットです(Camsari らが提唱)。量子コンピュータのような冷却装置が不要で、組合せ最適化などを確率的に解く素子として研究されています。
更新ルールはたった 1 行です。
m_i = sgn( tanh(β·I_i) − r ), r は −1〜1 の一様乱数
I_i = Σ_j J_ij·m_j + h_i (周りのビットからの影響 + 外からのバイアス)
- β が小さい(高温): ほぼコイン投げ。完全にランダム
- β が大きい(低温): 周りの影響に素直に従い、ほぼ決定的に動く
- β を少しずつ上げる: シミュレーテッド・アニーリング。最初は大きく揺らいで探索し、最後に落ち着く
C++ ではこう書けます。
// one asynchronous sweep: every p-bit updated once in random order
void sweep(double beta) {
std::shuffle(order_.begin(), order_.end(), rng_);
for (int i : order_) {
const double I = local_field(i);
m_[i] = (std::tanh(beta * I) > unif_(rng_)) ? int8_t{1} : int8_t{-1};
}
}
数字を復元する仕組み
画像は 10×12 = 120 画素の白黒で、1 画素を 1 個の p-bit に対応させます。
- 記憶させる: 0〜9 の 10 個の数字を、ビット同士のつながりの強さ(結合行列 J)として覚えさせる
- ノイズ画像を見せる: ノイズ画像を「ヒント」(バイアス h)として与える
- アニーリング: β を 1.0 → 6.0 と上げながら揺らがせる
- 固める: ヒントを少しずつ弱めながらさらに冷やし、きれいな数字に落ち着かせる
- 分類: 記憶している 10 個の数字のうち、一番よく重なるものを答えにする
作っている途中で、うまくいかなかった点が 3 つありました。
罠1: 素直な学習則だと「0」と「8」が混ざる
ホップフィールドネットワークで定番の Hebb 則(よく一緒に光る画素同士を強く結ぶ)で覚えさせると、うまく復元できませんでした。「0」と「8」は 120 画素のうち 9 割が一致するほど似ていて、記憶同士が干渉するためです。
そこで 擬似逆行列則 に切り替えました。記憶同士の重なり具合を表す行列 Q の逆行列で補正することで、10 個の数字すべてが「ぴったり安定する状態」になります。
// pseudo-inverse rule: J = (1/N) * Xi * Q^-1 * Xi^T, Q_uv = (1/N) xi_u . xi_v
// makes every stored pattern an exact fixed point of the beta->inf dynamics
罠2: 揺らがせると、見せた画像の情報が消える
アニーリングは高温(ランダム)から始めるのが基本ですが、そうすると最初の揺らぎで入力画像の情報が消えてしまい、何を復元すればよいか分からなくなります。
そこで、ノイズ画像をスタート地点にするだけでなく、揺らいでいる間もずっとヒントとして与え続けるようにしました(バイアス h_i = 0.6 × ノイズ画像の画素)。これで精度は 10% → 97.6% に上がりました。
記憶(J)を「事前の知識」、ヒント(h)を「観測」と見ると、ベイズ推論と同じ形になっています。
罠3: 最後にヒントを急に切ると、似た数字に滑り落ちる
最後の「固める」段階でヒントをいきなり 0 にすると、よく似た別の数字に引き込まれることがありました(95.3%)。ヒントを少しずつ弱めるように変えると、97.6% に戻り、復元画像の品質も上がりました。
// freeze: decay the evidence to zero while cooling further, so the state
// relaxes into a clean stored pattern without leaving the correct basin
for (int s = 0; s < freeze_sweeps; ++s) {
const double f = lambda * (freeze_sweeps - 1 - s) / decay_span;
for (int i = 0; i < n; ++i) net.h(i) = f * noisy[i];
net.sweep(beta_end + s);
}
結果
数字 10 個 × 100 回ずつ、ノイズの量を変えて試しました。
| 画素反転ノイズ | 分類精度 | 復元の一致度 |
|---|---|---|
| 10% | 100.0% | 1.000 |
| 20% | 97.6% | 0.993 |
| 25% | 92.1% | 0.984 |
| 30% | 81.2% | 0.960 |
| 40% | 44.9% | 0.793 |
画素の 2 割が反転していても 97.6% を正しく分類できます。4 割を超えると元の数字の面影がほとんどなくなるため、精度は大きく落ちます。
テスト
確率で動くプログラムは、正しく動いているのか分かりにくいので、理論と照らし合わせるテストを用意しました。
- 1 個だけの p-bit を 20 万回揺らし、平均が理論値 tanh(β·h) と 4σ 以内で一致するか
- β を非常に大きくすると、エネルギーが一度も増えないか
- 記憶した 10 個の数字が、それぞれ安定した状態になっているか
おまけ: QUBO ソルバ
同じ p-bit ネットワークで、QUBO(0/1 の変数を選ぶ組合せ最適化問題)も解けるようにしています。0/1 の変数 x を p-bit の ±1 に x = (1+m)/2 で置き換えると、QUBO の目的関数がそのまま p-bit のエネルギーになるので、アニーリングでエネルギーを下げれば最適化になります。
// J_ij = -Q_ij/4, h_i = -( q_i/2 + sum_{j!=i} Q_ij/4 )
// とすると E(m) = f(x) + const となり、E 最小化 = f 最小化。
build_qubo.bat で、銘柄選択(ポートフォリオ)を QUBO として解くサンプル qubo_portfolio.exe がビルドされます。
動かし方
MinGW-w64 の g++(13 以上)が必要です。
git clone https://github.com/purplecatman2005-dev/pbit-engine
cd pbit-engine
build.bat
demo.exe rem 復元の例と、ノイズ量ごとの精度表を表示
test_pbit.exe rem 不変条件テスト
限界
- 本物の p-bit 素子ではなく、普通の CPU 上でのシミュレーションです。速さで CNN に勝つものではありません
- 覚えている数字は自作フォントの 10 パターンだけで、手書き文字(MNIST など)ではありません
- 記憶できるパターン数には上限があり、似たパターンが増えるほど混ざりやすくなります
おわりに
「ランダムに揺らぐビットを、だんだん落ち着かせると答えが出る」という仕組みを、1 行の更新ルールから組み立ててみました。量子アニーリングと同じ問題設定(イジングモデルのエネルギー最小化)を、手元の PC で試せます。
感想や改善案があれば、コメントや GitHub の Issue で教えてください。

