전 게시물에서 CNN을 활용해서 졸음운전을 분류하는 프로젝트를 정리해보았는데, 이번엔 이어서 VGG와 ResNet을 사용해서 진행해본 부분을 정리해보겠다.
VGG
- 적절한 kernel(=filter)의 크기나 layer의 수 등을 찾는 것에 초점을 맞춘 모델.
- 특히 11x11, 5x5 등 다양한 크기의 kernel을 사용한 AlexNet과 달리 처음부터 끝까지 일관되게 3x3 크기의 kernel만 사용한 것이 VGGNet 모델의 특징임.
- VGG는 신경망의 깊이가 모델의 성능에 미치는 영향을 조사하기 위해 시작된 연구로 3x3 convolution을 이용한 Deep CNN 제안.
- VGG 모델은 3x3 convolution, max-pooling, fully connected network 3가지 연산으로만 구성되어 있음.
- 왜 3x3 크기의 커널만 사용했는가?
→ receptive field와 관련.
receptive field란?
feature map의 특정 영역이, input data에서 얼마만큼의 영역으로부터 도출됐는지를 말함.
after layer 1 - imput image에서 3x3 크기의 kernel을 가진 conv layer를 1번 사용했을 때
after layer 2 - imput image에서 3x3 크기의 kernel을 가진 conv layer를 2번 사용했을 때
after layer 2 보면, 3x3 크기의 kernel을 가진 conv layer를 2번 사용해서 얻은 1개의 feature 값이 5x5 크기의 input image 영역으로부터 도출됐음을 알 수 있음. 이처럼 1개의 feature 값이 input image에서 바라보는(수용하는) 영역을 receptive field라 함.
** 3x3 크기의 kernel을 가진 conv layer를 2번 사용하면 receptive field가 5x5이므로, 결과적으로 5x5 크기의 kernel을 가진 conv layer를 1번 사용한 것과 receptive field가 같다는 것.
ex) 128 채널의 데이터에, 3x3 커널을 가진 conv layer를 2번 사용하여 128 채널의 feature map을 만들 경우 파라미터 수는 약 20만 5천개임 (33128128+128+33128128+128)임.
↔ 반면, 5x5 커널을 가진 conv layer를 1번 사용하여 똑같이 128 채널의 feature map을 만들 경우 파라미터 수는 약 41만개임.
결과적으로, 3x3 커널을 가진 conv layer를 2번 사용한 것과 5x5 커널을 가진 conv layer를 1번 사용한 것은 receptive field는 같지만 파라미터 수는 3x3 커널을 가진 conv layer를 2번 사용한 것이 더 적음
즉!!! VGGNet은 더 적은 파라미터를 이용해 더 넒은 receptive field를 가질 수 있도록 모든 conv layer의 kernel을 3x3 크기로 구성함
- 3x3 convolution filters (stride:1)
- 2x2 max pooling (stride:2)
- activation function : ReLU
VGG16 모델(D)은 13개의 conv layer와 3개의 fully connected layer로 구성되며, VGG19 모델(E)은 16개의 conv layer와 3개의 fully connecter layer로 구성됨.
성능표를 보면, 깊이가 깊어질수록 모델의 성능이 좋아지는 것과, Local Response Normalization(LRN)은 성능에 큰 영향을 주지 않는단 걸 알 수 있음.
**** 눈여겨 볼만한 부분 : 최대 풀링 (max pooling)**
AlexNet은 stride 값을 kernel 크기보다 작게하여 overraping되는 최대 풀링을 사용했는데, VGGNet은 stride 값와 kernel 크기를 같게 ( 모두2)로 하여 non-overraping되는 최대 풀링을 사용함. 이 이후로 대부분의 모델이 VGGNet처럼 non-overraping되는 최대 풀링을 사용하는 듯.
정리)
VGGNet은 상대적으로 적은 수의 파라미터로 더 넓은 receptive field를 갖는 효율적인 아키텍처 제안.
그런데,
전 게시물에서 했던대로 48000개의 데이터를 가지고 하려고 하니 학습시간이 너무 오래걸려서 closed_eye와 open_eye 각각 1200장씩 2400장으로 줄여서 다시 학습을 진행시켜보았다.
- 데이터 전처리
이미지 개수가 너무 많아서 VGG와 ResNet 학습이 어려워서 각 폴더에서 1200장의 이미지를 랜덤으로 선택하여 모델 학습
import os
import random
import shutil
# 원본 이미지가 있는 폴더 경로
closed_eye_folder = "./data/closed_eye"
open_eye_folder = "./data/open_eye"
# 새로운 이미지를 저장할 폴더 경로
new_closed_eye_folder = "./data/sample/closed_eye_sampled"
new_open_eye_folder = "./data/sample/open_eye_sampled"
# 랜덤 샘플링할 이미지의 개수
sample_size = 1200
# 새로운 폴더 생성
os.makedirs(new_closed_eye_folder, exist_ok=True)
os.makedirs(new_open_eye_folder, exist_ok=True)
# closed_eye 폴더에서 랜덤으로 이미지 샘플링하여 복사
closed_eye_images = random.sample(os.listdir(closed_eye_folder), sample_size)
for image_name in closed_eye_images:
source = os.path.join(closed_eye_folder, image_name)
destination = os.path.join(new_closed_eye_folder, image_name)
shutil.copyfile(source, destination)
# open_eye 폴더에서 랜덤으로 이미지 샘플링하여 복사
open_eye_images = random.sample(os.listdir(open_eye_folder), sample_size)
for image_name in open_eye_images:
source = os.path.join(open_eye_folder, image_name)
destination = os.path.join(new_open_eye_folder, image_name)
shutil.copyfile(source, destination)
import os
folder_path = './data/sample/closed_eye_sampled'
files = os.listdir(folder_path)
image_count = 0
for file in files:
image_count += 1
print('closed_eye_sampled 폴더의 이미지 개수 : ', image_count)
closed_eye_sampled 폴더의 이미지 개수 : 1200
import os
folder_path = './data/sample/open_eye_sampled'
files = os.listdir(folder_path)
image_count = 0
for file in files:
image_count += 1
print('open_eye_sampled 폴더의 이미지 개수 : ', image_count)
open_eye_sampled 폴더의 이미지 개수 : 1200
나머지 데이터 로드 및 전처리는 위에서 한거랑 다 동일하게 해주었고,
VGG랑 ResNet한 이후에도 비교하기 위해서 전 게시물과 동일한 CNN 모델로 학습시켰을 때,
from timeit import default_timer as timer
from tqdm import tqdm
EPOCHS = 5
loss_func = nn.NLLLoss() #negative log likelihood 사용
optimizer = torch.optim.SGD(model1.parameters(), lr=0.01)
start_time = timer()
model1_res = training(model=model1,
train_dataloader=train_loader,
test_dataloader=test_loader,
optimizer=optimizer,
loss_fn=loss_func,
epochs=EPOCHS)
end_time = timer()
print(f"Total training time: {end_time-start_time:.3f} seconds")
20%|████████████████▊ | 1/5 [00:44<02:59, 44.78s/it]
Epoch: 0 | Train loss: 0.3315 | Train acc: 0.8645 | Test loss: 0.6966 | Test acc: 0.5175
40%|█████████████████████████████████▌ | 2/5 [00:59<01:21, 27.23s/it]
Epoch: 1 | Train loss: 0.1898 | Train acc: 0.9440 | Test loss: 0.6253 | Test acc: 0.6160
60%|██████████████████████████████████████████████████▍ | 3/5 [01:13<00:42, 21.24s/it]
Epoch: 2 | Train loss: 0.1688 | Train acc: 0.9420 | Test loss: 0.6208 | Test acc: 0.7970
80%|███████████████████████████████████████████████████████████████████▏ | 4/5 [01:27<00:18, 18.29s/it]
Epoch: 3 | Train loss: 0.1477 | Train acc: 0.9530 | Test loss: 0.1933 | Test acc: 0.9210
100%|████████████████████████████████████████████████████████████████████████████████████| 5/5 [01:42<00:00, 20.44s/it]
Epoch: 4 | Train loss: 0.1173 | Train acc: 0.9625 | Test loss: 0.2214 | Test acc: 0.9125 Total training time: 102.197 seconds
위에서는 1348.396 seconds 걸렸는데, 훨씬 빨리 학습됨.
원래 데이터 개수로 학습했을 때와 비슷하게 마지막에 살짝 과적합 되는 경향 있음.
Confusion Matrix:
[[191 30]
[ 11 248]]
Classification Report:
precision recall f1-score support
0 0.95 0.86 0.90 221
1 0.89 0.96 0.92 259
accuracy 0.91 480
macro avg 0.92 0.91 0.91 480
weighted avg 0.92 0.91 0.91 480
실제로 눈 감았는데, 감았다고 잘 예측한 거 191
실제로 눈 감았는데, 떴다고 잘못 예측한거 30
실제로 눈 떴는데, 감았다고 잘못 예측한거 11
실제로 눈 떴는데, 떴다고 잘 예측한거 248
클래스 0에 대한 precision : 0.95 - 눈 감고 있다고 예측한 샘플 중에서 실제로 눈 감고 있는 샘플의 비율.
클래스 0에 대한 recall : 0.86 - 실제로 눈 감고 있는 샘플 중에서 모델이 눈 감고 있다고 정확히 예측한 샘플의 비율
클래스 1에 대한 precision : 0.89 - 눈 뜨고 있다고 예측한 샘플 중에서 실제로 눈 뜨고 있는 샘플의 비율.
클래스 1에 대한 recall : 0.96 - 실제로 눈 뜨고 있는 샘플 중에서 모델이 눈 뜨고 있다고 정확히 예측한 샘플의 비율
위험한 오류→ 눈 감았는데, 떴다고 잘못 예측한거 (졸음운전 중인데, 정상이라고 예측한거니까)
여기서 이 값은 30으로 낮은 편.
- VGG16 (D모델) 적용
import torch.nn as nn
class VGGModel(nn.Module):
def __init__(self):
super().__init__()
self.conv_layers = nn.Sequential(
# 블록 1
nn.Conv2d(3, 64, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(64, 64, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=2, stride=2),
# 블록 2
nn.Conv2d(64, 128, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(128, 128, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=2, stride=2),
# 블록 3
nn.Conv2d(128, 256, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(256, 256, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(256, 256, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=2, stride=2),
# 블록 4
nn.Conv2d(256, 512, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(512, 512, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(512, 512, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=2, stride=2),
# 블록 5
nn.Conv2d(512, 512, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(512, 512, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(512, 512, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=2, stride=2),
)
self.avgpool = nn.AdaptiveAvgPool2d((2, 2))
#이 VGG 모델의 구조에서 마지막 컨볼루션 레이어의 출력 크기가 (2,2)이고, 이를 flatten 하여 FC 레이어에 전달 할 때 512*2*2의 크기가 되어야함.
#avgpool 안하면 평탄화된 텐서의 크기가 예상과 다른 것을 의미함.(입력 크기가 100x2048이 되어야하는데 25088x4096으로 되어있음)
#그래서 여기서 이 avgpool 레이어를 추가하여 컨볼루션 레이어의 출력을 (2,2)로 조정해야 함. 이 레이어 추가하면 평탄화된 텐서의 크기가 512*2*2이 되어서 예상대로 선형 레이어의 입력 크기와 호환됨.
self.classifier = nn.Sequential(
nn.Linear(512 * 2 * 2, 4096),
nn.ReLU(inplace=True),
nn.Dropout(),
nn.Linear(4096, 1000),
nn.ReLU(inplace=True),
nn.Dropout(),
nn.Linear(1000, 2), # 클래스 수에 맞게 마지막 nn.Linear 레이어 수정
nn.LogSoftmax(dim=-1)
)
def forward(self, x):
x = self.conv_layers(x)
x = self.avgpool(x)
x = x.view(x.size(0), -1)
x = self.classifier(x)
return x
model2 = VGGModel()
from timeit import default_timer as timer
from tqdm import tqdm
EPOCHS = 5
loss_func = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model2.parameters(), lr=0.01)
start_time = timer()
model2_res = training(model=model2,
train_dataloader=train_loader,
test_dataloader=test_loader,
optimizer=optimizer,
loss_fn=loss_func,
epochs=EPOCHS)
end_time = timer()
print(f"Total training time: {end_time-start_time:.3f} seconds")
20%|████████████████▌ | 1/5 [03:05<12:21, 185.39s/it]
Epoch: 0 | Train loss: 0.6932 | Train acc: 0.4940 | Test loss: 0.6932 | Test acc: 0.4895
40%|█████████████████████████████████▏ | 2/5 [06:16<09:27, 189.03s/it]
Epoch: 1 | Train loss: 0.6932 | Train acc: 0.4995 | Test loss: 0.6932 | Test acc: 0.4895
60%|█████████████████████████████████████████████████▊ | 3/5 [09:14<06:07, 183.58s/it]
Epoch: 2 | Train loss: 0.6931 | Train acc: 0.4995 | Test loss: 0.6932 | Test acc: 0.4895
80%|██████████████████████████████████████████████████████████████████▍ | 4/5 [12:10<03:00, 180.64s/it]
Epoch: 3 | Train loss: 0.6932 | Train acc: 0.4950 | Test loss: 0.6932 | Test acc: 0.4895
100%|███████████████████████████████████████████████████████████████████████████████████| 5/5 [15:46<00:00, 189.40s/it]
Epoch: 4 | Train loss: 0.6928 | Train acc: 0.5205 | Test loss: 0.6932 | Test acc: 0.4895 Total training time: 947.029 seconds
Confusion Matrix:
[[ 0 245]
[ 0 235]]
Classification Report:
precision recall f1-score support
0 0.00 0.00 0.00 245
1 0.49 1.00 0.66 235
accuracy 0.49 480
macro avg 0.24 0.50 0.33 480
weighted avg 0.24 0.49 0.32 480
실제로 눈 감았는데, 감았다고 잘 예측한 거 0
실제로 눈 감았는데, 떴다고 잘못 예측한거 245
실제로 눈 떴는데, 감았다고 잘못 예측한거 0
실제로 눈 떴는데, 떴다고 잘 예측한거 235
클래스 0에 대한 precision : 0.00 - 눈 감고 있다고 예측한 샘플 중에서 실제로 눈 감고 있는 샘플의 비율.
클래스 0에 대한 recall : 0.00 - 실제로 눈 감고 있는 샘플 중에서 모델이 눈 감고 있다고 정확히 예측한 샘플의 비율
클래스 1에 대한 precision : 0.49 - 눈 뜨고 있다고 예측한 샘플 중에서 실제로 눈 뜨고 있는 샘플의 비율.
클래스 1에 대한 recall : 1.00 - 실제로 눈 뜨고 있는 샘플 중에서 모델이 눈 뜨고 있다고 정확히 예측한 샘플의 비율
위험한 오류→ 눈 감았는데, 떴다고 잘못 예측한거 (졸음운전 중인데, 정상이라고 예측한거니까)
여기서 이 값은 245으로 제일 높음. 잘못된 모델임.
→일반적으로 이진 분류 작업에는 VGG와 같은 깊고 복잡한 모델은 필요 이상으로 많은 파라미터를 가지고 있어서 학습이 느리고, 작은 데이터셋에서는 과적합의 위험이 있음. 또한, VGG와 같은 대규모 모델은 추론 속도가 느리고 메모리 사용량이 많아서 배포 및 실시간 응용 프로그램에는 적합하지 않음.
이진 분류 작업에는 보다 간단하고 가벼운 모델이 종종 성능 면에서 더 우수함. (ResNet, DenseNet, MobileNet등과 같은 경량화된 신경망 아키텍처들) 이런 모델들은 깊이에 비해 파라미터 수가 적고, 계산 효율성이 뛰어나기 때문에 작은 데이터셋에서도 더 잘 일반화될 수 있음.
**결론)
해놓고 나니, 뭔가 이상한게 객관적으로 그렇게 많은 이미지가 아닌데 하루종일 돌려도 다 안돌아가는 게 코드의 문제가 있는 것 같기도 하다.
디버깅을 통해서 코드 어느 부분에서 오래걸리는지 확인해볼 필요성을 느꼈다.
다음 게시물에서 ResNet을 사용해서 분류를 진행한 부분까지 다뤄보겠다.
'Deep Learning & AI > CV' 카테고리의 다른 글
[Paper Review] CLIP (Learning Transferable Visual Models From Natural Language Supervision) (1) | 2024.07.01 |
---|---|
[ResNet] 졸음운전 분류 프로젝트 (1) | 2024.03.18 |
[CNN] 졸음운전 분류 프로젝트 (0) | 2024.03.04 |
[CNN] 전이 학습 - 특성 추출 기법 (1) | 2024.01.01 |
[CNN] Fashion MNIST 데이터로 CNN 실습하기 (1) | 2023.12.29 |