逸珺 · 2020年09月09日

数学之美:均值计算的两种算法(C实现)

首发:嵌入式客栈
作者:逸珺

导读

在嵌入式产品开发中,有时会需要利用一些数学统计的一些知识,并利用代码的方式实施在产品的应用中。有人会说均值有啥好聊的,不就是加起来除一下嘛?不妨来读一读。

本文目的不是数学,而在于分享如何进行工程应用实现。

什么是均值?

对于离散数据集,算术平均值也称为期望值或简称为平均值,是离散数据集合的中心值。假设有这样的数据序列:,其均值由下面的式子计算:

对,你说的没错就是加起来求平均。有盆友或许会问,为什么均值有的地方写的是,而这里写成,这其实是有缘由的:

  • 样本均值(sample mean):某类随机变量有限样本的算术平均值。
  • 总体均值(population mean):从随机变量概率分布的角度对随机变量趋势的度量,所以从这个角度而言,下面的公式正是描述了这个概念:

弄这样两个概念有什么必要呢?总体均值反应的是事务的总体规律,实际研究中,往往很难得到所有的数据,比如产品的某项指标规律,如果每一个产品都去测,代价可能极其高昂,实际往往是对产品进行抽样检测。(公式中多写了个X)

大数定律指出样本数量越大,样本均值越接近总体均值

如此一来这就有实用意义了。

均值除了上面这种算术平均值之外,还有几何平均值、谐波均值、功率均值、加权均值、截断均值、函数泛化均值等,有兴趣的可以去了解一下。

如何计算均值?

这里主要讨论对于嵌入式电子系统编程中,样本均值的计算方法以及C代码。分享直接法和递推法计算均值,重点介绍递推法。

直接法

按照公式定义,加和求平均。这个编代码很容易:

{  
   if(pSample==NULL || size<=0)  
       return NAN;  
    
   float sum = 0;  
   for(int i=0;i<size;i++)  
   {  
     sum += *pSample;  
     pSample++;  
   }  
  
   return (sum/size);  
}  

该方法简单直接,但是缺点是在内存比较小的单片机系统中如需要计算大样本集的场合,就捉襟见肘了。比如几万样本时,内存可能就不够了!

递推法

因为样本均值计算公式如下:

那么前个样本的均值为:

不难得出:

从而

这样就可以编代码了:

{  
  static int index = 0;  
  static float last_mean = 0.0f;  
  float mean = 0.0f;  
  
  if(index<size-1)  
  {  
    index++;  
    mean = last_mean+(xn-last_mean)/index;  
  }  
  else  
  {  
    mean = last_mean+(xn-last_mean)/size;  
    index     = 0;  
    last_mean = 0.0f;  
  }  
  
  last_mean = mean;  
  return mean;  
}  

这个代码很容易理解:

  • 在样本窗未满时,按实际传入样本大小递推。
  • 在样本窗满后,按实际传入样本大小递推,并复位索引。

函数内静态变量不推荐使用,但这里函数使用了内部静态变量,为什么使用静态变量呢?因为所实现的需求对外部不可见,这种需求本身的作用域就在函数本体内部。这样写个人理解会更好一些。

关于static的用法,前面写过两篇文章,有兴趣的可以去点进去看看:

在嵌入式应用中,如果所需要统计的样本非常大时,这种算法将非常有实用价值,只需要极小的内存开销。尤其在一些传感器测量应用中,该方法非常有价值。

测试一下

#include <stdbool.h>  
#include <math.h>  
  
float mean(float *pSample,int size)  
{  
   if(pSample==NULL || size<=0)  
       return NAN;  
   float sum = 0;  
   for(int i=0;i<size;i++)  
   {  
     sum += *pSample;  
     pSample++;  
   }  
  
   return (sum/size);  
}  
  
float recursive_mean(float xn,int size)  
{  
  static int index = 0;  
  static float last_mean = 0.0f;  
  float mean = 0.0f;  
  
  if(index<size-1)  
  {  
    index++;  
    mean = last_mean+(xn-last_mean)/index;  
  }  
  else  
  {  
    mean = last_mean+(xn-last_mean)/size;  
    index     = 0;  
    last_mean = 0.0f;  
  }  
  
  last_mean = mean;  
  return mean;  
}  
#define N            (1000)  
#define SAMPLE_SIZE  (100)  
int main(int argc, char *argv[])  
{  
    float sim[N];  
    float out[N/SAMPLE_SIZE];  
  
    for(int i=0;i<N;i++)  
    {  
        sim[i]=i*5+rand()%10;  
    }  
    printf("\n\n");  
    int j=0;  
    for(int i=0;i<N;i=i+SAMPLE_SIZE)  
    {  
        out[j] = mean(&sim[i],SAMPLE_SIZE);  
        j++;  
    }  
  
    for(j=0;j<N/SAMPLE_SIZE;j++)  
    {  
        printf("%.2f,",out[j]);  
    }  
    printf("\n");  
    j = 0;  
    for(int i=0;i<N;i++)  
    {  
        out[j]=recursive_mean(sim[i],SAMPLE_SIZE);  
  
        if((i+1)%SAMPLE_SIZE==0)  
            j++;  
    }  
    for(j=0;j<N/SAMPLE_SIZE;j++)  
    {  
        printf("%.2f,",out[j]);  
    }  
    printf("\n");  
    return 0;  
}  

看一下结果:

252.14,752.38,1251.85,1751.82,2252.57,2752.25,3251.78,3751.58,4252.06,4752.02,  

两种计算方法效果一样,但是第二种方法消耗极小的内存。当然这增加了函数调用次数,但是在大样本计算时非常有利。

总结一下

在实际应用中,常常需要求取测量的均值,或者依据均值做相应的应用,而且均值是计算信号序列或者样本集其他数学统计规律的基础计算,比如要计算方差、协方差等等。那么实际应用中学会如何计算均值并进行编码实现时很必要的。尤其在一些内存受限的场景,学会利用递推规律进行计算很有学习掌握的价值。

推荐阅读
关注数
2834
内容数
164
分享一些在嵌入式应用开发方面的浅见,广交朋友
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息