BIG_PY
home
Introduce
home

6장 합성곱 신경망II

질문

이름과 질문을 적어주세요
유성현 ResNet 이후 더 발전된 모델은 어떤 것들이 있을지 궁금합니다. https://deep-learning-study.tistory.com/528 https://benlee73.tistory.com/33
박소정 6.1.4. 패딩을 하면 데이터의 가장자리 정보들이 사라지는 문제를 해결할 수 있는데 책에는 풀링 연산을 할때 보통 패딩을 추가하지 않는 것 같습니다. 연산시간이 너무 길어져 패딩을 하지 않는 것일까요?
조민진
GoogLeNet에서 특징을 효율적으로 추출하기 위해 1×1, 3×3, 5×5의 합성곱 연산을 각각 수행한다고 하였는데 한꺼번에 학습하는 것보다 효과가 더 높은 이유는 무엇일까요?

6장. 합성곱 신경망II

6.1 이미지 분류를 위한 신경망

입력 데이터로 이미지를 사용한 분류(classification)는
특정 대상이 영상 내에 존재하는지 여부를 판단

6.1.1 LeNet-5

최초로 얀 르쿤(Yann LeCun)이 개발
현재 CNN의 초석
합성곱(convolutional)과 다운 샘플링(sub-sampling)(혹은 풀링)을 반복적으로 거치면서 마지막에 완전연결층에서 분류를 수행
LeNet-5를 사용하는 예제(앞 장에서 사용한 개와 고양이 데이터셋을 다시 사용)
32×32 크기의 이미지에 합성곱층과 최대 풀링층이 쌍으로 두 번 적용된 후 완전연결층을 거쳐 이미지가 분류되는 신경망
LeNet의 아키텍처로 개와 고양이 구분하는 모델 만들기 필요한 라이브러리 호출, 모델 학습에 필요한 데이터셋의 전처리(텐서 변환)
# 코드 6-1 필요한 라이브러리 호출 import torch import torchvision from torch.utils.data import DataLoader, Dataset from torchvision import transforms # ------ 이미지 변환(전처리) 기능을 제공하는 라이브러리 from torch.autograd import Variable from torch import optim # ------ 경사 하강법을 이용하여 가중치를 구하기 위한 옵티마이저 라이브러리 import torch.nn as nn import torch.nn.functional as F import os # ------ 파일 경로에 대한 함수들을 제공 import cv2 # 아나콘다 prompt에서 pip install opencv-python from PIL import Image from tqdm import tqdm_notebook as tqdm # ------ 진행 상황을 가시적으로 표현해 주는데, 특히 모델의 학습 경과를 확인하고 싶을 때 사용하는 라이브러리 import random from matplotlib import pyplot as plt device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") # ------ 파이토치는 텐서플로와 다르게 GPU를 자동으로 할당해 주지 않기 때문에 GPU 할당을 모델과 데이터에 선언해 주어야 합니다. 단 이 장에서는 CPU를 사용합니다. # cuda : gpu # 코드 6-2 이미지 데이터셋 전처리 class ImageTransform(): def __init__(self, resize, mean, std): self.data_transform = { 'train': transforms.Compose([ transforms.RandomResizedCrop(resize, scale=(0.5,1.0)), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize(mean, std) ]), # 토치 비전 라비브러리(이미지에 대한 전처리 쉽게 해준다) 'val': transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(resize), transforms.ToTensor(), transforms.Normalize(mean, std) ]) } def __call__(self, img, phase): # __call__메서드는 인스턴스가 호출되었을 때 실행 return self.data_transform[phase](img)
Python
복사
import cv2 → 아나콘다 prompt에서 pip install opencv-python
이미지가 위치한 디렉터리에서 데이터를 불러온 후 훈련용 400개, 검증용92개, 테스트 10개 분리
# 코드 6-3 이미지 데이터셋을 불러온 후 훈련, 검증, 데스트로 분리 cat_directory = 'C:/big_py/dogs-vs-cats/Cat/' # 파일명 한글 들어가면 cv2.imread에서 error dog_directory = 'C:/big_py/dogs-vs-cats/Dog/' cat_images_filepaths = sorted([os.path.join(cat_directory, f) for f in os.listdir(cat_directory)]) # ------ ① dog_images_filepaths = sorted([os.path.join(dog_directory, f) for f in os.listdir(dog_directory)]) images_filepaths = [*cat_images_filepaths, *dog_images_filepaths] # ------ 개와 고양이 이미지들을 합쳐서 images_filepaths에 저장 correct_images_filepaths = [i for i in images_filepaths if cv2.imread(i) is not None] # ------ ② random.seed(42) # 난수 생성 random.shuffle(correct_images_filepaths) train_images_filepaths = correct_images_filepaths[:400] # ------ 훈련용 400개의 이미지 val_images_filepaths = correct_images_filepaths[400:-10] # ------ 검증용 92개의 이미지 test_images_filepaths = correct_images_filepaths[-10:] # ------ 테스트용 열 개의 이미지 print(len(train_images_filepaths), len(val_images_filepaths), len(test_images_filepaths)) # 코드 6-4 테스트 데이터셋 이미지 확인 함수 def display_image_grid(images_filepaths, predicted_labels=(), cols=5): rows = len(images_filepaths) // cols figure, ax = plt.subplots(nrows=rows, ncols=cols, figsize=(12, 6)) for i, image_filepath in enumerate(images_filepaths): image = cv2.imread(image_filepath) image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) # ------ ① true_label = os.path.normpath(image_filepath).split(os.sep)[-2] # ------ ② predicted_label = predicted_labels[i] if predicted_labels else true_label # ------ ③ color = "green" if true_label == predicted_label else "red" # ------ 예측과 정답(레이블)이 동일하면 초록색으로 표시하고, 그렇지 않다면 빨간색으로 표시 ax.ravel()[i].imshow(image) # ------ 개별 이미지를 출력 ax.ravel()[i].set_title(predicted_label, color=color) # ------ predicted_label을 타이틀로 사용 ax.ravel()[i].set_axis_off() # ------ 이미지의 축 제거 plt.tight_layout() # ------ 이미지의 여백을 조정 plt.show() # 코드 6-5 테스트 데이터셋 이미지를 출력 display_image_grid(test_images_filepaths)
Python
복사
cv2.imread(i) → cat_directory, dog_directory에 파일명 한글들어가면 error
최종 목적 고양이와 개가 포함될 확률을 코드로 구현하기 위해 고양이가 있는 이미지의 레이블은 0, 개가 있는 이미지의 레이블은 1이 되도록 하는 코드 (라벨링)
# 코드 6-6 이미지 데이터셋 클래스 정의 # 레이블(정답) 이미지에서 고양이와 개가 포함될 확률을 코드로 구현, 고양이가 있는 이미지의 레이블은 0, 개가 있는 이미지의 레이블은 1 class DogvsCatDataset(Dataset): def __init__(self, file_list, transform=None, phase='train'): # ------데이터셋의 전처리(데이터 변형 적용) self.file_list = file_list self.transform = transform # ------ DogvsCatDataset 클래스를 호출할 때 transform에 대한 매개변수를 받아 옵니다. self.phase = phase # ------ ‘train’ 적용 def __len__(self): # ------ images_filepaths 데이터셋의 전체 길이를 반환 return len(self.file_list) def __getitem__(self, idx): # ------ 데이터셋에서 데이터를 가져오는 부분으로 결과는 텐서 형태가 됩니다. img_path = self.file_list[idx] img = Image.open(img_path) # ------ img_path 위치에서 이미지 데이터들을 가져옵니다. img_transformed = self.transform(img, self.phase) # ------ 이미지에 ‘train’ 전처리를 적용 label = img_path.split('/')[-1].split('.')[0] # 이미지 파일명 dog, cat 구분 if label == 'dog': label = 1 elif label == 'cat': label = 0 return img_transformed, label # 코드 6-7 변수 값 정의 # 전처리에서 사용할 변수 정의 size = 224 mean = (0.485, 0.456, 0.406) std = (0.229, 0.224, 0.225) batch_size = 32 # 코드 6-8 이미지 데이터셋 정의 # 훈련 데이터의 크기와 레이블에 대한 출력 결과 train_dataset = DogvsCatDataset(train_images_filepaths, transform=ImageTransform(size, mean, std), phase='train') # ------ 훈련 이미지에 train_transforms를 적용 val_dataset = DogvsCatDataset(val_images_filepaths, transform=ImageTransform(size, mean, std), phase='val') # ------ 검증 이미지에 test_transforms를 적용 index = 0 print(train_dataset.__getitem__(index)[0].size()) # ------ 훈련 데이터(train_dataset.__getitem__[0][0])의 크기(size()) 출력 print(train_dataset.__getitem__(index)[1]) # ------ 훈련 데이터의 레이블 출력 # 1은 개, 0은 고양이
Python
복사
훈련 데이터의 크기와 레이블 출력
0 → index 0번째 훈련데이터 레이블이 ‘고양이’
데이터를 메모리 효율을 위해 테이터로더에서 배치 크기만큼 분할하여 불러오기 데이터셋을 학습시킬 모델의 네트워크 설계(클래스 생성)
# 코드 6-9 데이터로더의 정의 # 데이터 로더로 배치 관리 ->한 번에 모든 데이터를 불러오면 메모리에 부담, 데이터를 그룹으로 쪼개서 조금씩 불러오기 train_dataloader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True) # ------ ① val_dataloader = DataLoader(val_dataset, batch_size=batch_size, shuffle=False) dataloader_dict = {'train': train_dataloader, 'val': val_dataloader} # ------ 훈련 데이터셋(train_dataloader)과 검증 데이터셋(val_dataloader)을 합쳐서 표현 # 데이터 로더를 이용하여 훈련 데이터셋을 메모리로 불러온 후 데이터셋의 크기와 레이블 출력 batch_iterator = iter(train_dataloader) inputs, label = next(batch_iterator) print(inputs.size()) print(label) # 코드 6-10 모델의 네트워크 클래스 class LeNet(nn.Module): def __init__(self): super(LeNet, self).__init__() self.cnn1 = nn.Conv2d(in_channels=3, out_channels=16, kernel_size=5, stride=1, padding=0) # ------ 2D 합성곱층이 적용됩니다. 이때 입력 형태는 (3, 224, 224)가 되며 출력 형태는 (weight-kernel_size+1)/stride에 따라 (16, 220, 220)이 됩니다. self.relu1 = nn.ReLU() # ------ ReLU 활성화 함수입니다. self.maxpool1 = nn.MaxPool2d(kernel_size=2) # ------ 최대 풀링이 적용됩니다. 적용 이후 출력 형태는 220/2가 되어 (16, 110, 110)입니다. self.cnn2 = nn.Conv2d(in_channels=16, out_channels=32, kernel_size=5, stride=1, padding=0) #------ 또다시 2D 합성곱층이 적용되며 출력 형태는 (32, 106, 106)입니다. self.relu2 = nn.ReLU() self.maxpool2 = nn.MaxPool2d(kernel_size=2) # ------ 최대 풀링이 적용되며 출력 형태는 (32, 53, 53)입니다. self.fc1 = nn.Linear(32*53*53, 512) self.relu5 = nn.ReLU() self.fc2 = nn.Linear(512, 2) self.output = nn.Softmax(dim=1) def forward(self, x): out = self.cnn1(x) out = self.relu1(out) out = self.maxpool1(out) out = self.cnn2(out) out = self.relu2(out) out = self.maxpool2(out) out = out.view(out.size(0), -1) # ------ 완전연결층에 데이터를 전달하기 위해 데이터 형태를 1차원으로 바꿉니다. out = self.fc1(out) out = self.fc2(out) out = self.output(out) return out # 코드 6-11 모델 객체 생성 model = LeNet() # Input type (torch.cuda.FloatTensor) and weight type (torch.FloatTensor) should be the same model.to("cuda") print(model) # 코드 6-12 torchsummary 라이브러리를 이용한 모델의 네트워크 구조 확인 from torchsummary import summary summary(model, input_size=(3, 224, 224)) # 코드 6-13 학습 가능한 파라미터 수 확인 def count_parameters(model): return sum(p.numel() for p in model.parameters() if p.requires_grad) print(f'The model has {count_parameters(model):,} trainable parameters')
Python
복사
input type과 weight type이 동시에 cuda이어야 하는데 그게 아니라서 그렇다. 다시 말해 input type(x,y)는 cuda를 먹였는데 weight type(신경망)은 cuda를 먹이지 않아서 생기는 에러 → model.to("cuda")
총 파라미터 수, 입력 크기, 네트워크의 총크기
옵티마이저와 손실 함수 정의하고 모델 학습 → 학습 용도이기 때문에 model.train()사용
# 코드 6-14 옵티마이저와 손실 함수 정의 # 경사 하강법으로 모멘텀 SGD사용 optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9) # ------ ① criterion = nn.CrossEntropyLoss() # 코드 6-15 모델의 파라미터와 손실 함수를 GPU에 할당 model = model.to(device) # device -> "cuda" criterion = criterion.to(device) # 코드 6-16 모델 학습 함수 정의 # model.train()사용 def train_model(model, dataloader_dict, criterion, optimizer, num_epoch): since = time.time() best_acc = 0.0 for epoch in range(num_epoch): # ------ epoch를 10으로 설정했으므로 10회 반복 print('Epoch {}/{}'.format(epoch+1, num_epoch)) print('-'*20) for phase in ['train', 'val']: if phase == 'train': model.train() # ------ 모델을 학습시키겠다는 의미 else: model.eval() epoch_loss = 0.0 epoch_corrects = 0 for inputs, labels in tqdm(dataloader_dict[phase]): # ------ 여기에서 dataloader_dict는 훈련 데이터셋(train_loader)을 의미 inputs = inputs.to(device) # ------ 훈련 데이터셋을 CPU에 할당 labels = labels.to(device) optimizer.zero_grad() # ------ 역전파 단계를 실행하기 전에 기울기(gradient)를 0으로 초기화 with torch.set_grad_enabled(phase == 'train'): outputs = model(inputs) _, preds = torch.max(outputs, 1) loss = criterion(outputs, labels) # ------ 손실 함수를 이용한 오차 계산 if phase == 'train': loss.backward() # ------ 모델의 학습 가능한 모든 파라미터에 대해 기울기를 계산 optimizer.step() # ------ optimizer의 step 함수를 호출하면 파라미터를 갱신 epoch_loss += loss.item() * inputs.size(0) # ------ ① epoch_corrects += torch.sum(preds == labels.data) # ------ 정답과 예측이 일치하면 그것의 합계를 epoch_corrects에 저장 epoch_loss = epoch_loss / len(dataloader_dict[phase].dataset) # ------ 최종 오차 계산(오차를 데이터셋의 길이(개수)로 나누어서 계산) epoch_acc = epoch_corrects.double() / len(dataloader_dict[phase].dataset) # ------ 최종 정확도(epoch_corrects를 데이터셋의 길이(개수)로 나누어서 계산) print('{} Loss: {:.4f} Acc: {:.4f}'.format(phase, epoch_loss, epoch_acc)) if phase == 'val' and epoch_acc > best_acc: # ------ 검증 데이터셋에 대한 가장 최적의 정확도를 저장 best_acc = epoch_acc best_model_wts = model.state_dict() time_elapsed = time.time() - since print('Training complete in {:.0f}m {:.0f}s'.format(time_elapsed // 60, time_elapsed % 60)) print('Best val Acc: {:4f}'.format(best_acc)) return model # 코드 6-17 모델 학습 import time num_epoch = 10 model = train_model(model, dataloader_dict, criterion, optimizer, num_epoch) # conda install -c conda-forge ipywidgets 설치
Python
복사
train_model → 아나콘다 prompt에서 conda install -c conda-forge ipywidgets 설치
검증 테이터셋 이용 최고 52.%정확도 별로 안좋다 → 데이터셋 늘리면 좋아진다.
테스트 데이터셋을 모델에 적용하여 정확도 측정 → model.eval() 사용
# 코드 6-18 모델 테스트를 위한 함수 정의 import pandas as pd id_list = [] pred_list = [] _id = 0 with torch.no_grad(): # ------ 역전파 중 텐서들에 대한 변화도를 계산할 필요가 없음을 나타내는 것으로, 훈련 데이터셋의 모델 학습과 가장 큰 차이점입니다. for test_path in tqdm(test_images_filepaths): # ------ 테스트 데이터셋 이용 img = Image.open(test_path) _id = test_path.split('/')[-1].split('.')[1] transform = ImageTransform(size, mean, std) img = transform(img, phase='val') # ------ 테스트 데이터셋 전처리 적용 img = img.unsqueeze(0) # ------ ① img = img.to(device) model.eval() outputs = model(img) preds = F.softmax(outputs, dim=1)[:, 1].tolist() # 0: 고양이일 확률, 1: 개일 확률 id_list.append(_id) pred_list.append(preds[0]) res = pd.DataFrame({ 'id': id_list, 'label': pred_list }) # ------ 테스트 데이터셋의 예측 결과인 id와 레이블(label)을 데이터 프레임에 저장 res.sort_values(by='id', inplace=True) res.reset_index(drop=True, inplace=True) res.to_csv('C:/big_py/LeNet', index=False) # ------ 데이터 프레임을 CSV 파일로 저장 # 예측 결과 레이블이 0.5보다 크면 개, 작으면 고양이를 의미 # 코드 6-19 테스트 데이터셋의 예측 결과 호출 res.head(10) # 코드 6-20 테스트 데이터셋 이미지를 출력하기 위한 함수 정의 class_ = classes = {0:'cat', 1:'dog'} # ------ 개와 고양이에 대한 클래스 정의 def display_image_grid(images_filepaths, predicted_labels=(), cols=5): rows = len(images_filepaths) // cols figure, ax = plt.subplots(nrows=rows, ncols=cols, figsize=(12, 6)) for i, image_filepath in enumerate(images_filepaths): image = cv2.imread(image_filepath) image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) a = random.choice(res['id'].values) # ------ 데이터 프레임의 id라는 칼럼에서 임의로 데이터를 가져옵니다. label = res.loc[res['id'] == a, 'label'].values[0] if label > 0.5: # ------ 레이블 값이 0.5보다 크다면 개 label = 1 else: # ------ 레이블 값이 0.5보다 작다면 고양이 (고양이로 하는 이유) label = 0 # 개일 확률 0.4 -> 고양이 확률 0.6 ( 개 + 고양이 = 1 ) ax.ravel()[i].imshow(image) ax.ravel()[i].set_title(class_[label]) ax.ravel()[i].set_axis_off() plt.tight_layout() plt.show() # 코드 6-21 테스트 데이터셋 예측 결과 이미지 출력 display_image_grid(test_images_filepaths)
Python
복사
label이 소프트맥스 결과값 (개일 확률) 레이블이 0.5보다 크면 개 0.5보다 작으면 고양이
개 확률 + 고양이 확률 = 1

6.1.2 AlexNet

6.1.3 VGGNet

테스트 데이터셋을 이용해 모델의 예측 결과 알아보기
# 코드 6-59 테스트 데이터셋을 이용한 모델 성능 측정 model.load_state_dict(torch.load('C:/big_py/VGG-model.pt')) # 저장된 모델 로드 test_loss, test_acc = evaluate(model, test_iterator, criterion, device) print(f'Test Loss: {test_loss:.3f} | Test Acc: {test_acc*100:.2f}%')
Python
복사
테스트 데이터셋 예측 결과 시각화
# 코드 6-60 테스트 데이터셋을 이용한 모델의 예측 확인 함수 def get_predictions(model, iterator): model.eval() images = [] labels = [] probs = [] with torch.no_grad(): for (x, y) in iterator: x = x.to(device) y_pred, _ = model(x) y_prob = F.softmax(y_pred, dim=-1) top_pred = y_prob.argmax(1, keepdim=True) # ------ ① images.append(x.cpu()) labels.append(y.cpu()) probs.append(y_prob.cpu()) images = torch.cat(images, dim=0) # ------ ② labels = torch.cat(labels, dim=0) probs = torch.cat(probs, dim=0) return images, labels, probs # 코드 6-61 예측 중에서 정확하게 예측한 것을 추출 images, labels, probs = get_predictions(model, test_iterator) pred_labels = torch.argmax(probs, 1) # ------ ① corrects = torch.eq(labels, pred_labels) # ------ 예측과 정답이 같은지 비교 correct_examples = [] for image, label, prob, correct in zip(images, labels, probs, corrects): # ------ ② if correct: correct_examples.append((image, label, prob)) correct_examples.sort(reverse=True, key=lambda x: torch.max(x[2], dim=0).values) # ------ ③ # 코드 6-62 이미지 출력을 위한 전처리 def normalize_image(image): image_min = image.min() image_max = image.max() image.clamp_(min=image_min, max=image_max) # ------ torch.clamp는 주어진 최소(min), 최대(max)의 범주에 이미지가 위치하도록 합니다. image.add_(-image_min).div_(image_max-image_min+1e-5) # ------ ① return image # 코드 6-63 모델이 정확하게 예측한 이미지 출력 함수 def plot_most_correct(correct, classes, n_images, normalize=True): rows = int(np.sqrt(n_images)) # ------ np.sqrt는 제곱근을 계산(0.5를 거듭제곱) cols = int(np.sqrt(n_images)) fig = plt.figure(figsize=(25,20)) for i in range(rows*cols): ax = fig.add_subplot(rows, cols, i+1) # ------ 출력하려는 그래프 개수만큼 subplot을 만듭니다. image, true_label, probs = correct[i] image = image.permute(1, 2, 0) # ------ ① true_prob = probs[true_label] correct_prob, correct_label = torch.max(probs, dim=0) true_class = classes[true_label] correct_class = classes[correct_label] if normalize: # ------ 본래 이미지대로 출력하기 위해 normalize_image 함수 호출 image = normalize_image(image) ax.imshow(image.cpu().numpy()) ax.set_title(f'true label: {true_class} ({true_prob:.3f})\n' \ f'pred label: {correct_class} ({correct_prob:.3f})') ax.axis('off') fig.subplots_adjust(hspace=0.4) # 코드 6-64 예측 결과 이미지 출력 classes = test_dataset.classes N_IMAGES = 5 plot_most_correct(correct_examples, classes, N_IMAGES)
Python
복사

6.1.4 GoogLeNet

6.1.5 ResNet

질문

유성현 ResNet 이후 더 발전된 모델은 어떤 것들이 있을지 궁금합니다. https://deep-learning-study.tistory.com/528 https://benlee73.tistory.com/33
DenseNet resnet의 숏컷을 모든 레이어로 확장하게 끔 바꾼것 resnet보다 기울기 소실 문제를 완화하고 파라미터 수와 연산량이 적다는 장점이 있다
ResNext
같은 block을 반복적으로 구축해서 더 적은 파라미터로 이미지 분류 ResNext 는 다른 모델들 (ResNet-101/152, ResNet200, Inception-v3, Inception-ResNet-v2) 보다 더 간단한 구조를 가졌지만 더 나은 성능을 가진다.
박소정 6.1.4. 패딩을 하면 데이터의 가장자리 정보들이 사라지는 문제를 해결할 수 있는데 책에는 풀링 연산을 할때 보통 패딩을 추가하지 않는 것 같습니다. 연산시간이 너무 길어져 패딩을 하지 않는 것일까요?
입출력 크기 맞추기 위한 것을 제외하고 풀링 연산에 패딩하는 것은 의미 없음, 3×3 최대 풀링은 입력과 출력의 높이와 너비가 같아야하므로, 패딩 연산 추가
풀링 → 특성 맵의 차원을 다운(연산량 감소), 주요한 특성 벡터 추출이 목적 최대풀링, 평균풀링 패딩(제로)해도 의미가 없음
조민진
GoogLeNet에서 특징을 효율적으로 추출하기 위해 1×1, 3×3, 5×5의 합성곱 연산을 각각 수행한다고 하였는데 한꺼번에 학습하는 것보다 효과가 더 높은 이유는 무엇일까요?
한꺼번에 학습?
1*1은 특성맵(채널)의 갯수를 줄이는 목적으로 사용
5*5*3 (rgb) → 1*1, 1*1, 1*1 → 5*5*1 (5*5는 유지 ,채널만 줄인다)
3*3 필터로 추출한 특성, 5*5 필터로 추출한 특성 쌓아서 같이 쓴다.