딥러닝(Deep learning)을 R로 구현하기 – Prediction Model

일반에 딥러닝으로 알려져 있는 머신러닝 모델의 가장 기본이 바로 신경망(Neural Network) 모델이다. 다른 글에서 신경망 모델로 Sin(X) 그래프를 추적하는 예제를 하나 풀어봤었는데, 기본적으로 아래처럼 네트워크가 100% 꽉 들어차 있는 Fully Connected Neural Network 모델이 기본형이다. (Deep Neural Network, 일반에는 딥러닝으로 알려져 있다.) 여기서 몇 개의 노드(node)가 사라지는 경우, 또 넣었다 뺐다 하는 경우(Dropout 이라고 한다) 등 다양한 경우로 응용을 할 수 있는데, 이렇게 옵션을 추가하는 이유는 신경망 모델의 노드가 늘어나면 늘어날수록 계산 비용(CPU, RAM, 전력, 시간 등)이 기하급수적으로 늘어나기 때문이다. (and over-fitting을 방지하기 위해서이기도 하다.)

딥러닝이라고 알려지게 된 가장 큰 이유는 신경망 모델이 단층적인 모델에서 다층적인 모델로 변화했기 때문이다. 위에서 말한대로 원래 신경망 모델에 계산 비용이 많이 들어가고, 또 다층적인 모델이되면서 계산 비용이 더더욱 많이 들어가기 때문에 예전에는 슈퍼컴퓨터를 이용하지 않으면 일반인이 쉽게 결과물을 접하기 힘들었었다.

위의 예시를 보면 단층 모델(Sinlge layer)이 다층 모델(Multi layer)보다 빨리 결과물을 볼 수 있는 대신 정확도가 떨어질 가능성이 높고, 같은 맥락으로 변수 4개 들어간 들어간 다층 모델보다 2개만 들어간 다층 모델이 계산 비용이 적으면서 정확도를 손해보게 된다. 슈퍼컴퓨터에만 계산을 의존하던 시절에는 많은 변수가 들어간 다층 모델을 고르는데 매우 조심스러웠지만, 최근에 CPU (중앙처리장치)의 멀티코어를 이용하고 심지어는 GPU (그래픽 카드)의 행렬(Matrix) 연산 능력까지 활용할 수 있게 되면서 일반적인 PC 여러대를 뭉치면 괜찮은 성과를 얻을 수 있게 되었다. 알려진 내용에 따르면 구글 딥마인드의 바둑 인공지능 알파고가 이세돌 9단과 경기할 때 1202개의 CPU와 176개의 GPU를 연산에 활용했다고 한다.

정말 일반 PC로 구현할 수 있다면 집에 있는 데스크탑이나 갖고 다니는 노트북으로 구현할 수 있을까? 물론이다.

이런 수요가 많기 때문에 소프트웨어 엔지니어 (이하 “개발자”)들이 많이 쓰는 개발 언어인 Python으로 유명한 패키지(TensorFlow, scikit-learn 등)가 나와있고, 통계학자들이 많이 활용하는 R에도 아래와 같은 패키지들이 자주 쓰인다. 특히 아래 리스트의 마지막에 있는 mxnet은 Amazon이 딥러닝에 적용하는 패키지로 알려져 있고, 컨펌되지 않은 평가에 따르면 구글에서 열심히 홍보하고 있는 TensorFlow보다 성능은 더 뛰어나다고 한다. 둘의 차이점에 대해서 더 궁금하신 분은 여기를 참고하시면 된다.

딥러닝을 R로 구현하는 패키지

R Packages Backend Computing Resources
nnet C/C++ Single thread
neuralnet C/C++ Single thread
DARCH C/C++ Single thread
deepnet R Single thread
H2O JAVA Multi-threads, multi-nodes
mxnet C/C++/CUDA Multi-threads, GPUs, multi-nodes

이 중에서 CPU의 싱글 코어만 쓰는 nnet다른 글에서 Sin(X) 그래프 추적하는데 적용해봤다. (궁금하시면 링크타고가서 확인해보시면 됩니다.)

명령어들 자체의 성능이나 활용법은 약간씩 다른데, 우선 이번 글에서는 Deep Neural Network를 공개된 패키지를 쓰지않고 직접 한번 만들어볼 생각이다.

굳이 이걸 Scratch부터 만들어보는 이유는 딥러닝에 대한 일반의 오해를 종식시키고 싶은 마음에서다. 인공지능이 무슨 알아서 척척척 박사라서 모든 걸 다해낸다고 착각들을 많이하고, 특히 개발자분들이 TensorFlow로 DNN이니 CNN이니 같은 약자만 쓰면서 정작 이 모델들을 설명해라고 하면 “블랙박스(BlackBox)”라는 표현을 쓰는게 안타까워서다. 그리고 이렇게 기본부터 이해를해야 저 위에서 잠깐 언급한대로 어떤 노드(Node)는 없애거나 넣었다 뺐다를 반복하면서 계산비용을 줄이는 아이디어를 쉽게 이해할 수 있을 것 같아서이기도 하다.

 

1. 간략한 용어 설명

  • 입력값(Input Layer): 초기 변수
  • 결과값(Output): 결과값이 0과 1 중 하나로 나올 경우를 Classification, 그 이외의 경우는 Regression 문제로 정의한다. 쓰이는 테크닉들이 다르다.
  • 편차(Bias): 각 Layer에서 처리하지 못하는 오차값
  • 가중치(Weight): 입력값이 뉴런에게 주는 가중치
  • 뉴런(Neuron): 위의 그림에서 초록색 원들을 말한다. (다른 교과서에서는 Perceptron이라고 표현하기도 한다.)
  • Hidden Layer(s): 변수들을 처리하는 가상의 계산 단계. 층위의 숫자가 많을수록 모델의 정확도는 올라가겠지만, 계산 비용이 많이 든다

 

2. 모델이란?

보통 어떤 모델을 만들고 나면 외부에 설명할때는 활용도에 중심을 두고 설명하겠지만, 모델링을 하는 사람들 입장에서 듣고 싶은 주제는,

  1. 어떤 변수들을 입력값으로 넣었고,
  2. Hidden Layer를 어떻게 구성했으며 (M x N),
  3. 시스템의 Loss Function은 무엇이었으며,
  4. Regularization은 어떻게 활용했는지 등

이다.

딥러닝이라고 불리는 모델이 장점이자 단점인 부분은, 모델 그 자체로는 수학적인 뒷받침이 있기보다는 단순히 변수들끼리 모든 조합의 가능성을 다 고려해서 가장 높은 결과값을 찾는 단순한 연산의 반복일 뿐이라서 모델링 그 자체만 놓고보면 복잡성의 깊이는 낮다. (변수들을 넣었다 뺐다 하는 식의 업그레이드 모델은 일단 예외로 한다.) 그렇기 때문에 더더욱 Over-fitting 이슈에서 자유로울 수 없는 모델이기는 하지만, 이 문제는 다른 글에서 논의하기로 하자

뭔가 복잡한 계산이 있을 것 같지만, 실제로는 단순한 행렬 계산으로 값을 구한다. 예를 들면 2×3 행렬과 3×2 행렬을 곱하면 2×2 행렬이 나온다. 고등학교 2학년 수학이다. 실제로 이용되는 수학적 사고의 틀은 이걸로 충분하다.

아래에 간단하게 가중치와 편차(추정식의 Level-높낮이-을 지정하는 값) 행렬을 만들어봤다.

 
weight.i <- 0.01*matrix(rnorm(layer.size(i)*layer.size(i+1)),
 			nrow = layer.size(i),
			ncol = layer.size(I+1))
bias.i    <- matrix(0, nrow=1, ncol = layer.size(i+1))

보통은 가중치와 편차를 하나의 행렬로 묶는다.

weight   <- 0.01*matrix(rnorm((layer.size(i)+1)*layer.size(i+1)),
 			nrow = layer.size(i)+1,
			ncol = layer.size(i+1))

딥러닝이라고 불리는 계산법, 혹은 Deep Neural Network 모델이 결정해야하는 값은 위의 Weight 값이 전부다. Input 데이터를 Weight 값으로 계산해주면 Output 값이 나온다. 그 Output이 누군가가 이 상품을 살지 말지에 대한 확률일수도 있고, 이세돌이 어느 점에 착점할 것이라는 예측값일 수도 있다. 그 모든 값은 바로 위의 Weight 값을 얼마나 잘 짜느냐에 달려있다.

 

3. 계산

자 이제 행렬 계산법을 응용해서 계산을 해 보자.

# 2X2 차원의 입력값 (Input, 혹은 Feature라고 불린다)
input <- matrix(1:4, nrow = 2, ncol = 2)

# 2x3 차원의 가중치
weights <- matrix(1:6, nrow = 2, ncol =3)
# 1x3 차원의 오차항
bias <- matrix(1:3, nrow = 1, ncol = 3)

# 입력값 x 가중치 + 오차항
input %*% weights + bias

# 당연히 차원이 맞지 않아 계산이 안 된다

입력값이 1×2 행렬이었으면 입력값 x 가중치가 1×3 행렬이 되어서 덧셈이 정상적으로 진행되었겠지만, 위의 경우에는 아마 에러값이 튀어나올 것이다. 이럴 때 머신러닝에서 이용하는 해결책은 크게 두 가지다. 첫번 째는 그냥 단순하게 오차항을 두 번 쓰는 것이다. 물론 이렇게하면 메모리를 많이 소모하므로 (Computational cost가 비싸다), Sweep 이라는 함수를 이용한다. 두 방법의 결과는 같다.

 
# 해결책 1: 오차항을 두번 반복해서 2X3을 만듦
s1 <- input %*% weights + matrix(rep(bias, each=2), ncol=3) 
 
# 해결책 2: sweep 함수 이용
s2 <- sweep(input %*% weights, 2, bias, '+')
 
all.equal(s1, s2)
[1] TRUE

 

4. Prediction 에 적용

아주 간단하게 Prediction에 적용하는 함수를 만들어 보자. 참고로 아래에 쓰인 pmax 함수는 Global Max 값이 아니라 Local Max 값을 찾는데 쓰이는 함수다. Neural Network를 비롯한 모든 머신러닝 함수는 수학식으로 나온 값을 찾는게 아니라 (Closed-form solution이라고 부른다), 시행착오(Trial-and-error)를 거치면서 값을 찾는데, 함수가 한 방향으로 증가, 감소하는게 아니라면 시작점이 어디냐에 따라 다른 값을 찾게 되는 경우가 있다.

말을 바꾸면, 시작점이 어디냐에 따라 결과값이 바뀌는 경우가 생길 수 있는데, Global Max 값을 찾는데 계산비용이 많이 들기 때문에 아래처럼 Local Max 값을 찾는 함수를 쓰는 경우가 많다.

# Prediction
predict.dnn <- function(model, data = X.test) {

  # 데이터를 행렬로 변형
  new.data <- data.matrix(data)
 
  # sweep을 이용해 hidden layer의 차원을 맞춰줌
  hidden.layer <- sweep(new.data %*% model$W1 ,2, model$b1, '+')

  # hidden.layer 값 중 최대치를 골라서 score -예측값-를 찾는데 적용
  hidden.layer <- pmax(hidden.layer, 0)
  score <- sweep(hidden.layer %*% model$W2, 2, model$b2, '+')
 
  # Loss Function: softmax (cross-entropy 라고도 알려져 있다)
  score.exp <- exp(score)
  probs <- sweep(score.exp, 1, rowSums(score.exp), '/') 
 
  # 가장 확률이 높은 경우를 고른다
  labels.predicted <- max.col(probs)
  return(labels.predicted)
}

 

5. Neural Network를 훈련(Training) 시키는 함수 생성

데이터를 훈련(Training)에 쓰는 방식을 결정하는 함수를 만들어보자. 여기서는 Hidden layer를 몇 개로 할지, Learning rate을 얼마로 잡을지 등등을 지정할 수 있다.

train.dnn <- function(x, y, traindata=data, testdata=NULL,
                  # hidden layer과 변수의 갯수를 고름 (아래는 1x6으로 지정)
                  hidden = c(6), 

                  # 최대 반복 수치 지정 (클수록 속도를 희생하고 정확성을 끌어올린다)
                  maxit=2000,

                  # delta loss (반복할 때마다 약간씩 수정하는 값의 크기)
                  abstol=1e-2,

                  # learning rate (학습 속도, 작을수록 느리지만 대신 정확하다)
                  lr = 1e-2,

                  # regularization rate (추가적인 feature를 도입하는 속도, 각 feature간 cross-product, 제곱 등이 쓰인다)
                  reg = 1e-3,

                  # 총 100번의 결과값을 보여준다
                  display = 100,
                  random.seed = 1)
{
  # seed를 지정하면 같은 seed 값에서 같은 결과값을 볼 수 있다
  set.seed(random.seed)
 
  # 훈련 집합의 전체 크기
  N <- nrow(traindata)
 
  # 데이터 불러오기
  X <- unname(data.matrix(traindata[,x]))
  Y <- traindata[,y]
  if(is.factor(Y)) { Y <- as.integer(Y) }

  # 행과 열에 각각 인덱스 생성 
  Y.len   <- length(unique(Y))
  Y.set   <- sort(unique(Y))
  Y.index <- cbind(1:N, match(Y, Y.set))
 
  # input 값들
  D <- ncol(X)

  # classification이 적용되는 값의 종류 (ex. 0과 1)
  K <- length(unique(Y)) # unique(Y) 값이 100개라면 총 100개의 가능성으로 결과값을 보여준다 (사과, 배, 참외 등등등)
  H <-  hidden
 
  # 최초 시작점을 정한다 - local max를 찾을 때 최초 시작점을 어떻게 정하느냐가 매우 중요하다. 
  W1 <- 0.01*matrix(rnorm(D*H), nrow = D, ncol = H)
  b1 <- matrix(0, nrow = 1, ncol = H)
 
  W2 <- 0.01*matrix(rnorm(H*K), nrow = H, ncol = K)
  b2 <- matrix(0, nrow = 1, ncol = K)
 
  # Training 데이터 전체를 Training에 활용 (N을 그룹으로 쪼개서 bagging을 할 수도 있음)
  batchsize <- N
  loss <- 100000
 
  # Training 데이터를 NN에 적용
  i <- 0
  while(i < maxit && loss > abstol ) {
 
    # iteration index
    i <- i +1
 
    # forward 로 계산 (아래에 backward로 계산도 있음)
    # 1은 행, 2는 열
    hidden.layer <- sweep(X %*% W1 ,2, b1, '+')

    # neurons : ReLU
    hidden.layer <- pmax(hidden.layer, 0)
    score <- sweep(hidden.layer %*% W2, 2, b2, '+')
 
    # softmax (Loss function을 지정. Cross entropy라고도 불린다)
    score.exp <- exp(score)
    probs <- sweep(score.exp, 1, rowSums(score.exp), '/') 
 
    # Loss 값을 찾는다 - 매우 작은 값이 나와야 모델이 제대로 "학습"한 것이다
    corect.logprobs <- log(probs[Y.index])
    data.loss  <- sum(corect.logprobs)/batchsize
    reg.loss   <- 0.5*reg* (sum(W1*W1) + sum(W2*W2))
    loss <- data.loss + reg.loss
 
    # 결과값을 보여줌
    if( i %% display == 0) {
        if(!is.null(testdata)) {
            model <- list( D = D,
                           H = H,
                           K = K,
                           # 가중치와 오차
                           W1 = W1, 
                           b1 = b1, 
                           W2 = W2, 
                           b2 = b2)
            labs <- predict.dnn(model, testdata[,-y])
            accuracy <- mean(as.integer(testdata[,y]) == Y.set[labs])
            cat(i, loss, accuracy, "\n")
        } else {
            cat(i, loss, "\n")
        }
    }
 
    # backward 로 계산
    dscores <- probs
    dscores[Y.index] <- dscores[Y.index] -1
    dscores <- dscores / batchsize
 
 
    dW2 <- t(hidden.layer) %*% dscores 
    db2 <- colSums(dscores)
 
    dhidden <- dscores %*% t(W2)
    dhidden[hidden.layer <= 0] <- 0
 
    dW1 <- t(X) %*% dhidden
    db1 <- colSums(dhidden) 
 
    # update ....
    dW2 <- dW2 + reg*W2
    dW1 <- dW1  + reg*W1
 
    W1 <- W1 - lr * dW1
    b1 <- b1 - lr * db1
 
    W2 <- W2 - lr * dW2
    b2 <- b2 - lr * db2
 
  }
 
  # 최종 결과값

  model <- list( D = D,
                 H = H,
                 K = K,
                 # 가중치와 오차
                 W1= W1, 
                 b1= b1, 
                 W2= W2, 
                 b2= b2)
 
  return(model)
}

6. 데이터로 테스트 해보기

흔히들 쓰는 R의 기본 데이터인 iris 데이터로 모델 테스트를 진행해보자.

 
# 0. 데이터 불러오기
summary(iris)
plot(iris)
 
# 1. Test 셋과 Training 셋으로 데이터 분리
samp <- c(sample(1:50,25), sample(51:100,25), sample(101:150,25))
 
# 2. 모델 훈련 시키기
ir.model <- train.dnn(x=1:4, y=5, traindata = iris[samp,], testdata = iris[-samp,], hidden=6, maxit=2000, display=50)
 
# 3. 예측값 계산
labels.dnn <- predict.dnn(ir.model, iris[-samp, -5])
 
# 4. 결과 확인
table(iris[-samp,5], labels.dnn)
# labels.dnn
# 1 2 3
#setosa 25 0 0
#versicolor 0 24 1
#virginica 0 0 25
 
# 정확성 확인
mean(as.integer(iris[-samp, 5]) == labels.dnn)
# 0.98

위의 그래프에서 확인할 수 있듯이, 훈련이 반복될수록 loss값은 작아지고, 정확도는 높아진다. 약 1000번의 훈련이 반복되고 나면 정확성이 거의 100%에 수렴하는 것을 확인할 수 있다.

 

7. R에서 가장 많이 쓰이는 Neural Network 패키지 중 하나인 nnet와 결과값 비교

 
library(nnet)
ird <- data.frame(rbind(iris3[,,1], iris3[,,2], iris3[,,3]),
			species = factor(c(rep("s",50), rep("c",50), rep("v",50))))
ir.nn2 <- nnet(species ~ ., data = ird, subset = samp, size = 6, rang = 0.1, decay = 1e-2, maxit = 2000)
 
labels.nnet <- predict(ir.nn2, ird[-samp,], type="class")
table(ird$species[-samp], labels.nnet)
# labels.nnet
# c s v
#c 22 0 3
#s 0 25 0
#v 3 0 22
 
# accuracy
mean(ird$species[-samp] == labels.nnet)
# 0.96

필자가 직접 만든 Neural Network 계산식과 널리 쓰이고 있는 R 머신러닝 패키지 nnet의 결과값이 96%정도 일치하는 것으로 나왔다. 100%가 나왔으면 같은 방식으로 계산하는구나라고 생각하겠지만, 약간 차이가 나는 것을 보니 nnet 패키지에 뭔가 다른 계산법이 숨겨져 있는 것 같다.

 

8. 나가며

위의 계산이 대단히 복잡해보이거나 어렵다고 생각할수도 있지만, 사실은 고등학교 수학시간에 배운 행렬 계산의 반복에 불과하다. 쫄지말자.

얼마전에 친구 하나가 왜 그래픽 카드가 그렇게 불티나게 팔리는지, 그래픽 카드가 왜 머신러닝의 핵심인지 묻더라. 그래픽 카드는 계산을 빨리하는 도구가 아니라, 3차원 그래픽을 위해서 행렬을 계산하는 보조 계산 장치라고 생각하면 된다. 근데 i5니 i7이니 불리는 CPU보다 계산속도는 늦어도 메모리는 엄청나게 커서 3차원의 행렬 계산을 한꺼번에 잘 처리해준다. 위에서 봤듯이, 딥러닝의 핵심은 행렬 계산을, 그것도 엄청나게 큰 데이터에서 한꺼번에 처리하는 능력이다. 그래서 그래픽 카드가, 특히 성능 좋은 그래픽 카드가 갑자기 불티나게 팔려나가는 것이다.

(물론 비트코인을 비롯한 가상화폐 채굴 때문이기도 한데, 가상 화폐 채굴에 쓰이는 모듈도 대규모 데이터를 행렬로 계산하는 능력이 중요해서 그렇다.)

 

You may also like...

댓글 남기기

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다