[Neural Network] Single Layer Perceptron

단층 퍼셉트론은 선형 분류기 이다. Neural Networks 의 기본이 되는 Neuron 1개짜리 Single Layer PerceptronC 로 구현한다.

입력의 크기는 고정되지 않지만, 이해를 위해 위 그림에서는 x1, x2 2개의 좌표를 입력으로 받는 단층 퍼셉트론을 고려한다.

선형분류

선형 분류란 무엇인가? 2차원으로 이해하면 아주 쉽다.

(x,y) 의 좌표를 가지는 점들을 2개의 클래스로 분류하고 이들을 구분짓는 결정선 (Decision boundary) 를 찾는 것이다.

위 이미지에서 빨간점과 초록선을 나누는 결정선의 함수중 하나는 y=2x+3 이다. 위와 같이 Sparse 한 점들 사이에서는 무수히 많은 결정선이 존재 한다.

만일 저 빨간점과 초록점이 무수히 많다면 어떨까? 무수히 많으면 정확히 y=2x+3을 찾을 것이다.

즉 입력 차원에 상관없이 선형적인 분류만 가능하다는 것이다. 예를 들어 아래와 같은 문제는 단층 퍼셉트론으로는 해결 할 수 가 없다.
(입력이 2개면 2차원이다. 위의 그림에서 y=2x+3, 즉 1차식이라고 1차원이 아니다. 차수와 차원은 다른 의미이다)

Bias

처음 뉴런 이미지에서 입력이 3개인것을 볼 수 있다. bias란 절편으로 해석하면 된다.
왜 존재하는지는 수식으로 간단히 증명 할 수 있다.
bias가 없다면 위 뉴런의 식은 아래와 같을 것이다.

각 W를 a,b로 치환하고 입력을 x,y로 치환하였다.
아래 식을 보면 알겠지만 이 함수는 반드시 (0,0)을 지나게 된다. 즉 제대로 선형 분류를 할 수가 없다는 뜻이 된다. 따라서 y절편 값을 추가한것이 bias이다.

Forward

Neural Networks 시리즈는 순전파와 역전파가 있다. 순전파를 통해 분류를 할 수 있고 순전파+역전파를 통해 학습을 할 수 있다.

단층 퍼셉트론의 순전파는 아래와 같이 쓸 수 있다.

입력값(bias포함) 과 가중치들을 곱한 값을 활성화 함수(f)를 거친값이 뉴런의 값이 된다.
활성화 함수는 간단하게 0보다크면 1, 아니면 0으로 2개의 클래스만 분류하도록 간단하게 구현하면 된다.

Backward

역전파는 앞선 Forward에서 사용하는 가중치를 갱신하는 작업을 한다. Forward는 0 또는 1의 값을 반환하므로 해당 결과 값으로 오차를 구해 가중치를 수정하면 된다.

그냥 수식만 보고 아! 하지말고 이해를 해야 된다. 먼저 learing rate는 제외하고 생각하자.
단순히 입력값에다 오차(정답과의 차이)를 곱한값을 더해준다.

우리의 오차는 (정답-추측값) 이기 때문에 양수와 음수 모두가 가능하다.
간단히 생각해서 정답이 1인데 우리가 가진 값은 0이라면 우리가 가진값에 1을 더해주면 된다.
반대로 정답이 0인데 가진값이 1이라면 1을 빼주면 된다.

고로 이 차이를 더해주면 원하는 정답을 찾아 갈 수 있는것이다. 하지만 우리는 변수가 3개이므로 각 변수가 추측값에 미친 영향만큼 배분하여 업데이트를 해줘야 한다.

입력값이 크면 크게 변화를주고, 작으면 작게 변화를 줘야한다. 간단하게 입력값을 그대로 곱해서 갱신해주면 된다. 하지만 이렇게 업데이트를 해버리면 한번 Backward 할 때 마다 각 입력이 원하는 함수로 바로 갱신되어 버리기 때문에 전체 데이터에 대해서 올바르게 갱신하기위해 조금씩 업데이트를 하는것이다. 그것을 조절하는 변수가 바로 learning rate 인 것이다.

SLP in C

이제 이 간단한 뉴런을 구현해볼수 있다.

가중치는 사실 아무렇게나 초기화 해도 된다. (나는 그냥 [-1,1]로 초기화 했다)

learning rate는 0.1정도로 초기화 해주면 된다.

bias의 값이 제일 중요한데 이 값은 반드시 1 이여야 한다. w 만으로 절편을 나타내야 하기 때문이다. -1로 구현해도 무방하다.

즉 단층 퍼셉트론을 학습할때 조절해야 하는 값은 learning rate 하나가 아니란 뜻이다.

많은 설명들이 이 bias를 상수취급하는데 극단적으로 y=2x+100000 이란 함수를 찾을때 bias를 1같은 값으로 놓고 쓴다면 아주 오랜 시간이 걸릴것이다.
(그래서 입력데이터 정규화가 필요한 것이다)

#include<stdio.h>
#include<stdlib.h>
typedef struct SLP SLP;
struct SLP {
	int n;			//입력의 크기
	float* w;		//가중치
	float* input;	//입력값(할당하지 않음)
	int output;	//출력값(0 또는 1)
};
SLP CreateSLP(int n){
	SLP slp;
	slp.n = n;
	slp.w = (float*)calloc(slp.n+1, sizeof(float));
	for (int i = 0; i < slp.n+1; i++) {
		slp.w[i] = ((float)rand()) / RAND_MAX ;
	}
	return slp;
}
int ForwardSLP(SLP* slp, float* input) {
	slp->input = input;
	float out = slp->w[slp->n];
	for (int i = 0; i < slp->n; i++) {
		out += slp->w[i] * input[i];
	}
	return slp->output = out >= 0;
}
void BackwardSLP(SLP* slp, int label,float learning_rate) {
	float error = (float)(label - slp->output);
	for (int i = 0; i < slp->n; i++) {
		slp->w[i] += learning_rate*slp->input[i] * error;
	}
	slp->w[slp->n] += learning_rate*error;
}

이렇게 단층 퍼셉트론은 30줄의 짧은 코드로 완성할 수 있다.

간단히 OpenCV로 직접 눈으로 보는 코드는 아래와 같다.

#include<opencv/cv.h>
#include<opencv/highgui.h>
/*
*  slp...
*/
void Coord2D_Test() {
	srand(time(0));
	int W = 500;
	int H = 500;
	int N = 200;
	float* x = (float*)calloc(N, sizeof(float));
	float* y = (float*)calloc(N, sizeof(float));
	int* label = (int*)calloc(N, sizeof(int));
	float a = 0.5;
	float b = 300;
	printf("y = %fx + %f(ground truth)\n", a, b);
	for (int i = 0; i < N; i++) {
		x[i] = rand() % W;
		y[i] = rand() % H;
		label[i] = a*x[i] + b >= y[i];	//좌표가 경계선위에 있으면 1, 밑에 있으면 0
	}
	float learning_rate = 0.1F;
	SLP slp = CreateSLP(2);
	int ans = 0;
	while (ans != N) {	//100% 분류를 할때까지 반복(이 데이터는 반드시 선형분류가 되기 때문)
		ans = 0;
		for (int i = 0; i < N; i++) {
			float input[2] = { x[i],y[i] };
			int a = ForwardSLP(&slp, input);
			if (a == label[i]) {
				ans++;
			}
			BackwardSLP(&slp, label[i], learning_rate);
		}
	}
	float at = -slp.w[0] / slp.w[1];
	float bt = -slp.w[2] / slp.w[1];
	printf("y = %fx %s %f\n", at, bt >= 0 ? "+" : "-", fabs(bt));
#ifdef CV_IMPL
	IplImage* img = cvCreateImage(cvSize(W, H), IPL_DEPTH_8U, 3);
	memset(img->imageData, 0, img->imageSize);
	cvLine(img, cvPoint(0, a * 0 + b), cvPoint(W, a*W + b), cvScalar(210, 139, 38, 0), 2, 8, 0);		//실제 정답 함수.
	cvLine(img, cvPoint(0, at * 0 + bt), cvPoint(W, at*W + bt), cvScalar(255, 255, 255, 0), 2, 8, 0);	//학습된 함수.
	for (int i = 0; i < N; i++) {
		if (label[i] == 1) {
			cvCircle(img, cvPoint(x[i], y[i]), 3, cvScalar(0, 0, 255, 0), CV_FILLED, 8, 0);
		} else {
			cvCircle(img, cvPoint(x[i], y[i]), 3, cvScalar(0, 255, 0, 0), CV_FILLED, 8, 0);
		}
	}
	cvFlip(img, img, 0);
	cvNamedWindow("slp", CV_WINDOW_AUTOSIZE);
	cvShowImage("slp", img);
	cvWaitKey(0);
	cvDestroyAllWindows();
#endif
	ReleaseSLP(&slp);
	free(x);
	free(y);
	free(label);
}

다차원에서의 선형 분류

다차원에서도 선형 분류가 가능하다. 대표적으로 MNIST 데이터의 01은 선형 분류가 된다.
즉 숫자 01을 분류하는것은 SLP로도 구현할 수 있다.

C언어에서 MNIST 데이터를 읽어오는것은 어렵지 않지만 MNIST나 CIFAR 등 여러가지 데이터를 쉽고 같은 포맷으로 읽어오기 위해서 아래의 라이브러리를 만들어 두었다.
OpenIMDS

이 곳에서 mnist.h 를 다운받아 사용해 볼 수 있다.

0과 1을 학습한뒤 valid set으로 정확도를 보았다.

#include"mnist.h"
void MNIST_Test() {
	IMDSImage train = GetMnistTrainData(0, 1 / 255.0F);
	IMDSImage valid = GetMnistValidData(0, 1 / 255.0F);
	int classify[2] = {0,1};
	for (int i = 0; i < train.n; i++) {
		if (train.label[i] == classify[0]) {
			train.label[i] = 0;
		} else if (train.label[i] == classify[1]) {
			train.label[i] = 1;
		} else {
			train.label[i] = 100;
		}
	}
	for (int i = 0; i < valid.n; i++) {
		if (valid.label[i] == classify[0]) {
			valid.label[i] = 0;
		} else if (valid.label[i] == classify[1]) {
			valid.label[i] = 1;
		} else {
			valid.label[i] = 100;
		}
	}

	float learning_rate = 0.1F;
	SLP slp = CreateSLP(28 * 28);
	for (int e = 0; e < 100; e++) {
		int answer = 0;
		int total = 0;
		for (int i = 0; i < train.n; i++) {
			if (train.label[i] <= 1) {
				int predict=ForwardSLP(&slp, train.image[i]);
				BackwardSLP(&slp, train.label[i], learning_rate);
				answer += predict == train.label[i];
				total++;
			}
		}
		printf("train accuracy : %f\n", answer * 100.0F / total);
		if (answer == total)break;
	}
	//valid
	int answer = 0;
	int total = 0;
	for (int i = 0; i < valid.n; i++) {
		if (valid.label[i] <= 1) {
			int predict = ForwardSLP(&slp, valid.image[i]);
			answer+=valid.label[i] == predict;
			total++;
		}
	}
	printf("valid accuracy : %f\n", answer * 100.0F / total);
}
결과
train accuracy : 100.000000
valid accuracy : 99.905434

선형 분류가 되는 데이터셋은 100% 의 정확도를 얻는다.
직접 2와3 을 분류하는 등 실험을 해보는것도 좋은 경험이 될 듯하다.