이번 시간에는 역전파의 완전 마지막! 활성화 함수 계층을 구현해 보도록 하겠습니다! 이 이상의 #10을 쪼개진 않겠습니다...
활성화 함수 계층 구현
드디어 지금 만든 계산 그래프를 신경망에 적용시켜 볼 수 있습니다!
여기에서는 신경망을 구성하는 층(계층) 각각을 클래스 하나씩으로 구현했는데요. 우선은 활성화 함수인 ReLU와 Sigmoid 계층을 구현해 보겠습니다!
ReLU 계층 만들기
먼저. 활성화 함수로 사용되는 ReLU의 수식은 다음과 같습니다.
위의 식을 미분하면 다음처럼 구할 수 있습니다.
순전파 때의 입력인 x가 0보다 크면 역전파는 상류의 값을 그래도 하류로 흘립니다.
반면에 순전파 때 x가 0 이하면 역전파 때는 하류로 신호를 보내지 않습니다.( 0을 보내기 때문입니다).
계산 그래프로는 다음 처럼 그릴 수 있겠네요!
그럼 본격적으로 ReLU 계층을 구현해 보도록 하겠습니다!
신경망 계층의 forward()와 backward() 함수는 넘파이 배열을 인수로 받는다고 가정합니다.
신경망 레이어 만들기
ReLU
Sigmoid
Affine layer( 기하학 레이어 - Fully Connected, Dense )
SoftMax + Loss layer
ReLU 구현
class ReLU:
# mask : 순전파 시에 0이나 음수였던 인덱스를 저장하기 위함이다.
# mask가 있어야 순전파 때 0이었던 부분을 역전파 때 0으로 만들어 줄 수 있다.
def __init__(self):
self.mask = None
def forward(self, x):
self.mask = (x <= 0)# 매개변수로 들어온 넘파이배열 x의 원소가 0이하인지 판단하기
out = x.copy() # 원본 배열 복사
out[self.mask] = 0 # 0보다 작은 원소들을 0으로 만들기
return out
# 순전파 때 음수였던 부분을 0으로 만든다.
# 음수였었던 인덱스를 기억하고 있다가 (self.mask) 미분값 전달시에 해당 인덱스를 0으로 만든다.
def backward(self, dout):
dout[self.mask] = 0 # 상류에서 들어온 값에서 0보다 작은 값들에 대해 0으로 치환
dx = dout # 완성된 ReLU 배열 리턴
return dx
완성된 ReLU 계층을 np.array 배열을 넣어서 테스트해보겠습니다.
x = np.array([ [1.0, -0.5],
[-2.0, 3.0] ])
print(x)
X와 + 노드 말고 exp와 '/' 노드가 새롭게 등장했는데요, exp 노드는𝑦=𝑒𝑥𝑝(𝑥)계산을 수행하고, '/' 노드는𝑦=1𝑥를 수행합니다. 그림과 같이 시그모이드의 계산은 국소적 계산의 순전파로 이루어집니다. 이제 역전파를 알아볼 텐데요, 각 노드에 대한 역전파를 단계별로 알아보도록 하겠습니다.
1단계
'/' 노드를 미분하면 다음과 같습니다.
위처럼 상류에서 흘러 들어온 값에−𝑦2(순전파의 출력을 제곱한 후 음수를 붙인 값)을 곱해서 하류로 전달시키게 됩니다. 계산 그래프에서는 다음과 같습니다.
2단계
'+' 노드는 상류의 값을 여과 없이 하류로 보냅니다. 계산 그래프의 결과는 다음과 같아집니다.
3단계
'exp'노드는𝑦=𝑒𝑥𝑝(𝑥)연산을 수행하며, 그 미분은 다음과 같습니다.
계산 그래프에서는 상류의 값에 순전파 때의 출력(이 예에서는𝑒𝑥𝑝(−𝑥))을 곱해 하류로 전파합니다.
4단계
제일 마지막 X 노드는 순전파 때의 값을 서로 바꿔 곱합니다. 이 예에서는 -1을 곱하면 될 것 같습니다.
총 4 단계를 거쳐 Sigmoid 계층의 역전파를 계산 그래프로 완성해 보았습니다. 역전파의 최종출력인∂𝐿/∂𝑦*𝑦2*𝑒𝑥𝑝(−𝑥)) 값이 하류 노드로 전파됩니다. 그런데∂𝐿/∂𝑦*𝑦2*𝑒𝑥𝑝(−𝑥)를 순전파의 입력𝑥와 출력𝑦만으로 계산할 수 있다는 것을 알 수 있는데요, 따라서 계산 중간 과정을 모두 묶어서 다음처럼 단순한 그림으로 표현이 가능합니다.
수치적인 오차역전파법, 계산 그래프를 이용한 역전파의 의미에 대해 살펴보았습니다. 본격적으로 파이썬으로 역전파를 구현하고, 이를 신경망에 집어넣어 실제 오차역전파를 구현해 보도록 하겠습니다. 일전에 수식을 이용해 오차역전파법을 공부했고, 계산 그래프를 이용해 쇼핑에 대한 역전파도 공부해 보았습니다. 신경망이라고 해서 별 다를 것이 없는 게, 신경망의 모든 계산은 덧셈과 곱셈을 이용해 수행됩니다. 따라서 아주 간단한 덧셈 계층과 곱셈 계층부터 구현해 보도록 하겠습니다. 모든 계층은 순전파와 역전파를 모두 처리할 수 있도록 공통의 메소드를 구현할 것입니다.
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.3⋅5=6.5. 다른 하나는1.3⋅10=13이 되는 것이 확인됩니다. 덧셈의 역전파에서는 상류의 값을 그대로 흘려보내서순방향 입력 신호의 값은 필요하지 않았습니다만, 곱셈의 역전파에서는 순방향 입력신호의 값이 필요합니다. 그래서 곱셈 노드를 구현할 때는 순전파의 입력 신호를 변수에 저장해 둡니다.
앞서 #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개를 사는 상황을 그려 보겠습니다.
일전에 수식으로 풀어본 오차역전파법은 수학을 오랫동안 놓았거나 수식으로만 생각하면 본질을 놓칠 우려가 있습니다. 이번에 우리가 해볼 내용은 계산 그래프를 이용해 오차역전파법을 이해하는 것인데요, 수식으로 오차역전파법을 이해하는 것보다는 약간은 부정확할 수 있으나 최종적으로는 수식으로 알아본 오차역전파법을 이해할 수 있고, 실제 코드 구현까지 해보도록 하겠습니다. 계산 그래프로 설명한다는 아이디어는 안드레 카패스의 블로그 또 그와 페이페이 리 교수가 진행한 스탠퍼드 대학교 딥러닝 수업 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원이 나오고 끝나게 됩니다. 계산 그래프를 이용한 문제풀이는 다음과 같이 해석할 수 있습니다.
계산 그래프를 구성
그래프에서 계산을 왼쪽에서 오른쪽으로 진행
이처럼 '계산을 왼쪽에서 오른쪽으로 진행'하는 단계를순전파(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의 미분이 연쇄법칙에 따라서
가 된다는 사실을 알아낼 수 있고, 이를 계산하면
가 된다는 사실을 알아낼 수 있습니다.
지금까지 아주아주 긴 오차역전파법을 위한 계산 그래프를 위한 이해를 수식으로 알아보았습니다! 다음 세션을 통해 최종적으로 코드 구현을 해보겠습니다.
가중치 매개변수에 대한 손실 함수의 기울기를 구하기 위해 이전에 경사하강법을 수치 미분을 이용해 구현해 보았습니다. 수치 미분은 단순하고 구현하기도 쉽지만 계산 시간이 오래 걸린다는 치명적인 단점이 있습니다. 이번에 배워볼 오차역전파법(Back Propagation)은 가중치 매개변수의 기울기를 효율적으로 계산시킬 수 있습니다.
비용함수(Cost Function) 미분의 어려움
최적의 가중치와 편향을 구할 수 있는 지표로써 비용함수(Cost Function)를 사용한다고 앞서 배웠습니다. 경사하강법을 이용하지 않고 가중치와 편향을 구해 볼 수도 있는데, 어떻게 해결하고, 왜 경사하강법을 사용해야 하는지 먼저 살펴보겠습니다. 이미지 입력을 예로 들어볼 텐데요, 3 X 4 형태의 이미지를 64개 입력한다고 생각해 보겠습니다.
입력층엔 12개의 데이터 (𝑥1~𝑥12)
은닉층에 3개의 유닛
출력층에 2개의 유닛
위의 규칙에서 비용함수로 제곱오차식을 이용한다고 생각하고, 64개의 이미지를 입력한다라고 예상 해 봤을 때𝑘번째 이미지의 제곱오차값𝐶를𝐶𝑘로 두면 다음과 같은 식을 구할 수 있습니다.
위의 식에서𝑡는𝑘번째 이미지의 정답 데이터,𝑎는 신경망이 예측한 데이터입니다. 이를 학습 데이터 전체로 적용한다고 생각하면
라는 비용함수 𝐶𝑇를 얻을 수 있습니다. 하지만 비용함수 𝐶𝑇는 매우 복잡한 함수입니다. 결정해야 할 가중치와 편향은 47개나 있습니다. (입력값 12개 X 입력에 대한 유닛의 가중치 3개 + 각 유닛별 편향 3개) + ( 출력층으로 향하는 입력값 3개 X 출력층으로 향하는 가중치 2개 + 출력층의 편향 2개)
따라서 각 가중치와 편향을 결정하기 위한 미분 방정식 자체가 총 47개라는 이야기가 됩니다.
위의 방정식을 푸는 것은 매우 어렵기 때문에 경사하강법이 등장합니다.
다시 살펴보는 경사하강법
다시 경사하강법을 정리해 보기 전 수학적으로 경사하강법을 정리해 보도록 하죠
미분 가능한 함수𝑓(𝑥1,𝑥2,⋯,𝑥𝑛))에서 변수에 차례로𝑥1+Δ𝑥1,𝑥2+Δ𝑥2,⋯,𝑥𝑛+Δ𝑥𝑛 이라고 작은 값을 더해 변화시켰을 때 함수𝑓가 최솟값이려면 다음 함수가 성립합니다.
위 식을 가중치와 편향을 갖는 식에 대입해 봤을 때는 다음과 같이 정리가 가능합니다.
위의 식을 이용하면 컴퓨터에서𝐶𝑇가 최솟값이 되는 가중치 및 편향을 계산할 수 있습니다. 좌변에 위치한 현재 매개변수의 위치에 우변에서 구현 변위 벡터를 더하면 새로운 위치가 구해진다고 볼 수 있는데요, 이를 매개변수들에 대한 행렬로 만들어 일반화시키면 다음과 같습니다.
위 계산식을 반복하여 최솟값이 되는 가중치와 편향을 계산한다고 보면 될 것 같습니다.
미분지옥
다시 한번 경사하강법이 아닌, 일반적인 다변수 미분을 통해 살펴볼 것이 있습니다. 위의 예에서 47개의 파라미터가 존재하고, 이를 미분을 통해 구해야 한다고 이야기했는데요, 간단한 예로 성분𝑤211의 기울기를 계산해 보겠습니다.
𝑘번째 이미지에서 얻은 출력과 정답 데이터의 제곱오차식 𝐶𝑘에 대한𝑤211의 기울기는 다음과 같이 구할 수 있습니다.
입력데이터를 넣었을 때 처음으로 만날 수 있는 가중치인𝑤211을 구하는데도 어마어마한 식이 필요한 것 같습니다. 여기서 64번째 이미지까지의𝑤211의 기울기를 구해보면 식은 다음과 같이 정리가 가능합니다.
위 식의 각 항마다 = 0을 붙여서 미분방정식으로 만들어 계산하면 편미분 결과를 가중치와 편향의 식으로 표현이 가능하겠죠?
항 하나하나의 계산은 단순하지만 여러 항을 한꺼번에 계산하려면 '미분 지옥'이라고 하는 미분의 복잡성과 방대함에 압도당하고 맙니다. 그래서 고안된 것이 오차역전파법(Back Propagation)입니다.
기울기 계산에 대한 단순화
상당히 복잡한 위 계산 과정에서 발견할 수 있는 특징이 있는데, 바로 기울기 성분은 학습 결과 하나하나의 단순한 합이라는 것입니다. 즉, 비용함수 𝐶𝑇의 편미분은 유닛마다 얻어낸 학습 결과 각각에서 얻은 미분의 합이라고 볼 수 있습니다! 즉, 가중치에 대한 기울기를 구하려면 제곱오차 𝐶의 편미분을 구한 결과에 학습 데이터를 대입한 후 학습 결과 전체를 더하면 64번의 편미분을 계산한 결과를 얻어낼 수 있다는 이야기입니다. 즉! 복잡해 보이는 위 수식에서𝑎,𝑧는 모두 우리가 구할 수 있는 상숫값 이라는 것입니다.
즉𝐶𝑇의 편미분의 결과에 학습 데이터만 입력하면 실제 우리가 할 계산은 아래와 같이만 하면 된다는 이야기가 됩니다!
유닛의 오차(𝛿𝑙𝑗)
간단한 다변수 함수의 최솟값을 찾을 때는 제일 단순하고 쉽게 구할 수 있는 경사하강법이 효율적이지만, 변수와 파라미터가 함수와 복잡하게 얽혀 잇는 신경망이라면 경사하강법조차 계산하기 어려워져 그대로 사용할 수가 없게 됩니다. 미분 지옥에 빠져버린다는 뜻이죠, 그래서 등장한 것이 오차역전파법입니다.
오차역전파법은 복잡한 미분의 계산을 '수열의 점화식'으로 대체합니다. 이 점화식을 제공할 수 있는 것이 각 유닛의 오차(error)라고 불리는 변수 𝛿𝑙𝑗입니다. 제곱 오차 𝐶를 이용하여 정의하면 다음과 같습니다.
위의 식에서𝑙은 레이어의 인덱스,𝑗는 유닛의 인덱스를 의미합니다. 만약 2층 1번째 유닛의 오차와 3층 2번째 유닛의 오차를 구한다면 다음과 같이 구할 수 있겠습니다.
𝑧는 일단 입력값, 가중치와 편향을 통해 얻어진 값이라고 판단할 수 있습니다. 하지만 우리가 원하는 것은 언제나 비용함수(손실함수)의 값을 최소화할 수 있는𝑤를 원하기 때문에, 손실함수 𝐶에 대한𝑤를 계산하기 위해 다음과 같이 먼저 생각해야 합니다. 즉∂𝐶∂𝑤를𝛿𝑙𝑗로 표현하기 위한 과정을 지금부터 알아보겠습니다.
먼저 편미분의 연쇄법칙을 이용해∂𝐶∂𝑤211211를 알아보겠습니다.
여기서𝑧2121은𝑧21=𝑤211𝑥1+𝑤212𝑥2+⋯+𝑏21이기 때문에 다음 식을 도출해 낼 수 있습니다.
위의 식을 모두 정리해 보면 다음과 같은 수식이 완성됩니다.
이것은 무엇을 의미할까요? 손실함수에 대한 가중치의 변화량을 구하기 위해서는, 현재 층의 오차에다가 이전 층에서 흘러들어오고 있는 우리가 알 수 있는 값을 곱했다는 것을 알 수 있습니다! 조금씩 오차역전파법에 다가서고 있습니다. 아직 끝이 아닙니다. 위의 식을 일반화 해보기 위해서 다른 유닛에 대한 내용도 알아보겠습니다. 이번엔∂𝐶∂𝑤311(출력층)을𝛿𝑖𝑗로 표현해 보겠습니다.
위의 식에서는 입력값인𝑥1,𝑥2같은 형태로 표현했지만, 유닛을 지나온 값이기 때문에 활성화 함수가 적용되어𝑎1,𝑎2로 표현하면𝑧31은 다음과 같이 표현 가능합니다.
아까와 비슷하게∂𝑧31∂𝑤311을 구해보면 그 결과는𝑎21가 등장합니다. 최종적으로
로 정리가 가능합니다. 그렇다면 편향𝑏는 어떨까요? 계산 과정은 똑같습니다. 그 결과𝑏21,𝑏31의 편미분은 다음과 같아집니다.
편향𝑏는 그 오차가 그대로 편미분 값으로 적용된다고 볼 수 있네요. 그렇다면 마지막으로𝛿𝑙𝑗을 일반화 해보면 다음과 같이 정리가 됩니다.
위의 정리된 식에서𝑙은 현재 층 인덱스,𝑗는 다음 유닛의 인덱스,𝑖는 이전 유닛의 인덱스를 의미합니다. 여기서 추가적으로 살펴봐야 할 사실은𝛿𝑙𝑗와𝛿𝑙+1𝑗의 관계에 대해서 알아볼 필요가 있습니다.
위의 일반화된 식의 의미는 유닛의 오차𝛿𝑙𝑗만을 구하게 되면 경사하강법을 위한 비용함수 𝐶의 편미분도 한꺼번에 구할 수 있다는 이야기가 됩니다. 정리해 보겠습니다.
이어서 알아볼 내용은 무엇일까요? 신경망에서의𝛿𝑙𝑗과𝛿𝑙+1𝑗의 관계에서𝛿𝑙𝑗을 구하는 오차역전파법을 실제로 다뤄 보는 것입니다!
신경망과 오차역전파법
오차역전파법은 유닛의 오차 𝛿𝑙𝑗의 점화식을 먼저 만들고, 해당 점화식에서 복잡한 미분 계산을 해결하는 방법입니다. 오차역전파법은 복잡한 미분 계산을 단순히 수열의 점화식으로 만들어 버린다는 점입니다. 이제 실제 신경망에서 오차역전파법을 적용시키는 방법에 대해 이야기해 보겠습니다.
오차역전파법은𝑙층𝑗번째 유닛이라고 할 수 있는 변수𝛿𝑙𝑗을 정의합니다. 그리고𝛿𝑙𝑗을 이용하면 제곱오차(손실함수)에 대한 기울기도 구할 수 있었는데요,𝛿𝑙𝑗을 단순한 수열에 비유하면 정말 쉽게 말항(마지막 항)을 구해낼 수 있습니다.
수열{𝛿𝑙𝑗}의 말항에 해당하는 오차, 즉 출력층의 오차를 구한다고 했을 때 세울 수 있는 식은𝛿3𝑗(𝑗=1,2) 으로 설정하고 오차를 구해보도록 하겠습니다.
활성화 함수는ℎ(𝑧)로 정의해 보도록 하겠습니다.
위 식에서 제곱오차𝐶및 활성화 함수에 대한 미분값ℎ′(𝑧3𝑗)이 주어졌습니다. 이를 이용하면 손쉽게 말할에 해당하는 출력층 유닛의 오차도 구할 수 있습니다.
출력층의 층 번호를𝐿로 일반화 해서 표현이 가능합니다.
그렇다면 신경망 유닛의 오차𝛿31을 구해보겠습니다. 활성화 함수ℎ(𝑧)는 시그모이드 함수𝜎(𝑧)로 하겠습니다. 제일 먼저 해야 할 일은 제곱오차 𝐶를 𝑎3131로 편미분 하는 것부터 시작합니다.
위 식을 유닛의 오차를 구하기 위한 식에 대입하면 다음과 같습니다.
𝛿32역시 같은 방식으로 계산이 가능합니다.
여기서 시그모이드 함수를 미분했었던 공식을 적용시키면
으로ℎ′(𝑧32)를 구체화시킬 수 있게 되고, 이를 최종적으로 적용시키면
식이 유도 됩니다. 위에서 유도된 식은 출력층에 대한 식입니다. 그렇다면 이전 층인 은닉층에 대한𝛿𝑙𝑗은𝛿𝑙+1𝑗과 간단한 관계식으로 연결되어 있기 때문에𝛿𝑙+1𝑗을 이용해𝛿𝑙𝑗을 구하는 것도 가능해집니다. 편미분의 연쇄 법칙을 이용해서𝛿21에 대한 다음 식을 유도할 수 있습니다.
잘 살펴보면 출력층으로부터 제곱오차를 편미분 하여 이전에 흘러들어온 값(𝑎)에 대한𝑧의 편미분 값을 연쇄법칙으로 구하고 있는 모습이 보입니다. 위를 부분별로 살펴보도록 하겠습니다. 먼저∂𝐶∂𝑧31과∂𝐶∂𝑧32은𝛿형태로 나타낼 수 있을 것 같습니다.
그다음∂𝑧31∂𝑎21와∂𝑧32∂𝑎21은 각각 다음처럼 구할 수 있습니다.
마지막∂𝑎21∂𝑧21는 활성화 함수ℎ(𝑧)를 통해 다음처럼 구할 수 있습니다.
이제 마지막으로 정리를 해보면
으로𝛿21을 나타낼 수 있습니다.𝛿22 와 𝛿23도 똑같이 정리할 수 있습니다.
이제 2층과 3층의 관계를 일반화시켜 볼 수도 있을 것 같습니다.
로 확인 2층과 3층의 관계에 대해 이야기할 수 있습니다. 이는 다시𝑙층과 그다음 층인𝑙+1의 관계식으로 일반화할 수 있습니다.
미분을 하지 않았다?
수식(𝛿31𝑤31𝑖+𝛿32𝑤32𝑖)ℎ′(𝑧2𝑖)을 살펴보면𝛿31과𝛿32은 각각𝛿31=(𝑎31−𝑡1)ℎ′(𝑧31)과𝛿32=(𝑎32−𝑡2)ℎ′(𝑧32)에서 얻을 수 있는 것이 확인됩니다. 즉 수식(𝛿31𝑤31𝑖+𝛿32𝑤32𝑖)ℎ′(𝑧2𝑖)을 이용하면 층 2의𝛿2𝑖의 값을 미분하지 않고 구할 수 있다는 뜻이 됩니다.
이것이 바로 오차역전파법입니다! 출력층에 있는 유닛의 오차만 구하면, 다른 유닛의 오차는 편미분 할 필요가 없게 됩니다. 보통 층의 번호가 높은 방향에서 층의 번호가 낮은 방향으로 값을 정해나갑니다. 이는 순차대로 흘러가는 수열과 반대되기 때문에 역이라는 단어를 사용하게 되는 것입니다. 마지막으로 실제 계산 예를 확인해 보겠습니다.𝛿22를 구한다고 가정하고, 활성화 함수는 시그모이드 함수𝜎(𝑧)를 사용해 보겠습니다.
위 과정에서 확인할 수 있듯이, 미분 없이𝛿22를 구하였습니다.
넘나 어렵죠... 수식을 통해서 오차역전파법을 이해하는 것이 좋지만, 다음은 조금 더 쉽게 이해할 수 있는 그림을 통한 오차역전파법을 다시 알아보겠습니다!
오차역전파법을 수식으로 이해하기 위해서는 미분의 연쇄법칙에 대해서 먼저 이야기해봐야 합니다. 계산 그래프를 통한 이해에도 연쇄법칙을 알고 들어가야 하기 때문에, 먼저 미분의 연쇄법칙부터 살펴보겠습니다.
합성 함수
함수𝑦=𝑓(𝑢)의𝑢가𝑔(𝑥)라면𝑦=𝑓(𝑔(𝑥))라고 중첩해서 표현이 가능합니다. 이때 중첩 함수𝑓(𝑔(𝑥))를 함수𝑓(𝑢)와𝑔(𝑥)의 합성 함수라고 합니다. 예를 들어 함수𝑧=(2−𝑦)2) 2은𝑢=2−𝑦와 함수𝑧=𝑢2의 합성 함수라고 할 수 있습니다. 신경망에서의 예를 들어보면
𝑦=𝑎(𝑤1𝑥1+𝑤2𝑥2+...+𝑤𝑛𝑥𝑛+𝑏)
에서 출력 함수 𝑦는 𝑥1,𝑥2,...𝑥𝑛의 1차함수 𝑓와 활성화 함수 𝑎의 합성 함수라고 볼 수 있겠습니다. 𝑧=𝑓(𝑥1,𝑥2,...,𝑥𝑛)=𝑤1𝑥1+𝑤2𝑥2+...+𝑤𝑛𝑥𝑛+𝑏𝑦=𝑎(𝑧)즉 다음과 같은 상황을 그려볼 수 있겠네요.
1) 데이터 입력𝑥1,𝑥2,...,𝑥𝑛
2) 가중 입력𝑧=𝑓(𝑥1,𝑥2,...,𝑥𝑛)=𝑤1𝑥1+𝑤2𝑥2+...+𝑤𝑛𝑥𝑛+𝑏
3) 출력𝑦=𝑎(𝑧) 일
변수 함수의 연쇄법칙
함수𝑦=𝑓(𝑢)의 변수𝑢가 일변수𝑢=𝑔(𝑥)일 때, 합성함수𝑓(𝑔(𝑥))의 도함수(미분)는 다음과 같이 정의할 수 있습니다.
위 식을 합성 함수의 미분공식이라고 볼 수 있습니다. 또한 이를 연쇄 법칙 - chain rule이라고도 합니다. 위 식을 토대로 살펴보면𝑥가 변화 했을 때 𝑦의 변화량을 구하기 위해
𝑢의 변화량에 대한𝑦의 변화량을 구하고
𝑥의 변화량에 대한𝑢의 변화량을 구해서 곱한다
또한𝑑𝑥,𝑑𝑦,𝑑𝑢를 하나의 변수로 놓고 생각해 봤을 때 좌변은 우변의𝑑𝑢를 약분한 것입니다. 따라서 합성함수의 미분은 분수처럼 약분할 수 있다고 생각할 수 있겠네요. 이 규칙을 토대로𝑦가𝑢의 함수,𝑢가 𝑣의 함수,𝑣가 𝑥의 함수일 때,𝑑𝑦𝑑𝑥는 다음과 같습니다.
또 다른 예를 들어 보겠습니다. 시그모이드 함수와 다른 함수를 합성해서 가정해 볼 텐데요 예를 들어 다음과 같은 식이 있다고 가정합니다.
y는 시그모이드 함수이므로, 시그모이드 함수를 미분하였을 때 다음 식으로 바꿀 수 있습니다.
그다음,𝑑𝑢𝑑𝑥즉𝑢를𝑥에 관해 미분하면𝑑𝑢𝑑𝑥=𝑤이므로 최종적인 결과는 다음처럼 계산이 가능합니다.
다변수 함수의 연쇄법칙
변수가 여러 개인 다변수 함수에도 연쇄법칙을 적용할 수가 있습니다. 위처럼 분수를 다루듯이 미분 방정식을 변형하겠지만 관련된 모든 변수에 연쇄법칙을 적용할 필요가 있으므로 단순하지는 않습니다.
먼저 변수가 2개인 함수일 때를 생각해 보도록 하겠습니다.𝑧=𝑓(𝑢,𝑣)라는 함수가 있고,𝑢와𝑣역시 각각𝑢=𝑔(𝑥,𝑦)와𝑣=ℎ(𝑥,𝑦)라는 함수라는 함수라고 생각할 때,𝑧=𝑓(𝑔(𝑥,𝑦),ℎ(𝑥,𝑦))라고 바꿀 수 있습니다. 이때 다음과 같은 다변수 함수의 연쇄법칙이 성립하게 됩니다.
x에 대한 편미분을 구해서 x가 포함된 모든 함수에 대한 미분값에 연쇄법칙을 적용시킨 다음 더해주면 됩니다. 다변수 함수의 예를 한번 들어보겠습니다.
이변수 함수가𝐶=𝑢2+𝑣2이고,𝑢=𝑎𝑥+𝑏𝑦,𝑣=𝑝𝑥+𝑞𝑦(𝑎,𝑏,𝑝,𝑞는 상수)라면 연쇄법칙의 계산 과정은 다음과 같습니다.
# feature 전처리
X_train = X_train.reshape(X_train.shape[0], -1)
X_train = X_train / 255.0 # 이미지 정규화 기법. 255.0 으로 나눠주면 모든 픽셀 데이터가 0 ~ 1사이의 값을 갖게 되고, 훈련이 쉽게 된다.
X_test = X_test.reshape(X_test.shape[0], -1)
X_test = X_test / 255.0
X_train.shape, X_test.shape
훈련(학습)
미니 배치 선정
반복 횟수 설정
학습률 선정
# 반복문 돌릴 때 진행 상황을 프로그래스 바로 확인하게 해줌
from tqdm import tqdm_notebook
# 반복 횟수 설정
iter_nums = 10000
# 미니 배치 설정
train_size = X_train.shape[0]
batch_size = 100
# 학습률 설정
learning_rate = 0.1
network = TwoLayerNet(input_size=input_size, hidden_size=hidden_size, output_size=10)
for i in tqdm_notebook(range(iter_nums)):
# 미니 배치 인덱스 선정
batch_mask = np.random.choice(train_size, batch_size)
# 미니 배치 만들기
X_batch = X_train[batch_mask]
t_batch = y_train_dummy[batch_mask]
'''
각 배치 마다의 기울기를 계산
network의 numerical_gradient_params에서 하는 일
1. 예측
2. cross_entropy_error를 이용한 Loss 구하기
3. 구해진 Loss값을 이용해 미분을 수행해서 << 각 층의 매개변수 기울기를 저장 >>
'''
grads = network.numerical_gradient_params(X_batch, t_batch)
# 모든 매개변수의 기울기를 업데이트 ( 경사하강법 )
# grads 딕셔너리와 params 딕셔너리의 매개변수 이름이 똑같죠?
for key in grads.keys():
network.params[key] -= learning_rate * grads[key] # 경사하강법 공식
# 갱신된 Loss 확인
loss = network.loss(X_batch, t_batch)
print("Step {} -> Loss : {}".format(i+1, loss))
위 코드에서는 미니배치 크기를 100으로 지정하였습니다.
매번 60,000 개의 훈련 데이터에서 임의로 100개의 데이터를 추려내고, 그 100개의 미니배치를 대상으로 확률적 경사 하강법(SGD)을 수행해 매개변수를 갱신합니다.
경사법에 의한 갱신 횟수(반복 횟수)를 10,000번으로 설정하고, 갱신할 때마다 훈련데이터에 대한 손실 함수를 계산해 그 값을 추가하여 그래프를 그릴 용도로 만들어 놓았습니다.
학습 횟수가 늘어갈수록 손실 함수의 값은 줄어들기 시작하며, 이는 학습이 잘 되고 있다는 것을 뜻합니다.
즉, 신경망의 가중치 매개변수가 데이터에 적응해 나간다고 생각할 수 있습니다.
정리하자면! 데이터를 반복해서 학습함으로써 최적 가중치 매개변수로 서서히 다가서고 있다는 것을 의미합니다.
에폭(epoch)
1 에폭으로 훈련 데이터와 시험 데이터에 대한 정확도를 기록하겠습니다.
여기서 에폭이란 하나의 단위인데요, 1 에폭은 학습에서 훈련 데이터를 모두 소진했을 때의 횟수에 해당합니다.
예를 들어 10,000개의 데이터를 100개의 미니배치로 학습할 경우, 경사법을 100번 진행하면서 반복하면 모든 훈련 데이터를 소진하겠죠? 이 경우에는 100회 훈련했을 때를 1 에폭으로 생각합니다.
테스트 데이터로 평가하기
훈련 데이터를 이용해서 손실 함수의 값이 서서히 내려가는 것이 확인되었습니다.
여기서의 손실 함수의 값이란 정확히는 '훈련 데이터의 미니배치에 대한 손실 함수'의 값이라고 생각할 수 있는데요, 훈련 데이터의 손실 함숫값이 작아지는 것은 신경망이 잘 학습하고 있다는 방증이지만, 다른 데이터셋에도 비슷한 실력을 발휘할지는 아직 확실하지 않습니다.
학습에서는 훈련 데이터 외의 데이터를 올바르게 인식하는지를 확인해야 합니다. 다른 말로 과대적합(overfitting)을 일으키지 않는지를 확인해야 하는데요, 과대적합이 되어버리면 훈련 데이터에 있는 이미지만 잘 구분하고, 그렇지 않은 이미지는 식별할 수 없다는 뜻이 됩니다.
신경망의 학습의 원래 목표는 처음 보는 데이터도 잘 구분할 수 있는 '범용 능력'을 익히는 것입니다.
이 범용 능력을 평가하기 위해서는 훈련 데이터에 포함되지 않는 새로운 데이터인 테스트 데이터를 사용해서 평가해 보아야 합니다. 따라서 학습 도중 정기적으로 훈련 데이터와 시험 데이터를 대상으로 정확도를 기록합니다.
위의 결과에서 알 수 있듯이, 1 에폭마다 (100 개의 미니배치 * 600 회씩 = 60,000 개의 데이터를 훈련용으로 사용할 때마다)의 모든 훈련 데이터와 시험 데이터에 대한 정확도를 계산하고, 그 결과를 기록했습니다.
보통 정확도는 1에폭마다 기록하는데요, for문 안에서 매번 계산하기에는 시간이 오래 걸리기도 하고, 또 그렇게까지 자주 기록할 필요도 없기 때문입니다. 그저 학습 횟수에 대한 정확도 변화 추이만 알 수 있으면 괜찮습니다.
학습이 진행되면서( 에폭이 진행될수록 ) 훈련 데이터와 시험 데이터를 사용하고 평가한 정확도가 모두 좋아지고 있다는 것이 확인됩니다. 또, 두 정확도에는 차이가 거의 없음을 알 수 있습니다. 이번 학습에서는 오버피팅이 일어났다고 보기 힘들다고 생각합니다!
이로써 신경망 학습을 마무리하였습니다!! 다음글에서는 역전파에 대하여 다뤄보도록 하겠습니다!
이에 이어, 경사법(경사하강법)의 원리와 신경망에서의 기울기에 대한 신경망 학습을 알아보도록 하겠습니다.
경사법(경사하강법)
머신러닝 문제 대부분은 학습 단계 "fit"에서 최적의 매개변수를 찾아냅니다.
신경망 역시 최적의 매개변수를 학습 시에 찾아야 하는데요, 이때의 매개변수는 가중치와 편향을 이야기합니다.
여기서 최적이란 손실 함수가 최솟값이 될 때의 매개변수 값을 의미하는데요, 일반적인 문제의 손실함수는 매우 복잡합니다.
왜냐하면 매개변수 공간(범위)이 광대하여 어디가 최솟값이 되는 곳인지를 알아내기가 힘들기 때문이죠.
이런 상황에서 손실 함수의 기울기를 잘 이용해 함수의 최솟값(또는 가능한 한 작은 값)을 찾으려는 방법이 경사 하강법 입니다.
여기서 미분이 활용됩니다! 각 지점에서 함수의 결괏값을 낮추는 방안을 제시하는 지표가 기울기이기 때문이죠. 하지만 어떤 특정 지점의 기울기가 가리키는 곳에 정말 함수의 최솟값이 있는지, 즉 그쪽으로 정말로 나아갈 방향인지는 보장할 수 없습니다.
실제로 복잡한 함수에서는 기울기가 가리키는 방향에 최솟값이 없는 경우가 대부분입니다.
주의할 점은 경사법은 기울기가 0인 장소를 찾지만, 그것이 반드시 최솟값이라고 할 수는 없습니다.
극솟값이나 안장점이 될 수도 있는데요, 찌그러진 모양의 함수라면 (사실 대부분이 찌그러진 함수) 함수의 그래프가 평평한 곳으로 파고들면서 고원이라고 하는, 학습이 진행되지 않는 정체기에 빠질 수가 있습니다.
즉, 기울어진 방향이 꼭 최솟값을 가리키는 것은 아니지만 그 방향으로 가야 함수의 값을 줄일 수 있습니다.
그래서 최솟값이 되는 장소를 찾는 문제에서는 기울기 정보를 단서로 나아가야 할 방향을 정해야 합니다.
경사법 원리
경사법은 현 위치에서 기울어진 방향으로 일정 거리만큼 이동합니다.
그런 다음 이동한 곳에서도 마찬가지로 기울기를 구하고, 또 그 기울어진 방향으로 계속 나아가기를 반복합니다.
이렇게 해서 함수의 결괏값을 줄이는 것이 경사법(gradient method) 입니다. 경사법은 머신러닝 모델을 최적화하는데 흔히 쓰이는 방법입니다. 특히나 저희가 해볼 신경망 학습에는 경사법을 많이 사용합니다. 경사법을 수식으로 나타내면 다음과 같습니다.
위의 식에서 𝜂(eta. 에타)는 갱신하는 양을 나타냅니다.
이를 신경망에서는 학습률(learning rate)라고 볼 수 있는데요. 한 번의 학습으로 얼마큼 학습할지, 즉 다음 지점을 얼마만큼 이동할지에 대한 거리가 됩니다. 위 식은 1회에 해당하는 갱신이고, 이 단계를 계속 반복합니다.
즉, 위 식 처럼 변수의 값을 갱신하는 단계를 여러 번 반복하면서 서서히 함수의 값을 줄이는 것입니다. 또 여기서는 변수가 2개인 경우를 이야기했지만, 변수가 늘어도 늘 같은 편미분 값으로 갱신하게 됩니다.
또한 학습률 𝜂 값은 미리 특정 값으로 고정시켜 놓아야 합니다. 0.01이나 0.001 같이 정해두게 되는데, 일반적으로 이 값이 너무 크거나 작으면 '좋은 장소'를 찾아갈 수 없습니다.
신경망 학습에서는 보통 이 학습률 값을 변경하면서 올바르게 학습하고 있는지를 확인하면서 진행합니다. 파이썬 코드로 위의 수식을 나타내 보겠습니다.
# 경사하강법 구현
# 편미분 대상 함수, 초기 x 좌표, lr, step_num,
def gradient_descent(f, init_x, lr=0.01, step_num=100):
x = init_x
for _ in range(step_num):
# step_num 만큼 반복 하면서 기울기 배열( 기울기 벡터 )를 구해서 x를 업데이트
# 1. 기울기 배열을 구하자
grad = numerical_gradient(f, x)
x -= lr * grad
print("기울기 : {} \t x 좌표 : {}".format(grad, x))
return x
첫 번째 인수 f는 최적화할 함수, init_x는 초깃값, lr은 learning_rate로써 학습률, step_num은 경사법에 다른 반복 횟수를 뜻합니다.
함수의 기울기는 미분 함수 numerical_gradient로 구하고, 그 기울기에 학습률을 곱한 값으로 갱신하는 처리를 step_num번 for문을 통해 반복합니다.
실제로 진정한 최솟값은 (0, 0) 이므로 경사법으로 거의 정확한 결과를 얻었다고 볼 수 있겠습니다.
갱신 과정을 그림으로 나타내면 다음과 같습니다. 검사할 때마다 거의 원점에 가까워지는 것이 확인됩니다.
# coding: utf-8
import numpy as np
import matplotlib.pylab as plt
def gradient_descent(f, init_x, lr=0.01, step_num=100):
x = init_x
x_history = []
for i in range(step_num):
x_history.append( x.copy() )
grad = numerical_gradient(f, x)
x -= lr * grad
return x, np.array(x_history)
init_x = np.array([-3.0, 4.0])
lr = 0.1
step_num = 20
x, x_history = gradient_descent(function_2, init_x, lr=lr, step_num=step_num)
plt.plot( [-5, 5], [0,0], '--b')
plt.plot( [0,0], [-5, 5], '--b')
plt.plot(x_history[:,0], x_history[:,1], 'o')
plt.xlim(-3.5, 3.5)
plt.ylim(-4.5, 4.5)
plt.xlabel("X0")
plt.ylabel("X1")
plt.show()
학습률이 크거나 너무 작으면 좋은 결과를 얻을 수 없습니다... 두 경우를 실험해 보죠!
# 학습률이 너무 큰 예. lr = 10.0
init_x = np.array([-3.0, 4.0])
result, _ = gradient_descent(function_2, init_x=init_x, lr=10.0, step_num=100)
print("Learning Rate 10.0 : {}".format(result))
# 학습률이 너무 작은 예. lr = 1e-10
init_x = np.array([-3.0, 4.0])
result, _ = gradient_descent(function_2, init_x=init_x, lr=1e-10, step_num=100)
print("Learning Rate 1e-10 : {}".format(result))
하이퍼 파라미터인 learning rate를 적절하게 조절하는 것이 좋습니다! adam 같은 알고리즘은 이러한 learning rate도 적절하게 조절해 줍니다. (물리의 가속도 개념)
학습률이 너무 크면 큰 값으로 발산해 버리고, 반대로 너무 작으면 최솟값에 도달하지 않은 채 갱신이 거의 안되고 끝나버립니다.
이것이 학습률을 잘 조절해야 하는 이유입니다!
참고로 이런 학습률 같은 사람이 직접 지정해야 하는 매개변수를 하이퍼 파라미터 라고 합니다.
신경망에서의 기울기
신경망 학습에서의 기울기도 위와 마찬가지로 구할 수 있습니다. 여기서 말하는 기울기는 가중치 매개변수 W에 대한 손실함수의 기울기입니다.
신경망의 학습이란? 손실함수 (Loss)의 값을 최소화시키는 가중치 W와 편향 B를 구하는 것
따라서 가중치의 기울기 배열을 구할 수 있지 않을까?
Loss에 대한 W의 미분값을 구하는 것
W가 변했을 때 Loss는 얼마만큼 변할 것인가?
예를 들어 2 X 3 형태의 가중치를 만들고, 손실함수를 L로 정의한 신경망을 생각해 볼 수 있습니다. 이때 경사도는 ∂𝐿 / ∂𝑊로 나타 낼 수 있습니다. 수식으로 자세히 살펴봅시다!
∂𝐿 / ∂𝑊의 각 원소는 각각의 원소에 대한 편미분입니다.
∂𝐿 / ∂𝑤11은 𝑤11을 조금 변경했을 때 손실함수 𝐿이 얼마나 변화하느냐를 나타낼 수 있습니다. 여기서 중요한 점은 ∂𝐿 / ∂𝑊의 형상(shape)이 𝑊와 같다는 것입니다. 둘 다 2 X 3 형태의 행렬인 것을 확인할 수 있습니다. 그럼 간단한 신경망을 예로 들어 실제로 기울기는 구하는 코드를 구해보도록 하겠습니다.
# 클래스로 하는 이유
# tf, keras, pytorch 등을 사용하면 보통 개발자가 직접 커스터마이징 하는 경우가 많습니다.
# 아주 기본적인 CNN, RNN (LSTM) 같은 것들은 그냥 쓰면 되는데, 실제 실무에서는 이런일이 많이 없습니다.
# 특정한 비즈니스에 맞춰서 레이어를 여러분들이 커스터마이징 하는 것이 필요합니다
class SimpleNet:
# 신경망에서의 초기화는 필요한 매개변수를 준비하는 과정
def __init__(self):
self.W = np.random.randn(2, 3) # 정규분포로 초기화
# 실제 신경망은 np.random.randn(2, 3) * 0.01 로 만드는 경우가 많습니다. (표준편차를 0.01로 설정)
def predict(self, x):
return np.dot(x, self.W)
def loss(self, x, t):
# loss를 구하는 과정
# 1. 예측
z = self.predict(x)
# 2. softmax 통과
y = softmax(z)
# 3. loss함수를 이용해 실제 loss값 구하기
loss = cross_entropy_error(y, t)
return loss
SimpleNet 클래스의 객체를 생성하면 자동으로 2 X 3 형태의 가중치 배열을 랜덤으로 생성하게 됩니다.
그다음 예측을 수행하는 predict는 각 뉴런에서 객체가 만들어질 때 생성된 가중치 행렬 W와 데이터인 x의 내적을 구하고, loss 메소드 에서는 예측된 결과(predict)에 softmax 활성화 함수를 적용시키고 나서 해당 결과에 손실 함수인 cross_entropy_error를 이용해 손실값을 찾습니다. x는 입력 데이터, t는 정답 레이블입니다.
net = SimpleNet()
print("가중치 확인 : \n{}".format(net.W))
x = np.array([0.6, 0.9])
p = net.predict(x)
# 예측값은 몇개가 나오지?
print("단순 예측 값 : \n{}".format(p))
print("최댓값의 인덱스 : {}".format(np.argmax(p)))
# 임의의 정답을 마련하자
t = np.array([0, 1, 0]) # 정답은 1
net.loss(x, t)
Loss에 대한 기울기를 구하는 함수를 따로 만들어보도록 하겠습니다! numerical_gradient(f, x)를 써서 구하면 됩니다.
# Loss의 대한 W의 기울기를 구하기 위한 함수
def f(W):
return net.loss(x, t)
# 기울기에 대한 Loss의 기울기 배열을 구할 수 있어요
dW = numerical_gradient(f, net.W)
dW
numerical_gradient(f, x)의 인수 f는 함수, x는 함수 f의 인수입니다. 그래서 가중치를 의미하는 net.W를 인수로 받아 손실 함수를 계산하는 새로운 함수 f를 정의하였습니다.
numerical_gradient에서는 함수 f와 net.W를 입력받아 가중치들에 대해 한꺼번에 기울기를 구합니다. dW는 numericalgradient의 결과로써, 2 X 3차원의 배열입니다. dW를 내용을 보겠습니다.
예를 들어 𝑤11에 위치한 0.12 정도의 값은 𝑤11의 값을 ℎ 만큼 늘리면 손실 함수의 값은 0.12ℎ 만큼 증가한다는 뜻이 됩니다.
마찬가지로 𝑤32는 대략 -0.47이니, 𝑤32를 ℎ만큼 늘리면 손실 함수의 값은 0.5ℎ만큼 감소한다고 볼 수 있습니다. 그래서 손실 함숫값을 줄인다는 관점에서 𝑤11은 음의 방향으로 갱신해야 하고, 𝑤32는 양의 방향으로 갱신해야 함을 알 수 있습니다.
또한 한 번에 갱신되는 양에는 𝑤32가 𝑤11보다 크게 바뀐다는 사실도 알 수 있죠 위 코드의 f(W)를 lambda 형태로 바꿀 수도 있습니다.
dW = numerical_gradient(lambda w : net.loss(x, t), net.W)
dW
Summary
신경망 학습에 대한 이야기는 여기까지면 충분합니다.
몇 가지 키워드로 정리해 보고, 다음 글에서 최종적으로 MNIST를 학습할 수 있는 신경망을 직접 구현하도록 해봅시다!
전제
신경망에는 적응 가능한 가중치와 편향이 있고, 이 가중치와 편향을 훈련 데이터에 적응하도록 조정하는 과정을 '학습'이라 합니다.
4 단계로 정리해 보겠습니다!
1단계 - 미니배치
훈련 데이터 중 일부를 무작위로 가져옵니다. 이렇게 무작위로 가져온 데이터들을 미니 배치라 하며, 그 미니배치의 손실 함숫값을 줄이는 것이 목표입니다.
2단계 - 기울기 산출
미니 배치의 손실 함숫값을 줄이기 위해 각 가중치 매개변수의 기울기를 구합니다. 기울기는 손실 함수의 값을 가장 작게 하는 방향을 제시합니다.
3단계 - 매개변수 갱신
가중치 매개변수를 기울기 방향으로 아주 조금씩 갱신시킵니다.
4단계 - 반복
1 ~ 3 단계를 계속 반복합니다.
정리한 내용은 경사 하강법으로 매개변수를 갱신하는 방법이라고 볼 수 있으며, 데이터를 미니 배치로 무작위로 가져오기 때문에 이를 확률적 경사 하강법(stochastic gradient descent, SGD)라고 합니다.
확률적으로 무작위로 골라낸 데이터에 대해 수행하는 경사하강법이라는 의미가 됩니다. 대부분의 딥러닝 프레임워크에서는 SGD라는 함수로 해당 기능을 구현하고 있습니다.
다음 글에서 본격적으로 MNIST 데이터셋을 이용한 2층 신경망(은닉층이 1개인 네트워크)을 구현해 보겠습니다!
이번 블로그로는 (상)&(하)로 나눠집니다. (상) 편에서 미분과 편미분에 대하여 설명하며, (하)에서 경사하강법과 신경망에서의 기울기에 대하여 알아보도록 하겠습니다.
경사법(경사하강법)을 위한 미분
경사법에서는 기울기 값을 기준으로 최적화하기 위한 방향을 결정합니다. 기울기의 성질을 알아보기 앞서 미분을 먼저 살펴보겠습니다.
미분
우리가 트랙에서 달리는 모습을 상상해 보겠습니다.
처음부터 10분 동안 2km씩 달렸다고 가정했을 때 속도는 2(거리) / 10(시간) = 0.2(속도)만큼의 속도(변화)로 뛰었다고 해석할 수 있을 것입니다.
위의 예에서는 달린 거리가 시간에 대해서 얼마나 변화했는지를 계산한 것입니다. 다만 여기에서 10분에 2km를 뛰었다는 이야기는 정확하게는 10분 동안의 평균 속도를 구한 것이죠.
하지만 미분은 '특정 순간'의 변화량을 뜻합니다. 따라서 10분이라는 시간을 최대한 줄여 한 순간의 변화량을 얻는다고 볼 수 있습니다.
이처럼 미분은 한순간의 변화량을 표시한 것입니다.
위 식은 함수의 미분을 나타낸 식입니다.
좌변은 함수𝑓(𝑥)의𝑥에 대한 미분(𝑥에 대한𝑓(𝑥)의 변화량)을 나타낸 기호라고 볼 수 있습니다.
결국,𝑥의 작은 변화가 함수𝑓(𝑥)를 얼마나 변화시키느냐를 의미합니다. 이때 시간의 작은 변화, 즉 시간을 뜻하는ℎ를 한없이 0에 가깝게 한다는 의미를limℎ→0로 나타내었습니다. 그렇다면 파이썬으로 미분 함수를 구현해 보겠습니다.
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)))
위의 문제를 풀기 위해 변수가 하나인 함수를 정의했으며, 그 함수를 미분하는 형태로 구현하여 풀어보았습니다.
가령,𝑥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)로 정리한 것을기울기(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()
기울기에 대한 그림은 위처럼 방향을 가진 벡터(화살표)로 그려집니다.
이 그림을 보면 기울기는 함수의 '가장 낮은 장소(최솟값)'를 가리키는 것 같습니다. 한 점을 향하고 있는 것이 확인되는데요, 또한 가장 낮은 곳에서 멀어질수록 화살표의 크기가 커지는 것도 확인이 가능합니다.
중요한 것은 기울기 자체는 가장 낮은 장소를 가리키고 있으나, 실제는 반드시 그렇다고는 할 수 없습니다.
사실 기울기는 각 지점에서 낮아지는 방향을 가리키는데요, 더 확실히 말하자면 기울기가 가리키는 쪽은 각 장소에서 함수의 출력 값(결괏값)을 가장 크게 줄이는 방향입니다. 확실히 기억해야 합니다.
지금까지 경사하강법을 위한 미분과 편미분을 알아보며 준비를 하였습니다. 다음 블로그에서는 경사법(경사하강법)의 원리와 신경망에서의 기울기에 대한 신경망 학습을 알아보도록 하겠습니다.