Logistic Regression

이번 글에서는 입력 데이터를 학습한 뒤, 데이터를 두 가지로 분류하는 방법을 다룹니다.
이 글에서 사용할 예제는 다음과 같습니다.

보고서 제출 여부(x1) 쪽지 시험 점수(x2) 출석 수(x3) 합격 여부(y)
1 2 1 0
0 3 2 0
1 3 3 0
1 5 5 1
1 7 5 1
1 2 5 1

Linear Regression에서는 가설 함수를 y = w1x1 + w2x2 + w3x3 형태로 작성을 하였습니다. 그러나 Logistic Regression에서는 결과를 0 또는 1로 만드는 것이 목표이기 때문에 가설 함수의 값을 0과 1 사이로 조정할 필요가 있습니다.
가설 함수를 다음과 같이 작성합시다.

아래줄에 있는 식은 시그모이드(sigmoid)함수라고 불리며, 입력 데이터를 0부터 1사이의 값으로 적절히 변환하여 반환해줍니다. 그래프는 아래 사진과 같습니다.

입력 값이 작아지면 0으로 수렴하고, 커지면 1로 수렴하는 함수입니다.

이제 비용 함수를 만들어봅시다.
Linear Regression에서의 비용함수는 단순히 오차를 제곱해서 만들었습니다.
오차가 선형으로 분포되어있기 때문에 제곱을 하면 매끄러운 이차 함수가 나오기 때문에 미분 값을 빼주어서 최저점의 도달할 수 있었습니다.
그러나 Logistic Regression에서의 오차는 선형이 아니기 때문에 매끄러운 이차 함수 모양이 나오지 않습니다. 그러므로 Linear Regression과 같이 미분 값을 빼주면서 학습을 하기 위해서는 매끄러운 곡선 형태를 띄는 오차 함수를 새로 정의해야 합니다.
만약 원하는 값이 1이라고 가정합시다. 출력 값이 1인 경우에는 비용이 0이며, 0인 경우에는 비용이 매우 커집니다.
반대로 원하는 값이 0이라고 가정합시다. 출력 값이 0인 경우네는 비용이 0이며, 0인 경우에는 비용이 매우 커집니다.
위 사항을 만족하면서 그래프가 매끄러운 곡선 형태의 함수를 만들어봅시다.
원하는 값이 1인 경우에는

이런 형태의 함수가 필요하고, y = -log x가 이런 그래프를 갖습니다.
원하는 값이 0인 경우에는

이런 형태의 함수가 필요하고, y = -log (1-x)가 이런 그래프를 갖습니다.

이제 가설함수와 비용함수를 코드로 구현해봅시다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
float sigmoid(float x) {
	return 1 / (1 + exp(-x));
}

float f(vf &w, vff &x, int idx) { //idx번째 데이터에 대한 가설 함수
	float sum = 0.0f;
	for (int i = 0; i<w.size(); i++) {
		sum += w[i] * x[i][idx];
	}
	return sigmoid(sum);
}

float cost(vf &w, vff &x, vf &y, int idx) {
	if (y[idx]) return -log(f(w, x, idx));
	return -log(1 - f(w, x, idx));
}

이렇게 하면, 나머지 부분은 Linear Regression의 코드와 같습니다.
전체 코드입니다.

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#include <stdio.h>
#include <math.h>
#include <vector>
#include <utility>
using namespace std;

const float alpha = 0.01;
const float target = 0.0001;
typedef vector<float> vf;
typedef vector<vf> vff;

inline float sigmoid(float x) {
	return 1 / (1 + exp(-x));
}

inline float f(vf &w, vff &x, int idx) { //idx번째 데이터에 대한 가설 함수
	register float sum = 0.0;
	for (register int i = 0; i<w.size(); i++) {
		sum += w[i] * x[i][idx];
	}
	return sigmoid(sum);
}

inline float cost(vf &w, vff &x, vf &y, int idx) {
	if (y[idx]) return -log(f(w, x, idx));
	return -log(1 - f(w, x, idx));
}

inline float descent(int i, vf &w, vff &x, vf &y, int idx) {
	float d = w[i];
	w[i] = d - alpha;
	float c1 = cost(w, x, y, idx);
	w[i] = d + alpha;
	float c2 = cost(w, x, y, idx);
	w[i] = d;
	d = (c2 - c1) / (alpha * 2);
	return d;
}

void logistic_descent_learning(vf &w, vff &x, vf &y, int cnt) {
	float c;
	vf d(w.size());
	for (int step = 1; step <= cnt; step++) {
		c = 0.0f; for (int i = 0; i<d.size(); i++) d[i] = 0.0f;
		for (int i = 0; i<x.size(); i++) {
			c += cost(w, x, y, i);
			for (int j = 0; j<d.size(); j++) d[j] += descent(j, w, x, y, i);
		}

		c /= d.size();
		for (int i = 0; i<w.size(); i++)
			w[i] -= alpha * d[i] / d.size();
		if(step%10000 == 0){
			printf("step : %d\tcost : %f\n", step, c);
			for (int i = 0; i<w.size(); i++) printf("%f ", w[i]);
			printf("\n");
		}
	}
}

int main() {
	vff data = {
		{1, 2, 1},
		{0, 3, 2},
		{1, 3, 3},
		{1, 5, 5},
		{1, 7, 5},
		{1, 2, 5}
	};
	vf y = { 0.0, 0.0, 0.0, 1.0, 1.0, 1.0 };
	vf w(3, 1);

	logistic_descent_learning(w, data, y, 2000000);
	for (int i = 0; i<w.size(); i++) printf("%f ", w[i]);
}

다음 글에서는 Softmax라고 불리는, 데이터를 2가지가 아닌 여러 가지로 분류하는 방법을 알아보도록 하겠습니다.