역전파를 사용하여 모델 학습하기


오차역전파를 사용한 학습도 손실함수를 최소화하는 가중치를 찾는 것을 목표로합니다. 다만, 역전파는 가중치를 구함에 있어 연쇄법칙에 기반한 국소적 미분을 활용합니다. 순전파와 비교했을 때 훨씬 빠른 시간 안에 효울적으로 계산한다는 장점이 있습니다. 이번에는 역전파법을 사용하여 모델 학습을 진행해 보겠습니다.

먼저, 라이브러리와 공통함수를 읽어옵니다.

1
2
3
4
5
6
#install.packages("dslabs")
library(dslabs)

source("./functions.R")
source("./utils.R")
source("./model.R")

1개의 은닉층을 갖는 네트워크를 생성합니다. 네트워크는 순전파와 동일합니다.

1
2
3
4
5
6
7
8
TwoLayerNet <- function(input_size, hidden_size, output_size, weight_init_std  =  0.01) {
W1 <- weight_init_std * matrix(rnorm(n = input_size*hidden_size), nrow = input_size, ncol = hidden_size)
b1 <- matrix(rep(0,hidden_size), nrow = 1, ncol = hidden_size)
W2 <- weight_init_std * matrix(rnorm(n = hidden_size*output_size), nrow = hidden_size, ncol = output_size)
b2 <- matrix(rep(0,output_size),nrow = 1, ncol = output_size)

return (list(W1 = W1, b1 = b1, W2 = W2, b2 = b2))
}

데이터를 불러와 트레이닝셋과 테스트셋으로 분리하는 init()함수를 생성합니다.

1
2
3
4
5
6
7
8
9
init <- function(){
mnist_data <- get_data()
#손글씨 데이터
x_train_normalize <<- mnist_data$x_train
x_test_normalize <<- mnist_data$x_test
#정답 레이블
t_train_onehotlabel <<- making_one_hot_label(mnist_data$t_train,60000, 10)
t_test_onehotlabel <<- making_one_hot_label(mnist_data$t_test,10000, 10)
}

앞서 역전파에서는 국소적 미분을 사용한다고 했습니다. 순전파와 반대방향으로 국소적 미분을 곱하여 이전 노드들에 값을 전달하는 것인데, 국소적 미분은 순전파 때의 미분을 구한다는 뜻입니다. 다시 말해, 순전파 때의 미분 값을 구해 다음 노드에 전달하는 함수가 필요합니다.
다음 코드는 순전파 때와 마찬가지로 입력신호와 가중치를 계산하고 Relu함수를 거쳐 다음 노드로 전달합니다.

1
2
3
4
5
6
forward <- function(x){
Affine_1 <- Affine.forward(network$W1, network$b1, x)
Relu_1 <- Relu.forward(Affine_1$out)
Affine_2 <- Affine.forward(network$W2, network$b2, Relu_1$out)
return(list(x = Affine_2$out, Affine_1.forward = Affine_1, Affine_2.forward = Affine_2, Relu_1.forward = Relu_1))
}

역전파도 마찬가지로 손실함수를 계산합니다.

1
2
3
4
5
6
loss <- function(model.forward, x, t){
temp <- model.forward(x)
y <- temp$x
last_layer.forward <- SoftmaxWithLoss.forward(y, t)
return(list(loss = last_layer.forward$loss, softmax = last_layer.forward, predict = temp))
}

순전파와 달리 마지막 노드에서부터 거꾸로 계산해 기울기를 구합니다.

1
2
3
4
5
6
7
8
9
10
11
12
gradient <- function(model.forward, x, t) {
# 순전파
temp <- loss(model.forward, x, t)
# 역전파
dout <- 1
last.backward <- SoftmaxWithLoss.backward(temp$softmax, dout)
Affine_2.backward <- Affine.backward(temp$predict$Affine_2.forward, dout = last.backward$dx)
Relu_1.backward <- Relu.backward(temp$predict$Relu_1.forward, dout = Affine_2.backward$dx)
Affine_1.backward <- Affine.backward(temp$predict$Affine_1.forward, dout = Relu_1.backward$dx)
grads <- list(W1 = Affine_1.backward$dW, b1 = Affine_1.backward$db, W2 = Affine_2.backward$dW, b2 = Affine_2.backward$db)
return(grads)
}

다음은 학습을 실제로 진행하는 코드입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
train_model <- function(batch_size, iters_num, learning_rate, debug=FALSE){
#seperate train, test data
init()
train_size <- dim(x_train_normalize)[1]

iter_per_epoch <- max(train_size / batch_size)
for(i in 1:iters_num){
batch_mask <- sample(train_size ,batch_size)
x_batch <- x_train_normalize[batch_mask,]
t_batch <- t_train_onehotlabel[batch_mask,]

grad <- gradient(model.forward=forward, x_batch, t_batch)
#update weights and biases using SGD
network <<- sgd.update(network,grad,lr=learning_rate)

if(debug == TRUE){
if(i %% iter_per_epoch == 0){
train_acc <- model.evaluate(forward, x_train_normalize, t_train_onehotlabel)
test_acc <- model.evaluate(forward, x_test_normalize, t_test_onehotlabel)
print(c(train_acc, test_acc))
}
}
}

train_accuracy = model.evaluate(forward, x_train_normalize, t_train_onehotlabel)
test_accuracy = model.evaluate(forward, x_test_normalize, t_test_onehotlabel)
return(c(train_accuracy, test_accuracy))
}

train_model()함수 중간에 sg.update()함수는 경사하강법으로 변경된 가중치를 업데이트하는 역할을 합니다.
코드는 아래와 같습니다.

1
2
3
4
sgd.update <- function(network, grads, lr = 0.01){
for(i in names(network)){network[[i]] <- network[[i]] - (grads[[i]]*lr)}
return(network)
}

이제 모든 준비를 마쳤습니다. 네트워크를 생성한 후 모델을 학습시켜봅니다.

1
2
network <<- TwoLayerNet(input_size = 784, hidden_size = 50, output_size = 10)
train_model(100, 10000, 0.1, TRUE)

위 코드를 실행시키고 3분 정도 지나면 아래와 같은 출력화면이 나올 것입니다. 한 행의 첫 번째 숫자는 훈련데이터 셋에 대한 정확도, 두 번째 숫자는 테스트 셋에 대한 정확도를 나타냅니다. 그리고 하나의 행은 1에폭(epoch)을 의미합니다. 에폭을 진행할수록 정확도가 높아지는 것을 확인할 수 있습니다!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[1] 0.9048 0.9059
[1] 0.9228 0.9247
[1] 0.9355833 0.9343000
[1] 0.9436167 0.9416000
[1] 0.9496167 0.9470000
[1] 0.9563167 0.9519000
[1] 0.9602167 0.9555000
[1] 0.9629167 0.9558000
[1] 0.9664833 0.9603000
[1] 0.9680333 0.9619000
[1] 0.9711167 0.9635000
[1] 0.97315 0.96520
[1] 0.97445 0.96570
[1] 0.9754167 0.9659000
[1] 0.9771167 0.9698000
[1] 0.9779 0.9679
[1] 0.9776833 0.9680000

순전파를 사용하여 모델 학습하기


순전파법을 사용하여 손글씨 추론 모델을 만들어보겠습니다. 순전파법의 기본 원리는 손실함수 값을 최소화 시키는 것입니다. 손실함수 값을 최소화 시키는 방법으로는 경사하강법(SGD)를 사용합니다. 순전파의 기본 설명은 다음 링크를 참고하세요.

먼저, 학습할 네트워크를 만듭니다. W1,W2는 각 층별 가중치이며 b1,b2는 편향 값을 의미합니다.

1
2
3
4
5
6
7
8
TwoLayerNet <- function(input_size, hidden_size, output_size, weight_init_std  =  0.01) {
W1 <- weight_init_std * matrix(rnorm(n = input_size*hidden_size), nrow = input_size, ncol = hidden_size)
b1 <- matrix(rep(0,hidden_size), nrow = 1, ncol = hidden_size)
W2 <- weight_init_std * matrix(rnorm(n = hidden_size*output_size), nrow = hidden_size, ncol = output_size)
b2 <- matrix(rep(0,output_size),nrow = 1, ncol = output_size)

return (list(W1 = W1, b1 = b1, W2 = W2, b2 = b2))
}

TwoLayerNet 네트워크는 아래와 같이 은닉층을 1개 갖습니다.

입력층에서 input_size 개수만큼의 노드를 갖고 은닉층에서는 hidden_size 개수만큼의 노드, 출력층에서는 output_size만큼의 노드를 갖습니다. W1b1은 입력층에서 은닉층으로 갈 때의 가중치와 편향이며 W2b2는 은닉층에서 출력층으로 갈 때 사용하는 가중치와 편향입니다. 그리고 weight_init_std는 가중치 초기값이 큰 값이 되는 것을 방지하는 파라미터입니다.

다음으로, 데이터를 불러오고 트레이닝 셋과 테스트 셋으로 분류합니다. 데이터는 MNIST 라이브러리의 손글씨 이미지입니다. R에서는 dslabs를 임포트합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
library(dslabs)
source("./functions.R")
source("./utils.R")
source("./model.R")

init <- function(){
mnist_data <- get_data()
#손글씨 데이터
x_train_normalize <<- mnist_data$x_train
x_test_normalize <<- mnist_data$x_test
#정답 레이블
t_train_onehotlabel <<- making_one_hot_label(mnist_data$t_train,60000, 10)
t_test_onehotlabel <<- making_one_hot_label(mnist_data$t_test,10000, 10)
}

손실함수는 교차엔트로피오차 함수를 사용합니다. 교차엔트로피오차 함수는 아래와 같이 구현합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
model.forward <- function(x){
z1 <- sigmoid(sweep((x %*% network$W1),2, network$b1,'+'))
return(softmax(sweep((z1 %*% network$W2),2, network$b2,'+')))
}

cross_entropy_error <- function(y, t){
delta <- 1e-7
batchsize <- dim(y)[1]
return(-sum(t * log(y + delta))/batchsize)
}

loss <-function(x,t){
return(cross_entropy_error(model.forward(x),t))
}

기본 교차엔트로피 함수식에 delta값을 추가하였는데, 이는 log0이 되면 -Inf가 되는 문제를 방지하기 위해서 입니다.

다음으로 경사하강법은 손실함수 값을 최소화 시키기 위해 사용합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
numerical_gradient_W <- function(f,x,t,weight){
h <- 1e-4
vec <- matrix(0, nrow = nrow(network[[weight]]) ,ncol = ncol(network[[weight]]))
for(i in 1:length(network[[weight]])){
origin <- network[[weight]][i]
network[[weight]][i] <<- (network[[weight]][i] + h)
fxh1 <- f(x, t)
network[[weight]][i] <<- (network[[weight]][i] - (2*h))
fxh2 <- f(x, t)
vec[i] <- (fxh1 - fxh2) / (2*h)
network[[weight]][i] <<- origin
}
return(vec)
}

numerical_gradient <- function(f,x,t) {
grads <- list(W1 = numerical_gradient_W(f,x,t,"W1"),
b1 = numerical_gradient_W(f,x,t,"b1"),
W2 = numerical_gradient_W(f,x,t,"W2"),
b2 = numerical_gradient_W(f,x,t,"b2"))
return(grads)
}

마지막으로 학습시키는 함수입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
train_model <- function(batch_size, iters_num, learning_rate, debug=FALSE){
#seperate train, test data
init()
train_size <- dim(x_train_normalize)[1]

iter_per_epoch <- max(train_size / batch_size)
for(i in 1:iters_num){
batch_mask <- sample(train_size,batch_size)
x_batch <- x_train_normalize[batch_mask,]
t_batch <- t_train_onehotlabel[batch_mask,]

grad <- numerical_gradient(loss, x_batch, t_batch)
network <<- sgd.update(network,grad,lr=learning_rate)

if(debug){
if(i %% iter_per_epoch == 0){
train_acc <- model.evaluate(model.forward, x_train_normalize, t_train_onehotlabel)
test_acc <- model.evaluate(model.forward, x_test_normalize, t_test_onehotlabel)
print(c(train_acc, test_acc))
}
}

train_accuracy = model.evaluate(model.forward, x_train_normalize, t_train_onehotlabel)
test_accuracy = model.evaluate(model.forward, x_test_normalize, t_test_onehotlabel)
return(c(train_accuracy, test_accuracy))
}
}

train_model()함수 중간에 sg.update()함수는 경사하강법으로 변경된 가중치를 업데이트하는 역할을 합니다.
코드는 아래와 같습니다.

1
2
3
4
sgd.update <- function(network, grads, lr = 0.01){
for(i in names(network)){network[[i]] <- network[[i]] - (grads[[i]]*lr)}
return(network)
}

이제 모든 준비를 마쳤습니다. 네트워크를 생성한 후 모델을 학습시켜봅니다.

1
2
network <<- TwoLayerNet(input_size = 784, hidden_size = 50, output_size = 10)
train_model(100, 10000, 0.1, TRUE)

딥러닝에 쓰이는 함수를 R과 Python으로 구현하기


딥러닝 책 『밑바닥부터 시작하는 딥러닝』 을 공부하면서, 책에 있는 Python코드를 R로 구현하는 프로젝트를 진행하고 있습니다.

Python으로 구현한 딥러닝 함수 코드와 R로 구현한 함수 코드를 동시에 살펴 보고자 합니다.

1. 시그모이드(Sigmoid) 함수

시그모이드 함수는 활성화 함수역할을 하는 대표 함수입니다.
함수 식은 다음과 같습니다.

파이썬의 경우, Numpy의 지수함수인 exp()를 사용해서 구현합니다.

1
2
def sigmoid(x):
return 1/ (1+np.exp(-x))
Read More