인공지능과 가위바위보 하기

ROK·2022년 1월 8일
0

AIFFEL

목록 보기
5/17

간단한 이미지 분류기

이미지를 분류하는 간단한 인공지능을 만들어 볼 예정이다. 이 인공지능은 손으로 쓴 숫자를 인식할 수 있고, 가위바위보 게임을 만들 수도 있다. 숫자는 0~9까지 총 10개의 class만 인식하면 되고, 가위바위보는 총 3개의 class만 구분하면 된다. 이와 같이 클래스가 몇개 안되는 경우, 인공지능은 간단하게 이미지를 분류해 낼 수 있다.

순서

일반적으로 딥러닝 기술은 "데이터 준비→딥러닝 네트워크 설계→학습→테스트(평가)"의 순서대로 만들게 된다

숫자 손글씨 인식기 만들기(Sequential Model 이용)

숫자 손글씨 분류기는 숫자 이미지를 입력으로 받으면, 그 이미지가 어떤 숫자를 나타내는지 출력해 낼 수 있다.

데이터 준비

MNIST 숫자 손글씨 Dataset 불러들이기

텐서플로우(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에 근접하는 걸 확인

Sequential Model을 사용

데이터가 모두 준비 되었다면, 딥러닝 네트워크를 만들어야 한다.
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))

코드 의미 설명

  • Conv2D 레이어의 첫 번째 인자는 사용하는 이미지 특징의 수, 16과 32를 사용했는데
    처음엔 16개의 이미지 특징을, 그 뒤에 32개의 이미지 특징을 고려하겠다는 뜻
    (지금 연습하는 숫자 이미지는 매우 단순한 형태의 이미지이다. 만약 강아지 얼굴 사진이 입력 이미지라면 훨씬 디테일하고 복잡한 형태이므로 그때는 이 특징 숫자를 늘려줘야 한다)
  • Dense 레이어의 첫 번째 인자는 분류기에 사용되는 뉴런의 숫자이다.
    이 값이 클수록 보다 복잡한 분류기를 만들 수 있다.
    (10개의 숫자가 아닌 알파벳을 구분하고 싶다면, 대문자 26개, 소문자 26개로 총 52개의 클래스를 분류해내야 한다. 그래서 32보다 큰 64, 128 등을 고려해 볼 수 있다.)
  • 마지막 Dense 레이어의 뉴런 숫자는 결과적으로 분류해 내야 하는 클래스 수로 지정하면 된다. (자 인식기에서는 10, 알파벳 인식기는 52)

만든 딥러닝 네트워크 모델을 확인해 보려면, 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)

이미지 파일 load하기

숫자 손글씨 인식기를 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()
profile
하루에 집중하자

0개의 댓글