自制深度学习推理框架之Tensor模板类的设计与实现

server/2024/10/18 10:15:31/

文章目录

    • 一、Tensort介绍
    • 二、Armadillo实现Tensor模板类
      • 2.1 tensor类模板
      • 2.2 Tensor类的设计
        • 2.2.1 矩阵存储顺序
        • 2.2.2 Tensor类具体实现

一、Tensort介绍

张量(Tensor)是一个多维数组的通用化概念,在数学和计算科学中被广泛使用,特别是在机器学习、物理学和工程学等领域。它是标量(0维张量)、向量(1维张量)和矩阵(2维张量)的一般化,可以扩展到更高的维度。

Tensor可以看作是一个具有任意维数的多维数组。每个张量有以下属性:

  • 秩(Rank): 张量的维数,即张量的轴的数量。0维张量是标量,1维张量是向量,2维张量是矩阵,依此类推。
  • 形状(Shape): 每个维度的长度。形状决定了张量在每个维度上包含的元素数量。
  • 数据类型(Data Type): 张量中的元素类型,如整型、浮点型等。

Tensor是深度学习推理框架必须提供的基础数据结构,神经网络的前向传播是基于 Tensor 类进行的。因此,在自制深度推理框架中我们需要实现Tensor类。

Armadillo 是一个高效的 C++ 线性代数库,通常用于矩阵和向量操作。虽然 Armadillo 本身没有直接提供张量(Tensor)类,但我们可以基于 Armadillo 提供的矩阵和向量功能,构建一个简单的张量模板类,使用 Armadilloarma::fcube 来支持三维张量的基本操作。arma::fcubeArmadillo库中的一个类型,表示一个三维浮点数矩阵(3D cube)。具体来说,它是一个包含float 类型元素的三维数组。fcube` 可以用于表示三维的张量数据结构,常用于处理彩色图像、体数据(如医学图像),或其它具有三个维度的数据。

将arma::fcube封装成Tensor可以使其更方便地在深度学习等领域中使用,提供了更加直观和易用的接口,同时与其他深度学习框架(如TensorFlow、PyTorch等)进行对接时也比较方便。除了Armadillo库,还有一些其他的常用的线性代数C++库,比如Eigen。

二、Armadillo实现Tensor模板类

Tensor用于在算子之间传递数据,Tensor不仅有存放数据的功能,还可以处理基本的Tensor操作。对于一个Tensor类而言,数据将被设计成依次摆放的三维格式,分别是channels(通道数), rows(行数),cols(列数)。一个张量类主要由以下部分组成:

  • 数据本身存储在该类的数据空间中,数据可包括双精度(double)、单精度(float)或整型(int),以满足不同的精度需求。

  • 为了处理多维张量数据,需要使用shape变量来存储张量的维度信息。例如,对于一个维度为3,长和宽均为224的张量,其维度信息可以表示为(3, 224, 224)

  • Tensor 类中定义了多个类方法,比如加、减、乘、除、返回张量的宽度、高度、填充数据和张量变形 (reshape)等操作。

2.1 tensor类模板

从上面可以知道,Tensor类是对armdillo库中cube类的封装,cube是多个Matrix的集合(二维矩阵的集合)。Tensor类模板如下所示:

// 定义一个通用的模板类 Tensor,T 是模板参数,默认为 float 类型
template <typename T = float>
class Tensor
{
};// 完全特化的模板类 Tensor,用于 uint8_t 类型
template <>
class Tensor<uint8_t>
{// 待实现
};// 完全特化的模板类 Tensor,用于 float 类型。
template <>
class Tensor<float>
{
public:explicit Tensor() = default;explicit Tensor(uint32_t channels, uint32_t rows, uint32_t cols);......uint32_t rows() const;uint32_t cols() const;uint32_t channels() const;......float at(uint32_t channel, uint32_t row, uint32_t col) const;void Padding(const std::vector<uint32_t> &pads, float padding_value);void Fill(const std::vector<float> &values, bool row_major = true);std::vector<float> values(bool row_major = true);void Reshape(const std::vector<uint32_t> &shapes, bool row_major = false);void Flatten(bool row_major = false);void Transform(const std::function<float(float)> &filter);......private:std::vector<uint32_t> raw_shapes_; // 张量数据的实际尺寸大小arma::fcube data_;                 // 张量数据
};// 定义别名
using ftensor = Tensor<float>;
using sftensor = std::shared_ptr<Tensor<float>>;

Tensor类模板中,Tensor共有两个类型,一个类型是Tensor<float>,另一个类型是Tensor<uint8_t>, Tensor<uint8_t>可能会在后续的量化中进行使用。

Tensor<float> 类实现了各种接口和功能,以便处理三维浮点数张量的数据。以下是实现的主要功能和接口的部分描述:

1.构造函数

实现了默认构造、拷贝构造、移动构造以及创建具有指定通道数、行数和列数的张量。

2.赋值运算符

实现移动赋值运算符拷贝赋值运算符

3.数据访问与操作

实现获取张量的行数、列数、通道数和大小、设置张量的数据、访问张量的数据等操作。

4.张量的操作

实现填充张量、Reshape、Flatten、Transform等操作。

5.数据成员

  • raw_shapes_: 保存张量的实际维度信息。

  • data_: 保存张量的 arma::fcube 数据。

对于Tensor类,主要做了以下的两个工作:

  1. 提供对外的接口,对外接口由Tensor类在fcube类的基础上进行提供,以供用户更好地访问多维数据。
  2. 封装矩阵相关的计算功能,这样一来不仅有更友好的数据访问和使用方式,也能有高效的矩阵算法实现。

2.2 Tensor类的设计

选择在arma::fcube(三维矩阵)的基础上进行开发。如下图所示,三维的arma::fcube是由多个二维矩阵matrixarma::fmat)沿通道维度叠加得到。在此基础上,张量类将在叠加而成的三维矩阵arma::fcube的基础上提供扩充和封装,以使其更适用于推理框架项目。

在这里插入图片描述

2.2.1 矩阵存储顺序

矩阵的存储形式可以分为两种:行主序(row-major order)和列主序(column-major order)。这两种方式在矩阵的内存布局上有着不同的顺序和特点。

  • 行主序(row-major order)

在行主序中,矩阵的数据按的顺序连续存储在内存中。这意味着矩阵的第一行的所有元素会依次存储在内存的连续位置上,然后是第二行,依此类推。

假如有一个 3×3的矩阵,这9个数据在一个行主序的3 x 3 矩阵中有如下的排布形式,其中箭头指示了内存地址的增长方向。从图中可以看出,内存地址增长的方向先是横向,然后是纵向,呈Z字形

在这里插入图片描述

在行主序中,这个矩阵在内存中的存储顺序是:[0,1,2,3,4,5,6,7,8]。

  • 列主序(column-major order)

在列主序中,矩阵的数据按的顺序连续存储在内存中。这意味着矩阵的第一列的所有元素会依次存储在内存的连续位置上,然后是第二列,依此类推。

同样的 3×3矩阵,将它摆放到一个列主序3 x 3的矩阵当中,并有如下的形式,其中箭头指示了内存地址的增长方向。从图中可以看出,内存地址增长的方向先是纵向,然后是横向,呈倒Z字形

在列主序中,这个矩阵在内存中的存储顺序是:[0,1,2,3,4,5,6,7,8]。

armadillo中默认的顺序就是列主序的,而Pytorch张量默认顺序是行主序的,所以在程序中需要进行一定适应和调整(转置)。了解矩阵的存储顺序可以帮助我们在进行矩阵运算时优化性能,特别是在涉及大规模数据和矩阵操作时,可以减少缓存未命中(cache miss),提高内存访问的效率。

2.2.2 Tensor类具体实现

Tensor类的各种操作其实底层都是对cube或者fcube的操作,下面先介绍fcube的基本操作,然后介绍如何封装成我们的Tensor类方法。

  • 构造与初始化

    arma::fcube F;  // 创建一个空的 fcube 对象,没有分配内存// 创建一个大小为 n_rows x n_cols x n_slices 的浮点立方体,元素默认未初始化
    // n_rows:矩阵的行数。
    // n_cols:矩阵的列数。
    // n_slices:矩阵的切片数(即第三维的大小)。
    arma::fcube F(uword n_rows, uword n_cols, uword n_slices);// 创建并用指定的方式初始化所有元素,例如 fill::zeros 用零填充,fill::ones 用一填充
    arma::fcube F(uword n_rows, uword n_cols, uword n_slices, fill::fill_type fill_value);
  • 访问元素

    可以使用 F(x, y, z) 来访问 fcube 中的某个位置的元素。

    float val = F(2, 3, 1);
    
  • 获取大小:可以使用 F.n_rowsF.n_colsF.n_slices 来获取 fcube 的维度信息。

    uword rows = F.n_rows;
    uword cols = F.n_cols;
    uword slices = F.n_slices;
    
  • 切片操作:可以使用 F.slice(z) 来获取 fcube 的某一层,它返回一个 arma::fmat 对象,表示这一层的二维矩阵。

    arma::fmat slice1 = F.slice(1);
    

    arma::fcubesubcube 方法用于获取或修改三维矩阵的子矩阵(子立方体),允许对指定的子区域进行操作。

    // 将原数据复制到新数据的中心位置
    // subcube( first_row, first_col, first_slice, last_row, last_col, last_col )
    // first_row   子矩阵的起始行索引
    // first_col   子矩阵的起始列索引
    // first_slice 子矩阵的起始切片(第三维度)索引// last_row    子矩阵的结束行索引
    // last_col    子矩阵的结束列索引
    // last_col    子矩阵的结束切片(第三维度)索引new_data.subcube(pad_rows1, pad_cols1, 0, pad_rows1 + orig_rows - 1, pad_cols1 + orig_cols - 1, orig_slices - 1) = this->data_;
    
  • 填充数据:可以使用 F.fill(value) 来用某个值填充整个立方体,或使用 F.zeros()F.ones() 等方法快速填充。

下面以如何创建张量为例,怎么使用fcube创建张量,其它成员函数的实现也是一样的。

对于Tensor这个多维矩阵需要用一个raw_shapes变量来存储张量的维度,而不同维度将决定了arma::fcube的具体结构。需要根据输入的维度信息创建相应维度的arma::fcube,且创建一个用于存储维度的变量。同时使用**data_**保存张量的 arma::fcube 数据。

  • 如果张量是1维的,则raw_shapes的长度就等于1;

  • 如果张量是2维的,则raw_shapes的长度就等于2,以此类推;

  • 在创建3维张量时,则raw_shapes的长度为3;

    值得注意的是,如果当channelrows同时等于1时,raw_shapes的长度也会是1,表示此时Tensor是一维的;而当channel等于1时,raw_shapes的长度等于2,表示此时Tensor是二维的。

// 创建1维张量
Tensor<float>::Tensor(uint32_t size) {data_ = arma::fcube(1, size, 1); // 传入的参数依次是,rows cols channelsthis->raw_shapes_ = std::vector<uint32_t>{size};
}// 创建2维张量
Tensor<float>::Tensor(uint32_t rows, uint32_t cols) {data_ = arma::fcube(rows, cols, 1); // 传入的参数依次是, rows cols channels this->raw_shapes_ = std::vector<uint32_t>{rows, cols};
}// 创建3维张量
Tensor<float>::Tensor(uint32_t channels, uint32_t rows, uint32_t cols) {data_ = arma::fcube(rows, cols, channels);if (channels == 1 && rows == 1) {// 当channel和rows同时等于1时,raw_shapes的长度也会是1,表示此时Tensor是一维的this->raw_shapes_ = std::vector<uint32_t>{cols};} else if (channels == 1) {// 当channel等于1时,raw_shapes的长度等于2,表示此时Tensor是二维的this->raw_shapes_ = std::vector<uint32_t>{rows, cols};} else {// 在创建3维张量时,则raw_shapes的长度为3,表示此时Tensor是三维的this->raw_shapes_ = std::vector<uint32_t>{channels, rows, cols};}
}

使用fcube封装Tensor后,就可以不用关心底层fcube,只需要使用提供的接口就可以完成张量的实例化。

// 将创建一个包含5个元素的张量,内部使用arma::fcube(1, 5, 1)存储数据。raw_shapes_设置为 {5},表示这是一个一维张量
Tensor<float> tensor1d(5);// 创建一个3行4列的二维张量,内部使用arma::fcube(3, 4, 1)存储数据。raw_shapes_设置为 {3, 4},表示这是一个二维张量
Tensor<float> tensor2d(3, 4);// 创建一个2通道3行4列的三维张量,使用 arma::fcube(3, 4, 2)存储数据。raw_shapes_设置为 {2, 3, 4},表示这是一个三维张量
Tensor<float> tensor3d(2, 3, 4);

对于返回张量的维度信息,底层也是使用fcube的成员函数实现的,如下所示。

uint32_t Tensor<float>::rows() const {CHECK(!this->data_.empty());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();
}

这四个方法分别返回张量的行数(rows)、列数(cols)、维度(channels)以及张量中数据的总数量(size)。

假设有一个大小为(3 × 3 × 2)的张量数据。

Tensor<float> tensor(3,3,2);
tensor.rows(); // 返回3
tensor.cols(); // 返回3
tensor.channels() // 返回2
tensor.size(); // 返回18

http://www.ppmy.cn/server/103214.html

相关文章

如何在 3 分钟内免费在 AWS 上运行 RStudio

欢迎来到雲闪世界。谈到数据分析&#xff0c;我有理由从本地计算机迁移到云端。最突出的是&#xff0c;您可以运行无限数量的机器&#xff0c;而无需拥有或维护它们。此外&#xff0c;您可以在几分钟内根据需要扩大或缩小规模。如果您选择运行 t2.micro 服务器&#xff0c;您可…

ansync/await 运行流程图

1、流程图&#xff1a; 2、await 之后的方法是何时执行&#xff0c;如何执行的&#xff1f; await 的方法在 Task 执行完成之后&#xff0c;通过调用 Finish 方法执行的。 具体的执行步骤是先将 MoveNext 方法注册到 Task 的回调里&#xff0c;然后在 Task 执行完后调用这个方法…

指针 (四)

一 . 指针的使用和传值调用 &#xff08;1&#xff09;strlen 的模拟实现 库函数 strlen 的功能是求字符串长度&#xff0c;统计的是字符串中 \0 之前的字符个数&#xff0c;函数原格式如下&#xff1a; 我们的参数 str 接收到一个字符串的起始地址&#xff0c;然后开始统计…

uni app 调用前置摄像头

uniapp开发app并没有相关Api调用前置摄像头。只能使用5app的api 调用前置摄像头拍照 plus.camera.getCamera(index) 获取需要操作的摄像头对象&#xff0c;如果要进行拍照或摄像操作&#xff0c;需先通过此方法获取摄像头对象 index指定要获取摄像头的索引值&#xff0c;1表…

Python基础知识学习总结(五)

一. 字典 字典是另一种可变容器模型&#xff0c;且可存储任意类型对象。 字典的每个键值 key>value 对用冒号 : 分割&#xff0c;每个对之间用逗号( , )分割&#xff0c;整个字典包括在花括号 {} 中 。 dict 作为 Python 的关键字和内置函数&#xff0c;变量名不建议命名…

洛谷 P1135 奇怪的电梯

链接直达&#xff1a;P1135 奇怪的电梯 - 洛谷 | 计算机科学教育新生态 题目来源 洛谷 题目内容 奇怪的电梯 题目背景 感谢 yummy 提供的一些数据。 题目描述 呵呵&#xff0c;有一天我做了一个梦&#xff0c;梦见了一种很奇怪的电梯。大楼的每一层楼都可以停电梯&…

opencv中Core中的Norm函数解释

1. Norm的类型 NORM_L1&#xff1a; L1 范数&#xff08;曼哈顿范数&#xff09;。数组中所有元素绝对值之和。 NORM_L2&#xff1a; L2 范数&#xff08;欧几里得范数&#xff09;。数组中所有元素平方和的平方根。 NORM_INF&#xff1a;无穷范数&#xff08;最大绝对值范数&…

如何将 ONLYOFFICE 与 Moodle 进行集成,让师生在学习管理平台中协作编辑办公文档

在教学过程中使用现代在线学习软件&#xff0c;已不再是什么稀奇事。在世界各地&#xff0c;越来越多的教师和学生都在使用现代技术&#xff0c;应用新的学习场景&#xff0c;包括学生在传统课堂之外更积极的参与、更密切的互动。 Moodle 支持各类学校和大学充分利用在线教育过…