본문 바로가기

이론/모두의 딥러닝

< 다섯째 마당 > 딥러닝 활용하기

16장 이미지 인식의 꽃, 컨볼루션 신경망(CNN)

- MNIST 데이터셋: 고등학생과 인구조사국 직원 등이 쓴 손글씨를 이용해 만든 데이터로 구성되어 있습니다. 7만 개의 글자 이미지에 각각 0부터 9까지 이름표를 붙인 데이터셋이다.

 

이미지를 인식하는 원리

 MNIST 데이터는 텐서플로의 케라스 API를 이용해 간단히 불러올 수 있습니다.

  • X = 이미지 데이터
  • Y = 이미지에 0 ~ 9를 붙인 이름표
  • 학습에 사용될 부분: X_train, y_train
  • 테스트에 사용될 부분: X_test, y_test

케라스의 MNIST 데이터는 총 7만 개 이미지 중 6만 개를 학습용으로 1만 개를 테스트용으로 미리 구분.
cmap='Gray' 옵션을 지정해 흑백으로 출력

 이 이미지는 가로 28 x 세로 28 = 총 784개의 픽셀로 이루어져 있습니다. 각 픽셀은 밝기 정도에 따라 0부터 255까지 등급을 매깁니다. 흰색 배경이 0이라면 글씨가 들어간 곳은 1~255의 숫자 중 하나로 채워져 긴 행렬로 이루어진 하나의 집합으로 변환됩니다.

 

 다음 코드로 확인할 수 있습니다.

이제 이것을 속성을 담은 데이터를 딥러닝에 집어넣고 클래스를 예측하는 문제로 전환시킵니다. 28 x 28 = 784개의 속성을 이용해 0 ~ 9의 클래스 열 개 중 하나를 맞히는 문제가 됩니다. 이제 주어직 가로 28, 세로 28의 2차원 배열을 784개의 1차원 배열로 바꾸어 주어야 합니다. reshape(총 샘플 수, 1차원 속성의 개수) 형식으로 지정합니다. 총 샘플 수는 앞서 사용한 X_train.shape[0]을 이용하고, 1차원 속성의 개수는 이미 살펴본 대로 784개입니다.

`

 케라스는 데이터를 0 ~ 1사이의 값으로 변환한 후 구동할 때 최적의 성능을 보입니다. 따라서 현재 0 ~ 255 사이의 값으로 이루어진 값을 255로 나누어 0 ~ 1사이의 값으로 바꾸어야 합니다. 이를 정규화라고 합니다.

 정규화를 위해 값들을 실수형으로 바꾸어야 합니다.

 X_test에도 마찬가지로 이 작업을 적용합니다.

 딥러닝의 분류 문제를 해결하려면 원-핫 인코딩 방식을 적용해야합니다. 즉, 0 ~ 9의 정수형 값을 갖는 현재 형태에서 0 또는 1로만 이루어진 벡터로 값을 수정해야 합니다.

 

 np_utils.to_categorical() 함수를 이용해 [5]를 [0,0,0,0,0,1,0,0,0,0]으로 바꾸는 것과 같은 과정을 진행할 것입니다. to_categorical(클래스, 클래스의 개수) 형식으로 지정합니다.

실습| MNIST 손글씨 인식하기: 데이터 전처리

from tensorflow.keras.datasets import mnist
from tensorflow.keras.utils import to_categorical

import matplotlib.pyplot as plt
import sys

# MNIST 데이터셋을 불러와 학습셋과 테스트셋으로 저장합니다. 
(X_train, y_train), (X_test, y_test) = mnist.load_data()

# 학습셋과 테스트셋이 각각 몇 개의 이미지로 되어 있는지 확인합니다. 
print("학습셋 이미지 수 : %d 개" % (X_train.shape[0]))
print("테스트셋 이미지 수 : %d 개" % (X_test.shape[0]))
# 첫 번째 이미지를 확인해 봅시다.
plt.imshow(X_train[0], cmap='Greys')
plt.show()
# 이미지가 인식되는 원리를 알아봅시다.
for x in X_train[0]:
    for i in x:
        sys.stdout.write("%-3s" % i)
    sys.stdout.write('\n')
# 차원 변환 과정을 실습해 봅니다.
X_train = X_train.reshape(X_train.shape[0], 784)
X_train = X_train.astype('float64')
X_train = X_train / 255

X_test = X_test.reshape(X_test.shape[0], 784).astype('float64') / 255

# 클래스 값을 확인해 봅니다.
print("class : %d " % (y_train[0]))

# 바이너리화 과정을 실습해 봅니다.
y_train = to_categorical(y_train, 10)
y_test = to_categorical(y_test, 10)

print(y_train[0])

 

딥러닝 기본 프레임 만들기

총 784개의 속성이 있고 열 개의 클래스가 있으므로 다음과 같이 딥러닝 프레임을 만들 수 있다.

 오차 함수로 categorical_crossentropy, 최적화 함수로 adam을 사용하겠습니다.

 모델 실행에 앞서 먼저 성과를 저장하고, 모델의 최적화 단계에서는 학습을 자동 중단하게끔 설정하겠습니다. 열 번 이상 모델 성능이 향상되지 않으면 자동으로 학습을 중단합니다.

 샘플 200개를 모두 30번 실행하게끔 설정합니다. 테스트셋으로 최종 모델의 성과를 측정해 그 값을 출력합니다.

 실행 결과를 그래프로 표현해 보겠습니다. 학습셋의 오차( 1 - 학습셋의 정확도 )를 그래프로 표현하겠습니다. 학습셋의 오차와 테스트셋의 오차를 그래프 하나로 나타내겠습니다.

 

실습| MNIST 손글씨 인식하기: 기본 프레임

from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense
from tensorflow.keras.callbacks import ModelCheckpoint,EarlyStopping
from tensorflow.keras.datasets import mnist
from tensorflow.keras.utils import to_categorical

import matplotlib.pyplot as plt
import numpy as np
import os

# MNIST 데이터를 불러옵니다. 
(X_train, y_train), (X_test, y_test) = mnist.load_data()

# 차원 변환 후, 테스트셋과 학습셋으로 나누어 줍니다.
X_train = X_train.reshape(X_train.shape[0], 784).astype('float32') / 255
X_test = X_test.reshape(X_test.shape[0], 784).astype('float32') / 255

y_train = to_categorical(y_train, 10)
y_test = to_categorical(y_test, 10)

# 모델 구조를 설정합니다.
model = Sequential()
model.add(Dense(512, input_dim=784, activation='relu'))
model.add(Dense(10, activation='softmax'))
model.summary()
# 모델 실행 환경을 설정합니다.
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])

# 모델 최적화를 위한 설정 구간입니다.
modelpath="./MNIST_MLP.hdf5"
checkpointer = ModelCheckpoint(filepath=modelpath, monitor='val_loss', verbose=1, save_best_only=True)
early_stopping_callback = EarlyStopping(monitor='val_loss', patience=10)

# 모델을 실행합니다.
history = model.fit(X_train, y_train, validation_split=0.25, epochs=30, batch_size=200, verbose=0, callbacks=[early_stopping_callback,checkpointer])

# 테스트 정확도를 출력합니다.
print("\n Test Accuracy: %.4f" % (model.evaluate(X_test, y_test)[1]))
# 검증셋과 학습셋의 오차를 저장합니다. 
y_vloss = history.history['val_loss']
y_loss = history.history['loss']

# 그래프로 표현해 봅니다.
x_len = np.arange(len(y_loss))
plt.plot(x_len, y_vloss, marker='.', c="red", label='Testset_loss')
plt.plot(x_len, y_loss, marker='.', c="blue", label='Trainset_loss')

# 그래프에 그리드를 주고 레이블을 표시해 보겠습니다.
plt.legend(loc='upper right')
plt.grid()
plt.xlabel('epoch')
plt.ylabel('loss')
plt.show()
 
앞선 하나의 은닉층을 둔 아주 단순한 모델을 도식화해서 표현하면 아래와 같습니다.
 

 컨볼루션 신경망(CNN)

: 입력된 이미지에서 다시 한 번 특징을 추출하기 위해 커널(슬라이딩 윈도)을 도입하는 기법입니다.

 

예시

 입력된 이미지가 다음과 같은 값을 가지고 있다고 하자.

 여기에 2 x 2 커널을 준비합니다. 각 칸에는 가중치가 들어 있습니다.

 커널을 맨 왼쪽 윗칸에 적용해보겠습니다.

 적용된 부분은 원래 있던 값에 가중치의 값을 곱합니다. 그 결과를 합하면 새로 추출된 값은 2가 됩니다.

( 1 x 1 ) + ( 0 x 0 ) + ( 0 x 0 ) + ( 1 x 1 ) = 2

 이 커널을 한 칸씩 옮겨 모두 적용해 보자.

 정리하면 다음과 같다.

컨볼루션(합성곱) 층

 컨볼루션 층을 만들면 입력 데이터가 가진 특징을 대략적으로 추출해서 학습을 진행할 수 있습니다. 이런 커널을 여러 개 만들 경우 여러 개의 컨볼루션 층이 만들어집니다.

 케라스에서 컨볼루션 층을 추가하는 함수는 Conv2D() 입니다.

  1. 첫 번째 인자: 커널을 몇 개 적용할지 정합니다. 여기서는 32개의 커널을 적용했습니다.

  2. kernel_size: 커널의 크기를 정합니다. kernel_size = (행, 열) 형식으로 정하며, 여기서는 3 x 3 크기의 커널을 사용하게        끔 정했습니다.

  3. input_shape: Dense 층과 마찬가지로 맨 처음 층에는 입력되는 값을 알려 주어야 합니다. input_shape = (행, 열, 색상        또는 흑백) 형식으로 정합니다. 만약 입력 이미지가 색상이면 3, 흑백이면 1을 지정합니다. 여기서는 28 x 28 크기의 흑        백 이미지를 사용하도록 정했습니다.

  4. activation: 사용할 활성화 함수를 정의합니다.

 

 이어서 컨볼루션 층을 하나 더 추가해 보겠습니다.

 

맥스 풀링, 드롭아웃, 플래튼

: 컨볼루션 층을 통해 이미지 특징을 도출해도 그 결과가 여전히 크고 복잡하면 이를 다시 한 번 축소해야 한다. 이 과정을 풀링(pooling) 또는 서브 샘플링(sub sampling)이라고 합니다.

: 맥스 풀링(max pooling): 정해진 구역 안에서 최댓값을 뽑아냄.

: 평균 풀링(average pooling): 평균 값을 뽑아냄

 

맥스 풀링의 예

 다음과 같은 이미지가 있다.

 맥스 풀링을 적용하면 다음과 같이 구역을 나눕니다.

 그리고 각 구역에서 가장 큰 값을 추출합니다.

 이 과정을 거쳐 불필요한 정보를 간추립니다. 맥스 풀링은 MaxPooling2D() 함수를 사용해서 다음과 같이 적용할 수 있습니다.

 pool_size를 통해 풀링 창의 크기를 정합니다.

 

드롭아웃, 플래튼

: 과적합을 효과적으로 피하는 과정을 도와주는 기법중 가장 효과가 큰 것이 드롭아웃(drop out) 기법입니다. 드롭아웃은 은닉층에 배치된 노드 중 일부를 임의로 꺼 주는 것입니다.

랜덤하게 노드를 꺼 주면 과적합을 방지할 수 있습니다. 케라스를 이용해 25%의 노드를 꺼봅시다.

이런것들을 앞에서 Dense() 함수를 이용해 만들었던 기본 층에 연결해보자. 이때 주의할 점은 컨볼루션 층이나 맥스 풀링은 주어진 이미지를 2차원 배열인 채로 다루기 때문에 이를 1차원 배열로 바꾸어 주어야 활성화 함수가 있는 층에서 사용할 수 있습니다. Flatten() 함수를 사용해 2차원 배열을 1차원으로 바꾸어 줍니다.

 

컨볼루션 신경망 실행하기

from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Dropout, Flatten, Conv2D, MaxPooling2D
from tensorflow.keras.callbacks import ModelCheckpoint,EarlyStopping
from tensorflow.keras.datasets import mnist
from tensorflow.keras.utils import to_categorical

import matplotlib.pyplot as plt
import numpy as np

# 데이터를 불러옵니다.
(X_train, y_train), (X_test, y_test) = mnist.load_data()
X_train = X_train.reshape(X_train.shape[0], 28, 28, 1).astype('float32') / 255
X_test = X_test.reshape(X_test.shape[0], 28, 28, 1).astype('float32') / 255
y_train = to_categorical(y_train)
y_test = to_categorical(y_test)

# 컨볼루션 신경망의 설정
model = Sequential()
model.add(Conv2D(32, kernel_size=(3, 3), input_shape=(28, 28, 1), activation='relu'))
model.add(Conv2D(64, (3, 3), activation='relu'))
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Dropout(0.25))
model.add(Flatten())
model.add(Dense(128,  activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(10, activation='softmax'))

# 모델의 실행 옵션을 설정합니다.
model.compile(loss='categorical_crossentropy',
              optimizer='adam',
              metrics=['accuracy'])

# 모델 최적화를 위한 설정 구간입니다.
modelpath="./MNIST_CNN.hdf5"
checkpointer = ModelCheckpoint(filepath=modelpath, monitor='val_loss', verbose=1, save_best_only=True)
early_stopping_callback = EarlyStopping(monitor='val_loss', patience=10)

# 모델을 실행합니다.
history = model.fit(X_train, y_train, validation_split=0.25, epochs=30, batch_size=200, verbose=0, callbacks=[early_stopping_callback,checkpointer])

# 테스트 정확도를 출력합니다.
print("\n Test Accuracy: %.4f" % (model.evaluate(X_test, y_test)[1]))
# 검증셋과 학습셋의 오차를 저장합니다.
y_vloss = history.history['val_loss']
y_loss = history.history['loss']

# 그래프로 표현해 봅니다.
x_len = np.arange(len(y_loss))
plt.plot(x_len, y_vloss, marker='.', c="red", label='Testset_loss')
plt.plot(x_len, y_loss, marker='.', c="blue", label='Trainset_loss')

# 그래프에 그리드를 주고 레이블을 표시하겠습니다.
plt.legend(loc='upper right')
plt.grid()
plt.xlabel('epoch')
plt.ylabel('loss')
plt.show()
 
 

딥러닝을 이용한 자연어 처리

: 자연어 처리는 사람이 말하는 음성이나 텍스트를 컴퓨터가 인식하고 처리하는 것을 말합니다.

텍스트의 토큰화

: 입력할 텍스트가 준비되면 이를 단어별, 문장별, 형태소별로 나눌 수 있는데 이렇게 작게 나누어진 하나의 단위를 토큰이라고 합니다. 그리고 이 과정을 토큰화하고 합니다.

 케라스가 제공하는 text 모듈의 text_to_word_sequence() 함수를 사용해보자.

 결과는 다음과 같습니다.

 Bag-of-Words 라는 방법은 같은 단어끼리 따로따로 가방에 담은 후 각 가방에 몇 개의 단어가 들어있는지 세는 기법입니다. 단어의 빈도수를 알면 텍스트에서 중요한 역할을 하는 단어를 파악할 수 있습니다.

 케라스의 Tokenizer() 함수를 사용하면 단어의 빈도수를 쉽게 계산할 수 있습니다.

 

 텍스트 전처리 함수 중 Tokenizer() 함수를 불러옵니다.

 전처리하려는 세 개의 문장을 docs라는 배열에 지정합니다.

 토큰화 함수인 Tokenizer()를 이용해 전처리하는 과정을 다음과 같습니다.

  word_counts는 단어의 빈도수를 계산해 주는 함수입니다. 출력 결과는 다음과 같습니다.

 순서를 기억하는 OrderedDict 클래스에 담겨 있는 형태로 출력되어 있습니다. document_count() 함수를 이용하면 총 몇 개의 문장이 들어 있는지도 셀 수 있습니다.

 또한, word_docs() 함수를 통해 각 단어들이 몇 개의 문장에 나오는지 세어서 출력할 수도 있습니다. 순서는 랜덤입니다.

 각 단어에 매겨진 인덱스 값을 출력하려면 word_index() 함수를 사용하면 됩니다.

 

단어의 원-핫 인코딩

: 단어가 문장의 다른 요소와 어떤 관계를 가지고 있는지 알아보는 방법입니다.

 

예시

다음과 같은 문장이 있다.

 각 단어를 모두 0으로 바꾸어 주고 원하는 단어만 1로 바꾸어 주는 것이 원-핫 인코딩이다. 이를 수행하기 위해 먼저 단어 수만큼 0으로 채워진 벡터 공간으로 바꾸자.

 각 단어가 배열 내에서 해당하는 위치를 1로 바꾸어서 벡터화할 수 있습니다.

 이러한 과정을 케라스로 실습해보자. 먼저 토큰화 함수를 불러와 단어 단위로 토큰화하고 각 단어의 인덱스 값을 출력해보자.

 texts_to_sequences() 함수를 사용해서 토큰의 인덱스로만 채워진 새로운 배열을 만들어주자.

 1 ~ 6의 정수로 인덱스되어 있는 것을 0과 1로만 이루어진 배열로 바꾸어 주는 to_categorical() 함수를 사용해 원-핫 인코딩 과정을 진행하자. 배열 맨 앞에 0이 추가되므로 단어 수보다 1이 더 많게 인덱스 숫자를 잡아 주는 것에 유의하자.

 

단어 임베딩

: 단어 임베딩은 주어진 배열을 정해진 길이로 압축시킵니다.

단어 임베딩은 각 단어 간의 유사도를 계산함.

 단어 간 유사도는 오차 역전파를 이용한 최적의 유사도를 계산하는 학습 과정을 거친다. Embedding() 함수를 사용해 간단히 할 수 있다. Embedding() 함수를 적용해 딥러닝 모델을 만들 수 있다.

                      - Embedding(16, 4)는 입력될 총 단어 수는 16, 임베딩 후 출력되는 벡터 크기는 4로 하겠다는

                        의미 입니다.

                      - Embedding(16, 4, input_length=2) 라고 하면 총 입력되는 단어 수는 16개이지만 매번 두 개씩

                        만 넣겠다는 의미입니다.

 

텍스트를 읽고 긍정, 부정 예측하기

 영화 리뷰를 딥러닝 모델로 학습해서 각 리뷰가 긍정적인지 부정적인지를 예측하는 것입니다. 먼저 짧은 리뷰 열 개를 불러와 긍정이면 1이라는 클래스를, 부정적이라면 0이라는 클래스로 지정합니다.

토큰화 과정을 진행합니다.

 토큰에 지정된 인덱스로 새로운 배열을 생성합니다.

 리뷰 데이터마다 토큰 수가 다릅니다. 딥러닝 모델에 입력하려면 학습 데이터의 길이가 동일해야 합니다. 이처럼 길이를 똑같이 맞추어 주는 작업을 패딩(padding) 과정이라고 합니다. 케라스의 pad_sequences() 함수를 사용하면 원하는 길이보다 짧은 부분은 숫자 0을 넣어서 채워주고, 긴 데이터는 잘라서 같은 길이로 맞춰줍니다.

 

실습| 영화 리뷰가 긍정적인지 부정적인지를 예측하기

from tensorflow.keras.preprocessing.text import Tokenizer
from tensorflow.keras.preprocessing.sequence import pad_sequences
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense,Flatten,Embedding
from tensorflow.keras.utils import to_categorical
from numpy import array
# 텍스트 리뷰 자료를 지정합니다.
docs = ["너무 재밌네요","최고예요","참 잘 만든 영화예요","추천하고 싶은 영화입니다","한번 더 보고싶네요","글쎄요","별로예요","생각보다 지루하네요","연기가 어색해요","재미없어요"]

# 긍정 리뷰는 1, 부정 리뷰는 0으로 클래스를 지정합니다.
classes = array([1,1,1,1,1,0,0,0,0,0])

# 토큰화 
token = Tokenizer()
token.fit_on_texts(docs)
print(token.word_index)
x = token.texts_to_sequences(docs)
print("\n리뷰 텍스트, 토큰화 결과:\n",  x)
# 패딩, 서로 다른 길이의 데이터를 4로 맞추어 줍니다.
padded_x = pad_sequences(x, 4)  
print("\n패딩 결과:\n", padded_x)
# 임베딩에 입력될 단어의 수를 지정합니다.
word_size = len(token.word_index) +1

# 단어 임베딩을 포함하여 딥러닝 모델을 만들고 결과를 출력합니다.
model = Sequential()
model.add(Embedding(word_size, 8, input_length=4))
model.add(Flatten())
model.add(Dense(1, activation='sigmoid'))
model.summary()
model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])
model.fit(padded_x, classes, epochs=20)
print("\n Accuracy: %.4f" % (model.evaluate(padded_x, classes)[1]))