벡터화(vectorization)
넘파이, 머신러닝, 딥러닝 패키지들은 다차원 배열(행렬) 연산을 빠르게 수행할 수 있다.
벡터화된 연산을 사용하면 알고리즘의 성능을 높일 수 있다.
SingleLayer 클래스에 배치 경사 하강법을 적용해 보자.
배치 경사 하강법은 모든 샘플을 한 번의 훈련에 사용하기 때문에 자원이 더 소모된다.
벡터 연산과 행렬 연산
벡터화된 연산을 제대로 사용하려면 벡터 연산과 행렬 연산을 알아야 한다.
벡터 연산 중 하나인 점 곱(스칼라 곱)과 행렬 곱셈에 대해 알아보자.
단일층 신경망을 나타낸 그림이다.
z를 구했던 강법은 가중치(w1, w,2, w3)와 입력(x1, x,2, x3)을 각각 곱하여 더했다.
코드는 다음과 같다.
z = np.sum(x * self.w) + self.b
넘파이의 원소별 곱셈 기능 덕분에 간단하게 구현이 가능했다.
벡터 x와 w를 곱하여 합을 구하는 계산을 점 곱(dot product)또는 스칼라 곱(scalar product)라고 한다.
행 방향 벡터와 열 방향벡터를 곱한 후 모두 더하는 것과 같은 원리이다.
np.dot()을 이용하여 코드를 변경하면 다음과 같다.
z = np.dot(x, self.w) + self.b
전체 샘플에 대한 z구하기
전체 샘플 중 하나의 샘플을 row로 특성을 column으로 표현하면 다음과 같다.
행렬 곱셈의 결과로 각 샘플마다 특성의 값과 가중치를 곱하여 더한 결과를 얻을 수 있다.
SingleLayer 클래스에 배치 경사 하강법 적용
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
cancer = load_breast_cancer()
x = cancer.data
y = cancer.target
x_train_all, x_test, y_train_all, y_test = train_test_split(x, y, stratify = y, test_size=0.2, random_state=42)
x_train, x_val, y_train, y_val = train_test_split(x_train_all, y_train_all, stratify = y_train_all, test_size=0.2, random_state=42)
print(x_train.shape, x_val.shape)
데이터를 준비했다.
위와 같은 컨셉으로 z값을 구해보자.
또한, 가중치를 업데이트하기 위해서는 그레이디언트를 구해야 한다.
그레이디언트는 오차와 입력 데이터의 곱이다.
이것을 행렬로 표현하면 다음과 같다.
X를 전치하여 E와 행렬곱을 하면 그레이디언트를 구할 수 있다.
forpass()와 backprop()에 적용하면 다음과 같다.
def forpass(self,x):
z = np.dot(x, self.w) + self.b
return z
def backprop(self, x, err):
m = len(x)
w_grad = np.dot(x.T, err) / m #가중치에 대한 평균 그레이디언트 계산
b_grad = np.sum(err) / m #절편에 대한 평균 그레이디언트 계산
return w_grad, b_grad
fit() 메서드를 수정해보자.
인덱스를 섞어서 진행했던 저번 미니 배치와는 다르게 전체 샘플에 대해 훈련을 진행하여 for문이 하나 줄어든다.
def fit(self, x, y, epochs=100, x_val=None, y_val=None):
y = y.reshape(-1,1) #타겟을 열벡터로 변환
y_val = y_val.reshape(-1,1) #val타겟을 열벡터로 변환
m = len(x)
self.w = np.ones((x.shape[1],1)) #가중치 초기화
self.b = 0
self.w_history.append(self.w.copy()) #가중치 기록
for i in range(epochs):
z = self.forpass(x) #정방향 계산
a = self.activation(z) #활성화 함수
err = -(y - a)
w_grad, b_grad = self.backprop(x,err) #역방향 계산
w_grad += (self.l1 * np.sign(self.w) + self.l2 * self.w) / m #그레이디언트 패널티적용
self.w -= self.lr * w_grad #가중치 업데이트
self.b -= self.lr * b_grad #절편 업데이트
self.w_history.append(self.w.copy()) #가중치 기록
a = np.clip(a, 1e-10,1-1e-10) #클리핑
loss = np.sum(-(y*np.log(a) + (1-y)*np.log(1-a))) #로그 손실과 규제 손실 적용
self.losses.append((loss + self.reg_loss()) / m)
self.update_val_loss(x_val, y_val) #검증
이제 나머지 메서드만 조금 수정하면 된다.
def predict(self, x):
z = self.forpass(x)
return z > 0
def update_val_loss(self, x_val, y_val):
z = self.forpass(x_val)
a = self.activation(z)
a = np.clip(a, 1e-10,1-1e-10) #클리핑
val_loss = np.sum(-(y_val*np.log(a) + (1-y_val)*np.log(1-a)))
self.val_losses.append((val_loss + self.reg_loss()) / len(y_val))
전체 코드는 다음과 같다.
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
cancer = load_breast_cancer()
x = cancer.data
y = cancer.target
x_train_all, x_test, y_train_all, y_test = train_test_split(x, y, stratify = y, test_size=0.2, random_state=42)
x_train, x_val, y_train, y_val = train_test_split(x_train_all, y_train_all, stratify = y_train_all, test_size=0.2, random_state=42)
print(x_train.shape, x_val.shape)
class SingleLayer:
def __init__(self, learning_rate=0.1, l1=2, l2=0):
self.w = None
self.b = None
self.losses = []
self.val_losses = []
self.w_history = []
self.lr = learning_rate
self.l1 = l1
self.l2 = l2
def forpass(self,x):
z = np.dot(x, self.w) + self.b
return z
def backprop(self, x, err):
m = len(x)
w_grad = np.dot(x.T, err) / m #가중치에 대한 평균 그레이디언트 계산
b_grad = np.sum(err) / m #절편에 대한 평균 그레이디언트 계산
return w_grad, b_grad
def activation(self, z):
a = 1 / (1 + np.exp(-z))
return a
def fit(self, x, y, epochs=100, x_val=None, y_val=None):
y = y.reshape(-1,1) #타겟을 열벡터로 변환
y_val = y_val.reshape(-1,1) #val타겟을 열벡터로 변환
m = len(x)
self.w = np.ones((x.shape[1],1)) #가중치 초기화
self.b = 0
self.w_history.append(self.w.copy()) #가중치 기록
for i in range(epochs):
z = self.forpass(x) #정방향 계산
a = self.activation(z) #활성화 함수
err = -(y - a)
w_grad, b_grad = self.backprop(x,err) #역방향 계산
w_grad += (self.l1 * np.sign(self.w) + self.l2 * self.w) / m #그레이디언트 패널티적용
self.w -= self.lr * w_grad #가중치 업데이트
self.b -= self.lr * b_grad #절편 업데이트
self.w_history.append(self.w.copy()) #가중치 기록
a = np.clip(a, 1e-10,1-1e-10) #클리핑
loss = np.sum(-(y*np.log(a) + (1-y)*np.log(1-a))) #로그 손실과 규제 손실 적용
self.losses.append((loss + self.reg_loss()) / m)
self.update_val_loss(x_val, y_val) #검증
def predict(self, x):
z = self.forpass(x)
return z > 0
def update_val_loss(self, x_val, y_val):
z = self.forpass(x_val)
a = self.activation(z)
a = np.clip(a, 1e-10,1-1e-10) #클리핑
val_loss = np.sum(-(y_val*np.log(a) + (1-y_val)*np.log(1-a)))
self.val_losses.append((val_loss + self.reg_loss()) / len(y_val))
def score(self, x, y):
return np.mean(self.predict(x) == y.reshape(-1,1))
def reg_loss(self):
return self.l1 * np.sum(np.abs(self.w)) + self.l2 / 2 * np.sum(self.w**2)
def update_val_loss(self, x_val, y_val):
z = self.forpass(x_val) #정방향 계산
a = self.activation(z) #활성화 함수
a = np.clip(a, 1e-10,1-1e-10) #클리핑
val_loss = np.sum(-(y_val*np.log(a) + (1-y_val)*np.log(1-a)))
self.val_losses.append((val_loss + self.reg_loss()) / len(y_val)) #로그 손실, 규제 손실 더하여 기록
훈련 데이터 표준화 전처리
StandardScaler 클래스를 사용해 데이터 세트의 특성을 평균이 0, 표준 편차가 1이 되도록 전처리를 해보자.
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
scaler.fit(x_train)
x_train_scaled = scaler.transform(x_train)
x_val_scaled = scaler.transform(x_val)
훈련 및 테스트
작성한 모델을 훈련과 테스트를 진행해보자.
손실 그래프와 학습 그래프를 그려서 분석해보자.
배치 경사 하강법을 적용하니 가중치를 찾는 경로가 거의 직선에 가깝다.
가중치의 변화가 연속적이므로 손실 값도 안정적으로 수렴한다. 하지만, 검증 데이터의 loss가 조금 높다.
이유를 찾아봐야 할 것 같다.