본문 바로가기

인공지능[AI]

케라스 창시자에게 배우는 딥러닝[12장: 생성 모델을 위한 딥러닝] part2 딥드림

728x90
반응형
SMALL

12장 생성 모델을 위한 딥러닝

12.2 딥드림

딥드림

  • 딥드림(DeepDream)은 합성곱 신경망(CNN)이 학습한 이미지 표현을 활용하여 예술적으로 이미지를 변형하는 기법

▶ 딥드림이 출력한 이미지

  • 딥드림 알고리즘은 9장에서 소개한 컨볼루션 신경망을 거꾸로 실행하는 컨브넷 필터 시각화 기법과 거의 동일
  • 특정 필터의 활성화를 최대화하기 위해 컨볼루션 신경망의 입력에 경사 상승법을 적용
  • 이때 딥드림은 단일 필터가 아니라 층의 활성화를 동시에 최대화하는 방식을 사용. 따라서 여러 다양한 시각적 특징을 한 번에 시각화 가능
  • 딥드림은 빈 이미지나 노이즈가 조금 있는 입력이 아니라 이미 가지고 있는 이미지를 사용. 이는 기존의 시각 패턴을 이용하여 이미지의 요소를 예술적인 스타일로 변형시키는 효과를 가져옴
  • 입력 이미지는 시각 품질을 높이기 위해 여러 다른 스케일(옥타브(octave))로 처리
  • 먼저 딥드림에 사용할 테스트 이미지를 준비
  • 바위가 많은 북부 캘리포니아 해안의 겨울 사진을 사용

케라스 딥드림 구현

  • 테스트 이미지 내려받기
# TensorFlow와 Keras를 사용하여 이미지를 로드하고 표시하는 작업을 수행
from tensorflow import keras
import matplotlib.pyplot as plt

# 이미지 파일을 다운로드하고 저장할 경로를 설정한다. get_file함수는 지정된 URL에서 파일을 다운로드하고 해당 파일의 경로를 반환한다.
base_image_path = keras.utils.get_file(
    "coast.jpg", origin="https://img-datasets.s3.amazonaws.com/coast.jpg")

# 축을 표시하지 않도록 설정
plt.axis("off")

# 다운로드한 이미지를 로드하고 imshow 함수를 사용하여 이미지를 표시한다. 
plt.imshow(keras.utils.load_img(base_image_path))

▶ 테스트 이미지

  • 사전 훈련된 컨브넷 선택: Keras에는 여러 개 사전 훈련된 컨브넷 모델이 있다(VGG16, VGG19, Xception, ResNet50 등). 이 모델들은 ImageNet데이터셋에서 학습된 가중치를 가지고 있다. 따라서 어떤 모델을 선택하든 딥드림을 구현할 수 있다. 각 모델은 다른 특성을 학습했기 때문에 선택한 모델에 따라 시각화 결과에 영향을 미친다.
  • 인셉션 모델: 원래 딥드림에서 사용한 컨브넷은 인셉션 모델이다. 이 모델은 딥드림 이미지를 잘 생성한다
  • 여기서도 케라스의 인셉션 V3 모델을 사용
  • 사전 훈련된 inceptionV3 모델 로드하기
# TensorFlow의 Keras 라이브러리에서 inception_v3 모델을 불러오기 위해 해당 모듈을 임포트
from tensorflow.keras.applications import inception_v3

# InceptionV3 클래스를 사용하여 InceptionV3 모델을 불러온다. 매개변수로는 다음을 사용
# weights="imagenet": ImageNet 데이터셋에서 사전 훈련된 가중치를 사용
# include_top=False : 마지막 fully connected 레이어를 포함하지 않고 모델을 불러온다. 이렇게 함으로 모델의 마지막 레이어를 제외하고 특성 추출 부분만을 사용할 수 있다.
model = inception_v3.InceptionV3(weights="imagenet", include_top=False)
  • 사전 훈련된 컨브넷을 사용하여 다양한 중간층의 활성화를 반환하는 특성 추출 모델을 생성
  • 경사 상승법 동안 최대화할 손실에 대한 각 층의 기여도에 가중치를 주기 위해 스칼라 값을 선택
  • 원하는 층을 선택하려면 'model.summary()'에서 제공되는 전체 층 이름을 참고
  • 딥드림 손실에 대한 각 층의 기여도 설정하기
layer_settings = { 
    # 활성화를 최대화할 층과 전체 손실에 대한 가중치. 이 설정을 바꾸면 새로운 시각 효과를 얻을 수 있음
    "mixed4": 1.0,
    "mixed5": 1.5,
    "mixed6": 2.0,
    "mixed7": 2.5,
}

# 각 층의 심볼릭 출력을 모으는 역할
# model.get_layer를 사용하여 각 층의 심볼릭 출력을 'output_dict'에 저장
outputs_dict = dict(
    [
        (layer.name, layer.output)
        for layer in [model.get_layer(name) for name in layer_settings.keys()]
    ]
)

# 각 타깃 층의 활성화 값을 (하나의 딕셔너리로) 변환하는 모델
# 이렇게 생성된 feature_extractor 모델은 딥드림 단계에서 각 층의 활성화 값을 활용하는 데 사용
feature_extractor = keras.Model(inputs=model.inputs, outputs=outputs_dict)
  • 딥드림 알고리즘의 핵심인 손실을 계산하는 부분
  • 여기서는 경사 상승법을 사용하여 각 스케일에서 최대화할 값을 계산한다.
  • 이를 위해 여러 층에 있는 모든 필터 활성화를 동시에 최대화하며, 특히 상위 층의 활성화의 L2놈에 가중치를 준 평균을 최대화한다.
  • 이렇게 함으로써 원하는 시각 요소를 만들어 내는 큰 영향을 미치는 층들을 선택할 수 있다. 이 선택된 층들은 어떤 시각적 특징이 강조되는지에 따라 다양한 시각 효과를 만들어 낼 수 있다.
  • 먼저 임의로 4개의 층을 선택해 보자
  • 나중에 다른 설정을 다양하게 시도해 보는 것이 좋음
  • 딥드림 손실
def compute_loss(input_image):
    # feature_extractor를 사용하여 입력 이미지의 다양한 중간층에서의 활성화 값을 추출한다
    features = feature_extractor(input_image)

    # 손실을 0으로 초기화
    loss = tf.zeros(shape=())

    for name in features.keys():
        # 미리 설정된 layer_settings에 따라 각 층에 대한 가중치 개수를 가져옴. 이 가중치는 해당 층의 활성화 값이 최대화되는 정도를 조절한다.
        coeff = layer_settings[name]

        activation = features[name]
        
        # 경계 부근의 인공적인 패턴을 피하기 위해 테두리가 아닌 픽셀만 손실에 추가
        # 활성화 값의 제곱을 계산하고 해당 값의 평균을 구한다
        # 가중치 계수를 곱하여 최종 손실에 누적
        loss += coeff * tf.reduce_mean(tf.square(activation[:, 2:-2, 2:-2, :]))

    # 최종적으로 계산된 손실 값을 반환
    return loss
  • 이제 각 옥타브에서 실행할 경사 상승법 단계를 준비. 9장의 필터 시각화 기법과 같음
  • 딥드림 알고리즘은 필터 시각화의 다중 스케일 버전일 뿐
  • 딥드림 경사 상승법 단계
import tensorflow as tf

@tf.function # tf.function으로 컴파일하여 훈련 스텝의 속도를 높인다.
# 현재 이미지에 대한 딥드림 손실의 그레이디언트를 계산하고, 그레이디언트를 정규화하여 이미지를 업데이트하는 함수
def gradient_ascent_step(image, learning_rate):

    # 현재 이미지에 대한 딥드림 손실의 그레이디언트를 계산
    with tf.GradientTape() as tape:
        tape.watch(image)
        loss = compute_loss(image)
    grads = tape.gradient(loss, image)

    # 그레이디언트를 L2 정규화하여 방향을 정규화한다.
    grads = tf.math.l2_normalize(grads)

    # 이미지를 학습률('learning_rate')과 그레이디언트를 이용하여 업데이트한다
    image += learning_rate * grads

    # 손실과 업데이트된 이미지를 반환한다.
    return loss, image


def gradient_ascent_loop(image, iterations, learning_rate, max_loss=None): # 주어진 이미지 스케일(옥타브)에 대한 경사 상승법을 수행
    # iterations 횟수만큼 딥드림 손실을 증가시키는 방향으로 반복적으로 이미지를 업데이트
    for i in range(iterations):
        loss, image = gradient_ascent_step(image, learning_rate)

        # 손실이 일정 임계 값을 넘으면 중지(과도하게 최적화하면 원치 않는 이미지를 만들 수 있음)
        if max_loss is not None and loss > max_loss:
            break
        
        # 각 손실에서의 손실 값을 출력
        print(f"... 스텝 {i}에서 손실 값: {loss:.2f}")
    return image
  • 마지막으로 딥드림 알고리즘의 바깥쪽 루프
  1. 옥타브 스케일의 리스트 정의: 3개의 다른 '옥타브'로 이미지를 처리
  2. 가장 작은 값에서 가장 큰 값까지 각 옥타브에서 gradient_ascent_loop()로 경사 상승법 단계를 30번 실행하여 앞서 정의한 손실을 최대화
  3. 옥타브 간 이미지 크기 조정: 작은 이미지에서 시작하여 점점 크기를 키운다.(각 옥타브 사이에서는 이미지가 40% 증가)

▶ 딥드림 과정: 연속적으로 스케일을 늘리고(옥타브), 스케일이 증가된 이미지에 디테일을 재주입

  • 다음 코드에서 이 과정에 필요한 파라미터를 정의
  • 이런 파라미터를 바꾸면 새로운 효과를 낼 수 있음
step = 20. # 경사 상승법 단계 크기
num_octave = 3 # 경사 상승법을 실행할 스케일 횟수
octave_scale = 1.4 # 연속적인 스케일 사이의 크기 비율
iterations = 30 # 스케일 단계마다 수행할 경사 상승법 단계 횟수
max_loss = 15. # 이보다 손실이 커지면 현재 스케일에서 경사 상승법 과정을 중지
  • 이미지를 로드하고 저장하는 유틸리티 함수도 구현
  • 이미지 처리 유틸리티
import numpy as np

# 이미지를 로드하고 크기를 바꾸어 적절한 배열로 변환하는 유틸리티 함수
def preprocess_image(image_path):
    img = keras.utils.load_img(image_path)
    img = keras.utils.img_to_array(img)
    img = np.expand_dims(img, axis=0)
    img = keras.applications.inception_v3.preprocess_input(img)
    return img

# 넘파이 배열을 이미지로 변환하는 유틸리티 함수
def deprocess_image(img):
    img = img.reshape((img.shape[1], img.shape[2], 3))

    # inceptionV3전처리 복원하기
    img += 1.0
    img *= 127.5

    # unit8로 바꾸고 [0,255] 범위로 클리핑한다.
    img = np.clip(img, 0, 255).astype("uint8")
    return img
  • 아래 코드는 딥드림 알고리즘의 이미지 처리하는 주요 단계들을 나타내는 부분. 이 부분은 이미지의 크기를 연속적으로 늘리면서 이미지 디테일을 유지하는 기법을 사용
  1. 원본 이미지를 불러오고 전처리
  2. 다양한 옥타브에서 이미지의 크기를 계산하고 준비
  3. 가장 작은 크기의 이미지로 시작하여 크기를 점점 키우면서 옥타브마다 경사 상승법을 적용
  4. 작은 버전의 원본 이미지를 현재 크기로 늘려서 픽셀 경계를 나타나게 한다.
  5. 현재 크기에 해당하는 원본 이미지와 픽셀 경계가 나타나는 이미지 간의 차이를 계산하여 디테일을 파악한다.
  6. 디테일을 딥드림 이미지에 재주입하고 다음 옥타브로 진행
  7. 모든 옥타브에 대해 반복하여 최종 딥드림 이미지를 생성하고 저장
  • 연속적인 여러 갸의 '옥타브'에 걸쳐 경사 상승법 실행하기
# 테스트 이미지를 로드한다.
original_img = preprocess_image(base_image_path)

# 원본 이미지의 형태(shape)의 높이와 너비를 추출한다.
original_shape = original_img.shape[1:3]

# 여러 옥타브에서 이미지 크기를 계산한다. 이미지를 크게 변형하면서 작은 세부 사항을 고려할 수 있도록 하기 위함
successive_shapes = [original_shape]
for i in range(1, num_octave):
    shape = tuple([int(dim / (octave_scale ** i)) for dim in original_shape])
    successive_shapes.append(shape)
successive_shapes = successive_shapes[::-1]

# 원본 이미지를 첫 번째 옥타브의 크기로 줄인다.
shrunk_original_img = tf.image.resize(original_img, successive_shapes[0])

# 이미지를 복사한다(원본 이미지는 그대로 보관). 이후 경사 상승법을 적용하여 이미지를 수정할 것
img = tf.identity(original_img)

# 여러 옥타브에 대해 반복
for i, shape in enumerate(successive_shapes):
    print(f"{shape} 크기의 {i}번째 옥타브 처리")

    # 딥드림 이미지 스케일을 높인다.(현재 옥타브의 크기로 이미지를 조절)
    img = tf.image.resize(img, shape)

    # 경사 상승법을 실행하고 딥드림 이미지를 수정한다.
    img = gradient_ascent_loop(
        img, iterations=iterations, learning_rate=step, max_loss=max_loss
    )

    # 작은 버전의 원본 이미지의 스케일을 높인다. 픽셀 경계가 보일 것이다.
    upscaled_shrunk_original_img = tf.image.resize(shrunk_original_img, shape)

    # 이 크기에 해당하는 고해상도 버전의 원본 이미지를 계산한다.
    same_size_original = tf.image.resize(original_img, shape)

    # 두 이미지의 차이가 스케일을 높였을 때 손실된 디테일이다.
    lost_detail = same_size_original - upscaled_shrunk_original_img

    # 손실된 디테일을 딥드림 이미지에 다시 주입한다.
    img += lost_detail

    # 다음 옥타브를 위해 작은 버전의 원본 이미지를 현재 옥타브 크기로 다시 조절한다.
    shrunk_original_img = tf.image.resize(original_img, shape)

# 최종 경과를 저장한다.
keras.utils.save_img("dream.png", deprocess_image(img.numpy()))
  • 원본 인셉션 V3 네트워크는 299*299 크기의 이미지에서 훈련되었기 때문에, 딥드림 구현에서는 이미지 크기를 300*300, 400*400 사이로 조절하면 훨씬 좋은 결과를 만듦
  • 하지만 이 코드는 어떤 크기나 비율의 이미지에서도 실행 가능

▶ 예제 이미지에서 딥드림 코드 실행 결과

  • 다양한 층의 조합으로 손실을 시도해보면, 네트워크의 하위 층은 기하학적 패턴이 많은 딥드림 이미지를 만들어내고, 상위 층은 뚜렷한 시각 패턴을 만듦.
  • 'layer_settings' 딕셔너리의 파라미터를 랜덤 하게 조정하여 여러 가지 층 조합을 빠르게 시도 가능

▶ 층 조합을 다르게 하여 얻은 홈메이드 페이스트리 이미지의 결과

정리

  • 딥드림은 네트워크가 학습한 표현을 기반으로 컨브넷을 거꾸로 실행하여 입력 이미지를 생성
  • 재미있는 결과가 만들어지고, 때로는 환각제 때문에 시야가 몽롱해진 사람이 만든 이미지 같기도 함
  • 이 과정은 이미지 모델이나 컨브넷에 국한되지 않고 음성, 음악 등에도 적용 가능
728x90
반응형
LIST