정보공간_1

[2기 수원 이동욱] 인공신경망 Neural Network #2 본문

IT 놀이터/Elite Member Tech & Talk

[2기 수원 이동욱] 인공신경망 Neural Network #2

알 수 없는 사용자 2012. 11. 22. 22:24


안녕하세요! 

수원멤버십 21-1기 이동욱입니다~

날씨가 많이 추워졌군요ㅎㅎ


지난 포스팅에 이어서 뉴럴 네트워크(Neural Network)에 관한 이야기를 이어가도록 하겠습니다.

지난 포스팅과 이어지므로 링크해두었습니다.

인공신경망 Neural Network #1 보러가기


1. 뉴런의 수학적 해석

먼저 한 걸음 더 나아가기에 앞서서, 지난번에 함께 논의한 뉴런의 입출력 동작을 수학적으로 해석해보겠습니다. 지난 포스팅에서 만들었던 뉴런을 도식화하면 아래와 같습니다. 편의상 그림에는 2 개의 입력만 그렸지만, 더 많은 입력이 있을 수 있습니다.



출력을 살펴보면, 우선 앞 뉴런들로 부터 입력을 받으면(1 또는 0) 각 연결가중치가 곱해지고, 이들의 합이 일정값(역치) T를 넘으면, 출력이 1, 그렇지 않으면 0 이 됩니다.

여기서 입력이 2 개만 있다고 가정했을 때만 살펴보면, 출력조건은 다음과 같습니다.


이 식을 x2 에 대해서 정리하면,

직선방정식이지요? 이 직선을 x1x2 좌표평면에 그렸을 때, 직선보다 윗부분에 나타나는 순서쌍은 True 를 출력할 것이고, 직선보다 아래쪽에 나타나는 순서쌍은 False를 출력하겠군요. 예를들면


w1, w2, T에 의한 직선이 위와 같이 그려졌다고 했을 때, (1,1)의 출력은 True가 되고 나머지 경우들의 출력은 False가 되는 것입니다. 즉 뉴런의 출력여부는 가중치 w1, w2 와 역치 T에 의해 판가름난다고 볼 수 있는 것입니다.


2. 퍼셉트론을 위한 뉴런의 개선

퍼셉트론(Perceptron)이란 1957년에 코넬 항공 연구소(Cornell Aeronautical Lab)의 Frank Rosenblatt에 의해 처음 제안된 용어로서, 뉴럴 네트워크를 이루는 뉴런들의 연결가중치를 조금씩 조정하므로써 목표와 최대한 근접한 해를 찾는 방법 및 구조를 뜻합니다. (퍼셉트론은 결과가 다시 앞 뉴런의 입력으로 사용되는 피드백 구조를 사용하지 않기 때문에 피드포워드 뉴럴 네트워크(Feed-forward Neural Network)라고도 불립니다.)

예를들면 위에서 보인 뉴런의 가중치 w1, w2 를 조금씩 조정하므로서 원하는 출력조건을 만들어내는 것입니다.

간단한 퍼셉트론을 만들어보기 전에, 우선 위에서 논의한 뉴런의 동작을 조금 개선시켜보려고 합니다.  물론 지금까지 논의한 뉴런의 동작이 실제 뉴런과 가장 유사한 동작이며, 이 뉴런도 퍼셉트론으로 사용될 수 있습니다.

하지만 조금 아쉬운 점이 2 가지 있습니다!

첫번째 아쉬운 점은 역치값(Threshold)이 고정되어 있다는 점입니다. 퍼셉트론은 가중치들을 조정하여 원하는 결과를 얻는 것인데, 역치값이 고정되어 있다면 힘들겠죠.

두번째 아쉬운 점은 입력과 출력이 0 또는 1 만 존재하기 때문에, 값이 맞고 틀렸을 때 얼마나 맞고 틀렸는지 알기가 힘듭니다. 즉 입력량이 역치을 훨씬 넘었는지, 아니면 아슬아슬하게 부족했는지 등을 알 수가 없겠죠.

그러면 이제 이 2 가지 단점을 극복한 새로운 뉴런을 소개하겠습니다.



바로 요놈입니다. 물론 입력은 더 많을 수 있지만, 그림에는 편의상 2 개만 표시하였습니다. 무엇이 달라졌을까요?

먼저, 역치값이 없어졌고, 대신 상수입력이 생겼습니다. 역치가 상수항의 역할을 했었지만, 이제 상수가중치 w3이 그 역할을 하게 되었습니다. 이로써 가중치 w3을 조절할 수 있으므로 고정된 역치의 문제를 해결하였습니다.

그리고, 출력이 True, False 밖에 없었는데 이제 sigmoid 라는 곡선함수로 대체되었습니다. sigmoid 함수는 0부터 1사이의 값을 갖는 부드러운 곡선 함수입니다.




그럼 뉴런 소스코드를 수정할 차례입니다.

  1 #include <cstdlib>
  2 #include <cmath>
  3 #define sigmoid(x) ( 1.0/(1.0+exp(-(x))) )
  4 
  5 class Neuron
  6 {
  7 private:
  8 	int num_of_input;		// Number of input synapse
  9 	double* input_weight;	// Chemical signal weight
 10 
 11 public:
 12 	Neuron(int num_of_input)
 13 	{
 14 		this->num_of_input = num_of_input;
 15 		input_weight = new double[num_of_input+1];	// The last one is for constant input!
 16 
 17 		for(int i=0; i<num_of_input+1; i++)
 18 			input_weight[i] = ((double)rand()/RAND_MAX)*2-1; // -1 ~ 1 Random
 19 
 20 	}
 21 	~Neuron()
 22 	{
 23 		delete[] input_weight;
 24 	}
 25 
 26 	double work(double input[])
 27 	{
 28 		double sum = 0;
 29 		for(int i=0; i<num_of_input; i++)
 30 		{
 31 			sum += input_weight[i] * input[i];
 32 		}
 33 		sum += input_weight[num_of_input] * 1.0;	// Constant input
 34 
 35 		return sigmoid(sum);		
 36 	}
 37 };

input_weight 배열의 길이가 하나 더 늘어났고, 입력과 출력이 double 형태로 변경되었습니다. work 함수에서 sum의 계산이 어떻게 달라졌는지, 그리고 sigmoid 함수를 이용한 출력을 유심히 봐주세요.

자 이로써 우리의 인공뉴런이 한결 정교해진 것 같군요!


3. 퍼셉트론

자 오래 기다리셨습니다. 그럼 우리의 뉴런을 학습시켜볼 차례입니다! 다시말해 퍼셉트론을 만들어 보겠습니다.

실제로 생체의 뉴런에서는 축색돌기로 신호 전달이 발생하면, 그에 해당하는 수상돌기의 시냅스의 강도를 더 강하게 해주어서 다음번에는 신호를 더 잘 받아들이게 된다고 합니다. 비슷한 원리를 적용시켜 볼텐데요. 

원하는 출력이 나오지 않는 경우, 오차를 줄이는 방향으로 각 연결가중치를 변경하는 것이 핵심입니다.

오차 E = (실제출력) - (원하는출력) 이라고 했을때 E가 양수이면 출력을 줄여야 하는 상황이고, 음수이면 출력을 키워야 하는 상황입니다. 예를들어 E가 양수여서 출력을 줄이려면? 양수를 입력받은 시냅스의 가중치를 줄이고, 음수를 입력받은 시냅스의 가중치는 높여주어야 하겠죠.

즉 오차가 E라고 했을 때 시냅스 i 의 가중치를 아래와 같이 수정할 수 있습니다.


식에서 알파는 민감도 상수입니다. 이 값이 작을 수록 작은 양의 수정만 이루어 지겠죠?

이런 식으로 각 시냅스의 가중치를 조금씩 수정하는 것이죠.

여기서 한 가지 더 고려해야 할 사항이 있습니다. 출력 함수가 곡선을 띄기 때문에, 수정강도도 출력에 따라 달라져야 한다는 것입니다. (출력 함수를 직선으로 설정한 경우에는 고려하지 않아도 됩니다.) 쉽게 말해서, 출력이 급격하게 바뀌는 구간이라면, 아주 조금만 수정해도 되지만, 완만한 구간에서는 더 많이 수정해야 한다는 것이죠. 이를 반영하기 위해서는 수정양에 출력의 기울기를 곱해주면 해결 됩니다.

저희가 사용하고 있는 sigmoid 함수의 도함수는 다행히 f'(x) = f(x)(1-f(x)) 라는 편리한 성질을 띄고 있기 때문에, 출력값으로부터 바로 기울기를 구할 수 있습니다! 이를 적용하여 식을 아래와 같이 수정하겠습니다. o는 뉴런의 출력값입니다. 기존의 수정양에 출력의 기울기가 곱해졌습니다.

편의상 가중치를 바로 수정하는 것처럼 식을 작성하였지만, 실제로는 학습과 동시에 가중치를 수정하는 것 보다, 수정양을 모아두었다가, 한 세트의 학습이 끝났을 때 수정하는 것이 더 효과적입니다.

자 그럼 이제 우리의 인공뉴런이 학습과 수정을 할 수 있도록 코딩을 해볼 차례입니다.

  1 #include <cstdlib>
  2 #include <cmath>
  3 #define sigmoid(x) ( 1.0/(1.0+exp(-(x))) )
  4 
  5 class Neuron
  6 {
  7 private:
  8 	int num_of_input;		// Number of input synapse
  9 	double* input_weight;	// Chemical signal weight
 10 	double* weight_error;	// Cumulative weight error
 11 	double alpha;			// Sensitivity
 12 
 13 public:
 14 	Neuron(int num_of_input, double alpha)
 15 	{
 16 		this->num_of_input = num_of_input;
 17 		this->alpha = alpha;
 18 		input_weight = new double[num_of_input+1];	// The last one is for constant input!
 19 		weight_error = new double[num_of_input+1];
 20 
 21 		for(int i=0; i<num_of_input+1; i++)
 22 		{
 23 			input_weight[i] = ((double)rand()/RAND_MAX)*2-1; // -1 ~ 1 Random
 24 			weight_error[i] = 0.0;
 25 		}
 26 
 27 	}
 28 	~Neuron()
 29 	{
 30 		delete[] input_weight;
 31 		delete[] weight_error;
 32 	}
 33 
 34 	double work(double input[])
 35 	{
 36 		double sum = 0;
 37 		for(int i=0; i<num_of_input; i++)
 38 		{
 39 			sum += input_weight[i] * input[i];
 40 		}
 41 		sum += input_weight[num_of_input] * 1.0;	// Constant input
 42 
 43 		return sigmoid(sum);		
 44 	}
 45 
 46 	void learn(double input[], double target)
 47 	{
 48 		double output = work(input);
 49 		double output_error = output - target;
 50 
 51 		for(int i=0; i<num_of_input; i++)
 52 		{
 53 			weight_error[i] += output_error * input[i] * output * (1-output);
 54 		}
 55 		weight_error[num_of_input] += output_error * 1.0 * output * (1-output);
 56 	}
 57 
 58 	void fix()
 59 	{
 60 		for(int i=0; i<num_of_input+1; i++)
 61 		{
 62 			input_weight[i] -= alpha * weight_error[i];
 63 			weight_error[i] = 0.0;
 64 		}
 65 		
 66 	}
 67 };

먼저 가중치의 에러를 누적해서 저장할 weight_error 배열과 민감도 상수 alpha가 멤버변수로 추가하였습니다.

그리고 정답셋을 입력받아 학습하는 learn 함수와 누적된 에러를 이용해 가중치를 수정하는 fix 함수를 추가하였습니다. learn 함수를 잘 살펴보면 방금 이야기한 가중치 수정양을 계산하여 weight_error에 누적합니다. 그리고 fix 함수에서는 그 동안 누적된 양에 민감도 상수 alpha를 곱하여 가중치를 수정합니다.

우선 이 뉴런 한개만으로도 퍼셉트론으로 동작할 수 있다는 것을 보여드리겠습니다. 뉴런에게 AND 연산을 가르쳐 보겠습니다.

  1 #include <iostream>
  2 #include <ctime>
  3 using namespace std;
  4 
  5 void main()
  6 {
  7 	srand((unsigned)time(NULL));	// Set random seed
  8 
  9 	Neuron* neuron = new Neuron(2, 0.1);
 10 
 11 	// Sample Sets //
 12 	double sample_input[4][2] = {{0,0},{0,1},{1,0},{1,1}};
 13 	double sample_output[4] = { 0, 0, 0, 1 };
 14 
 15 	for(int i=0; i<5000; i++)
 16 	{
 17 		for(int j=0; j<4; j++)
 18 		{
 19 			neuron->learn( sample_input[j], sample_output[j]);
 20 		}
 21 		neuron->fix();
 22 
 23 		// Print result //
 24 		if((i+1)%100==0)
 25 		{
 26 			cout<<"------ Learn "<<i+1<<" times -----"<<endl;
 27 			for(int j=0; j<4; j++)
 28 			{
 29 				cout<<sample_input[j][0]<<' '<<sample_input[j][1]<<" : "
 30 <<neuron->work(sample_input[j])<<endl;
 31 			}
 32 		}
 33 	}
 34 
 35 	delete neuron;
 36 }

올바른 입출력을 계속 가르쳐주면서 뉴런이 학습하는 과정을 관찰합니다. 총 5000번 학습을 실시하고 100회째 마다 값이 얼마나 근접했는지 출력합니다. 결과는 아래와 같습니다.

... 중략 ....


우와!

재밌게도 처음에는 어리버리(?)하지만 학습이 반복될 수록 정답에 근접하고 있는 것을 보실수 있습니다. 물론 계속 반복하면 더욱 정확도가 높아집니다. 

4. 마치며

이처럼 단 한개의 뉴런으로도 간단하게나마 퍼셉트론을 구현해 볼 수 있었습니다. 그러나 하나의 뉴런만으로는 XOR 과 같은 연산을 가르칠 수 없습니다. 즉 한계가 있다는 뜻이죠. 하지만 여러개의 뉴런을 이용하면 왠만한 연산이 다 가능합니다.

따라서 여러개의 뉴런들로 퍼셉트론을 만드는 것이 일반적이며, 이를 멀티레이어 퍼셉트론(Multi-Layer Perceptron) 이라고 합니다.

멀티레이어 퍼셉트론에 관해서는 다음 포스팅에 이어서 계속하겠습니다!