尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

TensorFlow 2.x风格迁移实战:VGG19特征与Gram矩阵详解

TensorFlow 2.x风格迁移实战:VGG19特征与Gram矩阵详解 简介这份资源是通过TensorFlow实现图像风格迁移的Python实战项目目标读者是人工智能、深度学习领域中希望亲自实践风格迁移算法的学习者。项目思路明确将一张图片的风格迁移到另一张图片上且训练时间只需几分钟适合在本地环境快速验证效果。资源包共18个文件主要包括1个Python核心代码、14张用于内容和风格输入的JPG图片、项目README及说明文档压缩后整体仅1MB下载与运行都很便捷。代码中已考虑TensorFlow、NumPy、SciPy、Pillow等依赖并配套VGG-19预训练模型的使用说明模型需自行下载并放入vgg_para目录。通过该资源学习者可以直观对比不同内容图与风格图的迁移效果理解神经风格迁移的特征重构与风格重构机制。目前已有397人浏览学习适合作为风格迁移入门与调参练习的轻量参考。1. 风格迁移不是滤镜它到底在优化什么把一张普通照片改造成梵高星空这类任务看起来像滤镜但真正的风格迁移不是像素叠加而是让目标图片在卷积网络的高层特征上靠近内容图、在低层纹理统计量上靠近风格图。我见过不少朋友下载别人写好的风格迁移脚本换了照片就出噪点或变得面目全非根本原因是看不懂损失函数、也调不动参数。这篇文章用 TensorFlow 2.x 加 Python 自带 Keras 实现一版可独立运行的风格迁移代码把 VGG 特征抽取、Gram 矩阵、内容损失与风格损失的组合方式讲透再给出可直接修改的参数表。适合想真正改代码而不是只跑通 demo 的人初学也能跟着步骤落地。2. 从 VGG19 特征图到 Gram 矩阵风格迁移的理论落点2.1 为什么风格迁移必须用预训练 CNN风格迁移的数学问题是给出一张内容图 C 和一张风格图 S找到一个输出图 X使 X 在与 C 的深层特征距离上尽量小同时 X 的风格统计量与 S 的风格统计量尽量接近。这里“特征”不能直接取像素否则退化成一堆卷积滤波无法把“画面里有什么”和“画面用笔触画出来”分开。常见做法是用在 ImageNet 上预训练过的 VGG19 作为特征提取器。VGG19 之所以成为默认选择不是因为它懂艺术而是它分类时需要同时编码“物体形状”和“表面纹理”这两类信息恰好分布在不同的卷积层中。底层卷积核学到的是边缘、颜色、小纹样高层卷积核学到的是轮廓、部件和整体结构。把 VGG 固定住不微调只把图片当成可优化变量就构成了一个特征空间下的约束优化问题。提示VGG19 在 TensorFlow 中通过 Keras 应用模块直接加载不需要自己搭网络权重首次运行时会自动下载到本地缓存目录。2.2 用 Keras 抽取内容层与风格层在 TensorFlow 2.x 中一个标准的做法是通过tf.keras.applications.VGG19拿到不含全连接层的卷积部分再根据层名抽取特征。下面这段代码可以直接放进训练脚本的开头import tensorflow as tf def build_extractor(img_height, img_width): vgg tf.keras.applications.VGG19( include_topFalse, weightsimagenet, input_shape(img_height, img_width, 3) ) vgg.trainable False content_layers [block4_conv2] style_layers [ block1_conv1, block2_conv1, block3_conv1, block4_conv1, block5_conv1, ] outputs [vgg.get_layer(name).output for name in content_layers style_layers] extractor tf.keras.Model(vgg.input, outputs) return extractor, content_layers, style_layers extractor, content_layers, style_layers build_extractor(512, 512)这段代码的关键点有三个。第一include_topFalse必须保留否则权重文件里会包含分类头输入尺寸也被锁死在 224。第二input_shape自定义为 512×512方便处理高清内容图但要留意显存占用。第三trainable False表示卷积层的权重完全不参与训练优化器只更新目标图片的像素。用途VGG 内层名经验上的特征作用内容约束block4_conv2物体轮廓、结构布局风格约束block1_conv1极细边缘、局部颜色分布风格约束block2_conv1短纹理、笔触方向风格约束block3_conv1中等尺寸花纹风格约束block4_conv1大块纹理区域风格约束block5_conv1全局排布、场景风格内容层只取一层且位置偏高是因为深层特征对物体语义响应更强更不容易被风格带偏。风格层取五个不同深度是为了同时约束“笔触粗细”和“整体颜色倾向”。这两类层在下方代码里得到的输出顺序是第 0 个是内容特征后面 5 个依次是风格特征。2.3 用 Gram 矩阵给“风格”一个可计算的数字风格不能靠某个通道的激活值大小来定义常见做法是使用 Gram 矩阵把特征图从(h, w, c)展平为(h*w, c)计算其转置矩阵与自身的乘积得到一个(c, c)矩阵。这个矩阵统计了不同卷积核在空间位置上的共现强度表达的是“这个通道的激活和那个通道的激活是否喜欢同时出现”。def gram_matrix(feature_map): b tf.shape(feature_map)[0] h tf.shape(feature_map)[1] w tf.shape(feature_map)[2] c tf.shape(feature_map)[3] features tf.reshape(feature_map, (b, h * w, c)) gram tf.matmul(features, features, transpose_aTrue) return gram / tf.cast(h * w * c, tf.float32)这里用了tf.shape而不是 Python 的feature_map.shape是为了在tf.function或 AutoGraph 下保持动态维度可用避免因图编译导致维度推断失败。归一化分母取h*w*c把不同分辨率下的 Gram 值压到同一数量级否则 512 分辨率会比 256 分辨率产生更大的损失值换分辨率后参数全部要重调。Gram 矩阵是(batch, c, c)形状后续计算风格损失时直接对两张图片的 Gram 矩阵做均方误差即可。3. 用 TensorFlow 写训练循环把一张图片当作可训练变量3.1 内容损失、风格损失与总变差损失组合有了特征提取器和 Gram 矩阵剩下的工作是把目标图片喂进去计算三种损失的加权和。内容损失直接用 L2 距离比较输出图和内容图在block4_conv2上的特征差异风格损失在每一个风格层上比较 Gram 矩阵的 L2 距离再取平均值总变差损失则对输出图的相邻像素做差分抑制噪声和过度锐利。下面这段代码把三种损失封装成一个函数。注意preprocess_input会把 RGB 转为 BGR 并按通道减去均值这是 VGG 预训练权重的要求该操作可导梯度可以穿过它作用到目标图片变量上def compute_losses(extractor, content_layers, style_layers, content_img, style_img, target_img, alpha1e-3, beta1.0, tv_weight1e-4): with tf.GradientTape() as tape: tape.watch(target_img) target_features extractor( tf.keras.applications.vgg19.preprocess_input(target_img)) content_features extractor( tf.keras.applications.vgg19.preprocess_input(content_img)) style_features extractor( tf.keras.applications.vgg19.preprocess_input(style_img)) content_loss tf.reduce_mean( tf.square(target_features[0] - content_features[0])) style_loss 0.0 for i in range(len(style_layers)): target_gram gram_matrix(target_features[i 1]) style_gram gram_matrix(style_features[i 1]) style_loss tf.reduce_mean( tf.square(target_gram - style_gram)) / len(style_layers) tv_loss tf.reduce_mean(tf.image.total_variation(target_img)) total_loss (alpha * content_loss beta * style_loss tv_weight * tv_loss) return total_loss, content_loss, style_loss, tv_loss代码里tape.watch(target_img)是必需的因为target_img是tf.Variable而不是普通张量虽然 Variable 默认会被记录但在自定义训练循环里显式 watch 能避免后续改动代码时漏掉。content_features和style_features都在同一段梯度停止范围内它们不会被反传只有target_img的梯度被计算。风格层索引 i 从 0 到 4对应target_features[1]到target_features[5]与前面outputs的拼接顺序一致。3.2 像素级优化Adam 与图像裁剪循环真正训练时目标图片的初始值常用内容图本身也可以使用随机噪声但内容图初始化收敛更快风格迁移结果也更稳定。优化器通常选 Adam学习率在 1 到 10 之间这比常规模型的 1e-3 大得多因为优化空间是 512×512×3 的像素值不是网络权重。def load_image(path, target_size): raw tf.io.read_file(path) img tf.image.decode_image(raw, channels3) img tf.image.convert_image_dtype(img, tf.float32) img tf.image.resize(img[None], (target_size, target_size)) return img * 255.0 content_img load_image(content.jpg, 512) style_img load_image(style.jpg, 512) target_img tf.Variable(content_img, trainableTrue) optimizer tf.keras.optimizers.Adam(learning_rate5.0) steps 800 for step in range(steps): total_loss, content_loss, style_loss, tv_loss compute_losses( extractor, content_layers, style_layers, content_img, style_img, target_img) grads tf.GradientTape().gradient(total_loss, target_img) optimizer.apply_gradients([(grads, target_img)]) target_img.assign(tf.clip_by_value(target_img, 0.0, 255.0)) if step % 50 0: print(fstep {step}: total{total_loss.numpy():.2f} fcontent{content_loss.numpy():.2f} fstyle{style_loss.numpy():.2f} ftv{tv_loss.numpy():.2f})每一轮迭代后用clip_by_value把像素拉回 0 到 255 的范围。如果不做这一步输出图像会逐渐出现高亮噪点或纯黑区域。apply_gradients前不要忘记目标变量是target_img而不是网络里的任何 layer。这里的load_image把读入图像统一转成 0 到 255 的 float32为preprocess_input提供正确输入范围。3.3 风格迁移参数的调法先抓三组权重参数决定最终效果偏向内容还是偏向风格。常见新手错误是把alpha和beta都设为 1结果风格完全压过内容输出图根本看不出原来拍的什么。下面给出一组经验上的起始值后续按图观察调整。参数含义建议起始值调大后的表现alpha内容损失权重1e-3轮廓更清晰风格变淡beta风格损失权重1.0纹理更浓结构可能扭曲tv_weight总变差权重1e-4图像更平滑太大则模糊learning_rateAdam 学习率5.0收敛更快过大则抖动steps总迭代步数800风格更充分过程更慢内容损失的数量级天然比风格损失小因为深层特征图经过多层卷积后数值被压缩所以alpha通常要设得非常小。相反风格损失来自多个层的 Gram 矩阵数值普遍偏大beta设为 1 就能达到显著效果。如果迭代后期发现损失不再下降可以二阶段衰减学习率比如前 400 步用 5.0后 400 步降为 1.0能减少像素震荡。想直观感受梯度下降本身如何工作可以先去 TensorFlow Playground 这类交互网页上做几个基础实验把学习率从大到小拖一遍能看到同样的抖动和停滞现象。4. 代码下载后的运行流程让风格迁移脚本能落地4.1 单文件结构与环境准备很多读者拿到的风格迁移代码是一个单文件脚本外加内容图和风格图。这里推荐一个常见做法把代码保存成st.py放在与图片相同的目录下目录结构如下st/ ├── content.jpg ├── style.jpg └── st.py运行前先确认 Python 和 TensorFlow 版本。建议使用 Python 3.8 到 3.10 环境安装 TensorFlow 2.10 左右的版本新版 TensorFlow 2.15 之后部分 API 对旧代码不再兼容。安装命令直接使用pip install tensorflow2.10.*第一次运行脚本时Keras 会自动下载 VGG19 的预训练权重到用户目录如果下载失败把权重文件手动放到对应缓存目录即可。之后不用重复下载。运行命令为python st.py --content content.jpg --style style.jpg \ --output result.jpg --size 512 --steps 8004.2 用 argparse 把参数暴露出来代码不能把参数写死在循环里尤其是换图换风格时经常要调权重。把关键参数全部通过命令行传入是让脚本具备可复用性的关键一步。下面是常见做法中的核心片段import argparse parser argparse.ArgumentParser(descriptionTensorFlow Style Transfer) parser.add_argument(--content, requiredTrue, helpcontent image path) parser.add_argument(--style, requiredTrue, helpstyle image path) parser.add_argument(--output, defaultresult.jpg, helpoutput image path) parser.add_argument(--size, typeint, default512, helpoutput size) parser.add_argument(--steps, typeint, default800) parser.add_argument(--content-weight, typefloat, default1e-3) parser.add_argument(--style-weight, typefloat, default1.0) parser.add_argument(--tv-weight, typefloat, default1e-4) parser.add_argument(--learning-rate, typefloat, default5.0) args parser.parse_args()argparse最大的好处是把“换参数”这件事从修改代码中剥离出来。对比实验时只需要在 shell 里换命令比如想得到更偏风格的输出把--style-weight增大到 2.0同时压低--content-weight到 5e-4。这样保存的每一次命令即一次实验记录不会被不小心改掉的代码变量干扰。4.3 运行中常见的四个坑及处理拿到任何一份风格迁移代码先按照下面四项检查能避开绝大多数运行失败。第一是 VGG 加载方式不一致。部分老代码还在用tf.keras.applications.vgg19.VGG19配合tf.compat.v1TensorFlow 2.x 下会直接报属性错误。正确做法是用tf.keras.applications开头的函数式接口并确保include_topFalse。第二是显存不足。风格迁移在每次迭代中都要同时保存内容图、风格图和目标图的中间特征512 分辨率以上需要约 3 到 4 GB 显存。报ResourceExhaustedError时优先把--size降到 384 或 256而不是调整 TensorFlow 的显存配置。第三是输出图像发灰或颜色失真。这个问题几乎都出在图像的预处理和后处理不对称上。VGG19 的preprocess_input做了 BGR 通道翻转和均值减法保存结果前需要做相反变换def deprocess(img): img img[0] tf.constant([103.939, 116.779, 123.68]) img img[..., ::-1] return tf.clip_by_value(img, 0, 255).numpy().astype(uint8)这里先加回均值再通过[..., ::-1]把 BGR 转回 RGB最后裁剪到 0 到 255 的范围。第四是损失显示为 0 或梯度为None通常是目标图片没有作为tf.Variable创建或者GradientTape的作用域没有包住extractor的前向计算。检查目标变量是否在tape.watch中并确认只对target_img求梯度。5. 风格迁移的进阶验证TensorBoard 曲线与多尺度优化5.1 用 TensorBoard 观察损失构成只盯着终端里打印的总损失很难判断权重是否合理完整方案是在训练循环中加入 TensorBoard 记录把内容损失、风格损失和总变差损失分开画出来。在循环前创建 writer循环中写入数值writer tf.summary.create_file_writer(logs) for step in range(steps): total_loss, content_loss, style_loss, tv_loss compute_losses(...) with writer.as_default(): tf.summary.scalar(loss/total, total_loss, stepstep) tf.summary.scalar(loss/content, content_loss, stepstep) tf.summary.scalar(loss/style, style_loss, stepstep) tf.summary.scalar(loss/tv, tv_loss, stepstep) if step % 100 0: tf.summary.image(output, tf.cast(target_img, tf.uint8), stepstep)跑完训练后执行tensorboard --logdir logs在浏览器中观察三条损失曲线。风格损失下降但内容损失上升说明beta过大内容损失平坦但风格损失波动说明learning_rate太高。中间过程图比最终图更能看出问题如果第 100 步时已经出现彩色噪点优先调低学习率如果第 500 步结构还完整但笔触很弱增大style_weight或增加迭代轮数。5.2 多尺度优化的分阶段训练单次在 512 分辨率上训练 800 步耗时长且容易糊掉细节。常见做法是分两个尺度先在 256 分辨率跑 400 步获得整体笔触再用双线性插值把目标图放大到 512继续跑 400 步细化边缘。实现时只需在第二轮用上一轮的输出初始化target_imgsmall_target target_img.numpy() big_target tf.image.resize(small_target, (512, 512)) target_img tf.Variable(big_target, trainableTrue)多尺度优化结束后把内容图、风格图、256 阶段结果和 512 阶段结果并排保存成一张对比图检查风格纹路是否在放大后出现断裂或重影。这一步能帮助判断是风格层权重问题还是上采样带来的伪影。对于需要更高分辨率的输出可以继续套用这个流程但第三轮的tv_weight要适当调大防止高分辨率下相邻像素差异过大导致画面发花。本文还有配套的精品资源点击获取
返回列表