FPGA—YOLO

激活函数 fpga实现

激活函数是为了实现多层神经网络而引入的非线性函数,而不是最终得到线性函数。

热露
relu激活

注意:由于 RELU 函数的输出根据输入的符号而有所不同,因此只有当我们开始使用有符号数字而不仅仅是整数时,它才能在硬件中实现。然而,正负的概念只是软件层面的一个数学概念。在硬件中,没有负数这样的东西。一切都只是一定位数的寄存器(在我们的例子中是 32 位)。但是,有符号二进制表示等概念允许我们在硬件中实现正数和负数的概念。当然,在实际硬件之上还需要另一层,以便解释这些数字的符号并根据需要使用它们。

在 Verilog 中实现 ReLu 函数

//file: relu.v
`timescale 1ns / 1ps
module relu(
    input [31:0] din_relu,
    output [31:0] dout_relu
    );
    assign dout_relu = (din_relu[31] == 0)? din_relu : 0;   //if the sign bit is high, send zero on the output else send the input
endmodule

在 Verilog 中实现 leakrelu函数 (0.125)

`timescale 1ns / 1ps
module leakrelu(
    input signed[31:0]  din_relu,
    output signed[31:0]  dout_relu
    );
    parameter N = 0;//通过右移位实现除2的幂操作
    assign dout_relu = (din_relu[31] == 0)? din_relu:din_relu>>>N;   //if the sign bit is high, send zero on the output else send the input
endmodule
除2操作
除8操作(*.125)

在 Verilog 中实现sigmod

https://blog.csdn.net/kebu12345678/article/details/81673111

参考论文 :神经网络激活函数及其导数的FPGA实现 作者:张智明 张仁杰

采用折线斜率为2的次幂的分段线性逼近方法实现激活函数(sigmoid函数)及其导数的映射。该方法在FPGA实现时不需要使用硬件乘法器,而且可以节约大量的RAM单元。由于神经网络的并行计算需要消耗大量的硬件乘法器和RAM,因此,与其他方法相比,该方法为整个神经网络的FPGA实现有效地节省了大量宝贵的FPGA资源,可以较好地应用在BP神经网络的在线训练中。

//输入16位,最高位为符号位,整数占3位,小数占12位;输出16位,最高位为符号位,小数占15位

module sigmoid(clk,rst,a,b
    );
 
input clk;
input rst;
input [15:0] a;
 
output[15:0] b;
reg[15:0] b;
reg[15:0] a_reg;
 
always@(posedge clk)
begin
    if(rst)
    b=0;
    else
    begin
        if(a[15]==0)
        begin
            b[15:12]=4'b0000;
            case(a[14:12])
            3'b000:b[11:0]=12'b100000000000+(a[11:0]>>2);//加号的优先级大于移位运算的优先级,记得加括号
            3'b001:b[11:0]=12'b110000000000+(a[11:0]>>3);
            3'b010:b[11:0]=12'b111000000000+(a[11:0]>>4);
            3'b011:b[11:0]=12'b111100000000+(a[11:0]>>5);
            3'b100:b[11:0]=12'b111110000000+(a[11:0]>>6);
            3'b101:b[11:0]=12'b111111000000+(a[11:0]>>7);
            3'b110:b[11:0]=12'b111111100000+(a[11:0]>>8);
            3'b111:b[11:0]=12'b111111110000+(a[11:0]>>9);
            endcase
            b=b<<3;//输入16位,最高位为符号位,整数占3位,小数占12位;输出16位,最高位为符号位,小数占15位
        end
        else
        begin
            a_reg=~a+1;//取a的绝对值
            b[15:12]=4'b0000;
            case(a_reg[14:12])
            3'b000:b[11:0]=12'b100000000000+(a_reg[11:0]>>2);
            3'b001:b[11:0]=12'b110000000000+(a_reg[11:0]>>3);
            3'b010:b[11:0]=12'b111000000000+(a_reg[11:0]>>4);
            3'b011:b[11:0]=12'b111100000000+(a_reg[11:0]>>5);
            3'b100:b[11:0]=12'b111110000000+(a_reg[11:0]>>6);
            3'b101:b[11:0]=12'b111111000000+(a_reg[11:0]>>7);
            3'b110:b[11:0]=12'b111111100000+(a_reg[11:0]>>8);
            3'b111:b[11:0]=12'b111111110000+(a_reg[11:0]>>9);
            endcase
            b=b<<3;//输入16位,最高位为符号位,整数占3位,小数占12位;输出16位,最高位为符号位,小数占15位
            b[14:0]=~b[14:0]+1;//f(x)=1-f(-x)      
        end
    end
 
end
endmodule

在 Verilog 中实现双曲正切

Tanh 是另一个非常流行和广泛使用的激活函数。它只是一个缩放的 sigmoid,并且具有一些非常有用的属性。这是这个函数的图:

tanh

在硬件实现方面,我们可以通过多种方式实现这种非线性函数。例如:

  • 简单查找表,恰好是实现一个功能的最简单、最快的方法,但在需要非常高的精度时也占用了大量资源。
  • 带有插值的查找表,这也是一种基于查找表的方法,但使用一些额外的算术来改善结果,超出为查找表本身分配的精度。还有一些方法使用两个查找表,一个具有粗粒度值,另一个具有细粒度值,这些值被添加到初始近似值上。
  • CORDIC,这是另一种非常流行的方法,广泛用于大多数需要非线性函数(如正弦、sqrt、tanh 等)的 DSP 应用程序中。这是一种非常优雅的方法,它使用基本的移位加法电路来实现非常好的结果。您可以阅读这篇文章以了解 CORDIC 的良好实现。大多数 FPGA 供应商还为 CORDIC 单元提供封装的 IP 块。
  • 泰勒级数和多项式逼近,这些方法使用三角函数的传统多项式逼近,并尝试在这些多项式中实现高阶变量。
  • DCT 插值和更复杂的方法,这些方法使用更专业的方法来实现所需的精确精度和资源使用。本文展示了一种这样的方法。

简单的表查找

  • 对于这个实现,我们可以利用 Tanh 函数的对称性,即我们只需要存储与函数的正相关的值,并且由于负输入的函数输出只是镜像,我们可以输出相同的值以他们的 2 的补码格式。
  • 我们可以轻松利用的另一个属性是 Tanh 函数在超过某个输入值后饱和到 1(或在另一侧为 -1)。在准确度没有任何显着损失的情况下,我们可以直接为高于阈值的输入输出 1(或 -1)。因此,我们不必将函数值存储在查找表中以查找高于阈值的输入。我使用输入值 3(或 -3)作为此阈值,因为tanh(3) = 0.9950547536867305 并且对于高于 3(或低于 -3)的所有输入,可以轻松地将输出视为 1(或 -1) .
  • 对于这个实现,我使用了一个具有 1024 个位置的 RAM,每个位置都保存 16 位数据,以实现查找表。这意味着我们将有 10 个(= log2(1024))地址位来访问这个 ram。但是我们的相位输入将是一个 16 位的定点数。那么我们如何选择 10 个特定的位来寻址这个 ram?

以下是 tanh 函数的 (3,12) 格式的 16 位定点输入示例:

reg [15:0] phase = 16'b0_001_011000100101

在这,

  • MSB ( phase[15] ) 是符号位,它将告诉我们是输出直接值还是 2 的补码形式。
  • 接下来的三个 MSB 代表这个数字的整数部分。由于我们只关心幅度小于 3 的数字,如果第二个 MSB ( phase[14] ) 很高(对于一个正数),它告诉我们这个数字的幅度大于 3 并且我们可以直接输出 1 。否则,我们可以从表中输出值。
  • 接下来的 10 个 MSB(阶段 [13:4])将用作查找表的输入。是的,我们将丢失 LSB(phase[3:0])中的信息,但如果我们也想使用它们,我们需要一个 16 倍大的查找表。只要我们的应用程序获得合理的准确性,我们就不必担心。再往下,我们将研究一种也利用这些 LSB 的技术。
 `timescale 1ns / 1ps
    module tanh_lut #(
        parameter AW = 10, //AW will be based on the size of the ROM we can afford in our design.
                           //in the best case AW = N;
        parameter DW = 16,
        parameter N = 16,
        parameter Q = 12
        )(
        input clk,
        input [AW-1:0] phase,
        output [DW-1:0] tanh
       );
     reg [AW-1:0] addr_reg;
  (* ram_style = "block" *)reg [DW-1:0] mem [1<<AW-1:0];  //ram_style can be 'block' or 'distributed' 									                    // based on the utilization and other
                                                          //requirements in the project
    initial 
    begin
        $readmemb("tanh_data.mem",mem); //loading data into our RAM via a file
    end
        
    always@(posedge clk)
    begin
        addr_reg <= phase[AW-1:0];
    end
        
    assign tanh = mem[addr_reg];

具有线性插值的查找表

线性插值技术是数学中常用的技巧,用于在数据中没有足够的分辨率时改进值的近似值。即,当您想要两个连续数据点之间的离散函数的值时,您可以绘制一条连接这两个点的直线,并将您的输出近似为该线上的某个位置。这显着提高了我们函数的准确性。

线性插值

现在,为了解释这个想法,看看上面的图片。假设ii+1是两个连续的点,我们知道函数 f(a i ) 和 f(a i+1 ) 的值。也就是说,在我们的例子中,这些是 RAM 中的两个条目,用于两个连续点的 Tanh 值。

假设我们要在i+α点找到函数的值, 其中α是小于 1 的小数值。这告诉我们,我们需要两个已知点之间的 f(x) 值,因为我们没有关于这两个点之间曲线的实际形状的信息,一种近似的方法是假设两个已知点。通过使用基本的线性数学,我们可以找出对应于 x = a i+α的直线上的点的值。

你可以猜到,这个近似值是 f( i ) + x( (f( i+1 ) – i )/1 ) = xf( i ) + (1-x)f( i+ 1 ) 产生的误差为 xf( i ) + (1-x)f( i+1 ) – f( i+α )

这个结果将比查找表中的初始近似值相对更准确。在我们的例子中, α的值将来自在实际查找表中被忽略的剩余 4 个 LSB。下面是这种方法的代码:

注意:我正在为 16 位宽的数据路径编写代码。ie N = 16 如果您一直在阅读本系列的其他文章。但是,它可以非常参数化以使用任何位宽。

`timescale 1ns / 1ps
module tanh_lut #(
    parameter AW = 10, //AW will be based on the size of the ROM we can afford in our design.
                       //in the best case AW = N;
    parameter DW = 16,
    parameter N = 16,
    parameter Q = 12
    )(
    input clk,
    input [N-1:0] phase,
    output [DW-1:0] tanh
    );
    
    reg [9:0] addra_reg;
    reg [9:0] addrb_reg;
    wire [15:0] tanha;
    wire [15:0] tanhb;
    wire ovr1,ovr2;

    wire [15:0] frac,one_minus_frac;
    wire [15:0] A1,A2;
    wire [15:0] one;
    wire [DW-1:0] tanh_temp;

    
    (* ram_style = "block" *)reg [15:0] mem [1<<10-1:0];  //ram_style can be 'block' or 'distributed' based on the
                                                            //utilization and other requirements in the project
    
    initial 
    begin
        $readmemb("tanh_data.mem",mem); //loading our RAM via a file
    end
    
    always@(posedge clk)
    begin
        addra_reg <= phase[9:0];
        addrb_reg <= phase[9:0] + 1'b1;
    end

    assign tanha = mem[addra_reg];
    assign tanhb = mem[addrb_reg];
    
    assign frac = {'d0,phase[N-AW-'d2-1:0]}; //rest of the LSBs that were not accounted for owing to the limited ROM size
    assign one = 16'b0001000000000000;       //'d1 in (N,Q) = (3,12) format
    assign one_minus_frac = one - frac;
    
    //qmult is the fixed point multiplier module, visit the fixed point arithmetic
    //article further in the series to learn of its exact operation
    qmult #(N,Q) mul1 (tanha,frac,A1,ovr1);              //calculates x*f(Ai)
    qmult #(N,Q) mul2 (tanhb,one_minus_frac,A2,ovr2);    //calculates (1-x)*f(Ai+1)
    
    assign tanh_temp = A1 + A2;    // linear interpolation formula: x*Ai + (1-x)*Ai+1
    
    //now, if the phase input is above 3 or below -3 then we just output 1, otherwise we output the calculated value
    //we also check for the sign, if the phase is negative, we return 2's complemented version of the calculated value
    assign tanh = (phase [N-1]) ? (phase[N-2] ? (16'b1111000000000000) : (~tanh_temp + 1'b1)) :(phase[N-2] ? (16'b0001000000000000):(tanh_temp));
    
endmodule

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注