从零开始山寨Caffe·拾:IO系统(三)

来源:互联网 发布:天气预报数据下载 编辑:程序博客网 时间:2024/06/07 06:01

数据变形

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;            }        }    }}
复制代码

上面是几种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

0 0
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 荣耀10有点卡怎么办 8g内存吃鸡会崩怎么办 玩看门狗很卡怎么办 拼多多人数不够怎么办 玩cf想吐怎么办 玩手机头晕恶心怎么办 玩手机头疼恶心怎么办 看手机想吐怎么办 英雄联盟取名后怎么办 王者荣耀改名重复怎么办 刺激战场改名重复怎么办 省钱快报忘记密码怎么办 手机直播网速卡怎么办 触手tv直播黑屏怎么办 酷狗id密码忘记怎么办 打游戏网络不稳定怎么办 电脑打字法没了怎么办 家庭版密钥专业版系统怎么办 win7应用程序不能启动怎么办 win7用户密码忘记了怎么办 win7用户密码忘了怎么办 windows开不了机怎么办 网卡被卸载了怎么办 win7注销黑屏了怎么办 w7密码忘了怎么办 笔记本电脑键盘进水了怎么办 笔记本键盘进水了怎么办 笔记本进水键盘失灵怎么办 win7进不了系统怎么办 电脑显示屏两边黑屏怎么办 win8关机关不了怎么办 win10没激活黑屏怎么办 忘了产品密钥怎么办 小马易贷逾期怎么办 10系统未激活怎么办 win10账户被停用怎么办 win7激活码无效怎么办 windows显示不是正版怎么办 优酷上传错误怎么办 盗版系统会黑屏怎么办 安装了盗版系统怎么办