신경망(Neural network) 예제 for 딥러닝 – 그래프 모양 맞추기

신경망 모델 예시 (Source: Wikipedia)

요즘 인터넷 기사들을 보면 머신러닝이라는 단어보다, 신경망(Neural network) 모델이라는 (좀 더 전문적인 스멜이 나는) 단어를 쓰기 시작했다. 그리고 이 단어가 “딥러닝”이라는 개념과 맞닿아있다는 걸 인지하고 있는 기자들도 많이 늘어난 것 같다. (뿌듯하다 ㅋㅋ)

그런데 정작 신경망 모델이 어떻게 돌아가는지 제대로 된 작동 원리를 전혀 모르는지 기자하는 친구들이 필자에게 가끔 물어보는 전화가 온다. 전화로 설명하기가 좀 힘들어서 Wikipedia에 그림하나를 보내줬는데, 그래도 이해가 잘 안 된단다. 신경망이나 딥러닝이 뭔지에 대해서 말들은 많이 하는데, 정작 이걸 어떻게 쓰는건지 아는 사람은 별로 없다.

왼쪽에 있는 그림이 신경망 모델의 예시다. 그래도 뭔 말인지 잘 모르겠다고?

일단 Input이 3개가 있고, Output이 2개가 있다. 일반적인 모델은 3개의 변수가 2개를 어떻게 설명하는지만 본다. 신경망 모델은 3개의 변수가 서로 어떻게 영향을 줬고, 그래서 뭔가 가공된 지식(그림에서는 Hidden으로 된 4개의 가상 변수)이 생기고, 그 가공된 지식이 2개 결과물이 어떤 영향을 주는지를 본다. 인간의 인지도 실제로 사물의 부분들을 조합해서 어떤 이미지를 만들고, 그 이미지가 기억 속에 있는 특정 사물과 매칭되는 것을 찾아서 어떤 사물을 봤다고 최종적으로 인식한다. (노트북의 스크린, 키보드, 터치 패드를 각각 보고 난 다음에, 저렇게 3개가 다 있는 건 PC도 아니고, 스마트폰도 아니고, 노트북이라고 인식하는거다)

보통 이걸 어떻게 쓰는지 설명할 때 사진에 있는 고양이를, 차를, 사람을 인식하는데 쓰는데 그런 뻔한 사례말고 좀 복잡한 그래프 인식하는 예시를 보자. (우리는 Machine learner가 아니라 Data scientist니까!)

 

1. 신경망 모델 예시 – Sin(X) 그래프

y= sin (x) 라는 그래프를 한번 생각해보자. 학부 시절도 아니고 새파란 고딩 때 한번쯤 배웠던 기억이 날 것이다. x가 0에서 2π 사이를 오가는 동안 y 값은 0에서 1까지 올라갔다가, -1까지 내려갔다가, 다시 0으로 돌아오는 주기(cycle) 함수다. 요렇게 생겼다.


일정하게 증가, 감소만 하는 함수는 찾기가 쉽다. 그런데 이런식으로 오르락, 내리락이 반복되면 찾기가 좀 어렵다. 특히 선형 모델로 이런 주기 함수의 matching function (매칭 함수)을 찾아내는 건 거의(가 아니라 완전히) 불가능 하다.

그런데 자연계의 많은 사건들이 한방향으로 증가, 감소만 하지는 않는다. 이래서 통계학과 학부 2학년 때 회귀분석에서 배운 지식만으로는 matching function을 찾는게 불가능한 것이다.

 
x<-runif(200, -10, 10)
y<-sin(x)

 

위의 그래프처럼 -10에서 +10까지 값을 200개 뽑아서, Sin(X) 값을 찾아준다.

 
index<-sample(1:length(x),round(0.75*length(x)),replace=FALSE)
reg.train<-data.frame(X=x[index],Y=y[index])
reg.test<-data.frame(X=x[-index],Y=y[-index])

더 깊게 들어가기 전에 솔직하게 말하면, 신경망 모델 (Neural Network)은 좀 구린 모델이다. 내가 갖고 있는 데이터에서는 완벽하게 맞아들어가는 함수식을 찾아내도, 정작 새로운 데이터에 적용하면 결과값이 엉망으로 나오는 경우다 매우 흔하다. (이걸 Over-fitting, 과적합 문제라고 한다.)

그래서 일부의 데이터를 샘플로 뽑아서 훈련 집합 (Training set)으로, 나머지를 검증 집합 (Test set)으로 나눈 다음, 신경망으로 찾아낸 모델이 새로운 데이터에도 얼마나 잘 맞는지 검증을 해 보는 작업이 필수적이다. (특히 위의 사례처럼 아래 위로 움직임이 많은 주기 함수의 경우, 이런 작업을 거치지 않은 함수식은 십중팔구 시간 낭비가 될 가능성이 높다.)

위의 코드를 간략하게 설명하면, length(x)는 x에 있는 데이터 개수(200개)를 가르쳐주고, 1:200은 모든 데이터 셋을 골랐다는 뜻이다. 각 데이터별로 번호표가 붙어있을테고 (1번부터 200번), 그 중에서 75%를 샘플로 뽑는데, 이미 뽑았던 숫자는 다시 안 뽑는다는 의미에서 replace = FALSE가 들어가 있다. (Lotto 번호 고를 때 같은 번호를 2번 고르진 않는다.)

이렇게 데이터 각각에 번호를 붙여서 index라는 변수로 만들고, reg.train이라는 변수에는 75%의 샘플을 집어넣고, reg.test라는 변수에는 나머지 25%를 넣는다. (-index는 index에 안 들어간 숫자들 전체를 말한다.)

 
library(nnet)
weight<-runif(10, -1, 1)
reg.model.1<-nnet(reg.train$X,reg.train$Y,size=3,maxit=50,Wts=weight,linout=TRUE)

 

자 이제 신경망 모델을 돌려보자. R이 좋은게, 다양한 (이라 쓰고 헤아릴 수 없이 많은) 패키지들이 존재하고, 덕분에 사용자가 새로 함수를 만들어야할 경우가 매우 적다. 여기서도 어떤 천재가 만들어놓은 신경망 모델 패키지 (nnet)를 불러와서 쓰자. 

3번째 라인은 nnet을 이용해서 훈련 집합 (Training Set, 우리의 경우는 reg.train이었다.) 의 변수 X와 Y를 넣는다. 그리고 size = 3 은 Hidden으로 된 가상 변수를 3개만 쓰겠다는 뜻이고, maxit는 iteration (반복작업, Trial-and-error를 몇 번 반복할지를 정한다)를 50번을 한다는 뜻이다. 여기서 iteration을 5000번 정도 하겠다고 설정하면 정확도는 매우 올라가겠지만, 아마 500번으로 한 것보다 시간은 몇 백배 더 걸리면서 정확도는 아주 쪼~끔 더 좋아질 것이다. 딱 정해진 숫자는 없고, 데이터마다 다른 숫자를 넣으면 되는데, 한 50번, 100번 올려가면서 얼마나 값 차이가 나는지 확인해보면 된다.

linout은 여러개 값이 가능한 경우를 모두 고려해라는 뜻인데, 하나 짚고 넘어가보자. weight는 각 변수에 얼마만큼의 가중치를 준 상태에서 모델을 테스트해볼지를 정하는 것이다. 글 처음에 나왔던 신경망 모델의 그림을 기준으로, 각각의 화살표 (3개 원에서 4개 원으로 가는 방향의 선들)에 어느만큼의 가중치를 줄 것인지인데, 이 가중치에 따라서 중간에 있는 가상 변수들이 최종 결과값에 미치는 가중치 값이 달라진다. 그리고, 그 가중치 값에 따라서 최종 결과값도 달라질 수 있다.

쉽게 말하면, 가중치를 얼마로 잡느냐에 따라 완전히 다른 결과값을 얻을 수도 있다. linout = TRUE는 그런 경우를 모두 고려하겠다는 뜻이다.

 
predict.model.1<-predict(reg.model.1,data.frame(X=reg.test$X))

 

자, 이제 R의 predict라는 함수를 활용해서, 위에 찾은 모델이 검증 집합(Test Set)에서 얼마나 좋은 결과를 보여주는지 살펴보자. predict라는 함수 자체가 잘 만들어져서 사실 복잡한 부분은 없다. 내 모델 (reg.model.1)을 지정하고, 데이터(data.frame(X=reg.test$X))를 지정해주면 된다. 여기서 data.frame은 숫자들을 데이터 프레임으로 만들어주는 함수고, reg.test$X는 reg.test라는 변수에서 X값들 (-10에서 +10사이 200개 값들 중 25%)을 지정해주는 거다.

 
rmse.reg<-sqrt(sum((reg.test$Y-predict.model.1)^2))

 

이제 Mean-Squared Error (MSE)라는 방법으로 (평균과 분산을 통합해서 예측값이 실제값이랑 얼마나 멀리 있는지 확인하는 방법) 위의 신경망 모델이 얼마나 잘 예측을 했는지 검증하면 된다.

 
plot(sin, -10, 10)
points(reg.test$X,predict.model.1)

 

plot of chunk NN_part1

실선은 위에서 보여준 Sin(X) 그래프고, 점선은 신경망 모델의 예측값이다. 보시다시피 0이상에서는 좀 맞는데, 0 미만에서는 완전히 틀렸다. (예상했던 바다.)

 
reg.model.2<-nnet(reg.train$X,reg.train$Y,size=7,maxit=50,Wts=runif(22, -1, 1),linout=TRUE)

똑같은 작업을 하는데 이번에는 모델을 살짝만 바꿔보자. 일단 Hidden에 나오는 가상 변수를 3개에서 7개로 늘린다. 다시말해서 3개의 특징만을 잡아내다가, 이제 7개의 특징을 잡아내겠다는거다. 당연하겠지만 좀 더 정확한 결과값이 나올 (가능성이 높을) 것이다. 그리고 Weight (가중치)를 좀 더 세분화했다.

 
predict.model.2<-predict(reg.model.2,data.frame(X=reg.test$X))
rmse.reg<-sqrt(sum((reg.test$Y-predict.model.2)^2))
plot(sin, -10, 10)
points(reg.test$X,predict.model.2)

 

다시 predict를 써보고, MSE 값을 구해서 예측이 얼마나 정확한지 살펴보자. 그리고 아래는 그래프다.

plot of chunk NN_part1

아까보다 훨씬 더 정확성이 높아졌다. 물론 아직도 완벽하지는 않다.

 

2. 신경망 모델 예시 – iris 데이터 셋

자 보통 R에서 데이터로 뭔가 작업을 하는 예시를 보여줄 때 제일 많이 쓰는 데이터가 iris 데이터다. 필자의 수업을 오면 필자가 만든 시뮬레이션 데이터를 이용할 수 있지만, 일단 여기서는 R의 기본 데이터 셋을 이용해보자. 

 
data<-iris
scale.data<-data.frame(lapply(data[,1:4], function(x) scale(x)))
scale.data$Species<-data$Species
index<-sample(1:nrow(scale.data),round(0.75*nrow(scale.data)),replace=FALSE)
clust.train<-scale.data[index,]
clust.test<-scale.data[-index,]

 

iris 데이터를 불러와서 data 변수에 입력한 다음, 저 위 섹션 1에서 했던 것과 같은 작업을 했다. lapply를 쓴 이유는 데이터의 1행에서 4행까지의 숫자들에 scale 함수를 지정한 값을 얻기 위함이다. 왜 scale을 쓰냐고? 변수의 스케일을 맞춰주기 위함이다. (0.001 ~ 0.01 사이를 움직이는 값과, 100~1000을 움직이는 값이 사실은 10배 구간을 움직인다는 점에서 같다.)

length(x) 대신에 nrow(scale.data)를 썼는데, nrow는 데이터의 행의 갯수 값을 주는것이라서 여기서는 사용 예시가 같다.

 
clust.model<-nnet(Species~.,size=10,Wts=runif(83, -1, 1),data=clust.train)
predict.model.clust<-predict(clust.model,clust.test[,1:4],type="class")
Table<-table(clust.test$Species ,predict.model.clust)
accuracy<-sum(diag(Table))/sum(Table)

 

자, 위의 섹션1에서 봤듯이, Hidden 가상 변수의 숫자가 늘어나면 모델이 정확해지고, weight도 세분화되면 좀 더 정확한 결과값이 나온다. 여기서는 가상 변수가 10개, weight의 구간이 무려 83개다. predict 함수로 예측값을 찾고, 그걸 Table로 정리하고, 마지막으로 정확도(accuracy)를 측정해본다. 
 
 

끝으로

여기서 데이터가 계속해서 흘러들어오고, 시간 단위로 모델을 업데이트 시키면 우리가 일반적으로 알고 있는 머신이 알아서 배우는 신경망 모델이 되고, Hidden 가상 변수를 복잡하게 셋팅하는걸 “딥러닝”이라고 바꿔서 부른다. (물론 “딥러닝”의 구체적인 정의는 학자마다 조금씩 다를 수 있다.)
이런 간단한 예제말고, 실제 데이터에 적용하는 사례를 보고 싶으면 필자의 수업에 찾아오면 된다 ㅋ
 

문제 소스코드 소스

X