FPGA에서 신경망 구축

Ari Mahpour
|  작성 날짜: 팔월 8, 2024  |  업데이트 날짜: 시월 9, 2024
FPGA에서 신경망 구축

신경망 이해하기에서 우리는 인공 신경망의 개요를 살펴보고, 이를 더 높은 수준에서 이해하기 위해 실제 세계의 예를 들어 보았습니다. 이 글에서는 신경망을 어떻게 훈련시키고, 그 다음에 hls4ml이라는 오픈 소스 라이브러리를 사용하여 필드 프로그래머블 게이트 어레이(FPGA)에 배포하는 방법을 살펴볼 것입니다.

모델

신경망 이해하기와 대조적으로, 우리는 두 가지 주요 이유로 훨씬 더 간단한 모델을 살펴볼 것입니다. 일부에게는 28 x 28 픽셀에서 전체 모델로 넘어가는 것이 큰 도약이었습니다. 비록 저는 그것을 큰 다차원 행렬에서의 조각별 함수의 연속으로 설명했지만, 여전히 이해하기 어려웠습니다. 우리는 종종 "매개변수"에 대해 듣지만, 이미지 처리 모델에서는 그것들이 추상적이고 복잡해 보일 수 있습니다. 또한, 우리는 하드웨어에 그것을 합성하기 때문에 더 간단한 모델을 사용하고자 합니다. 그래픽 처리 장치(GPU), 심지어 표준 가정용 데스크탑 GPU조차도, 모델을 신속하게 계산하고 현장에서 훈련시킬 수 있습니다. 그것은 특히 그 작업(즉, 숫자 계산)을 위해 설계되었습니다. 정교한 물리 엔진과 이미지 렌더링이 포함된 3D 게임에 얼마나 많은 계산이 들어가는지 생각해 보십시오. GPU는 또한 숫자를 계산하는 능력 때문에 다양한 유형의 암호화폐 채굴에도 인기가 있습니다. 그리고 마지막으로, 신경망을 훈련시키고 실행하는 것도 행렬 곱셈과 병렬 컴퓨팅의 연속일 뿐입니다. FPGA는 그 구조 내에 수학 블록을 포함하고 있지만, 이러한 숫자 계산 시퀀스를 컴파일하는 소프트웨어 도구는 GPU처럼 최적화되어 있지 않습니다(적어도 아직은 아닙니다).

이 두 가지 이유로 저는 훨씬 간단한 모델인 아이리스 분류 모델을 선택했습니다. 비전 기반 모델(예: MNIST 데이터베이스)만큼 재미있지는 않지만 훨씬 더 직관적입니다. 이 모델을 훈련시키는 데 사용된 데이터셋은 세토사(Setosa), 버시컬러(Versicolor), 버지니카(Virginica)라는 세 가지 다른 종류의 아이리스 꽃에 대한 150개의 관찰 결과입니다. 각 관찰 결과(즉, 데이터셋)는 해당 꽃에 고유한 여러 측정값을 포함하고 있습니다. 그것들은 다음과 같습니다:

  1. 꽃받침 길이(cm)
  2. 꽃받침 너비(cm)
  3. 꽃잎 길이(cm)
  4. 꽃잎 너비(cm)

이러한 측정값을 통해 우리는 어떤 종류의 꽃인지 (상당히 높은 수준의 확실성으로) 알려줄 수 있는 모델을 만들 수 있습니다. 우리는 그 네 가지 입력값을 제공하고, 그것이 세토사, 버시컬러, 또는 버지니카 종류의 아이리스인지 알려줍니다. 이 데이터셋의 좋은 점은 이러한 값들 사이에 큰 겹침이 없다는 것입니다. 예를 들어, 세토사, 버시컬러, 버지니카는 모두 꽃받침 길이가 상당히 구별됩니다. 다른 3가지 특성에 대해서도 마찬가지입니다. 이는 데이터셋의 산점도에서 보여집니다:

Pairwise Scatterplots of Iris Dataset

그림 1: 아이리스 데이터셋의 쌍별 산점도

신경망을 이해하기에서 신경망을 사용하는 이유에 대해 이야기할 때, 컴퓨터가 따를 수 있는 일련의 규칙(즉, 알고리즘)인 규칙 기반 프로그래밍 개념을 논의했습니다. 이 경우, 이 문제를 알고리즘으로 코딩할 수 있습니다. 위의 산점도는 아마도 일련의 수학적 방정식으로 표현될 수 있을 것입니다. 우리는 이 데이터 세트를 더 복잡해질 것(예를 들어 MNIST 데이터베이스 생각하기)에 대한 예로 사용합니다. 이로써 우리는 다음 주제로 넘어갑니다: 구현.

FPGA를 사용하는 이유는?

FPGA에 이를 구현하기 위해 준비하면서, 아마도 "왜 FPGA에 신경망을 구축해야 하는가?"라고 물어볼 것입니다. 이 애플리케이션에는 GPU가 훨씬 더 적합하다는 것을 이미 확인했습니다. 아시다시피, GPU는 비싸며 많은 전력을 요구합니다. 또한, 전체 용량으로 작동할 때 많은 열을 발생시킵니다. 애플리케이션 특정 하드웨어(또는 칩)를 설계할 때, 공간, 전력, 열, 비용을 최적화합니다. 애플리케이션 특정 집적 회로(ASIC)를 설계하는 첫 단계는 먼저 FPGA에서 설계하는 것입니다. ASIC에서 신경망을 구축할 계획이라면 이것이 첫 단계가 될 것입니다.

어떻게 해야 할까요?

이제 우리는 이것을 시도해 볼 준비가 되었습니다. 모델을 훈련하는 방법에 대한 예시를 보았고, 모델을 단순화하면 FPGA에 구현하는 것이 꽤 간단할 것이라고 가정했죠? 그렇지 않습니다. 많은 행렬과 방정식 시리즈에 대해 이야기한 것을 기억하나요? 모든 계산을 작은 FPGA에 넣으려고 시도하는 것은 매우 도전적일 수 있습니다. 다행히도, Fast Machine Learning Lab (그리고 커뮤니티)에서 작업한 오픈 소스 프로젝트인 hls4ml이라는 정말 좋은 지름길이 있습니다. 이 프로젝트는 기존 모델을 고급 합성 언어(예: SystemC)로 변환한 다음, Vivado와 같은 FPGA 합성 도구를 사용하여 Verilog나 VHDL과 같은 일반적으로 사용되는 FPGA 언어로 변환할 수 있습니다. 이 변환 자체만으로도 엄청난 양의 단계를 줄일 수 있습니다. Verilog나 VHDL만을 사용하여 완전한 신경망(특히 복잡한 것)을 구축하려고 시도하는 것은 매우 도전적일 것입니다. 이 도구는 우리가 핵심으로 바로 가도록 도와주지만, 몇 가지 중간 단계 없이는 안 됩니다.

최적화

우리는 모델을 훈련시키고 이제 hls4ml을 실행하여 FPGA에서 이를 테스트해보고자 하는 지점에 도달했습니다. 하지만 그렇게 빠르게 진행할 수는 없습니다. Iris 데이터셋을 기본 기술로 훈련시키고 hls4ml로 코드를 합성한다면, 아마 합성 과정은 통과할 수 있을 겁니다. 이제 배치 및 라우팅으로 넘어가면, 그대로의 모델을 작은 FPGA에 맞출 수 없을 것입니다. 우리는 FPGA에서 데이터 처리와 통신을 둘러싼 모든 로직도 필요로 한다는 것을 기억하세요. 그들의 튜토리얼에서, 그들은 FPGA에서 모델을 실행하는 것을 보여주기 위해 Pynq-Z2 보드를 사용합니다. 이 보드는 FPGA뿐만 아니라 칩(Zynq 7000)에 마이크로프로세서도 포함하고 있어 선택되었습니다. 이는 전체 Jupyter Notebook 웹 서버를 실행합니다. 이는 FPGA에서 하드웨어 가속 기능을 실행할 수 있을 뿐만 아니라 데이터를 로드하고 테스트하기 위한 간단하고 사용하기 쉬운 인터페이스를 경험할 수 있음을 의미합니다. 운영 체제와 FPGA 사이의 전송 계층으로 작동하는 이 인터페이스는 FPGA 자체에 공간을 차지합니다(마치 우리가 Spartan이나 Virtex FPGA와 같은 순수 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,),  # Iris 데이터셋을 위한 입력 형태 (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'))

    # 두 번째 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'))

    # 세 번째 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 호출을 하고, 바이올라, 비트파일을 갖게 됩니다!

테스팅

이제 bitfile을 가지고 있으니 실제 FPGA에서 이를 테스트해 보고 싶습니다. Pynq 환경을 사용하면 이 작업이 더 쉬워집니다. 시작하려면 bitfile, 테스트 스크립트, 그리고 테스트 벡터를 Pynq 보드로 복사해야 합니다. 이는 간단한 SCP 명령이나 웹 인터페이스를 통해 수행할 수 있습니다. 일을 더 쉽게 하기 위해( hls4ml 튜토리얼에서 수행한 것처럼) 모든 것을 "package"라는 단일 폴더에 넣은 다음 SCP를 통해 대상 장치로 복사합니다(자세한 내용은 저장소의 BuildModel.py 스크립트를 참조하세요). 여기서 중요하게 주목해야 할 것은 axi_stream_driver.py라는 추가적으로 생성된 라이브러리입니다. 이 파일은 Zynq의 FPGA 측을 플래시하는 것뿐만 아니라 FPGA 기반 신경망 모델을 테스트하기 위한 데이터 전송을 수행하는 도우미 함수를 포함하고 있습니다.

파일들이 Pynq 보드로 전송되었으면, 대상에서 SSH 셸을 열거나 새 노트북을 생성하여 on_target.py 내에 있는 코드를 실행할 수 있습니다. 저는 명령 줄을 통해 스크립트를 실행하는 것을 선호하지만, 루트 권한이 필요하므로 장치에 SSH로 접속한 후에는 sudo -s를 먼저 실행해야 합니다. Pynq 보드에서 루트 셸이 실행되면 jupyter_notebooks/iris_model_on_fpgas로 이동하여 python3 on_target.py 명령으로 테스트를 실행할 수 있습니다. 모든 것이 올바르게 실행되었다면 다음과 같은 출력을 볼 수 있어야 합니다:

Expected output of on_targer.py script

그림 2: on_target.py 스크립트의 예상 출력

검증

그렇다면 모든 것이 제대로 작동했는지 어떻게 알 수 있을까요? 이제 FPGA가 생성한 출력(즉, 예측)을 검증하고 이를 우리가 GPU(또는 CPU)를 사용하여 생성한 로컬 모델과 비교해야 합니다. 평범한 말로, 입력으로 제공하는 각각의 꽃받침/꽃잎 길이/너비에 대해, 우리는 결과로 Setosa, Versicolor, 또는 Virginica 타입의 아이리스를 숫자로 예상하고 있습니다(물론 모두 숫자로). 우리는 FPGA가 로컬 기계에서 실행되는 모델만큼 정확하기를 바랍니다.

스크립트의 출력을 우리 기계로 복사해야 합니다. SCP 명령어를 다시 실행하거나 Jupyter 노트북 인터페이스를 통해 다운로드할 수 있습니다. 출력 파일은 y_hw.npy라고 불립니다. 파일을 복사한 후에는 utilities/validate_model.py를 실행해야 합니다. 출력은 다음과 같아야 합니다:

Results from validation script

그림 3: 검증 스크립트의 결과

보시다시피, 최적화된 모델(PC에서)과 합성된 모델(FPGA에서) 모두 동일한 정확도 수준을 공유합니다: 96.67%. 30개의 테스트 포인트 중 단 하나만 예측에 실패했습니다 - 멋지네요!

결론

이 글에서는 Iris 분류 데이터셋을 사용하여 신경망 모델을 만들었습니다. hls4ml을 사용하여 비트파일과 Pynq-Z2 보드에서 동일한 모델을 실행하기 위한 필요한 라이브러리를 구축했습니다. 또한 컴퓨터 기반 모델과 FPGA 기반 모델을 비교하고 원본 데이터에 대해 어떻게 쌓였는지를 비교하는 검증 스크립트를 실행했습니다. 모델과 데이터셋이 비교적 단순했음에도 불구하고, 이 튜토리얼은 FPGA에서 복잡한 신경망을 설계하는 것이 무엇인지에 대한 기초를 마련했습니다.

참고: 이 프로젝트의 모든 코드는 이 저장소에서 찾을 수 있습니다.

작성자 정보

작성자 정보

Ari is an engineer with broad experience in designing, manufacturing, testing, and integrating electrical, mechanical, and software systems. He is passionate about bringing design, verification, and test engineers together to work as a cohesive unit.

관련 자료

관련 기술 문서

홈으로 돌아가기
Thank you, you are now subscribed to updates.