我试图在Python中实现卷积神经网络。最初,我使用scipy.signal的convolve2d函数来进行卷积,但是它有很多开销,而且在C中实现我自己的算法并从python中调用它会更快,因为我知道是什么我的输入看起来像。
我已经实现了2个功能:
这两个函数都没有填充,因为我需要降低维数。
// a - 2D matrix (as a 1D array), w - kernel
double* conv2(double* a, double* w, double* result)
{
register double acc;
register int i;
register int j;
register int k1, k2;
register int l1, l2;
register int t1, t2;
for(i = 0; i < RESULT_DIM; i++)
{
t1 = i * RESULT_DIM; // loop invariants
for(j = 0; j < RESULT_DIM; j++)
{
acc = 0.0;
for(k1 = FILTER_DIM - 1, k2 = 0; k1 >= 0; k1--, k2++)
{
t2 = k1 * FILTER_DIM; // loop invariants
for(l1 = FILTER_DIM - 1, l2 = 0; l1 >= 0; l1--, l2++)
{
acc += w[t2 + l1] * a[(i + k2) * IMG_DIM + (j + l2)];
}
}
result[t1 + j] = acc;
}
}
return result;
}
// a - 2D matrix, w1, w2 - the separated 1D kernels
double* conv2sep(double* a, double* w1, double* w2, double* result)
{
register double acc;
register int i;
register int j;
register int k1, k2;
register int t;
double* tmp = (double*)malloc(IMG_DIM * RESULT_DIM * sizeof(double));
for(i = 0; i < RESULT_DIM; i++) // convolve with w1
{
t = i * RESULT_DIM;
for(j = 0; j < IMG_DIM; j++)
{
acc = 0.0;
for(k1 = FILTER_DIM - 1, k2 = 0; k1 >= 0; k1--, k2++)
{
acc += w1[k1] * a[k2 * IMG_DIM + t + j];
}
tmp[t + j] = acc;
}
}
for(i = 0; i < RESULT_DIM; i++) // convolve with w2
{
t = i * RESULT_DIM;
for(j = 0; j < RESULT_DIM; j++)
{
acc = 0.0;
for(k1 = FILTER_DIM - 1, k2 = 0; k1 >= 0; k1--, k2++)
{
acc += w2[k1] * tmp[t + (j + k2)];
}
result[t + j] = acc;
}
}
free(tmp);
return result;
}
使用gcc&#39; s -O3标志进行编译并在2.7GHz Intel i7上进行测试,使用4000x4000矩阵和5x5内核,我分别得到(平均5):
271.21900 ms
127.32000 ms
这仍然比scipy.signal的convolve2d有了相当大的改进,相同操作大约需要2秒钟,但我需要更多的速度,因为我会调用这个函数数千次。将数据类型更改为浮动目前不是一个选项,即使它会导致相当大的加速。
有没有办法可以进一步优化这些算法?我可以应用任何缓存技巧或例程来加快速度吗?
任何建议都将不胜感激。
答案 0 :(得分:2)
如果您只在x86上运行,请考虑使用SSE或AVX SIMD优化。对于double
数据,吞吐量的改善将是适度的,但如果您可以切换到float
那么您可以使用SSE或使用AVX获得大约4倍的改进。关于StackOverflow上的这个主题,有很多问题和答案,您可以从中获得有关实现的一些想法。或者,也存在许多可用的库,其包括高性能2D卷积(过滤)例程,并且这些例程通常利用SIMD来实现性能,例如,英特尔的IPP(商业)或OpenCV(免费)。
另一种可能性是利用多个核心 - 将您的图像拆分为块并在其自己的线程中运行每个块。例如。如果你有一个4核CPU,那么将你的图像分成4个块。 (见pthreads)。
如果您真的想要完全优化此操作,您当然可以结合上述两个想法。
您可以应用于当前代码以及任何未来实现(例如SIMD)的一些小优化:
如果您的内核是对称的(或奇数对称的),那么您可以通过添加(减去)对称输入值并执行一次乘法而不是两次来减少操作次数
对于可分离的情况,而不是分配一个完整的帧临时缓冲区,考虑使用“条带挖掘”方法 - 分配一个较小的缓冲区,这是一个全宽,但行数相对较少,然后处理你的图像在“条带”中,交替应用水平内核和垂直内核。这样做的好处是,您拥有更多缓存友好的访问模式和更小的内存占用。
关于编码风格的一些评论:
register
关键字多年来一直是多余的,现代编译器会在您尝试使用它时发出警告 - 通过抛弃它来节省一些噪音(和一些打字)
在C中投射malloc
的结果是不受欢迎的 - 它是redundant and potentially dangerous。
创建任何输入参数const
(即只读)并使用restrict
表示任何永远不会混淆的参数(例如a
和result
) - 这不仅有助于避免编程错误(至少在const
的情况下),但在某些情况下,它可以帮助编译器生成更好的优化代码(特别是在潜在别名指针的情况下)。