이미지를 분류하는 간단한 인공지능을 만들어 볼 예정이다. 이 인공지능은 손으로 쓴 숫자를 인식할 수 있고, 가위바위보 게임을 만들 수도 있다. 숫자는 0~9까지 총 10개의 class만 인식하면 되고, 가위바위보는 총 3개의 class만 구분하면 된다. 이와 같이 클래스가 몇개 안되는 경우, 인공지능은 간단하게 이미지를 분류해 낼 수 있다.
일반적으로 딥러닝 기술은 "데이터 준비→딥러닝 네트워크 설계→학습→테스트(평가)"의 순서대로 만들게 된다
숫자 손글씨 분류기는 숫자 이미지를 입력으로 받으면, 그 이미지가 어떤 숫자를 나타내는지 출력해 낼 수 있다.
텐서플로우(TensorFlow)의 표준 API인 tf.keras
의 Sequential API를 이용하여 숫자 손글씨 인식기를 만든다.
TensorFlow는 구글에서 오픈소스로 제공하는 머신러닝 라이브러리로 많이 사용되고 있다.
실습환경은 Tensorflow --version 2.4.1
예제 코드
처음부터 완벽하게 이해하긴 어려우니 조금씩 설명
import tensorflow as tf
from tensorflow import keras
import numpy as np
import matplotlib.pyplot as plt
import os
print(tf.__version__) # Tensorflow의 버전을 출력
mnist = keras.datasets.mnist
# MNIST 데이터를 로드. 다운로드하지 않았다면 다운로드까지 자동으로 진행됩니다.
(x_train, y_train), (x_test, y_test) = mnist.load_data()
print(len(x_train)) # x_train 배열의 크기를 출력
위 코드를 실행하면 숫자 손글씨 데이터베이스인 MNIST 데이터 셋을 읽을 수 있다.
MNIST 데이터 셋은 Yann Lecun 교수가 공개한 데이터이다.
참고 MNIST Dataset
불러들인 숫자 손글씨 이미지 하나를 출력해본다.
MNIST 데이터셋의 X항목(x_train, x_test)은 이미지 데이터를 담은 행렬이다
plt.imshow(x_train[1], cmap=plt.cm.binary)
plt.show()
print(y_train[1])
숫자 0이미지가 나왔다. 주의, x_train[1]
에 담긴 이미지는 x_train
행렬의 1번째가 아니라 2번째 이미지이다. 1번째 이미지는 x_train[0]
에 담겨 있다.
Y항목(y_train, y_test)에는 X항목의 이미지들에 대응하는 실제 숫자 값이 담겨있다.
다른 이미지 출력하기
# index에 0에서 59999 사이 숫자를 지정해 보세요.
index=10000
plt.imshow(x_train[index],cmap=plt.cm.binary)
plt.show()
print( (index+1), '번째 이미지의 숫자는 바로 ', y_train[index], '입니다.')
Matplotlib 란?
파이썬에서 제공하는 시각화(Visualization) 패키지인 Matplotlib는 차트(chart), 플롯(plot) 등 다양한 형태로 데이터를 시각화할 수 있는 강력한 기능을 제공한다.
Matplotlib활용사례
mnist.load()
함수를 통해 학습용 데이터 (x_train, y_train)
와 시험용 데이터(x_test, y_test)
를 나누어 받아들이는 것을 볼 수 있다.
앞으로 만들 손글씨 분류기는 학습용 데이터 (x_train, y_train)
만을 가지고 학습시킨다. 학습이 끝난 후에는 이 손글씨 분류기가 얼마나 좋은 성능을 보이는지 확인하고 싶을 때, 시험용 데이터 (x_test, y_test)
로 테스트 할 수 있다.
MNIST 데이터셋은 약 500명의 사용자가 작성한 숫자 이미지를 가지고 있다. 그 중 250명의 데이터가 학습용 데이터, 다른 250명의 데이터가 시험용 데이터로 이용된다.
학습용 데이터와 시험용 데이터가 각각 몇 장인지 확인해보자
print(x_train.shape)
print(x_test.shape)
학습용 데이터는 (60000,28,28) 이라는 값을 볼 수 있다. 이것은 28X28 크기의 숫자 이미지가 60,000장 있다는 뜻이다.
마찬가지로 시험용 데이턴는 28X28 크기의 숫자 이미지가 10,000장 있다.
학습용 데이터, 검증용 데이터, 시험용 데이터의 의미와 그 차이점을 더 자세히 알아보기
숫자 손글씨 이미지의 실제 픽셀 값은 0~255 사이의 값을 가진다
print('최소값 : ', np.min(x_train), '최대값 " ', np.max(x_train))
결과값을 보면 0~255 사이의 값이 맞는걸 알 수 있다.
인공지능 모델을 훈련시키고 사용핼 때는, 일반적으로 입력은 0~1사이의 값으로 정규화 시켜주는 것이 좋다. MNIST 데이터는 각 픽셀의 값이 0~255 사이 범위에 있으므로 데이터들을 255.0으로 나눠주면 된다. 최솟값이 0, 최댓값이 1에 근접하는지 확인해보자
x_train_norm, x_test_norm = x_train / 255.0, x_test / 255.0
print('최소값: ', np.min(x_train_norm), '최대값 : ', np.max(x_train_norm))
최솟값이 0, 최댓값이 1에 근접하는 걸 확인
데이터가 모두 준비 되었다면, 딥러닝 네트워크를 만들어야 한다.
Tensorflow keras에서 Sequential API라는 방법을 사용한다.
Sequential API는 개발의 자유도는 떨어지지만, 간단하게 딥러닝 모델을 만들어낼 수 있는 방법이다. 이 방법을 통해 미리 정의된 딥러닝 레이어(layer)를 손쉽게 추가할 수 있다.
keras에서 모델을 만드는 방법은 Sequential API 외에도 Function API를 이용하는 방법, 처음부터 직접 코딩하는 방법 등 여러가지 방법이 있다.
tf.keras의 Sequential API를 이용하여 LeNet이라는 딥러닝 네트워크를 설계한 예
간단하지만 손글씨 숫자 분류기를 구현하는 데는 충분하다
model=keras.models.Sequential()
model.add(keras.layers.Conv2D(16, (3,3), activation='relu', input_shape=(28,28,1)))
model.add(keras.layers.MaxPool2D(2,2))
model.add(keras.layers.Conv2D(32, (3,3), activation='relu'))
model.add(keras.layers.MaxPooling2D((2,2))
model.add(keras.layers.Flatten())
model.add(keras.layers.Dense(32, activation='relu'))
model.add(keras.layers.Dense(10, activation='softmax'))
print('Model에 추가된 Layer 개수 : ', len(model.layers))
코드 의미 설명
만든 딥러닝 네트워크 모델을 확인해 보려면, model.summary()
메소드 이용
model.summary()
지금까지 만든 네트워크 입력은 (데이터 개수, 이미지 크기 x, 이미지 크기 y, 채널 수)
와 같은 형태를 가진다.
모델링 코드에서 input_shape = (28,28,1)로 지정했었다.
하지만 x_train.shape
를 확인해 보면 (60000,28,28)
로 채널 수에 대한 정보가 없다. 따라서 (60000,28,28,1)
로 만들어 주어야 한다.(채널 수 1 = 흑백, 컬러라면 RGB 때문에 3)
print("Before Reshape - x_train_norm shape : {}".format(x_train_norm.shape))
print("Before Reshape - x_test_norm shape : {}".format(x_test_norm.shape))
x_train_reshaped = x_train_norm.reshape( -1, 28, 28, 1)
x_test_reshaped = x_test_norm.reshape( -1, 28, 28, 1)
# 데이터 개수에 -1을 쓰면 reshape시 자동 계산이 된다.
print("After Reshape - x_train_reshaped shape : {}".format(x_train_reshaped.shape))
print("After Reshape - x_test_reshaped shape : {}".format(x_test_reshaped.shape))
이제 x_train
학습 데이터로 딥러닝 네트워크를 학습시켜 본다.
epochs=10은 전체 60,000개 데이터를 10번 반복 사용해서 학습시키라는 뜻이다.
model.compile(optimizer='adam',
loss='sparse_categorical_crossentropy',
metrics=['accuracy'])
model.fit(x_train_reshaped, y_train, epochs=10)
각 학습이 진행됨에 따라 epoch 별로 어느 정도 인식 정확도(accuracy)가 올라가는지 확인할 수 있다.
인식정확도가 0.9423에서 0.9955까지 높게 올라갔다 (학습할 때마다 인식 정확도는 달라짐)
위의 인식 정확도는 학습용 데이터(x_train)을 가지고 구한 것이다.
수학 연습문제를 푼 것과 같다. 그럼 본 문제를 잘 푸는지 확인해보자
시험용 데이터(x_test)를 사용한다.
test_loss, test_accuracy = model.evaluate(x_test_reshaped, y_test, verbose=2)
print("test_loss : {}".format(test_loss))
print("test_accuracy : {}".format(test_accuracy))
결과가 연습문제는 0.9955 까지 올라간 반면, 본 문제에서는 0.9880에 그쳤다. (테스트 데이터도 학습할 때마다 결과가 달라짐)
MNIST 데이터셋 참고문헌을 보면 학습용 데이터와 시험용 데이터의 손글씨 주인이 달라 한 번도 본 적없는 필체의 손 글씨가 섞여있을 가능성이 높다.(인식률이 떨어질 수 밖에 없다)
model.evaluate()
대신 model.predict()
를 사용하면 model이 입력값을 보고 실제로 추론한 확률분포를 출력할 수 있다.
우리가 만든 model은 10개의 숫자 중 어느 것일지에 대한 확률값을 출력하는 함수로, 함수의 확률값(=출력값)이 가장 높은 숫자가 model이 추론한 숫자가 된다.
predicted_result = model.predict(x_test_reshaped) # model이 추론한 확률값
predicted_labels = np.argmax(predicted_result, axis=1)
idx=0
print('model.predict() 결과 : ', predicted_result[idx])
print('model이 추론한 가장 가능성이 높은 결과 : ', predicted_labels[idx])
print('실제 데이터의 라벨 : ', y_test[idx])
model.predict()의 결과값은 순서대로 0,1,2,...,7,8,9일 확률을 의미한다
이 모델은 입력한 이미지가 숫자 7일 확률이 1.00에 근접한다. 즉 이 모델은 입력한 이미지가 7이라고 강력하게 확신하고 있다는 뜻이다
그럼 7이 맞는지 확인해보자
plt.imshow(x_test[idx], cmap=plt.cm.binary)
plt.show()
반대로 model이 추론해 낸 숫자와 실제 라벨의 값이 다른 경우는 어떤 경우인지 직접 확인할 수 있다.
import random
wrong_predict_list=[]
for i, _ in enumerate(predicted_labels):
# i번째 test_labels과 y_test가 다른 경우만 모아본다.
if predicted_labes[i] != y_test[i]:
wrong_predict_list.append(i)
# wrong_predict_list에서 랜덤하게 5개만 추출
samples = random.choices(population=wrong_predict_list, k=5)
for n in samples:
print("예측확률분포: " + str(predicted_result[n]))
print("라벨: " + str(y_test[n]) + ", 예측결과: " + str(predicted_labels[n]))
plt.imshow(x_test[n], cmap=plt.cm.binary)
plt.show()
결과값을 보면 틀린 경우에 model이 추론 결과에 대한 확신도가 낮게 나와 혼란스러워 하는 것을 볼 수 있다. model의 추론 결과를 시각화하여 살펴보는 것은 향후 model 성능 개선에 도움이 되는 아이디어를 얻을 수 있는 좋은 방법 중 하나이다.
인식률을 더 높이는 방법, 딥러닝 네트워크 구조 자체는 바꾸지 않으면서 해볼 수 있는 방법들이 많다
앞서 딥러닝 네트워크 설계 단게에서 작성한 하이퍼파라미터들을 바꿔보는 것이다.
Conv2D
레이어에서 입력 이미지의 특징 수를 늘리거나 줄여보거나, Dense
레이어에서 뉴런수를 바꿔보거나, 학습 반복 횟수인 epoch
값을 변경해 볼 수 있다.
# 바꿔 볼 수 있는 하이퍼파라미터들
n_channel_1=16
n_channel_2=32
n_dense=32
n_train_epoch=10
model=keras.models.Sequential()
model.add(keras.layers.Conv2D(n_channel_1, (3,3), activation='relu', input_shape(28,28,1)))
model.add(keras.layers.MaxPool2D(2,2))
model.add(keras.layers.Conv2D(n_channel_2, (3,3), activation='relu'))
model.add(keras.layers.MaxPooling2D((2,2)))
model.add(keras.layers.Flatten())
model.add(keras.layers.Dense(n_dense, activation='relu'))
model.add(keras.layers.Dense(10, activation = 'softmax'))
model.summary()
model.compile(optimizer='adam',
loss='sparse_categorical_crossentropy',
metrics=['accuracy'])
# 모델 훈련
model.fit(x_train_reshaped, y_train, epochs=n_train_epoch)
# 모델 시험
test_loss, test_accuracy = model.evaluate(x_test_reshaped, y_test, verbose=2)
print("test_loss : {} ".format(test_loss))
print("test_accuracy : {}".format(test_accuracy))
배운 내용을 바탕으로 가위바위보 분류기를 만들어 보자.
가위바위보 이미지는 직접 사진을 찍어서 모아 본다
데이터 디렉토리는 rock_scissor_paper
디렉토리를 만들고 내부에 rock
, scissor
, paper
, test
를 만들고 test
안에는 rock
, scissor
, paper
폴더를 한 개 더 만들어준다
상위 폴더의 rock
, scissor
, paper
는 시험용 데이터이고
test
하위 폴더의 rock
, scissor
, paper
테스트용 데이터이다.
노트북 전면 카메라를 활용하여 가위, 바위, 보 이미지를 각 100장 씩 만들어 보자
구글의 teachable machine 사이트에서 쉽게 데이터를 만들 수 있다.
사이트에서 Get Started 버튼을 누르고, Image Project - standard image model을 선택하면, Webcam을 구동해 클래스별 이미지 데이터를 직접 촬영해서 데이터를 만들 수 있다.
Hole to Record
버튼을 누르면 이미지가 캡쳐된다.
캡쳐할 때 생각할 점
캡쳐를 했다면 우상단의 점 세개를 눌러 다운로드를 한다.
가위, 바위, 보 별로 (scissor.zip, rock.zip, paper.zip)으로 저장한다.
from PIL import Image
import glob
def resize_images(img_path):
images=glob.glob(img_path + "/*.jpg")
print(len(images), " images to be resized.")
# 파일마다 모두 28x28 사이즈로 바꾸어 저장합니다.
target_size=(28,28)
for img in images:
old_img=Image.open(img)
new_img=old_img.resize(target_size,Image.ANTIALIAS)
new_img.save(img, "JPEG")
print(len(images), " images resized.")
# 가위 이미지가 저장된 디렉토리 아래의 모든 jpg 파일을 읽어들여서
image_dir_path = "scissor"
resize_images(image_dir_path)
print("가위 이미지 resize 완료!")
가위 이미지를 28x28로 바꿔보았다.
다른 이미지 바위, 보도 바꿔본다
image_dir_path = "rock"
resize_images(image_dir_path)
image_dir_path = "paper"
resize_images(image_dir_path)
숫자 손글씨 인식기를 mnist.load_data()
라는 함수로 데이터를 읽었던 것처럼, 가위,바위,보 데이터를 읽을 수 있는 load_data()
함수를 만든다. 이 코드를 활용하면 다른 데이터에도 적용할 수 있다.
load_data()
함수는 입력으로 이미지가 있는 폴더 위치를 받는다. 여기서 rock_scissor_paper 폴더 위치를 적어준다. 숫자 손글씨는 0~9 총 10개의 클래스, 가위바위보는 총 3개의 클래스(가위 : 0, 바위 : 1, 보 : 2)로 라벨링된다.
import numpy as np
import os
import matplotlib.pyplot as plt
def load_data(img_path, number_of_data=300): # 가위바위보 이미지 개수 총합에 주의하세요.
# 가위 : 0, 바위 : 1, 보 : 2
img_size=28
color=3
#이미지 데이터와 라벨(가위 : 0, 바위 : 1, 보 : 2) 데이터를 담을 행렬(matrix) 영역을 생성합니다.
imgs=np.zeros(number_of_data*img_size*img_size*color,dtype=np.int32).reshape(number_of_data,img_size,img_size,color)
labels=np.zeros(number_of_data,dtype=np.int32)
idx=0
for file in glob.iglob(img_path+'/scissor/*.jpg'):
img = np.array(Image.open(file),dtype=np.int32)
imgs[idx,:,:,:]=img # 데이터 영역에 이미지 행렬을 복사
labels[idx]=0 # 가위 : 0
idx=idx+1
for file in glob.iglob(img_path+'/rock/*.jpg'):
img = np.array(Image.open(file),dtype=np.int32)
imgs[idx,:,:,:]=img # 데이터 영역에 이미지 행렬을 복사
labels[idx]=1 # 바위 : 1
idx=idx+1
for file in glob.iglob(img_path+'/paper/*.jpg'):
img = np.array(Image.open(file),dtype=np.int32)
imgs[idx,:,:,:]=img # 데이터 영역에 이미지 행렬을 복사
labels[idx]=2 # 보 : 2
idx=idx+1
print("학습데이터(x_train)의 이미지 개수는", idx,"입니다.")
return imgs, labels
image_dir_path = "rock_scissor_paper" #폴더명
(x_train, y_train)=load_data(image_dir_path)
x_train_norm = x_train/255.0 # 입력은 0~1 사이의 값으로 정규화
print("x_train shape: {}".format(x_train.shape))
print("y_train shape: {}".format(y_train.shape))
load_data
를 완료 했다면 이미지를 한번 불러본다
plt.imshow(x_trein[0])
print('라벨 : ', y_train[0])
데이터 준비가 완료되었으니 이제는 딥러닝 네트워크를 설계할 차례다.
import tensorflow as tf
from tensorflow import keras
import numpy as np
model=keras.models.Sequential()
model.add(keras.layers.Conv2D(32, (3,3), activation='relu', input_shape=(28,28,3)))
model.add(keras.layers.MaxPool2D(2,2))
model.add(keras.layers.Conv2D(64, (3,3), activation='relu'))
model.add(keras.layers.MaxPooling2D((2,2)))
model.add(keras.layers.Flatten())
model.add(keras.layers.Dense(128, activation='relu'))
model.add(keras.layers.Dense(20, activation='softmax'))
model.summary()
앞에 손글씨 분석할 때 사용했던 모델들을 하이퍼파라미터들을 조금 수정해서 사용했다.
네트워크 설계를 완료했으니 데이터 학습을 할 차례이다.
model.compile(optimizer='adam',
loss='sparse_categorical_crossentropy',
metrics=['accuracy'])
model.fit(x_train_norm, y_train, epochs=10)
optimizer를 adam
에서 rmsprop
로 바꿔보았으나 더 낮은 accuracy
값이 나와 다시 adam
으로 변경했다.
이제 테스트를 하기위해 테스트 데이터를 만들고(혹은 받아온다) 테스트를 해보자
우선 테스트 데이터 x_test
, y_test
를 만들자
image_dir_path = "/rock_scissor_paper/test"
(x_test, y_test)=load_data(image_dir_path)
x_test_norm = x_test/255.0
print("x_test shape : {}".format(x_test.shape))
print("y_test shape : {}".format(y_test.shape))
이렇게 테스트용 데이터 준비도 완료 되었으니 훈련시킨 model을 사용하여 test_accuracy를 측정해 본다.
test_loss, test_accuracy = model.evaluate(x_test_norm, y_test, verbose=2)
print("test_loss : {}".format(test_loss))
print("test_accuracy : {}".format(test_accuracy))
만족할만한 결과가 나왔을까??
각 데이터 별로 추론 결과를 보고 싶다면 아래 코드를 실행해보자
predicted_result = model.predict(x_test_norm) # model이 추론한 확률값
predicted_labels = np.argmax(predicted_result, axis=1)
idx=600 # 값을 변경해서 찾아보자
print('model.predict() 결과 : ', predicted_result[idx])
print('model이 추론한 가장 가능성이 높은 결과 : ', predicted_labels[idx])
print('실제 데이터의 라벨 : ', y_test[idx])
idx에 자기가 원하는 인덱스 값을 넣어서 각 예측 값과 실제 값을 확인해 볼 수 있다.
그리고 인덱스 값의 사진을 보고 싶다면 아래 코드를 사용하자
plt.imshow(x_test[idx], cmap=plt.cm.binary)
plt.show()
또한 model이 틀린 경우를 찾아보자
아래 코드를 사용하면 예측 결과가 틀린 값들을 랜덤해서 10개씩 보여준다
import random
wrong_predict_list=[]
for i, _ in enumerate(predicted_labels):
if predicted_labels[i] != y_test[i]:
wrong_predict_list.append(i)
samples = random.choices(population=wrong_predict_list, k=10) # k값을 수정하면 더 많은 값의 수를 볼 수 있다.
for n in samples:
print("예측확률분포: " + str(predicted_result[n]))
print("라벨: " + str(y_test[n]) + ", 예측결과: " + str(predicted_labels[n]))
plt.imshow(x_test[n], cmap=plt.cm.binary)
plt.show()