머신러닝을 마무리하고 이제 딥러닝을 공부해보자.
패션 MNIST
MNIST는 머신러닝과 딥러닝을 배울 때 자주 사용하는 데이터셋이다. 딥러닝에서는 손으로 쓴 0~9까지의 숫자로 이루어진 데이터를 사용한다. 패션 MNIST는 크기와 개수가 동일하지만 숫자 대신 패션 아이템으로 이루어진 데이터이다.
패션 MNIST 데이터는 워낙 유명해서 많은 딥러닝 라이브러리에서 가져올 수 있는 도구를 제공한다.
이 책에서는 텐서 플로(Tensor Flow)를 사용해서 데이터를 불러온다.
패션 MNIST가 어떤 데이터인지 확인해 보자.
텐서 플로도 코랩에서 사용할 수 있다. 텐서 플로의 케라스(Keras) 패키지를 임포트해서 패션 MNIST 데이터를 다운로드하자.
keras.datasets.fashion_mnist 모듈 아래 load_data() 함수는 훈련 데이터와 테스트 데이터를 나눠서 반환해 준다.
왼쪽의 폴더 아이콘을 클릭하면 sample_data가 있는데 그 안에 데이터가 있다.
데이터의 크기를 확인해 보자.
훈련 데이터는 60,000개의 이미지로 이루어져 있고, 각 이미지의 크기는 28 X 28이다. 타깃도 60,000개의 원소가 있는 1차원 배열이다.
테스트 데이터도 출력해 보면, 10,000개의 28 X 28 이미지와 10,000개의 타깃 원소로 이루어져 있다.
이전에 맷플롯립 라이브러리로 과일 사진을 출력했던 것처럼 어떤 이미지인지 확인해보자.
크기가 28 X 28이라서 조금 작고 흐릿하다. cmap='gray_r'로 설정해서 반전된 흑백 이미지이다. 이 샘플들의 타깃 값을 확인해보자.
패션 MNIST의 타깃은 0~9까지의 숫자 레이블로 구성된다.
마지막 샘플 두 개의 타깃값이 같은 것으로 미루어보아 종류별로 타깃 값이 설정이 된 것을 추측해볼 수 있겠다.
패션 MNIST에 포함된 10개 레이블은 다음과 같다고 한다.
레이블 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
아이템 | 티셔츠 | 바지 | 스웨터 | 드레스 | 코트 | 샌달 | 셔츠 | 스니커즈 | 가방 | 앵글부츠 |
unique() 함수로 레이블 당 샘플 개수를 확인해 보자.
0~9까지의 레이블마다 6000개의 샘플이 들어 있는 것을 확인할 수 있다.
지금까지의 과정을 요약하면, tensor flow의 keras 패키지를 임포트 해서 패션 MNIST 데이터를 다운로드하고, load_data() 함수로 훈련 데이터와 테스트 데이터를 나눴다. 또한 각 데이터의 크기는 60,000개의 훈련 데이터와 10,000개의 테스트 데이터이고, 타깃 데이터의 레이블이 의미하는 바는 위의 표와 같다. unique() 함수로 출력해본 결과 각 레이블 당 샘플은 6000개씩 들어있는 것을 확인할 수 있었다.
로지스틱 회귀로 패션 아이템 분류하기
훈련 샘플의 개수가 60,000개나 되기 때문에 전체 데이터를 한꺼번에 사용하여 모델을 훈련하는 것 보다 샘플을 하나씩 꺼내서 모델을 훈련하는 방법이 좋아 보인다. 이럴 때 확률적 경사 하강법을 사용하는 것이 좋다.
이전에 SGDClassifier 클래스의 loss 매개변수를 'log'로 지정하여 로지스틱 손실 함수를 최소화하는 확률적 경사 하강법 모델을 만들었었다. 궁금하면 https://aodtns.tistory.com/52?category=1003863 를 참고하자.
SGDClassifier를 사용할 때 표준화 전처리된 데이터를 사용했다. 확률적 경사 하강법은 여러 특성 중 기울기가 가장 가파른 방향을 따라서 이동하기 때문에 특성마다 값의 범위의 차이가 크면 올바르게 손실 함수의 경사를 내려올 수 없다.
패션 MNIST의 경우 각 픽셀은 0~255 사이의 정수 값을 가진다. 이런 이미지의 경우 보통 255로 나누어 0~1 사이의 값으로 정규화한다. 표준화는 아니지만 양수 값으로 이루어진 이미지를 전처리할 때 널리 사용하는 방법이다.
reshape() 메서드를 사용해서 2차원 배열인 각 샘플을 펼쳐서 1차원 배열로 만들자. 이렇게 하는 이유는 SGDClassifier가 2차원 입력을 다루지 못하기 때문이다.
train_input 값을 255.0으로 나눠서 데이터를 전처리하고, 2차원 배열을 1차원 배열로 바꾸었다.
SGDClassifier 클래스와 cross_validate 함수를 사용해서 교차 검증으로 성능을 확인해 보자.
max_iter의 크기를 5로 설정하여 반복 횟수를 5번으로 지정했다. 반복 횟수를 늘려도 성능이 크게 향상되지는 않는다.
이전에 생선의 특성들로 로지스틱 회귀를 사용했을 때의 공식은 아래와 같았다.
z = a * (Weight) + b * (Length) + c * (Diagonal) + d * (Height) + e * (Width) + f
이 식을 패션 MNIST 데이터에 맞게 변형하면 다음과 같다.
z_티셔츠 = w1 * (픽셀1) + w2 * (픽셀2) + w3 * (픽셀3) + ... + w784 * (픽셀 784) + b
z_바지 = w1' * (픽셀1) + w2' * (픽셀2) + w3' * (픽셀3) + ... + w784' * (픽셀 784) + b'
총 784개의 특성(픽셀)이 있기 때문에 아주 긴 식이 만들어진다.
티셔츠가 아니라 바지인 경우에도 식의 기본적인 틀은 변하지 않는다. 하지만 가중치와 절편이 모두 같다면 모델은 티셔츠와 바지를 구분할 수 없을 것이다. 그러므로 가중치와 절편은 다른 값을 사용해야 한다.
다시 말해, w1과 w1', b와 b'는 다른 값을 사용해야 한다는 것이다. 아무리 픽셀이 다르다고 해도 가중치와 절편의 값이 같으면 구분할 수 없다.
SGDClassifier 모델은 패션 MNIST 데이터의 클래스를 가능한 잘 구분할 수 있도록 10개의 방정식(각 타깃값에 따른 방정식)에 대한 모델 파라미터(가중치와 절편)를 찾는다.
z_티셔츠, z_바지와 같이 10개의 클래스에 대한 선형 방정식을 모두 계산한 다음에는 소프트맥스 함수를 통과하여 각 클래스에 대한 확률을 얻을 수 있다.
인공 신경망
우리는 이미 인공 신경망을 만들었다고 볼 수 있다. 가장 기본적인 인공 신경망은 확률적 경사 하강법을 사용하는 로지스틱 회귀와 같다. 그렇다면 어떻게 인공 신경망을 사용해서 성능을 높일 수 있을까?
위의 그림을 우리가 공부하고 있는 내용에 대입해 보자.
Output부터 생각해 보면, 위에서 우리가 계산했던 z_티셔츠, z_바지일 것이다. 패션 MNIST의 경우 클래스의 개수가 10개이기 때문에 Output은 10개일 것이다. z1~z10까지 계산하고 이를 바탕으로 클래스를 예측하기 때문에 신경망의 최종 값을 만든다는 의미에서 이를 출력층(Output Layer)라고 부른다.
인공 신경망에서는 z 값을 계산하는 단위를 뉴런(Neuron)이라고 한다. 하지만 뉴런에서 일어나는 일은 선형 계산(덧셈과 곱셈)이 전부이다. 이제는 뉴런이란 표현 대신에 유닛(unit)이라고 부르는 사람이 더 많아지고 있다고 한다.
Input에 들어가야 할 것은 각각의 픽셀값(이제 x1, x2,...,라고 하겠다)이다. 그러므로 총 784개의 Input값이 있을 것이다. 이를 입력층(Input Layer)라고 한다. 즉 입력층은 픽셀 값 자체이고 특별한 계산을 수행하지는 않는다.
z1을 만들기 위해 픽셀 1인 x1에 곱해지는 가중치는 w1,1이라고 쓰고 z2를 만들기 위해 픽셀 1인 x1에 곱해지는 가중치는 w1,2라고 쓴다. 절편은 뉴런마다 하나씩이므로 순서대로 b1~b10까지 있다.
딥러닝은 인공 신경망과 거의 동의어로 사용되는 경우가 많다. 혹은 심층 신경망(Deep Neural Network, DNN)을 딥러닝이라고 부른다.
그렇다면 확률적 경사 하강법을 사용한 로지스틱 회귀 모델이 가장 간단한 인공 신경망이라면 인공 신경망을 만들어도 성능이 그렇게 좋아지지는 않을 것 같다. 하지만 인공 신경망 모델을 만드는 최신 라이브러리들은 SGDClassifier에는 없는 몇 가지 기능을 제공한다. 이런 기능들 덕분에 더 좋은 성능을 얻을 수 있다. 가장 인기가 많은 라이브러리인 텐서 플로를 이용해서 인공 신경망 모델을 만들어 보자.
텐서 플로와 케라스
텐서 플로(tensor flow)는 구글이 2015년 11월 오픈소스로 공개한 딥러닝 라이브러리이다.
이 책에서는 텐서플로의 2.x 최신 버전을 사용하니 참고하자.
텐서 플로에는 저수준 API와 고수준 API가 있는데, 케라스(Keras)가 텐서플로의 고수준 API이다.
딥러닝 라이브러리가 다른 머신러닝 라이브러리와 다른 점 중 하나는 그래픽 처리 장치인 GPU를 사용하여 인공 신경망을 훈련한다는 것이다. GPU는 벡터와 행렬 연산에 매우 최적화되어 있기 때문에 곱셈과 덧셈이 많이 수행되는 인공 신경망에 큰 도움이 된다. 코랩에서 무료로 GPU를 사용할 수 있다.
케라스 라이브러리는 직접 GPU 연산을 수행하지 않는다. 대신 GPU 연산을 수행하는 다른 라이브러리를 백엔드(Backend)로 사용한다. 예를 들면 텐서 플로가 케라스의 백엔드 중 하나이다. 이외에도 씨아노(Theano), CNTK와 같은 여러 딥러닝 라이브러리를 케라스 백엔드로 사용할 수 있다. 이런 케라스를 멀티-백엔드 케라스라고 한다. 케라스 API만 익히면 다양한 딥러닝 라이브러리를 골라서 사용할 수 있다. 이를 위해 케라스는 직관적이고 사용하기 편한 고수준 API를 제공한다.
이전에는 이렇게 멀티-백엔드 케라스를 지원했지만 2.3.1 버전 이후로 더 이상 개발되지 않는다. 사실상 케라스와 텐서플로가 거의 동의어가 된 셈이다. 이제 한번 사용해 보자.
인공 신경망으로 모델 만들기
로지스틱 회귀에서는 교차 검증을 사용해 모델을 평가했지만, 인공 신경망에서는 교차 검증을 잘 사용하지 않고 검증 세트를 별도로 덜어내어 사용한다.
이렇게 하는 이유는 딥러닝 분야의 데이터셋은 충분히 크기 때문에 검증 점수가 안정적이고, 교차 검증을 수행하기에는 훈련 시간이 너무 오래 걸리기 때문이다. 어떤 딥러닝 모델은 훈련하는 데 몇 시간, 심지어 며칠이 걸릴 수도 있다.
패션 MNIST 데이터셋이 그만큼 크지는 않지만, 관례를 따라 검증 세트를 나누어 보자. 사이킷런의 train_test_split() 함수를 사용하면 된다.
훈련 세트에서 20%를 검증 세트로 만들었다. 훈련 세트와 검증 세트의 크기를 확인해 보자.
20%만큼 만들어진 것을 확인할 수 있다.
훈련 세트로 모델을 만들고, 검증 세트를 이용해서 모델을 평가해보자.
케라스의 레이어(keras.layer) 패키지 안에는 다양한 층이 준비되어 있다. 가장 기본이 되는 층은 밀집층(Dense Layer)이다. 왜 밀집이라고 부르는 지는 위의 그림을 보면 감이 올 것이다. 그림에서는 3개의 입력층과 2개의 출력 층만 있어서 그렇지만, 우리가 할 것은 784개의 입력층과 10개의 출력층이 있기 때문에 매우 빽빽하게 연결되어 있을 것이다.
이런 층을 양쪽의 뉴런이 모두 연결하고 있기 때문에 완전 연결층(Fully Connected Layer)이라고 부른다.
그럼 케라스의 Dense 클래스를 사용해 밀집층을 만들어 보자. 필요한 매개변수는 뉴런의 개수, 뉴런의 출력에 적용할 함수, 입력의 크기이다.
10개의 패션 아이템을 분류하기 때문에 뉴런의 개수를 10으로, 10개의 뉴런에서 출력되는 값을 확률로 바꾸기 위해서는 소프트맥스 함수를 사용하기 때문에 activation= 에는 softmax함수를 인자로 넘겨준다. 다중 분류에서는 softmax함수를 넘겨주지만 이진 분류라면 시그모이드 함수를 사용하기 위해 activation='sigmoid' 와 같이 설정한다.
784개의 픽셀을 넘겨줘야하기 때문에 튜플로 만들어서 넘겨준다.
케라스의 Sequential 클래스를 사용하자.
객체를 만들 때 앞에서 만든 밀집층 객체 dense를 전달한다. 여기서 만든 model 객체가 신경만 모델이다.
위의 그림에서 z를 구하면 그 z를 소프트맥스 함수를 통과시켜 최종 결괏값을 얻는다.
소프트맥스와 같이 뉴런의 선형 방정식 계산 결과에 적용되는 함수를 활성화 함수(Activation Function)라고 불린다. 소프트맥스 함수 외에도 다양한 활성화 함수가 있다.
인공 신경망으로 패션 아이템 분류하기
지금까지 사용했던 사이킷런에 비해 케라스에서 모델을 만드는 방식은 조금 다르다.
케라스 모델은 훈련하기 전에 설정 단계가 있다. 이런 설정을 model 객체의 compile() 메서드에서 수행한다. 여기서 꼭 지정해야 할 것은 손실 함수의 종류이다. 그다음 훈련 과정에서 계산하고 싶은 측정값을 지정한다.
이진 분류와 다중 분류에서 사용하는 손실 함수의 종류가 다르다.
- 이진 분류 : loss = 'binary_crossentropy'
- 다중 분류 : loss = 'categorical_crossentropy'
- 회귀 모델 : loss = 'mean_square_error'
이진 분류에서는 출력층의 뉴런이 하나이다. 이 뉴런이 출력하는 확률 값 a(시그모이드 함수의 출력 값)를 사용해 양성 클래스와 음성 클래스에 대한 크로스 엔트로피를 계산한다.
이진 분류의 출력 뉴런은 오직 양성 클래스에 대한 확률(a)만 출력하기 때문에 음성 클래스에 대한 확률은 1에서 a를 빼준 1-a로 구할 수 있다(양성일 확률이 80퍼센트라면, 음성일 확률이 20퍼센트 인 것과 같은 원리이다). 원래는 타깃 값을 곱해줘야 하지만 타깃 값인 0을 곱하면 어떤 계산이든지 0이 되기 때문에 음성 샘플인 경우 1로 바꾸어서 1-타깃값을 계산한다. 그러므로 이진 분류라고 해서 두 개의 뉴런이 있는 것이 아니라 한 개의 뉴런만으로도 두 클래스에 대한 크로스 엔트로피 손실을 모두 계산할 수 있는 것이다.
그렇다면, 패션 MNIST 데이터셋과 같이 다중 분류일 경우 어떻게 계산할까?
출력층은 10개의 뉴런이 있고 10개의 클래스에 대한 확률(a1, a2, ... )를 출력한다. 이진 분류와 달리 각 클래스에 대한 확률이 출력되기 때문에 타깃에 해당하는 확률만 남겨 놓기 위해서 나머지 확률에는 모두 0을 곱한다.
예를 들어 샘플이 티셔츠일 경우 첫 번째 뉴런의 활성화 함수 출력인 a1에 크로스 엔트로피 손실 함수를 적용하고 나머지 활성화 함수 출력 a2~a10까지는 모두 0으로 만든다. 즉 배열[1,0,0,0,0,0,0,0,0,0]을 만들어서 a1~a10과 곱해주면 된다.
넘파이 브로드캐스팅이 여기에도 적용된다. 그러므로 [a1,a2,...a10] * [1,0,..0]를 하면 각 원소마다 곱해진다. 결국 다른 원소는 모두 0이 되고 a1만 남는다.
결국 신경망은 티셔츠 샘플에서 손실을 낮추려면 첫 번째 뉴런의 활성화 출력 a1의 값을 가능한 1에 가깝게 만들어야 한다. 바로 이것이 크로스 엔트로피 손실 함수가 신경망에 원하는 바이다.
이렇게 타깃값을 해당 클래스만 1이고 나머지는 모두 0인 배열로 만드는 것을 원-핫 인코딩(One-Hot Encoding)이라고 한다.
따라서 다중 분류에서 크로스 엔트로피 손실 함수를 사용하려면 0,1,2와 같이 정수로 된 타깃 값을 원-핫 인코딩으로 변환해야 한다.
패션 MNIST 데이터의 타깃값은 모두 0~9 사이의 정수로 되어 있다. 하지만 텐서 플로에서는 정수로 된 타깃 값을 원-핫 인코딩으로 바꾸지 않아도 된다. 그 계산을 해주는 것이 sparse_categorical_crossentropy 이다. 빽빽한 배열 말고 정숫값 하나만 사용한다는 뜻에서 sparse(희소)라는 이름을 붙인 것 같다. 굳이 이 함수를 사용하지 않고 원-핫 인코딩을 준비해서 loss='categorical_crossentropy'를 사용해도 된다.
두 번째 매개변수인 metrics에 대해 알아보자.
케라스는 모델이 훈련할 때 기본으로 에포크마다 손실 값을 출력해 준다. 손실이 줄어드는 것을 보고 훈련이 잘 되었다는 것을 알 수 있지만 정확도를 함께 출력하기 위해 정확도 지표를 의미하는 accuracy를 지정한다.
모델을 훈련하는 방법인 fit() 메서드는 사이킷런과 매우 비슷하다. 처음 두 매개변수에 입력과 타깃을 지정하고 반복할 에포크 횟수를 epochs 매개변수로 지정한다. 사이킷런의 로지스틱 모델과 동일하게 5번 반복해 보자.
케라스는 에포크마다 걸린 시간과 손실(loss), 정확도(accuracy)를 출력해 준다. 5번 반복해서 정확도가 85%가 넘은 것을 볼 수 있다. 앞에서 떼어 놓은 검증 세트에서 모델의 성능을 확인해 보자. 모델의 성능을 평가하는 메서드는 evaluate() 메서드이다.
검증 세트의 점수는 훈련 세트의 점수보다 조금 낮은 것이 일반적이다.