0.前言 与 Eigen库的使用整理

[C++ 基于Eigen库实现CRN前向推理]

前言:背景 与 Eigen库的使用整理

  • 前言:(Eigen库使用记录)
  • 第一部分:WavFile.class (实现读取wav/pcm,实现STFT)
  • 第二部分:Conv2d实现
  • 第三部分:TransposedConv2d实现 (mimo,padding,stride,dilation,kernel,outpadding)
  • 第四部分:NonLinearity (Sigmoid,Tanh,ReLU,ELU,Softplus)
  • 第五部分:LSTM

1. 前言

最近要用C++ 实现一下CRN网络的前向推理过程,了解了一下相关的库,决定使用Eigen提供的Tensor作为数据结构,并使用一些Eigen的常规的库函数。(纯C++实现嵌套的循环太多层了,容易写迷糊了,先降低难度写一波)


2. C++ Eigen库的使用

Eigen是可以用来进行线性代数、矩阵、向量操作等运算的C++库,它里面包含了很多算法。它的License是MPL2。它支持多平台。

Eigen采用源码的方式提供给用户使用,在使用时只需要包含Eigen的头文件即可进行使用。之所以采用这种方式,是因为Eigen采用模板方式实现,由于模板函数不支持分离编译,所以只能提供源码而不是动态库的方式供用户使用。

在找资料的时候发现了Eigen 矩阵操作与Matlab的对应关系,虽然在这个项目里没有用到,但后面可能会用到,先码住,链接在这。

2.1 下载与导入
  • 第一步

Eigen下载地址官网在这,直接去官网下载解压包,解压到本地。
0.前言 与 Eigen库的使用整理_第1张图片
我这路径为E:\eigen-3.4.0,版本为3.4.0。

  • 第二步
    把Eigen-3.4.0目录中的需要用到的包复制到对应C++项目的目录下,本文就用到Eigen核心和unsupported中的Tensor。
    0.前言 与 Eigen库的使用整理_第2张图片
  • 第三步
    在CmakeList中添加lib,红框圈起来的部分就是Eigen的引用啦。至于上面的一大串是MATLAB库的导入,不用管就好了。
    0.前言 与 Eigen库的使用整理_第3张图片
  • 第四步
    到这里就差不多能用啦,写个初始化测试一下:
    int main() {
    	Eigen::Vector<double, 3> point(2, 1, 1);
        cout << point << endl;
        return 0;
    }	
    >>>>>
    2
    1
    1
    
    如果还是不行,可以尝试重启一下编译器,或者File->Reload CMake Project

2.2 Tensor的初始化

这里只讲Tensor的用法,Matrix 和Vector的教程网上很多。
Tensor数据类型不在Eigen/Core 和Eigen/Dense中,而是需要额外引用unsupported。

#include 

简单声明:Tensor声明格式如Eigen::Tensor,data_type是数据类型,n_dim是多维矩阵的维度。这里声明了一个2维矩阵,但是没有定义具体的大小,在输出时为空。

	Eigen::Tensor<double, 2> a;
	std::cout << a << std::endl;
	(blank)

在看指定具体大小的情况↓,可以看到为矩阵随机分配了初始值,矩阵只有在指定大小(即为其分配空间时才会产生内容)。通过resize方式,也可以进行初始化

    Eigen::Tensor<float, 2> var(2, 3);
    cout << var << endl;
    -1.83108e+23 -1.82998e+23            0
 	7.00649e-43  7.00649e-43            0

    Eigen::Tensor<float, 2> var;
    var.resize(2, 3);
    cout << var << endl;
    -2.6161e+31 -2.61461e+31            0
 	5.73131e-43  5.73131e-43            0

几种初始化函数
常数初始化

	var.setConstant(12.3f);
	cout << "Constant: " << endl << var << endl << endl;
	=>
	Constant:
	12.3 12.3 12.3
	12.3 12.3 12.3

置零初始化

	Eigen::Tensor<float, 2> var(2, 3);
    var.setZero();
    cout << "Constant: " << endl << var << endl << endl;
    =>
    Constant:
	0 0 0
	0 0 0

赋值初始化

	Eigen::Tensor<float, 2> var(2, 3);
    var.setValues({{0.0f, 1.0f, 2.0f},
                   {3.0f, 4.0f, 5.0f}});
    cout << "Constant: " << endl << var << endl << endl;
    =>
    Constant:
	0 1 2
	3 4 5

随机初始化

    Eigen::Tensor<float, 2> var(2, 3);
    var.setRandom();
    cout << "Constant: " << endl << var << endl << endl;
    Constant:
	0.896227 0.872269 0.605188
	0.290171  0.24641 0.251816

2.3 Tensor常用库函数
  • 重塑-reshape
    可以看到Eigen的Tensor是Col-Major的,所以存储顺序为0.896277,0.290171,0.872269…,重塑完按照(3,2)的形状分配。这里与Pytorch的矩阵reshape结果是不一致的
    Eigen::Tensor<float, 2> var(2, 3);
    var.setRandom();
    cout << "Constant: " << endl << var << endl << endl;
    Eigen::array<int, 2> new_shape{3, 2};
    auto vat_T = var.reshape(new_shape);
    cout << "Constant: " << endl << vat_T << endl << endl;
    Constant:
	0.896227 0.872269 0.605188
	0.290171  0.24641 0.251816
	
	Constant:
	0.896227  0.24641
	0.290171 0.605188
	0.872269 0.251816
	
	Pytorch.reshpe:
	0.896227 0.872269
	0.605188 0.290171
	0.24641 0.251816
  • 填充-Padding
    采用auto或者声明变量的形式保存pad函数的返回值都可以,不同的是auto类型保存的是PadOp,是一个操作,后续怎么操作也不清楚,希望有知道的大佬指点一二。用Tensor变量保存的话就是赋值存储了,后续正常按Tensor操作就可以。
	auto d0 = std::make_pair(1, 2);
	auto d1 = std::make_pair(2, 1);
	// auto dims = std::experimental::make_array(d0, d1); // 便利的写法
	Eigen::array<std::pair<int, int>, 2> dims{d0, d1};
	auto padded = var.pad(dims, -1);
	// Eigen::Tensor padded;
	// padded = var.pad(dims,-1);
	std::cout << padded << std::endl;
	=>
	Constant:
	0.896227 0.872269 0.605188
	0.290171  0.24641 0.251816

      -1       -1       -1       -1       -1       -1
      -1       -1 0.896227 0.872269 0.605188       -1
      -1       -1 0.290171  0.24641 0.251816       -1
      -1       -1       -1       -1       -1       -1
      -1       -1       -1       -1       -1       -1

pad函数不知道能不能扩展到多维,官方示例只给出了二维的样例,我自己在尝试的时候三维就报错了,有没有大佬指点一下。~

    Eigen::Tensor<float, 3> var(2, 2, 3);
    var.setRandom();
    cout << "Constant: " << endl << var << endl << endl;
    auto d0 = std::make_pair(0, 1);
    auto d1 = std::make_pair(1, 2);
    auto d2 = std::make_pair(2, 3);
    // auto dims = std::experimental::make_array(d0, d1); // 便利的写法
    Eigen::array<std::pair<int, int>, 3> dims{d0, d1, d2};
    Eigen::Tensor<float, 3> padded;
    **padded = var.pad(dims, -1);**    //这边的赋值操作已经报错了,编译不通过
    std::cout << padded << std::endl;

error: no matching function for call to 'Eigen::DSizes::DSizes(const Dimensions&)'128 | m_dimensions = (DSizes) (DSizes) static_cast>(m_impl.dimensions());

  • 降维切片-chip
    降维切片, 有两种写法,
    一种是 offset 是模板参数var.chip<0>(1),另一种是参数var.chip(offset=1, dim=0)
    第一种的切片维度必须是编译器常量。
    offset其实就是下标,dim是维度,似乎是在dim维从offset切出一条来。
    Eigen::Tensor<float, 2> var(3, 3);
    var.setRandom();
    cout << "Constant: " << endl << var << endl << endl;
    auto c1 = var.chip(1, /*dim=*/0);
    std::cout << "dim=0,offset=1: \n" << c1 << std::endl;
    auto c2 = var.chip<0>(2);
    std::cout << "dim=0,offset=2: \n" << c2 << std::endl;
    auto c3 = var.chip(1, 1);
    std::cout << "dim=1,offset=1: \n" << c3 << std::endl;
    =>
	Constant:
	0.896227  0.24641 0.259925
	0.290171 0.605188 0.354927
	0.872269 0.251816 0.827175
	
	dim=0,offset=1:
	0.290171
	0.605188
	0.354927
	dim=0,offset=2:
	0.872269
	0.251816
	0.827175
	dim=1,offset=1:
	 0.24641
	0.605188
	0.251816

chip切片是会降维的,在原来(3,3)上切出第一行,会降维成(3,),不能用(1,3)Tensor来接收

    Eigen::Tensor<float, 2> row(1, 3);
    **row = var.chip(1, /*dim=*/0);** 
    =>
    报错error: no matching function for call to 'Eigen::DSizes<long long int, 2>::DSizes(const Dimensions&)'
    Eigen::Tensor<float, 1> row(3);
    row = var.chip(1, /*dim=*/0);
    std::cout << "dim=0,offset=1: \n" << row << std::endl;
    =>
    dim=0,offset=1:
	0.290171
	0.605188
	0.354927

扩展到多维看看,也是能切出来的,只不过这个Col-Major是真难看。

    Eigen::Tensor<float, 3> var(2, 3, 3);
    var.setRandom();
    cout << "Constant: " << endl << var << endl << endl;
    auto c1 = var.chip(1, /*dim=*/0);
    std::cout << "dim=0,offset=1: \n" << c1 << std::endl;
    auto c2 = var.chip(1, /*dim=*/1);
    std::cout << "dim=1,offset=1: \n" << c2 << std::endl;
    =>
    Constant:
	 0.896227  0.872269  0.605188  0.259925  0.827175  0.224867  0.713551  0.032015 0.0700275
	 0.290171   0.24641  0.251816  0.354927  0.828251  0.990138 0.0767354  0.757985   0.31538
	
	dim=0,offset=1:
	 0.290171  0.354927 0.0767354
	 0.24641  0.828251  0.757985
	 0.251816  0.990138   0.31538
	 
	dim=1,offset=1:
	 0.872269 0.827175 0.032015
	 0.24641 0.828251 0.757985
  • 切片-slice
    先看二维切片,简单命明了,通过调用var.slice(offsets, extents)实现。
    其中,offset可以看做是起始位置的下标,包含在切片内。extents可以看做是切片的patch,范围为1到维度长度,也就是切片内容的大小,所以对应维度的切片大小是[offsets,offset+extents)。
    Eigen::Tensor<float, 2> var(3, 3);
    var.setRandom();
    cout << "Constant: " << endl << var << endl << endl;
    Eigen::array<int, 2> offstes{1, 1};
    Eigen::array<int, 2> extents{2, 2};
    auto sliced = var.slice(offstes, extents);
    std::cout << "sliced: " << sliced << std::endl;
    =>
	Constant:
	0.896227  0.24641 0.259925
	0.290171 0.605188 0.354927
	0.872269 0.251816 0.827175
	
	sliced:
	0.605188 0.354927
	0.251816 0.827175

在来看多维切片,其实是一样的,参数上要指定多维的起始位置与patch。为了方便看,我手动调整了一下输出的格式。
可以看出,slice的切片是不会降维的,chip和slice类似于在pytorch中的a[1,:[]和a[1:2,:]的区别。

    Eigen::Tensor<float, 3> var(2, 3, 3);
    var.setRandom();
    cout << "Constant: " << endl << var << endl << endl;
    Eigen::array<int, 3> offstes{1, 1, 1};
    Eigen::array<int, 3> extents{1, 2, 2};
    auto sliced = var.slice(offstes, extents);
    std::cout << "sliced: " << endl << sliced << std::endl;
    =>
    Constant:
	[[[0.896227  0.259925  0.713551]
	[0.872269  0.827175  0.032015]  
	[0.605188  0.224867  0.0700275]]
	
	[[0.290171  0.354927  0.0767354]
	[0.24641    0.828251  0.757985]
	[0.251816   0.990138  0.31538]]]
	
	sliced:
	[[[0.828251   0.757985]]  
	 [[0.990138    0.31538]]]
	
  • 跳步- stride
    通过var.stride(strides);调用,strides是一个N维的array类型,指定在每一维要跳步的大小。以下是一个简单的二维跳步
    Eigen::Tensor<float, 2> var(5, 5);
    var.setRandom();
    cout << "Constant: " << endl << var << endl << endl;
    Eigen::array<int, 2> strides{2, 2};
    Eigen::Tensor<float, 2> stride = var.stride(strides);
    std::cout << "Stride: " << endl << stride << std::endl;
    =>
    Constant:
	0.896227  0.251816  0.224867  0.757985  0.186436
	0.290171  0.259925  0.990138 0.0700275  0.730255
	0.872269  0.354927  0.713551   0.31538  0.943635
	 0.24641  0.827175 0.0767354  0.517878  0.946459
	0.605188  0.828251  0.032015  0.694123  0.279912
	
	Stride:
	0.896227 0.224867 0.186436
	0.872269 0.713551 0.943635
	0.605188 0.032015 0.279912

扩展到多维情况,stride函数也不会降维,可以看做是整个多维矩阵按stride进行slice的结果

    Eigen::Tensor<float, 3> var(3, 3, 3);
    var.setRandom();
    cout << "Constant: " << endl << var << endl << endl;
    Eigen::array<int, 3> strides{2, 2, 2};
    Eigen::Tensor<float, 3> stride = var.stride(strides);
    std::cout << "Stride: " << endl << stride << std::endl;
    =>
    Constant:
	[[[0.896227   0.828251   0.517878]
	  [ 0.24641   0.713551   0.730255]
	  [0.259925   0.757985   0.279912]]
	  
	 [[0.290171  0.224867   0.694123]
	 [ 0.605188  0.0767354  0.943635]
	 [ 0.354927  0.0700275  0.446424]]
	 
	 [[0.872269    0.990138  0.186436]
	  [0.251816    0.032015  0.946459]
	  [0.827175     0.31538  0.767464]]]
	
	Stride:
	[[[0.896227  0.517878]
	  [0.259925  0.279912]]
	 
	 [[0.872269 0.186436] 
	  [0.827175 0.767464]]]
  • 跳步切片-sliceStride

    下面这个切片就是多维矩阵的跳步切片,其结果与stride的结果是一致的。
    跳步切片通过var.stridedSlice(starts, stops, strides)实现,其中
    start是进行切片的起始位置,是一个Eigen::array,N为该矩阵的维度,至于int64的类型,并不确定,是我试出来的。起初指定int32会报错,long long int也可以。
    stop是切片的终止位置,类型与start一致,不是切片的结果个数,与slice中的patch不同。slice中的patch是输出结果的大小(因为没有stride)。而sliceStride中,若从[start=0,stop=5,stride=2]中切片,输出大小为3.
    stride是各维度的跳步大小,类型与start一致

    Eigen::Tensor<float, 3> var(3, 3, 3);
    var.setRandom();
    cout << "Constant: " << endl << var << endl << endl;
    Eigen::array<int64_t, 3> starts{0, 0, 0}; // 必须用 long, 不能用 int, 为什么呢7302
    Eigen::array<int64_t, 3> stops{3, 3, 3};
    Eigen::array<int64_t, 3> strides{2, 2, 2};
    auto region = var.stridedSlice(starts, stops, strides);
    std::cout << region << std::endl;
  • 矩阵乘-contraction(tensor dot)
    Contraction是矩阵乘积在多维情况下的推广。先看二维,通过调用a.contract(b, dims)
    实现矩阵乘,其中dims是一个索引对,表明第一个张量a的第几维与第二个张量b的第几维对应进行矩阵乘。
    Eigen::Tensor<int, 2> a(2, 3);
    a.setValues({{1, 2, 3},
                 {6, 5, 4}});
    Eigen::Tensor<int, 2> b(3, 2);
    b.setValues({{1, 2},
                 {4, 6},
                 {5, 6}});
    cout << "a:" << endl << a << endl;
    cout << "b:" << endl << b << endl;

    // Compute the traditional matrix product
    Eigen::array<Eigen::IndexPair<int>, 1> product_dims = {Eigen::IndexPair<int>(1, 0)};
    Eigen::Tensor<int, 2> AB = a.contract(b, product_dims);
    cout << "AB:" << endl << AB << endl;

    // Compute the product of the transpose of the matrices
    Eigen::array<Eigen::IndexPair<int>, 1> transposed_product_dims = {Eigen::IndexPair<int>(0, 1)};
    Eigen::Tensor<int, 2> AtBt = a.contract(b, transposed_product_dims);
    cout << "AtBt:" << endl << AtBt << endl;
    =>
    a:
	1 2 3
	6 5 4
	b:
	1 2
	4 6
	5 6
	AB:
	24 32
	46 66
	AtBt:
	13 40 41
	12 38 40
	11 36 39
	AdoubleContractedA:
	91

扩展到三维情况(不知道能不能行),试了一下,不行!。。

  • 转置-transpose(permute)
	Eigen::Tensor<int, 3> m(2, 3, 3);
    m.setValues(
        {
            {{1, 2, 3},
            {4, 5, 6},
            {7, 8, 9}},
            {{10, 11, 12},
            {13, 14, 15},
            {16, 17, 18}}
        });
    Eigen::array<int, 3> shuffling({ 2, 1, 0});
    Eigen::Tensor<int, 3> transposed = m.shuffle(shuffling);
    => transposed
    1,10
	4,13
	7,16
	2,11
	5,14
	8,17
	3,12
	6,15
	9,18

2.4 自制Eigen常用函数
  • print(Tensor4d)
    因为Eigen是ColMajor的,cout看起来非常难看,拿循环打个表,直接代码不再赘述。
void print(Eigen::Tensor<float_t, 4> input) {
    const Eigen::Tensor<size_t, 4>::Dimensions &dim_inp = input.dimensions();
    cout << "Variable:" << endl;
    cout << "[";
    for (int i = 0; i < dim_inp[0]; i++) {
        if (i > 0) {
            cout << " ";
        }
        cout << "[";
        for (int j = 0; j < dim_inp[1]; j++) {
            if (j > 0) {
                cout << "  ";
            }
            cout << "[";
            for (int k = 0; k < dim_inp[2]; k++) {
                if (k > 0) {
                    cout << "   ";
                }
                cout << "[";
                for (int l = 0; l < dim_inp[3]; l++) {
                    cout << input(i, j, k, l);
                    if (l < dim_inp[3] - 1) {
                        cout << "\t";
                    }
                }
                cout << "]";
                if (k < dim_inp[2] - 1) {
                    cout << "," << endl;
                }
            }
            cout << "]";
            if (j < dim_inp[1] - 1) {
                cout << endl << endl;
            }
        }
        cout << "]";
        if (i < dim_inp[0] - 1) {
            cout << endl;
        }
    }
    cout << "]";
}
  • Tensor.transpose()

在网上发现了Tensor.transpose()在Eigen里是叫做Tensor.suffle,在上一节进行了更新

我的目的是想实现与pytorch中的tensor.transpose的效果,主要用于在输入LSTM之前,对C和T维度进行置换
如果用循环的话,其实就是在幅值的时候把要置换的维度下标换一下就可以了。
在函数中,我用trans_idx存储了变换后的维度位置,如要从a(1,2,3,4)变换为b(1,3,2,4),即置换1,2维,则传入[0,2,1,3],表示原来维度变换后所在的维度。(有点绕,可以按照torch.permute的参数进行理解,只不过我这里只能置换2维,理论上多维置换应该是可以通过嵌套实现的,我这里就不列出了)

	Eigen::Tensor<float_t, 4> transpose(Eigen::Tensor<float_t, 4> &input, Eigen::array<int64_t, 4> trans_idx) {
	    const Eigen::Tensor<size_t, 4>::Dimensions &dim_inp = input.dimensions();
	    Eigen::Tensor<size_t, 4>::Dimensions dim_out;
	    for (int i = 0; i < 4; i++) {
	        dim_out[i] = dim_inp[trans_idx[i]];
	        cout << dim_out[i] << endl;
	    }
	
	    Eigen::Tensor<float_t, 4> output(dim_out[0], dim_out[1], dim_out[2], dim_out[3]);
	    for (int64_t i = 0; i < dim_out[0]; i++) {
	        for (int64_t j = 0; j < dim_out[1]; j++) {
	            for (int64_t k = 0; k < dim_out[2]; k++) {
	                for (int64_t l = 0; l < dim_out[3]; l++) {
	                    int64_t idx_inp[4] = {i, j, k, l};
	                    output(i, j, k, l) = input(idx_inp[trans_idx[0]], idx_inp[trans_idx[1]], idx_inp[trans_idx[2]],
	                                               idx_inp[trans_idx[3]]);
	                }
	            }
	        }
	    }
	    return output;
	}

这里和pytorch的transpose对比了一下,结果一致,这里放上pytorch的结果。

tensor([[[[ 1,  2,  3,  4],
          [ 5,  6,  7,  8],
          [ 9, 10, 11, 12]],
         [[13, 14, 15, 16],
          [17, 18, 19, 20],
          [21, 22, 23, 24]]]])
tensor([[[[ 1,  2,  3,  4],
          [13, 14, 15, 16]],
         [[ 5,  6,  7,  8],
          [17, 18, 19, 20]],
         [[ 9, 10, 11, 12],
          [21, 22, 23, 24]]]])
  • Tensor.reshape(ColMajor)
    在CRN中送入LSTM之前,需要把[B,C,T,F]的形状转化为[B,T,C*F],即把通道和频率轴叠到一起,上面已经完成了transpose的功能,下面完成view或者reshape的功能。
    测试一下Eigen自带的reshape能不能完成任务,
	print(output1);
    Eigen::array<Eigen::DenseIndex, 3> one_dim({1, 3, 4 * 2});
    Eigen::Tensor<float, 3, Eigen::ColMajor> reshaped;
    reshaped = output1.reshape(one_dim);
    cout << endl;
    for (int i = 0; i < 1; i++) {
        for (int j = 0; j < 3; j++) {
            for (int k = 0; k < 8; k++) {
                cout << reshaped(i, j, k) << " ";
            }
            cout << endl;
        }
    }
Output1:
[[[[1   2       3       4],
   [13  14      15      16]]

  [[5   6       7       8],
   [17  18      19      20]]

  [[9   10      11      12],
   [21  22      23      24]]]]
reshaped:
1 13 2 14 3 15 4 16
5 17 6 18 7 19 8 20
9 21 10 22 11 23 12 24

看样子是不行了,那手写个

参考链接:
[1] Eigen库下载与使用
[2] Eigen Tenosr的库函数基础使用

你可能感兴趣的:(C++,深度学习,c++)