[Week 4]

chelseey·2025년 3월 27일

전이 학습

대규모 데이터셋으로 미리 훈련된 모델의 가중치(weight)를 가져와,
해결하고자 하는 새로운 과제에 맞춰 약간만 수정(fine‑tuning) 해서 사용하는 기법

결과적으로 적은 데이터로도 좋은 성능 달성 가능
특성 추출, 미세 조정 기법이 있음

특성 추출 기법

ImageNet 등 대규모 데이터셋으로 이미 학습된 CNN의 합성곱 부분만 사용하고, 마지막 완전연결층(fully‑connected layer) 만 새로 학습시켜 새로운 클래스 분류에 활용

  • 합성곱층
    이미지에서 유용한 특징(feature)을 뽑아내는 부분
  • 데이터 분류기(완전연결층)
    추출된 특징을 입력받아 최종 클래스(레이블) 예측

사용 가능한 사전 학습 모델
Xception, Inception V3, ResNet50, VGG16 / VGG19, MobileNet

데이터 전처리(transform), 데이터셋(ImageFolder) 구성

transform = transforms.Compose([
    transforms.Resize([256, 256]),     
    transforms.RandomResizedCrop(224),  
    transforms.RandomHorizontalFlip(),  
    transforms.ToTensor()              
])
  • Resize
    : 이미지 크기 조정
  • RandomResizedCrop
    : 이미지를 무작위 비율과 크기로 잘라(crop) 다시 224×224로 맞춤
    데이터 증강(Data Augmentation) 효과가 있어서,
    모델이 다양한 위치,배율의 이미지를 학습하게 됨.
  • RandomHorizontalFlip()
    : 이미지를 확률(기본 0.5) 로 수평 뒤집기함
    데이터 증강을 통해 모델이 좌우 뒤집힌 이미지를 모두 학습하도록 유도
  • ToTensor()
    : 이미지를 PyTorch 텐서로 변환
    픽셀값을 [0,1] 범위(실수형)로 정규화
train_dataset = torchvision.datasets.ImageFolder(
    root=data_path,
    transform=transform
)
  • root=data_path
    : 이미지가 들어있는 디렉터리 경로를 지정
  • transform=transform
    : 앞서 정의한 전처리 파이프라인을 각 이미지에 적용
train_loader = torch.utils.data.DataLoader(
    train_dataset,    # (a)
    batch_size=32,    # (b)
    num_workers=8,    # (c)
    shuffle=True      # (d)
)
  • train_dataset
    : 데이터셋 지정
  • batch_size
    : 한 번에 모델로 전달할 샘플 수 (미니배치 크기)
  • num_workers
    : 데이터를 불러올 때 사용하는 서브 프로세스 수
  • shuffle
    : 에포크마다 데이터를 무작위로 섞을지 여부

RandomResizedCrop

• show_images 함수 (이미지 격자 출력)

def show_images(imgs, num_rows, num_cols, scale=2):
    aspect_ratio = imgs[0].shape[0] / imgs[0].shape[1]
    figsize = (num_cols * scale, num_rows * scale * aspect_ratio)
    _, axes = plt.subplots(num_rows, num_cols, figsize=figsize)

    for i in range(num_rows):
        for j in range(num_cols):
            idx = i * num_cols + j
            axes[i][j].imshow(imgs[idx].asnumpy())
            axes[i][j].axes.get_xaxis().set_visible(False)
            axes[i][j].axes.get_yaxis().set_visible(False)

    plt.subplots_adjust(hspace=0.1, wspace=0)
    return axes
  • aspect_ratio
    : 원본 이미지의 세로/가로 비율(height/width) 을 계산해서, Matplotlib이 이미지를 왜곡(distortion) 없이 표시하도록 figsize의 높이(height) 를 조정
  • set_visible(False)
    : x축, y축 눈금 제거

• apply 함수 (Augmentation 여러번 실행)

def apply(img, aug, num_rows=2, num_cols=4, scale=3):
    Y = [aug(img) for _ in range(num_rows * num_cols)]
    show_images(Y, num_rows, num_cols, scale)
  • aug(img)
    : img에 데이터 증강(RandomResizedCrop 등)을 한 번 적용한 결과를 반환
  • show_images
    : 생성된 여러 이미지를 격자(grid) 형태로 시각화하는 함수

• RandomResizedCrop 객체 생성 & 적용

shape_aug = transforms.RandomResizedCrop(
    size=(200, 200),
    scale=(0.1, 1),
    ratio=(0.5, 2)
)
apply(example_image, shape_aug)
  • scale=(0.1, 1)
    : 원본 이미지의 전체 면적 중에서 잘라낼(크롭) 영역의 면적이 차지할 비율을 무작위로 선택하는 범위를 설정
    → 크롭의 크기를 다양하게 하여 모델이 다양한 크기의 입력을 학습
  • ratio=(0.5, 2.0)
    : 잘라낼 영역의 가로:세로 비율을 0.5부터 2.0 사이에서 무작위로 선택

• RandomResizedCrop 적용 결과

PyTorch DataLoader 에서 가져온 샘플(batch)과 레이블(클래스) 시각화

# 배치에서 이미지와 레이블을 가져옴
samples, labels = iter(train_loader).next()

# 클래스 매핑 
classes = {0: 'cat', 1: 'dog'}

# 그림 생성 (전체 figure 크기 조절)
fig = plt.figure(figsize=(16, 24))

# 24개의 이미지를 4행 x 6열 격자로 출력
for i in range(24):
    ax = fig.add_subplot(4, 6, i + 1)
    # PyTorch 텐서는 (채널, 높이, 너비) 순서
    img = samples[i].cpu().numpy()
    img = np.transpose(img, (1, 2, 0))
    ax.imshow(img)
    # 레이블을 제목으로 표시
    ax.set_title(classes[labels[i].item()])
    ax.axis('off')  # 축 눈금 제거

# 전체 서브플롯 간 간격 조절
plt.subplots_adjust(bottom=0.2, top=0.6, hspace=0.5)
plt.show()
  • iter()
    : 반복 가능한 객체(train_loader)를 iterator로 만들어 줌
  • next()
    : iterator에서 다음 요소(다음 배치)를 꺼내옴
  • np.transpose
    : 차원 순서를 변경하여 배열(텐서)을 재배열

Matplotlib/OpenCV는 (높이, 너비, 채널) 순서를 가정하는 반면,
딥러닝 프레임워크(PyTorch, TensorFlow)는 (채널, 높이, 너비)로 텐서를 저장

ResNet18

: ImageNet으로 미리 학습된 모델을 제공

import torchvision.models as models

resnet18 = models.resnet18(pretrained=True)

pretrained=True를 지정하여 ImageNet으로 미리 학습된 가중치를 다운로드해 모델에 로드
→ 모델 구조(레이어)는 그대로 유지하되, 가중치(weight)를 이미 학습된 상태로 가져옴

이외에도 PyTorch에서 torchvision.models 모듈이 제공하는
여러 모델(ResNet, VGG, DenseNet, MobileNet, etc.) 사용 가능

기존 가중치를 고정(freeze)

def set_parameter_requires_grad(model, feature_extracting=True):
    if feature_extracting:
        for param in model.parameters():
            param.requires_grad = False

feature_extracting=True를 지정하여 모델 전체 파라미터의 requires_grad를 False로 설정
→ 모델의 기존 가중치(합성곱, 배치 정규화 등)는 학습 대상에서 제외되고, 역전파(backprop) 시 업데이트되지 않음

ResNet18 모델의 마지막 레이어(fc)를 새로 정의

  • 마지막 레이어 변경
resnet18.fc = nn.Linear(512, 2)

ImageNet용으로 (512,1000) 설정된 원래 ResNet18을
새로운 작업(2개 클래스 분류)에 맞춰 (512, 2)로 레이어를 재정의

  • 파라미터 확인
for name, param in resnet18.named_parameters():
    print(name, param.data)

모델의 모든 파라미터(합성곱, 배치 정규화, fc 레이어)에 대해 이름, 데이터를 확인

  • requires_grad 설정
for param in resnet18.parameters():
    param.requires_grad = False

for param in resnet18.fc.parameters():
    param.requires_grad = True

기존 레이어는 requires_grad=False로 설정해 동결(freeze)
마지막 레이어만 requires_grad=True로 둬서 학습 대상으로 지정
→ 새로운 레이어만 학습되어 계산량과 데이터 요구량이 크게 감소

  • Optimizer / Loss 함수 정의
optimizer = torch.optim.Adam(resnet18.fc.parameters(), lr=0.001)
criterion = nn.CrossEntropyLoss()

Adam 옵티마이저로 마지막 레이어 파라미터만 업데이트
CrossEntropyLoss로 분류 문제 학습

ResNet18 모델 학습을 위한 train_model 함수

  • 함수 정의 및 초기 설정
def train_model(model, dataloaders, criterion, optimizer, device, num_epochs=13, is_train=True):
    since = time.time()  # 학습 시작 시간 기록
    acc_history = []
    loss_history = []
    best_acc = 0.0

since : 학습 시간을 측정하기 위해 시작 시각을 기록

  • Epoch 반복
for epoch in range(num_epochs):
    print('Epoch {}/{}'.format(epoch, num_epochs-1))
    print('-'*10)

    running_loss = 0.0
    running_corrects = 0
  • 미니배치 단위 학습
for inputs, labels in dataloaders:
    inputs = inputs.to(device)
    labels = labels.to(device)
    
    model.train()  # 훈련 모드(드롭아웃, 배치정규화 등)
    optimizer.zero_grad()
    outputs = model(inputs)
    loss = criterion(outputs, labels)
    loss.backward()
    optimizer.step()

    _, preds = torch.max(outputs, 1)
    running_loss += loss.item() * inputs.size(0)
    running_corrects += torch.sum(preds == labels.data)

outputs = model(inputs) : 예측 수행 (순전파 학습)
loss.backward() : 가중치 기울기 계산 (역전파 학습)
running_corrects : 정답 개수 누적

  • Epoch 결과 요약
epoch_loss = running_loss / len(dataloaders.dataset)
epoch_acc = running_corrects.double() / len(dataloaders.dataset)

print('Loss: {:.4f} Acc: {:.4f}'.format(epoch_loss, epoch_acc))

if epoch_acc > best_acc:
    best_acc = epoch_acc

loss_history.append(epoch_loss)
acc_history.append(epoch_acc.item())

epoch_loss : 누적 손실을 데이터셋 전체 개수로 나눈 평균
epoch_acc : running_corrects(정답 개수)를 전체 개수로 나눈 값

  • 모델 저장 & 최종 출력
torch.save(model.state_dict(), os.path.join('/chap05/data/catanddog','{0:0=2d}.pth'.format(epoch)))

time_elapsed = time.time() - since
print('Training complete in {:.0f}m {:.0f}s'.format(time_elapsed//60, time_elapsed%60))
print('Best Acc: {:4f}'.format(best_acc))

return acc_history, loss_history

time_elapsed = time.time() - since : 실행 시간 (학습 시간) 계산
acc_history, loss_history : 모델 정확도, 오차 결과 반환

  • 특정 파라미터만 학습할 준비
params_to_update = []
for name, param in model.named_parameters():
    if param.requires_grad == True:
        params_to_update.append(param)
        print("\t", name)

param.requires_grad == True인 파라미터(=학습 대상)만 params_to_update에 추가

  • Optimizer 정의
optimizer = torch.optim.Adam(params_to_update, lr=0.001)

Optimizer는 params_to_update에 있는 파라미터만 업데이트

  • 학습 함수 호출
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
criterion = nn.CrossEntropyLoss()

train_acc_hist, train_loss_hist = train_model(
    resnet18, 
    train_loader, 
    criterion, 
    optimizer, 
    device
)
  • 테스트 데이터 전처리 및 로더 생성
test_path = './chap05/data/catanddog/test'

transform = transforms.Compose([
    transforms.Resize((224,224)),
    transforms.ToTensor()
])

test_dataset = torchvision.datasets.ImageFolder(root=test_path, transform=transform)
test_loader = torch.utils.data.DataLoader(test_dataset, batch_size=32, num_workers=2, shuffle=True)

print(len(test_dataset))
  • 테스트 평가 함수
def eval_model(model, dataloaders, device):
    since = time.time()
    acc_history = []
    best_acc = 0.0

    saved_models = glob.glob('./chap05/data/catanddog/test'+'*.pth')
    saved_models.sort()  # 파일 이름 기준 정렬
    print("saved_models:", saved_models)

    for model_path in saved_models:
        print("Loading model", model_path)
        
        # 모델 파라미터 로드
        model.load_state_dict(torch.load(model_path))
        model.eval()
        model.to(device)

        running_corrects = 0

        for inputs, labels in dataloaders:
            inputs = inputs.to(device)
            labels = labels.to(device)

            with torch.no_grad():
                outputs = model(inputs)
                _, preds = torch.max(outputs, 1)

            preds[preds >= 0.5] = 1
            preds[preds < 0.5] = 0

            running_corrects += preds.eq(labels).int().sum()

        epoch_acc = running_corrects.float() / len(dataloaders.dataset)
        print("Acc: {:.4f}".format(epoch_acc))

        if epoch_acc > best_acc:
            best_acc = epoch_acc
        acc_history.append(epoch_acc.item())
        print()

    time_elapsed = time.time() - since
    print("Validation complete in {:.0f}m {:.0f}s".format(time_elapsed // 60, time_elapsed % 60))
    print("Best val Acc: {:4f}".format(best_acc))

    return acc_history

glob.glob(...) : 지정한 경로 내의 모든 .pth 파일 목록을 가져옴
sort() : 파일 이름 순으로 정렬하여, 01.pth, 02.pth.. 순서로 평가할 수 있도록 함
for inputs, labels in dataloaders : DataLoader로부터 한 배치씩 가져옴
with torch.no_grad() : 평가 시에는 기울기 계산이 필요 없으므로 메모리 사용을 줄이기 위해 사용
preds[preds >= 0.5] = 1 : torch.max로 출력된 값이 0.5보다 크면 올바르게 예측. → 예측값에 임계값 적용 (이진 분류용 threshold)
epoch_acc : 배치별 누적 정답 수를 전체 데이터셋 크기로 나누어 정확도를 계산

  • eval_model 함수 호출
val_acc_hist = eval_model(resnet18, test_loader, device)

학습을 마친 ResNet18 모델에 테스트 데이터를 입력, 최종 정확도 측정

saved_model : 
  .../chap05/data/catanddog/00.pth
  .../chap05/data/catanddog/01.pth
  ...
Loading model .../chap05/data/catanddog/00.pth
Acc: 0.7347
Loading model .../chap05/data/catanddog/01.pth
Acc: 0.8878
...
Best Acc: 0.948980
  • 에포크별 정확도(Accuracy) 그래프
plt.plot(train_acc_hist)
plt.plot(val_acc_hist)
plt.show()
  • 훈련 데이터에 대한 에포크별 오차(Loss) 그래프
plt.plot(train_loss_hist)
plt.show()

학습된 모델이 예측한 이미지를 시각화

  • 예측 이미지 출력을 위한 전처리
def in_convert(tensor):
    image = tensor.clone().detach().numpy()       # (C, H, W) 텐서를 복사 & 분리 후 NumPy 배열로 변환
    image = image.transpose(1, 2, 0)             # (C, H, W) → (H, W, C)
    image = image * np.array([0.5, 0.5, 0.5]) + np.array([0.5, 0.5, 0.5])
    image = image.clip(0, 1)                     # 값 범위를 [0, 1]로 제한
    return image

tensor.clone().detach().numpy() :
clone(): 원본 텐서의 복사본을 만듦(메모리 공유 X)
detach(): 그래디언트 추적(연산 그래프)에서 분리
.numpy(): CPU 상의 NumPy 배열로 변환

clip(0, 1) : 계산 과정에서 픽셀값을 0~1 사이로 잘라냄

계산 그래프(Computational Graph)

수학적 연산들을 노드(node)로, 그 연산 간의 입출력 관계를 간선(edge) 로 나타낸 그래프
딥러닝 프레임워크는 계산 그래프를 생성하고, 역전파 시 이 그래프를 뒤에서 앞으로 따라가며 기울기를 계산

g(Z) = aZ + b 같은 연산(함수)도 노드가 될 수 있음
국소적 계산이 가능 : Z 값이 변경된 경우, X,Y 결과를 유지한 채 F=A×Z 계산 가능

연쇄 법칙(Chain Rule)

합성 함수의 미분은 각 부분 미분의 곱으로 표현됨
딥러닝에서 역전파는 연쇄 법칙에 기반하여, 출력에서부터 입력까지 기울기를 순차적으로 곱해가며 전달

PyTorch 텐서(이미지)와 예측 결과(클래스) 출력

classes = {0: 'cat', 1: 'dog'}

dataiter = iter(test_loader)  # 테스트 로더에서 배치 하나 추출
images, labels = dataiter.next()
preds = model(images.to(device)).argmax(dim=1).cpu()

fig = plt.figure(figsize=(25,4))

for i in range(20):
    ax = fig.add_subplot(2, 10, i+1, xticks=[], yticks=[])
    # 이미지 시각화
    img = in_convert(images[i])
    plt.imshow(img)

    # 제목 표시 (예: "cat (dog)")
    # color = "green" if 정답이면, "red" otherwise
    ax.set_title(
       "{} ({})".format(
           classes[preds[i].item()],
           classes[labels[i].item()]
       ),
       color=("green" if preds[i] == labels[i] else "red")
    )

plt.subplots_adjust(bottom=0.2, top=0.8, hspace=0.3)
plt.show()

• add_subplot(rows, cols, index, ...) :
2: 서브플롯을 2행으로 구성
10: 서브플롯을 10열로 구성
i+1: 현재 subplot의 위치(왼쪽→오른쪽, 위→아래 순)
xticks=[], yticks=[] : x축·y축 눈금(tick)을 표시하지 않음

• plt.subplots_adjust(...) :
bottom, top : 그림(figure)의 아래·위쪽 여백 조절
hspace : 수직 간격(행 간 간격) 조절

미세 조정(fine-tuning) 기법

사전 학습된 모델의 일부(또는 전체) 합성곱층까지 학습을 허용해 더 세밀한 최적화를 수행

  • 데이터셋이 크고 사전 학습 모델과 유사성이 높은 경우
    모델 전체를 재학습

  • 데이터셋이 크고 사전 학습 모델과 유사성이 낮은 경우
    합성곱층의 뒷부분(상위 레이어)과 데이터 분류기 학습

  • 데이터셋이 작고 사전 학습 모델과 유사성이 작은 경우
    합성곱층의 일부분과 데이터 분류기 학습

  • 데이터셋이 작고 사전 학습 모델과 유사성이 큰 경우
    데이터 분류기(마지막 레이어)만 재학습

설명 가능한 CNN (explainable CNN)

일반적인 CNN 모델은 최종 분류 결과는 제공하지만,
내부에서 어떤 특징을 뽑았고 왜 그런 예측을 했는지 명확히 알려주지 않음
→ blackbox

설명 가능한 CNN : 딥러닝 모델의 내부 처리를 사람이 이해할 수 있는 방식으로 드러내, 결정 근거와 특징 추출 과정을 해석하고 시각화

특성 맵 시각화

특성 맵(feature map)
: CNN의 합성곱 연산 결과로 생성되는 중간 출력 텐서

특성 맵을 시각화하여 어떤 필터가 어떤 패턴(에지, 텍스처 등)에 반응하는지 확인

  • 신경망 정의
class XAI(torch.nn.Module):
    def __init__(self, num_classes=2):
        super(XAI, self).__init__()
        self.features = nn.Sequential(
            # 1) Conv2d(3->64), BN, ReLU, Dropout
            nn.Conv2d(3, 64, kernel_size=3, bias=False),
            nn.BatchNorm2d(64),
            nn.ReLU(inplace=True),
            nn.Dropout(0.3),

            # 2) Conv2d(64->128), BN, ReLU, Dropout
            nn.Conv2d(64, 128, kernel_size=3, padding=1, bias=False),
            nn.BatchNorm2d(64),    # 주의: 보통 128이어야 하나, 원본 코드를 유지
            nn.MaxPool2d(kernel_size=2, stride=2),

            # 3) Conv2d(128->256), BN, ReLU, Dropout
            nn.Conv2d(128, 256, kernel_size=3, padding=1, bias=False),
            nn.BatchNorm2d(128),
            nn.ReLU(inplace=True),
            nn.Dropout(0.4),
            nn.Conv2d(128, 128, kernel_size=3, padding=1, bias=False),
            nn.BatchNorm2d(128),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),
            
            # 4) Conv2d(128->256), BN, ReLU, Dropout ×3
            nn.Conv2d(128, 256, kernel_size=3, padding=1, bias=False),
            nn.BatchNorm2d(256),
            nn.ReLU(inplace=True),
            nn.Dropout(0.4),
            nn.Conv2d(256, 256, kernel_size=3, padding=1, bias=False),
            nn.BatchNorm2d(256),
            nn.ReLU(inplace=True),
            nn.Dropout(0.4),
            nn.Conv2d(256, 256, kernel_size=3, padding=1, bias=False),
            nn.BatchNorm2d(256),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),
            
            # 5) Conv2d(256->512), BN, ReLU, Dropout ×3
            nn.Conv2d(256, 512, kernel_size=3, padding=1, bias=False),
            nn.BatchNorm2d(512),
            nn.ReLU(inplace=True),
            nn.Dropout(0.4),
            nn.Conv2d(512, 512, kernel_size=3, padding=1, bias=False),
            nn.BatchNorm2d(512),
            nn.ReLU(inplace=True),
            nn.Dropout(0.4),
            nn.Conv2d(512, 512, kernel_size=3, padding=1, bias=False),
            nn.BatchNorm2d(512),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),
        )
        
        self.classifier = nn.Sequential(
            nn.Linear(512, 512, bias=False),
            nn.Dropout(0.5),
            nn.BatchNorm1d(512),
            nn.ReLU(inplace=True),
            nn.Dropout(0.5),
            nn.Linear(512, num_classes)
        )

    def forward(self, x):
        x = self.features(x)            # 합성곱 부분(특징 추출)
        x = x.view(-1, 512)            # (배치, 채널, H, W) -> (배치, 512)
        x = self.classifier(x)         # 분류기 부분
        return F.log_softmax(x, dim=1)

return F.log_softmax(x, dim=1) : 출력에 log-softmax를 적용해 로그 확률을 반환

log_softmax 는 softmax 결과의 로그값을 반환
지수 연산을 직접 하지 않아 softmax 함수의 기울기 소멸 문제를 줄여줌

  • 모델 생성 및 설정
model = XAI()         # XAI 클래스 인스턴스 생성
model.to(device)      # 모델을 지정한 device(CPU/GPU)에 로드
model.eval()          # 평가 모드: Dropout, BatchNorm 고정

XAI() : 커스텀 CNN 클래스(class XAI(nn.Module))의 객체를 생성
model.to(device) : 모델의 파라미터와 버퍼를 GPU나 CPU로 이동
model.eval() : 평가 모드로 전환

모델 구조 출력 결과

XAI(
  (features): Sequential(
    (0): Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), bias=False)
    (1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (2): ReLU(inplace=True)
    (3): Dropout(p=0.3, inplace=False)
    (4): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
    (5): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (6): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (7): Conv2d(128, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
    (8): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (9): ReLU(inplace=True)
    (10): Dropout(p=0.4, inplace=False)
    (11): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
    (12): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (13): ReLU(inplace=True)
    (14): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (15): Conv2d(128, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
    (16): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (17): ReLU(inplace=True)
    (18): Dropout(p=0.4, inplace=False)
    (19): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
    (20): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (21): ReLU(inplace=True)
    (22): Dropout(p=0.4, inplace=False)
    (23): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
    (24): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (25): ReLU(inplace=True)
    (26): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (27): Conv2d(256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
    (28): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (29): ReLU(inplace=True)
    (30): Dropout(p=0.4, inplace=False)
    (31): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
    (32): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (33): ReLU(inplace=True)
    (34): Dropout(p=0.4, inplace=False)
    (35): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
    (36): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (37): ReLU(inplace=True)
    (38): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (39): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
    (40): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (41): ReLU(inplace=True)
    (42): Dropout(p=0.4, inplace=False)
    (43): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
    (44): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (45): ReLU(inplace=True)
    (46): Dropout(p=0.4, inplace=False)
    (47): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
    (48): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (49): ReLU(inplace=True)
    (50): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  )
  (classifier): Sequential(
    (0): Linear(in_features=512, out_features=512, bias=False)
    (1): Dropout(p=0.5, inplace=False)
    (2): BatchNorm1d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (3): ReLU(inplace=True)
    (4): Dropout(p=0.5, inplace=False)
    (5): Linear(in_features=512, out_features=2, bias=True)
  )
)
  • 특성 맵을 확인하기 위한 클래스 정의
class LayerActivations:
    def __init__(self, model_layer):
        self.features = None
        self.hook = model_layer.register_forward_hook(self.hook_fn)
    
    def hook_fn(self, module, input, output):
        self.features = output.detach().cpu().numpy()
    
    def remove(self):
        self.hook.remove()

model_layer.register_forward_hook(self.hook_fn)
: PyTorch의 hook 기능은 해당 레이어의 순전파(forward) 결과를 hook_fn이 받아볼 수 있게 등록

  • 이미지 변환
img = cv2.imread("./chap05/data/cat.jpg")
plt.imshow(img)
img = cv2.resize(img, (100, 100), interpolation=cv2.INTER_LINEAR)
img = ToTensor()(img).unsqueeze(0)
print(img.shape)

• cv2.resize(img, (100, 100), interpolation=cv2.INTER_LINEAR) :
이미지를 100×100 크기로 축소/확대
INTER_LINEAR : 보간법(픽셀값 추정 방식)

• .unsqueeze(0) :
이미지 데이터를 텐서로 변환하고 1차원으로 변경
맨 앞에 배치 차원(batch dimension)을 추가 → (1, C, H, W)

  • 모델에 입력할 예시 이미지
  • 특성 맵 추출
result = LayerActivations(model.features, 0)  # 0번째 레이어 hook
output = model(img)                            # 순전파 실행
activations = result.features                  # (배치, 채널, 높이, 너비)
  • 특성 맵 시각화
fig, axes = plt.subplots(4, 4, figsize=(12, 8))
fig.subplots_adjust(left=0, right=1, bottom=0, top=1, wspace=0.05, hspace=0.05)

for row in range(4):
    for col in range(4):
        axis = axes[row][col]
        axis.get_xaxis().set_ticks([])
        axis.get_yaxis().set_ticks([]

        # 예: i번째 채널의 특성 맵 시각화
        channel_idx = row*4 + col
        axis.imshow(activations[0][channel_idx], cmap='gray')
plt.show()
  • 첫번째 Conv2d 계층 특성 맵

  • 20번째 Conv2d 계층 특성 맵

  • 40번째 Conv2d 계층 특성 맵

→ CNN이 깊어질수록 모델이 점점 추상적인 표현을 학습

그래프 합성곱 네트워크 (GCN, Graph Convolutional Network)

그래프

방향성이 있거나 없는 에지로 연결된 노드의 집합

노드(Node): 그래프의 각 점(정점) → 원소
엣지(Edge): 노드 간 연결 관계(간선)

그래프 신경망

그래프 데이터 표현 방식

  • 인접 행렬(adjacency matrix)
    그래프의 노드 간 연결 관계(엣지)를 나타내는 n×n 행렬
    ex. 노드 i와 노드 j가 연결돼 있으면 행렬 [i,j]의 값이 1, 연결이 없으면 0

  • 특성 행렬(feature matrix)
    각 노드가 가진 속성(feature)을 행렬 형태로 나타낸 것
    n×d 형태가 일반적 (노드 n개, 노드당 d차원 특성)

→ 특성 행렬 과정을 거쳐 그래프 특성 추출

그래프 합성곱 네트워크

그래프 형태의 데이터에 합성곱 아이디어를 적용하는 모델

과정

  • CNN 특성 추출
    (여러) 이미지에서 합성곱 신경망(CNN)을 통해 특징 벡터를 추출

  • 그래프 구성
    각 노드(이미지 특징) 사이의 관계를 인접 행렬(edge)로 정의
    노드와 엣지가 정의되면 그래프 데이터가 완성

  • 그래프 합성곱(GCN) 레이어
    그래프 데이터에 합성곱 개념을 확장한 연산을 적용
    노드 임베딩을 이웃 노드에서 가져온 정보와 합성(convolution)하여 업데이트

  • 리드아웃
    GCN 레이어들을 거쳐 각 노드의 임베딩이 업데이트된 후에, 전체 그래프의 정보를 하나의 대표 벡터로 집계

CNN은 이미지 특징을 잘 추출하지만, 이미지/영역 간 관계를 직접 처리하기는 어려움
GCN은 그래프 구조를 잘 다루지만, 노드의 초기 특성이 좋아야 성능이 향상
→ CNN으로 각 노드에 대한 특징을 얻고, GCN으로 노드 간 관계를 반영해 학습

0개의 댓글