文章来源:OpenFPGA
YoloV3 在FPGA上运行-量化、编译和推理
在Yolo这个复杂度级别的神经网络基本很少从零开始使用HDL搭建了,基本借助于FPGA厂商推出的AI工具链(AMD的Vitis AI/Intel的OpenVINO)可以快速搭建基于神经网络的应用。
今天搭建基于 Yolov3 在FPGA上运行对象检测的示例,侧重过程。
YoloV3 架构
YOLOv3(You Only Look Once Version 3)是一种先进的实时物体检测算法。旨在快速准确地检测图像或视频帧中的物体。YOLOv3 在其前身 YOLOv2 的成功基础上进行了改进,并引入了多项增强功能以提高检测准确性和性能。
YOLOv3 的一个显著改进是使用了特征金字塔网络 (FPN),这使得模型能够提取多个尺度的特征。通过结合高分辨率和低分辨率特征图,YOLOv3 可以有效地检测各种尺寸的物体。
YOLOv3 的另一个关键特性是集成了多尺度检测方法。YOLOv3 不是以固定分辨率处理图像,而是以三种不同的尺度进行操作。这使得模型能够准确检测小物体和大物体。
YOLOv3 还采用了一种更深的神经网络架构,即 Darknet-53。该架构基于残差连接,由 53 个卷积层组成。增加的深度使 YOLOv3 能够学习更复杂的表示并捕捉输入图像中的更精细的细节。
综上所述,YOLOv3 是一种最先进的物体检测算法,它结合了特征金字塔网络、多尺度检测和更深的神经网络架构,实现了高精度和实时性能。
要探索完整的 YOLOv3 模型代码,请参阅YoloV3 主模型提供的 GitHub 存储库。
使用 Vitis AI 3.0 量化 Yolov3 Pytorch
在 PyTorch 中量化 YOLOv3 涉及将模型的参数从浮点数转换为低精度定点数或整数表示,以减少内存和计算要求,同时保持可接受的精度。
我们将使用 Vitis AI 3.0 (GPU) 进行量化,也可以在 Vitis AI 的 CPU docker 上执行量化。GPU docker 需要在本地构建,而 CPU docker 可以直接拉取使用。在 GPU docker 上执行量化比 CPU Docker 更快。
量化的步骤
加载模型 量化→量化模型 前向传递 处理量化模型
要访问 YOLOv3 量化的代码库,请参阅以下 GitHub 存储库:
步骤 1:加载模型
加载浮点训练模型,可以对其进行量化。
state_dict = torch.load(config["pretrain_snapshot"])
model.load_state_dict(state_dict)
首先,代码片段从指定文件加载 PyTorch 模型的状态字典。加载状态字典后,此行代码将状态字典加载到 PyTorch 模型中。模型变量表示我们要将参数加载到的神经网络模型。通过调用 load_state_dict,将训练后的权重和参数从保存的状态字典传输到模型中。
步骤 2:量化(生成量化模型)
量化是使用Vitis AI 提供的pytorch_nndct库中的torch_quantizer函数执行的。
from pytorch_nndct.apis importtorch_quantizer
input = torch.randn([batch_size, 3, 416, 416])
quantizer = torch_quantizer(quant_mode, model, (input), device=device, quant_config_file=config_file, target=target)
quant_model = quantizer.quant_model
上面的代码片段创建了一个量化器对象的实例。通过将参数传递给pytorch_nndct库的torch_quantizer函数来创建量化器对象。
torch_quantizer函数接受以下参数:
quant_mode 可以是“calib”或“test”
模型是我们加载的浮动模型
输入是浮点模型所需的虚拟输入,具有 batch_size 个图像,每个图像具有 3 个颜色通道(RGB),每张图片的分辨率为高 416 像素、宽 416 像素
设备代表应执行量化的“cpu”或“cuda”。
quant_config_file 是包含量化设置和选项的配置文件。由于“calib”步骤使用默认配置文件,因此配置文件为空。在“test”步骤中,它使用从“calib”步骤导出的配置文件。
目标是量化模型所针对的平台或硬件。
创建量化器对象后,使用 quant_config_file 中指定的设置和选项对模型进行量化。量化后的模型存储在 quant_model 变量中,与原始模型相比,其权重和激活精度通常较低。
步骤 3:前向传递
前向传递是指将输入数据通过网络层传播以计算输出的过程。在此前向传递过程中,输入数据流经模型的各个层,进行各种数学运算(例如矩阵乘法、激活函数和池化),最终产生预测或输出。
# Forward -- Dry Run
input_data = torch.randn([batch_size, 3, 416, 416]).to(device)
quant_model(input_data)
如上面的代码所示,对 quant_model 进行前向传递时会进行试运行,这是一个至关重要的步骤,如果不执行则会引发错误。
来自 Ug1414 [ https://docs.xilinx.com/r/en-US/ug1414-vitis-ai/Error-Codes ]:
错误代码ID:QUANTIZER_TORCH_NO_FORWARD
错误消息:导出量化结果前必须调用 torch_quantizer.quant_model FORWARD 函数。请参阅
https://github.com/Xilinx/Vitis-AI/blob/master/src/Vitis-AI-Quantizer/va...
上的示例代码。
步骤 4:处理量化模型
量化结果基于两种不同的量化模式进行处理:校准(quant_mode == 'calib')和测试(quant_mode == 'test')。
# Handle quantization result
if quant_mode == 'calib':
quantizer.export_quant_config()
if quant_mode == 'test':
quantizer.export_torch_script(verbose=False)
quantizer.export_xmodel(deploy_check=True, dynamic_batch=True)
校准模式(quant_mode == 'calib'):在此模式下,脚本将配置量化器以执行模型校准并导出量化配置。模型校准是一个收集有关模型输入的统计数据(例如激活的最小值和最大值)的过程,稍后有效地量化模型。
quantizer.export_quant_config():此函数导出校准过程中获得的量化配置。此配置对于在部署期间正确量化模型至关重要。
测试模式(quant_mode == 'test'):在此模式下,脚本正在执行量化测试并以不同格式导出量化模型。此模式还可用于在将量化模型部署到生产环境之前验证其性能。
quantizer.export_torch_script(verbose=False):此函数将量化模型导出为 TorchScript iept 文件格式。TorchScript 是一种序列化 PyTorch 模型的方法,并且对于量化模型的推理必不可少,因为 VitisAI 仅支持加载 torch 脚本量化模型进行推理。
quantizer.export_xmodel(deploy_check=True, dynamic_batch=True):此函数导出量化模型“xmodel”格式。参数 deploy_check 和 dynamic_batch 表明此导出可能包括部署准备就绪检查和对动态批处理大小的支持。量化 xmodel 是生成给定目标的编译 xmodel 所必需的。
量化模型的推理
量化模型的推理涉及使用已量化的神经网络模型,通常从浮点精度到较低精度(例如定点或整数),目的是减少内存和计算要求,同时保持可接受的精度。
来自Ug1414:
可以在PyTorch框架中运行TorchScript格式的量化模型,即.pt文件。在推理之前必须导入pytorch_nndct模块,因为它设置了此模型中使用的量化运算符。
目前XIR格式的量化模型还不能被任何工具运行。
import torch
import pytorch_nndct
# Load the model
quantized_model = torch.jit.load('quantized_result/ModelMain_int.pt')
# Feed input data to quantized model and do inference
output = quantized_model(input)
上面的代码片段使用torch.jit.load加载量化的 torch 脚本模型。因此,这个量化模型可以进一步用于适当的输入推理。
对于 YoloV3,对图像执行的推理将以预处理后的图像作为量化模型的输入。
要访问使用 TorchScript 中的量化 YOLOv3 模型执行推理的代码库,请参阅以下 GitHub 存储库:
量化 YOLOv3 推理:示例图像上的对象检测:
编译量化模型
编译量化模型是指准备量化神经网络模型部署到目标平台或硬件的过程。量化模型是针对目标硬件或加速器进行编译的。这涉及调整模型充分利用硬件的功能,例如矢量化指令、硬件加速器或专用内存布局。编译后的模型集成到目标应用程序的推理管道中。
经过编译和集成后,量化模型即可部署到目标平台或设备上。它可用于高效地进行预测或计算,从而充分利用降低的精度和硬件优化。
对于 PyTorch,量化器 NNDCT 直接以 XIR 格式输出量化模型,即compiled.xmodel 格式的模型。
使用vai_c_xir编译量化模型:
vai_c_xir --xmodel /PATH/TO/quantized.xmodel --arch /PATH/TO/arch.json --output_dir /OUTPUTPATH --net_name netname
编译后的 YOLOv3 量化模型可从以下 GitHub 存储库下载:
除了编译的模型之外,还需要以下文件:
md5sum.txt:一个校验和文件,用于确保模型文件的完整性。 meta.json:包含有关模型的元数据和配置信息的 JSON 文件。
DPU 推理
DPU(Deep Learning Processing Unit,深度学习处理单元)推理是指在专用硬件加速器(称为 DPU)上运行深度学习模型的过程。与通用 CPU 或 GPU 相比,DPU 在推理任务方面具有显著的性能优势。它们旨在最大限度地提高吞吐量并最大限度地降低功耗,使其成为边缘设备和实时应用程序的理想选择。
在 DPU 推理期间,脚本会从其原始深度学习框架格式(例如 TensorFlow、PyTorch)转换为适合在目标硬件上部署的格式。此转换通常包括独立于框架的方法。
准备主板-Kria KV260:
使用 Vitis AI 3.0 预构建图像设置 KV260 板:
https://xilinx.github.io/Vitis-AI/3.0/html/docs/quickstart/mpsoc.html#setup-the-target
使用 vai_runtime 进行 DPU 推理:
在推理过程中,vai_runtime 库负责将编译的模型加载到 FPGA 或 SoC 上并执行该模型对输入数据进行 AI 推理。
DPU推理的步骤:
反序列化 xmodel 获取子图 创建 Runner 预处理输入 主要执行逻辑 输出后处理 可视化结果
要访问在 DPU 上使用编译的 YOLOv3 模型(XIR 格式)执行推理的代码库,请参考以下 GitHub 存储库:
步骤1:反序列化xmodel
反序列化 xmodel 是加载已序列化或以特定格式保存的机器学习模型的过程,通常用于推理或进一步训练。
g = xir.Graph.deserialize(argv[1])
代码片段从命令行参数“argv[1]”指定的文件中反序列化 DPU 图。
步骤2:获取子图
在深度学习或基于图的处理的背景下,获取子图会提取较大计算图的较小、独立的部分。
subgraphs = get_child_subgraph_dpu(g)
代码片段从图中提取 DPU 子图。
def get_child_subgraph_dpu(graph: "Graph") -> List["Subgraph"]:
assert graph is not None, "'graph' should not be None."
root_subgraph = graph.get_root_subgraph()
assert (root_subgraph is not None), "Failed to get root subgraph of input Graph object."
if root_subgraph.is_leaf:
return []
child_subgraphs = root_subgraph.toposort_child_subgraph()
assert child_subgraphs is not None and len(child_subgraphs) > 0
return [cs for cs in child_subgraphs
if cs.has_attr("device") and cs.get_attr("device").upper() == "DPU"]
提供的函数“get_child_subgraph_dpu”是一个 Python 函数,它将图形对象作为输入并返回代表输入图内的 DPU(深度学习处理器单元)内核的子图列表。
用代码片段解释上面的流程图:
步骤 3:创建 Runner
DPU 运行器通常用于 Xilinx 深度处理单元 (DPU),是一种软件组件,负责在 DPU 上执行机器学习模型或推理任务。DPU 运行器充当主机 CPU 和 DPU 硬件之间的接口。它处理模型加载、输入数据预处理和在 DPU 上执行模型等任务。
"""Creates DPU runner, associated with the DPU subgraph."""
dpu_runners = vart.Runner.create_runner(subgraphs[0], "run")
在代码片段中,使用 Vitis AI (vart) 库为 DPU 子图创建 DPU(深度学习处理单元)运行器。DPU 运行器允许在兼容的硬件加速器上执行 DPU 子图。
步骤 4:预处理输入
预处理为图像的进一步处理做好准备,例如将其输入机器学习模型或神经网络中以执行图像分类或对象检测等任务。
# Preprocessing
image_path = argv[2]
image = cv2.imread(image_path, cv2.IMREAD_COLOR)
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
image = cv2.resize(image, (config["img_w"], config["img_h"]),
interpolation=cv2.INTER_LINEAR)
image = image.astype(np.float32)
image /= 255.0
在给定的代码片段中,
图像路径从作为第三个命令行参数(argv[2])提供的图像文件中读取。
然后使用带有 cv2.IMREAD_COLOR 标志的 OpenCV 的 cv2.imread 函数从指定的 image_path 读取图像,该函数以彩色(3 个通道:BGR)加载图像。
将图像从BGR颜色空间(OpenCV常用)转换为RGB颜色空间。
然后使用 cv2.resize 函数将图像调整为 config["img_w"](宽度)和 config["img_h"](高度)指定的尺寸。它使用 cv2.INTER_LINEAR 插值方法进行调整大小,与其他方法相比,该方法通常会产生更平滑的结果。
图像数据类型转换为float32。
预处理的最后一步是将图像中的像素值除以 255.0,将每个像素值标准化为 [0, 1] 范围。
注意:对于 YoloV3,预处理后的图像为 (416, 416, 3),这意味着图像的高度和宽度为 416x416,具有 3 个通道 (RGB)。与浮动模型(GPU 上的推理)不同,形状不需要转置为 (3, 416, 416),并且严格避免,因为板推理使用编译的 xmodel,而这又需要具有形状 (416, 416, 3) 的输入。
步骤5:runYolo(主要执行逻辑)
输入张量/输出张量
# get the model input tensor
inputTensors = dpu_runner_tfYolo.get_input_tensors()
# get the model output tensor
outputTensors = dpu_runner_tfYolo.get_output_tensors()
代码片段使用 runner 提取输入张量和输出张量,这是准备输入和输出缓冲区所必需的,最终被执行。
例如,输入张量如下所示:
inputTensor[0]: {name: 'ModelMain__input_0_fix',
shape: [1, 416, 416, 3],
type: 'xint8',
attrs: {'location': 1,
'ddr_addr': 1264, 'bit_width': 8,
'round_mode': 'DPU_ROUND',
'reg_id': 2,
'fix_point': 4,
'if_signed': True}}
准备批量输入/输出
outputHeight_0 = outputTensors[0].dims[1]
outputWidth_0 = outputTensors[0].dims[2]
outputChannel_0 = outputTensors[0].dims[3]
outputHeight_1 = outputTensors[1].dims[1]
outputWidth_1 = outputTensors[1].dims[2]
outputChannel_1 = outputTensors[1].dims[3]
outputHeight_2 = outputTensors[2].dims[1]
outputWidth_2 = outputTensors[2].dims[2]
outputChannel_2 = outputTensors[2].dims[3]
runSize = 1
shapeIn = (runSize,) + tuple([inputTensors[0].dims[i] for i in range(inputTensors[0].ndim)][1:])
Yolov3 有 3 个输出,因此有 3 个输出张量。上面的代码片段提取了三个不同张量的高度、宽度和通道数:输出张量的 outputTensors[0]、outputTensors[1] 和 outputTensors[2]。对于输入张量,它构造了一个名为 shapeIn 的元组,其维度为 [batchSize、高度、宽度、通道]。
'''prepare batch input/output '''
outputData = []
inputData = []
outputData.append(np.empty((runSize,outputHeight_0,outputWidth_0,outputChannel_0), dtype = np.float32, order = 'C'))
outputData.append(np.empty((runSize,outputHeight_1,outputWidth_1,outputChannel_1), dtype = np.float32, order = 'C'))
outputData.append(np.empty((runSize,outputHeight_2,outputWidth_2,outputChannel_2), dtype = np.float32, order = 'C'))
inputData.append(np.empty((shapeIn), dtype = np.float32, order = 'C'))
在上面的代码片段中,runSize 是标量值,表示批处理大小。此代码为 outputData 和 inputData 创建空的 NumPy 数组,并将它们附加到相应的列表中。由于 Yolov3 有 3 个不同网格大小的输出,因此列表 outputData 为 3 个输出附加一个空数组,列表 inputData 为输入图像附加一个空数组。每个输入或输出的尺寸由 [batchSize、高度、宽度、通道] 给出。
异步执行
job_id =dpu_runner_tfYolo.execute_async(inputData,outputData)
dpu_runner_tfYolo.wait(job_id)
该代码片段使用 DPU 运行器异步执行 YOLO 模型。它以inputData作为输入,并预计产生存储在outputData中的结果。inputData包含模型的输入数据,outputData是模型输出的存储位置。
dpu_runner_tfYolo.execute_async(inputData, outputData)的结果被赋值给 job_id。这个 job_id 代表模型本次特定执行的唯一标识符。
dpu_runner_tfYolo.wait(job_id)等待指定job_id的YOLO模型执行完成。
步骤 6:对输出进行后处理
非最大抑制 (NMS)是一种常用于物体检测任务(包括 YOLO)的后处理算法,用于过滤掉冗余和重叠的边界框预测。NMS 的目标是选择最相关和最准确的边界框,同时抑制冗余检测。
输入:
一组边界框及其相应的置信度分数。
算法:
根据边界框的置信度得分按降序排列。 初始化一个空列表来存储选定的边界框。 当排序列表中仍有边界框时:
选择置信度得分最高的边界框(顶部框)。
将顶部框添加到选定边界框的列表中。
对于排序列表中的每个剩余边界框:
计算与顶部框的并集交点 (IoU)。
如果 IoU 高于预定义阈值(例如 0.5),则丢弃边界框,因为它与顶部框有明显重叠。
如果 IoU 低于阈值,则将边界框保留为单独检测。
输出:
已通过NMS的选定边界框列表。
并集交点 (IoU):
并集交比 (IoU) 是一种常用于评估两个边界框或感兴趣区域 (ROI) 之间重叠程度的指标。它测量预测边界框和真实边界框之间的空间一致性。IoU 广泛用于对象检测任务,包括非最大抑制 (NMS) 和评估对象检测模型的性能。
IoU 计算为两个边界框的交集面积与并集面积之比。计算 IoU 的公式如下:
IoU = (交集面积)/(并集面积)
认为,
预测值:(x,y,w,h) = (0.338,0.4667, 0.184, 0.106)
实际值:(x,y,w,h) = (0.546, 0.481, 0.136, 0.130)
角点计算公式:
x1 = x - w/2
x2 = x + w/2
y1 = y - h/2
y2 = y + h/2
然后,
预测(x1,y1,x2,y2)=(0.246,0.4137,0.43,0.5197)
实际(x1,y1,x2,y2)=(0.478,0.416,0.614,0.546)
计算交点的公式:
交点 = (x2-x1) * (y2-y1)
然后,
x1=max(0.246,0.478)=0.478 x2 = max(0.43, 0.614) = 0.614
y1=max(0.4137,0.416)=0.416 y2 = max(0.5197, 0.546) = 0.546
因此,交点 = 0.01768
面积计算公式:
Box_area = abs((x2-x1) * (y2-y1))
所以,
预测面积 = 0.019504
实际面积 = 0.01768
IOU计算公式:
IoU = 交点/(预测面积 + 实际面积 - 交点)
因此,
IoU = 0.9064
步骤 7:可视化结果
for x1, y1, x2, y2, conf, cls_conf, cls_pred in detections:
color = bbox_colors[int(cls_pred)]
# Rescale coordinates to original dimensions
ori_h, ori_w, _ = im.shape
pre_h, pre_w = config["img_h"], config["img_w"]
box_h = ((y2 - y1) / pre_h) * ori_h
box_w = ((x2 - x1) / pre_w) * ori_w
y1 = (y1 / pre_h) * ori_h
x1 = (x1 / pre_w) * ori_w
# Create a Rectangle patch
cv2.rectangle(im, (int(x1), int(y1)), (int(x1 + box_w),
int(y1 + box_h)), color, 2)
# Add label
label = classes[int(cls_pred)]
cv2.putText(im, label, (int(x1), int(y1) - 10),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2)
检测是YoloV3模型在给定图像中检测到的物体。
x1、y1、x2、y2、conf、cls_conf 和 cls_pred 从检测中解包。这些变量表示边界框的坐标、置信度分数和检测到的对象的类别预测。
根据 cls_pred(类别预测)分配颜色。
重新调整边界框的坐标 (x1, y1, x2, y2) 以匹配图像的原始尺寸 (ori_h, ori_w)。这是必要的,因为对象检测通常在调整大小的图像上执行,并且这些坐标需要调整为原始图像大小。
使用 cv2.rectangle 在图像上绘制一个矩形。此矩形表示检测到的边界框。color 变量决定矩形的颜色。
使用 cv2.putText 向图像添加标签。此标签表示检测到的对象的类别。标签放置在检测到的对象的边界框正上方,颜色变量决定标签的颜色。
FPGA 板的 DPU 推理结果:示例图像上的对象检测
Git Repo:
尽管我们讨论的是 Yolov7 和 Yolov8,但了解“量化、编译和 DPU 板推理”的完整流程有助于将较新的 Yolo 或其他 CNN 移植到带有 Vitis AI 的 FPGA 中。
项目链接