Chapter 07. 딥러닝을 시작합니다.¶
- 학습목표
- 딥러닝의 핵심 알고리즘인 인공 신경망을 배운다.
- 대표적인 인공 신경망 라이브러리인 텐서플로와 케라스를 소개한다.
- 인공 신경망 모델의 훈련을 돕는 도구를 익힌다.
07-1 인공 신경망¶
- 핵심키워드
- 인공 신경망
- 텐서플로
- 밀집층
- 원-핫 인코딩
- 딥러닝과 인공 신경망 알고리즘을 이해하고 텐서플로를 사용해 간단한 인공 신경망 모델을 만들어 본다.
패션 MNIST¶
판매할 패션 상품의 데이터는 아직 없지만, 마침 한빝 마켓에서 판매할 상품과 똑같은 데이터를 구할 수 있다. 이를 대신해 사용해보겠다.7장과 8장에서는 패션 MNIST 데이터셋을 사용하겠다. 이 데이터셋은 10종류의 패션 아이템으로 구성되어 있다.
- MNIST가 뭐죠?
- 머신러닝과 딥러닝을 처음 배울 때 많이 사용하는 데이터셋이 있다. 머신러닝에서는 붓꽃 데이터셋이 유명하다. 딥러닝에서는 MNIST 데이터셋이 유명하다. 이 데이터는 손으로 쓴 0~9까지의 숫자로 이루어져 있다. MNIST와 크기, 개수가 동일하지만 숫자 대신 패션 아이템으로 이루어진 데이터가 바로 패션 MNIST 이다.
패션 MNIST 데이터는 워낙 유명하기 때문에 많은 딥러닝 라이브러리에서 이 데이터를 가져올 수 있는 도구를 제공한다. 여기서는 텐서플로 TensorFlow 를 사용해 이 데이터를 불러오겠다. 딥러닝이나 텐서플로에 대해 설명하기 전에 먼저 패션 MNIST가 어떤 데이터인지 확인해 보겠다.
편리하게도 텐서플로도 코랩에서 바로 사용할 수 있다. 다음 명령으로 텐서플로의 케라스Keras패키지를 임포트 하고 패션 MNIST 데이터를 다운로드 한다.
#!pip3 install tensorflow
from tensorflow import keras
(train_input, train_target), (test_input, test_target) = keras.datasets.fashion_mnist.load_data()
/home/ubuntu/.local/lib/python3.6/site-packages/tensorflow/python/framework/dtypes.py:516: FutureWarning: Passing (type, 1) or '1type' as a synonym of type is deprecated; in a future version of numpy, it will be understood as (type, (1,)) / '(1,)type'. _np_qint8 = np.dtype([("qint8", np.int8, 1)]) /home/ubuntu/.local/lib/python3.6/site-packages/tensorflow/python/framework/dtypes.py:517: FutureWarning: Passing (type, 1) or '1type' as a synonym of type is deprecated; in a future version of numpy, it will be understood as (type, (1,)) / '(1,)type'. _np_quint8 = np.dtype([("quint8", np.uint8, 1)]) /home/ubuntu/.local/lib/python3.6/site-packages/tensorflow/python/framework/dtypes.py:518: FutureWarning: Passing (type, 1) or '1type' as a synonym of type is deprecated; in a future version of numpy, it will be understood as (type, (1,)) / '(1,)type'. _np_qint16 = np.dtype([("qint16", np.int16, 1)]) /home/ubuntu/.local/lib/python3.6/site-packages/tensorflow/python/framework/dtypes.py:519: FutureWarning: Passing (type, 1) or '1type' as a synonym of type is deprecated; in a future version of numpy, it will be understood as (type, (1,)) / '(1,)type'. _np_quint16 = np.dtype([("quint16", np.uint16, 1)]) /home/ubuntu/.local/lib/python3.6/site-packages/tensorflow/python/framework/dtypes.py:520: FutureWarning: Passing (type, 1) or '1type' as a synonym of type is deprecated; in a future version of numpy, it will be understood as (type, (1,)) / '(1,)type'. _np_qint32 = np.dtype([("qint32", np.int32, 1)]) /home/ubuntu/.local/lib/python3.6/site-packages/tensorflow/python/framework/dtypes.py:525: FutureWarning: Passing (type, 1) or '1type' as a synonym of type is deprecated; in a future version of numpy, it will be understood as (type, (1,)) / '(1,)type'. np_resource = np.dtype([("resource", np.ubyte, 1)]) /home/ubuntu/.local/lib/python3.6/site-packages/tensorboard/compat/tensorflow_stub/dtypes.py:541: FutureWarning: Passing (type, 1) or '1type' as a synonym of type is deprecated; in a future version of numpy, it will be understood as (type, (1,)) / '(1,)type'. _np_qint8 = np.dtype([("qint8", np.int8, 1)]) /home/ubuntu/.local/lib/python3.6/site-packages/tensorboard/compat/tensorflow_stub/dtypes.py:542: FutureWarning: Passing (type, 1) or '1type' as a synonym of type is deprecated; in a future version of numpy, it will be understood as (type, (1,)) / '(1,)type'. _np_quint8 = np.dtype([("quint8", np.uint8, 1)]) /home/ubuntu/.local/lib/python3.6/site-packages/tensorboard/compat/tensorflow_stub/dtypes.py:543: FutureWarning: Passing (type, 1) or '1type' as a synonym of type is deprecated; in a future version of numpy, it will be understood as (type, (1,)) / '(1,)type'. _np_qint16 = np.dtype([("qint16", np.int16, 1)]) /home/ubuntu/.local/lib/python3.6/site-packages/tensorboard/compat/tensorflow_stub/dtypes.py:544: FutureWarning: Passing (type, 1) or '1type' as a synonym of type is deprecated; in a future version of numpy, it will be understood as (type, (1,)) / '(1,)type'. _np_quint16 = np.dtype([("quint16", np.uint16, 1)]) /home/ubuntu/.local/lib/python3.6/site-packages/tensorboard/compat/tensorflow_stub/dtypes.py:545: FutureWarning: Passing (type, 1) or '1type' as a synonym of type is deprecated; in a future version of numpy, it will be understood as (type, (1,)) / '(1,)type'. _np_qint32 = np.dtype([("qint32", np.int32, 1)]) /home/ubuntu/.local/lib/python3.6/site-packages/tensorboard/compat/tensorflow_stub/dtypes.py:550: FutureWarning: Passing (type, 1) or '1type' as a synonym of type is deprecated; in a future version of numpy, it will be understood as (type, (1,)) / '(1,)type'. np_resource = np.dtype([("resource", np.ubyte, 1)])
keras.datasets.fashion_mnist 모듈 아래 load_data() 함수는 친절하게 훈련 데이터와 테스트 데이터를 나누어 반환한다. 이 데이터는 각각 입력과 타깃의 쌍으로 구성되어 있다.
전달받은 데이터의 크기를 확인해 보겠다.
print(train_input.shape, train_target.shape)
(60000, 28, 28) (60000,)
훈련 데이터는 60,000개의 이미지로 이루어져 있다. 각 이미지는 28 x 28 크기이다. 타깃도 60,000개의 원소가 있는 1차원 배열이다.
테스트 세트의 크기도 확인해 보겠다.
print(test_input.shape, test_target.shape)
(10000, 28, 28) (10000,)
테스트 세트는 10,000개의 이미지로 이루어져 있다. 6장에서 맷플롯립 라이브러리로 과일을 출력했던 것처럼 훈련 데이터에서 몇 개의 샘플을 그림으로 출력해 볼까요? 어떤 이미지인지 직접 보는것이 문제를 이해하는데 큰 도움이 된다.
import matplotlib.pyplot as plt
fig, axs = plt.subplots(1, 10, figsize=(10,10))
for i in range(10):
axs[i].imshow(train_input[i], cmap='gray_r')
axs[i].axis('off')
plt.show()
크기가 28 x 28 이다 보니 꽤 작고 흐릿하다. 또 6장에서 다루었던 것처럼 반전된 흑백 이미지이다. 신발과 다양한 종류의 옷들이 보인다. 이 샘플들의 타깃값을 확인해 보자.
파이썬 리스트 내포를 사용해서 처음 10개 타깃값을 리스트로 만든 후 출력하겠다. 다음 명령을 실행해 보자.
print([train_target[i] for i in range(10)])
[9, 0, 0, 3, 0, 2, 7, 2, 5, 5]
패션 MNIST 의 타깃은 0 ~9 까지의 숫자 레이블로 구성된다. 각 숫자의 의미는 아직 모르지만 마지막 2개의 샘플이 같은 레이블(숫자5)을 가지고 있다. 앞서 출력한 이미지를 보더라도 이 2개의 샘플은 같은 종류의 신발 이다. 패션 MNIST에 포함된 10개 레이블의 의미는 다음과 같다.
이 값을 앞에서 출력한 결과와 비교해 보자. 마지막으로 넘파이 unique()함수로 레이블 당 샘플 개수를 확인해 보겠다.
import numpy as np
print(np.unique(train_target, return_counts = True))
(array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype=uint8), array([6000, 6000, 6000, 6000, 6000, 6000, 6000, 6000, 6000, 6000]))
0 ~ 9 까지 레이블마다 정확히 6,000개의 샘플이 들어 있는 것을 볼 수 있다.
좋다. 이 과정을 통해서 패션 MNIST 데이터셋을 저장했고, 어떤 종류의 이미지로 이루어져 있는지 감을 잡았다. 이어서 홍 성배의 로지스틱 회귀 모델을 살짝 훔쳐보겠다.
로지스틱 회귀로 패션 아이템 분류하기¶
이 훈련 샘플은 60,000개나 되기 때문에 전체 데이터를 한꺼번에 사용하여 모델을 훈련하는 것보다 샘플을 하나씩 꺼내서 모델을 훈련하는 방법이 더 효율적으로 보인다. 이런 상황에 잘 맞는 방법이 확률적 경사 하강법이다. 4장에서 배웠던 SGDClassifier을 기억하죠?
- 넘파이 배열의 바이트 크기를 알 수 있나요?
- 넘파이 배열의 nbytes 속성에 실제 해당 배열이 차지하는 바이트 용량이 저장되어 있다.
4장에서 SGDClassifier 클래스의 loss 매개변수를 'log'로 지정하여 로지스틱 손실 함수를 최소화 하는 확률적 경사 하강법 모델을 만들었다.
4장의 내용을 다시 되짚어 보자. 4장에서 SGDClassifier를 사용할 때 표준화 전처리된 데이터를 사용했다. 그 이유를 기억 하나?
- 확률적 경사 하강법은 여러 특성 중 기울기가 가장 가파른 방향을 따라 이동한다. 만약 특성마다 값의 범위가 많이 다르면 올바르게 손실 함수의 경사를 내려 올 수 없다. 패션 MNIST의 경우 각 픽셀은 0 ~ 255 사이의 정숫값을 가진다. 이런 이미지의 경우 보통 255로 나누어 0 ~ 1 사이의 값으로 정규화 한다. 이는 표준화는 아니지만 양수 값으로 이루어진 이미지를 전처리할 때 널리 사용하는 방법이다.
6장에서 했듯이 reshape() 메서드를 사용해 2차원 배열인 각 샘플을 1차원 배열로 펼치겠다. 4장에서 보았듯이 SGDClassifier는 2차원 입력을 다루지 못하기 때문에 각 샘플을 1차원 배열로 만들어야 한다.
train_scaled = train_input / 255.0
train_scaled = train_scaled.reshape(-1, 28*28)
reshape() 메서드의 두 번째 매개변수를 28 x 28 이미지 크기에 맞게 지정하면 첫 번째 차원(샘플 개수)은 변하지 않고 원본 데이터의 두 번째, 세 번째 차원이 1차원으로 합쳐진다. 변환된 train_scaled의 크기를 확인해 보자.
print(train_scaled.shape)
(60000, 784)
네. 기대한 대로 784개으 픽셀로 이루어진 60,000 개의 샘플이 준비되었다. 4장에서처럼 SGDClassifier 클래스와 cross_validate 함수를 사용해 이 데이터에서 교차 검증으로 성능을 확인해 보자.
from sklearn.model_selection import cross_validate
from sklearn.linear_model import SGDClassifier
sc = SGDClassifier(loss='log', max_iter=5, random_state=42)
scores=cross_validate(sc, train_scaled, train_target, n_jobs=-1)
print(np.mean(scores['test_score']))
0.8195666666666668
여기에서는 SGDClassifier 의 반복 횟수(max_iter)를 5번으로 지정했다. 반복 횟수를 늘려도 성능이 크게 향상되지는 않는다. 직접 9나 20등의 여러 숫자를 넣어서 테스트해 보자.
from sklearn.model_selection import cross_validate
from sklearn.linear_model import SGDClassifier
sc = SGDClassifier(loss='log', max_iter=9, random_state=42)
scores=cross_validate(sc, train_scaled, train_target, n_jobs=-1)
print(np.mean(scores['test_score']))
0.8303833333333334
from sklearn.model_selection import cross_validate
from sklearn.linear_model import SGDClassifier
sc = SGDClassifier(loss='log', max_iter=20, random_state=42)
scores=cross_validate(sc, train_scaled, train_target, n_jobs=-1)
print(np.mean(scores['test_score']))
0.8436999999999999
역시 만족할 만한 수준은 아니다. 그러면 여기에서 4장에서 배웠던 로지스틱 회귀 공식을 떠올려 보자.
z = a x (Weigth) + b x (Length) + c x (Diagonal) + d x (Height) + e x (Width) + f
이 식을 패션 MNIST 데이터에 맞게 변형 하면 다음과 같다.
z_티셔츠 = w1 x (픽셀1) + w2 x (픽셀2) + ... + w784 x (픽셀784) + b
총 784개의 픽셀, 즉 특성이 있으므로 아주 긴 식이 만들어진다. 가중치 개수도 많아지기 때문에 a,b,c 대신에 w1,w2,w3과 같은 식으로 바꾸었다. 마지막에 절편 b를 더한다. 어렵지 않다. 생선에 대한 특성을 픽셀 특성으로 바꾼 것 뿐이다. 대신 개수가 아주 많아졌다.
이번에는 두 번째 레이블인 바지에 대한 방정식은 어떻게 쓸 수 있을까?
z_바지 = w1' x (픽셀1) + w2' x (픽셀2) + ... + w784' x (픽셀784) + b'
이식은 티셔츠에 대한 선형 방정식과 매우 비슷하다. 동일하게 784개의 픽셀값ㅇ을 그대로 사용하고 있다. 다만 바지에 대한 출력을 계산하기 위해 가중치와 절편은 다른 값을 사용해야 한다. 티셔츠와 같은 가중치를 사용한다면 바지와 티셔츠를 구분할 수 있을 리가 없다.
이런 식으로 나머지 클래스에 대한 선형 방정식을 모두 생각해 볼 수 있다. SGDClassifier 모델은 패션 MNIST 데이터의 클래스를 가능한 잘 구분할 수 있도록 이 10개의 방정식에 대한 모델 파라미터(가중치와 절편)을 찾는다.
첫 번째 픽셀1이 w1과 곱해져서 z_ 티셔츠에 더해진다. 두 번째 픽셀2도 w2와 곱해져서 z_티셔츠에 더해진다. 마지막 픽셀 784도 w784와 곱해져서 z_티셔츠에 더하고 절편 b를 더한다. z_바지에 대해서도 동일한 계산 과정이 수행된다.
여기에서 중요한 점은 앞에서도 언급했듯이 티셔프를 계산하기 위해 픽셀 784개와 곱하는 가중치 784개(w1~w784)와 절편 (b)이 바지를 계산하기 위해 픽셀 784개와 곱하는 가중치 784개(w1~w784), 절편(b)과 다라다는 것이다.
z_티셔츠, z_바지와 같이 10개의 클래스에 대한 선형방정식을 모두 계산한 다음에는 4장에서 보았듯이 소프트맥스 함수를 통과하여 각 클래스에 대한 확률을 얻을 수 있다.
좋다. 로지스틱 회귀에 대해 확실히 복습을했다. 그럼 이제 인공 신경망을 만들어 패션 아이템 분류 문제의 성능을 높일 수 있는지 지켜보자.
인공 신경망¶
이미지 분류 문제에는 인공 신경망이 잘 맞는다는 것을 기억하자. 조금 어이없게 들릴 수 있지만, 가장 기본적인 인공 신경망을 확률적 경사 하강법을 사용하는 로지스틱 회귀와 같다. 아니 그렇다면 어떻게 인공 신경망으로 성능을 높일 수 있을까? 이에 대해 대답을 하기 전에 패션 아이템 분류 문제를 인공 신경망으로 표현해 보자.
앞서 로지스틱 회귀를 표현한 그림과 비슷하다. 여기에서는 z_티셔츠, z_바지를 z1, z2와 같이 바꾸었다. 클래스가 총 10개이므로 z10까지 계산한다. z1 ~ z10 을 계산하고 이를 바탕으로 클래스를 예측하기 때문에 신경망의 최종 값을 만든다는 의미에서 출력층 output layer이라고 부른다.
인공 신경망에서는 z값을 계산하는 단위를 뉴런 neuron 이라고 부른다. 하지만 뉴런에서 일어나는 일은 선형 계산이 전부이다.! 이제는 뉴런이란 표현 대신에 유닛 unit이라고 부르는 사람이 더 많아지고 있다.
그 다음 픽셀1, 픽셀2를 x1,x2와 같이 바꾸었다. 784번째 픽셀에 해당하는 x784까지 나타냈다. 인공 신경망은 x1 ~ x784 까지를 입력층 input layer이라고 부른다. 즉 입력층은 픽셀값 자체이고 특별한 계산을 수행하지 않는다. 하지만 많은 사람이 입력층이라 부르기 때문에 여기에서도 관례를 따른다.
z1을 만들기 위해 픽셀1인 x1에 곱해지는 가중치는 w1,1 이라고 쓰고 z2를 만들기 위해 픽셀1인 x1에 곱해지는 가중치는 w1,2 라고 쓴다. 절편은 뉴런마다 하나씩이므로 순서대로 b1,b2 와 같이 나타내었다.
1장에서 소개했듯이 인공 신경망은 1943년 워런 매컬러와월터 피츠 가 제안한 뉴런 모델로 거슬러 올라간다. 이를 매컬러-피츠 뉴런이라고 부른다. 이런 인공 뉴런은 다음과 같은 생물학적 뉴런에서 영감을 얻어 만들었다.
생물학적 뉴런은 수상 돌기로부터 신호를 받아 세포체에 모은다. 신호가 어떤 임곗값에 도달하면 축삭 돌기를 통하여 다른 세포에 신호를 전달한다. 앞서 그렸던 인공 신경망의 출력층에 있는 인공 뉴런 하나와 비교하면 비슷하지 않나?
하지만 생물학적 뉴런이 가중치(w1,1 , w2,1 )와 입력을 곱하여 출력을 만드는 것은 아니다. 4장에서 보았던 시그모이드 함수나 소프트맥수 함수를 사용하는 것은 더욱 아니다. 인공 뉴런은 생물학적 뉴런의 모양을 본뜬 수학 모델에 불과하다. 생물학전 뉴런이 하는 일을 실제로 구현한 것이 아니다.
앞으로 계속 보겠지만 인공 신경망은 정말 우리의 뇌에 있는 뉴런과 같지 않다. 인공 신경망이란 말을 많이 사용할 수밖에 없지만 정말 뇌 속에 있는 무언가를 만드는 일이 아니라는 것을 꼭 기억하세요. 인공 신경망은 기존의 머신러닝 알고리즘이 잘 해결하지 못했던 문제에서 높은 성능을 발휘하는 새로운 종류의 머신러닝 알고리즘 뿐이다. 여기에 대해서는 앞으로 배우면서 더 잘 이해할 수 있을 것이다.
그럼 딥러닝은 무엇 인가?
- 딥러닝은 인공 신경망과 거의 동의어로 사용되는 경우가 많다. 혹은 심층 신경망(deep neural network, DNN)을 딥러닝이라고 부른다. 심층 신경망은 다음 절에서 보겠지만 여러 개의 층을 가진 인공 신경망 이다.
그럼 확률적 경사 하강법을 사용한 로지스틱 회귀 모델이 가장 간단한 인공 신경망이라면 인공 신경망을 만들어도 성능이 좋아지지 않을 것 같다. 하지만 인공 신경망 모델을 만드는 최신 라이브러리들은 SGDClassifier에는 없는 몇 가지 기능을 제공한다. 이런 기능 덕택에 더 좋은 성능을 얻을 수 있다.
그럼 가장 인기가 높은 딥러닝 라이브러리인 텐서플로를 사용해 인공 신경망 모델을 만들어 보자.
텐서플로와 케라스
텐서플로는 구글이 2015년 11월 오픈소스로 공개한 딥러닝 라이브러리이다. 이때를 기점으로 딥러닝에 대한 개발자의 관심이 늘어났고, 2016년 3월 알파고가 이세돌 9단을 이겨 대중에 알려지면서 그야말로 폭발적으로 인기가 높아졌다.
텐서플로는 그 후 많은 발전을 거듭하면서 2019년 9월 2.0버전이 릴리스 되었다. 이 책에서는 텐서플로 2.x 최신 버전을 사용한다. 이미 예상했겠지만 코랩에는 이미 텐서플로가 설치되어 있기 때문에 다음처럼 간단히 임포트하여 사용할 수 있다.
import tensorflow as tf
텐서플로에는 저수준 API와 고수준 API가 있다. 바로 케라스 keras가 텐서플로의 고수준 API이다. 케라스는 2015년 3월 프랑소와 숄레 Francois Chollet가 만든 딥러닝 라이브러리이다.
딥러닝 라이브러리가 다른 머신러닝 라이브러리와 다른 점 중 하나는 그래픽 처리 장치인 GPU를 사용하여 인공 신경망을 훈련한다는 것이다. GPU는 벡터와 행렬 연산에 매우 최적화되어 있디 때문에 곱셈과 덧셈이 많이 수행되는 인공 신경망에 큰 도움이 된다.
- 그럼 이장의 예제를 실행할 때 GPU를 사용해야 되나요?
- 네, GPU를 사용하면 텐서플로로 만든 딥러닝 모델을 훨씬 빠르게 훈련시킬 수 있다. 다행히 코랩에서 무료로 GPU를 사용할 수있다.
케라스 라이브러리는 직접 GPU 연산을 수행하지 않는다. 대신 GPU 연산을 수행하는 다른 라이브러리를 백엔드backend 로 사용한다. 예를 들면 텐서플로가 케라스의 백엔드 중 하나이다. 이외에도 씨아노,CNTK와 같은 딥러닝 라이브러리를 케라스 백엔드로 사용할 수 있다. 이런 케라스를 멀티-백엔드 케라스라고 부른다. 케라스 API만 익히면 다양한 딥러닝 라이브러리를 입맛대로 골라서 쓸 수 있는 셈이다. 이를 위해 케라스는 직관적이고 사용하기 편한 고수준 API를 제공한다.
프랑소와가 구글에 합류한 뒤 텐서플로 라이브러리에 케라스 API가 내장되었다. 텐서플로 2.0부터는 케라스 API를 남기고 나머지 고수준 API를 모두 정리해고, 케라스는 텐서플로의 핵심 API가 되었다. 다양한 백엔드를 지원했던 멀티-백엔드는 케라스는 2.3.1 버전 이후로 더 이상 개발되지 않는다. 이제는 케라스와 텐서플로가 거의 동의어가 된 셈이다.
와 ~설명이 길었다. 텐서플로에서 케라스를 사용하려면 다음과 같이 임포트 한다.
from tensorflow import keras
좋다. 텐서플로와 케라스에 대한 충분한 설명을 들었다. 그럼 케라스 API를 사용해 패션 아이템을 분류하는 가장 간단한 인공 신경망을 만들어 보자.
인공 신경망으로 모델 만들기¶
여기에서는 앞서 로지스틱 회귀에서 만든 훈련 데이터 train_scleddhk train_target을 사용하겠다. 로지스틱 회귀에서는 교차 검증을 사용해 모델을 평가했지만, 인공 신경망에서는 교차 검증을 잘 사용하지 않고 검증 세트를 별도로 덜어내어 사용한다.
이렇게 하는 이유는
- 딥러닝 분야의 데이터셋은 충분히 크기 때문에 검증 점수가 안정적이고,
- 교차 검증을 수행하기에는 훈련 시간이 너무 오래 걸리기 때문이다.
어떤 딥러닝 모델은 훈련하는 데 몇 시간, 심지어 며칠이 걸릴 수도 있다. 패션 MNIST 데이터셋이 그만큼 크지는 않지만, 관례를 따라 검증 세트를 나우어 보겠다. 사이킷런의 train_test_split() 함수를 사용한다.
from sklearn.model_selection import train_test_split
train_scaled, val_scaled, train_target, val_target = train_test_split(train_scaled, train_target, test_size=0.2, random_state=42)
- 사실 패션 MNIST 데이터는 이미 잘 섞인 데이터따라 train_test_split() 함수를 사용하지 않고 앞이나 뒤에서 10,000개 정도의 샘플을 덜어서 검증 세트로 만들어도 된다. 하지만 우리는 일반적인 상황을 가정하여 데이터를 섞어서 나우었다.
훈련 세트에서 20%를 검증 세트로 덜어 내었다. 훈련 세트와 검증 세트의 크기를 알아보자.
print(train_scaled.shape, train_target.shape)
(48000, 784) (48000,)
print(val_scaled.shape, val_target.shape)
(12000, 784) (12000,)
60,000개 중에 12,000개가 검증 세트로 분리되었다. 먼저 훈련 세트(train_scaled, train_target)로 모델을 만든다. 그 다음 검증 세트(val_scaled, val_target)로 훈련한 모델을 평가해 본다.
먼저 인공 신경망 그림의 오른쪽 놓인 층을 만들어 보자. 이 층은 다음 그림처럼 10개의 패션 아이템을 분류하기 위해 10개의 뉴런으로 구성된다.
케라스의 레이어(keras.layers) 패키지 안에는 다양한 층이 준비되어 있다. 가장 기본이 되는 층은 밀집층 dense layer 이다. 왜 밀집이라고 부를까? 다음 그림에서 왼쪽에 있는 784개의 픽셀과 오른쪽에 있는 10개의 뉴런이 모두 연결된 선을 생각해 보자. 784 x 10 = 7,840개의 연결된 선이 있다. 정말 빽빽하다. 그래서 밀집층이다.
이런 층을 양쪽의 뉴런이 모두 연결하고 있기 때문에 완전 연결층 fully connected layer이라고 부른다. 그럼 케라스의 Dense 클래스를 사용해 밀집층을 만들어 보자. 필요한 매개 변수는 뉴런 개수, 뉴런의 출력에 적용할 함수, 입력의 크기이다.
dense = keras.layers.Dense(10, activation='softmax', input_shape=(784,))
WARNING:tensorflow:From /home/ubuntu/.local/lib/python3.6/site-packages/tensorflow/python/ops/init_ops.py:1251: calling VarianceScaling.__init__ (from tensorflow.python.ops.init_ops) with dtype is deprecated and will be removed in a future version. Instructions for updating: Call initializer instance with the dtype argument instead of passing it to the constructor
첫 번째 매개변수로 뉴런 10개로 지정한다. 10개의 패션 아이템을 분류하기 때문이다. 10개의 뉴런에서 출력되는 값을 확률로 바꾸기 위해서는 소프트맥스 함수를 사용한다. 4장에서 다중 분류에 왜 소프트맥스 함수를 사용하는지 자세히 설명했다. 케라스 층에서 activation 매개변수에 이 함수를 지정한다. 만약 2개의 클래스를 분류하는 이진 분류라면 시그모이드 함수를 사용 하기 위해 activation='sigmoid'와 같이 설정한다. 마지막으로 세번째 매개변수는 입력값의 크기이다. 10개의 뉴런이 각각 몇 개의 입력을 받는지 튜플로 지정한다. 여기에서는 784개의 픽셀값을 받는다. 처음으로 신경망 층을 만들었다. 이제 이 밀집층을 가진 신경망 모델을 만들어야 한다. 케라스의 Sequential 클래스를 사용하겠다.
model = keras.Sequential([dense])
Sequential 클래스의 객체를 만들 때 앞에서 만든 밀집층의 객체 dense를 전달했다. 여기서 만든 model 객체가 바로 신경망 모델이다. 다음 그림에 지금까지 만든 신경망을 나타냈다. 마지막에 소프트맥스 함수를 적용한 것을 주목하자.
소프트맥스와 같이 뉴런의 선형 방정식 계산 결과에 적용되는 함수를 활성화 함수 activation function라고 부른다. 이 책에서는 이 값을 a로 표시하겠다. 앞으로 인공 신경망을 배워가면서 다양한 활성화 함수를 하나씩 소개하겠다.
- 소프트맥스 함수는 별도의 층인가?
- 시그모이드 함수나 소프트맥스와 같은 활성화 함수는 뉴런의 출력에 바로 적용되기 때문에 보통 층의 일부로 나타낸다. 하지만 종종"소프트 맥스 층을 적용했어"와 같이 따로 부르는 경우가 많다. 가중치와 절편으로 선형 계산을 수행하는 층을 좁은 개념의 신경망 층으로 생각한다면 소프트맥스 층은 넓은 의미의 층이라 볼 수 있다. 케라스 API에도 층의 개념을 폭넓게 적용하고 있다.
인공 신경망으로 패션 아이템 분류하기¶
지금까지 사용했던 사이킷런에 비해 케라스에서 모델을 만드는 방식은 조금 다르다.
케라스 모델은 훈련하기 전에 설정 단계가 있다. 이런 설정을 model 객체이 compile()메서드에서 수행한다. 꼭 지정해야 할 것은 손실 함수의 종류이다. 그 다음 훈련과정에서 계산하고 싶은 측정값을 지정한다.
model.compile(optimizer="adam",
loss="sparse_categorical_crossentropy",
metrics = ["accuracy"])
'sparse_categorical_crossentropy' 이름이 엄청 길다. 조금씩 나누어 생각해보자. 혹시 4장의 이진 분류에서 이진 크로스 엔트로피 손실 함수를 사용한다고 했던 것을 기억하나? 다중 분류에서는 크로스 엔트로피 손실 함수를 사용한다. 케라스에서는 이 두 손실 함수를 각 각 'binary_crossentropy', 'categorical_crossentropy'로 나누어 부른다.
- 이진 분류: loss= 'binary_crossentropy'
- 다중 분류: loss= 'categorical_crossentropy'
그런데 sparse란 단어는 왜 앞에 붙었을까? 이진 크로스 엔트로피 손실을 위해 -log(예측확률)에 타깃값(정답)을 곱했었다.
이진 분류에는 출려층의 뉴런이 하나이다. 이 뉴런이 확률하는 확률값 a(시그모이드 함수의 출력값)를 사용해 양성 클래스와 음성 클래스에 대한 크로스 엔트로피를 계산한다. 이 계산은 4장에서 본 로지스틱 손실 함수와 동일하다.
이진 분류의 출력 뉴런은 오직 양성 클래스에 대한 확률(a)만 출력하기 때문에 음성 클래스에 대한 확률은 간단히 1-a 로 구할 수 있다. 역시 이진 분류의 타깃값은 양성 샘플일 경우에는 1, 음성 샘플일 경우에는 0으로 되어 있다. 0을 곱하면 어떤 계산이든지 모두 0이 되기 때문에 특별히 음성 샘플일 경우 1로 바꾸어 (1-타깃값) 계산한다. 이렇게 하면 하나의 뉴런만으로 양성과 음성 클래스에 대한 크로스 엔트로피 손실을 모두 계산할 수 있다.
그럼 패션 MNIST 데이터셋과 가티 다중 분류일 경우 어떻게 계산할수 있을까?
출력층은 10개의 뉴런이 있고 10개의 클래스에 대한 확률을 출력한다. 첫 번째 뉴런의 티셔츠일 확률이고 두 번째 뉴런은 바지일 확률을 출력한다. 이진 분류와 달리 각 클래스에 대한 확률이 모두 출력되기 때문에 타깃에 해당하는 확률만 남겨 놓기 위해서 나머지 확률에는 모두 0을 곱한다.
예를 들어 샘플이 티셔프일 경우 첫 번째 뉴런의 활성화 함수 a1에 크로스 엔트로피 손실 함수를 적용하고 나머지 활성화 함수 출력 a2 ~ a 10까지는 모두 0으로 만든다. 이렇게 하기 위해 티셔츠 샘플의 타깃값은 첫 번째 원소만 1이고 나머지는 모두 0인 배열로 만들 수 있다. 이 배열과 출력층의 활성화 값의 배열과 곱하면 된다. 2장에서 배웠던 넘파이 브로드 캐스팅이 여기에서도 적용된다. 배열에서 동일한 위치의 원소끼리 곱셈이 된다. 결국 다른 원소는 모두 0이 되고 a1만 남게된다.
결국 신경망은 티셔츠 샘프에서 손실을 낮추려면 첫 번째 뉴런의 활성화 출력 a1의 값을 가능한 1에 가깝게 만들어야 한다. 바로 이것이 크로스 엔트로피 손실 함수가 신경망에 원하는 바이다.!
이와 같이 타깃값을 해당 클래스만 1이고 나머지는 모두 0인 배열로 만드는 것은 원-핫 인코딩 one-hot encoding이라고 부른다.
따라서 다중 분류에서 크로스 엔트로피 손실 함수를 사용하려면 0,1,2와 같이 정수로 된 타깃값을 원-핫 인코딩으로 변환해야 한다.
그런데 패션 MNIST 데이터의 타깃값은 어떻게 되어 있나?
print(train_target[:10])
[7 3 5 8 6 9 3 3 9 9]
모두 정수로 되어 있다. 하지만 텐서플로에서는 정수로 된 타깃값을 원-핫 인코딩으로 바꾸지 않고 그냥 사용할 수 있다. 정수로된 타깃값을 사용해 크로스 엔트로피 손실을 계산하는 것이 바로 'sparse_categorical_crossentropy' 이다. 빽빽한 배열 말고 정숫값 하나만 사용한다는 뜻에서 sparse희소 라는 이름을 붙인 것 같다. 타깃값을 원-핫 인코딩으로 준비했다면 compile()메서드에 손실 함수를 loss='categorical_crossentropy'로 지정한다.
이제 compile() 메서드의 두 번째 매개변수인 metrics에 대해 알아보자. 케라스는 모델이 훈련할때 기본으로 에포크마다 손실 값을 출력해 준다. 손실이 줄어드는 것을 보고 훈련이 잘되었다는 것을 알 수 있지만 정확도르 함께 출력하면 더 좋다. 이를 위해 metrics 매개변수에 정확도 지표를 의미하는 'accuracy'를 지정했다.
자 이제 모델을 훈련해 보자. 훈련하는 fit()메서드는 사이킷런과 매우 비슷하다. 처음 두 매개변수에 입력(train_scaled)와 타깃(train_target)을 지정한다. 그 다음 반복할 에포크 횟수를 epochs 매개변수로 지정한다. 사이킷런의 로지스틱 모델과 동일하게 5번 반복해 보겠다.
model.fit(train_scaled, train_target, epochs=5)
Epoch 1/5 48000/48000 [==============================] - 2s 39us/sample - loss: 0.6267 - acc: 0.7879 Epoch 2/5 48000/48000 [==============================] - 2s 36us/sample - loss: 0.4737 - acc: 0.8391 Epoch 3/5 48000/48000 [==============================] - 2s 36us/sample - loss: 0.4445 - acc: 0.8468 Epoch 4/5 48000/48000 [==============================] - 2s 36us/sample - loss: 0.4282 - acc: 0.8531 Epoch 5/5 48000/48000 [==============================] - 2s 36us/sample - loss: 0.4176 - acc: 0.8557
<tensorflow.python.keras.callbacks.History at 0x7fa2536d0940>
케라스는 친절하게 에포크마다 걸린 시간과 손실(loss), 정확도(accuracy)를 출력해 준다. 5번 반복에 정확도가 85%를 넘었다. 그럼 앞서 따로 떼어 놓은 검증 세트 (val_scaled, val_target)에서 모델의 성능을 확인해 본다. 케라스에서 모델의 성능을 평가하는 메서드는 evaluate()메서드 이다.
model.evaluate(val_scaled, val_target)
12000/12000 [==============================] - 0s 19us/sample - loss: 0.4249 - acc: 0.8525
[0.42493097853660583, 0.8525]
evaluate()메서드도 fit()메서드와 비슷한 출력을 보여준다. 검증 세트의 점수는 훈련 세트 점수보다 조금 낮은것이 일반적이다. 예상대로 평가 결과는 훈련 세트의 점수보다 조금 낮은 83%정도의 정확도를 냈다.
인공 신경망 알고리즘을 사용해 패션 아이템을 분류하는 모델을 잘 훈련했다.
인공 신경망 모델로 성능 향상 - 문제해결 과정¶
이 절에서는 28 x 28 크기의 흑백 이미지로 저장된 패션 아이템 데이터셋인 패션 MNIST 데이터셋을 사용했다. 먼저 로지스틱 손실 함수를 사용한 SGDClassifier 모델을 만들어 교차 검증 점수를 확인했다.
그 다음 가장 인기 있는 딥러닝 라이브러리인 텐서플로와 케라스 API를 소개하고 케라스를 사용해 간단한 인공 신경망 모델을 만들어 패션 아이템을 분류해 보았다. 이 간단한 인공 신경망은 사실상 앞에서 만든 경사 하강법을 사용한 로지스틱 회귀 모델과 거의 비슷하다. 하지만 몇 가지 장점 덕분에 조금 더 높은 성능을 냈다. 이에 대해서는 다음 절에 이어서 설명하겠다.
인공 신경망 모델을 만들면서 이전 장에서 배웠던 로지스틱 손실 함수와 크로스 엔트로피 손실 함수를 다시 되새겨 보았다. 그리고 신경망에서 이런 손실 함수를 어떻게 계산하는지 그림을 통해 배웠다. 이 과정에서 원-핫 인코딩을 배웠고 케라스 API에 대해 조금 더 자세히 알 수 있다.
다음 그림에서 사이킷런의 SGDClassifier와 케라스 Sequential 클래스 사용법의 차이를 그림으로 정리하면서 이 절을 마무리하겠다.
- 출처: 혼자 공부하는 머신러닝 + 딥러닝
'혼자공부하는 머신러닝 + 딥러닝' 카테고리의 다른 글
Chapter07-3 신경망 모델 훈련 (0) | 2021.06.01 |
---|---|
Chapter07-2 심층 신경망 (0) | 2021.05.31 |
Chapter 06. 비지도 학습(주성분 분석) (0) | 2021.05.29 |
Chapter06. 비지도 학습 (군집알고리즘, k-평균) (0) | 2021.05.29 |
Chapter05.트리의 앙상블 (0) | 2021.05.28 |