텐서플로우(Tensorflow)는 머신러닝을 위한 오픈소스 플랫폼으로, 딥러닝 과제를 수행하기위해 제공하는 라이브러리입니다. ML 모델을 개발하고 학습시키는 데 도움이 되는 핵심적인 오픈소스 라이브러리입니다. 현 시점 기준(20/02/06)으로, 세계에서 가장 많이 사용되는 딥러닝 프레임워크입니다. 구글에서 주도적으로 개발하는 플랫폼이며, 구글 코랩(Colab)을 이용하여 주피터 노트북기반 딥러닝 코드를 사용가능합니다.
2.0버전에서는 기존의 1버전보다 성능과 편의성을 확보하였습니다. API를 간소화하였고, 즉시 실행 모드(Eager Execution)을 기본값으로 사용합니다. 또한 기존 1버전에서 사용하던 session대신, function을 사용하여 그래프 연산이 가능하도록 수정하였습니다. 또한 TPU를 지원합니다.
tf.keras를 2.0에서는 유일한 고수준 API로 선정하여 복잡하고 어려운 신경망 레이어를 손쉽게 간단한 문법으로 생성하고 학습시킬 수 있습니다.
실습환경
해당 포스팅에서는 구글에서 제공하는 Colab 환경을 사용합니다. Colab은 간단하게 구글 드라이브에서 생성이 가능하며, GPU를 제공해주는 기존의 Jupyter Notebook기반의 실습환경입니다. Colab에서는 Tensorflow라이브러리를 제공합니다. 해당 포스팅에서는 Tensorflow 2.1.0버전을 기준으로 작성하였습니다.
Uniform(균일) 분포 난수uniform함수는 균일 분포의 난수를 얻는 함수입니다. 균일 분포(uniform distribution)란 최댓값과 최솟값 사이에 있는 모든 수가 나올 확률이 동일한 분포를 의미합니다.
shape : 위에서 선언한 [1]은 return되는 난수의 모양(행과 열등의 차원)을 의미합니다.
minval: 최솟값
maxval: 최댓값
rand = tf.random.uniform([1], 0, 1)
Normal(정규) 분포 난수정규 분포(normal distribution)는 가운데가 높고 양극단으로 갈수록 낮아지는 분포를 의미합니다.
shape: 이 매개변수는 uniform과 동일합니다.
mean: 평균값을 의미합니다.
standard deviation: 표준편차를 의미합니다.
mean이 0이고, std가 1이면 표준 정규 분포라고 합니다.
rand = tf.random.normal([1],0,1)
Initialization
신경망은 간단하게 생각하면, 많은 수로 이루어진 행렬입니다. 입력-신경망-출력 구조로 이루어져 있으며, 신경망에는 수많은 숫자로 이루어진 행렬이 있다고 생각하면 됩니다. 학습은 이러한 행렬의 숫자들을 적합하게 수정하는 단계라고 보면 좋을 것 같습니다.
그렇다면, 처음으로 신경망을 생성한다면 어떤 숫자들을 넣어주는 것이 좋을까요? 초깃값은 랜덤한 난수들을 지정해주게 됩니다. 신경망의 초깃값을 정하는 것을 Initialization(초기화)라고 합니다. 초깃값을 설정하는 것은 아주 중요한 역할을 하게됩니다. 잘못 설정할 경우, 기울기 소실 또는 다양한 문제를 발생시킬 수 있습니다.
Initialization을 제대로 안하면?
모두 0으로 두는 경우에는 층마다 동일하게 값을 가지게 되며, 역전파시 가중치의 update가 동일하게 이루어집니다. 입력 값의 데이터를 무시하는 문제가 발생합니다. 또한 층을 나누는 의미가 없어집니다. (모두 같은 update를 하기 때문에..)
평균 0에 표준편차가 1인 정규분포를 가지도록 가중치를 랜덤하게 두면, 활성함수에 따라 (예를들어, sigmoid) 0과 1로만 치우치는 Gradient Vanishing 문제가 생기게 됩니다.
Gradient Vanishing 문제를 완화시키기 위해서 표준편차를 작게하여 생성한다해도 중간값으로 가중치들이 몰리는 문제가 발생할 수 있습니다.
대표적인 Initialization
Xavier Initialization (Glorot Initialization)
목적: 비선형 함수(ex. S자함수, sigmoid)의 출력값들을 표준 정규 분포 형태를 갖도록 초기화시키는 것이 목적입니다.
위의 결과들의 손실(loss, error)은 초기화된 가중치에 따라 다르지만, y-output 값을 오차 또는 에러(error)라고 합니다. 오차 값이 0에 가까워지면 정확해지고 있다는 증거가 됩니다. 위의 예제에서는 오차가 -0.356, -0.323 정도의 값을 얻었습니다. 뉴런은 학습을 통하여 오차 값을 0에 가깝게 만드는 것을 목표로 합니다. 이것은 가중치를 업데이트 하는 것으로 진행됩니다. w를 업데이트 하는 방법은 경사 하강법(Gradient Descent)라는 방법을 사용합니다.
손실(error, cost)의 최소값을 찾기 위해서 그레디언트 반대 방향으로 정의한 step size를 사용하여 조금씩 변화하면서 최적의 값을 찾아가는 방식입니다.
경사하강법의 순서
w의 시작점을 선정합니다.
해당 점에서의 손실 곡선의 기울기(편미분의 벡터)를 계산합니다. 이것은 손실 값이 가장 크게 증가하는 방향을 알려줍니다.
기울기의 반대 방향으로 이동합니다.
기울기의 크기의 일부를 시작점에 더해서 위의 과정을 반복합니다.
최소값에 점점 접근합니다.
손실이 최소값이 되는 지점을 판단하여 이것을 사용합니다.
학습률(Learning rate)
경사하강법은 기울기에 학습률(보폭)을 곱하여 다음 지점을 결정합니다.
이것은 학습을 얼마나 정밀하게 할지 또는 빠르게 할지를 결정합니다.
학습률이 너무 낮으면 학습시간이 매우 오래 걸립니다.
학습률이 너무 크면 학습은 빠르지만, 정확한 최소값을 찾지 못하고 지나칠 수 있습니다.
적절한 학습률을 설정하는 것이 좋습니다. 골디락스 학습률이란, 손실 함수의 기울기를 보고 판단하여 학습률을 설정하는 것입니다. 기울기가 작다면 더 큰 학습률을 시도하며, 단점을 보완할 수 있습니다.
간단한 학습을 하는 뉴런
이제는 경사하강법을 뉴런에 사용해보겠습니다.
아래와 같이 경사하강법을 이용한 가중치 업데이트 공식을 이용하도록 하겠습니다.
sigmoid 사용하는 뉴런
x = 1y = 0w = tf.random.normal([1],0,1) a = 0.1for i in range(1000): output = sigmoid(x*w) err = y - output w = w + x*a*err if i%100 == 99: print(i, err, output)
ReLU 사용하는 뉴런
x = 1y = 0w = tf.random.normal([1],0,1) a = 0.1for i in range(1000): output = ReLU(x*w) err = y - output w = w + x*a*err if i%100 == 99: print(i, err[0], output[0])
문제점
만약에 x = 0, y = 1로 입력과 출력을 바꿔도 학습이 잘 될까요?
x = 0
y = 1
w = tf.random.normal([1],0,1)
a = 0.1
for i in range(1000):
output = sigmoid(x*w)
err = y - output
w = w + x*a*err
if i%100 == 99:
print(i, err, output)
경사하강법에서 입력이 0이기 때문에 가중치가 업데이트 되지 못하는 현상이 발생합니다. 그래서 항상 0.5가 나오는 현상이 나타납니다. 이러한 경우를 방지하기 위해서 편향(Bias)라는 값도 뉴런에 존재합니다.
편향이 존재하는 뉴런
간단하게 말하자면 w가 업데이트 되지 않는 경우(x가 0인 경우)에 편향 값은 해당 함수의 예측 값과 정답이 얼마나 떨어져 있는지를 표현하는 값입니다. w가 업데이트 되지 않는 상황을 알아차릴 수 있는 값인 셈입니다. 편향은 가중치와 마찬가지로 예측과 정답의 차이를 통해 업데이트 됩니다. 하지만 가중치와 달리 입력값에 영향을 안받고 에러 값이 얼마나 변하냐에 따라서만 바뀌기 때문에, 위의 문제를 해결할 수 있습니다.
다시 위에서 w 가중치가 업데이트에 실패했던 입력값 0 문제를 편향을 사용하여 해결해 보겠습니다.
sigmoid 사용 뉴런학습이 지날 수록 예측 값이 목표에 가까웠던 1에 다가가며, 에러 값도 줄어드는걸 확인할 수 있습니다.
x = 0y = 1w = tf.random.normal([1],0,1) b = tf.random.normal([1],0,1) a = 0.1for i in range(1000): output = sigmoid(x*w+b) err = y - output w = w + x*a*err b = b + a*err if i%100 == 99: print(i, err, output)
ReLU 사용 뉴런ReLU 같은 경우에는 적은 학습을 통해서도 낮은 에러 값과 정확한 예측 값을 보여주고 있기때문에 초반의 에러 값과 예측 값이 학습을 더 해도 더 이상 변하지 않는 것을 확인 가능합니다.
x = 0y = 1w = tf.random.normal([1],0,1) b = tf.random.normal([1],0,1) a = 0.1for i in range(1000): output = ReLU(x*w+b) err = y - output w = w + x*a*err b = b + a*err if i%100 == 99: print(i, err[0], output[0])