VGG 기반 딥러닝 이미지 스타일 변환 구현

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. 주요 구현 코드

전체 프로세스는 다음 단계로 구성됩니다:

  1. VGG-19 모델 로드 및 네트워크 재구성
  2. 콘텐츠 및 스타일 이미지 전처리
  3. 손실 함수 정의
  4. 최적화를 통한 이미지 생성
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. 결과 예시

적절한 하이퍼파라미터(α와 β의 비율)를 선택하면 콘텐츠와 스타일이 조화롭게 혼합된 결과를 얻을 수 있습니다. 일반적으로 β/α의 비율이 높을수록 스타일이 더 강하게 적용됩니다.

태그: VGG VGG-19 스타일 변환 콘텐츠 손실 스타일 손실

7월 4일 22:30에 게시됨