
이번에는 패션 MNIST 데이터셋을 사용해서 실습을 진행할 것이다
해당 데이터는 10종류의 패션 아이템으로 구성되어 있고
이번에는 텐서플로를 사용해서 이 데이터를 불러올 것이다
본격적으로 들어가기전에 데이터에 대해서 확인을 해보는 작업을 해보겠다
from tensorflow import keras
(train_input, train_target), (test_input, test_target) =\
keras.datasets.fashion_mnist.load_data()
텐서플로의 케라스 패키지를 임포트하고 패션 MNIST 데이터를 다운로드했다
keras.datasets.fashino_mnist 모듈아래에 load_data() 함수는
훈련 데이터와 테스트 데이터를 나누어서 반환을 한다
해당 데이터 각각 입력과 타깃의 쌍으로 구성되었다

확인해보면 잘 전달받았다는 것을 알 수 있다
그러면 전달받은 데이터의 크기들을 알아보자
print(train_input.shape, train_target.shape) # (60000, 28, 28) (60000,)
print(test_input.shape, test_target.shape) # (10000, 28, 28) (10000,)
훈련 데이터는 6만개의 이미지로 각 이미지는 28x28 크기이고
타깃도 6만개의 원소가 있는 1차원 배열이다
테스트 데이터는 1만개의 이미지로 이루어져 있다
그러면 이미지는 어떤 크기로 구성되어 있는지 출력을 해보도록 하자
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()

크기가 28x28 이다 보니 꽤 작고 흐릿하다
그러면 이 샘플의 타깃값은 어떻게 될까?
파이썬의 리스트 내포를 사용해서 처음 10개의 타깃값을 리스트로 만든 후에 출력해보자
print([train_target[i] for i in range(10)])
# [np.uint8(9), np.uint8(0), np.uint8(0), np.uint8(3), np.uint8(0),
# np.uint8(2), np.uint8(7), np.uint8(2), np.uint8(5), np.uint8(5)]
패션 MNIST의 타깃은 0~9까지의 숫자 레이블로 구성되어 있다
마지막 2개의 샘플이 같은 레이블(숫자5)를 가지고 있다
출력한 이미지를 봐도 마지막의 두 그림이 비슷한 것을 확인할 수 있다
그러면 마지막으로 넘파이 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천개의 샘플이 들어있는 것을 확인할 수 있다
이 훈련 샘플은 6만개나 되기 때문에 전체 데이터를 한꺼번에 사용하여
모델을 훈련하는 것보다 샘플을 하나씩 꺼내서 모델을 훈련하는 방법이 더 좋아보인다
이럴 때 좋은 방법이 경사하강법이다
우리가 과거에 SGDClassifier 클래스의 loss 매개변수를 'log'로 지정하여
로지스틱 손실 함수를 최소화하는 확률적 경사 하강법 모델을 만들었다
그때에 SGDClassifier를 사용할 때 표준화 전처리된 데이터를 사용했는데
그 이유가 확률적 경사 하강법은 여러 특성 중에서 기울기가 가장 가파른 방향을
따라 이동하는데 특성마다 값이나 범위가 다르면 올바르게
경사를 내려올 수가 없어 전처리를 해준다고 했다
패션 MNIST의 경우에는 픽셀값을 0~255 사이의 정수값을 갖게 된다
이런 이미지의 경우 255로 나누어서 0~1 사이의 값으로 정규화하면 된다
이 방법이 표준화는 아니지만 양수 값으로 이루어진 이미지를
전처리할 때 널리 사용하는 방법 이다
먼저 reshape() 메서드를 사용해서 2차원 배열인 각 샘플을 1차원 배열로 바꾸자
왜냐하면 SGDClassifier는 2차원 입력을 다루지 못하기 때문에
각 샘플을 1차원으로 바꿔줘야 한다
train_scaled = train_input / 255.0
train_scaled = train_scaled.reshape(-1, 28*28)
print(train_scaled.shape) # (60000, 784)
reshape() 메서드의 두 번째 매개변수를 28x28 이미지 크기에 맞게
지정하면 첫 번째 차원 (샘플 개수)는 변하지 않고
원본 데이터의 두 번째, 세 번째 차원이 1차원으로 합쳐진다
그래서 합치게 되면 784개의 픽셀로 이루어진 6만개의 샘플이 만들어진다
이제 그러면 SGDClassifier 클래스와 cross_validate 함수를 사용해서
교차 검증으로 결과값을 확인해보면 다음과 같다
from sklearn.model_selection import cross_validate
from sklearn.linear_model import SGDClassifier
sc = SGDClassifier(loss='log_loss',max_iter=5, random_state=42)
scores = cross_validate(sc, train_scaled, train_target, n_jobs=-1)
print(np.mean(scores['test_score'])) # 0.8194166666666666
해당 코드는 max_iter를 5로 지정했다
반복횟수를 한 번 늘려보도록 하겠다
하지만 그래도 성능이 크게 향상되지는 않는다
한 번 여기에서 로지스틱 회귀 공식을 생각해보자
z = a x (Weight) + 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개의 픽셀, 즉 특성이 있으므로 아주 긴 식이 만들어지게 된다
그러면 두 번째 레이블인 바지에 대한 방정식은 어떨까?
z_바지 = w1` x (픽셀1) + w2` x (픽셀2) + ... + w784` x (픽셀784) + b`
동일하게 784개의 픽셀값을 그대로 사용하고 있지만 바지에 대한 출력을 계산하기 위해
가중치와 절편은 다른 값을 사용해야 한다

이런식으로 나머지 클랙스에 대한 선형 방정식을 모두 생각해 볼 수 있다
SGDClassifier 모델은 패션 MNIST 데이터의 클래스를 가능한 잘 구분할 수 있도록
이 10개의 방정식에 대한 모델 파라미터 (가중치와 절편)를 찾는다
이렇게 z_티셔츠, z_바지와 같이 10개의 클래스에 선형 방정식을 모두 계산한 다음
소프트맥스 함수를 통과하여 각 클래스에 대한 확률을 얻을 수 있다
사실 이미지 분류 문제에는 인공 신경망이 더 잘 어울린다
가장 기본적인 인공 신경망은 확률적 경사 하강법을 사용하는
로지스틱 회귀와 같다 그렇다면 어떻게 인공 신경망으로
성능을 높일 수 있는지 알아보도록 하자

앞서 로지스틱 회귀를 표현한 그림과 매우 비슷하다
클래스가 총 10개이므로 z10까지 계산한다
z1~z10을 계산하고 이를 바탕으로 클래스를 예측하기에
신경망의 최종값을 만든다는 의미에서 출력층(output layer)라고 한다
인공신경망에서는 z값을 계산하는 단위를 뉴런(neuron)이라고 한다
하지만 뉴런에서 일어나는 일은 선형 계산이 전부이다
그리고 요즘에는 뉴런이라는 표현 대신에 유닛(unit)이라고
부르는 사람이 늘어나고 있다
그 다음 픽셀 1, 픽셀 2를 x1, x2와 같이 바꾸었다
인공신경망은 x1~x784까지를 입력층(input layer)라고 부른다
입력층은 픽셀값 자체이고 특별한 계산을 수행하지 않는다
이러한 인공신경망은 1943년 워런 매컬러와 월터 피츠가 제안한 뉴런 모델로
거슬러 올라가게 된다 이를 매컬러-피츠 뉴런이라고 부른다
이런 인공 뉴런은 생물학적 뉴런에서 영감을 받아 만들어지게 되었다

생물학적 뉴런은 수상돌기로부터 신호를 받아 세포체에 모은다
신호가 어떤 임계값에 도달하면 축삭 돌기를 통하여 다른 세포에 신호를 전달하게 된다
앞에서 그렸던 인공신경망의 출력층에 있는 인공지능 뉴런 하나와 비교하면 비슷해보인다
하지만 생물학적 뉴런이 가중치와 입력을 곱해서 출력을 만드는 것도 아니고
시그모이드 함수나 소프트맥스 함수를 사용하는 것도 아니다
그냥 뉴런의 모양을 본뜬 수학 모델에 불과하다

딥러닝은 인공 신경망과 거의 동의어로 사용되는 경우가 많다
혹은 심층 신경망(DNN)을 딥러닝이라고 부른다
심층 신경망은 여러 개의 층을 가진 인공 신경망을 이야기한다
생각을 해보면 확률적 경사 하강법을 사용한 로지스틱 회귀 모델이
가장 간단한 인공 신경망이라면
인공 신경망을 만들어도 성능이 좋아지지 않을 것 같아 보인다
하지만 인공 신경망 모델을 만드는 최신 라이브러리들은
SGDClassifier에는 없는 몇 가지 기능을 제공한다
이런 기능 덕분에 더 좋은 성능을 낼 수 있다
텐서플로는 구글이 오픈소스로 공개한 딥러닝 라이브러리이다
텐서플로에는 저수준 API와 고수준 API가 있다
케라스(Keras)가 텐서플로의 고수준 API이다
케라스는 2015년 3월에 프랑소와 숄레가 만든 딥러닝 라이브러리이다
딥러닝 라이브러리가 다른 머신러닝 라이브러리와
다른 점 중 하나는 그래픽 처리 장치인 GPU를 사용하여
인공 신경망을 훈련한다는 점이다
GPU는 벡터와 행렬 연산에 매우 최적화되어 있기 때문에
곱셈과 덧셈이 많이 수행되는 인공 신경망에 큰 도움이 된다
케라스 라이브러리는 직접 GPU 연산을 수행하지 않는다
대신 GPU 연산을 수행하는 다른 라이브러리를 백엔드로 사용한다
예를 들면 텐서플로가 케라스의 벡엔드 중 하나이다
이외에도 씨아노, CNTK와 같은 여러 딥러닝 라이브러리를 백엔드로 쓸 수 있다
이런 케라스를 멀티-백엔드 케라스라고 부른다
케라스 API만 익히면 다양한 딥러닝 라이브러리를 사용해볼 수 있다
이를 위해 케라스는 직관적이고 사용하기 편한 고수준 API를 제공한다
프랑소와가 구글에 합류한 뒤 텐서플로 라이브러리에 케라스 API가 내장되었다
텐서플로 2.0부터는 케라스 API만 남기고 나머지 고수준 API를 모두 정리했고
케라스는 텐서플로의 핵심 API가 되었다
다양한 백엔드를 지원했던 멀티-벡엔드 케라스는 2.3.1 버전 이후로
더 이상 개발되지 않는다 이제 케라스와 텐서플로가 거의 동의어가 되었다
import tensorflow as tf
from tensorflow import keras
이제 본격적으로 간단한 인공신경망 모델을 만들어보자
데이터는 로지스틱 회귀에서 만든 훈련 데이터 train_scaled와
train_target을 사용할 것이다
로지스틱 회귀에서는 교차 검증을 사용하여 모델을 평가했지만
인공 신경망에서는 교차 검증을 잘 사용하지 않고
검증 세트를 별도로 덜어내서 사용한다
이렇게 하는 이유는 딥러닝 분야의 데이터셋은 충분히 크기 때문에
검증 점수가 안정적이고, 교차 검증을 수행하기에는 훈련 시간이 너무 오래걸린다
또 어떤 딥러닝 모델은 훈련하는데 몇 시간, 심지어 며칠이 걸릴 수도 있다
패션 MNIST 데이터셋이 그렇게 크지는 않지만
관례를 따라 검증 데이터를 나누어 보자
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)
훈련 세트에서 20% 정도를 검증 세트로 덜어냈다
print(train_scaled.shape, train_target.shape) # (48000, 784) (48000,)
print(val_scaled.shape, val_target.shape) # (12000, 784) (12000,)
6만개 중에서 1만2천개가 검증 데이터로 분리되었다
먼저 훈련 데이터로 모델을 만들고 검증 데이터로 평가를 해보도록 하겠다
먼저 10개의 패션아이템을 분류하기 위한 10개의 뉴런으로 구성된 부분을 만들겠다
케라스의 레이어(keras.layers) 패키지 안에는 다양한 층이 준비되어 있다
가장 기본이 되는 층은 밀집층(dense layer)이다
밀집층이라고 부르는 이유는 아까를 그림을 생각해보면 된다
왼쪽에 있는 784개의 픽셀과 오른쪽에 있는 뉴런 10개이 모두 연결된 선은
무려 784 x 10 = 7840으로 무려 7840개가 된다
엄청 많지 않은가? 그래서 우리는 이를 밀집층이라고 부른다
이런 층을 양쪽 뉴런이 모두 연결하고 있기 때문에 완전 연결층이라고도 부른다
그럼 케라스의 Dense 클래스를 사용해서 밀집층을 만들어보자
필요한 매개변수는 뉴런 개수, 뉴런의 출력에 사용할 함수, 입력의 크기이다
dense = keras.layers.Dense(10,activation='softmax',input_shape=(784,))
첫 번째 매개변수로 뉴런 개수를 10개로 지정했다
왜냐하면 10개의 패션 아이템을 구분하기 때문이다
10개의 뉴런에서 출력되는 값을 확률로 바꾸기 위해서는 소프트맥스 함수를 사용해야 한다
케라스 층에서는 activation 매개변수에 해당 함수를 정의한다
만약 2개의 클래스를 분류하는 이진 분류라면 시그모이드 함수를 사용하기 위해
activation = 'sigmoid'와 같이 사용하게 된다
마지막으로 세 번째 매개변수는 입력값의 크기이다
10개의 뉴런이 각각 몇 개의 입력을 받는지 튜플로 지정한다
여기에서는 784개의 픽셀 값을 받는다
이렇게 신경망 층을 만들었다
그러면 이 밀집층을 가진 신경망 모델을 만들어야 한다
이 때 사용할 수 있는 것이 케라스 Sequential 클래스이다
model = keras.Sequential([dense])
Sequential 클래스의 객체를 만들 때 만든 밀집층의 객체 dense를 전달했다
여기에서 만든 model 객체가 바로 신경망 모델이다
우리는 소프트맥스와 같이 뉴런의 선형 방정식 계산 결과에
적용되는 함수를 활성화 함수(actviation function)이라고 한다
케라스 모델은 훈련하기 전에 설정 단계가 있다
이런 설정을 model 객체의 compile() 메서드에서 수행한다
이 때 꼭 지정해야 하는 것이 손실함수의 종류이다
손실함수를 지정하고 나면 다음으로는 훈련 과정에서 계산하고 싶은 측정값을 지정한다
model.compile(loss='sparse_categorical_crossentropy',metrics=['accuracy'])
손실함수 sparse_categorical_crossentropy는 처음 등장하는데
이 내용을 한 번 생각해보자
과거에서 우리는 이진 분류에서 이진 크로스 엔트로피 손실 함수를 사용했었다
그러니 자연스럽게 다중 분류에서는 다중 크로스 엔트로피 손실 함수를 사용한다고 알 수 있다
케라스에서는 두 손실 함수를 각각 다음과 같이 부른다
loss = 'binary_crossentropyloss = 'categorical_crossentropy이름으로 이진 분류와 다중 분류의 손실 함수가 명확히 구분되는 것을 볼 수 있다
그런데 왜 sparse란 단어는 붙었을까요?
우리는 이진 크로스 엔트로피 손실을 위해 -log(예측확률)에 타깃값(정답)을 곱했다

이진 분류에서는 출력층의 뉴런이 하나였다
이 뉴런이 출력하는 확률값(시그모이드 함수의 출력값)을 사용해
양성 클래스와 음성 클래스에 대한 크로스 엔트로피를 계산했다
이 내용은 로지스틱 손실 함수와 같다
이진 분류의 출력 뉴런은 오직 양성 클래스에 대한 확률(a)만 출력하기 때문에
음성 클래스에 대한 확률은 간단히 1-a로 구할 수 있다
역시 이진 분류의 타깃값은 양성 샘플일 경우에는 1
음성 샘플일 경우에는 0으로 되어 있다
0을 곱하면 어떤 계산이든지 모두 0이 되기 때문에
특별히 음성 샘플일 경우 1로 바꾸어 (1-타깃값) 계산을 한다
이렇게 하면 하나의 뉴런만으로 양성과 음성 클래스에 대한
크로스 엔트로피 손실을 모두 계산할 수 있다
그러면 다중 분류일 경우에는 어떻게 될까?

출력층은 10개의 뉴런이 있고 10개에 대한 확률을 출력한다
첫 번째 뉴런은 티셔츠 확률이고 두 번째 뉴런은 바지일 확률을 출력한다
이진 분류와 달리 각 클래스에 대한 확률이 모두 출력되기 때문에
타깃에 해당하는 확률만 남겨 놓기 위해서 나머지 확률에는 모두 0을 곱한다
위의 예시처럼 샘플이 티셔츠일 경우에는 첫 번째 뉴런의 활성화
함수 출력인 a1에 크로스 엔트로피 손실 함수를 적용하고
나머지 활성화 함수 출력은 a2~a10까지는 모두 0으로 만든다
이렇게 하기 위해 티셔츠 샘플의 타깃값은
첫 번째 원소만 1이고 나머지는 모두 0인 배열로 만들 수 있다
[a1,a2,a3,a4,a5,a6,a7,a8,a9,a10] x [1,0,0,0,0,0,0,0,0,0]
해당 배열과 출력층의 활성화 값의 배열과 곱하면 된다
길이가 같은 넘파이 배열의 곱셈은 원소별 곱셈으로 수행되고
즉, 배열에서 동일한 위치의 원소끼리 곱셈이 된다
결국 다른 원소는 모두 0이 되고 a1만 남게 된다
결국 신경망은 티셔츠 샘플에서 손실을 낮추려면
첫 번째 뉴런의 활성화 출력 a1의 값을 가능한 1에 가깝게 만들어야 한다
바로 이것이 크로스 엔트로피 손실 함수가 신경망에 원하는 것이다
이와 같이 타깃값을 해당 클래스만 1이고
나머지는 모두 0인 배열로 만드는 것을 원-핫 인코딩 (one-hot encoding)이라고 부른다
따라서 다중 분류에서 크로스 엔트로피 손실 함수를 사용하려면
0,1,2와 같이 정수로 된 타깃값을 원-핫 인코딩으로 변환해야 한다
티셔츠 샘플 : [1,0,0,0,0,0,0,0,0,0]
바지 샘플 : [0,1,0,0,0,0,0,0,0,0]
지금 패션 MNIST 데이터의 타깃값은 정수로 되어있다
print(train_target[:10])
# [7 3 5 8 6 9 3 3 9 9]
텐서플로에서는 정수로 된 타깃값을 원-핫 인코딩으로 바꾸지 않고
그냥 사용할 수 있다 !!
정수로된 타깃값을 사용해 크로스 엔트로피를 계산하는 것이 바로
'sparse_categorical_crossentropy'이다
빽빽한 배열 말고 정수값 하나만 사용한다는 뜻에서
최소 sparse 라는 이름을 붙인 것 같다
타깃값을 원-핫 인코딩으로 준비했다면 compile() 메서드에
손실 함수를 loss='categorical_crossentropy'로 지정한다
이제 complie() 메서드의 두 번째 매개변수인 metrics에 대해 알아보자
케라스는 모델이 훈련할 때 에포크마다 손실 값을 출력해준다
손실이 줄어드는 것을 보고 훈련이 잘되었다는 것을 알 수 있지만
정확도를 함께 출력하면 더 좋다
이를 위해 metrics 매개변수에 정확도 지표를 의미하는 'accuracy'를 지정한다
이제 본격적으로 모델을 훈련시켜보겠다
훈련하는 fit() 메서드는 사이킷런과 매우 비슷하다
처음 두 매개변수에 입력과 타깃을 지정한다 그 다음 반복할 에포크 횟수를
epochs 매개변수로 지정한다 사이킷런의 로지스틱 모델과 동일하게 5번 반복해보겠다
model.fit(train_scaled,train_target, epochs=5)

케라스는 에포크마다 걸린 시간과 손실(loss), 정확도(accuracy)를 출력해준다
5번 반복해서 정확도가 85%를 넘겼다 아까의 모델보다 더 좋아진 것을 확인할 수 있다
그러면 앞서 따로 떼어 놓은 검증 세트에서 모델의 성능을 확인해보겠다
케라스에서 모델의 성능을 평가하는 메서드는 evaluate() 메서드이다
model.evaluate(val_scaled,val_target)

evaluate() 메서드도 fit() 메서드와 비슷한 출력을 보여준다
검증 데이터의 점수는 훈련 데이터 점수보다 조금 낮은 것이 일반적이다
우리는 이번 시간에 28x28 크기의 흑백 이미지로 저장된
패션 아이템셋인 패션 MNIST 데이터셋을 사용했다
먼저 로지스틱 손실 함수를 사용한 SGDClassifier 모델을 만들어
교차 검증 점수를 확인했다
그 다음엔 딥러닝 라이브러리인 텐서플로와 케라스 API에 대해서 알아봤고
케라스를 활용하여 간단한 인공 신경망 모델을 만들어봤다
간단한 인공 신경망 모델은 경사 하강법을 사용한 로지스틱 회귀 모델과 거의 비슷하다
하지만 몇 가지 장점 덕분에 더 높은 성능을 냈다
마지막으로 사이킷런 모델과 케라스 모델에 코드를 적어보면서 정리를 해보자
# 사이킷런 모델
## 1) 모델
sc = SGDClassifier(loss='log_loss', max_iter=5) # 손실함수, 반복횟수
## 2) 훈련
sc.fit(train_scaled, train_target)
### 3) 평가
sc.score(val_scaled,val_target) # 0.8008333333333333
# 케라스 모델
dense = keras.layers.Dense(10, activation='softmax',input_shape=(784,)) # 층 생성
## 1) 모델
model = keras.Sequential([dense])
model.compile(loss='sparse_categorical_crossentropy',metrics=['accuracy'])
## 2) 훈련
model.fit(train_scaled, train_target, epochs=5)
## 3) 평가
model.evaluate(val_scaled,val_target)
