AIFFEL 20일차

LMS : Numpy로 신경망 구현하기

# 입력층 데이터의 모양(shape)
print(x_train_reshaped.shape)

# 테스트를 위해 x_train_reshaped의 앞 5개의 데이터를 가져온다.
X = x_train_reshaped[:5]
print(X.shape)
weight_init_std = 0.1
input_size = 784
hidden_size=50

# 인접 레이어간 관계를 나타내는 파라미터 W를 생성하고 random 초기화
W1 = weight_init_std * np.random.randn(input_size, hidden_size)  
# 바이어스 파라미터 b를 생성하고 Zero로 초기화
b1 = np.zeros(hidden_size)

a1 = np.dot(X, W1) + b1   # 은닉층 출력

print(W1.shape)
print(b1.shape)
print(a1.shape)

이게 기본적인 신경망 layer이다. 사실 활성화함수가 아직 없어서 완전하지는 않다. 구현 과정을 설명해보자면,
np.random.randn(input_size, hidden_size)을 통해 표준정규분포를 따르는 행렬을 반환하고, weight_init_std 를 곱해 가중치 W를 생성한다.
그 후 bias 벡터를 생성하고 zero로 초기화한다.
입력데이터인 X와 W를 곱하고, bias를 더해 은닉층의 출력 a1을 만든다.

그럼 이제 시그모이드 함수를 구현해보자.

# 위 수식의 sigmoid 함수를 구현해 봅니다.
def sigmoid(x):
    return 1 / (1 + np.exp(-x))  


z1 = sigmoid(a1)
print(z1[0])  # sigmoid의 출력은 모든 element가 0에서 1사이

우리가 만든 layer를 이제 겹겹이 쌓아야 하기 때문에, layer를 함수로 구현해보자.

# 단일 레이어 구현 함수
def affine_layer_forward(X, W, b):
    y = np.dot(X, W) + b
    cache = (X, W, b)
    return y, cache
input_size = 784
hidden_size = 50
output_size = 10

W1 = weight_init_std * np.random.randn(input_size, hidden_size)
b1 = np.zeros(hidden_size)
W2 = weight_init_std * np.random.randn(hidden_size, output_size)
b2 = np.zeros(output_size)

a1, cache1 = affine_layer_forward(X, W1, b1)
z1 = sigmoid(a1)
a2, cache2 = affine_layer_forward(z1, W2, b2)    # z1이 다시 두번째 레이어의 입력이 됨

print(a2[0])  # 최종 출력이 output_size만큼의 벡터

이제 softmax 함수를 구현하여, 마지막 출력까지 계산해보자.

def softmax(x):
    if x.ndim == 2:
        x = x.T
        x = x - np.max(x, axis=0)
        y = np.exp(x) / np.sum(np.exp(x), axis=0)
        return y.T 

    x = x - np.max(x) # 오버플로 대책
    return np.exp(x) / np.sum(np.exp(x))
    
y_hat = softmax(a2)
y_hat[0]  # 10개의 숫자 중 하나일 확률 출력

다음으로, 학습을 위한 손실 함수를 정의해야한다. 그 전에 error을 정의해야 하므로, 정답 레이블인 1,2,3...,10을 One-hot 인코딩을 통해 변환해보자.

# y_hat을 One-hot 인코딩하는 함수
def _change_one_hot_label(X, num_category):
    T = np.zeros((X.size, num_category))
    for idx, row in enumerate(T):
        row[X[idx]] = 1
        
    return T

Y_digit = y_train[:5]
t = _change_one_hot_label(Y_digit, 10)
t     # 정답 라벨의 One-hot 인코딩

이제 손실 함수를 정의해보자.

def cross_entropy_error(y, t):
    if y.ndim == 1:
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)
        
    # 훈련 데이터가 원-핫 벡터라면 정답 레이블의 인덱스로 반환
    if t.size == y.size:
        t = t.argmax(axis=1)
             
    batch_size = y.shape[0]
    return -np.sum(np.log(y[np.arange(batch_size), t])) / batch_size

Loss = cross_entropy_error(y_hat, t)
Loss

Loss함수를 정의하였으니, 이제 경사하강법을 구현해보자.

batch_num = y_hat.shape[0]
dy = (y_hat - t) / batch_num
dy    # softmax값의 출력으로 Loss를 미분한 값

dy를 저렇게 정의하는 원리는 Classification and Loss Evaluation - Softmax and Cross Entropy Loss에 나와있다.

dy를 정의하였으니, 다른 기울기들을 chain-rule로 구해보자.

def sigmoid_grad(x):
    return (1.0 - sigmoid(x)) * sigmoid(x)
    
dW2 = np.dot(z1.T, dy)    
db2 = np.sum(dy, axis=0)
dz1 = np.dot(dy, W2.T)
da1 = sigmoid_grad(a1) * dz1
dW1 = np.dot(X.T, da1)
db1 = np.sum(dz1, axis=0)

이제, 경사하강법의 원리에 따라 파라미터를 업데이트하는 함수를 구현해보자

learning_rate = 0.1

def update_params(W1, b1, W2, b2, dW1, db1, dW2, db2, learning_rate):
    W1 = W1 - learning_rate*dW1
    b1 = b1 - learning_rate*db1
    W2 = W2 - learning_rate*dW2
    b2 = b2 - learning_rate*db2
    return W1, b1, W2, b2

방금 우리가 계산해 주었던 미분값을, 레이어 마다 계산해야 하므로, 함수를 통해 나타내보자.

def affine_layer_backward(dy, cache):
    X, W, b = cache
    dX = np.dot(dy, W.T)
    dW = np.dot(X.T, dy)
    db = np.sum(dy, axis=0)
    return dX, dW, db

정리된 내용을 바탕으로, FP와 BP가 이루어지는 한 사이클을 다음과 같이 나타낼 수 있다.

# 파라미터 초기화
W1 = weight_init_std * np.random.randn(input_size, hidden_size)
b1 = np.zeros(hidden_size)
W2 = weight_init_std * np.random.randn(hidden_size, output_size)
b2 = np.zeros(output_size)

# Forward Propagation
a1, cache1 = affine_layer_forward(X, W1, b1)
z1 = sigmoid(a1)
a2, cache2 = affine_layer_forward(z1, W2, b2)

# 추론과 오차(Loss) 계산
y_hat = softmax(a2)
t = _change_one_hot_label(Y_digit, 10)   # 정답 One-hot 인코딩
Loss = cross_entropy_error(y_hat, t)

print(y_hat)
print(t)
print('Loss: ', Loss)
        
dy = (y_hat - t) / X.shape[0]
dz1, dW2, db2 = affine_layer_backward(dy, cache2)
da1 = sigmoid_grad(a1) * dz1
dX, dW1, db1 = affine_layer_backward(da1, cache1)

# 경사하강법을 통한 파라미터 업데이트    
learning_rate = 0.1
W1, b1, W2, b2 = update_params(W1, b1, W2, b2, dW1, db1, dW2, db2, learning_rate)

위에서 나타낸 과정은 FP와 BP를 통해 학습하는 한 사이클을 나타낸 것이다. 이제 이 과정을 통째로 함수로 나타내, 여러 스텝을 구현할 수 있도록 만들어보자.

def train_step(X, Y, W1, b1, W2, b2, learning_rate=0.1, verbose=False):
    a1, cache1 = affine_layer_forward(X, W1, b1)
    z1 = sigmoid(a1)
    a2, cache2 = affine_layer_forward(z1, W2, b2)
    y_hat = softmax(a2)
    t = _change_one_hot_label(Y, 10)
    Loss = cross_entropy_error(y_hat, t)

    if verbose:
        print('---------')
        print(y_hat)
        print(t)
        print('Loss: ', Loss)
        
    dy = (y_hat - t) / X.shape[0]
    dz1, dW2, db2 = affine_layer_backward(dy, cache2)
    da1 = sigmoid_grad(a1) * dz1
    dX, dW1, db1 = affine_layer_backward(da1, cache1)
    
    W1, b1, W2, b2 = update_params(W1, b1, W2, b2, dW1, db1, dW2, db2, learning_rate)
    
    return W1, b1, W2, b2, Loss

위 함수를 이용해 5번 사이클을 반복해보자.

X = x_train_reshaped[:5] #데이터 입력
Y = y_train[:5]

# train_step을 다섯 번 반복 돌립니다.
for i in range(5):
    W1, b1, W2, b2, _ = train_step(X, Y, W1, b1, W2, b2, learning_rate=0.1, verbose=True)

loss가 점점 줄어드는것을 확인할 수 있다.

우리는 위 함수를 통해 loss를 확인할 수 있었다. 하지만, 아직 몇개중 몇개를 맞췄는지 정확도는 모르기 때문에, 정확도를 평가하는 함수를 만들어보자.

def predict(W1, b1, W2, b2, X):
    a1 = np.dot(X, W1) + b1
    z1 = sigmoid(a1)
    a2 = np.dot(z1, W2) + b2
    y = softmax(a2)

    return y

위 함수는 업데이트된 파라미터들을 이용하여 softmax의 출력값을 나타내주는 함수이다.
이 함수를 이용하여 y값을 예측해보자.

# X = x_train[:100] 에 대해 모델 추론을 시도합니다. 
X = x_train_reshaped[:100]
Y = y_test[:100]
result = predict(W1, b1, W2, b2, X)
result[0] >>첫번째 데이터의 추정 확률이 출력됨

predict함수를 바탕으로 정확도를 계산하는 함수를 만들어보면 다음과 같다.

def accuracy(W1, b1, W2, b2, x, y):
    y_hat = predict(W1, b1, W2, b2, x)
    y_hat = np.argmax(y_hat, axis=1) #제일 큰 확률 값이 y_hat

    accuracy = np.sum(y_hat == y) / float(x.shape[0])
    return accuracy

마지막으로 위에서 나타낸 식과 함수들을 정리하여, numpy로 가중치를 업데이트 하는과정을 나타내보자.
먼저,파라미터를 초기화 하는 함수를 나타내자.

def init_params(input_size, hidden_size, output_size, weight_init_std=0.01):

    W1 = weight_init_std * np.random.randn(input_size, hidden_size)
    b1 = np.zeros(hidden_size)
    W2 = weight_init_std * np.random.randn(hidden_size, output_size)
    b2 = np.zeros(output_size)

    
    return W1, b1, W2, b2

아래 코드를 통해 학습을 진행시킨다.

# 하이퍼파라미터
iters_num = 50000  # 반복 횟수를 적절히 설정한다.
train_size = x_train.shape[0]
batch_size = 100   # 미니배치 크기
learning_rate = 0.1

train_loss_list = []
train_acc_list = []
test_acc_list = []

# 1에폭당 반복 수
iter_per_epoch = max(train_size / batch_size, 1)

W1, b1, W2, b2 = init_params(784, 50, 10)

for i in range(iters_num):
    # 미니배치 획득
    batch_mask = np.random.choice(train_size, batch_size)
    x_batch = x_train_reshaped[batch_mask]
    y_batch = y_train[batch_mask]
    
    W1, b1, W2, b2, Loss = train_step(x_batch, y_batch, W1, b1, W2, b2, learning_rate=0.1, verbose=False)

    # 학습 경과 기록
    train_loss_list.append(Loss)
    
    # 1에폭당 정확도 계산
    if i % iter_per_epoch == 0:
        print('Loss: ', Loss)
        train_acc = accuracy(W1, b1, W2, b2, x_train_reshaped, y_train)
        test_acc = accuracy(W1, b1, W2, b2, x_test_reshaped, y_test)
        train_acc_list.append(train_acc)
        test_acc_list.append(test_acc)
        print("train acc, test acc | " + str(train_acc) + ", " + str(test_acc))

회고

수식으로만 하나하나 따라가봤던 BP를 numpy로 따라가보니 훨씬 수활하게 이해할 수 있었다. 사실 cs231n을 들으면서, 텐서플로우를 안쓰고 딥러닝 알고리즘을 짜는것이 딥러닝 프레임워크 이해하는데에 굉장히 좋다기에 한번 해봐야겠다라는 생각만 하고 있었는데, 오늘 노드를 통해 수행할 수 있었던것 같다.
매번 딥러닝 코드를 짤 때, 굉장히 코드가 간단해서 이론으로만 배우는게 조금 아쉬웠는데, 그런 부분들을 해결하 수 있는 노드였다.

좋은 웹페이지 즐겨찾기