news 2026/4/15 20:18:47

动手学深度学习——样式迁移代码

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
动手学深度学习——样式迁移代码

1. 前言

上一篇我们已经从整体上理解了**样式迁移(Style Transfer)**的思想:

  • 内容图提供结构与布局

  • 样式图提供纹理与风格

  • 生成图通过不断优化得到

  • 优化目标同时考虑内容损失和风格损失

这一节我们就正式进入代码实现。

和前面 FCN 那种“搭模型”不太一样,样式迁移代码的核心不是新建一个复杂网络,而是:

固定一个预训练特征提取网络,然后把生成图本身当作待优化参数。

所以这一节真正要看懂的是:

  • 怎么提取内容特征

  • 怎么提取风格特征

  • Gram 矩阵怎么计算

  • 总损失怎么写

  • 怎么一步步优化生成图


2. 样式迁移代码的整体流程

李沐这里的实现思路非常清楚,可以概括成下面几步:

第一步:读取内容图和样式图

把两张图都处理成网络可接受的张量格式。

第二步:加载预训练 CNN

通常使用 VGG 这类网络提取中间层特征。

第三步:指定哪些层表示内容,哪些层表示风格

  • 内容通常取较深层

  • 风格通常取多个层

第四步:初始化生成图

常见做法是直接用内容图初始化,而不是随机噪声。

第五步:定义损失函数

包括:

  • 内容损失

  • 风格损失

  • 总变差损失

第六步:优化生成图

不断更新生成图,使其同时接近内容图和样式图。


3. 读取与预处理图像

首先要做的是把图片读进来,并统一成网络需要的输入格式。

常见写法大致如下:

import torch from torch import nn from torchvision import models, transforms from PIL import Image

然后定义图像预处理:

rgb_mean = torch.tensor([0.485, 0.456, 0.406]) rgb_std = torch.tensor([0.229, 0.224, 0.225]) preprocess = transforms.Compose([ transforms.Resize((300, 450)), transforms.ToTensor(), transforms.Normalize(mean=rgb_mean, std=rgb_std) ])

这里做了三件事:

  • 调整图像尺寸

  • 转成张量

  • 按 ImageNet 的均值和方差做标准化

为什么要标准化?

因为后面用的是预训练网络,它就是在这种输入分布下训练出来的。


4. 为什么显示图像时要反归一化

输入网络时用了标准化,但如果想把结果显示出来,就必须把它恢复回正常图像范围。

通常会写一个“后处理”函数,把图像从标准化空间还原回来。

核心思想是:

  • 先乘标准差

  • 再加均值

  • 最后裁剪到0~1

因为网络处理的是“适合训练的张量”,
而我们人眼想看的是“正常 RGB 图片”。


5. 加载预训练特征提取网络

经典样式迁移里,最常用的是 VGG 网络。
例如:

pretrained_net = models.vgg19(weights=models.VGG19_Weights.DEFAULT).features

这里取的是.features,而不是整个分类头。

原因很简单:

样式迁移只需要卷积层提取的中间特征,不需要最后的分类器。

所以这里保留的是特征提取部分。


6. 为什么样式迁移主要用中间层特征

因为我们不关心最终的分类结果,
我们关心的是图像在不同层上的表示。

不同层特征有不同作用:

  • 较浅层:更偏向边缘、纹理、颜色局部模式

  • 较深层:更偏向物体结构和语义布局

这刚好对应样式迁移里的两个目标:

  • 内容

  • 风格

所以中间层特征才是关键。


7. 指定内容层和风格层

李沐这里通常会显式指定:

  • 哪些层用来表示内容

  • 哪些层用来表示风格

例如:

style_layers = [0, 5, 10, 19, 28] content_layers = [25]

这表示:

  • 风格用多个层共同表示

  • 内容通常只用一个较深层表示

为什么风格要用多个层?

因为风格不是单一层次的信息,而是从浅层纹理到稍深层模式的综合统计。

为什么内容只用较深层?

因为内容更关心整体结构和语义,不需要太多浅层细节。


8. 提取中间层特征

通常会定义一个函数,把指定层的输出取出来:

def extract_features(X, content_layers, style_layers): contents = [] styles = [] for i, layer in enumerate(pretrained_net): X = layer(X) if i in style_layers: styles.append(X) if i in content_layers: contents.append(X) return contents, styles

这段代码的意义非常大。

它不是只拿最终输出,而是在网络前向传播过程中,
把我们关心的中间层结果都收集起来。

最终返回两组特征:

  • 内容特征

  • 风格特征


9. 内容特征怎么用

对于内容图,我们提取一次内容特征并固定下来:

content_Y, _ = extract_features(content_X, content_layers, style_layers)

这里的content_Y就是内容目标。

之后每次优化生成图时,都要让生成图的对应内容特征接近它。

所以内容图特征在整个训练过程中相当于:

被模仿的内容标准答案


10. 风格特征为什么不能直接拿原特征比较

因为风格不强调空间位置精确一致。

如果你直接逐元素比较某层特征,就会隐含一个要求:

这个纹理必须出现在完全对应的位置

这显然不符合“风格”的含义。

风格更像一种整体统计规律,而不是某个具体位置上的精确匹配。

所以风格不能直接比较特征本身,而要比较其统计关系。


11. Gram 矩阵的代码实现

样式迁移中最核心的一个函数就是 Gram 矩阵。

常见写法如下:

def gram(X): num_channels, n = X.shape[1], X.numel() // X.shape[1] X = X.reshape((num_channels, n)) return torch.matmul(X, X.T) / (num_channels * n)

这段代码一定要理解。

第一步:把特征图按通道展开

原来的特征图可能是:

(batch, channels, height, width)

这里本质上是把每个通道拉成一个长向量。

第二步:计算通道之间内积

X @ X.T

得到的是一个:

channels × channels

的矩阵。

它描述的是:

不同特征通道之间的相关性

这正是风格的重要表达。


12. 为什么 Gram 矩阵能表示风格

因为风格更像:

  • 颜色分布规律

  • 纹理重复模式

  • 特征通道之间共同激活的结构

而 Gram 矩阵正好抓住了:

通道之间的整体统计相关性

它弱化了“具体在哪里”,强化了“整体像不像这种风格”。

所以经典样式迁移里,风格目标通常就是:

让生成图的 Gram 矩阵接近样式图的 Gram 矩阵。


13. 先提取样式图的 Gram 矩阵目标

通常会先把样式图跑一遍网络,并计算各风格层的 Gram 矩阵:

_, style_Y = extract_features(style_X, content_layers, style_layers) style_Y_gram = [gram(Y) for Y in style_Y]

这相当于固定下:

  • 样式图在多个层上的风格表示

后面优化生成图时,就让生成图的 Gram 矩阵逐渐接近这些目标。


14. 内容损失怎么写

内容损失通常很直接,就是比较内容特征之间的差异。

例如:

def content_loss(Y_hat, Y): return torch.square(Y_hat - Y.detach()).mean()

这里:

  • Y_hat是生成图的内容特征

  • Y是内容图的内容特征

detach()的作用是把目标视为常量,不参与梯度更新。

内容损失越小,说明生成图在高层语义结构上越接近内容图。


15. 风格损失怎么写

风格损失则比较 Gram 矩阵:

def style_loss(Y_hat, gram_Y): return torch.square(gram(Y_hat) - gram_Y.detach()).mean()

这里:

  • gram(Y_hat)是生成图当前风格特征的 Gram 矩阵

  • gram_Y是样式图对应层的 Gram 矩阵目标

风格损失越小,说明生成图越具有样式图的纹理和风格统计特征。


16. 总变差损失是什么

除了内容和风格损失,通常还会加一个总变差损失。

例如:

def tv_loss(Y_hat): return 0.5 * ( torch.abs(Y_hat[:, :, 1:, :] - Y_hat[:, :, :-1, :]).mean() + torch.abs(Y_hat[:, :, :, 1:] - Y_hat[:, :, :, :-1]).mean() )

它的作用是:

让生成图在相邻像素之间更平滑,减少噪声和过度抖动

因为如果只优化内容和风格,有时图像会出现很多局部噪点。
总变差损失能让结果更自然一些。


17. 总损失如何组合

接下来就要把三类损失加起来。

常见形式是:

total_loss = content_weight * content_l \ + style_weight * style_l \ + tv_weight * tv_l

这里的三个权重非常重要:

  • content_weight决定保留内容的强度

  • style_weight决定风格迁移的强度

  • tv_weight决定平滑程度

如果:

  • 内容权重大,结果更像原图

  • 风格权重大,结果更像画作

  • 平滑权重大,图更干净但可能少些细节

所以样式迁移效果很大程度上取决于这些权重平衡。


18. 生成图为什么常常直接用内容图初始化

在理论上,生成图可以随机初始化。
但实际中,常见做法是:

gen_img = content_X.clone().requires_grad_(True)

为什么?

因为如果直接从内容图开始优化,那么:

  • 一开始就已经有正确的内容结构

  • 后面只需要逐渐叠加风格

这样训练更稳定,也更容易得到视觉上合理的结果。

如果从纯随机噪声开始,优化过程通常更慢,也更难控制。


19. 为什么优化的是图像,而不是模型参数

这是样式迁移代码最特别的一点。

平时训练神经网络时,我们更新的是:

  • 卷积核参数

  • 全连接层参数

但样式迁移里:

  • 预训练网络固定不动

  • 内容图和样式图固定不动

  • 真正更新的是生成图本身

也就是说,生成图被视为一个可学习参数。

这是一种非常经典的“输入优化”思路。


20. 优化循环怎么写

优化流程一般长这样:

optimizer = torch.optim.Adam([gen_img], lr=0.3)

然后循环做:

  1. 提取生成图特征

  2. 计算内容损失

  3. 计算风格损失

  4. 计算总变差损失

  5. 合成总损失

  6. 反向传播更新生成图

伪代码如下:

for epoch in range(num_epochs): optimizer.zero_grad() contents_Y_hat, styles_Y_hat = extract_features(gen_img, content_layers, style_layers) contents_l = [content_loss(Y_hat, Y) for Y_hat, Y in zip(contents_Y_hat, content_Y)] styles_l = [style_loss(Y_hat, Y) for Y_hat, Y in zip(styles_Y_hat, style_Y_gram)] tv_l = tv_loss(gen_img) l = content_weight * sum(contents_l) + \ style_weight * sum(styles_l) + \ tv_weight * tv_l l.backward() optimizer.step()

这就是样式迁移的核心优化主循环。


21. 每轮优化后生成图发生了什么

每做一次反向传播,生成图像素都会被轻微调整。

这些调整方向由损失函数共同决定:

  • 内容损失要求它保持内容结构

  • 风格损失要求它具有样式纹理

  • 总变差损失要求它更平滑

所以随着迭代次数增加,生成图会逐渐从“普通照片”变成“带画风的内容图”。

这也是样式迁移最直观、最有成就感的地方。


22. 为什么要把不同层的风格损失加起来

因为单层特征不能完整表示风格。

浅层更擅长描述:

  • 边缘

  • 简单纹理

  • 局部颜色

稍深层则能描述:

  • 更复杂的纹理模式

  • 更抽象的局部结构关系

所以经典样式迁移通常会取多个风格层,把它们的损失加权求和。

这相当于从多个尺度共同约束“风格像不像”。


23. 这节代码最该掌握什么

如果从学习重点看,这一节最应该吃透的是下面几件事。

23.1 特征提取函数

知道如何从预训练网络中拿到中间层特征。

23.2 Gram 矩阵

知道它怎么写、为什么能表示风格。

23.3 三种损失

  • 内容损失

  • 风格损失

  • 总变差损失

23.4 生成图是优化变量

这是样式迁移区别于普通训练的关键。

23.5 总损失的权重平衡

知道为什么不同权重会明显影响最终图像效果。


24. 样式迁移代码的主线可以怎么背

这篇代码其实非常适合用一句主线来背:

固定预训练网络,提取内容图和样式图的目标特征,然后不断更新生成图,让它同时匹配内容和风格。

如果再拆细一点,就是:

  • 读图

  • 提特征

  • 算 Gram

  • 定义损失

  • 优化生成图

只要这五步不乱,样式迁移代码整体就能看懂。


25. 本节总结

这一节我们学习了样式迁移的代码实现,核心内容可以总结为以下几点。

25.1 用预训练 CNN 提取中间层特征

不同层特征分别用于内容和风格表示。

25.2 内容损失比较深层特征差异

它负责保留原图的结构和布局。

25.3 风格损失比较 Gram 矩阵差异

它负责迁移样式图的纹理和风格。

25.4 总变差损失让图像更平滑

防止结果中出现太多噪声。

25.5 样式迁移优化的是生成图本身

这与普通训练网络参数的思路完全不同。


26. 学习感悟

样式迁移代码很有意思,因为它让我们看到:

神经网络不只是“拿来训练参数”的工具,它还可以作为一个固定的特征空间,指导我们直接优化输入本身。

这是一种非常漂亮的思路。

你会发现,到了这里,卷积网络已经不再只是分类器,
而变成了一个“视觉特征度量器”:

  • 它告诉我们什么叫内容接近

  • 什么叫风格接近

  • 然后再利用梯度一步步把图像改出来

这就是样式迁移最迷人的地方。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/15 16:27:52

开源可部署!Qwen3-4B-Thinking-GGUF在vLLM上的GPU算力优化部署指南

开源可部署!Qwen3-4B-Thinking-GGUF在vLLM上的GPU算力优化部署指南 想快速体验一个经过GPT-5-Codex数据微调、推理速度飞快的开源大模型吗?今天,我们就来手把手教你,如何在vLLM框架上,高效部署Qwen3-4B-Thinking-GGUF…

作者头像 李华
网站建设 2026/4/15 20:51:23

忍者像素绘卷开源可部署:支持国产操作系统(OpenEuler)的兼容方案

忍者像素绘卷开源可部署:支持国产操作系统(OpenEuler)的兼容方案 1. 项目概述 忍者像素绘卷是一款基于Z-Image-Turbo深度优化的图像生成工作站,专为像素艺术创作而设计。这款工具将传统漫画创作与现代AI技术相结合,创…

作者头像 李华
网站建设 2026/4/14 5:03:09

知网AI率高怎么降最省钱?3款工具价格效果全对比

知网AI率高怎么降最省钱?3款工具价格效果全对比 说句大实话:对于大部分毕业生来说,钱是真的紧。 论文从写到查重到降AI率,每一步都要花钱。检测一次知网要几十到一百多,降AI率又是一笔开销,万一一次没降下来…

作者头像 李华