Developer's Development

3.3.23 [LLM] 자연어-이미지 멀티모달: 이미지 딥러닝 응용(스타일 전이 학습, GAN) 본문

LLM

3.3.23 [LLM] 자연어-이미지 멀티모달: 이미지 딥러닝 응용(스타일 전이 학습, GAN)

mylee 2025. 9. 28. 20:50
Style Transfer Learning (스타일 전이 학습)

 

Style Transfer는 하나의 이미지로부터 콘텐츠(content)를, 또 다른 이미지로부터 스타일(style)을 추출하여 두 요소를 결합한 새로운 이미지를 생성하는 기법이다.

예를 들어, 직접 찍은 풍경 사진을 반 고흐의 화풍으로 변환하거나, 유명한 그림의 스타일을 우리가 찍은 인물 사진에 적용하는 것이 가능하다. 이 기술은 컴퓨터 비전뿐 아니라 예술 창작, 디자인, 애니메이션 분야에서도 널리 활용되고 있다.

 

- 콘텐츠(Content): 이미지의 윤곽, 구조, 배치 등 실제 장면을 구성하는 정보이다. 예를 들어 어떤 물체가 어디에 배치되어 있는지에 대한 정보를 의미한다.

- 스타일(Style): 색상 분포, 질감, 붓 터치 등의 시각적 요소로서, 예술적인 표현에 해당한다.

 

👉🏻 Style Transfer는 일반적으로 사전 학습된 CNN(주로 VGG-19)의 중간 계층에서 추출된 Feature Map을 사용하여 콘텐츠와 스타일을 분리한다.

 

  • 이미지 스타일 변환 응용

- 예술 작품처럼 보이는 필터를 사진에 적용하여 사진을 회화적으로 바꾸는 데 활용한다.

  ex) 셀카를 고흐, 모네, 클림트 등 유명 화가의 화풍으로 변환

- 동영상에도 적용하여 실시간 스타일 전이 네트워크 적용 사례가 된다.

- 패션, 인테리어, 게임 디자인 등에서 창의적인 시각 효과를 제공한다.

 

  • 핵심 원리

이미지 스타일 전이에서는 콘텐츠와 스타일을 반영한 손실 함수를 설계하여, 새로운 이미지를 반복적으로 업데이트하여 학습한다.

최종적으로 원하는 스타일이 잘 반영되면서도 원래 콘텐츠 구조는 유지된 이미지를 생성하는 것이 목표이다.

 

1. Content Loss (내용 손실)

  - 콘텐츠 이미지와 생성 이미지의 중간 feature map 간의 차이를 계산한다.

  - 일반적으로 VGG-19의 conv4_2 층의 출력을 기준으로 한다.

 

2. Style Loss (스타일 손실)

  - 스타일 이미지와 생성 이미지의 feature map을  Gram Matrix로 변환하여 두 스타일 간의 차이를 계산한다.

  - Gram Matrix는 feature map의 상호 상관관계를 측정하는 행렬이다.

 

3. Total Variation Loss (총 변동 손실)

  - 생성 이미지의 픽셀 간 차이를 줄여 더 부드럽고 자연스러운 이미지를 만든다.

 

4. 최종 손실 함수

 

  • 알고리즘

1. Neural Style Transfer (Gatys et al.2016)

- 입력 이미지를 초기값으로 설정하고, 이 이미지를 업데이트하면서 손실 함수를 최소화한다.

- 이미지 자체가 학습 대상(변수)이다.

- 수천 번의 반복 학습이 필요하며, 고성능 GPU가 요구된다.

- 하지만 다양한 스타일을 적용하는 데 유연하며, 품질이 매우 우수한 이미지를 생성할 수 있다.

 

2. Fast Style Transfer

- 스타일마다 하나의 feed-forward network를 학습시켜서 빠른 스타일 변환이 가능하게 한다.

- 훈련 시간이 필요하지만, 한 번 훈련된 후에는 실시간 변환이 가능하다.

- Perceptual Loss를 사용하여 단순한 픽셀 비교가 아닌 고차원 feature 비교 기반의 손실 계산을 수행한다.

 

 

실습 (스타일 전이 학습, colab)

 

  • 1. 이미지 로드 및 전처리
import requests
from PIL import Image
from io import BytesIO
import torchvision.transforms as transforms

# 함수 정의: 이미지 로드 + Transform
def load_image(img_url, max_size=400):
    response = requests.get(img_url)
    image =  Image.open(BytesIO(response.content)).convert("RGB")
    size = max(image.size) if max(image.size) < max_size else max_size
    transform = transforms.Compose([
        transforms.Resize((size, size)),
        transforms.ToTensor(),
        transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
    ])
    return transform(image).unsqueeze(0)
    
# 함수 정의: Tensor -> 이미지 변환
def img_convert(tensor):
    image = tensor.to("cpu").clone().detach().squeeze()
    image = image.numpy().transpose(1, 2, 0)
    image = image * [0.229, 0.224, 0.225]
    image = image + [0.485, 0.456, 0.406]
    return image.clip(0, 1)
    
# 이미지 url
content_url = "https://pytorch.org/tutorials/_static/img/neural-style/dancing.jpg"
style_url = "https://pytorch.org/tutorials/_static/img/neural-style/picasso.jpg"
import torch

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# 이미지 로드
content = load_image(content_url).to(device)
style = load_image(style_url).to(device)

 

  • 2. 모델 로드
import torchvision.models as models

# 모델 로드 (특징 추출 레이어)
vgg = models.vgg19(pretrained=True).features.to(device).eval()

for param in vgg.parameters():
    param.requires_grad_(False)

 

  • 3. 학습 준비
# 함수 정의: 특정 레이어를 통과한 feature를 반환
def get_features(image, model):
    layers = {
        "0": "conv1_1",
        "5": "conv2_1",
        "10": "conv3_1",
        "19": "conv4_1",
        "21": "conv4_2",
        "28": "conv5_1",
    }
    features = {}

    x = image
    for name, layer in model._modules.items():
        x = layer(x)
        if name in layers:
            features[layers[name]] = x
    
    return features
    
# 함수 정의: 스타일 표현에 대한 행렬 반환
def gram_matrix(tensor):
    b, c, h, w = tensor.size()
    tensor = tensor.view(c, h * w)
    return torch.mm(tensor, tensor.t())
    
# feature 추출
content_features = get_features(content, vgg)
style_features = get_features(style, vgg)
style_grams = {
    feature: gram_matrix(style_features[feature]) for feature in style_features
}

# 학습 준비
target = content.clone().requires_grad_(True)
optim = torch.optim.Adam([target], lr=0.003)
style_weight = 1e6
content_weight = 1

 

  • 4. 스타일 전이 학습
for i in range(200):
	# 결과 정의
    target_features = get_features(target, vgg)

	# 콘텐츠 손실 계산
    content_loss = torch.mean((target_features['conv4_2'] - content_features['conv4_2']) ** 2)
    
    # 스타일 손실 계산
    style_loss = 0
    for i in style_grams:
        target_gram = gram_matrix(target_features[i])
        style_gram = style_grams[i]
        layer_loss = torch.mean((target_gram - style_gram) ** 2)
        style_loss += layer_loss / (target_features[i].shape[1] ** 2)
    
    total_loss = content_weight * content_loss + style_weight * style_loss	# 총손실 계산
    optim.zero_grad()
    total_loss.backward()
    optim.step()

 

  • 5. 결과 시각화
import matplotlib.pyplot as plt

plt.figure(figsize=(10, 5))
plt.subplot(1, 2, 1)
plt.imshow(img_convert(content))
plt.title('original')
plt.axis('off')

plt.subplot(1, 2, 2)
plt.imshow(img_convert(target))
plt.title('transfer style')
plt.axis('off')
plt.show()

색감과 질감의 변화를 볼 수 있음

 

 

실습 (Style mixing, colab)

 

두 개 이미지의 스타일 -> 하나의 콘텐츠 이미지에 적용

 

  • 1. 이미지 로드 및 전처리 함수 정의
import requests
from PIL import Image
from io import BytesIO
import torchvision.transforms as transforms

# 함수 정의: 이미지 로드 + Transform
def load_image(img_url, max_size=400, shape=None):
    response = requests.get(img_url)
    image =  Image.open(BytesIO(response.content)).convert("RGB")
    if shape:
        image = image.resize(shape, Image.LANCZOS)
    else:
        image.thumbnail((max_size, max_size))

    transform = transforms.Compose([
        transforms.ToTensor(),
        transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
    ])
    return transform(image).unsqueeze(0)
    
# 함수 정의: Tensor -> 이미지 변환
def img_convert(tensor):
    image = tensor.to("cpu").clone().detach().squeeze()
    image = image.numpy().transpose(1, 2, 0)
    image = image * [0.229, 0.224, 0.225] + [0.485, 0.456, 0.406]
    return image.clip(0, 1)

 

  • 2. 특징 추출 함수 정의
import torch

# 함수 정의: 특정 레이어를 통과한 feature를 반환
def get_features(image, model):
    layers = {
        "0": "conv1_1",
        "5": "conv2_1",
        "10": "conv3_1",
        "19": "conv4_1",
        "21": "conv4_2",
        "28": "conv5_1",
    }
    features = {}

    x = image
    for name, layer in model._modules.items():
        x = layer(x)
        if name in layers:
            features[layers[name]] = x

    return features

# 함수 정의: 스타일 표현에 대한 행렬 반환
def gram_matrix(tensor):
    b, c, h, w = tensor.size()
    tensor = tensor.view(c, h * w)
    return torch.mm(tensor, tensor.t())

 

  • 3. Style Mixing 함수 정의
def style_transfer_mix(content, style1, style2, model, mix_ratio=0.5, steps=200, content_weight=1, style_weight=1e6):

  # 특징 추출
  with torch.no_grad():
    content_features = get_features(content, model)
    style1_features = get_features(style1, model)
    style2_features = get_features(style2, model)

    style_grams_mix = {}
    for i in style1_features:
      gram1 = gram_matrix(style1_features[i])
      gram2 = gram_matrix(style2_features[i])
      style_grams_mix[i] = mix_ratio * gram1 + (1 - mix_ratio) * gram2

  target = content.clone().detach().requires_grad_(True)
  optim = torch.optim.Adam([target], lr=0.003)

  for i in range(steps):
    target_features = get_features(target, model)
    content_loss = torch.mean((target_features["conv4_2"] - content_features["conv4_2"].detach()) ** 2)

    style_loss = 0
    for j in style_grams_mix:
      target_gram = gram_matrix(target_features[j])
      style_gram = style_grams_mix[j]
      layer_loss = torch.mean((target_gram - style_gram) ** 2)
      style_loss += layer_loss / (target_features[j].shape[1] ** 2)

    total_loss = content_weight * content_loss + style_weight * style_loss
    optim.zero_grad()
    total_loss.backward()
    optim.step()

  return target

 

  • 함수 호출 -> 작업 진행

1. 이미지 url 설정

2. 모델 로드

3. 이미지 로드

4. style_transfer_mix 함수 호출

5. 결과 시각화

# 이미지 url
content_url = "https://www.fitpetmall.com/wp-content/uploads/2023/10/GettyImages-492548888-1.png"
style1_url = "https://pytorch.org/tutorials/_static/img/neural-style/dancing.jpg"
style2_url = "https://pytorch.org/tutorials/_static/img/neural-style/picasso.jpg"
import torchvision.models as models

# 모델 로드
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
vgg = models.vgg19(pretrained=True).features.to(device).eval()

# 이미지 로드
content = load_image(content_url).to(device)
style1 = load_image(style1_url, shape=content.shape[-2:]).to(device)
style2 = load_image(style2_url, shape=content.shape[-2:]).to(device)

# style_transfer_mix 수행
result = style_transfer_mix(content, style1, style2, vgg)
import matplotlib.pyplot as plt

plt.figure(figsize=(15, 5))

plt.subplot(1, 4, 1)
plt.imshow(img_convert(content))
plt.title("Original Image")
plt.axis("off")

plt.subplot(1, 4, 2)
plt.imshow(img_convert(style1))
plt.title("Style 1")
plt.axis("off")

plt.subplot(1, 4, 3)
plt.imshow(img_convert(style2))
plt.title("Style 2")
plt.axis("off")

plt.subplot(1, 4, 4)
plt.imshow(img_convert(result))
plt.title("Mixed Style Image")
plt.axis("off")

plt.tight_layout()
plt.show()

 

 

GAN (Generative Adversarial Networks)

 

GAN(적대적 생성 신경망)은 2014년 Ian Goodfellow가 처음 제안한 딥러닝 생성 모델로, Generator(생성자)와 Discriminator(판별자)라는 두 개의 모델이 서로 경쟁하면서 학습하는 방식이다.

생성자는 가짜 이미지를 만들고, 판별자는 그것이 진짜인지 가짜인지 구분하려 한다.

 

- Generator(G): 무작위 노이즈(z)를 받아서 진짜 같은 이미지를 생성하는 네트워크

- Discriminator(D): 진짜 이미지와 Generator가 만든 이미지를 구분하여 진짜인지 가짜인지 판별하는 네트워크

 

👉🏻 이 두 네트워크는 경쟁 구조로 학습되며, 이는 게임 이론의 한 형태인 제로섬 게임(Zero-sum Game)으로 볼 수 있다.

제로섬 게임은 한 쪽의 승리가 다른 쪽의 손실로 이어지는 구조를 뜻한다.

즉, G가 더 잘 속이면 D는 손해를 보고, D가 잘 구분하면 G는 손해를 본다.

 

실습 (VAE, colab)

 

1. 데이터 로드 및 설정

import torch

device = "cuda" if torch.cuda.is_available() else "cpu"
import torchvision.datasets as datasets
from torchvision import transforms
from torch.utils.data import DataLoader

dataset = datasets.MNIST(root='dataset/', train=True, transform=transforms.ToTensor(), download=True)
train_loader = DataLoader(dataset=dataset, batch_size=32, shuffle=True)

 

2. 모델 생성

import torch.nn as nn

class VAE(nn.Module):
    def __init__(self, input_dim, hidden_dim=200, z_dim=20):
        super(VAE, self).__init__()

        # 레이어 구성
        self.img2hid = nn.Linear(input_dim, hidden_dim)
        self.hid2mu = nn.Linear(hidden_dim, z_dim)
        self.hid2sigma = nn.Linear(hidden_dim, z_dim)

        self.z2hid = nn.Linear(z_dim, hidden_dim)
        self.hid2img = nn.Linear(hidden_dim, input_dim)

        self.relu = nn.ReLU()

    def encoder(self, x):
        x = self.img2hid(x)
        x = self.relu(x)
        mu, sigma = self.hid2mu(x), self.hid2sigma(x)
        return mu, sigma

    def decoder(self, z):
        z = self.z2hid(z)
        z = self.relu(z)
        x = self.hid2img(z)
        x = torch.sigmoid(x)
        return x

    def forward(self, x):
        mu, sigma = self.encoder(x)
        epsilon = torch.randn_like(sigma)
        z_reparam = mu + sigma + epsilon
        x_reconst = self.decoder(z_reparam)
        return x_reconst, mu, sigma
model = VAE(784, 200, 20).to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=3e-4)
criterion = nn.BCELoss(reduction='sum')

 

3. 모델 학습

from tqdm import tqdm

for epoch in range(10):
    for i, (x, _) in tqdm(enumerate(train_loader)):
        x = x.to(device).view(x.shape[0], 784)

        x_reconst, mu, sigma = model(x)

        reconst_loss = criterion(x_reconst, x)
        kl_div = -torch.sum(1 + torch.log(sigma.pow(2)) - mu.pow(2) - sigma.pow(2))

        loss = reconst_loss + kl_div
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

 

4. 추론 (이미지 생성)

from torchvision.utils import save_image

model = model.to("cpu")

def inference(digit, num_samples=3):
    images = []
    idx = 0

    for x, y in dataset:
        if y == digit:
            images.append(x)
            idx += 1
            if idx == num_samples:
                break
    
    encoding_digit = []
    for img in images:
        with torch.no_grad():
            mu, sigma = model.encoder(img.view(1, 784))
        encoding_digit.append((mu, sigma))

    mu, sigma = encoding_digit[0]

    for example in range(num_samples):
        epsilon = torch.randn_like(sigma)
        z = mu + sigma * epsilon
        out = model.decoder(z)
        out = out.view(-1, 1, 28, 28)
        save_image(out, f"digit{digit}_sample_{example}.png")
inference(7)

 

 

실습 (GAN with CelebA, colab)

 

  • Kaggle 회원가입 및 API 토큰 생성

https://www.kaggle.com/

 

Kaggle: Your Machine Learning and Data Science Community

Kaggle is the world’s largest data science community with powerful tools and resources to help you achieve your data science goals.

www.kaggle.com

Profile > Settings > API > Create New Token (json 파일 다운로드됨)

 

  • 1. Kaggle 데이터셋 다운로드
from google.colab import files
files.upload()		# kaggle.json 파일 선택해서 업로드하기
import os
os.makedirs('/root/.kaggle', exist_ok=True)

!mv kaggle.json /root/.kaggle/kaggle.json
!chmod 600 /root/.kaggle/kaggle.json
!kaggle datasets download -d jessicali9530/celeba-dataset
!unzip -q celeba-dataset.zip
"""
# 셀럽들의 얼굴을 가지고 있는 데이터셋
Dataset URL: https://www.kaggle.com/datasets/jessicali9530/celeba-dataset
License(s): other
Downloading celeba-dataset.zip to /content
100% 1.33G/1.33G [00:16<00:00, 247MB/s]
100% 1.33G/1.33G [00:16<00:00, 86.9MB/s]
"""

 

  • 2. 이미지 전처리
import glob

images = glob.glob('./img_align_celeba/img_align_celeba/*.jpg')
print(len(images))		# 202599
import matplotlib.pyplot as plt
from PIL import Image

image = Image.open('./img_align_celeba/img_align_celeba/005031.jpg')
plt.imshow(image)

from torchvision import transforms
from torchvision.datasets import ImageFolder
from torch.utils.data.dataloader import DataLoader

transform = transforms.Compose([
    transforms.Resize((64, 64)),
    transforms.CenterCrop(64),
    transforms.ToTensor(),
    transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])

dataset = ImageFolder(
    root='./img_align_celeba/',
    transform=transform
)

data_loader = DataLoader(
    dataset,
    batch_size=128,
    shuffle=True
)
import numpy as np

iter_data = iter(data_loader)
img, label = next(iter(iter_data))

img = img[3].numpy()
print(label[3])
plt.imshow(np.transpose(img, (1, 2, 0)))

  • 3. GAN 모델 생성
import torch.nn as nn

class Generator(nn.Module):
    def __init__(self):
        super(Generator, self).__init__()

        self.gen = nn.Sequential(
            nn.ConvTranspose2d(100, 512, kernel_size=4, bias=False),
            nn.BatchNorm2d(512),
            nn.ReLU(),
            
            nn.ConvTranspose2d(512, 256, kernel_size=4, stride=2, padding=1, bias=False),
            nn.BatchNorm2d(256),
            nn.ReLU(),
            
            nn.ConvTranspose2d(256, 128, kernel_size=4, stride=2, padding=1,bias=False),
            nn.BatchNorm2d(128),
            nn.ReLU(),
            
            nn.ConvTranspose2d(128, 64, kernel_size=4, stride=2, padding=1,bias=False),
            nn.BatchNorm2d(64),
            nn.ReLU(),
            
            nn.ConvTranspose2d(64, 3, kernel_size=4, stride=2, padding=1, bias=False),
            nn.Tanh(),
        )

    def forward(self, x):
        return self.gen(x)
        
class Discriminator(nn.Module):
    def __init__(self):
        super(Discriminator, self).__init__()

        self.disc = nn.Sequential(
            nn.Conv2d(3, 64, kernel_size=4, stride=2, padding=1, bias=False),
            nn.BatchNorm2d(64),
            nn.LeakyReLU(0.2),
            
            nn.Conv2d(64, 128, kernel_size=4, stride=2, padding=1, bias=False),
            nn.BatchNorm2d(128),
            nn.LeakyReLU(0.2),

            nn.Conv2d(128, 256, kernel_size=4, stride=2, padding=1, bias=False),
            nn.BatchNorm2d(256),
            nn.LeakyReLU(0.2),

            nn.Conv2d(256,512, kernel_size=4, stride=2, padding=1, bias=False),
            nn.BatchNorm2d(512),
            nn.LeakyReLU(0.2),

            nn.Conv2d(512, 1, kernel_size=4),
            nn.Sigmoid()
        )

    def forward(self, x):
        return self.disc(x)

 

4. 모델 학습

def weights_init(m):
    classname = type(m).__name__

    if classname.find('Conv') != -1:
        nn.init.normal_(m.weight.data, 0.0, 0.02)

    elif classname.find('BatchNorm') != -1:
        nn.init.normal_(m.weight.data, 1.0, 0.02)
        nn.init.constant_(m.bias.data, 0)
import torch
import tqdm
from torch.optim.adam import Adam

device = 'cuda' if torch.cuda.is_available() else 'cpu'

G = Generator().to(device)
D = Discriminator().to(device)

G.apply(weights_init)
D.apply(weights_init)

G_optim = Adam(G.parameters(), lr=0.001, betas=(0.5, 0.999))
D_optim = Adam(D.parameters(), lr=0.001, betas=(0.5, 0.999))
for epoch in range(20):
    iterator = tqdm.tqdm(enumerate(data_loader), total=len(data_loader))

    for idx, data in iterator:
        D_optim.zero_grad()
        label = torch.ones_like(data[1], dtype=torch.float32).to(device)
        label_fake = torch.zeros_like(data[1], dtype=torch.float32).to(device)

        real = D(data[0].to(device))
        D_loss_real = nn.BCELoss()(torch.squeeze(real), label)
        D_loss_real.backward()

        noise = torch.randn(label.shape[0], 100, 1, 1, device=device)
        fake = G(noise)
        output = D(fake.detach())
        D_loss_fake = nn.BCELoss()(torch.squeeze(output), label_fake)
        D_loss_fake.backward()

        D_loss = D_loss_real + D_loss_fake
        D_optim.step()

        G_optim.zero_grad()
        output = D(fake)
        G_loss = nn.BCELoss()(torch.squeeze(output), label)
        G_loss.backward()
        G_optim.step()

        iterator.set_description(f'Epoch: {epoch} | D_loss {D_loss}, G_loss {G_loss}')

    torch.save(G.state_dict(), './generator.pt')
    torch.save(D.state_dict(), './discriminator.pt')