卷积神经网络LeNet-5的RTL实现(二):Padding
前文回顾
上一期文章简单介绍了工程用到的一些结构性电路:FIFO、Shift RAM和二叉加法树。这篇文章将使用FIFO和padding电路实现卷积神经网络的padding。
Padding简介
卷积神经网络结构中,padding是通过为输入FMAP边缘补充像素的方式改变输出FMAP的尺寸。为了不改变原图信息,padding补充的像素值一般为0,下图展示了N为1的padding:
令输入FMAP尺寸为Ni×Ni,卷积核尺寸为K×K,窗口滑动步长为S,则可以计算出输出FMAP的长宽为
(Ni + 2 * N - K + 1) / S。
特征图输入
上文提到,本工程中的FMAP数据在模块和层之间均以串行方式流动,特征图输入时会被被打平成一维。padding前后FMAP的尺寸会发生变化,因此在输入端设置有一个FIFO对图像数据进行暂存。后级padding电路可以根据需要读取输入FIFO中的数据。
Padding电路
接口
Padding电路的接口如下所示:
module PADDING
#(
parameter DATA_WIDTH = 16, //数据位宽
parameter FMAP_SIZE = 28, //输入特征图尺寸
parameter N = 2 //padding尺寸
)
(
input clk,
input rst_n,
input ena, //电路使能信号,高电平有效
input clear, //电路数据清零信号,上升沿有效
input [DATA_WIDTH-1:0]fmap_raw, //输入特征图
output [DATA_WIDTH-1:0]fmap_pad, //补零后输出特征图
output rd_en, //FIFO数据读取信号
output reg valid, //输出有效信号
output reg done //padding完成信号
);
计数信号
Padding电路的核心是判断当前输入像素在FMAP中的位置,以此作为是否补零的依据。模块中使用多个计数变量进行像素位置的记录:
reg [CNT_BIT_NUM-1:0]cnt;
reg [LINE_CNT_BIT_NUM-1:0]cnt_col;
reg [LINE_CNT_BIT_NUM-1:0]cnt_row;
cnt为总输出像素计数,cont_col和cnt_row分别记录列和行输出像素。Padding电路在每个时钟周期输出一个像素值,因此每个周期cnt、cnt_col、cnt_row分别加一:
always @(posedge clk or negedge rst_n)
if(!rst_n)
cnt <= 0;
else if(clear)
cnt <= 0;
else if(ena) begin
if(cnt == (FMAP_SIZE + N * 2) * (FMAP_SIZE + N * 2) - 1)
cnt <= 0;
else
cnt <= cnt + 1;
end
always @(posedge clk or negedge rst_n)
if(!rst_n) begin
cnt_col <= 0;
cnt_row <= 0;
end
else if(clear) begin
cnt_col <= 0;
cnt_row <= 0;
end
else if(ena) begin
if (cnt_col >= FMAP_SIZE + N * 2 - 1) begin
cnt_col <= 0;
cnt_row <= cnt_row + 1;
end
else
cnt_col <= cnt_col + 1;
end
外部标志信号
valid和done是电路的外部标志信号,valid表示padding电路正输出有效数据,高电平有效;done表示电路输出结束,上升沿有效。在电路被使能的情况下,valid信号在cnt计数到达输出特征图的最后一个像素之前都有效,done信号则在cnt计数到达输出特征图的最后一个像素时输出一个高电平脉冲。
assign valid = (ena) ? ((cnt < (FMAP_SIZE + N * 2) * (FMAP_SIZE + N * 2) - 1) ? 1 : 0) : 0;
assign done = (ena) ? ((cnt == (FMAP_SIZE + N * 2) * (FMAP_SIZE + N * 2) - 1) ? 1 : 0) : 0;
输入和输出信号
Padding模块的输入数据来自FIFO,因此需要提供一个FIFO的读使能信号rd_en。由计数信号可得知下一个输出像素在特征图的有效数据位置还是padding位置。如果在有效数据位置,需要读取FIFO数据并在下一个时钟输出;若在padding位置,则不需要读取FIFO数据,在下一个时钟直接输出0即可。rd_en信号的逻辑如下:
generate
if(N != 0) begin: padding
assign rd_en = (!ena || cnt_row < N || cnt_row > FMAP_SIZE + N - 1) ? 0 : ((cnt_col < N - 1 || cnt_col > FMAP_SIZE + N - 2) ? 0 : 1);
end
else begin: no_padding
assign rd_en = (ena) ? 1 : 0;
end
endgenerate
这里使用了generate-if块来生成rd_en:当N为0时不需要padding,每个时钟都需要读取FIFO数据;当N不为0时,行计数或列计数值在输入特征图的长宽与输出特征图的长宽之间时,下一个输出像素位于padding区域,不需要读取FIFO数据,rd_en为低;反之则位于有效数据区域,需要读取FIFO数据,rd_en为高。
输出信号fmap_pad的逻辑也基本相同:
assign fmap_pad = (!ena || cnt_row < N || cnt_row > FMAP_SIZE + N - 1) ? 0 : ((cnt_col < N || cnt_col > FMAP_SIZE + N - 1) ? 0 : fmap_raw);
可以注意到,rd_en和fmap_pad在对cnt_col的判断中相差了一个周期,这是因为在rd_en有效后,下一个周期FIFO才会输出数据,本质上rd_en记录的是下一个像素的信息;而fmap_pad是当前的输出数据,而且是一个组合电路的输出,立即有效。因此rd_en的信号变化要比fmap_pad提前一个周期。
电路完整代码
/* PADDING.v */
module PADDING
#(
parameter DATA_WIDTH = 16,
parameter FMAP_SIZE = 28,
parameter N = 2
)
(
input clk,
input rst_n,
input ena,
input clear,
input [DATA_WIDTH-1:0]fmap_raw,
output [DATA_WIDTH-1:0]fmap_pad,
output rd_en,
output valid,
output done
);
function integer clogb2 (input integer bit_depth);
begin
for(clogb2=0; bit_depth>0; clogb2=clogb2+1)
bit_depth = bit_depth >> 1;
end
endfunction
localparam CNT_BIT_NUM = clogb2((FMAP_SIZE+N*2)*(FMAP_SIZE+N*2));
localparam LINE_CNT_BIT_NUM = clogb2(FMAP_SIZE+N*2);
reg [CNT_BIT_NUM-1:0]cnt;
reg [LINE_CNT_BIT_NUM-1:0]cnt_col;
reg [LINE_CNT_BIT_NUM-1:0]cnt_row;
assign valid = (ena) ? ((cnt < (FMAP_SIZE + N * 2) * (FMAP_SIZE + N * 2) - 1) ? 1 : 0) : 0;
assign done = (ena) ? ((cnt == (FMAP_SIZE + N * 2) * (FMAP_SIZE + N * 2) - 1) ? 1 : 0) : 0;
assign fmap_pad = (!ena || cnt_row < N || cnt_row > FMAP_SIZE + N - 1) ? 0 : ((cnt_col < N || cnt_col > FMAP_SIZE + N - 1) ? 0 : fmap_raw);
//conditional generate
generate
if(N != 0) begin: no_padding
assign rd_en = (!ena || cnt_row < N|| cnt_row > FMAP_SIZE + N - 1) ? 0 : ((cnt_col < N - 1 || cnt_col > FMAP_SIZE + N - 2) ? 0 : 1);
end
else begin: padding
assign rd_en = (ena) ? 1 : 0;
end
endgenerate
always @(posedge clk or negedge rst_n)
if(!rst_n)
cnt <= 0;
else if(clear)
cnt <= 0;
else if(ena) begin
if(cnt == (FMAP_SIZE + N * 2) * (FMAP_SIZE + N * 2) - 1)
cnt <= 0;
else
cnt <= cnt + 1;
end
always @(posedge clk or negedge rst_n)
if(!rst_n) begin
cnt_col <= 0;
cnt_row <= 0;
end
else if(clear) begin
cnt_col <= 0;
cnt_row <= 0;
end
else if(ena) begin
if (cnt_col >= FMAP_SIZE + N * 2 - 1) begin
cnt_col <= 0;
cnt_row <= cnt_row + 1;
end
else
cnt_col <= cnt_col + 1;
end
endmodule
TestBench
`timescale 1ns / 1ns
module tb_padding();
parameter DATA_WIDTH = 16,
FMAP_SIZE = 28,
PADDING_SIZE = 2;
reg clk, rst_n;
reg ena;
wire fifo_rd_en;
reg fifo_wr_en;
reg[DATA_WIDTH-1:0] fmap_data;
wire[DATA_WIDTH-1:0] fifo_out;
wire[DATA_WIDTH-1:0] padding_out;
wire padding_done;
wire padding_valid;
initial begin
clk = 0;
rst_n = 0;
ena = 0;
fmap_data = 0;
#100;
rst_n = 1;
ena = 1;
fifo_wr_en = 1;
//generate feature map
repeat(784) begin
#100
clk = ~clk;
#100
clk = ~clk;
fmap_data = fmap_data + 1;
end
fifo_wr_en = 0;
fmap_data = 0;
forever begin
#100
clk = ~clk;
#100
clk = ~clk;
end
end
FIFO
# (
.DATA_WIDTH(DATA_WIDTH),
.BUF_SIZE(FMAP_SIZE*FMAP_SIZE)
)
u_fifo_1(
.clk(clk),
.rst_n(rst_n),
.buf_in(fmap_data),
.wr_en(fifo_wr_en),
.rd_en(fifo_rd_en),
.rd_rewind(),
.fifo_empty(),
.fifo_full(),
.buf_out(fifo_out)
);
PADDING
#(
.DATA_WIDTH(DATA_WIDTH),
.FMAP_SIZE(FMAP_SIZE),
.N(PADDING_SIZE)
)
u_padding
(
.clk(clk),
.rst_n(rst_n),
.clear(),
.ena(ena),
.fmap_raw(fifo_out),
.fmap_pad(padding_out),
.rd_en(fifo_rd_en),
.valid(padding_valid),
.done(padding_done)
);
endmodule
本工程的FMAP测试数据均为从0开始,步长为1的序列,在initial中的repeat块中生成,在此TestBench中为0-783。fmap_data首先输入一个FIFO中暂存,然后被padding模块读取,最后输出的padding_out为补零后的FMAP。
仿真结果
可以看到,在输入FMAP第一行最后一个像素27结束后,padding_out输出了4个周期的0,即左右两侧的补零。与此同时,在27输出的前一个周期rd_en就已经拉低,不再读取FIFO中数据。
总结
本期文章实现了卷积神经网络中的padding电路,利用FIFO暂存特征图数据并受padding模块的控制进行数据输出。Padding电路的实现关键是处理好rd_en和fmap_pad两个信号。下一期文章将详细讲解基于Shift RAM的卷积层的实现。