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

지난 글을 통해 계산 그래프의 역전파가 연쇄 법칙에 따라서 진행되는 모습을 이야기해 보았습니다. 

2024.02.15 - [Programming/Deep Learning] - [Python/DeepLearning] #10.3. 역전파) 계산 그래프를 통한 역전파 이해

 

[Python/DeepLearning] #10.3. 역전파) 계산 그래프를 통한 역전파

지난 시간, 수식을 통한 오차역전파법에 대하여 이해해 보았습니다. 2024.02.08 - [Programming/Deep Learning] - [Python/DeepLearning] #10.2. 역전파) 수식을 통한 오차역전파

yuja-k.tistory.com

이번에는 덧셈 노드와 곱셈 노드의 역전파의 구조에 대하여 알아보도록 하겠습니다!

 

단순한 계층부터 구현하기

 

수치적인 오차역전파법, 계산 그래프를 이용한 역전파의 의미에 대해 살펴보았습니다. 본격적으로 파이썬으로 역전파를 구현하고, 이를 신경망에 집어넣어 실제 오차역전파를 구현해 보도록 하겠습니다. 일전에 수식을 이용해 오차역전파법을 공부했고, 계산 그래프를 이용해 쇼핑에 대한 역전파도 공부해 보았습니다. 신경망이라고 해서 별 다를 것이 없는 게, 신경망의 모든 계산은 덧셈과 곱셈을 이용해 수행됩니다. 따라서 아주 간단한 덧셈 계층과 곱셈 계층부터 구현해 보도록 하겠습니다. 모든 계층은 순전파와 역전파를 모두 처리할 수 있도록 공통의 메소드를 구현할 것입니다.

  • forward : 순전파
  • backward : 역전파

 

덧셈 노드의 역전파

먼저 𝑧=𝑥+𝑦라는 식을 대상으로 역전파를 살펴보도록 하겠습니다. 우선 𝑧=𝑥+𝑦의 미분은 다음과 같이 계산할 수 있을 것입니다.

𝑥, 𝑦에 대한 각각의 편미분 값이 모두 1이 되는 것을 확인할 수 있습니다. 계산 그래프로는 다음과 같이 표현이 가능합니다.

 

역전파 때는 상류에서 보내진 미분 값( 여기에서는 𝐿𝑧 )에 1을 곱하여 하류로 흘려보냅니다. 즉, 덧셈 노드의 역전파는 1을 곱하기만 할 뿐 입력된 값을 그대로 다음 노드로 보내게 됩니다. 위 예에서는 상류에서 전해진 미분 값을 𝐿𝑧로 표현 하였는데 이는 최종적으로 𝐿이라는 값을 출력하는 큰 계산 그래프를 가정하였기 때문입니다.

 

즉, 국소적 미분이 가장 오른쪽의 출력에서 시작하여 노드를 타고 역방향(왼쪽)으로 전파된 것을 그렸다고 생각하면 됩니다. 덧셈 노드에 대한 역전파를 간단하게 살펴보겠습니다. 가령 10+5=15라는 계산이 있고, 상류에서 (오른쪽) 1.3이라는 값이 흘러나오면 다음과 같이 계산 그래프를 그려볼 수 있습니다.

 

다시 말해, 덧셈 노드 역전파는 입력 신호를 다음 노드로 출력할 뿐이므로 1.3을 그대로 다음 노드로 전파합니다. 바로 코드로 구현해 보도록 하겠습니다.

  • 순전파 : 단순히 두 값을 더한다.
  • 역전파 : 다음 노드로부터 들어온 미분값을 그대로 흘려보낸다.
  • 순전파 시에 입력된 값을 저장하고 있을 필요는 없다.
import numpy as np

class AddLayer:

  def __init__(self):
    pass

  # 덧셈 노드로 들어온 값을 더해서 리턴
  def forward(self, x, y):
    out = x + y
    return out

  def backward(self, dout):
    # 모양새 맞추기
    dx = dout * 1
    dy = dout * 1

    return dx, dy

곱셈 노드의 역전파

이번엔 곱셈 노드의 역전파입니다.𝑧=𝑥𝑦 라는 식을 예로 들어 보겠습니다. 이 식의 미분은 다음과 같습니다.

계산 그래프로는 다음과 같이 그릴 수 있습니다.

 

곱셈 노드의 역전파는 상류의 값에 순전파 때의 입력 신호들을 서로 바꾼 값을 곱해서 하류로 보내고 있습니다. 즉, 순전파 때 𝑥였다면 역전파에서는 𝑦, 순전파 때 𝑦였으면 역전파에서는 𝑥로 바꾼다는 의미가 됩니다. 구체적인 예로 10⋅5=50이라는 계산이 있고, 역전파 때 상류에서 1.3 값이 흘러온다고 가정해 보겠습니다. 이를 계산 그래프로 그리면 다음과 같이 됩니다.

  • 순전파 : 두 값을 곱한다
  • 역전파 : 들어온 두 값에 미분값을 곱해서 반대로 전달한다.
  • 곱할 값들을 저장하고 있어야 한다. 역전파시에 미분값을 곱한 다음 반대로 전달해야 하니까
class MulLayer:

  def __init__(self):
    self.x = None
    self.y = None


  def forward(self, x, y):
    # 계산할 값들을 노드에 저장하고 있는다.
    self.x = x
    self.y = y

    out = x * y
    return out

  def backward(self, dout):
    dx = dout * self.y
    dy = dout * self.x

    return dx, dy

 

곱셈의 역전파에서는 입력 신호를 바꾼 값을 곱하여야 하는1.35=6.5. 다른 하나는 1.310=13이 되는 것이 확인됩니다. 덧셈의 역전파에서는 상류의 값을 그대로 흘려보내서 순방향 입력 신호의 값은 필요하지 않았습니다만, 곱셈의 역전파에서는 순방향 입력신호의 값이 필요합니다. 그래서 곱셈 노드를 구현할 때는 순전파의 입력 신호를 변수에 저장해 둡니다.

 

2024.02.15 - [Programming/Deep Learning] - [Python/DeepLearning] #10.3. 역전파) 계산 그래프를 통한 역전파 이해

 

[Python/DeepLearning] #10.3. 역전파) 계산 그래프를 통한 역전파

지난 시간, 수식을 통한 오차역전파법에 대하여 이해해 보았습니다. 2024.02.08 - [Programming/Deep Learning] - [Python/DeepLearning] #10.2. 역전파) 수식을 통한 오차역전파

yuja-k.tistory.com

앞서 #10. 3에서 사과와 귤 계산 기억하시나요? 사과 쇼핑의 예를 다시 한번 이어가 보자면!

이 문제에서는 '사과의 가격', '사과의 개수', 소비세'라는 세 변수 각각이 최종 금액에 어떻게 영향을 주느냐를 풀고자 합니다. 이는

  • 사과 가격에 대한 지불 금액의 미분
  • 사과 개수에 대한 지불 금액의 미분
  • 소비세에 대한 지불 금액의 미분을 구하는 것에 해당합니다. 이를 계산 그래프의 역전파를 사용해서 풀면 다음 그림과 같게 됩니다.

곱셈 노드의 역전파에서는 입력 신호를 서로 바꿔서 하류로 흘려보내는 것이 보입니다. 위 그림의 결과를 보면 사과 가격의 미분은 2.2, 사과 개수의 미분은 110, 소비세의 미분은 200입니다. 이를 해석해 보면

  • 소비세와 사과 가격이 같은 양만큼 오른다면
    • 최종 금액에는 소비세가 200의 크기로, 사과 가격이 2.2 크기로 영향을 준다고 할 수 있겠습니다.
    • 단, 소비세 1은 100%, 사과 가격 1은 1원

정리할 겸 해서 '사과와 귤 쇼핑'의 역전파를 풀어보죠!

 

초기화 함수인 init에서는 단순히 인스턴스 변수 x, y를 초기화시킵니다. 이 두 변수는 순전파 시의 입력값을 유지하기 위해 사용됩니다. forward 메소드는 순전파로써, x, y 를 입력받아 그 값을 인스턴스 변수에 저장한 후 곱해서 반환 합니다. backward 메소드는 상류에서 넘어온 미분값(dout)을 입력받아 서로 바꿔 곱한 후 반환하여 하류로 흘려보냅니다. 사과 쇼핑의 예를 들어보면 다음처럼 그림으로 설명이 가능합니다. MulLayer를 사용하여 위 그림의 순전파를 다음과 같이 확인해 볼 수 있습니다.

# 사과만 테스트
apple = 100 # 사과 1개당 가격
apple_cnt = 2 # 사과 개수
tax = 1.1 # 소비세

# 계층은 2개가 필요함
# (apple * apple_cnt) * tax

# 레이어 준비
mul_apple_layer = MulLayer()
mul_tax_layer   = MulLayer()

# 순전파 먼저 수행
apple_price = mul_apple_layer.forward(apple, apple_cnt)
price       = mul_tax_layer.forward(apple_price, tax)

print("최종 사과 가격 : {}".format(price))

각 변수에 대한 미분은 backward에서 구할 수 있습니다.

# 역전파 구현

dprice = 1 # d돈통 / d포스기 # 최종 가격에 대한 미분. d가격 / d가격 = 1
dapple_price, dtax = mul_tax_layer.backward(dprice)
dapple, dapple_cnt = mul_apple_layer.backward(dapple_price)

print("사과 가격 * 사과 개수에 대한 미분값 : {}".format(dapple_price))
print("사과 가격에 대한 미분값 : {}".format(dapple))
print("사과 개수에 대한 미분값 : {}".format(dapple_cnt))
print("소비세에 대한 미분값 : {}".format(dtax))

여기서 눈여겨보아야 할 것은 backward()의 호출 순서는 forward() 호출 순서의 반대라는 점입니다. 그리고 backward()가 받는 매개변수는 '순전파 출력에 대한 미분'임을 주의해야 합니다.

덧셈 계층 구현

두 번째로 구현할 계층은 덧셈 계층입니다.

 
덧셈 계층에서는 초기화 자체가 필요 없기 때문에 init에서 아무런 일도 하지 않습니다. 덧셈 계층의 forward() 에서는 입력받은 두개의 매개변수 x, y를 더해서 반환시키겨, backward() 에서는 상류에서 내려온 미분(dout)을 그대로 하류로 흘릴 뿐입니다. 이제 사과 2개와 귤 3개를 사는 상황을 그려 보겠습니다.
# 사과와 귤 계산하기
apple = 100
apple_cnt = 2

orange = 150
orange_cnt = 3

tax = 1.1

 

1 계층

  • 각 과일에 대한 개수 계산 ( 과일 가격 * 개수 )
mul_apple_layer = MulLayer() # 사과 개수 * 사과 가격
mul_orange_layer = MulLayer() # 귤 개수 * 귤 가격

 

2 계층

  • 사과 총 가격 + 귤 총 가격
add_apple_orange_layer = AddLayer() # 사과 총 가격 + 오렌지 총 가격

3 계층

  • 과일들의 총 가격 * 소비세
mul_tax_layer = MulLayer() # 과일 총 가격 * 소비세

 

연산 수행 (순전파)
# 1 계층
apple_price = mul_apple_layer.forward(apple, apple_cnt)
orange_price = mul_orange_layer.forward(orange, orange_cnt)

# 2 계층
total_price = add_apple_orange_layer.forward(apple_price, orange_price)

# 3 계층
price = mul_tax_layer.forward(total_price, tax)

print("최종 가격 : {}".format(int(price)))

연산 수행 (역전파)
dprice = 1 # d돈통 / d포스기

# dprice / dtotal_price, dprice / dtax
dtotal_price, dtax = mul_tax_layer.backward(dprice)

# d돈통 / dapple_price = ( d돈통 / d포스기 ) * (d돈통 / dtotal_price) * (dtotal_price / dapple_price)
dapple_price, dorange_price = add_apple_orange_layer.backward(dtotal_price)

dorange, dorange_cnt = mul_orange_layer.backward(dorange_price)
dapple, dapple_cnt = mul_apple_layer.backward(dapple_price)

print("사과 개수에 대한 미분값 : {}".format(dapple_cnt))
print("사과 가격에 대한 미분값 : {}".format(dapple))

print("귤 개수에 대한 미분값 : {}".format(dorange_cnt))
print("귤 가격에 대한 미분값 : {}".format(dorange))

지금까지 역전파의 덧셈 노드와 곱셈 노드를 알아보았습니다! 길고 긴 역전파의 마지막 종착지는 활성화 함수의 계층 구현으로 마무리하려 합니다. 다음 시간에는 신경망 레이어 만들기를 설명해 보도록 하겠습니다.

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

지난 시간, 수식을 통한 오차역전파법에 대하여 이해해 보았습니다.

2024.02.08 - [Programming/Deep Learning] - [Python/DeepLearning] #10.2. 역전파) 수식을 통한 오차역전파법 이해

 

이번 시간에서는 계산 그래프를 통한 역전파에 대하여 알아보도록 하겠습니다!

오차역전파법을 위한 계산 그래프

 

일전에 수식으로 풀어본 오차역전파법은 수학을 오랫동안 놓았거나 수식으로만 생각하면 본질을 놓칠 우려가 있습니다. 이번에 우리가 해볼 내용은 계산 그래프를 이용해 오차역전파법을 이해하는 것인데요, 수식으로 오차역전파법을 이해하는 것보다는 약간은 부정확할 수 있으나 최종적으로는 수식으로 알아본 오차역전파법을 이해할 수 있고, 실제 코드 구현까지 해보도록 하겠습니다. 계산 그래프로 설명한다는 아이디어는 안드레 카패스의 블로그 또 그와 페이페이 리 교수가 진행한 스탠퍼드 대학교 딥러닝 수업 CS321n을 참고했습니다.

 

계산 그래프

 

계산 그래프(computational graph)는 계산 과정을 그래프로 그려낸 것입니다. 그래프는 우리가 잘 아는 그래프 자료 구조 형태로 되어 있으며, 처음에 쉽게 접근하기 위해 계산 그래프를 통한 간단한 문제를 풀어보도록 하겠습니다. 먼저 익숙해지자!라는 이야기입니다. 예를 들어 다음과 같은 예시가 있다고 하죠, "A라는 사람이 1개 100원인 사과를 2개 샀습니다. 이때 지불 금액을 구하세요, 단 소비세 10%가 부과됩니다."라는 예시를 계산그래프로 표현하면 다음과 같아집니다.

 

처음에 사과의 100원이 'x 2' 노드로 흘러 200원이 된 다음 소비세 계산을 위해 'x 1.1' 노드를 거쳐 최종적으로는 220원이 됩니다. 위 그래프에 따르면 최종 답은 220원이 된다는 사실을 알 수 있네요 위의 그림에서는 계산 노드를 각각 'x 2', 'x 1.1'로 표현했지만 '2'와 '1.1'을 각각 사과의 개수와 소비세에 대한 변수가 되기 때문에 따로 빼서 다음과 같이 표기할 수 있습니다.

 

그럼 다음 문제를 풀어 보도록 하겠습니다.

"A가 사과를 2개, 귤을 3개 샀습니다. 사과는 1개에 100원, 귤은 1개 150원입니다. 소비세가 10% 부과될 때 A가 지불해야 할 금액은?" 위 문제도 계산그래프로 풀어볼 수 있습니다. 이때의 계산 그래프는 다음과 같겠네요!

 

위 문제에서는 새로운 노드인 덧셈 노드가 추가되었습니다. 덧셈 노드가 추가되어 사과의 가격과 귤의 가격을 합치는 모습이 보이고 있습니다. 왼쪽에서 오른쪽으로 순차적으로 계산을 끝내고 제일 마지막에 1.1을 곱하면 우리가 원하는 값인 715원이 나오고 끝나게 됩니다. 계산 그래프를 이용한 문제풀이는 다음과 같이 해석할 수 있습니다.

  1. 계산 그래프를 구성
  2. 그래프에서 계산을 왼쪽에서 오른쪽으로 진행

이처럼 '계산을 왼쪽에서 오른쪽으로 진행'하는 단계를 순전파(forward propagation)라고 합니다. 순전파는 계산 그래프의 출발점부터 종착점으로의 전파단계를 그려줍니다. 역전파(backword propagation)는 무엇일까요? 바로 '오른쪽에서 왼쪽으로 전파되는 단계를 의미합니다!


국소적 계산

 

계산 그래프의 특징은 '국소적 계산'을 전파함으로써 최종 결과를 얻는다는 점에 있습니다. 여기서 '국소적'이란, "자신과 직접 관계된 작은 범위"를 의미하는데, 뭔가 떠오르지 않으시나요? 수학으로 따지면 바로 편미분을 의미한다는 것입니다. 즉, 국소적 계산은 전체에서 어떤 일이 벌어지든 상관없이 자신과 관계된 정보만을 토대로 결과를 낼 수 있다는 이야기입니다. 구체적인 예를 들어 보겠습니다. 여러분이 마트에서 사과 2개를 포함한 여러 가지의 물품들을 구매하는 상황을 구해보겠습니다. 그렇다면 사과에 대한 국소적 계산을 진행한다고 이해할 수 있는데요, 그래프로 확인해 보겠습니다.

 

위 그림에서 여러 식품을 구매하여( 복잡한 계산을 하여) 4,000원이라는 금액이 나왔고, 여기에 사과 가격인 200원을 더해 총 4,200원이 나왔습니다. 이는 '사과에 대한 국소적 계산'이기 때문에, 4,000원이 어떻게 나왔는지는 전혀 신경 쓸게 없다는 이야기가 됩니다. 그냥 단순히 복잡한 계산의 결과물인 4,000원과 사과의 가격인 200원을 더해 4,200을 알아내면 된다는 것이죠. 중요한 점은 계산 그래프는 이처럼 국소적 계산에 집중한다는 것입니다. 전체 계산 자체가 아무리 복잡해도 각 단계에서 하는 일은 해당 노드의 '국소적 계산'일뿐입니다. 국소적 계산은 단순하지만 그 결과를 전달함으로써 전체를 구성하는 복잡한 계산을 해낼 수 있습니다. 마치 자동차 조립을 하는 것과 비슷한데요, 각각의 부품을 복잡하게 만들어 내고, 최종적으로 합쳐 차를 완성하는 단계라고 볼 수 있습니다.

 

계산 그래프를 사용하는 이유

 

계산 그래프의 이점은 무엇일까요? 바로 국소적 계산입니다. 전체가 아무리 복잡해도 각 노드에서는 단순한 계산에 집중하여 문제를 단순화시킬 수 있기 때문이지요, 또한 계산 그래프는 중간 계산 결과를 모두 보관할 수 있습니다. 에지에 저장되어 있는 숫자들이 그것을 의미하고 있지요, 하지만 이것 때문에 계산 그래프를 사용하진 않습니다! 계산 그래프를 사용하는 가장 큰 이유는 역전파를 통해 '미분'을 효율적으로 계산할 수 있기 때문입니다.

계산 그래프의 역전파 첫 번째 문제에 대한 계산 그래프는 사과 2개를 사서 소비세를 포함한 최종 금액을 구하는 것이었습니다. 여기서 새로운 문제를 제시해 보겠습니다. "사과 가격이 오르면 최종 금액에 어떠한 영향을 미칠 것인가?"가 문제입니다. 즉 이는 사과 가격에 대한 지불 금액의 미분을 구하는 문제에 해당됩니다. 사과 값을 x로, 지불 금액을 L이라 했을 때

로 표현이 가능하다는 것이죠, 즉 이 미분값은 사과 값이 '아주 조금' 올랐을 때 지불 금액이 얼마나 증가하느냐를 표시한 것입니다. 즉, '사과 가격에 대한 지불 금액의 미분' 같은 값은 계산 그래프에서 역전파를 하면 구할 수 있게 됩니다. 다음 그림에서는 계산 그래프 상의 역전파에 의해 미분을 구할 수가 있습니다. 아직 역전파가 어떻게 이뤄지는지에 대해서는 이야기하지 않았습니다!

위 그림에서 굵은 화살표로 역전파를 표현해 보았습니다. 이 전파는 각각 노드에 대한 국소적 미분을 전달합니다. 즉, 들어오고 있는 사과의 개수나 소비세에 대한 국소적으로 미분을 진행하였기 때문에, 소비세와 사과의 개수 같은 변수에 대한 미분만 진행했다는 이야기입니다. 그리고 그 미분값은 화살표 방향으로 적어내고 있습니다. 이 예에서 역전 파는 오른쪽에서 왼쪽으로 '1 -> 1.1 -> 2.2' 순으로 미분값을 전달하고 있습니다. 이 결과로부터 알 수 있는 사실은 '사과 가격에 대한 지불금액이 미분'값은 2.2라는 것을 알 수 있게 됩니다. 즉, 사과 가격이 1원 오르면 최종 가격은 2.2원 오른다는 것이죠. 여기에서는 사과 가격에 대한 미분만 구했지만, '소비세에 대한 지불 금액의 미분'이나 '사과 개수에 대한 지불 금액의 미분'도 같은 순서로 구해낼 수가 있습니다. 그리고 그때는 중간까지 구한 미분 결과를 공유할 수 있어서 다수의 미분을 효율적으로 계산할 수 있습니다. 이처럼 계산 그래프의 이점은 순전파와 역전파를 활용해서 각 변수의 미분을 효율적으로 구할 수 있다는 것입니다.

 

연쇄법칙과 계산 그래프

 

연쇄법칙 계산을 계산 그래프로 나타낼 수 있습니다. 2 제곱 계산을 '**2' 노드로 나타내면 다음과 같습니다.

오른쪽에서 왼쪽으로 신호가 전파되는 모습을 볼 수 있습니다. 역전파에서의 계산 절차는 노드로 들어온 입력 신호에 그 노드의 국소적 미분인 편미분을 곱한 후 다음 노드로 전달합니다. 예를 들어 **2 노드에서의 역전파를 보면 입력은 𝑧∂𝑧이며, 이에 대한 국소적 미분인 𝑧𝑡를 곱해 다음 노드로 넘깁니다. 맨 왼쪽의 역전파를 보면 x에 대한 z의 미분이 연쇄법칙에 따라서

가 된다는 사실을 알아낼 수 있고, 이를 계산하면

가 된다는 사실을 알아낼 수 있습니다.

지금까지 아주아주 긴 오차역전파법을 위한 계산 그래프를 위한 이해를 수식으로 알아보았습니다! 다음 세션을 통해 최종적으로 코드 구현을 해보겠습니다.

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

지난 시간 손실 함수 Loss Function의 종류에 대하여 알아보았습니다.

2024.01.27 - [Programming/Deep Learning] - [Python/DeepLearning] #8. 손실 함수의 종류(Loss Function)

 

[Python/DeepLearning] #8. 손실 함수의 종류(Loss Function)

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

yuja-k.tistory.com

오늘은 신경망 학습의 마지막 수치 미분과 경사하강법에 대하여 설명해 보도록 하겠습니다!

이번 블로그로는 (상)&(하)로 나눠집니다. (상) 편에서 미분과 편미분에 대하여 설명하며, (하)에서 경사하강법과 신경망에서의 기울기에 대하여 알아보도록 하겠습니다. 

 

경사법(경사하강법)을 위한 미분

경사법에서는 기울기 값을 기준으로 최적화하기 위한 방향을 결정합니다. 기울기의 성질을 알아보기 앞서 미분을 먼저 살펴보겠습니다.

 

미분

우리가 트랙에서 달리는 모습을 상상해 보겠습니다.

처음부터 10분 동안 2km씩 달렸다고 가정했을 때 속도는 2(거리) / 10(시간) = 0.2(속도)만큼의 속도(변화)로 뛰었다고 해석할 수 있을 것입니다.

위의 예에서는 달린 거리가 시간에 대해서 얼마나 변화했는지를 계산한 것입니다. 다만 여기에서 10분에 2km를 뛰었다는 이야기는 정확하게는 10분 동안의 평균 속도를 구한 것이죠.

하지만 미분은 '특정 순간'의 변화량을 뜻합니다. 따라서 10분이라는 시간을 최대한 줄여 한 순간의 변화량을 얻는다고 볼 수 있습니다.

이처럼 미분은 한순간의 변화량을 표시한 것입니다.

위 식은 함수의 미분을 나타낸 식입니다.

좌변은 함수 𝑓(𝑥) 𝑥에 대한 미분(𝑥에 대한 𝑓(𝑥)의 변화량)을 나타낸 기호라고 볼 수 있습니다.

결국, 𝑥의 작은 변화가 함수 𝑓(𝑥)를 얼마나 변화시키느냐를 의미합니다. 이때 시간의 작은 변화, 즉 시간을 뜻하는 를 한없이 0에 가깝게 한다는 의미를 lim0로 나타내었습니다. 그렇다면 파이썬으로 미분 함수를 구현해 보겠습니다.

h에 최대한 작은 값을 대입해서 다음과 같이 계산이 가능합니다. 참고로  Δ𝑥로도 표현합니다.

import numpy as np

# 미분의 나쁜 구현
def numerical_diff(f, x):
  h = 10e-50 # 0.0000 0이 50개
  return (f(x+h) - f(x)) / h

 

함수의 이름은 수치 미분(numerical differentiation)에서 따왔습니다.

이 함수는 함수 𝑓와 함수 𝑓에 넘겨질 𝑥를 받아서 실제 미분을 구현하지만, 고쳐야 할 부분들이 약간 있습니다.

위의 예에서는 가능한 한 h에 작은 값을 넣기 위해 10e-50이라는 매우 작은 수를 이용했었는데, 이 방식은 반올림 오차(rounding error) 문제를 일으킵니다. 반올림 오차는 작은 값(보통 소수점 8자리)이 생략되어 최종 계산 결과에 오차가 됩니다.

즉, 너무나 작은 숫자로 나눗셈을 하게 되면, 반올림 오차가 발생하여 컴퓨팅 시스템상 부동소수점 오류가 나게 됩니다.

 

파이썬에서의 반올림 오차는 보통 다음 상황에서 발생합니다.

np.float32(10e-50)

1000 / np.float32(10e-50)

너무 작은 값을 파이썬에서 소수점으로 나타내면 0.0이 되는 모습을 확인할 수 있습니다. 너무 작은 값을 컴퓨터로 계산하면 문제가 되기 때문에 이 미세한 값 h를 10의 −4제곱 정도로 놓으면 좋은 값을 얻는다고 알려져 있습니다.

 

그다음 개선할 점은 함수 f의 차분(임의 두 점에서 함숫값들의 차이)에 관련한 것입니다. 

𝑥+ 𝑥 사이의 함수 𝑓의 차분을 계산하고 있지만, h 자체가 매우 작은 값이기 때문에 애당초 위 계산에는 오차가 있다고 생각해야 합니다.

미분 자체가 어떤 순간 지점(시간(ℎ)이 최대한 0이 되는 지점)의 기울기를 의미하기 때문에 진정한 미분은 𝑥 위치 에서의 함수의 기울기를 이야기해야 하지만, 가 절대 0이 될 수가 없기 때문에 ( 0에 가까워지기는 하지만 ) 컴퓨터를 통한 미분 계산은 언제나 오차를 동반해야만 합니다.

이 오차를 최대한 줄이기 위해 (𝑥+ℎ)(𝑥−ℎ)일 때의 함수 𝑓의 차분을 계산하는 방법을 사용하기도 합니다. 이 차분은 x를 중심으로 그 전후의 차분을 계산한다는 의미로, 중심 차분 혹은 중앙 차분이라 합니다. 참고로 (𝑥+) 𝑥의 차분은 전방 차분이라고도 합니다.

위 두 가지 개선사항을 개선한 후의 미분 함수를 구현해 보겠습니다.

# 좋은 수치미분 구현
def numerical_diff(f, x):
  h = 1e-4 # 0.0001
  return (f(x+h) - f(x-h)) / (2 * h)
  • 아주 작은 값을 의미하는 ℎ는 1e-4
  • 수학적인 기교를 이용해 컴퓨터를 활용한 미분에서의 나눗셈 오류를 해결할 수 있다 (중앙차분과 전방차분을 활용)

위처럼 아주 작은 차분으로 미분하는 것을 수치 미분이라고 합니다.

수치 미분과 기울기의 관계

 

위의 미분 함수를 이용해 실제 간단한 함수를 미분해 보겠습니다. 먼저. 다음과 같은 2차 함수가 있다고 가정합니다.

def function_1(x):
  return 0.01*x**2 + 0.1 * x
import numpy as np
import matplotlib.pylab as plt

%matplotlib inline
x = np.arange(0.0, 20.0, 0.1)
y = function_1(x)

plt.xlabel("x")
plt.ylabel("f(x)")
plt.plot(x, y)
plt.show()

이번엔 x가 5와 10일 때의 해당 함수의 미분값입니다.

# x가 5일 때의 미분값과 10일 때의 해당 함수의 미분값 구하기
print("x가 5일 때 미분 값 : {}".format(numerical_diff(function_1, 5)))
print("x가 10일 때 미분 값 : {}".format(numerical_diff(function_1, 10)))

def tangent_line(f, x):
    d = numerical_diff(f, x)
    y = f(x) - d*x
    return lambda t: d*t + y

fig, axes = plt.subplots(1,2, figsize=(20,8))

x = np.arange(0.0, 20.0, 0.1)
y = function_1(x)
axes[0].set_xlabel("x")
axes[0].set_ylabel("f(x)")

tf = tangent_line(function_1, 5)
y2 = tf(x)

axes[0].plot(x, y)
axes[0].plot(x, y2)
axes[0].set_title("x = 5")

axes[1].set_xlabel("x")
axes[1].set_ylabel("f(x)")

tf = tangent_line(function_1, 10)
y2 = tf(x)

axes[1].plot(x, y)
axes[1].plot(x, y2)
axes[1].set_title("x = 10")
plt.show()

해당하는 위치(x)에서의 접선이 그려지는 것이 확인됩니다.

편미분

한쪽 변수 방향으로만 미분하는 것입니다. 다른 쪽 변수는 상수로 취급합니다.

인수들이 1개가 아닌, 2개 이상의 인수가 있는 상황에서 의 미분은 편미분을 통해 해결할 수 있습니다. 다음 식을 확인해 보겠습니다.

def function_2(x):
    return x[0]**2 + x[1]**2
from mpl_toolkits.mplot3d import Axes3D
fig = plt.figure(figsize=(10,8))
ax = fig.add_subplot(111, projection='3d')

X0 = np.arange(-3, 3, 0.05)
X1 = np.arange(-3, 3, 0.05)

X0, X1 = np.meshgrid(X0, X1)
Z = function_2([X0,X1])

ax.plot_wireframe(X=X0, Y=X1, Z=Z,rstride=10, cstride=10)
ax.set_xlabel('x0')
ax.set_ylabel('x1')
ax.set_zlabel('f(x)')
plt.show()

  1. 미분을 함에 있어서 고려해야 할 방향이 2개
    • 𝑥0, 𝑥1 방향으로 고려
  2. 둘 중 하나의 방향으로만 미분을 수행 -> 편미분
    • 𝑥0의 미분을 수행할 때는 𝑥1을 상수처럼 취급 𝑓 / 𝑥0
    • 𝑥1의 미분을 수행할 때는 𝑥0을 상수처럼 취급 𝑓 / 𝑥1

 


 

𝑥0=3, 𝑥1=4일 때 𝑥0에 대한 편미분 𝑓 / 𝑥0을 구하는 코드를 구현

 

변수가 두 개인 함수의 미분은 '어느 변수에 대한 미분이냐'를 따져줘야 합니다.

즉, 𝑥0, 𝑥1 중 어느 변수에 대한 미분이냐를 구별해야 한다고 생각할 수 있는데요, 이와 같이 변수가 여럿인 함수에 대한 미분을 편미분이라고 합니다.

수식으로는 𝑓 / 𝑥0 또는 𝑓 / 𝑥1 처럼 표현합니다. 연습 삼아 편미분 문제를 풀어보겠습니다. 

𝑥0=3, 𝑥1=4 일 때 𝑥0에 대한 편미분 𝑓𝑥0를 구하는 코드를 구현해 보겠습니다.

def function_tmp1(x0):
  return x0 ** 2 + 4.0 ** 2

numerical_diff(function_tmp1, 3.0)

다음은 𝑥0=3, 𝑥1=4 일 때 𝑥1에 대한 편미분 𝑓 / 𝑥1를 구하는 코드를 구현해 보겠습니다.

def function_tmp2(x1):
  return 3.0 ** 2 + x1 ** 2

numerical_diff(function_tmp2, 4.0)

위의 문제를 풀기 위해 변수가 하나인 함수를 정의했으며, 그 함수를 미분하는 형태로 구현하여 풀어보았습니다.

가령, 𝑥1=4로 고정된 새로운 함수를 하나 정의하여 변수가 𝑥0 하나뿐인 함수에 대해 수치 미분(numerical_diff)을 적용시킵니다. 이처럼 편미분은 변수가 하나인 미분과 마찬가지로 특정 장소의 기울기를 구해낼 수 있습니다.

단, 여러 변수 중 목표 변수 하나에 초점을 맞추고 다른 변수는 값을 고정합니다(상수처럼 활용).

앞의 예에서는 목표 변수를 제외한 나머지를 특정 값에 고정하기 위해서 새로운 함수인 function_tmp1, function_tmp2를 정의한 것이지요! 그리고 그 새로 정의한 함수에 대해 그동안 사용한 미분값을 적용시켜 편미분을 구한 것입니다.

기울기에 대하여

앞서는 𝑥0와 𝑥1의 편미분을 변수별로 따로 계산해 봤습니다.

그렇다면 𝑥0와 𝑥1의 편미분을 동시에 계산하고 싶으면 어떻게 해야 할까요?

위의 문제처럼 𝑥0=3, 𝑥1=4일 때 (𝑥0,𝑥1) 양쪽의 편미분을 묶어서 (𝑓 / 𝑥0, 𝑓 / 𝑥1)을 계산한다고 생각해 볼 수 있습니다. (𝑓 / 𝑥0, 𝑓 / 𝑥1)처럼 모든 변수의 편미분을 벡터로 정리한 것을 기울기(gradient)라고 합니다.

기울기를 구하는 예를 구현해 보겠습니다.

∂𝑓 / ∂𝑥0 ,  ∂𝑓 / ∂𝑥1

𝑥0과 𝑥1 각각의 편미분 결과를 (𝑓 / 𝑥0, 𝑓 / 𝑥1)로 정리한 것을 기울기(gradient)라고 한다.

# 기울기를 구하는 코드 - 각각의 편미분을 배열로 만든다.
def numerical_gradient(f, x):
  h = 1e-4
  # 기울기를 저장할 배열
  grad = np.zeros_like(x) # x와 형상(shape)이 같은 배열을 생성

  for idx in range(x.size):
    # x의 값을 idx에 맞게 하나씩 빼온다.
    tmp_val = x[idx]

    # 각 좌표에서의 미분을 수행 -> 편미분

    # 1. f(x+h) 계산
    x[idx] = tmp_val + h
    fxh1 = f(x)

    # 2. f(x-h) 계산
    x[idx] = tmp_val - h
    fxh2 = f(x)

    # 3. 기울기 계산해서 grad 배열에 저장 ( 미분 수행 )
    grad[idx] = (fxh1 - fxh2) / 2*h

    # x[idx]를 원래 값으로 복구
    x[idx] = tmp_val

  return grad

numerical_gradient 함수가 약간 복잡하게 생기긴 했지만 간단하게 생각해 보면 인자로 들어온 모든 x의 값(배열)에 대한 함수 f를 미분한 코드입니다. 위 함수를 이용하면 인자가 몇 개가 들어오든 모든 기울기를 벡터로 나타내는 배열을 확인할 수 있습니다.

result = numerical_gradient(function_2, np.array([3.0, 4.0]))
print("x = [3, 4] 일 때의 기울기 배열 : {}".format(result))

result = numerical_gradient(function_2, np.array([0.0, 2.0]))
print("x = [0, 2] 일 때의 기울기 배열 : {}".format(result))

result = numerical_gradient(function_2, np.array([3.0, 0.0]))
print("x = [3, 0] 일 때의 기울기 배열 : {}".format(result))

이처럼 (𝑥2,𝑥1)의 각 점에서의 기울기를 계산할 수 있습니다. 각 값별 기울기를 시각적으로 확인해 보겠습니다.

# coding: utf-8
# cf.http://d.hatena.ne.jp/white_wheels/20100327/p3
import numpy as np
import matplotlib.pylab as plt
from mpl_toolkits.mplot3d import Axes3D


def _numerical_gradient_no_batch(f, x):
    h = 1e-4 # 0.0001
    grad = np.zeros_like(x) # x와 형상이 같은 배열을 생성
    
    for idx in range(x.size):
        tmp_val = x[idx]
        
        # f(x+h) 계산
        x[idx] = float(tmp_val) + h
        fxh1 = f(x)
        
        # f(x-h) 계산
        x[idx] = tmp_val - h 
        fxh2 = f(x) 
        
        grad[idx] = (fxh1 - fxh2) / (2*h)
        x[idx] = tmp_val # 값 복원
        
    return grad


def numerical_gradient(f, X):
    if X.ndim == 1:
        return _numerical_gradient_no_batch(f, X)
    else:
        grad = np.zeros_like(X)
        
        for idx, x in enumerate(X):
            grad[idx] = _numerical_gradient_no_batch(f, x)
        
        return grad


def function_2(x):
    if x.ndim == 1:
        return np.sum(x**2)
    else:
        return np.sum(x**2, axis=1)


def tangent_line(f, x):
    d = numerical_gradient(f, x)
    print(d)
    y = f(x) - d*x
    return lambda t: d*t + y
     
if __name__ == '__main__':
    x0 = np.arange(-2, 2.5, 0.25)
    x1 = np.arange(-2, 2.5, 0.25)
    X, Y = np.meshgrid(x0, x1)
    
    X = X.flatten()
    Y = Y.flatten()
    
    grad = numerical_gradient(function_2, np.array([X, Y]) )
    
    plt.figure()
    plt.quiver(X, Y, -grad[0], -grad[1],  angles="xy",color="#666666")
    plt.xlim([-2, 2])
    plt.ylim([-2, 2])
    plt.xlabel('x0')
    plt.ylabel('x1')
    plt.grid()
    plt.draw()
    plt.show()

기울기에 대한 그림은 위처럼 방향을 가진 벡터(화살표)로 그려집니다.

이 그림을 보면 기울기는 함수의 '가장 낮은 장소(최솟값)'를 가리키는 것 같습니다. 한 점을 향하고 있는 것이 확인되는데요, 또한 가장 낮은 곳에서 멀어질수록 화살표의 크기가 커지는 것도 확인이 가능합니다.

중요한 것은 기울기 자체는 가장 낮은 장소를 가리키고 있으나, 실제는 반드시 그렇다고는 할 수 없습니다.

사실 기울기는 각 지점에서 낮아지는 방향을 가리키는데요, 더 확실히 말하자면 기울기가 가리키는 쪽은 각 장소에서 함수의 출력 값(결괏값)을 가장 크게 줄이는 방향입니다. 확실히 기억해야 합니다.

 

지금까지 경사하강법을 위한 미분과 편미분을 알아보며 준비를 하였습니다. 다음 블로그에서는 경사법(경사하강법)의 원리와 신경망에서의 기울기에 대한 신경망 학습을 알아보도록 하겠습니다.

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.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