본 블로그 포스팅은 수도권 ICT 이노베이션 스퀘어에서 진행하는 인공지능 고급-시각 강의의 CNN알고리즘 강좌 내용을 필자가 다시 복기한 내용에 관한 것입니다.
https://tutorials.pytorch.kr/advanced/neural_style_tutorial.html
Neural Style Transfer은
Leon A. Gatys
, Alexander S. Ecker
and Matthias Bethge
에 의해 발표된 A neural algorithm of artistic style, Image style transfer using convolutional neural network 논문을 기반으로 한 이미지 변환&생성 기법 중 하나로
위 그림처럼
content 이미지에 어떤 예술적인 style 이미지를 적용하여 예술적 Style를 닮은 이미지을 생성하거나 변환하는 방법을 말한다.
우선 위 그림처럼 content 이미지, style 이미지 그리고 변환시키고 싶은 이미지를 Task 이미지라 정의하고 이 3가지 이미지를 2차원 좌표공간
에 있다 상상을 해보자
그러면 content 이미지 - Task 이미지 거리 :
style 이미지 - Task 이미지 거리 :
두가지 정량화된 거리값을 만들어 낼 수 있을 것이다.
이 거리값을 가장 짧게 만드는 최적화 방법을 수행하는 것이다.
그러나, 위 2차원 좌표공간
에서 두 거리값을 최적화 하면 아래의 결과가 예상된다.
이거는 우리가 원하는 결과가 아니다.
그 이유에 대해 생각해보자면
위 사진처럼
그림에 관한것은 수학적으로 정량화를 수행하여 거리값을 만들어 냈지만 그림을 그린 화가에 대한 화풍이란 정보를 정량화하지 못했기 때문에 발생하는 문제이다.
이 정성지표
인 화풍을 Z
축위에서 정량화되었다고 다시한번 상상력을 발휘해 보자
이렇게 위 사진처럼 화풍을 Z
축 정량화한다면 사진처럼
화풍의 style 이미지 - Task 이미지 거리 : 을 연상할 수 있을 것이다.
그림 간 거리 content 이미지 - Task 이미지 거리 : 는 그대로 두고
화풍 간 거리 style 이미지 - Task 이미지 거리 : 를 새로이 정의하여
이 두 값 간의 거리를 최소화 하는 최적화를 수행한다.
이것이 Neural Style Transfer의 메인 아이디어라 할 수 있다.
논문에서는 Neural Style Transfer의 전체 workflow를 표현했으나,
전체적인 과정이 인공지능 고급(시각) 강의 복습 - 23. DeepDream (2)
에서 표현한 Deep Dream workflow랑 유사하게 표현을 할 수 있다.
이를 그림으로 도식화 하면 아래와 같다.
전체 workflow를 본다면 인공지능 고급(시각) 강의 복습 - 23. DeepDream (2)
에서 거의 다 설명했던 방법론이라
해당 포스트를 참조해주기 바란다.
이 중에서 주요하게 변경된 부분이 Style Loss - Gram Matirx이기에 이 부분만 집중해서 설명을 진행하겠다.
Style Loss - Gram Matirx
앞서 인공지능 고급(시각) 강의 복습 - 11. CNN 모델 중간 출력물 알아보기
에서 특정 레이어의 출력물 Feature out
를 추출해 내고 이를 이미지로 표현하면 아래와 같이 표현할 수 있다.
다양한 레이어에서 Feature를 추출하고, 이 추출한 Fatutres 항목들 중에서 Feature 내 여러 채널이 존재하니
각 채널간 유사도를 산출하면 그게 화풍이란 정보를 정량화하는 과정이라 보면 될 것 같다.
따라서 같은 작가가 그린 그림 간의 Style Loss
를 측정하면
다른 작가가 그린 그림 간의 Style Loss
보다 작은 값이 나오는
경향성이 있다는 것을 알아냈고
그것을 정량지표로 삼았다 라고 보면 될 듯 하다.
실제로도 피카소 그림이랑 레오나르도 다 빈치 그림이랑 Style Loss
를 비교하면 위와 같은 결과치를 얻을 수 있었다.
피카소의 경우 워낙 다양한 화풍의 그림을 그리다 보니 자신의 작품인데도 Style Loss
가 높게 나오지만 그 외로는 같은 작가의 작품일 경에는 Style Loss
가 낮게 나오는 경향성이 있음을 확인할 수 있다.
이게 관련 자료를 공부하다 보니 SVM도 튀어나오고 음.. 뭐..
잘 이해가 안된다..
(그래도 벡터 내적이 유사도에 대한 지표라는건 알아두고 넘어가자)
아무튼 여러 레이어에서 Feature를 추출
각 레이어의 채널 유사도를 나타내는 gram matrix 여러개 만듬
이걸로 Style Loss를 비교 연산
이렇게 알고 넘어가자
코드의 순서는
1) 데이터 전처리
2) backbone 모델의 조정, Feature 캡쳐 Net 설계
3) Loss 함수 설게
4) Style Transfer 수행
4 단계로 나뉜다
1) 데이터 전처리
import torch
import torch.nn as nn
from torchvision.transforms import v2
from PIL import Image
import numpy as np
#GPU 사용 가능 여부 확인
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# 데이터셋 표준화를 위한 기본정보 # 이게 이미지 정규화임
imgNet_val = {'mean' : [0.485, 0.456, 0.406], 'std' : [0.229, 0.224, 0.225]}
from img_utils import * #필자가 따로 만든 데이터 전처리 라이브러리
content_img = './Stormveil Castle.png'
style_img = './Great Wave of Kanagawa.png'
tensor_size = 1024 #이미지를 텐서 자료형으로 변환 시 크기 통일을 위해 변수 설정
content_tensor, img_shape = preprocess_img(content_img, tensor_size, imgNet_val, device)
style_tensor, _ = preprocess_img(style_img, tensor_size, imgNet_val, device)
# 이게 진짜 작업을 수행할 메인 자료 -> 1) 노이즈, 2) 콘텐츠 이미지 복사 형식으로 초기화
task_img = create_random_noise_img(img_shape)
task_img.save("random_noise.jpg")
task_tensor, _ = preprocess_img('random_noise.jpg', tensor_size, imgNet_val, device)
우선 from img_utils import *
여기에서 img_utils.py
파일에 대해 설명하자면
이전 포스트 인공지능 고급(시각) 강의 복습 - 23. DeepDream (2)
에서 사용한 이미지 전처리 함수를 엮은 파일로
https://github.com/tbvjvsladla/metacodeM_pytorch_bootcamp/blob/main/img_utils.py
여기에서 다운로드 받을 수 있다.
해당 코드의 기능은 아래 그림으로 표현할 수 있다.
매번 이미지 처리용 함수를 설계하는 것보다는 코드 재사용성을 높이고자 정리를 했다.
따라서 위 과정을 정리하자면
content 이미지, style 이미지, Task 이미지를 모두 Tensor 자료형 변환
을 수행했다 라고 보면 된다.
2) backbone 모델 조정, Feature 캡쳐 Net 설계
DeepDream에서 주로 사용되는 backbone
는
Pre-trained Inception_v3 이었으나
Neural Style Transfer에서 주로 사용하는 backbone
는
Pre-trained VGG19이다.
from torchvision import models
# 뉴럴 스타일 트랜스퍼에 사용할 백본 모델 불러오기 # torchvision에 있는 VGG19모델을 사용
pr_model = models.vgg19(weights=models.VGG19_Weights.IMAGENET1K_V1).to(device)
따라서 torchvision
라이브러리르 통해 Pre-trained VGG19를 다운로드 받는다.
그러나 다운로드 받은 VGG19를 살펴본다면
위 와 같이 조~금 인덱싱하기 어렵게 만들어 놧다.
그리고 Neural Style Transfer를 수행할 때는 Relu
의 inplace
옵션을 꺼야 한다.
이 inplace
옵션이 True
이면 역전파 연산 중간과정에서 사용되는 Feature정보가 덮어쓰기로 유실되기에 중간과정의 Feature(grad)정보를 남겨둘 필요성이 있다.
그래서 이를 수행할 ReModelVgg19
라는 클래스를 설계한다.
class ReModelVGG19(nn.Module): #기존vgg19모델을 인덱싱하기 편하게 블럭화
def __init__(self, origin_model):
super(ReModelVGG19, self).__init__()
origin_modeul = origin_model.features
self.module = nn.ModuleDict()
block = []
block_idx = 0
for layer in origin_modeul.children():
if isinstance(layer, nn.Conv2d) and block: #블럭이 비어있지 않은 경우
# 새로운 conv2d가 나오면 기존 블록을 저장하고 초기화
block_name = f"conv_{block_idx}_block"
self.module[block_name] = nn.Sequential(*block)
#블록 리스트 초기화
block = []
block_idx += 1
if isinstance(layer, nn.ReLU):
#in-place기능을 off -> 이렇게 해야 연산오류 발생 안함
layer = nn.ReLU(inplace=False)
#레이어를 계속 블럭 리스트에 넣기
block.append(layer)
if block: #가장 마지막 블록을 추가
block_name = f"conv_{block_idx}_block"
self.module[block_name] = nn.Sequential(*block)
def forward(self, x):
for block in self.module.values():
x = block(x)
return x
backbone = ReModelVGG19(pr_model)
#backbone는 평가 모드로 설정
backbone.eval()
해당 코드를 수행하면 VGG19가 인덱싱 하기 편하게 몇개 레이어씩 묶어서 블럭 처리화 하고
Relu(inplace=False)
로 inplace옵션을 비활성화 한다.
다음으로 전체 모델에서 특정 레이어의 Feature
정보를 캡쳐하는
네트워크를 설계한다.
#캡쳐할 블럭은 콘텐츠, 스타일별로 다르니 이걸 한번에 딕셔너리로 관리하자
block_setting = {'content' : ['conv_3_block'],
'style' : ['conv_0_block',
'conv_1_block',
'conv_2_block',
'conv_3_block',
'conv_4_block', ]}
class NeuralStyleNet(nn.Module): #백본 모델에서 활성화 레이어를 캡쳐하는 Net를 따로설계
def __init__(self, backbone, block_setting):
super(NeuralStyleNet, self).__init__()
self.backbone = backbone #백본 모델의 인스턴스화
#block_setting이 복잡한 딕셔너리 형태이니 여기서 필요 정보만 추출하는 함수 실행
self.block_setting = self.extract_list(block_setting)
self.outputs = {} #캡쳐할 feature out 저장
def hook_fn(module, input, output, name):
#캡쳐한 블럭 이름이 outputs 딕셔너리에 없을 경우
#블럭의 출력feature이랑 block이름을 key,value로 묶어서 outputs에 저장
if name not in self.outputs:
self.outputs[name] = output
for name, module in backbone.named_modules():
if any(block in name for block in self.block_setting):
#여기까지 수행하면 block_setting에 기재된 이름이 포함된 레이어+블럭이 추출됨
if isinstance(module, nn.Conv2d): #해당 레이어가 conv2d인지 확인
self._register_hook(name, module, hook_fn)
#위 _register_hook는 특정 블럭이 아니라 특정 블럭 내 레이어로 캡쳐해야한다
#블럭으로 캡쳐할 시 향후 outfeature로 loss계산 후 backward 수행할 때
#블럭 내 레이어까지 순회하게 되면서 backward가 2회 이상 반복되는 오류가 발생함
# 콘텐츠, 스타일 항목별로 캡쳐하는 모든 유니크 블록명을 추출하는 함수
def extract_list(self, block_setting):
value = list()
for k, v in block_setting.items():
for element in v:
value.append(element)
return sorted(set(value))
def _register_hook(self, name, module, hook_fn):
def hook(module, input, output): #여기서 name = 캡쳐한 모듈(블럭)의 이름
return hook_fn(module, input, output, name)
module.register_forward_hook(hook)
def forward(self, x):
self.outputs = {} # 매번 호출될 때마다 초기화
#init 에서 한번 초기화 했다고 forward에서 outputs를 초기화 안하면 낭패가 발생함
_ = self.backbone(x)
return self.outputs
#해당 클래스가 nn.Module를 상속하니 parameters메서드 사용가능
@property #이 데코레이터를 쓰면 클래스 함수를 클래스 변수처럼 쓰는게 가능
def device(self):
return next(self.parameters()).device
NeuralStyleNet
은 Backbone
인 VGG19의 특정 레이어 Feature를 캡쳐하는 클래스이며, 캡쳐방식은 nn.Module
클래스에서 제공하는 기능인 register_forward_hook
메서드 함수를 사용한다 보면 된다.
register_forward_hook
는 3가지 인자값 (module, input, output)
을 무조건 받아야 하며, 그 외 인자값 추가나 return값을 조정할 때는 함수로 여러개를 싸매서 사용해야 한다.
그래서 위 사진처럼 사용이 조금 난해하다..
아무튼 캡쳐를 위한 Net
를 설계했으니 사용방법은 아래와 같다.
3) Loss 함수 설계
import torch.nn.functional as F
# 항목별로 lossfn 설계
class TransferLoss(nn.Module):
def __init__(self, block_setting):
super(TransferLoss, self).__init__()
self.block_setting = block_setting
self.item_list = list(block_setting.keys())
# 그람 행렬(Gram matrix) 함수 정의
def gram_matrix(self, data_tensor):
bs, c, h, w = data_tensor.size() #여기서 bs는 1이다.
#Feature Map에서 채널별로 분리 (Features)
#그 다음 이 features는 3차원이니 H*W를 곱해서 2D로 차원축소
features = data_tensor.view(bs*c, h*w)
#모든 Features별로 내적을 땡겨버리자 -> 모든 instance pair값 계산
#torch.mm은 행렬곱 메서드임
G = torch.mm(features, features.t())
#gram matrix의 값을 정규화 수행
return G.div(bs*c*h*w)
#nn.Module를 상속받아서 forward함수를 선언하면 메서드함수명 안써도 됨
def forward(self, target_feature, pred_feature, item):
if item not in self.item_list:
raise Exception("item 종류 잘못 입력")
#계산할 target, pred feature를 리스트로 추출
target = [v for k, v in target_feature.items()
if any(block in k for block in self.block_setting[item])]
pred = [v for k, v in pred_feature.items()
if any(block in k for block in self.block_setting[item])]
device = pred[0].device
loss = torch.zeros(1, device=device)
# 콘텐츠 로스는 1개의 output_feature에 대해서 loss계산
if item == 'content':
loss += F.mse_loss(target[0], pred[0])
# 스타일 로스는 여러개의 output_feature를 그람 매트릭스 해서 Loss계산
elif item == 'style':
for e_target, e_pred in zip(target, pred):
G_target = self.gram_matrix(e_target)
G_pred = self.gram_matrix(e_pred)
loss += F.mse_loss(G_target, G_pred)
return loss
설계해야 할 Loss함수는 content loss
, style loss
두개이니 이를 한번에 관리하기 위해 Class 방식으로 설계한다.
사용방법은 위와 같으며, 항목별로 필요한 Feature out
에 대해
content loss
, style loss
를 계산한다.
이때 두 값의 초기 Loss값을 확인할 필요성이 있는데
보면 알 수 있듯이 통상 content loss
가 style loss
보다 몇십배 더 큰 값을 갖는게 일반적이다.
이것 때문에 content loss
, style loss
의 비율조정을 위한
Loss Weight
를 설계하여 곱해줘야 한다.
4) Style Transfer 수행
마지막으로 2, 3과정에서 인스턴스화 한
transfer_net = NeuralStyleNet(backbone, block_setting)
loss_fn = TransferLoss(block_setting)
를 받아서 Style Transfer을 수행하는 클래스를 설계한다.
from tqdm import tqdm
class StyleTransfer:
def __init__(self, net, loss_fn, block_setting,
learning_rate = 0.01,
weight_c = 15, weight_s = 1e4):
self.net = net #backbone에서 특정 Feature를 캡쳐하는 net
self.block_setting = block_setting #캡쳐할 block 정보가 담긴 딕셔너리
self.item_list = list(block_setting.keys())
self.loss_fn = loss_fn #항목별 로스 함수를 계산하는 클래스
self.weight = [weight_c, weight_s] #loss항목별 추가 가중치
self.lr = learning_rate #옵티마이저 종류에 따른 러닝 레이트
self.optimizer_fn = None #모델이랑 옵티마이저는 생성자에서 None로 초기화
#초기화 메서드 -> 1회만 수행한다.
def initailize(self, content_tensor, style_tensor):
with torch.no_grad():
self.content_feature = self.net(content_tensor)
self.style_feature = self.net(style_tensor)
#초기화는 content, style를 최초로 한번 입력하여 그 결과(feature)를 저장해둔다.
def compute_loss(self, outputs): #작업이미지가 모델을 통과한 결과물로 로스 계산
content_loss = self.loss_fn(self.content_feature, outputs, self.item_list[0])
style_loss = self.loss_fn(self.style_feature, outputs, self.item_list[1])
#콘텐츠 로스랑, 스타일로스에 사전에 정의한 로스 가중치를 곱해서 합산 -> 토탈 로스 계산
total_loss = content_loss * self.weight[0] + style_loss * self.weight[1]
loss_component = [content_loss.item(), style_loss.item()]
return total_loss, loss_component
def set_optimizer(self, tensor):
# 옵티마이저에 입력하는 텐서는 그래디언트 기능을 활성화 한다.
return torch.optim.Adam([tensor.requires_grad_(True)], lr=self.lr)
# return torch.optim.LBFGS([tensor.requires_grad_(True)])
#딥드림 함수 설계 방법론을 바탕으로 Gradient Descent Step 함수 설계
def gradient_decent_step(self, img_tensor):
#설정한 옵티마이저를 활성화
#옵티마이저는 input_tensor이 재귀로 동작하니 매번 parm(input_tensor)를
#바꿔서 재 활성화 해줘야 한다.
self.optimizer_fn = self.set_optimizer(img_tensor)
def closure(): #옵티마이저의 역전파까지 따로 떼서 closure 함수를 생성
self.optimizer_fn.zero_grad() # 옵티마이저의 기울기를 0으로 초기화
outputs = self.net(img_tensor) #Net에 img_tensor입력(전사과정)
total_loss, _= self.compute_loss(outputs)
total_loss.backward() #설계한 loss를 바탕으로 역전파 수행
return total_loss
self.optimizer_fn.step(closure) #경사하강법을 옵티마이저 step으로 수행
#--------------경사 하강법 수행의 low 코드-----------------------#
# with torch.no_grad():
# # 옵티마이저 파라미터가 [img_tensor] 단 하나니 [0]번째 리스트임
# for param in self.optimizer_fn.param_groups[0]['params']:
# param.sub_(self.lr * param.grad) #이게 경사 하강법의 코드임
# grads_norm = param.grad.norm()
#--------------경사 하강법 수행의 low 코드-----------------------#
# #그래디언트의 norm값만 출력하기
grads_norm = 0.0 # 초기값 설정
for param in self.optimizer_fn.param_groups[0]['params']:
grad_norm = param.grad.norm().item()
grads_norm += grad_norm
img_tensor = img_tensor.detach() # 그래디언트 추적 중단
# 항목별 loss를 다시 계산하기 위해 그래디언트 추적이 중단된 상태에서 loss연산
outputs = self.net(img_tensor)
total_loss, loss_component = self.compute_loss(outputs)
loss_component.append(total_loss.item())
return img_tensor, loss_component, grads_norm
def gradient_descent_loop(self, task_tensor,
content_tensor, style_tensor,
num_steps=2500, sample_step = 100):
self.initailize(content_tensor, style_tensor)
pbar = tqdm(range(num_steps)) #tqdm라이브러리로 진행상황 체크
his_metrics = list() #Loss, grad값을 향후 분석그래프 그리려고 저장
for step in pbar: #num_steps 반복 횟수만큼 경사하강 수행
task_tensor, loss, grads_norm = self.gradient_decent_step(task_tensor)
#매 스탭마다 loss, grad를 저장
his_metrics.append([loss[2], loss[0], loss[1], grads_norm])
#tqdm에 추가 정보 표시하기
desc = (f"{step+1:04d}, "
f"[콘텐츠:{loss[0]*self.weight[0]:.2f}, "
f"스타일:{loss[1]*self.weight[1]:.2f}, "
f"tot:{loss[2]:.2f}], grad: {grads_norm:.4f}")
pbar.set_description(desc)
if (step+1) % sample_step == 0: #특정 스탭마다 중간 산출물 출력
res_img = deprocess_img(task_tensor, img_shape, imgNet_val)
name = f'combine_{step+1}'
res_img.save(f"{name}.jpg")
return task_tensor, his_metrics
첫번째 생성자 메서드에서 learning_rate
,
콘텐츠 로스 가중치(weight_c
)
스타일 로스 가중치(weight_s
)에 대한 기본값을 넣었다.
이는 실험과 조건을 다르게 하면서 조정해야 할 인자값이다.
(스타일 로스 가중치(weight_s
)는 적어도 콘텐츠 로스 가중치(weight_c
)보다는 천배 이상은 큰 값을 넣어야 그나마 잘 동작한다..)
두번째로 옵티마이저의 경우 일반적인 Adam
을 써도 되지만
Neural Style Transfer에는 더 적합한 옵티마이저인 L-BGFS
라는 최적화 방법론이 존재한다.
한국말로는 준-뉴턴 방식의 최적화 알고리즘.. 이라고 하는데
솔직히 필자는 이해를 못한 알고리즘이다
아무튼 Learning Rate는 인자로 필요하지 않으며,
L-BGFS
는 최적화를 수행하는데
1) 옵티마이저 초기화
2) 전사(forward)과정
3) Loss값 산출
4) Loss값 기반 역전파(backward)
위 1~4)과정을 내부에서 반복 수행해야 한다는 것이다.
따라서 L-BGFS
를 사용할 때는 1~4)과정에 해당하는 것을
def closure()
라는 함수로 묶어서 이를
.step(closure)
라는 메서드로 넘겨줘야 한다.
참고로 코드의 범용성을 위해 Adam
도 step()
메서드에
closure
함수를 인자로 받을 수 있다.
대신 Adam
은 closure
과정을 L-BGFS
와 다르게 1회 만 수행한다
라고 보면 된다.
마지막으로 경사하강법의 step을 반복수행하는 loop
함수에서는
반복 횟수인 num_steps
를 설정하는데
이게 옵티마이저를 Adam
, L-BGFS
둘 중 어느것을 쓰느냐에 따라 전체 반복횟수가 크게 차이가 난다.
L-BGFS
를 쓰면 더 적은 횟수만 돌려도 충분히 만족스러운 결과값을 얻을 수 있고
최종 결과 Task 이미지 도 더 훌륭하게 나온다.
그러나 1회 step를 수행하는데 소요되는 시간은 더 길기에
이것저것 고려하면 Adam
을 쓰나L-BGFS
을 쓰나 전체 소요시간은 비슷비슷했다.
그러나 최종 결과물은 L-BGFS
이 더 좋았다.
transfer_net = NeuralStyleNet(backbone, block_setting)
loss_fn = TransferLoss(block_setting)
N_S_T = StyleTransfer(transfer_net, loss_fn, block_setting,
weight_c = 1, weight_s = 1e6)
res_tensor, his_metrics = N_S_T.gradient_descent_loop(task_tensor, content_tensor, style_tensor,
num_steps=70, sample_step = 2)
전체적인
Neural Style Transfer의 수행은 위 코드를 사용하면 결과물을 확인할 수 있다.
위 사진처럼 noise이미지에서 Neural Style Transfer를 수행한 결과이다.
1) 아담 옵티마이저 사용 시
Adam
옵티마이저 사용 시 하이퍼 파라미터는
Learning Rate = 0.01
콘텐츠 손실 가중치 : 15, 스타일 손실 가중치 : 1e4
반복횟수(num_steps) : 2500
으로 설정한다.
2) L-BGFS 옵티마이저 사용 시
L-BGFS
옵티마이저 사용 시 하이퍼 파라미터는
콘텐츠 손실 가중치 : 15, 스타일 손실 가중치 : 1e4
반복횟수(num_steps) : 70
으로 설정한다.
위 4.1에서 알 수 있듯이 Task 이미지를 노이즈 이미지부터 시작하는건
Neural Style Transfer의 결과물이 그렇게 썩 만족스러운 편이 아니다.
# 콘텐츠 이미지의 사본으로 작업 수행해보기
task_tensor = content_tensor.clone()
그래서 Task 이미지를 content 이미지의 클론을 만들어서 작업을 수행한다.
결론만 이야기하면 이 경우가 결과물이 더 좋다.
1) 아담 옵티마이저 사용 시
Adam
옵티마이저 사용 시 하이퍼 파라미터는
Learning Rate = 0.01
콘텐츠 손실 가중치 : 1, 스타일 손실 가중치 : 1e6
반복횟수(num_steps) : 2500
으로 설정한다.
2) L-BGFS 옵티마이저 사용 시
L-BGFS
옵티마이저 사용 시 하이퍼 파라미터는
콘텐츠 손실 가중치 : 1, 스타일 손실 가중치 : 1e6
반복횟수(num_steps) : 70
으로 설정한다.
코드 수행결과를 보면 알알 수 있듯이
Task 이미지를 content 이미지의 클론버전으로 활용하면서 동시에 L-BGFS
옵티마이저로 수행하는게 가장 좋은 성능을 낸다.
작업한 코드의 결과물은
https://github.com/tbvjvsladla/metacodeM_pytorch_bootcamp/blob/main/Neural_style_transfer.ipynb
에 업로드를 하였다.
Neural Style Transfer 코드를 작성하면서 디버깅 하기 힘든 오류
1) .backward()
가 두번 이상 반복되어 수행되는 오류
2) 인스턴스화 한 transfer_net
이 전사과정에서 이미지가 갱신되도 feature out이 변동이 없는 경우
두개가 발생하면서 이것저것 에로사항이 많이 발생했는데
1)의 경우는 block의 feature out을 쓰는게 아니라
특정 레이어의 feature out를 캡쳐하게 변경
2)forward 함수에서 output = {}로 초기화를 꼭 반복해서 하기
등으로 오류를 잡을 수 있었다.
물론 오류잡는데 하루 이상 걸린건 함정