728x90

퍼셉트론과 신경망

퍼셉트론과 신경망은 공통점이 많다고 한다.

중간에 은닉층은 사람에게는 아예 보이지 않기 때문에 은닉층이라고 한다.

그냥 간단하게 퍼셉트론이 연결되어 있는 상태라고만 생각해도 괜찮다.

 

이런 퍼셉트론의 수식은 다음과 같았다.

이 수식을 좀 편집해서 다음과 같이 나타내보자.

이 함수를 y라고 하면 저 위의 식을 아래와 같이 표현가능하다.

그냥 입력 신호 x가 h(x)라는 함수를 거쳐서 0을 넘으면 1을 주고, 아니면 0을 돌려주는 단순한 함수가 된다.

 

이렇게 h(x)처럼 입력 신호의 총합을 출력 신호로 변환하는 함수를 활성화 함수(activation function)이라고 한다.

라고 한다면, 저 위의 y는 h(a)로 표현이 가능하다.

y = h(a)로 먼저 a를 구하고, 이 a가 0을 넘는지 아닌지에 따라 0과 1을 출력하는 함수가 되는 것이다.

 

활성화 함수

그럼 이제 활성화 함수는 임곗값을 경계로 출력이 바뀌는데, 이런 함수를 계단함수라고 한다.

갑자기 어떤 값에서 일정해지던 출력이 아예 바뀌는 것을 말한다.

퍼셉트론은 계단 함수를 사용하고 있지만, 이 활성화 함수를 다른 함수로 변경하는 것이 신경망의 첫번째 step이다.

 

  • 시그모이드 함수

신경망에서 가장 대표적으로 사용하는 함수라고 한다.

e는 공학에서 많이 나오는 자연상수이며, 일단은 그냥 단순한 함수이다.

 

  • 계단 함수 구현하기

일단은 간단하게 0을 기준으로 값이 바뀌는 계단 함수를 구현해보자.

def step_function(x: float):
  return 1 if x > 0 else 0

이러면 0.0000000000000~~~1부터는 값이 1로 뿅 바뀌게 될 것이다.

 

이 함수를 직접 그려보자.

import numpy as np
import matplotlib.pyplot as plt

x = np.arange(-5.0, 5.0, 0.01)
y = step_function(x)
plt.plot(x, y)
plt.ylim(-0.1, 1.1)
plt.show()

 

  • 시그모이드 함수 구현하기

이번에는 계단함수가 아닌 시그모이드 함수를 그려보자.

이 식 자체의 함수는 다음과 같이 작성한다.

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

 

뭐 일단 -2, -1, 0, 1, 2를 넣어보면 값은 다음과 같다.

이렇게 0과 1 사이의 값이 나온다.

 

다음과 같은 코드로 그래프를 그려보면

x = np.arange(-5.0, 5.0, 0.01)
y = sigmoid(x)
plt.plot(x, y)
plt.ylim(-0.1, 1.1)
plt.show()

이거를 계단 함수와 비교를 해보면, 둘 다 반환값이 0~1이지만 시그모이드는 매끄러운 값을, 계단 함수는 0 혹은 1만을 반환한다.

이렇게 시그모이드는 입력이 클수록 1에 가까워지는 구조이다.

 

다차원 배열의 계산

  • 행렬의 곱

행렬의 곱은 n x m, m x k -> n x k 뭐 이런게 있는데, 대학 다니면서 배웠을테니 여기서는 그냥 넘어가도록 하겠다.

numpy에서는 np.dot으로 행렬곱을 계산하며, 입력이 1차원 배열이면 벡터곱을, 2차원 배열이면 행렬 곱을 계산한다.

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

np.dot(A, B)

 

  • 신경망에서의 행렬 곱

x = [1, 2]

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

이라고 한다면 행렬곱으로 y1, y2, y3를 구할 수 있다.

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

Y = np.dot(X, W)
Y

 

이러면 앞으로 신경망에서 각 층을 빠르게 계산 할 수 있을 것 같다.

 

3층 신경망 구현하기

여기에서도 계산을 해보려면 0층 -> 1층 계산, 1층 -> 2층 계산 이렇게 과정을 거치면 가능해보인다.

 

우선 1층의 가장 위 노드로 가는 신호를 분석해보자.

우선 편향을 추가하고, 편향에서 오는 값은 b가 그대로 오기 때문에 무조건 1이 들어오게 된다.

그렇게 된다면

a1-1 = (x1 * w1-(1->1)) + (x2 * w1-(2->1)) + (b1-1)

이러면 이 1층은 다음과 같이 바꿀 수 있다.

A1 = X * W1 + B1

 

A1 = (a1-1, a1-2, a1-3)

X = (x1, x2)

B1 = (b1-1, b1-2, b1-3)

로 나타 낼 수 있고

W1 = [[w1-(1->1), w1-(2->1), w1-(3->1)], [w1-(1->2), w1-(2->2), w1-(3->2)]]

이렇게 나타낼 수 있다.

 

이렇게 a1-1만 먼저 구해본다면

X = np.array([1.0, 0.5])
W1 = np.array([[0.1, 0.3, 0.5], [0.2, 0.4, 0.6]])
B1 = np.array([0.1, 0.2, 0.3])

A1 = np.dot(X, W1) + B1
A1

이렇게 작성 할 수 있고, 결과는 아래와 같다.

이제 이 0.3, 0.7, 1.1을 그대로 활성화 함수에 넣어주면

 

이렇게 나오게 되며, 이제 또 이거를 x로 생각하여 다시 그대로 다음 노드에도 가중치 행렬 계산하고, 활성화 함수로 값을 넣어줘야 한다.

그렇게 나머지를 모두 구현하면 다음과 같다.

def init_node()-> dict[str, np.ndarray]:
  nodes: dict[str, np.ndarray] = {}
  # 0층 -> 1층 가중치
  nodes['W1'] = np.array([[0.1, 0.3, 0.5], [0.2, 0.4, 0.6]])
  # 0층 편향
  nodes['b1'] = np.array([0.1, 0.2, 0.3])
  # 1층 -> 2층 가중치
  nodes['W2'] = np.array([[0.1, 0.4], [0.2, 0.5], [0.3, 0.6]])
  # 1층 편향
  nodes['b2'] = np.array([0.1, 0.2])
  # 2층 -> 3층 가중치
  nodes['W3'] = np.array([[0.1, 0.3], [0.2, 0.4]])
  # 2층 편향
  nodes['b3'] = np.array([0.1, 0.2])
  return nodes


def forward(nodes: dict[str, np.ndarray], x: np.ndarray) -> np.ndarray:
  W1, W2, W3 = nodes['W1'], nodes['W2'], nodes['W3']
  b1, b2, b3 = nodes['b1'], nodes['b2'], nodes['b3']

  # 1층
  a1 = np.dot(x, W1) + b1
  z1 = sigmoid(a1)

  # 2층
  a2 = np.dot(z1, W2) + b2
  z2 = sigmoid(a2)

  # 3층(출력층)
  a3 = np.dot(z2, W3) + b3
  y = a3

  return y

nodes = init_node()
x = np.array([1.0, 2.0])
y = forward(nodes, x)
y

여기에서는 단순히 0, 1, 2, 3층이 고정적으로 있다고 생각하고, 그거만 받아서 계산하기에 조금 더 범용적으로 사용하기 위해서는 수정해야한다.

 

출력층 설계하기

위에서는 결과를 그대로 y = a3에 그냥 대입해서 그대로 출력해줬다.

이렇게 입력과 출력이 같은 함수를 항등함수라고 한다.

 

분류에서는 소프트맥스라는 다른 함수를 사용한다고 한다.

exp는 많이 보던대로 e^x의 지수함수이다.

n은 출력층의 뉴런 수, y_k는 그 중 k번째의 출력임을 뜻한다.

 

그냥 간단하게 파이썬만 사용해서 계산을 해보면

a = np.array([0.3, 2.9, 4.0])

exp_a = np.exp(a)
print(exp_a)

sum_exp_a = np.sum(exp_a)
print(sum_exp_a)

y = exp_a / sum_exp_a
print(y)

그냥 단순하게 다 더해서 나누면 된다.

 

이거를 일단 함수로 빼서, 편하게 계산할 수 있도록 해보자.

 

def softmax(a):
  exp_a = np.exp(a)
  sum_exp_a = np.sum(exp_a)
  y = exp_a / sum_exp_a

  return y

 

근데 여기에는 치명적인 문제가 있다고 한다.

exp() 함수 자체가 지수함수이기 때문에, 적당한 값만 넣어도 무한대 혹은 오버플로우가 발생한다.

 

그렇기에 이 비율?을 유지한채로 오버플로우가 발생하지 않도록 해야한다.

다음식을 보면

결국 저 처음의 식이 a 값에 같은 수를 더한 값과 같은 것을 볼 수 있다.

보통은 오버플로를 막기 위해 신호의 최대값을 사용한다고 한다.

 

이렇게 같은 값을 빼면 반환값이 같은 것을 볼 수 있다.

 

이 방법을 통해 다음과 같이 원래는 오버플로우가 생기는 값들도 계산이 가능하다.

소프트맥스에서의 반환값도 계단함수, 시그모이드와 같이 0에서 1 사이의 값을 가진다.

그렇기에 확률로도 사용할 수 있다.

0에 가까울수록 낮고, 1에 가까울수록 높은 확률로 말이다.

 

신경망을 이용한 분류에서는 일반적으로 가장 큰 값을 출력하는 뉴런만 인식한다.

소프트맥스에서도 대소관계는 뒤집히지 않기에 소프트맥스를 사용할 수 있으며, 그렇기에 출력층에 소프트맥스 함수의 존재를 생략하기도 한다.

 

'인공지능 > 딥러닝' 카테고리의 다른 글

학습 관련 기술들  (0) 2026.06.21
오차역전파법  (0) 2026.06.20
신경망 학습  (0) 2026.06.15
퍼셉트론  (0) 2026.06.08

+ Recent posts