🤖 AI/ML

도메인 적응

Domain Adaptation

다른 도메인 데이터에 모델 적응. 전이학습의 세부 분야.

📖 상세 설명

도메인 적응(Domain Adaptation)은 소스 도메인에서 학습된 모델을 타겟 도메인에서도 잘 동작하도록 조정하는 전이 학습의 핵심 기법입니다. 현실에서 학습 데이터와 실제 운영 데이터는 분포가 다른 경우가 많습니다. 예를 들어, 주간 이미지로 학습한 자율주행 모델은 야간이나 악천후에서 성능이 급격히 저하됩니다. 도메인 적응은 이러한 도메인 시프트(Domain Shift) 문제를 해결합니다.

도메인 적응은 타겟 도메인의 레이블 유무에 따라 분류됩니다. 비지도 도메인 적응(UDA)은 타겟 도메인의 레이블이 전혀 없는 상황을, 준지도 도메인 적응(SSDA)은 일부 레이블만 있는 상황을 다룹니다. 2024-2025년 기준으로 CLIP, DINO v2 같은 foundation 모델을 활용한 도메인 적응이 주류가 되었으며, 프롬프트 튜닝을 통한 제로샷 도메인 적응도 실용화 단계에 접어들었습니다.

기술적으로는 도메인 불변 특징(Domain-Invariant Feature)을 학습하는 방식이 핵심입니다. DANN(Domain Adversarial Neural Network)은 도메인 구분자를 속이도록 특징 추출기를 학습시키고, MMD(Maximum Mean Discrepancy) 기반 방식은 두 도메인의 특징 분포 거리를 최소화합니다. 최근에는 소스 프리(Source-Free) 도메인 적응도 주목받는데, 이는 프라이버시 문제로 소스 데이터를 직접 사용할 수 없는 상황을 위한 것입니다.

실무에서는 시뮬레이션-실환경(Sim-to-Real) 전이가 가장 흔한 활용 사례입니다. 로봇 조작, 자율주행 등에서 시뮬레이터로 대량의 학습 데이터를 생성하고, 도메인 적응을 통해 실제 환경에서의 성능 격차를 줄입니다. 또한 다국어 NLP에서 영어로 학습한 모델을 다른 언어에 적응시키거나, 신제품 이미지 분류에서 기존 제품 데이터를 활용하는 것도 대표적인 사례입니다.

💻 코드 예제

DANN (Domain Adversarial Neural Network) 구현

import torch
import torch.nn as nn
from torch.autograd import Function

# Gradient Reversal Layer - 도메인 적응의 핵심
class GradientReversal(Function):
    @staticmethod
    def forward(ctx, x, alpha):
        ctx.alpha = alpha
        return x.view_as(x)

    @staticmethod
    def backward(ctx, grad_output):
        return grad_output.neg() * ctx.alpha, None

class DANN(nn.Module):
    def __init__(self, num_classes=10, backbone='resnet50'):
        super().__init__()
        # 특징 추출기 (소스와 타겟이 공유)
        self.feature_extractor = torch.hub.load(
            'pytorch/vision', backbone, pretrained=True
        )
        feature_dim = self.feature_extractor.fc.in_features
        self.feature_extractor.fc = nn.Identity()

        # 레이블 분류기
        self.classifier = nn.Sequential(
            nn.Linear(feature_dim, 256),
            nn.ReLU(),
            nn.Dropout(0.5),
            nn.Linear(256, num_classes)
        )

        # 도메인 분류기 (소스=0, 타겟=1 구분)
        self.domain_classifier = nn.Sequential(
            nn.Linear(feature_dim, 256),
            nn.ReLU(),
            nn.Dropout(0.5),
            nn.Linear(256, 2)
        )

    def forward(self, x, alpha=1.0):
        features = self.feature_extractor(x)
        class_output = self.classifier(features)
        # Gradient Reversal로 도메인 구분자를 '속이는' 학습
        reversed_features = GradientReversal.apply(features, alpha)
        domain_output = self.domain_classifier(reversed_features)
        return class_output, domain_output

# 학습 루프
def train_dann(model, source_loader, target_loader, epochs=100):
    optimizer = torch.optim.Adam(model.parameters(), lr=1e-4)
    ce_loss = nn.CrossEntropyLoss()

    for epoch in range(epochs):
        # Alpha 스케줄링 (점진적 증가)
        p = epoch / epochs
        alpha = 2. / (1. + np.exp(-10 * p)) - 1

        for (src_x, src_y), (tgt_x, _) in zip(source_loader, target_loader):
            # 소스 도메인 분류 손실
            class_out, domain_out = model(src_x, alpha)
            class_loss = ce_loss(class_out, src_y)
            src_domain_loss = ce_loss(domain_out, torch.zeros(len(src_x)).long())

            # 타겟 도메인 적대적 손실
            _, tgt_domain_out = model(tgt_x, alpha)
            tgt_domain_loss = ce_loss(tgt_domain_out, torch.ones(len(tgt_x)).long())

            total_loss = class_loss + src_domain_loss + tgt_domain_loss
            optimizer.zero_grad()
            total_loss.backward()
            optimizer.step()

CLIP 기반 제로샷 도메인 적응 (2025년 실무 표준)

import torch
from transformers import CLIPProcessor, CLIPModel

# CLIP을 활용한 프롬프트 기반 도메인 적응
class CLIPDomainAdapter:
    def __init__(self, model_name="openai/clip-vit-large-patch14"):
        self.model = CLIPModel.from_pretrained(model_name)
        self.processor = CLIPProcessor.from_pretrained(model_name)
        self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
        self.model.to(self.device)

    def create_domain_prompts(self, class_names, source_domain, target_domain):
        """도메인 특화 프롬프트 생성"""
        prompts = []
        for cls in class_names:
            # 타겟 도메인에 맞는 프롬프트
            prompts.append(f"a {target_domain} photo of a {cls}")
            prompts.append(f"a {cls} in {target_domain} conditions")
        return prompts

    def adapt_and_classify(self, image, class_names, target_domain="night"):
        """도메인 적응된 제로샷 분류"""
        prompts = [f"a {target_domain} photo of a {cls}" for cls in class_names]
        inputs = self.processor(
            text=prompts,
            images=image,
            return_tensors="pt",
            padding=True
        ).to(self.device)

        with torch.no_grad():
            outputs = self.model(**inputs)
            logits = outputs.logits_per_image
            probs = logits.softmax(dim=1)

        return class_names[probs.argmax()], probs.max().item()

# 사용 예시: 주간 학습 -> 야간 적용
adapter = CLIPDomainAdapter()
classes = ["car", "pedestrian", "bicycle", "truck"]
predicted_class, confidence = adapter.adapt_and_classify(
    night_image, classes, target_domain="nighttime"
)
print(f"예측: {predicted_class} (신뢰도: {confidence:.2%})")

📊 성능 & 비용

도메인 적응 기법 타겟 정확도 향상 추가 학습 시간 레이블 필요
직접 전이 (No Adaptation) 기준 (60-70%) 없음 타겟 X
DANN +10-15% +50% 학습 시간 타겟 X
MMD 기반 +8-12% +30% 학습 시간 타겟 X
CLIP 제로샷 +15-25% (vs. 직접 전이) 추론만 (학습 불필요) 타겟 X
Few-shot Fine-tuning +20-30% +20% 학습 시간 타겟 소량 필요

실무 가이드: 타겟 도메인 레이블이 전혀 없다면 CLIP 기반 제로샷 적응을, 소량이라도 있다면 Few-shot Fine-tuning을 추천합니다. Sim-to-Real 시나리오에서는 Domain Randomization과 DANN을 병행하면 효과적입니다.

🗣️ 실무에서 이렇게 말하세요

프로젝트 킥오프에서:

"학습 데이터는 미국 공장에서 수집했는데 한국 공장에 배포해야 합니다. 도메인 시프트가 예상되니, 한국 공장의 비레이블 이미지를 수집해서 비지도 도메인 적응을 적용하는 방안을 검토해야 합니다."

기술 리뷰에서:

"시뮬레이터 데이터로 학습하면 데이터 수집 비용은 절감되지만 Sim-to-Real 갭이 문제입니다. DANN으로 도메인 불변 특징을 학습하고, 실환경 데이터 1%만 Fine-tuning에 사용하면 충분한 성능을 확보할 수 있습니다."

면접에서 도메인 적응 설명 시:

"도메인 적응의 핵심은 소스와 타겟의 특징 분포를 정렬하는 것입니다. DANN은 적대적 학습으로 도메인 구분이 불가능한 특징을 학습하고, MMD 기반 방법은 통계적 거리를 직접 최소화합니다. 최근에는 CLIP 같은 foundation 모델이 다양한 도메인에서 학습되어 제로샷 적응이 가능해졌습니다."

⚠️ 흔한 실수 & 주의사항

❌ 실수 1: 소스와 타겟의 레이블 공간이 다른 경우 무시

도메인 적응은 기본적으로 소스와 타겟이 같은 클래스를 가정합니다. 타겟에만 존재하는 새로운 클래스가 있거나 일부 클래스가 없으면 Open-set Domain Adaptation 또는 Partial Domain Adaptation 기법을 사용해야 합니다.

❌ 실수 2: 도메인 갭 크기 과소평가

합성 데이터(렌더링 이미지)와 실제 데이터의 갭은 매우 큽니다. 단순 도메인 적응만으로 해결되지 않을 수 있으며, Domain Randomization(색상, 조명, 텍스처 랜덤화)을 함께 적용해야 합니다.

✅ 올바른 방법

먼저 t-SNE나 UMAP으로 소스와 타겟의 특징 분포를 시각화하여 도메인 갭의 크기를 파악하세요. 갭이 크면 CLIP 같은 강력한 사전학습 모델을 백본으로 사용하고, 갭이 작으면 간단한 Fine-tuning으로도 충분할 수 있습니다. 실제 배포 전에 반드시 타겟 도메인의 테스트셋으로 성능을 검증하세요.

🔗 관련 용어

📚 더 배우기