【Kuiperinfer】笔记03 张量(Tensor)类设计与实现,单元测试解析

时间:2024-03-09 07:41:23

在这里插入图片描述

文章目录

  • Tensor类的组成
  • Tensor类设计
  • 数据顺序(行主序/列主序)
  • 使用单元测试
  • Tensor类方法描述
    • C++中的类模板
    • 张量创建
      • 单元测试
    • 返回维度信息
      • 单元测试
    • 返回张量中的数据
      • 单元测试
    • 张量填充
      • 单元测试
    • reshape
      • 单元测试
    • 逐元素处理
      • 单元测试
    • 其他辅助函数
      • 判空
      • 返回数据存储的起始位置
      • 返回张量的shape
    • 练习
      • Flatten
      • Padding
  • 参考

Tensor类的组成

张量的数据按照channels,rows,cols的顺序排放,主要包含以下部分:

  1. 数据,可以是double,float或int
  2. shape信息
  3. 各种类方法,例如返回张量的形状、填充张量数据和reshape等。

Tensor类设计

Tensor类需要提供高效的矩阵计算算法,同时也应该在软件工程的层面上优化接口。

Kuiperinfer中的张量是以arma::fcube为基础进行开发的,三维的arma::fcube是由多个二维的matrix在channel维度叠加形成的。

fcubeCube<float>的简写,是armadillo做的typedef。

其constructor形式为cube(n_rows, n_cols, n_slices),分别对应行、列、通道数。

对这样的一个Tensor类,需要进行以下工作:

  1. 提供对外接口,在fcube类的基础上进行;
  2. 封装矩阵计算,提供更友好的数据访问和使用接口。

类定义:

template <>
class Tensor<float> {
public:
    uint32_t rows() const;
    uint32_t cols() const;
    uint32_t channels const;
    uint32_t size() const;
    void set_data(const arma::fcube& data);
    ...
private:
    std::vector<uint32_t> raw_shapes_; // 数据的shape
    arma::fcube data_; // 数据存储
    // 在变量名后面加下划线是c++中常见的一种命名规范,用于说明该变量为类的数据成员,而不是方法成员;另一种常见的命名方法为m_data
}

数据顺序(行主序/列主序)

矩阵存储有两种形式:行主序和列主序。行主序先填行,列主序先填列。

在这里插入图片描述

在这里插入图片描述

armadillo是默认列主序的,而PyTorch是行主序的,想要和PyTorch对齐,应当做出一些调整。

使用单元测试

在VS中配置单元测试的方法:

  1. 打开CourseLists.txt文件,在文件的最后一行添加

    gtest_discover_tests(kuiper_datawhale_course2) // 参数对应前面add_executable中的项目名称
    
  2. 在CMake设置中,设置将生成输出复制回本地计算机为true;或者编辑CMakeSettings.json,在对应的配置字典中添加

    "remoteCopySources": true,
    
  3. 生成项目,此时项目文件夹中应该会多出一个out/build文件夹。

  4. 测试 > 测试资源管理器,可以看到项目中所有的测试

    在这里插入图片描述

可以顺带在选项 > 适用于Google Test的测试适配器 > 并行化中,设置并行测试执行为True,可以加快多个测试的运行速度

Tensor类方法描述

主要包含下面几类方法:

  • 张量创建(constructor)
  • 返回维度信息
  • 获取张量数据
  • 填充数据
  • element-wise处理
  • reshape
  • 辅助函数:判空、返回地址、shape
  • Flatten
  • Padding

C++中的类模板

C++中的模板类以下面的代码开头

template <typename Type>

此时,类外类方法成员的限定符也应该从ClassName::改为ClassName<Type>::

在Kuiperinfer的代码实现中,Tensor模板类的定义如下:

template <typename T = float> 
class Tensor; // 模板类声明,T为模板参数,float为模板参数默认值

template <>
class Tensor<float>{}; // template specialization,模板具体化,使用具体的类型(这里是float)给出对应的类定义

张量创建

张量创建方法通过构造函数(constructor)来实现;当程序声明对象时,会自动调用符合传入参数的构造函数;在对象被销毁时,会调用析构函数。

在Tensor类中,需要实现以下几种传参的构造函数:

  • 一维张量
  • 二维张量
  • 三维张量

为方便起见,在底层都使用三维的arma::fcube来表示,因此需要在不需要的维度填1。设shapes的参数顺序为rows, cols, channels,实现如下:

Tensor<float>::Tensor(uint32_t size){
    data_ = arma::fcube(1, size, 1); // 默认为列向量
    this->raw_shapes_ = std::vector<uint32_t>{size};
}
Tensor<float>::Tensor(uint32_t rows, uint32_t cols){
    data_ = arma::fcube(rows, cols, 1);
    this->raw_shapes_ = std::vector<uint32_t>{rows, cols};
}
Tensor<float>::Tensor(uint32_t channels, uint32_t rows, uint32_t cols){
    data_ = arma::fcube(rows, cols, channels);
    this->raw_shapes_ = std::vector<uint32_t>{channels, rows, cols};
}

在头文件的声明中,这些方法都标了explicit,从而避免错误传参导致隐式类型转换。

单元测试

在test_create_tensor.cpp中,进行张量创建的测试

  • 测试对应张量的fcube形状
  • 测试shapes的值

kuiperinfer源码中给出的测试代码为在LOG中输出相关信息,这里尝试用ASSERT_EQ和ASSERT_TRUE进行判别。

TEST(test_tensor, tensor_init1DEQ) {
	using namespace kuiper_infer;
	Tensor<float> f1(4);
	f1.Fill(1.0f); // this->data_.fill(value)
    
	const auto& shapes = f1.raw_shapes(); // return this->raw_shapes_;
	std::vector<uint32_t> shapes_should_be = std::vector<uint32_t>{ 4 };
	ASSERT_EQ(shapes, shapes_should_be);
	const auto& data = f1.data(); // return this->data_;
	arma::fcube data_should_be = arma::fcube(1, 4, 1, arma::fill::value(1.0f));
	ASSERT_TRUE(approx_equal(data, data_should_be, "absdiff", 1e-6));
}
TEST(test_tensor, tensor_inti2DEQ) {
	using namespace kuiper_infer;
	Tensor<float> f1(4, 4);
	f1.Fill(1.0f);

	const auto& shapes = f1.raw_shapes();
	const auto& data = f1.data();
	std::vector<uint32_t> shapes_s = std::vector<uint32_t>{ 4, 4 };
	arma::fcube data_s = arma::fcube(4, 4, 1, arma::fill::value(1.0f));
	ASSERT_EQ(shapes, shapes_s);
	ASSERT_TRUE(arma::approx_equal(data, data_s, "absdiff", 1e-6));
}
TEST(test_tensor, tensor_init3DEQ) {
	using namespace kuiper_infer;
	Tensor<float> f1(2, 3, 4);
	f1.Fill(1.0f);

	const auto& shapes = f1.raw_shapes();
	const auto& data = f1.data();
	std::vector<uint32_t> shapes_s = std::vector<uint32_t>{ 2, 3, 4 };
	arma::fcube data_s = arma::fcube(3, 4, 2, arma::fill::value(1.0f));
	ASSERT_EQ(shapes, shapes_s);
	ASSERT_TRUE(arma::approx_equal(data, data_s, "absdiff", 1e-6));
}

返回维度信息

实现以下方法:

  • rows()
  • cols()
  • channels()
  • size()

其实直接返回shapes里存储的值也可以

uint32_t Tensor<float>::rows const{
    CHECK(!this->data_.empty()); // CHECK dies with a fatal error if the condition not true
    return this->data_.n_rows;
}
uint32_t Tensor<float>::cols const{
    CHECK(!this->data_.empty());
    return this->data_.n_cols;
}
uint32_t Tensor<float>::channels const{
    CHECK(!this->data_.empty());
    return this->data_.n_slices;
}
uint32_t Tensor<float>::size() const{
    CHECK(!this->data_.empty());
    return this->data_.size();
}

单元测试

TEST(test_tensor_size, tensor_size1) {
  using namespace kuiper_infer;
  Tensor<float> f1(2, 3, 4);
  
  ASSERT_EQ(f1.channels(), 2);
  ASSERT_EQ(f1.rows(), 3);
  ASSERT_EQ(f1.cols(), 4);
}

返回张量中的数据

实现以下方法:

  • slice(uint32_t channel),返回对应channel的数据,返回类型为arma::fmat
  • at(uint32_t channel, uint32_t row, uint32_t col),返回对应(channel, row, col)的数据
const arma::fmat Tensor<float>::slice(uint32_t channel) const{
    CHECK_LT(channel, this->channels());
    return this->data_.slice(channel);
}
float Tensor<float>::at(uint32_t channel, uint32_t row, uint32_t col) const{
    CHECK_LT(channel, this->channels());
    CHECK_LT(row, this->rows());
    CHECK_LT(col, this->cols());
    return this->data_.at(row, col, channel);
}
arma::fcube Tensor<float>::data() const{
    return this->data_;
}

单元测试

TEST(test_tensor_values, tensor_values1) {
  using namespace kuiper_infer;
  Tensor<float> f1(2, 3, 4);
  f1.Fill(1.0f);
    
  ASSERT_EQ(1, f1.at(1, 1, 1));
  const auto& mat = f1.slice(0);
  arma::fmat mat_s = arma::fmat(3, 4, arma::fill::value(1.0f));
  ASSERT_TRUE(arma::approx_equal(mat, mat_s, "absdiff", 1e-6));
}

张量填充

实现以下方法:

  • Fill(float value)
  • Fill(const std::vector<float>& values, bool row_major)
  • Rand()
  • Ones()
  • values(bool row_major):返回特定顺序的值

第二个参数用于控制填充顺序,如果为true则按行主序填充

void Tensor<float>::Fill(float value){
    CHECK(!this->data_.empty());
    this->data_.fill(value)
}
void Tensor<float>::Rand(){
    CHECK(!this->data_.empty());
    this->data_.randn();
}
void Tensor<float>::Ones(){
    CHECK(!this->data_.empty());
    this->Fill(1.0f);
}
void Tensor<float>::Fill(const std::vector<float>& values, bool row_major){
    CHECK(!this->data_.empty());
    const uint32_t total_elems = this->data_size();
    CHECK_EQ(values.size(), total_elems);
    if(row_major){
        const uint32_t rows = this->rows();
        const uint32_t cols = this->cols();
        const uint32_t planes = rows * cols;
        const uint32_t channels = this->data_n_slices();
        
        for (uint32_t i = 0; i < channels; ++i){
            auto& channel_data = this->data_slice(i);
            const arma::fmat& channel_data_t = arma::fmat(values.data() + i * planes, this->cols(), this->rows());
            channel_data = channel_data_t.t();
        }
    }
    else{
        std::copy(values.begin(), values.end(), this-data_.memptr());
        // fcube本来就是列主序,所以直接copy
    }
}
std::vector<float> Tensor<float>::values(bool row_major){
    CHECK_EQ(this->data_.empty(), false);
    std::vector<float> values(this->data_.size()); // values length shapes
    
    if(!row_major){
        std::copy(this->data_.mem, this->data_.mem + this->data_.size(), values.begin()); // 列主序直接copy
    }
    else{
        uint32_t index = 0;
        for (uint32_t c = 0; c < this->data_.n_slices; ++c){ // 转序每个channel
            const arma::fmat& channel = this->data_.slice(c).t();
            std::copy(channel.begin(), channel.end(), values.begin() + index);
            index += channel.size();
        }
        CHECK_EQ(index, values.size());
    }
    return values;
}

单元测试

直接输出每个channel更加直观

TEST(test_fill_reshape, fill1) {
  using namespace kuiper_infer;
  Tensor<float> f1(2, 3, 4);
  std::vector<float> values(2 * 3 * 4);
  // 将1到24填充到values中
  for (int i = 0; i < 24; ++i) {
    values.at(i) = float(i + 1);
  }
  f1.Fill(values, true);
  f1.Show();
}
I20240229 05:25:13.321183  2142 tensor.cpp:199] Channel: 0
I20240229 05:25:13.321188  2142 tensor.cpp:200]
    1.0000    4.0000    7.0000   10.0000
    2.0000