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을 들으면서, 텐서플로우를 안쓰고 딥러닝 알고리즘을 짜는것이 딥러닝 프레임워크 이해하는데에 굉장히 좋다기에 한번 해봐야겠다라는 생각만 하고 있었는데, 오늘 노드를 통해 수행할 수 있었던것 같다.
매번 딥러닝 코드를 짤 때, 굉장히 코드가 간단해서 이론으로만 배우는게 조금 아쉬웠는데, 그런 부분들을 해결하 수 있는 노드였다.
Author And Source
이 문제에 관하여(AIFFEL 20일차), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@whrudah98/AIFFEL-20일차저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)