이미지 분류 : 특정 대상이 영상에 존재하는지 판단
합성곱과 풀링을 번갈아 쌓고, 마지막에 완전연결을 통해 분류하는
초기 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)
)
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()
: 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 확률로 뉴런을 비활성화하여 오버피팅 방지
• 커널 크기: 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-1과 GPU-2에 필터(채널)를 나눠서 배치
기존 AlexNet은 1000개 클래스(ImageNet 기준)를 분류하지만,
2개 클래스(개·고양이)에 맞춰 마지막 층을 수정
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)을 직접 지정해 풀링 연산을 수행.
결과로 어떤 크기가 나올지는 이 파라미터들로 결정
• 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 층 추가
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
• 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)를 추가