Trong Hiểu về Mạng Neural, chúng ta đã xem xét tổng quan về Mạng Neural Nhân Tạo và cố gắng đưa ra các ví dụ trong thế giới thực để hiểu chúng ở một cấp độ cao hơn. Trong bài viết này, chúng ta sẽ xem xét cách huấn luyện mạng neural và sau đó triển khai chúng lên một Mảng Cổng Lập Trình Được (FPGA) sử dụng một thư viện mã nguồn mở gọi là hls4ml.
Trái ngược với Hiểu về Mạng Neural, chúng ta sẽ xem xét một mô hình đơn giản hơn vì hai lý do chính. Đối với một số người, việc chuyển từ 28 x 28 pixel sang một mô hình đầy đủ là một bước nhảy vọt lớn. Mặc dù tôi đã giải thích sự phân chia như một chuỗi các hàm từng phần trong một ma trận đa chiều lớn, nhưng nó vẫn khó nắm bắt. Chúng ta thường nghe về “tham số”, nhưng trong các mô hình xử lý hình ảnh, chúng có thể trở nên trừu tượng và phức tạp. Chúng ta cũng muốn sử dụng một mô hình đơn giản hơn vì chúng ta sẽ tổng hợp nó lên phần cứng. Một Đơn vị Xử lý Đồ họa (GPU), ngay cả GPU tiêu chuẩn của máy tính để bàn tại nhà, có thể nhanh chóng tính toán và huấn luyện mô hình ngay lập tức. Nó được thiết kế đặc biệt cho nhiệm vụ đó (tức là tính toán số). Hãy nghĩ về bao nhiêu phép tính được thực hiện trong các trò chơi 3D với động cơ vật lý tinh vi và kỹ thuật render hình ảnh của chúng. GPU cũng đã trở nên phổ biến với việc khai thác các loại tiền điện tử khác nhau do khả năng tính toán số của nó. Và, cuối cùng, việc huấn luyện và chạy một mạng neural cũng chỉ là một chuỗi các phép nhân ma trận và tính toán song song. Mặc dù FPGA chứa các khối toán học trong cấu trúc của chúng nhưng các công cụ phần mềm để biên dịch những chuỗi tính toán số này không được tối ưu hóa như GPU (ít nhất là chưa).
Vì hai lý do này, tôi đã chọn một mô hình đơn giản hơn nhiều, mô hình phân loại Iris. Mặc dù nó không thú vị như một mô hình dựa trên hình ảnh (ví dụ: cơ sở dữ liệu MNIST), nhưng nó đơn giản hơn nhiều. Bộ dữ liệu được sử dụng để huấn luyện mô hình là một tập hợp 150 quan sát về ba loài hoa Iris khác nhau: Setosa, Versicolor và Virginica. Mỗi quan sát (tức là bộ dữ liệu) chứa một số phép đo đặc trưng cho những loài hoa đó. Chúng là:
Với những phép đo này, chúng ta có thể tạo ra một mô hình có thể cho chúng ta biết (với một mức độ chắc chắn khá cao) loài hoa nào đó. Chúng ta cung cấp bốn đầu vào đó và nó sẽ cho chúng ta biết liệu Iris có phải là loại Setosa, Versicolor, hay Virginica. Điều tốt về bộ dữ liệu này là không có nhiều sự chồng chéo giữa các giá trị này. Ví dụ, Setosa, Versicolo và Virginica đều có chiều dài đài hoa khá riêng biệt. Tương tự với ba đặc điểm khác. Điều này được thể hiện trong biểu đồ phân tán của bộ dữ liệu:
Hình 1: Biểu đồ phân tán từng cặp của Bộ dữ liệu Iris
Khi chúng ta nói về lý do tại sao sử dụng mạng nơ-ron trong Hiểu về Mạng Nơ-ron, chúng ta đã thảo luận về khái niệm lập trình dựa trên quy tắc: một tập hợp các quy tắc mà máy tính có thể thực hiện (tức là một thuật toán). Trong trường hợp này, chúng ta hoàn toàn có thể mã hóa vấn đề này dưới dạng một thuật toán. Các biểu đồ phân tán ở trên có thể được biểu diễn bởi một loạt các phương trình toán học. Chúng ta sử dụng bộ dữ liệu này như một ví dụ cho sự phức tạp sẽ xuất hiện (hãy nghĩ về cơ sở dữ liệu MNIST). Với điều đó, chúng ta chuyển sang chủ đề tiếp theo: Triển khai.
Khi chúng ta chuẩn bị triển khai điều này trên một FPGA, bạn có thể đang tự hỏi, "Tại sao lại xây dựng một mạng nơ-ron trên FPGA?" Chúng ta đã xác định rằng GPU chỉ đơn giản là hợp lý hơn cho ứng dụng này. Như bạn có thể biết hoặc không, GPU đắt tiền và yêu cầu nhiều năng lượng. Chúng cũng tạo ra rất nhiều nhiệt khi hoạt động ở công suất tối đa. Khi thiết kế phần cứng cụ thể cho ứng dụng (hoặc một chip), bạn tối ưu hóa cho không gian, năng lượng, nhiệt và chi phí. Bước đầu tiên để thiết kế một Mạch Tích Hợp Cụ Thể Ứng Dụng (ASIC) là trước tiên thiết kế nó trên một FPGA. Nếu chúng ta bao giờ có kế hoạch xây dựng mạng nơ-ron trên ASICs thì đây sẽ là bước đầu tiên.
Vậy là bây giờ chúng ta đã thực sự được thuyết phục để thử nghiệm điều này. Chúng ta đã thấy các ví dụ về cách huấn luyện một mô hình và chúng ta giả định rằng nếu chúng ta đơn giản hóa mô hình thì việc triển khai nó trên một FPGA sẽ khá đơn giản, phải không? Sai. Nhớ lại những gì chúng ta đã thảo luận về nhiều ma trận và chuỗi phương trình? Cố gắng nhồi nhét tất cả những tính toán đó vào một FPGA nhỏ có thể cực kỳ thách thức. May mắn thay, có một phím tắt thực sự tuyệt vời mà những người tốt bụng tại Phòng Thí Nghiệm Học Máy Nhanh (và cộng đồng) đã làm việc trên một dự án mã nguồn mở gọi là hls4ml. Dự án này chuyển đổi các mô hình hiện có thành một ngôn ngữ tổng hợp cấp cao (ví dụ: SystemC) sau đó có thể được chuyển đổi thành các ngôn ngữ FPGA thường được sử dụng như Verilog hoặc VHDL (sử dụng các công cụ tổng hợp FPGA như Vivado từ AMD Xilinx). Việc chuyển đổi đó, bản thân nó, loại bỏ một lượng lớn các bước. Cố gắng xây dựng một mạng nơ-ron hoàn chỉnh chỉ bằng Verilog hoặc VHDL (đặc biệt là một cái phức tạp) sẽ cực kỳ thách thức. Công cụ này thực sự giúp chúng ta tiếp cận trực tiếp với phần quan trọng - nhưng không phải không có một số bước trung gian.
Chúng ta đã đến lúc mà chúng ta đã huấn luyện mô hình của mình và muốn chạy hls4ml để thử nghiệm trên một FPGA. Không nhanh như vậy. Nếu bạn lấy bộ dữ liệu Iris, huấn luyện nó với các kỹ thuật cơ bản, và sau đó tổng hợp mã với hls4ml, bạn có thể vượt qua quá trình tổng hợp. Bây giờ hãy tiến tới bước đặt và định tuyến và bạn sẽ không bao giờ có thể phù hợp mô hình đó như nó đang có trên một FPGA nhỏ. Nhớ rằng, chúng ta cũng cần tất cả logic xung quanh việc xử lý dữ liệu và giao tiếp trên FPGA của mình nữa. Trong các hướng dẫn của họ, họ sử dụng một bảng mạch Pynq-Z2 để minh họa việc chạy một mô hình trên FPGA. Bảng mạch này được chọn vì nó không chỉ chứa một FPGA mà còn có một vi xử lý trên chip (Zynq 7000) chạy một máy chủ web Jupyter Notebook đầy đủ. Điều này có nghĩa là bạn có thể chạy các chức năng được tăng tốc phần cứng trên FPGA nhưng cũng trải nghiệm một giao diện đơn giản, dễ sử dụng để tải và kiểm tra dữ liệu của bạn. Giao diện này đóng vai trò như một lớp vận chuyển giữa hệ điều hành và FPGA vẫn chiếm không gian trên chính FPGA (giống như nếu chúng ta đã dự định sử dụng một FPGA thuần túy như Spartan hoặc Virtex FPGAs).
Thách thức, như đã đề cập ở trên, là FPGA có kích thước hạn chế và không có cùng khả năng như GPU. Kết quả là chúng ta không thể chứa một mô hình đầy đủ trên FPGA - nó sẽ không vừa. Ngay cả mô hình Iris đơn giản cũng không thể được đặt trên Pynq-Z2 ban đầu. Bằng Bài hướng dẫn 4 bạn bắt đầu trở nên quen thuộc hơn với các kỹ thuật tối ưu hóa (một số trong số đó rất esoteric đến mức tôi chưa thậm chí hiểu được chúng hoạt động như thế nào bên dưới). Một khi bạn áp dụng những kỹ thuật tối ưu hóa này, bạn nên có thể đưa mô hình (ít nhất là những mô hình đơn giản) lên FPGA. Trong kho lưu trữ này chúng ta có thể xem xét BuildModel.py (cụ thể là hàm huấn luyện mô hình) và nhận thấy rằng chúng ta không chỉ đang huấn luyện một mô hình cơ bản mà còn đang tối ưu hóa nó:
def train_model(self):
"""
Xây dựng, huấn luyện và lưu một mô hình mạng nơ-ron được cắt tỉa và lượng tử hóa sử dụng QKeras.
Chức năng này xây dựng một mô hình Tuần tự với các lớp QKeras. Mô hình được cắt tỉa để giảm số lượng tham số
và được lượng tử hóa để sử dụng độ chính xác thấp hơn cho trọng số và kích hoạt. Quá trình huấn luyện bao gồm thiết lập
các callback để cập nhật các bước cắt tỉa. Sau khi huấn luyện, các bộ bọc cắt tỉa được loại bỏ, và mô hình cuối cùng được lưu.
Tham số:
Không
Trả về:
Không
"""
# Khởi tạo một mô hình Tuần tự
self.model = Sequential()
# Thêm lớp QDense đầu tiên với lượng tử hóa và tỉa
self.model.add(
QDense(
64, # Số neuron trong lớp
input_shape=(4,), # Hình dạng đầu vào cho bộ dữ liệu Iris (4 đặc điểm)
name='fc1',
kernel_quantizer=quantized_bits(6, 0, alpha=1), # Lượng tử hóa trọng số thành 6 bit
bias_quantizer=quantized_bits(6, 0, alpha=1), # Lượng tử hóa bias thành 6 bit
kernel_initializer='lecun_uniform',
kernel_regularizer=l1(0.0001),
)
)
# Thêm một lớp kích hoạt ReLU lượng tử hóa
self.model.add(QActivation(activation=quantized_relu(6), name='relu1'))
# Thêm lớp QDense thứ hai với lượng tử hóa và tỉa
self.model.add(
QDense(
32, # Số neuron trong lớp
name='fc2',
kernel_quantizer=quantized_bits(6, 0, alpha=1), # Lượng tử hóa trọng số thành 6 bit
bias_quantizer=quantized_bits(6, 0, alpha=1), # Lượng tử hóa độ chệch thành 6 bit
kernel_initializer='lecun_uniform',
kernel_regularizer=l1(0.0001),
)
)
# Thêm một lớp kích hoạt ReLU lượng tử hóa
self.model.add(QActivation(activation=quantized_relu(6), name='relu2'))
# Thêm lớp QDense thứ ba với lượng tử hóa và tỉa
self.model.add(
QDense(
32, # Số nơ-ron trong lớp
tên='fc3',
kernel_quantizer=quantized_bits(6, 0, alpha=1), # Lượng tử hóa trọng số thành 6 bit
bias_quantizer=quantized_bits(6, 0, alpha=1), # Lượng tử hóa bias thành 6 bit
kernel_initializer='lecun_uniform',
kernel_regularizer=l1(0.0001),
)
)
# Thêm một lớp kích hoạt ReLU đã được lượng tử hóa
self.model.add(QActivation(activation=quantized_relu(6), tên='relu3'))
# Thêm lớp đầu ra QDense với lượng tử hóa và tỉa giảm
self.model.add(
QDense(
3, # Số nơ-ron trong lớp đầu ra (tương ứng với số lớp trong bộ dữ liệu Iris)
name='output',
kernel_quantizer=quantized_bits(6, 0, alpha=1), # Lượng tử hóa trọng số thành 6 bit
bias_quantizer=quantized_bits(6, 0, alpha=1), # Lượng tử hóa độ lệch thành 6 bit
kernel_initializer='lecun_uniform',
kernel_regularizer=l1(0.0001),
)
)
# Thêm một lớp kích hoạt softmax cho phân loại
self.model.add(Activation(activation='softmax', name='softmax'))
# Thiết lập các tham số tỉa để loại bỏ 75% trọng số, bắt đầu sau 2000 bước và cập nhật mỗi 100 bước
pruning_params = {"pruning_schedule": pruning_schedule.ConstantSparsity(0.75, begin_step=2000, frequency=100)}
self.model = prune.prune_low_magnitude(self.model, **pruning_params)
# Biên dịch mô hình với bộ tối ưu Adam và hàm mất mát categorical crossentropy
adam = Adam(lr=0.0001)
self.model.compile(optimizer=adam, loss=['categorical_crossentropy'], metrics=['accuracy'])
# Thiết lập callback tỉa để cập nhật các bước tỉa trong quá trình huấn luyện
callbacks = [
pruning_callbacks.UpdatePruningStep(),
]
# Huấn luyện mô hình
self.model.fit(
self.X_train_val,
self.y_train_val,
batch_size=32, # Kích thước lô cho quá trình huấn luyện
epochs=30, # Số lượng kỳ huấn luyện
validation_split=0.25, # Phần trăm dữ liệu huấn luyện được sử dụng làm dữ liệu kiểm định
shuffle=True, # Xáo trộn dữ liệu huấn luyện trước mỗi kỳ
callbacks=callbacks, # Bao gồm callback cắt tỉa
)
# Gỡ bỏ lớp bọc cắt tỉa khỏi mô hình
self.model = strip_pruning(self.model)
Có rất nhiều thông tin cần tiếp thu ở đây nhưng điểm chính của những gì đang xảy ra (ngoài việc đào tạo) là thông qua quá trình lượng tử hóa, chuẩn hóa, tỉa bớt, khởi tạo trọng số và tối ưu hóa (Adam), chúng ta đang giảm kích thước và độ phức tạp của các trọng số để làm cho nó hiệu quả và nhanh hơn. Một số phương pháp này được sử dụng để cải thiện độ chính xác và đảm bảo quá trình đào tạo diễn ra tốt. Một khi chúng ta đã chạy mô hình của mình qua những kỹ thuật này, nó nên đủ nhỏ để chuyển thành mã FPGA. Hãy nhớ, có rất nhiều cách để làm điều này. Đây là cách tiếp cận được hướng dẫn trong bài học nên tôi đã tuân theo những gì hiệu quả.
Vậy là bây giờ chúng ta đã sẵn sàng để biên dịch, tổng hợp, và đặt và định tuyến thiết kế của mình. Khi quá trình này kết thúc, chúng ta sẽ có một tệp bitstream mà, cơ bản, hoạt động như một tệp flash để hướng dẫn FPGA cách hoạt động. Do Zynq có cả bộ xử lý và FPGA nên việc lập trình FPGA qua Python khá dễ dàng (chúng ta sẽ nói về điều này sau). Sử dụng kho lưu trữ đã tham khảo, chúng ta có thể định nghĩa và biên dịch mô hình của mình sử dụng HLS và sau đó xây dựng tệp bitstream chỉ trong vài dòng mã Python. Trong đoạn mã sau, chúng ta thực hiện một số xác thực bổ sung để đảm bảo rằng mô hình HLS được tạo ra có độ chính xác giống (hoặc gần giống) với mô hình gốc.
def build_bitstream(self):
"""
Xây dựng bitstream HLS cho mô hình đã được huấn luyện.
Chức năng này chuyển đổi mô hình Keras đã được huấn luyện thành mô hình HLS sử dụng hls4ml, biên dịch nó, và tạo ra bitstream cho việc triển khai FPGA.
Nó cũng xác thực mô hình HLS so với mô hình gốc và in ra độ chính xác của cả hai mô hình.
"""
# Tạo cấu hình HLS từ mô hình Keras, với độ granular của tên lớp
config = hls4ml.utils.config_from_keras_model(self.model, granularity='name')
# Đặt độ chính xác cho lớp softmax
config['LayerName']['softmax']['exp_table_t'] = 'ap_fixed<18,8>'
config['LayerName']['softmax']['inv_table_t'] = 'ap_fixed<18,4>'
# Đặt ReuseFactor cho các lớp kết nối đầy đủ thành 512
for layer in ['fc1', 'fc2', 'fc3', 'output']:
config['LayerName'][layer]['ReuseFactor'] = 512
# Chuyển đổi mô hình Keras thành mô hình HLS
hls_model = hls4ml.converters.convert_from_keras_model(
self.model, hls_config=config, output_dir='hls4ml_prj_pynq', backend='VivadoAccelerator', board='pynq-z2'
)
# Biên dịch mô hình HLS
hls_model.compile()
# Dự đoán sử dụng mô hình HLS
y_hls = hls_model.predict(np.ascontiguousarray(self.X_test))
np.save('package/y_hls.npy', y_hls)
# Xác thực mô hình HLS so với mô hình gốc
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"Độ chính xác của mô hình gốc đã được cắt tỉa và lượng tử hóa: {accuracy_original * 100:.2f}%")
print(f"Độ chính xác của mô hình HLS: {accuracy_hls * 100:.2f}%")
# Xây dựng mô hình HLS
hls_model.build(csim=False, export=True, bitfile=True)
Khó có thể đánh giá hết công việc được thực hiện ở đây nhưng lượng công việc cần thiết để chuyển từ một mô hình Keras sang một tệp bitstream là không thể vượt qua. Điều này làm cho nó trở nên dễ tiếp cận hơn nhiều đối với người bình thường (mặc dù việc lượng tử hóa, giảm thiểu và tối ưu hóa không hề đơn giản chút nào).
Các cấu hình, một lần nữa, là thứ đã được mượn từ hướng dẫn và yếu tố tái sử dụng là một con số có thể thay đổi dựa trên việc chúng ta muốn tái sử dụng bao nhiêu logic. Sau đó chỉ cần vài lời gọi đến hls4ml và, viola, bạn có một tệp bit!
Vậy giờ đây, khi chúng ta đã có một bitfile, chúng ta muốn thử nghiệm nó trên FPGA thực tế. Sử dụng môi trường Pynq làm cho việc này trở nên dễ dàng hơn. Để bắt đầu, chúng ta phải chép bitfile, script kiểm tra, và các vector kiểm tra vào bảng Pynq. Việc này có thể được thực hiện bằng một lệnh SCP đơn giản hoặc qua giao diện web. Để làm cho mọi thứ trở nên dễ dàng hơn (như đã được thực hiện trong hướng dẫn hls4ml), chúng ta đổ tất cả vào một thư mục duy nhất có tên là “package” và sau đó chép nó qua thiết bị đích qua SCP (xem script BuildModel.py trong kho lưu trữ để biết thêm chi tiết). Điều quan trọng cần lưu ý ở đây là một thư viện tự động được tạo ra có tên axi_stream_driver.py. Tệp này chứa các hàm trợ giúp không chỉ để flash phần FPGA của Zynq mà còn thực hiện việc chuyển dữ liệu để kiểm tra mô hình mạng nơ-ron dựa trên FPGA.
Sau khi các tệp đã được chuyển sang bảng Pynq, chúng ta có thể mở một shell SSH trên mục tiêu hoặc tạo một sổ ghi chép mới để chạy mã số sống trong on_target.py. Tôi thích chạy script qua dòng lệnh nhưng nó sẽ yêu cầu quyền root nên bạn cần phải chạy sudo -s sau khi bạn đã SSH vào thiết bị của mình. Sau khi bạn đã có một shell root chạy trên bảng Pynq, bạn có thể điều hướng đến jupyter_notebooks/iris_model_on_fpgas và chạy thử nghiệm với lệnh python3 on_target.py. Nếu mọi thứ chạy đúng, bạn sẽ thấy kết quả đầu ra như sau:
Hình 2: Kết quả đầu ra mong đợi của script on_targer.py
Vậy làm thế nào chúng ta biết mọi thứ đã hoạt động? Bây giờ chúng ta cần phải xác nhận các đầu ra (tức là các dự đoán) mà FPGA tạo ra và so sánh nó với mô hình địa phương mà chúng ta đã tạo sử dụng GPU (hoặc CPU) của mình. Nói một cách dễ hiểu, cho mỗi chiều dài/chiều rộng của đài hoa/cánh hoa mà chúng ta cung cấp làm đầu vào, chúng ta đang mong đợi một loại Iris là Setosa, Versicolor, hoặc Virginica là đầu ra (tất nhiên là dưới dạng số). Chúng ta hy vọng rằng FPGA sẽ chính xác như mô hình chạy trên máy địa phương của chúng ta.
Chúng ta sẽ cần sao chép kết quả từ script trở lại máy của mình. Bạn có thể chạy lệnh SCP một lần nữa hoặc chỉ cần tải xuống qua giao diện sổ ghi chú Jupyter. Tệp đầu ra sẽ được gọi là y_hw.npy. Sau khi sao chép tệp, chúng ta sẽ cần chạy utilities/validate_model.py. Kết quả sẽ trông giống như thế này:
Hình 3: Kết quả từ script xác thực
Như bạn có thể thấy, mô hình được tối ưu hóa của chúng tôi (trên PC) và mô hình được tổng hợp (trên FPGA) đều chia sẻ cùng một mức độ chính xác: 96.67%. Trong tất cả 30 điểm kiểm tra, chúng tôi chỉ không dự đoán đúng một điểm duy nhất - tốt đấy!
Trong bài viết này, chúng tôi đã sử dụng bộ dữ liệu phân loại Iris và tạo ra một mô hình mạng nơ-ron từ nó. Sử dụng hls4ml, chúng tôi đã xây dựng một bitfile và các thư viện cần thiết để chạy mô hình tương tự trên bảng Pynq-Z2. Chúng tôi cũng đã chạy một script xác thực so sánh mô hình dựa trên máy tính với mô hình dựa trên FPGA và cách chúng đều so sánh với dữ liệu gốc. Mặc dù mô hình và bộ dữ liệu khá đơn giản, hướng dẫn này đã đặt nền móng cho việc thiết kế các mạng nơ-ron phức tạp trên FPGA là gì.
Lưu ý: Tất cả mã nguồn cho dự án này có thể được tìm thấy trong kho lưu trữ này.