1. 1주차 - EDA, Data Augmentation, Data Balancing
https://deepdeepit.tistory.com/164
2. 2주차 - Image Classification (K-Means, XGBoost)
https://deepdeepit.tistory.com/170
3. 3주차 문제 (Problems) - 데이터 전처리와 모델 학습 및 평가
3주차에서는 학습을 하기 위해서 데이터를 알맞은 형태로 전처리한 후 모델을 디자인(설계)하고 학습합니다. 학습된 모델을 평가하여 높은 정확도(Accuracy)와 점수를 받을 수 있도록 튜닝하고 모델을 수정합니다.
1주차 대회 소개에서 말씀드렸던 최종 목표는 일러스트 이미지를 최종 20개의 클래스로 분류하는 분류기(Classifier)를 만드는 것이 최종 목표입니다. 이 대회에서는 Accuracy가 아닌 F1 Score를 최종 점수로 택합니다.
$\text{Precision} = \frac{\text{True Positive}}{\text{True Positive}+\text{False Positive}}$
$\text{Recall} = \frac{\text{True Positive}}{\text{True Positive}+\text{False Negative}}$
F1 Score는 Precision과 Recall의 조화평균입니다.
$\text{F1 Score}=2\times(\frac{1}{\text{Precision}} + \frac{1}{\text{Recall}})^{-1}$
와 같이 구할 수 있습니다. F1 Score는 0 이상, 1 이하의 값을 가지며 1에 가까울수록 정확하다는 의미입니다.
Precision, Recall, F1 Score에 대한 더 자세한 정보는 아래 블로그를 참고하시면 됩니다.
https://blog.naver.com/PostView.nhn?isHttpsRedirect=true&blogId=wideeyed&logNo=221531940245
4. 아이디어 (Main Idea)
저희는 전처리 이전 Augmentation 단계에서 다양한 방식의 Augmentation 알고리즘을 도입했습니다.
전처리 알고리즘의 경우 1주차 EDA 과정에서 드러났던 문제인 아래 문제를 해결해야 합니다.
- 파일 형식(확장자)이 다름
- 이미지 색상맵(color map)이 다름
- 채널 차원수가 다름
- 이미지 너비와 높이(규격)가 다름
따라서, 위에 나열한 차이를 하나로 통일해야 합니다.
5. Data 전처리 (Preprocessing) baseline code
sampledf = dftest.sample(n=1)
display(sampledf)
label = list(sampledf['label'])[0]
imgdir = f"{list(sampledf['img'])[0]}.{list(sampledf['ftype'])[0]}"
print(label, imgdir)
dsize = (400, 400)
img = Image.open(op(base_dir, label, imgdir))
먼저 전처리할 이미지를 읽습니다.
def remove_alpha(img, isprint=True):
img = np.array(img)
h, w, c = img.shape
for i in range(h):
for j in range(w):
if img[i, j, 3] < 30:
img[i, j, :] = 0
if isprint:
plt.imshow(img)
plt.show()
return img
if img.mode == 'RGBA':
img = remove_alpha(img, isprint=False)
img = Image.fromarray(img)
img = img.convert('L')
img = img.convert('RGB')
이미지의 컬러맵이 RGBA라면 alpha값이 100인 투명한 부분의 각 R, G, B값을 모두 0으로 치환합니다.
for i in range(3):
img = img.filter(ImageFilter.SHARPEN)
이미지의 특성을 강조하도록 Sharpening 필터를 적용해줍니다.
img = np.array(img)
img = cv2.resize(img, (400, 400))
h, w, *_ = img.shape
plt.imshow(img)
plt.show()
이미지를 특정 사이즈로 변환합니다.
def getannotation(img, threshold=30, isprint=True):
thresholdarr = np.array([threshold, threshold, threshold])
# top
x, y, c = img.shape
x1 = 0
for i in range(x):
if np.all(abs(img[i, 0, :] - img[i, :, :]) <= thresholdarr):
continue
else:
x1 = i - 1
break
if x1 == -1: x1 = 0
# bottom
x, y, c = img.shape
x2 = x - 1
for i in range(x):
if np.all(abs(img[x-i-1, 0, :] - img[x-i-1, :, :]) <= thresholdarr):
continue
else:
x2 = x-i
break
if x2 == x: x2 = x - 1
# left
x, y, c = img.shape
y1 = 0
for j in range(y):
if np.all(abs(img[0, j, :] - img[:, j, :]) <= thresholdarr):
continue
else:
y1 = j - 1
break
if y1 == -1: y1 = 0
# right
x, y, c = img.shape
y2 = y - 1
for j in range(y):
if np.all(abs(img[0, y-j-1, :] - img[:, y-j-1, :]) <= thresholdarr):
continue
else:
y2 = y-j
break
if y2 == y: y2 = y - 1
if isprint:
fig, ax = plt.subplots()
imgplot = ax.imshow(img)
ax.add_patch(
patches.Rectangle(
(y1, x1), # (x, y)
(y2 - y1), (x2 - x1), # width, height
edgecolor = 'deeppink',
#facecolor = 'lightgray',
fill=False))
return x1, y1, x2, y2
변환된 이미지에서 Object(물체)를 Detect(감지)하여 Annotation Coordinate를 뽑아냅니다.
위 사진을 보면 사진 안에서 물체의 위치를 분홍색 사각형으로 표시하였습니다. 3번째 사진의 경우 배경이 존재하여 이미지의 크기와 분홍색 사각형이 일치합니다.
def get_background_color(img, annotation):
x1, y1, x2, y2 = annotation
h, w, c = img.shape
xleft = (h - (x2 - x1))
yleft = (w - (y2 - y1))
if xleft > yleft:
if x1 < 4:
part = img[h-4:h, w//2 - 2 : w//2 + 2, :]
elif x2 > h - 4:
part = img[0:4, w//2 - 2 : w//2 + 2, :]
else:
part = (img[0:4, w//2 - 2 : w//2 + 2, :] / 2. + img[h-4:h, w//2 - 2 : w//2 + 2, :] / 2.).astype(np.uint8)
else:
if y1 < 4:
part = img[h//2 - 2 : h//2 + 2, w-4:w, :]
elif y2 > w - 4:
part = img[h//2 - 2 : h//2 + 2, 0:4, :]
else:
part = (img[h//2 - 2 : h//2 + 2, 0:4, :] / 2. + img[h//2 - 2 : h//2 + 2, w-4:w, :] / 2.).astype(np.uint8)
# white
if np.mean(part) > 240.:
return 0
# black
elif np.mean(part) < 15:
return 1
else: # else
return 2
이미지의 배경색을 가져오는 함수입니다.
def erase_background(img, rectangle, isprint=True):
h, w, c = img.shape
# rectangle = (0, 0, h - 1, w - 1)
mask = np.zeros(img.shape[:2], np.uint8)
# bgdModel = np.zeros((1, 65), np.float64)
# fgdModel = np.zeros((1, 65), np.float64)
bgdModel = np.zeros((1, 65), np.float64)
fgdModel = np.zeros((1, 65), np.float64)
# grabCut 실행
cv2.grabCut(img, # 원본 이미지
mask, # 마스크
rectangle, # 사각형
bgdModel, # 배경을 위한 임시 배열
fgdModel, # 전경을 위한 임시 배열
1, # 반복 횟수
cv2.GC_INIT_WITH_RECT) # 사각형을 위한 초기화
# 배경인 곳은 0, 그 외에는 1로 설정한 마스크 생성
mask_2 = np.where((mask==2) | (mask==0), 0, 1).astype('uint8')
# 이미지에 새로운 마스크를 곱행 배경을 제외
image_rgb_nobg = img * mask_2[:, :, np.newaxis]
# plot
if isprint:
plt.imshow(image_rgb_nobg)
return image_rgb_nobg
이미지의 배경을 제거하는 함수입니다. 일러스트 이미지의 경우 대부분이 흰색 또는 검은색 배경이지만 예외가 있는 (3번째 요트 일러스트처럼) 이미지의 배경을 제거하기 위해 OpenCV의 GrabCut 함수를 사용했습니다.
def color_inversion(img, isprint=True):
img_rev = [255, 255, 255] - img
if isprint:
plt.imshow(img_rev, cmap='gray')
plt.show()
return img_rev
사진의 전체 색을 반전하는 함수입니다.
print("background color is", ['white', 'black', 'other'][backcolor])
if backcolor == 0: # white
img = color_inversion(img, isprint=True)
elif backcolor == 1: # black
pass
else: # backcolor == -1:
img = erase_background(img, rectangle=(x1, y1, x2-1, y2-1))
일러스트 이미지의 경우 배경이 대부분 흰색(255, 255, 255) 또는 검은색(0, 0, 0)이므로 배경을 하나로 통일하기 위해 배경이 흰색인 경우 색 반전을 통해 배경을 검은색으로 변환합니다.
x1, y1, x2, y2 = getannotation(img)
img = img[x1:x2, y1:y2, :]
위에서 작성한 Object Detect 함수를 통해 물체의 좌표를 다시 받아옵니다.
def makesquere(img, isprint=True):
h, w, c = img.shape
if h == w:
timg = img
elif h > w:
padding1 = np.zeros(shape=(h, (h - w) // 2, 3), dtype=np.int8)
padding2 = np.zeros(shape=(h, h - w - (h - w) // 2, 3), dtype=np.int8)
timg = np.concatenate((padding1, img, padding2), axis=1)
else: # h < y
padding1 = np.zeros(shape=((w - h) // 2, w, 3), dtype=np.int8)
padding2 = np.zeros(shape=(w - h - (w - h) // 2, w, 3), dtype=np.int8)
timg = np.concatenate((padding1, img, padding2), axis=0)
if isprint:
plt.imshow(timg)
return timg
오브젝트의 Annotation을 모두 포함하면서 사진의 크기를 정사각형으로 변경하는 함수입니다.
img = makesquere(img, isprint=True)
img = img.astype(np.uint8)
final = cv2.resize(img, dsize)
# cv2.imwrite(savedir, final)
plt.imshow(final)
plt.show()
이러한 과정을 모두 거치면
모든 종류의 이미지가 이렇게 통일되게 됩니다. 배경도 전부 제거되었고, 오브젝트 주변에 공백(margin)도 최소화하였으며 컬러맵(color map)과 파일 형식, 채널 수(grayscale)도 모두 통일했습니다.
6. NVIDIA CUDA/CuDNN
i) CUDA/CuDNN 설치
저희는 아래 기술하겠지만 총 4대의 NVIDIA GTX/RTX 계열의 GPU를 사용하였습니다. 이러한 GPU는 CUDA와 CuDNN 라이브러리를 사용할 수 있습니다. 이를 사용하면 딥러닝의 처리 속도가 비약적으로 향상하게 됩니다. 딥러닝은 많은 양의 사칙연산을 수행하게 되는데, 단순 사칙연산은 병렬화가 쉽고 여러 코어를 가진 GPU에서 더욱 빠르게 계산이 가능하기 때문입니다. 따라서 이러한 병렬화 연산을 GPU를 사용해 학습할 수 있도록 지원해주는 것이 바로 CUDA와 CuDNN입니다. 이는 NVIDIA GTX/RTX 계열의 GPU에만 적용이 가능합니다.
CUDA와 CuDNN 설치에 대한 자세한 정보는 아래 글에서 확인할 수 있습니다.
https://deepdeepit.tistory.com/129
ii) GPU 관련 이슈들
저희가 학습을 진행할 때 GPU 관련해서 여러 이슈가 있었습니다.
먼저 GPU Out of Memory 문제입니다. GPU에는 각각 사용할 수 있는 메모리가 있는데 CUDA를 통해 학습하게 되고, 학습이 비정상적으로 종료되게 되면 램이 비워지지 않는 문제입니다. 이 경우는 학습할 때 batch 사이즈를 너무 크게 해서 컴퓨터의 RAM(메모리)를 가득 채우게 되며 Overflow가 발생했을 때 주로 나타납니다.
이러한 경우 런타임 메모리를 필요한 만큼 할당하거나, GPU에 할당되는 메모리 크기에 제한을 거는 방법이 있습니다.
런타임 메모리를 필요한 만큼만 할당
gpus = tf.config.experimental.list_physical_devices('GPU')
if gpus:
try:
# Currently, memory growth needs to be the same across GPUs
for gpu in gpus:
tf.config.experimental.set_memory_growth(gpu, True)
logical_gpus = tf.config.experimental.list_logical_devices('GPU')
print(len(gpus), "Physical GPUs,", len(logical_gpus), "Logical GPUs")
except RuntimeError as e:
# Memory growth must be set before GPUs have been initialized
print(e)
GPU에 할당 가능한 메모리 크기를 제한
gpus = tf.config.experimental.list_physical_devices('GPU')
if gpus:
# Restrict TensorFlow to only allocate 1GB of memory on the first GPU
try:
tf.config.experimental.set_virtual_device_configuration(
gpus[0],
[tf.config.experimental.VirtualDeviceConfiguration(memory_limit=1024)])
logical_gpus = tf.config.experimental.list_logical_devices('GPU')
print(len(gpus), "Physical GPUs,", len(logical_gpus), "Logical GPUs")
except RuntimeError as e:
# Virtual devices must be set before GPUs have been initialized
print(e)
출처 : https://inpages.tistory.com/155
위 코드를 사용하면 메모리 Overflow 문제 없이 학습을 할 수 있습니다. 물론 그 전에 PC에 메모리에 알맞은 Batch 사이즈를 지정해주는 것 또한 중요합니다.
또한, 저희는 GPU가 두 대 이상 달린 Multi-GPU 환경에서 학습을 진행하였습니다. 이러면 동시에 두 개 이상의 학습을 각각 하나의 GPU에 매칭하여 학습을 진행할 수 있습니다. 물론 그만큼 메모리(RAM)이 많이 필요하겠지만요. 이럴 때 GPU를 수동으로 선택해줄 수 있는 코드는 아래와 같습니다.
from tensorflow.python.client import device_lib
device_lib.list_local_devices()
[name: "/device:CPU:0"
device_type: "CPU"
memory_limit: 268435456
locality {
}
incarnation: 6302798070552010975
xla_global_id: -1,
name: "/device:GPU:0"
device_type: "GPU"
memory_limit: 5486477312
locality {
bus_id: 1
links {
}
}
incarnation: 10870881188267820007
physical_device_desc: "device: 0, name: NVIDIA GeForce RTX 3070, pci bus id: 0000:01:00.0, compute capability: 8.6"
xla_global_id: 416903419]
os.environ["CUDA_VISIBLE_DEVICES"] = "0"
위 PC는 RTX3070가 하나 탑재된 PC여서 하나만 뜨지만 만약 두 개 이상이라면 name이 "/device:GPU:1"
처럼 0 초과하여 뜨게 됩니다. 맨 아래 파이썬 코드에 숫자를 문자열형(string type)으로 지정해주면 됩니다. (ex = "0"
, = "1"
, ...)
7. 학습 준비 (Preparing before Training)
i) Data Loader Module
저희 팀은 이전 글에서 보셨다시피 데이터에 대한 메타 데이터를 CSV 파일로 저장합니다. 또한, 학습에 사용할 전처리된 데이터(preprocessed data)는 따로 특정 경로 내의 폴더에 저장되어 있습니다. 이를 데이터 오브젝트로 로딩하기 위해서 따로 모듈을 만들어서 사용하였습니다.
from dataloader import load
dfdir = "./adata_no_mix_df2.csv"
pdir = "/home/dmsai2/Desktop/DCC2022/adata_no_mix2"
pdir2 = "/home/dmsai2/Desktop/DCC2022/ddata2"
dsize = (28, 28)
# load preprocessed data
X_train, X_test, y_train, y_test, X_test2, y_test2, error_list = load(dfdir,
pdir,
pdir2,
dsize,
train_rate=0.9,
isprint=True,
all_illust=True,
smoothing=False,
smoothing_val=0.1)
vdir = "/home/dmsai2/Desktop/DCC2022/vdata/"
# load validation 931 dataset (validation only)
X_test2, y_test2 = load2(vdir=vdir)
# dfdif : 라벨링, 이미지 데이터 포함된 dataframe csv 파일 경로
# pdir : 전처리된 데이터 경로
# pdir2 : week2에서 빠진 242개의 데이터의 경로
# dsize : 불러올 때 조정할 이미지 데이터의 사이즈
# train_rate : train, test split 할 때의 train set의 비율
# isprint : 로그 출력 여부
# all_illust : week2에서 빠진 242개의 데이터를 test set에 포함 여부
# smoothing : data label smoothing 여부
# smoothing_val : label smoothing 시 smooth 값
전처리 데이터 종류, label smoothing 여부, 이미지 데이터의 사이즈 등 여러 파라메터를 인수로 넘겨서 간단하게 다양한 실험을 할 수 있도록 하였습니다.
ii) 데이터셋 종류
저희는 Augmentation과 Preprocessing을 여러 종류로 조합하여 실험을 진행하였습니다.
2주차에서 설명드린 내용과 같이 Augmentation을 할 때 라벨링이 One-Hot Encoding (원소가 1 한 개, 나머지 0)이 아니라 [0.7, 0.3]
과 같이 두 개 이상의 다른 클래스가 섞여 있는 데이터셋에 대하여 Mixed 데이터셋이라고 부르고, 여집합의 경우는 Unmixed 데이터셋이라고 불렀습니다. 또한, Augmenation을 할 때 Basic Augmentation만으로 증강하거나, Slice Mix만으로 증강하는 등 여러 종류의 데이터셋을 준비하고 학습하며 실험하였습니다.
8. 학습 환경 (Training Environment)
저희는 아래와 같은 총 4대의 서버PC에서 학습을 진행했습니다.
메인 PC (1대) | 서브 PC 1, 2 (2대) | 서브 PC 3 (1대) |
CPU :Intel i5 GPU : RTX 3070 Ti 8GB RAM : 32GB (16GB * 2) SSD : 256GB / HDD : 1TB |
CPU : 8-Core Intel Xeon E5-2620v4 GPU : GTX 1080 Ti 8GB * 2 RAM : 64GB (16GB * 4) SSD : 256GB / HDD : 1TB |
CPU : 8-Core Intel Xeon E5-2620v4 GPU : GTX Titan X Pascal D5X 12GB * 2 RAM : 128GB (32GB * 4) SSD : 256GB / HDD : 4TB |
9. 모델 설계 (Design Model)
저희는 최종 목표인 일러스트 사진을 총 20개의 클래스로 구분하기 위해서 아래 3가지 모델을 주로 사용했습니다.
i) CNN baseline (Convolutional Neural Network)
https://arxiv.org/abs/1511.08458
CNN은 이미지 처리를 위한 가장 최초의 딥러닝 네트워크이며 가장 간단하지만, 현재까지 쓰일만큼 강력하고 효율적인 딥러닝 네트워크 모델입니다.
컴퓨터 비전과 인공지능을 조금 공부하셨던 분이라면 누구나 알법한 합성곱층(Convolutional Layer)과 풀링층(Pooling Layer)로 구성되어 있는 간단한 구조입니다. 자세한 수학적 원리를 알고싶으시다면 아래 블로그를 참고해주세요.
https://excelsior-cjh.tistory.com/180
ii) ResNet (Deep Residual Learning Network)
https://arxiv.org/abs/1512.03385
ResNet은 기존 모델들의 문제점인 모델의 깊이(layer의 수)가 깊어질수록 성능이 떨어지는 문제를 해결한 모델입니다. 이는 Gradient Vanishing, Exploding 문제 때문인데 layer가 깊어지면 미분에 미분을 계속 하기 때문에 역전파(Backpropagation)를 통해 학습하는 과정에서 위쪽 layer까지 영향이 덜 가게 된다고 합니다. 이를 극복하기 위해서 ResNet이 개발되었고, Skip Connection(일부 레이어를 건너뛰는)를 이용한 Residual Learning을 통해 Layer가 깊어짐에 따라 나타나는 Gradient Vanishing을 해결하였다고 합니다.
자세한 내용은 아래 블로그 글을 참고하시면 좋습니다.
https://ganghee-lee.tistory.com/41
iii) EfficientNet
https://arxiv.org/abs/1905.11946
EfficientNet은 Image Classification 분야에서 기존의 네트워크(ResNet, Inception, Xception, SENet 등)보다 훨씬 적은 파라메터 개수를 가지고 있음에도 더욱 좋은 성능을 달성한 모델입니다.
간단하게 설명하자면 모델의 성능을 늘리기 위해 3가지의 Scale-Up 방식을 통해 모델의 성능을 개선할 수 있는데
1) Depth Scale-Up : 모델 네트워크의 depth를 깊게 하는 것 (layer의 개수를 늘림)
2) Channel Width Scale-Up : CNN 네트워크 중 filter의 channel 수(width)를 넓게 할수록 필터가 더욱 미세한 정보를 걸러낼 수 있음
3) Input Image Resolution Scale-Up : 입력 이미지의 해상도를 높이면 성능이 증가
와 같은 세 가지 방법에 대한 최적의 조합을 AutoML을 통해 찾은 모델입니다. 자세한 내용은 아래 블로그 글을 참고하시면 좋습니다.
https://lynnshin.tistory.com/53
iv) Label Smoothing
Label Smoothing은 딥러닝의 신뢰도를 개선하기 위한 모델 보정(Model Calibration) 기법 중 하나입니다. Label Smoothing은 One-Hot Encoding (0 또는 1)으로 구성된 라벨을 Soft Label (0 ~ 1 사이)으로 smoothing하는 것입니다.
$K$개의 클래스에 대해, Smoothing Parameter를 $\alpha$라고 할 때, $k$번째 클래스에 대해서 아래와 같이 스무딩을 합니다.
$y_{k}^{LS} = y_k{(1-\alpha) + \frac{\alpha}{K}}$
예를 들어, 클래스가 5개이고($K=5$), 2번째 클래스가 정답인 라벨은 아래와 같아집니다. ($\alpha=0.1$이라고 가정)
Hard Label : [0, 1, 0, 0, 0]
Soft Label (Smoothing) : [0.02, 0.92, 0.02, 0.02, 0.02]
Label Smoothing에 대한 자세한 정보는 아래 블로그 글을 참고하시면 되겠습니다.
https://blog.si-analytics.ai/21
저희는 위에 소개한 DataLoader에 Label Smoothing 적용 기능을 넣어 여러 테스트를 해보았습니다.
v) Matrics
저희는 최종적으로 F1 Score를 높게 만들어야 합니다. 하지만 대부분의 모델은 Accuracy를 목표로 학습합니다. 따라서 모델이 잘 학습되고 있는지 알려줄 Metrics에 F1 Score를 추가하여 줍니다.
def get_f1(y_true, y_pred): #taken from old keras source code
true_positives = K.sum(K.round(K.clip(y_true * y_pred, 0, 1)))
possible_positives = K.sum(K.round(K.clip(y_true, 0, 1)))
predicted_positives = K.sum(K.round(K.clip(y_pred, 0, 1)))
precision = true_positives / (predicted_positives + K.epsilon())
recall = true_positives / (possible_positives + K.epsilon())
f1_val = 2*(precision*recall)/(precision+recall+K.epsilon())
return f1_val
위 함수를 학습하기 전 metrics을 지정하는 부분에 함수 포인터를 인수로 넘기면 실시간으로 F1 Score를 볼 수 있습니다. 아래 코드와 같이 Model을 Compile할 때 metrics에 이터레이션 타입(iteration type) 안에 넣어주면 됩니다.
with tf.device(f'/device:GPU:{gpunum}'):
model.compile(optimizer=keras.optimizers.Adam(1e-4),
loss='categorical_crossentropy',
metrics=['accuracy', 'mse', 'KLDivergence', get_f1])
vi) Tensorboard
실시간으로 Loss와 Accuracy, Metrics(F1 Score 포함)들을 관찰하고 추적하기 위해 Tensorboard를 사용했습니다. Tensorboard는 어느 모델도 사용하기 쉽게 만들어져 있습니다.
설치하기
따로 설치하지 않아도 Tensorflow를 설치할 때 포함되는 경우도 있습니다.
python3 -m pip install tensorboard // 또는
pip install tensorboard
사용하기
from tensorflow.python.keras.callbacks import TensorBoard # import library
tensorboard = TensorBoard(log_dir="logs/{}".format(datetime.now())) # Object 생성
위처럼 라이브러리를 로드한 후 tensorboard 오브젝트를 생성합니다.
history = model.fit(X_train, y_train,
batch_size=8,
epochs=100,
validation_data=(X_val, y_val),
callbacks=[tensorboard])
만든 모델을 학습(fitting)할 때 callbacks 매개변수에 각 함수의 이름을 문자열 형을 원소로 가진 iteration 타입(예를 들면, 리스트형)으로 넘겨주면 됩니다. 쉽게 생각해서 위처럼 tensorboard 오브젝트를 리스트형에 담아 넘기면 됩니다.
Tensorboard를 열어보는 방법은 로그 폴더가 있는 위치로 가서
tensorboard --logdir=./logs/
명령어를 Shell에 입력하면 자동으로 localhost:6006과 같이 내부 서버가 호스트되게 됩니다. 만약, 외부에서도 접속하고 싶다면 해당 포트를 방화벽에서 허용해주고 아래처럼 입력하면 됩니다.
sudo ufw allow 6006 // 6006 포트 방화벽 허용
tensorboard --logdir=./logs/ --bind_all --port=6006
위처럼 입력하면 외부에서도 해당 IP의 IP나 도메인 주소로 접속이 가능합니다. (물론 포트도 지정 가능합니다)
10. 모델 구현 및 학습
여기서는 가장 성능이 좋았던 ResNet (17층)에 대해 코드를 설명해보겠습니다. 위 사진은 ResNet 34층이지만 저희는 ImageNet 데이터셋과 같이 큰 데이터셋을 사용하는 것이 아니므로 층 수를 절반으로 줄인 17층짜리를 사용하도록 하겠습니다.
데이터 종류와 Loss 함수 종류 등과 같이 파라메터는 일부 변수를 수정함으로써 쉽게 변경할 수 있기 때문입니다.
i) Import Libraries
import numpy as np
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
from keras import backend as K
from keras.layers import Layer
from keras import models
from keras import layers
from keras import optimizers
from sklearn.model_selection import train_test_split
from warnings import filterwarnings
filterwarnings(action='ignore')
import os
import matplotlib.pyplot as plt
import seaborn as sns
ii) GPU Setting
# GPU Device Setting
from tensorflow.python.client import device_lib
device_lib.list_local_devices()
학습할 때 사용할 GPU Device 목록을 불러옵니다.
[name: "/device:CPU:0"
device_type: "CPU"
memory_limit: 268435456
locality {
}
incarnation: 12636797320459375714
xla_global_id: -1,
name: "/device:GPU:0"
device_type: "GPU"
memory_limit: 3872718848
locality {
bus_id: 1
links {
}
}
incarnation: 7264430425705365010
physical_device_desc: "device: 0, name: NVIDIA GeForce RTX 3070, pci bus id: 0000:01:00.0, compute capability: 8.6"
xla_global_id: 416903419]
os.environ["CUDA_VISIBLE_DEVICES"] = "0"
사용할 GPU 번호를 0번으로 지정합니다.
gpus = tf.config.experimental.list_physical_devices('GPU')
if gpus:
try:
# Currently, memory growth needs to be the same across GPUs
for gpu in gpus:
tf.config.experimental.set_memory_growth(gpu, True)
logical_gpus = tf.config.experimental.list_logical_devices('GPU')
print(len(gpus), "Physical GPUs,", len(logical_gpus), "Logical GPUs")
except RuntimeError as e:
# Memory growth must be set before GPUs have been initialized
print(e)
1 Physical GPUs, 1 Logical GPUs
GPU 메모리 Overflow를 방지하기 위해 메모리 할당량을 지정해줍니다. (위에서 GPU 관련 이슈로 설명)
iii) ResNet Class (Classifier)
import tensorflow as tf
from tensorflow.keras import layers, Model, Sequential
class ResidualUnit(keras.layers.Layer):
def __init__(self, filters, strides=1, activation='leaky_relu', **kwargs):
super().__init__(**kwargs)
self.activation = keras.activations.get(activation)
self.main_layers = [keras.layers.Conv2D(filters,3,strides=strides,
kernel_initializer = 'he_normal',
kernel_regularizer = regularizers.l2(0.001),
padding='same',use_bias=False),
keras.layers.BatchNormalization(),
self.activation,
keras.layers.Conv2D(filters,3,strides=1,
kernel_initializer = 'he_normal',
kernel_regularizer = regularizers.l2(0.001),
padding='same',use_bias=False),
keras.layers.BatchNormalization(),
self.activation,
keras.layers.Conv2D(filters,3,strides=1,
kernel_initializer = 'he_normal',
kernel_regularizer = regularizers.l2(0.001),
padding='same',use_bias=False),
keras.layers.BatchNormalization()]
self.skip_layers=[]
if strides > 1:
self.skip_layers = [keras.layers.Conv2D(filters, 1, strides=strides,
kernel_initializer = 'he_normal',
kernel_regularizer = regularizers.l2(0.001),
padding='same', use_bias=False),
keras.layers.BatchNormalization()]
def get_config(self):
config = super().get_config().copy()
config.update({
'activation': self.activation,
'main_layers': self.main_layers,
'skip_layers': self.skip_layers,
})
return config
def call(self,inputs):
z = inputs
for layer in self.main_layers:
z=layer(z)
skip_z = inputs
for layer in self.skip_layers:
skip_z=layer(skip_z)
return self.activation(z + skip_z)
class Classifier(tf.keras.Model):
def __init__(self, input_shape, num_classes, **kwargs):
super().__init__()
self.model = Sequential()
self.input_shape = (224,224, 1)
self.num_classes = 20
self.prev_filters= 64
def call(self, x):
self.model.add(layers.Conv2D(64, 7,kernel_initializer = 'he_normal',strides = 2, padding="same", input_shape=self.input_shape))
self.model.add(keras.layers.BatchNormalization())
self.model.add(keras.layers.Activation('leaky_relu'))
self.model.add(layers.MaxPool2D(2))
self.model.add(keras.layers.MaxPool2D(pool_size=2))
for filters in [64] * 2 + [128] * 2 + [256] * 2 + [512] * 2:
strides = 1 if filters == self.prev_filters else 2
self.model.add(ResidualUnit(filters, strides=strides))
prev_filters=filters
print(filters)
model.add(layers.GlobalAveragePooling2D())
model.add(keras.layers.Flatten())
model.add(layers.Dense(64, activation= 'relu'))
model.add(layers.Dense(num_classes, activation="softmax"))
return model
def get_classifier(num_classes=None):
return Classifier(num_classes=num_classes,input_shape = (224,224, 1))
Classifier 클래스 내에 ResNet 17층 모델을 구현합니다. Residual Unit은 ResNet에서 쓰이는 특수한 레이어입니다.
resnet = get_classifier(20)
모델 오브젝트를 생성합니다.
iv) Load Preprocessed DataSet
from dataloader import load
dfdir = "./adata_no_mix_df2.csv"
pdir = "/home/dmsai2/Desktop/DCC2022/adata_no_mix2"
pdir2 = "/home/dmsai2/Desktop/DCC2022/ddata2"
dsize = (150, 150)
# load preprocessed data
X_train, X_test, y_train, y_test, X_test2, y_test2, error_list = load(dfdir,
pdir,
pdir2,
dsize,
train_rate=0.9,
isprint=True,
all_illust=True,
smoothing=True,
smoothing_val=0.01)
위에서 설명한 Data Loader를 사용해 학습할 데이터셋을 온메모리로 불러옵니다.
class img label type
0 L2_15 a0_vfvufbwdefmzkzefhczo.jpg L2_15:1 original
1 L2_15 a1_xgiyglytytgmaqmsvbxe.jpg L2_15:1 original
2 L2_15 a2_lzjfudcqkcbgfmhabpwr.jpg L2_15:1 original
3 L2_15 a3_mncctrflagtlrduvquku.jpg L2_15:1 original
4 L2_15 a4_ahvvwwrvvhshkhoxmxml.jpg L2_15:1 original
... ... ... ... ...
60015 L2_44 augmentedimg3001.jpg L2_44:1 augmix
60016 L2_45 augmentedimg3001.jpg L2_45:1 augmix
60017 L2_46 augmentedimg3001.jpg L2_46:1 cutout
60018 L2_50 augmentedimg3001.jpg L2_50:1 cutout
60019 L2_52 augmentedimg3001.jpg L2_52:1 augmix
[60020 rows x 4 columns]
train : 54018, test : 6002
{'L2_10': 0, 'L2_12': 1, 'L2_15': 2, 'L2_20': 3, 'L2_21': 4, 'L2_24': 5, 'L2_25': 6, 'L2_27': 7, 'L2_3': 8, 'L2_30': 9, 'L2_33': 10, 'L2_34': 11, 'L2_39': 12, 'L2_40': 13, 'L2_41': 14, 'L2_44': 15, 'L2_45': 16, 'L2_46': 17, 'L2_50': 18, 'L2_52': 19}
1/60020 L2_15 a0_vfvufbwdefmzkzefhczo.jpg original L2_15 1 -> test
2/60020 L2_15 a1_xgiyglytytgmaqmsvbxe.jpg original L2_15 1 -> test
3/60020 L2_15 a2_lzjfudcqkcbgfmhabpwr.jpg original L2_15 1 -> test
4/60020 L2_15 a3_mncctrflagtlrduvquku.jpg original L2_15 1 -> train
5/60020 L2_15 a4_ahvvwwrvvhshkhoxmxml.jpg original L2_15 1 -> train
...
60260/60020 L2_25 kiesoomkbbmkmtyojztu -> test
60261/60020 L2_25 utmfgcjhjfkqhwtuymhc -> test
60262/60020 L2_25 kzomkfexbslmstyanidy -> test
train data dict
{'L2_21': 2797, 'L2_24': 2800, 'L2_15': 2609, 'L2_27': 2766, 'L2_30': 2816, 'L2_50': 2797, 'L2_40': 2909, 'L2_39': 2779, 'L2_20': 2786, 'L2_45': 2681, 'L2_3': 2908, 'L2_12': 2937, 'L2_41': 2913, 'L2_10': 2573, 'L2_34': 2781, 'L2_52': 2811, 'L2_33': 2164, 'L2_46': 2378, 'L2_44': 2714, 'L2_25': 2099}
test data dict
{'L2_21': 205, 'L2_24': 205, 'L2_15': 530, 'L2_27': 235, 'L2_30': 186, 'L2_50': 206, 'L2_40': 92, 'L2_39': 242, 'L2_20': 223, 'L2_45': 322, 'L2_3': 94, 'L2_12': 65, 'L2_41': 90, 'L2_10': 456, 'L2_34': 220, 'L2_52': 191, 'L2_33': 841, 'L2_46': 626, 'L2_44': 291, 'L2_25': 924}
X_train (54018, 150, 150, 1)
X_test (6244, 150, 150, 1)
y_train (54018, 20)
y_test (6244, 20)
60263 data loaded with 0 errors.
v) Split Test and Validation Data
import random
from time import time
random.seed(int(time()))
X_test2, X_val, y_test2, y_val = train_test_split(X_test, y_test, test_size = 0.5, shuffle=True)
np.unique(list(map(lambda x : np.argmax(x), y_test2)), return_counts=True)
np.unique(list(map(lambda x : np.argmax(x), y_val)), return_counts=True)
불러온 데이터셋을 학습에 사용할 Training 데이터셋과 검증용 Validation 데이터셋으로 분리합니다.
(array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
17, 18, 19]),
array([224, 37, 252, 104, 110, 89, 458, 122, 44, 116, 433, 108, 119,
49, 39, 130, 161, 316, 117, 94]))
0번부터 20번 각 클래스에 포함된 데이터 수입니다. (y_val 변수)
vi) Tensorboard
from tensorflow.python.keras.callbacks import TensorBoard
from datetime import datetime
logname = input()
tensorboard = TensorBoard(log_dir="logs/{}".format(logname))
로그를 기록할 Tensorboard 오브젝트를 생성합니다.
vii) Compile Model with model.compile()
model.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=1e-4)
, loss='categorical_crossentropy'
, metrics=['accuracy','mse','KLDivergence', get_f1])
위에서 만든 모델 오브젝트를 컴파일해줍니다. 이때, metrics에 get_f1
함수포인터를 지정해주어야 f1 score의 변화를 모니터링할 수 있습니다.
viii) Callback
import keras
callbacks_list = [
keras.callbacks.ReduceLROnPlateau(
monitor = 'val_loss',
factor = 0.1,
patience = 2),
keras.callbacks.EarlyStopping(
monitor = 'val_accuracy',
patience = 8,
),
keras.callbacks.ModelCheckpoint(
filepath='ResNet34.h5',
monitor='val_loss',
save_best_only=True,
),
tensorboard
]
Tensorboard를 포함한 콜백 함수입니다. 이를 통해 모니터링할 변수들과 학습이 되지 않을 경우 학습을 중지하는 Early Stopping 등을 설정해줄 수 있습니다.
ix) Training Model with model.fit()
history = model.fit(X_train, y_train,
batch_size=8,
epochs=50,
validation_data=(X_val, y_val),
callbacks=callbacks_list)
model.save_weights('ResNet17_1031_nomix_smooth')
모델을 학습합니다. 학습할 데이터(X)와 라벨링(y)를 지정해주고, 배치 크기, epoch, 검증(validation) 데이터, 콜백 함수를 지정해줍니다. 이를 history
라는 변수에 담아주어야 학습 과정을 볼 수 있습니다.
학습이 끝난 후 모델의 가중치를 파일로 저장해주는 코드도 추가합니다.
x) Evaluate Model with model.evaluate()
test_loss, test_acc, test_mse = model.evaluate(X_test2, y_test2, verbose='auto')
model.evaluate
함수를 사용해 모델을 평가합니다. 이때는 validation 데이터셋과 별도의 데이터셋을 사용하여 검증합니다.
xi) Calculate F1 Score
from sklearn.metrics import classification_report
y_pred = model.predict(X_test)
dirlist = os.listdir("/home/dmsai2/Desktop/DCC2022/pdata2/")
dirlist.sort()
y_pred1 = list(map(lambda x : np.argmax(x), y_pred))
y_test1 = list(map(lambda x : np.argmax(x), y_test))
print(classification_report(y_test1, y_pred1, target_names=dirlist))
Scikit Learn 라이브러리에 있는 Classification Report 기능을 사용하면 F1 Score를 계산할 수 있습니다.
xii) Show Wrong-predicted Images
cnt = 0
for i in range(len(y_pred1)):
if y_pred1[i] != y_test1[i]:
cnt += 1
print(i, "예측:", dirlist[y_pred1[i]], "정답:", dirlist[y_test1[i]])
print(y_pred[i])
print("예측:",y_pred[i][y_pred1[i]])
print("정답:",y_pred[i][y_test1[i]])
testimg = (X_test[i]*255).astype(np.uint8)
plt.figure(figsize=(3, 3))
plt.imshow(testimg, cmap='gray')
plplt.show()
print("오답 :", cnt, "개")
잘못 예측한 이미지를 보여주는 코드입니다.
11. 모델 평가 및 비교 (Model Evaluation and Comparison)
i) 모델 평가
class Classifier(tf.keras.Model):
def __init__(self, num_classes=None, **kwargs):
super(Classifier, self).__init__()
# model here
def call(self, x):
# preprocess code and call model
return x
위 Classifier 클래스 내에 모델을 구현해주면 됩니다.
def get_classifier(num_classes=None):
return Classifier(num_classes=num_classes, **model_configs)
구현된 모델을 위 함수에서 오브젝트로 생성하여 반환하게 됩니다.
DATA_DIR = "/home/Users/Desktop/DCC2022/data/"
IMAGENET_DEFAULT_MEAN = (0.485, 0.456, 0.406)
IMAGENET_DEFAULT_VAR = (0.229 ** 2, 0.224 ** 2, 0.225 ** 2)
CLF = get_classifier(num_classes=20)
CKPT_PATH = "/home/Users/Desktop/DCC2022/final_mode_weight"
CLF.load_weights(CKPT_PATH).expect_partial()
CLF.compile(metrics=['accuracy'])
ACC_LIST = [] # accuracy
F1_LIST = [] # f1 score
tf.random.set_seed(seed)
loader = tf.keras.preprocessing.image_dataset_from_directory(
directory=DATA_DIR,
image_size=(256, 256),
batch_size=128,
shuffle=False
)
augmentation_layer = tf.keras.Sequential([
tf.keras.layers.CenterCrop(224, 224),
tf.keras.layers.Rescaling(1. / 255.),
tf.keras.layers.experimental.preprocessing.Normalization(mean=IMAGENET_DEFAULT_MEAN, variance=IMAGENET_DEFAULT_VAR)
])
loader = loader.map(lambda x, y: (augmentation_layer(x), y))
RESULT_DF = run_eval(CLF, loader)
print("accuracy:", RESULT_DF['accuracy'].values[0])
print("f1 score:", RESULT_DF.loc['f1-score', 'macro avg'])
위는 대회측에서 제공해준 모델 평가 코드를 각색한 것입니다. (저작권 문제로 원본 코드는 공개 불가) 따라서 정상적으로 작동하지 않을 수 있습니다.
여기서 get_classifier()
함수 내에 모델을 작성해놓고 해당 함수로 오브젝트를 생성하게 되는 것입니다. 아래는 단순하게 정규화(normalization)한 이미지 데이터를 넣어 추정(Inference)을 통해 accuracy와 f1 score를 계산하는 것입니다.
ii) 모델 비교
노션에 리더보드를 만들어두었습니다. 사용한 모델과 Activation 함수, Loss 함수, Label Smoothing 여부, 데이터셋 종류 등과 함께 Accuracy, Test Accuracy, Test F1 Score를 함께 표기해두었습니다.
학습 과정에서 가장 성능이 좋았던 모델은 ResNet 17층 모델이었습니다. F1 Score는 약 0.993점이 나왔고 Test accuracy는 0.994점이 나왔습니다.
12. 결론
이 대회는 약 4주간의 예선(+ 멘토링) 이후, 결과물의 점수에 따라 일부 팀만 본선에 진출하게 됩니다. 50여 팀 중 12팀이 진출하는 것으로 알고 있습니다.
비록 이번 대회에 본선에는 진출하지 못했지만 (본선 진출하면 수상권) 컴퓨터 비전 모델들에 대해 공부할 수 있게 되었고, 기본적인 이미지 분류기(Image Classification)를 만들 때 EDA, Augmentation, Data Balancing 등 어떠한 과정을 따라 진행하는지 배웠습니다. ResNet, EfficientNet과 같은 이미지 분류 분야에서 최고인 모델들에 대해서도 구조를 공부하고 원리르 배울수 있었습니다.
하나 아쉬운 점은 1~2주차 미션을 계속 진행하느라 정작 중요했던 모델의 학습과 튜닝에 대해서는 얼마 시간이 없었다는 것입니다. 제출 직전까지 모델을 학습하고, 테스트했는데 시간이 너무 부족했습니다. 팀원 모두 이런 대회가 처음이라 시간 분배와, 중요도에 대한 생각이 부족했다고 생각합니다.
2021년에 인공지능 입문 전공 수업을 들었는데 그 때 배웠던 머신러닝의 기초와 기본에 대해 직접 실습해보며 복습할 수 있는 기회가 되었습니다.
아직은 컴퓨터 비전 인공지능 분야에선 많이 부족하지만 더 많은 연습과 실습을 통해 다음 프로젝트에서는 더 좋은 결과를 기대할 수 있도록 노력하겠습니다.
이상, 작년 9월~10월에 진행했던 Data Creator Camp 복습 글이었습니다.