상세 컨텐츠

본문 제목

딥러닝 (2) - 텐서플로우 2.0 기초와 뉴런 만들기

IT/Deep Learning

by HarimKang 2020. 2. 6. 15:48

본문

Writer: Harim Kang

해당 포스팅은 '시작하세요! 텐서플로 2.0 프로그래밍'책의 흐름을 따라가면서, 책 이외에 검색 및 다양한 자료들을 통해 공부하면서 정리한 내용의 포스팅입니다. 해당 내용은 텐서플로우 2.0 소개와 환경, 난수와 가중치 초기화, 뉴런을 만드는 법에 대한 내용을 담고 있습니다.

2020/02/04 - [IT/Deep Learning] - 딥러닝 (1) - Deep Learning 소개 및 용어 정리

 

딥러닝 (1) - Deep Learning 소개 및 용어 정리

딥러닝 - 1. Intro to Deep Learning Writer: Harim Kang 해당 포스팅은 딥러닝 공부를 시작하는 의미로 기본적인 딥러닝에 대한 설명과 용어를 정리하였습니다. 내용 구성은 전공 수업과 개인 검색, 관련 책에..

davinci-ai.tistory.com

위의 포스팅에 이어지는 내용입니다.

텐서플로우란?

텐서플로우(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버전을 기준으로 작성하였습니다.

시작하기

텐서플로우 버전 설정 및 확인

try:
  %tensorflow_version 2.x
except Exception:
  pass
import tensorflow as tf
print(tf.__version__)

Tensorflow에서의 난수 생성

  • Uniform(균일) 분포 난수

      rand = tf.random.uniform([1], 0, 1)

    uniform함수는 균일 분포의 난수를 얻는 함수입니다. 균일 분포(uniform distribution)란 최댓값과 최솟값 사이에 있는 모든 수가 나올 확률이 동일한 분포를 의미합니다.

    • shape : 위에서 선언한 [1]은 return되는 난수의 모양(행과 열등의 차원)을 의미합니다.

    • minval: 최솟값

    • maxval: 최댓값

  • Normal(정규) 분포 난수

      rand = tf.random.normal([1],0,1)

    정규 분포(normal distribution)는 가운데가 높고 양극단으로 갈수록 낮아지는 분포를 의미합니다.

    • shape: 이 매개변수는 uniform과 동일합니다.

    • mean: 평균값을 의미합니다.

    • standard deviation: 표준편차를 의미합니다.

    • mean이 0이고, std가 1이면 표준 정규 분포라고 합니다.

Initialization

신경망은 간단하게 생각하면, 많은 수로 이루어진 행렬입니다. 입력-신경망-출력 구조로 이루어져 있으며, 신경망에는 수많은 숫자로 이루어진 행렬이 있다고 생각하면 됩니다. 학습은 이러한 행렬의 숫자들을 적합하게 수정하는 단계라고 보면 좋을 것 같습니다.

그렇다면, 처음으로 신경망을 생성한다면 어떤 숫자들을 넣어주는 것이 좋을까요? 초깃값은 랜덤한 난수들을 지정해주게 됩니다. 신경망의 초깃값을 정하는 것을 Initialization(초기화)라고 합니다. 초깃값을 설정하는 것은 아주 중요한 역할을 하게됩니다. 잘못 설정할 경우, 기울기 소실 또는 다양한 문제를 발생시킬 수 있습니다.

Initialization을 제대로 안하면?

  • 모두 0으로 두는 경우에는 층마다 동일하게 값을 가지게 되며, 역전파시 가중치의 update가 동일하게 이루어집니다. 입력 값의 데이터를 무시하는 문제가 발생합니다. 또한 층을 나누는 의미가 없어집니다. (모두 같은 update를 하기 때문에..)
  • 평균 0에 표준편차가 1인 정규분포를 가지도록 가중치를 랜덤하게 두면, 활성함수에 따라 (예를들어, sigmoid) 0과 1로만 치우치는 Gradient Vanishing 문제가 생기게 됩니다.
  • Gradient Vanishing 문제를 완화시키기 위해서 표준편차를 작게하여 생성한다해도 중간값으로 가중치들이 몰리는 문제가 발생할 수 있습니다.

대표적인 Initialization

  • Xavier Initialization (Glorot Initialization)

    http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf

    • 목적: 비선형 함수(ex. S자함수, sigmoid)의 출력값들을 표준 정규 분포 형태를 갖도록 초기화시키는 것이 목적입니다.

    • 표준 정규 분포를 입력 개수의 표준 편차로 나누어 준 방식입니다.

        weight = np.random.randn(input_data, output_data)/sqrt(input_data)
    • 이전 노드와 다음 노드의 개수에 의존하는 방식으로, Uniform(균일)분포와 Normal(정규)분포를 따르는 두가지 방법으로 사용됩니다. 최적화된 상수값을 제공해줍니다.

    • 해당 초기화는 비선형함수 (sigmoid, tanh)에서 효과적입니다.

    • 하지만, ReLU에서는 출력이 0으로 수렴하는 현상이 나타나는 문제가 발생합니다.

  • He Initialization

    https://arxiv.org/abs/1502.01852

    • Xavier에서 ReLU 활성화 함수 사용시에 발생하는 문제를 해결하기 위해서 해당 초기화 방법을 사용합니다. Xavier 초기화 방식과 많이 다르지 않습니다.

    • 입력 데이터 개수의 절반의 제곱근으로 나누는 방식입니다.

        weight = np.random.randn(input_data, output_data) / sqrt(input_data/2)
    • 해당 초기화 방식도 균일과 정규 분포를 사용합니다.

    • ReLU함수와 He초기화를 사용하면, 표준 편차가 0으로 수렴하지 않습니다.

뉴런(Neuron)

뉴런은 신경망의 가장 기본적인 구성요소입니다. 이를 퍼셉트론이라고도 부르며, 입력을 받아서 계산하여 출력하는 구조로 이루어집니다.

활성 함수(Activation Function)에 대해서 살펴보겠습니다. 앞선 포스팅에서 자주 사용되는 세가지의 활성 함수를 살펴보았습니다. 그 중에서도 sigmoid와 ReLU함수를 사용하여 뉴런을 만들어보겠습니다.

활성함수 관련 설명: https://davinci-ai.tistory.com/20

  • sigmoid 함수

      import math
      def sigmoid(x):
        return 1/(1+math.exp(-x))
  • ReLU 함수

      import numpy as np
      def ReLU(x):
          return np.maximum(0,x)

간단한 예제

위와 같은 공식으로 생각을 하면 쉽습니다. 가중치와 입력값을 넣으면 출력값이 나오도록 설정해주시면 됩니다. 여기서 가중치는 정규분포의 난수로 설정하였습니다.

  1. sigmoid 사용하는 뉴런 만들기

     x = 1
     y = 0
     w = tf.random.normal([1], 0, 1)
     output = sigmoid(x*w)
     print(output)

    x = 1이고, 가중치 w를 정규 분포 난수로 선언하였습니다. 이것을들 sigmoid함수에 넣어주면 출력이 나옵니다. (아래의 출력값은 가중치 초기화 난수에 따라 달라집니다.)

  2. ReLU 사용하는 뉴런 만들기

     x = 1
     y = 0
     w = tf.random.normal([1], 0, 1)
     output = ReLU(x*w)
     print(output)

    함수만 ReLU로 바꾸고 설정은 같습니다. 가중치에 따라 출력값은 계속 바뀌며, 출력이 0인 경우, 가중치 난수 값이 0 또는 음수라는 의미입니다.

위의 결과들의 손실(loss, error)은 초기화된 가중치에 따라 다르지만, y-output 값을 오차 또는 에러(error)라고 합니다. 오차 값이 0에 가까워지면 정확해지고 있다는 증거가 됩니다. 위의 예제에서는 오차가 -0.356, -0.323 정도의 값을 얻었습니다. 뉴런은 학습을 통하여 오차 값을 0에 가깝게 만드는 것을 목표로 합니다. 이것은 가중치를 업데이트 하는 것으로 진행됩니다. w를 업데이트 하는 방법은 경사 하강법(Gradient Descent)라는 방법을 사용합니다.

경사 하강법(Gradient Descent)

설명 및 이미지: https://developers.google.com/machine-learning/crash-course/reducing-loss/gradient-descent?hl=ko

손실(error, cost)의 최소값을 찾기 위해서 그레디언트 반대 방향으로 정의한 step size를 사용하여 조금씩 변화하면서 최적의 값을 찾아가는 방식입니다.

  • 경사하강법의 순서

    1. w의 시작점을 선정합니다.
    2. 해당 점에서의 손실 곡선의 기울기(편미분의 벡터)를 계산합니다. 이것은 손실 값이 가장 크게 증가하는 방향을 알려줍니다.
    3. 기울기의 반대 방향으로 이동합니다.
    4. 기울기의 크기의 일부를 시작점에 더해서 위의 과정을 반복합니다.
    5. 최소값에 점점 접근합니다.
    6. 손실이 최소값이 되는 지점을 판단하여 이것을 사용합니다.
  • 학습률(Learning rate)

    • 경사하강법은 기울기에 학습률(보폭)을 곱하여 다음 지점을 결정합니다.

    • 이것은 학습을 얼마나 정밀하게 할지 또는 빠르게 할지를 결정합니다.

    • 학습률이 너무 낮으면 학습시간이 매우 오래 걸립니다.

    • 학습률이 너무 크면 학습은 빠르지만, 정확한 최소값을 찾지 못하고 지나칠 수 있습니다.

    • 적절한 학습률을 설정하는 것이 좋습니다. 골디락스 학습률이란, 손실 함수의 기울기를 보고 판단하여 학습률을 설정하는 것입니다. 기울기가 작다면 더 큰 학습률을 시도하며, 단점을 보완할 수 있습니다.

간단한 학습을 하는 뉴런

이제는 경사하강법을 뉴런에 사용해보겠습니다.

아래와 같이 경사하강법을 이용한 가중치 업데이트 공식을 이용하도록 하겠습니다.

  1. sigmoid 사용하는 뉴런

     x = 1
     y = 0
     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)
  2. ReLU 사용하는 뉴런

     x = 1
     y = 0
     w = tf.random.normal([1],0,1)
     a = 0.1
     for 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 문제를 편향을 사용하여 해결해 보겠습니다.

  1. sigmoid 사용 뉴런

     x = 0
     y = 1
     w = tf.random.normal([1],0,1)
     b = tf.random.normal([1],0,1)
     a = 0.1
     for 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)

    학습이 지날 수록 예측 값이 목표에 가까웠던 1에 다가가며, 에러 값도 줄어드는걸 확인할 수 있습니다.

  2. ReLU 사용 뉴런

     x = 0
     y = 1
     w = tf.random.normal([1],0,1)
     b = tf.random.normal([1],0,1)
     a = 0.1
     for 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])

    ReLU 같은 경우에는 적은 학습을 통해서도 낮은 에러 값과 정확한 예측 값을 보여주고 있기때문에 초반의 에러 값과 예측 값이 학습을 더 해도 더 이상 변하지 않는 것을 확인 가능합니다.

실습 코드는 https://github.com/harimkang/tensorflow2_deeplearning/blob/master/tensor2_1.ipynb에 작성하였습니다.

 

harimkang/tensorflow2_deeplearning

tensorflow2, deep learning study example codes. Contribute to harimkang/tensorflow2_deeplearning development by creating an account on GitHub.

github.com

Reference

Xavier: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf
Initialization: https://reniew.github.io/13/
경사 하강법: https://developers.google.com/machine-learning/crash-course/reducing-loss/gradient-descent?hl=ko

관련글 더보기