Open In Colab

넘파이

import numpy as np

a = [1,2,3]
b = [4,5,6]

print('a+b:', a+b)

try:
    print(a*b)
except TypeError:
    print('불가능')

print()

a = np.array([1,2,3])
b = np.array([4,5,6])
print('a+b', a+b)
print('a*b', a*b)
a+b: [1, 2, 3, 4, 5, 6]
불가능

a+b [5 7 9]
a*b [ 4 10 18]

R의 벡터 자료형 처럼 파이썬의 리스트 자료형은 더하기, 곱하기가 자유롭지 않습니다.

이를 해결하기 위해 C언어 기반 넘파이 패키지를 활용합니다.

a = np.array([[1,2],[3,4]])
print(a)

print('axis =0', a.sum(axis = 0))
print('axis =1', a.sum(axis = 1))
[[1 2]
 [3 4]]
axis =0 [4 6]
axis =1 [3 7]

간단한 행렬 연산입니다. axis = 0과 1의 차이점을 나타냅니다.

a = np.array([[1,2,3],[4,5,6]])
b = np.array([10,20,30])

print('a+b:\n', a+b)
a+b:
 [[11 22 33]
 [14 25 36]]

R과 마찬가지로 재활용 규칙이 사용됩니다.

신경망 기초 행렬 연산

from typing import Callable

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

def deriv(func, input_, delta = 0.001):
    return (func(input_+delta) - func(input_-delta)) / (2 * delta)

def matrix_function_forward_sum(X,W,sigma):    
    assert X.shape[1] == W.shape[0] 
    # assert은 아래 코드가 다음조건이 맞을때만 성립한다는 것을 알려줍니다.

    N = np.dot(X,W)

    S = sigma(N)

    L = np.sum(S)
    return L   

def matrix_function_backward_sum_1(X,W,sigma):    
    assert X.shape[1] == W.shape[0] 
    # assert은 아래 코드가 다음조건이 맞을때만 성립한다는 것을 알려줍니다.

    N = np.dot(X,W)

    S = sigma(N) # 입력받은 함수를 적용합니다.

    L = np.sum(S)

    dLdS = np.ones_like(S) # S배열 차원크기 유지, 값은 모두 1로

    dSdN = deriv(sigma, N)

    dLdN = dLdS * dSdN
    
    dNdX = np.transpose(W, (1,0))

    dLdX = np.dot(dLdN, dNdX)

    return dLdX

행렬 미분입니다. X는 입력값 행렬, W는 가중치행렬 입니다.

np.random.seed(190204)

X = np.random.randn(3,3)
W = np.random.randn(3,2)

print('X:')
print(X)

print('L:')
print(round(matrix_function_forward_sum(X,W,sigmoid), 4))
print()
print('dLdX:')
print(matrix_function_backward_sum_1(X,W,sigmoid))
X:
[[-1.57752816 -0.6664228   0.63910406]
 [-0.56152218  0.73729959 -1.42307821]
 [-1.44348429 -0.39128029  0.1539322 ]]
L:
2.3755

dLdX:
[[ 0.2488887  -0.37478057  0.01121962]
 [ 0.12604152 -0.27807404 -0.13945837]
 [ 0.22992798 -0.36623443 -0.02252592]]

X는 입력하는 행렬이고 L은 출력값 dLdX은 X 행렬이 변할 때 L값의 변화량, 즉 미분값입니다.

이 미분값은 d(sigma)/d(u) (XW) (W)^T으로 구할 수 있습니다. (손으로 증명 가능)

X1 = X.copy()
X1[0,0] += 0.001

print(round((matrix_function_forward_sum(X1,W,sigmoid) - matrix_function_forward_sum(X,W,sigmoid))/0.001, 4))
0.2489

X[0,0] 값을 0.001 증가시켰을때 L 값의 변화량을 구했습니다. dLdX [0,0] 값과 유사한 것을 알 수 있어요.

선형회귀 행렬 연산

def forward_linear_regression(X_batch, y_batch, weights):
    # X는 행렬, y는 벡터, weight는 딕셔너리형태(W + B)
    assert X_batch.shape[0] == y_batch.shape[0] # 데이터 크기가 같은가
    assert X_batch.shape[1] == weights['W'].shape[0] # 가중치 개수가 맞는가.(행렬 연산이 가능한가)
    assert weights['B'].shape[0] ==  weights['B'].shape[1] == 1

    N = np.dot(X_batch, weights['W'])
    P = N + weights['B']

    loss = np.mean(np.power(y_batch-P, 2))

    forward_info = {}
    forward_info['X'] = X_batch
    forward_info['N'] = N # XW
    forward_info['P'] = P # XW + B
    forward_info['y'] = y_batch

    return loss, forward_info

입력값(X) 파라미터(가중치, W)가 주어졌을때 선형 회귀 값을 구하는 함수입니다.

하지만 우리가 원하는건 파라미터(가중치)를 추정하는 일이죠.

L = mean(Sigma(y(i) - yhat(i))) 을 최소로 하는 파라미터를 미분을 이용해서 찾아봅시다.

def loss_gradients(forward_info, weights):
    batch_size = forward_info['X'].shape[0]
    dLdP = -2 * (forward_info['y'] - forward_info['P'])
    dPdN = np.ones_like(forward_info['N'])
    dPdB = np.ones_like(weights['B'])

    dLdN = dLdP * dPdN

    dNdW = np.transpose(forward_info['X'], (1,0))

    dLdW = np.dot(dNdW, dLdN) # X^T가 행렬 곱법칙 때문에 먼저나옴.
    dLdB = (dLdP * dPdB).sum(axis = 0)

    loss_gradients = {}
    loss_gradients['W'] = dLdW
    loss_gradients['B'] = dLdB

    return loss_gradients

앞서 구한 신경망 기초 행렬 연산식을 이용해 선형회귀 가중치의 도함수를 구했습니다.

밑바박부터 만드는 신경망

def forward_loss(X, y, weights):
    M1 = np.dot(X, weights['W1'])
    N1 = M1 + weights['B1']
    O1 = sigmoid(N1)
    
    M2 = np.dot(O1, weights['W2'])
    P = M2 + weights['B2']

    loss = np.mean(np.power(y - P, 2))

    forward_info = {}
    forward_info['X1'] = X1 # 입력값
    forward_info['M1'] = M1 # X * W(1), 여러개의 선형결합 결과들
    forward_info['N1'] = N1 # 상수항 더하기
    forward_info['O1'] = O1 # 시그모이드 함수 씌우기
    forward_info['M2'] = M2 # 시그모이드 함수 씨운 13개 값 다시 선형결합
    forward_info['P'] = P # 다시 상수항 더한 값 최종 Y_HAT으로 생각
    forward_info['y'] = y

간단한 신경망 구조입니다. 선형결합을 행 개수만큼 하고 나온 여러개의 결과값에 시그모이드 함수를 씌우고 다시 선형결합해서 결과를 냅니다.

def loss_gradients(forward_info, weights):
    '''
    신경망의 각 파라미터에 대한 손실의 편미분을 계산
    '''    
    dLdP = -(forward_info['y'] - forward_info['P'])
    
    dPdM2 = np.ones_like(forward_info['M2']) # P = M2 + B2
    dLdM2 = dLdP * dPdM2
    dPdB2 = np.ones_like(weights['B2'])
    dLdB2 = (dLdP * dPdB2).sum(axis=0)
    
    dM2dW2 = np.transpose(forward_info['O1'], (1, 0)) # O1 * W2    
    dLdW2 = np.dot(dM2dW2, dLdP)

    dM2dO1 = np.transpose(weights['W2'], (1, 0)) # 이게 중요
    dLdO1 = np.dot(dLdM2, dM2dO1)
    
    dO1dN1 = sigmoid(forward_info['N1']) * (1- sigmoid(forward_info['N1']))
    dLdN1 = dLdO1 * dO1dN1
    
    dN1dB1 = np.ones_like(weights['B1'])
    dN1dM1 = np.ones_like(forward_info['M1'])
    
    dLdB1 = (dLdN1 * dN1dB1).sum(axis=0)
    
    dLdM1 = dLdN1 * dN1dM1
    
    dM1dW1 = np.transpose(forward_info['X'], (1, 0)) 

    dLdW1 = np.dot(dM1dW1, dLdM1)

    loss_gradients = {}
    loss_gradients['W2'] = dLdW2
    loss_gradients['B2'] = dLdB2.sum(axis=0)
    loss_gradients['W1'] = dLdW1
    loss_gradients['B1'] = dLdB1.sum(axis=0)
    
    return loss_gradients

조금 복잡하긴 하지만 직접 합성함수 미분을 해봤습니다.

느낀점

코드 실습은 많지 않지만 공부시간이 상당히 오래걸렸습니다.

다변량 벡터나 행렬을 사용한 합성함수를 미분하는게 여간 힘든게 아니였어요. 수학적 지식, 머신러닝 경험이 조금 필요합니다.

직관적으로 이해하기위해 많이 노력했습니다.

그래도 딥러닝이 어떤것인지 토대를 배웠는데 정말 흥미로웠어요.

그동안 겉핥기 식으로 대충 모형만 보고 패키지만 갔다가 썼는데 밑바닥부터 구현하니 딥러닝의 기초 구조를 알수 있어서 좋았습니다.