구현해볼 모델 반지도 학습 아키텍처
반지도학습(Semi-Supervised Learning)의 핵심
- 지도학습(Supervised Learning): 라벨이 있는 데이터(inputs_l)에 대해 Cross Entropy Loss를 사용하여 학습
- 비지도 학습(Consistency Loss 적용): 라벨이 없는 데이터(inputs_u)에 대해 Teacher 모델의 출력을 정답처럼 학습
- EMA(Exponential Moving Average) 방식: Teacher 모델이 Student 모델보다 더 안정적인 가중치를 유지하면서 업데이트
PyTorch를 활용한 CNN 기반 이미지 분류 모델 구현
CNN(Convolutional Neural Network) 모델 정의
CNN을 활용하여 32×32 크기의 RGB 이미지(3채널)를 입력으로 받아 10개의 클래스로 분류하는 구조를 가진다.
모델은 다층 합성곱(Convolutional Layers), 풀링(Pooling), 드롭아웃(Dropout), 활성화 함수(LeakyReLU), Fully Connected Layer(FC Layer) 등을 포함하여 강건한 이미지 분류 성능을 목표로 한다.
이 코드는 반지도학습(Semi-Supervised Learning) 환경에서 Student-Teacher 모델을 적용하기 위한 **Student 모델(CNN)**을 정의한 것이다.
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
import torchvision
import torchvision.transforms as transforms
from torch.utils.data import DataLoader, Subset
import numpy as np
class CNNModel(nn.Module): # nn.Module super().__init__()
def __init__(self):
super(CNNModel, self).__init__() # 반드시 Add class의 부모 클래스인 nn.Module을 super()을 사용해서 초기화 시켜줘야 한다.
# 입력 텐서 크기: (배치 크기, 3, H, W) # RGB 3채널 입력 이미지
self.conv1a = nn.Conv2d(in_channels=3, out_channels=128, kernel_size=3, padding=1) # (배치 크기, 128, H, W)
self.conv1b = nn.Conv2d(in_channels=128, out_channels=128, kernel_size=3, padding=1) # (배치 크기, 128, H, W)
self.conv1c = nn.Conv2d(in_channels=128, out_channels=128, kernel_size=3, padding=1) # (배치 크기, 128, H, W)
self.conv2a = nn.Conv2d(in_channels=128, out_channels=256, kernel_size=3, padding=1) # (배치 크기, 256, H, W)
self.conv2b = nn.Conv2d(in_channels=256, out_channels=256, kernel_size=3, padding=1) # (배치 크기, 256, H, W)
self.conv2c = nn.Conv2d(in_channels=256, out_channels=256, kernel_size=3, padding=1) # (배치 크기, 256, H, W)
self.conv3a = nn.Conv2d(in_channels=256, out_channels=512, kernel_size=3) # (배치 크기, 512, H-2, W-2) 패딩 없이 kernel_size=3이므로 크기 감소
self.conv3b = nn.Conv2d(in_channels=512, out_channels=256, kernel_size=1) # (배치 크기, 256, H-2, W-2)
self.conv3c = nn.Conv2d(in_channels=256, out_channels=128, kernel_size=1) # (배치 크기, 128, H-2, W-2)
self.dense = nn.Linear(in_features=128, out_features=10) # (배치 크기, 10) # 클래스 수 10개로 변환
self.drop1 = nn.Dropout(0.5)
self.drop2 = nn.Dropout(0.5)
self.maxpool = nn.MaxPool2d(2) # (배치 크기, 채널 수, H/2, W/2)
self.avgpool = nn.AvgPool2d(6) # (배치 크기, 채널 수, H/6, W/6)
self.relu = nn.LeakyReLU(negative_slope=0.1)
def forward(self, x):
x = self.relu(self.conv1a(x))
x = self.relu(self.conv1b(x))
x = self.relu(self.conv1c(x))
x = self.drop1(self.maxpool(x))
x = self.relu(self.conv2a(x))
x = self.relu(self.conv2b(x))
x = self.relu(self.conv2c(x))
x = self.drop2(self.maxpool(x))
x = self.relu(self.conv3a(x))
x = self.relu(self.conv3b(x))
x = self.relu(self.conv3c(x))
x = self.avgpool(x)
x = x.reshape(-1, 128)
x = self.dense(x)
return x
PyTorch CNN 기본 개념 정리
PyTorch에서 CNN(Convolutional Neural Network)을 구현할 때 사용하는 주요 함수와 개념에 대해 정리한다.
1. PyTorch에서 사용하는 주요 함수
1.1 nn.Conv2d(in_channels, out_channels, kernel_size, padding)
합성곱(Convolution) 연산을 수행하는 계층이다. 입력 이미지에서 특징을 추출하는 역할을 하며, 주요 인자는 다음과 같다.
- in_channels: 입력 채널 수
- 예를 들어, RGB 이미지라면 in_channels=3이 된다.
- out_channels: 출력 채널 수 (필터 개수)
- 여러 개의 필터를 적용하여 여러 개의 특징 맵을 추출한다.
- kernel_size: 필터(커널)의 크기
- 예를 들어, kernel_size=3이면 3×3 크기의 필터를 의미한다.
- padding: 입력 데이터 주위에 0을 추가하여 크기를 유지할지 여부
- padding=1이면 한 겹의 0을 추가하여 입력과 출력 크기를 동일하게 유지한다.
예제
conv = nn.Conv2d(in_channels=3, out_channels=128, kernel_size=3, padding=1)
- 3채널(RGB) 입력을 받아 128개의 특징 맵을 생성한다.
- 3×3 크기의 필터를 적용한다.
- padding=1이므로 출력 크기가 입력과 동일하게 유지된다.
1.2 nn.MaxPool2d(kernel_size)
최대 풀링(Max Pooling) 연산을 수행하는 계층이다. 특징 맵의 크기를 줄이고, 중요한 정보만 남기기 위해 사용된다.
- kernel_size=2이면 2×2 영역에서 최댓값을 선택하여 크기를 절반으로 줄인다.
예제
maxpool = nn.MaxPool2d(2)
- 2×2 크기의 필터를 적용하여 특징 맵의 크기를 절반으로 줄인다.
1.3 nn.AvgPool2d(kernel_size)
평균 풀링(Average Pooling) 연산을 수행하는 계층이다. 특징 맵의 크기를 줄이는 과정에서 평균값을 사용한다.
예제
avgpool = nn.AvgPool2d(6)
- 6×6 크기의 영역을 평균값으로 변환하여 크기를 줄인다.
1.4 nn.Linear(in_features, out_features)
완전 연결(Fully Connected, FC) 레이어이다.
CNN의 합성곱 연산 후, 최종적으로 분류를 수행할 때 사용된다.
- in_features: 입력 뉴런 수
- out_features: 출력 뉴런 수
예제
dense = nn.Linear(in_features=128, out_features=10)
- 입력 크기가 128인 벡터를 받아 10개의 클래스로 변환한다.
1.5 nn.LeakyReLU(negative_slope=0.1)
활성화 함수(Activation Function)로 사용된다.
ReLU(Rectified Linear Unit)의 변형으로, 0 이하의 값도 작은 기울기를 유지하도록 한다.
예제
relu = nn.LeakyReLU(negative_slope=0.1)
- 음수 입력에 대해 기울기 0.1을 적용하여 뉴런이 완전히 비활성화되지 않도록 한다.
1.6 nn.Dropout(p)
과적합 방지(Regularization) 기법으로, 일부 뉴런을 랜덤하게 비활성화한다.
- p=0.5이면 뉴런 절반을 비활성화한다.
예제
dropout = nn.Dropout(0.5)
- 학습 중 50%의 뉴런을 랜덤하게 비활성화하여 모델이 특정 뉴런에 의존하지 않도록 한다.
2. in_channels와 out_channels가 자동으로 맞춰지는 원리
CNN의 in_channels와 out_channels는 서로 연결되는 방식에 따라 자동으로 맞춰진다.
즉, 한 층의 출력 채널 수(out_channels)는 다음 층의 입력 채널 수(in_channels)로 설정해야 한다.
2.1 예제 코드 분석
self.conv1a = nn.Conv2d(in_channels=3, out_channels=128, kernel_size=3, padding=1)
- 처음 입력은 RGB 이미지이므로 in_channels=3이다.
- out_channels=128이므로 이 층을 거친 후 출력은 128개의 채널을 갖는다.
self.conv1b = nn.Conv2d(in_channels=128, out_channels=128, kernel_size=3, padding=1)
- 이전 층의 출력이 128채널이므로, in_channels=128로 설정된다.
self.conv2a = nn.Conv2d(in_channels=128, out_channels=256, kernel_size=3, padding=1)
- 채널 수를 128에서 256으로 늘려 특징을 더 많이 추출한다.
self.conv3a = nn.Conv2d(in_channels=256, out_channels=512, kernel_size=3)
self.conv3b = nn.Conv2d(in_channels=512, out_channels=256, kernel_size=1)
self.conv3c = nn.Conv2d(in_channels=256, out_channels=128, kernel_size=1)
- conv3a: 256채널을 받아 512채널을 생성한다.
- conv3b: 1×1 필터를 사용하여 512채널을 256채널로 줄인다.
- conv3c: 다시 1×1 필터를 사용하여 256채널을 128채널로 줄인다.
3. 데이터 텐서의 형식
1️⃣ inputs_l의 텐서 형식
inputs_l.shape # (batch_size, 3, 32, 32)
- batch_size → 한 번에 처리하는 이미지 개수 (예: 64)
- 3 → 채널 개수 (RGB 이미지이므로 3)
- 32 × 32 → CIFAR-10의 이미지 크기
✔️ 예제 텐서 예시 (batch_size=2일 때)
inputs_l = torch.tensor([
[ # 첫 번째 이미지 (3, 32, 32)
[[0.1, 0.2, ...], [0.3, 0.4, ...], ...], # Red 채널 (32, 32)
[[0.5, 0.6, ...], [0.7, 0.8, ...], ...], # Green 채널 (32, 32)
[[0.9, 1.0, ...], [1.1, 1.2, ...], ...] # Blue 채널 (32, 32)
],
[ # 두 번째 이미지 (3, 32, 32)
[[0.2, 0.3, ...], [0.4, 0.5, ...], ...], # Red 채널 (32, 32)
[[0.6, 0.7, ...], [0.8, 0.9, ...], ...], # Green 채널 (32, 32)
[[1.0, 1.1, ...], [1.2, 1.3, ...], ...] # Blue 채널 (32, 32)
]
]) # (2, 3, 32, 32)
여기서 첫 번째 차원(batch_size=2)이 가장 바깥에 있고, 그 안에 각각 3채널(RGB)의 32×32 이미지가 포함되어 있다.
2️⃣ targets_l의 텐서 형식
targets_l.shape # (batch_size,)
- 라벨 값(정답)은 배치 크기만큼의 1D 텐서로 저장된다.
- CIFAR-10에는 10개의 클래스가 있기 때문에 0~9 범위의 정수 값이 들어간다.
✔️ 예제 텐서 예시 (batch_size=2일 때)
targets_l = torch.tensor([3, 7]) # (2,)
- 첫 번째 이미지는 클래스 3 (예: 고양이)
- 두 번째 이미지는 클래스 7 (예: 말)
3️⃣ inputs_u의 텐서 형식
inputs_u.shape # (batch_size, 3, 32, 32)
- 라벨이 없는 데이터도 inputs_l과 동일한 4D 텐서 형식이다.
- 다만, targets_u(정답 레이블)는 제공되지 않으므로 _로 무시한다.
✔️ 예제 텐서 예시 (batch_size=2일 때)
inputs_u = torch.tensor([
[ # 첫 번째 이미지 (3, 32, 32)
[[0.15, 0.25, ...], [0.35, 0.45, ...], ...], # Red 채널 (32, 32)
[[0.55, 0.65, ...], [0.75, 0.85, ...], ...], # Green 채널 (32, 32)
[[0.95, 1.05, ...], [1.15, 1.25, ...], ...] # Blue 채널 (32, 32)
],
[ # 두 번째 이미지 (3, 32, 32)
[[0.22, 0.33, ...], [0.44, 0.55, ...], ...], # Red 채널 (32, 32)
[[0.66, 0.77, ...], [0.88, 0.99, ...], ...], # Green 채널 (32, 32)
[[1.10, 1.21, ...], [1.32, 1.43, ...], ...] # Blue 채널 (32, 32)
]
]) # (2, 3, 32, 32)
📢 데이터 구조 정리
텐서 이름 크기(shape) 데이터 유형 설명
inputs_l | (batch_size, 3, 32, 32) | Float | 라벨이 있는 입력 이미지 |
targets_l | (batch_size,) | Long | 라벨(정답) → 0~9 |
inputs_u | (batch_size, 3, 32, 32) | Float | 라벨이 없는 입력 이미지 |
CIFAR-10 데이터셋 로드 및 데이터 분할
앞서 CNN 모델을 정의한 후, 이제 CIFAR-10 데이터셋을 불러와 학습을 위한 데이터 전처리를 진행할 것 이다.
훈련 데이터셋에 데이터 증강을 적용하고, 라벨이 있는 데이터와 없는 데이터를 분리하여 반지도학습(Semi-Supervised Learning) 환경을 구축한다.
# LOAD DATA AND DROP SOME OF LABELS
train_transform = transforms.Compose([
transforms.RandomHorizontalFlip(), # 50% 확률로 좌우 반전
transforms.RandomCrop(32, padding=4), # 4픽셀 패딩 후 32×32 크기 랜덤 크롭
transforms.ToTensor(), # [0, 255] 범위의 이미지를 [0, 1]로 정규화
])
# 데이터셋 불러오기
trainset = torchvision.datasets.CIFAR10(root='.', train=True, download=True,
transform=train_transform)
testset = torchvision.datasets.CIFAR10(root='.', train=False, download=True,
transform=transforms.ToTensor())
num_labeled = 4000
indices = np.arange(len(trainset)) # 50,000개 훈련 데이터의 인덱스 생성
labeled_indices = indices[:num_labeled] # 처음 4000개만 라벨이 있는 데이터로 사용
unlabeled_indices = indices[num_labeled:] # 나머지 46,000개는 라벨 없이 사용
# Subset()을 사용하여 CIFAR-10 훈련 데이터셋을 두 개의 서브셋으로 나눈다
labeled_dataset = Subset(trainset, labeled_indices)
unlabeled_dataset = Subset(trainset, unlabeled_indices)
batch_size = 64
labeled_loader = DataLoader(labeled_dataset, batch_size=batch_size, shuffle=True)
# labeled_loader: 라벨이 있는 4,000개 데이터를 batch_size=64로 배치 단위로 로드.
unlabeled_loader = DataLoader(unlabeled_dataset, batch_size=batch_size, shuffle=True)
test_loader = DataLoader(testset, batch_size=batch_size, shuffle=False)
Student-Teacher 모델 설정 및 EMA 적용
CIFAR-10 데이터셋을 로드한 후, Student-Teacher 모델을 구성하고, Exponential Moving Average(EMA)를 적용하는 과정을 설명한다.
이를 통해 반지도학습(Semi-Supervised Learning) 환경을 구축한다.
# CNNModel()을 사용하여 두 개의 동일한 모델을 생성
student_model = CNNModel().cuda()
teacher_model = CNNModel().cuda() # 모델을 GPU에서 학습할 수 있도록 설정
teacher_model.load_state_dict(student_model.state_dict())
# Student 모델의 가중치를 Teacher 모델에 복사한다.
# 즉, 초기에는 두 모델이 완전히 동일한 상태에서 시작한다.
# 학습이 진행되면서 Student 모델은 최적화를 통해 변화하지만, Teacher 모델은 EMA 방식으로 천천히 가중치를 업데이트한다.
optimizer = optim.Adam(student_model.parameters(), lr=0.001) # Adam 옵티마이저(Optimizer)를 사용하여 Student 모델을 최적화
ema_decay = 0.995
consistency_weight = 0.1
- Student 모델: 일반적인 모델 학습 과정에서 최적화(Optimization)를 수행하는 모델.
- Teacher 모델: Student 모델의 가중치를 지수 이동 평균(EMA) 방식으로 업데이트하는 모델.
- Teacher 모델은 EMA(지수 이동 평균, Exponential Moving Average) 방식으로 Student 모델의 가중치를 업데이트한다.
- ema_decay = 0.995 → 이전 Teacher 가중치의 99.5%를 유지하고, 0.5%만 새로운 Student 모델의 가중치에서 업데이트한다.
- EMA를 사용하면 Teacher 모델이 Student 모델보다 더 안정적이며 일반화 성능이 좋아진다.
라벨이 있는 데이터만 사용하여 모델 학습 (Supervised Learning)
이제 Adam 옵티마이저를 적용하여 Student 모델을 학습하는 과정을 설명한다.
라벨이 있는 데이터(labeled data)만을 사용하여 지도학습(Supervised Learning)을 진행하며,
손실 함수는 교차 엔트로피 손실(Cross Entropy Loss)를 사용한다.
# TRAIN MODEL WITH LABELED DATA ONLY
# epoch란 전체 데이터셋을 한 번 학습하는 주기를 의미
for epoch in range(50): # 총 50번(epoch) 동안 학습을 진행
print(f"Epoch {epoch+1}")
student_model.train() # PyTorch에서는 train()을 호출하면 모델이 학습 모드(training mode)로 설정
for labeled_data in labeled_loader: # labeled_loader에서 라벨이 있는 데이터(batch 단위)를 하나씩 불러온다.
inputs_l, targets_l = labeled_data
inputs_l, targets_l = inputs_l.cuda(), targets_l.cuda() # 데이터를 .cuda()를 통해 GPU 메모리로 이동
outputs_l = student_model(inputs_l)
supervised_loss = F.cross_entropy(outputs_l, targets_l)
# 현재는 라벨이 있는 데이터만 사용하므로, 손실 값은 supervised_loss와 동일
loss = supervised_loss
optimizer.zero_grad()
loss.backward()
optimizer.step()
- Epoch 반복: 총 50번 학습을 진행한다.
- Train 모드 설정: student_model.train()을 호출하여 학습 모드로 전환.
- 데이터 불러오기: labeled_loader에서 배치 단위로 데이터를 가져온다.
- inputs_l: 입력 이미지 데이터(batch_size, 3, 32, 32)
- targets_l: 해당 이미지의 정답 라벨(batch_size, )
- CIFAR-10은 10개의 클래스를 가지므로 targets_l 값은 [0~9] 중 하나이다.
- GPU 이동: inputs_l, targets_l을 .cuda()로 GPU에 올린다.
- Forward Propagation: student_model(inputs_l)을 통해 예측값 계산.
- inputs_l(입력 이미지)를 Student 모델에 전달하여 예측값을 계산한다.
- outputs_l의 형태: (batch_size, 10)
- 10개의 클래스에 대한 확률 값(logits)을 출력.
- Loss 계산: F.cross_entropy()를 이용하여 Cross Entropy Loss 계산.
- F.cross_entropy()는 분류 문제에서 가장 많이 사용되는 손실 함수이다.
- outputs_l: 모델이 예측한 로짓(logits) 값 (Softmax 이전 값)
- targets_l: 정답 레이블 (0~9 사이의 정수 값)
- Cross Entropy Loss의 역할:
- 모델이 정답 라벨에 가까운 확률을 출력하도록 유도한다.
- 예측값과 정답의 차이를 계산하여 모델이 더 나은 방향으로 업데이트되도록 한다.
- 현재는 라벨이 있는 데이터만 사용하므로, 손실 값은 supervised_loss와 동일하다.
- Gradient 초기화: optimizer.zero_grad()로 이전 기울기 제거.
- 이전 배치에서 계산된 기울기(Gradient)를 초기화한다.
- PyTorch의 autograd는 기본적으로 loss.backward()를 호출할 때 기존 기울기에 새로운 기울기를 더하는 방식이므로,
zero_grad()를 먼저 호출하여 기존 기울기를 초기화해야 한다.
- Backward Propagation: loss.backward()로 가중치의 기울기 계산.
- 가중치 업데이트: optimizer.step()으로 Adam 옵티마이저를 사용하여 모델 업데이트.
모델 평가(Evaluation) - 테스트 데이터셋에서 성능 측정
이제 학습이 완료된 Student 모델의 성능을 평가하는 과정이다.
학습 데이터가 아닌 CIFAR-10 테스트 데이터셋을 사용하여 정확도(Accuracy)를 측정한다.
평가 단계에서는 역전파(Backpropagation)를 하지 않으며, 모델을 평가 모드(Evaluation Mode)로 전환해야 한다.
# EVALUATE MODEL
student_model.eval() # 모델을 평가 모드(Evaluation Mode)로 설정
correct, total = 0, 0
with torch.no_grad(): # 평가 과정에서는 기울기를 계산할 필요가 없으므로, .no_grad()를 사용하면 모델이 불필요한 그래디언트 저장을 방지
for images, labels in test_loader: # test_loader에서 배치 단위로 데이터를 가져옴
images, labels = images.cuda(), labels.cuda()
predicted = student_model(images).argmax(dim=1)
total += labels.size(0)
correct += (predicted == labels).sum()
print(f'Accuracy of stuent model on CIFAR-10: {100 * correct / total:.2f}%')
평가 모드에서는 다음과 같은 변화가 생긴다
- Dropout → 학습 시 랜덤하게 뉴런을 비활성화하지만, 평가 시에는 그대로 유지.
- Batch Normalization → 학습 시에는 배치(batch) 단위로 정규화를 하지만, 평가 시에는 전체 데이터셋의 통계를 사용.
모델의 예측값(Argmax) 가져오기
- student_model(images)를 실행하면, 모델이 각 클래스(0~9)에 대한 확률 값(logits)을 출력한다.
- argmax(dim=1)을 사용하여 가장 확률이 높은 클래스를 선택한다.
- 예를 들어, 모델이 outputs = [0.1, 0.3, 0.2, 0.05, 0.05, 0.1, 0.05, 0.02, 0.01, 0.02]를 출력했다면:
- argmax(dim=1)을 적용하면 가장 큰 값(0.3)의 인덱스인 1을 선택한다.
정확도 계산
- (predicted == labels).sum(): 올바르게 예측한 개수를 더함.
- 전체 테스트 데이터에 대해 total을 증가시키고, 맞춘 개수를 correct에 더한다.
단계설명
1. 모델을 평가 모드로 전환 | student_model.eval()을 호출하여 Dropout과 BatchNorm을 평가 모드로 설정 |
2. 평가를 위한 변수 초기화 | correct = 0, total = 0으로 초기화 |
3. torch.no_grad() 사용 | 그래디언트 계산을 비활성화하여 평가 속도를 높이고 메모리 절약 |
4. 데이터 불러오기 | test_loader에서 배치 단위로 데이터 가져오기 |
5. 모델 예측값 계산 | student_model(images).argmax(dim=1)을 사용하여 가장 높은 확률의 클래스를 선택 |
6. 정확도 업데이트 | 맞춘 개수와 전체 개수를 누적 |
7. 정확도 출력 | 최종 정확도를 % 단위로 출력 |
반지도학습(Semi-Supervised Learning) - 라벨이 없는 데이터도 학습
이제 라벨이 있는 데이터와 없는 데이터를 동시에 사용하여 Student 모델을 학습하는 과정을 설명한다.
Teacher 모델과 Student 모델을 함께 사용하여, Consistency Loss를 적용한 반지도학습(Semi-Supervised Learning)을 수행한다.
Teacher 모델은 EMA(Exponential Moving Average) 방식으로 Student 모델을 업데이트한다.
# TRAIN MODEL WITH UNLABELED DATA
for epoch in range(50):
print(f"Epoch {epoch+1}")
# Student 모델과 Teacher 모델을 학습 모드로 설정
student_model.train()
teacher_model.train()
for (labeled_data, unlabeled_data) in zip(labeled_loader, unlabeled_loader):
inputs_l, targets_l = labeled_data
inputs_u, _ = unlabeled_data
# inputs_l: 라벨이 있는 데이터 (batch_size, 3, 32, 32)
# targets_l: 정답 레이블(batch_size,)
inputs_l, targets_l = inputs_l.cuda(), targets_l.cuda()
inputs_u = inputs_u.cuda()
outputs_l = student_model(inputs_l)
outputs_u_student = student_model(inputs_u)
outputs_u_teacher = teacher_model(inputs_u).detach()
# .detach()를 사용하여 역전파(Backpropagation)에 사용되지 않도록 설정 (Teacher 모델은 학습되지 않음)
# 라벨이 있는 데이터에 대해 Cross Entropy Loss를 계산
supervised_loss = F.cross_entropy(outputs_l, targets_l)
# 라벨이 없는 데이터에 대한 손실을 계산
consistency_loss = F.mse_loss(F.softmax(outputs_u_student, dim=1),
F.softmax(outputs_u_teacher, dim=1))
# 최종 손실 = 지도 학습 손실 + 일관성 손실
loss = supervised_loss + consistency_weight * consistency_loss
optimizer.zero_grad()
loss.backward()
optimizer.step()
# Teacher 모델 업데이트 (EMA 방식 적용)
with torch.no_grad():
for teacher_param, student_param in zip(teacher_model.parameters(),
student_model.parameters()):
teacher_param.data.mul_(ema_decay).add_(student_param.data * (1 - ema_decay))
Teacher 모델 업데이트 (EMA 방식 적용)
- torch.no_grad()를 사용하여 Teacher 모델을 EMA 방식으로 업데이트한다.
- Teacher 모델의 가중치는 Student 모델의 가중치를 조금씩 반영하면서 천천히 업데이트된다.
- teacher_param.data.mul_(ema_decay): 이전 Teacher 모델 가중치의 99.5% 유지 (ema_decay=0.995)
- .add_(student_param.data * (1 - ema_decay)): Student 모델의 가중치 중 0.5%만 반영
- 이렇게 하면 Teacher 모델이 Student 모델보다 더 안정적인 가중치를 가지게 된다.
📌 Teacher 모델에서 .detach()를 사용하는 이유
teacher_model이 학습되지 않는데, ema_decay를 이용해서 Student 모델의 가중치를 반영한다고 했으니 헷갈릴 수 있다.
여기서 "학습"과 "업데이트"를 구분해야 한다.
1️⃣ detach()는 역전파(Backpropagation)에서 제외하기 위한 것
outputs_u_teacher = teacher_model(inputs_u).detach()
- detach()를 사용하면 PyTorch의 오토그래드(Autograd) 시스템에서 Teacher 모델의 그래디언트(기울기)가 계산되지 않음.
- 즉, Teacher 모델은 Student 모델처럼 역전파(Backpropagation)를 통해 직접 학습되지 않는다.
- Teacher 모델은 단순히 Student 모델의 출력을 비교할 기준(reference) 역할을 한다.
즉, Teacher 모델은 "학습"되지 않고, 오직 Student 모델만 학습됨!
하지만 Teacher 모델의 가중치는 계속 업데이트(EMA 방식)된다.
이게 핵심 차이점이다!
2️⃣ Teacher 모델은 EMA 방식으로 업데이트
Teacher 모델이 직접 학습되지 않는 대신, Student 모델의 가중치를 기반으로 천천히 업데이트된다.
즉, Teacher 모델의 가중치는 EMA(Exponential Moving Average) 방식으로 Student 모델의 가중치를 따라간다.
with torch.no_grad():
for teacher_param, student_param in zip(teacher_model.parameters(), student_model.parameters()):
teacher_param.data.mul_(ema_decay).add_(student_param.data * (1 - ema_decay))
여기서 중요한 부분
- torch.no_grad()를 사용하여 Teacher 모델의 가중치를 수동으로 업데이트한다.
- teacher_param.data.mul_(ema_decay):
- Teacher 모델의 기존 가중치를 99.5% 유지 (ema_decay = 0.995)
- .add_(student_param.data * (1 - ema_decay)):
- Student 모델의 가중치 중 0.5%만 반영
- 즉, Teacher 모델이 Student 모델을 따라가지만, 급격히 변화하지 않도록 천천히 업데이트된다.
3️⃣ 만약 .detach()를 사용하지 않으면?
만약 아래 코드에서 .detach()를 제거하면:
outputs_u_teacher = teacher_model(inputs_u) # `.detach()` 제거
- Teacher 모델도 역전파 과정에 포함되어 불필요한 그래디언트 계산이 발생한다.
- 하지만, Teacher 모델은 EMA 방식으로만 업데이트되기 때문에, 역전파 과정에서 의미 없는 그래디언트가 계산될 뿐 적용되지 않는다.
- 이로 인해 메모리 낭비와 연산 속도 저하가 발생할 수 있다.
따라서 .detach()를 사용하여 Teacher 모델이 학습되지 않고 단순한 기준(reference) 역할만 수행하도록 설정하는 것이 올바른 방식이다.
'AI' 카테고리의 다른 글
[인최기] Semi-supervised learning (준지도학습) (0) | 2025.03.12 |
---|---|
Residual Block 이해하기 (0) | 2025.02.21 |
Batch, Step, Epoch 이해하기 (1) | 2025.02.20 |
[논문] A Survey of Resource-efficient LLM and Multimodal Foundation Models (0) | 2025.02.13 |
[Object Detection] R-CNN, Fast R-CNN, Faster R-CNN (2) | 2024.12.18 |