2-10 Rosenblatt N = 2 Perceptron Weight Update 알고리듬 Processing 코딩

in #kr6 years ago (edited)

noname01.png

알고리듬으로 설명되고 있는 Rosenblatt의 퍼셉트론 코드가 오래되어서인지 특히 오늘날의 경사하강법을 대신하는 그의 웨이트업데이트 알고리듬이 제대로 돌아가는 것인지 의문스럽게 느껴졌던 것이 사실이다. 퍼셉트론에 대한 유튜브 동영상 및 인터넷 검색 결과 Rosenblatt의 알고리듬에 직접 쓰여 있는 것은 아니지만 error 개념을 도입하여 이 부분을 처리하고 있었다. 중간에 필요한 의견을 넣어 작성하였지만 이 error 개념이 Rosenblatt이 직접 고안했던 기법인지 여부는 알 수 없으나 여하튼 Rosenblatt의 알고리듬을 잘 반영하는 기법인 듯하다. 아울러 후반에 400개의 점 데이터를 작도한 결과들이 있는데 Rosenblatt 의 N = 400 과 결코 혼동해서는 안된다. 이번 블로그 내용은 점의 수와 관계없이 N = 2인 경우라는 점을 다시 강조한다. 아울러 이 문제를 해결함에 있어서 C/C++ 바탕의 Processing을 사용하게 되면 그래픽 처리가 아주 쉬워지는 장점이 있음에 주목할 필요가 있다.

이 블로그에 해설한 코드는 유튜브에 올라온 Coding Train의 털보 강사가 진행하는 10.2 Neural Networks: Perceptron Part1 The Nature of Code 동영상을 참조 해설하였다. 따라서 영어 히어링 문제가 있을지라도 유트브 동영상을 먼저 공부 한 후에 본문을 읽어 보는 것도 좋을듯해 보인다.

Simple_perceptron_01 코드예제에서 입력 벡터 X를 다음과 같이 임의로 설정하였다.
∙∙∙
float[] inputs = {-1, 0.5};
∙∙∙

Rosenblatt 의 알고리듬에서 입력벡터의 데이타가 2개인 예제는 N=2 인 경우에 해당하며, 좌표 평면에서 생각한다면 1개의 점에 해당한다고 볼 수도 있다.

만약 보다 많은 점 데이터를 생성할 수 있도록 Processing의 별도의 새 탭을 사용하여 trainning이라는 명칭으로 편집하도록 하자. 하나의 코드로 다 작성할 수 있겠지만 별도의 탭을 사용하면 덜 복잡해 보이는 장점이 있을 것 같다. 참고로 새로이 생성하는 tainning은 Simple_perception_01에 함께 폴더에 저장되며 Simple_perception_01을 불러내면 함께 오픈된다는 점에 유의하자.

noname02.png

trainning에서 다음과 같이 하나의 점을 생성하고 출력하는 class 루틴 Point 를 코딩하자.
지난 번에 setup()에서 임의로 입력했던 숫자 데이터 {-1, 0.5} 는 더 이상 필요치 않으므로 삭제하도록 한다.

// float[] inputs = {-1, 0.5};
// int guess = brain.guess(inputs);
// println(guess);

각각 실수인 (x, y) 좌표는 그래픽 화면 size를 정의하는 파라메터인 width 와 height를 사용하여 그래픽 화면 크기 안에 들어오는 random 넘버를 생성한다. 에를 들어 x=random(400) 명령을 실행하면 0∼400 사이의 실수형 random number 하나가 생성된다. 점이 생성되면
( x - y ) 값이 0 보다 큰지 비교하여 라벨 값 +1 과 –1을 부여한다.

noname03.png

한편 label 값을 주기 위해 사용한 조건 x > y 는 그래픽 화면 size(width, height)에서 width 와 height 가 같다면 사각형 영역을 대각선으로 나눈 영역의 윗부분을 뜻한다. 여기서 지적해야 해야 할 점은 점 데이터를 생성할 때에 항상 라벨 값이 주어지기 때문에 이 데이터는 학습용이지 절대 테스트용 데이터가 아니란 점이다. 테스트용 데이터는 라벨 값을 모르는 상태에서 학습이 끝난 후 라벨 값을 찾아내는 과정이다.

다음의 show() 루틴에서는 라벨 값에 따라서 흰 동그라미 또는 검은 동그라미를 출력한다
void show() {
stroke(0);
if ( label == 1) {
fill(255);
} else {
fill(0);
}
ellipse(x,y,18,18);
}
}

점 데이터를 1개 이상 랜덤하게 많이 생성하여 코드를 실행해 보는 중요한 이유는 입력 벡터를 발생 시키는 영역 전체 어디에서라도 Rosenblatt의 알고리듬이 제대로 잘 실행되는지 여부 확인과 아울러 classifier가 선형적으로 분리 가능한지를 확인해 보고자 함이다.

물론 컴퓨터 코드이므로 점 1개로부터 N개까지 실행이 가능해야 한다.

웨이트를 준비해야 하는데 Rosenblatt의 알고리듬에서는 웨이트와 바이아스는 0으로 초기화한다. 이것도 하나의 random 한 방법으로 볼 수 있으나 아예 0이 아닌 random number로 시작하는 것이 더욱 일반적이지 않나 싶다. 하지만 그래도 바이아스는 0으로 처리하기로 한다.
(※참고로 바이아스를 넣어서 코드를 작성하려면 문제의 성격이 N=2에서 N=3으로 바뀔 수 있다. 즉 입력 벡터의 데이터를 3개로 잡고 그중 첫 번째를 1.0으로 취하게 되면 처번째에 대응하는 웨이트 값을 0으로 두든지 또는 random number로 둘 수 있을 것이다. 즉 이 문제는 입력 벡터의 데이터가 3개인 문제로 귀착되는 것이다.)

∙∙∙
//Constructor
Perceptron() {
//initiallize the weights randomly
for ( int i = 0; i < weights.length; i++) {
weights[i] = random(-1, 1);
}
∙∙∙
입력벡터의 데이타가 2개이므로 웨이트도 2개만 준비하자. 아울러 이 부분은 Perceptron 이란 명칭으로 class 처리를 하여 메인 프로그램에 해당하는 setup에서 2개짜리 배열 오브젝트로 불러다 쓸 예정이다.
∙∙∙
float[] weights = new float[2];
∙∙∙

메인 프로그램인 Simple_perceptron_01로 돌아와서 class Perceptron 선언 다음에 1(N)개짜리 배열 Point 오브젝트를 선언해 둔다. setup에서 배열 points[]를 클라스인Point()를 불러 초기화 하기로 한다. 차후에 필요하면 Rosenblatt처럼 N = 400으로 바꾸면 된다. 점들을 생성하는데 사용된 trick 으로서 그래픽 화면의 size(width, height)가 정의 되면 x = random(width), y = random(height) 명령을 사용하여 그래픽 화면에 맞춰 어느 정도 균일하게 random해 보이는 좌표들을 얻을 수 있다. 꼭 1.0X1.0에 맞출 필요는 없다.

Perceptron brain;
Point[] points = new Point[1];

void setup() {
∙∙∙
for (int i = 0; i<points.length;i++) {
points[i] = new Point();
}
∙∙∙
}

점 생성을 위한 setup()이 완료되면 draw() 로 넘어가서 실제로 1(N)개에 해당하는 점을 생성하여 그래픽 화면에 작도하도록 한다.

void draw() {
background(255);
for (Point pt : points) { //special grammar
pt.show();
}
}

클라스 Point()는 한번씩 부를 때 마다 random 넘버 명령으로 생성된 (x, y) 좌표를 하나씩 제공한다. draw()에서 1(N)개의 점을 출력해주는 for looping에 special grammar가 사용된듯하다. 그 뜻은 Point pt까지는 class 선언인 듯하고 : 다음의 points 는 1(N)개로 잡아둔 배열 전체를 의미하는 듯하다. looping 과정에서 점이 생성 되자 마자 클라스 pt의 pt.show 루틴을 불러 그래픽 결과를 출력해 보기로 한다. 화면에 출력되는 점들은 점의 좌표를 중심으로 ellipse 명령을 사용하여 동그라미를 작도한다. show에서 그려 주는 동그라미는 stroke(0)의 검은색 경계선에 label 값이 +1 이면 fill(255) 즉 흰색이고 –1이면 fill(0) 즉 검은 색을 칠하도록 한다. N=1, 10, 100 인 경우의 작도 결과이다.

noname04.png

하지만 이어지는 학습과정에서 다시 빨간색 과 초록색으로 덧칠이 일어나 흰색과 회색 또는 흰색과 검은색 동그라미는 보이지 않게 됨에 유의하자.

위에 출력된 데이터는 random하게 생성만 되었을 따름이지 아직 학습이 이루어진 데이터가 아니다. 하지만 사선을 경계로 2종류의 데이터로 분류될 가능성이 대단히 높다. 즉 x가 y보다 크면 라벨 값이 “+”이고 그렇지 않을 경우에는 라벨 값이 “–”이다.
1(N)개의 점이 생성되었으면 Rosenblatt 퍼셉트론 알고리듬을 바탕으로 웨이트를 어떻게 업데이트 할 것인지 셍각해 보자.

noname05.png

이 업데이트의 뒷부분은 라벨 값에 입력 벡터를 곱하는 형태이다. 특히 binary classification 에 있어서 라벨 값에 해당하는 사전에 알려진 yi 의 값은 +1이거나 –1 이 되며 중간 값은 없다.

그러므로 어떻게 해서든 라벨 값에 가까워질 수 있도록 학습과정 즉 train 과정에서 웨이트를 업데이트할 필요가 있다. 다음과 같이 웨이트 업데이트를 위한 train루틴 코딩을 고려하자. (※단 루틴 명 train 은 앞에서 언급했던 trainning 탭의 클라스 Point 와는 무관함에 유의하자.)

noname06.png

현재의 웨이트 벡터와 입력 벡터를 사용하여 guess 루틴에서 시그마 합산 처리한 후 sign 함수 값을 return 받아 guess로 두자. 처음부터 알고 있는 정답 라벨 값을 target이라 두고 그 차이에 해당하는 error 값을 계산하자. target 의 값은 이미 새 탭 trainning 의 클라스 Point 루틴에서 점 생성과 아울러 정확하게 값이 주어지는데 Rosenblatt 의 yi 값에 해당한다고 보면 된다.

이 시점에서 guess 값을 계산해 보는 이유를 생각해 보자. Rosenblett의 알고리듬에서처럼 정확한 값을 알고 있는 yi 값을 사용해 웨이트를 업데이트 했어도 guess에 의해 rerun 받은 값이 target (또는 yi)값과 맞지 않은 경우 즉 라벨 값에 도달하지 못한 경우 학습이 계속 이루어져야 한다.

즉 라벨 값을 찾는 학습 과정에서 결국 아래 표에서 4가지 경우처럼 error 가 유발 될 수도 있다.

noname07.png

웨이트 업데이트 과정에서 error가 0인 경우는 더 이상 웨이트 값의 변화가 없으며 error가 0이라는 것은 결국 학습이 완료되었다는 점을 뜻한다. 반면에 error가 0이 아닌 경우에는 정확한 라벨 값에 도달할 수 있도록 웨이트 업데이트와 더불어 학습이 계속 이루어져야 한다.

weights[i] += lr * error * inputs[i]

error 개념을 도입하면 기껏해야 입력 벡터의 2배에 해당하는 웨이트 값이 얻어지는데 여기에다 learning rate를 곱하도록 하자. Rosenblatt 의 알고리듬에서는 learning rate 개념이 명시되어 있지 않은데 계속적으로 곱하기와 시그마에 의한 합산을 해 나가다 보면 숫자가 점점 커질 수도 있으므로 0.0∼1.0 사이의 작은 값을 사용하는 learning rate를 도입하여 곱하도록 한다. 클라스 Perceptron의 헤더 영역에 설정해 둔 후 train 루틴에서 사용한다.

class Perceptron {
float[] weights = new float[2];
float lr = 0.1;
∙∙∙
void train(float[] inputs, int target) {
∙∙∙
for (int i =0; i< weights.length; i++) {
weights[i] += lr * error * input[i];
}
}

클라스 Perceptron 에 train 루틴이 추가 되었으므로 darw 루틴에서 점 데이터를 그냥 출력만 할 것이 아니라 학습(train) 시킨 후에도 결과를 출력하도록 하자.

∙∙∙
void draw() {
background(255);
for (Point pt : points) { //special grammar
pt.show();
//학습(train)을 위해 필요한 루틴이 자리 잡을 곳
}
}

학습(train) 관련된 루틴 코드만 따로 작성해 보자.
∙∙∙
for (Point pt : points) {
float[] inputs ={pt.x, pt.y}; // trainning탭에 class Point pt 로 선언하였음
int target = pt.label; // class pt에서 label 값도 넘겨받아 변수 target과 등치
brain.train(inputs, target); // 입력 벡터와 target 을 제공하여 학습을 실시한다
int guess =brain.guess(inputs); // 입력 벡터를 사용 웨이트 업데이트 이전에
// guess 값 산출
if (guess == target) { // 학습완료면 녹색 출력
fill(0, 255,0);
} else {
fill(255, 0, 0); // 학습 미완료면 빨간색 출력
}
noStroke();
ellipse(pt.x,pt.y, 16, 16); //경계선 없는 동그라미 출력
}
∙∙∙

2가지 관점 하에서 코드를 실행해 보자.
draw() 루틴에서 brain.train()을 실행하지 않은 상태에서 즉 현재 상태의 guess 와 target 즉 label 값이 맞지 않을 경우 웨이트 업데이트가 안되므로 아무리 draw 루틴이 실행되어도 맞지 않는 입력 벡터 성분 및 해당 guess 값이 처음부터 일치하지 않으면 계속 일치하지 않으며 빨간색을 출력한다. 즉 처음부터 운좋게 맞는 데이터는 초록색 출력이고 맞지 않는 데이터는 빨간색 출력이다. 코드를 재실행할 때마다 초기화가 달라지므로 빨간색 영역도 변한다.

draw 루틴에서 brain.train()을 실행한 상태에서는 학습에 따른 웨이트 업데이트가 원활히 이루어지므로 모조리 초록색이 출력된다. 다음의 그래픽 처리 결과는 점 데이터 수가 400개이 지만 N = 2인 경우이다. 점 데이터 수와 Rosenblatt 의 N은 뜻하는 바가 다름에 유의하자.

noname08.png

점 데이터 1개를 생성하여 다음과 같이 print 명령을 넣고 웨이트 값을 출력하여 보았다.
대부분 1회 학습 이후 웨이트 값이 변화가 없으므로 학습이 완료된 듯하다. 한편 웨이트 값이 모조리 0이 나오는 경우는 처음부터 target 과 guess 계산 결과가 일치하는 경우로서 error 값이 0이므로 웨이트 값 변동이 없다.

noname09.png

다소 지루할 정도로 긴 코드를 다루는 목적은 Rosenblatt의 알고리듬에서 다소 의아하게 느껴졌던 웨이트 업데이트를 통한 학습과정 계산이 제대로 라벨 값에 수렴하느냐는 의문을 해소하기 위한 것이다. 실제로 해본 경과 깔끔하게 1번 계산에 수렴하는 값을 주었다.

noname10.png

이 블로그에 해설한 코드는 유튜브에 올라온 Coding Train의 털보 강사가 진행하는 10.2 Neural Networks: Perceptron Part1 The Nature of Code 동영상을 참조 해설하였다. 아래의 코드는 동영상에서 나온 내용을 기록하였으면 실행해서 검증을 마쳤다.
생성되는 점의 수를 늘리고 싶으면 setup() 위 헤더 영역의 Point[] points = new Point[1]에서 1을 원하는 점의 수로 바꾸고 앞서 지적했던 불필요한 print문을 지우도록 한다. 아루러 trainning 탭에 들어 있는 코드는 메인코드와 동일한 폴더속에 저장해야 한다.

//TwoD_perceptron_01
//The activation function
int sign(float n) {
if (n >= 0) {
return 1;
}
else {
return -1;
}
}

class Perceptron {
float[] weights = new float[2];
float lr = 0.01;

//Constructor
Perceptron() {
//initiallize the weights randomly
print("number of weights =");
println(weights.length);
for ( int i = 0; i < weights.length; i++) {
//weights[i] = random(-1, 1);
weights[i] = 0.0;
println(weights[i]);
}
}

int guess(float[] inputs) {
float sum = 0;
for ( int i = 0; i < weights.length; i++) {
sum += inputs[i]*weights[i];
}
int output = sign(sum);
return output;
}

void train(float[] inputs, int target) {
int guess = guess(inputs);
int error = target -guess;

for (int i =0; i< weights.length; i++)  {
  weights[i] += lr * error * inputs[i];
 // println(weights[i]);
}
  print("weight[0]= ");
  print(weights[0]);
  print("  weight[1]= ");
  println(weights[1]); 

}
}

Perceptron brain;
Point[] points = new Point[1];
int count = 1;

void setup() {
size(400,400);
brain = new Perceptron();

for (int i = 0; i < points.length;i++) {
points[i] = new Point();
}

// float[] inputs = {-1, 0.5};
// int guess = brain.guess(inputs);
// println(guess);
}

void draw() {
background(255);
stroke(0);
line(0,0,width,height);
for (Point pt : points) { // all points from Point
pt.show();
}

for (Point pt : points) {
float[] inputs ={pt.x, pt.y};//
int target = pt.label;

brain.train(inputs, target);

int guess =brain.guess(inputs);
if (guess == target)  {
  fill(0, 255,0);
  print("iteration=");
  println(count);
}  else  {
  fill(255, 0, 0);
}
noStroke();
ellipse(pt.x,pt.y, 16, 16);

}
count++;
delay(1000);
}//End

//trainning
class Point {
float x;
float y;
int label;

Point() {
x = random(width);
y = random(height);

if (x > y)  {
  label = +1;
} else  {
  label = -1;
}

}

void show() {
stroke(0);
if ( label == 1) {
fill(255);
} else {
fill(0);
}
ellipse(x,y,18,18);
}
}//End

Sort:  

이오스 계정이 없다면 마나마인에서 만든 계정생성툴을 사용해보는건 어떨까요?
https://steemit.com/kr/@virus707/2uepul

Coin Marketplace

STEEM 0.26
TRX 0.13
JST 0.031
BTC 62133.38
ETH 2905.43
USDT 1.00
SBD 3.59