이전 내용
역전파
딥러닝에서 순전파(forward propagation)는 Neural Network 모델의 입력층부터 출력층까지 순서대로 변수들을 계산하고 저장하는 것을 의미한다.
순전파가 입력층에서 출력층으로 향한다면 역전파는 반대로 출력층에서 입력층 방향으로 계산하면서 가중치를 업데이트해간다.
[이전 포스팅에서 진행했던 무작위한 가중치와 편향값을 사용하는 간단한 정방향 계산 코드]
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
all_data = pd.read_csv('http://tinyurl.com/y2qmhfsr')
# 입력값을 추출해 255로 나눔
all_inputs = (all_data.iloc[:, 0:3].values / 255.0)
all_outputs = all_data.iloc[:, -1].values
# 훈련 데이터와 테스트 데이터 분할
X_train, X_test, y_train, y_test = train_test_split(all_inputs, all_outputs, test_size=1/3)
n = X_train.shape[0] # 훈련 샘플 개수
# 신경망의 가중치와 편향을 랜덤하게 초기화 (w: 가중치, b: 편향)
w_hidden = np.random.rand(3, 3)
w_output = np.random.rand(1, 3)
b_hidden = np.random.rand(3, 1)
b_output = np.random.rand(1, 1)
# 활성화 함수
relu = lambda x: np.maximum(x, 0)
logistic = lambda x: 1 / (1 + np.exp(-x))
# 입력을 신경망에 통과시키고 출력 얻음
def forward_prop(X):
Z1 = w_hidden @ X + b_hidden
A1 = relu(Z1)
Z2 = w_output @ A1 + b_output
A2 = logistic(Z2)
return Z1, A1, Z2, A2
# 정확도 계산
test_predictions = forward_prop(X_test.transpose())[3] # 출력 층의 결과 A2 만 사용
test_comparisons = np.equal((test_predictions >= .5).flatten().astype(int), y_test)
accuracy = sum(test_comparisons.astype(int) / X_test.shape[0])
print("정확도: {:.2f}".format(accuracy))
확률적 경사 하강법으로 신경망을 최적화기 전에 직면한 과제는 가중치와 편향값을 변경하는 방법을 파악하는 것이다.
가중치와 편향값이 서로 얽혀서 출력을 생성하고, 이를 사용해 잔차를 계산한다.
◆ 가중치와 편향에 대한 도함수 구하기
가중치(W)와 편향(B)에 대한 편도함수를 구해야 하기 때문에, 아직 신경망을 훈련하기 위한 확률적 경사 하강법을 적용할 준비가 되지 않았다. 경사 하강법을 수행하는 동안 전체 비용 함수를 줄이기 위해 어떤 가중치와 편향을 얼마나 조정해야 하는지 파악해야 한다.
신경망에 확률적 경사 하강법을 적용하려면 한 층의 노드는 가중치와 편향을 적용한 결과를 다음 층에 전달하고, 다음 층은 또 다른 가중치와 편향을 적용한다. 이렇게 하면 출력 층부터 풀어야 하는 양파 껍질 같은 중첩이 생긴다.
손실을 최소화하기 위해 기울기를 조금 수정하면서 손실을 최소화하는 데 가장 큰 영향을 미치는 가중치와 편향을 변경할 수 있으나, 가중치나 편향을 조금 수정하면 비용 함수까지 영향이 전파된다.
가중치 W_2의 변화는 활성화되지 않은 출력 Z_2를 변경시켜 활성화된 출력 A_2를 바꾸어 비용 함수 C를 바꾼다.
연쇄 법칙을 사용하면 W_2에 대한 C의 편도함수는 아래와 같이 정의된다.
※ 합성함수의 미분을 두 함수의 증가율의 곱으로 표현하는 법칙을 연쇄법칙이라고 한다.
[연쇄법칙 관련 글]
세 개의 그레이디언트를 곱하면 W_2의 변화가 비용 함수 C를 얼마나 변화시키는지 측정할 수 있다.
[심파이를 사용한 A_2에 대한 비용 함수의 도함수 계산]
# 심파이를 사용한 A_2에 대한 비용 함수의 도함수 계산
from sympy import *
A2, y = symbols('A2 Y')
C = (A2 - y)**2
dC_dA2 = diff(C, A2)
print(dC_dA2)
[Z_2에 대한 A_2의 도함수 구하기]
# Z_2에 대한 A_2의 도함수 구하기
from sympy import *
Z2 = symbols('Z2')
logistic = lambda x: 1 / (1 + exp(-x))
A2 = logistic(Z2)
dA2_dZ2 = diff(A2, Z2)
print(dA2_dZ2)
A_2는 활성화 함수의 출력이기 때문에 실제로는 시그모이드 곡선의 도함수를 계산한다.
[W_2에 대한 Z_2의 도함수]
# W_2에 대한 Z_2의 도함수
from sympy import *
A1, W2, B2 = symbols('A1 W2 B2')
Z2 = A1*W2 + B2
dZ2_dW2 = diff(Z2, W2)
print(dZ2_dW2)
Z_2를 계산하는 함수는 선형 함수이고 미분하면 기울기를 반환하므로 W_2에 대한 Z_2의 도함수는 A_1이 된다.
이 모든 것을 종합하면, 가중치 W_2의 변화가 비용 함수에 얼마나 영향을 미치는지 찾는 도함수는 아래와 같다.
세 개의 R, G, B 값으로 구성된 입력 X를 신경망에 통과시키면 A_1, A_2, Z_2, y를 얻는다.
하지만 W_2에 대한 편도함수는 역전파를 위한 도함수 중 하나일 뿐이므로, 필요한 나머지 편도함수를 심파이로 구해본다.
[신경망에 필요한 모든 편도함수 계산]
# 신경망에 필요한 모든 편도함수 계산
from sympy import *
W1, W2, B1, B2, A1, A2, Z1, Z2, X, y = symbols(' W1 W2 B1 B2 A1 A2 Z1 Z2 X Y')
# A2에 대한 비용 함수의 도함수
C = (A2 - y)**2
dC_dA2 = diff(C, A2)
print("dC_dA2 = ", dC_dA2)
# Z2에 대한 A2의 도함수
logistic = lambda x: 1 / (1 + exp(-x))
A2 = logistic(Z2)
dA2_dZ2 = diff(A2, Z2)
print("dA2_dZ2 = ", dA2_dZ2)
# A1에 대한 Z2의 도함수
_Z2 = A1*W2 + B2
dZ2_dA1 = diff(_Z2, A1)
print("dZ2_dA1 =", dZ2_dA1)
# W2에 대한 Z2의 도함수
dZ2_dW2 = diff(_Z2, W2)
print("dZ2_dW2 = ", dZ2_dW2)
# B2에 대한 Z2의 도함수
dZ2_dB2 = diff(_Z2, B2)
print("dZ2_dB2 = ", dZ2_dB2)
# Z1에 대한 A1의 도함수
relu = lambda x: Max(X, 0)
_A1 = relu(Z1)
d_relu = lambda x: x > 0 # 양수이면 기울기가 1, 그렇지 않으면 0
dA1_dZ1 = d_relu(Z1)
print("dA1_dZ1 = ", dA1_dZ1)
# W1에 대한 Z1의 도함수
_Z1 = X*W1 + B1
dZ1_dW1 = diff(_Z1, W1)
print("dZ1_dW1 =", dZ1_dW1)
# B1에 대한 Z1의 도함수
dZ1_dB1 = diff(_Z1, B1)
print("dZ1_dB1 =", dZ1_dB1)
◆ 확률적 경사 하강법
[확률적 경사 하강법 관련 글]
[확률적 경사 하강법을 사용해 신경망 구현하기]
# 확률적 경사 하강법을 사용해 신경망 구현하기
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
all_data = pd.read_csv('http://tinyurl.com/y2qmhfsr')
# 학습률은 솔루션에 얼마나 느리게 도착할지 제어
# 너무 작으면 시간 오래 걸리고, 너무 크면 솔루션을 지나치거나 놓침
L = 0.05
# 입력값을 추출해 255로 나눔
all_inputs = (all_data.iloc[:, 0:3].values / 255.0)
all_outputs = all_data.iloc[:, -1].values
# 훈련 데이터와 테스트 데이터 분할
X_train, X_test, y_train, y_test = train_test_split(all_inputs, all_outputs, test_size=1/3)
n = X_train.shape[0] # 훈련 샘플 개수
# 신경망의 가중치와 편향을 랜덤하게 초기화 (w: 가중치, b: 편향)
w_hidden = np.random.rand(3, 3)
w_output = np.random.rand(1, 3)
b_hidden = np.random.rand(3, 1)
b_output = np.random.rand(1, 1)
# 활성화 함수
relu = lambda x: np.maximum(x, 0)
logistic = lambda x: 1 / (1 + np.exp(-x))
# 입력을 신경망에 통과시키고 출력 얻음
def forward_prop(X):
Z1 = w_hidden @ X + b_hidden
A1 = relu(Z1)
Z2 = w_output @ A1 + b_output
A2 = logistic(Z2)
return Z1, A1, Z2, A2
# 활성화 함수의 도함수
d_relu = lambda x: x>0
d_logistic = lambda x: np.exp(-x) / (1 + np.exp(-x)) ** 2
# 연쇄 법칙을 사용해 가중치와 편향의 그레이디언트 반환
def backward_prop(Z1, A1, Z2, A2, X, y):
dC_dA2 = 2*A2 - 2*y
dA2_dZ2 = d_logistic(Z2)
dZ2_dA1 = w_output
dZ2_dW2 = A1
dZ2_dB2 = 1
dA1_dZ1 = d_relu(Z1)
dZ1_dW1 = X
dZ1_dB1 = 1
dC_dW2 = dC_dA2 @ dA2_dZ2 @ dZ2_dW2.T
dC_dB2 = dC_dA2 @ dA2_dZ2 * dZ2_dB2
dC_dA1 = dC_dA2 @ dA2_dZ2 @ dZ2_dA1
dC_dW1 = dC_dA1 @ dA1_dZ1 @ dZ1_dW1.T
dC_dB1 = dC_dA1 @ dA1_dZ1 * dZ1_dB1
return dC_dW1, dC_dB1, dC_dW2, dC_dB2
# 경사 하강법 실행
for i in range(100000):
# 훈련 데이터에서 하나의 샘플을 무작위로 선택
idx = np.random.choice(n, 1, replace=False)
X_sample = X_train[idx].transpose()
y_sample = y_train[idx]
# 선택한 샘플을 신경망에 통과시킴
Z1, A1, Z2, A2 = forward_prop(X_sample)
# 역전파를 통해 오차를 전파하고 가중치와 편향에 대한 그레이디언트 얻음
dW1, dB1, dW2, dB2 = backward_prop(Z1, A1, Z2, A2, X_sample, y_sample)
# 가중치와 편향 업데이트
w_hidden -= L * dW1
b_hidden -= L * dB1
w_output -= L * dW2
b_output -= L * dB2
# 정확도 계산
test_predictions = forward_prop(X_test.transpose())[3] # 출력 층의 결과 A2 만 사용
test_comparisons = np.equal((test_predictions >= .5).flatten().astype(int), y_test)
accuracy = sum(test_comparisons.astype(int) / X_test.shape[0])
print("정확도: {:.2f}".format(accuracy))
훈련된 신경망은 테스트 데이터의 97~995를 올바르게 식별하고 밝고 어두운 글꼴을 예측한다.
backward_prop() 함수는 연쇄 법칙을 구현해 출력 노드의 오차(제곱 잔차)를 출력과 은닉 층의 가중치/편향에 거꾸로 전파하고 나누어 각 가중치/편향에 대한 그레이디언트를 얻는다.
이 그레이디언트를 가지고 학습률 L을 곱해 for 루프 안에서 가중치/편향을 반복하며 수정한다.
그레이디언트를 기반으로 오차를 뒤로 전파하기 위해 행렬-벡터 곱셈을 수행하고, 행과 열 사이의 차원이 일치해야 할 때 행렬과 벡터를 전치한다.
[신경망에 대화형 셸 추가하기]
신경망을 조금 더 대화형으로 만들고 싶다면 다양한 배경색을 입력하고 (R, G, B 값) 밝은 글꼴 도는 어두운 글꼴을 예측하는 코드를 만들어 확인할 수 있다.
기존 코드에 아래 코드를 추가해본다.
# 새로운 색깔 테스트
def predict_probability(r, g, b):
X = np.array([[r, g, b]]).transpose(X) / 255
Z1, A1, Z2, A2 = forward_prop()
return A2
def predict_font_shade(r, g, b):
output_values = predict_probability(r, g, b)
if output_values > .5:
return "DARK"
else:
return "LIGHT"
while True:
col_input = input("밝은 글꼴과 어두운 글꼴 예측. R, G, B 입력:")
(r, g, b) = col_input.split(",")
print(predict_font_shade(int(r), int(g), int(b)))
사이킷런 사용하기
사이킷런에는 '다층 퍼셉트론 분류기(MLPClassifier)'를 비롯한 몇 가지 편리한 모델을 사용할 수 있다. MLPClassifier는 분류를 위해 설계된 신경망으로, 기본적으로 출력에 로지스틱 활성화 함수를 사용한다.
단, 사이킷런의 신경망 기능은 제한적이므로, 딥러닝을 공부할 때에는 파이토치나 텐서플로를 공부하고, 강력한 GPU를 갖춘 컴퓨터를 구입하는 것을 권장한다. 또한, 요즘에는 파이토치를 많이 사용하고 있다.
[딥러닝 프레임워크 관련 글]
[배경색 분류 애플리케이션 - 사이킷런 버전]
# 사이킷런의 신경망 분류기 사용하기
import pandas as pd
# 데이터 로드
from sklearn.model_selection import train_test_split
from sklearn.neural_network import MLPClassifier
df = pd.read_csv('https://bit.ly/3GsNzGt', delimiter=',')
# 입력 변수(마지막 열을 제외한 모든 열)를 추출하고 255로 나눔
X = (df.values[:, :-1] / 255.0)
# 출력 열(마지막 열) 추출
Y = df.values[:, -1]
# 훈련 데이터와 테스트 데이터 분할
X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size=1/3)
nn = MLPClassifier(solver='sgd', hidden_layer_sizes=(3, ),
activation='relu',
max_iter=100000,
learning_rate_init=.05)
nn.fit(X_train, Y_train)
# 가중치와 편향 출력
print(nn.coefs_)
print(nn.intercepts_)
print("훈련 세트 점수: %f" % nn.score(X_train, Y_train))
print("테스트 세트 점수: %f" % nn.score(X_test, Y_test))
테스트 데이터 세트에서 약 99.3%의 정확도를 얻는다.
[출처]
개발자를 위한 필수 수학
딥러닝을 이용한 자연어 처리 입문
한국 딥러닝 - 네이버 블로그
공데셍 - 티스토리 블로그
'[파이썬 Projects] > <파이썬 - 수학 | 통계학>' 카테고리의 다른 글
[파이썬+통계학] 현대통계학 연습문제 파이썬 구현(ch.11) - 1 (0) | 2024.10.21 |
---|---|
[파이썬+통계학] 현대통계학 연습문제 파이썬 구현(ch.10) (1) | 2024.10.20 |
[개발자를 위한 수학] 신경망 - 1 (3) | 2024.10.20 |
[개발자를 위한 수학] 로지스틱 회귀와 분류 - 2 (0) | 2024.10.18 |
[개발자를 위한 수학] 로지스틱 회귀와 분류 - 1 (3) | 2024.10.18 |