Main Content

本页翻译不是最新的。点击此处可查看最新英文版本。

使用深度学习的神经样式迁移

此示例说明如何使用预训练的 VGG-19 网络将一个图像的样式外观应用于另一个图像的场景内容。

加载数据

加载样式图像和内容图像。此示例使用梵高的名作《星夜》作为样式图像,使用一个灯塔照片作为内容图像。

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

以蒙太奇方式显示样式图像和内容图像。

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

加载特征提取网络

在此示例中,您使用一个经过修改的预训练 VGG-19 深度神经网络在不同层提取内容图像和样式图像的特征。这些多层特征用于计算各自的内容和样式损失。该网络使用组合损失生成样式化的迁移图像。

要获得一个预训练 VGG-19 网络,请安装 vgg19 (Deep Learning Toolbox)。如果没有安装所需的支持包,软件会提供下载链接。

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

为内容损失和样式损失指定权重因子 alphabetaalphabeta 的比率应在 1e-3 或 1e-4 左右 [1]

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

指定训练选项

进行 2500 次迭代的训练。

numIterations = 2500;

指定 Adam 优化的选项。将学习率设置为 2 以加快收敛速度。您可以通过观察输出图像和损失来尝试调整学习率。用 [] 初始化尾部平均梯度和尾部平均梯度平方衰减率。

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

训练网络

将样式图像、内容图像和迁移图像转换为基础类型为 single 和维度标签为 "SSC"dlarray (Deep Learning Toolbox) 对象。

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

在 GPU 上(如果有)进行训练。使用 GPU 需要 Parallel Computing Toolbox™ 和支持 CUDA® 的 NVIDIA® GPU。有关详细信息,请参阅GPU Computing Requirements (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 (Deep Learning Toolbox) 函数更新迁移图像。

  • 选择最佳样式迁移图像作为最终输出图像。

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

计算格拉姆矩阵

styleLoss 辅助函数使用 calculateGramMatrix 辅助函数来计算特征图的格拉姆矩阵。

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

另请参阅

(Deep Learning Toolbox) | (Deep Learning Toolbox) | (Deep Learning Toolbox) | (Deep Learning Toolbox)

相关主题