從零開始山寨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。

關於如何存儲均值,見:https://github.com/neopenx/Dragon/blob/master/Dragon/compute_mean.cpp

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

https://github.com/neopenx/Dragon/blob/master/Dragon/include/utils/io.hpp

data_transformer.hpp

https://github.com/neopenx/Dragon/blob/master/Dragon/include/data_transformer.hpp

data_transformer.cpp

https://github.com/neopenx/Dragon/blob/master/Dragon/src/data_transformer.cpp


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM