从零开始山寨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..2Gbool 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_fileif (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 3if (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: 102mean_val: 107mean_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 stringconst 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.";//replicateif (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 cropingif (phase == TRAIN){h_off = rand(datum_height - height + 1);w_off = rand(datum_width - width + 1);}// test phase using expected cropingelse{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-> batchfor (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_rightelse 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 <- floatif (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
转载于:https://www.cnblogs.com/neopenx/p/5315945.html
从零开始山寨Caffe·拾:IO系统(三)相关推荐
- 从零开始山寨Caffe·零:必先利其器
工作环境 巧妇有了米炊 众所周知,Caffe是在Linux下写的,所以长久以来,大家都认为跑Caffe,先装Linux. niuzhiheng大神发起了caffe-windows项目(解决了一些编译. ...
- 从零开始山寨Caffe·壹:仰望星空与脚踏实地
请以"仰望星空与脚踏实地"作为题目,写一篇不少于800字的文章.除诗歌外,文体不限. --2010·北京卷 仰望星空 规范性 Caffe诞生于12年末,如果偏要形容一下这个框架,可 ...
- 从零开始山寨Caffe·叁:全局线程管理器
你需要一个管家,随手召唤的那种,想吃啥就吃啥. --设计一个全局线程管理器 一个机器学习系统,需要管理一些公共的配置信息,如何存储这些配置信息,是一个难题. 设计模式 MVC框架 在传统的MVC编程框 ...
- 从零开始山寨Caffe·伍:Protocol Buffer简易指南
你为Class外访问private对象而苦恼嘛?你为设计序列化格式而头疼嘛? --欢迎体验Google Protocol Buffer 面向对象之封装性 历史遗留问题 面向对象中最矛盾的一个特性,就是 ...
- 从零开始山寨Caffe·贰:主存模型
你左手是内存,右手是显存,内存可以打死显存,显存也可以打死内存. -- 请协调好你的主存 从硬件说起 物理之觞 大部分Caffe源码解读都喜欢跳过这部分,我不知道他们是什么心态,因为这恰恰是最重要的一 ...
- 树莓派 + Home Assistant + HomeKit 从零开始打造个人智能家居系统 篇三:进阶配置 Home Assistant
树莓派 + Home Assistant + HomeKit 从零开始打造个人智能家居系统 篇三:进阶配置 Home Assistant 通过本篇教程,你将完成对 Home Assistant 的进一 ...
- 树莓派 + Home Assistant + HomeKit 从零开始打造个人智能家居系统 篇二:初步配置 Home Assistant 并连接小米设备与 HomeKit
树莓派 + Home Assistant + HomeKit 从零开始打造个人智能家居系统 篇二:初步配置 Home Assistant 并连接小米设备与 HomeKit 通过本篇教程,你将完成对 H ...
- 从零开始开发Android相机app(三)简单介绍图像滤镜功能
目前章节 1.从零开始安卓端相机功能开发(一)了解用什么去开发以及流程 2.从零开始安卓端相机功能开发(二)让我们来开发一个相机 3.从零开始开发Android相机app(三)简单介绍图像滤镜功能 文 ...
- 使用Node.js+Koa 从零开始写个人博客系统——后端部分(一)
使用Node.js+Koa 从零开始写个人博客系统系列 提示:在此文章中你可以学习到的内容如下: 1 如何使用Koa快速搭建项目 2 对Koa的核心组件Koa-Route的简单使用 3 3层架构思想 ...
最新文章
- 2018-2019-1 20165214 《信息安全系统设计基础》第六周学习总结
- 微信公众号自定义菜单跳转小程序
- 营销型网站吸引用户说难也难,说简单也简单
- AndFix解析——(上)
- ffmpeg-win32-v3.2.4 下载_为知笔记安卓版下载 8.1.4
- Java中的装饰器设计模式
- JS 异步编程及常考面试题
- 埋点是什么意思_(一百二十二)埋点方案举例,如何做埋点方案
- 饭店流量预测-多表关联+lightgbm
- 地狱已满 服务器无响应,steam地狱已满怎么联网
- ASIHTTPRequest类库的简单介绍
- 如何理解D触发器延迟一拍
- 微信小程序提交审核时提示:“ 小程序页面内容涉及账户充值服务,需补充商家自营-预付卡销售-发行方类目。请在基础信息处申请该类目”的解决方法
- Linux常用库函数
- Android屏幕直播方案
- 三菱Q PLC案例程序,三菱Q系列程序。 QD75MH总线伺服本案例是液晶电视导光板加工
- 【图像数学形态学处理】膨胀与腐蚀运算
- char变量能不能存储一个汉字
- 如何关闭或启动mysql服务
- 计算机毕业设计:基于Springboot+vue口腔牙科诊所管理系统