Linear Regression - 2

이전 글에서는 오차에 learning rate를 곱한 값을 이용해 학습을 진행하였습니다. 이번 글에서는 미분을 이용해 학습을 시켜보도록 하겠습니다.

머신러닝에서 원하는 값가설 함수의 값의 차이값을 비용(cost)라고 정의하고, 비용 함수를 이용해 학습을 진행합니다. 차이값은 양수 또는 음수로 나타낼 수 있는데, 비용은 차이값의 절대값과 비례한다고 할 수 있습니다. 따라서 비용 함수는 다음과 같이 정의합니다.
cost(w) = (wx - y)2
가로 축을 w, 세로 축을 cost(w)로 잡은 뒤 비용 함수를 그리면 아래로 볼록한 꼴의 그래프가 그려지게 됩니다. 학습의 목포는 오차를 줄이는 것, 즉 비용을 최소화 시키는 것이기 때문에 최대한 w가 비용 함수의 꼭짓점쪽으로 가야 합니다.

이제 비용 함수의 꼭짓점을 찾아나갑시다.

위 그림과 같이 꼭짓점을 찾아갈 것입니다. 오차가 클 때에는 많이 내려가고, 오차가 작을 때에는 조금씩만 내려가고 있습니다.
그래프를 잘 관찰해보면, 오차가 클 때에는 그 곳의 순간 변화율, 즉 미분 값이 크다는 것을 알 수 있습니다. 반대로, 오차가 작을 때에는 미분 값이 작다는 것을 알 수 있습니다.
이 점을 이용해서 미분 값을 이용해 학습을 할 것입니다.

이제 미분 값을 구해야 합니다. 구하는 것은 쉽습니다.

미분의 정의를 이용한다면 쉽게 구할 수 있습니다. 코드로 바로 구현해봅시다.

1
2
3
4
5
inline float descent(float w, float x, float y){
	float c1 = cost(w-0.001, x, y);
	float c2 = cost(w+0.001, x, y);
	return (c2-c1) / (0.001*2);
}

이전 글에서는 오차와 learning rate를 곱한 값으로 w를 조절해주었지만, 이번에는 미분 값과 learning rate를 곱한 값으로 w를 조절합니다.
코드로 구현해봅시다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void linear_learning(vector<Data> &data, float w){
	while(1){
		float d = 0.0, c = 0.0; //미분 값의 합, 비용의 합
		for(int i=0; i<data.size(); i++){
			c += cost(w, data[i].x, data[i].y);
			d += descent(w, data[i].x, data[i].y);
		}
		c /= data.size();
		float z = alpha * d / data.size();
		if(z < target) break;
		w -= z;
		printf("cost : %f, weight : %f\n", c, w);
	}
	printf("final value : %f", w);
}

위 코드에서 target은 허용 가능한 오차 범위를 의미합니다.
모든 데이터들에서 미분 값을 뽑아낸 뒤, 평균을 구하고 learning_rate를 곱해서 최종적으로 조절할 양을 정해줍니다.

지난 글에서 설명한 부분까지 합친 전체 코드는 다음과 같습니다.

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
48
49
50
51
#include <stdio.h>
#include <vector>
#include <utility>
#define x first
#define y second
using namespace std;

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

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

inline float cost(float w, float x, float y){ // y = wx
	float tmp = f(w, x) - y;
	return tmp * tmp;
}

inline float descent(float w, float x, float y){ // w에 대해 미분
	float c1 = cost(w-alpha, x, y);
	float c2 = cost(w+alpha, x, y);
	return (c2-c1) / (alpha*2);
}

void linear_learning(vector<Data> &data, float w){
	while(1){
		float d = 0.0, c = 0.0;
		for(int i=0; i<data.size(); i++){
			c += cost(w, data[i].x, data[i].y);
			d += descent(w, data[i].x, data[i].y);
		}
		c /= data.size();
		float z = alpha * d / data.size();
		if(z < target) break;
		w -= z;
		printf("cost : %f, weight : %f\n", c, w);
	}
	printf("final value : %f", 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, 100);
}

글을 마치기에 앞서, 이전 글에서 설명하지 않고 넘어간 learning rate의 크기에 따른 특징을 잠시 설명하도록 하겠습니다.

위 그림을 보면 아시다시피, learning rate가 너무 크면 최소값을 지나서 반대편으로 뻗어나가는, overshooting현상이 발생하게 됩니다. 반대의 경우에는 학습이 지나치게 천천히 되는 단점이 있습니다. 왼쪽 현상이 더 심각해 보이지만, 해당 현상은 쉽게 알아차릴 수 있는 반면, 오른쪽의 경우에는 쉽게 알아차릴 수 없어 고치기 힘듭니다.