이미지 로딩 중...

전이학습 완벽 가이드 - 슬라이드 1/9
A

AI Generated

2025. 11. 24. · 5 Views

전이학습 완벽 가이드

AI 모델을 처음부터 학습시키지 않고, 이미 학습된 모델의 지식을 활용하는 전이학습 전략을 배워보세요. 데이터가 부족하거나 컴퓨팅 자원이 제한적일 때 효과적으로 모델을 개발하는 방법을 소개합니다.


목차

  1. 전이학습의 기본 개념 - 이미 학습된 지식 재활용하기
  2. 특징 추출 방식 - 사전 학습 모델을 특징 추출기로 활용
  3. 미세 조정 - 사전 학습 모델을 세밀하게 조정하기
  4. 도메인 적응 - 다른 도메인으로 지식 전이하기
  5. 멀티태스크 학습 - 여러 작업을 동시에 학습하기
  6. 제로샷 학습 - 본 적 없는 클래스 인식하기
  7. 점진적 학습 - 잊지 않고 계속 배우기
  8. 메타 학습 - 학습하는 방법을 학습하기

1. 전이학습의 기본 개념 - 이미 학습된 지식 재활용하기

시작하며

여러분이 새로운 이미지 분류 프로젝트를 시작할 때 이런 상황을 겪어본 적 있나요? 데이터는 겨우 500장밖에 없는데, 처음부터 딥러닝 모델을 학습시키려니 성능이 형편없이 나오는 상황 말이죠.

이런 문제는 실제 개발 현장에서 자주 발생합니다. 딥러닝 모델은 보통 수만에서 수백만 장의 데이터가 필요한데, 실제로는 그렇게 많은 데이터를 모으기가 매우 어렵습니다.

또한 모델을 처음부터 학습시키려면 강력한 GPU와 며칠씩 걸리는 학습 시간이 필요합니다. 바로 이럴 때 필요한 것이 전이학습(Transfer Learning)입니다.

마치 수학을 잘하는 학생이 물리학을 배울 때 수학 지식을 활용하듯이, 이미 다른 데이터로 학습된 모델의 지식을 우리 문제에 재활용하는 방법입니다.

개요

간단히 말해서, 전이학습은 이미 대규모 데이터셋으로 학습된 모델을 가져와서 우리의 특정 문제에 맞게 조금만 수정하여 사용하는 기법입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하면, 첫째로 데이터가 부족한 상황에서도 높은 성능을 낼 수 있고, 둘째로 학습 시간과 비용을 크게 줄일 수 있습니다.

예를 들어, 고양이와 강아지를 구분하는 모델을 만들 때, ImageNet이라는 수백만 장의 이미지로 학습된 모델을 가져오면 단 몇백 장의 데이터로도 90% 이상의 정확도를 얻을 수 있습니다. 기존에는 모델을 처음부터 설계하고 수만 장의 데이터를 모아서 며칠 동안 학습시켜야 했다면, 이제는 사전 학습된 모델을 가져와 몇 시간만에 원하는 성능을 얻을 수 있습니다.

전이학습의 핵심 특징은 첫째, 일반적인 특징(edge, texture 등)을 이미 학습했다는 점, 둘째, 마지막 레이어만 교체하여 새로운 문제에 적용할 수 있다는 점, 셋째, 적은 데이터로도 효과적이라는 점입니다. 이러한 특징들이 실무에서 빠른 프로토타입 개발과 비용 절감을 가능하게 만듭니다.

코드 예제

# 전이학습의 기본 예제 - ResNet50 모델 활용
from tensorflow.keras.applications import ResNet50
from tensorflow.keras.layers import Dense, GlobalAveragePooling2D
from tensorflow.keras.models import Model

# 사전 학습된 ResNet50 모델 불러오기 (ImageNet 가중치 포함)
base_model = ResNet50(weights='imagenet', include_top=False, input_shape=(224, 224, 3))

# 사전 학습된 레이어들을 동결 (학습하지 않도록)
for layer in base_model.layers:
    layer.trainable = False

# 새로운 분류 레이어 추가
x = base_model.output
x = GlobalAveragePooling2D()(x)  # 특징 벡터로 변환
x = Dense(256, activation='relu')(x)  # 새로운 Dense 레이어
predictions = Dense(10, activation='softmax')(x)  # 10개 클래스 분류

# 최종 모델 생성
model = Model(inputs=base_model.input, outputs=predictions)

설명

이것이 하는 일: 이미 ImageNet이라는 거대한 데이터셋으로 학습된 ResNet50 모델을 가져와서, 우리가 원하는 10개 클래스를 분류하는 모델로 변환합니다. 첫 번째로, ResNet50 모델을 불러올 때 weights='imagenet'을 지정하여 이미 학습된 가중치를 포함시키고, include_top=False로 원래의 분류 레이어는 제거합니다.

이렇게 하는 이유는 ResNet50이 원래 1000개 클래스를 분류하도록 설계되었지만, 우리는 10개 클래스만 분류하면 되기 때문입니다. 사전 학습된 레이어들은 이미 엣지, 텍스처, 패턴 같은 일반적인 시각적 특징을 추출하는 방법을 알고 있습니다.

그 다음으로, for layer in base_model.layers: layer.trainable = False를 실행하면서 모든 사전 학습된 레이어를 동결합니다. 동결한다는 것은 이 레이어들의 가중치를 학습 과정에서 업데이트하지 않는다는 의미입니다.

왜냐하면 이미 좋은 특징 추출 능력을 갖추고 있으므로, 굳이 다시 학습시킬 필요가 없기 때문입니다. 이렇게 하면 학습 시간도 크게 줄어듭니다.

세 번째 단계에서는 GlobalAveragePooling2D로 특징 맵을 1차원 벡터로 변환하고, 새로운 Dense 레이어들을 추가합니다. 이 새로운 레이어들만 우리의 데이터로 학습됩니다.

마지막 Dense 레이어의 출력 크기를 10으로 설정하여 우리가 원하는 10개 클래스를 분류하도록 만듭니다. 마지막으로, Model 객체를 생성하여 입력은 ResNet50의 입력을, 출력은 우리가 새로 만든 분류 레이어를 사용하도록 연결합니다.

이제 이 모델을 우리의 작은 데이터셋으로 학습시키면, ResNet50이 ImageNet에서 배운 일반적인 시각 지식과 우리 데이터에서 배운 특정 클래스 구분 능력이 결합됩니다. 여러분이 이 코드를 사용하면 처음부터 학습할 때 필요한 데이터의 10분의 1 정도만으로도 비슷하거나 더 좋은 성능을 얻을 수 있습니다.

또한 학습 시간이 몇 시간에서 몇 분으로 단축되고, GPU 메모리 사용량도 줄어들며, 과적합(overfitting) 위험도 크게 감소합니다.

실전 팁

💡 처음에는 모든 사전 학습 레이어를 동결하고, 새로운 레이어만 학습시킨 후, 성능이 안정되면 상위 몇 개 레이어를 해제하여 미세 조정(Fine-tuning)하면 더 좋은 결과를 얻을 수 있습니다.

💡 사전 학습된 모델이 요구하는 입력 크기와 정규화 방법을 정확히 따라야 합니다. ResNet50은 224x224 크기와 특정 정규화 방식을 기대하므로, preprocess_input 함수를 사용하세요.

💡 데이터가 사전 학습 데이터와 비슷할수록 전이학습 효과가 좋습니다. 의료 이미지처럼 완전히 다른 도메인이라면 더 많은 레이어를 재학습시켜야 할 수 있습니다.

💡 배치 정규화(Batch Normalization) 레이어가 있는 모델을 미세 조정할 때는 layer.trainable = True 설정 후 model.compile()을 다시 호출해야 정상 작동합니다.

💡 학습률(learning rate)을 사전 학습 때보다 10~100배 낮게 설정하세요. 이미 좋은 가중치를 가지고 있으므로, 너무 큰 학습률로 업데이트하면 오히려 성능이 나빠질 수 있습니다.


2. 특징 추출 방식 - 사전 학습 모델을 특징 추출기로 활용

시작하며

여러분이 이미지 데이터를 가지고 작업할 때, "이 이미지들에서 중요한 특징을 어떻게 뽑아낼까?"라는 고민을 한 적 있나요? 예를 들어, 고양이 사진에서 귀 모양, 수염, 눈 색깔 같은 특징들을 자동으로 추출하고 싶은데, 이런 특징 추출기를 직접 만들기는 너무 어렵습니다.

이런 문제는 머신러닝 프로젝트 초반에 자주 발생합니다. 좋은 특징을 추출하지 못하면 아무리 좋은 분류 알고리즘을 사용해도 성능이 나쁩니다.

전통적으로는 도메인 전문가가 수작업으로 특징을 설계했지만, 이는 시간이 오래 걸리고 주관적입니다. 바로 이럴 때 필요한 것이 특징 추출(Feature Extraction) 방식의 전이학습입니다.

사전 학습된 딥러닝 모델을 특징 추출기로만 사용하고, 추출된 특징으로 간단한 분류기를 학습시키는 방법입니다.

개요

간단히 말해서, 특징 추출 방식은 사전 학습된 모델의 중간 레이어 출력을 특징 벡터로 사용하여, 이를 기반으로 새로운 분류기를 학습시키는 방법입니다. 왜 이 방식이 필요한지 실무 관점에서 설명하면, 첫째로 사전 학습된 모델을 전혀 수정하지 않으므로 매우 빠르고 안전하며, 둘째로 추출된 특징을 다양한 머신러닝 알고리즘(SVM, Random Forest 등)과 함께 사용할 수 있습니다.

예를 들어, 의료 이미지 분류 프로젝트에서 ResNet으로 특징을 추출하고 SVM으로 분류하면, 딥러닝의 강력한 특징 추출 능력과 SVM의 해석 가능성을 동시에 얻을 수 있습니다. 기존에는 SIFT, HOG 같은 전통적인 특징 추출 방법을 사용했다면, 이제는 사전 학습된 딥러닝 모델이 자동으로 학습한 고차원 특징을 활용할 수 있습니다.

특징 추출 방식의 핵심은 첫째, 사전 학습된 레이어를 완전히 동결한다는 점, 둘째, 모델의 중간 출력을 특징 벡터로 저장한다는 점, 셋째, 간단한 분류기만 새로 학습시킨다는 점입니다. 이러한 특징들이 빠른 실험과 다양한 알고리즘 비교를 가능하게 만듭니다.

코드 예제

# 특징 추출 방식 - VGG16을 특징 추출기로 활용
from tensorflow.keras.applications import VGG16
from tensorflow.keras.preprocessing import image
from tensorflow.keras.applications.vgg16 import preprocess_input
import numpy as np
from sklearn.svm import SVC

# VGG16 모델을 특징 추출기로 불러오기
feature_extractor = VGG16(weights='imagenet', include_top=False, pooling='avg')

# 이미지를 특징 벡터로 변환하는 함수
def extract_features(img_path):
    img = image.load_img(img_path, target_size=(224, 224))
    img_array = image.img_to_array(img)
    img_array = np.expand_dims(img_array, axis=0)
    img_array = preprocess_input(img_array)
    features = feature_extractor.predict(img_array)
    return features.flatten()

# 여러 이미지에서 특징 추출
image_paths = ['cat1.jpg', 'cat2.jpg', 'dog1.jpg', 'dog2.jpg']
features_list = [extract_features(path) for path in image_paths]
labels = [0, 0, 1, 1]  # 0: 고양이, 1: 강아지

# 추출된 특징으로 SVM 분류기 학습
X = np.array(features_list)
y = np.array(labels)
classifier = SVC(kernel='rbf')
classifier.fit(X, y)

설명

이것이 하는 일: VGG16이라는 사전 학습된 모델을 사용하여 이미지를 512차원의 특징 벡터로 변환하고, 이 특징 벡터를 SVM 분류기로 학습시켜 고양이와 강아지를 구분합니다. 첫 번째로, VGG16 모델을 include_top=Falsepooling='avg'로 불러옵니다.

include_top=False는 원래의 분류 레이어를 제거하고, pooling='avg'는 마지막 합성곱 레이어의 출력을 평균 풀링하여 고정된 크기의 벡터로 만듭니다. 이렇게 하면 VGG16의 깊은 레이어들이 학습한 복잡한 시각 패턴을 512개 숫자로 압축한 특징 벡터를 얻을 수 있습니다.

그 다음으로, extract_features 함수가 각 이미지를 불러와서 224x224 크기로 조정하고, VGG16이 기대하는 형식으로 전처리한 후, 모델을 통과시켜 특징 벡터를 추출합니다. preprocess_input은 VGG16 학습 시 사용된 정규화 방법을 적용하여 모델이 최상의 성능을 내도록 합니다.

flatten()으로 다차원 배열을 1차원으로 펼치면 머신러닝 알고리즘이 바로 사용할 수 있는 형태가 됩니다. 세 번째 단계에서는 여러 이미지에서 특징을 추출하여 리스트로 저장합니다.

실제 프로젝트에서는 수천 개의 이미지를 처리하겠지만, 이 예제에서는 4개만 사용했습니다. 중요한 점은 VGG16이 한 번만 불러와지고, 각 이미지마다 순전파(forward pass)만 한 번씩 수행된다는 것입니다.

학습이 아니라 추론만 하므로 매우 빠릅니다. 마지막으로, 추출된 특징 벡터들을 NumPy 배열로 변환하여 SVM 분류기를 학습시킵니다.

여기서 핵심은 VGG16은 전혀 학습되지 않고, SVM만 우리 데이터로 학습된다는 점입니다. VGG16이 추출한 풍부한 특징 덕분에 SVM도 적은 데이터로 좋은 성능을 냅니다.

여러분이 이 방식을 사용하면 GPU 없이도 CPU만으로 빠르게 실험할 수 있고, 특징을 한 번만 추출해서 저장한 후 다양한 분류기를 시도해볼 수 있습니다. 또한 딥러닝 프레임워크에 익숙하지 않아도 scikit-learn 같은 친숙한 도구를 사용할 수 있으며, 모델 해석이 더 쉬워집니다.

실전 팁

💡 대량의 이미지를 처리할 때는 특징을 미리 추출하여 파일로 저장해두면, 이후 실험에서 반복 계산을 피할 수 있어 시간이 크게 절약됩니다.

💡 어떤 레이어의 출력을 특징으로 사용할지 실험해보세요. 초반 레이어는 엣지 같은 저수준 특징을, 후반 레이어는 객체 부분 같은 고수준 특징을 담고 있습니다.

💡 추출된 특징의 차원이 너무 높으면 PCA나 t-SNE로 차원을 축소하여 분류기 성능을 높이고 과적합을 방지할 수 있습니다.

💡 여러 사전 학습 모델(VGG16, ResNet, Inception 등)에서 특징을 추출하여 결합하면 앙상블 효과로 성능이 향상됩니다.

💡 특징 벡터를 정규화(L2 normalization)하면 거리 기반 분류기(KNN, SVM)의 성능이 개선되는 경우가 많습니다.


3. 미세 조정 - 사전 학습 모델을 세밀하게 조정하기

시작하며

여러분이 전이학습으로 모델을 만들었는데 성능이 80%에서 더 올라가지 않는 상황을 겪어본 적 있나요? 특히 여러분의 데이터가 ImageNet 같은 일반 이미지와 조금 다를 때, 예를 들어 X-ray 이미지나 위성 사진처럼 특수한 도메인일 때 이런 일이 자주 발생합니다.

이런 문제는 사전 학습된 모델이 일반적인 특징은 잘 포착하지만, 여러분의 특수한 데이터에 완벽히 맞지는 않기 때문에 발생합니다. 동결된 레이어들이 여러분의 데이터에 최적화되지 않은 특징을 추출하고 있는 것입니다.

바로 이럴 때 필요한 것이 미세 조정(Fine-tuning)입니다. 사전 학습된 레이어 중 일부를 "해동"하여 여러분의 데이터로 다시 학습시켜, 모델이 여러분의 특정 문제에 더 잘 맞도록 조정하는 방법입니다.

개요

간단히 말해서, 미세 조정은 사전 학습된 모델의 상위 레이어들을 해제하여 여러분의 데이터로 재학습시킴으로써, 일반적인 지식과 도메인 특화 지식을 균형있게 가지도록 만드는 기법입니다. 왜 이 방식이 필요한지 실무 관점에서 설명하면, 특징 추출 방식보다 더 높은 성능을 달성할 수 있고, 여러분의 데이터가 사전 학습 데이터와 다를 때 특히 효과적입니다.

예를 들어, 의료 영상 분류 프로젝트에서 처음에는 전체 모델을 동결했다가, 나중에 상위 50개 레이어를 미세 조정하면 정확도가 80%에서 92%로 향상될 수 있습니다. 기존에는 전체 모델을 동결하거나 처음부터 학습시켰다면, 이제는 그 중간 지점인 미세 조정으로 최적의 균형을 찾을 수 있습니다.

미세 조정의 핵심 특징은 첫째, 하위 레이어는 동결하고 상위 레이어만 해제한다는 점, 둘째, 매우 작은 학습률을 사용한다는 점, 셋째, 이미 잘 학습된 모델을 조금씩만 수정한다는 점입니다. 이러한 특징들이 과적합을 방지하면서도 성능을 크게 향상시킵니다.

코드 예제

# 미세 조정 - ResNet50의 상위 레이어 재학습
from tensorflow.keras.applications import ResNet50
from tensorflow.keras.layers import Dense, GlobalAveragePooling2D
from tensorflow.keras.models import Model
from tensorflow.keras.optimizers import Adam

# 1단계: 먼저 새로운 레이어만 학습
base_model = ResNet50(weights='imagenet', include_top=False, input_shape=(224, 224, 3))
for layer in base_model.layers:
    layer.trainable = False

x = base_model.output
x = GlobalAveragePooling2D()(x)
x = Dense(256, activation='relu')(x)
predictions = Dense(10, activation='softmax')(x)
model = Model(inputs=base_model.input, outputs=predictions)

model.compile(optimizer=Adam(learning_rate=0.001), loss='categorical_crossentropy', metrics=['accuracy'])
# model.fit(train_data, epochs=5)  # 새 레이어 먼저 학습

# 2단계: 상위 레이어 해제하고 미세 조정
for layer in base_model.layers[-30:]:  # 마지막 30개 레이어 해제
    layer.trainable = True

# 매우 작은 학습률로 재컴파일
model.compile(optimizer=Adam(learning_rate=0.00001), loss='categorical_crossentropy', metrics=['accuracy'])
# model.fit(train_data, epochs=10)  # 미세 조정 학습

설명

이것이 하는 일: ResNet50 모델의 하위 레이어는 동결된 상태로 유지하고, 상위 30개 레이어만 여러분의 데이터로 재학습시켜 일반 지식과 특화 지식을 모두 갖춘 모델을 만듭니다. 첫 번째로, 1단계에서는 전체 사전 학습 모델을 동결하고 새로 추가한 분류 레이어만 학습시킵니다.

이 과정이 왜 필요한지 설명하면, 새로운 레이어는 무작위로 초기화되어 있어서 처음에는 엉터리 출력을 만듭니다. 이 상태에서 사전 학습 레이어까지 함께 학습시키면, 엉터리 그래디언트가 역전파되어 사전 학습된 좋은 가중치를 망가뜨립니다.

따라서 먼저 새 레이어만 학습시켜 안정화시킵니다. 그 다음으로, 새 레이어가 어느 정도 학습된 후 2단계로 넘어갑니다.

base_model.layers[-30:]은 파이썬의 슬라이싱 문법으로 마지막 30개 레이어를 선택합니다. 왜 마지막 레이어들만 해제하냐면, 딥러닝 모델은 초기 레이어에서 엣지나 색상 같은 일반적 특징을, 후기 레이어에서 도메인 특화적인 복잡한 패턴을 학습하기 때문입니다.

일반적 특징은 대부분의 이미지에 공통적이므로 그대로 두고, 특화 특징만 여러분의 데이터에 맞게 조정합니다. 세 번째 단계에서 매우 중요한 부분이 학습률 조정입니다.

처음 학습 시 0.001이었던 학습률을 0.00001로 100배 줄입니다. 왜냐하면 해당 레이어들은 이미 좋은 가중치를 가지고 있으므로, 큰 학습률로 급격히 변경하면 좋은 특징들을 잃어버릴 수 있기 때문입니다.

작은 학습률로 천천히 미세하게 조정하는 것이 핵심입니다. 마지막으로, model.compile()을 다시 호출해야 합니다.

Keras에서는 레이어의 trainable 속성을 변경한 후 반드시 재컴파일해야 변경사항이 적용됩니다. 이 부분을 놓치면 여전히 모든 레이어가 동결된 상태로 학습되어 미세 조정 효과가 없습니다.

여러분이 이 방법을 사용하면 특징 추출 방식보다 5~15% 높은 정확도를 얻을 수 있고, 여러분의 특수한 도메인에 맞춤화된 모델을 만들 수 있습니다. 다만 학습 시간은 더 걸리고 과적합 위험이 있으므로 검증 세트로 성능을 모니터링해야 합니다.

실전 팁

💡 미세 조정은 반드시 2단계로 진행하세요. 처음부터 모든 레이어를 함께 학습시키면 사전 학습 가중치가 망가져 성능이 오히려 나빠집니다.

💡 해제할 레이어 개수는 실험으로 찾아야 합니다. 데이터가 많고 사전 학습 데이터와 다르면 더 많은 레이어를, 데이터가 적고 비슷하면 더 적은 레이어를 해제하세요.

💡 배치 정규화 레이어를 포함한 모델(ResNet, Inception 등)을 미세 조정할 때는 training=False 모드로 추론하거나, BN 레이어만 동결 유지하는 것을 고려하세요.

💡 학습률 스케줄링을 사용하여 에폭이 지날수록 학습률을 더 줄이면 미세 조정 효과가 극대화됩니다. ReduceLROnPlateau 콜백이 유용합니다.

💡 검증 손실이 증가하기 시작하면 즉시 학습을 중단하세요(Early Stopping). 미세 조정은 과적합 위험이 높으므로 모니터링이 필수입니다.


4. 도메인 적응 - 다른 도메인으로 지식 전이하기

시작하며

여러분이 자연 이미지(고양이, 자동차 등)로 학습된 모델을 의료 X-ray 이미지 분류에 사용하려고 할 때, 성능이 기대만큼 나오지 않는 경험을 해보셨나요? 이미지의 특성이 너무 달라서 전이학습 효과가 제한적인 상황입니다.

이런 문제는 소스 도메인(사전 학습 데이터)과 타겟 도메인(여러분의 데이터)이 크게 다를 때 발생합니다. 예를 들어 자연 이미지는 컬러이고 다양한 조명을 가지지만, X-ray는 흑백이고 특수한 대비 패턴을 가집니다.

단순 전이학습으로는 이 간격을 충분히 메우지 못합니다. 바로 이럴 때 필요한 것이 도메인 적응(Domain Adaptation)입니다.

두 도메인 사이의 차이를 체계적으로 줄여서, 한 도메인에서 학습한 지식이 다른 도메인에서도 잘 작동하도록 만드는 고급 전이학습 기법입니다.

개요

간단히 말해서, 도메인 적응은 소스 도메인과 타겟 도메인의 데이터 분포 차이를 모델이 무시하도록 학습시켜, 도메인에 관계없이 일관된 특징을 추출하도록 만드는 기법입니다. 왜 이 방식이 필요한지 실무 관점에서 설명하면, 타겟 도메인의 레이블된 데이터가 부족하거나 없을 때도 모델을 만들 수 있고, 완전히 다른 종류의 데이터 간에도 지식 전이가 가능해집니다.

예를 들어, 낮 시간 자율주행 데이터로 학습한 모델을 야간 주행에 적응시키거나, 합성 데이터로 학습한 로봇을 실제 환경에 적응시킬 수 있습니다. 기존에는 타겟 도메인의 데이터를 처음부터 대량으로 수집하고 레이블링해야 했다면, 이제는 적은 양의 타겟 데이터나 심지어 레이블이 없는 데이터만으로도 도메인 간 전이가 가능합니다.

도메인 적응의 핵심은 첫째, 도메인 불변 특징(domain-invariant features)을 학습한다는 점, 둘째, 적대적 학습이나 통계적 정렬 기법을 사용한다는 점, 셋째, 타겟 도메인의 레이블 없이도 작동할 수 있다는 점입니다. 이러한 특징들이 실무에서 데이터 수집 비용을 크게 줄이고 모델의 일반화 능력을 향상시킵니다.

코드 예제

# 도메인 적응 - 간단한 도메인 혼동 기법
from tensorflow.keras.layers import Input, Dense, Lambda
from tensorflow.keras.models import Model
from tensorflow.keras import backend as K
import tensorflow as tf

# 그래디언트 반전 레이어 (Gradient Reversal Layer)
@tf.custom_gradient
def gradient_reversal(x, lambda_param=1.0):
    def grad(dy):
        return -lambda_param * dy, None
    return x, grad

class GradientReversalLayer(tf.keras.layers.Layer):
    def __init__(self, lambda_param=1.0, **kwargs):
        super().__init__(**kwargs)
        self.lambda_param = lambda_param

    def call(self, x):
        return gradient_reversal(x, self.lambda_param)

# 도메인 적응 모델 구축
input_img = Input(shape=(224, 224, 3))
# 특징 추출기 (소스와 타겟 공유)
feature_extractor = base_model(input_img)  # 사전 학습 모델
features = GlobalAveragePooling2D()(feature_extractor)

# 작업 분류기 (레이블 예측)
task_output = Dense(128, activation='relu')(features)
task_output = Dense(10, activation='softmax', name='task_output')(task_output)

# 도메인 분류기 (도메인 구분 - 그래디언트 반전)
domain_features = GradientReversalLayer(lambda_param=0.1)(features)
domain_output = Dense(64, activation='relu')(domain_features)
domain_output = Dense(1, activation='sigmoid', name='domain_output')(domain_output)

model = Model(inputs=input_img, outputs=[task_output, domain_output])

설명

이것이 하는 일: 하나의 모델이 두 가지 일을 동시에 학습합니다. 작업 분류기는 이미지의 클래스를 맞추고, 도메인 분류기는 이미지가 어느 도메인에서 왔는지 구분합니다.

그런데 특징 추출기는 도메인 분류기를 속이도록 학습되어, 결과적으로 도메인에 무관한 특징을 추출하게 됩니다. 첫 번째로, GradientReversalLayer라는 특수한 레이어를 정의합니다.

이 레이어의 마법은 순전파 때는 입력을 그대로 통과시키지만, 역전파 때는 그래디언트의 부호를 반전시킨다는 점입니다. 이것이 왜 중요한지 설명하면, 도메인 분류기는 도메인을 잘 구분하도록 학습되지만, 그래디언트 반전 때문에 특징 추출기는 반대로 도메인을 구분하지 못하게 만드는 특징을 학습하게 됩니다.

이는 적대적 학습(adversarial learning)의 핵심 아이디어입니다. 그 다음으로, 모델 구조를 보면 하나의 특징 추출기에서 두 개의 브랜치로 나뉩니다.

첫 번째 브랜치는 작업 분류기로, 소스 도메인의 레이블된 데이터로 학습되어 실제 작업(예: 고양이 vs 강아지)을 수행합니다. 두 번째 브랜치는 도메인 분류기로, 이미지가 소스 도메인인지 타겟 도메인인지 구분하려고 합니다.

하지만 그래디언트 반전 레이어가 중간에 있어서, 특징 추출기는 도메인 분류기를 혼란스럽게 만들도록 학습됩니다. 세 번째 단계에서 실제 학습이 일어날 때, 모델은 세 가지 목표를 동시에 추구합니다.

  1. 소스 도메인에서 작업을 정확히 수행, 2) 도메인을 잘 구분, 3) 동시에 도메인을 구분하지 못하게 만들기. 이 세 번째 목표가 역설적으로 들리지만, 그래디언트 반전 덕분에 특징 추출기는 "두 도메인에서 공통적으로 유용한 특징"만 학습하게 됩니다.

마지막으로, 학습된 모델을 타겟 도메인에 적용하면, 특징 추출기가 도메인 차이를 무시하고 핵심 특징만 추출하므로, 타겟 도메인에서도 작업 분류기가 잘 작동합니다. 심지어 타겟 도메인의 레이블이 전혀 없어도, 도메인 불변 특징을 학습했기 때문에 일반화가 가능합니다.

여러분이 이 방법을 사용하면 완전히 다른 종류의 데이터 간에도 지식을 전이할 수 있고, 타겟 도메인의 레이블링 비용을 크게 줄일 수 있으며, 모델의 강건성(robustness)이 향상됩니다. 예를 들어 시뮬레이션 데이터로 학습하고 실제 환경에 적용하는 sim-to-real 문제에 매우 효과적입니다.

실전 팁

💡 그래디언트 반전의 lambda 파라미터는 학습 초기에는 작게(0.01), 후기에는 크게(1.0) 조정하면 안정적으로 수렴합니다.

💡 소스와 타겟 데이터를 배치에서 균형있게 섞어서 학습하세요. 50:50 비율이 일반적으로 좋습니다.

💡 도메인 분류기의 정확도가 50% 근처(무작위 추측 수준)가 되면 도메인 적응이 성공적으로 이루어진 것입니다.

💡 타겟 도메인에 일부 레이블 데이터가 있다면, 이를 함께 사용하는 준지도 도메인 적응(semi-supervised domain adaptation)으로 성능을 더 높일 수 있습니다.

💡 Maximum Mean Discrepancy(MMD)나 CORAL 같은 통계적 정렬 기법도 시도해보세요. 구현이 더 간단하고 경우에 따라 효과적입니다.


5. 멀티태스크 학습 - 여러 작업을 동시에 학습하기

시작하며

여러분이 이미지에서 객체를 분류하는 모델과 이미지를 세그멘테이션하는 모델을 각각 따로 만들었는데, 두 모델이 비슷한 특징을 학습하고 있어서 중복이 많다는 느낌을 받은 적 있나요? 각 모델을 독립적으로 학습시키려니 시간도 두 배로 걸리고 GPU 메모리도 많이 필요합니다.

이런 문제는 관련된 여러 작업을 별도로 처리할 때 자주 발생합니다. 예를 들어 자율주행에서는 차선 검출, 객체 인식, 거리 추정 등 여러 작업이 필요한데, 각각 독립 모델로 만들면 비효율적이고 추론 시간도 오래 걸립니다.

바로 이럴 때 필요한 것이 멀티태스크 학습(Multi-Task Learning)입니다. 하나의 특징 추출기를 여러 작업이 공유하도록 만들어, 작업 간 지식을 전이하면서 효율성과 성능을 동시에 높이는 방법입니다.

개요

간단히 말해서, 멀티태스크 학습은 여러 관련 작업을 하나의 모델에서 동시에 학습시켜, 공통 특징을 공유하고 작업 간 상호 보완적인 지식을 활용하는 기법입니다. 왜 이 방식이 필요한지 실무 관점에서 설명하면, 첫째로 모델 크기와 추론 시간이 줄어들고, 둘째로 한 작업의 데이터가 다른 작업의 학습을 도와 데이터 효율성이 높아지며, 셋째로 공유 특징이 더 일반화된 표현을 학습하여 과적합을 방지합니다.

예를 들어, 얼굴 인식과 나이 추정을 함께 학습하면 각각 따로 학습할 때보다 둘 다 성능이 향상됩니다. 기존에는 각 작업마다 별도의 모델을 학습시켜 관리했다면, 이제는 하나의 통합 모델로 여러 작업을 효율적으로 처리할 수 있습니다.

멀티태스크 학습의 핵심은 첫째, 공유 레이어(shared layers)와 작업별 레이어(task-specific layers)로 구성된다는 점, 둘째, 여러 손실 함수를 가중 합산하여 학습한다는 점, 셋째, 작업 간 암묵적 데이터 증강 효과가 있다는 점입니다. 이러한 특징들이 모델의 일반화 능력과 효율성을 동시에 향상시킵니다.

코드 예제

# 멀티태스크 학습 - 이미지 분류와 회귀를 동시에
from tensorflow.keras.applications import MobileNetV2
from tensorflow.keras.layers import Input, Dense, GlobalAveragePooling2D, Dropout
from tensorflow.keras.models import Model

# 공유 백본 (특징 추출기)
input_img = Input(shape=(224, 224, 3))
backbone = MobileNetV2(weights='imagenet', include_top=False, input_tensor=input_img)
shared_features = GlobalAveragePooling2D()(backbone.output)

# 작업 1: 이미지 분류 (예: 동물 종류)
classification_branch = Dense(256, activation='relu')(shared_features)
classification_branch = Dropout(0.5)(classification_branch)
classification_output = Dense(10, activation='softmax', name='classification')(classification_branch)

# 작업 2: 나이 회귀 (예: 동물 나이 추정)
regression_branch = Dense(128, activation='relu')(shared_features)
regression_branch = Dropout(0.3)(regression_branch)
age_output = Dense(1, activation='linear', name='age_regression')(regression_branch)

# 작업 3: 속성 분류 (예: 색상)
attribute_branch = Dense(128, activation='relu')(shared_features)
color_output = Dense(5, activation='softmax', name='color_classification')(attribute_branch)

# 멀티태스크 모델 생성
model = Model(inputs=input_img, outputs=[classification_output, age_output, color_output])

# 손실 가중치로 작업 균형 조정
model.compile(
    optimizer='adam',
    loss={'classification': 'categorical_crossentropy',
          'age_regression': 'mse',
          'color_classification': 'categorical_crossentropy'},
    loss_weights={'classification': 1.0, 'age_regression': 0.5, 'color_classification': 0.8},
    metrics={'classification': 'accuracy', 'age_regression': 'mae', 'color_classification': 'accuracy'}
)

설명

이것이 하는 일: MobileNetV2라는 경량 백본을 공유 특징 추출기로 사용하고, 세 가지 작업(분류, 나이 회귀, 색상 분류)을 동시에 수행하는 멀티태스크 모델을 만듭니다. 각 작업은 공유 특징에서 분기된 별도의 헤드를 가집니다.

첫 번째로, MobileNetV2를 백본으로 선택한 이유는 경량이면서도 강력한 특징 추출 능력을 가지고 있어, 여러 작업을 실시간으로 처리해야 하는 상황에 적합하기 때문입니다. GlobalAveragePooling2D로 공간 차원을 제거하여 작업별 헤드가 사용할 수 있는 고정 크기 특징 벡터를 만듭니다.

이 벡터가 세 가지 작업 모두의 기반이 됩니다. 그 다음으로, 세 개의 작업별 브랜치를 설계합니다.

각 브랜치는 공유 특징을 입력받지만 서로 다른 구조와 출력을 가집니다. 분류 브랜치는 256 차원의 큰 Dense 레이어로 복잡한 분류를, 회귀 브랜치는 128 차원으로 연속값 예측을, 속성 브랜치는 128 차원으로 또 다른 분류를 수행합니다.

Dropout을 각기 다르게 설정한 것은 작업마다 과적합 위험이 다르기 때문입니다. 세 번째 단계에서 손실 함수를 정의할 때, 각 작업의 특성에 맞는 손실을 선택합니다.

분류는 categorical_crossentropy, 회귀는 MSE(평균 제곱 오차), 색상 분류도 categorical_crossentropy를 사용합니다. 중요한 것은 loss_weights인데, 이것으로 각 작업의 중요도를 조절합니다.

예를 들어 나이 회귀의 가중치를 0.5로 낮춘 것은 이 작업이 상대적으로 덜 중요하거나 손실 스케일이 다르기 때문입니다. 마지막으로, 학습 시 모델은 세 손실의 가중 합을 최소화하려고 합니다.

역전파 때 그래디언트가 공유 레이어로 흘러들어갈 때, 세 작업의 그래디언트가 합쳐집니다. 이 과정에서 각 작업이 서로를 도와주는 효과가 발생합니다.

예를 들어 동물 종류를 분류하며 배운 시각 패턴이 나이 추정에도 유용하고, 색상 분류가 종류 분류의 정확도를 높이는 식입니다. 여러분이 이 방법을 사용하면 모델 하나로 여러 작업을 처리하여 메모리와 추론 시간을 절약할 수 있고, 데이터가 부족한 작업이 다른 작업의 데이터로부터 간접적으로 혜택을 받으며, 공유 특징이 더 강건하고 일반화된 표현을 학습합니다.

예를 들어 메인 작업의 데이터가 1000개, 보조 작업이 5000개라면 보조 작업이 메인 작업의 학습을 도와 마치 6000개의 데이터로 학습한 것 같은 효과를 냅니다.

실전 팁

💡 손실 가중치는 매우 중요합니다. 각 손실의 스케일을 비슷하게 맞추거나, 검증 세트에서 그리드 서치로 최적 가중치를 찾으세요.

💡 작업 간 관련성이 높을수록 멀티태스크 학습 효과가 큽니다. 완전히 무관한 작업을 억지로 결합하면 오히려 성능이 나빠질 수 있습니다.

💡 하드 파라미터 공유(모든 작업이 같은 백본 공유) 외에 소프트 파라미터 공유(각 작업이 별도 백본을 가지되 정규화로 유사하게 유지)도 시도해보세요.

💡 동적 가중치 조정 기법(예: GradNorm, Uncertainty Weighting)을 사용하면 학습 중 자동으로 손실 가중치를 최적화할 수 있습니다.

💡 모든 작업의 검증 성능을 모니터링하세요. 한 작업이 다른 작업을 방해하는 "negative transfer" 현상이 나타나면 작업 조합을 재고해야 합니다.


6. 제로샷 학습 - 본 적 없는 클래스 인식하기

시작하며

여러분이 개와 고양이를 인식하는 모델을 만들었는데, 갑자기 사용자가 "여우도 인식해줘"라고 요청하는 상황을 생각해보세요. 여우 데이터는 하나도 없는데, 처음부터 여우 사진을 수집하고 레이블링해서 모델을 재학습시켜야 할까요?

이런 문제는 실제 서비스 운영 중 새로운 클래스가 계속 추가될 때 자주 발생합니다. 전통적인 방법으로는 새 클래스마다 데이터를 모으고 모델을 재학습해야 하는데, 이는 시간과 비용이 많이 듭니다.

특히 희귀한 클래스는 데이터 수집 자체가 거의 불가능할 수 있습니다. 바로 이럴 때 필요한 것이 제로샷 학습(Zero-Shot Learning)입니다.

학습 데이터에 없던 클래스도 클래스의 의미적 설명만으로 인식할 수 있게 만드는, 마치 사람이 "긴 목을 가진 노란 동물"이라는 설명만으로 기린을 인식하는 것과 비슷한 능력을 모델에 부여하는 방법입니다.

개요

간단히 말해서, 제로샷 학습은 시각적 특징과 의미적 정보(텍스트 설명, 속성 등)를 연결하는 공간을 학습하여, 한 번도 본 적 없는 클래스도 그 설명을 통해 인식할 수 있게 만드는 기법입니다. 왜 이 방식이 필요한지 실무 관점에서 설명하면, 새로운 클래스가 자주 추가되는 동적 환경에서 재학습 없이 대응할 수 있고, 데이터 수집이 어려운 희귀 클래스도 다룰 수 있으며, 클래스 수가 수천 개로 늘어나도 확장 가능합니다.

예를 들어, 전자상거래에서 매일 새 상품이 추가되는데, 각 상품마다 이미지를 모아 학습시킬 수 없으므로 상품 설명만으로 인식하는 제로샷 학습이 유용합니다. 기존에는 미리 정의된 고정된 클래스만 인식할 수 있었다면, 이제는 새로운 클래스의 설명만 제공하면 즉시 인식할 수 있습니다.

제로샷 학습의 핵심은 첫째, 시각 공간과 의미 공간을 연결하는 임베딩을 학습한다는 점, 둘째, 속성 기반 또는 텍스트 기반 설명을 활용한다는 점, 셋째, 학습 시 본 클래스로 학습하여 보지 못한 클래스로 일반화한다는 점입니다. 이러한 특징들이 유연하고 확장 가능한 인식 시스템을 만듭니다.

코드 예제

# 제로샷 학습 - CLIP 스타일 간소화 예제
from tensorflow.keras.layers import Input, Dense, Lambda
from tensorflow.keras.models import Model
import tensorflow as tf

# 이미지 인코더 (시각 특징 추출)
image_input = Input(shape=(224, 224, 3))
image_backbone = ResNet50(weights='imagenet', include_top=False, pooling='avg')(image_input)
image_embedding = Dense(512, activation=None)(image_backbone)  # 임베딩 공간
image_embedding = Lambda(lambda x: tf.nn.l2_normalize(x, axis=1))(image_embedding)

# 텍스트 인코더 (의미 특징 추출)
text_input = Input(shape=(300,))  # 사전 학습된 단어 임베딩 (예: Word2Vec)
text_hidden = Dense(512, activation='relu')(text_input)
text_embedding = Dense(512, activation=None)(text_hidden)  # 임베딩 공간
text_embedding = Lambda(lambda x: tf.nn.l2_normalize(x, axis=1))(text_embedding)

# 멀티모달 모델 생성
image_encoder = Model(inputs=image_input, outputs=image_embedding)
text_encoder = Model(inputs=text_input, outputs=text_embedding)

# 학습: 이미지-텍스트 쌍의 유사도 최대화 (Contrastive Learning)
# 추론: 새 클래스의 텍스트 설명을 임베딩하고, 이미지와 유사도 계산
def zero_shot_predict(image, class_descriptions):
    img_emb = image_encoder.predict(image)
    text_embs = text_encoder.predict(class_descriptions)
    # 코사인 유사도 계산
    similarities = tf.matmul(img_emb, text_embs, transpose_b=True)
    return tf.argmax(similarities, axis=1)

설명

이것이 하는 일: 이미지와 텍스트를 각각 512차원의 공통 임베딩 공간으로 변환하여, 이미지가 어떤 텍스트 설명과 가장 유사한지를 계산함으로써 클래스를 예측합니다. 학습 때 보지 못한 클래스도 그 설명만 있으면 인식할 수 있습니다.

첫 번째로, 이미지 인코더는 ResNet50으로 시각 특징을 추출한 후 512차원 벡터로 투영합니다. 텍스트 인코더는 Word2Vec 같은 사전 학습된 단어 임베딩을 입력받아 역시 512차원 벡터로 투영합니다.

핵심은 두 인코더의 출력 차원이 같고, L2 정규화로 단위 벡터로 만든다는 점입니다. 이렇게 하면 두 벡터 간 코사인 유사도를 내적으로 간단히 계산할 수 있습니다.

그 다음으로, 학습 단계에서는 대조 학습(contrastive learning)을 사용합니다. "개 사진 - '개' 설명" 같은 매칭 쌍은 임베딩 공간에서 가깝게, "개 사진 - '고양이' 설명" 같은 비매칭 쌍은 멀게 배치되도록 학습합니다.

이를 위해 수천 개의 이미지-텍스트 쌍으로 학습하면, 모델은 시각적 개념과 언어적 설명 사이의 대응 관계를 배웁니다. CLIP 모델이 바로 이 방식으로 4억 개의 이미지-텍스트 쌍으로 학습되어 강력한 제로샷 능력을 얻었습니다.

세 번째 단계인 추론에서 제로샷의 마법이 일어납니다. 여러분이 학습 때 없던 "여우" 클래스를 인식하고 싶다면, "a photo of a fox"라는 텍스트 설명을 텍스트 인코더에 넣어 임베딩을 얻습니다.

그리고 입력 이미지의 임베딩과 "개", "고양이", "여우" 설명들의 임베딩을 모두 비교하여, 가장 유사한 것을 선택합니다. 모델은 학습 중 "개"와 "고양이"를 보며 "동물의 시각적 특징이 어떻게 언어로 표현되는지"를 배웠으므로, 새로운 동물인 여우도 그 설명과 시각적 특징을 연결할 수 있습니다.

마지막으로, 이 접근법의 강력함은 일반화 능력에 있습니다. 모델이 충분히 다양한 클래스로 학습되었다면, 완전히 새로운 클래스도 높은 정확도로 인식합니다.

예를 들어 100개 동물 클래스로 학습했다면, 101번째 동물도 추가 학습 없이 인식할 수 있습니다. 심지어 "striped horse-like animal"처럼 조합된 설명도 이해할 수 있어, 얼룩말을 본 적 없어도 인식할 수 있습니다.

여러분이 이 방법을 사용하면 클래스가 동적으로 변하는 환경에서 유연하게 대응할 수 있고, 롱테일 분포의 희귀 클래스도 다룰 수 있으며, 클래스 간 관계와 속성을 이해하는 더 똑똑한 모델을 만들 수 있습니다. 다만 성능은 일반적으로 완전 지도 학습보다는 낮으며, 좋은 클래스 설명을 만드는 것이 중요합니다.

실전 팁

💡 클래스 설명은 구체적이고 차별적이어야 합니다. "개"보다 "a photo of a dog with four legs and a tail"처럼 시각적 특징을 포함하면 성능이 향상됩니다.

💡 사전 학습된 언어 모델(BERT, GPT)을 텍스트 인코더로 사용하면 더 풍부한 의미 표현을 얻을 수 있습니다.

💡 제로샷과 퓨샷(few-shot)을 결합하면 좋습니다. 새 클래스의 예시가 몇 개만 있어도 제로샷보다 훨씬 높은 성능을 얻을 수 있습니다.

💡 학습 클래스와 테스트 클래스가 너무 다르면 성능이 떨어집니다. 학습 클래스를 최대한 다양하게 구성하여 일반화 능력을 높이세요.

💡 CLIP 같은 대규모 사전 학습 모델을 활용하면 처음부터 학습하는 것보다 훨씬 강력한 제로샷 성능을 얻을 수 있습니다.


7. 점진적 학습 - 잊지 않고 계속 배우기

시작하며

여러분이 개와 고양이를 인식하는 모델을 만들고, 이후에 새로 새와 물고기 클래스를 추가하려고 새 데이터로 재학습했더니, 원래 잘 인식하던 개와 고양이를 갑자기 못 알아보는 경험을 해본 적 있나요? 이것은 "파국적 망각(Catastrophic Forgetting)"이라는 딥러닝의 고질적 문제입니다.

이런 문제는 모델을 지속적으로 업데이트해야 하는 실제 서비스에서 매우 심각합니다. 새로운 데이터로 학습할 때마다 이전 지식을 잊어버리면, 결국 모든 이전 데이터를 계속 보관하고 처음부터 재학습해야 하는데, 이는 데이터 저장 비용과 학습 시간 측면에서 비현실적입니다.

바로 이럴 때 필요한 것이 점진적 학습(Incremental Learning) 또는 지속적 학습(Continual Learning)입니다. 사람처럼 새로운 것을 배우면서도 이전 지식을 유지하는 능력을 모델에 부여하는 방법입니다.

개요

간단히 말해서, 점진적 학습은 새로운 클래스나 데이터를 학습할 때 이전에 학습한 지식을 보존하면서 새 지식을 추가하는 기법으로, 파국적 망각 문제를 해결합니다. 왜 이 방식이 필요한지 실무 관점에서 설명하면, 첫째로 모든 과거 데이터를 보관할 필요가 없어 저장 비용이 절감되고, 둘째로 새 클래스 추가 시 처음부터 재학습할 필요가 없어 시간이 단축되며, 셋째로 개인정보 보호 규정으로 과거 데이터를 삭제해야 할 때도 모델을 유지할 수 있습니다.

예를 들어, 얼굴 인식 시스템에서 새 직원이 입사할 때마다 전체 데이터셋으로 재학습하지 않고 그 직원 데이터만으로 업데이트할 수 있습니다. 기존에는 새 클래스를 추가할 때 모든 이전 데이터와 함께 처음부터 재학습했다면, 이제는 새 데이터만으로도 이전 성능을 유지하면서 확장할 수 있습니다.

점진적 학습의 핵심은 첫째, 중요한 파라미터를 보호하는 정규화 기법(예: EWC), 둘째, 이전 데이터의 일부를 유지하는 리허설 기법, 셋째, 동적으로 네트워크를 확장하는 구조적 방법입니다. 이러한 기법들이 지식 보존과 새 학습의 균형을 맞춥니다.

코드 예제

# 점진적 학습 - Elastic Weight Consolidation (EWC) 기법
import tensorflow as tf
from tensorflow.keras.models import clone_model
import numpy as np

class EWC:
    def __init__(self, model, data, lambda_ewc=1000):
        self.model = model
        self.lambda_ewc = lambda_ewc
        self.fisher_matrix = self._compute_fisher(data)
        self.optimal_weights = [w.numpy() for w in model.trainable_weights]

    def _compute_fisher(self, data):
        # Fisher Information Matrix 계산 (파라미터 중요도 측정)
        fisher = [tf.zeros_like(w) for w in self.model.trainable_weights]
        for x, y in data:
            with tf.GradientTape() as tape:
                predictions = self.model(x, training=True)
                loss = tf.keras.losses.categorical_crossentropy(y, predictions)
            grads = tape.gradient(loss, self.model.trainable_weights)
            fisher = [f + tf.square(g) for f, g in zip(fisher, grads)]
        return [f / len(data) for f in fisher]

    def ewc_loss(self, y_true, y_pred):
        # 원래 손실 + EWC 페널티
        ce_loss = tf.keras.losses.categorical_crossentropy(y_true, y_pred)
        ewc_penalty = 0
        for i, w in enumerate(self.model.trainable_weights):
            ewc_penalty += tf.reduce_sum(self.fisher_matrix[i] * tf.square(w - self.optimal_weights[i]))
        return ce_loss + (self.lambda_ewc / 2) * ewc_penalty

# 사용 예시
# 작업 1 학습
model.compile(optimizer='adam', loss='categorical_crossentropy')
# model.fit(task1_data)

# EWC 객체 생성 (작업 1 지식 보호)
ewc = EWC(model, task1_validation_data, lambda_ewc=1000)

# 작업 2 학습 (작업 1 지식 유지하면서)
model.compile(optimizer='adam', loss=ewc.ewc_loss)
# model.fit(task2_data)

설명

이것이 하는 일: EWC(Elastic Weight Consolidation)는 이전 작업에서 중요했던 가중치들을 큰 페널티로 보호하여, 새 작업을 학습할 때 그 가중치들이 크게 변하지 않도록 제약합니다. 첫 번째로, Fisher Information Matrix를 계산하는 부분이 핵심입니다.

Fisher Matrix는 각 파라미터가 이전 작업의 성능에 얼마나 중요한지를 측정합니다. 구체적으로는 손실 함수의 그래디언트 제곱을 데이터에 대해 평균내어 계산합니다.

그래디언트가 크다는 것은 그 파라미터가 조금만 변해도 손실이 크게 변한다는 의미이므로, 중요한 파라미터라고 판단합니다. 반대로 그래디언트가 작으면 그 파라미터는 덜 중요하므로 새 작업을 위해 자유롭게 변경해도 됩니다.

그 다음으로, optimal_weights에 작업 1을 학습한 후의 최적 가중치를 저장합니다. 이것이 "보호해야 할 지식"입니다.

나중에 작업 2를 학습할 때, 가중치가 이 최적값에서 너무 멀어지지 않도록 페널티를 부과합니다. 마치 고무줄로 가중치를 최적값에 묶어두는 것과 같습니다.

중요한 파라미터일수록 더 강한 고무줄로 묶입니다(Fisher 값이 클수록). 세 번째 단계에서 ewc_loss 함수는 두 가지 항을 합칩니다.

첫째는 새 작업의 일반적인 손실(categorical_crossentropy)이고, 둘째는 EWC 페널티입니다. 페널티는 fisher_matrix[i] * (현재 가중치 - 최적 가중치)^2의 합으로, 중요한 파라미터가 최적값에서 멀어질수록 큰 페널티를 부과합니다.

lambda_ewc는 이 페널티의 강도를 조절하는 하이퍼파라미터로, 값이 클수록 이전 지식을 강하게 보호합니다. 마지막으로, 작업 2를 학습할 때 이 EWC 손실 함수를 사용하면, 모델은 새 데이터를 맞추면서도 동시에 중요한 가중치를 크게 바꾸지 않으려고 노력합니다.

결과적으로 새 작업을 어느 정도 학습하면서도 이전 작업의 성능 저하를 최소화합니다. 완벽하지는 않지만(약간의 성능 저하는 불가피), 아무 조치 없이 학습할 때보다 훨씬 적게 잊어버립니다.

여러분이 이 방법을 사용하면 모델을 지속적으로 업데이트하는 환경에서 과거 데이터를 보관하지 않아도 되고, 새 클래스나 데이터를 효율적으로 추가할 수 있으며, 데이터 프라이버시 규정을 준수하면서도 모델 성능을 유지할 수 있습니다. 실제로 Google Photos 같은 서비스에서 사용자가 늘어나도 모든 과거 사진을 재학습하지 않고 점진적으로 업데이트하는 데 이런 기법이 활용됩니다.

실전 팁

💡 lambda_ewc 값은 작업 간 유사도에 따라 조정하세요. 작업이 매우 다르면 큰 값으로, 비슷하면 작은 값으로 설정합니다.

💡 EWC 외에도 Memory Replay(이전 데이터 일부를 소량 저장)를 함께 사용하면 훨씬 좋은 성능을 얻을 수 있습니다. 전체의 5~10%만 유지해도 효과적입니다.

💡 여러 작업을 순차적으로 학습할 때는 각 작업마다 Fisher Matrix를 누적하거나 가중 평균하여 모든 과거 작업을 보호하세요.

💡 Learning without Forgetting(LwF) 같은 지식 증류 기반 방법도 시도해보세요. 이전 모델의 출력을 "소프트 타겟"으로 사용하여 지식을 전달합니다.

💡 작업 순서에 따라 성능이 달라질 수 있습니다. 어려운 작업을 먼저, 쉬운 작업을 나중에 배우면 일반적으로 더 좋은 결과를 얻습니다.


8. 메타 학습 - 학습하는 방법을 학습하기

시작하며

여러분이 새로운 프로그래밍 언어를 배울 때, 첫 언어(예: Python)를 배울 때보다 두 번째 언어(예: JavaScript)를 배울 때가 훨씬 빠른 경험을 해보셨을 겁니다. 왜냐하면 "프로그래밍 언어를 배우는 방법" 자체를 이미 알기 때문이죠.

모델도 이렇게 학습할 수 있을까요? 이런 능력은 데이터가 극도로 부족한 상황에서 특히 중요합니다.

예를 들어 의료 영상에서 희귀 질병은 사례가 5~10개밖에 없을 수 있는데, 전통적인 딥러닝으로는 이렇게 적은 데이터로 학습이 불가능합니다. 하지만 사람 전문가는 몇 개 사례만 봐도 새 질병을 진단할 수 있습니다.

바로 이럴 때 필요한 것이 메타 학습(Meta-Learning) 또는 "학습하는 법을 학습하기(Learning to Learn)"입니다. 많은 다양한 작업으로 학습하여 새로운 작업을 빠르게 배우는 능력 자체를 획득하는 방법입니다.

개요

간단히 말해서, 메타 학습은 여러 개의 학습 작업으로 훈련하여 모델이 새로운 작업을 매우 적은 데이터(퓨샷)로 빠르게 적응할 수 있도록 만드는 기법입니다. 왜 이 방식이 필요한지 실무 관점에서 설명하면, 첫째로 각 작업마다 1~10개의 예시만으로도 새 작업을 수행할 수 있고, 둘째로 빠른 적응이 가능하여 몇 번의 그래디언트 업데이트만으로 충분하며, 셋째로 다양한 작업에 대한 일반화된 표현을 학습합니다.

예를 들어, 제조 공정에서 새로운 결함 유형이 발견되었을 때, 그 결함 사진 5장만으로 즉시 검출 모델을 만들 수 있습니다. 기존에는 각 작업마다 수천 개의 데이터를 모아 처음부터 학습했다면, 이제는 몇 개의 예시만으로 몇 분 안에 새 작업을 학습할 수 있습니다.

메타 학습의 핵심은 첫째, 에피소드 기반 학습(각 에피소드가 하나의 작은 학습 문제), 둘째, 좋은 초기 파라미터 찾기(MAML) 또는 메트릭 학습(Prototypical Networks), 셋째, 서포트 셋과 쿼리 셋으로 나누어 평가합니다. 이러한 특징들이 빠른 적응력을 만듭니다.

코드 예제

# 메타 학습 - Prototypical Networks 간소화 버전
import tensorflow as tf
from tensorflow.keras.layers import Input, Conv2D, Flatten, Dense
from tensorflow.keras.models import Model

# 임베딩 네트워크 (각 이미지를 벡터로 변환)
def build_embedding_network():
    inputs = Input(shape=(84, 84, 3))
    x = Conv2D(64, 3, activation='relu')(inputs)
    x = Conv2D(64, 3, activation='relu')(x)
    x = Conv2D(64, 3, activation='relu')(x)
    x = Flatten()(x)
    embeddings = Dense(64, activation=None)(x)
    return Model(inputs, embeddings)

embedding_net = build_embedding_network()

# 프로토타입 계산 및 분류
def prototypical_loss(support_embeddings, support_labels, query_embeddings, query_labels, n_way):
    # 각 클래스의 프로토타입 계산 (클래스별 평균 임베딩)
    prototypes = []
    for c in range(n_way):
        class_embeddings = tf.boolean_mask(support_embeddings, support_labels == c)
        prototype = tf.reduce_mean(class_embeddings, axis=0)
        prototypes.append(prototype)
    prototypes = tf.stack(prototypes)

    # 쿼리 임베딩과 각 프로토타입 간 유클리디안 거리 계산
    distances = tf.norm(query_embeddings[:, None] - prototypes[None, :], axis=2)
    log_probs = -distances  # 거리가 가까울수록 높은 점수

    loss = tf.keras.losses.sparse_categorical_crossentropy(query_labels, log_probs, from_logits=True)
    return tf.reduce_mean(loss)

# 5-way 1-shot 학습 예시 (5개 클래스, 각 1개 예시)
# support_set: (5, 84, 84, 3) - 각 클래스당 1개 이미지
# query_set: (15, 84, 84, 3) - 테스트할 이미지들
# support_embeddings = embedding_net.predict(support_set)
# query_embeddings = embedding_net.predict(query_set)
# loss = prototypical_loss(support_embeddings, support_labels, query_embeddings, query_labels, n_way=5)

설명

이것이 하는 일: 임베딩 네트워크를 학습시켜 같은 클래스의 이미지는 가깝게, 다른 클래스는 멀게 배치되는 공간을 만듭니다. 새 작업에서는 각 클래스의 몇 개 예시로 프로토타입을 계산하고, 쿼리 이미지가 어느 프로토타입과 가장 가까운지 판단하여 분류합니다.

첫 번째로, 임베딩 네트워크는 합성곱 레이어들로 이미지를 64차원 벡터로 변환합니다. 이 네트워크의 목표는 의미적으로 유사한 이미지들이 임베딩 공간에서 가까이 위치하도록 만드는 것입니다.

학습은 수많은 "에피소드"로 이루어지는데, 각 에피소드는 랜덤하게 선택된 N개 클래스와 각 클래스의 K개 예시(N-way K-shot)로 구성됩니다. 예를 들어 5-way 1-shot이면 5개 클래스를 선택하고 각각 1개씩만 제공합니다.

그 다음으로, 프로토타입 계산 과정을 살펴봅니다. 서포트 셋의 각 클래스별 임베딩들을 평균내어 프로토타입을 만듭니다.

만약 고양이 클래스에 3개 이미지가 있다면, 3개의 임베딩을 평균하여 "고양이 프로토타입"을 얻습니다. 이 프로토타입은 해당 클래스를 대표하는 중심점입니다.

1-shot이면 예시가 1개뿐이므로 그 임베딩 자체가 프로토타입이 됩니다. 세 번째 단계에서 쿼리 이미지를 분류할 때는 매우 간단합니다.

쿼리 이미지의 임베딩과 모든 프로토타입 사이의 유클리디안 거리를 계산하고, 가장 가까운 프로토타입의 클래스로 분류합니다. 거리 기반 분류는 매개변수가 필요 없어 새 클래스를 즉시 추가할 수 있다는 큰 장점이 있습니다.

손실 함수는 정답 프로토타입과는 가깝게, 다른 프로토타입과는 멀게 만들도록 임베딩 네트워크를 학습시킵니다. 마지막으로, 메타 학습의 핵심은 다양한 에피소드로 학습한다는 점입니다.

예를 들어 동물 100종으로 학습한다면, 매번 다른 5종을 랜덤하게 선택하여 수천 번의 에피소드를 수행합니다. 이 과정에서 모델은 "새로운 클래스들을 빠르게 구분하는 방법"을 배웁니다.

학습 후 완전히 새로운 5종의 동물을 제시해도, 각 종의 이미지 1~5장만으로 높은 정확도로 분류할 수 있습니다. 여러분이 이 방법을 사용하면 데이터 수집이 어려운 희귀 케이스도 다룰 수 있고, 새로운 클래스를 실시간으로 추가할 수 있으며, 사람처럼 적은 예시로 빠르게 학습하는 AI 시스템을 만들 수 있습니다.

예를 들어 개인화된 추천 시스템에서 새 사용자의 선호도를 몇 번의 클릭만으로 파악하는 데 활용됩니다.

실전 팁

💡 메타 학습은 메타 훈련 데이터의 다양성이 핵심입니다. 클래스 수가 많고 다양할수록 일반화 능력이 향상됩니다.

💡 MAML(Model-Agnostic Meta-Learning)도 시도해보세요. 몇 번의 그래디언트 업데이트로 빠르게 적응할 수 있는 초기 파라미터를 찾는 방법입니다.

💡 메타 테스트 시 K-shot의 K를 늘리면(1→5→10) 성능이 향상됩니다. 실무에서는 5-shot 정도가 좋은 균형점입니다.

💡 임베딩 차원은 작업 복잡도에 맞게 조정하세요. 간단한 작업은 32차원, 복잡한 작업은 128~512차원이 적합합니다.

💡 메타 학습과 전이학습을 결합하세요. ImageNet 사전 학습 모델로 시작하여 메타 학습을 적용하면 더 적은 메타 훈련 데이터로도 좋은 결과를 얻습니다.


#Python#TransferLearning#DeepLearning#PretrainedModels#FineTuning#Data Science

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.