样本熵(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);
}
这里的所有步骤基本都是按照原算法描述的内容进行编写的,所以非常好理解啊。而且我已经故意避开了诸如fmax之类的函数,优化找最大差值的逻辑,都是为了提高性能。
不过这个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
测试操作系统:Windows 11 21H2
- 编译命令
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进行运算的。
如果编译32位时给编译器加上-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模式就行了。
更新记录
- 2023-02-04:修正错别字,优化部分表述。
样本熵(SampEn)的C/C++代码实现与优化的更多相关文章
- matlab计算样本熵
计算14通道得脑电数据吗,将得出的样本熵插入Excel表格 a = zeros(1,14); b = a'; for i =1:14 b(i) = SampEn(d1_1(i,1:3000),2,0. ...
- 基于SKLearn的SVM模型垃圾邮件分类——代码实现及优化
一. 前言 由于最近有一个邮件分类的工作需要完成,研究了一下基于SVM的垃圾邮件分类模型.参照这位作者的思路(https://blog.csdn.net/qq_40186809/article/det ...
- iOS开发UI篇—从代码的逐步优化看MVC
iOS开发UI篇—从代码的逐步优化看MVC 一.要求 要求完成下面一个小的应用程序. 二.一步步对代码进行优化 注意:在开发过程中,优化的过程是一步一步进行的.(如果一个人要吃五个包子才能吃饱,那么他 ...
- 解析Android开发优化之:从代码角度进行优化的技巧
下面我们就从几个方面来了解Android开发过程中的代码优化,需要的朋友参考下 通常我们写程序,都是在项目计划的压力下完成的,此时完成的代码可以完成具体业务逻辑,但是性能不一定是最优化的.一般来说 ...
- user模式下编译android 代码被proguard优化导致类和变量丢失
在Android项目中用到JNI,当用了proguard后,发现native方法找不到很多变量,原来是被produard优化掉了.所以,在JNI应用中该慎用progurad啊. 解决办法: 1.在An ...
- Asp.Net异常:"由于代码已经过优化或者本机框架位于调用堆栈之上,无法计算表达式的值"的解决方法
今天项目中碰到一个以前从没有见过的异常信息“由于代码已经过优化或者本机框架位于调用堆栈之上,无法计算表达式的值”,于是查了一下资料,原来此异常是由于我在代码中使用了"Response.End ...
- Spark性能调优之代码方面的优化
Spark性能调优之代码方面的优化 1.避免创建重复的RDD 对性能没有问题,但会造成代码混乱 2.尽可能复用同一个RDD,减少产生RDD的个数 3.对多次使用的RDD进行持久化(ca ...
- 异常:Data = 由于代码已经过优化或者本机框架位于调用堆栈之上,无法计算表达式的值。
做项目的时候,将DataTable序列化成Json,通过ashx向前台返回数据的时候,前台总是获取不到数据,但是程序运行却没问题, 没抛出异常.一时找不到办法,减小输出的数据量,这时前台可以接收到页面 ...
- Unity3d代码及效率优化总结
1.PC平台的话保持场景中显示的顶点数少于200K~3M,移动设备的话少于10W,一切取决于你的目标GPU与CPU. 2.如果你用U3D自带的SHADER,在表现不差的情况下选择Mobile或Unli ...
- ReSharper的功能真的很强大主要是针对代码规范和优化,园子里介绍的也不少,如果你没有安装,那我只能表示你们会相见恨晚
二.ReSHarper 代码规范.单元测试.... ReSharper的功能真的很强大,主要是针对代码规范和优化,园子里介绍的也不少,如果你没有安装,那我只能表示你们会相见恨晚! 1.像命名不规范,f ...
随机推荐
- 微信公众号没有scope授权
微信公众号有自己的appid 开发平台的绑定也有自己的appid 看文档的时候,注意是使用公众号的appi还是开放平台的appid
- Python-OpenCV的安装及学习资料
Conda环境安装 OpenCV pip install opencv-python opencv-contrib-python -i https://mirrors.aliyun.com/pypi/ ...
- TKE 超级节点,Serverless 落地的最佳形态
陈冰心,腾讯云产品经理,负责超级节点迭代与客户拓展,专注于 TKE Serverless 产品演进. 背景 让人又爱又恨的 Serverless Serverless 炙手可热,被称为云原生未来发展的 ...
- Django三大主流Web框架(django版本安装-项目创建-应用创建-django三板斧)
目录 一:python三大主流web框架 1.python三大主流Web框架 2:三大主流web框架特点 二:正常运行Django项目所需要知道的注意事项 1.计算机的名称不能有中文,不然bug在哪你 ...
- LeetCode HOT 100:下一个排列
题目:31. 下一个排列 题目描述: 本题是给你一个整数数组,返回该数组的下一个线性顺序排列. 举个例子:给你一个[1, 2, 3]的数组,他的线性排列顺序从小到大依次为[1, 3, 2],[2, 1 ...
- day01-家具网购项目说明
家具网购项目说明 1.项目前置技术 Java基础 正则表达式 Mysql JDBC 数据库连接池技术 满汉楼项目(包括框架图) JavaWeb 2.相关说明 这里先使用原生的servlet/过滤器,后 ...
- python解释器下载与基本使用
python介绍与解释器下载基本使用 1.python发展方向 web方向.自动化运维.自动化测试.自动化办公.网络爬虫.金融量化.人工智能.机器学习.数据分析 2.python解释器 历史 ...
- vue 原生js-实现下拉框
<template> <div class="por"> <div class="selectBox" style="w ...
- pythonfloat优雅的四舍五入
开发中经常会有float四舍五入转int的需求,先看看浮点数直接转int的情形:无论如何float直接转int都不会四舍五入,而是直接抹去小数点. 这个需求很简单,实现也很简单,看过网友的实现,都不够 ...
- 【转载】EXCEL VBA UBound(arr,1),UBound(arr,2)解释
Resize(UBound(arr, 1), UBound(arr, 2) 这句什么意思 resize()是一个扩展单元格地址区域的函数,有两个参数,第一个是行扩展数,第二个是列扩展数 UBo ...