# INTRO
這份筆記是從 [CNN-FPGA](https://github.com/omarelhedaby/CNN-FPGA) 這份開源資料中整理而得的。
整理了如何用Verilog實現CNN的幾個步驟,原版作者已經將內容發布成一篇論文,也將內容整理成了一本書。如果想更深入的探討可以再從作者的官方github中探討。
這份筆記的大多數圖片與內容都出自於這作者出的這本書:

# Network
首先要先知道一個CNN是包含哪幾層,以下是整個網路的結構:

* 有三層卷積層(CONV)
* 兩層平均池化層(avgPool)
* 每層卷積層後面都有一個激活函數(Tanh)
* 再來還有兩層全連階層(FClayer)
* 最後再一個SoftMax函數輸出一個結果
所用的數據集是MNIST
# Convolution
以下先介紹卷積層,是以綠色標出來的模塊:

實現出來的方案在並行度、速度以及資源占用中進行了一些權衡。
在vivado中打開後可以看到這個畫面

從這裡就看的出來他Network的flow是像我們前面說的
Conv2D->Tanh Activation->AvgPool->Conv2D->Tanh Activation->AvgPool->Conv2D->Tanh Activation->Fully Connected Layer->Relu->Fully Connected Layer->Softmax
而我們這邊要先整理**自頂而下**的卷積層邏輯,目的是了解設計者的思路,再**自底而上**的去詳細的進行功能分析與原理了解。
## Block diagrams
### (1) Multi Filter Layer
一個卷積層,有多個Filter,image輸入到不同的Filter得到不同特徵的feature map


### (2) Single Filter Layer
單個filter,輸入image,輸出feature map。


### (3) Convolutional Unit
每個卷積核與輸入圖像進行卷積,得到特徵圖。


### (4) Processing Element
卷積的具體操作是內積(dot product),本質是乘法與加法。這裡輸入的是float,硬體實現也就是定點數。


## 功能解析
### (4) Processing Element

* floatMult16:兩個半精度浮點數的乘法
* floatADD16:兩個半精度浮點數的加法
半經度浮點數(16bits),簡寫為FP16:
* 符號位1bit
* 指數位5bits
* 尾數位10bits

$$
\textbf{Number} = (-1)^s \times (1.M) \times 2^{(E - \text{Bias})}, \quad \text{Bias} = 15
$$
### FP16加法器
浮點數的加減法其實不難,就五個步驟:
1. 對階
2. 尾數運算
3. 規格化
4. 捨入
5. 溢出判斷。
以下是代碼以及我自己加的代碼註釋做參考:
```
module floatAdd16 (floatA,floatB,sum);
input [15:0] floatA, floatB; //兩個FP16
output reg [15:0] sum;
reg sign; // sign bit
reg signed [5:0] exponent; //sixth bit is sign 可以決定指數是否溢出
reg [9:0] mantissa; //10位小數部分 (1.mantissa)
reg [4:0] exponentA, exponentB; //指數位
reg [10:0] fractionA, fractionB, fraction; //fraction = {1,mantissa} 比mantissa多一個隱藏1 變成11位
reg [7:0] shiftAmount; //在標準化裡需要移位
reg cout;
always @ (floatA or floatB) begin
exponentA = floatA[14:10]; //取出floatA的指數位
exponentB = floatB[14:10]; //取出floatB的指數位
fractionA = {1'b1,floatA[9:0]}; //把floatA的小數取出來(mantissaA)
fractionB = {1'b1,floatB[9:0]};//把floatA的小數取出來(mantissaB)
exponent = exponentA; //隨便給個初值
============特殊情況===========
if (floatA == 0) begin //special case (floatA = 0)
sum = floatB;
end else if (floatB == 0) begin //special case (floatB = 0)
sum = floatA;
end else if (floatA[14:0] == floatB[14:0] && floatA[15]^floatB[15]==1'b1) // floatA 與 floatB的大小相等符號相反(1 XOR 0 = 1)
begin
sum=0;
end
============非特殊情況==============
else
begin
if (exponentB > exponentA) begin //對階,使兩個數的階碼相等,小的對齊大的,若尾數向右移一格,指數位加一
shiftAmount = exponentB - exponentA;
fractionA = fractionA >> (shiftAmount);
exponent = exponentB; //exponent等於大的
end
else if (exponentA > exponentB)
begin
shiftAmount = exponentA - exponentB;
fractionB = fractionB >> (shiftAmount);
exponent = exponentA; //exponent等於大的
end
if (floatA[15] == floatB[15]) begin //same sign
{cout,fraction} = fractionA + fractionB;
if (cout == 1'b1) begin //檢查是否溢出
{cout,fraction} = {cout,fraction} >> 1; //若溢出則向右移一位
exponent = exponent + 1; //指數位加一
end
sign = floatA[15];
end else begin //different signs
if (floatA[15] == 1'b1) begin //A- B+
{cout,fraction} = fractionB - fractionA; //fraction是無號數
end
else //符號位不同
begin
{cout,fraction} = fractionA - fractionB;
end
sign = cout;
if (cout == 1'b1) begin
fraction = -fraction;
end else begin
end
if (fraction [10] == 0) begin //第十位那個隱藏位必須要是1,如果是0就要調整,就像一般的科學記號那樣。
if (fraction[9] == 1'b1) begin
fraction = fraction << 1; //左移一位,指數就要減一,以下同理
exponent = exponent - 1;
end else if (fraction[8] == 1'b1) begin
fraction = fraction << 2;
exponent = exponent - 2;
end else if (fraction[7] == 1'b1) begin
fraction = fraction << 3;
exponent = exponent - 3;
end else if (fraction[6] == 1'b1) begin
fraction = fraction << 4;
exponent = exponent - 4;
end else if (fraction[5] == 1'b1) begin
fraction = fraction << 5;
exponent = exponent - 5;
end else if (fraction[4] == 1'b1) begin
fraction = fraction << 6;
exponent = exponent - 6;
end else if (fraction[3] == 1'b1) begin
fraction = fraction << 7;
exponent = exponent - 7;
end else if (fraction[2] == 1'b1) begin
fraction = fraction << 8;
exponent = exponent - 8;
end else if (fraction[1] == 1'b1) begin
fraction = fraction << 9;
exponent = exponent - 9;
end else if (fraction[0] == 1'b1) begin
fraction = fraction << 10;
exponent = exponent - 10;
end
end
end
mantissa = fraction[9:0];
if(exponent[5]==1'b1) begin //exponent is negative 溢出
sum = 16'b0000000000000000; //當0
end
else begin
sum = {sign,exponent[4:0],mantissa};
end
end
end
endmodule
```
### FP16乘法器
乘法器跟前面的加法器差不多,也有三個步驟:
1. 階碼相加
2. 尾數相乘
3. 結果規格化
除法的話是:
1. 尾數調整
2. 階碼求差
3. 尾數相除
以下是代碼以及我自己加的代碼註釋做參考:
```
module floatMult16 (floatA,floatB,product);
input [15:0] floatA, floatB;
output reg [15:0] product;
reg sign;
reg signed [5:0] exponent; //6th bit is the sign 有符號的exponent
reg [9:0] mantissa;
reg [10:0] fractionA, fractionB; //fraction = {1,mantissa}
reg [21:0] fraction; //因為有尾數相成,所以尾數會變兩倍
===============特殊情況============
always @ (floatA or floatB) begin
if (floatA == 0 || floatB == 0) begin
product = 0;
end
else
=============非特殊情況============
begin
sign = floatA[15] ^ floatB[15]; //進行XOR得到符號位
exponent = floatA[14:10] + floatB[14:10] - 5'd15 + 5'd2; //後面減15(因為exp-bias,FP的bias=15),所以這裡的exp要-15才是真正的exp。加2應該是拓展位寬
fractionA = {1'b1,floatA[9:0]};
fractionB = {1'b1,floatB[9:0]};
fraction = fractionA * fractionB;
============規格化=================
if (fraction[21] == 1'b1) begin //要找mantissa 所以不要最前面的1
fraction = fraction << 1;
exponent = exponent - 1;
end else if (fraction[20] == 1'b1) begin
fraction = fraction << 2;
exponent = exponent - 2;
end else if (fraction[19] == 1'b1) begin
fraction = fraction << 3;
exponent = exponent - 3;
end else if (fraction[18] == 1'b1) begin
fraction = fraction << 4;
exponent = exponent - 4;
end else if (fraction[17] == 1'b1) begin
fraction = fraction << 5;
exponent = exponent - 5;
end else if (fraction[16] == 1'b1) begin
fraction = fraction << 6;
exponent = exponent - 6;
end else if (fraction[15] == 1'b1) begin
fraction = fraction << 7;
exponent = exponent - 7;
end else if (fraction[14] == 1'b1) begin
fraction = fraction << 8;
exponent = exponent - 8;
end else if (fraction[13] == 1'b1) begin
fraction = fraction << 9;
exponent = exponent - 9;
end else if (fraction[12] == 1'b0) begin
fraction = fraction << 10;
exponent = exponent - 10;
end
mantissa = fraction[21:12];
if(exponent[5]==1'b1) begin //exponent is negative
product=16'b0000000000000000;
end
else begin
product = {sign,exponent[4:0],mantissa};
end
end
end
endmodule
```
### (3) Convolution Unit
一個窗口卷積出一個計算結果(FP16),所以ConvUnit的作用就是**循環使用PE完成一個窗口的卷積運算,並輸出最後計算結果。**(用速度換面積)

### (2) Single Filter Layer
由RFselector與CU所組成,執行一個Filter與image的卷積操作,輸出為一個feature map

### (5) RFselector:
作用是對一個已經展開的一維image tensor進行數據重排,再對應分發給n個CU進行窗口卷積。

可以從下圖看出RFselector作為多個CU的輸入:

## (1) Multi Filter Layer
在Network固定了有兩個Layer,所以並行度為2。
這份資料有趣的地方除了把CNN拆解得很細以外,有另外一個點是他所有的模塊都進行層層分裝了,所以可以把一個模塊拆解成很多的子模塊,就可以讓我們去調整他的並行度。
那最基礎的單位就是PE,然後由PE構成CU,我們也可以去調整CU的數量,再去構建Single Filter Layer,所以在這個專案裡面每個模塊的並行度都可以自己做調整。

# SoftMax Acitivation

SoftMax是這整個網路的最後一層,輸入為FClayer生成的10個值,然後在最終的分類給出結果。
## Block diagrams


作用:SoftMax函數,將輸入標準化,求得各種類的概率。
電路邏輯:
1. 指數計算
2. 計算指數和
3. 求指數和倒數
4. 計算每個元素的SoftMax值(將指數值乘上指數和倒數)
如果你有一組數值V,Vi是V的第i個元素,則這個元素的SoftMax值為
$$
S_i = \frac{e^{z_i}}{\sum_{j} e^{z_j}}
$$
電路類型:時序邏輯電路
### SoftMax
所以SoftMax就是透過前面所述的邏輯分成幾個步驟來進行運算。
1.先將多個input分別輸入到各自的exponent來求指數。

2.然後透過加法器來求指數和。再由floatReciprocal來計算指數和的倒數。

3.最後透過乘法器來計算各輸入的SoftMax值

### (6) exponet


作用:指數函數,求解e^x值。
邏輯:用泰勒展開近似,包含兩個乘法器以及一個加法器。
電路類型:時序邏輯電路。
$$
e^x = 1 + x + \frac{x^2}{2!} + \frac{x^3}{3!} + \frac{x^4}{4!} + \frac{x^5}{5!} + \frac{x^6}{6!}
$$
# Average Pooling

在這個網路中有兩個平均池化層,作用是採集卷積層輸出,在減小數據量的時候盡可能保持數據的特徵。
## Block diagram
### (1) Average Pool Multi Layer


功能:進行多通道的image的平均池化,但這裡的併行度為1,透過循環使用AvgPollSingle完成所有通道的平均池化。
電路類型:時序邏輯電路
### (2) Average Pool Single Layer


功能:執行單個通道的平均池化,這裡為全並行度,每一個窗口都有一個AvgU
電路類型:組合邏輯電路
全並行度意謂著可以一次計算完一個窗口需要計算的數量。如下圖,若窗口為2x2,則全並行度就是可以一次計算四個,不用循環使用AvgU。所以有多少個窗口,就有多少個AvgU模塊。

### Average Unit



功能:求輸入四個數的均值
邏輯:先求和,在將和乘以0.25(Const)即為四個數的均質。
電路類型:組合邏輯電路(然後我覺得這裡可以加pipeline)
# TanH Activation
即雙曲正切函數:

以下為雙曲正切函數的公式
$$
f(x)
= \frac{\sinh x}{\cosh x}
= \frac{1 - e^{-2x}}{1 + e^{-2x}}
= \frac{e^{x} - e^{-x}}{e^{x} + e^{-x}}
= \frac{e^{2x} - 1}{e^{2x} + 1}
= 2\,\operatorname{sigmoid}(2x) - 1
$$

TahH如上圖藍線所示為其函數圖,其有以下幾個特點:
1. 函數輸出以(0,0)為中值
2. 其收斂速度比Sigmoid更快
### Block design
### (1) Using The Tanh


功能:把輸入壓縮到 [−1,1],給神經網路做非線性映射。
邏輯:把大資料 bus 拆成單一輸入 → tanh → 合併回去,並用 MUX/RESET 控制流程。
電路類型:時序邏輯電路
### (2) HyperbolicTangent




功能:算Tanh的值
邏輯:用前面給的那個公式算近似,包含三個乘法器和一個加法器。
電路類型:時序邏輯電路
# Integrating Network

將所有模塊連接在一起,調通控制通路與數據通路,是項目中最難工作量最大的一個部分
## Block Design
### (1) IntegrationFC

功能:積體的全連階層,包含兩個全連階層,TanH激活函數以及SoftMax函數層。
* Weight:儲存全連階層的權重
* Layer:進行線性運算
* TanH:激活非線性
* SoftMax:多分類
### (2) Layer

功能:進行線性計算
最主要的功能是在下面的這個always block,用來控制各個模塊的啟動,是透過Counter來決定。
```
always @(posedge clk or posedge reset) begin
if (reset == 1'b1) begin
FC1reset = 1'b1;
FC2reset = 1'b1;
TanhReset = 1'b1;
SMaxEnable = 1'b0;
counter = 0;
address1 = -1;
address2 = -1;
end
else begin
counter = counter + 1; //工作週期記數
//第一個if
if (counter > 0 && counter < IntIn + 10) begin
FC1reset = 1'b0; //啟動第一個FClayer
end
else if (counter > IntIn + 10 && counter < IntIn + 12 + FC_1_out*6) begin
TanhReset = 1'b0; //啟動Tanh
address2 = -3;
end
else if (counter > IntIn + 12 + FC_1_out*6 && counter < IntIn + 12 + FC_1_out*6 + FC_1_out + 10) begin
FC2reset = 1'b0; //啟動FC2
end
else if (counter > IntIn + 12 + FC_1_out*6 + FC_1_out + 10) begin
SMaxEnable = 1'b1; //啟動SoftMax
end
if (address1 != 8'hfe) begin
address1 = address1 + 1;
end
else
address1 = 8'hfe;
address2 = address2 + 1;
end
end
endmodule
```