✏️ PS/알고리즘 이론

[ Average Filter ] 실시간으로 들어오는 데이터의 평균을 구해보자!

반응형

 

안녕하세요? 수구리입니다.

이번 포스팅에서는 평균 필터 알고리즘(Average Filter)이라는 알고리즘을 소개하려고 합니다.

리소스 모니터링 프로젝트를 진행하고 있는 도중,

실시간으로 CPU 사용량을 가져오는데, 이 데이터를 가지고 1분동안의 평균 CPU 사용량에 대해서 계산이 필요하게 되었습니다.

처음에 생각했던 것은 각 프로세스별로 1초마다 CPU 사용량을 받아오고 있기 때문에 1분에 대한 평균을 구하기 위해서는 60개짜리 크기의 배열을 하나씩 만들어주고, 평균을 구하면 되지 않을까?라고 생각했습니다.

하지만 이는 매우 무식한 방법이고, 공간적으로도 낭비가 심하고 또한 첫 1분에 대한 CPU 사용량을 계산하기 위해서는 최소 1분이라는 시간을 기다려야 한다는 점입니다.

그렇다면 평균 필터 알고리즘을 소개하기 전에 산술평균에 대한 의미를 알아보겠습니다.

 

 

[ 산술 평균 ]

일반적으로 우리가 흔히 아는 평균, 즉 산술평균을 구하는 방법은 다음과 같습니다.

이를 코드로 간단하게 표현하자면 ..

int sum = 0;
double avg = 0.0;

int arr[10] = {1, 2, 3, 4 ,5, 6, 7, 8, 9, 10};

for (int i=0; i < 10; i++) {
	sum += arr[i];
}

avg = sum / 10;

위와 같이 배열에 10개의 데이터가 있고, 모두 더한 값을 구합니다.

그러고 난 뒤, 총길이로 나눠주는 과정이 산술평균을 구하는 방법이죠.

 

 

 

[ 평균 필터 알고리즘 ]

하지만 데이터가 계속해서 1초 단위로 해서 새로운 값이 들어오고 있고, 배열의 길이가 계속해서 늘어나게 된다면?

산술평균을 구하는 방법을 써도 가능은 하겠다만,, 계속해서 모든 데이터를 순회하면서 총합을 구하고, 평균을 구한다?

데이터가 엄청 많아진다면 너무 비효율 적인 방법이죠.

이때 사용할 수 있는 방법이 오늘 소개할 평균 필터 알고리즘입니다.

우선 공식부터 살펴보겠습니다.

 

Avg(N) = ( N - 1 ) / N * Avg( N - 1 ) + 1 / N * newValue

각 변수가 의미하는 것부터 알아보겠습니다.

N : 현재까지의 데이터의 길이로, 위의 예제에서는 배열의 길이가 되겠죠?

Avg( N - 1 ) : 이는 새로 들어온 데이터를 제외하고 이전 데이터까지의 평균을 의미합니다.

newValue : 이 값이 새로 들어온 데이터입니다.

그러면 이제 빨간색으로 강조된 부분이 궁금하실 텐데요.

바로 가중치입니다. 즉, 이전 평균에 대해서는 길이가 N-1 일 때의 평균값이므로

이전 길이(N-1)를 새로운 데이터가 들어온 시점의 길이 N으로 나누어준 만큼 곱해준다는 것입니다.

그리고 새로운 데이터에게는 1 / N 만큼의 가중치가 곱해지는 것이죠.

이렇게 두 값을 더해주면 평균이 됩니다! 아니 이게 어떻게 된 일이죠? 저게 왜 평균이 되는 겁니까!

자자.. 브라우저를 열고 cpp.sh로 접속해봅시다..

 

#include <iostream>
using namespace std;

int main()
{
    int prevDatas[3] = {1, 2, 3, 4};
    int prevAvg = (1 + 2 + 3 + 4) / 4;
    cout << prevAvg; // 2.5
}

 

위와 같이 이전 데이터들이 1, 2, 3, 4이 있고 이에 대한 평균은 2.5가 됩니다. 아래에 새로운 데이터가 추가되었고,

평균 필터 알고리즘과 산술 평균의 값을 비교해봅시다.

 

#include <iostream>
using namespace std;

int main()
{
    int prevDatas[4] = {1, 2, 3, 4, 5};
    double prevAvg = (1.0 + 2.0 + 3.0 + 4.0 + 5.0) / 5.0;
    cout << prevAvg << "\n"; // 3
    
    double filterAvg = (2.5 * 0.8) + (5.0 * 0.2);
    cout << filterAvg; // 3
    
}

 

4/5와 1/5은 각각 0.8과 0.2로 치환하여 계산하니 산술평균과 같아지는 모습입니다..!

그렇다면 위의 알고리즘을 재귀식의 형태로 풀어낼 수 있으니 아래와 같은 형태의 코드로 나타낼 수 있습니다.

double avgFilter(int length, double prevAvg, double newValue) {

	double oldWeight = (length - 1) / length;
    	double newWeight = 1 / length;
    
    	double newAvg = (preAvg * oldWeight) + (newValue * newWeight);
    
    	return newAvg;

}

우선 double 자료형으로 선언한 이유는 평균값이므로 소수점까지 나올 수 있기 때문에 그렇습니다.

위에서 보았던 공식을 그대로 옮겨 놓았기 때문에 형태만 보시기 바랍니다.

그렇다면 이제 실시간으로 데이터를 가져오면서 1분 데이터의 평균을 구해봅시다.

 

 

[ 평균 필터 적용하기 ]

여기서 추가로 고려해야 할 점은 바로 1분 평균을 구하기입니다.

프로그램이 시작하자마자 1분 동안의 데이터를 가져오지 않으기 위해서 이 알고리즘을 적용하였기 떄문에, 위의 코드상에서 길이 변수인 length는 시간을 나타내게 될 것입니다.

그러므로 길이가 처음에는 1부터 시작을 하게 될 것이고 만약 어떤 프로세스가 1분동안 실행 계속해서 실행되어지고 있다면 length를 60으로 고정시켜주고 평균을 계속해서 구해주면 되겠죠?

아래에는 직접 적용한 코드입니다.

// PerfDataPerProcess.cpp

double CPerfDataPerProcess::CumulativeAverage (int &length, double prevAvg, double newNumber) {
	if(length == 1)	{
		length++;
		return newNumber;
	}

	double oldWeight = (length - 1) / (double)length;
	double newWeight = 1 / (double)length;
	double res = (prevAvg * oldWeight) + (newNumber * newWeight);
    
	if(length < 60)	{
		length++;
	}
	return res;
}

처음 길이가 1부터 시작하기 때문에 그 경우에는 평균을 구할 수 없기 때문에 그냥 새로 들어온 값이 그 시점에서는 평균이 됩니다.

그 이후부터는 시간이 계속 지나가면서 length가 증가하면서 새로 들어온 값에 가중치, 이전 평균에 대한 가중치를 구해서 새로운 평균을 구해주고, 만약 시간이 아직 1분이 지나지 않았다면 길이를 증가시켜 주고 아니라면 증가하지 않습니다.

이 함수는 각 프로세스마다 실행되고 있기 때문에 프로세스가 중간에 새로 들어올 수도 있고, 중간에 종료되어서 사라질 수도 있겠죠? 이에 대한 처리 또한 필요했습니다.

이 처리를 해주기 전에, 각 프로세스별로 변수부터 지정해주었습니다. 위의 코드에서 length(초)에 대한 변수와 1분 CPU 사용량에 대한 변수를 헤더 파일에서 아래와 같이 선언해주었습니다.

 

// PerfDataPerProcess.h

struct PerProcessDataObj : public DataObj
{
	// 생략
    
	int averageLength;
	double prevAvg;
	PerProcessDataObj()	{
		averageLength = 1;
		prevAvg = 0;
	}
    
	// 생략
};

위의 두 변수는 모든 프로세스마다 가지고 있는 변수가 되겠고, 초기에 프로그램이 실행됨과 동시에 생성자를 호출하므로 averageLength(초)를 1로 초기화, 이전 평균을 0으로 초기화해줍니다.

 

아래의 코드에서는 평균 필터 알고리즘을 적용한 CumulativeAverage 함수를 사용하는 부분입니다.

void CPerfDataPerProcess::SetTableInstance()
{
	PerProcessDataObj* procDataObj = (PerProcessDataObj*)dataObj;
	
    // 프로세스에 대한 CPU 사용률 계산
	usingPercent = _wtoi64(procDataObj->usageRate);
	if ((procDataObj->name.Compare(_T("Idle")) == 0))
	{
		idlePercent = _wtoi64(procDataObj->usageRate);
	}
	if (procDataObj->name.Compare(_T("_Total")) == 0)
	{
		usingPercent = abs(_wtoi64(procDataObj->usageRate) - idlePercent);
	}
    
    // 코어 수로 나눔
	usingPercent = (usingPercent / nCores);
    // 0.00 형태로 데이터 저장
	procDataObj->usageRate.Format(_T("%.02f"), usingPercent);
	
	if((*m_table).find(ID) == (*m_table).end())
	{
    	// 만약 종료된 프로세스라면?
		procDataObj->meanUsageRate.Format(_T("%.02f"), usingPercent);
	} else {
    	// 살아있는 프로세스라면?
		double meanPercent;
		if (procDataObj->name.Compare(_T("Idle")) != 0)
		{
			meanPercent = CumulativeAverage((*m_table)[ID].averageLength, (*m_table)[ID].prevAvg, usingPercent);
			procDataObj->meanUsageRate.Format(_T("%.02f"), meanPercent);
			procDataObj->averageLength= (*m_table)[ID].averageLength;
			procDataObj->prevAvg = meanPercent;
		} else {
			procDataObj->prevAvg = (*m_table)[ID].prevAvg;
		}
	}
	// update Table
	(*m_table)[ID] = *procDataObj;
}

 

 

위와 같은 코드를 통해서 실시간으로 살아있는 모든 프로세스에 대해 cpu 사용률을 가져오는 값으로

평균 필터 알고리즘을 적용하여 1분 동안의 평균 cpu 사용량을 구할 수 있었습니다!!

감사합니다!

반응형