들어가는 글
Pytorch보다는 Keras가 대중적으로 많이 사용하고 있어서 Pytorch 아래 예제를 Keras 를 이용해서 변환하는 작업을 진행하였습니다.
https://swlock.blogspot.com/2022/07/machine-learning-pytorch.html
작업을 하면서 Keras에는 이미 ImageDataGenerator 가 따로 있는데 이것을 이용하지 않고 CustomDataLoader를 만들어 사용하다 보니 막히는 곳이 많아서 구현에 어려움이 있었습니다.
(tf.keras.preprocessing.image 모듈의 ImageDataGenerator 클래스가 준비되어 있으며 예제가 많으니 쉽게 응용이 가능할 겁니다. 여기서는 해당 클래스를 사용하지 않았습니다.)
여기에서 하는 동작은 개 이미지와 고양이 이미지를 미리 훈련해서 주어지는 이미지를 보고 고양이인지 개인지 맞추는 것입니다. 즉 불량과 양품 이미지를 학습하고 결과를 출력하는 것과 비슷하다고 생각 할 수 있습니다.
참고로 여기에서는 Regression에서 많이 사용하는 MSE loss함수를 사용하였습니다. loss 함수가 개 고양이 분류하는데 적절하지 않을 수는 있긴 하지만 그건 읽는 분 몫으로 남겨둡니다. https://keras.io/api/losses/
개 고양이 분류
캐글에서 있던 부분이고 좀 더 자세한 내용은 아래 링크를 참고 하시면 됩니다.
https://www.kaggle.com/competitions/dogs-vs-cats
여기에서 사용된 자료는 아래 링크로부터 받았습니다.
https://storage.googleapis.com/mledu-datasets/cats_and_dogs_filtered.zip
위 링크에서 직접 받을 필요는 없고 다음 python 스크립트로 자동으로 받아옵니다.
훈련 데이터 이미지 가져오기
zip파일을 다운로드 받고 폴더에 압축을 풉니다. 실행시켜도 폴더가 존재하거나 하면 다시 다운로드 하지 않습니다.
from requests import get from os.path import exists import zipfile def download(url, file_name=None): if not file_name: file_name = url.split('/')[-1] with open(file_name, "wb") as file: response = get(url) file.write(response.content) if __name__ == '__main__': zip_folder = r"cats_and_dogs_filtered" url = "https://storage.googleapis.com/mledu-datasets/cats_and_dogs_filtered.zip" if not exists(zip_folder+".zip"): print("download data") download(url) if not exists(zip_folder): print("unzip") zip_ref = zipfile.ZipFile(zip_folder+".zip", 'r') zip_ref.extractall() zip_ref.close()
배치(batch)와 데이터 로딩
데이터를 읽어오는 부분은 이전 포스팅을 조금 참조 부탁드립니다.
배치란 물건을 놓는것을 말하는건 아니고 일괄 작업의 단위를 말하는 것입니다.
이미지는 아래 폴더에 존재합니다.
cats_and_dogs_filtered/train/cats cats_and_dogs_filtered/train/dogs cats_and_dogs_filtered/validation/cats cats_and_dogs_filtered/validation/dogs
훈련이나 검증이냐에 따라서 base_path를 지정해서 class를 호출해 줄겁니다.
즉 cats_and_dogs_filtered/train , cats_and_dogs_filtered/validation 여기까지는 호출하는 쪽에서 경로를 알주고 cats, dogs라는 경로를 붙여 파일 목록을 구해냅니다.
데이터를 넘겨야 하는 __getitem__ 에서는 cats+dogs 이미지가 포함되어 있으므로 하위순위에서는 cats 이미지를 상위 숫자에서는 dogs 이미지를 넘기도록 합니다. 목표값도 개는 0,1을 고양이는 1,0을 리턴 하도록 구현 하였습니다.
def __getitem__(self, index):
if index >= len(self.cat_fnames): # dog
else: # cat
transform 처리가 있는데, 학습을 위해서 이미지 그대로 학습을 할 수 없습니다. 이미지를 Tensor로 변환하는 작업도 해야하고 또 다른 중요한 이유는 학습 데이터를 늘리기 위해 이미지에 변형을 하는 작업을 합니다.
아래 부분이 transform 부분입니다. 이미지를 array로 변환하고 255로 나누어 주는 부분이 있는데
if self.transform: image = tf.keras.preprocessing.image.img_to_array(image) image = image.astype('float32') image /= 255.0 image = self.transform(image)
이 부분을 설명하자면 이미지는 일반적으로 R,G,B 3개의 채널을 가지고 있고 하나의 채널에 대해서 0~255 까지 1byte의 숫자로 표시됩니다. 따라서 1개의 채널에 대해서 255로 나누어 주면 값 범위가 0~1 사이의 숫자로 표현됩니다. 학습에 있어서 도움이 되기 때문에 이렇게 구현하였습니다.
아래는 인자로 넘겨준 부분에 의해서 이미지 변형이 일어나는 구간입니다.
image = self.transform(image)
호출 쪽에서는 다음과 같은 형태로 호출하게 되는데
train_dataset = CustomDataset("cats_and_dogs_filtered/train", transform=train_data_augmentation) val_dataset = CustomDataset("cats_and_dogs_filtered/validation", transform=val_processing)
이 부분은 tf.keras.Sequential 을 이용해서 구현하였습니다.
val_processing 부분과 train_data_augmentation 두개로 분리해서 검증용 일때는 random으로 이미지 변형이 되지 않도록 하였습니다.
val_processing = tf.keras.Sequential([ layers.Resizing(IMG_SIZE, IMG_SIZE), layers.CenterCrop(IMG_SIZE, IMG_SIZE), layers.Normalization(mean=[0.485, 0.456, 0.406], variance=[0.229, 0.224, 0.225]), ]) train_data_augmentation = tf.keras.Sequential([ layers.RandomFlip("horizontal_and_vertical"), val_processing, ])
다음은 CustomDataset 의 전체 소스입니다.
class CustomDataset: def __init__(self, base_path, transform=None, target_transform=None, base_idx=None, cnt=None, file_name=None): self.file_name = None self.base_idx = base_idx self.cnt = cnt self.base_dir = base_path self.transform = transform self.cat_fnames = [] self.dog_fnames = [] self.target_transform = target_transform # 훈련에 사용되는 고양이/개 이미지 경로 self.cats_dir = os.path.join(self.base_dir, 'cats') self.dogs_dir = os.path.join(self.base_dir, 'dogs') if file_name is not None: self.file_name = file_name self.total_len = 1 return ''' cats_and_dogs_filtered/train/cats cats_and_dogs_filtered/train/dogs cats_and_dogs_filtered/validation/cats cats_and_dogs_filtered/validation/dogs ''' # os.listdir() 경로 내에 있는 파일의 이름을 리스트의 형태로 반환합니다. # [...'cat.102.jpg', 'cat.103.jpg', .... ] self.cat_fnames = os.listdir(self.cats_dir) self.dog_fnames = os.listdir(self.dogs_dir) if self.base_idx is None: self.total_len = len(self.cat_fnames) + len(self.dog_fnames) else: self.total_len = cnt print(f"total images:{self.total_len}") def __getitem__(self, index): """ 주어진 인덱스 index 에 해당하는 샘플을 데이터셋에서 불러오고 반환합니다. cat[xxx] cat[xxx] dog[xxx] dog[xxx] """ if self.base_idx is not None: index = index + self.base_idx if self.file_name is not None: y_data = np.array([0.0, 1.0]) image = Image.open(self.file_name) elif index >= len(self.cat_fnames): # dog x_data = self.dog_fnames[index - len(self.cat_fnames)] y_data = np.array([0.0, 1.0]) img_path = os.path.join(self.dogs_dir, x_data) image = Image.open(img_path) # plt.imshow(image) # print(y_data) # plt.show() else: # cat x_data = self.cat_fnames[index] y_data = np.array([1.0, 0.0]) img_path = os.path.join(self.cats_dir, x_data) image = Image.open(img_path) # plt.imshow(image) # print(y_data) # plt.show() if self.transform: image = tf.keras.preprocessing.image.img_to_array(image) image = image.astype('float32') image /= 255.0 image = self.transform(image) # plt.imshow(image) # plt.show() if self.target_transform: y_data = self.target_transform(y_data) return image, y_data def __len__(self): """ 데이터셋의 샘플 개수를 반환합니다. """ return self.total_len class CustomDataloader(Sequence): def __init__(self, _dataset, batch_size=1, shuffle=False): self.dataset = _dataset self.batch_size = batch_size self.total_len = math.ceil(len(self.dataset) / self.batch_size) self.shuffle = shuffle self.indexer = np.arange(len(self.dataset)) self.on_epoch_end() def __getitem__(self, index): indexer = self.indexer[index * self.batch_size:(index + 1) * self.batch_size] batch_x = [self.dataset[i][0] for i in indexer] batch_y = [self.dataset[i][1] for i in indexer] batch_x = np.array(batch_x) batch_y = np.array(batch_y) return batch_x, batch_y def __len__(self): return self.total_len def on_epoch_end(self): if self.shuffle: np.random.shuffle(self.indexer)
이미지 모델
이미지 모델은 keras.Sequential 을 이용해서 만들었습니다.
Conv2D이 핵심이 되며 구조는 Pytorch때와 동일하게 구현하였습니다.
# https://keras.io/api/layers/ model = keras.Sequential() model.add(keras.Input(shape=(IMG_SIZE, IMG_SIZE, 3))) model.add(layers.Conv2D(16, kernel_size=3, activation='relu')) model.add(layers.MaxPooling2D(pool_size=(8, 8))) model.add(layers.Flatten()) model.add(layers.Dense(512, activation='relu')) model.add(layers.Dense(2, activation='relu')) model.summary()
이미지 처리에 있어서 Conv2D 이걸 빼면 학습이 잘 되지 않습니다.
LOSS함수와 optim
Loss함수도 많긴 하지만 클래스 분류에는 일반적으로 BCELoss 나 CrossEntropyLoss 함수를 많이 사용하는데 꼭 해당 함수를 사용해야 하는것은 아닙니다. 여기에서는 이전 기초에서 했던것도 있어서 단순하게 출력단을 2가지 0,1 을 목표값으로 설정해서 진행했습니다.
# https://keras.io/api/optimizers/ # https://keras.io/api/losses/ INPUT_OPTIMIZER = tf.keras.optimizers.SGD(learning_rate=lr) LOSS = tf.keras.losses.MeanSquaredError(reduction="auto", name="mean_squared_error") model.compile(optimizer=INPUT_OPTIMIZER, loss=LOSS) model.fit(train_loader, batch_size=batch_size, epochs=n_epochs, validation_data=val_loader)
전체소스
from tensorflow import keras from tensorflow.keras.utils import Sequence from tensorflow.keras import layers import tensorflow as tf import numpy as np import matplotlib.pyplot as plt import os from PIL import Image import math batch_size = 20 lr = 1e-3 n_epochs = 20 IMG_SIZE = 150 # fig = plt.gcf() class CustomDataset: def __init__(self, base_path, transform=None, target_transform=None, base_idx=None, cnt=None, file_name=None): self.file_name = None self.base_idx = base_idx self.cnt = cnt self.base_dir = base_path self.transform = transform self.cat_fnames = [] self.dog_fnames = [] self.target_transform = target_transform # 훈련에 사용되는 고양이/개 이미지 경로 self.cats_dir = os.path.join(self.base_dir, 'cats') self.dogs_dir = os.path.join(self.base_dir, 'dogs') if file_name is not None: self.file_name = file_name self.total_len = 1 return ''' cats_and_dogs_filtered/train/cats cats_and_dogs_filtered/train/dogs cats_and_dogs_filtered/validation/cats cats_and_dogs_filtered/validation/dogs ''' # os.listdir() 경로 내에 있는 파일의 이름을 리스트의 형태로 반환합니다. # [...'cat.102.jpg', 'cat.103.jpg', .... ] self.cat_fnames = os.listdir(self.cats_dir) self.dog_fnames = os.listdir(self.dogs_dir) if self.base_idx is None: self.total_len = len(self.cat_fnames) + len(self.dog_fnames) else: self.total_len = cnt print(f"total images:{self.total_len}") def __getitem__(self, index): """ 주어진 인덱스 index 에 해당하는 샘플을 데이터셋에서 불러오고 반환합니다. cat[xxx] cat[xxx] dog[xxx] dog[xxx] """ if self.base_idx is not None: index = index + self.base_idx if self.file_name is not None: y_data = np.array([0.0, 1.0]) image = Image.open(self.file_name) elif index >= len(self.cat_fnames): # dog x_data = self.dog_fnames[index - len(self.cat_fnames)] y_data = np.array([0.0, 1.0]) img_path = os.path.join(self.dogs_dir, x_data) image = Image.open(img_path) # plt.imshow(image) # print(y_data) # plt.show() else: # cat x_data = self.cat_fnames[index] y_data = np.array([1.0, 0.0]) img_path = os.path.join(self.cats_dir, x_data) image = Image.open(img_path) # plt.imshow(image) # print(y_data) # plt.show() if self.transform: image = tf.keras.preprocessing.image.img_to_array(image) image = image.astype('float32') image /= 255.0 image = self.transform(image) # plt.imshow(image) # plt.show() if self.target_transform: y_data = self.target_transform(y_data) return image, y_data def __len__(self): """ 데이터셋의 샘플 개수를 반환합니다. """ return self.total_len class CustomDataloader(Sequence): def __init__(self, _dataset, batch_size=1, shuffle=False): self.dataset = _dataset self.batch_size = batch_size self.total_len = math.ceil(len(self.dataset) / self.batch_size) self.shuffle = shuffle self.indexer = np.arange(len(self.dataset)) self.on_epoch_end() def __getitem__(self, index): indexer = self.indexer[index * self.batch_size:(index + 1) * self.batch_size] batch_x = [self.dataset[i][0] for i in indexer] batch_y = [self.dataset[i][1] for i in indexer] batch_x = np.array(batch_x) batch_y = np.array(batch_y) return batch_x, batch_y def __len__(self): return self.total_len def on_epoch_end(self): if self.shuffle: np.random.shuffle(self.indexer) """ # https://keras.io/api/layers/preprocessing_layers/image_augmentation/ https://www.tensorflow.org/tutorials/images/data_augmentation?hl=ko 참고: data_augmentation 데이터 증강은 테스트할 때 비활성화되므로 입력 이미지는 model.fit(model.evaluate 또는 model.predict가 아님) 호출 중에만 증강됩니다. RandomCrop layer RandomFlip layer RandomTranslation layer RandomRotation layer RandomZoom layer RandomHeight layer RandomWidth layer RandomContrast layer RandomBrightness layer """ val_processing = tf.keras.Sequential([ layers.Resizing(IMG_SIZE, IMG_SIZE), layers.CenterCrop(IMG_SIZE, IMG_SIZE), layers.Normalization(mean=[0.485, 0.456, 0.406], variance=[0.229, 0.224, 0.225]), ]) train_data_augmentation = tf.keras.Sequential([ layers.RandomFlip("horizontal_and_vertical"), val_processing, ]) if __name__ == '__main__': train_dataset = CustomDataset("cats_and_dogs_filtered/train", transform=train_data_augmentation) val_dataset = CustomDataset("cats_and_dogs_filtered/validation", transform=val_processing) train_loader = CustomDataloader(_dataset=train_dataset, batch_size=batch_size, shuffle=True) val_loader = CustomDataloader(_dataset=val_dataset, batch_size=batch_size, shuffle=True) # https://keras.io/api/layers/ model = keras.Sequential() model.add(keras.Input(shape=(IMG_SIZE, IMG_SIZE, 3))) model.add(layers.Conv2D(16, kernel_size=3, activation='relu')) model.add(layers.MaxPooling2D(pool_size=(8, 8))) model.add(layers.Flatten()) model.add(layers.Dense(512, activation='relu')) model.add(layers.Dense(2, activation='relu')) model.summary() # https://keras.io/api/optimizers/ # https://keras.io/api/losses/ INPUT_OPTIMIZER = tf.keras.optimizers.SGD(learning_rate=lr) LOSS = tf.keras.losses.MeanSquaredError(reduction="auto", name="mean_squared_error") model.compile(optimizer=INPUT_OPTIMIZER, loss=LOSS) model.fit(train_loader, batch_size=batch_size, epochs=n_epochs, validation_data=val_loader) model.save("model.keras")
훈련 결과
total images:2000 total images:1000 Model: "sequential_2" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= conv2d (Conv2D) (None, 148, 148, 16) 448 max_pooling2d (MaxPooling2D (None, 18, 18, 16) 0 ) flatten (Flatten) (None, 5184) 0 dense (Dense) (None, 512) 2654720 dense_1 (Dense) (None, 2) 1026 ================================================================= Total params: 2,656,194 Trainable params: 2,656,194 Non-trainable params: 0 _________________________________________________________________ Epoch 1/20 100/100 [==============================] - 29s 293ms/step - loss: 0.3131 - val_loss: 0.2775 Epoch 2/20 100/100 [==============================] - 32s 318ms/step - loss: 0.2806 - val_loss: 0.2635 Epoch 3/20 100/100 [==============================] - 61s 607ms/step - loss: 0.2680 - val_loss: 0.2556 Epoch 4/20 100/100 [==============================] - 47s 469ms/step - loss: 0.2581 - val_loss: 0.2543 Epoch 5/20 100/100 [==============================] - 33s 327ms/step - loss: 0.2535 - val_loss: 0.2477 Epoch 6/20 100/100 [==============================] - 32s 324ms/step - loss: 0.2474 - val_loss: 0.2436 Epoch 7/20 100/100 [==============================] - 33s 326ms/step - loss: 0.2450 - val_loss: 0.2413 Epoch 8/20 100/100 [==============================] - 32s 320ms/step - loss: 0.2425 - val_loss: 0.2379 Epoch 9/20 100/100 [==============================] - 32s 319ms/step - loss: 0.2360 - val_loss: 0.2377 Epoch 10/20 100/100 [==============================] - 33s 333ms/step - loss: 0.2353 - val_loss: 0.2386 Epoch 11/20 100/100 [==============================] - 34s 342ms/step - loss: 0.2317 - val_loss: 0.2347 Epoch 12/20 100/100 [==============================] - 32s 320ms/step - loss: 0.2326 - val_loss: 0.2316 Epoch 13/20 100/100 [==============================] - 33s 329ms/step - loss: 0.2298 - val_loss: 0.2310 Epoch 14/20 100/100 [==============================] - 32s 321ms/step - loss: 0.2297 - val_loss: 0.2395 Epoch 15/20 100/100 [==============================] - 32s 320ms/step - loss: 0.2215 - val_loss: 0.2326 Epoch 16/20 100/100 [==============================] - 32s 325ms/step - loss: 0.2241 - val_loss: 0.2275 Epoch 17/20 100/100 [==============================] - 34s 336ms/step - loss: 0.2231 - val_loss: 0.2281 Epoch 18/20 100/100 [==============================] - 32s 323ms/step - loss: 0.2217 - val_loss: 0.2266 Epoch 19/20 100/100 [==============================] - 31s 308ms/step - loss: 0.2207 - val_loss: 0.2265 Epoch 20/20 100/100 [==============================] - 31s 311ms/step - loss: 0.2192 - val_loss: 0.2249
Test
이제는 원하는 이미지를 넣어서 어떤 결과가 나오는지 테스트 해볼 시간입니다.
import tensorflow as tf import trainer if __name__ == '__main__': model = tf.keras.models.load_model("model.keras") model.summary() train_dataset = trainer.CustomDataset("cats_and_dogs_filtered/train", transform=trainer.val_processing) val_dataset = trainer.CustomDataset("cats_and_dogs_filtered/validation", transform=trainer.val_processing) train_loader = trainer.CustomDataloader(_dataset=train_dataset, batch_size=1, shuffle=True) val_loader = trainer.CustomDataloader(_dataset=val_dataset, batch_size=1, shuffle=True) # test test_images = ["cats_and_dogs_filtered/train/cats/cat.1.jpg", "cats_and_dogs_filtered/train/cats/cat.2.jpg", "cats_and_dogs_filtered/train/dogs/dog.1.jpg", "cats_and_dogs_filtered/train/dogs/dog.2.jpg", "cats_and_dogs_filtered/validation/cats/cat.2001.jpg", "cats_and_dogs_filtered/validation/dogs/dog.2001.jpg", "cats_and_dogs_filtered/validation/cats/cat.2004.jpg", "cats_and_dogs_filtered/validation/dogs/dog.2004.jpg", ] for test_image in test_images: print(test_image) dataset = trainer.CustomDataset("cats_and_dogs_filtered/train", transform=trainer.val_processing, file_name=test_image) dataloader = trainer.CustomDataloader(_dataset=dataset, batch_size=1, shuffle=False) x_prd = dataloader[0][0] # batch 0 , x print(x_prd.shape) y = model.predict(x_prd) print("result:", y) if y[0][0] > y[0][1]: print("cat") else: print("dog")
실행 결과
Model: "sequential_2" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= conv2d (Conv2D) (None, 148, 148, 16) 448 max_pooling2d (MaxPooling2D (None, 18, 18, 16) 0 ) flatten (Flatten) (None, 5184) 0 dense (Dense) (None, 512) 2654720 dense_1 (Dense) (None, 2) 1026 ================================================================= Total params: 2,656,194 Trainable params: 2,656,194 Non-trainable params: 0 _________________________________________________________________ total images:2000 total images:1000 cats_and_dogs_filtered/train/cats/cat.1.jpg (1, 150, 150, 3) 1/1 [==============================] - 0s 60ms/step result: [[0.48046866 0.43707907]] cat cats_and_dogs_filtered/train/cats/cat.2.jpg (1, 150, 150, 3) 1/1 [==============================] - 0s 12ms/step result: [[0.74900883 0.4924001 ]] cat cats_and_dogs_filtered/train/dogs/dog.1.jpg (1, 150, 150, 3) 1/1 [==============================] - 0s 12ms/step result: [[0.45707452 0.7523619 ]] dog cats_and_dogs_filtered/train/dogs/dog.2.jpg (1, 150, 150, 3) 1/1 [==============================] - 0s 12ms/step result: [[0.15739511 0.7101962 ]] dog cats_and_dogs_filtered/validation/cats/cat.2001.jpg (1, 150, 150, 3) 1/1 [==============================] - 0s 10ms/step result: [[0.63321704 0.2585028 ]] cat cats_and_dogs_filtered/validation/dogs/dog.2001.jpg (1, 150, 150, 3) 1/1 [==============================] - 0s 12ms/step result: [[0.1274486 0.65480846]] dog cats_and_dogs_filtered/validation/cats/cat.2004.jpg (1, 150, 150, 3) 1/1 [==============================] - 0s 11ms/step result: [[0.73557734 0.3226891 ]] cat cats_and_dogs_filtered/validation/dogs/dog.2004.jpg (1, 150, 150, 3) 1/1 [==============================] - 0s 12ms/step result: [[0.48342577 0.7902105 ]] dog
cat 사진을 넣었을때 cat이 제대로 나오는지, dog를 넣었을때 dog가 제대로 나오는지 보면됩니다. 위 결과로는 모두 제대로 나오고 있습니다.
사실 Pytorch와 비슷하게 RandomZoom을 넣었었는데... (Pytorch에서는 RandomResize항목이 있었음) 학습이 제대로 안되는 경우가 많아서 해당 부분을 뺐습니다.
sourcecode/keras/02_image_class at main · donarts/sourcecode · GitHub
댓글 없음:
댓글 쓰기