1. VGG 네트워크 이해
스타일 변환을 구현하기 전에 VGG 네트워크의 기본 구조를 이해해야 합니다. VGG는 연속적인 합성곱(convolution) 연산을 통해 이미지의 특징을 추출하고 분류하는 데 탁월한 성능을 보입니다. 특히 VGG-16과 VGG-19 모델은 스타일 변환에서 콘텐츠와 스타일 특징을 분리하는 데 자주 사용됩니다.
VGG 네트워크는 A에서 E까지 여러 구성이 있으며, 각각 VGG-11, VGG-13, VGG-16, VGG-19에 해당합니다. 입력 이미지는 여러 합성곱과 풀링(pooling) 레이어를 거쳐 최종적으로 분류 결과를 출력합니다.
2. 스타일 변환의 개념
이미지 스타일 변환에서는 두 가지 핵심 요소를 고려해야 합니다:
- 콘텐츠 보존: 생성된 이미지가 원본 콘텐츠 이미지의 구조와 객체를 유지해야 합니다.
- 스타일 전이: 생성된 이미지가 스타일 이미지의 질감, 색상, 패턴을 반영해야 합니다.
이를 위해 두 가지 손실(loss) 함수를 정의합니다:
- 콘텐츠 손실: 생성 이미지와 콘텐츠 이미지의 특징 차이
- 스타일 손실: 생성 이미지와 스타일 이미지의 질감 특징 차이
VGG 네트워크는 이미 학습된 가중치를 제공하므로, 별도의 학습 없이 특징 추출기로 사용할 수 있습니다. 생성된 이미지를 VGG에 통과시켜 각 레이어에서 특징을 추출한 후, 콘텐츠와 스타일에 대한 손실을 계산합니다.
3. 콘텐츠 손실(Content Loss)
콘텐츠 손실은 일반적으로 VGG 네트워크의 특정 레이어(예: relu3_3 또는 relu4_2)에서 두 이미지의 특징 맵(feature map) 간 차이를 측정합니다. 수식은 다음과 같습니다:
L_content = (1 / (C * H * W)) * Σ(φ(y) - φ(y'))²
여기서 φ(y)는 콘텐츠 이미지의 특징 행렬, φ(y')는 생성 이미지의 특징 행렬을 나타냅니다. C는 채널 수, H와 W는 높이와 너비입니다.
코드 구현 예시:
def compute_content_loss(content_feats, generated_feats):
content_layers = [('relu3_3', 1.0)]
total_loss = 0.0
for layer_name, weight in content_layers:
p = extract_vgg_feature(content_feats, layer_name)
x = extract_vgg_feature(generated_feats, layer_name)
# 특징 맵의 크기: 배치 x 높이 x 너비 x 채널
num_elements = p.shape[1] * p.shape[2] * p.shape[3]
layer_loss = (1.0 / num_elements) * tf.reduce_sum(tf.square(p - x))
total_loss += layer_loss * weight
return total_loss / len(content_layers)
4. 스타일 손실(Style Loss)
스타일 손실은 Gram 행렬(Gram Matrix)을 사용하여 이미지의 질감 특징을 포착합니다. Gram 행렬은 특징 맵 간의 상관관계를 나타내며, 대각선 요소는 각 특징의 강도를, 비대각선 요소는 특징 간의 동시 발생 패턴을 보여줍니다.
Gram 행렬 계산:
G = φ(x)ᵀ * φ(x)
스타일 손실은 여러 레이어(보통 낮은 레벨부터 높은 레벨까지)에서 Gram 행렬의 차이를 합산하여 계산합니다.
def compute_gram_matrix(feature_map, height, width, channels):
# 특징 맵을 2D 행렬로 변환
flattened = tf.reshape(feature_map, (height * width, channels))
gram = tf.matmul(tf.transpose(flattened), flattened)
return gram
def compute_style_loss(style_feats, generated_feats):
style_layers = [('relu1_2', 0.25), ('relu2_2', 0.25),
('relu3_3', 0.25), ('relu4_3', 0.25)]
total_loss = 0.0
for layer_name, weight in style_layers:
a = extract_vgg_feature(style_feats, layer_name)
x = extract_vgg_feature(generated_feats, layer_name)
H, W, C = a.shape[1], a.shape[2], a.shape[3]
M = H * W
N = C
A_gram = compute_gram_matrix(a, M, N)
G_gram = compute_gram_matrix(x, M, N)
layer_loss = (1.0 / (4 * M * M * N * N)) * tf.reduce_sum(tf.square(G_gram - A_gram))
total_loss += layer_loss * weight
return total_loss / len(style_layers)
5. 전체 변분 손실(Total Variation Loss)
선택적으로 사용되는 전체 변분 손실은 생성된 이미지의 공간적 평활성을 유지하기 위한 정규화 항입니다. 이웃 픽셀 간의 차이를 최소화하여 노이즈를 줄입니다:
L_tv = Σ|y_(n+1) - y_n|
6. 주요 구현 코드
전체 프로세스는 다음 단계로 구성됩니다:
- VGG-19 모델 로드 및 네트워크 재구성
- 콘텐츠 및 스타일 이미지 전처리
- 손실 함수 정의
- 최적화를 통한 이미지 생성
import tensorflow as tf
import numpy as np
import cv2
import scipy.io
# 하이퍼파라미터 설정
IMAGE_HEIGHT = 300
IMAGE_WIDTH = 450
LEARNING_RATE = 1.0
NOISE_RATIO = 0.5
ALPHA = 1.0 # 콘텐츠 손실 가중치
BETA = 500.0 # 스타일 손실 가중치
TRAIN_STEPS = 200
def build_vgg19():
"""VGG-19 네트워크 재구성"""
layer_names = (
'conv1_1','relu1_1','conv1_2','relu1_2','pool1',
'conv2_1','relu2_1','conv2_2','relu2_2','pool2',
'conv3_1','relu3_1','conv3_2','relu3_2','conv3_3','relu3_3','conv3_4','relu3_4','pool3',
'conv4_1','relu4_1','conv4_2','relu4_2','conv4_3','relu4_3','conv4_4','relu4_4','pool4',
'conv5_1','relu5_1','conv5_2','relu5_2','conv5_3','relu5_3','conv5_4','relu5_4','pool5'
)
# 사전 학습된 가중치 로드
vgg_data = scipy.io.loadmat('imagenet-vgg-verydeep-19.mat')
weights = vgg_data['layers'][0]
network = {}
input_tensor = tf.Variable(np.zeros([1, IMAGE_HEIGHT, IMAGE_WIDTH, 3]), dtype=tf.float32)
network['input'] = input_tensor
current = input_tensor
for idx, name in enumerate(layer_names):
layer_type = name[:4]
if layer_type == 'conv':
kernel = weights[idx][0][0][0][0][0]
bias = weights[idx][0][0][0][0][1]
conv = tf.nn.conv2d(current, tf.constant(kernel),
strides=[1,1,1,1], padding='SAME')
current = tf.nn.relu(conv + bias)
elif layer_type == 'pool':
current = tf.nn.max_pool(current, ksize=[1,2,2,1],
strides=[1,2,2,1], padding='SAME')
network[name] = current
return network
def precompute_features(session, model, content_img, style_img):
"""콘텐츠 및 스타일 특징 사전 계산"""
# 콘텐츠 특징
session.run(tf.assign(model['input'], content_img))
content_features = {}
for layer_name, _ in CONTENT_LAYERS:
content_features[layer_name] = session.run(model[layer_name])
# 스타일 특징
session.run(tf.assign(model['input'], style_img))
style_features = {}
for layer_name, _ in STYLE_LAYERS:
style_features[layer_name] = session.run(model[layer_name])
return content_features, style_features
def main():
# 모델 초기화
model = build_vgg19()
# 이미지 로드 및 전처리
content_img = cv2.imread('content.jpg')
content_img = cv2.resize(content_img, (IMAGE_WIDTH, IMAGE_HEIGHT))
content_img = np.reshape(content_img, (1, IMAGE_HEIGHT, IMAGE_WIDTH, 3)) - [128.0, 128.0, 128.0]
style_img = cv2.imread('style.jpg')
style_img = cv2.resize(style_img, (IMAGE_WIDTH, IMAGE_HEIGHT))
style_img = np.reshape(style_img, (1, IMAGE_HEIGHT, IMAGE_WIDTH, 3)) - [128.0, 128.0, 128.0]
# 초기 생성 이미지 (노이즈 + 콘텐츠 혼합)
noise = np.random.uniform(-20, 20, [1, IMAGE_HEIGHT, IMAGE_WIDTH, 3])
init_img = noise * NOISE_RATIO + content_img * (1 - NOISE_RATIO)
with tf.Session() as sess:
# 특징 사전 계산
content_feats, style_feats = precompute_features(sess, model, content_img, style_img)
# 손실 함수 및 최적화
total_loss = (ALPHA * compute_content_loss(content_feats, model) +
BETA * compute_style_loss(style_feats, model))
optimizer = tf.train.AdamOptimizer(LEARNING_RATE).minimize(total_loss)
sess.run(tf.global_variables_initializer())
sess.run(tf.assign(model['input'], init_img))
# 학습 루프
for step in range(TRAIN_STEPS):
sess.run(optimizer)
if step % 10 == 0:
current_img = sess.run(model['input'])
current_img += [128.0, 128.0, 128.0]
current_img = np.clip(current_img, 0, 255).astype(np.uint8)
cv2.imwrite(f'output_{step}.jpg', current_img[0])
# 최종 결과 저장
final_img = sess.run(model['input'])
final_img += [128.0, 128.0, 128.0]
final_img = np.clip(final_img, 0, 255).astype(np.uint8)
cv2.imwrite('final_result.jpg', final_img[0])
if __name__ == '__main__':
main()
7. 결과 예시
적절한 하이퍼파라미터(α와 β의 비율)를 선택하면 콘텐츠와 스타일이 조화롭게 혼합된 결과를 얻을 수 있습니다. 일반적으로 β/α의 비율이 높을수록 스타일이 더 강하게 적용됩니다.