머신러닝 카테고리 소개 & Linear Regression - 1

이 카테고리는 파이썬을 싫어하고, 라이브러리 사용보다 직접 구현하는 변태같은 필자의 특성에 맞게 머신러닝을 C++로 바닥부터 구현하는 방법을 다루는 흥미로운 글들이 올라올 것입니다.
Linear Regression, Classification 등 기초적인 것을 먼저 알아보고 신경망의 원리를 알아가며 구현한 뒤, 최종적으로 CNN 구현을 목표로 하고 있습니다. 이제 시작합니다!


Linear Regression은 선형적으로 분포되어 있는 데이터를 반복적으로 학습한다는 것으로 해석할 수 있습니다.
각각 좌표가 (1, 4), (4, 16), (3, 12) 인 점들이 이차원 직교 좌표계에 있다고 해봅시다. 우리는 직관적으로 y = 4x 형태의 직선 위에 있다는 것을 알 수 있지만, 배경 지식이 전혀 없는 기계는 직관적으로 판별하지 못합니다. 기계가 데이터의 규칙성을 찾아내도록 학습시키는 것이 머신러닝의 기초가 됩니다.

중학교 2, 3학년 정도에 이미 일차함수를 배웠기 때문에 선형으로 분포되어 있는 자료에 대해서는 일차함수 형태로 나타낼 수 있습니다.
이번 글에서는 y = wx 형태, 즉 y절편이 없는 형태의 일차함수만 생각해봅시다.

이 글에서는 (1, 2), (2, 4), (3, 6), (4, 8), (5, 10)으로 이루어진 데이터를 예시로 설명하도록 하겠습니다.
위에서 말했듯이 기계는 배경 지식이 전혀 없으므로 이 데이터들이 어떤 규칙을 갖고 있는지 모릅니다. 우리는 기계가 규칙을 찾는 방법을 학습하도록 만들어주면 됩니다.
먼저, 가설 함수 라는 것을 만듭시다. 가설 함수는 말 그대로 “데이터가 이런 규칙을 갖고 있을 것이다.” 라고 예상을 하는 함수입니다. 이 함수를 이용하여 학습을 진행할 것입니다. 가중치와 x값을 넣어주면 함수값을 반환해주는 가설 함수를 코드로 짜봅시다.

1
2
3
float f(float w, float x){
	return w * x;
}

이 가설함수를 이용해서 어떻게 학습을 하는지 알아봅시다.
처음에 기계는 w가 어떤 값인지 모르기 때문에 임의의 값 10을 주고 학습을 해봅시다.
가설함수에서 x에 1을 넣어주면, 10을 반환합니다. 우리는 10이 아닌 2가 나오게 하도록 학습을 시켜야 합니다.
학습을 위해서 가설함수가 반환한 값과 우리가 원하는 값을 비교해 얼마나 차이가 나는지 계산합시다.
E = (가설함수의 반환값) - (원하는 값)
오차 E는 10 - 2, 즉 8이 됩니다. w를 다음과 같이 수정해줍시다.
w -= E * a
여기서 a는 learning rate(보정율)라고 불리는 상수입니다. 보정율은 오차를 학습에 얼마나 적용시킬지를 정하는 상수입니다. 만약 보정율이 크다면 오차가 많이 반영되고, 반대의 경우에는 적게 반영됩니다. 각각의 장단점에 대해서는 나중에 다시 설명하겠습니다.

a를 0.25로 잡고 계산을 해봅시다. (계산의 편의를 위해 소수점 이하는 반올림하겠습니다.)
기존의 w가 10이고, f에 1을 넣었을 때의 E가 8이므로 w에서 2를 빼줍시다.(8 * 0.25) 그러면 w는 8이 됩니다
이제 f에 2를 넣어봅시다. w가 8으로 변했으므로 f(2)의 값은 16이 됩니다. E는 12이므로 w에서 3을 빼줍시다. 그러면 w는 5가 됩니다.
f에 3을 넣으면 15가 나오고, E는 9가 됩니다. w에서 2를 빼줍시다. (9 * 0.25 반올림) w는 3이 됩니다.
f에 4를 넣으면 12가 나오고, E는 4가 됩니다. w에서 1을 빼주면 w는 2가 됩니다.
다시 f에 1을 넣어보면 E는 0이 됩니다. w에서 0을 빼주면 w는 그대로 2입니다.

오차가 0이 되면 더 이상 보정이 되지 않는 것을 알 수 있습니다.

  • x : 입력 데이터
  • y : 원하는 값
  • w : 가중치
  • f : 가설 함수의 값
  • E : 오차
  • z : 수정값

으로 잡은 경우, 표는 다음과 같이 채워집니다.

x y w f E z
1 2 10 10 8 2
2 4 8 16 12 3
3 6 5 15 9 2
4 8 3 12 4 1
1 2 2 2 0 0

여기까지의 내용을 코드로 구현해봅시다.
위에서 가설 함수 부분은 이미 구현을 했으니 가설 함수를 이용해서 E를 구하는 함수를 만들어보겠습니다.

1
2
3
getE(float w, float x, float y){ //가중치, x값, 원하는 값
	return f(w, x) - y;
}

데이터는 x값과 원하는 y값이 {float, float}형태의 pair로 주어진다고 했을 때 학습 부분은 다음과 같이 작성됩니다.
std::pair<float, float> 는 이미 Data로 define 했으며, first와 second는 각각 x, y로 define 했습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
void linear_learning(vector<Data> &data, float w){
	int idx = 0;
	while(1){
		float e = getE(w, data[idx].x, data[idx].y);
		int z = round(e * alpha); //round함수는 반올림 함수입니다.
		if(z == 0) break;
		printf("x : %.1f, y : %.1f, w : %.1f, f : %.1f, E : %.1f, z : %.1f\n",
		data[idx].x, data[idx].y, w, f(w, data[idx].x), e, (float)z);
		w -= z;
		idx = (idx+1)%data.size();
	}
	printf("final value : %.1f", w);
}

로직을 간단히 설명하자면,
먼저 함수를 호출할 때에는 데이터들이 들어있는 vector와 초기 w값이 인자로 들어옵니다.
그 다음, 모든 데이터를 차례로 순회하면서 오차를 구하고, learning rate를 곱해서 w값을 조절해줍니다.
조절할 양이 0이 된다면 학습을 종료합니다.

전체 코드는 다음과 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <stdio.h>
#include <math.h>
#include <vector>
#include <utility>
#define x first
#define y second
using namespace std;

typedef pair<float, float> Data;
const float alpha = 0.25;
const float target = 0.0001;

inline float f(float w, float x){ // 가설 함수, f(x) = wx
	return w * x;
}

inline float getE(float w, float x, float y){ //가중치, x값, 원하는 값
	return f(w, x) - y;
}

inline int round(float x){
	return floor(x + 0.5);
}

void linear_learning(vector<Data> &data, float w){
	int idx = 0;
	while(1){
		float e = getE(w, data[idx].x, data[idx].y);
		int z = round(e * alpha);
		if(z == 0) break;
		printf("x : %.1f, y : %.1f, w : %.1f, f : %.1f, E : %.1f, z : %.1f\n",
		data[idx].x, data[idx].y, w, f(w, data[idx].x), e, (float)z);
		w -= z;
		idx = (idx+1)%data.size();
	}
	printf("final value : %.1f", w);
}

int main(){
	vector<Data> data;
	data.push_back({1.0, 2.0});
	data.push_back({2.0, 4.0});
	data.push_back({3.0, 6.0});
	data.push_back({4.0, 8.0});

	linear_learning(data, 10);
}