1. 程式人生 > >從零開始山寨Caffe·拾:IO系統(三)

從零開始山寨Caffe·拾:IO系統(三)

資料變形

IO(二)中,我們已經將原始資料緩衝至Datum,Datum又存入了生產者緩衝區,不過,這離消費,還早得很呢。

在消費(使用)之前,最重要的一步,就是資料變形。

ImageNet

ImageNet提供的資料相當Raw,不僅影象尺寸不一,ROI焦點內容比例也不一,如圖:

[Krizhevsky12]給出了CNN打ImageNet的基本預處理,非常經典的" Random 256-224 Crop",即:

首先,對圖片進行統一的縮放,無視寬高比,統一縮放成256*256(可利用OpenCV)

(注:保留寬高比是沒有意義的,CNN的滑動卷積本身就會破壞寬高比,見Faster-RCNN的RPN設計原理)

預先計算好256*256影象的均值,在硬碟上儲存為均值檔案。之後,分為訓練階段和測試階段。

【訓練階段】

對256*256的圖片,只選擇224*224的crop區域,目的是做Data Augmentation。

crop方式很特殊,採用的是隨機crop。由於256-224=32,寬高軸上各有32單元的平移空間。

於是在訓練時,每次Rand(0,32),寬高軸一共就有32*32種crop結果,達到了資料增幅效果。

同時,還要對crop結果,做一次映象,這樣就有2*32*32=2048倍的增幅資料了。

【測試階段】

對256*256的圖片,將224*224的crop區域分別定位在4角和圖片中心,加上映象,共計10種結果。

累加Softmax的prob,做平均,得到最終prob,最後再作出prediction。

均值標準化

作為經典的通用資料預處理手段,均值標準化相當廉價,效果不俗。

預設有倆種均值標準化:逐畫素(精)、逐通道(糙)。

Caffe中對逐畫素均值資料進行的是外掛儲存,和影象資料是分開的,這樣的儲存相當靈活。

代價就是,對每一張圖要進行減均值操作,在GPU模式中,CPU的這點計算量其實沒什麼。

對於逐通道均值,直接在proto文字中,作為引數指定。

數值縮放

[Krizhevsky12] 中,使用更靈活的Gaussian初始化,網路首層引數初始化的標準差縮小100倍(0.0001)

以此免除了傳統意義上的數值縮放。

如果你需要使用Xavier初始化,仍然需要校正輸入範圍至[-1,1]。

[0,256]範圍需要乘以1/256=0.00390625的縮放因子。

[-128,128]範圍(做了均值標準化)需要乘以1/128=0.0078125的縮放因子。

映象

可以OpenCV做。因為映象不涉及插值,也可以人工逆轉座標完成。

資料結構

(注:Transformer中含有大量OpenCV函式的使用,以下將精簡掉所有OpenCV功能,請讀者按需自行補充)

在proto檔案中,補上TransformationParameter

message TransformationParameter{
    optional float scale=1 [default=1.0];
    optional bool mirror=2 [default=false];
    optional uint32 crop_size=3 [default=0];
    optional string mean_file=4;
    repeated float mean_value=5;
    optional bool force_color=6 [default=false];
    optional bool force_gray=7 [default=false];
}

在LayerParameter,補上:

optional TransformationParameter transform_param=XX;

Transformer將作為DataLayer的成員變數,接受LayerParameter傳進來的transform_param進行構造。

建立data_transformer.hpp

template <typename Dtype>
class DataTransformer
{
public:
    DataTransformer(const TransformationParameter& param, Phase phase);
    vector<int> inferBlobShape(const Datum& datum);
    void transform(const Datum& datum, Blob<Dtype>* shadow_blob);
    void transform(const Datum& datum, Dtype* shadow_data);
    void initRand();
    ~DataTransformer() {}
    int rand(int n);
private:
    TransformationParameter param;
    Phase phase;
    Blob<Dtype> mean_blob;
    vector<Dtype> mean_vals;
    boost::shared_ptr<Dragon::RNG> ptr_rng;
};

inferBlobShape、transfrom都是外調成員函式,將被DataLayer使用。

分別用於根據資料推測DataLayer的Blob大小、以及對資料變形。

initRand將構造梅森發生器ptr_rng,rand用於Random-Crop。

根據均值標準化的不同,mean_blob儲存逐畫素均值,mean_val則是簡單的逐通道均值。

Protocol Buffer的檔案IO封裝

反序列化以二進位制儲存的均值檔案,需要操作Protocol Buffer的底層檔案系統API,為了便於呼叫,做一個Wrapper。

建立io.hpp。

#include <fcntl.h>
#include <unistd.h>
#include <google/protobuf/message.h>
#include <google/protobuf/io/coded_stream.h>
#include <google/protobuf/io/zero_copy_stream_impl.h>
#include <google/protobuf/text_format.h>
inline bool readProtoFromBinaryFile(const char* filename, Message* proto){
    //    get OS kernel‘s file descriptor(fd)
    //    successful range:    [0,OPEN_MAX]
    //    replace open(filename, O_RDONLY) as open(filename, O_RDONLY | O_BINARY)
    int fd = open(filename, O_RDONLY | O_BINARY);
    CHECK_NE(fd, -1) << "File not found: " << filename;
    ZeroCopyInputStream *raw_input = new FileInputStream(fd);
    CodedInputStream *coded_input = new CodedInputStream(raw_input);
    coded_input->SetTotalBytesLimit(INT_MAX, 536870912);  //  0..512M..2G
    bool success = proto->ParseFromCodedStream(coded_input);
    delete raw_input;
    delete coded_input;
    close(fd);
    return success;
}

值得在意的是OS提供的API函式open,返回的是fd(file descriptor),這和OS的檔案系統有關。

Linux的open函式,預設是以O_RDONLY開啟的,而Windows則不是。

因此,移植Linux版Caffe的第一步就是追加O_RDONLY這個Flag。

ZeroCopyInputStream相比於PB提供的InputStream,速度要更快。

CodedInputStream為了解除二進位制的編碼,SetTotalBytesLimit兩引數分別是檔案大小上界和警告閾值(2G/512M)。

最後,將二進位制編碼資料,反序列化成為Message結構。

實現

建立data_transformer.cpp

template <typename Dtype>
DataTransformer<Dtype>::DataTransformer(const  TransformationParameter& param, Phase phase):
    param(param), phase(phase)
{
    //    normally, we get mean_value from mean_file
    if (param.has_mean_file()){
        CHECK_EQ(param.mean_value_size(), 0) 
        << "System wants to use mean_file but specified mean_value."; const string& mean_file = param.mean_file(); LOG(INFO) << "Loading mean file from: " << mean_file; BlobProto proto; readProtoFromBinaryFileOrDie(mean_file.c_str(), &proto); mean_blob.FromProto(proto); } // using each channel's mean value // mean_value_size() is between 1 and 3 if (param.mean_value_size()>0){ CHECK(param.has_mean_file() == false)
        << "System wants to use mean_value but specified mean_file."; for (int i = 0; i < param.mean_value_size(); i++) mean_vals.push_back(param.mean_value(i)); }
initRand(); }

建構函式中,主要做兩件事:

①恢復均值資料,逐畫素從檔案讀,逐通道從指定的proto引數裡讀。

逐通道引數指定方法:

layer {
     .........
  transform_param {
    mean_val: 102
    mean_val: 107
    mean_val: 112
    .........
   }
}

proto的repeated型別,可以通過相同的名字,連續指定。

②初始化梅森發生器。

均值資料的序列化,是放在BlobProto裡的,反序列會成為BlobProto。

template<typename Dtype>
vector<int> DataTransformer<Dtype>::inferBlobShape(const Datum& datum){
    const int crop_size = param.crop_size();
    const int channels = datum.channels();
    const int height = datum.height();
    const int width = datum.width();
    CHECK_GT(channels, 0);
    CHECK_GE(height, crop_size);
    CHECK_GE(width,crop_size);
    vector<int> shape(4);
    shape[0] = 1; shape[1] = channels;
    shape[2] = crop_size ? crop_size : height;
    shape[3] = crop_size ? crop_size : width;
    return shape;
}

InferBlobShape接受一個Datum,返回推測的shape,用於構建DataLayer中,Flow的Blob。

template<typename Dtype>
void DataTransformer<Dtype>::initRand(){
    const bool must_rand = (phase == TRAIN && param.crop_size());
    if (must_rand){
        const unsigned int rng_seed = Dragon::get_random_value();
        ptr_rng.reset(new Dragon::RNG(rng_seed));
    }
}

梅森發生器的構建使用了主程序管理器的梅森發生器提供的一個隨機數作為種子。

這步可以省略,使用程序相關的cluster_seedgen也是可以的。

template<typename Dtype>
int DataTransformer<Dtype>::rand(int n){
    CHECK(ptr_rng);
    CHECK_GT(n, 0);
    rng_t* rng = ptr_rng->get_rng();
    return (*rng)() % n;
}

32位的梅森發生器預設產生一個unsigned int32值,如果需要指定範圍,需要做求餘操作。

同時,注意Random-Crop不需要負隨機值。

template<typename Dtype>
void DataTransformer<Dtype>::transform(const Datum& datum, Dtype* shadow_data){
    //    pixel can be compressed as a string
    //    cause each pixel ranges from 0~255 (a char)
    const string& data = datum.data();
    const int datum_channels = datum.channels();
    const int datum_height = datum.height();
    const int datum_width = datum.width();
    const int crop_size = param.crop_size();
    const Dtype scale = param.scale();
    const bool must_mirror = param.mirror(); //need rand!!!
    const bool has_mean_file = param.has_mean_file();
    const bool has_uint8 = data.size() > 0; //pixels are compressed as a string
    const bool has_mean_value = mean_vals.size() > 0;
    CHECK_GT(datum_channels, 0);
    CHECK_GE(datum_height, crop_size);
    CHECK_GE(datum_width, crop_size);
    Dtype *mean = NULL;
    if (has_mean_file){
        CHECK_EQ(datum_channels, mean_blob.channels());
        CHECK_EQ(datum_height, mean_blob.height());
        CHECK_EQ(datum_width, mean_blob.width());
        mean = mean_blob.mutable_cpu_data();
    }
    if (has_mean_value){
        CHECK(mean_vals.size() == 1 || mean_vals.size() == datum_channels)
            << "Channel's mean value must be provided as a single value or as many as channels.";
        //replicate
        if (datum_channels > 1 && mean_vals.size() == 1)
            for (int i = 0; i < datum_channels - 1; i++)
                mean_vals.push_back(mean_vals[0]);
    }
    int h_off = 0, w_off = 0, height = datum_height, width = datum_width;
    if (crop_size){
        height = crop_size;
        width = crop_size;
        //    train phase using random croping
        if (phase == TRAIN){
            h_off = rand(datum_height - height + 1);
            w_off = rand(datum_width - width + 1);
        }
        //    test phase using expected croping
        else{
            h_off = (datum_height - height) / 2;
            w_off = (datum_width - width) / 2;
        }
    }
    Dtype element;
    int top_idx, data_idx;
    //copy datum values to shadow_data-> batch
    for (int c = 0; c < datum_channels; c++){
        for (int h = 0; h < height; h++){
            for (int w = 0; w < width; w++){
                data_idx = (c*datum_height + h_off + h)*datum_width + w_off + w;
                if (must_mirror)    top_idx = (c*height + h)*width + (width - 1 - w); //top_left=top_right
                else    top_idx = (c*height + h)*width + w;
                if (has_uint8){
                    //    char type can not cast to Dtype directly
                    //    or will generator mass negative number(facing Cifar10)
                    element=static_cast<Dtype>(static_cast<uint8_t>(data[data_idx]));
                }
                else element = datum.float_data(data_idx);    //Dtype <- float
                if (has_mean_file) shadow_data[top_idx] = (element - mean[data_idx])*scale;
                else if (has_mean_value) shadow_data[top_idx] = (element - mean_vals[c])*scale;
                else shadow_data[top_idx] = element*scale;
            }
        }
    }
}
DataTransformer::transform()

上面是幾種transform的核心操作,還是比較冗繁的。

首先從Datum獲得輸入資料尺寸,做Random-Crop。

在訓練階段,得到基於原圖的兩個偏移h_off,w_off。

在測試階段,預設沒有實現[Krizhevsky12]的10個測試區域多重預測,只提供單中心crop區域。

需要根據具體要求,重寫這部分程式碼。比如GoogleNet就擴大到了144個測試區域,具體見[Szegedy14]

接著,逐通道、逐畫素(crop之後的寬高):

data_idx由crop位置+偏移位置聯合而成,代表原圖的畫素位置。

top_idx代表的是crop圖的位置。

如果需要映象(反轉width軸),在計算top_idx的最後,用(width - 1 - w)替代w。

uint8這裡需要特別注意:

string裡的字元型別是char,而uint8是unsigned char,需要強制轉換。

諸如MNIST、Cifar10這樣的資料集,畫素單元是以uint8儲存的。

8Bit的頂位用於儲存符號位,unit8範圍是[0,255],int8範圍是[-127,127]。

如果不轉換,從char(string)中獲取的值,頂位將用於符號,顯然不能表達我們的畫素要求。

最後,均值和縮放可以在一行完成。

template<typename Dtype>
void DataTransformer<Dtype>::transform(const Datum& datum, Blob<Dtype>* shadow_blob){
    const int num = shadow_blob->num();
    const int channels = shadow_blob->channels();
    const int height = shadow_blob->height();
    const int width = shadow_blob->width();
    CHECK_EQ(channels, datum.channels());
    CHECK_GE(num, 1);
    CHECK_LE(height, datum.height()); //allowing crop
    CHECK_LE(width, datum.width());
    Dtype *base_data = shadow_blob->mutable_cpu_data();
    transform(datum, base_data);
}

這個transform的過載函式是對Blob的封裝。(可選)

完整程式碼

io.hpp

data_transformer.hpp

data_transformer.cpp

相關推薦

開始山寨Caffe·IO系統()

資料變形 IO(二)中,我們已經將原始資料緩衝至Datum,Datum又存入了生產者緩衝區,不過,這離消費,還早得很呢。 在消費(使用)之前,最重要的一步,就是資料變形。 ImageNet ImageNet提供的資料相當Raw,不僅影象尺寸不一,ROI焦點內容比例也不一,如圖: [Krizhev

開始山寨Caffe·陸IO系統(一)

你說你學過作業系統這門課?寫個無Bug的生產者和消費者模型試試!                               ——你真的學好了作業系統這門課嘛? 在第壹章,展示過這樣圖: 其中,左半部分構成了新版Caffe最惱人、最龐大的IO系統。 也是歷來最不重視的一部分。 第伍章又對左半

開始山寨Caffe·捌IO系統(二)

生產者 雙緩衝組與訊號量機制 在第陸章中提到了,如何模擬,以及取代根本不存的Q.full()函式。 其本質是:除了為生產者提供一個成品緩衝佇列,還提供一個零件緩衝佇列。 當我們從外部給定了固定容量的零件之後,生產者的產能就受到了限制。 由兩個阻塞佇列組成的QueuePair,並不是Caffe的獨創,

開始山寨Caffe·IO系統(四)

消費者 回憶:生產者提供產品的介面 在第捌章,IO系統(二)中,生產者DataReader提供了外部消費介面: class DataReader { public: ......... BlockingQueue<Datum*>& free() const

開始山寨Caffe·貳主存模型

本文轉自:https://www.cnblogs.com/neopenx/p/5190282.html 從硬體說起 物理之觴 大部分Caffe原始碼解讀都喜歡跳過這部分,我不知道他們是什麼心態,因為這恰恰是最重要的一部分。 記憶體的管理不擅,不僅會導致程式的立即崩潰,還會導致記憶體的

開始山寨Caffe·柒KV資料庫

你說你會關係資料庫?你說你會Hadoop? 忘掉它們吧,我們既不需要網路支援,也不需要複雜關係模式,只要讀寫夠快就行。                                         ——論資料儲存的本質 淺析資料庫技術 記憶體資料庫——STL的map容器 關係資料庫橫行已久,似乎大

開始山寨Caffe·玖BlobFlow

聽說Google出了TensorFlow,那麼Caffe應該叫什麼?                           ——BlobFlow 神經網路時代的傳播資料結構 我的程式碼 我最早手寫神經網路的時候,Flow結構是這樣的: struct Data { vector<d

開始山寨Caffe·伍Protocol Buffer簡易指南

你為Class外訪問private物件而苦惱嘛?你為設計序列化格式而頭疼嘛?                             ——歡迎體驗Google Protocol Buffer 面向物件之封裝性 歷史遺留問題 面向物件中最矛盾的一個特性,就是“封裝性”。 在上古時期,大牛們無聊地設計了

開始caffe(七)利用GoogleNet實現影象識別

一、準備模型 在這裡,我們利用已經訓練好的Googlenet進行物體影象的識別,進入Googlenet的GitHub地址,進入models資料夾,選擇Googlenet 點選Googlenet的模型下載地址下載該模型到電腦中。 模型結構 在這裡,我們利用之前講

開始caffe(十)caffe中snashop的使用

在caffe的訓練期間,我們有時候會遇到一些不可控的以外導致訓練停止(如停電、裝置故障燈),我們就不得不重新開始訓練,這對於一些大型專案而言是非常致命的。在這裡,我們介紹一些caffe中的snashop。利用snashop我們就可以實現訓練的繼續進行。 在之前我們訓練得到的檔案中,我們發現

開始caffe(九)在Windows下實現影象識別

本系列文章主要介紹了在win10系統下caffe的安裝編譯,運用CPU和GPU完成簡單的小專案,文章之間具有一定延續性。 step1:準備資料集 資料集是進行深度學習的第一步,在這裡我們從以下五個連結中下載所需要的資料集: animal flower plane hou

開始caffe(八)Caffe在Windows環境下GPU版本的安裝

之前我們已經安裝過caffe的CPU版本,但是在MNIST手寫數字識別中,我們發現caffe的CPU版本執行速度較慢,訓練效率不高。因此,在這裡我們安裝了caffe的GPU版本,並使用GPU版本的caffe同樣對手寫MNIST數字集進行訓練。 step1: 安裝CUDA

開始caffe(四)mnist手寫數字識別網路結構模型和超引數檔案的原始碼閱讀

下面為網路結構模型 %網路結構模型 name: "LeNet" #網路的名字"LeNet" layer { #定義一個層 name: "mnist" #層的名字"mnist" type:

開始caffe(二)caffe在win10下的安裝編譯

環境要求 作業系統:64位windows10 編譯環境:Visual Studio 2013 Ultimate版本 安裝流程 step1:檔案的下載 從GitHub新增連結描述中下載Windows版本的caffe,並進行解壓到電腦中。 step2:檔案修改 將壓縮包

開始系列-Caffe入門到精通之一 環境搭建

python 資源暫時不可用 強制 rec htm color 查看 cpu blog 先介紹下電腦軟硬件情況吧: 處理器:Intel? Core? i5-2450M CPU @ 2.50GHz × 4 內存:4G 操作系統:Ubuntu Kylin(優麒麟) 16.04

【視訊】Kubernetes1.12開始(六)程式碼編譯到自動部署

作者: 李佶澳   轉載請保留:原文地址   釋出時間:2018/11/10 16:14:00 說明 kubefromscratch-ansible和kubefromscratch介紹 使用前準備

開始理解caffe網路的引數

LeNet網路介紹 LeNet網路詳解 網路名稱 name: "LeNet" # 網路(NET)名稱為LeNet mnist層-train layer {

開始學習Servlet(1) 作用和生命週期

Servlet 作用 Servlet 是實現了 javax.servlet.Servlet 介面的 Java 類, 負責處理客戶端的 HTTP 請求。是客戶端 與 資料庫或後臺應用程式之間互動的媒介 。功能: 1. 讀取客戶端傳送的資料 2. 處理

ubuntu 14.04 開始安裝caffe

一、前言 很多人不太喜歡看官方教程,但其實 caffe 的官方安裝指導做的非常好。我在看到 2) 之前,曾根據官方指導在 OSX 10.9, 10.10, Ubuntu 12.04, 14.04 下安裝過 10 多次不同版本的 caffe,都成功了。 本文有不少內容參考了 1)和 2),但又有一些內容

PHPUnit開始(2)編寫 PHPUnit 測試

計劃永遠趕不上變化,本計劃本月完成所有PHPUnit的部落格內容。今天一看日曆發現都TMD的二月底了,而我才寫了一篇而已。情何以堪…… 今天寫第二篇,詳細說一說如何寫出一個測試用例。 這裡會涉及到一些什麼自動載入之類的,我就不再這裡補充了,大家可以查閱相關P