FC2カウンター FPGAの部屋 DNN

FPGAやCPLDの話題やFPGA用のツールの話題などです。 マニアックです。 日記も書きます。

FPGAの部屋

FPGAの部屋の有用と思われるコンテンツのまとめサイトを作りました。Xilinx ISEの初心者の方には、FPGAリテラシーおよびチュートリアルのページをお勧めいたします。

手書き数字認識用畳み込みニューラルネットワーク回路の製作2(手書き数字認識用四角枠表示回路)

手書き数字認識用畳み込みニューラルネットワーク回路の製作1(概要)”の続き。

前回は、カメラで撮影した画像から手書き数字を指定し、手書き数字認識用畳み込みニューラルネットワークを使用して認識する回路の概要を解説した。今回は、そのパーツとなる手書き数字認識用四角枠表示回路をVivado HLS を使用して作ってみよう。

手書き数字認識用四角枠表示回路は、ある一定の幅と高さのピンクの枠を画像に追加するIP となっている。入出力はAXI4-Stream で、引数はAXI4-Lile でレジスタにマップされている。
ヘッダファイルの square_frame_gen.h を示す。

// square_frame_gen.h
// 2017/06/28 by marsee
//

#ifndef __SQUARE_FRAME_GEN_H__
#define __SQUARE_FRAME_GEN_H__

#define HORIZONTAL_PIXEL_WIDTH    800
#define VERTICAL_PIXEL_WIDTH    600

#define ALL_PIXEL_VALUE    (HORIZONTAL_PIXEL_WIDTH*VERTICAL_PIXEL_WIDTH)

#endif


ソースコードのsquare_frame_gen.cpp を示す。

// square_frame_gen.cpp
// 2017/06/28 by marsee
//

#include <ap_int.h>
#include <hls_stream.h>
#include <ap_axi_sdata.h>

#include "square_frame_gen.h"

// AXI4-Streamで流れてきた画像に1ピクセルの幅の四角い枠を書く
// x_pos : 四角枠の内側の左上のx位置
// y_pos : 四角枠の内側の左上のy位置
// width : 四角枠内側のピクセルの幅
// height : 四角枠内側のピクセルの高さ
// off_on : 四角枠OFF - 0, ON - 1
int square_frame_gen(hls::stream<ap_axis<32,1,1,1> >& ins,
        hls::stream<ap_axis<32,1,1,1> >& outs,
        int x_pos, int y_pos, int width, int height, int off_on){
#pragma HLS INTERFACE s_axilite port=off_on
#pragma HLS INTERFACE s_axilite port=width
#pragma HLS INTERFACE s_axilite port=x_pos
#pragma HLS INTERFACE s_axilite port=height
#pragma HLS INTERFACE s_axilite port=y_pos
#pragma HLS INTERFACE s_axilite port=return
#pragma HLS INTERFACE axis register both port=outs
#pragma HLS INTERFACE axis register both port=ins

    ap_axis<32,1,1,1> pix;
    int gray_pix, val, i, j, x, y;

    do {
#pragma HLS LOOP_TRIPCOUNT min=1 max=1 avg=1
    // user が 1になった時にフレームがスタートする
        ins >> pix;
    } while(pix.user == 0);

    for (y=0; y<VERTICAL_PIXEL_WIDTH; y++){
        for (x=0; x<HORIZONTAL_PIXEL_WIDTH; x++){
#pragma HLS PIPELINE II=1
            if (!(x==0 && y==0))    // 最初の入力はすでに入力されている
                ins >> pix;    // AXI4-Stream からの入力

            if (off_on){
                if (y==y_pos-1 && x>=x_pos-1 && x<=(x_pos+width+1)) // 上の枠線
                    pix.data = (0xff << 16)+0xff; // R = 0xff, B = 0xff, pink
                else if (y>=y_pos-1 && y<=y_pos+height+1 && (x==x_pos-1 || x==x_pos+width+1)) // 横枠線
                    pix.data = (0xff << 16)+0xff; // R = 0xff, B = 0xff, pink
                else if (y==y_pos+width+1 && x>=x_pos-1 && x<=(x_pos+width+1)) // 下の枠線
                    pix.data = (0xff << 16)+0xff; // R = 0xff, B = 0xff, pink
            }

            outs << pix;
        }
    }

    return(0);
}


Vivado HLS 2016.4 で square_frame_gen プロジェクトを作った。
hand_draw_num_44_170629.png

C シミュレーションを行った。
hand_draw_num_45_170629.png

画像の真ん中を指定したので、四角枠線が書かれている。中のサイズは 28 x 28 ピクセルだ。
hand_draw_num_46_170629.jpg

C コードの合成を行った。結果を示す。
hand_draw_num_47_170629.png

レイテンシは 480005 クロックで、画像サイズは 800 x 600 = 480000 なので、ほぼ1クロックで 1 ピクセルの処理が行えることになる。
AXI4-Lile でのレジスタ・マップを貼っておく。

//------------------------Address Info-------------------
// 0x00 : Control signals
//        bit 0  - ap_start (Read/Write/COH)
//        bit 1  - ap_done (Read/COR)
//        bit 2  - ap_idle (Read)
//        bit 3  - ap_ready (Read)
//        bit 7  - auto_restart (Read/Write)
//        others - reserved
// 0x04 : Global Interrupt Enable Register
//        bit 0  - Global Interrupt Enable (Read/Write)
//        others - reserved
// 0x08 : IP Interrupt Enable Register (Read/Write)
//        bit 0  - Channel 0 (ap_done)
//        bit 1  - Channel 1 (ap_ready)
//        others - reserved
// 0x0c : IP Interrupt Status Register (Read/TOW)
//        bit 0  - Channel 0 (ap_done)
//        bit 1  - Channel 1 (ap_ready)
//        others - reserved
// 0x10 : Data signal of ap_return
//        bit 31~0 - ap_return[31:0] (Read)
// 0x18 : Data signal of x_pos
//        bit 31~0 - x_pos[31:0] (Read/Write)
// 0x1c : reserved
// 0x20 : Data signal of y_pos
//        bit 31~0 - y_pos[31:0] (Read/Write)
// 0x24 : reserved
// 0x28 : Data signal of width
//        bit 31~0 - width[31:0] (Read/Write)
// 0x2c : reserved
// 0x30 : Data signal of height
//        bit 31~0 - height[31:0] (Read/Write)
// 0x34 : reserved
// 0x38 : Data signal of off_on
//        bit 31~0 - off_on[31:0] (Read/Write)
// 0x3c : reserved
// (SC = Self Clear, COR = Clear on Read, TOW = Toggle on Write, COH = Clear on Handshake)


次に、C/RTL協調シミュレーションを行った。
hand_draw_num_48_170629.png

C/RTL協調シミュレーション波形を示す。まずは全体の波形。
hand_draw_num_49_170629.png

最初のAXI4-Lile でのレジスタ設定時の波形を貼っておく。
hand_draw_num_50_170629.png

WDATA は10進数にしてある。x_pos に400、y_pos に 300 、四角枠の幅と高さに 28 を設定するのが分かると思う。

Export RTL を行った。
hand_draw_num_51_170629.png

インプリメントしてもタイミングは満たされているようだ。

最後に、テストベンチのsquare_frame_gen_tb.cpp を貼っておく。

// square_frame_gen_tb.cpp
// 2017/06/28 by marsee
//

#include <stdio.h>
#include <stdlib.h>
#include <ap_int.h>
#include <hls_stream.h>
#include <ap_axi_sdata.h>

#include "square_frame_gen.h"
#include "bmp_header.h"

int square_frame_gen(hls::stream<ap_axis<32,1,1,1> >& ins,
        hls::stream<ap_axis<32,1,1,1> >& outs,
        int x_pos, int y_pos, int width, int height, int off_on);

#define READ_BMP_FILE_NAME    "bmp_file0.bmp"
#define WRITE_BMP_FILE_NAME    "square_frame_gen.bmp"
#define X_POS    400
#define Y_POS    300
#define WIDTH    28
#define HEIGHT    28
#define ON        1

int main(){
    using namespace std;

    hls::stream<ap_axis<32,1,1,1> > ins;
    hls::stream<ap_axis<32,1,1,1> > outs;

    ap_axis<32,1,1,1> pix;
    ap_axis<32,1,1,1> vals;

    BITMAPFILEHEADER bmpfhr; // BMPファイルのファイルヘッダ(for Read)
    BITMAPINFOHEADER bmpihr; // BMPファイルのINFOヘッダ(for Read)
    FILE *fbmpr, *fbmpw;
    int *rd_bmp, *seq_frm;
    int blue, green, red;

    if ((fbmpr = fopen(READ_BMP_FILE_NAME, "rb")) == NULL){ // test.bmp をオープン
        fprintf(stderr, "Can't open ");
        fprintf(stderr, READ_BMP_FILE_NAME);
        fprintf(stderr, " by binary read mode\n");
        exit(1);
    }
    // bmpヘッダの読み出し
    fread(&bmpfhr.bfType, sizeof(uint16_t), 1, fbmpr);
    fread(&bmpfhr.bfSize, sizeof(uint32_t), 1, fbmpr);
    fread(&bmpfhr.bfReserved1, sizeof(uint16_t), 1, fbmpr);
    fread(&bmpfhr.bfReserved2, sizeof(uint16_t), 1, fbmpr);
    fread(&bmpfhr.bfOffBits, sizeof(uint32_t), 1, fbmpr);
    fread(&bmpihr, sizeof(BITMAPINFOHEADER), 1, fbmpr);

    // ピクセルを入れるメモリをアロケートする
    if ((rd_bmp =(int *)malloc(sizeof(int) * (bmpihr.biWidth * bmpihr.biHeight))) == NULL){
        fprintf(stderr, "Can't allocate rd_bmp memory\n");
        exit(1);
    }
    if ((seq_frm =(int *)malloc(sizeof(int) * (bmpihr.biWidth * bmpihr.biHeight))) == NULL){
        fprintf(stderr, "Can't allocate seq_frm memory\n");
        exit(1);
    }

    // rd_bmp にBMPのピクセルを代入。その際に、行を逆転する必要がある
    for (int y=0; y<bmpihr.biHeight; y++){
        for (int x=0; x<bmpihr.biWidth; x++){
            blue = fgetc(fbmpr);
            green = fgetc(fbmpr);
            red = fgetc(fbmpr);
            rd_bmp[((bmpihr.biHeight-1)-y)*bmpihr.biWidth+x] = (blue & 0xff) | ((green & 0xff)<<8) | ((red & 0xff)<<16);
        }
    }
    fclose(fbmpr);

    // ins に入力データを用意する
    for(int i=0; i<5; i++){    // dummy data
        pix.user = 0;
        pix.data = i;
        ins << pix;
    }

    // BMP画像をins に入力する
    for(int j=0; j < bmpihr.biHeight; j++){
        for(int i=0; i < bmpihr.biWidth; i++){
            pix.data = (ap_int<32>)rd_bmp[(j*bmpihr.biWidth)+i];

            if (j==0 && i==0)    // 最初のデータの時に TUSER を 1 にする
                pix.user = 1;
            else
                pix.user = 0;

            if (i == bmpihr.biWidth-1// 行の最後でTLASTをアサートする
                pix.last = 1;
            else
                pix.last = 0;

            ins << pix;
        }
    }


    square_frame_gen(ins, outs, X_POS, Y_POS, WIDTH, HEIGHT, ON);

    // 書き出すファイルをオープン
    if ((fbmpw=fopen(WRITE_BMP_FILE_NAME, "wb")) == NULL){
        fprintf(stderr, "Can't open ");
        fprintf(stderr, WRITE_BMP_FILE_NAME);
        fprintf(stderr, " by binary write mode\n");
        exit(1);
    }

    // BMPファイルヘッダの書き込み
    fwrite(&bmpfhr.bfType, sizeof(uint16_t), 1, fbmpw);
    fwrite(&bmpfhr.bfSize, sizeof(uint32_t), 1, fbmpw);
    fwrite(&bmpfhr.bfReserved1, sizeof(uint16_t), 1, fbmpw);
    fwrite(&bmpfhr.bfReserved2, sizeof(uint16_t), 1, fbmpw);
    fwrite(&bmpfhr.bfOffBits, sizeof(uint32_t), 1, fbmpw);
    fwrite(&bmpihr, sizeof(BITMAPINFOHEADER), 1, fbmpw);

    for(int j=0; j < bmpihr.biHeight; j++){
        for(int i=0; i < bmpihr.biWidth; i++){
            outs >> vals;
            ap_int<32> val = vals.data;

            seq_frm[(j*bmpihr.biWidth)+i] = (int)val;
        }
    }

    // RGB データの書き込み、逆順にする
    for (int y=0; y<bmpihr.biHeight; y++){
        for (int x=0; x<bmpihr.biWidth; x++){
            blue = seq_frm[((bmpihr.biHeight-1)-y)*bmpihr.biWidth+x] & 0xff;
            green = (seq_frm[((bmpihr.biHeight-1)-y)*bmpihr.biWidth+x] >> 8) & 0xff;
            red = (seq_frm[((bmpihr.biHeight-1)-y)*bmpihr.biWidth+x]>>16) & 0xff;

            fputc(blue, fbmpw);
            fputc(green, fbmpw);
            fputc(red, fbmpw);
        }
    }
    fclose(fbmpw);

    return(0);
}

  1. 2017年06月29日 04:54 |
  2. DNN
  3. | トラックバック:0
  4. | コメント:0

手書き数字認識用畳み込みニューラルネットワーク回路の製作1(概要)

手書き数字認識用畳み込みニューラルネットワークができたので、カメラで撮影した画像の中の手書き数字を認識してみようと思う。
使用するボードはPYNQボードとする。以前に、PYNQボードでFASTX コーナー検出IP を使用したプロジェクトがあるので、それをベースとする。

ブロック図を下に示す。
hand_draw_num_43_170627.png

上の図では、AXI4-Stream Switcher が2つ使用されているが、四角枠が出っ放しでよければ、この2 つのAXI4-Stream Switcher は除くかもしれない。
カメラからの入力をAXI4-Stream で出力しているが、それに手書き数字認識用四角枠表示回路で28 x 28 ピクセルの四角枠を書く。どの位置に四角枠を書いたか?の位置は、レジスタにマップされてAXI4-Lile で読めるようにしておく。
AXI4 Master インターフェースに手書き数字認識用畳み込みニューラルネットワークIP がつながっている。プロセッサ経由で手書き数字認識用四角枠表示回路から位置情報をもらって、四角枠の中のピクセルデータをAXI4 Master インターフェースでDMA してくる。手書き数字認識用畳み込みニューラルネットワークIP も四角枠をDMA するので、入力の記述を変更する必要がある。書き換えよう。
例を示す。ピンクの四角枠で囲われている中の手書き数字を手書き数字認識用畳み込みニューラルネットワークで認識する。
hand_draw_num_40_170627.jpg

四角枠は手書き数字認識用四角枠表示回路で、表示位置 X, Y をレジスタで持っていてその位置に描画するようにしよう。
人とのインターフェースだが、キーを押したら四角枠が動いていくようにしたい。
  1. 2017年06月28日 05:19 |
  2. DNN
  3. | トラックバック:0
  4. | コメント:0

MNISTの畳み込みニューラルネットワークで自分の手書き数字を認識する3(カメラ画像)

MNISTの畳み込みニューラルネットワークで自分の手書き数字を認識する2”の続き。

前回はGIMP2 で幅 2 ピクセルの鉛筆ツールを使用して書いた手書き数字を認識させてみた。今回は、手書き数字をカメラで撮影した画像を切り出して認識させてみよう。

まずは、久しぶりにZYBO をブートして、”Zybot のカメラ画像をBMPファイルに変換するアプリケーションを作成した”の cam_capture_bmp.cpp をコンパイルして、実行ファイルの cam_capture_bmp を生成した。
cam_capture_bmp を起動して、BMP画像を取得した。
hand_draw_num_32_170627.png

取得した bmp_file0.bmp を示す。表示ツールは gthumb を使用している。
hand_draw_num_33_170627.png

WinSCP を使用して、bmp_file0.bmp をZYBOからパソコンに持ってきた。
GIMP2 を立ち上げて、手書き数字を切り出して 28 x 28 のBMPファイルにした。
hand_draw_num_34_170627.png

0 (test0.bmp) の画像を示す。
hand_draw_num_35_170627.png

手書き数字が大きいときは、32 x 32 位に切り取って、GIMP2 の「画像」メニューの「画像の拡大・縮小」を選択して、28 x 28 に縮小する。
BMPファイルにエクスポートするときは、「互換性のオプション」の「色空間の情報を書き込まない」にチェックを入れて、「詳細設定」は「24 ビット R8 G8 B8」のラジオボタンをクリックしてから、エクスポートボタンをクリックする。
hand_draw_num_36_170627.png

必要なBMPファイルができた。
hand_draw_num_37_170627.png

VirtualBox 上のUbuntu 16.04 のVivado HLS 2016.4 に持って行った。
hand_draw_num_38_170627.png

Vivado HLS で C シミュレーションを行った。
hand_draw_num_39_170627.png

レポートを示す。

INFO: [SIM 2] *************** CSIM start ***************
INFO: [SIM 4] CSIM will launch GCC as the compiler.
Compiling ../../../mnist_conv_nn_tb_bmp.cpp in debug mode
Compiling ../../../mnist_conv_nn10.cpp in debug mode
Generating csim.exe
test0.bmp
id = 0, max_id_hw = 0
id = 0, max_id_sw = 0
test1.bmp
id = 1, max_id_hw = 1
id = 1, max_id_sw = 1
test2.bmp
id = 2, max_id_hw = 3
id = 2, max_id_sw = 2
test3.bmp
id = 3, max_id_hw = 3
id = 3, max_id_sw = 3
test4.bmp
id = 4, max_id_hw = 4
id = 4, max_id_sw = 4
test5.bmp
id = 5, max_id_hw = 5
id = 5, max_id_sw = 5
test6.bmp
id = 6, max_id_hw = 6
id = 6, max_id_sw = 6
test7.bmp
id = 7, max_id_hw = 7
id = 7, max_id_sw = 7
test8.bmp
id = 8, max_id_hw = 8
id = 8, max_id_sw = 8
test9.bmp
id = 9, max_id_hw = 4
id = 9, max_id_sw = 4
INFO: [SIM 1] CSim done with 0 errors.
INFO: [SIM 3] *************** CSIM finish ***************


ハードウェアでは 2 が 3 に間違っていて、ソフトウェア、ハードウェア共に、9 を 4 に間違っていた。
まあ、精度はいまいちだが、一応、カメラで撮影した手書き数字を認識できているようなので、本格的にPYNQボードに組み込んで、カメラで撮影した画像から手書き数字を認識させてみようかな?
  1. 2017年06月27日 05:13 |
  2. DNN
  3. | トラックバック:0
  4. | コメント:0

MNISTの畳み込みニューラルネットワークで自分の手書き数字を認識する2

MNISTの畳み込みニューラルネットワークで自分の手書き数字を認識する1”の続き。

前回はお絵描きソフトのPaint.net でブラシ幅 2 のペイントブラシ・ツールを使用して書いた手書き数字を認識してみたが、認識精度はいまいちだった。今回は、GIMP2 で幅 2 ピクセルの鉛筆ツールを使用して書いた手書き数字を認識させてみようと思う。

最初にGIMP2 で幅 2 ピクセルの鉛筆ツールを使用して 0 ~ 9 までの数字を書いた。
hand_draw_num_16_170626.png

hand_draw_num_17_170626.png

hand_draw_num_18_170626.png

hand_draw_num_19_170626.png

hand_draw_num_20_170626.png

hand_draw_num_21_170626.png

hand_draw_num_22_170626.png

hand_draw_num_23_170626.png

hand_draw_num_24_170626.png

hand_draw_num_25_170626.png

hand_draw_num_26_170626.png

これを前回のmnist_conv_nn10_bmp プロジェクトに入れる。
hand_draw_num_27_170626.png

早速、C シミュレーションを行った。
hand_draw_num_28_170626.png

INFO: [SIM 2] *************** CSIM start ***************
INFO: [SIM 4] CSIM will launch GCC as the compiler.
make: 'csim.exe' は更新済みです.
test0.bmp
id = 0, max_id_hw = 0
id = 0, max_id_sw = 6
test1.bmp
id = 1, max_id_hw = 5
id = 1, max_id_sw = 5
test2.bmp
id = 2, max_id_hw = 2
id = 2, max_id_sw = 2
test3.bmp
id = 3, max_id_hw = 3
id = 3, max_id_sw = 3
test4.bmp
id = 4, max_id_hw = 4
id = 4, max_id_sw = 4
test5.bmp
id = 5, max_id_hw = 5
id = 5, max_id_sw = 5
test6.bmp
id = 6, max_id_hw = 5
id = 6, max_id_sw = 5
test7.bmp
id = 7, max_id_hw = 7
id = 7, max_id_sw = 7
test8.bmp
id = 8, max_id_hw = 8
id = 8, max_id_sw = 8
test9.bmp
id = 9, max_id_hw = 2
id = 9, max_id_sw = 2
INFO: [SIM 1] CSim done with 0 errors.
INFO: [SIM 3] *************** CSIM finish ***************


前回よりも間違いが多い。1 は 5 に間違っているし、6 も 5 に間違っている。9 は 2 に間違っている。
こうしてみると、私の書く 6 はMNISTの 6 と違うのかもしれない? 6 の丸の上の線がもう少し下がっていなくてはいけないのかもしれない。
もう一度、GIMP2 で 6 を書いてみた。
hand_draw_num_29_170626.png

mnist_conv_nn10_bmp プロジェクトを示す。
hand_draw_num_30_170626.png

C シミュレーションを行った。
hand_draw_num_31_170626.png

今度は 6 が 3 として認識されてしまった。どう書けば良いのだろうか?

自分でマウスで書いた手書き数字を認識するのは難しい様だ。自分で本当に紙に書いた手書き数字をカメラで撮影した画像で認識させたらどうなのだろうか?
  1. 2017年06月26日 04:43 |
  2. DNN
  3. | トラックバック:0
  4. | コメント:0

MNISTの畳み込みニューラルネットワークで自分の手書き数字を認識する1

「ゼロから作るDeep Learning」の畳み込みニューラルネットワークのハードウェア化8(性能向上を図る)”までで、「ゼロから作るDeep Learning」の畳み込みニューラルネットワークのハードウェア化は完了した。しかし、手書き数字を認識するとは言ってもMNISTの手書き数字を認識する精度が 99 % と言っても自分の手書き数字を認識できたわけではない。ということで、自分で手描きの数字を書いて認識させてみることにした。

手描き数字は、お絵描きソフトのPaint.net で書いた。書いたのはブラシ幅 2 のペイントブラシ・ツールだ。
hand_draw_num_1_170625.png

これだけだと、今まで使ってきたBMPファイル読み込みルーチンで読み込めないので、GIMP2 で「色空間の情報を書き込まない」にチェックを入れて、24ビット R8 G8 B8 でエクスポートした。
hand_draw_num_2_170625.png

これで、0 ~ 9 までの手書き数字を作成した。(temp0.bmp ~ temp9.bmp)
hand_draw_num_3_170625.png

hand_draw_num_4_170625.png

hand_draw_num_5_170625.png

hand_draw_num_6_170625.png

hand_draw_num_7_170625.png

hand_draw_num_8_170625.png

hand_draw_num_9_170625.png

hand_draw_num_10_170625.png

hand_draw_num_11_170625.png

hand_draw_num_12_170625.png

VirtualBox 上のUbuntu 16.04 に mnist_conv_nn10_bmp という名前のVivado HLS 2016.4 プロジェクトを作成して、mnist_conv_nn_tb_bmp.cpp テストベンチを作成した。ソースコードは以前と変わりは無い。
hand_draw_num_13_170625.png

hand_draw_num_14_170625.png

これで C シミュレーションを行った。結果を示す。
hand_draw_num_15_170625.png

大体行けてはいるのだが、精度はより低くなっている感じだ。特に 6 が 5 と誤認識されている。
MNISTデータセットと書いた人が違うのもあるが、ブラシ幅 2 のペイントブラシ・ツールで書いたという環境の違いなどがあると思う。汎化能力が足りないということも言えるが環境が違うので精度が落ちるのは仕方がないところだろう。精度を上げるには、ブラシ幅 2 のペイントブラシ・ツールで書いた6万枚くらいの手書き数字を集める必要があるのかもしれない?
もう一度、他のツールで書いた手書き数字で確かめてみよう。

最後に、mnist_conv_nn_tb_bmp.cpp を貼っておく。

// mnist_conv_nn_tb_bmp.cpp
// 2017/06/14 by marsee
// 2017/06/21 : test0.bmp ~ test9.bmpファイルを読み込んで結果を出力する
//

#include <stdio.h>
#include <ap_fixed.h>
#include <string.h>

#include "conv1_weight.h"
#include "conv1_bias.h"
#include "af1_weight.h"
#include "af1_bias.h"
#include "af2_weight.h"
#include "af2_bias.h"

#include "bmp_header.h"


int mnist_conv_nn(ap_ufixed<80, AP_TRN_ZERO, AP_SAT> in[784], ap_fixed<127, AP_TRN_ZERO, AP_SAT> out[10]);
int mnist_conv_nn_float(float in[784], float out[10]);
int max_ap_fixed(ap_fixed<127, AP_TRN_ZERO, AP_SAT> out[10]);
int max_float(float out[10]);
int conv_rgb2y(int rgb);

#define NUM_ITERATIONS    10 // C Simulation

int main(){
    ap_fixed<127, AP_TRN_ZERO, AP_SAT> result_ap_fixed[NUM_ITERATIONS][10];
    float result_float[NUM_ITERATIONS][10];
    int max_id_hw, max_id_sw, max_id_ref;
    BITMAPFILEHEADER bmpfhr; // BMPファイルのファイルヘッダ(for Read)
    BITMAPINFOHEADER bmpihr; // BMPファイルのINFOヘッダ(for Read)
    char namestr[100];
    FILE *fbmpr;
    int *rd_bmp;
    float *rd_bmpf;
    ap_ufixed<80, AP_TRN_ZERO, AP_SAT> *rd_bmp_apf;
    int blue, green, red;

    for(int i=0; i<NUM_ITERATIONS; i++){
        sprintf(namestr, "test%d.bmp", i);
        printf("%s\n", namestr);
        if ((fbmpr = fopen(namestr, "rb")) == NULL){ // test??.bmp をオープン
            fprintf(stderr, "Can't open test%d.bmp by binary read mode\n", i);
            exit(1);
        }
        // bmpヘッダの読み出し
        fread(&bmpfhr.bfType, sizeof(uint16_t), 1, fbmpr);
        fread(&bmpfhr.bfSize, sizeof(uint32_t), 1, fbmpr);
        fread(&bmpfhr.bfReserved1, sizeof(uint16_t), 1, fbmpr);
        fread(&bmpfhr.bfReserved2, sizeof(uint16_t), 1, fbmpr);
        fread(&bmpfhr.bfOffBits, sizeof(uint32_t), 1, fbmpr);
        fread(&bmpihr, sizeof(BITMAPINFOHEADER), 1, fbmpr);

        // ピクセルを入れるメモリをアロケートする
        if ((rd_bmp =(int *)malloc(sizeof(int) * (bmpihr.biWidth * bmpihr.biHeight))) == NULL){
            fprintf(stderr, "Can't allocate rd_bmp memory\n");
            exit(1);
        }
        if ((rd_bmpf =(float *)malloc(sizeof(float) * (bmpihr.biWidth * bmpihr.biHeight))) == NULL){
            fprintf(stderr, "Can't allocate rd_bmpf memory\n");
            exit(1);
        }
        if ((rd_bmp_apf =(ap_ufixed<80, AP_TRN_ZERO, AP_SAT> *)malloc(sizeof(ap_ufixed<80, AP_TRN_ZERO, AP_SAT>) * (bmpihr.biWidth * bmpihr.biHeight))) == NULL){
            fprintf(stderr, "Can't allocate rd_bmp_apf memory\n");
            exit(1);
        }

        // rd_bmp にBMPのピクセルを代入。その際に、行を逆転する必要がある
        for (int y=0; y<bmpihr.biHeight; y++){
            for (int x=0; x<bmpihr.biWidth; x++){
                blue = fgetc(fbmpr);
                green = fgetc(fbmpr);
                red = fgetc(fbmpr);
                rd_bmp[((bmpihr.biHeight-1)-y)*bmpihr.biWidth+x] = (blue & 0xff) | ((green & 0xff)<<8) | ((red & 0xff)<<16);
                rd_bmp[((bmpihr.biHeight-1)-y)*bmpihr.biWidth+x] = conv_rgb2y(rd_bmp[((bmpihr.biHeight-1)-y)*bmpihr.biWidth+x]);

                rd_bmpf[((bmpihr.biHeight-1)-y)*bmpihr.biWidth+x] = 1.0 - (float)rd_bmp[((bmpihr.biHeight-1)-y)*bmpihr.biWidth+x]/256.0; // 白地に黒文字から黒字に白文字に変換
                //if (rd_bmpf[((bmpihr.biHeight-1)-y)*bmpihr.biWidth+x] < 0.01)
                    //rd_bmpf[((bmpihr.biHeight-1)-y)*bmpihr.biWidth+x] = 0.0;

                rd_bmp_apf[((bmpihr.biHeight-1)-y)*bmpihr.biWidth+x] = (ap_ufixed<80, AP_TRN_ZERO, AP_SAT>)rd_bmpf[((bmpihr.biHeight-1)-y)*bmpihr.biWidth+x];
            }
        }
        fclose(fbmpr);

        /* for (int y=0; y<bmpihr.biHeight; y++){            for (int x=0; x<bmpihr.biWidth; x++){                printf("%f ", (float)rd_bmp_apf[y*bmpihr.biWidth+x]);            }            printf("\n");        }        printf("\n"); */

        mnist_conv_nn(rd_bmp_apf, &result_ap_fixed[i][0]);
        mnist_conv_nn_float(rd_bmpf, &result_float[i][0]);

        max_id_hw = max_ap_fixed(&result_ap_fixed[i][0]);
        max_id_sw = max_float(&result_float[i][0]);
        printf("id = %d, max_id_hw = %d\n", i, max_id_hw);
        printf("id = %d, max_id_sw = %d\n", i, max_id_sw);

        free(rd_bmp);
        free(rd_bmpf);
        free(rd_bmp_apf);
    }

    return(0);
}

int mnist_conv_nn_float(float in[784], float out[10]){
    float buf[28][28];
    float conv_out[10][24][24];
    float pool_out[10][12][12];
    float dot1[100];
    float dot2[10];

    buf_copy1: for(int i=0; i<28; i++)
        buf_copy2: for(int j=0; j<28; j++)
            buf[i][j] = in[i*28+j];

    // Convolutional Neural Network 5x5 kernel, Stride = 1, Padding = 0
    // + ReLU
    CONV1: for(int i=0; i<10; i++){    // カーネルの個数
        CONV2: for(int j=0; j<24; j++){
            CONV3: for(int k=0; k<24; k++){
                conv_out[i][j][k] = 0;
                CONV4: for(int m=0; m<5; m++){
                    CONV5: for(int n=0; n<5; n++){
                        conv_out[i][j][k] += buf[j+m][k+n] * conv1_fweight[i][0][m][n];
                    }
                }
                conv_out[i][j][k] += conv1_fbias[i];

                if(conv_out[i][j][k]<0)    // ReLU
                    conv_out[i][j][k] = 0;
            }
        }
    }

    // Pooling Kernel = 2 x 2, Stride = 2
    POOL1: for(int i=0; i<10; i++){
        POOL2: for(int j=0; j<24; j += 2){
            POOL3: for(int k=0; k<24; k += 2){
                POOL4: for(int m=0; m<2; m++){
                    POOL5: for(int n=0; n<2; n++){
                        if(m==0 && n==0){
                            pool_out[i][j/2][k/2] = conv_out[i][j][k];
                        } else if(pool_out[i][j/2][k/2] < conv_out[i][j+m][k+n]){
                            pool_out[i][j/2][k/2] = conv_out[i][j+m][k+n];
                        }
                    }
                }
            }
        }
    }

    af1_dot1: for(int col=0; col<100; col++){
        dot1[col] = 0;
        af1_dot2: for(int i=0; i<10; i++){
            af1_dot3: for(int j=0; j<12; j++){
                af1_dot4: for(int k=0; k<12; k++){
                    dot1[col] += pool_out[i][j][k]*af1_fweight[i*12*12+j*12+k][col];
                }
            }
        }
        dot1[col] += af1_fbias[col];

        if(dot1[col] < 0)    // ReLU
            dot1[col] = 0;
    }

    af2_dot1: for(int col=0; col<10; col++){
        dot2[col] = 0;
        af2_dot2: for(int row=0; row<100; row++){
            dot2[col] += dot1[row]*af2_fweight[row][col];
        }
        dot2[col] += af2_fbias[col];

        out[col] = dot2[col];
    }

    return(0);
}

int max_ap_fixed(ap_fixed<127, AP_TRN_ZERO, AP_SAT> out[10]){
    int max_id;
    ap_fixed<127, AP_TRN_ZERO, AP_SAT> max;

    for(int i=0; i<10; i++){
        if(i == 0){
            max = out[0];
            max_id = 0;
        }else if(out[i]>max){
            max = out[i];
            max_id = i;
        }
    }
    return(max_id);
}

int max_float(float out[10]){
    int max_id;
    float max;

    for(int i=0; i<10; i++){
        if(i == 0){
            max = out[0];
            max_id = 0;
        }else if(out[i]>max){
            max = out[i];
            max_id = i;
        }
    }
    return(max_id);
}

// RGBからYへの変換
// RGBのフォーマットは、{8'd0, R(8bits), G(8bits), B(8bits)}, 1pixel = 32bits
// 輝度信号Yのみに変換する。変換式は、Y =  0.299R + 0.587G + 0.114B
// "YUVフォーマット及び YUV<->RGB変換"を参考にした。http://vision.kuee.kyoto-u.ac.jp/~hiroaki/firewire/yuv.html
// 2013/09/27 : float を止めて、すべてint にした
int conv_rgb2y(int rgb){
    int r, g, b, y_f;
    int y;

    b = rgb & 0xff;
    g = (rgb>>8) & 0xff;
    r = (rgb>>16) & 0xff;

    y_f = 77*r + 150*g + 29*b; //y_f = 0.299*r + 0.587*g + 0.114*b;の係数に256倍した
    y = y_f >> 8// 256で割る

    return(y);
}

  1. 2017年06月25日 06:39 |
  2. DNN
  3. | トラックバック:0
  4. | コメント:0

「ゼロから作るDeep Learning」の畳み込みニューラルネットワークのハードウェア化8(性能向上を図る)

「ゼロから作るDeep Learning」の畳み込みニューラルネットワークのハードウェア化7(C/RTL協調シミュレーション)”の続き。

前回は、C/RTL協調シミュレーションを行って、波形を観察し、IP 化を行った。今回は、実際に使用するインターフェースのAXI4-Stream 入力、AXI4-Lite 出力にして、少しチューニングをしてみよう。

書き換えた mnist_conv_nn10.cppを示す。

// mnist_conv_nn10.cpp
// 2017/06/12 by marsee
// 畳み込み層のカーネル数 10
//

#include <ap_fixed.h>

#include "conv1_weight.h"
#include "conv1_bias.h"
#include "af1_weight.h"
#include "af1_bias.h"
#include "af2_weight.h"
#include "af2_bias.h"

int mnist_conv_nn(ap_ufixed<80, AP_TRN_ZERO, AP_SAT> in[784], ap_fixed<127, AP_TRN_ZERO, AP_SAT> out[10]){
#pragma HLS INTERFACE s_axilite register port=out
#pragma HLS INTERFACE axis register both port=in
#pragma HLS INTERFACE s_axilite port=return
    ap_ufixed<80, AP_TRN_ZERO, AP_SAT> buf[28][28];
    ap_fixed<103, AP_TRN_ZERO, AP_SAT> conv_out[10][24][24];
#pragma HLS ARRAY_PARTITION variable=conv_out cyclic factor=2 dim=3
    ap_fixed<103, AP_TRN_ZERO, AP_SAT> pool_out[10][12][12];
#pragma HLS ARRAY_PARTITION variable=pool_out block factor=12 dim=3
    ap_fixed<137, AP_TRN_ZERO, AP_SAT> dot1[100];
    ap_fixed<137, AP_TRN_ZERO, AP_SAT> dot2[10];

    buf_copy1: for(int i=0; i<28; i++)
        buf_copy2: for(int j=0; j<28; j++)
#pragma HLS PIPELINE II=1
            buf[i][j] = in[i*28+j];

    // Convolutional Neural Network 5x5 kernel, Stride = 1, Padding = 0
    // + ReLU
    CONV1: for(int i=0; i<10; i++){    // カーネルの個数
        CONV2: for(int j=0; j<24; j++){
            CONV3: for(int k=0; k<24; k++){
                conv_out[i][j][k] = 0;
                CONV4: for(int m=0; m<5; m++){
#pragma HLS PIPELINE II=7
                    CONV5: for(int n=0; n<5; n++){
                        conv_out[i][j][k] += buf[j+m][k+n] * conv1_weight[i][0][m][n];
                    }
                }
                conv_out[i][j][k] += conv1_bias[i];

                if(conv_out[i][j][k]<0)    // ReLU
                    conv_out[i][j][k] = 0;
            }
        }
    }

    // Pooling Kernel = 2 x 2, Stride = 2
    POOL1: for(int i=0; i<10; i++){
        POOL2: for(int j=0; j<24; j += 2){
            POOL3: for(int k=0; k<24; k += 2){
                POOL4: for(int m=0; m<2; m++){
#pragma HLS PIPELINE II=4
                    POOL5: for(int n=0; n<2; n++){
                        if(m==0 && n==0){
                            pool_out[i][j/2][k/2] = conv_out[i][j][k];
                        } else if(pool_out[i][j/2][k/2] < conv_out[i][j+m][k+n]){
                            pool_out[i][j/2][k/2] = conv_out[i][j+m][k+n];
                        }
                    }
                }
            }
        }
    }

    af1_dot1: for(int col=0; col<100; col++){
        dot1[col] = 0;
        af1_dot2: for(int i=0; i<10; i++){
            af1_dot3: for(int j=0; j<12; j++){
                af1_dot4: for(int k=0; k<12; k++){
#pragma HLS PIPELINE II=3
                    dot1[col] += pool_out[i][j][k]*af1_weight[i*12*12+j*12+k][col];
                }
            }
        }
        dot1[col] += af1_bias[col];

        if(dot1[col] < 0)    // ReLU
            dot1[col] = 0;
    }

    af2_dot1: for(int col=0; col<10; col++){
        dot2[col] = 0;
        af2_dot2: for(int row=0; row<100; row++){
#pragma HLS PIPELINE II=4
            dot2[col] += dot1[row]*af2_weight[row][col];
        }
        dot2[col] += af2_bias[col];

        out[col] = dot2[col];
    }

    return(0);
}


このソースコードでのC コードの合成結果を示す。
nn_fpga_ch7_44_170623.png

Estimated は 13.48 ns で 10 ns をオーバーしている。
レイテンシは 690991 クロックで、100 MHz の場合は、6.91 ms となる。大幅にリソースを増やすこと無しに、これ以上速くならなかったのが残念だ。
リソースもBRAM_18K が 55 % から 59 % になった。FF も LUT も増えているが、AXI バスにしたので、少し増えたのもある。なお、Vivado HLS のバージョンは 2016.4 だ。
AXI バスにしたので、Address Info を貼っておく。これを見るとレジスタとそのオフセット・アドレスが分かる。

//------------------------Address Info-------------------
// 0x00 : Control signals
//        bit 0  - ap_start (Read/Write/COH)
//        bit 1  - ap_done (Read/COR)
//        bit 2  - ap_idle (Read)
//        bit 3  - ap_ready (Read)
//        bit 7  - auto_restart (Read/Write)
//        others - reserved
// 0x04 : Global Interrupt Enable Register
//        bit 0  - Global Interrupt Enable (Read/Write)
//        others - reserved
// 0x08 : IP Interrupt Enable Register (Read/Write)
//        bit 0  - Channel 0 (ap_done)
//        bit 1  - Channel 1 (ap_ready)
//        others - reserved
// 0x0c : IP Interrupt Status Register (Read/TOW)
//        bit 0  - Channel 0 (ap_done)
//        bit 1  - Channel 1 (ap_ready)
//        others - reserved
// 0x10 : Data signal of ap_return
//        bit 31~0 - ap_return[31:0] (Read)
// 0x20 ~
// 0x3f : Memory 'out_V' (10 * 12b)
//        Word n : bit [11: 0] - out_V[2n]
//                 bit [27:16] - out_V[2n+1]
//                 others      - reserved
// (SC = Self Clear, COR = Clear on Read, TOW = Toggle on Write, COH = Clear on Handshake)


次に、実際にインプリメントしてみて、10 ns に収まるかどうか?を見るために、Place and Route にチェックを入れてExport RTL を行った。
結果はちょっと危ないが、一応 10 ns に収まっている。
nn_fpga_ch7_45_170623.png

VirtualBox 上のUbuntu 16.04 上のVivado HLS 2016.4 でC/RTL協調シミュレーションをやってみた。結果を示す。
nn_fpga_ch7_46_170623.png

C/RTL協調シミュレーション波形を見た。全体の波形を示す。
nn_fpga_ch7_47_170623.png

入力部分のAXI4-Stream 部分を拡大した。8ビットで転送されている。
nn_fpga_ch7_48_170623.png

結果の出力部分だ。
nn_fpga_ch7_49_170623.png

0x2C 番地のデータが 0x0ee901b4 で、bit [11: 0] - out_V[2n] が 0x01b4 で最大なので、結果は 6 で正解だ。あとの数はすべてマイナスだった。

とりあえず、これでIP にしようと思う。IP 化しVivado のプロジェクトで使用する前に、私の書いた手描き数字を認識できるかどうか?のテストをしたいと思う。
  1. 2017年06月23日 05:01 |
  2. DNN
  3. | トラックバック:0
  4. | コメント:0

「ゼロから作るDeep Learning」の畳み込みニューラルネットワークのハードウェア化7(C/RTL協調シミュレーション)

”「ゼロから作るDeep Learning」の畳み込みニューラルネットワークのハードウェア化6(再度Vivado HLS )”の続き。

前回は、畳み込みニューラルネットワークをVivado HLS でハードウェア化することができた。今回は、HDLコードを見たり、C/RTL協調シミュレーションをしてみてから、IP化を行ってみよう。ただし、デフォルトのインターフェースのままでは、IP にしたときに使いにくいので、実際に使用する際にはAXI バスにする。

最初に生成されたHDLコードを見てみよう。VHDL コードを見る。トップファイルのmnist_conv_nn.vhd はメモリのインターフェースとなっている。
nn_fpga_ch7_33_170621.png

さて、次にC/RTL協調シミュレーションをやってみよう。ただし、100 回は長すぎるので、1 回だけやってみる。
nn_fpga_ch7_34_170621.png

C/RTL協調シミュレーションの結果を示す。
nn_fpga_ch7_35_170621.png

172089 クロックかかっている。

C/RTL協調シミュレーションの波形を示す。まずは全体の波形から。
nn_fpga_ch7_43_170621.png

入力の部分を拡大してみた。
nn_fpga_ch7_36_170621.png

更に拡大した。値を見ることができる。
nn_fpga_ch7_37_170621.png

出力部分を見た。答えの 6 以外はMSB のビットが立っているので、マイナスの値になっているのが分かる。
nn_fpga_ch7_38_170621.png

試しに、テストベンチの固定小数点の最大値を調べる関数にプリント文を追加して値を見ることにした。
nn_fpga_ch7_39_170621.png

そして、C シミュレーションを行った結果を示す。
nn_fpga_ch7_40_170621.png

やはり、6 だけプラスの値で、13.625 だった。分かりやすい結果となった。

次に、試しにIP 化を行ってみた。
インプリメントして、動作周波数が間に合うのか?を調べるために、Place and Route にチェックを入れた。
nn_fpga_ch7_41_170621.png

その結果、問題ないようだった。
nn_fpga_ch7_42_170621.png
  1. 2017年06月21日 05:11 |
  2. DNN
  3. | トラックバック:0
  4. | コメント:0