第一步实现:ETL的设计分三部分:数据抽取(Data Extraction)、数据的清洗转换(Data Transformation)、数据的加载(Data Loading).
构建一个数据容器类,其中包含转换后的MNIST手写数据。还实现了一个数据处理程序,该数据处理程序将提取并转换数据以供将来的算法实现使用。
cpp
#ifndef __DATA_H
#define __DATA_H
#include <iostream>
#include <vector>
#include "stdint.h"
#include "stdio.h"
// 数据类
class data
{
std::vector<uint8_t>* feature_vector; // 特征向量
uint8_t label; // 标签
int enum_label; // 枚举标签 A->1, B->2, C->3, D->4, E->5, F->6, G->7, H->8, I->9, J->10
public:
data(); // 构造函数
~data(); // 析构函数
void set_feature_vector(std::vector<uint8_t> *); // 设置特征向量
void append_to_feature_vector(uint8_t); // 向特征向量追加数据
void set_label(uint8_t); // 设置标签
void set_enum_label(int); // 设置枚举标签
int get_feature_vector_size(); // 获取特征向量大小
uint8_t get_label(); // 获取标签
uint8_t get_enumerated_label(); // 获取枚举标签
std::vector<uint8_t>* get_feature_vector(); // 获取特征向量
};
#endif
这段代码定义了一个名为 data
的类,用于处理特征向量和标签。首先,代码使用了头文件保护机制,通过 #ifndef
、#define
和 #endif
来防止重复包含头文件 data.hpp
。
在 data
类中,有三个私有成员变量:feature_vector
是一个指向 std::vector<uint8_t>
的指针,用于存储特征向量;label
是一个 uint8_t
类型的变量,用于存储标签;enum_label
是一个整数,用于存储枚举标签,注释中说明了不同字符对应的整数值(例如,A 对应 1,B 对应 2,依此类推)。
在公共成员函数部分,data
类提供了一些方法来操作和访问这些成员变量:
void set_feature_vector(std::vector<uint8_t> *)
:设置特征向量的指针。void append_to_feature_vector(uint8_t)
:向特征向量中追加一个uint8_t
类型的值。void set_label(uint8_t)
:设置标签。void set_enum_label(int)
:设置枚举标签。
此外,还有一些方法用于获取成员变量的值:
int get_feature_vector_size()
:获取特征向量的大小。uint8_t get_label()
:获取标签。uint8_t get_enumerated_label()
:获取枚举标签。std::vector<uint8_t>* get_feature_vector()
:获取特征向量的指针。
这些方法使得 data
类能够灵活地操作和访问特征向量和标签,适用于需要处理大量数据的场景。
cpp
#include "data.hpp"
data::data(){
feature_vector = new std::vector<uint8_t>;
}
data::~data()
{
delete feature_vector;
}
void data::set_feature_vector(std::vector<uint8_t> *vect)
{
feature_vector = vect;
}
void data::append_to_feature_vector(uint8_t val)
{
feature_vector->push_back(val);
}
void data::set_label(uint8_t val)
{
label = val;
}
void data::set_enum_label(int val)
{
enum_label = val;
}
int data::get_feature_vector_size()
{
return feature_vector->size();
}
uint8_t data::get_label()
{
return label;
}
uint8_t data::get_enumerated_label()
{
return enum_label;
}
std::vector<uint8_t>* data::get_feature_vector()
{
return feature_vector;
}
这段代码实现了 data
类的构造函数、析构函数以及多个成员函数。首先,代码包含了头文件 data.hpp
,以确保类的声明可用。
构造函数 data::data()
初始化了 feature_vector
,为其分配了一个新的 std::vector<uint8_t>
对象。析构函数 data::~data()
则负责释放该内存,防止内存泄漏。
new出来的是对象,但是返回的是指向这个对象的指针;
set_feature_vector
方法接受一个指向 std::vector<uint8_t>
的指针,并将其赋值给 feature_vector
。append_to_feature_vector
方法向 feature_vector
中追加一个 uint8_t
类型的值。
set_label
和 set_enum_label
方法分别设置 label
和 enum_label
的值。
get_feature_vector_size
方法返回 feature_vector
的大小。get_label
和 get_enumerated_label
方法分别返回 label
和 enum_label
的值。最后,get_feature_vector
方法返回 feature_vector
的指针。
总体来说,这段代码实现了 data
类的基本功能,使其能够管理和操作特征向量和标签。
2.处理数据
cpp
#ifndef __DATA_HANDLER_H
#define __DATA_HANDLER_H
#include<fstream>
#include "stdint.h"
#include"data.hpp"
#include<vector>
#include<string>
#include<map>
#include<unordered_set>
// 数据处理类
class data_handler
{
std::vector<data *> *data_array; // 数据数组
std::vector<data *> *training_data; // 训练数据
std::vector<data *> *testing_data; // 测试数据
std::vector<data *> *validation_data; // 验证数据
int num_classes; // 类别数量
int feature_vector_size; // 特征向量大小
std::map<uint8_t, int> class_map; // 类别映射
const double TRAIN_SET_PERCENTAGE = 0.75; // 训练集比例
const double TEST_SET_PERCENTAGE = 0.20; // 测试集比例
const double VALIDATION_SET_PERCENTAGE = 0.05; // 验证集比例
public:
data_handler(); // 构造函数
~data_handler(); // 析构函数
void read_feature_vector(std::string path); // 读取特征向量
void read_label_vector(std::string path); // 读取标签向量
void split_data(); // 分割数据
void count_classes(); // 统计类别数量
uint32_t convert_to_little_endian(const unsigned char* bytes); // 转换为小端序
std::vector<data *> *get_training_data(); // 获取训练数据
std::vector<data *> *get_testing_data(); // 获取测试数据
std::vector<data *> *get_validation_data(); // 获取验证数据
};
#endif
这个类名为 data_handler
,用于处理数据集的读取、分割和分类等操作。以下是对该类的详细解释:
成员变量
-
std::vector<data *> *data_array
:- 指向一个
std::vector
容器的指针,该容器存储了所有的数据对象的指针。
- 指向一个
-
std::vector<data *> *training_data
:- 指向一个
std::vector
容器的指针,该容器存储了训练数据集的数据对象的指针。
- 指向一个
-
std::vector<data *> *testing_data
:- 指向一个
std::vector
容器的指针,该容器存储了测试数据集的数据对象的指针。
- 指向一个
-
std::vector<data *> *validation_data
:- 指向一个
std::vector
容器的指针,该容器存储了验证数据集的数据对象的指针。
- 指向一个
-
int num_classes
:- 存储数据集中类别的数量。
-
int feature_vector_size
:- 存储特征向量的大小。
-
std::map<uint8_t, int> class_map
:- 一个映射,用于将类别标签(
uint8_t
类型)映射到整数值。
- 一个映射,用于将类别标签(
-
const double TRAIN_SET_PERCENTAGE
:- 常量,表示训练数据集所占的比例,值为 0.75。
-
const double TEST_SET_PERCENTAGE
:- 常量,表示测试数据集所占的比例,值为 0.20。
-
const double VALIDATION_SET_PERCENTAGE
:- 常量,表示验证数据集所占的比例,值为 0.05。
构造函数和析构函数
-
data_handler()
:- 构造函数,用于初始化
data_handler
对象。
- 构造函数,用于初始化
-
~data_handler()
:- 析构函数,用于释放
data_handler
对象所占用的资源。
- 析构函数,用于释放
成员函数
-
void read_feature_vector(std::string path)
:- 从指定路径读取特征向量数据。
-
void read_label_vector(std::string path)
:- 从指定路径读取标签数据。
-
void split_data()
:- 将数据集分割为训练集、测试集和验证集。
-
void count_classes()
:- 统计数据集中各个类别的数量。
-
uint32_t convert_to_little_endian(const unsigned char* bytes)
:- 将字节数组转换为小端格式的
uint32_t
类型。
- 将字节数组转换为小端格式的
-
std::vector<data *> *get_training_data()
:- 返回指向训练数据集的指针。
-
std::vector<data *> *get_testing_data()
:- 返回指向测试数据集的指针。
-
std::vector<data *> *get_validation_data()
:- 返回指向验证数据集的指针。
cpp
#include <iostream>
#include "data_handler.hpp"
data_handler::data_handler()
{
data_array = new std::vector<data *>;
testing_data = new std::vector<data *>;
training_data = new std::vector<data *>;
validation_data = new std::vector<data *>;
}
data_handler::~data_handler()
{
// Free memory Dynamically allocated
delete data_array;
delete testing_data;
delete training_data;
delete validation_data;
}
/*
这段代码的主要目的是为读取图像数据文件的头部信息做好准备。以下是逐行的详细解释:
uint32_t header[4]; // |magic number|number of images|number of rows|number of columns|
这行代码定义了一个大小为4的无符号整数数组header,用于存储图像数据文件的头部信息。具体的含义是:
magic number:用于标识文件格式的数字,通常用于验证文件类型。
number of images:图像的总数量。
number of rows:每张图像的行数(高度)。
number of columns:每张图像的列数(宽度)。
unsigned char bytes[4];
这行代码定义了一个大小为4的无符号字符数组bytes,用于暂时存放从文件中读取的字节,以便后续转换为整型数据。
FILE *f = fopen(path.c_str(), "r");
这行代码尝试打开指定路径的文件,并以只读模式("r")打开。如果打开成功,f指针将指向该文件;如果失败,f将为nullptr。path.c_str()用于将std::string类型的path转换为C风格的字符串。
总结
这段代码的主要功能是为从文件中读取和解析图像数据的头部信息做准备。通过定义适当的数据结构来存储所需的头部信息,同时准备打开文件并进行读取操作。这是图像数据处理中的重要一步,因为正确解析头部信息对于后续数据的正确读取和处理至关重要。
*/
void data_handler::read_feature_vector(std::string path)
{
uint32_t header[4]; // |magic number|number of images|number of rows|number of columns|
unsigned char bytes[4];
FILE *f = fopen(path.c_str(), "r");
if(f)
{
for(int i=0;i<4;i++)
{
if(fread(bytes,sizeof(bytes),1,f))
{
header[i] = convert_to_little_endian(bytes);
}
}
printf("Done getting Input File header\n");
int image_size = header[2] * header[3];
for(int i =0;i<header[1];i++)
{
data *d = new data();
uint8_t element[1];
for(int j=0;j<image_size;j++)
{
if(fread(element,sizeof(element),1,f))
{
d->append_to_feature_vector(element[0]);
} else
{
printf("Error reading file\n");
exit(1);
}
}
data_array->push_back(d);
}
printf("Successful read and stored\n");
} else{
printf("Could not find file\n");
exit(1);
}
}
void data_handler::read_feature_labels(std::string path)
{
uint32_t header[2]; // |magic number|number of images
unsigned char bytes[4];
FILE *f = fopen(path.c_str(), "r");
if(f)
{
for(int i=0;i<2;++i)
{
if(fread(bytes,sizeof(bytes),1,f))
{
header[i] = convert_to_little_endian(bytes);
}
}
printf("Done getting Lable File header\n");
for(int i =0;i<header[1];++i)
{
uint8_t element[1];
if(fread(element,sizeof(element),1,f))
{
data_array->at(i)->set_label(element[0]);
} else
{
printf("Error reading file\n");
exit(1);
}
}
printf("Successful read and stored\n");
} else{
printf("Could not find file\n");
exit(1);
}
}
void data_handler::split_data(){
std::unordered_set<int> used_indexes;
int train_size = data_array->size() * TRAIN_SET_PERCENTAGE;
int test_size = data_array->size() * TEST_SET_PERCENTAGE;
int validation_size = data_array->size() * VALIDATION_SET_PERCENTAGE;
// Training Data
int count = 0;
while(count<train_size){
int rand_index = rand() % data_array->size();
if(used_indexes.find(rand_index)==used_indexes.end()){
training_data->push_back(data_array->at(rand_index));
used_indexes.insert(rand_index);
count++;
}
}
// Testing Data
count = 0;
while(count<test_size)
{
int rand_index = rand() % data_array->size();
if(used_indexes.find(rand_index)==used_indexes.end())
{
testing_data->push_back(data_array->at(rand_index));
used_indexes.insert(rand_index);
count++;
}
}
// Validation Data
count = 0;
while(count<validation_size)
{
int rand_index = rand() % data_array->size();
if(used_indexes.find(rand_index)==used_indexes.end())
{
validation_data->push_back(data_array->at(rand_index));
used_indexes.insert(rand_index);
count++;
}
}
std::cout << "Training data size: " << training_data->size() << "." << std::endl;
std::cout << "Test data size: " << testing_data->size() << "." << std::endl;
std::cout << "Validation data size: " << validation_data->size() << "." << std::endl;
}
void data_handler::count_classes()
{
int count = 0;
for(unsigned i=0;i<data_array->size();i++)
{
if(class_map.find(data_array->at(i)->get_label())==class_map.end())
{
class_map[data_array->at(i)->get_label()] = count;
data_array->at(i)->set_enum_label(count);
count++;
} else
{
data_array->at(i)->set_enum_label(class_map[data_array->at(i)->get_label()]);
}
}
std::cout << "Successfully extracted " << count << " unique classes." << std::endl;
}
uint32_t data_handler::convert_to_little_endian(const unsigned char* bytes)
{
return (uint32_t) ((bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | (bytes[3] << 0));
}
std::vector<data *> * data_handler::get_training_data()
{
return training_data;
}
std::vector<data *> * data_handler::get_testing_data()
{
return testing_data;
}
std::vector<data *> * data_handler::get_validation_data()
{
return validation_data;
}
int main()
{
data_handler *dh = new data_handler();
dh->read_feature_vector("./train-images-idx3-ubyte");
dh->read_feature_labels("./train-labels-idx1-ubyte");
dh->split_data();
dh->count_classes();
}
总结
data_handler
类提供了一系列方法,用于读取数据、分割数据集、统计类别数量以及获取训练集、测试集和验证集。通过这些方法,可以方便地管理和处理数据集,适用于机器学习和数据分析等场景。
遇到的问题1:abort()作用
回答:
问题2:make clean作用
回答:
简单一点来说就是make clean 就是执行makefile文件里面的clean规则,将编译产生的所有东西清理,项目回到编译之前的状态;
在data_handler.cpp文件里面写了int main(){...} 可以做一个测试,看看对数据的读取有没有出现问题,
于是:g++ -std=c++11 -I.include/ src/* -o main 就是找到头文件,并根据源文件生成可执行文件,实际上就是通过编译data_handler.cpp得到的,当然其中会链接到头文件
Makefile文件更加清晰地展现了 编译链接的过程;
前面都是一些替换符
all下面才是编译的整体流程,可以从下往上面看
cpp
CC=g++
INCLUDE_DIR :=${MNIST_ML_ROOT}/include
SRC_DIR :=${MNIST_ML_ROOT}/src
CFLAGS=-std=c++11 -g
LIB_DATA :=libdata.so
all : ${LIB_DATA}
${LIB_DATA} : libdir objdir obj/data_handler.o obj/data.o
${CC} ${CFLAGS} -o ${MNIST_ML_ROOT}/lib/${LIB_DATA} obj/*.o
rm -r ${MNIST_ML_ROOT}/obj
libdir :
mkdir -p ${MNIST_ML_ROOT}/lib
objdir :
mkdir -p ${MNIST_ML_ROOT}/obj
obj/data_handler.o : ${SRC_DIR}/data_handler.cpp
${CC} -fPIC ${FLAGS} -o obj/data_handler.o -I$(INCLUDE_DIR) -c $(SRC_DIR)/data_handler.cpp
obj/data.o : ${SRC_DIR}/data.cpp
${CC} -fPIC ${FLAGS} -o obj/data.o -I$(INCLUDE_DIR) -c $(SRC_DIR)/data.cpp
clean :
rm -r ${MNIST_ML_ROOT}/lib
rm -r ${MNIST_ML_ROOT}/obj
#这里有意思的一个点就是${MNIST_ML_ROOT} 没有被定义,可以在makefile中定义,也可以在运行make命令时指定,如:make MNIST_ML_ROOT=/home/zy/mnist_ml
#也可以 export MNIST_ML_ROOT=/home/zy/mnist_ml,然后在makefile中使用$(MNIST_ML_ROOT)
这段代码是一个 Makefile,主要用于管理和自动化 C++ 项目的构建过程。下面是逐步分解和详细解释:
- 变量定义
CC=g++:指定 C++ 编译器为 g++。
INCLUDE_DIR :=${MNIST_ML_ROOT}/include:指定头文件的目录,这里的 ${MNIST_ML_ROOT} 需要在执行命令时定义。
SRC_DIR :=${MNIST_ML_ROOT}/src:指定源代码的目录。
CFLAGS=-std=c++11 -g:指定编译选项,这里使用 C++11 标准并包括调试信息。
LIB_DATA :=libdata.so:指定生成的共享库文件名。
- 目标与依赖关系
all : ${LIB_DATA}:默认目标是生成共享库 libdata.so。
${LIB_DATA} : libdir objdir obj/data_handler.o obj/data.o:表示生成 libdata.so 依赖于 libdir、objdir 以及两个目标文件 data_handler.o 和 data.o。
- 目录创建
libdir : 和 objdir ::这两个目标负责创建存放生成文件的目录 lib 和 obj,使用 mkdir -p 确保目录存在。
- 文件编译
obj/data_handler.o : ${SRC_DIR}/data_handler.cpp:表示目标文件 data_handler.o 依赖于源文件 data_handler.cpp。
obj/data.o : ${SRC_DIR}/data.cpp:表示目标文件 data.o 依赖于源文件 data.cpp。
这两个规则中,使用 g++ 进行编译,选项包括 -fPIC (生成位置无关的代码),-o 指定输出文件,-I$(INCLUDE_DIR) 指定头文件的搜索路径,-c 表示只编译而不链接。
- 清理操作
clean ::定义了一个清理目标,通过删除 lib 和 obj 目录下的文件来清理构建生成的内容。
总结
这个 Makefile 的主要功能是自动化构建一个 C++ 项目,生成共享库 libdata.so。它通过定义相应的规则和目标,确保在编译源文件之前创建必要的目录,并在构建结束后提供清理功能。重要的是,${MNIST_ML_ROOT} 变量未在 Makefile 中定义,而是可以在命令行中传递,这提高了灵活性。整体来说,这个 Makefile 适合用于处理较为复杂的 C++ 项目构建过程。
递归进行