FPGA上でのニューラルネットワークの構築

Ari Mahpour
|  投稿日 2024/08/8, 木曜日  |  更新日 2024/10/9, 水曜日
FPGA上でのニューラルネットワークの構築

ニューラルネットワークの理解では、人工ニューラルネットワークの概要を見て、それらをより高いレベルで理解するために実世界の例を挙げてみました。この記事では、ニューラルネットワークをどのように訓練し、その後、hls4mlと呼ばれるオープンソースライブラリを使用してフィールドプログラマブルゲートアレイ(FPGA)にデプロイするかを見ていきます。

モデル

ニューラルネットワークの理解とは対照的に、私たちは2つの主な理由から、はるかにシンプルなモデルを見ていくことになります。一部の人にとって、28 x 28ピクセルから完全なモデルへの移行は大きな飛躍でした。大規模な多次元行列の中で一連の逐次関数として分解を説明しましたが、それでも理解するのは難しかったです。私たちはしばしば「パラメーター」について聞きますが、画像処理モデルでは、それらは抽象的で複雑に見えることがあります。また、ハードウェアに合成するために、よりシンプルなモデルを使用したいと考えています。グラフィックス処理ユニット(GPU)、標準的な家庭用デスクトップGPUでさえ、モデルを即座に計算してトレーニングすることができます。それは特にそのタスク(つまり、数値計算)のために設計されています。洗練された物理エンジンと画像レンダリングを備えた3Dゲームにどれだけ多くの計算が必要かを考えてみてください。GPUは、数値を処理する能力のために、さまざまな種類の暗号通貨のマイニングにも人気があります。そして、最後に、ニューラルネットワークのトレーニングと実行も、行列の乗算と並列計算の一連の作業に過ぎません。FPGAはそのファブリック内に数学ブロックを含んでいますが、これらの数値計算シーケンスをコンパイルするソフトウェアツールは、GPUのように最適化されていません(少なくともまだです)。

これら2つの理由から、私はずっとシンプルなモデル、アイリス分類モデルを選びました。これはビジョンベースのモデル(例:MNISTデータベース)ほど楽しいものではありませんが、はるかに直接的です。このモデルを訓練するために使用されるデータセットは、アイリスの花の3つの異なる種類:セトサ、バーシカラー、バージニカの150の観測値からなります。各観測値(すなわちデータセット)には、それらの花に固有のいくつかの測定値が含まれています。それらは次のとおりです:

  1. がくの長さ(cm)
  2. がくの幅(cm)
  3. 花びらの長さ(cm)
  4. 花びらの幅(cm)

これらの測定値を用いて、(かなり高い確率で)どの種類の花であるかを教えてくれるモデルを作成することができます。これら4つの入力を提供し、アイリスがセトサ、バーシカラー、またはバージニカのどのタイプであるかを教えてくれます。このデータセットの良い点は、これらの値の中にあまり重複がないことです。例えば、セトサ、バーシカラー、バージニカは、がくの長さがかなり異なります。他の3つの特性についても同様です。これは、データセットの散布図で示されています:

Pairwise Scatterplots of Iris Dataset

図1:アイリスデータセットのペアワイズ散布図

ニューラルネットワークを使用する理由について話したとき、ルールベースのプログラミングの概念について議論しました。つまり、コンピュータが流れることができる一連のルール(つまり、アルゴリズム)です。この場合、この問題をアルゴリズムとしてコード化することは確かに可能です。上記の散布図は、一連の数学的方程式によって表現される可能性があります。このデータセットを使用するのは、これから来る複雑さの一例としてのみです(MNISTデータベースを考えてみてください)。それでは、次のトピックへと進みます:実装。

FPGAを選ぶ理由は?

FPGAにこれを実装する準備をしている際に、「なぜFPGA上にニューラルネットワークを構築するのか?」と疑問に思うかもしれません。このアプリケーションにはGPUを使用する方が理にかなっていることは既に確立されています。ご存知かもしれませんが、GPUは高価で、多くの電力を必要とします。また、全容量で動作しているときには多くの熱を発生させます。アプリケーション特化型ハードウェア(またはチップ)を設計する際には、空間、電力、熱、コストを最適化します。アプリケーション特化型集積回路(ASIC)を設計する最初のステップは、まずFPGA上でそれを設計することです。ASIC上にニューラルネットワークを構築する計画がある場合、これが最初のステップになります。

どのようにして行うのか?

では、これを試してみることについに説得されました。モデルをトレーニングする方法の例を見てきましたが、モデルを簡素化すれば、FPGAに実装するのはかなり簡単だと思いますよね?違います。多くの行列や方程式のシリーズについて話したことを覚えていますか?それらの計算を小さなFPGAに詰め込もうとすると、非常に困難になる可能性があります。幸いなことに、Fast Machine Learning Lab(およびコミュニティ)の親切な人々がオープンソースプロジェクトhls4mlで取り組んだ、とても素晴らしいショートカットがあります。このプロジェクトは、既存のモデルを高レベル合成言語(例:SystemC)に変換し、その後、VerilogやVHDLなどの一般的に使用されるFPGA言語に変換できます(AMD XilinxのVivadoなどのFPGA合成ツールを使用して)。その翻訳自体が、膨大な量のステップを省略します。VerilogやVHDLだけで完全なニューラルネットワーク(特に複雑なもの)を構築しようとするのは非常に困難です。このツールは本当に私たちを直接的な部分に導いてくれますが、いくつかの中間ステップなしにはいきません。

最適化

私たちはモデルを訓練し終え、hls4mlを実行してFPGAでこれをテストしたいところです。しかし、そう急ぐことはありません。もしIrisデータセットを基本的な技術で訓練し、その後hls4mlでコードを合成した場合、合成はうまくいくかもしれません。しかし、配置と配線に進むと、そのモデルをそのまま小さなFPGAに収めることは決してできません。また、データ処理と通信に関するすべてのロジックもFPGA上に必要であることを忘れないでください。チュートリアルでは、FPGA上でモデルを実行するデモンストレーションとしてPynq-Z2ボードが使用されています。このボードはFPGAだけでなく、フルJupyter Notebookウェブサーバーを実行するチップ上のマイクロプロセッサ(Zynq 7000)も含んでいるため選ばれました。これは、FPGA上でハードウェアアクセラレーションされた機能を実行できるだけでなく、データをロードしてテストするためのシンプルで使いやすいインターフェースを体験できることを意味します。このインターフェースは、オペレーティングシステムとFPGAの間のトランスポート層として機能しますが、FPGA自体(SpartanやVirtexのFPGAを直接使用する予定だった場合と同様に)にもスペースを取ります。

上述の通りの課題は、FPGAはサイズが限られており、GPUと同じ容量を持っていないことです。その結果、完全なモデルをFPGAにダンプすることはできません - それは決して収まりません。シンプルなIrisモデルでさえ、最初はPynq-Z2に配置できませんでした。チュートリアル4では、最適化技術(その中には、私がまだ表面をかじっただけで、どのように機能するのかを完全に理解していないほど奥深いものもあります)についてより詳しく知ることになります。これらの最適化技術を適用すると、(少なくともよりシンプルな)モデルをFPGAに乗せることができるはずです。このリポジトリでは、BuildModel.py(特にモデルトレーニング関数)を見て、基本的なモデルをトレーニングするだけでなく、それを最適化していることがわかります:

def train_model(self):
    """
    QKerasを使用して、プルーニングされ量子化されたニューラルネットワークモデルを構築、トレーニング、保存します。

    この関数は、QKerasレイヤーを使用してシーケンシャルモデルを構築します。モデルは、パラメータの数を減らすためにプルーニングされ
    重みとアクティベーションの精度を下げるために量子化されます。トレーニングプロセスには、プルーニングステップを更新するためのコールバックの設定が含まれます。トレーニング後、プルーニングラッパーは剥がされ、最終モデルが保存されます。

    パラメータ:
    なし

    戻り値:
    なし
    """
    # シーケンシャルモデルを初期化
    self.model = Sequential()

    # 量子化とプルーニングを伴う最初のQDenseレイヤーを追加
    self.model.add(
        QDense(
            64,  # レイヤーのニューロン数
            input_shape=(4,),  # アイリスデータセットの入力形状(4つの特徴)
            name='fc1',
            kernel_quantizer=quantized_bits(6, 0, alpha=1),  # 重みを6ビットに量子化
            bias_quantizer=quantized_bits(6, 0, alpha=1),  # バイアスを6ビットに量子化
            kernel_initializer='lecun_uniform',
            kernel_regularizer=l1(0.0001),
        )
    )

    # 量子化ReLU活性化レイヤーを追加
    self.model.add(QActivation(activation=quantized_relu(6), name='relu1'))

    # 2番目のQDenseレイヤーを量子化とプルーニングで追加
    self.model.add(
        QDense(
            32,  # レイヤーのニューロン数
            name='fc2',
            kernel_quantizer=quantized_bits(6, 0, alpha=1),  # 重みを6ビットに量子化
            bias_quantizer=quantized_bits(6, 0, alpha=1),  # バイアスを6ビットに量子化
            kernel_initializer='lecun_uniform',
            kernel_regularizer=l1(0.0001),
        )
    )

    # 量子化ReLU活性化レイヤーを追加
    self.model.add(QActivation(activation=quantized_relu(6), name='relu2'))

    # 3番目のQDenseレイヤーを量子化およびプルーニングで追加
    self.model.add(
        QDense(
            32,  # レイヤーのニューロン数
            name='fc3',
            kernel_quantizer=quantized_bits(6, 0, alpha=1),  # 重みを6ビットに量子化
            bias_quantizer=quantized_bits(6, 0, alpha=1),  # バイアスを6ビットに量子化
            kernel_initializer='lecun_uniform',
            kernel_regularizer=l1(0.0001),
        )
    )

    # 量子化ReLU活性化レイヤーを追加
    self.model.add(QActivation(activation=quantized_relu(6), name='relu3'))

    # 量子化とプルーニングを備えた出力QDenseレイヤーを追加
    self.model.add(
        QDense(
            3,  # 出力層のニューロン数(アイリスデータセットのクラス数と一致)
            name='output',
            kernel_quantizer=quantized_bits(6, 0, alpha=1),  # 重みを6ビットに量子化
            bias_quantizer=quantized_bits(6, 0, alpha=1),  # バイアスを6ビットに量子化
            kernel_initializer='lecun_uniform',
            kernel_regularizer=l1(0.0001),
        )
    )

    # 分類のためのソフトマックス活性化レイヤーを追加
    self.model.add(Activation(activation='softmax', name='softmax'))

    # 重みを75%プルーニングするためのパラメータを設定し、2000ステップ後に開始して、100ステップごとに更新
    pruning_params = {"pruning_schedule": pruning_schedule.ConstantSparsity(0.75, begin_step=2000, frequency=100)}
    self.model = prune.prune_low_magnitude(self.model, **pruning_params)

    # Adamオプティマイザとカテゴリカルクロスエントロピー損失でモデルをコンパイル
    adam = Adam(lr=0.0001)
    self.model.compile(optimizer=adam, loss=['categorical_crossentropy'], metrics=['accuracy'])

    # トレーニング中にプルーニングステップを更新するためのプルーニングコールバックを設定
    callbacks = [
        pruning_callbacks.UpdatePruningStep(),
    ]

    # モデルを訓練する
    self.model.fit(
        self.X_train_val,
        self.y_train_val,
        batch_size=32,  # 訓練のバッチサイズ
        epochs=30,  # 訓練のエポック数
        validation_split=0.25,  # 訓練データのうち検証データとして使用する割合
        shuffle=True,  # 各エポック前に訓練データをシャッフルする
        callbacks=callbacks,  # プルーニングコールバックを含む
    )

    # モデルからプルーニングラッパーを剥がす
    self.model = strip_pruning(self.model)

 

ここには消化しなければならない情報が膨大にありますが、要点としては(トレーニングに加えて)、量子化、正則化、プルーニング、重みの初期化、そして(Adam)最適化を通じて、重みのサイズと複雑さを減らし、より効率的かつ高速にすることです。これらの方法のいくつかは、精度を向上させ、トレーニングプロセスがうまく機能することを保証するために使用されます。これらの技術をモデルに適用した後、FPGAコードに変換できるほど小さくなるはずです。これを行う方法はたくさんあります。これはチュートリアルで取り上げられたアプローチだったので、うまくいった方法に従いました。

合成

では、これでコンパイル、合成、配置と経路設定の準備が整いました。プロセスの最後には、FPGAがどのように動作するかを指示するフラッシュファイルとして機能するビットストリームファイルが得られるはずです。ZynqにはプロセッサとFPGAの両方があるため、Pythonを介してFPGAをプログラムするのはかなり簡単です(これについては後で説明します)。参照されたリポジトリを使用して、HLSを使用してモデルを定義およびコンパイルし、Pythonコードの数行ですべてのビットストリームファイルをビルドできます。次のコードでは、HLSで生成されたモデルが元のモデルと同じ(または十分に近い)精度を持っていることを確認するために、追加の検証を行います。

def build_bitstream(self):
    """
    訓練されたモデルのHLSビットストリームをビルドします。

    この関数は、訓練されたKerasモデルをhls4mlを使用してHLSモデルに変換し、それをコンパイルして、FPGAデプロイメントのためのビットストリームを生成します。
    また、HLSモデルを元のモデルと検証し、両モデルの精度を出力します。

    """
    # KerasモデルからHLS設定を作成し、レイヤー名の粒度で設定
    config = hls4ml.utils.config_from_keras_model(self.model, granularity='name')

    # softmaxレイヤーの精度を設定
    config['LayerName']['softmax']['exp_table_t'] = 'ap_fixed<18,8>'
    config['LayerName']['softmax']['inv_table_t'] = 'ap_fixed<18,4>'

    # 全結合層のReuseFactorを512に設定
    for layer in ['fc1', 'fc2', 'fc3', 'output']:
        config['LayerName'][layer]['ReuseFactor'] = 512

    # KerasモデルをHLSモデルに変換
    hls_model = hls4ml.converters.convert_from_keras_model(
        self.model, hls_config=config, output_dir='hls4ml_prj_pynq', backend='VivadoAccelerator', board='pynq-z2'
    )

    # HLSモデルをコンパイル
    hls_model.compile()

    # HLSモデルを使用して予測する
    y_hls = hls_model.predict(np.ascontiguousarray(self.X_test))
    np.save('package/y_hls.npy', y_hls)

    # HLSモデルを元のモデルと比較して検証する
    y_pred = self.model.predict(self.X_test)
    accuracy_original = accuracy_score(np.argmax(self.y_test, axis=1), np.argmax(y_pred, axis=1))
    accuracy_hls = accuracy_score(np.argmax(self.y_test, axis=1), np.argmax(y_hls, axis=1))

    print(f"元の剪定され量子化されたモデルの精度: {accuracy_original * 100:.2f}%")
    print(f"HLSモデルの精度: {accuracy_hls * 100:.2f}%")

    # HLSモデルを構築する
    hls_model.build(csim=False, export=True, bitfile=True)


ここで行われていることの価値を理解するのは難しいですが、Kerasモデルからビットストリームファイルに至るまでの作業量は計り知れません。これにより、平均的なジョーにとってもかなりアクセスしやすくなります(ただし、量子化、削減、最適化は決して簡単ではありませんでした)。

設定は、再び、チュートリアルから借用されたもので、再利用ファクターは、私たちがどれだけのロジックを再利用したいかに基づいて変更可能な数値です。その後は、hls4mlへの数回の呼び出しで、ビットファイルが手に入ります!

テスト

では、ビットファイルを手に入れたので、実際のFPGAでこれをテストしたいと思います。Pynq環境を使用すると、これが簡単になります。始めるためには、ビットファイル、テストスクリプト、テストベクトルをPynqボードにコピーする必要があります。これは、シンプルなSCPコマンドまたはWebインターフェースを通じて行うことができます。物事を簡単にするために(hls4mlチュートリアルで行われたように)、すべてを「package」と呼ばれる単一のフォルダにダンプしてから、SCPを介してターゲットデバイスにコピーします(詳細については、リポジトリのBuildModel.pyスクリプトを参照してください)。ここで重要なのは、axi_stream_driver.pyと呼ばれる追加の自動生成ライブラリです。このファイルには、ZynqのFPGA側をフラッシュするだけでなく、FPGAベースのニューラルネットワークモデルをテストするためのデータ転送を実行するヘルパー関数が含まれています。

ファイルをPynqボードに転送した後、SSHシェルをターゲット上で開くか、on_target.py内にあるコードを実行するための新しいノートブックを作成することができます。私はコマンドライン経由でスクリプトを実行する方を好みますが、root権限が必要になるので、デバイスにSSHで接続した後、最初にsudo -sを実行する必要があります。Pynqボード上でrootシェルが実行されている状態になったら、jupyter_notebooks/iris_model_on_fpgasに移動し、python3 on_target.pyコマンドでテストを実行します。すべてが正しく実行された場合、次の出力が表示されるはずです:

Expected output of on_targer.py script

図2:on_target.pyスクリプトの期待される出力

検証

では、すべてがうまくいったかどうかをどうやって知ることができるでしょうか?今、FPGAが生成した出力(つまり、予測)を検証し、GPU(またはCPU)を使用して作成したローカルモデルと比較する必要があります。簡単に言うと、入力として提供する各がく/花びらの長さ/幅に対して、出力としてセトサ、バーシカラー、またはバージニカのタイプのアイリスが期待されます(もちろんすべて数字で)。FPGAがローカルマシン上で実行されているモデルと同じくらい正確であることを期待しています。

スクリプトの出力を私たちのマシンにコピーする必要があります。SCPコマンドを再度実行するか、Jupyterノートブックのインターフェースからダウンロードすることができます。出力ファイルはy_hw.npyと呼ばれます。ファイルをコピーした後、utilities/validate_model.pyを実行する必要があります。出力は次のようになるはずです:

Results from validation script

図3:検証スクリプトからの結果

ご覧の通り、最適化されたモデル(PC上)と合成されたモデル(FPGA上)は、同じ精度レベル、96.67%を共有しています。全30のテストポイントのうち、たった1つを予測できなかったのは素晴らしいことです!

結論

この記事では、アイリス分類データセットを取り上げ、それを用いてニューラルネットワークモデルを作成しました。hls4mlを使用して、同じモデルをPynq-Z2ボード上で実行するためのビットファイルと必要なライブラリを構築しました。また、コンピューターベースのモデルとFPGAベースのモデルを比較し、それらが元のデータに対してどのように積み重なるかを検証するスクリプトを実行しました。モデルとデータセットは比較的単純でしたが、このチュートリアルはFPGA上で複雑なニューラルネットワークを設計することの基礎を築きました。

注意:このプロジェクトのすべてのコードは、このリポジトリ

にあります。

筆者について

筆者について

Ariは、設計、デバイスパッケージ、テスト、および電気、機械、およびソフトウェアシステムの統合において幅広い経験を持つエンジニアです。彼は、設計/デザイン、検証、テストのエンジニアをまとめて団結したグループとして機能させることに情熱を注いでいます。

関連リソース

関連する技術文書

ホームに戻る
Thank you, you are now subscribed to updates.