尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

FPGA加速CNN卷积计算:从硬件架构到实时图像处理实战

FPGA加速CNN卷积计算:从硬件架构到实时图像处理实战 做实时图像处理的人很多都纠结过一个问题手里的视频流已经进来了明明白白要跑一个CNN做检测或分类GPU功耗压不住DSP又不太擅长这种大量乘加并行计算那还有什么选择FPGA。我最初接触FPGA上跑CNN也是在项目里被逼着做的——摄像头输出的1080p30图像流要在几毫秒内算完一个轻量级卷积网络同时功耗还不能超过几瓦。折腾了几个月后我确认了一件事用FPGA做CNN卷积加速不是噱头只要结构选对、量化做好、数据通路打通完全能实打实地做到实时。这篇博文就把整个过程拆开讲覆盖从平台选型、卷积计算单元设计、行缓存与数据复用到与ARM端协同工作的完整链路最后附上我踩过的几个真坑。适合手里有FPGA开发板、想做图像AI加速的人参考。1. 整体设计与思路拆解1.1 为什么是FPGA而不是GPU或DSP先把结论放在前面FPGA做CNN本质上是“用空间换时间”。GPU的核心优势是海量线程并行但代价是高功耗和大散热很多嵌入式视觉设备根本塞不进去DSP擅长流水线很强的信号处理但面对CNN这种规则却又极度重复的乘加操作主频再高也显得吃力。FPGA则完全不同它允许你把卷积计算直接“铺”成硬件用可编程逻辑搭出一条专用的管线每个时钟周期都能往里塞数据完成一次完整的乘加运算。举个例子你就明白差异了。如果跑一个3x3卷积GPU的方案是调度成千上万个线程去各算各的像素FPGA的方案是在逻辑里生成一个3x3的滑动窗口每来一个像素窗口里的9个数据同时进入9个乘法器下一拍相加输出一个结果。这个“每拍出一个结果”的能力是FPGA做实时图像CNN最值钱的地方。当然我不是说FPGA万能。如果今天跑的是ResNet152这种上百层的重网络或者需要快速迭代算法结构GPU依然更适合。FPGA适合的是模型固定、通道数和层数可控、对延迟和功耗有硬性要求的场景比如工业检测、医疗内窥镜、无人小车等。1.2 系统级架构从哪里来算完了往哪去做FPGA图像CNN最大的误区是把注意力全放在卷积计算上忽略了数据怎么进出。实际上一个可用的实时系统至少包含三层第一层是图像采集与预处理。视频流可以来自MIPI摄像头、HDMI输入或者ARM端内存里的图像帧。这一层决定了原始图像格式一般是RGB888或灰度图也可能是RAW数据。你需要先把图像转成适合CNN输入的格式还要做归一化、缩放等操作这些工作在FPGA里通常用简单的移位器和LUT就能完成代价很低。第二层是CNN加速器本体。它负责一堆卷积层、池化层、激活函数这块是整个系统的核心需要仔细设计。第三层是结果后处理与输出。分类任务输出的是分类向量检测任务还要接非极大值抑制或边界框解码这一层可以放在FPGA上硬实现也可以把特征图搬回ARM核用软件算。我更推荐前者因为一旦特征图搬回ARM又会引入DMA传输延迟和带宽占用问题。我实际项目里最常用的是Zynq平台也就是ARM核加FPGA逻辑一体的芯片。ARM核做控制和调度FPGA做卷积加速两者之间用AXI总线通信。这样做的好处是既能用Linux或裸机环境做图像采集和网络管理又能保证卷积部分的实时性。1.3 为什么选择先跑通单层卷积第一次上手不要急着在FPGA里塞一堆层。我的经验是先把“单层卷积池化激活”整条链路跑通然后用这一套结构去堆叠多层网络。理由很简单CNN各层之间除了通道数和特征图尺寸不一样计算逻辑完全一致。你把一个3x3卷积核的流水线调通了后面做16层32层只是资源复用和拼接问题不会再出现方向性错误。因此下面这篇博文里的核心流程都聚焦在“如何让一个可配置的卷积计算单元稳定、高效、实时地工作”而不是教你怎么把某个特定模型硬塞进FPGA。通用性这件事比跑通某个模型重要得多。2. 核心细节解析与实操要点2.1 卷积计算的本质与硬件化抽象说到卷积很多人第一反应是“滑动窗口加权求和”。这没错但在硬件上要把它拆得更细。一次3x3卷积实际上做的就是把特征图上的一个局部3x3邻域与卷积核的9个系数分别相乘然后累加再加上偏置最后经过激活函数得到输出特征图的一个像素。如果输入是三通道彩色图那每一个输出像素需要计算3个通道的3x3窗口也就是3x3x327次乘法和26次加法。如果网络是16通道的特征图再接着卷计算量成倍增加。硬件化抽象之后这些操作可以用乘累加单元MAC来执行FPGA里面一般用DSP48E1硬核资源来实现一块Zynq-7020就有220个DSP单元可以轻松组建多个并行MAC。一个关键认知是卷积在硬件里不是一个“像素一个像素地算”而是一整条流水线在跑。输入像素流进来经过延迟对齐、行缓存、窗口生成、并行MAC、激活输出像素流出去。每一级之间用valid信号控制数据有效性整条链路没有握手停顿才能做到“每拍出一个结果”。2.2 数据量化的选择全精度还是不归路很多人一开始会天真地想FPGA反正精度高直接用float不就行了实际工程里只有极少数场景会这么做。float32的乘法器用FPGA里的DSP资源一次只能做一个而且需要大量的LUT做浮点运算延迟和面积都不可接受。工业界最常见的做法是固定点数或整型量化。我对模型做量化的通常路径是先用软件平台训练好浮点模型然后做离线校准把权重和激活值量化到INT8。为了验证量化误差我在软件开发板上用Python脚本对比了量化前后模型的输出向量通常余弦相似度能做到0.99以上对分类和检测任务几乎没有可见损失。如果模型更小比如只需要几路二值化特征我甚至会直接量化到INT4甚至1-bit进一步压缩带宽和计算量。选择INT8的关键原因有三个一是8位乘法在Xilinx FPGA里可以直接用DSP48E1的17x25乘法器进行单个DSP就能完成二是8位存到BRAM里密度很高一张512x512x16的特征图用BRAM完全可以存下三是图像本身大多数来自8位传感器线性映射到INT8几乎不损失原始信息。2.3 行缓存与滑动窗口为什么要三个FIFO3x3卷积要建立“当前像素”和“周围像素”之间的关系而图像数据是逐行扫描进来的一个像素进来的时候它的上一行数据已经跑过去了。解决办法就是行缓存。最简单的方式是用FPGA里的移位寄存器或者三块BRAM把最近三行数据缓存下来当第三行当前像素到达时三行缓存里恰好就有3x3窗口的9个像素。这个过程中最容易被忽略的点是“等待时间”。第一个输出像素不是图像进来就有的而是要等前两行全部缓存完、第三行第三个像素到达时才产生第一个有效输出。这就意味着从图像首像素进来到第一行输出硬件上会有大约“两行像素宽度”的周期延迟。虽然听起来挺多但对视频流来说这个延迟固定且可控完全不影响实时性。我见过很多新手在这里犯错他们让行缓存FIFO里的数据“边读边丢”结果窗口错位算出来的特征图像“被撕裂”一样错乱。正确的做法是用valid信号同步行有效期间持续向三行缓存写入行场消隐期间保持复位状态窗口永远只从三行缓存对应的同一列位置取值。2.4 数据复用少读一遍能快多少CNN的硬件实现里有一个隐藏瓶颈就是“数据搬运”。如果窗口每次移动一格就重新去读一遍内存你会很快把带宽打满。实际工程标准做法称之为“行缓存滑动窗口”的数据复用机制图像数据进入片上缓存后被多个相邻窗口反复使用而不是从DDR里反复读。举个具体数字。假设输入特征图是1920x10803x3卷积。不用数据复用的话每个输出像素要读9个数总读取量是1920x1080x9约1860万次用了滑动窗口之后每个输入像素只需要从外部读一次后续8个相邻窗口都直接从缓存里取外部读取量只剩1920x1080约207万次不到原来的九分之一。这个优化一旦在硬件里落地DDR带宽的压力直线下降。2.5 工具链与开发环境准备要动手做这个项目开发环境和工具有点讲究。我建议先准备好下面这些FPGA开发板首推Xilinx Zynq-7000系列理由很直接ARMFPGA一体方便做图像采集和系统控制。我手上这块是Zynq-7020资源够写好几种规模的卷积网络。开发工具Vivado是必须的版本建议2019.1以上对Zynq的IP支持更好。如果你是想HLS开发Vitis HLS也可以但纯Verilog的时序可控性更好我这个项目用的是纯RTL方式。图像输入如果你手头没有摄像头图像源可以直接用PC生成一张测试图通过UART或DMA搬到FPGA的DDR里再喂给加速器这样验证起来更可控。软件环境如果做Zynq上的运行开发Linux驱动或者裸机代码都需要一个交叉编译环境即便只跑纯FPGA逻辑Vivado里做仿真也够了。一个容易踩的坑是版本兼容问题我之前Vivado和Vitis版本混用导致IP核无法升级所以建议PC上同一工程环境统一版本最好虚拟机隔离两套环境。3. 实操过程与核心环节实现3.1 先写一个简单的3x3卷积窗口生成模块在把整套系统做复杂之前先写一个最简单的Verilog模块把卷积窗口生成这件事练熟。这个模块接收单通道灰度图的像素流输出3x3邻域窗口。module window_3x3 #( parameter DW 8, parameter IW 64, parameter IH 64 )( input clk, input rst_n, input valid_in, // 输入像素有效 input [DW-1:0] pixel_in, output reg [DW-1:0] w00, w01, w02, output reg [DW-1:0] w10, w11, w12, output reg [DW-1:0] w20, w21, w22, output reg valid_out ); // 行缓存这里用两个移位寄存器实现两行延迟 reg [DW-1:0] line0 [0:IW-1]; reg [DW-1:0] line1 [0:IW-1]; reg [DW-1:0] line2 [0:IW-1]; reg [$clog2(IW)-1:0] col_cnt; reg [1:0] row_state; reg valid_delay; reg [DW-1:0] p00, p01, p02; reg [DW-1:0] p10, p11, p12; always (posedge clk or negedge rst_n) begin if (!rst_n) begin col_cnt 0; valid_delay 1b0; end else if (valid_in) begin // 写入当前像素到第三行缓存 line2[col_cnt] pixel_in; // 同时把三行缓存打包成窗口 if (col_cnt 2) begin w00 line0[col_cnt - 2]; w01 line0[col_cnt - 1]; w02 line0[col_cnt]; w10 line1[col_cnt - 2]; w11 line1[col_cnt - 1]; w12 line1[col_cnt]; w20 line2[col_cnt - 2]; w21 line2[col_cnt - 1]; w22 pixel_in; valid_out 1b1; end else begin valid_out 1b0; end if (col_cnt IW - 1) begin col_cnt 0; // 三行整体下移line2变line1line1变line0 for (int i 0; i IW; i i 1) begin line0[i] line1[i]; line1[i] line2[i]; end end else begin col_cnt col_cnt 1; end end end endmodule这段代码最核心的思想是三个行缓存和窗口打包逻辑。当col_cnt大于等于2时三行缓存里对应的列位置恰好构成了3x3窗口的9个像素。valid_out拉高表示窗口数据有效。这个模块不会单独工作它会作为卷积计算单元的“前端窗口生成器”使用后面喂给MAC阵列。3.2 卷积计算单元的设计乘累加流水线窗口生成之后下一拍就要做乘累加。在FPGA里做CNN卷积我强烈建议把乘累加做成流水线结构而不是用一个“大的组合逻辑一次性算完9个乘法”。组合逻辑延迟太大会导致时序收敛困难而流水线结构能用资源换频率。下面是核心MAC的设计思路。假设3x3窗口权重已经预先存入寄存器每个时钟周期完成一组窗口的卷积计算。注意我在乘法器和加法器之间插入了寄存器这是避免路径延迟过长的一种有效手段。module conv3x3_mac #( parameter DW 8 )( input clk, input rst_n, input valid_in, input [DW-1:0] w00, w01, w02, input [DW-1:0] w10, w11, w12, input [DW-1:0] w20, w21, w22, input [DW-1:0] k00, k01, k02, input [DW-1:0] k10, k11, k12, input [DW-1:0] k20, k21, k22, output reg [DW3:0] conv_out, output reg valid_out ); // 第一级流水乘法 reg signed [DWDW-1:0] mul00, mul01, mul02; reg signed [DWDW-1:0] mul10, mul11, mul12; reg signed [DWDW-1:0] mul20, mul21, mul22; always (posedge clk or negedge rst_n) begin if (!rst_n) begin mul00 d0; mul01 d0; mul02 d0; mul10 d0; mul11 d0; mul12 d0; mul20 d0; mul21 d0; mul22 d0; end else if (valid_in) begin mul00 $signed(w00) * $signed(k00); mul01 $signed(w01) * $signed(k01); mul02 $signed(w02) * $signed(k02); mul10 $signed(w10) * $signed(k10); mul11 $signed(w11) * $signed(k11); mul12 $signed(w12) * $signed(k12); mul20 $signed(w20) * $signed(k20); mul21 $signed(w21) * $signed(k21); mul22 $signed(w22) * $signed(k22); end end // 第二级流水9个乘积累加 reg signed [DWDW3:0] sum0, sum1, sum2; always (posedge clk or negedge rst_n) begin if (!rst_n) begin sum0 d0; sum1 d0; sum2 d0; end else if (valid_in) begin sum0 mul00 mul01 mul02; sum1 mul10 mul11 mul12; sum2 mul20 mul21 mul22; end end // 第三级流水行累加输出 always (posedge clk or negedge rst_n) begin if (!rst_n) begin conv_out d0; valid_out 1b0; end else if (valid_in) begin conv_out sum0 sum1 sum2; valid_out 1b1; end else begin valid_out 1b0; end end endmodule这个模块的好处是清晰且容易扩展。如果你有4个这样的MAC并行那就能做到每拍算4个输出通道。如果加一层循环在不同时钟周期复用同一组MAC算多个通道那就是“分时复用”方案资源少但吞吐下降。实际选择取决于你有多少DSP资源。3.3 与ARM端互联Zynq上的AXI-DMA传输在Zynq平台上图像数据通常存放在DDR里加速器的输入要先把图像从DDR通过AXI总线搬到FPGA侧的BRAM里算完之后再搬回去。这段搬运我一般用Xilinx官方AXI DMA IP。AXI DMA的配置有几个关键点数据宽度根据FPGA侧处理位宽设为32位因为RGB888可以一个周期塞进32位总线。地址位宽设32位即可对应DDR地址空间。Buffer Length传输一行或一帧的总字节数可以通过寄存器配置。Stream接口与Memory Map接口之间需要有FIFO做位宽转换AXI DMA会自动处理。我实际用的是S2MM方向把DDR里的图像搬到FPGA侧算完之后用MM2S方向把结果搬到另一段DDR缓冲区。两边可以使用同一个AXI DMA IP的两个通道也可以例化两个DMA。同一个DMA做双向搬运要小心死锁我建议直接例化两个DMA一个搬入一个搬出工程上更直观。驱动和地址映射也不难。裸机环境下用Xil_DCacheFlush()刷新ARM侧Cache保证DMA读的是DDR里最新的数据FPGA侧算完后调用Xil_DCacheInvalidate()让ARM能看到最新结果。否则你会遇到一个非常经典的问题计算结果“偶尔正常偶尔全乱”。3.4 一个简单的单层卷积完整流程把上面模块串起来一个最小的完整单层卷积系统如下ARM端通过DMA把图像从DDR搬到FPGA侧的输入BRAM等待搬运完成中断。FPGA状态机启动从BRAM中按行读出像素序列送入行缓存生成窗口。每个时钟周期窗口数据进入3x3 MAC流水线经过三级流水输出一个卷积结果。结果写入输出BRAM再经过ReLU截断负数和2x2最大池化得到最终特征图。DMA把结果搬回DDR同时产生中断通知ARM处理结束。这一套流程的延迟换算非常直观。假设512x512单通道输入工作时钟150MHz输入输出都在片内BRAM完成不需要外部DDR等待那么整个计算只需要大约512x512/150MHz秒也就是1.75毫秒左右。帧率完全能跑到30fps以上这只是单层的基础能力如果做多路通道并行速度还会更快。3.5 多通道与多层扩展方式做工程不能只停留在单层。一旦要跑真正的CNN输入会有多个通道网络会有多个特征图。我的扩展方案有两种视资源而定空间并行扩展把多个MAC阵列在空间上复制每个MAC对应一个输出通道输入特征图广播给所有MAC。这种方案实时性最强但DSP和BRAM消耗成倍增加。时间复用扩展只放置一套MAC阵列用一个控制器按顺序计算不同输出通道中间结果暂存到BRAM。资源占用小但吞吐量下降实时性可能不足。我实际的取舍是先把输入图像三通道的空间并行做出来三个通道分别进入三组MAC输出相加后再交给后面的激活和池化。等到层数增多再考虑用时间复用扩展堆叠更多层。4. 实时性分析与资源优化4.1 延迟打包拆开算一帧到底只花多少秒实时这个词经常被滥用。做嵌入式图像处理实时意味着“处理速度不低于图像采集速度”。要判断系统是否实时必须把一帧图像从进入到出结果的延迟拆开算一遍以下是常用计算方式图像输入时间假设输入是1080p30每秒30帧每帧1920x1080则每帧约207万个像素像素时钟大约在74.25MHz左右。FPGA端通常在像素时钟同步下工作接收一帧图像所需时间约33.3ms。卷积计算时间若系统工作时钟150MHz使用空间并行方式理论上每周期算一个输出点第一层特征图尺寸1080x1920大约需要约13.9ms。但因为有行缓存初始化的前两行等待实际约14.2ms。结果搬出时间通过AXI DMA搬回DDR带宽通常是128bit150MHz约2.4GB/s一帧特征图按单通道算只有2MB左右搬出时间不足1ms。总延迟输入33.3ms的管道化延迟、14.2ms的计算延迟以及1ms的搬出延迟实际上因为管道化流水线叠加最终系统能达到的瓶颈是“下一个输入帧到来之前能否完成后一帧计算”即只要计算时间小于33.3ms就能做到实时。我做的一个单层512x512灰度图卷积整体延迟做到大约2.1ms单帧处理能力远超实时要求。这里也提醒一下真正限制系统的常常不是计算单元而是图像输入带宽和DMA的搬运开销所以“实时性设计”要从摄像头到DDR整体看不能只看卷积阵列。4.2 资源占用表用数据说话下面是一份我在Zynq-7020上用Vivado跑出一个最小三层CNN每层都用3x3卷积的实际资源报告给大家一个量级感觉资源类型总资源使用量占比LUT532002745351%FF1064001822017%DSP48E12205826%BRAM 36Kb1407654%时钟频率—150MHz达标这个结果说明三层轻量网络在中等规模芯片上跑完全没压力。如果你要跑更大网络要么换大芯片要么减少并行度。别小看BRAM占比它主要被行缓存和特征图暂存吃掉当特征图通道多时BRAM会成为第一个瓶颈。4.3 优化手段怎么把吞吐再往上拉如果计算时间成了瓶颈有几个被验证有效的优化手段卷积核分解把5x5拆成两个3x3把3x3拆成1x3加3x1都可以显著减少乘法次数。比如5x5需要25次乘法拆成两个3x3后只需要18次省了将近三分之一计算资源。深度可分离卷积把普通卷积拆成逐通道卷积和1x1点卷积参数计算量能减少到原来的十分之一左右。这种网络结构天生更适合FPGA流水线。Winograd算法对3x3卷积使用F(2x2, 3x3)变换可以把每个输出点的乘法次数从9次降到4次但会牺牲一些有限的精度和增加变换逻辑。这个优化在DSP资源吃紧时特别划算我真实项目里省了一半多的DSP块。通道间的并行度调整如果空间并行不足可以改成同一窗口、多通道并行也就是让多个输入通道的计算共享同一窗口数据对BRAM带宽的利用效率更高。还有一个容易忽视的优化方向是“输入裁剪”。很多实时场景里整幅图像并不是所有区域都值得跑卷积。比如工业检测经常只关注ROI区域直接在FPGA侧把非ROI数据丢掉只把ROI送入卷积阵列吞吐量立刻翻倍。5. 常见问题与排查技巧实录5.1 结果全零先从数据通路查起这个是我遇到最多的问题刚把卷积跑通时输出特征图全零。原因归根结底是数据没有正确到达或者权重没有正确加载。排查路径我一般按这个顺序走先查权重是否被正确初始化。很多FPGA设计里权重存在ROM或BRAM里如果地址计算错误很容易读到全零。我们可以在仿真里直接打印ROM内容确认。再查输入图像是否真的进入了行缓存。可以用一个计数器统计进入的valid_in次数如果一直为零说明上游DMA或图像源没工作。接着查窗口数据。在仿真波形里看w00~w22的波形看是否在像素流进到一定数量之后开始有规律变化。如果窗口全是固定值说明行缓存或地址指针有bug。一个典型坑是忘记给权重做有符号处理。INT8量化后的权重就是有符号数如果Verilog里用了无符号乘法负数权重的计算全部错误表现出来就是特征图看起来像“噪点但是形状不对”。5.2 时序不收敛加法树级数太深Vivado综合后如果时序报红最常见原因是乘累加阵列的组合逻辑路径太长。我遇到过一版24个乘法器直接串成一个大加法器150MHz的约束报出负slack路径延迟接近12ns根本收不了。解决方法很直接在加法树中间每两三层就插一组寄存器把组合逻辑切割成多级流水。流水带来的代价是输出延迟增加了几个周期但对视频流实时性毫无影响因为一帧数据几千行多几个周期完全可以忽略。另外一个时序优化技巧是让DSP的级联寄存器参与流水。Xilinx DSP48E1自带输入寄存器和中间寄存器综合工具通常能自动推断。但为了保险我会在代码里显式地把乘法结果用reg打拍帮助工具识别可以映射到DSP内部寄存器的路径。5.3 行缓存错位特征图“撕裂”曾经在换用更大分辨率图像时特征图出现了横向错位也就是图像像被切割后错开拼上去一样。这个问题本质是“行缓存的宽度与图像实际宽度不匹配”。我之前写的window_3x3模块里行缓存用固定深度IW但当修改分辨率的时候只改了参数而忘了ARP地址映射导致一行还没存满新一行数据就开始覆盖窗口里的数据横跨了两行彼此错位。查了好久才通过仿真发现问题出在行有效信号没有跟DMA突发长度对齐。解决方法并不复杂确保行缓存深度与图像实际行宽一致并在状态机里用行同步信号对行缓存做一次清零。这样每行数据到来时行缓存都是干净的起点窗口永远只取同一行的数据。5.4 AXI DMA和Cache一致性问题Zynq平台最常见的一个深坑ARM写入输入图像后DMA拿到的不是最新数据或者DMA写完结果后ARM读到的还是旧数据。这就是Cache一致性问题。裸机开发里记住两个函数就够Xil_DCacheFlush(); // 写入图像后刷新Cache到DDR Xil_DCacheInvalidate(); // 读取结果前使Cache失效但是函数的调用时机大有讲究。必须在DMA启动前做完Flush在DMA搬运完成中断里做Invalidate。如果顺序搞反时好时坏非常折腾。Linux环境下如果使用UIO或DMA子系统一般驱动会维护好一致性映射这个坑会小一些。5.5 常见问题速查表现象可能原因解决方向输出全零权重没加载、输入数据没来、MAC没有有效窗口仿真打印权重ROM、统计valid_in次数、检查窗口信号特征图左移/右移一个像素窗口对齐位置偏移检查行缓存读地址、列计数器初值时序不收敛组合逻辑过深、未在加法树插流水多级寄存器分割、使用DSP内部寄存器结果时好时坏Cache一致性问题Flush/Invalidate时机不对DMA启动前必须保证数据落DDR帧率达不到预期DDR带宽被占满、MAC空闲等待太多增加数据复用、降低不必要搬移、通道并度调整特征图有周期性横条行有效信号与行缓存清零不同步行同步脉冲与行缓存复位严格对齐5.6 量化精度崩了怎么办最后说一个关于模型侧的坑。把权重量化到INT8后分类准确率突然掉了十几个点这种情况大多不是因为“8位不够”而是因为“量化范围没选好”。权重分布通常比较集中直接按min/max来标定会把大量区间浪费在极端值上导致低比特位噪声被放大。我的做法是先统计权重的直方图用百分位而不是min/max确定缩放因子一般取99.9%分位点就行。激活值那边则要真正喂一批真实图像数据统计分布不能拍脑袋定。校准集最好覆盖各种光照和工况否则部署到现场会因为输入分布漂移导致精度下降。如果实测还差一点可以考虑对敏感层保留更高精度或者做混合量化只把最敏感的那几层用更高位数。最后一个私心建议如果这篇博文里面的东西你都能亲手调通那FPGA跑CNN对你来说就不算什么难题了。我最深的体会是这类项目最花时间的不是写卷积计算单元而是处理“图像怎么进来、结果怎么出去”这些看不见摸不着的边界问题。所以如果你刚开始做建议一定先做仿真级验证再上板实测免得波形满天飞却找不到bug。另外一个很实用的建议是把FPGA工程的时序约束尽早做完整别等最后综合不过再补那时会发现一堆模块要返工。这套东西第一版慢一点没关系踩过一轮之后后面再换模型、换平台都会快很多。
返回列表