在现代计算机视觉和机器学习领域,矩阵运算是核心任务之一。为了提高计算效率,通常会使用SIMD(单指令流多数据流)指令集,如SSE(Streaming SIMD Extensions)来优化这些运算。本文将介绍一个支持SSE优化的高性能矩阵运算库的使用和实现,该库适用于需要进行快速矩阵运算的应用,如神经网络、支持向量机分类器等。
要使用这个库,需要对矩阵及其背后的数学有一定的了解。可以快速浏览一下SOS数学的相关内容,或者查看CodeProject上的SSE编程文章。
首先,确保在项目中包含了以下头文件:
#include <mm3dnow.h>
#include <float.h>
#include <time.h>
#include <math.h>
#include <windows.h>
#include <stdio.h>
接下来,将介绍如何使用这个库中的vec2D类。vec2D类允许创建一个N x M的二维向量,或者一个简单的一维向量,可以是行向量1xM或列向量Nx1。
vec2D类提供了两种构造函数。第一种构造函数可以从磁盘上的文本文件中读取数组:
vec2D(const wchar_t* file);
例如,matrix.txt文件提供了一个2x3的矩阵,所有元素都是0。
vec2D(unsigned int ysize = 1, unsigned int xsize = 1, int yoffset = 0, int xoffset = 0, const float* data = 0);
第二种构造函数允许初始化向量。如果不提供任何参数,将得到一个1x1的矩阵,即一个标量。ysize表示行数,xsize表示列数。通过yoffset和xoffset,可以选择负数或正数索引来访问矩阵的第一个元素。
vec2D v1(1, 10); // 是1x10的行向量
vec2D v2(10, 1); // 是10x1的列向量
还可以初始化具有只读访问权限的gaus_filter矩阵:
const float gblur[] = {0.0625f, 0.125f, 0.0625f, 0.125f, 0.25f, 0.125f, 0.0625f, 0.125f, 0.0625f};
const vec2D gaus_filter(3, 3, -1, -1, gblur);
使用以下函数,可以访问矩阵的行和列的第一个和最后一个可用索引:
inline int xfirst() const;
inline int yfirst() const;
inline int xlast() const;
inline int ylast() const;
可以使用以下方式访问矩阵的所有元素:
for(int y = gaus_filter.yfirst(); y <= gaus_filter.ylast(); y++) {
for(int x = gaus_filter.xfirst(); x <= gaus_filter.xlast(); x++) {
wprintf(L"%g", gaus_filter(y, x));
wprintf(L"\n");
}
}
或者使用print函数:
void print(const wchar_t* file = 0) const;
如果文件不是NULL,将矩阵保存到磁盘上的文件中;否则,将其打印到stdout。
使用以下函数获取矩阵的大小:
inline unsigned int width() const; // 数组宽度,列数
inline unsigned int height() const; // 数组高度,行数
inline unsigned int length() const; // 数组总大小 width*height
通过重载的[]运算符,可以像Matlab一样访问矩阵的所有元素。但是,矩阵中的第一行和列索引应该从0开始。
vec2D v(3, 5);
for(unsigned int i = 0; i < v.length(); i++)
wprintf(L"%g\n", v[i]);
还可以使用get函数访问矩阵的元素:
inline float get(int y, int x) const;
它允许索引超出矩阵尺寸。在这种情况下,将获得周期性边界扩展。所以,如果第一行和列索引等于0,并尝试访问vec(-1, -1),将获得vec(1, 1)元素。
使用重载的赋值运算符,可以复制矩阵,或者用外部数据缓冲区初始化它:
inline const vec2D& operator=(const vec2D& v);
inline const vec2D& operator=(const float* pf);
使用set元素函数,可以将矩阵内容初始化为特定值:
void set(float scalar); // 将所有矩阵元素设置为标量
void set(float scalar, RECT& r); // 将RECT中的矩阵元素设置为标量
void setrand(); // 将矩阵元素设置为-1.0到1.0之间的随机值
RECT只是一个Windows结构。
以下函数处理SSE优化的数学运算:
void add(float scalar);
void sub(float scalar);
void mul(float scalar);
void div(float scalar);
可以将矩阵的所有元素与标量相加、减、乘或除。
vec2D v(3, 5);
v.print();
v.add(3.4f);
v.print();
矩阵的算术运算函数如下:
void add(const vec2D& a, const vec2D& b); // c = a + b SSE优化
void sub(const vec2D& a, const vec2D& b); // c = a - b SSE优化
void mul(const vec2D& a, const vec2D& b); // c = a * b
void div(const vec2D& a, const vec2D& b); // c = a / b SSE优化
其中c是正在调用的函数的矩阵,它的元素将是操作的结果。add、sub和div是逐元素操作,所以a和b矩阵以及c应该具有相同的大小。mul(乘法)操作不是SSE优化的,因为在乘法过程中,a的每一行都乘以b的每一列。
在信号处理中,另一个有用的函数是与滤波器的卷积:
void conv2D(const vec2D& a, const vec2D& filter);
void conv2D(const vec2D& a, const vec2D& re, const vec2D& im);
第一个是与滤波器的卷积,第二个是与由实部和虚部组成的复数滤波器的卷积。
vec2D v_original(100, 100);
vec2D v_smoothed = v_original;
v_smoothed.conv2D(v_original, gaus_filter); // 高斯平滑
void normalize(float a, float b); // 归一化到a...b范围
void histeq(vec2D& hist); // 直方图归一化
vec2D hist(1, 256);
vec2D v_heq(100, 100);
v_heq.setrand();
v_heq.norm(0.0f, 255.0f);
v_heq.histeq(hist);