Developer's Development
3.3.23 [LLM] 자연어-이미지 멀티모달: 이미지 딥러닝 응용(스타일 전이 학습, GAN) 본문
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 토큰 생성
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')
'LLM' 카테고리의 다른 글
| 3.3.24 [LLM] 자연어-이미지 멀티모달: 텍스트 기반 이미지 생성, Image Captioning (0) | 2025.09.28 |
|---|---|
| 3.3.22 [LLM] 자연어-이미지 멀티모달: 주요 CNN 모델 (0) | 2025.09.28 |
| 3.3.21 [LLM] 자연어-이미지 멀티모달: CNN 개요 및 구조 (0) | 2025.09.16 |
| 3.3.20 [LLM] 자연어-이미지 멀티모달: etc (0) | 2025.09.15 |
| 3.3.19 [LLM] 파인튜닝 기법: DPO (0) | 2025.09.15 |