케라스 창시자에게 배우는 딥러닝/ 신경망 시작하기

신경망의 구조

  • 신경망 훈련에는 다음 요소들이 관련되어 있다.
    • 네트워크(또는 모델)를 구성하는 층
    • 입력 데이터와 그에 상응하는 타깃
    • 학습에 사용할 피드백 신호를 정의하는 손실 함수
    • 학습 진행 방식을 결정하는 옵티마이저
  • 이들 간의 상호 작용은 아래 그림에 나타냈다.
    • 연속된 층으로 구성된 네트워크가 입력 데이터를 예측으로 매핑한다.
    • 손실 함수는 예측과 타깃을 비교하여 네트워크의 예측이 기댓값에 얼마나 잘 맞는지를 측정하는 손실 값을 만든다.
    • 옵티마이저는 손실 값을 사용하여 네트워크 가중치를 업데이트 한다.

층: 딥러닝의 구성 단위

  • 신경망의 핵심적인 데이터 구조는 층이다. 층은 하나 이상의 텐서를 입력으로 받아 하나 이상의 텐서를 출력하는 데이터 처리 모듈이다.
    • 어떤 종류의 층은 상태가 없지만 대부분의 경우 가중치라는 층의 상태를 가진다.
    • 가중치는 확률적 경사 하강법에 의해 학습되는 하나 이상의 텐서이며 여기에 네트워크가 학습한 지식이 담겨 있다.
  • 층마다 적절한 텐서 포맷과 데이터 처리 방식이 다르다.
    • 예컨대 (samples, features) 크기의 2D 텐서가 저장된 간단한 벡터 데이터는 완전 연결 층(fully connected layer)이나 밀집 층(dense layer)라고도 불리는 밀집 연결 층(densely connected layer)에 의해 처리되는 경우가 많다. (케라스에서는 Dense 클래스이다)
    • (samples, timesteps, features) 크기의 3D 텐서로 저장된 시퀀스 데이터는 보통 LSTM 같은 순환 층(recurrent layer)에 의해 처리된다.
    • 4D 텐서로 저장되어 있는 이미지 데이터는 일반적으로 2D 합성곱 층(convolution layer)에 의해 처리 된다. (Conv2D 클래스)
  • 층을 딥러닝의 레고 블록처럼 생각할 수 있다. 이런 비유는 케라스 같은 프레임워크 때문에 생겼는데, 케라스에서는 호환 가능한 층들을 엮어 데이터 변환 파이프라인(pipeline)을 구성함으로써 딥러닝 모델을 만든다.
    • 여기에서 층 호환성(layer compatibility)은 각 층이 특정 크기의 입력 텐서만 받고 특정 크기의 출력 텐서를 반환한다는 사실을 말한다.
  • 다음 예를 살펴 보자.
from keras import layers
layer = layers.Dense(32, input_shape=(784,)) --32개의 유닛으로 밀집된 층
  • 첫 번째 차원이 784인 2D 텐서만 입력으로 받는 층을 만들었다. (배치 차원인 0번째 축은 지정하지 않기 때문에 어떤 배치 크기도 입력으로 받을 수 있다.)
    • 이 층은 첫 번째 차원 크기가 32로 변환된 텐서를 출력할 것이다.
    • 따라서 이 층에는 32차원의 벡터를 입력으로 받는 하위 층이 연결되어야 한다. 케라스에서는 모델에 추가된 층을 자동으로 상위 층의 크기에 맞추어주기 때문에 호환성을 걱정하지 않아도 된다.
  • 예컨대 다음과 같이 작성 했다고 가정하자.
from keras import models
from keras import layers

model = models.Sequential()
model.add(layers.Dense(32, input_shape=(784,)))
model.add(layers.Dense(10))
  • 두 번째 층에는 input_shape 매개변수를 지정하지 않았는데, 이러면 앞선 층의 출력 크기를 입력 크기로 자동으로 채택한다.

모델: 층의 네트워크

  • 딥러닝 모델은 층으로 만든 비순환 유향 그래프(Directed Acyclic Graph, DAG)이다.
    • 가장 일반적인 예가 하나의 입력을 하나의 출력으로 매핑하는 층을 순서대로 쌓는 것이다.
  • 앞으로 공부하다 보면 아주 다양한 네트워크 구조를 보게 될 것이다. 자주 등장하는 것들은 다음과 같다.
    • 가지(branch)가 2개인 네트워크
    • 출력이 여러 개인 네트워크
    • 인셉션(Inception) 블록
  • 네트워크 구조는 가설 공간(hypothesis space)을 정의한다.
    • 1장에서 머신 러닝을 ‘가능성 있는 공간을 사전에 정의하고 피드백 신호의 도움을 받아 입력 데이터에 대한 유용한 변환을 찾는 것’으로 정의했는데, 네트워크 구조를 선택함으로써 가능성 있는 공간(가설 공간)을 입력 데이터에서 출력 데이터로 매핑하는 일련의 특정 텐서 연산으로 제한하게 된다.
    • 우리가 찾아야 할 것은 이런 텐서 연산에 포함도니 가중치 텐서의 좋은 값이다.
  • 딱 맞는 네트워크 구조를 찾아내는 것은 과학보다 예술에 가깝다. 신뢰할 만한 모범적인 사례와 원칙이 있지만 연습을 해야만 적절한 신경망을 설계할 수 있는 기술을 갖추게 될 것이다.
    • (여기서 과학은 이론적으로 구성 가능한 것을 의미하고, 예술은 훈련을 통해 향상 시키는 기예의 의미에 가깝다. 실제 Art에는 그런 의미가 포함 되어 있음. 예술 작품이 아니라)

손실 함수와 옵티마이저: 학습 과정을 조절하는 열쇠

  • 네트워크 구조를 정의하고 나면 두 가지를 더 선택해야 한다.
    • 손실 함수(loss function) (목적 함수(objective function)): 훈련하는 동안 최소화 될 값이다. 주어진 문제에 대한 성공 지표가 된다.
    • 옵티마이저(optimizer): 손실 함수를 기반으로 네트워크가 어떻게 업데이트될지 결정한다. 특정 종류의 확률적 경사 하강법(SGD)를 구현한다.
  • 여러 개의 출력을 내는 신경망은 여러 개의 손실 함수를 가질 수 있다. (출력당 하나씩).
    • 하지만 경사 하강법 과정은 하나의 스칼라 손실 값을 기준으로 한다. 따라서 손실이 여러 개인 네트워크에서는 모든 손실이 (평균을 내서) 하나의 스칼라 양으로 합쳐진다.
  • 문제에 맞는 올바른 목적 함수를 선택하는 것이 아주 중요하다. 네트워크가 손실을 최소화하기 위해 편법을 사용할 수 있기 때문이다.
    • 목적 함수가 주어진 문제의 성공과 전혀 관련이 없다면 원하지 않은 일을 수행하는 모델이 만들어질 것이다.
    • “모든 인류의 평균 행복 지수를 최대화하기” 같은 잘못된 목적 함수에서 SGD로 훈련된 멍청하지만 전지전능한 AI가 있다고 가정하자. 이 문제를 쉽게 해결하기 위해 이 AI는 몇 사람을 남기고 모든 인류를 죽여서 남은 사람들의 행복에 초점을 맞출 수 있다.
    • 우리가 만든 모든 신경망은 단지 손실 함수를 최소화하기만 한다는 것을 기억하라. 목적 함수를 현명하게 선택하지 않으면 원하지 않는 부수 효과가 발생할 것이다.
  • 다행히 분류, 회귀와 시퀀스 예측 같은 일반적인 문제에서는 올바른 손실 함수를 선택하는 간단한 지침이 있다.
    • 예컨대 2개의 클래스가 있는 분류 문제에는 이진 크로스엔트로피(binary crossentropy), 여러 개의 클래스가 있는 분류 문제에는 범주형 크로스엔트로피(categorical croessentropy), 회귀 문제에는 평균 제곱 오차, 시퀀스 학습 문제에는 CTC(Connection Temporal Classification) 등을 사용한다.
    • 완전히 새로운 연구를 할 때만 독자적인 목적 함수를 만들게 된다.

케라스 소개

  • 이 책에서는 코드 예제를 위해 케라스(https://keras.io)를 사용한다.
    • 케라스는 거의 모든 종류의 딥러닝 모델을 간편하게 만들고 훈련시킬 수 있는 파이썬을 위한 딥러닝 프레임워크이다.
    • 케라스는 MIT 라이선스를 따르고 있으므로 상업적인 프로젝트에도 자유롭게 사용할 수 있다.

케라스, 텐서플로, 씨아노, CNTK

  • 케라스는 딥러닝 모델을 만들기 위한 고수준의 구성 요소를 제공하는 모델 수준의 라이브러리이다. 텐서 조작이나 미분 같은 저수준의 연산을 다루지 않는다. 
    • 대신 케라스의 백엔드 엔진에서 제공하는 최적화되고 특화된 텐서 라이브러리를 사용한다.
    • 케라스는 하나의 텐서 라이브러리에 국한하여 구현되어 있지 않고 모듈 구조로 구성되어 있다.
    • 여러 가지 백엔드 엔진이 케라스와 매끄럽게 연동된다. 현재는 텐서플로, 씨아노, 마이크로소프트 코그니티브 툴킷(Microsoft Cognitive Toolkit, CNTK) 3개를 백엔드 엔진으로 사용할 수 있다.

  • 텐서플로, CNTK, 씨아노는 딥러닝을 위한 주요 플랫폼 중 하나이다.
    • 씨아노는 몬트리올 대학 MILA 연구소에서 개발했고, 텐서플로는 구글에서 개발했으며, CNTK는 마이크로소프트에서 개발했다.
    • 케라스로 작성한 모든 코드는 아무런 변경 없이 이런 백엔드 중 하나를 선택해서 실행시킬 수 있다.
    • 개발하는 중간에 하나의 백엔드가 특정 작업에 더 빠르다고 판단되면 언제든 백엔드를 바꿀 수 있다.
    • 가장 널리 사용되고 확장성이 뛰어나며 상용 제품에 쓸 수 있기 때문에 딥러닝 작업에 텐서플로 백엔드가 기본으로 권장된다.
  • 텐서플로(또는 씨아노, CNTK)를 사용하기 때문에 케라스는 CPU와 GPU에서 모두 작동될 수 있다.
    • CPU에서 실행될 때 텐서플로는 Eigen이라고 불리는 저수준 텐서 연산 라이브러리를 사용하고, GPU에서는 NVIDIA CUDA 심층 신경망 라이브러리(cuDNN)라고 불리는 고도로 최적화된 딥러닝 연산 라이브러리를 이용한다.

케라스를 사용한 개발: 빠르게 둘러보기

  • 이미 케라스 모델의 예로 MNIST 예제를 살펴보았다. 전형적인 케라스 작업 흐름은 이 예제와 비슷하다.
    1. 입력 텐서와 타깃 텐서로 이루어진 훈련 데이터를 정의한다.
    2. 입력과 타깃을 매핑하는 층으로 이루어진 네트워크(또는 모델)를 정의한다.
    3. 손실 함수, 옵티마이저, 모니터링하기 위한 측정 지표를 선택하여 학습 과정을 설정한다.
    4. 훈련 데이터에 대해 모델의 fit() 메서드를 반복적으로 호출한다.
  • 모델을 정의하는 방법은 두 가지인데, Sequential 클래스(가장 자주 사용하는 구조인 층을 순서대로 쌓아 올린 네트워크) 또는 함수형 API(완전히 임의의 구조를 만들 수 있는 비순환 유향 그래프)를 사용한다.
  • Sequential 클래스를 사용하여 정의한 2개의 층으로 된 모델을 다시 살펴보자.
from keras import models
from keras import layers

model = models.Sequential()
model.add(layers.Dense(32, activation='relu', input_shape=(784,)))
model.add(layers.Dense(10, activation='softmax'))
  • 같은 모델을 함수형 API를 사용해서 만들면 다음과 같다.
input_tensor = layers.Input(shape=(784,))
x = layers.Dense(32, activation='relu')(input_tensor)
output_tensor = layers.Dense(10, activation='softmax')(x)

model = models.Model(inputs=input_tensor, outputs=output_tensor)
  • 함수형 API를 사용하면 모델이 처리할 데이터 텐서를 만들고 마치 함수처럼 이 텐서에 층을 적용한다.
  • 모델 구조가 정의된 후에는 Sequential 모델을 사용했는지 함수형 API를 사용했는지 상관없이 이후 단계가 동일하다.
  • 컴파일 단계에서 학습 과정이 설정된다. 여기에서 모델이 사용할 옵티마이저와 손실 함수, 훈련 하는 동안 모니터링하기 위해 필요한 측정 지표를 지정한다.
  • 다음이 하나의 손실 함수를 사용하는 가장 흔한 경우의 예이다.
from keras import optimizers

model.compile(optimizer=optimizers.RMSprop(lr=0.001), loss='mse', metrics=['accuracy'])
  • 마지막으로 입력 데이터의 넘파이 배열을 모델의 fit() 메서드에 전달함으로써 학습 과정이 이루어진다.
    • 이는 사이킷런(Scikit-Learn)이나 몇몇 다른 머신 러닝 라이브러리에서 하는 방식과 비슷하다.
model.fit(input_tensor, target_tensor, batch_size=128, epochs=10)

딥러닝 컴퓨터 셋팅

  • 딥러닝 애플리케이션을 실행할 때는 NVIDIA GPU에서 실행하는 것을 권장한다.
  • 운영체제는 유닉스(Unix) 운영체제를 사용하는 것이 좋다.
    • 기술적으로 윈도우에서 케라스를 사용할 수 있지만 권장하지는 않는다.
    • 번거로울 수 있지만 우분투를 사용하면 장기적으로 시간이 절약되고 문제가 발생할 가능성이 적다.

주피터 노트북: 딥러닝 실험을 위한 최적의 방법

  • 주피터 노트북(Jupyter Notebook)은 데이터 과학과 머신 러닝 커뮤니티에서 폭넓게 사용된다.
    • 노트북은 주피터 노트북 애플리케이션(https://jupyter.org)으로 만든 파일이며 웹 브라우저에서 작성할 수 있다.
    • 작업 내용을 기술하기 위해 서식 있는 텍스트 포맷을 지원하며 파이썬 코드를 실행할 수 있는 기능도 있다.
    • 필수적이지는 않지만, 케라스를 배울 때 주피터 노트북을 사용할 것을 권장한다. 하지만 파이참(PyCharm) 같은 IDE에서 코드를 실행하거나 독립된 파이썬 스크립트를 실행할 수도 있다.

케라스 시작하기: 두 가지 방법

  • 케라스를 실행하려면 다음 두 방법 중 하나를 권장한다.
    • 공식 EC2 딥러닝 AMI(https://aws.amazon.com/amazon-ai/amis)를 사용해서 EC2에서 주피터 노트북으로 케라스 예제를 실행한다. 로컬 컴퓨터에 GPU가 없을 때 이 방법을 사용한다.
  • 로컬 유닉스 컴퓨터에 처음부터 모든 것을 설치한다. 그 다음 로컬 컴퓨터에서 주피터 노트북을 실행하든지 일반 파이썬 스크립트를 실행한다.
    • 이미 고사양 GPU 카드가 있을 때 이 방법을 사용한다.

클라우드에서 딥러닝 작업을 수행했을 때 장단점

  • (생략)

어떤 GPU 카드가 딥러닝에 최적일까?

  • (생략)

영화 리뷰 분류: 이진 분류 예제

IMDB 데이터셋

  • 인터넷 영화 데이터베이스(Internet Movie Database)로부터 가져온 양극단의 리뷰 5만 개로 이루어진 IMDB 데이터셋을 사용하겠다.
    • 이 데이터셋은 훈련 데이터 25,000개와 테스트 데이터 25,000개로 나뉘어 있고 각각 50/50으로 긍정/부정 리뷰가 구성되어 있다.
  • MNIST 데이터셋처럼 IMDB 데이터셋도 케라스에 포함되어 있다. 이 데이터는 전처리되어 있어 각 리뷰(단어 시퀀스)가 숫자 시퀀스로 변환되어 있다.
    • 여기서 각 숫자는 사전에 있는 고유한 단어를 나타낸다.
  • 다음 코드는 데이터셋을 로드한다.
from keras.datasets import imdb

(train_data, train_labels), (test_data, test_labels) = imdb.load_data(num_words=10000)
  • (이하 데이터 설명 생략)

데이터 준비

  • 신경망에 숫자 리스트를 주입할 수는 없다. 리스트를 텐서로 바꾸는 두 가지 방법이 있다.
    • 같은 길이가 되도록 리스트에 패딩(padding)을 추가하고 (samples, sequence_length) 크기의 정수 텐서로 변환한다. 그 다음 이 정수 텐서를 다룰 수 있는 층을 신경망의 첫 번째 층으로 사용한다(Embedding 층)
    • 리스트를 원-핫 인코딩(one-hot encoding)하여 0과 1의 벡터로 변환한다. 예컨대 시퀀스 [3, 5]를 인덱스 3과 5의 위치는 1이고 그 외는 모두 0인 10,000차원의 벡터로 각각 변환한다. 그 다음 부동 소수 벡터 데이터를 다룰 수 있는 Dense 층을 신경망의 첫 번째 층으로 사용한다.
  • 여기서는 두 번째 방식을 사용하고 이해를 돕기 위해 직접 데이터를 원-핫 벡터로 만들겠다.
import numpy as np

def vectorize_sequences(sequences, dimension=10000):
results = np.zeros((len(sequences), dimension)) #크기가 (len(sequences), dimension)이고 모든 원소가 0인 행렬을 만든다.
for i, sequence in enumerate(sequences):
results[i, sequence] = 1. #results[i]에서 특정 인덱스의 위치를 1로 만든다.
return results

x_train = vectorize_sequences(train_data) #훈련데이터를 벡터로 변환한다
x_test = vectorize_sequences(test_data) #테스트 데이터를 벡터로 변환한다.

x_train[0]
#array([0., 1., 1., ..., 0., 0., 0.])

y_train = np.asarray(train_labels).astype('float32')
y_test = np.asarray(test_labels).astype('float32')

신경망 모델 만들기

  • 입력 데이터가 벡터이고 레이블은 스칼라(1 또는 0)이다. 이런 문제에 잘 작동하는 네트워크 종류는 relu 활성화 함수를 사용한 완전 연결 층(즉 Dense(16, activation=’relu’))을 그냥 쌓은 것이다.
  • Dense 층에 전달한 매개변수(16)은 은닉 유닛(hidden unit)의 개수이다.
    • 하나의 은닉 유닛은 층이 나타내는 표현 공간에서 하나의 차원이 된다.
    • 2장에서 relu 활성화 함수를 사용한 Dense 층을 다음 텐서 연산을 연결하여 구현했었다.
output = relu(dot(W, input) + b)
  • 16개의 은닉 유닛이 있다는 것은 가중치 행렬 W의 크기가 (input_dimension, 16)이라는 뜻이다.
    • 입력 데이터와 W를 내적하면 입력 데이터가 16차원으로 표현된 공간으로 투영된다(그리고 편향 벡터 b를 더하고 relu 연산을 적용한다)
    • 표현 공간의 차원을 ‘신경망이 내재된 표현을 학습할 때 가질 수 있는 자유도’로 이해할 수 있다.
    • 은닉 유닛을 늘리면(표현 공간을 더 고차원으로 만들면) 신경망이 더욱 복잡한 표현을 학습할 수 있지만 계산 비용이 커지고 원하지 않는 패턴을 학습할 수도 있다. (훈련 데이터에서는 성능이 향상되지만 테스트 데이터에서는 그렇지 않은 패턴)
  • Dense 층을 쌓을 때 두 가지 중요한 구조상의 결정이 필요하다.
    • 얼마나 많은 층을 사용할 것인가?
    • 각 층에 얼마나 많은 은닉 유닛을 둘 것인가?
  • 중간에 있는 은닉 층은 활성화 함수로 relu를 사용하고 마지막 층은 확률을 출력하기 위해 시그모이드 활성화 함수를 사용한다.
    • relu는 음수를 0으로 만드는 함수이다.
    • 시그모이드는 임의의 값을 [0, 1] 사이로 압축하므로 출력 값을 확률처럼 해석할 수 있다.

  • 아래 그림은 이 신경망을 보여준다.

from keras import models
from keras import layers

model = models.Sequential()
model.add(layers.Dense(16, activation='relu', input_shape=(10000,)))
model.add(layers.Dense(16, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))
  • Note)
    • relu와 같은 활성화 함수(또는 비선형성(non-linearity)라고도 한다)가 없다면 Dense 층은 선형적인 연산인 내적과 덧셈 2개로 구성된다.
      • output = dot(W, input) + b
    • 그러므로 이 층은 입력에 대한 선형 변환(아핀 변환)만을 학습할 수 있다. 이 층의 가설 공간은 입력 데이터를 16차원의 공간으로 바꾸는 가능한 모든 선형 변환의 집합이다.
    • 이런 가설 공간은 매우 제약이 많으며, 선형 층을 깊게 쌓아도 여전히 하나의 선형 연산이기 때문에 층을 여러 개로 구성하는 장점이 없다. 즉 층을 추가해도 가설 공간이 확장되지 않는다.
    • 가설 공간을 풍부하게 만들어 층을 깊게 만드는 장점을 살리기 위해서는 비선형성 또는 활성화 함수를 추가해야 한다. relu는 딥러닝에서 가장 인기 있는 활성화 함수이다.
    • prelu, elu등 비슷한 다른 함수들도 많다.
  • 마지막으로 손실 함수와 옵티마이저를 선택해야 한다.
    • 이진 분류 문제고 신경망의 출력이 확률이기 때문에 binary_crossentropy 손실이 적합하다. 이 함수가 유일한 선택은 아니고 mean_squared_error도 사용할 수 있다. 확률을 출력하는 모델을 사용할 때는 크로스엔트로피가 최선의 선택이다.
    • 크로스엔트로피(Crossentropy)는 정보 이론(Information Theory) 분야에서 온 개념으로 확률 분포 간의 차이를 측정한다. 여기서는 원본 분포와 예측 분포 사이를 측정한다.
  • 다음은 rmsprop 옵티마이저와 binary_crossentropy 손실 함수로 모델을 설정하는 단계이다. 훈련하는 동안 정확도를 사용하여 모니터링 하겠다.
model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['accuracy'])
  • 케라스에 rmsprop, binary_crossentropy, accuracy가 포함되어 있기 때문에 옵티마이저, 손실 함수, 측정 지표를 문자열로 지정하는 것이 가능하다.
  • 이따금 옵티마이저의 매개변수를 바꾸거나 자신만의 손실 함수, 측정 함수를 전달해야 할 경우가 있다.
  • 전자의 경우에는 아래와 같이 간이 옵티마이저 파이썬 클래스를 사용해 객체를 직접 만들어 optimizer 매개변수에 전달하면 된다.
from keras import optimizers

model.compile(optimizer=optimizers.RMSprop(lr=0.001), loss='binary_crossentropy', metrics=['accuracy'])
  • 후자의 경우는 아래와 같이 loss와 metrics 매개변수에 함수 객체를 전달하면 된다.
from keras import losses
from keras import metrics

model.compile(optimizer=optimizers.RMSprop(lr=0.001), loss=losses.binary_crossentropy, metrics=[metrics.binary_accuracy])

훈련 검증

  • 훈련하는 동안 처음 본 데이터에 대한 모델의 정확도를 측정하기 위해 원본 훈련 데이터에서 10,000의 샘플을 떼어 검증 세트를 만들어야 한다.
x_val = x_train[:10000]
y_val = y_train[:10000]
partial_x_train = x_train[10000:]
partial_y_train = y_train[10000:]
  • 이제 모델을 512개의 샘플씩 미니 배치를 만들어 20번의 에포크 동안 훈련시킨다. 동시에 따로 떼어 놓은 1만 개의 샘플에서 손실과 정확도를 측정할 것이다.
    • 이렇게 하려면 validation_data 매개변수에 검증 데이터를 전달해야 한다.
model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['acc'])
history = model.fit(partial_x_train, partial_y_train, epochs=20, batch_size=512, validation_data=(x_val, y_val))
  • model.fit() 메서드는 History 객체를 반환한다. 이 객체는 훈련하는 동안 발생한 모든 정보를 담고 있는 딕셔너리인 history 속성을 갖고 있다.
history_dict = history.history
history_dict.keys()
#dict_keys(['val_loss', 'val_acc', 'loss', 'acc'])
  • 이 딕셔너리는 훈련과 검증하는 동안 모니터링할 측정 지표당 하나씩 모두 4개의 항목을 담고 있다.
    • 이어지는 두 목록에서 맷플롯립을 사용하여 훈련과 검증 데이터에 대한 손실과 정확도를 그려보자.
import matplotlib.pyplot as plt

history_dict = history.history
loss = history_dict['loss']
val_loss = history_dict['val_loss']

epochs = range(1, len(loss) + 1)

plt.plot(epochs, loss, 'bo', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend()
plt.show()

plt.clf()
acc = history_dict['acc']
val_acc = history_dict['val_acc']

plt.plot(epochs, acc, 'bo', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc')
plt.title('Trainging and validation accuracy')
plt.xlabel('Epochs')
plt.ylabel('Accuracy')
plt.legend()
plt.show()

  • 여기에서 볼 수 있듯이 훈련 손실이 에포크마다 감소하고 훈련 정확도는 에포크마다 증가한다.
    • 경사 하강법 최적화를 사용했을 때 반복마다 최소화되는 것이 손실이므로 기대했던대로이다.
    • 검증 손실과 정확도는 이와 같지 않은데, 네 번째 에포크에서 그래프가 역전되는 것을 볼 수 있다.
    • 이것이 훈련 세트에서 잘 작동하는 모델이 처음 보는 데이터에서 잘 작동하지 않을 수 있다고 한 과대적합(overfitting)이다.

훈련된 모델로 새로운 데이터에 대해 예측하기

  • 모델을 훈련 시킨 후에 이를 실전 환경에서 사용하고 싶을 것이다. predict 메서드를 사용해서 어떤 리뷰가 긍정일 확률을 예측할 수 있다.
model.predict(x_test)

추가 실험

  • (생략)

정리

  • 원본 데이터를 신경망에 텐서로 주입하기 위해서는 꽤 많은 전처리가 필요하다. 단어 시퀀스는 이진 벡터로 인코딩될 수 있고 다른 인코딩 방식도 있다.
  • relu 활성화 함수와 함께 Dense 층을 쌓은 네트워크는 여러 종류의 문제에 적용할 수 있어 앞으로 자주 사용하게 될 것이다.
  • 출력 클래스가 2개인 이진 분류 문제에서 네트워크는 하나의 유닛과 sigmoid 활성화 함수를 가진 Dense 층으로 끝나야 한다. 이 신경망의 출력은 확률을 나타내는 0과 1사이의 스칼라 값이다.
  • 이진 분류 문제에서 이런 스칼라 시그모이드 출력에 대해 사용할 손실 함수는 binary_crossentropy이다.
  • rmsprop 옵티마이저는 문제에 상관없이 일반적으로 충분히 좋은 선택이다.
  • 훈련 데이터에 대해 성능이 향상됨에 따라 신경망은 과대적합되기 시작하고 이전에 본적 없는 데이터에서는 결과가 점점 나빠진다. 항상 훈련 세트 이외의 데이터에서 성능을 모니터링 해야 한다.

뉴스 기사 분류: 다중 분류 문제

  • 여기서는 로이터 뉴스를 46개의 상호 배타적인 토픽으로 분류하는 신경망을 만들어 보겠다. 
    • 클래스가 많기 때문에 이 문제는 다중 분류(multiclass classification)의 예이다.
    • 각 데이터 포인트가 정확히 하나의 범주로 분류되기 때문에 좀 더 정확히 말하면 단일 레이블 다중 분류 문제이다.
    • 각 데이터 포인트가 여러 개의 범주에 속할 수 있다면 이것은 다중 레이블 다중 분류 문제가 된다.

로이터 데이터셋

  • IMDB, MINIST와 마찬가지로 로이터 데이터셋도 케라스에 포함되어 있다.
    • IMDB와 마찬가지로 각 샘플은 정수 리스트이다.
from keras.datasets import reuters

(train_data, train_labels), (test_data, test_labels) = reuters.load_data(num_words=10000)

데이터 준비

  • 이전 예제와 동일한 코드를 사용해서 단어를 벡터로 변환한다.
    • (코드 생략)
  • 레이블을 벡터로 바꾸는 방법은 두 가지이다. 레이블의 리스트를 정수 텐서로 변환하는 것과 원-핫 인코딩을 사용하는 것이다.
    • 원-핫 인코딩이 범주형 데이터에 널리 사용되기 때문에 범주형 인코딩(categorical encoding)이라고도 부른다.
def to_one_hot(labels, dimension=46):
results = np.zeros((len(labels), dimension))
for i, label in enumerate(labels):
results[i, label] = 1.
return results

one_hot_train_labels = to_one_hot(train_labels)
one_hot_test_labels = to_one_hot(test_labels)

  • MINIST 예제에서 보았듯이 케라스에는 이를 위한 내장 함수가 이미 있다.
from keras.utils.np_utils import to_categorical

one_hot_train_labels = to_categorical(train_labels)
one_hot_test_labels = to_categorical(test_labels)

모델 구성

  • 이 토픽 분류 문제는 영화 리뷰 분류 문제와 비슷해 보인다. 두 경우 모두 짧은 텍스트를 분류하는 것이기 때문이다.
    • 여기서는 새로운 제약 사항이 추가되었는데, 출력 클래스의 개수가 2에서 46개로 늘어났다는 점이다. 출력 공간의 차원이 훨씬 커졌다.
  • 이전에 사용했던 것처럼 Dense 층을 쌓으면 각 층은 이전 층의 출력에서 제공한 정보만 사용할 수 있다.
    • 한 층이 분류 문제에 필요한 일부 정보를 누락하면 그 다음 층에서 이를 복원할 방법이 없다.
    • 각 층은 잠재적으로 정보의 병목(information bottleneck)이 될 수 있다.
    • 이전 예제에서 16차원을 가진 중간층을 사용했지만 16차원 공간은 46개의 클래스를 구분하기에 너무 제약이 많을 것 같다. 이렇게 규모가 작은 층은 유용한 정보를 완전히 잃게 되는 정보의 병목 지점처럼 동작할 수 있다.
  • 이런 이유로 좀 더 규모가 큰 층을 사용해 보겠다. 64개의 유닛을 사용해 보자.
from keras import models
from keras import layers

model = models.Sequential()
model.add(layers.Dense(64, activation='relu', input_shape=(10000,)))
model.add(layers.Dense(64, activation='relu'))
model.add(layers.Dense(46, activation='softmax'))
  • 이 구조에서 주목해야 할 점이 두 가지 있다.
    • 마지막 Dense 층의 크기가 46이다. 각 입력 샘플에 대해 46차원의 벡터를 출력한다는 뜻이다. 이 벡터의 각 원소(각 차원)는 각기 다른 출력 클래스가 인코딩 된 것이다.
    • 마지막 층에 softmax 활성화 함수가 사용되었다.
  • 이런 문제에 사용할 최선의 손실 함수는 categorical_crossentropy이다.
    • 이 함수는 두 확률 분포 사이의 거리를 추정한다. 여기서는 네트워크가 출력한 확률 분포와 진짜 레이블의 분포 사이의 거리이다.
    • 두 분포 사이의 거리를 최소화하면 진짜 레이블에 가능한 가까운 출력을 내도록 모델을 훈련하게 된다.
model.compile(optimizer='rmsprop', loss='categorical_crossentropy', metrics=['accuracy'])

훈련 검증

  • 훈련 데이터에서 1,000개의 샘플을 떼어 검증 세트로 사용하자.
x_val = x_train[:1000]
partial_x_train = x_train[1000:]

y_val = one_hot_train_labels[:1000]
partial_y_train = one_hot_train_labels[1000:]
  • 20번의 에포크로 모델을 훈련시킨다.
import numpy as np

history = model.fit(partial_x_train, partial_y_train, epochs=20, batch_size=512, validation_data=(x_val, y_val))
  • 손실과 정확도 곡선을 그려보자.
    • (코드 생략)

새로운 데이터에 대해 예측하기

  • 모델 객체의 predict 메서드는 46개의 토픽에 대한 확률 분포를 반환한다.
model.predict(x_test)

레이블과 손실을 다루는 다른 방법

  • 레이블을 인코딩 하는 다른 방법은 다음과 같이 정수 텐서로 변환하는 것이다.
y_train = np.array(train_labels)
y_test = np.array(test_labels)
  • 이 방식을 사용하려면 손실 함수 하나만 바꾸면 된다. categorical_crossentropy는 레이블이 범주형 인코딩되어 있을 것이라고 기대한다.
    • 정수 레이블을 사용할 때는 sparse_categorical_crossentropy를 사용해야 한다.
model.compile(optimizer='rmsprop', loss='sparse_categorical_crossentropy', metrics=['acc'])
  • 이 손실 함수는 인터페이스만 다를 뿐이고 수학적으로는 categorical_crossentropy와 동일하다.

충분히 큰 중간층을 두어야 하는 이유

  • 마지막 출력이 46차원이기 때문에 중간의 히든 유닛이 46개보다 많이 적어져서는 안된다.
  • 46차원보다 훨씬 작은 중간층을 두면 정보의 병목이 어떻게 나타나는지 확인해 보겠다.
    • (생략 – 정확도가 매우 떨어짐)

추가 실험

  • (생략)

정리

  • N개의 클래스로 데이터 포인트를 분류하려면 네트워크의 마지막 Dense 층의 크기는 N이어야 한다.
  • 단일 레이블, 다중 분류 문제에서는 N개의 클래스에 대한 확률 분포를 출력하기 위해 softmax 활성화 함수를 사용해야 한다.
  • 이런 문제에는 항상 범주형 크로스엔트로피를 사용해야 한다. 이 함수는 모델이 출력한 확률 분포와 타깃 분포 사이의 거리를 최소화 한다.
  • 다중 분류에서 레이블을 다루는 두 가지 방법이 있다.
    • 레이블을 범주형 인코딩(또는 원-핫 인코딩)으로 인코딩하고 categorical_crossentropy 손실 함수를 사용한다.
    • 레이블을 정수로 인코딩하고 sparse_categorical_crossentropy 손실 함수를 사용한다.
  • 만은 수의 범주를 분류할 때 중간층의 크기가 너무 작아 네트워크에 정보의 병목이 생기지 않도록 주의해야 한다.

주택 가격 예측: 회귀 문제

  • 앞선 두 예제는 분류 문제로 입력 데이터 포인트의 개별적인 레이블 하나를 예측하는 것이 목적이었다.
  • 또 다른 종류의 머신 러닝 문제는 개별적인 레이블 대신 연속적인 값을 예측하는 회귀(regression)가 있다.

보스턴 주택 가격 데이터셋

  • 1980년 중반 보스턴 외곽 지역에 대한 데이터셋은 이전 2개의 예제와 다른데, 데이터 포인트가 506개로 비교적 적다. 404개는 훈련 샘플, 102개는 테스트 샘플로 나뉘어 있다.
    • 입력 데이터에 있는 특성(feature)은 스케일이 서로 다르다. 어떤 값은 0-1 사이의 비율이고, 어떤 것은 1-12, 1-100 사이의 값을 갖는 것도 있다.
from keras.datasets import boston_housing

(train_data, train_targets), (test_data, test_targets) = boston_housing.load_data()

데이터 준비

  • 상이한 스케일을 가진 값을 신경망에 주입하면 문제가 된다. 
    • 네트워크가 이런 다양한 데이터에 자동으로 맞추려고 할 수 있지만, 이는 확실히 학습을 더 어렵게 만든다. 이런 데이터를 다룰 때 대표적인 방법은 특성별로 정규화를 하는 것이다.
    • 입력 데이터에 있는 각 특성에 대해 특성의 평균을 내고 표준편차로 나눈다. 특성의 중앙이 0 근처에 맞추어지고 표준 편차가 1이 된다. 
    • 넘파이를 사용하면 간단하게 할 수 있다.
mean = train_data.mean(axis=0)
std = train_data.std(axis=0)

train_data -= mean
train_data /= std

test_data -= mean
test_data /= std

모델 구성

  • 샘플 개수가 적기 때문에 64개의 유닛을 가진 2개의 은닉 층으로 작은 네트워크를 구성하여 사용하겠다.
    • 일반적으로 훈련 데이터의 개수가 적을수록 과대적합이 더 쉽게 일어나므로 작은 모델을 사용하는 것이 과대적합을 피하는 방법이다.
from keras import models
from keras import layers

def build_model():
model = models.Sequential()
model.add(layers.Dense(64, activation='relu', input_shape=(train_data.shape[1],)))
model.add(layers.Dense(64, activation='relu'))
model.add(layers.Dense(1))
model.compile(optimizer='rmsprop', loss='mse', metrics=['mae'])
return model
  • 이 네트워크의 마지막 층은 하나의 유닛을 가지고 있고 활성화 함수가 없다 (선형 층이라고 부른다). 이것이 전형적인 스칼라 회귀(하나의 연속적인 값을 예측하는 회귀)를 위한 구성이다.
    • 활성화 함수를 적용하면 출력 값의 범위를 제한하게 된다. 예컨대 마지막 층에 sigmoid 활성화 함수를 적용하면 네트워크가 0-1 사이의 값을 예측하도록 학습될 것이다.
    • 여기서는 마지막 층이 순수한 선형이므로 네트워크가 어떤 범위의 값이라도 예측하도록 자유롭게 학습된다.
  • 이 모델은 mse 손실 함수를 사용하여 컴파일한다.
    • 이 함수는 평균 제곱 오차(mean squared error)의 약어로 예측과 타깃 사이 거리의 제곱이다. 회귀 문제에서 널리 사용되는 손실함수이다.
  • 훈련하는 동안 모니터링을 위해 새로운 지표인 평균 절대 오차(Mean Absolute Error, MAE)를 측정한다.
    • 이는 예측과 타깃 사이 거리의 절댓값이다. 예컨대 이 예제에서 MAE가 0.5면 예측이 평균적으로 500달러 정도 차이가 난다는 뜻이다.

K-겹 검증을 사용한 훈련 검증

  • 매개변수들을 조정하면서 모델을 평가하기 위해 이전 예제에서 했던 것처럼 데이터를 훈련 세트와 검증 세트로 나눈다.
    • 데이터 포인트가 많지 않기 때문에 검증 세트도 매우 작아진다(약 100개)
    • 결국 검증 세트와 훈련 세트로 어떤 데이터 포인트가 선택되었는지에 따라 검증 점수가 크게 달라진다. 검증 세트의 분할에 대한 검증 점수의 분산이 높다. 이렇게 되면 신뢰 있는 모델 평가를 할 수 없다.
  • 이런 상황에서 가장 좋은 방법은 K-겹 교차 검증(K-fold cross-validation)을 사용하는 것이다.
    • 데이터를 K개의 분할(즉 폴드(fold))로 나누고 (일반적으로 K = 4 또는 5), K개의 모델을 각각 만들어 K-1개의 분할에서 훈련하고 나머지 분할에서 평가하는 방법이다.
    • 모델의 검증 점수는 K개의 검증 점수 평균이 된다.

import numpy as np

k = 4

num_val_samples = len(train_data) // k
num_epochs = 100
all_scores = []

for i in range(k):
print('처리 중인 폴드 #', i)
val_data = train_data[i * num_val_samples: (i + 1) * num_val_samples]
val_targets = train_targets[i * num_val_samples: (i + 1) * num_val_samples]

partial_train_data = np.concatenate([train_data[:i*num_val_samples], train_data[(i+1)*num_val_samples:]], axis=0)
partial_train_targets = np.concatenate([train_targets[:i*num_val_samples], train_targets[(i+1)*num_val_samples:]], axis=0)

model = build_model()
model.fit(partial_train_data, partial_train_targets, epochs=num_epochs, batch_size=1, verbose=0)

val_mse, val_mae = model.evaluate(val_data, val_targets, verbose=0)
all_scores.append(val_mae)
  • num_epochs = 100으로 실행하면 다음 결과를 얻을 수 있다.
all_scores
# [2.0956787838794217, 2.220593797098292, 2.859968412040484, 2.40535704039111]

np.mean(all_scores)
# 2.3953995083523267
  • 검증 세트가 다르므로 검증 점수의 변화가 크다. 평균값이 각각의 점수보다 훨씬 신뢰할만 하다. 이것이 K-겹 교차 검증의 핵심이다.
num_epochs = 500
all_nae_histories = []

for i in range(k):
print('처리 중인 폴드 #', i)
val_data = train_data[i * num_val_samples: (i + 1) * num_val_samples]
val_targets = train_targets[i * num_val_samples: (i + 1) * num_val_samples]

partial_train_data = np.concatenate([train_data[:i*num_val_samples], train_data[(i+1)*num_val_samples:]], axis=0)
partial_train_targets = np.concatenate([train_targets[:i*num_val_samples], train_targets[(i+1)*num_val_samples:]], axis=0)

model = build_model()
history = model.fit(partial_train_data, partial_train_targets, epochs=num_epochs, batch_size=1, verbose=0)

mae_history = history.history['val_mean_absolute_error']
all_mae_histories.append(mae_history)
  • 그 다음 모든 폴드에 대해 에포크의 MAE 점수 평균을 계산한다.
average_mae_history = [np.mean([x[i] for x in all_mae_histories]) for i in range(num_epochs)]
  • 그래프로 나타내면 다음과 같다.

  • 이 그래프는 범위가 크고 변동이 심하기 때문에 보기가 어렵다. 다음과 같이 해보자.
    • 곡선의 다른 부분과 스케일이 많이 달느 첫 10개의 데이터 포인트를 제외한다.
    • 부드러운 곡선을 얻기 위해 각 포인트를 이전 포인트의 지수 이동 평균(exponential moving average)으로 대체한다.
def smooth_curve(points, factor=0.9):
smoothed_points = []
for point in points:
if smoothed_points:
previous = smoothed_points[-1]
smoothed_points.append(previous * factor + point * (1-factor))
else:
smoothed_points.append(point)
return smoothed_points

smooth_mae_history = smooth_curve(average_mae_history[10:])

  • 이 그래프를 보면 검증 MAE가 80번째 에포크 이후에 줄어드는 것이 멈추었다. 이 지점 이후로는 과대적합이 시작된다.
  • 모델의 여러 변수에 대한 튜닝이 끝나면 모든 훈련 데이터를 사용하고 최상의 매개변수로 최종 실전에 투입될 모델을 훈련시킨다. 그 다음 테스트 데이터로 성능을 확인한다.
model = build_model()
model.fit(train_data, train_targets, epochs=80, batch_size=16, verbose=0)
test_mse_score, test_mae_score = model.evaluate(test_data, test_targets)

정리

  • 회귀는 분류에서 사용했던 것과는 다른 손실 함수를 사용한다. 평균 제곱 오차(MSE)는 회귀에서 자주 사용되는 손실 함수이다.
  • 비슷하게 회귀에서 사용되는 평가 지표는 분류와 다르다. 당연히 정확도 개념은 회귀에 적용되지 않는다. 일반적인 회귀 지표는 평균 절대 오차(MAE)이다.
  • 입력 데이터의 특성이 서로 다른 범위를 가지면 전처리 단계에서 각 특성을 개별적으로 스케일 조정해야 한다.
  • 가용한 데이터가 적다면 K-겹 검증을 사용하는 것이 신뢰할 수 있는 모델 평가 방법이다.
  • 가용한 훈련 데이터가 적다면 과대적합을 피하기 위해 은닉 층의 수를 줄인 모델이 좋다. (일반적으로 1개 또는 2개)

요약

  • 이제 벡터 데이터를 사용하여 가장 일반적인 머신 러닝인 이진 분류, 다중 분류, 스칼라 회귀 작업을 다룰 수 있다.
  • 보통 원본 데이터를 신경망에 주입하기 전에 전처리해야 한다.
  • 데이터에 범위가 다른 특성이 있다면 전처리 단계에서 각 특성을 독립적으로 스케일 조정해야 한다.
  • 훈련이 진행됨에 따라 신경망의 과대적합이 시작되고 새로운 데이터에 대해 나쁜 결과를 얻게 된다.
  • 훈련 데이터가 많지 않으면 과대적합을 피하기 위해 1개 또는 2개의 은닉 층을 가진 신경망을 사용한다.
  • 데이터가 많은 범주로 나뉘어 있을 때 중간층이 너무 작으면 정보의 병목이 생길 수 있다.
  • 회귀는 분류와 다른 손실 함수와 평가 지표를 사용한다.
  • 적은 데이터를 사용할 때는 K-겹 검증이 신뢰할 수 있는 모델 평가를 도와준다.
[ssba]

The author

Player가 아니라 Creator가 되려는 자/ suyeongpark@abyne.com

댓글 남기기

이 사이트는 스팸을 줄이는 아키스밋을 사용합니다. 댓글이 어떻게 처리되는지 알아보십시오.