news 2026/2/15 19:59:59

生成式深度学习(神经风格迁移)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
生成式深度学习(神经风格迁移)

神经风格迁移

除DeepDream 之外,深度学习推动图像修改的另一项重大进展是神经风格迁移(neural
style transfer),它由Leon A. Gatys 等人于2015 年夏天提出a。自首次提出以来,神经风格迁移算
法已经做了很多改进,并衍生出许多变体,而且还成功转化为多款智能手机图片应用。为简单
起见,本节将重点介绍原始论文所描述的方法。

神经风格迁移是指将参考图像的风格应用于目标图像,同时保留目标图像的内容。图12-9
给出了一个示例。

这里所说的风格(style)是指图像中不同空间尺度的纹理、颜色和视觉图案,内容(content)
是指图像中更高层次的宏观结构。举个例子,在图12-9 中(参考图像是梵高的名作《星月夜》),
蓝黄色圆形笔触被视为风格,图宾根照片中的建筑则被视为内容。

风格迁移这一想法与纹理生成密切相关,在2015 年神经风格迁移出现之前,这一想法就已
经在图像处理领域拥有悠久的历史。但事实证明,与之前经典计算机视觉技术相比,基于深度学
习的风格迁移得到的效果是无可比拟的,并且再次引发人们对计算机视觉创造性应用的巨大兴趣。

实现风格迁移背后的关键概念与所有深度学习算法的核心思想是一样的:定义一个损失函
数来指定想实现的目标,然后将损失最小化。我们知道想实现的目标是什么,那就是保留原始
图像的内容,同时采用参考图像的风格。如果我们能够在数学上给出内容和风格的定义,那么
可以像下面这样定义损失函数并将损失最小化。

loss=(distance(style(reference_image)-style(combination_image))+distance(content(original_image)-content(combination_image)))

这里的distance 是一个范数函数,比如L2 范数;content 是一个函数,它接收一张图像作
为输入,并计算图像内容的表示;style 是一个函数,它接收一张图像作为输入,并计算图像风格
的表示。将这个损失最小化,会使得style(combination_image) 接近于style(reference_
image),并且content(combination_image) 接近于content(original_image),从而实现
我们定义的风格迁移。

Gatys 等人有一个很重要的发现,那就是深度卷积神经网络能够从数学上定义content 和
style 这两个函数。我们来看一下如何定义。

内容损失

如你所知,神经网络更靠近底部的层的激活值包含图像的局部信息,而更靠近顶部的层的
激活值则包含更加抽象的全局信息。卷积神经网络不同层的激活值,用另一种方式提供了图像
内容在不同空间尺度上的分解。因此,图像内容是更加全局、更加抽象的,应该能够被卷积神
经网络更靠近顶部的层的表示所捕捉。

因此,好的内容损失函数可以是两个激活值之间的L2 范数,其中一个激活值是预训练卷
积神经网络更靠近顶部的某一层在目标图像上计算得到的,另一个激活值是同一层在生成图像
上计算得到的。这可以确保,在更靠近顶部的层看来,生成图像与原始目标图像看起来很相似。
假设卷积神经网络更靠近顶部的层看到的就是输入图像的内容,那么利用这种方法可以保存图
像内容。

风格损失

内容损失只使用一个更靠近顶部的层,但Gatys 等人定义的风格损失则使用了卷积神经网
络的多个层。我们想获得卷积神经网络在所有空间尺度上从风格参考图像提取的外观,而不仅
仅是在单一尺度上。对于风格损失,Gatys 等人使用层激活的格拉姆矩阵(Gram matrix),即某
一层特征图的内积。这个内积可以看作表示该层特征之间相互关系的映射。这些相互关系抓住
了在某个空间尺度上的模式的统计规律。根据经验,它对应于在这个尺度上的纹理外观。

因此,风格损失的目的是在风格参考图像与生成图像之间,在不同的层激活内保留相似的
内部相互关系。反过来,这也保证了在风格参考图像与生成图像之间,不同空间尺度的纹理看
起来都很相似。

简而言之,你可以使用预训练卷积神经网络来定义一个损失函数,它具有以下特点。

  • 在原始图像与生成图像之间,让靠近顶部的层激活非常相似,从而保留内容。卷积神经
    网络应该将原始图像与生成图像“视为”包含相同的内容。
  • 在靠近顶部的层与靠近底部的层的激活中保持相似的相互关系,从而保留风格。特征相
    互关系捕捉到的是纹理,生成图像与风格参考图像在不同的空间尺度上应该具有相同的
    纹理。

接下来,我们用Keras 实现2015 年的原始神经风格迁移算法。你会发现,它与12.2 节介绍
的DeepDream 实现有许多相似之处。

用Keras 实现神经风格迁移

神经风格迁移可以用任意预训练卷积神经网络来实现。我们这里将使用Gatys 等人所使用
的VGG19 网络。VGG19 是第8 章介绍的VGG16 网络的简单变体,增加了3 个卷积层。

神经风格迁移的一般过程如下。

  • 创建一个神经网络,它能够同时计算风格参考图像、原始图像与生成图像的 VGG19 层
    激活。
  • 利用在这三张图像上计算的层激活来定义如前所述的损失函数。为了实现风格迁移,我
    们需要将这个损失函数最小化。
  • 设置梯度下降过程来将这个损失函数最小化。

我们首先给出风格参考图像与原始图像的路径,如代码清单12-16 所示。为了确保处理后
的图像具有相似的尺寸(如果图像尺寸差异很大,会使风格迁移变得更加困难),稍后需要将所
有图像的高度调整为400 像素。

代码清单12-16 获取风格图像和内容图像

内容图像如图12-10 所示,风格图像如图12-11 所示。

我们还需要一些辅助函数,用于对通过VGG19 网络的图像进行加载、预处理和后处理,如
代码清单12-17 所示。

代码清单12-17 辅助函数


下面构建VGG19 网络。与DeepDream 示例一样,我们将使用预训练卷积神经网络来创建
一个特征提取器模型,模型返回中间层的激活值(本例是指模型的所有层),如代码清单12-18
所示。

代码清单12-18 使用预训练VGG19 模型来创建一个特征提取器


下面来定义内容损失函数,如代码清单12-19 所示,它要保证在VGG19 网络靠近顶部的层
看来,内容图像和组合图像很相似。

代码清单12-19 内容损失函数

defcontent_loss(base_img,combination_img):returntf.reduce_sum(tf.square(combination_img-base_img))

接下来定义风格损失函数,如代码清单12-20 所示。它利用辅助函数来计算输入矩阵的格
拉姆矩阵,即原始特征矩阵中相互关系的映射。

代码清单12-20 风格损失函数

def gram_matrix(x): x = tf.transpose(x, (2, 0, 1)) features = tf.reshape(x, (tf.shape(x)[0], -1)) gram = tf.matmul(features, tf.transpose(features)) return gram def style_loss(style_img, combination_img): S = gram_matrix(style_img) C = gram_matrix(combination_img) channels = 3 size = img_height * img_width return tf.reduce_sum(tf.square(S - C)) / (4.0 * (channels ** 2) * (size ** 2))

除了这两个损失分量,我们还需要添加第3 个分量——总变差损失(total variation loss),它
对生成的组合图像的像素进行操作,如代码清单12-21 所示。它促使生成图像具有空间连续性,
从而避免得到过度像素化的结果。你可以将它理解为正则化损失。

代码清单12-21 总变差损失函数

deftotal_variation_loss(x):a=tf.square(x[:,:img_height-1,:img_width-1,:]-x[:,1:,:img_width-1,:])b=tf.square(x[:,:img_height-1,:img_width-1,:]-x[:,:img_height-1,1:,:])returntf.reduce_sum(tf.pow(a+b,1.25))

我们要最小化的损失值是这3 项损失的加权平均值,如代码清单12-22 所示。为了计算内
容损失,我们只使用一个靠近顶部的层,即block5_conv2 层;对于风格损失,我们需要使用
一个层列表,其中既包括靠近顶部的层,也包括靠近底部的层;最后还要添加总变差损失。

根据所使用的风格参考图像和内容图像,可能还需要调节content_weight 系数(内容损
失对总损失的贡献比例)。较大的content_weight 表示目标内容更容易在生成图像中被识别
出来。

代码清单12-22 定义需要最小化的最终损失函数


最后,我们来设置梯度下降过程,如代码清单12-23 所示。在Gatys 等人的原始论文中,优
化是通过L-BFGS 算法实现的,但这种算法在TensorFlow 中不可用,所以我们只能用SGD 优化
器做小批量梯度下降。我们将使用一个之前没有见过的优化器功能:学习率计划。利用这个功能,
我们将学习率从一个非常大的值(100)逐渐减小到一个很小的最终值(约为20)。这样一来,
我们可以在训练初期取得快速进展,然后在接近损失最小值时更加谨慎地前进。

代码清单12-23 设置梯度下降过程


得到的结果如图12-12 所示。请记住,这种技术只能改变图像纹理,即纹理迁移。如果风
格参考图像具有明显的纹理结构且高度自相似,并且内容目标不需要高层次细节就能够被识别,
那么这种方法的效果最好。它通常无法实现比较抽象的迁移,比如将一幅肖像的风格迁移到另
一幅肖像中。这种算法更接近于经典的信号处理,而不是更接近于人工智能,所以不要指望它
能实现魔法般的效果。

此外请注意,这种风格迁移算法的运行速度很慢。但它的变换非常简单,只要拥有适量的训
练数据,一个小型的快速前馈卷积神经网络就可以学会这种变换。因此,实现快速风格迁移的方
法是:首先利用这里介绍的方法,花费大量计算时间对固定的风格参考图像生成许多输入− 输出
训练示例,然后训练一个简单的卷积神经网络来学习这种特定风格的变换。一旦完成之后,对
一张图像进行风格迁移是非常快的,只需对这个小型卷积神经网络运行一次前向传播。


完整代码

import numpy as np import tensorflow as tf from tensorflow import keras from tensorflow.keras.applications import vgg19 from PIL import Image import time # 配置GPU内存增长(可选) gpus = tf.config.experimental.list_physical_devices('GPU') if gpus: try: for gpu in gpus: tf.config.experimental.set_memory_growth(gpu, True) except RuntimeError as e: print(e) # 图像路径 style_image_path = "vangogh_starry_night.jpg" # 风格图像路径 content_image_path = "san_francisco.jpg" # 内容图像路径 # 图像尺寸设置 img_width = 400 img_height = 400 # ==================== # 辅助函数 # ==================== def preprocess_image(image_path): """加载图像并进行预处理""" img = keras.utils.load_img(image_path, target_size=(img_height, img_width)) img = keras.utils.img_to_array(img) img = np.expand_dims(img, axis=0) img = vgg19.preprocess_input(img) return img def deprocess_image(x): """将图像后处理为可显示格式""" x = x.reshape((img_height, img_width, 3)) # 移除零中心(VGG19预处理的反操作) x[:, :, 0] += 103.939 x[:, :, 1] += 116.779 x[:, :, 2] += 123.68 # BGR -> RGB x = x[:, :, ::-1] x = np.clip(x, 0, 255).astype("uint8") return x # ==================== # 加载和预处理图像 # ==================== # 加载风格图像和内容图像 style_image = preprocess_image(style_image_path) content_image = preprocess_image(content_image_path) # 生成初始图像(从内容图像开始) combination_image = tf.Variable(preprocess_image(content_image_path)) # ==================== # 构建VGG19特征提取器 # ==================== # 加载预训练的VGG19模型 model = vgg19.VGG19(weights="imagenet", include_top=False) # 获取特定层的输出 layer_names = [ "block1_conv1", "block2_conv1", "block3_conv1", "block4_conv1", "block5_conv1", "block5_conv2" # 内容层 ] outputs_dict = {layer.name: layer.output for layer in model.layers if layer.name in layer_names} feature_extractor = keras.Model(inputs=model.inputs, outputs=outputs_dict) # 冻结模型权重 model.trainable = False # ==================== # 损失函数定义 # ==================== def content_loss(base_img, combination_img): """内容损失函数""" return tf.reduce_sum(tf.square(combination_img - base_img)) def gram_matrix(x): """计算格拉姆矩阵""" x = tf.transpose(x, (2, 0, 1)) features = tf.reshape(x, (tf.shape(x)[0], -1)) gram = tf.matmul(features, tf.transpose(features)) return gram def style_loss(style_img, combination_img): """风格损失函数""" S = gram_matrix(style_img) C = gram_matrix(combination_img) channels = 3 size = img_height * img_width return tf.reduce_sum(tf.square(S - C)) / (4.0 * (channels ** 2) * (size ** 2)) def total_variation_loss(x): """总变差损失函数(用于平滑图像)""" a = tf.square( x[:, :img_height-1, :img_width-1, :] - x[:, 1:, :img_width-1, :] ) b = tf.square( x[:, :img_height-1, :img_width-1, :] - x[:, :img_height-1, 1:, :] ) return tf.reduce_sum(tf.pow(a + b, 1.25)) # ==================== # 计算损失 # ==================== # 定义权重 content_weight = 1e4 style_weight = 1e-2 total_variation_weight = 1e-4 def compute_loss(combination_image, content_image, style_image): """计算总损失""" # 组合三张图像为一批次 input_tensor = tf.concat([content_image, style_image, combination_image], axis=0) # 提取特征 features = feature_extractor(input_tensor) # 初始化损失 loss = tf.zeros(()) # 内容损失(使用block5_conv2层) layer_features = features["block5_conv2"] content_image_features = layer_features[0, :, :, :] combination_features = layer_features[2, :, :, :] loss = loss + content_weight * content_loss( content_image_features, combination_features ) # 风格损失(使用多个层) style_layer_names = [ "block1_conv1", "block2_conv1", "block3_conv1", "block4_conv1", "block5_conv1" ] for layer_name in style_layer_names: layer_features = features[layer_name] style_features = layer_features[1, :, :, :] combination_features = layer_features[2, :, :, :] sl = style_loss(style_features, combination_features) loss += (style_weight / len(style_layer_names)) * sl # 总变差损失 loss += total_variation_weight * total_variation_loss(combination_image) return loss # ==================== # 设置优化过程 # ==================== # 创建优化器 optimizer = keras.optimizers.SGD( keras.optimizers.schedules.ExponentialDecay( initial_learning_rate=100.0, decay_steps=100, decay_rate=0.96 ) ) # 保存损失历史 loss_history = [] # ==================== # 训练循环 # ==================== @tf.function def train_step(combination_image, content_image, style_image): """单次训练步骤""" with tf.GradientTape() as tape: loss = compute_loss(combination_image, content_image, style_image) grads = tape.gradient(loss, combination_image) optimizer.apply_gradients([(grads, combination_image)]) # 限制像素值范围 combination_image.assign(tf.clip_by_value(combination_image, -127.5, 127.5)) return loss # 训练参数 epochs = 4000 save_interval = 100 print("开始神经风格迁移训练...") print(f"总迭代次数: {epochs}") print(f"内容权重: {content_weight}") print(f"风格权重: {style_weight}") print(f"总变差权重: {total_variation_weight}") print("-" * 50) start_time = time.time() for epoch in range(1, epochs + 1): loss = train_step(combination_image, content_image, style_image) loss_history.append(loss.numpy()) if epoch % save_interval == 0 or epoch == 1: elapsed_time = time.time() - start_time print(f"迭代 {epoch:4d}/{epochs} - 损失: {loss.numpy():.2f} - 时间: {elapsed_time:.1f}s") # 保存中间结果 img = deprocess_image(combination_image.numpy()) keras.utils.save_img(f"style_transfer_result_{epoch}.png", img) # ==================== # 保存最终结果 # ==================== print("\n训练完成!") print(f"总训练时间: {time.time() - start_time:.1f}秒") # 保存最终结果 final_image = deprocess_image(combination_image.numpy()) Image.fromarray(final_image).save("style_transfer_final_result.png") # 显示最终图像 Image.fromarray(final_image).show() # ==================== # 绘制损失曲线(可选) # ==================== import matplotlib.pyplot as plt plt.figure(figsize=(10, 6)) plt.plot(loss_history) plt.title('训练损失曲线') plt.xlabel('迭代次数') plt.ylabel('损失') plt.grid(True) plt.savefig('loss_curve.png') plt.show() print("\n最终结果已保存为:") print("1. style_transfer_final_result.png - 最终风格迁移结果") print("2. style_transfer_result_XXX.png - 中间结果") print("3. loss_curve.png - 损失曲线")
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/13 0:10:59

基于微信小程序的社区智慧养老系统毕业设计源码

博主介绍:✌ 专注于Java,python,✌关注✌私信我✌具体的问题,我会尽力帮助你。一、研究目的本研究旨在设计并实现一个基于微信小程序的社区智慧养老系统,以满足我国老龄化社会背景下养老服务的需求。具体研究目的如下:构建一个全面…

作者头像 李华
网站建设 2026/2/10 6:29:05

【DevOps效率革命】:利用Docker Buildx实现极致镜像压缩

第一章:DevOps效率革命的起点在现代软件交付体系中,DevOps 已成为提升开发与运维协同效率的核心实践。它打破了传统“开发完成即交付”的孤岛模式,通过自动化流程、持续反馈和文化变革,实现从代码提交到生产部署的快速、可靠流转。…

作者头像 李华
网站建设 2026/2/16 3:52:50

四步破局:CTF解题思维链与12周从入门到实战的进阶指南

CTF(Capture The Flag)作为网络安全领域的实战型竞赛,是检验安全技术、锻炼攻防思维的核心平台。对于新手而言,盲目刷题易陷入“只见树木不见森林”的困境,而掌握科学的解题思维链系统的进阶路径,能快速实现…

作者头像 李华
网站建设 2026/2/12 15:24:40

24、系统管理脚本实用指南

系统管理脚本实用指南 在系统管理的日常操作中,我们常常会遇到诸如定时任务管理、数据库读写、用户管理以及图像批量处理等任务。本文将详细介绍如何使用脚本完成这些常见的系统管理任务,包括移除定时任务表、读写 MySQL 数据库、用户管理和批量图像调整大小与格式转换。 1…

作者头像 李华
网站建设 2026/2/4 13:15:40

EmotiVoice语音合成在音乐剧配音中的创造性应用

EmotiVoice语音合成在音乐剧配音中的创造性应用 在一场即将上演的原创音乐剧中,导演需要为主角录制一段充满悲愤情绪的独白:“你竟用谎言将我推入深渊!”然而,原定配音演员突发疾病无法进棚。时间紧迫,重找声优成本高…

作者头像 李华
网站建设 2026/2/15 11:55:37

Spring Boot性能调优

一、先搞懂:性能瓶颈都藏在哪里?性能调优的前提是精准定位瓶颈,盲目修改配置只会事倍功半。Spring Boot应用的性能问题主要集中在四个层面,可通过“日志分析监控工具”组合排查:接入层瓶颈:内嵌Tomcat/Jett…

作者头像 李华