
논문에서 설명하는 Feature Extractor(사전 학습된 모델)
→ Feature Adapter(단일 FC 레이어)
→ Gaussian Noise 주입 과정 까지만 간단하게 구현하면 다음과 같다.
📂 데이터셋 확인됨: mvtec_ad
📸 무작위 선택된 이미지: mvtec_ad/screw/test/thread_side/008.png
📊 SimpleNet Random Inference Results
✅ 정상 특징(Adapted) 크기: torch.Size([1, 2048, 7, 7])
✅ 이상 특징(Anomalous) 크기: torch.Size([1, 2048, 7, 7])
📏 특징 간 L2 거리 (Distance): 4.756063
💡 결과: 'test' 클래스
import torch
import torch.nn as nn
import torchvision.models as models
import torchvision.transforms as transforms
from PIL import Image
import numpy as np
import os
import glob
import random
# --- [수정된 SimpleNet 핵심 모듈] ---
class SimpleNetComponent(nn.Module):
def __init__(self, feature_dim):
super(SimpleNetComponent, self).__init__()
# 1. Feature Extractor
resnet = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V1)
self.feature_extractor = nn.Sequential(*(list(resnet.children())[:-2]))
# 2. Feature Adapter: 논문 설계대로 Bias 없는 Linear 레이어
self.feature_adapter = nn.Linear(feature_dim, feature_dim, bias=False)
def forward(self, x):
with torch.no_grad():
# features shape: [B, C, H, W] -> 예: [1, 2048, 7, 7]
features = self.feature_extractor(x)
# [핵심 수정 부분]
# nn.Linear는 마지막 차원에 적용되므로, [B, C, H, W] -> [B, H, W, C]로 변경
# 이렇게 해야 2048(채널) 차원에 대해 연산이 수행됩니다.
features_permuted = features.permute(0, 2, 3, 1)
# 차원 변환 후 Linear 적용: [B, H, W, 2048]
adapted_features = self.feature_adapter(features_permuted)
# 다시 원래의 [B, C, H, W] 순서로 복원: [B, 2048, H, W]
adapted_features = adapted_features.permute(0, 3, 1, 2)
return adapted_features
class AnomalyFeatureGenerator:
def __init__(self, sigma=0.015):
self.sigma = sigma
def generate(self, adapted_features):
# 특징 공간에서 노이즈 주입
noise = torch.randn_like(adapted_features) * self.sigma
anomalous_features = adapted_features + noise
return anomalous_features
# --- [데이터 로드 및 실행 로직] ---
def find_random_test_image(base_dir):
search_pattern = os.path.join(base_dir, "*", "test", "**", "*.png")
image_files = glob.glob(search_pattern, recursive=True)
return random.choice(image_files) if image_files else None
def run_inference_pipeline(image_path, model_components, generator):
transform = transforms.Compose([
transforms.Resize((224, 224)),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])
img = Image.open(image_path).convert('RGB')
img_tensor = transform(img).unsqueeze(0)
with torch.no_grad():
adapted = model_components(img_tensor)
anomalous = generator.generate(adapted)
return adapted, anomalous
if __name__ == "__main__":
DATASET_ROOT = "mvtec_ad"
if not os.path.exists(DATASET_ROOT):
print(f"❌ 오류: '{DATASET_ROOT}' 폴더를 찾을 수 없습니다.")
else:
print(f"📂 데이터셋 확인됨: {DATASET_ROOT}")
test_image = find_random_test_image(DATASET_ROOT)
if test_image is None:
print("❌ 오류: 이미지를 찾을 수 없습니다.")
else:
print(f"📸 무작위 선택된 이미지: {test_image}")
feature_dim = 2048
model = SimpleNetComponent(feature_dim)
generator = AnomalyFeatureGenerator(sigma=0.015)
adapted, anomalous = run_inference_pipeline(test_image, model, generator)
print("\n" + "="*40)
print("📊 SimpleNet Random Inference Results")
print("="*40)
print(f"✅ 정상 특징(Adapted) 크기: {adapted.shape}")
print(f"✅ 이상 특징(Anomalous) 크기: {anomalous.shape}")
dist = torch.dist(adapted, anomalous).item()
print(f"📏 특징 간 L2 거리 (Distance): {dist:.6f}")
print("-" * 40)
print(f"💡 결과: '{os.path.basename(os.path.dirname(os.path.dirname(test_image)))}' 클래스")
print("="*40)
제공된 논문에는 모델의 전체 아키텍처와 가중치(weights)가 포함되어 있지 않으므로, 논문에 서술된 구조적 정의(Architecture Description)를 바탕으로 실제 동작 원리를 재현한 파이썬 코드를 작성해 드립니다.
이 코드는 논문에서 설명하는 Feature Extractor(사전 학습된 모델) Feature Adapter(단일 FC 레이어) Gaussian Noise 주입 과정을 충실히 따릅니다.
import torch
import torch.nn as nn
import torchvision.models as models
import torchvision.transforms as transforms
from PIL import Image
import numpy as np
class SimpleNetComponent(nn.Module):
def __init__(self, feature_dim):
super(SimpleNetComponent, self).__init__()
# 1. Feature Extractor: 사전 학습된 모델 (논문은 WideResNet50 사용)
# 여기서는 구현을 위해 표준 ResNet50을 사용합니다.
resnet = models.resnet50(pretrained=True)
# 중간 계층의 특징을 뽑기 위해 마지막 분류 레이어 제거
self.feature_extractor = nn.Sequential(*(list(resnet.children())[:-2]))
# 2. Feature Adapter: 단일 FC 레이어 (논문: "single FC layer without bias")
# 입력과 출력 채널을 동일하게 설정
self.feature_adapter = nn.Linear(feature_dim, feature_dim, bias=False)
def forward(self, x):
# 특징 추출
with torch.no_grad():
features = self.feature_extractor(x)
# 특징 공간을 타겟 도메인으로 변환 (Feature Adapter)
adapted_features = self.feature_adapter(features)
return adapted_features
class AnomalyFeatureGenerator:
"""
3. Anomaly Feature Generator: 정상 특징에 가우시안 노이즈를 더해
이상 특징(Anomalous features)을 생성함.
"""
def __init__(self, sigma=0.015):
self.sigma = sigma # 논문에서 최적이라고 언급된 값 (Citation 3)
def generate(self, adapted_features):
# 특징 공간에서 가우시안 노이즈 생성 (정상 특징과 동일한 shape)
noise = torch.randn_like(adapted_features) * self.sigma
# 정상 특징 + 노이즈 = 이상 특징
anomalous_features = adapted_features + noise
return anomalous_features
def process_image(image_path, model_components, generator, sigma=0.015):
# 이미지 전처리 (표준적인 ResNet 입력 크기 224x224)
transform = transforms.Compose([
transforms.Resize((224, 224)),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.459, 0.406], std=[0.229, 0.224, 0.224]),
])
# 이미지 로드 및 배치 차원 추가
img = Image.open(image_path).convert('RGB')
img_tensor = transform(img).unsqueeze(0)
# 단계 1 & 2: 특징 추출 및 어댑터 적용
adapted_features = model_components(img_tensor)
# 단계 3: 특징 공간에서 가우시안 노이즈 주입 (이상치 생성)
anomalous_features = generator.generate(adapted_features)
return adapted_features, anomalous_features
# --- 실행 예시 ---
if __name__ == "__main kind: 'image_processing'":
# 모델 초기화 (ResNet50의 특징 차원인 2048 사용)
feature_dim = 2048
components = SimpleNetComponent(feature_dim)
generator = AnomalyFeatureGenerator(sigma=0.015)
# 테스트용 가상 이미지 경로 (실제 이미지 경로로 변경 필요)
# 예: 'path/to/mvtec/bottle/image.jpg'
try:
# 임의의 텐서로 테스트 (이미지 파일이 없는 경우를 대비)
dummy_img = torch.randn(1, 3, 224, 224)
# 실제 구현 로직 수행
adapted, anomalous = process_image_dummy(dummy_img, components, generator)
print(f"결과 확인:")
print(f"- 정상 어댑션 특징 텐서 크기: {adapted.shape}")
print(f"- 생성된 이상 특징 텐서 크기: {anomalous.shape}")
print(f"- 두 벡터 사이의 평균 차이(L2): {torch.dist(adapted, anomalous).item():.4f}")
except Exception as e:
print(f"오류 발생: {e}")
def process_image_dummy(img_tensor, model_components, generator):
# 파일 로드 대신 텐서를 직접 받는 함수
with torch.no_grad():
adapted = model_components(img_tensor)
anomalous = generator.generate(adapted)
return adapted, anomalous
self.feature_extractor): 논문의 (1)번 구성 요소입니다. resnet50에서 마지막 분류 레이어를 제거하여 이미지의 국소적 특징(local features)을 추출하도록 구현했습니다.self.feature_adapter): 논문의 (2)번 구성 요소입니다. 논문에서 명시한 대로 bias=False를 설정한 단일 선형 레이어(nn.Linear)로 구현하여 타겟 도메인으로 특징을 변환합니다.AnomalyFeatureGenerator 클래스): 논문의 (3)번 구성 요소입니다. 이미지 자체가 아닌 특징 공간(Feature Space)에서 torch.randn_like를 이용해 가우시안 노이즈를 생성하고, 이를 어댑션된 정상 특징에 더해 "합성된 이상 특징(Synthesized anomalous features)"을 만듭니다.generate 단계는 모델 내부가 아닌 별도의 생성기 객체로 분리하여 학습 시에만 사용됨을 시사하도록 설계했습니다.