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

+ Recent posts