ANN의 학습
인공신경망(Artificial Neural Network, ANN)은 데이터를 학습하고 패턴을 인식하며 새로운 데이터를 예측하는 혁신적인 알고리즘입니다. 앞선 글에서는 인공신경망의 역사와 개념을 살펴보며, 신경망이 어떻게 발전해 왔는지 이해했습니다.
이번 글에서는 ANN의 학습 과정을 자세히 탐구합니다. 임의의 가중치로 패턴의 예측값을 찾아내는 순전파(Forward Propagation)와 손실을 줄이기 위해 가중치를 업데이트하는 역전파(Backpropagation)의 원리를 설명하며, 이를 실제로 구현해볼 것입니다. 학생들의 학교 생활 데이터를 활용해 대입 합격 여부를 예측하는 신경망을 구축하면서, ANN의 작동 원리를 직접 확인할 수 있습니다.
이 글을 통해 ANN이 데이터를 학습하고 예측값을 도출하는 과정뿐만 아니라, 학습의 각 단계에서 발생하는 문제를 이해하고 이를 해결하는 방법까지 배우게 될 것입니다. 또한, 마지막에는 TensorFlow를 활용해 인공신경망 모델을 보다 간단하고 효율적으로 구축하는 방법도 소개합니다. ANN의 실질적인 활용과 구현에 대한 통찰을 얻어갈 수 있는 시간이 되길 바랍니다.
학생의 대입 합격 여부 데이터셋
이번 글에서는 인공신경망의 학습 과정을 설명하면서, ‘학생의 학교생활 데이터를 입력받아 대입 합격 여부를 예측하는 인공신경망’을 함께 구현해 보겠습니다.
import numpy as np
import pandas as pd
data = {
"absences": [0, 3, 1, 1, 0, 0, 1, 12, 2, 3], # 결석 일수
"night_study": [0, 1, 1, 0, 1, 1, 0, 0, 1, 1], # 야간자율학습 참여 여부
"club_activity": [0, 1, 1, 0, 1, 0, 0, 1, 1, 1], # 전공 관련 동아리 활동 여부
"essay_length": [532, 427, 735, 635, 142, 395, 439, 519, 498, 425], # 자소서 글자 수
"exam_score": [98.5, 89.0, 72.6, 87.3, 85.0, 96.2, 92.8, 97.2, 71, 86.2], #시험 점수
"admission": [1, 1, 0, 1, 0, 1, 1, 0, 1, 0] # 대입 합격 여부
}
# 데이터프레임으로 변환
df= pd.DataFrame(data)
absences | night_study | club_activity | essay_length | exam_score | admission | |
---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 532 | 98.5 | 1 |
1 | 3 | 1 | 1 | 427 | 89 | 1 |
2 | 1 | 1 | 1 | 735 | 72.6 | 0 |
3 | 1 | 0 | 0 | 635 | 87.3 | 1 |
4 | 0 | 1 | 1 | 142 | 85 | 0 |
5 | 0 | 1 | 0 | 395 | 96.2 | 1 |
6 | 1 | 0 | 0 | 439 | 92.8 | 1 |
7 | 12 | 0 | 1 | 519 | 97.2 | 0 |
8 | 2 | 1 | 1 | 498 | 71 | 1 |
9 | 3 | 1 | 1 | 425 | 86.2 | 0 |
사용할 데이터 셋은 아래와 같은 입력 데이터를 포함하고 있습니다:
-
absences
: 학교 결석일 수 -
night_study
: 야간자율학습 참여 여부 (0: 미참여, 1: 참여) -
club_activity
: 전공 관련 동아리 활동 여부 (0: 미활동, 1: 활동) -
essay_length
: 자기소개서 글자 수 -
exam_score
: 대입 시험 평균 점수
마지막 항목인 admission은 대입 합격 여부로, 이 인공신경망이 예측해야 하는 목푯값(Target)입니다.
이 데이터에서 대학 합격 여부를 0 또는 1로 판단해야 하므로, 이 문제는 이진 분류(Binary Classification) 문제로 분류됩니다.
인공신경망(ANN)의 학습 목표
인공 신경망(Artificial Neural Network, 이하 ANN)의 핵심 목표는 데이터에서 패턴을 학습하여 이를 하나의 선이나 면으로 표현하는 것입니다. 궁극적으로는 학습한 패턴을 활용해 새로운 데이터에 대해 정확한 예측을 수행해야 하죠. 이를 위해서는 주어진 학습용 데이터를 가지고 최적의 패턴을 학습해 내야 합니다. 여기서 패턴을 학습한다는 것은 최적의 가중치와 편향을 찾아내는 과정을 의미합니다.
ANN이 패턴을 학습하면, 학습한 패턴을 토대로 입력값에 대한 예측값을 도출할 수 있습니다. 이 예측값과 실제 정답(학습 데이터에서 입력값에 대응하는 출력값) 사이의 오차를 줄이면, 패턴이 더 정교해집니다. 오차는 은닉층 각 노드의 가중치와 편향 값을 조절하여 줄일 수 있습니다. 예를 들어, ‘아까는 $x_1$에 무게를 더 뒀다면, 이번엔 $x_2$에 무게를 더 둬 볼까?’라는 생각을 두 입력 데이터의 가중치와 편향 값을 조절함으로써 구현하는 거죠.
이렇게 예측값과 정답 간의 오차를 줄이는 것은 예측의 손실(Loss)을 최소화하는 것을 의미하며, 손실을 최소화하는 학습을 반복함으로써 ANN의 성능을 개선할 수 있습니다.
우리가 구현할 대입 합격 예측 인공신경망은 5가지 입력 데이터를 기반으로 정확한 대입 합격 여부를 예측할 수 있도록 학습되어야 합니다.
순전파(Forward Propagation): 임의의 가중치로 예측값 계산하기
순전파
손실을 줄이기 위해서는 정답과 비교할 예측값이 필요합니다. 이 예측값을 구하기 위해 앞서 알아봤던 신경망의 입력층, 은닉층, 출력층을 따라 입력 데이터를 ANN에 입력해 예측값을 도출합니다. 이 과정을 순전파(Forward Propagation)라고 합니다.
간단한 인공신경망
예시로 가져온 위 그림의 인공신경망은 은닉층이 1개인 매우 단순한 구조입니다. 입력층으로 전달되는 입력 데이터 $x$는 두 개의 피처 $x_1$과 $x_2$를 가지고 있습니다. 은닉층의 유일한 노드는 이 입력 데이터를 전달받아 가중치 $w_1$, $w_2$와 편향 $b$를 계산한 후, 계산 결과 $z$를 활성화 함수인 시그모이드 함수에 입력해 노드의 결괏값 $a$를 도출합니다. 은닉층에는 노드가 하나만 있으므로, 이 노드의 출력값 $a$는 출력층으로 전달되고 그대로 인공신경망 전체의 출력값인 예측값 $\hat{y}$로 반환됩니다.
여기서 중요한 점은 임의의 가중치와 편향을 가지고 값을 예측했다는 것입니다. 따라서 이후에는 ‘학습’을 통해 가중치와 편향을 최적의 값으로 조정하고, 신경망의 예측값 $\hat{y}$과 정답 $y$ 사이의 오차를 줄여 신경망의 예측 성능을 개선해야 합니다.
잠깐, 모든 입력 데이터에 대해 이 과정을 수행해야할텐데? → 벡터화
학습 데이터는 여러 개의 입력데이터와 정답 쌍으로 구성되어 있습니다. 그렇다면 데이터 하나하나에 대해 위 과정을 반복해야 할까요? 코드로 구현한다면, 단순히 for 문을 사용하면 될까요?
Andrew Ng 교수님의 유명 강의에서도 언급되었듯이, 단순한 연산을 여러 번 반복해야 할 때에는 for 문보다 더 효율적인 방법이 있습니다. 바로, 여러 데이터를 벡터화(Vectorization)하여 동시에 병렬 연산을 수행하는 것입니다.
import numpy as np
import time
a = np.random.randn(1000000)
b = np.random.randn(1000000)
# 벡터화한 뒤 연산 수행시간 측정
start = time.time()
c = np.dot(a, b)
end = time.time()
print("벡터화한 뒤 연산: " + str(1000*(end - start)) + "ms")
# 반복문으로 연산 수행시간 측정
c = 0
start = time.time()
for i in range(1000000):
c += a[i]*b[i]
end = time.time()
print("반복문으로 연산: " + str(1000*(end - start)) + "ms")
벡터화 결과 비교
위 과정을 실행할 때마다 수행 시간이 조금씩 달라질 수 있지만, for 문을 활용한 연산이 벡터화를 통한 연산에 비해 몇십 배에서 몇백 배 더 오래 걸린다는 사실은 변함이 없습니다.
대입 합격 예측 인공신경망의 첫 번째 순전파
앞에서 배운 순전파 과정을 활용해 대입 합격 여부를 예측하는 인공신경망을 직접 구축해 보겠습니다.
먼저 데이터를 정리하겠습니다. 데이터 셋에서 독립변수 X
(입력 데이터)와 종속변수 y
(예측값이 가까워져야 할 정답)를 분리한 뒤, 입력 데이터를 정규화했습니다.
X
의 타입을 출력해 보니 numpy 배열임을 알 수 있습니다. 이는 여러 입력 데이터를 동시에 처리하기 위해 numpy 배열로 벡터화한 것입니다. X
의 크기는 10x5
로, 데이터 샘플 수(10)가 행, 입력 데이터의 종류 수(5)가 열로 나타난 것을 알 수 있습니다.
# 독립변수(X, 입력 데이터)와 종속변수(y, 정답) 분리
X = df[["absences", "night_study", "club_activity", "essay_length", "exam_score"]].values
y = df["admission"].values.reshape(-1, 1)
# 데이터 정규화: 평균이 0, 표준편차가 1인 값으로 입력값의 스케일 조정
X = (X - X.mean(axis=0)) / X.std(axis=0)
print(type(X))
print(X.shape)
독립변수 출력 결과
이번 인공 신경망에서는 두 가지 활성화 함수를 사용했습니다. 시그모이드 함수와 함께 ReLU 함수도 사용했는데, 이 두 함수의 차이점에 대해서는 뒤에서 자세히 설명하겠습니다.
이 인공신경망은 입력층에 5개의 뉴런을 가지고 있습니다. 입력 데이터가 5개의 피처를 가지고 있기 때문입니다. 입력층 다음 순서로는 은닉층을 하나 거치게 되는데, 은닉층에는 임의로 3개의 뉴런을 설정했습니다. 출력층은 하나의 예측값(합격 여부 혹은 합격 확률)을 계산하므로 뉴런 1개가 필요합니다.
초기 가중치와 편향은 확실한 학습 결과를 보여주기 위해 랜덤 값으로 설정했습니다.
# 활성화 함수 정의
def relu(x):
return np.maximum(0, x)
def sigmoid(x):
return 1 / (1 + np.exp(-x))
# 초기 가중치와 편향 랜덤 초기화
np.random.seed(42) # 결과 재현성을 위한 랜덤 시드 설정
# 입력층 -> 은닉층
weights_input_hidden = np.random.randn(5, 3) # (입력층 뉴런 5개 -> 은닉층 뉴런 3개)
bias_hidden = np.random.randn(1, 3) # 은닉층 편향
# 은닉층 -> 출력층
weights_hidden_output = np.random.randn(3, 1) # (은닉층 뉴런 3개 -> 출력 뉴런 1개)
bias_output = np.random.randn(1, 1) # 출력층 편향
이제 순전파 과정을 함수로 정의하고 실행해 보겠습니다.
forward_propagation()
함수는 순전파 과정을 구현하며, 두 행렬(층에 입력된 값 행렬과 층의 가중치 행렬)을 곱하기 위해 numpy.dot()
메소드를 사용합니다. 그리고 가중치와 편향 값을 함수의 파라미터로 전달해, 이후에 업데이트된 가중치와 편향에 대해 재사용할 수 있도록 설계했습니다.
# 순전파 구현
def forward_propagation(X, weights_input_hidden, bias_hidden, weights_hidden_output, bias_output):
# 은닉층 계산
z_hidden = np.dot(X, weights_input_hidden) + bias_hidden
a_hidden = relu(z_hidden) # ReLU 활성화 함수 적용
# 출력층 계산
z_output = np.dot(a_hidden, weights_hidden_output) + bias_output
a_output = sigmoid(z_output) # Sigmoid 활성화 함수 적용
return a_output, a_hidden
# 순전파 실행
output, hidden_activation = forward_propagation(X,
weights_input_hidden,
bias_hidden,
weights_hidden_output,
bias_output)
결과는 아래와 같습니다.
# 결과 출력
print("입력 데이터 (정규화 후):\n", X)
print("\n은닉층 활성화 값:\n", hidden_activation)
print("\n출력층 예측값 (합격 확률):\n", output)
순전파 출력 결과
현재는 임의의 가중치와 편향 값을 사용했기 때문에, 예측값 또한 랜덤한 양상을 띠고 있습니다.
손실함수: 예측값과 정답 사이의 오차 계산
순전파 과정에서 모델이 현재 패턴으로 예측값을 도출하면, 이 예측값과 정답 사이의 오차인 손실을 계산해야 합니다. 이를 기반으로 가중치를 조정하여 손실을 줄이는 방향으로 모델의 성능을 점진적으로 개선해 나가죠. 이때, 손실을 계산하는 데 사용되는 함수를 손실함수(Loss Function)라고 하며, 예측값($\hat{y}$)과 정답($y$) 사이의 손실을 $L(\hat{y}, y)$로 표현합니다.
어떤 데이터와 문제를 다루는지에 따라 다양한 손실 함수 중 적절한 함수를 선택해야 합니다. 대표적인 손실 함수 중 하나인 평균제곱오차(Mean Squared Error, MSE)는 말 그대로 예측값과 실제 값의 오차를 제곱한 뒤 평균을 내어 계산합니다. 따라서 결괏값이 작을수록 모델의 예측 오차가 작다는 것을 의미합니다. $$ MSE=\frac{1}{n}\sum_{i=1}^{n}(y_i-\hat{y}_i)^2$$
- $y_i$: 실제값
- $\hat{y}_i$: 예측값
- $n$: 데이터의 총 개수
MSE의 주요 특징은, 오차를 제곱해 양수로 만들기 때문에 데이터 간의 양수와 음수 오차가 상쇄되지 않는다는 점입니다. 또한, 오차를 제곱하기 때문에 큰 오차일수록 더 큰 영향을 미쳐 민감하게 반응하게 됩니다. MSE는 제곱 연산과 평균 연산으로 구성되어 있으며, 두 연산 모두 연속적이고 미분 가능하기 때문에 연속적인 값을 예측하는 회귀 문제에서 주로 사용됩니다.
대입 합격 예측 인공신경망의 손실함수
대입 합격 여부를 예측하는 문제는 이진 분류 문제에 해당합니다. 이러한 이진 분류 문제를 다루는 인공신경망에는 손실함수로 Binary Cross-Entropy(이하 BCE)가 적합합니다.
앞선 순전파 과정에서 출력된 예측값은 0과 1 사이의 확률값을 나타내며, 이는 모델이 특정 클래스(합격)에 속할 가능성을 예측한 결과입니다. Cross-Entropy는 이러한 모델의 확률 출력값과 실제 정답 간의 차이를 측정하는 데에 최적화되어 있습니다. BCE는 실제 정답이 1일 때 예측 확률값이 1에 가까울수록, 실제 정답이 0일 때 예측 확률 값이 0에 가까울수록 손실 값을 낮게 계산합니다. 이러한 특징 때문에 BCE는 이진 분류 문제에서 널리 사용됩니다.
Binary Cross-Entropy 손실함수는 다음과 같이 정의됩니다.
$$BCE=-\frac{1}{n}\sum_{i=1}^{n}[y_i\log{\hat{y_i}}+(1-y_i)\log{(1-\hat{y_i})}]$$
- $y_i$: 실제 클래스(0 또는 1)
- $\hat{y}_i$: 예측 확률값(0~1)
- $n$: 데이터의 총 개수
Binary Cross-Entropy를 함수로 정의하고 이번 순전파 과정에서 계산된 예측값과 정답 사이의 손실을 계산해보겠습니다.
# Binary Cross-Entropy 손실 함수 정의
def binary_cross_entropy(y_true, y_pred):
# y_true: 실제값 (0 또는 1)
# y_pred: 예측 확률값 (0~1)
# 안정성을 위해 log의 입력값 범위를 제한
epsilon = 1e-10
y_pred = np.clip(y_pred, epsilon, 1 - epsilon)
# BCE 계산
loss = -np.mean(y_true * np.log(y_pred) + (1 - y_true) * np.log(1 - y_pred))
return loss
# BCE 손실 계산
loss = binary_cross_entropy(y, output)
print("Binary Cross-Entropy 손실 값:", loss)
BCE 출력 결과
BCE 손실 값은 0에 가까울수록 예측이 정확하다는 것을 의미합니다. 0.78이면 정답과 예측값 사이의 오차가 꽤 크다고 할 수 있겠죠? 이제 이 오차를 줄이기 위한 다음 단계를 살펴봅시다.
경사하강법: 손실함수의 최저점으로 다가가는 과정
손실함수
적합한 손실함수를 사용하면 손실을 그래프로 그렸을 때 위 그림처럼 손실이 최저가 되는 최적의 지점이 존재합니다. 우리의 목표이자 인공신경망(ANN)의 학습 목표는 바로 이 최저 손실 지점에 도달할 수 있는 (즉, 오차가 최저가 되는) 가중치를 찾아내는 것입니다. 학습 초기에는 가중치가 임의의 값으로 설정되었기 때문에, 단번에 최적의 지점을 찾기란 사실상 불가능합니다. 따라서 그림에서처럼 현재 가중치의 조합으로 계산한 손실 값을 확인한 뒤, 가중치를 수정해서 손실 함수를 따라 내리막 방향으로 한 걸음씩 이동해 나가는 과정을 반복해야 합니다. 이 과정을 경사하강법(Gradient Descent)라고 합니다.
경사하강법은 다음 두 단계로 이루어집니다:
- 가중치를 어느 방향으로, 얼만큼 움직일지 결정하기
- 계산한 크기만큼 기존의 가중치 업데이트하기
1) 가중치를 어느 방향으로, 얼만큼 움직일지 결정하기
손실함수에서 기울기 따라 이동
현재 시작점에서 손실을 줄여 최적의 지점으로 다가가려면, 현재 지점에서의 기울기(Gradient)를 계산해 그 기울기에 -1을 곱한 방향으로 이동해야 합니다. 그리고 이 방향으로 얼마나 큰 한 걸음을 내딛을 것인지는 학습률(Learning Rate, α)로 결정합니다. 학습률은 경사하강법에서 각 단계의 이동 크기를 조절하는 중요한 하이퍼파라미터(딥러닝 모델에서 학습 과정에 영향을 미치는, 사용자가 직접 설정하는 값)입니다.
손실함수에서 학습률에 따른 이동 양상
학습률이 지나치게 크다면, 한 걸음이 너무 커져서 최적의 지점을 계속 지나쳐 손실 값이 발산할 위험이 있습니다. 반대로 학습률이 너무 작다면, 한 걸음씩 이동하는 속도가 너무 느려 전체 학습에 지나치게 많은 시간이 소요될 수 있습니다. 따라서 학습률을 반복적으로 실험하며 최적의 값을 찾아야 하며, 일반적으로 0.1에서 시작해 0.1배씩 줄여나가며 테스트합니다(출처).
이렇게 한 걸음의 방향과 크기가 정해지면, 가중치가 한 번 움직이는(=업데이트되는) 값은 음의 기울기와 학습률의 곱인 $-α\frac{dL}{dw}$가 됩니다.
2) 계산한 크기만큼 기존의 가중치 업데이트하기
다음은 간단합니다. 기존의 가중치에 움직일 값을 더해 새로운 가중치로 업데이트하면 되는 것이죠. 이렇게 계산된 값만큼 가중치를 이동시키며 손실 값을 점진적으로 줄여갑니다.
경사하강법을 우리의 대입 합격 예측 인공신경망에 적용하기 위해서는, 가중치를 움직일 방향인 ‘-기울기(음의 기울기)’를 구할 수 있어야 합니다. 따라서 손실 함수에서의 도함수를 구하는 과정을 먼저 알아본 뒤, 우리 인공신경망에서 경사하강법을 구현해보겠습니다.
역전파(Backpropagation): 손실함수에 필요한 도함수를 구하는 과정
ANN은 예측값과 정답 사이의 손실을 최소화하기 위해 가중치를 조정해서 경사하강법으로 최적의 가중치에 한 걸음씩 다가갑니다. 이를 위해 기울기의 반대 방향과 적절한 학습률을 곱해 가중치를 조정하는 과정이 필요합니다. 그런데 이 때, 기울기를 어떻게 계산할 수 있을까요?
기울기를 구하기 위해서는 손실 함수를 가중치에 대해 미분합니다. 하지만 복잡한 신경망의 경우 수십억 개의 가중치를 가지고 있기도 하며, 이 모든 가중치에 대해 일일이 미분하고 값을 수정하려면 계산량이 기하급수적으로 증가하게 됩니다. 처리 시간이 비현실적으로 길어져 비효율적이게 되죠.
이 문제를 해결하기 위해 딥러닝에서는 역전파(Backpropagation)라는 알고리즘을 사용합니다.
역전파
앞서 살펴본 순전파(Forward Propagation)가 인공신경망의 입력층에서 출력층으로 계산을 진행하며 예측값을 구하는 과정이었다면, 역전파(Backpropagation)는 출력층에서 입력층으로 계산을 진행합니다. 순전파의 목적이 주어진 입력값에 대해 신경망의 예측값을 계산하는 데 있었다면, 역전파의 목적은 손실함수의 가중치에 대한 도함수(기울기)를 구하고 이를 이용해 출력층에 가까운 은닉층부터 가중치를 업데이트하는 데 있습니다.
미분의 연쇄법칙
다소 복잡해보일 수 있지만, 신경망을 거슬러가는 역전파 과정의 빨간색 화살표는 모두 공통된 법칙을 사용하고 있습니다. 바로 미분의 연쇄법칙(chain rule)입니다. 순전파에서 예측값($\hat{y}$)을 출력한 후, 출력층에서 손실함수($L(\hat{y},y)$)를 구했었습니다. 이제는 반대 방향으로 이동하며 각 노드의 도함수를 구하고, 역전파의 이전 단계에서 계산된 도함수와의 연쇄법칙을 통해 손실함수에 대한 여러 변수의 도함수를 계산합니다.
위 그림의 신경망에서는 최종적으로 손실 함수에 대한 가중치 $w_1$과 $w_2$ 각각의 도함수($\frac{dL}{dw_1}$, $\frac{dL}{dw_2}$)를 구할 수 있습니다. 이렇게 계산된 도함수를 사용해 $w_1$과 $w_2$의 값을 업데이트합니다. 출력층 부근에서 계산한 오차가 입력층 부근의 가중치 $w_1$과 $w_2$까지 역전파되어 손실 함수에 대한 도함수를 구할 수 있게 된 것이죠.
위 그림에서는 은닉층이 한 층뿐이었지만, 여러 은닉층이 쌓여 있다면 출력층에서 입력층으로 향하는 역전파의 방향성 때문에 출력층에 가까운 은닉층의 가중치부터 수정됩니다. 입력층에 가까워질수록 누적되는 도함수가 많아지겠죠. 여기서, 활성화 함수인 시그모이드 함수로 인한 문제가 발생할 수 있습니다.
활성화함수 제 2탄: 어떤 활성화 함수를 선택할 것인가?
시그모이드와 도함수 (출처)
파란색으로 그려진 시그모이드 함수를 보면, 입력값이 너무 크거나 작아질 경우 기울기(도함수)가 매우 작아지는 것을 볼 수 있습니다. 실제로 빨간 점선으로 그려진 시그모이드의 도함수를 보았을 때, 입력 값이 일정 수준을 넘어서면 도함수 값이 0에 가까워지는 경향을 보입니다.
역전파 과정에서 입력층으로 갈수록 기울기가 누적된다고 했는데, 극단적으로 모든 활성화 함수로 시그노이드 함수를 쓰게 되면 결국 누적된 도함수 값이 0에 매우 가까워지는 상황이 발생합니다. 이로 인해 발생하는 문제가 바로 기울기 소실 문제(Gradient Vanishing)입니다.
이 경우 층이 깊은 신경망에서 출력층 부근 은닉층의 가중치는 어느 정도 학습될 수 있지만, 입력층에 가까운 가중치들은 제대로 학습되기 어렵습니다. 따라서 상황에 따라 다른 활성화 함수를 채택해야 합니다.
그럼에도 불구하고 시그모이드 함수는 출력 값을 0~1의 범위로 제한한다는 특징 덕분에, 이진 분류를 위한 신경망의 출력층에서는 효과적으로 사용됩니다. 그 외의 은닉층에서는 시그노이드 함수 사용을 신중히 고려해야겠죠.
ReLU 함수 (Rectified Linear Unit)
시그모이드 함수의 기울기 소실 문제를 해결한 함수가 바로 ReLU(Rectified Linear Unit) 함수입니다. ReLU 함수는 입력값과 0 중 더 큰 값을 선택하는 방식으로 동작합니다.
ReLU 함수 (출처)
ReLU함수는 음수를 0으로 바꾸기 때문에 노드에 비선형성을 추가하는 기존 활성화 함수의 목적을 달성합니다. 동시에 양수의 입력값에 대해 기울기가 항상 1이기 때문에, 기울기 소실 문제에 대한 걱정이 필요 없죠.
다만, ReLU 함수의 음수의 입력값에 대한 기울기는 0이기 때문에, 음수의 입력값이 지속적으로 주어지면 해당 뉴런의 가중치는 학습되지 않는 죽은 뉴런 문제(Dying ReLU)가 발생하기도 합니다.
Leaky ReLU 함수 (출처)
이를 해결하기 위해 Leaky ReLU와 같은 변형된 활성화 함수를 사용할 수도 있습니다. Leaky ReLU는 음수의 입력값에 대해서도 약간의 기울기를 부여해, ReLU 함수의 죽은 뉴런 문제를 완화합니다.
하지만 Leaky ReLU는 추가적인 하이퍼파라미터(음수 기울기 값)를 설정해야 하며, 이 값의 최적화가 문제에 따라 달라질 수 있어 복잡성을 증가시킬 수 있습니다. 또한, ReLU는 구현이 간단하고 대부분의 경우 기본값으로도 충분히 좋은 성능을 보여주기 때문에 여전히 널리 사용됩니다.
대입 합격 예측 인공신경망의 오차 역전파와 경사하강법
경사하강법을 적용해 앞서 계산한 손실(약 0.783)을 줄여보도록 하겠습니다.
경사하강법을 적용하기 위해서는 두 가지 값이 필요했습니다. 손실값이 손실 함수의 경사를 따라 내려가도록 할 때, 한 걸음의 보폭이 되는 학습률과 한 걸음의 방향이 되는 기울기였습니다. 이번 실습에서는 학습률을 0.01로 설정했습니다.
기울기를 구하는 함수를 backward_propagation()
으로 구현했습니다. 함수가 다소 복잡하지만, 각 가중치와 편향에 대한 기울기($d$~)가 np.dot()
연산을 통해 입력층에 다가갈수록 누적되고 있다는 것만 확인하고 넘어가도 좋습니다.
# 학습률 (learning rate)
learning_rate = 0.01
def backward_propagation(X, y_true, a_hidden, a_output, weights_input_hidden, weights_hidden_output):
# 출력층의 오차
d_output = a_output - y_true # 출력층의 손실의 미분
# 은닉층 -> 출력층의 가중치와 편향에 대한 기울기
d_weights_hidden_output = np.dot(a_hidden.T, d_output) # 은닉층 가중치 기울기
d_bias_output = np.sum(d_output, axis=0, keepdims=True) # 출력층 편향 기울기
# 은닉층의 오차
d_hidden = np.dot(d_output, weights_hidden_output.T) * (a_hidden > 0) # ReLU의 미분 적용
# 입력층 -> 은닉층의 가중치와 편향에 대한 기울기
d_weights_input_hidden = np.dot(X.T, d_hidden) # 입력층 가중치 기울기
d_bias_hidden = np.sum(d_hidden, axis=0, keepdims=True) # 은닉층 편향 기울기
return d_weights_input_hidden, d_bias_hidden, d_weights_hidden_output, d_bias_output
def update_weights(weights, gradients, learning_rate):
# weights: 기존 가중치
# gradients: 가중치 기울기
# learning_rate: 학습률
return weights - learning_rate * gradients
# 역전파를 통해 기울기 계산
d_weights_input_hidden, d_bias_hidden, d_weights_hidden_output, d_bias_output = backward_propagation(
X, y, hidden_activation, output, weights_input_hidden, weights_hidden_output
)
이렇게 구한 기울기를 가지고 입력층&은닉층의 가중치/편향과 은닉층&출력층의 가중치/편향 값을 업데이트 하겠습니다. 그 다음, 앞서 사용했던 순전파 함수를 재사용해 업데이트된 예측값을 계산해보았습니다.
# 가중치와 편향 업데이트
weights_input_hidden = update_weights(weights_input_hidden, d_weights_input_hidden, learning_rate)
bias_hidden = update_weights(bias_hidden, d_bias_hidden, learning_rate)
weights_hidden_output = update_weights(weights_hidden_output, d_weights_hidden_output, learning_rate)
bias_output = update_weights(bias_output, d_bias_output, learning_rate)
# 순전파 재실행 (업데이트된 가중치 사용)
updated_output, updated_hidden_activation = forward_propagation(X, weights_input_hidden, bias_hidden, weights_hidden_output, bias_output)
print("\n업데이트된 예측값 (합격 확률):\n", updated_output)
예측값 업데이트 출력
각 입력 데이터에 대해 도출한 예측값만 봐서는 앞선 순전파 결과와 크게 달라진 점이 눈에 띄지 않습니다. 예측값과 정답 사이의 오차를 계산하는 손실 함수를 적용해보면, 예측의 정확도가 어떻게 변화하는지 확인할 수 있겠죠?
# 업데이트된 예측값의 손실
updated_loss = binary_cross_entropy(y, updated_output)
print("초기 Binary Cross-Entropy 손실 값: ", loss)
print("업데이트된 Binary Cross-Entropy 손실 값:", updated_loss)
손실 비교
손실이 줄었습니다! 미묘한 값이지만, 우리는 인공신경망에게 1회의 학습을 진행했습니다. 이 과정을 여러번 거듭할수록 손실이 줄고, 우리 인공신경망은 패턴을 더 잘 학습해서 더 나은 예측을 할 수 있습니다.
앞의 과정을 10번 더 반복해보고 손실이 어떻게 변화하는지 보겠습니다.
for epoch in range(10):
# 기울기 계산
d_weights_input_hidden, d_bias_hidden, d_weights_hidden_output, d_bias_output = backward_propagation(
X, y, updated_hidden_activation, updated_output, weights_input_hidden, weights_hidden_output
)
# 가중치와 편향 업데이트
weights_input_hidden = update_weights(weights_input_hidden, d_weights_input_hidden, learning_rate)
bias_hidden = update_weights(bias_hidden, d_bias_hidden, learning_rate)
weights_hidden_output = update_weights(weights_hidden_output, d_weights_hidden_output, learning_rate)
bias_output = update_weights(bias_output, d_bias_output, learning_rate)
# 순전파 재실행 (업데이트된 가중치 사용)
updated_output, updated_hidden_activation = forward_propagation(X, weights_input_hidden, bias_hidden, weights_hidden_output, bias_output)
# 업데이트된 예측값의 손실
updated_loss = binary_cross_entropy(y, updated_output)
print(f"{epoch+2}회차 업데이트된 Binary Cross-Entropy 손실 값:", updated_loss)
epoch += 1
가중치 업데이트 반복 결과
처음 실행했던 학습까지 포함해서 총 11회의 학습을 반복한 결과, 손실이 약 0.556까지 줄어들었습니다.
TensorFlow로 인공신경망 구현하기
지금까지 인공신경망의 학습 과정을 하나하나 구현해보았는데요, TensorFlow라는 라이브러리를 사용하면 인공신경망을 학습시키기 위한 각종 함수를 직접 구현하지 않고도 동일한 과정을 수행할 수 있습니다. Google이 개발한 TensorFlow는 딥러닝과 머신러닝 모델을 손쉽게 구축하고 학습시킬 수 있는 도구입니다. 딥러닝 모델 중 하나인 인공신경망도 TensorFlow를 활용하면 구현과 학습 과정이 굉장히 간단해집니다. TensorFlow를 활용해서 앞서 구현한 인공신경망과 동일한 구조의 모델을 구현해보겠습니다.
동일한 데이터셋(df
)에서 독립변수와 종속변수를 분리하는 첫번째 과정은 동일하게 진행됩니다. 대신 데이터 정규화 과정에서는 또 다른 라이브러리, Scikit-learn(sklearn
)을 사용했습니다. Scikit-learn은 데이터 전처리, 모델 학습, 평가 등 다양한 머신러닝 작업을 수행하는 파이썬 라이브러리입니다. 라이브러리를 사용하면 정규화를 위한 수식을 직접 구현하지 않고 해당 기능을 수행하는 모듈을 불러와 데이터 정규화와 같이 원하는 과정을 간단히 구현할 수 있죠.
import tensorflow as tf
import numpy as np
import pandas as pd
from sklearn.preprocessing import StandardScaler
# 독립변수(X)와 종속변수(y) 분리
X = df[["absences", "night_study", "club_activity", "essay_length", "exam_score"]].values
y = df["admission"].values.reshape(-1, 1)
# 데이터 정규화
scaler = StandardScaler()
X = scaler.fit_transform(X)
이제 데이터를 학습할 인공 신경망 모델을 구현해보겠습니다. 이전 과정(TensorFlow 없이 인공신경망을 구현)에서는 은닉층과 출력층의 가중치와 편향을 초기화하고, 순전파 함수와 활성화 함수 등을 정의함으로써 인공신경망을 구현했습니다. TensorFlow를 사용하면 인공신경망 모델을 하나의 객체로 구현할 수 있습니다. TensorFlow로 구현한 이번 인공신경망 객체 model
은 이전 과정에서의 모델과 동일하게 다음과 같은 구조로 되어있습니다:
- 3개의 노드를 가진 하나의 은닉층
- 활성화 함수: 은닉층 - ReLU, 출력층 - Sigmod
# TensorFlow 모델 정의
model = tf.keras.Sequential([
# 입력층 -> 은닉층 (5 -> 3), 활성화 함수: ReLU
tf.keras.layers.Dense(3, activation='relu', input_shape=(5,)),
# 출력층 (3 -> 1), 활성화 함수: Sigmoid
tf.keras.layers.Dense(1, activation='sigmoid')
])
가중치와 편향값을 임의의 값으로 직접 초기화했던 이전 과정과 달리, TensorFlow를 사용하면 가중치와 편향은 각 층의 초기화 전략에 따라 초기화됩니다. 각 층의 특성에 맞게 효율적으로 초기화되도록 기본적으로 설정되어 있는거죠. 원한다면 tf.keras.initializers
를 사용해서 초기화 방법을 지정할 수도 있습니다.
모델을 정의하고 나면, 학습률과 손실함수를 지정해 모델을 컴파일합니다. 이전 과정과 동일하게 학습률로는 0.01, 손실함수로는 Binary Cross-Entropy를 사용했습니다.
# 모델 컴파일
model.compile(optimizer=tf.keras.optimizers.SGD(learning_rate=0.01), # 학습률 0.01로 설정
loss='binary_crossentropy') # 이진 분류를 위한 손실 함수
모델에 대한 설정을 마쳤으니, 미리 준비해둔 데이터(X
와 y
)를 전달해 학습을 진행하겠습니다. model
객체에 fit()
메소드를 적용합니다. 각 파라미터의 역할은 다음과 같습니다.
-
X
,y
: 학습 데이터의 독립변수와 종속변수 -
epochs
: 반복 진행할 학습의 횟수 -
batch_size
: 모델이 한 번에 처리할 학습 데이터의 개수. 크기가 작아질수록 가중치 업데이트가 세밀해지지만 학습 속도가 느려짐 -
verbose
: 학습 중 출력되는 로그 메세지의 상세 수준
아래는 모델 학습 코드를 실행한 뒤 출력되는 학습 과정의 로그 메세지입니다. 반복문으로 일련의 학습 과정을 반복했던 이전 과정과 달리 훨씬 간단해진 코드를 확인할 수 있습니다.
# 모델 학습
history = model.fit(X, y, epochs=10, batch_size=4, verbose=2)
TensorFlow 모델 학습
이렇게 TensorFlow를 활용하면 학습에 필요한 함수들을 일일이 정의하거나, 반복 학습을 위해 복잡한 함수들을 반복문 안에 직접 나열할 필요가 없습니다. 인공신경망을 직접 구현하며 그 동작 원리를 하나하나 이해하는 과정도 물론 중요하지만, 실제 프로젝트에서 모델을 활용할 때는 TensorFlow와 같은 잘 설계된 라이브러리를 사용해 중간 과정을 단순화하는 것이 효율적입니다.
글을 마무리하며..
이번 글에서는 인공신경망(ANN)의 학습 과정을 이해하고, 학생의 학교 생활 데이터를 활용해 대입 합격 여부를 예측하는 모델을 직접 구축하고 학습시켜 보았습니다. ANN은 단순한 구조와 강력한 학습 능력으로 다양한 문제를 해결할 수 있지만, 시계열 데이터나 순차적 정보 처리에는 한계가 있습니다. 이러한 문제를 해결하기 위해 등장한 것이 순환 신경망(RNN)입니다. 또한 이미지나 영상 처리와 같이 공간적 관계를 파악해야 하는 문제에서는 합성곱 신경망(CNN)이 주로 사용됩니다. CNN은 이미지의 특징을 효과적으로 추출하고 학습할 수 있는 구조를 가지고 있어, 컴퓨터 비전과 같은 분야에서 큰 혁신을 가져왔습니다. 다음 글에서는 RNN의 기본 개념과 이를 개선한 LSTM 및 GRU를 활용한 시계열 문제 해결 방법을 다루고, CNN과 RNN의 차별성과 활용 사례를 비교해 깊이 있게 살펴보겠습니다.