728x90
반응형
SMALL
728x90
반응형
SMALL

지난 신경망 내용에서 매끄럽게 변화하는 시그모이드 함수와 퍼셉트론에서는 갑자기 변화하는 계단 함수를 사용하는 신경망 학습의 차이에 대하여 알아보았습니다.

2023.11.03 - [Programming/Deep Learning] - [Python/DeepLearning] #7. 출력층 뉴런 개수 정하기 & MNIST 데이터(Via Tensor flow)

 

[Python/DeepLearning] #7. 출력층 뉴런 개수 정하기 & MNIST 데이터(Via Tensorflow)

지난 블로그, 신경망의 출력층에 대하여 알아보고 설계에 대하여 알아보았습니다. 2023.10.11 - [Programming/Deep Learning] - [Python/DeepLearning] #6. 출력층(output layer) 설계 [Python/DeepLearning] #6. 출력층(output lay

yuja-k.tistory.com

이번 블로그에서는 모델을 학습 때, 모델의 출력값과 예측값의 오차를 계산해 주는 함수, 손실 함수 Loss Function의 종류에 대하여 알아보도록 하겠습니다.

손실 함수 "Loss Function"

"배가 얼마나 고파요?"라는 질문에 뭐라고 대답할까요?

"배가 많이 고파요" 또는 "적당히 배가 고파요"라는 그냥 막연한 대답이 돌아와야 하는 게 보통입니다.

만약 "저는 지금 63.33 만큼 배가 고파요"라고 이야기하면 질문한 사람이 조금은 당황하겠죠?

이 사람이 개인적으로 만들어낸 이 사람만의 지표라는 것을 토대로 배고픔을 수치적으로 표현했기 때문입니다.

위 사람은 아마 일정 수치가 되면 "배가 고프다"라고 표현하고, 일정 수치가 되면 배가 고프지 않게 밥을 먹는 조금은 특이한 사람이죠.

위에서 이야기한 지표는 하나의 은유적인 비유이지만, 신경망 학습에서는 현재의 상태를 '하나의 지표'로 표현하고, 그 지표를 가장 좋게 만들어주는 가중치 매개변수의 값을 탐색하는 것입니다.

 

위에서 언급한 사람이 자신만의 '배고픔 지표'를 가지고 '최적의 배고픔'을 찾기 위해 노력하듯, 신경망도 '하나의 지표'를 기준으로 최적의 매개변수 값을 탐색합니다.

신경망 학습에서 사용하는 지표를 "손실 함수 Loss Function"라고 합니다. 이 손실 함수는 임의의 함수를 마음대로 사용할 수 있지만, 일반적으로는 평균 제곱 오차 (MSE)교차 엔트로피 오차를 사용합니다.

 

손실 함수는 신경망 성능의 '나쁨'을 나타내는 지표로써, 현재의 신경망이 훈련데이터를 얼마나 잘 처리하지 '못'하느냐를 나타냅니다.

'나쁨'을 지표로 한다는 것이 약간은 어색하지만 그냥 음수만 곱하게 되면 반대를 나타내기 때문에 크게 신경 쓰지 않아도 됩니다.

결론적으로 나쁨을 최소로 하는 것과 좋음을 최대로 하는 것은 같은 의미가 됩니다.

"'나쁨'을 지표로 삼아도 수행하는 일은 다르지 않다"라는 것입니다.

 

평균 제곱 오차 (MSE - Mean Squared Error)

가장 많이 쓰이는 손실 함수로 평균 제곱 오차(Mean Squared Error)가 있습니다. 수식은 다음과 같습니다.

앞쪽이 1/2인 이유는 미분했을 때 남는 식이 (𝑦𝑘−𝑡𝑘) 이기 때문이다.

  • 𝑦𝑘는 예측값을 의미한다. (𝑦̂ 𝑘)
  • 𝑡𝑘는 정답(타깃값)을 의미한다.
import numpy as np
y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]
t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]

def mean_squared_error(y, t):
  return 0.5 * np.sum((y-t) ** 2)
  
print("정답을 2로 추정했을 때의 MSE 값 : {:.3f}".format(mean_squared_error(np.array(y), np.array(t))))

이 배열들의 원소는 첫 번째 인덱스부터 순서대로 '0', '1', '2'를 의미합니다. 

  • y는 𝑠𝑜𝑓𝑡𝑚𝑎𝑥의 출력물로 가정하고, 정답이 2일 확률을 60%로 추정하고 있다.
  • t는 One-Hot Encoding 된 정답 레이블로써, 원래 정답이 2라고 정해져 있다.

소프트맥스 함수의 결과물은 확률이라고 해석할 수 있기 때문에 위 예에서 이미지가 0으로 분류될 확률은 0.1, '1'로 분류될 확률은 0.05, '2'로 분류될 확률은 0.6... 이런 식으로 해석해 보도록 합시다. 

𝑡는 정답 레이블로써, 정답을 가리키는 위치의 원소는 1로, 그 외에는 0으로 표기한 모습입니다. 숫자 '2'를 의미하는 원소 값이 1이기 때문에 정답은 '2'로 생각할 수 있습니다. 평균 제곱오차 수식을 직접 구현해 보겠습니다.

# 일부로 오답 예측을 만들어서 MSE 값을 측정하기. 예측값을 7이라고 가정한 경우로 테스트
y = [0.1, 0.05, 0.1, 0.0, 0.05, 0.1, 0.0, 0.6, 0.0, 0.0]
print("정답을 7로 추정했을 때의 MSE 값 : {:.3f}".format(mean_squared_error(np.array(y), np.array(t))))

두 가지 예를 살펴보았습니다. 첫 번째 예는 정답이 '2'이고, 신경망의 출력도 '2'에서 가장 높은 경우라고 생각하면 됩니다.

두 번째 예에서도 정답은 똑같이 '2' 지만, 출력이 '7'인 경우가 가장 높다고 가정한 것입니다.

결괏값을 확인해 보면 정답을 '2'로 예측했을 때의 손실 함수 오차가 더 작으며, 정답 레이블과의 오차도 작은 것을 알 수 있습니다.

즉, 평균 제곱 오차를 기준으로는 첫 번째 추정 결과가 오차가 더 작기 때문에 정답에 가깝다!라고 판단할 수 있을 것입니다.

교차 엔트로피 오차 (CEE - Cross Entropy Error)

또 다른 손실 함수로써 교차 엔트로피 오차(cross entropy error)도 많이 활용됩니다. 교차 엔트로피 오차 수식은 다음과 같습니다.

여기에서의 𝑙𝑜𝑔는 밑이 𝑒인 자연로그 (𝑙𝑜𝑔𝑒)입니다.

𝑦𝑘는 신경망의 출력, 𝑡𝑘는 정답 레이블입니다. 𝑡𝑘는 정답에 해당하는 인덱스의 원소만 1이고, 나머지는 0인 One-Hot-Encoding 된 형식의 데이터입니다.

그래서 위 식은 실질적으로 정답일 때의 추정(𝑡𝑘가 1일 때의 𝑦𝑘)의 자연로그를 계산하는 식이 됩니다.

정답이 아닌 나머지 모드는 𝑡𝑘가 0이므로 𝑙𝑜𝑔𝑦𝑘와 곱해도 0이 되어 결과에 영향을 주지 않습니다. 예를 들어 정답 레이블은 '2'가 정답이라 하고, 이때의 신경망 출력이 0.6이라면 교차 엔트로피 오차는 −𝑙𝑜𝑔0.6=0.51이 됩니다. 또한 같은 조건에서 신경망 출력이 0.1이라면 −𝑙𝑜𝑔0.1=2.30이 됩니다.

즉, 교차 엔트로피 오차는 정답일 때의 출력이 전체 값을 정하게 됩니다. 교차 엔트로피를 구현해 보도록 하겠습니다.

  • One Hot Encoding 된 𝑡𝑘가 곱해지기 때문에, 정답이 아닌 타깃은 신경을 전혀 쓰지 않는다. ( 𝑙𝑜𝑔𝑦𝑘 X '0 값' )
  • 예측값 자체가 오차 출력값이 된다.
def cross_entropy_error(y, t):
  delta = 1e-7
  return -np.sum(t * np.log(y + delta))

 

위에서 아주 작은 값인 delta를 더하는 이유는 np.log 함수에 0이 대입되면 마이너스 무한대를 나타내게 됩니다. 따라서 더 이상 계산을 할 수 없기 때문에 y에 아주 작은 값 delta를 더해 절대 0이 되지 않도록, 즉 마이너스 무한대가 되지 않도록 방지한 코드입니다.

위의 함수를 이용해서 2를 예측하는 간단한 계산을 해보겠습니다.

# 정답은 2
t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]

# 정답이 2일 확률이 가장 높다고 추정함(0.6)
y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]
print("정답을 2로 추정했을 때의 CEE값 : {:.3f}".format(cross_entropy_error(np.array(y), np.array(t))))

# 정답이 7일 확률이 가장 높다고 추정함(0.6)
y = [0.1, 0.05, 0.1, 0.0, 0.05, 0.1, 0.0, 0.6, 0.0, 0.0]
print("정답을 7로 추정했을 때의 CEE값 : {:.3f}".format(cross_entropy_error(np.array(y), np.array(t))))

 

MSE와 마찬가지로 첫 번째 예에서는 오차가 약 0.51, 두 번째 예에서는 2.30 정도로 많은 차이가 나타나고 있는 것이 확인됩니다.

즉, 결과가 더 작은 첫 번째 추정이 정답일 가능성이 높다고 판단한 것으로, 앞서 평균 제곱 오차의 판단과 일치하게 됩니다.

미니 배치 학습

일전에 배치에 대해 이야기해 보았습니다.

2023.11.03 - [Programming/Deep Learning] - [Python/DeepLearning] #7. 출력층 뉴런 개수 정하기 & MNIST 데이터(Via Tensor flow)

 

[Python/DeepLearning] #7. 출력층 뉴런 개수 정하기 & MNIST 데이터(Via Tensorflow)

지난 블로그, 신경망의 출력층에 대하여 알아보고 설계에 대하여 알아보았습니다. 2023.10.11 - [Programming/Deep Learning] - [Python/DeepLearning] #6. 출력층(output layer) 설계 [Python/DeepLearning] #6. 출력층(output lay

yuja-k.tistory.com

입력한 데이터의 묶음에 대한 결과물들의 묶음이었는데요, 이 데이터들을 모두 훈련 데이터로 사용하게 된다면, 모든 데이터에 대해 손실함수를 적용시켜야 할 것입니다.

즉, 훈련데이터가 100 묶음이라면, 그로부터 계산한 100개의 손실 함숫값들의 합을 지표로 삼아야 한다는 이야기가 될 것입니다.

지금까지 데이터 하나에 대한 손실 함수만 생각해 보았으니, 이제 훈련 데이터 모두에 대한 손실 함수를 적용시킨다고 생각해 보겠습니다.

그렇다면 교차 엔트로피의 오차는 다음과 같겠네요!

𝑁 개의 배치 데이터를 활용했을 때 바뀐 CEE 공식

식이 조금 복잡해진 것 같지만, 간단합니다.

데이터가 𝑁개라면 𝑡𝑛𝑘는 𝑛번째 데이터의 𝑘번째 값을 의미합니다.

(𝑦𝑛𝑘는 신경망의 출력, 𝑡𝑛𝑘는 정답 레이블) 단순히 교차 엔트로피 함수를 𝑁개로 확장하고, 그 오차를 모두 더해서 마지막에 𝑁으로 나누어 정규화하고 있을 뿐입니다.

즉, 제일 마지막에 𝑁으로 나눔으로써 평균 손실 함수를 구하는 것이다라고 생각할 수 있겠습니다.

이렇게 평균을 구해 사용하면 훈련 데이터의 개수와 관계없이 언제든 통일된 지표를 얻을 수 있을 것 같습니다. 예를 들어 훈련 데이터가 1,000개든, 10,000개든 상관없이 평균 손실 함수를 구할 수 있다는 뜻이 됩니다.

 

많은 양의 데이터셋에 대한 처리

일전에 사용했은 MNIST 데이터셋은 훈련 데이터가 무려 60,000개였습니다.

그래서 모든 데이터를 대상으로 손실 함수의 합을 구하려면 시간이 많이 걸리게 됩니다. 더 나아가 빅데이터 수준이 되면 그 수는 수백만에서 수천만도 넘는 거대한 값이 되기도 합니다.

이 많은 데이터를 대상으로 일일이 손실 함수를 계산하는 것은 비현실적이기 때문에, 데이터 일부를 추려 '근사치'로 이용할 수 있습니다.

이 일부가 되는 데이터를 미니배치(mini-batch)라고 합니다. MNIST에서 미니배치 학습을 구현하는 코드를 작성해 보도록 하겠습니다.

# mnist 데이터셋 로딩
from tensorflow.keras import datasets
mnist = datasets.mnist

(X_train, y_train), (X_test, y_test) = mnist.load_data()
X_train.shape

y_train.shape

 

Cross Entropy는 One Hot Encoding 이 되어 있어야 한다.

  • X_train.shape -> (60000, 784)
  • y_train.shape -> (60000, 10)
# step 1 : reshape 부터 진행 -> (60000, 1)
y_train_step_1 = y_train.reshape(-1, 1)
y_train_step_1.shape

y_train_step_1[:3]

# OneHotEncoder를 이용해서 원핫 인코딩 수행
from sklearn.preprocessing import OneHotEncoder

y_train_one_hot = OneHotEncoder().fit_transform(y_train_step_1)
y_train_one_hot = y_train_one_hot.toarray()

y_train_one_hot.shape

y_train_one_hot[:3]

X_train = X_train.reshape(60000, -1)
X_train.shape

미니 배치 구현하기
# 훈련 데이터에서 무작위로 10장만 빼내오기
train_size = X_train.shape[0] # 전체 훈련 데이터셋 크기
batch_size = 10 # 미니 배치의 사이즈
batch_mask = np.random.choice(train_size, batch_size) # train_size에서 무작위로 batch_size만큼의 정수를 선택

# 랜덤으로 선택된 인덱스에 있는 데이터만 추려내기
X_batch = X_train[batch_mask] 

# 원핫 인코딩된 y_train에서 선택된 인덱스에 있는 데이터만 추려내기
y_batch = y_train_one_hot[batch_mask]

np.random.choice 함수를 이용해 지정한 범위의 수 중에서 무작위로 원하는 개수만 꺼내는 역할을 합니다.

위의 예에서는 60000 미만의 수 중에서 10개의 무작위 한 숫자를 뽑아낸 것입니다.

실제 batch_mask를 확인하면 10개의 인덱스가 무작위로 들어있는 배열을 확인할 수 있습니다.

print("무작위로 선택된 인덱스 : {}".format(batch_mask))

 

배치용 교차 엔트로피 구현하기

미니배치 같은 배치데이터를 지원하는 교차 엔트로피 오차는 다음과 같이 구현합니다.

단순히 배열 하나만 처리하던 부분에서, 여러 개의 배열을 한꺼번에 처리할 수 있도록만 바꿔주면 간단히 구현 가능 합니다.

# 배치 및 배치가 아닐 때 까지 고려
def cross_entropy_error(y, t):

  # 1차 원일 때에 대한 처리( 배치가 아닐 때의 처리 )
  if y.ndim == 1:
    # 강제로 2차원 배열화 시키는 것
    t = t.reshape(1, t.size)
    y = y.reshape(1, y.size)

  batch_size = y.shape[0]
  return -np.sum(t * np.log(y)) / batch_size

위 코드에서 y는 신경망의 출력, t는 정답 레이블을 의미합니다.

y가 1차원이라면, 즉 데이터 하나당 교차 엔트로피 오차를 구하는 경우는 reshape 함수로 데이터의 형상을 바꿔줍니다. 그리고 배치의 크기로 나눠 정규화시키고 이미지 1장당 평균의 교차 엔트로피 오차를 계산합니다.

정답 레이블이 원 - 핫 인코딩이 아니라 '2'나 '7' 등의 숫자 레이블로 주어졌을 때의 교차 엔트로피 오차는 다음과 같이 구현 가능합니다.

# 원-핫 인코딩이 되어있지 않은 경우 대응
def cross_entropy_error(y, t):
  if y.ndim == 1:
    t = t.reshape(1, t.size)
    y = y.reshape(1, y.size)

  batch_size = y.shape[0]
  return -np.sum(np.log(y[np.arange(batch_size), t])) / batch_size

위 구현에서는 원-핫 인코딩일 때 t가 0인 원소는 교차 엔트로피 오차도 0이기 때문에 그 계산은 무시해도 좋다는 점이 핵심입니다.

다시 말해, 정답에 해당하는 신경망의 출력만으로 교차 엔트로피 오차를 계산할 수 있다는 이야기가 됩니다.

그래서 원-핫 인코딩 시 t * np.log(y)였던 부분을 레이블 표현일 때는 np.log(y [np.arange(batch_size), t]) 로 구현합니다.

 

np.log(y[np.arange(batch_size), t]를 간단히 설명하면, np.arange(batch_size)는 0부터 batch_size - 1까지의 배열을 생성합니다.

즉, batch_size가 5라면 np.arange(batch_size)는 [0, 1, 2, 3, 4] 같은 Numpy 배열을 생성하게 되고, t에는 레이블이 [2, 7, 0, 9 ,8]와 같이 저장되어 있으므로 y [np.arange(batch_size), t]는 각 데이터의 정답 레이블에 해당하는 신경망의 출력을 추출합니다.

위 예에서의 출력은 [y [0, 2], y [1, 7], y [2, 0], y [3, 9], y [4, 8]]인 Numpy 배열이 만들어지는 것을 알 수 있습니다.

마지막 신경망 학습을 위한 경사법을 위한 미분은 다음 시간에 다뤄보도록 하겠습니다!

 

728x90
반응형
LIST
728x90
반응형
SMALL

지난 블로그, 신경망의 출력층에 대하여 알아보고 설계에 대하여 알아보았습니다.

2023.10.11 - [Programming/Deep Learning] - [Python/DeepLearning] #6. 출력층(output layer) 설계

 

[Python/DeepLearning] #6. 출력층(output layer) 설계

지난 블로그, 행렬과 신경망에 대하여 알아보았습니다. 2023.08.30 - [Programming/Deep Learning] - [Python/DeepLearning] #5. 행렬과 신경망 [Python/DeepLearning] #5. 행렬과 신경망 지난 블로그에서는 차원의 수에 관

yuja-k.tistory.com

 

이번 블로그에서는 출력층의 뉴런 개수를 정하는 방법에 대하여 알아보고, Tensorflow MNIST 데이터를 불러오고 형상 다루는 방법과 추론/분류를 수행하는 신경망을 구현해 보도록 하겠습니다!

 

출력층의 뉴런 수 정하기

 

출력층의 뉴런 개수는 적절하게 정해줘야 합니다. 분류문제 에서는 분류하고 싶은 클래스의 개수대로 설정하는 것이 일반적인 상황이라고 볼 수 있습니다. 예를 들어 필기체 숫자 이미지를 입력해 숫자 0부터 9 중 하나로 분류하는 문제라면 출력층의 뉴런을 10개로( 숫자는 0 ~ 9까지 10개이기 때문에 ) 설정합니다.

 

손글씨 숫자 인식

 

신경망의 구조를 배웠으니, 실제 손글씨 필기체 숫자 인식을 해보도록 하겠습니다. 이미 학습된 매개변수를 사용하여 학습하는 과정은 생략하며, 추론(분류) 하는 과정만 구현해 볼 것입니다. 앞에서 뒤로 순서대로 이동하며 추론을 하기 때문에 이러한 추론 과정을 신경망의 순전파(forward propagation)라고도 합니다.

 

먼저 유명한 손글씨 데이터셋인 MNIST 손글씨 숫자 이미지 집합을 가져와 보겠습니다. MNIST 데이터셋은 0부터 9까지의 숫자 이미지로 구성되어 있으며, 훈련이미지가 60,000장, 테스트용 이미지가 10,000장이 준비되어 있습니다.

 

훈련 이미지를 사용하여 모델을 학습하고, 학습한 모델로 시험 이미지들을 얼마나 정확하게 분류했는지 평가할 수 있습니다.

 

MNIST 이미지는 28px * 28px의 그레이스케일 이미지이며, 각 픽셀은 0 ~ 255 까지의 값을 취하고, 각 이미지에는 해당 이미지에 맞는 숫자가 레이블로 붙어 있습니다.

 

필요한 모듈은 다음과 같습니다.

  • mnist.py : 이미지를 내려받고 numpy 배열로 변경해 주는 역할 및 실제 MNIST 데이터를 가져올 수 있는 역할을 합니다.

먼저 mnist 파일을 import 해주세요!

# tensorflow의 keras를 이용해서 MNIST 불러오기
import tensorflow as tf
import matplotlib.pyplot as plt

%matplotlib inline

 

# mnist 데이터셋 로딩
from tensorflow.keras import datasets
mnist = datasets.mnist

(X_train, y_train), (X_test, y_test) = mnist.load_data()

 

모아놓은 데이터의 형상(shape) 부터 반드시 확인할 것!

X_train.shape
y_train.shape

# 이미지 시각화
image = X_train[0] # 첫 번째 이미지 가져오기
image.shape

# matplotlib의 imshow를 사용한 이미지 시각화
plt.imshow(image, 'gray')
plt.title(y_train[0])
plt.show()

plt.imshow(X_train[1], 'gray')
plt.title(y_train[1])
plt.show()

 

신경망 준비

 

훈련과 학습 모두다 똑같습니다.

1차원 형태로 데이터를 받는 레이어를 다음과 같이 이야기합니다.

  • 신경망 - Dense Layer
  • 기하학 - Affine Layer
  • 통합적 - Fully Connected Layer (완전 연결 계층)

세 방식의 공통점 : 입력되는 데이터의 차원이 (N, M) 여기서 N은 BATCH_SIZE, M은 데이터의 스칼라 개수

  • 배치를 이야기하지 않으면 (M, )
  • 배치를 이야기하면(N, M)

CNN은 Fully Connected Layer가 아니고, 2차원 BATCH 데이터를 사용하기 때문에 입력 데이터의 형상이 (N, H, W, C)가 된다.

image = X_train[0].flatten()
image.shape

image = X_train[0].reshape(-1)
image.shape

 여기서 의문!

ravel()을 사용해도 되는가?

image = np.ravel(X_train [0])

원본 이미지가 훼손될 염려가 있기 때문에 잘 사용하지 않습니다.

 

위처럼 2차원 데이터를 1차원으로 펴는 작업을 데이터 평탄화라고 한다.

image.shape

# 평탄화된 이미지를 원래대로 복구
image_bokgu = image.reshape(28, 28)  # 원래 이미지의 픽셀인 28 * 28 형태의 배열로 복구
plt.imshow(image_bokgu, 'gray')
plt.show()

신경망의 분류 처리

 

MNIST 데이터셋을 가지고 추론(분류)을 수행하는 신경망을 구현해볼 차례입니다.

 

이 신경망은 각 픽셀 하나하나의 입력을 받기 때문에 총입력은 784개가 되며( 28px * 28px ), 출력층은 10개로 설정(숫자 0~9를 분류할 것이기 때문에)할 것입니다.

 

은닉층은 총두 개로, 첫 번째 은닉층은 50개의 뉴런을, 두 번째 은닉층에는 100개의 뉴런을 배치해 보도록 하겠습니다. 50개, 100개의 뉴런들은 임의로 배치시킨 것입니다.

 

MNIST 분류 신경망을 함수화 시켜서 만들어 보도록 하겠습니다. 이때 임의의 가중치와 절편이 들어있는 sample_weight.pkl 파일을 읽어와야 하기 때문에 첨부된 파일을 반드시 실행시키고 있는 폴더 안쪽에 같이 넣어주세요!

 

MNIST 손글씨 데이터셋을 위한 신경망 만들기 Fully Connected Layer ( ANN - Affine Neural Network)
# 활성화 함수 구현( 시그모이드 )
def sigmoid(x):
  return 1 / (1 + np.exp(-x))

# 테스트용 데이터 가져오기
def get_test_data():
  (X_train, y_train), (X_test, y_test) = mnist.load_data()

  IMAGE_SIZE = X_test.shape[0]
  X_test_reshaped = X_test.reshape(IMAGE_SIZE, -1)

  return X_test_reshaped, y_test


# 훈련된 신경망 가져오기
def init_network():
  import pickle
  with open('./sample_weight.pkl', 'rb') as f:
    network = pickle.load(f)

  return network

def predict(network, x):

  W1, W2, W3 = network['W1'], network['W2'], network['W3']
  b1, b2, b3 = network['b1'], network['b2'], network['b3']

  # 구현시에 나는 대부분의 오류는 데이터의 shape 때문이다.
  # 1. 각 층은 입력되는 값과 해당층의 가중치를 곱하고 편향을 더한다.
  # 2. (1)에 의해서 계산된 값에 각 층의 활성화 함수를 씌워주고 다음층으로 넘긴다.

  # Layer 1 계산하기 ( 입력 : x )
  z1 = np.dot(x, W1) + b1
  a1 = sigmoid(z1)

  # Layer 2 계산하기 ( 입력 : a1 )
  z2 = np.dot(a1, W2) + b2
  a2 = sigmoid(z2)

  # Layer 3 계산하기 ( 입력 : a2 ) - 출력층이기 때문에 활성화 함수로 softmax를 사용한다.
  z3 = np.dot(a2, W3) + b3
  y = softmax(z3)

  return y

 

init_network() 함수를 이용해 필요한 가중치와 절편 등 신경망이 직접 습득해야 할 정보들을 미리 저장하고 가져왔습니다.

이 세 가지 함수를 이용해 신경망에 의한 분류를 수행해 보고, 정확도를 평가해 보겠습니다.

X, y = get_test_data()

test_image, test_target = X[0], y[0]

test_image.shape, test_target.shape

 

network = init_network()

test_result = predict(network, test_image)
test_result, test_target

np.argmax(test_result) == test_target

plt.imshow(test_image.reshape(28, 28), 'gray')
plt.title(test_target)
plt.show()

network의 정확도 accuracy 구하기
X, y = get_test_data()

network = init_network()
accuracy_count = 0 # 정답을 맞춘 개수를 저장 -> 맞췄을 떄 1 증가

# 사진을 한장씩 꺼내서 predict 하기
for i in range(len(X)):
  pred = predict(network, X[i]) # (10, )
  pred = np.argmax(pred) # 확률이 가장 높은 원소의 인덱스

  # 예측한 것과 정답이 같으면
  if pred == y[i]:
    accuracy_count += 1   # 정답 카운트를 1 증가


print(float(accuracy_count) / len(X)) # 평균 정답 구하기 -> 정확도

 

위의 코드는 for 문을 돌며 x_test에 들어있는 데이터를 하나씩 predict 함수를 통해 테스트용 데이터인 t와 일치하는지를 묻는 코드입니다.

predict 함수의 결과는 softmax 함수에 의해 [0.1, 0,3, 0.6, 0.9.. ]의 배열 형태로 예측하고, argmax 함수를 이용해 최고로 잘 분류된 확률을 뽑아내게 됩니다.

argmax에 의해 등장한 인덱스가 곧 숫자가 되고, 정답 레이블과 비교하여 분류 정확도를 얻게 됩니다.

가중치와 편향을 미리 가져온 신경망 이기 때문에 추후에 신경망 구조와 학습 방법을 고민하여 정확도를 높여볼 수 있을 것 같습니다!

mnist의 normalize가 True입니다.

이는 픽셀의 범위인 0 ~ 255를 0 ~ 1 사이로 축소시키는 MinMaxScaler와 같은 역할을 합니다.

딥러닝에서 전처리는 항상 자주 사용됩니다.

데이터 전체의 분포를 균일하게 바꿔줄 수 있기 때문이죠!

 

위의 코드의 단점

  • 10,000장의 이미지를 한 장씩 예측을 하고 있다.
  • 시간이 오래 걸린다는 이야기이다.

어떻게 해결할 수 있을까?

  • BATCH를 사용해서 한 장씩이 아닌 뭉터기로 예측을 하게 하겠다.
  • 배치란 데이터의 뭉터기
배치 처리

 

입력 데이터와 가중치 매개변수의 shape를 확인해 보겠습니다.

network = init_network()
W1, W2, W3 = network['W1'], network['W2'], network['W3']
b1, b2, b3 = network['b1'], network['b2'], network['b3'],

W1.shape, W2.shape, W3.shape

b1.shape, b2.shape, b2.shape

 

이미지 한 장을 예측 (predict)

  • 각 은닉층의 활성화 함수는 𝑠𝑖𝑔𝑚𝑜𝑖𝑑로 설정
  • 제일 마지막 출력층의 활성화 함수는 𝑠𝑜𝑓𝑡𝑚𝑎𝑥로 설정

전체적으로 봤을 때 원소 784개로 구성된 1차원 배열이 입력되어 마지막에는 원소가 10개인 1차원 배열이 출력되는 구조로 볼 수 있습니다. 이는 이미지 데이터가 1장만 입력했을 때의 결과이며, 100개를 입력했을 경우에는 100장 분량의 데이터를 하나의 입력 데이터로 표현하면 될 것 같습니다.

 

위의 예에서는 10000장을 입력했기 때문에 결과는 10000장의 결과가 배열의 형태로 한꺼번에 출력될 것입니다. 예를 들어 x [0]의 데이터는 y [0]으로, x [1]의 데이터는 y [1]로 말이죠.

이처럼 하나로 묶은 입력 데이터배치라 합니다. 배치란 곧 입력 데이터들의 묶음이라고 생각할 수 있습니다.

 

배치 처리에 의해 컴퓨터로 계산할 때 큰 이점을 줍니다. 이미지 1장당 처리 시간을 대폭 줄여주게 되는데, 대부분의 수치 계산 라이브러리들이 큰 배열을 효율적으로 처리할 수 있도록 고도의 최적화가 되어 있기 때문입니다.

 

배치 처리를 함으로 인해서 큰 배열로 이뤄진 계산을 하게 되는데, 컴퓨터에서는 큰 배열을 한꺼번에 계산하는 것이 분할된 작은 배열을 여러 번 계산하는 것보다 빠릅니다.

 

이전에 구현한 내용은 배치라고 보기엔 약간 애매모호합니다. 데이터를 하나씩 분류(predict)하고 있기 때문이죠, 따라서 배치 처리 개념을 위 코드에 추가해 보도록 하겠습니다.

# 배치를 활용한 예측
X, y = get_test_data()

network = init_network()

# 배치란? 데이터의 묶음이다.
# batch_size : 1 배치당 들어있어야 하는 데이터의 개수
# ex) 60,000개의 데이터를 batch_size 100으로 묶으면 600개의 배치가 생긴다.

batch_size = 100   # 배치 크기 지정
accuracy_count = 0

# batch_size 개씩 건너 뛰면서 예측 수행
for i in range(0, len(X), batch_size):
  X_batch = X[i : i + batch_size]
  pred_batch = predict(network, X_batch) # 데이터를 100개씩 예측
  pred_batch = np.argmax(pred_batch, axis=1) # axis 설정을 어떻게 해야 할까요?

  accuracy_count += np.sum( pred_batch == y[i : i + batch_size]) # 예측값 + 정답 (100개씩 한꺼번에 계산)

print(float(accuracy_count) / len(X)) # 전체 평균 구하기

 

x [i : i+batch_size]가 입력 데이터의 i 번째부터 i + batch_size 번째까지의 데이터를 묶어주는 부분입니다.

batch_size가 100이므로 100개씩 이미지를 묶어서 예측하고 계산처리를 하게 됩니다.

 

배치를 활용해서 데이터를 모두 소모하면( 활용하면 ) 이것을 1 에폭(epoch)이라고 한다.

np.argmax의 axis가 1인 이유

  1. 𝑠𝑜𝑓𝑡𝑚𝑎𝑥의 결과는 10개의 원소(결과물)를 가진 "1차원 배열"
  2. 이 결과물이 100개씩 묶음 지어져 있다. (batch)
  3. np.argmax의 axis를 1로 줘야지만 각 행별로 최댓값의 인덱스를 구한다.
  4. axis=0은 1차원 배열이 추가되는 방향이기 때문에 "0"

이상으로 신경망의 순전파에 대해 살펴봤습니다.

신경망의 각 층의 뉴런들이 다음 층의 뉴런으로 신호를 전달한다는 점에서 앞 장의 퍼셉트론과 같습니다.

2023.08.16 - [Programming/Deep Learning] - [Python/DeepLearning] #2. 퍼셉트론 (단층 & 다층)

 

[Python/DeepLearning] #2. 퍼셉트론 (단층 & 다층)

이번 블로그에서는 퍼셉트론에 대하여 알아보도록 하겠습니다. 퍼셉트론의 개념은 1957년도에 구현된 알고리즘이며, 신경망(딥러닝)의 기원이 되는 알고리즘이라고 할 수 있습니다. 즉, 퍼셉트

yuja-k.tistory.com

하지만 다음 뉴런으로 갈 때 신호를 변화시키는 활성화 함수에 큰 차이가 있습니다.

신경망에서는 매끄럽게 변화하는 시그모이드 함수를, 퍼셉트론에서는 갑자기 변화하는 계단 함수를 활성화 함수로 사용했습니다.

이 차이가 신경망 학습에 매우 중요하다는 점입니다.

 

다음 블로그에서는 손실 함수에 대하여 알아보도록 하겠습니다!

728x90
반응형
LIST
728x90
반응형
SMALL

지난 블로그, 행렬과 신경망에 대하여 알아보았습니다.

2023.08.30 - [Programming/Deep Learning] - [Python/DeepLearning] #5. 행렬과 신경망

 

[Python/DeepLearning] #5. 행렬과 신경망

지난 블로그에서는 차원의 수에 관련하여 알아보았습니다. 2023.08.23 - [Programming/Deep Learning] - [Python/DeepLearning] #4. 다차원 배열의 계산 [Python/DeepLearning] #4. 다차원 배열의 계산 지난 블로그에서는

yuja-k.tistory.com

이번 시간에는 신경망의 출력층에 대하여 알아보고 설계를 해보도록 하겠습니다~!

신경망의 사용처

신경망은 분류와 회귀 모두 사용할 수 있습니다.

다만 둘 중 어떤 문제냐에 따라 출력층에서 사용하는 활성화 함수가 달라집니다!

일반적으로 회귀에서는 항등함수, 분류에서는 소프트맥스 함수를 사용합니다.

 

항등 함수(Identity Function)와 소프트맥스 함수(Softmax Function) 구현해 볼까요?

출력층의 활성화 함수
  • 이진 분류(Binary Classification) : 𝑠𝑜𝑓𝑡𝑚𝑎𝑥, 𝑠𝑖𝑔𝑚𝑜𝑖𝑑 함수를 주료 사용
  • 다중 분류(Mulitple Classification) : 𝑠𝑜𝑓𝑡𝑚𝑎𝑥

보통 전천후 함수로 𝑠𝑜𝑓𝑡𝑚𝑎𝑥 함수를 주로 사용하는 추세이고, 𝑠𝑖𝑔𝑚𝑜𝑖𝑑를 출력층의 활성화 함수로 사용할 때는 출력층의 뉴런은 한 개로 설정한다.

 

출력층에서 사용하는 활성화 함수인 항등함수는 정말 쉽습니다.

일전에도 살펴봤지만, 입력한 값을 그대로 출력값으로 내보내면 됩니다.

한편, 분류에서 사용되는소프트맥스의 식은 다음과 같습니다.

여기서 𝑒𝑥𝑝(𝑥))는𝑒𝑥을 뜻하는 지수함수입니다.

𝑛은 출력층의 뉴런의 개수이며, 𝑦𝑘는 출력층 뉴런의𝑘 번째 출력을 의미합니다.

소프트맥스 함수의 분자는 출력층으로 입력되는 입력값 𝑎𝑘로 구성되며, 분모는 모든 입력값의 지수 함수의 합으로 구성됩니다.

계속해서 소프트맥스 함수를 직접 구현해 보도록 하겠습니다. 먼저 원리부터 살펴보겠습니다.

# softmax 함수 구현하기 - 원리
import numpy as np

a = np.array([0.3, 2.9, 4.0]) # 입력 신호

# 분자 계산
exp_a = np.exp(a) # 모든 입력 신호에 대한 지수 함수 적용 (분자 값)
print("각 입력 함수에 대한 지수 함수 적용 결과 : {}".format(exp_a))

# 분모 계산
sum_exp_a = np.sum(exp_a)  # 분모가 될 모든 입력 신호에 대한 지수 함수의 합
print("softmax 함수의 분모값 : {}".format(sum_exp_a))

# 소프트맥 계산, numpy 배열 형태로 등장하기 때문에 한꺼번에 계산 가능
y = exp_a / sum_exp_a
print("softmax 결괏값 : {}".format(y)) # 각 입력 값 별 softmax 결괏값

각 입력 함수에 대한 지수 함수 적용 결과 : [ 1.34985881 18.17414537 54.59815003]
softmax 함수의 분모값 : 74.1221542101633
softmax 결괏값 : [0.01821127 0.24519181 0.73659691]

print("softmax 결괏값의 총 합 : {}".format(np.sum(y)))

softmax 결괏값의 총 합 : 1.0

 

복잡해 보였던 소프트맥스 함수의 식을 파이썬으로 표현해 보았습니다.

생각보다 어렵진 않죠? 그럼 직접적으로 소프트맥스 함수를 구현해 보겠습니다.

# 𝑠𝑜𝑓𝑡𝑚𝑎𝑥  함수 직접 구현
def softmax(a):
  exp_a = np.exp(a)
  sum_exp_a = np.sum(exp_a)
  y = exp_a / sum_exp_a

  return y
softmax(a)

𝑠𝑜𝑓𝑡𝑚𝑎𝑥 함수 튜닝하기

소프트맥스 함수는 지수 함수를 사용합니다.

따라서 입력 값이 약간만 커져도 굉장히 큰 값을 연산해야 합니다.

예를 들어 𝑒10=20,000 정도이고, 𝑒100은 0이 40개 넘는 수, 보통 𝑒10001000은 무한대를 의미하는 𝑖𝑛𝑓를 만들어 냅니다.

또한 이렇게 큰 값으로 나눗셈을 하면 수치가 '불안정'해지기 마련입니다.

따라서 이 문제를 해결하기 위해 소프트맥스 함수를 약간 개선해 줄 필요가 있습니다.

log의 개념을 활용하면 손쉽게 처리가 가능합니다!

위 식의 전개 과정은 다음과 같습니다.

 

1. 분자와 분모 모두에 𝐶라는 임의의 정수를 곱합니다. 

2. 𝐶를 지수 함수 exp 안으로 옮겨서 𝑙𝑜𝑔𝐶로 만들어 줍니다. 

3. 𝑙𝑜𝑔𝐶 𝐶′라는 새로운 기호로 바꿔 줍니다.

 

소프트맥스 함수의 지수 함수를 계산할 때 어떤 정수를 더하거나 빼도 결과는 바뀌지 않습니다.

여기서 𝐶에 어떤 값을 대입해도 상관은 없지만, 오버플로를 막을 목적으로는 입력 신호들 중 최댓값을 이용하는 것이 일반적입니다.

다음 예를 살펴보겠습니다.

big_a = np.array( [1010, 1000, 990] )
print(softmax(big_a))                    # 소프트맥스 함수 계산

너무 큰 숫자를 계산하는 바람에 모든 값에 NaN 이 출력되는 것이 확인됩니다.

입력 특성의 최댓값을 𝐶으로 집어넣어서 계산해 보겠습니다.

overflow 발생 원인

𝑒𝑥𝑝함수는 지수함수이다. 𝑒10 은 약 20,000이고,𝑒100은 0이 40개 넘어가고, 𝑒1000은 컴퓨팅 시스템에서 무한대를 의미하는 𝑖𝑛𝑓를 의미한다.

해결하기 위해서는 𝑙𝑜𝑔를 활용한다. 참고로 지수함수에서의 𝑙𝑜𝑔는 뺄셈을 의미한다.

  1. 분자와 분모에 𝐶라는 임의의 정수를 곱합니다.
  2. 𝐶를 지수 함수 𝑒𝑥𝑝안으로 옮겨서 𝑙𝑜𝑔𝐶로 만들어 준다.
  3. 𝑙𝑜𝑔𝐶 𝐶′이라는 새로운 기호로 바꿔준다.
# 보통 상수 C는 입력값 중에 제일 큰값으로 선정한다.
c = np.max(big_a)
print(big_a-c)

print(np.exp(big_a-c) / np.sum(np.exp(big_a-c)))

정상적으로 잘 구해지는 것이 확인됩니다!

이를 토대로 소프트맥스 함수를 다시 정의해 보겠습니다.

def softmax(a):
  c = np.max(a) # 상수 c 구하기( 입력의 최댓값 ) c=1010
  exp_a = np.exp( a - c ) # a + log C ; 각 원소마다 차를 구함 " overflow 대책 "

  sum_exp_a = np.sum(exp_a)
  y = exp_a / sum_exp_a

  return y

 

big_y = softmax(big_a)
print("softmax 결과 : {}".format(big_y))
print("softmax 총합 : {}".format(np.sum(big_y)))

𝑠𝑜𝑓𝑡𝑚𝑎𝑥 함수의 특징

소프트맥스 함수를 사용하면 다음과 같이 신경망의 출력을 계산할 수 있습니다.

y = softmax(a)
print("softmax 결과 : {}".format(y))
print("softmax 총합 : {}".format(np.sum(y)))

소프트맥스 함수 결괏값 배열의 원소들은 항상 0과 1 사이의 실수입니다.

가장 중요한 성질은 소프트맥스 함수의 출력 총합은 1이라는 것입니다.

 

총합이 1이 된다는 점이 소프트맥스 함수의 가장 중요한 성질입니다.

이 성질 덕분에 소프트맥스 함수의 출력을 '확률'로 재해석할 수 있습니다.

위의 결과물을 예시로 들자면, "y [0]의 확률은 약 1.8%, y [1]의 확률은 24.5%, y [2]의 확률은 73.7%로 해석 가능하며,

  • 약 74% 확률로 2번째 클래스, 25%의 확률로 1번째 클래스, 1%의 확률로 0번째 클래스이다.

라는 식으로 확률적인 결론을 낼 수 있게 됩니다.

즉, 소프트맥스 함수를 이용함으로 인해 문제를 확률적(통계적)으로 대응할 수 있게 되는 것입니다.

 

여기서 주의할 점으로, 소프트맥스 함수를 적용해도 각 원소의 대소 관계는 변하지 않습니다.

이는 지수 함수 𝑦=𝑒𝑥𝑝(𝑥)가 단조함수( 정의역 원소 a, b가𝑎𝑏 일 때, 𝑓(𝑎)𝑓(𝑏)가 되는 함수)이기 때문입니다.

실제로 앞의 예에서의 a의 원소들 사이의 대소 관계가 y의 원소들 사이의 대소 관계로 그대로 이어지는 것이 확인됩니다.

신경망을 이용한 분류에서는 일반적으로 가장 큰 출력을 내는 뉴런에 해당하는 클래스로만 인식됩니다.

 

그리고 소프트맥스 함수를 적용해도 출력이 가장 큰 뉴런의 위치는 변화되지 않습니다.

결과적으로 신경망으로 분류할 때는 출력층의 소프트맥스 함수를 생략해도 됩니다.

실제 현업에서도 지수 함수 계산에 드는 자원 낭비를 줄이고자 소프트맥스 함수를 생략하는데요, 그렇다면 왜 소프트맥스 함수를 배웠을까요?

 

이는 기계학습의 문제 풀이는 학습과 추론 두 단계를 거치기 때문입니다.

학습 단계에서 모델을 학습하고, 추론 단계에서 앞서 학습한 모델로 미지의 데이터에 대해서 추론(분류)을 수행하게 됩니다.

이때, 추론 단계에서는 소프트맥스 함수를 생략하는 것이 일반적입니다만, 신경망을 학습시킬 때는 출력층에서 소프트맥스 함수를 사용하게 됩니다. 신경망을 학습시킨다는 개념은 조금 나중에 이야기하도록 하겠습니다.

 

다음 블로그에서는 출력층의 뉴런 수 정하는 방법과 Tensorflow MNIST 데이터를 불러와서 형상 다루는 방법에 대하여 알아보도록 하겠습니다!

728x90
반응형
LIST
728x90
반응형
SMALL

지난 블로그에서는 차원의 수에 관련하여 알아보았습니다.

2023.08.23 - [Programming/Deep Learning] - [Python/DeepLearning] #4. 다차원 배열의 계산

 

[Python/DeepLearning] #4. 다차원 배열의 계산

지난 블로그에서는 계단 함수와 함께 활성화 함수에 대하여 알아보았습니다. 2023.08.18 - [Programming/Deep Learning] - [Python/DeepLearning] #3. 활성화 함수의 기본 [Python/DeepLearning] #3. 활성화 함수의 기본 앞

yuja-k.tistory.com

이번에는 행렬과 신경망에 대하여 알아보도록 하겠습니다.

신경망의 입력과 가중치

신경망으로 들어오는 입력값은 여러 개고, 입력값에 해당하는 가중치도 여러 개입니다.

만약 두 개의 입력값이 있고, 3개의 유닛으로 입력값들을 보낸다면 어떻게 해야 할까요?

 

편향과 활성화 함수가 없는, 아주 간단한 신경망을 만들어 보겠습니다.

import numpy as np

X = np.array([1, 2])
print("입력값 X의 shape : {}".format(X.shape)) # 입력값의 개수는 두개

W = np.array([[1,3,5], [2,4,6]])             # 3개의 유닛으로 전달 되어지는 입력값에 대한 각각의 가중치 행렬
print("가중치 W의 shape : {}".format(W.shape))

Y = np.dot(X, W)                             # 행렬의 내적을 이용한 결과 Y 계산
print("출력값 Y : \n{}".format(Y))

np.dot 함수 한 번으로 단번에 결과 Y를 낼 수 있습니다. 이번엔 3층 신경망을 구현해 보도록 하겠습니다. 입력값은 두 개(𝑥1, 𝑥2)가 있고, 첫 번째 은닉층(1층)은 3개, 두 번째 은닉층(2층)은 2개, 출력층(3층)은 2개의 뉴런으로 구성합니다. 코드를 작성하기 전에, 각 수식을 따져보기 위해 다음과 같은 규칙을 정하겠습니다. 가중치 𝑤를 기준으로 작성해 보면 다음과 같습니다.


 

를 봤을 때 𝑤(1)은 1층의 가중치, 즉 층의 숫자를 의미하고, 아래에 있는 𝑤1212에서 앞의 1은 𝑤가 흘러가는 다음 층의 1번째 뉴런, 뒤에 있는 2는 앞층의 2번째 뉴런을 의미합니다.


위의 식을 토대로 첫 번째 층(입력층)의 가중치와 편향을 받은 첫 번째 은닉 유닛의 결과를 𝑎(1)1(1) 1이라고 가정해 보겠습니다. 이때 해당 뉴런의 계산 식은 다음과 같이 볼 수 있습니다. 편향도 같이 1로 입력이 된다고 생각해 보겠습니다.

 


 

여기에서 행렬의 내적을 이용해 생각해 볼 수 있겠습니다. 총 3개의 은닉 유닛이 존재하기 때문에 각 첫 번째 층을 행렬로 만들어 일반화시켜보면 다음과 같습니다.


 

각 행렬의 값은 다음과 같습니다.

복잡하지만 어려울 것 없습니다! 기본 선형 함수를 사용했던 뉴런 하나를 여러 개 쌓은 것뿐이고, 거기에 번호를 붙인 것뿐입니다. 본격적으로 첫 번째 은닉층에 대한 구현을 해보겠습니다.

X = np.array([1.0, 0.5])
W1 = np.array([[0.1, 0.3, 0.5], [0.2, 0.4, 0.6]])  # 2개씩 입력을 받는 3개의 뉴런
B1 = np.array([0.1, 0.2, 0.3])                     # 3개의 뉴런에 더해질 편향들

print("입력값 X의 shape : {}".format(X.shape))
print("첫 번째 층 가중치 W1의 shape : {}".format(W1.shape))
print("편향 B1의 shape : {}".format(B1.shape))

# Result
A1 = np.dot(X, W1) + B1
print("첫 번째 층의 각 노드의 계산값 : {}".format(A1))

여기서 끝나는 게 아닌, 계산된 최종 결과물에 활성화 함수를 사용해 주면 완벽한 신경망 뉴런이 됩니다.

일전에 했었던 시그모이드 함수를 적용시켜 보겠습니다. 첫 번째 층의 결과물 행렬인 A1에 활성화 함수 시그모이드 함수를 적용시킨 결과물은 Z1이라고 칭하겠습니다.


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

# Result array
Z1 = sigmoid(A1)
print("시그모이드 적용 후 : {}".format(Z1))

그다음은 1층에서 2층으로 가는 과정입니다.  그럼, 앞서했던 것처럼 2층은 이렇게 표현되겠죠?

W2 = np.array([[0.1, 0.4],
               [0.2, 0.5], 
               [0.3, 0.6]])                # 3개의 입력을 받는 2개의 뉴런
               
B2 = np.array([0.1, 0.2])

print("1층의 결과물 Z1의 shape : {}".format(Z1.shape))
print("1층에서 2층으로 넘어가기 위한 가중치 W2 shape : {}".format(W2.shape))
print("2층 노드의 편향들 B1의 shape : {}".format(B2.shape))

A2 = np.dot(Z1, W2) + B2                    # 1층에서 넘어온 값에 가중치 및 편향 계산 추가
Z2 = sigmoid(A2)                            # 시그모이드 함수 적용
print("시그모이드 적용 후 : {}".format(Z2))

1층의 출력 Z1의 2층의 입력이 된다는 점 빼고는 위에서 구현한 코드와 똑같습니다.

numpy 배열만 사용해도 충분히 망을 구축할 수 있을 것 같습니다. 마지막으로 2층에서 출력층으로의 신호 전달입니다.

은닉층에서는 시그모이드 활성화 함수를 사용했지만, 출력층에서는 항등함수, 교차 엔트로피, 소프트맥스 함수 등을 사용해서 결과를 냅니다. 여기에서는 입력값을 그대로 출력하는 항등함수를 활용해 보겠습니다.

굳이 구현할 필요는 없지만, 지금까지 만들어 왔던 패턴과 일치하기 위해 항등함수를 만들겠습니다.

# 출력층
# 항등함수
def identity_function(x):
    return x

W3 = np.array([[0.1, 0.3], 
               [0.2, 0.4]])                     # 2개의 입력을 받는 2개의 뉴런
               
B3 = np.array([0.1, 0.2])

A3 = np.dot(Z2, W3) + B3
Y  = identity_function(A3)                      # Y = A3와 똑같음
print("출력층 결과 : {}".format(Y))

최종 구현 정리

3층 신경망에 대한 정리였습니다.

뉴런이 많아지기 때문에 각각을 행렬로 표현하였고, 각각의 뉴런들은 지금까지 봐왔던 선형함수를 사용하며, 그 결과를 활성화 함수에 넣어 유동적인 값을 가질 수 한다는 것이 이번 구현의 핵심 포인트입니다.

출력층의 결과를 내기 위한 활성화 함수도 따로 있다는 사실이 잠깐 등장하였습니다.

def init_network():

    # 네트워크 초기화 과정
    # 네트워크가 초기화 되면 일반적으로 정규분포 * 0.01 정도의 랜덤값으로 초기화 한다.
    # 다른 방법으로 카이밍 He 초깃값 또는 Xavier 초깃값을 사용한다. (이때, 활성화 함수에 따라 달라진다!)
    network = {}
    network['W1'] = np.array([[0.1, 0.3, 0.5], [0.2, 0.4, 0.6]])
    network['b1'] = np.array([0.1, 0.2, 0.3])
    network['W2'] = np.array([[0.1, 0.4], [0.2, 0.5], [0.3, 0.6]])
    network['b2'] = np.array([0.1, 0.2])
    network['W3'] = np.array([[0.1, 0.3], [0.2, 0.4]])
    network['b3'] = np.array([0.1, 0.2])
    
    return network

# 순전파 과정
def forward(network, x):
    W1, W2, W3 = network['W1'], network['W2'], network['W3']
    b1, b2, b3 = network['b1'], network['b2'], network['b3']
    
    z1 = np.dot(x, W1) + b1
    a1 = sigmoid(z1)
    
    z2 = np.dot(a1, W2) + b2
    a2 = sigmoid(z2)
    
    z3 = np.dot(a2, W3) + b3
    y  = identity_function(z3)
    
    return y
network = init_network()
x = np.array([1.0, 0.5])
y = forward(network, x)
print(y)

여기까지가 신경망의 순방향 구현은 끝입니다!

넘파이의 배열만 잘 사용해도 효율적인 신경망을 구현할 수 있습니다.

다음 블로그에서는 출력층 설계에 관하여 더 알아보도록 하겠습니다!

728x90
반응형
LIST
728x90
반응형
SMALL

지난 블로그에서는 계단 함수와 함께 활성화 함수에 대하여 알아보았습니다. 

2023.08.18 - [Programming/Deep Learning] - [Python/DeepLearning] #3. 활성화 함수의 기본

 

[Python/DeepLearning] #3. 활성화 함수의 기본

앞서 퍼셉트론에 대하여 알아보며, 단층 & 다층 퍼셉트론에 대한 이야기를 나눠 보았습니다. 2023.08.16 - [Programming/Deep Learning] - [Python/DeepLearning] #2. 퍼셉트론 (단층 & ) [Python/DeepLearning] #2. 퍼셉트론

yuja-k.tistory.com

이번 블로그에서는 차원의 수에 대하여 알아보도록 하겠습니다.

다차원 배열

차원수가 많아도 일단 배열은 숫자의 집합이라는 점이 가장 중요합니다!

Python을 활용한 차원 계산은 numpy 모듈을 이용하면 손쉽게 다차원 배열을 만들 수 있고, 계산할 수 있습니다.

먼저, 1차원 배열을 통해 여러 가지 배열 정보를 확인해 보겠습니다.

import numpy as np

A = np.array([1,2,3,4])
print("배열의 원소 : {}".format(A))              # [1,2,3,4] 출력
print("배열의 차원수 : {}".format(np.ndim(A)))   # 1차원
print("배열의 모양 : {}".format(A.shape))        # (4,) 출력

다음은 2차원 배열 입니다.

# 2차원 배열
B = np.array([[1,2],[3,4],[5,6]])
print("배열의 원소 :\n{}".format(B))
# [[1 2]
#  [3 4]
#  [5 6]]  의 결과값 출력
print("배열의 차원수 : {}".format(np.ndim(B)))    # 2차원
print("배열의 모양 : {}".format(B.shape))        # (3,2)

2차원 배열은 보통 행렬(matrix)라고 부르고, 가로 방향을 행(row), 세로 방향을 열(column)이라고 이야기합니다.


행렬의 내적 "행렬 곱"

2차원 배열인 행렬의 내적을 구하는 방법에 대해 알아보겠습니다. 예를 들어 2 x 2 행렬의 내적은 다음과 같이 구할 수 있습니다.

핼렬 곱

위의 수식처럼 행렬 내적은 왼쪽 행렬의 행(가로)과 오른쪽 행렬의 열(세로)을 원소별로 곱하고 그 값들을 더해서 계산합니다. 그리고 계산 결과가 새로운 다차원 배열의 원소가 되는 것이 확인됩니다. 파이썬에서의 변수 표기에서 행렬은 일반 변숫값(스칼라값 이라고도 합니다.)과는 다르게 대문자로 표기하는 것이 관례입니다.

A = np.array([[1,2],[3,4]])
print("행렬 A의 shape : {}".format(A.shape))

B = np.array([[5,6],[7,8]])
print("행렬 B의 shape : {}".format(B.shape))

위의 행렬에서 𝐴×𝐵 는 계산이 가능하다. WHY? A.shape [1]과 B.shape [0]이 같기 때문에 가능!

# numpy의 dot 함수를 이용해 행렬의 내적을 구할 수 있다
print("A X B = \n{}".format(np.dot(A, B)))
print("B X A = \n{}".format(np.dot(B ,A)))

여기서 중요한 점은 행렬의 내적은 교환법칙이 성립하지 않는다. (BUT 단위행렬 제외)

위와 같이 2 X 2 형태의 행렬을 곱하는 예를 확인해 보았습니다.

shape 가 다른 행렬끼리 곱할 수 있는데, 이때 주의 해야 할 것은 첫 번째 행렬(A)의 첫 번째 차원의 원소 수(열의 수)와 두 번째 행렬(B)의 0번째 차원의 원소 수(행의 수)가 같아야 한다는 점입니다.

그렇다면, double check를 위해 이번에는 원소 수가 일치하는 경우와,

A = np.array([[1, 2, 3],
              [4, 5, 6]])

B = np.array([[1, 2],
              [3, 4],
              [5, 6]])

print("행렬 A의 shape : {}".format(A.shape))
print("행렬 B의 shape : {}".format(B.shape))

print(" A X B : \n{}\n".format( np.dot(A, B)))
print(" B X A : \n{}".format( np.dot(B, A)))

# 행렬 A의 1 번째 차원 원소 수 3,
# 행렬 B의 0 번째 차원 원소 수 3  =>  행의 수가 같기 때문에 계산 가능!

A[1] == 3 & B[0] == 3 (계산 가능)

다음처럼 원소 수가 일치하지 않을 때의 경우를 확인해 보도록 하겠습니다!

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

print("행렬 C의 shape : {}".format(C.shape))
print("행렬 A의 shape : {}".format(A.shape))

print("A X C = \n{}".format(np.dot(A, C)))
# A의 1번째 차원의 원소 수가 3, 
# C의 0번째 차원의 원소 수가 2  =>  #오류  계산 불가능!

이처럼, A X C의 계산에서 원소수가 맞지 않아서 계산이 되지 않고 오류가 나타나는 걸 볼 수 있습니다.

하지만! C X A의 형태의 계산은 가능합니다.

print(" C X A : \n{}".format(np.dot(C, A)))
# C의 1번째 원소수가 2, 
# A의 0번째 원소수가 2  => 계산 가능!

다시 말해, 차원 수가 다른 경우의 계산도 가능합니다.

A = np.array([[1, 2],
              [3, 4],
              [5, 6]])

B = np.array([7, 8])

print("행렬 A의 shape : {}".format(A.shape))
print("행렬 B의 shape : {}".format(B.shape))

print(" A X B : \n{}".format(np.dot(A, B)))
 # A와 B의 차원 수는 다르지만, 내적에서 대응되는 차원의 원소수가 같기 때문에 계산 가능

print(" B X A : \n{}".format(np.dot(B, A)))

즉, 뒤에 오는 행렬의 대응되는 차원의 원소의 수만 일치 시켜주면 상관 없다는 것을 알 수 있습니다!

원소의 수 == 행의 수를 잘 고려하여 다차원 계산을 하면 조금 더 쉬운 계산이 될 것 같습니다.

다음 블로그에서는 행렬과 신경망에 대하여 알아보도록 하겠습니다!

728x90
반응형
LIST
728x90
반응형
SMALL

앞서 퍼셉트론에 대하여 알아보며, 단층 & 다층 퍼셉트론에 대한 이야기를 나눠 보았습니다.

2023.08.16 - [Programming/Deep Learning] - [Python/DeepLearning] #2. 퍼셉트론 (단층 & )

 

[Python/DeepLearning] #2. 퍼셉트론 (단층 & 다층)

이번 블로그에서는 퍼셉트론에 대하여 알아보도록 하겠습니다. 퍼셉트론의 개념은 1957년도에 구현된 알고리즘이며, 신경망(딥러닝)의 기원이 되는 알고리즘이라고 할 수 있습니다. 즉, 퍼셉트

yuja-k.tistory.com

다층 퍼셉트론은 단층 퍼셉트론만으로는 계산할 수 없는 복잡한 함수들을 표현할 수 있다는 사실을 알게 되었습니다.

신경망의 등장

이 처럼 여러 개의 다층 퍼셉트론을 층처럼 겹겹이 쌓은 것을 인공 신경망 (Neural Network)라고 합니다.

 

활성화 함수( activation function)

인공 신경망의 퍼셉트론을 유닛 혹은 노드라고 불립니다. 각 유닛은 입력 값을 받아 해당 입력 값의 가중치를 이용해 계산을 한다는 것을 퍼셉트론을 공부하며 알았습니다. 다시 한번 퍼셉트론에서 보았던 입력 값이 두 개일 때의 수식을 확인해 보겠습니다!

𝑧=𝑤1𝑥1+𝑤2𝑥2+𝑏

퍼셉트론을 공부하며 확인했던 𝑦가 활성화( 1로 출력 ) 되는 조건 수식은 완벽한 계단 함수(u)였었습니다.

𝑢(𝑧)={0(𝑧0)1(𝑧>0)

위 계단 함수식이 바로 각 유닛의 출력값을 0 또는 1로 설정하기 위한 퍼셉트론( 유닛 또는 노드 )의 활성화 함수가 됩니다. 즉 계단 함수를 이용해 퍼셉트론의 결과물을 0 또는 1로 활성화시키는 조건을 마련했다고 생각하면 됩니다. 계단 함수만을 사용하지 않고, 다른 활성화 함수들에 대해 공부해 볼 텐데요, 이때 𝑦가 출력 되기 위한 활성화 함수를 적용시킨 수식은 다음과 같이 일반화시켜 볼 수 있겠습니다.

𝑦=𝑎(𝑤1𝑥1+𝑤2𝑥2+𝑏)

위의 𝑎를 활성화 함수라고 이야기하며, 각 입력값에 대한 활성화 함수를 적용시켜 최종적으로 𝑦값이 결정된다고 보면 됩니다. 중요한 점은 생물학적이라는 조건을 굳이 주지 않으면 0과 1로 표현하지 않아도 된다라는 점입니다. 하지만 우리가 지금까지 봐왔던 활성화 함수인 𝑢(𝑧)는 0과 1로만 표현하기 때문에, 신경망에서 자주 사용되는 다른 활성화 함수들에 대해 이야기해 볼까 합니다. 사실 퍼셉트론에서는 활성화 함수로 계단 함수를 이용한다라고 이야기합니다. 즉, 활성화 함수로 쓸 수 있는 여러 후보 함수 중에 퍼셉트론은 계단 함수를 채용하고 있다고 생각하면 됩니다. 이는 퍼셉트론, 즉 유닛(노드)의 활성화 함수를 우리 마음대로 바꿀 수 있다고 생각하는 것이 우리가 배울 딥러닝 인공 신경망의 시작이라고 생각해 볼 수 있을 것입니다.

 

비선형 함수

앞서 이야기해 본 계단 함수, 그리고 뒤에 이야기할 시그모이드(𝜎(𝑧))와 ReLU 함수 모두 비선형 함수입니다. 신경망에서는 활성화 함수로 비선형 함수를 사용해야 합니다. 왜냐하면 선형 함수를 사용하게 되면 신경망의 층을 깊게 해서 네트워크를 구성하는 것이 의미가 없어지기 때문입니다.

 

예를 들어 활성화 함수로𝑎(𝑥)=𝑐𝑥를 사용한다고 했을 때 3층 네트워크를 구축하게 되면

𝑦(𝑥)=ℎ(ℎ(ℎ(𝑥)))

가 됩니다. 즉,

𝑦(𝑥)=𝑐∗𝑐∗𝑐∗𝑥

처럼 곱셈을 세 번 수행하는 것처럼 보이지만 실제로는

𝑦(𝑥)=𝑎𝑥

와 똑같은 식이 되어 버립니다. 그냥𝑎=𝑐33이라고 생각해 버리면 끝이기 때문이죠. 이는 은닉층을 여러 층으로 구성하는 이점을 살릴 수가 없기 때문에 비선형 함수를 이용해서 은닉층들을 구성합니다.

No.1 "시그모이드 (𝜎)"

시그모이드 함수(sigmoid function) 𝜎(𝑧)는 가장 유명한 활성화 함수의 예시입니다. 수식 𝑧은 다음과 같습니다.

z가 양의 무한대로 커지면 1에 가까워 지고, 음의 무한대로 작아지면 0에 가까워 진다. 절대 1또는 0이 안된다. 실숫값으로 값이 표현된다. ( 신호의 세기 )

 

시그모이드 함수 𝑢(𝑧)의 출력은 항상 0보다 크고 1보다 작은 임의의 실숫값이 됩니다. 또한 미분 가능 함수라고 볼 수 있는데요. 이 두 가지 특징이 시그모이드 함수를 사용하는 이유가 됩니다. 계단 함수와 시그모이드 함수를 시각적으로 확인해 보겠습니다.

import numpy as np
import matplotlib.pyplot as plt
# 계단 함수 구현
def step_function(x):
  return np.array( x > 0, dtype=np.int )

# 시그모이드 구현
# np.exp 함수를 이용해 자연상수(e) 의 지수함수를 구현 할 수 있다.

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

# 시각화
fig, (ax1, ax2) = plt.subplots(1, 2,figsize=(20,8))
x = np.arange(-5.0, 5.0, 0.1)

ax1.plot(x, step_function(x))
ax1.set_title('step function')

ax2.plot(x, sigmoid(x))
ax2.set_title('sigmoid function')

plt.show()

계단 함수의 출력값은 반응 여부를 표현하는 0 또는 1입니다. 하지만 시그모이드 함수는 언제나 0보다 크고 1보다 작은 값을 출력하며, 이는 각 유닛의 흥분도 또는 반응도를 나타냅니다. 시그모이드 함수는 계산식 𝑧 결과물이 몇이 나오던 항상 0 또는 1에 가까운 값이 되게 됩니다. 이는 각 유닛의 '흥분도' 또는 '반응도'를 나타내게 되며, 이는 출력값이 1에 가까우면 흥분도가 높고, 0에 가까우면 흥분도가 낮다고 생각할 수 있게 됩니다. 참고로 이 흥분도를 둔감하게, 또는 민감하게 만들어 주는 것이 편향 ( bias )입니다.

시그모이드 함수와 계단 함수의 비교

위의 계단함수와 sigmoid 함수를 비교해 보겠습니다. 공통점과 차이점을 알아볼 텐데요.

제일 먼저 느껴지는 차이는 매끄러움입니다. 시그모이드 함수는 부드러운 곡선의 형태를 띠고 있으며 입력에 따른 출력이 연속적으로 변화되는 것이 확인됩니다.

 

그에 반에 계단 함수는 어느 순간 계단 형태로 출력이 갑작스레 변화되는데요, 시그모이드의 매끄러움이 신경망 학습에서 중요한 역할을 하게 됩니다. 계단 함수는 0과 1중 하나의 값만 돌려주는 반면에, 시그모이드 함수는 실수 형태로 돌려줍니다. 즉, 퍼셉트론에서는 0과 1만 출력되지만, 신경망에서는 연속적인 실수가 흐른다고 볼 수 있겠습니다. 둘의 공통점은 입력이 커지면 1에 가까워지거나 1이 되며, 반대로 입력이 작아지게 되면 0에 가까워지거나 0이 된다는 사실입니다.

즉, 계단 함수와 시그모이드 함수 모두 입력이 중요하면(입력값이 크면) 1에 가까워지고, 반대로 입력이 중요하지 않으면 작은 값(0에 가까워지는) 값을 출력한 닦도 이해할 수 있습니다.

 

No.2 "ReLU"

시그모이드 함수는 신경망 분야에서 오래전부터 이용해 왔었던 함수입니다.

최근에는 ReLU(Rectified Linear Unit) 함수를 많이 이용하는 추세입니다. ReLU는 간단합니다. 입력값이 0을 넘으면 그 입력을 그대로 출력하고, 0 이하면 0을 출력하는 함수입니다.

def relu(x):
    return np.maximum(0, x)

#visualisation
x = np.arange(-5.0, 5.0, 0.1)
plt.title('ReLU function')
plt.plot(x, relu(x))
plt.show()

 

수식으로 나타내면 다음과 같습니다.

음수데이터를 제거하여, 필요한 양수데이터만 강조 ( 보통 이미지 처리에서 많이 사용 된다 )

수식과 그래프에서 보는 것처럼 ReLU는 굉장히 간단한 함수입니다. 시그모이드 함수는 부드러운 곡선을 만들어서 신경망을 구축하고, ReLU는 여러 개의 직선을 연결해서 신경망을 구축합니다.

그런데 컴퓨터에서는 곡선을 만들어서 표현하는 것보다 직선을 여러 개 놓고 표현하는 것이 더 효율이 좋기 때문에 시그모이드보다 ReLU가 더 많이 사용되는 추세입니다.

다음 블로그에서는 다차원 배열의 계산에 대하여 알아보도록 하겠습니다!

 

728x90
반응형
LIST
728x90
반응형
SMALL

이번 블로그에서는 퍼셉트론에 대하여 알아보도록 하겠습니다.

 

퍼셉트론의 개념은 1957년도에 구현된 알고리즘이며, 신경망(딥러닝)의 기원이 되는 알고리즘이라고 할 수 있습니다. 즉, 퍼셉트론의 구조를 배워야만 신경망과 딥러닝으로 확장되는 아이디어를 생각해 낼 수 있는 중요한 기초 내용이 됩니다.

 

퍼셉트론이란?

퍼셉트론(인공 뉴런 이라고도 합니다)은 다수의 신호를 받아 하나의 신호를 출력합니다.

여기서 신호는 뇌과학에서는 일정한 자극, 신경망에서는 feature(특성)가 된다고 할 수 있는데요, 어떤 흐름을 타고 데이터(신호 - feature)가 들어와 처리된 결과를 다음 퍼셉트론으로 보내는 것으로 생각해 보면 됩니다.

보통 전류와 비교하게 되는데, 전류는 보통 실수값( ex : n볼트의 전류 )이 흐르지만, 지금 배워볼 우리의 퍼셉트론에서는 오로지 흐른다(1) / 흐르지 않는다.(0)으로 표현합니다.

이번 장에서는 1 => 흐른다 / 0 => 흐르지 않는다.로 표현하겠습니다.

 

신호가 퍼셉트론을 통과하는 과정

우리의 뇌는 여러가지 신호를 동시에 받아, 뇌에 있는 뉴런뉴런이라는 존재가 이 신호들을 처리합니다.

생물학적으로 뇌에 부담이 가지 않는 범위를 결정지어 주기 위해 신호별 가중치(weight)가 존재하는데요, 이 가중치가 존재하는 이유는 뇌가 받은 전기적 신호에 대해 적절하게 반응하기 위해 설정되어야 하기 때문입니다.

예를 들어 친구들과 게임을 하다가 이마를 맞았을 때( 입력 신호 ) 아픈 느낌이 점점 사라져야겠죠? 처음 입력받은 신호(자극)가 계속 사라지지 않고 유지되면 살 수가 없습니다. 따라서 처음 이마를 맞았을 때 받은 신호를 점점 줄여가는 식으로 가중치라는 것이 각 신호마다 곱해지게 됩니다.

이제는 퍼셉트론에 빗대어서 이야기해보겠습니다. 입력 신호( 입력 특성 )가 두 가지라고 가정해 보겠습니다. 그리고 이 신호들을 각각 x1, x2라고 가정해 보겠습니다.

퍼셉트론은 뇌의 뉴런과 대응되기 때문에 각각의 신호에 대한 가중치도 있다고 가정해 볼 수 있는데요. 이 각각의 가중치들을 w1, w2라고 가정하겠습니다.

 

본격적으로 퍼셉트론을 알아보자~!

입력 신호( x1, x2 )와 각 신호들에 대한 가중치( w1, w2 )가 준비되었습니다.신경망에서 입력 신호를 가중치와 함께 처리하여 출력하는 것을 뉴런 또는 노드라고 합니다. 즉 퍼셉트론 = 뉴런(노드)라고 이야기할 수 있습니다.

퍼셉트론에서 입력 신호를 받을 때는 각 가중치가 곱해지면서 받아집니다. 수식으로 표현하면 각 퍼셉트론마다 $w_1x_1 + w_2x_2$ 의 계산이 된다고 볼 수 있겠네요

 

퍼셉트론의 출력 결과는 흐른다(1) / 안 흐른다(0)

입력 특성과 가중치가 곱해져서 퍼셉트론에 흘러 들어오고, 거기에 대한 결과값이 나온 다는 것이 확인되었습니다.

여기서 중요한 것은, 각 퍼셉트론마다정해진 한계값이라는 것이 있습니다. 이것을 임계값( $\theta$ theta )이라고 합니다. 퍼셉트론로 들어온 입력 값들과 가중치가 곱해진 결과물의 합이 임계값 $\theta$를 넘어가면 0으로 출력, 넘어가지 않으면 1로 출력됩니다. 수식으로 표현하면 다음과 같습니다.

이를 반응 조건 계산식이라고 하겠습니다.

위의 수식을 일반화한 그래프를 그려 보도록 하겠습니다. 각 특성별 가중치는 임의로 0.5, 0.5로 설정하고, 임계값 𝜃는 2로 임의로 설정하겠습니다.

import matplotlib.pyplot as plt
import numpy as np
  • 사전에 알 수 있는 값 : 특성 값
  • 가중치는 아직 우리가 몰라요 : 모델이 학습하는 값

def func1(x1, x2):
    w1, w2, theta = 0.5, 0.5,  2 # 가중치를 각각 0.5, 0.5로 설정하고, 임계값은 2로 설정
    y = w1*x1 + w2*x2
    
    return y, y >= theta

# 입력 데이터 생성
x1 = np.linspace(-3, 7, 100)
x2 = np.linspace(-3, 7, 100)

y_value, y_result = func1(x1, x2)
plt.plot(y_value, y_result)
plt.yticks([0, 1])
plt.xticks([-3,2,7], [r'-$\infty$', r'$\theta$', r'-$\infty$'])
plt.xlabel(r"$w_1x_1+w_2x_2$")
plt.ylabel("y", rotation=0)
plt.show()

중간에 𝜃 를 기준으로 𝑤1𝑥1+𝑤2𝑥2의 결과가(y축) 0, 1로 결정지어지는 것이 확인됩니다. 또한 위 그래프를 함수로 일반화하면 다음과 같습니다. 이러한 함수를 단위 계단 함수(u)라고 합니다.

계산 식 𝑧의 결과가 계단 함수 u의 인자로 전달되었을 때 0보다 크면 1, 0보다 작으면 0으로 설정되는 것이 확인됩니다. 계단 함수 u(𝑧)의 그래프는 다음과 같습니다.

z = np.linspace(-3, 3, 100)
plt.plot(z, z > 0)
plt.yticks([0, 1])
plt.xticks([-3, 0, 3],[r'-$\infty$', r'$0$', r'-$\infty$'])
plt.xlabel(r"$z$")
plt.ylabel(r"$y=\mathcal{u}(z)$", rotation=0)
plt.show()

이항 법칙에 따라 반응 조건 계산식의 𝜃를 이항 하고, 계단 함수를 적용하면 다음과 같은 수식이 완성됩니다.

결과적으로 각 퍼셉트론의 입력 특성(x1, x2)과 가중치(w1, w2)를 이용한 결과를 계단 함수 u로 표현하기 위한 인자 𝑧


 

가 됩니다. 참고로 계단 함수의 인자로 들어가는 것을 퍼셉트론의 가중입력 이라고도 표현합니다. 계단 함수의 결과에 따라 𝑦의 결과물은 반응 조건 계산식 𝑤1𝑥1+𝑤2𝑥2𝜃 에 따라 항상 0 또는 1의 값만 가지게 됩니다.

 

단층 퍼셉트론 vs 다층 퍼셉트론

퍼셉트론은 이처럼 입력 값에 따른 가중치를 이용해서 출력 값을 내게 됩니다. 이번 예제는 하나의 퍼셉트론만 이용하는 단층 퍼셉트론의 한계에 대해 알아보고, 이를 해결할 수 있는 다층 퍼셉트론에 대해 알아봅니다.

 

AND Gate

논리 회로 게이트 문제를 이용해 알아보도록 하겠습니다. 먼저 가장 유명한 게이트죠? AND 게이트의 진리표를 확인해 보겠습니다. 

AND GATE 입력 𝑥1,  𝑥2, 출력 𝑦

AND 게이트의 특징은 입력값 𝑥1, 𝑥2가 둘 다 1일 때만 결과물이 1이 나옵니다. 이제 저희가 해야 할 일은 퍼셉트론에 적용되는 매개변수인 각각의 가중치들과 임계값을 정하는 일들입니다. AND 게이트의 매개변수의 조합은 무수히 많습니다. 여기는 𝑤1, 𝑤2를 각각 0.5로 설정하고, 𝜃는 0.7로 사용해 보겠습니다. 다음은 NAND와 OR 게이트입니다.

 

NAND Gate

NAND 게이트는 Not AND라는 뜻을 가지고 있으며, AND 게이트의 반대 동작을 하게 됩니다. 다음은 NAND 게이트의 진리표입니다.

NAND GATE 입력 𝑥1,  𝑥2, 출력 𝑦

입력 특성 𝑥1 𝑥2가 둘 다 1일 때만 0이고, 나머지 상황에 대해서는 모두 1이 출력되는 것이 확인됩니다. NAND 게이트의 가중치와 임계치는 AND 게이트 매개변수 부호의 반대를 넣어 주면 됩니다. 즉 위의 AND 게이트의 가중치와 임계값의 부호를 바꾼 NAND 게이트의 𝑤1, 𝑤2, 𝜃 는 각각 -0.5, -0.5, -0.7로 설정되면 됩니다.

 

OR Gate

OR 게이트는 입력 신호 중 하나 이상이 1 이상이면 출력이 1이 되는 게이트입니다. 진리표를 바로 확인해보겠습니다.

OR GATE 입력 𝑥1,  𝑥2, 출력 𝑦

각 진리표를 이용해 모두 퍼셉트론으로 AND, NAND, OR 게이트를 표현할 수 있습니다. 여기서 중요하게 생각해야 할 것은 퍼셉트론의 구조 자체는 AND, NAND, OR 게이트 모두 똑같다는 것입니다. 다른 점은 각 게이트의 매개변수(가중치와 임계값)만 다르다는 점입니다. 즉, 퍼셉트론의 매개변수 값만 적절히 조절하면 하나의 퍼셉트론이 AND, NAND, OR로 변신할 수 있다는 것입니다.

 

Gate의 퍼셉트론화

먼저 가장 기본 공식인 𝑤1𝑥1+𝑤2𝑥2𝜃를 함수와 시킨 AND게이트의 퍼셉트론입니다.

def AND(x1, x2):
    w1, w2, theta = 0.5, 0.5, 0.7
    tmp = w1 * x1 + w2 * x2 - theta
    
    if tmp <= 0 :
        return 0
    elif tmp > 0 : 
        return 1

AND(0, 0), AND(1, 0), AND(0, 1), AND(1, 1)

생각했던 대로 잘 동작하는 것 같습니다! NAND와 OR 게이트도 매개변수만 적절히 조절하면 똑같이 만들 수 있겠습니다만, 위의 함수를 약간만 손보도록 하겠습니다.

편향(bias) 도입

나름 𝑤1𝑥1+𝑤2𝑥2𝜃 같은 계산 식도 활용하기는 좋지만, 𝜃  𝑏로 치환하는 것이 더 보기도 좋고 일반적입니다. 즉, 우리가 앞으로 볼 퍼셉트론의 일반화된 식은


 

로 표현 됩니다. 결론을 내자면 퍼셉트론은 입력 신호에 가중치를 곱한 값과 편향(bias)을 합하여, 그 값이 0을 넘으면 1로, 그렇지 않으면 0이 출력된다고 일반화해볼 수 있겠습니다. 약간 프로그래밍 적으로, 입력 특성과 가중치를 두 개만 사용했지만, 여러 개의 입력 특성과 가중치가 있을 수 있으므로, 넘파이 배열을 이용해서 계산해보도록 하겠습니다. 위의 편향을 사용한 일반화된 공식에서 배열 곱에 대해 잠시 살펴보겠습니다. 위에서 구현한 AND 함수의 매개변수를 그대로 사용합니다.

# theta를 -b로 치환시킨 AND 게이트
def AND(x1, x2):
    x = np.array([x1, x2])
    w = np.array([0.5, 0.5])
    b = -0.7
    
    tmp = np.sum(w*x) + b # 계산
    
    if tmp <= 0:
        return 0
    else:
        return 1
    
AND(0, 0), AND(1, 0), AND(0, 1), AND(1, 1)

간단히 𝜃가 편향 𝑏로 치환된 것을 확인할 수 있습니다!

여기서 중요한 점은 가중치와 편향의 기능은 다르다는 것인데요, 가중치의 역할은 입력 신호가 결과에 주는 영향력(중요도)을 조절하는 매개변수고, 편향은 퍼셉트론이 얼마나 쉽게 활성화( 1로 출력 ) 되는가를 조정하는 매개변수입니다. 즉 편향이 크면 퍼셉트론의 흥분도가 커져 쉽게 활성화되고( 민감 ), 편향이 작으면 흥분도가 낮아 활성화가 잘 안 된다(둔감) 고 생각하면 됩니다. 이어서 NAND와 OR Gate를 모두 구현해 보도록 하겠습니다.

def NAND(x1, x2):
    x = np.array([x1, x2])
    w = np.array([-0.5, -0.5]) # AND 게이트의 매개 변수 부호를 반대로
    b = 0.7 
    
    tmp = np.sum(w * x) + b
    if tmp <= 0:
        return 0
    else:
        return 1

NAND(0, 0), NAND(1, 0), NAND(0, 1), NAND(1, 1)

def OR(x1, x2):
    x = np.array([x1, x2])
    w = np.array([0.5, 0.5])
    b = -0.2 # 적절하게 편향만 조절하여 OR 게이트를 구현!
    tmp = np.sum(w*x) + b
    
    if tmp <= 0:
        return 0
    else:
        return 1

OR(0, 0), OR(1, 0), OR(0, 1), OR(1, 1)

단층 퍼셉트론

여태 AND, NAND, OR 게이트를 이야기하다 갑자기 단층 퍼셉트론이라는 주제로 바뀌었습니다.

하지만 여러분들이 구현한 위의 함수들이 곧 퍼셉트론이 된다는 사실! 그렇다면 단층 퍼셉트론이 어떤 것인지 알아보죠, 위의 각 퍼셉트론들은 각각 입력값을 받아 각자의 역할을 수행합니다. 즉 한 번의 입력에 한번의 출력이 이루어 진다는 이야기가 되겠죠. 이처럼 한번의 입력, 한번의 결과를 내는 퍼셉트론들을 단층 퍼셉트론이라고 합니다.

 

단층 퍼셉트론의 한계, XOR 게이트 구현

XOR 게이트를 하나의 퍼셉트론에서 구현할 수 있을까요? XOR 게이트란 배타적 논리합이라는 논리회로로써𝑥1 𝑥2 중 한쪽의 값만 1일 때 결과가 1이 됩니다. 즉 𝑥1, 𝑥2 가 똑같은 입력이라면 그 결과가 0이 된다고 볼 수 있습니다. 다음은 XOR 게이트의 진리표입니다.

XOR GATE 입력 𝑥1, 𝑥2, 출력 𝑦

지금까지 AND, NAND, OR 게이트를 만들었던 것처럼 XOR 게이트도 하나의 퍼셉트론으로 처리할 수 있을까요?

 

정답은 XOR 게이트는 단일 퍼셉트론으로 처리할 수 없다!

임의로 OR 게이트의 매개변수 𝑤1,𝑤2, 𝑏 = ( 1.0, 1.0, -0.5 )로 설정했다고 생각해 보겠습니다. 𝑥1+𝑥2−0.52−0.5라는 식으로 표현가능 할 것 같습니다. 간단히 결과만 살펴보면 𝑥1 또는 𝑥2 가 1만 넘어가면 결과가 양수가 되기 때문에 좌표평면에 그래프를 그려보면 완벽하게 직선 그래프를 이용해 간단히 그려질 것 같습니다.

  • XOR는 어떻게 구현해야 할까
  • XOR : 배타적 논리합
# OR 시각화

plt.figure(figsize=(4,4))

plt.scatter([0],[0], marker='o')
plt.scatter([1,0,1],[0,1,1], marker='^')
plt.xticks([0, 0.5, 1])
plt.yticks([0, 0.5, 1])
plt.xlim((-0.2,1.2))
plt.ylim((-0.2,1.2))
plt.xlabel(r'$x_1$')
plt.ylabel(r'$x_2$', rotation=0)

plt.show()

직선 한 개를 그어서 위의 점들을 두 분류(동그라미, 세모)로 나눌 수 있을까요? 당연히 할 수 있습니다..! 이처럼 OR 게이트를 만들면 직선으로 1 또는 0으로 나눠지는 구간을 그려줄 수 있습니다.

 

하지만 XOR은 이야기가 좀 다릅니다!

XOR 진리표를 이용해 그래프를 찍어 보면 다음과 같습니다.

# XOR 시각화
plt.figure(figsize=(4,4))

plt.scatter([0,1],[0,1], marker='o')
plt.scatter([1,0],[0,1], marker='^')
plt.xticks([0, 0.5, 1])
plt.yticks([0, 0.5, 1])
plt.xlim((-0.2,1.2))
plt.ylim((-0.2,1.2))
plt.xlabel(r'$x_1$')
plt.ylabel(r'$x_2$', rotation=0)

plt.show()

XOR은 위의 그래프처럼 교차 되어있는 형태 이기 때문에 하나의 직선으로 점들을 구분하기는 불가능합니다.

단층 퍼셉트론의 한계가 보이시나요?

퍼셉트론 자체가 직선 하나의 영역으로 나눈 영역만 설정할 수 있다는 한계가 있습니다. 위의 XOR 같은 경우는 곡선 또는 여러 번의 직선을 그어야 구분할 수 있는데요, 이때 직선 하나로만 영역을 나누는 것을 선형으로 표현하고, 그 외에 곡선이나 끊긴 직선들은 비선형으로 표현합니다. 따라서 단층 퍼셉트론으로는 XOR 게이트를 표현할 수 없다. 즉 단층 퍼셉트론으로는 비선형 영역을 분리할 수 없다로 이해할 수 있겠습니다.

어떻게 해결하는가?

단층 퍼셉트론의 층을 쌓으면 된다!

각 게이트에 대한 중요한 이야기를 다루지 않기 때문에, 결과적으로만 이해해보겠습니다. XOR 게이트는 지금까지 위에서 이야기했었던 AND, NAND, OR 게이터를 적절하게 조합하면 구현해 낼 수 있습니다. 방법은 바로 NAND의 결과와 OR의 결과를 AND 게이트의 결과물로 사용하면 됩니다. 진리표로 확인해 보겠습니다.

입력&nbsp; 𝑥1, &nbsp; 𝑥2 &nbsp;=> NAND 결과 𝑠1/ OR 결과 𝑠2/ AND 결과 𝑦

 

𝑥1 𝑥2의 입력 값에 따라 출력되는 결과물 𝑦의 값이 위에 작성했던 XOR 진리표와 일치하는 것을 볼 수 있습니다. XOR 게이트를 함수로 구현하면 다음과 같습니다.

def XOR(x1, x2):
    s1 = NAND(x1, x2)
    s2 = OR(x1, x2)
    y = AND(s1, s2)
    
    return y

XOR(0, 0), XOR(1, 0), XOR(0, 1), XOR(1, 1)

정확하게 XOR이 구현된 것이 확인됩니다!

입력값 𝑥1, 𝑥2에 대해 각각 다른 류의 퍼셉트론인 NAND, OR 퍼셉트론을 통과시켰고, 그 결과를 다시 하나의 퍼셉트론인 AND 게이트에 통과시켰습니다. 이처럼 두 개 이상의 퍼셉트론 층이 생기는 구조를 다층 퍼셉트론이라고 합니다.

 

정리하자면 NAND, OR 게이트는 𝑥1, 𝑥2를 전달받아 각자의 작업을 한 후 최종적으로 AND 게이트에 부품을 전달하는 역할만 수행하게 되고 AND 게이트는 전달받은 부품으로 최종 결과물을 수행해 내는 역할을 하게 됩니다. 위처럼 2층 이상의 다층 퍼셉트론을 이용하면 단층 퍼셉트론으로는 표현하지 못한 것을 층을 늘려 구현할 수 있습니다. 퍼셉트론은 층을 쌓아 더 다양한 것을 표현해 낼 수 있습니다.

 

지금까지 단층 & 다층 퍼셉트론에 대하여 알아보았습니다. 다음 포스팅에서는 신경망과 활성화 함수에 대하여 알아보도록 하겠습니다~ ;)

728x90
반응형
LIST
728x90
반응형
SMALL

이번 포스팅에서는 딥러닝에서 필연적으로 마주하게 되는 기본 Numpy에 대하여 알아보도록 하겠습니다.

 

numpy는 내부적으로 C로 구현이 되어 있으며, 수치에 관련된 프로그래밍을 할 때 일반 python보다 훨씬 빠릅니다.

특히, 배열 연산에 특화되어 있습니다.

한번 예제를 통하여 바로 알아보도록 하겠습니다!

 
import numpy as np

# 일반 파이썬 리스트를 numpy 배열화 ( numpy 배열 : ndarray )
py_list = [1, 2, 3]
arr1 = np.array(py_list)
arr1

# ndarray 타입 확인하기
type(arr1)

py_list = [ [1, 2, 3],
            [4, 5, 6] ]

arr2 = np.array(py_list)
type(arr2)

numpy에서는 숫자 배열을 만들기 위한 여러 가지 함수를 제공합니다.

np.arrange()
# np.arange 함수는 파이썬의 range 함수와 매우 흡사하다.
arr = np.arange(10) # 0 ~ 9 까지 들어있는 숫자 배열을 생성
arr

arr = np.arange(1, 10) # 1 ~ 9 까지 들어있는 숫자 배열을 생성
arr

arr = np.arange(1, 10, 2) # 1부터 9까지 2씩 띄워가면서 숫자 배열 생성
arr


다차원 배열 만들기
  • np.zeros
  • np.ones
  • np.full
np.zeros: 지정한 차원에 모든 원소를 0으로 채워주는 함수
np.zeros(5) # 1차원 배열에 5개의 0을 채운다.

np.zeros((4, 5)) # 4행 5열(2차원 배열)의 배열에 0을 채운다.

np.zeros((3, 4, 5))

np.ones: 지원한 차원에 모든 원소를 1로 채워주는 함수
np.ones(5)

np.ones((4,5))

np.ones((4, 5)) * 3

np.full: 지정한 차원에 모든 원소를 원하는 수로 채우는 함수
np.full((3,4),7)


단위행렬 만들기
  • np.eye
  • 단위 행렬 : 대각선 방향의 원소가 1이고, 나머지 원소는 0인 행렬
np.eye(3)

np.eye(3, 4)


구간 나누기
  • np.linspace
np.linspace(1, 10, 3) # 1부터 10까지 2개의 구간 만들기

np.linspace(1, 10, 4)


Random을 사용하기 위한 numpy
  • 보통 랜덤한 배열은 딥러닝에서 매개변수의 초기화를 하기 위해 사용
  • 딥러닝에서는 일반적으로 Xavier 초깃값, He 초깃값, 정규분포 초깃값을 계산해 준다.
np.random.rand: 완전한 랜덤 만들기 (정규분포나 균등분포가 아닌 랜덤값)
np.random.randn(10)

np.random.uniform: 균등분포 랜덤값 만들기
np.random.uniform(1.0, 3.0, size=(4, 5)) # 1.0 ~ 3.0 까지 균등분포 랜덤값 생성


정수 랜덤 만들기
np.random.randint
np.random.randint(1, 100, size=(5, )) # 1차원 배열의 원소 5개에 1 ~ 100까지의 랜덤값으로 채우기

np.random.randint(1, 100, size=(5, 1)) # 2차원 배열에 랜덤 정수 채우기

np.random.randint(1, 100, size=(3, 4, 5))


정수 랜덤 샘플링 하기
np.random.choice
  • 랜덤 샘플링 : 일정한 범위 내에서 필요한 정수를 랜덤으로 추출
  • 미니배치를 만들 때 사용
np.random.choice(100, size=(3, 4))

# 임의의 배열을 만들어서 그 안에서 랜덤하게 추출할 수 있도록 함
arr = np.arange(1, 6)

np.random.choice(arr, size=(2, 2), replace=False) # replace가 True면 원소를 중복 추출, False면 중복 추출 X


인덱스와 슬라이싱
arr = np.arange(1, 11).reshape(2, 5)
arr

arr[:, 2] # 2차원 배열 내에서 1차원 배열은 전체 선택, 0차원 스칼라값은 2번째 인덱스에 있는 것을 선택

arr = np.arange(1, 37).reshape(3, 4, 3)
arr

arr[:, :2, :2]


배열의 차원과 형상
  • 형상( shape )
  • 차원수( ndim )
arr = np.arange(12).reshape(3, 4)
arr

arr.shape

arr.ndim


차원수 확장하기
  • 항상 같은 예시는 아니지만, 보통은 이미지 분석( CNN )을 하기 위해 데이터를 전처리 하면서 사용된다.
  • tensor flow는 tf.newaxis를 활용, numpy는 np.newaxis를 활용해서 추가 차원을 만들어 낼 수 있다.
arr

arr.shape

tmp_arr = arr[np.newaxis, :, :, np.newaxis] # arr의 첫 번째와 마지막에 차원을 1씩 확장
tmp_arr

# 실제 데이터를 다룰 때는 실제 데이터를 보는 것이 아닌, shape만 보자..
tmp_arr.shape

# 각 차원이 1인 차원 삭제하기
tmp_arr2 = np.squeeze(tmp_arr)
tmp_arr2.shape

spread 연산자 활용하기
  • 슬라이싱 도중에 특정 차원부터는 전체를 선택할 때 사용한다.
tmp_arr3 = arr[np.newaxis, ..., np.newaxis] # ... -> spread 연산자
tmp_arr3.shape


배열의 형상 (shape) 바꾸기
  • np.newaxis를 활용해서 축을 추가한다. ( 차원수를 늘린다. )

평탄화

  • ravel : 원본 배열을 평탄화시킨참조배열을 만들어 낸다. ( View 또는 Reference를 만들었다고 표현한다. )
  • flatten : 평탄화시킨 복사된배열을 만들어 낸다.

형상 변환

  • reshape
ravel 사용하기
x = np.arange(15).reshape(3, 5)
x.shape

print("원본 x : \n{}".format(x))

temp = np.ravel(x)
print("ravel로 평탄화된 x의 shape : {}".format(x.shape))
print("ravel로 평탄화된 x : \n{}".format(temp))

ravel함수는 원본의 참조본을 만들어 낸다.

print(temp[0])

temp[0] = 100
print(x)

flatten 사용하기
# flatten은 복사본을 만들어 낸다.
y = np.arange(15).reshape(3, 5)
temp2 = y.flatten()
print("원본 y : \n{}".format(y))
print("flatten으로 평탄화된 y의 shape : {}".format(temp2.shape))
print("flatten으로 평탄화된 y : \n{}".format(temp2))

temp2[0] = 100 # flatten을 활용해서 y의 복사본을 만들어 냈기 때문에 원본 y에는 영향을 미치지 않는다.
print(temp2)
print(y)

reshape 사용하기
x = np.arange(20)
x

reshape 팁 : reshape 함수 내의 모든 숫자를 곱했을 때 스칼라 원소의 개수와 같기만 하면 된다.

x.reshape(2, 5, 2)

-1 reshape()에 넣으면 남는 숫자를 자동으로 계산해 준다.

x.reshape(2, 2, -1) # 2 * 2 는 4니까, -1은 5를 의미하게 된다.

x.reshape(2, -1, 2)

x.reshape(3, -1) # 오류... 한쪽 차원의 수가 3이면 원소의 개수인 20만큼 딱 떨어지지 않기 때문에 오류


브로드 캐스팅
  • 차원 수가 다른 배열끼리의 연산
  • 차원이 달라도 0차원 스칼라의 개수가 똑같으면 연산이 가능
  • 저 차원의 배열을 고차원으로 확장
x = np.arange(20).reshape(4, 5)
y = np.arange(20, 40).reshape(4, 5)

print(x)
print()
print(y)

x.shape, y.shape

x + y

x - y

x * y

원소의 개수와 차원이 같으면 자연스럽게 원소끼리 연산을 할 수 있다.

x * 2

x는 2차원, 2는 0차원 스칼라 값이다. 2의 차원이 곱해지는 배열인 x shape으로 확장된다.

# shape이 다를 때의 연산
a = np.arange(12).reshape(4, 3) # ( 4, 3 )
b = np.arange(100, 103) #  (3, )
c = np.arange(1000, 1004) # (4, )
d = b.reshape(1, 3) # (1, 3)

# b와 d의 다른점 : b는 1차원, d는 2차원
a + b

a + c

a + d


전치행렬 만들기
  • 행렬 A의 행과 열의 인덱스를 바꾼 것을 전치행렬이라고 한다.
  • 𝐴(𝑖,𝑗)의 위치를 𝐴(𝑗,𝑖)로 바꾼 것
  • 보통 Transpose 했다고 이야기한다.
e = np.arange(6).reshape(2, 3)
e

# ndarray에서 T만 호출해 주면 된다.
e.T

특히 역전파에 사용된다!

지금까지 파이썬을 활용하여 딥러닝을 위한 numpy의 기초를 총 정리 해보았습니다.

다음 블로그에서는 퍼셉트론에 관하여 알아보도록 하겠습니다.

728x90
반응형
LIST

+ Recent posts