将功能应用于滚动窗口

时间:2015-01-21 08:10:49

标签: matlab vector

假设我有一个很长的A值列表(比如长度1000),我想以100对的方式计算std,即我想要计算std(A(1:100))std(A(2:101))std(A(3:102)),...,std(A(901:1000))

在Excel / VBA中,可以通过以下方式轻松完成此操作: =STDEV(A1:A100)在一个单元格中,然后一次填写。现在我的问题是,如何在Matlab中有效地实现这一点,而不必使用任何昂贵的for循环。


编辑:是否也可以为时间序列列表执行此操作,例如:当 A 的尺寸为1000 x 4(即长度为1000的4个时间序列)?输出矩阵的尺寸应为901 x 4。

5 个答案:

答案 0 :(得分:8)

注意:要获得最快的解决方案,请参阅Luis Mendo's answer

所以首先使用for循环(特别是如果那些是你的实际尺寸)真的不会很贵。除非您使用的是非常旧版本的Matlab,否则JIT编译器(以及当然的预分配)会使循环变得便宜。

其次 - 你试过循环了吗?因为在开始过早优化之前,你应该先尝试一下天真的实现。

第三 - arrayfun可以使它成为一个单行,但它基本上只是一个带有额外开销的for循环,并且如果速度确实是你关注的话,很可能比for循环慢。

最后一些代码:

n = 1000;
A = rand(n,1);
l = 100;

for循环(几乎不笨重,可能效率很高):

S = zeros(n-l+1,1);  %//Pre-allocation of memory like this is essential for efficiency!
for t = 1:(n-l+1)
    S(t) = std(A(t:(t+l-1)));
end

矢量化(内存不足!)解决方案:

[X,Y] = meshgrid(1:l)
S = std(A(X+Y-1))

一个可能更好的矢量化解决方案(和一个单线程),但仍然没有内存效率:

S = std(A(bsxfun(@plus, 0:l-1, (1:l)')))

请注意,使用所有这些方法,您可以将std替换为任何函数,只要它适用于矩阵的列(这是Matlab中的标准)


走2D:

要去2D,我们需要去3D

n = 1000;
k = 4;
A = rand(n,k);
l = 100;

ind = bsxfun(@plus, permute(o:n:(k-1)*n, [3,1,2]), bsxfun(@plus, 0:l-1, (1:l)'));    %'
S = squeeze(std(A(ind)));
M = squeeze(mean(A(ind)));
%// etc...

OR

[X,Y,Z] = meshgrid(1:l, 1:l, o:n:(k-1)*n);
ind = X+Y+Z-1;
S = squeeze(std(A(ind)))
M = squeeze(mean(A(ind)))
%// etc...

OR

ind = bsxfun(@plus, 0:l-1, (1:l)');                                                  %'
for t = 1:k
    S = std(A(ind));
    M = mean(A(ind));
    %// etc...
end

OR(取自Luis Mendo's answer - 在他的回答中注意到他显示了这个简单循环的更快替代方案)

S = zeros(n-l+1,k);
M = zeros(n-l+1,k);
for t = 1:(n-l+1)
    S(t,:) = std(A(k:(k+l-1),:));
    M(t,:) = mean(A(k:(k+l-1),:));
    %// etc...
end

答案 1 :(得分:7)

您所做的基本上是过滤操作。

如果您有权访问图像处理工具箱,

stdfilt(A,ones(101,1)) %# assumes that data series are in columns

会做到这一点(无论A的维度如何)。请注意,如果您还可以访问并行计算工具箱,则可以让这些过滤器操作在GPU上运行,尽管您的问题可能太小而无法产生明显的加速。

答案 2 :(得分:7)

为了尽量减少操作次数,您可以利用这样一个事实,即标准偏差可以计算为差异涉及第二和第一时刻

enter image description here
使用累积总和(使用cumsum)有效获得滚动窗口上的时刻:

A = randn(1000,4); %// random data
N = 100; %// window size
c = size(A,2);
A1 = [zeros(1,c); cumsum(A)];
A2 = [zeros(1,c); cumsum(A.^2)];
S = sqrt( (A2(1+N:end,:)-A2(1:end-N,:) ...
    - (A1(1+N:end,:)-A1(1:end-N,:)).^2/N) / (N-1) ); %// result

基准

这是使用timeit与基于循环的解决方案的比较。循环方法与Dan's solution中一样,但适用于2D情况,以矢量化方式显示std沿每列工作的事实。

%// File loop_approach.m
function S = loop_approach(A,N);
[n, p] = size(A);
S = zeros(n-N+1,p);
for k = 1:(n-N+1)
    S(k,:) = std(A(k:(k+N-1),:));
end

%// File bsxfun_approach.m
function S = bsxfun_approach(A,N);
[n, p] = size(A);
ind = bsxfun(@plus, permute(0:n:(p-1)*n, [3,1,2]), bsxfun(@plus, 0:n-N, (1:N).')); %'
S = squeeze(std(A(ind)));

%// File cumsum_approach.m
function S = cumsum_approach(A,N);
c = size(A,2);
A1 = [zeros(1,c); cumsum(A)];
A2 = [zeros(1,c); cumsum(A.^2)];
S = sqrt( (A2(1+N:end,:)-A2(1:end-N,:) ...
    - (A1(1+N:end,:)-A1(1:end-N,:)).^2/N) / (N-1) );

%// Benchmarking code
clear all
A = randn(1000,4); %// Or A = randn(1000,1);
N = 100;
t_loop   = timeit(@() loop_approach(A,N));
t_bsxfun = timeit(@() bsxfun_approach(A,N));
t_cumsum = timeit(@() cumsum_approach(A,N));
disp(' ')
disp(['loop approach:   ' num2str(t_loop)])
disp(['bsxfun approach: ' num2str(t_bsxfun)])
disp(['cumsum approach: ' num2str(t_cumsum)])
disp(' ')
disp(['bsxfun/loop gain factor: ' num2str(t_loop/t_bsxfun)])
disp(['cumsum/loop gain factor: ' num2str(t_loop/t_cumsum)])

<强>结果

我正在使用Matlab R2014b,Windows 7 64位,双核处理器,4 GB RAM:

  • 4栏案例:

    loop approach:   0.092035
    bsxfun approach: 0.023535
    cumsum approach: 0.0002338
    
    bsxfun/loop gain factor: 3.9106
    cumsum/loop gain factor: 393.6526
    
  • 单列案例:

    loop approach:   0.085618
    bsxfun approach: 0.0040495
    cumsum approach: 8.3642e-05
    
    bsxfun/loop gain factor: 21.1431
    cumsum/loop gain factor: 1023.6236
    

所以基于cumsum的方法似乎是最快的:比4列情况下的循环快<400> 快1000倍在单列案例中。

答案 3 :(得分:4)

有几个功能可以在Matlab中有效地完成工作。

一方面,您可以使用colfiltnlfilter等函数来执行滑块的计算。 colfiltnlfilter更有效,但只有在块内元素的顺序无关紧要时才能使用S = colfilt(A, [100,1], 'sliding', @std); 。以下是如何在数据上使用它:

S = nlfilter(A, [100,1], @std);

floor((100-1)/2) = 49

在您的示例中,您可以清楚地看到性能的差异。但有一个技巧:两个函数都填充输入数组,以便输出向量与输入数组具有相同的大小。要仅获取输出向量的相关部分,您需要跳过第一个1000-100+1第一个元素,并取S(50:end-50) 个值。

colfilt

但是还有另一种解决方案,接近colfilt,效率更高。 col2im调用colfilt将输入向量重新整形为一个矩阵,在矩阵上将每个不同列应用给定函数。这会将大小为[1000,1]的输入向量转换为大小为[100,901]的矩阵。但colfilt用0或1填充输入数组,你不需要它。因此,您可以在没有填充步骤的情况下运行std,然后在每列上应用std,这很容易,因为应用于矩阵的S = std(im2col(X,[100 1],'sliding')).'; 会返回列的stds的行向量。最后,如果需要,将其转置以获得列向量。简而言之,一行:

colfilt

备注:如果您想应用更复杂的功能,请参阅{{1}}的代码,第144和147行(适用于v2013b)。

答案 4 :(得分:1)

如果你关心的是for循环的速度,你可以通过将矢量折叠成一个数组(使用reshape)和具有你的元素数量的列来大大减少循环迭代次数想要应用你的功能。

这将让Matlab和JIT通过在数组的每一列上计算函数来执行优化(在大多数情况下,它们比我们做得更好)。

然后,您重塑一个offseted版本的数组并执行相同的操作。您仍然需要一个循环,但迭代次数仅为l(在示例中为100),而不是经典n-l+1=901循环中的for一次窗口)。 当你完成后,你将结果数组重新整形为一个向量,然后你仍然需要手动计算最后一个窗口,但总的来说它仍然要快得多。

采用与Dan相同的输入符号:

n = 1000;
A = rand(n,1);
l = 100;

它将采取这种形式:

width = (n/l)-1 ;           %// width of each line in the temporary result array
tmp = zeros( l , width ) ;  %// preallocation never hurts
for k = 1:l                 
    tmp(k,:) = std( reshape( A(k:end-l+k-1) , l , [] ) ) ; %// calculate your stat on the array (reshaped vector)
end
S2 = [tmp(:) ; std( A(end-l+1:end) ) ] ;  %// "unfold" your results then add the last window calculation

如果我tic ... toc完整循环版本和折叠版本,我会得到这个平均结果:

Elapsed time is 0.057190 seconds. %// windows by window FOR loop
Elapsed time is 0.016345 seconds. %// "Folded" FOR loop

我知道tic/toc不是完美计时的方法,但我的matlab版本上没有timeit函数。此外,差异足以表明存在改进(虽然这种方法无法精确量化)。我删除了第一次运行,我检查结果是否与不同的矩阵大小一致。


现在关于你的“单线程”请求,我建议你把这段代码包装成一个这样的函数:

function out = foldfunction( func , vec , nPts )

n = length( vec ) ;
width = (n/nPts)-1 ;
tmp = zeros( nPts , width ) ;

for k = 1:nPts
    tmp(k,:) = func( reshape( vec(k:end-nPts+k-1) , nPts , [] ) ) ;
end
out = [tmp(:) ; func( vec(end-nPts+1:end) ) ] ;

在主代码中允许您在一行中调用它:

S = foldfunction( @std , A , l ) ;

此格式的另一大好处是,您可以使用与其他统计功能完全相同的子功能。例如,如果您想要窗口的“平均值”,则只需更改func参数即可调用:

S = foldfunction( @mean , A , l ) ;

仅限制,因为它仅适用于矢量作为输入,但通过一些返工,可以将数组作为输入。