首页 > 编程语言 >样本熵(SampEn)的C/C++代码实现与优化

样本熵(SampEn)的C/C++代码实现与优化

时间:2023-01-30 00:11:05浏览次数:60  
标签:SampEn 0.000850702 09 int double 2.21505 样本 C++ ms

样本熵(SampEn)的C/C++代码实现与优化

本文不介绍什么是样本熵,具体推荐看此文https://blog.csdn.net/Cratial/article/details/79742363,写的很好,里面的示例也被我拿来测试代码写的对不对。

本文所以代码可以在此处找到https://gitcode.net/PeaZomboss/miscellaneous,文件夹231424-sampen

前言

一开始有人找我帮忙写些C++代码,实现一些算法,然后就帮忙写了,其中就有一个样本熵的。

当时粗写了一个,后来似乎性能不太够,当然实际上性能是没问题的,虽然确实不如现在优化的,但也不会很差,原因是代码被魔改了一部分,当然这个和算法的优化没关系,就不展开了。

不过既然都要优化了,那不如在算法层面也做一些改进,让速度再快一点,于是就想了想办法提升了性能。于是在此之后就整理了一下代码,并严谨测试,然后与大家分享。

开头提到的那篇文章里有用python写的代码,不过糖比较多让人看了比较乱,还有一个matlab写的。但是不管是python还是matlab都不是拿来实际应用的,一般追求性能的模块会用C/C++,当然用Java应该也不会太差。

代码实现

首先是一般情况下很容易想到的代码:

static double step(double *X, int N, int m, double r)
{
    double sum = 0;
    for (int i = 0; i <= N - m; i++) {
        int Bi = 0;
        for (int j = 0; j <= N - m; j++) {
            if (i != j) {
                /* 找出最大差值 */
                double D = fabs(X[i] - X[j]);
                for (int k = 1; k < m; k++) {
                    double t = fabs(X[i + k] - X[j + k]);
                    if (D < t)
                        D = t;
                }
                if (D <= r)
                    Bi++;
            }
        }
        sum += 1.0 * Bi / (N - m); // 会有累加误差
    }
    return sum / (N - m + 1);
}

double SampEn(double *X, int N, int m, double r)
{
    double B = step(X, N, m, r);
    if (B == 0) // 尽管大部分时候不会是0
        return 0;
    double A = step(X, N, m + 1, r);
    if (A == 0)
        return 0;
    return -log(A / B);
}

这里的所有步骤基本都是按照原算法描述的内容进行编写的,所以非常好理解啊。

不过这个step函数的sum += 1.0 * Bi / (N - m);在N较大的时候累加误差会增加,而实际上这个Bi变量完全可以累加到最后在进行运算,不但减少误差还能降低运算量,所以可以改成:

static double step(double *X, int N, int m, double r)
{
    int Bi = 0;
    for (int i = 0; i <= N - m; i++) {
        for (int j = 0; j <= N - m; j++) {
            if (i != j) {
                double D = fabs(X[i] - X[j]);
                for (int k = 1; k < m; k++) {
                    double t = fabs(X[i + k] - X[j + k]);
                    if (D < t)
                        D = t;
                }
                if (D <= r)
                    Bi++;
            }
        }
    }
    return 1.0 * Bi / (N - m) / (N - m + 1);
}

这样子几乎就是按照原来的算法思路进行了一些简单处理,但提升并不明显。

代码优化

仔细看这个step()函数,那复杂度可是O(n^2)啊,而且还要跑两次,那损耗肯定惊人。但是仔细看这两次计算,只有第一次的m变成了m+1,那能不能把两次运算整合到一起呢?

注意到m+1和m的区别在于循环次数少了一次,寻找最大差值的向量多了一个,其余是没有区别的,那我们完全可以在处理m的时候顺便把m+1的情况也放一起就行了。

请看:

double FastSampEn(double *X, int N, int m, double r)
{
    int Ai = 0, Bi = 0;
    int LoopsSub1 = N - m; // 循环次数减一,因为算法中的表述是从1到N-m+1,而我们是从0开始的
    for (int i = 0; i <= LoopsSub1; i++) {
        for (int j = 0; j <= LoopsSub1; j++) {
            if (i != j) {
                // 这里一样找m个的最大差值
                double D = fabs(X[i] - X[j]);
                for (int k = 1; k < m; k++) {
                    double t = fabs(X[i + k] - X[j + k]);
                    if (D < t)
                        D = t;
                }
                if (D <= r)
                    Bi++;
                // 对于m+1的情况,当到达m维数的边界的时候显然是不行的
                // 所以我们只要限制边界情况就行了
                if (i != LoopsSub1 && j != LoopsSub1) {
                    double t = fabs(X[i + m] - X[j + m]);
                    if (D < t) // 判断最后一个是不是最大值
                        D = t;
                    if (D <= r)
                        Ai++;
                }
            } // i!=j
        } // j
    } // i
    double B = 1.0 * Bi / (N - m) / (N - m + 1);
    double A = 1.0 * Ai / (N - m - 1) / (N - m);
    if (B == 0 || A == 0)
        return 0;
    return -log(A / B);
}

这样修改以后速度可以达到原先的1.5倍,算上循环内部的开销,达到这样也是挺不错的。

至此,针对样本熵代码的基本优化就到这了。当然一定还会有更好的方案,但是碍于水平有限,目前只能如此。

其实方法是有的,比如暂存计算的结果,因为有一半是可以复用的,或者别的办法,不过这样空间复杂度会比较高,而目前的实现几乎没有额外的内存开销。

不过,考虑到实际应用情况,这个m大多数时候都是取值2,所以针对m为2的情况又专门优化了一下:

double FastSampEn_m2(double *X, int N, double r)
{
    int Ai = 0, Bi = 0;
    int LoopsSub1 = N - 2;
    for (int i = 0; i <= LoopsSub1; i++) {
        for (int j = 0; j <= LoopsSub1; j++) {
            if (i != j) {
                double D = fabs(X[i] - X[j]);
                double t = fabs(X[i + 1] - X[j + 1]);
                if (D < t)
                    D = t;
                if (D <= r)
                    Bi++;
                if (i != LoopsSub1 && j != LoopsSub1) {
                    double t = fabs(X[i + 2] - X[j + 2]);
                    if (D < t)
                        D = t;
                    if (D <= r)
                        Ai++;
                }
            }
        }
    }
    double B = 1.0 * Bi / (N - 2) / (N - 1);
    double A = 1.0 * Ai / (N - 3) / (N - 2);
    if (B == 0 || A == 0)
        return 0;
    return -log(A / B);
}

这样算是进一步压榨了CPU。

测试代码

接下来为了测试上述代码是否正确,就用开头提到的那篇文章的数据进行测试看看:

std::vector<double> x;
for (int i = 0; i < 17; i++) {
    x.push_back(85);
    x.push_back(80);
    x.push_back(89);
}
std::cout << SampEn(x.data(), x.size(), 2, 3) << '\n';
std::cout << FastSampEn(x.data(), x.size(), 2, 3) << '\n';
std::cout << FastSampEn_m2(x.data(), x.size(), 3) << '\n';

原文给出的结果是0.0008507018803128114,不过由于std::cout的舍入问题,实际输出的是0.000850702,是符合结果的。

然后再测试一下性能:

for (int i = 0; i < 10000; i++) {
    x.push_back(85);
    x.push_back(80);
    x.push_back(89);
}

double se;
clock_t t;

t = clock();
se = SampEn(x.data(), x.size(), 2, 3);
t = clock() - t;
std::cout << se << ", time = " << t << " ms\n";

t = clock();
se = FastSampEn(x.data(), x.size(), 2, 3);
t = clock() - t;
std::cout << se << ", time = " << t << " ms\n";

t = clock();
se = FastSampEn_m2(x.data(), x.size(), 3);
t = clock() - t;
std::cout << se << ", time = " << t << " ms\n";

先是塞了30000个数据,然后用clock()函数计时,依次测试每一个函数,然后看看结果是否一致,时间差距有多少。

如果有兴趣的话,可以去用之前提到那篇文章的python代码跑跑看,30000个数据花了我半个多小时才跑完,而我的这个代码即使没经过优化也不超过一分钟。而matlab的代码我没试过,电脑上没这个软件,也不是学这个的。

完整代码在开头给出的仓库里,打开test.cpp就可以看到。

运行测试

编译器和版本:g++ (x86_64-win32-seh-rev0, Built by MinGW-W64 project) 8.5.0

测试机器CPU:AMD Ryzen 5 4600H

  • 编译命令g++ -D ALL_IN_ONE test.cpp -o test_x64_O0 -O0
0.000850702
0.000850702
0.000850702
2.21505e-09, time = 15275 ms
2.21505e-09, time = 9409 ms
2.21505e-09, time = 8020 ms
  • 编译命令g++ -D ALL_IN_ONE test.cpp -o test_x64_O1 -O1
0.000850702
0.000850702
0.000850702
2.21505e-09, time = 2512 ms
2.21505e-09, time = 1604 ms
2.21505e-09, time = 1151 ms
  • 编译命令g++ -D ALL_IN_ONE test.cpp -o test_x64_O2 -O2
0.000850702
0.000850702
0.000850702
2.21505e-09, time = 2654 ms
2.21505e-09, time = 1604 ms
2.21505e-09, time = 1143 ms
  • 编译命令g++ -D ALL_IN_ONE test.cpp -o test_x64_O3 -O3
0.000850702
0.000850702
0.000850702
2.21505e-09, time = 2656 ms
2.21505e-09, time = 1603 ms
2.21505e-09, time = 1145 ms
  • 编译命令g++ -D ALL_IN_ONE test.cpp -o test_x64_Os -Os
0.000850702
0.000850702
0.000850702
2.21505e-09, time = 3058 ms
2.21505e-09, time = 1631 ms
2.21505e-09, time = 1167 ms

可以看到开了-O1及以后差距已经不明显了,说明后面的两段代码对编译器是比较友好的。

32位的就不贴了,性能肯定是不如64位的,但是依然可以看到的是即使是开了编译器优化,性能提升也是非常显著的。

不过32位有个有意思的情况,就是用-Os优化的性能居然比-O3优化的高出一截,这点是挺有意思的,因为不管开哪个优化32位默认都是用x87 FPU进行运算的。

另一个有意思的是如果编译器指令加上-msse2 -mfpmath=sse之后,不论开-Os还是-O3性能都是差不多的了,而且和性能64位也是基本上一样的了,毕竟64位默认就用的SSE2 FPU。

以上都是用g++测试的,如果用vs的话结果也是差不多的。

编译方案

对于x86来说,使用SSE2 FPU肯定是最好的,因为上述代码涉及大量double类型浮点数的运算,所以可以获得最佳性能。至于说什么CPU支持SSE2,那就这么说吧,20年前(以2023年为基准)的新CPU大部分都支持,因为SSE2是Intel在奔腾4(2000年)加入的,而AMD也在K8(2003年)加入了。因此可以在开编译器优化的基础上选择让编译器生成SSE2的代码。至于具体怎么操作,各家编译器有自己的方法,这个去搜一下就行了。

而对于x64来说,默认就会用SSE2,所以只要开编译器优化就行了。

如果用vs的话似乎x86默认就是开启SSE2的,而且也不用管优化,直接Release模式就行了。

标签:SampEn,0.000850702,09,int,double,2.21505,样本,C++,ms
From: https://www.cnblogs.com/PeaZomboss/p/17074163.html

相关文章

  • C++图书借阅信息管理系统[2023-01-29]
    C++图书借阅信息管理系统[2023-01-29]二、图书借阅信息管理系统1.基于动态数组或者链表实现图书借阅信息的管理LibraryMIS,可以使用STL的vector或者list。2.图书信息主要......
  • C++学生成绩管理系统[2023-01-29]
    C++学生成绩管理系统[2023-01-29]某专业2022-2023-1学期开设10门课程,6门必修课,4门选修课(任选两门)。专业有2个班级,每个班级20人,现要求统计必修课,选修课平均成绩,统计每名学......
  • [快速学]Dev C++查看内存中的值
    1、获得a在内存中的地址print/x&a2、查看内存中的值可以看到a在内存中的地址为0x62fe1cx/32bc0x62fe1c可看到内存0x62fe1c处存储的值为10(竟然是十进制显示的),后......
  • C/C++聊天程序设计[2023-01-29]
    C/C++聊天程序设计[2023-01-29]实验四聊天程序设计一、实验目的熟练掌握socket编程命令,设计一个聊天程序。二、实验内容1.熟悉socket,简单编写程序。socket编程的......
  • Python与小熊猫Dev-C++海龟作图比较
    前言少儿编程一般都遵循如下顺序:Scratch(或者变种,例如编程猫、腾讯扣叮)-Python-C++Scratch使用国际积木化搭建思路,学习起来,学生能够很容易上手上瘾,因为它能够通过积木化编程......
  • C/C++图的实现与分析[2023-01-29]
    C/C++图的实现与分析[2023-01-29]8.图的实现与分析问题描述分别对有向图、无向图、带权有向图、带权无向图实现对图的基本操作(创建、求顶点的度数、增加/删除边、判断......
  • c++针对特定数据结构创建堆
    make_heaphttps://en.cppreference.com/w/cpp/algorithm/make_heapstructds{ doublevalue; intidx; ds(doublev,intindex):value(v),idx(index){}......
  • C/C++不知道为什么最后输出是1不是0
    提问: 我觉得应该输出9876543210不知道为什么会是9876543211  而这样写就没问题  解答: 第一个循环用的是前置--,走到n=1的时候,前置--为0,不会进while循环,不会......
  • C++ : 引发了异常: 写入访问权限冲突。 this 是 nullptr。
    在写代码的时候遇到了一个问题引发了异常:写入访问权限冲突。this是nullptr。程序抛异常。前情提要:MFC程序,我自己写了一个类MyVolt,里面有一个成员函数CollectVolt......
  • C++复健:运算符重载,实现string容器,实现string和vector的迭代器
    使得对象的运算像内置类型一样a.operator+(b);重载运算符的一些注意点:不能重载运算符操作基础数据类型:(1)重载运算符必须和用户定义的class类型一起使用(2)重载的运算符......