IO(二)中,咱們已經將原始數據緩衝至Datum,Datum又存入了生產者緩衝區,不過,這離消費,還早得很呢。c++
在消費(使用)以前,最重要的一步,就是數據變形。git
ImageNet提供的數據至關Raw,不只圖像尺寸不一,ROI焦點內容比例也不一,如圖:github
[Krizhevsky12]給出了CNN打ImageNet的基本預處理,很是經典的" Random 256-224 Crop",即:網絡
首先,對圖片進行統一的縮放,無視寬高比,統一縮放成256*256(可利用OpenCV)數據結構
(注:保留寬高比是沒有意義的,CNN的滑動卷積自己就會破壞寬高比,見Faster-RCNN的RPN設計原理)app
預先計算好256*256圖像的均值,在硬盤上存儲爲均值文件。以後,分爲訓練階段和測試階段。dom
【訓練階段】:ide
對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的底層文件系統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; } } } }
上面是幾種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