Main Content

딥러닝을 사용한 신경망 스타일 전이

이 예제에서는 사전 훈련된 VGG-19 신경망을 사용하여 한 영상의 스타일을 두 번째 영상의 장면 콘텐츠에 적용하는 방법을 보여줍니다.

데이터 불러오기

스타일 영상과 콘텐츠 영상을 불러옵니다. 이 예제에서는 고흐의 유명한 그림 "별이 빛나는 밤"을 스타일 영상으로 사용하고 등대 사진을 콘텐츠 영상으로 사용합니다.

styleImage = im2double(imread("starryNight.jpg"));
contentImage = imread("lighthouse.png");

스타일 영상과 콘텐츠 영상을 몽타주 형태로 표시합니다.

imshow(imtile({styleImage,contentImage},BackgroundColor="w"));

특징 추출 신경망 불러오기

이 예제에서는 수정된 사전 훈련된 VGG-19 심층 신경망을 사용하여 여러 계층에서 콘텐츠 및 스타일 영상의 특징을 추출합니다. 이러한 다층 특징은 각각의 콘텐츠 및 스타일 손실을 계산하는 데 사용됩니다. 신경망은 결합된 손실을 사용하여 스타일이 적용된 전이 영상을 생성합니다.

시전 훈련된 VGG-19 신경망을 가져오려면 vgg19를 설치하십시오. 필요한 지원 패키지가 설치되어 있지 않으면 이를 다운로드할 수 있는 링크가 제공됩니다.

net = vgg19;

VGG-19 신경망을 특징 추출에 적합하게 만들려면 신경망에서 모든 완전 연결 계층을 제거합니다.

lastFeatureLayerIdx = 38;
layers = net.Layers;
layers = layers(1:lastFeatureLayerIdx);

VGG-19 신경망의 최댓값 풀링 계층으로 인해 페이딩 효과가 발생합니다. 페이딩 효과를 줄이고 기울기 흐름을 향상하려면 모든 최댓값 풀링 계층을 평균값 풀링 계층으로 바꾸십시오[1].

for l = 1:lastFeatureLayerIdx
    layer = layers(l);
    if isa(layer,"nnet.cnn.layer.MaxPooling2DLayer")
        layers(l) = averagePooling2dLayer(layer.PoolSize,Stride=layer.Stride,Name=layer.Name);
    end
end

수정된 계층으로 계층 그래프를 만듭니다.

lgraph = layerGraph(layers);

특징 추출 신경망을 플롯으로 시각화합니다.

plot(lgraph)
title("Feature Extraction Network")

사용자 지정 훈련 루프를 사용하여 신경망을 훈련시키고 자동 미분을 활성화하기 위해 계층 그래프를 dlnetwork 객체로 변환합니다.

dlnet = dlnetwork(lgraph);

데이터 전처리하기

처리 속도를 높이기 위해 스타일 영상과 콘텐츠 영상의 크기를 작게 조정합니다.

imageSize = [384,512];
styleImg = imresize(styleImage,imageSize);
contentImg = imresize(contentImage,imageSize);

사전 훈련된 VGG-19 신경망은 채널별 평균 감산 영상에 대해 분류를 수행합니다. 신경망의 첫 번째 계층인 영상 입력 계층에서 채널별 평균을 가져옵니다.

imgInputLayer = lgraph.Layers(1);
meanVggNet = imgInputLayer.Mean(1,1,:);

채널별 평균의 값은 픽셀 값이 [0, 255] 범위 내에 있는 부동소수점 데이터형으로 구성된 영상에 적합합니다. [0, 255] 범위를 사용하여 스타일 영상과 콘텐츠 영상을 데이터형 single로 변환합니다. 그런 다음 스타일 영상과 콘텐츠 영상에서 채널별 평균을 뺍니다.

styleImg = rescale(single(styleImg),0,255) - meanVggNet;
contentImg = rescale(single(contentImg),0,255) - meanVggNet;

전이 영상 초기화하기

전이 영상은 스타일 전이의 결과로 생성되는 출력 영상입니다. 전이 영상은 스타일 영상, 콘텐츠 영상 또는 임의의 영상으로 초기화할 수 있습니다. 스타일 영상이나 콘텐츠 영상으로 초기화하면 스타일 전이 절차에서 편향이 발생하여 입력 영상과 더 비슷한 전이 영상이 생성됩니다. 반면에 백색 잡음으로 초기화하면 편향이 제거되지만 스타일이 적용된 영상에서 수렴하는 데 더 오래 걸립니다. 이 예제에서는 향상된 스타일 적용과 더 빠른 수렴을 위해 출력 전이 영상을 콘텐츠 영상과 백색 잡음 영상의 가중 조합으로 초기화합니다.

noiseRatio = 0.7;
randImage = randi([-20,20],[imageSize 3]);
transferImage = noiseRatio.*randImage + (1-noiseRatio).*contentImg;

손실 함수와 스타일 전이 파라미터 정의하기

콘텐츠 손실

콘텐츠 손실의 목적은 전이 영상의 특징이 콘텐츠 영상의 특징과 일치하게 하는 것입니다. 콘텐츠 손실은 각 콘텐츠 특징 계층에 대해 콘텐츠 영상 특징과 전이 영상 특징 간의 평균 제곱 차이로 계산됩니다[1]. Yˆ는 전이 영상에 대해 예측된 특징 맵이고, Y는 콘텐츠 영상에 대해 예측된 특징 맵입니다. Wcllth 계층에 대한 콘텐츠 계층 가중치입니다. H,W,C는 각각 특징 맵의 높이, 너비, 채널입니다.

Lcontent=lWcl×1HWCi,j(Yˆi,jl-Yi,jl)2

콘텐츠 특징 추출 계층 이름을 지정합니다. 이러한 계층에서 추출된 특징은 콘텐츠 손실을 계산하는 데 사용됩니다. VGG-19 신경망에서, 얕은 계층의 특징을 사용하여 훈련시키는 것보다 깊은 계층의 특징을 사용하여 훈련시키는 것이 더 효과적입니다. 따라서 콘텐츠 특징 추출 계층을 네 번째 컨벌루션 계층으로 지정합니다.

styleTransferOptions.contentFeatureLayerNames = "conv4_2";

콘텐츠 특징 추출 계층의 가중치를 지정합니다.

styleTransferOptions.contentFeatureLayerWeights = 1;

스타일 손실

스타일 손실의 목적은 전이 영상의 텍스처가 스타일 영상의 텍스처와 일치하게 하는 것입니다. 영상의 스타일 표현은 그람 행렬로 표현됩니다. 따라서 스타일 손실은 스타일 영상의 그람 행렬과 전이 영상의 그람 행렬 간의 평균 제곱 차이로 계산됩니다[1]. ZZˆ는 각각 스타일 영상과 전이 영상에 대해 예측된 특징 맵입니다. GZGZˆ는 각각 스타일 특징과 전이 특징에 대한 그람 행렬입니다. Wsllth 스타일 계층에 대한 스타일 계층 가중치입니다.

GZˆ=i,jZˆi,j×Zˆj,i

GZ=i,jZi,j×Zj,i

Lstyle=lWsl×1(2HWC)2(GZˆl-GZl)2

스타일 특징 추출 계층의 이름을 지정합니다. 이러한 계층에서 추출된 특징은 스타일 손실을 계산하는 데 사용됩니다.

styleTransferOptions.styleFeatureLayerNames = ["conv1_1","conv2_1","conv3_1","conv4_1","conv5_1"];

스타일 특징 추출 계층의 가중치를 지정합니다. 단순한 스타일의 영상에 대해서는 작은 가중치를 지정하고, 복잡한 스타일의 영상에 대해서는 가중치를 늘립니다.

styleTransferOptions.styleFeatureLayerWeights = [0.5,1.0,1.5,3.0,4.0];

총 손실

총 손실은 콘텐츠 손실과 스타일 손실의 가중 조합입니다. αβ는 각각 콘텐츠 손실과 스타일 손실에 대한 가중치 인자입니다.

Ltotal=α×Lcontent+β×Lstyle

콘텐츠 손실과 스타일 손실에 대한 가중치 인자 alphabeta를 지정합니다. alphabeta의 비율은 약 1e-3 또는 1e-4이 됩니다[1].

styleTransferOptions.alpha = 1; 
styleTransferOptions.beta = 1e3;

훈련 옵션 지정하기

훈련을 2500번 수행합니다.

numIterations = 2500;

Adam 최적화에 대한 옵션을 지정합니다. 빠른 수렴을 위해 학습률을 2로 설정합니다. 출력 영상과 손실을 관찰하면서 여러 학습률을 실험해 볼 수 있습니다. 후행 평균 기울기 감쇠율과 후행 평균 기울기 제곱 감쇠율을 []로 초기화합니다.

learningRate = 2;
trailingAvg = [];
trailingAvgSq = [];

신경망 훈련시키기

스타일 영상, 콘텐츠 영상, 전이 영상을 기본 유형 single과 차원 레이블 "SSC"를 사용하여 dlarray 객체로 변환합니다.

dlStyle = dlarray(styleImg,"SSC");
dlContent = dlarray(contentImg,"SSC");
dlTransfer = dlarray(transferImage,"SSC");

사용 가능한 GPU가 있으면 GPU에서 훈련시킵니다. GPU를 사용하려면 Parallel Computing Toolbox™와 CUDA® 지원 NVIDIA® GPU가 필요합니다. 자세한 내용은 릴리스별 GPU 지원 (Parallel Computing Toolbox) 항목을 참조하십시오. GPU 훈련을 위해 데이터를 gpuArray로 변환합니다.

if canUseGPU
    dlContent = gpuArray(dlContent);
    dlStyle = gpuArray(dlStyle);
    dlTransfer = gpuArray(dlTransfer);
end

콘텐츠 영상에서 콘텐츠 특징을 추출합니다.

numContentFeatureLayers = numel(styleTransferOptions.contentFeatureLayerNames);
contentFeatures = cell(1,numContentFeatureLayers);
[contentFeatures{:}] = forward(dlnet,dlContent,Outputs=styleTransferOptions.contentFeatureLayerNames);

스타일 영상에서 스타일 특징을 추출합니다.

numStyleFeatureLayers = numel(styleTransferOptions.styleFeatureLayerNames);
styleFeatures = cell(1,numStyleFeatureLayers);
[styleFeatures{:}] = forward(dlnet,dlStyle,Outputs=styleTransferOptions.styleFeatureLayerNames);

사용자 지정 훈련 루프를 사용하여 모델을 훈련시킵니다. 각 반복에 대해 다음을 수행합니다.

  • 콘텐츠 영상, 스타일 영상, 전이 영상의 특징을 사용하여 콘텐츠 손실과 스타일 손실을 계산합니다. 손실과 기울기를 계산하려면 (이 예제의 지원 함수 섹션에 정의된) 헬퍼 함수 imageGradients를 사용하십시오.

  • adamupdate 함수를 사용하여 전이 영상을 업데이트합니다.

  • 가장 좋은 스타일 전이 영상을 최종 출력 영상으로 선택합니다.

figure

minimumLoss = inf;

for iteration = 1:numIterations
    % Evaluate the transfer image gradients and state using dlfeval and the
    % imageGradients function listed at the end of the example
    [grad,losses] = dlfeval(@imageGradients,dlnet,dlTransfer,contentFeatures,styleFeatures,styleTransferOptions);
    [dlTransfer,trailingAvg,trailingAvgSq] = adamupdate(dlTransfer,grad,trailingAvg,trailingAvgSq,iteration,learningRate);
  
    if losses.totalLoss < minimumLoss
        minimumLoss = losses.totalLoss;
        dlOutput = dlTransfer;        
    end   
    
    % Display the transfer image on the first iteration and after every 50
    % iterations. The postprocessing steps are described in the "Postprocess
    % Transfer Image for Display" section of this example
    if mod(iteration,50) == 0 || (iteration == 1)
        
        transferImage = gather(extractdata(dlTransfer));
        transferImage = transferImage + meanVggNet;
        transferImage = uint8(transferImage);
        transferImage = imresize(transferImage,size(contentImage,[1 2]));
        
        image(transferImage)
        title(["Transfer Image After Iteration ",num2str(iteration)])
        axis off image
        drawnow
    end   
    
end

표시를 위해 전이 영상 후처리하기

업데이트된 전이 영상을 가져옵니다.

transferImage = gather(extractdata(dlOutput));

신경망으로 훈련된 평균을 전이 영상에 더합니다.

transferImage = transferImage + meanVggNet;

일부 픽셀 값은 콘텐츠 영상과 스타일 영상의 원래 범위 [0, 255]를 초과할 수 있습니다. 데이터형을 uint8로 변환하여 값을 [0, 255] 범위로 자를 수 있습니다.

transferImage = uint8(transferImage);

전이 영상의 크기를 콘텐츠 영상의 원래 크기로 조정합니다.

transferImage = imresize(transferImage,size(contentImage,[1 2]));

콘텐츠 영상, 전이 영상, 스타일 영상을 몽타주 형태로 표시합니다.

imshow(imtile({contentImage,transferImage,styleImage}, ...
    GridSize=[1 3],BackgroundColor="w"));

지원 함수

영상 손실과 기울기 계산하기

imageGradients 헬퍼 함수는 콘텐츠 영상, 스타일 영상, 전이 영상의 특징을 사용하여 손실과 기울기를 반환합니다.

function [gradients,losses] = imageGradients(dlnet,dlTransfer,contentFeatures,styleFeatures,params)
 
    % Initialize transfer image feature containers
    numContentFeatureLayers = numel(params.contentFeatureLayerNames);
    numStyleFeatureLayers = numel(params.styleFeatureLayerNames);
 
    transferContentFeatures = cell(1,numContentFeatureLayers);
    transferStyleFeatures = cell(1,numStyleFeatureLayers);
 
    % Extract content features of transfer image
    [transferContentFeatures{:}] = forward(dlnet,dlTransfer,Outputs=params.contentFeatureLayerNames);
     
    % Extract style features of transfer image
    [transferStyleFeatures{:}] = forward(dlnet,dlTransfer,Outputs=params.styleFeatureLayerNames);
 
    % Calculate content loss
    cLoss = contentLoss(transferContentFeatures,contentFeatures,params.contentFeatureLayerWeights);
 
    % Calculate style loss
    sLoss = styleLoss(transferStyleFeatures,styleFeatures,params.styleFeatureLayerWeights);
 
    % Calculate final loss as weighted combination of content and style loss 
    loss = (params.alpha * cLoss) + (params.beta * sLoss);
 
    % Calculate gradient with respect to transfer image
    gradients = dlgradient(loss,dlTransfer);
    
    % Extract various losses
    losses.totalLoss = gather(extractdata(loss));
    losses.contentLoss = gather(extractdata(cLoss));
    losses.styleLoss = gather(extractdata(sLoss));
 
end

콘텐츠 손실 계산하기

contentLoss 헬퍼 함수는 콘텐츠 영상 특징과 전이 영상 특징 간의 가중 평균 제곱 차이를 계산합니다.

function loss = contentLoss(transferContentFeatures,contentFeatures,contentWeights)

    loss = 0;
    for i=1:numel(contentFeatures)
        temp = 0.5 .* mean((transferContentFeatures{1,i} - contentFeatures{1,i}).^2,"all");
        loss = loss + (contentWeights(i)*temp);
    end
end

스타일 손실 계산

styleLoss 헬퍼 함수는 스타일 영상 특징의 그람 행렬과 전이 영상 특징의 그람 행렬 간의 가중 평균 제곱 차이를 계산합니다.

function loss = styleLoss(transferStyleFeatures,styleFeatures,styleWeights)

    loss = 0;
    for i=1:numel(styleFeatures)
        
        tsf = transferStyleFeatures{1,i};
        sf = styleFeatures{1,i};    
        [h,w,c] = size(sf);
        
        gramStyle = calculateGramMatrix(sf);
        gramTransfer = calculateGramMatrix(tsf);
        sLoss = mean((gramTransfer - gramStyle).^2,"all") / ((h*w*c)^2);
        
        loss = loss + (styleWeights(i)*sLoss);
    end
end

그람 행렬 계산하기

calculateGramMatrix 헬퍼 함수는 styleLoss 헬퍼 함수가 특징 맵의 그람 행렬을 계산하는 용도로 사용합니다.

function gramMatrix = calculateGramMatrix(featureMap)
    [H,W,C] = size(featureMap);
    reshapedFeatures = reshape(featureMap,H*W,C);
    gramMatrix = reshapedFeatures' * reshapedFeatures;
end

참고 문헌

[1] Leon A. Gatys, Alexander S. Ecker, and Matthias Bethge. "A Neural Algorithm of Artistic Style." Preprint, submitted September 2, 2015. https://arxiv.org/abs/1508.06576

참고 항목

| | |

관련 항목