웹어셈블리 #1 Emscripten 시작하기
TensorFlow와 Keras를 사용해서 딥러닝을 해보자 #2 야옹이vs멍멍이
2024-08-05
Explanation
TensorFlow와 Keras를 사용한 간단한 학습 모델 만들기 2탄!
어제는 TensorFlow와 Keras를 사용한 학습하는 코드가 어떻게 생겼고 어떻게 동작하는지 살짝 해봤는데요.
오늘은 조금 더 진행해 보도록 할게요.
어제의 아래 글에서 이어지는 내용입니다.
https://falsy.me/tensorflow와-keras를-사용해서-딥러닝을-해보자-1-시행착오/
제가 어제 진행한 학습 코드가 생각처럼 동작하지 않았던 이유에 대해서 생각해 봤는데요.
일단, 어제 제가 호기롭게 한 땀 한 땀 고양이 이미지와 그 밖의 동물 이미지를 각각 60개씩 만들었다고 말했었는데요. 이 데이터의 양이 턱없이 부족했던 것 같아요.
그리고 또 하나가, 가장 간단한 모델로 이미지가 주어졌을 때 이게 고양이인지 아닌지 구분하는 것을 생각했는데. 생각해 보니까 ‘이것은 고양이다’라는 명제의 학습은 괜찮은데, ‘이것들이 고양이가 아니다’라는 명제는 해당하는 클래스가 너무 많아서 학습하기 어려울 것 같았어요.
그래서 아직 초보자고 하니까, 주제를 바꿔서 야옹이와 멍멍이의 이미지를 학습시켜서 이게 야옹이인지, 멍멍이인지 구분하는 모델을 만들어 보는 것으로 바꾸었습니다!
공부하며 글을 작성하고 있어서, 글 내용 중에 잘못된 부분이 많이 있을 수 있습니다. 글 중간 중간에 레퍼런스 링크를 함께 확인해 주세요 :)
늘 그렇듯, 이 글에 작성된 코드는 깃허브 저장소를 통해서 모두 확인할 수 있습니다.
https://github.com/falsy/blog-post-example/tree/master/tensorflow-keras-v2
어제는 미숙한 개발자답게 한 땀 한 땀 학습 이미지 만들고, 또 숙련된 개발자라면 스크래핑 서버를 만들어서 이미지를 수집할 거라는 이야기를 했는데, 그 생각 또한 굉장히 미숙한 개발자였습니다…
좀 알아보니까 캐글(Kaggle)이라는 예측 모델 및 분석 대회 플랫폼 서비스가 있더라고요?! 아무래도 모델을 만드는 대회 플랫폼이다 보니, 다양한 주제에 대한 많은 데이터 셋도 제공하고 있었습니다.
Kaggle: https://www.kaggle.com/
저는 많은 데이터 셋 중에 야옹이와 멍멍이의 가장 업보트가 많은 데이터 셋을 다운로드 받았습니다.
https://www.kaggle.com/datasets/tongpython/cat-and-dog
살짝, 짧게나마 새롭게 알게 된 사실을 정리해 보자면,
1 2 3 |
# 데이터 경로 설정 train_dir = 'dataset/train' val_dir = 'dataset/validation' |
어제 밤에 문득 코드를 감감히 보다보니 이렇게 ‘train’, ‘validation’ 까지만 있고 ‘cats’와 ‘others’의 경로에 대한 부분이 없더라고요?! 뭘까.. 하고 좀 알아봤는데. 예를 들어 디렉토리 구조가 아래와 같다면,
1 2 3 4 5 6 7 8 9 10 11 |
/dataset /train /cats ... images /others ... images /validation /cats ... images /others ... images |
이후에 사용되는 ‘ImageDataGenerator’의 ‘flow_from_directory’ 메서드가 알아서 디렉토리를 찾아서 정의해 준다고 합니다.
1 2 3 4 5 6 |
train_generator = train_datagen.flow_from_directory( train_dir, target_size=(150, 150), batch_size=32, class_mode='binary' ) |
여기서 ‘class_mode’ 속성의 값이 ‘binary’로 하면, 디렉토리 구조 순서로 ‘cats’가 ‘0’ 그리고 ‘others’가 ‘1’이 된답니다.
1 |
print(train_generator.class_indices) |
위 코드를 직접 추가해보면, ‘{‘cats’: 0, ‘others’: 1}’ 이렇게 확인할 수 있답니다.
이제 이 모델의 결괏값이 ‘0’에 가까우면 ‘cats’ 으로 예측되는 것이고 ‘1’에 가까우면 ‘others’로 예측되는 것이랍니다.
자 이제 학습을 위한 데이터 생성까지 완벽히 이해했고 (← 이해못했음)
(대충, 이 자리에 노랑머리 캐릭터 짤)
1 2 3 |
// train_model.py # 사전 학습된 ResNet50 모델 로드 base_model = ResNet50(weights='imagenet', include_top=False) |
어제에 이어서 ‘ResNet50’ 모델을 이야기하는 것은 뭐랄까.. 이제 막 숫자 표기법 배우는데, 사칙 연산을 이야기하는 느낌이랄까?!
우선은, 간단하게! 많이 사용되는 이미지 분류 모델 정도로만 알고, 모델들에 대해서는 차차 알아가기로..
1 2 3 4 5 6 |
// train_model.py # 모델 아키텍처 수정 x = base_model.output x = GlobalAveragePooling2D()(x) x = Dense(1024, activation='relu')(x) predictions = Dense(1, activation='sigmoid')(x) |
모델을 이해하진 못해도 모델을 수정하는 방법에 대해선 좀 알아볼까요?
‘GlobalAveragePooling2D’
Global Average Pooling 레이어는 입력한 이미지에 나온 ‘특징 맵’에서 평균을 구해서 그 평균을 하나의 숫자로 만드는 과정이라고 합니다.
여기서 ‘특징 맵’이란?! 야옹이 이미지라고 하면 야옹이의 눈, 귀, 꼬리, 털 패턴 등이 이 특징 맵에 저장됩니다. 보통 ‘특징 맵’은 2차원 배열로 되어 있는데(높이x너비), 이 값을 한줄로 쭉 나열해서 평균을 구하는 것을 ‘공간 차원 평균화’라고 하고 이 값들의 평균 값을 구해서 하나의 ‘특징 맵’을 하나의 숫자로 요약합니다.
그렇게 하나의 이미지가 주어지면 거기엔 많은 ‘특징 맵’이 있고 그 특징 맵의 평균을 계산해서 최종적으로 여러 개의 숫자로 해당 이미지를 요약하여 모델이 더 효율적으로 학습할 수 있도록 하는 기능입니다!
대략, 아래와 같이 요약할 수 있을 거 같아요. 이미지가 주어졌을 때,
1 2 3 4 5 6 7 8 9 10 11 |
// 특징 맵 1: [[1, 2, 1, 0], [0, 1, 2, 3], [3, 1, 0, 2], [1, 2, 1, 0]] // 특징 맵 2: [[2, 3, 1, 1], [0, 2, 3, 4], [4, 2, 1, 3], [2, 3, 2, 1]] |
1 2 3 |
// 특징 평균 계산 특징 맵 1: (1+2+1+0+0+1+2+3+3+1+0+2+1+2+1+0) / 16 = 20 / 16 = 1.25 특징 맵 2: (2+3+1+1+0+2+3+4+4+2+1+3+2+3+2+1) / 16 = 34 / 16 = 2.125 |
1 2 |
// 최종 출력 [1.25, 2.125] |
다음으로,
1 |
x = Dense(1024, activation='relu')(x) |
여기서 1024는 ‘뉴런’의 개 수를 입력한 것입니다. 그리고 뉴런은 ‘신경망’의 기본 구성 요소인데요.
‘뉴런’, ‘신경망’이라는 단어만 들어도 벌써 아득해 지기 시작한다면, 삐빅!
정상입니다.
위 코드를 예로들면, 1024개의 뉴런으로 이전의 ‘GlobalAveragePooling2D’의 모든 출력과 연결이 됩니다. 각 뉴런은 이전 모든 출력 값을 각 연결마다 가중치를 곱하고, 각 뉴런은 가중치를 곱한 결과를 모두 합산합니다.
그리고 합산된 값에 ‘바이어스(bias, 편향)’를 추가하고 활성화 함수를 적용해서 최종 출력을 계산합니다. 여기서 활성화 합수는 ‘activation’ 속성으로 사용되는 값입니다. 이번엔 ‘바이어스(bias, 편향)’가 나왔는데요.
(무슨 영영사전처럼, 어려운 단어를 알아가는데, 그 과정에 또 어려운 단어가 나오네요..)
‘바이어스(bias, 편향)’란 뉴런의 출력값에 추가로 더 해주는 상수입니다.
하얀종이개발자님의 포스팅: https://jh2021.tistory.com/3
위 블로그에 글의 예시를 함께 참조해서 이야기 해보자면, 뉴런들을 사람들이라고 하고, 파티에 갈지, 안갈지 예측한다면, ‘GlobalAveragePooling2D’의 전달 받은 값(날씨, 같이가는 친구, 비용)이 있을 때, 여기에 파티를 갈지, 안갈지에 대한 전달받은 출력에 대한 가중치가 있겠죠? 날씨의 중요도, 같이가는 친구의 중요도, 비용 값의 중요도를 곱해서 값을 구하는 것이죠!
그리고 여기서 사람의 성향에 따라서 A는 사람은 파티를 너무 좋아해서 날씨가 안좋고 돈이 없어도 파티에 가려고 할 것이고 파티를 안 좋아하는 B는 날씨가 좋고 돈이 많아도 파티에 가지 않으려 하기 때문에 ‘바이어스(bias, 편향)’ 값을 추가해 주는 것이라고 합니다!
다음으로 가중치 함수로는 일단 위 코드에서는 ‘relu’와 ‘sigmoid’가 사용되었는데요, 이 부분은 아직 잘 이해가 안되서, 일단 간단하게 둘은 결과 값의 범위에 차이가 있는데요. ‘relu’는 0 ~ ∞ 이고(0 이하면 0 출력) ‘sigmoid’는 0 ~ 1 입니다.
1 |
x = Dense(1024, activation='relu')(x) |
위 코드에서는 ‘relu’ 활성화 함수는 입력이 음수일 때 출력을 0으로 변환하여, 기울기 소실 문제를 완화, 딥러닝 모델의 학습을 더 효과적으로 만듭니다.
라고 적었지만 저도 아직 정확하게 이해하지 못하였습니다..
1 |
predictions = Dense(1, activation='sigmoid')(x) |
위 코드에서는 최종적으로 0부터 1사이의 확률을 출력하게 합니다.
가상 환경을 만들고 활성화해주고
1 2 |
$ python3 -m venv .venv $ source .venv/bin/activate |
학습 시작!
1 |
$ python3 train_model.py |
아까 캐글(Kaggle)에서 다운로드 받은 학습 이미지 8000개와 검증 이미지 2000개를 가지고 학습을 시켰는데, 이미지가 많아서 그런지 한 2시간 걸렸네요..
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
Found 8005 images belonging to 2 classes. Found 2023 images belonging to 2 classes. Epoch 1/10 251/251 ━━━━━━━━━━━━━━━━━━━━ 711s 3s/step - accuracy: 0.8928 - loss: 0.2320 - val_accuracy: 0.4993 - val_loss: 1.0928 Epoch 2/10 251/251 ━━━━━━━━━━━━━━━━━━━━ 859s 3s/step - accuracy: 0.9711 - loss: 0.0807 - val_accuracy: 0.5260 - val_loss: 0.6985 Epoch 3/10 251/251 ━━━━━━━━━━━━━━━━━━━━ 828s 3s/step - accuracy: 0.9744 - loss: 0.0687 - val_accuracy: 0.7954 - val_loss: 0.4874 Epoch 4/10 251/251 ━━━━━━━━━━━━━━━━━━━━ 706s 3s/step - accuracy: 0.9765 - loss: 0.0655 - val_accuracy: 0.9684 - val_loss: 0.0933 Epoch 5/10 251/251 ━━━━━━━━━━━━━━━━━━━━ 801s 3s/step - accuracy: 0.9826 - loss: 0.0477 - val_accuracy: 0.9763 - val_loss: 0.0916 Epoch 6/10 251/251 ━━━━━━━━━━━━━━━━━━━━ 667s 3s/step - accuracy: 0.9784 - loss: 0.0610 - val_accuracy: 0.9718 - val_loss: 0.0804 Epoch 7/10 251/251 ━━━━━━━━━━━━━━━━━━━━ 704s 3s/step - accuracy: 0.9847 - loss: 0.0423 - val_accuracy: 0.9733 - val_loss: 0.0971 Epoch 8/10 251/251 ━━━━━━━━━━━━━━━━━━━━ 653s 3s/step - accuracy: 0.9881 - loss: 0.0338 - val_accuracy: 0.9768 - val_loss: 0.0728 Epoch 9/10 251/251 ━━━━━━━━━━━━━━━━━━━━ 616s 2s/step - accuracy: 0.9882 - loss: 0.0315 - val_accuracy: 0.9753 - val_loss: 0.0711 Epoch 10/10 251/251 ━━━━━━━━━━━━━━━━━━━━ 720s 3s/step - accuracy: 0.9926 - loss: 0.0250 - val_accuracy: 0.9644 - val_loss: 0.1168 |
해당 테스트의 정확도(‘val_accuracy’)가 0.4993 → 0.5260 → 0.7954 → 0.9684 → 0.9763 로 에포크마다 쭉쭉 올라가는게 신기하네요.
그리고 또 개인적으로, 이미지 120장으로 학습시켰을 때 최종적으로 만들어진 모델 ‘cat_detector_model.keras’의 크기가 300MB 정도였는데, 이번에 이미지 10,000장으로 학습한 결과도 크기가 300MB 정도인 게 신기했어요.
그냥 로컬에서 테스트할 수 있는 함수를 만들어 확인할 수 도 있지만, 나중에 실제 사용할 좋은 모델을 만들면 서버에 올려서 사용할 거라 간단하게 플라스크로 로컬 서버에서 동작하도록 만들어 볼게요.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
from flask import Flask, request, jsonify from tensorflow.keras.models import load_model from tensorflow.keras.preprocessing import image import numpy as np import requests from io import BytesIO from PIL import Image app = Flask(__name__) # 모델 로드 model = load_model('cat_detector_model.keras') def is_cat(img_url): # 이미지 다운로드 response = requests.get(img_url) response.raise_for_status() # 요청이 성공하지 않으면 예외 발생 img = Image.open(BytesIO(response.content)).convert('RGB') # 이미지 전처리 img = img.resize((150, 150)) img_array = image.img_to_array(img) img_array = np.expand_dims(img_array, axis=0) / 255.0 # 예측 prediction = model.predict(img_array) return prediction[0][0] < 0.5 @app.route('/predict', methods=['GET']) def predict(): img_url = request.args.get('imgURL') if not img_url: return jsonify({"error": "imgURL parameter is required"}), 400 try: result = is_cat(img_url) return jsonify({'is_cat': bool(result)}) except Exception as e: return jsonify({"error": str(e)}), 500 if __name__ == '__main__': app.run(host='0.0.0.0', port=7777) |
방금 만든 서버를 로컬에서 실행시키고
1 |
$ python3 app.py |
이제 드디어 어제 한 땀 한 땀 만들었던 야옹이 이미지와 멍멍이 이미지를 가지고 확인을 해보겠습니다.
아래와 같이 10개의 이미지를 확인해 볼 거에요.
(순서대로 image-1.png ~ image-10.png)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
http://localhost:7777/predict?imgURL=https://falsy.me/wp-content/uploads/2024/08/image-1.png // {"is_cat":true} http://localhost:7777/predict?imgURL=https://falsy.me/wp-content/uploads/2024/08/image-2.png // {"is_cat":true} http://localhost:7777/predict?imgURL=https://falsy.me/wp-content/uploads/2024/08/image-3.png // {"is_cat":true} http://localhost:7777/predict?imgURL=https://falsy.me/wp-content/uploads/2024/08/image-4.png // {"is_cat":true} http://localhost:7777/predict?imgURL=https://falsy.me/wp-content/uploads/2024/08/image-5.png // {"is_cat":true} http://localhost:7777/predict?imgURL=https://falsy.me/wp-content/uploads/2024/08/image-6.png // {"is_cat":false} http://localhost:7777/predict?imgURL=https://falsy.me/wp-content/uploads/2024/08/image-7.png // {"is_cat":false} http://localhost:7777/predict?imgURL=https://falsy.me/wp-content/uploads/2024/08/image-8.png // {"is_cat":false} http://localhost:7777/predict?imgURL=https://falsy.me/wp-content/uploads/2024/08/image-9.png // {"is_cat":false} http://localhost:7777/predict?imgURL=https://falsy.me/wp-content/uploads/2024/08/image-10.png // {"is_cat":false} |
오! 모두 다 맞췄어요. 신기하네요.
여기까지 간단하게 야옹이와 멍멍이의 이미지를 학습시키고 직접 이미지가 주어졌을 때 야옹이인지, 멍멍이인지 예측하는 서버까지 간단하게 만들어 보았습니다!
아주 짧게 공부해 보고 간단한 주제로 진행해 봤지만, 생각보다 훨씬 재미있는 거 같아요.
어제 이야기했던 것처럼 사실 궁극적으로 만들고 싶은 건 딥페이크 이미지를 구분하는 모델을 만드는 것이기 때문에 이 부분은 앞으로 틈틈이 공부하면서 진척이 있으면 3탄으로 다시 돌아도록 하겠습니다!