# INTRO 這份筆記是從 [CNN-FPGA](https://github.com/omarelhedaby/CNN-FPGA) 這份開源資料中整理而得的。 整理了如何用Verilog實現CNN的幾個步驟,原版作者已經將內容發布成一篇論文,也將內容整理成了一本書。如果想更深入的探討可以再從作者的官方github中探討。 這份筆記的大多數圖片與內容都出自於這作者出的這本書: ![image](https://hackmd.io/_uploads/r1hAwAPtxx.png) # Network 首先要先知道一個CNN是包含哪幾層,以下是整個網路的結構: ![image](https://hackmd.io/_uploads/SycB_0wFeg.png) * 有三層卷積層(CONV) * 兩層平均池化層(avgPool) * 每層卷積層後面都有一個激活函數(Tanh) * 再來還有兩層全連階層(FClayer) * 最後再一個SoftMax函數輸出一個結果 所用的數據集是MNIST # Convolution 以下先介紹卷積層,是以綠色標出來的模塊: ![image](https://hackmd.io/_uploads/B1qAtCvKxl.png) 實現出來的方案在並行度、速度以及資源占用中進行了一些權衡。 在vivado中打開後可以看到這個畫面 ![image](https://hackmd.io/_uploads/B1vh6NuYel.png) 從這裡就看的出來他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 ![image](https://hackmd.io/_uploads/ryGR1SdFxx.png) ![image](https://hackmd.io/_uploads/H128-rdtgl.png =300x) ### (2) Single Filter Layer 單個filter,輸入image,輸出feature map。 ![image](https://hackmd.io/_uploads/Syw9fBdKgx.png) ![image](https://hackmd.io/_uploads/Skk3MBOKlx.png =300x) ### (3) Convolutional Unit 每個卷積核與輸入圖像進行卷積,得到特徵圖。 ![image](https://hackmd.io/_uploads/HyVFXBOtlg.png) ![image](https://hackmd.io/_uploads/BJ4s7HOKee.png =300x) ### (4) Processing Element 卷積的具體操作是內積(dot product),本質是乘法與加法。這裡輸入的是float,硬體實現也就是定點數。 ![image](https://hackmd.io/_uploads/SyTCmruYgl.png) ![image](https://hackmd.io/_uploads/ryvU4BdYxe.png =300x) ## 功能解析 ### (4) Processing Element ![image](https://hackmd.io/_uploads/HylR8r_Kxe.png) * floatMult16:兩個半精度浮點數的乘法 * floatADD16:兩個半精度浮點數的加法 半經度浮點數(16bits),簡寫為FP16: * 符號位1bit * 指數位5bits * 尾數位10bits ![image](https://hackmd.io/_uploads/Skl3vruYxg.png) $$ \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完成一個窗口的卷積運算,並輸出最後計算結果。**(用速度換面積) ![image](https://hackmd.io/_uploads/SyJsFSOFxe.png) ### (2) Single Filter Layer 由RFselector與CU所組成,執行一個Filter與image的卷積操作,輸出為一個feature map ![image](https://hackmd.io/_uploads/Syw9fBdKgx.png) ### (5) RFselector: 作用是對一個已經展開的一維image tensor進行數據重排,再對應分發給n個CU進行窗口卷積。 ![image](https://hackmd.io/_uploads/HkTQk8_Yxl.png) 可以從下圖看出RFselector作為多個CU的輸入: ![image](https://hackmd.io/_uploads/rJjBkL_Fle.png) ## (1) Multi Filter Layer 在Network固定了有兩個Layer,所以並行度為2。 這份資料有趣的地方除了把CNN拆解得很細以外,有另外一個點是他所有的模塊都進行層層分裝了,所以可以把一個模塊拆解成很多的子模塊,就可以讓我們去調整他的並行度。 那最基礎的單位就是PE,然後由PE構成CU,我們也可以去調整CU的數量,再去構建Single Filter Layer,所以在這個專案裡面每個模塊的並行度都可以自己做調整。 ![image](https://hackmd.io/_uploads/ryGR1SdFxx.png) # SoftMax Acitivation ![image](https://hackmd.io/_uploads/B1I1aADKxe.png) SoftMax是這整個網路的最後一層,輸入為FClayer生成的10個值,然後在最終的分類給出結果。 ## Block diagrams ![image](https://hackmd.io/_uploads/S1Rl4LOteg.png) ![image](https://hackmd.io/_uploads/r1ftN8_Yxg.png) 作用: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來求指數。 ![image](https://hackmd.io/_uploads/rkwgtIOYgx.png) 2.然後透過加法器來求指數和。再由floatReciprocal來計算指數和的倒數。 ![image](https://hackmd.io/_uploads/HJrdFLutxx.png) 3.最後透過乘法器來計算各輸入的SoftMax值 ![image](https://hackmd.io/_uploads/HyYkqLuYxl.png) ### (6) exponet ![image](https://hackmd.io/_uploads/S1m0qUdFll.png) ![image](https://hackmd.io/_uploads/S1qnqLutll.png) 作用:指數函數,求解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 ![image](https://hackmd.io/_uploads/By8SpCwYxg.png) 在這個網路中有兩個平均池化層,作用是採集卷積層輸出,在減小數據量的時候盡可能保持數據的特徵。 ## Block diagram ### (1) Average Pool Multi Layer ![image](https://hackmd.io/_uploads/BksA4vOKxl.png) ![image](https://hackmd.io/_uploads/HyKgHPutlg.png =300x) 功能:進行多通道的image的平均池化,但這裡的併行度為1,透過循環使用AvgPollSingle完成所有通道的平均池化。 電路類型:時序邏輯電路 ### (2) Average Pool Single Layer ![image](https://hackmd.io/_uploads/rkp_tYdYel.png) ![image](https://hackmd.io/_uploads/r19KFF_tgx.png =300x) 功能:執行單個通道的平均池化,這裡為全並行度,每一個窗口都有一個AvgU 電路類型:組合邏輯電路 全並行度意謂著可以一次計算完一個窗口需要計算的數量。如下圖,若窗口為2x2,則全並行度就是可以一次計算四個,不用循環使用AvgU。所以有多少個窗口,就有多少個AvgU模塊。 ![image](https://hackmd.io/_uploads/SkhJM9dKxx.png) ### Average Unit ![image](https://hackmd.io/_uploads/rJxsG5OKxe.png) ![image](https://hackmd.io/_uploads/r123GcdFgl.png) ![image](https://hackmd.io/_uploads/HkICfcdFlx.png) 功能:求輸入四個數的均值 邏輯:先求和,在將和乘以0.25(Const)即為四個數的均質。 電路類型:組合邏輯電路(然後我覺得這裡可以加pipeline) # TanH Activation 即雙曲正切函數: ![image](https://hackmd.io/_uploads/BkxcDqCvKgx.png) 以下為雙曲正切函數的公式 $$ 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 $$ ![image](https://hackmd.io/_uploads/HJshj0PKge.png) TahH如上圖藍線所示為其函數圖,其有以下幾個特點: 1. 函數輸出以(0,0)為中值 2. 其收斂速度比Sigmoid更快 ### Block design ### (1) Using The Tanh ![image](https://hackmd.io/_uploads/HJMDMs_Kex.png =300x) ![image](https://hackmd.io/_uploads/SJvbNouFxg.png) 功能:把輸入壓縮到 [−1,1],給神經網路做非線性映射。 邏輯:把大資料 bus 拆成單一輸入 → tanh → 合併回去,並用 MUX/RESET 控制流程。 電路類型:時序邏輯電路 ### (2) HyperbolicTangent ![image](https://hackmd.io/_uploads/r1jWri_Ylx.png =300x) ![image](https://hackmd.io/_uploads/Sy87SodFgg.png) ![image](https://hackmd.io/_uploads/B1EIHjutgg.png) ![image](https://hackmd.io/_uploads/HJHDSjdKgg.png) 功能:算Tanh的值 邏輯:用前面給的那個公式算近似,包含三個乘法器和一個加法器。 電路類型:時序邏輯電路 # Integrating Network ![image](https://hackmd.io/_uploads/rku360PKxg.png) 將所有模塊連接在一起,調通控制通路與數據通路,是項目中最難工作量最大的一個部分 ## Block Design ### (1) IntegrationFC ![image](https://hackmd.io/_uploads/SkxKkiOKll.png) 功能:積體的全連階層,包含兩個全連階層,TanH激活函數以及SoftMax函數層。 * Weight:儲存全連階層的權重 * Layer:進行線性運算 * TanH:激活非線性 * SoftMax:多分類 ### (2) Layer ![image](https://hackmd.io/_uploads/SJVOejOYxl.png) 功能:進行線性計算 最主要的功能是在下面的這個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 ```