ランダムビットパターン系列を連想記憶するホップフィールドネットワークをC言語で書いてみた

はじめに

前回の記事でホップフィールドネットワークのPythonを実装を書いたのだった。

tam5917.hatenablog.com

プログラムを高速実行させたく、今回C言語で書き直したということ。

実装

以下に置いた。Enjoy!

main関数における処理の流れは共通なので説明は省略する。Python実装にはなかった明示的なメモリ確保・解放処理が入るぐらいの違いしかない。

int main(int argc, char *argv[]) {
    HopfieldConfig config;
    srand((unsigned int)time(NULL));

    // 1. Configure the Hopfield network
    configure(argc, argv, &config);

    // 2. Define bit patterns to be learned
    int **patterns =
        allocate_int_matrix(config.num_patterns, config.num_neurons);
    define_patterns(&config, patterns);

    // 3. Learn patterns by Hebbian learning rule
    config.weights =
        allocate_double_matrix(config.num_neurons, config.num_neurons);
    learn(&config, (const int *const *)patterns);

    // 4. Run recall test (initial state is a noisy version of each pattern)
    recall_test(&config, (const int *const *)patterns);

    // 5. Cleaning up
    free_double_matrix(config.weights, config.num_neurons);
    free_int_matrix(patterns, config.num_patterns);

    return 0;
}

ネットワーク関連の設定は HopfieldConfig 構造体に格納する。

typedef struct {
    int num_neurons;          // Number of neurons
    int num_patterns;         // Number of patterns to be learned
    double **weights;         // Weights of network
    double neuron_threshold;  // Activation threshold for neurons
    double noise_strength;    // Strength of bit-flip noise to each neuron
    char update_rule[6];      // Update rule for state (sync/async)
    int max_iterations;       // Maximum number of iterations for recall
    double recall_threshold;  // Threshold for recall convergence
    int async_iterations;     // Number of iterations for asynchronous update
} HopfieldConfig;

オプション引数は基本的に前回記事のものを逐次置き換えていくだけである。

オプション名 説明 デフォルト値
-n ネットワークのニューロン 20
-p 記憶対象のパターン数 3
-t ニューロン発火のしきい値 0.0
-s ニューロンがビット反転される確率 0.2
-u 想起にかかる状態更新の方法 "sync"(同期型)
-i 想起の最大繰り返し回数 100
-r 想起の収束しきい値 1e-6
-a 非同期型の状態更新の繰り返し回数 100

前回実装の HopfieldNetwork クラスがなくなったので、learnenergyrecall のメソッドは関数として独立させて定義した。

実行時のコンソール出力の見た目も、先のPython実装とまったく変わらない。ゆえに実行例の紹介も省略する。

余談:コンパイル時の最適化オプションおよびバイナリの実行速度について

コンパイルはO2の最適化オプションをつけるのが初手(定石)という筆者の認識だった。

$ cc -O2 hopnet_1d.c

しかし、今回はOfastオプションを試してみる。

$ cc -Ofast hopnet_1d.c

これにより、さらに高速に実行可能なバイナリが生成されることが期待できる。ちなみにOfastオプションは最適化のために安全性を色々と犠牲にしているようである。

ニューロン数を500、パターン数を50と、それぞれ十分に大きく設定し、各コンパイルオプションで生成したバイナリの実行時間を以下の表に示す。計測にはtimeコマンドを用いた。マシン環境は Apple M1 (MacBookAir)、コア数 8、メモリ 16 GBである。clangのバージョンは 15.0.0 である。

オプション 実行時間
O0 0.868
O1 0.264
O2 0.209
O3 0.203
Ofast 0.053

O2オプションと比較してOfastは実行時間が約4分の1であった(4倍の高速化)。

Pythonについて計測した実行時間はこのようになった。トータルは0.265秒であるが、system 610% とマルチコアを駆使した結果であった(ライブラリの影響か?)。Pythonのバージョンは3.11である。

1.16s user
0.46s system
610% cpu
0.265s total

この実行時間 0.265 秒と比較すると、O2オプションはPython版の約 75 %、さらにOfastオプションで約 20 %となり、C言語版の高速実行が示された。

おわりに

本記事では高速化を実現すべくC言語で実装しなおした。コンパイルの最適化オプション Ofast は万能ではなく、その適用にはリスクがあるようだが、少なくとも今回のシミュレーション用途には問題なく利用できた。