[Week 5]

chelseey·2025년 4월 3일

이미지 분류를 위한 신경망

이미지 분류 : 특정 대상이 영상에 존재하는지 판단

LeNet-5

합성곱과 풀링을 번갈아 쌓고, 마지막에 완전연결을 통해 분류하는
초기 CNN 모델

  • 입력 (32×32)

  • Conv1 (합성곱 1)
    커널(필터) 크기: 5×5
    출력 채널(필터 수): 6
    스트라이드(stride): 1
    활성화 함수: ReLU
    → 32×32 입력에 대해 5×5 필터 6개를 적용해,
    6개의 특성맵(feature map)을 만듦

  • Max Pooling 1
    커널 크기: 2×2
    스트라이드: 2
    → Conv1 결과(6×28×28)를 2×2로 풀링해
    출력 맵의 가로세로 크기를 절반으로 축소

  • Conv2 (합성곱 2)
    커널(필터) 크기: 5×5
    출력 채널(필터 수): 16
    스트라이드(stride): 1
    활성화 함수: ReLU
    → 첫 번째 풀링 결과를 입력으로 받아,
    5×5 필터 16개를 적용해 16개의 특성맵을 생성

  • Max Pooling 2
    커널 크기: 2×2
    스트라이드: 2

  • FC1 (완전연결층 1)
    출력 뉴런 수: 120
    활성화 함수: ReLU
    → 풀링 결과를 1차원 벡터로 펼친 뒤, 120차원의 완전연결층으로 매핑

  • FC2 (완전연결층 2)
    출력 뉴런 수: 84
    → FC1 출력을 입력으로 받아 84차원으로 매핑
    활성화 함수: ReLU

  • 출력층 (FC3)
    출력 뉴런 수: 10(Softmax)
    → 10개의 출력을 가지는 완전연결층

stride = 1 : 필터를 한 번 적용한 후에 한 픽셀씩 오른쪽이나 아래로 이동시킴

이미지 데이터셋 전처리(텐서 변환)

import torchvision.transforms as transforms

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):
        if phase not in self.data_transform:
            raise ValueError("phase must be 'train' or 'val'")
        return self.data_transform[phase](img)

• RandomResizedCrop(resize, scale=(0.5, 1.0))
: 이미지를 무작위로 일정 비율(원본의 50%~100%)로 자른 뒤,
지정된 크기(resize)로 조정
• RandomHorizontalFlip()
: 이미지 가로 방향을 무작위로 뒤집음
• ToTensor()
: PIL 이미지(또는 NumPy 배열)를 PyTorch 텐서로 변환
픽셀 값 범위가 [0, 255]에서 [0, 1] 사이로 정규화,
채널 순서 H×W×C에서 C×H×W로 변경
• Normalize(mean, std)
: 각 채널(RGB)에 대해 지정된 mean과 std를 사용해 정규화

이미지 데이터셋을 훈련,검증,테스트로 분리

cat_directory=r'wk5/dogs-vs-cats/Cat'
dog_directory=r'wk5/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)])
cat_images_filepaths=[*cat_images_filepaths,*dog_images_filepaths]
correct_images_filepaths=[i for i in dog_images_filepaths if cv2.imread(i) is not None]

random.seed(42)
random.shuffle(correct_images_filepaths)
train_images_filepaths=correct_images_filepaths[:400]
val_images_filepaths=correct_images_filepaths[400:-10]
test_images_filepaths=correct_images_filepaths[-10:]
print(len(train_images_filepaths),len(val_images_filepaths), len (test_images_filepaths))

• sorted
: 데이터를 정렬된 리스트로 만들어서 반환
• os.path.join
: 디렉터리 경로와 파일 이름을 합쳐서 전체 경로를 생성
• os.listdir(cat_directory)
: cat_directory에 있는 파일 이름 목록을 가져옴

• if cv2.imread(i) is not None
: 데이터를 찾을 수 없을 때까지 (not None) 모든 이미지 데이터를 읽어옴

테스트 데이터셋 이미지 확인 함수

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)
        ax.ravel()[i].set_axis_off()
    plt.tight_layout()
    plt.show()

• cv2.COLOR_BGR2RGB
: BGR 채널 이미지를 RGB로 변경
• predicted_label = predicted_labels[i]
: 미리 계산된 예측 결과 리스트에서 i번째 값을 가져옴

테스트 데이터셋 이미지 출력

이미지 데이터셋 클래스 정의

class DogvsCatDataset(Dataset):
    def __init__(self,file_list,transform=None,phase='train'):
        self.file_list=file_list
        self.transform=transform
        self.phase=phase
        
    def __len__(self):
        return len(self.file_list)
    
    def __getitem__(self,idx):
        img_path=self.file_list[idx]
        img=Image.open(img_path)
        img_transformed=self.transform(img, self.phase)
        label=img_path.split('/')[-1].split('.')[0]
        if label=='dog':
            label=1
        elif label=='cat':
            label=0
        return img_transformed, label

• imgpath.split('/')[-1]
: 이미지의 전체 경로에서 '/'를 제거
• split('
')[0]
: img_path.split('/')[-1]의 결과에서 '.'을 제거한 값들 중 첫번째 값

데이터로더 정의

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}

batch_iterator=iter(train_dataloader)
inputs,label=next(batch_iterator)
print(inputs.size())
print(label)

• dataloaders_dict
: 학습 단계와 검증 단계를 쉽게 관리하기 위해 딕셔너리로 묶음

모델의 네트워크 클래스

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)
        self.relu1=nn.ReLU()
        self.maxpool1=nn.MaxPool2d(kernel_size=2)
        self.cnn2=nn.Conv2d(in_channels=16, out_channels=32, kernel_size=5,
                                    stride=1, padding=0)
        self.relu2=nn.ReLU()
        self.maxpool=nn.MaxPool2d(kernel_size=2)
                
        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.maxpool(out)
            out=out.view(out.size(0),-1)
            out=self.fc1(out)
            out=self.fc2(out)
            out=self.output(out)
            return out

• 합성곱 레이어(Conv2d) 출력 크기 공식

InputSize (W): 입력 feature map의 너비(또는 높이)
KernelSize (F): 필터(커널)의 크기
Padding (P): 입력 테두리를 0으로 채우는 양
Stride (S): 필터를 이동시키는 보폭

• MaxPool2d 출력 크기 공식

InputSize: 풀링 직전에 들어오는 특성맵의 높이(또는 너비)
K (kernel_size): 풀링 윈도우의 크기
S (stride): 풀링 윈도우가 이동하는 보폭
P (padding): 풀링 전에 양 옆을 얼마나 0으로 채우는지

S = F이고 P = 0인 경우,

모델 객체 생성

model=LeNet()
print(model)
LeNet(
  (cnn1): Conv2d(3, 16, kernel_size=(5, 5), stride=(1, 1))
  (relu1): ReLU()
  (maxpool1): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  (cnn2): Conv2d(16, 32, kernel_size=(5, 5), stride=(1, 1))
  (relu2): ReLU()
  (maxpool): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  (fc1): Linear(in_features=89888, out_features=512, bias=True)
  (relu5): ReLU()
  (fc2): Linear(in_features=512, out_features=2, bias=True)
  (output): Softmax(dim=1)
)

torchsummary 라이브러리를 사용하여 모델의 네트워크 구조 확인

from torchsummary import summary
summary(model,input_size=(3,224,224))
----------------------------------------------------------------
        Layer (type)               Output Shape         Param #
================================================================
            Conv2d-1         [-1, 16, 220, 220]           1,216
              ReLU-2         [-1, 16, 220, 220]               0
         MaxPool2d-3         [-1, 16, 110, 110]               0
            Conv2d-4         [-1, 32, 106, 106]          12,832
              ReLU-5         [-1, 32, 106, 106]               0
         MaxPool2d-6           [-1, 32, 53, 53]               0
            Linear-7                  [-1, 512]      46,023,168
            Linear-8                    [-1, 2]           1,026
           Softmax-9                    [-1, 2]               0
================================================================
Total params: 46,038,242
Trainable params: 46,038,242
Non-trainable params: 0
----------------------------------------------------------------
Input size (MB): 0.57
Forward/backward pass size (MB): 19.47
Params size (MB): 175.62
Estimated Total Size (MB): 195.67
----------------------------------------------------------------

• summary(model, input_size=(3, 224, 224))
: model은 모델의 네트워크,
input_size=(3, 224, 224)은 입력 값의 (채널,너비,높이)

옵티마이저와 손실 함수 정의

optimizer=optim.SGD(model.parameters(),lr=0.001,momentum=0.9)
criterion=nn.CrossEntropyLoss()

• optim.SGD
: 파이토치의 확률적 경사 하강법(Stochastic Gradient Descent)
• model.parameters()
: 학습 대상이 되는 모델 파라미터(가중치와 편향 등)를 전달
• lr=0.001
: 학습률(learning rate)을 0.001로 설정
• momentum=0.9
: 모멘텀(Momentum)을 0.9로 설정.

모멘텀 : 이전 단계의 기울기 정보를 일정 비율로 누적하여, 경사가 일관된 방향이면 더 빠르게 이동하고, 진동(oscillation)을 줄여주는 역할

모델 학습 함수 정의

def train_model(model,dataLoader_dict,criterion,optimizer,num_epoch):
    since=time.time()
    best_acc=0.0
    
    for epoch in range(num_epoch):
        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]):
                inputs=inputs.to(device)
                labels=labels.to(device)
                optimizer.zero_grad()
                
                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()
                        
                    epoch_loss += loss.item() * inputs.size(0)
                    epoch_corrects+=torch.sum(preds==labels.data)
                
                epoch_loss=epoch_loss/len(dataLoader_dict[phase].dataset)
                epoch_acc = epoch_corrects.float() / len(dataLoader_dict[phase].dataset)
                
                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('Traing complete in{:.0f}m {:.0f}s'.format(
                time_elapsed//60, time_elapsed%60))
            print('Best val Acc: {:4f}'.format(best_acc))
            return model

• reduction='mean'
: 배치 내 각 샘플의 손실값을 모두 더한 뒤, 배치 크기로 나누어 평균을 구함

모델 테스트를 위한 함수 정의

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()
        id_list.append(_id)
        pred_list.append(preds[0])
        
res=pd.DataFrame({
    'id':id_list,
    'label':pred_list
})
res.sort_values(by='id',inplace=True)
res.reset_index(drop=True, inplace=True)

res.to_csv('wk5/result.csv',index=False)

• torch.unsqueeze(0)
: PyTorch 텐서에 새로운 차원(dim)을 추가하는 함수
(0)은 차원이 추가될 위치
• F.softmax(outputs, dim=1)[:, 1].tolist() :
F.softmax(outputs, dim=1) : 각 샘플(행)마다 소프트맥스를 적용하여, 해당 샘플에 대한 모든 클래스 확률의 합이 1이 되도록 변환
[:, 1] : 텐서나 배열 a에서 모든 행에 대해 두 번째 열(인덱스 1)의 값을 가져옴
.tolist() : 배열을 리스트 형태로 변환

테스트 데이터셋 이미지를 출력하기 위한 함수 정의

# classes: ['cat', 'dog'] 라벨 목록
classes = ['cat', 'dog']

# 예측 결과를 담고 있는 데이터프레임 res
# res는 id(이미지 이름)와 label(모델이 예측한 확률 값)을 가지고 있음

a = random.choice(res['id'].values)  # 1) 결과 테이블에서 임의로 한 개의 이미지 ID를 고름
label = res.loc[res['id'] == a, 'label'].values[0]  # 2) 해당 ID의 예측 확률(label)을 가져옴

if label >= 0.5:
    class_label = 1  # dog
else:
    class_label = 0  # cat

image_filepath = ...  # 3) 선택된 id(a)와 매칭되는 실제 이미지 파일 경로를 구함
image = cv2.imread(image_filepath)
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

plt.imshow(image)
plt.title(classes[class_label])
plt.axis('off')
plt.show()
일부의 데이터를 이용하여 낮은 예측력

AlexNet

: 5개의 합성곱층과 3개의 완전연결층으로 구성
마지막 완전연결층은 1000개 클래스를 분류하기 위해
소프트맥스 활성화 함수 사용

  • 입력(Input)
    227×227 크기의 RGB 이미지(채널=3)

  • Conv1
    필터 크기: 11×11
    스트라이드(stride): 4
    출력 채널: 96
    LRN(Local Response Normalization) 적용
    Max Pooling (kernel=3, stride=2)로 공간 크기를 줄임

  • Conv2
    필터 크기: 5×5
    스트라이드: 1
    출력 채널: 256
    LRN 적용 후 Max Pooling

  • Conv2
    필터 크기: 5×5
    스트라이드: 1
    출력 채널: 256
    LRN 적용 후 Max Pooling

  • Conv3
    필터 크기: 3×3
    스트라이드: 1
    출력 채널: 384
    (이후 Max Pooling 없이 바로 다음 합성곱으로 연결)

  • Conv4
    필터 크기: 3×3
    스트라이드: 1
    출력 채널: 384

  • Conv5
    필터 크기: 3×3
    스트라이드: 1
    출력 채널: 256
    이후 Max Pooling 적용

  • FC6 (Fully Connected)
    입력을 1차원으로 펼쳐 완전연결
    뉴런 수: 4096
    ReLU + Dropout(0.5) 적용

  • FC7 (Fully Connected)
    뉴런 수: 4096
    ReLU + Dropout

  • FC8 (출력층)
    뉴런 수: 1000 (ImageNet 1000개 클래스)
    최종적으로 Softmax를 통해 각 클래스 확률을 산출

LRN(Local Response Normalization)
: ReLU로 인해 큰 값을 갖는 뉴런들을 인접 뉴런과 비교하여 정규화하는 기법

Dropout
: 완전연결층(FC6, FC7)에서 0.5 확률로 뉴런을 비활성화하여 오버피팅 방지

AlexNet의 첫 번째 합성곱 층의 구조

• 커널 크기: 11×11×3
: 하나의 필터가 색상(R, G, B)을 동시에 고려하여 11×11 범위를 살핌
• 스트라이드(stride) = 4
: 필터를 한 번 적용한 후, 오른쪽 혹은 아래로 4픽셀씩 건너뛰며 이동
출력 특성맵(feature map)의 크기를 크게 줄이는 효과
• 출력 채널 수: 96
Conv1에서 96개의 필터(가중치 집합)를 학습하여, 96개의 특징맵을 생성
각 필터는 입력 이미지에서 서로 다른 유형의 시각적 패턴(색, 모양 등)을 학습
• 출력 크기: 55×55 (가로×세로)
: 227×227 입력 이미지에 11×11 필터, 스트라이드 4를 적용하면 약 55×55 크기의 특성맵이 생성

GPU 병렬 처리와 필터 특성 분화

GPU-1과 GPU-2에 필터(채널)를 나눠서 배치

  • GPU-1 필터: 선명한 색 대비(빨강-초록, 파랑-노랑 등)나 채널 간 혼합 등을 포착
  • GPU-2 필터: 사선, 수평선, 곡선 등 형태(edge) 위주로 민감하게 반응

고양이·개 분류를 위한 수정

기존 AlexNet은 1000개 클래스(ImageNet 기준)를 분류하지만,
2개 클래스(개·고양이)에 맞춰 마지막 층을 수정

AlexNet 모델 네트워크 정의

class AlexNet(nn.Module):
    def __init__(self) -> None:
        super(AlexNet, self).__init__()
        self.features = nn.Sequential(
            nn.Conv2d(3, 64, kernel_size=11, stride=4, padding=2),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2),
            nn.Conv2d(64, 192, kernel_size=5, padding=2),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2),
            nn.Conv2d(192, 384, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(384, 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=3, stride=2),
        )
        self.avgpool = nn.AdaptiveAvgPool2d((6, 6))
        self.classifier = nn.Sequential(
            nn.Dropout(),
            nn.Linear(256 * 6 * 6, 4096),
            nn.ReLU(inplace=True),
            nn.Dropout(),
            nn.Linear(4096, 512),
            nn.ReLU(inplace=True),
            nn.Linear(512, 2),
        )

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        x = self.features(x)
        x = self.avgpool(x)
        x = torch.flatten(x, 1)
        x = self.classifier(x)
        return x

• nn.ReLU(inplace=True) :
inplace 연산은 기존 값을 연산 결괏값으로 대체
• nn.AdaptiveAvgPool2d((6, 6))
: 어떤 입력 크기가 들어오든, 최종 출력이 항상 6×6이 되도록 PyTorch가 커널/스트라이드 등을 자동으로 결정

↔︎ nn.AvgPool2d : 커널 크기(kernel_size), 스트라이드(stride), 패딩(padding)을 직접 지정해 풀링 연산을 수행.
결과로 어떤 크기가 나올지는 이 파라미터들로 결정

VGGNet

• 3×3 합성곱 필터를 반복
모든 합성곱 층에서 3×3 필터(stride=1, padding=1)를 사용해 출력 채널 수를 늘려나감
큰 필터 대신 3×3 필터 여러 개를 연속 적용하면,
동일한 수용 영역을 얻으면서도, 파라미터 수를 줄여 효율적으로 학습할 수 있고,
각 레이어마다 비선형 활성화(ReLU)를 적용해 모델이 더 다양한 특징을 학습 가능

수용 영역 : 한 뉴런이 영향을 받는 입력 이미지의 영역
ex. 3×3 필터를 연속으로 3번 사용하면, 첫 번째 레이어는 3×3 영역,
두 번째 레이어는 그 주변까지 고려해서 효과적으로 5×5,
세 번째 레이어를 거치면 7×7 정도의 수용 영역이 형성

• 단순하고 일관된 구조
VGGNet은 합성곱(3×3, stride=1, padding=1) × n번 → 풀링(2×2, stride=2) 패턴을 반복하며 깊이를 늘림
VGG16은 합성곱 층 13개 + 완전연결(FC) 층 3개 = 총 16개 층으로 구성

• 깊이 증가에 따른 성능 향상
합성곱 층을 더 많이 쌓으면(VGG16, VGG19), 특징 추출 능력이 향상되어 ImageNet 등 대규모 분류 문제에서 우수한 성능을 발휘

• ReLU 활성화 함수
각 합성곱 층 뒤에 ReLU를 적용해 비선형성을 부여하고, 학습 속도를 빠르게 함

• 많은 파라미터 수
VGG16의 경우, 약 1억 3천만 개(논문에는 ~138M) 파라미터가 필요
메모리 요구량이 큼

합성곱(Conv) + 풀링(Pool) 블록을 5개 반복
마지막에 3개의 완전연결층(FC) + Softmax로 분류

라이브러리 호출

import copy
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import torch.utils.data as data
import torchvision
import torchvision.transforms as transforms
import torchvision.datasets as Datasets

device = torch.device("mps") if torch.backends.mps.is_available() else torch.device("cpu")

• 얕은 복사(shallow copy)
최상위 객체(리스트 등)의 구조만 복사하고, 내부에 포함된 원소(하위 객체)들은 참조(레퍼런스)를 복사
원본과 복사본이 공유하는 하위 객체가 있음

• 깊은 복사(deep copy)
최상위 객체뿐 아니라 내부에 포함된 모든 하위 객체까지 재귀적으로 새로 복사
복사본과 원본이 완전히 독립된 객체, 복사본을 변경해도 원본에는 영향이 없음

모델 유형 정의

vgg11_config = [64, 'M', 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M']

vgg13_config = [64, 64, 'M', 128, 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M']

vgg16_config = [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'M', 512, 512, 512, 'M', 512, 512, 
                512, 'M']

vgg19_config = [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 256, 'M', 512, 512, 512, 512, 'M', 
                512, 512, 512, 512, 'M']

숫자는 합성곱 층의 출력 채널 수(out_channels)를 의미
'M'은 Max Pooling
→ 숫자가 나오면 Conv 층 추가, 'M'이 나오면 MaxPool 층 추가

VGG 계층 정의

def get_vgg_layers(config, batch_norm):    
    layers = []
    in_channels = 3
    
    for c in config:
        assert c == 'M' or isinstance(c, int)
        if c == 'M':
            layers += [nn.MaxPool2d(kernel_size = 2)]
        else:
            conv2d = nn.Conv2d(in_channels, c, kernel_size = 3, padding = 1)
            if batch_norm:
                layers += [conv2d, nn.BatchNorm2d(c), nn.ReLU(inplace = True)]
            else:
                layers += [conv2d, nn.ReLU(inplace = True)]
            in_channels = c
            
    return nn.Sequential(*layers)

• assert c == 'M' or isinstance(c, int)
: c가 'M'이거나 정수가 아닐 경우 에러를 발생시키는 검사(Assertion)
→ VGG config 리스트에 'M'(맥스풀링) 또는 정수(합성곱 채널 수)만
들어가야 하므로, 다른 값이 있으면 에러를 내도록 하는 장치

테스트 데이터셋을 이용한 모델의 예측 확인 함수

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

• top_pred = y_prob.argmax(1, keepdim=True) :
argmax(1) : 행(0), 열(1)을 따라 가장 큰 확률을 갖는 인덱스(클래스 번호)를 찾음
keepdim=True : 출력 텐서를 입력과 동일한 크기로 유지

• torch.cat
: images, labels, probs 리스트에 들어 있는 여러 배치의 텐서들을 이어 붙여 하나의 큰 텐서로 만듦

예측 중에서 정확하게 예측한 것을 추출

device = torch.device('cpu')
model = model.to(device)
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)

• zip(a, b)
: 여러 이터러블(리스트, 튜플 등)을 병렬로 묶어서 튜플을 생성하는 함수

a = [1, 2, 3]
b = ['a', 'b', 'c']
-----------------------
1 a
2 b
3 c

GoogLeNet

• Inception 모듈
: 다양한 커널 크기를 병렬 적용
GoogLeNet의 Inception 모듈은 1×1, 3×3, 5×5 합성곱과 3×3 Max Pooling을 동시에 적용함

서로 다른 크기의 커널을 동시에 사용하면, 작은 수용 영역(ex. 1×1, 3×3)은 미세한 특징을, 큰 수용 영역(5×5)은 보다 거시적인 패턴을 포착할 수 있음.
이후 각 연산의 결과를 채널 차원에서 연결(concatenate)함으로써, 하나의 모듈에서 다양한 스케일의 특징을 모두 반영 가능

• 1×1 합성곱의 활용 (Bottleneck 기법)
3×3이나 5×5 필터를 바로 적용하면, 입력 채널 수가 클수록 각 필터가 가지는 파라미터 수가 기하급수적으로 늘어남

1×1 합성곱은 각 픽셀에서 채널 간의 선형 조합을 수행함으로써, 입력 데이터의 채널 수를 효과적으로 줄여줌.
→ 차원을 축소한 후, 3×3 또는 5×5 필터를 적용하면 전체 연산량과 파라미터 수를 크게 줄일 수 있음.
같은 수용 영역을 확보하면서도 모델의 크기와 계산 비용은 줄어들어, 더 효율적인 학습과 추론이 가능.

• 네트워크 깊이 증가와 vanishing gradient 문제 해결
네트워크를 깊게 쌓으면 더 복잡한 특징을 학습할 수 있어 성능이 향상되지만 기울기 소실(vanishing gradient) 문제가 발생해, 초반 층들이 제대로 학습되지 않음
→ GoogLeNet은 네트워크의 중간, 특징 추출 단계에서
보조 분류기(auxiliary classifier)를 추가

0개의 댓글