이번 포스팅에서는 이전 포스팅에서 만들었던 로지스틱 회귀 모델을 좀 더 다듬어본다.
STEP1. 손실함수 결괏값 추적하기
저번 포스팅에서 로지스틱 회귀모델이 경사하강법을 통해 최적의 가중치를 찾도록 구현했다. 그런데 경사하강법은 손실함수의 결괏값을 최소화하는 방향으로 가중치를 업데이트 하기 때문에 손실함수 결괏값의 추이를 보면 가중치가 잘 업데이트 되었는지, 즉 모델의 학습과정이 타당했는지 판단할 수 있다. 위 그림과 같이 우하향하는 그래프라면 손실함수의 결괏값이 감소하고 있다는 뜻이기 때문에 모델이 올바른 방향으로 학습을 진행하고 있다고 볼 수 있다. 반면에 손실함수의 결괏값이 감소하지 않는다면 모델의 학습과정에 문제가 있는 것이다.
(1) 초기화 함수 추가
# (1) 초기화 함수 추가
def __init__(self) :
self.w = None
self.b = None
self.losses = []
우선 초기화함수를 추가하고, 손실함수의 결괏값을 저장할 배열을 정의했다.
(2) a 값 조정
why?
def fit(self, x, y, epochs=100) :
self.w = np.ones(x.shape[1])
self.b = 0
for i in range(epochs) :
loss = 0
indexes = np.random.permutation(np.arange(len(x)))
for i in indexes :
z = self.linear(x[i])
a = self.activation(z)
err = -(y[i] - a)
w_grad, b_grad = self.backprop(x[i], err)
self.w -= w_grad
self.b -= b_grad
a = np.clip(a, 1e-10, 1-1e-10) # (2) a 값 조정
loss += -(y[i]*np.log(a) + (1-y[i])*np.log(1-a))
self.losses.append(loss/len(y))
손실함수의 결괏값을 추적하기 위해서는 활성화함수의 결과값 a의 범위를 조정하는 작업이 필요하다. a는 손실함수값을 계산하기 위해 log()의 입력값이 되어야하는데, a가 0에 가까우면 log()함수의 값은 음의 무한대가 되고, a가 1에 가까워지면 log()함수의 값은 0이 된다. 이렇게 log함수의 값이 무한해지면 정확한 손실함수값을 계산할 수 없으므로 a가 0 또는 1이 되지 않도록 범위를 조정해주어야한다.
how?
이를 위해 clip()함수를 추가했다. clip()은 입력값이 주어진 범위의 밖일 때, 입력값을 각 범위 끝값으로 대치한다. minimum()과 동일한 기능인데 속도는 더 빠르다고 한다. 매개변수는 왼쪽에서부터 입력값, 최소값, 최대값, 출력형식을 의미한다. 여기서 출력형식의 디폴트 값은 None인데 배열을 지정하면 출력값이 그 배열에 출력된다. 여기서는 -1 * 10^(-10) ~ 1 - 1 * 10^10로 범위로 설정할 것이고, 출력형식은 배열이 아닌 값이기 때문에 a = np.clip(a,1e-10,1-1e-10)을 추가했다.
(3) 손실함수 계산 및 그래프 로직 추가
def fit(self, x, y, epochs=100) :
self.w = np.ones(x.shape[1])
self.b = 0
for i in range(epochs) :
loss = 0
indexes = np.random.permutation(np.arange(len(x)))
for i in indexes :
z = self.linear(x[i])
a = self.activation(z)
err = -(y[i] - a)
w_grad, b_grad = self.backprop(x[i], err)
self.w -= w_grad
self.b -= b_grad
a = np.clip(a, 1e-10, 1-1e-10)
loss += -(y[i]*np.log(a) + (1-y[i])*np.log(1-a)) # (3) 손실함수 계산로직 추가
self.losses.append(loss/len(y)) # (3) 손실함수 계산로직 추가
...
#(3) 손실함수 그래프 그리기
import matplotlib.pyplot as plt
plt.plot(layer.losses)
plt.xlabel('epoch')
plt.ylabel('loss')
plt.show()
마지막으로 손실값을 계산하고 그래프 그리는 로직을 추가했다. 손실값을 계산하는 식에 대해서는 이전에 포스팅들에서 많이 다뤘으므로 아래 추천 포스팅을 확인바란다..!! 다만 모든 샘플에 대해서 손실함수 값을 계산할 수는 있지만 여기서는 에포크별로 손실함수 결괏값을 평균을 내어서 추적할 것이기 때문에 loss를 len(y)로 나눠주는 로직을 추가했다.
STEP2. 확률적 경사하강법 사용하기
샘플을 랜덤으로 추출하는 로직 추가
def fit(self, x, y, epochs=100) :
self.w = np.ones(x.shape[1])
self.b = 0
for i in range(epochs) :
loss = 0
# 랜덤 추출 로직 추가
indexes = np.random.permutation(np.arange(len(x)))
for i in indexes :
z = self.linear(x[i])
a = self.activation(z)
err = -(y[i] - a)
w_grad, b_grad = self.backprop(x[i], err)
self.w -= w_grad
self.b -= b_grad
a = np.clip(a, 1e-10, 1-1e-10)
loss += -(y[i]*np.log(a) + (1-y[i])*np.log(1-a))
self.losses.append(loss/len(y))
지금까지 사용한 경사하강법은 샘플 데이터 1개에 대한 그레이디언트를 계산했다. 이를 확률적 경사하강법(stochastic gradient descent)이라고 한다. 이전 코드에서는 zip()이라는 함수를 썼는데, 여기서 np.random.permutation()이라는 함수로 바꿔줄 것이다. np.random.permutation()은 zip()과 달리 주어진 배열을 무작위로 추출하는 역할을 하는데 이를 사용하는 이유는 경사하강법에서는 매 에포크마다 훈련세트의 샘플 순서를 섞어서 가중치의 최적값을 계산해야하기 때문이다. 훈련세트의 샘플 순서를 섞으면 가중치 최적값의 탐색 과정이 다양해져 가중치 최적값을 제대로 찾을 수 있고, 이는 모델의 학습효율을 높인다. (실제로 학습 정확도가 높아졌다.)
STEP3. score() 함수 추가하기
def predict(self, x) :
z = [self.linear(x_i) for x_i in x]
return np.array(z) > 0
def score(self, x, y) :
return np.mean(self.predict(x) == y)
모델의 성능을 계산하는 용도의 score() 메소드를 추가했다. 계산원리는 이전 포스팅과 동일하다. predict() 메소드도 수정해주었는데, 이전 소스에서 활성화함수를 계산하는 로직을 뺐다. 그 이유는 활성화 함수의 출력값은 0~1 사이의 확률값이고, 양성 클래스를 판단하는 기준은 0.5 이상이다. 그런데 z가 0보다 크면 활성화 함수의 출력값은 0.5보다 크고 z가 0보다 작으면 활성화함수의 출력값은 0보다 작다. 그래서 predict() 메소드에는 굳이 활성화함수에 대한 계산 없이 z가 0보다 큰지 작은지만 따지면 예측값을 알 수 있다. 그래서 predict()메소드에는 로지스틱 함수를 적용하지 않고 z값의 크기만 비교하여 결과를 변환할 수 있도록 수정했다.
최종 소스 코드
from sklearn.datasets import load_breast_cancer
cancer = load_breast_cancer()
x = cancer.data
y = cancer.target
from sklearn.model_selection import train_test_split
x_train, x_test, y_train, y_test = train_test_split(x, y, stratify=y, test_size = 0.2, random_state = 42)
import numpy as np
class SingleLayer :
def __init__(self) :
self.w = None
self.b = None
self.losses = []
# (1) 직선방정식 계산
def linear(self, x) :
z = np.sum(x * self.w) + self.b
return z
# (2) 활성화함수 계산
def activation(self, z) :
a = 1 / (1 + np.exp(-z))
return a
# (3) 오류역전파 계산
def backprop(self, x, err) :
w_grad = x * err
b_grad = 1 * err
return w_grad, b_grad
# (4) 훈련메소드
def fit(self, x, y, epochs=100) :
self.w = np.ones(x.shape[1])
self.b = 0
for i in range(epochs) :
loss = 0
indexes = np.random.permutation(np.arange(len(x)))
for i in indexes :
z = self.linear(x[i])
a = self.activation(z)
err = -(y[i] - a)
w_grad, b_grad = self.backprop(x[i], err)
self.w -= w_grad
self.b -= b_grad
a = np.clip(a, 1e-10, 1-1e-10)
loss += -(y[i]*np.log(a) + (1-y[i])*np.log(1-a))
self.losses.append(loss/len(y))
# (5) 예측메소드
def predict(self, x) :
z = [self.linear(x_i) for x_i in x]
return np.array(z) > 0
def score(self, x, y) :
return np.mean(self.predict(x) == y)
layer = SingleLayer()
layer.fit(x_train, y_train)
layer.score(x_test, y_test)
import matplotlib.pyplot as plt
plt.plot(layer.losses)
plt.xlabel('epoch')
plt.ylabel('loss')
plt.show()
이상의 수정사항들을 반영하여 소스를 다시 작성하고 훈련 메소드 및 score()메소드를 사용하여 정확도를 출력했다. 정확도를 보면 기본편에서 보다 정확도가 높아진 것을 확인할 수 있다. 이는 에포크마다 훈련세트를 무작위로 섞어 손실함수의 값을 줄였기 때문이다.
[Reference.]
도서
- Do it! 정직하게 코딩하며 배우는 딥러닝 입문, 박해선 지음, 이지스퍼블리싱
웹 사이트
- Numpy Documentation https://numpy.org/doc/stable/index.html
- Python Built-in Function Documentation https://docs.python.org/3/library/functions.html
[Recommended Post.]
2020/07/23 - [Deep Learning/[Books] Do it! 정직하게 코딩하며 배우는 딥러닝 입문] - [모델 구축] 이진분류 로지스틱 회귀모델 구현하기 - 기본
2020/07/13 - [Deep Learning/[Books] Do it! 정직하게 코딩하며 배우는 딥러닝 입문] - [모델 구축] 로지스틱 손실함수와 오류 역전파 이해하기
'Deep Learning > [Books] Do it! 정직하게 코딩하며 배우는 딥러닝 입문' 카테고리의 다른 글
[모델 튜닝] 머신러닝/딥러닝 Validation Set 만드는 이유와 방법 (0) | 2020.09.22 |
---|---|
[데이터 전처리] 스케일 조정 (2) | 2020.07.30 |
[모델 구축] 이진분류 로지스틱 회귀모델 구현하기 - 기본 (2) | 2020.07.23 |
[모델 평가] 훈련데이터셋 나누기 (feat.train_test_split()) (5) | 2020.07.20 |
[데이터 탐색] 데이터 탐색에 유용한 함수 2탄 - boxplot(), unique() (2) | 2020.07.16 |