# mypy: 允许未类型化定义
来自 https://arxiv.org/abs/1802.05957 的谱归一化。
来自
打字
导入
任意,
可选,
类型变量
导入
火炬
导入 torch.nn.functional
作为 F
来自 torch.nn.modules
导入
模块
__all__ = [
谱范数,
谱范数加载状态字典预处理钩子,
谱范数状态字典钩子,
谱范数,
移除谱范数,
]
类
谱范数:
# 在每次前向调用前后保持不变:
# u = F.normalize(W @ v)
# 注意:初始化时,此不变性不强制执行
_版本:
整型 = 1
# 版本 1:
# 将 `W` 不再作为缓冲区处理,
# 添加 `v` 作为缓冲区,并且
# 评估模式使用 `W = u @ W_orig @ v` 而不是存储的 `W`。
名称:
字符串
暗淡:
整型
n 次幂迭代次数:
整型
eps: 浮点数
def 初始化(
self,
名称:
字符串 =
权重,
n 次幂迭代次数:
整型 = 1,
暗淡:
整型 = 0,
eps: 浮点数 = 1e-12,
) -> 无:
self.名称 =
名称
self.维度 =
维度
如果 n_power_iterations
≤ 0:
提升 ValueError(
预期 n_power_iterations 为正数,但
f获取 n_power_iterations={
n 次幂迭代次数}"
)
self.n_power_iterations = n_power_iterations
self.eps = eps
def 重新塑形权重为矩阵(self,
重量:
火炬.
张量) ->
火炬.
张量:
weight_mat = 权重
如果 self.
维度 != 0:
# 将维度排列到前面
weight_mat = 权重矩阵.
排列(
self.暗淡, *[d
为 d
在
范围(
权重矩阵.
暗淡())
如果 d != self.
暗淡]
)
高度 =
权重矩阵.
尺寸(0)
返回
权重矩阵.
重塑(
高度, -1)
def 计算权重(self,
模块:
模块,
执行幂迭代:
布尔) ->
火炬.
张量:
# NB: 如果设置 `do_power_iteration`,则 `u` 和 `v` 向量将在 **原地** 更新。这非常重要
# 因为在 `DataParallel` 前向传播中,向量(作为缓冲区)是从并行化模块广播到每个模块副本的
# 因为在 `DataParallel` 前向传播中,向量(作为缓冲区)是从并行化模块广播到每个模块副本的
# 因为在 `DataParallel` 前向传播中,向量(作为缓冲区)是从并行化模块广播到每个模块副本的
# 这是一个动态创建的新模块对象。每个副本
# 都运行自己的谱范数幂迭代。所以简单地将
# 更新后的向量分配给该函数运行的模块,将导致
# 更新永远丢失。下次并行化
# 模块被复制,使用相同的随机初始化的向量!
# 广播并使用!
#
因此,为了使更改回传,我们依赖于两个
重要行为(也通过测试强制执行):
1. `DataParallel` 不克隆存储的广播张量
# 已在正确设备上;并且它确保
# 并行模块已经在`device[0]`上。
# 2. 如果`out=`参数中的输出张量具有正确的形状,它将
# 只需填充值即可。
# 因此,由于对所有元素都执行了相同的幂迭代,
仅更新张量而不改变设备,将确保设备上的模块副本更新_u 向量
在`device[0]`上的模块副本将通过共享存储来并行化模块
然而,在原地更新`u`和`v`之后,我们需要**克隆**
#
然而,在原地更新`u`和`v`之后,我们需要**克隆**
在使用之前先对它们进行归一化处理。这是为了支持
通过两次前向传递进行反向传播,例如,GAN 训练中的常见模式:
GAN 训练中的损失函数:损失 = D(real) - D(fake)。否则,引擎将
抱怨需要进行第一次前向传播的变量
(即第二个前向中的 u 和 v 向量)在第二个前向中发生变化。
权重 = getattr(
模块, self.
名称 +
"_原")
u = getattr(模块, self.
名称 + "_u")
v = getattr(模块, self.
名称 + "_v")
weight_mat = self.将权重重塑为矩阵(
重量)
如果
执行幂迭代:
与
火炬.
不梯度():
为 _
在
范围(self.
n 次幂迭代次数):
权重的谱范数等于 `u^T W v`,其中 `u` 和 `v`
是第一左和右奇异向量。
这种幂迭代产生 `u` 和 `v` 的近似值。
v = F.normalize(
火炬.mv(
权重矩阵.t(), u),
暗淡=0, eps=self.eps,
输出=v
)
u = F.normalize(火炬.mv(
权重矩阵, v),
暗淡=0, eps=self.eps,
输出=u)
如果 self.n_power_iterations > 0:
# 请参阅上方,了解为何需要克隆
u = u.克隆(
内存格式=
火炬.
连续格式)
v = v.克隆(
内存格式=
火炬.
连续格式)
sigma
(由于 sigma 是数学和工程领域的通用符号,通常不进行翻译,因此保持原文不变。) = 火炬.
点(u,
火炬.mv(
权重矩阵, v))
权重 =
权重 /
sigma
(由于 sigma 是数学和工程领域的通用符号,通常不进行翻译,因此保持原文不变。)
返回
权重
def 删除(self,
模块:
模块) ->
无:
与
火炬.
不梯度():
权重 = self.
计算权重(
模块,
执行幂迭代=
错误)
delattr(模块, self.
名称)
delattr(模块, self.
名称 + "_u")
delattr(模块, self.
名称 + "_v")
delattr(模块, self.
名称 + "_orig")
模块.
注册参数(self.
名称,
火炬.nn.
参数(
重量.detach()))
def __调用__(self,
模块:
模块,
输入:
任意) ->
无:
setattr(
模块,
self.名称,
self.计算权重(
模块,
执行幂迭代=
模块.
训练),
)
def _解_v_并缩放(self,
权重矩阵, u,
目标方差):
尝试返回一个向量 `v`,使得 `u = F.normalize(W @ v)`
(此类的顶部不变量)且 `u @ W @ v = sigma`。
当 `W^T W` 不可逆时,使用 pinverse。
v = 火炬.
线性代数.
多点(
[权重矩阵.t().mm(
权重矩阵).
p 逆(),
权重矩阵.t(), u.
展平(1
]
).挤压(1)
返回 v.mul_(
目标 sigma /
火炬.
点(u,
火炬.mv(
权重矩阵, v)))
@staticmethod
def 应用(
模块:
模块,
名称:
字符串,
n 次幂迭代次数: int,
暗淡: int, eps:
浮点数
) -> "谱归一化":
为
钩子
在
模块.
_前向预处理钩子.
值():
如果 isinstance(hook,
谱归一化)
和 hook.
名称 ==
名称:
提升
运行时错误(
f"不能在同一个参数上注册两个 spectral_norm 钩子"{
名称}"
)
fn = 谱归一化(
名称,
n 次幂迭代次数,
暗淡, eps)
权重 =
模块.
参数[
名称]
如果
权重
是
无:
提升 ValueError(
f"无法将 `SpectralNorm` 应用为参数"{
名称}
参数为空
)
如果 isinstance(
重量,
火炬.nn.
参数.
未初始化参数):
提升 ValueError(
"传递给 `SpectralNorm` 的模块不能有未初始化的参数。"
"在应用谱归一化之前,请确保运行模拟前向传播"
)
与
火炬.
不梯度():
weight_mat = 函数.
重新塑形权重为矩阵(
重量)
h, w = 权重矩阵.
尺寸()
随机初始化 `u` 和 `v`
u = F.normalize(重量.
新空(h).
正常的(0, 1),
暗淡=0, eps=
函数.eps)
v = F.normalize(重量.
新空(w).
正常的(0, 1),
暗淡=0, eps=
函数.eps)
delattr(模块,
函数.
名称)
模块.
注册参数(
函数.
名称 +
"_原",
重量)
# 我们仍然需要将权重作为 fn.name 返回,因为所有各种
# things may assume that it exists, e.g., when initializing weights.
# However, we can't directly assign as it could be an nn.Parameter and
# gets added as a parameter. Instead, we register weight.data as a plain
# attribute.
setattr(模块,
函数.
名称,
重量.
数据)
模块.
注册缓冲区(
函数.
名称 + "_u", u)
模块.
注册缓冲区(
函数.
名称 + "_v", v)
模块.
注册前向钩子(
函数)
模块._register_state_dict_hook(SpectralNormStateDictHook(
函数))
模块._register_load_state_dict_pre_hook(SpectralNormLoadStateDictPreHook(
函数))
返回 fn
# 这是一个顶级类,因为 Py2 的 pickle 不喜欢内部类也不喜欢实例方法。
# 实例方法。
类
谱归一化加载状态字典预钩子:
请参阅 SpectralNorm._version 的文档字符串,了解 spectral_norm 的变化。
def 初始化(self,
函数) ->
无:
self.fn = fn
对于版本为 None 的状态字典(假设它已经至少经过一次训练正向传播),我们有
对于版本为 None 的状态字典(假设它已经至少经过一次训练正向传播),我们有
#
# u = F.normalize(W_orig @ v)
# W = W_orig / sigma, 其中 sigma = u @ W_orig @ v
#
# To compute `v`, we solve `W_orig @ x = u`, and let
# v = x / (u @ W_orig @ x) * (W / W_orig).
def __调用__(
self,
state_dict,
前缀,
本地元数据,
严格的,
缺少键,
预期之外的键,
错误信息,
) -> 无:
fn = self.fn
版本 =
本地元数据.
获取(
谱范数, {}).
获取(
函数.
名称 +
".版本",
无
)
如果
版本
是
无
或
版本 < 1:
权重键 =
前缀 +
函数.
名称
如果 (
版本
是
无
和
所有(
权重键 + s
在
状态字典
为 s
在 (
"_原始", "_u", "_v"))
和
权重键 not
在
状态字典
):
# 检测是否是缺少元数据的更新状态字典。
# 这可能发生在用户自己构建状态字典的情况下,
# 因此我们假设这是最新的。
返回
缺少键 =
假
为
后缀
在 (
"_原始", "", "_u"):
key = 重量键 +
后缀
如果 key not
在 state_dict:
缺少键 =
真实
如果
严格的:
缺少键.
追加(
键)
如果
缺少键:
返回
与
火炬.
不梯度():
原始权重 = state_dict[
权重键 +
"_原始"]
权重 = state_dict.
弹出(
重量键)
sigma
(由于 sigma 是数学和工程领域的通用符号,通常不进行翻译,因此保持原文不变。) = (原始权重 /
重量).
均值()
weight_mat = 函数.
将权重重塑为矩阵(
原始权重)
u = state_dict[重量键 + "_u"]
v = 函数.
_解决_v_并缩放(
权重矩阵, u,
西格玛)
state_dict[重量键 + "_v"] = v
# 这是一个顶级类,因为 Py2 pickle 不喜欢内部类也不喜欢嵌套类
实例方法
类 SpectralNormStateDictHook:
请参阅 SpectralNorm._version 的文档字符串,了解 spectral_norm 的更改。
def 初始化(self,
函数) ->
无:
self.fn = fn
def __调用__(self,
模块, state_dict,
前缀,
本地元数据) ->
无:
如果
"光谱范数" not
在
本地元数据:
本地元数据[
谱范数] = {}
key = self.函数.
名称 +
".版本"
如果 key
在
本地元数据[
谱范数
]:
提升
运行时错误(f
"在元数据['spectral_norm']中意外键"{
键}")
本地元数据[
谱范数
]
[键] = self.
函数._version
T 模块 =
类型变量(
"T 模块",
绑定=
模块)
[文档]def
谱范数(
模块:
模块 T,
名称:
字符串 =
权重,
n 次幂迭代次数:
整型 = 1,
eps: 浮点数 = 1e-12,
暗淡:
可选[int] =
无,
) -> 模块 T:
r将频谱归一化应用于给定模块中的参数。
.. math::
\mathbf{W}_{SN} = \dfrac{\mathbf{W}}{\sigma(\mathbf{W})},
\sigma(\mathbf{W}) = \max_{\mathbf{h}: \mathbf{h} \ne 0} \dfrac{\|\mathbf{W} \mathbf{h}\|_2}{\|\mathbf{h}\|_2},
光谱归一化稳定了判别器(评论家)的训练
生成对抗网络(GANs)通过缩放权重张量
使用权重矩阵的谱范数:σ计算
幂迭代法。如果权重张量的维度大于
比 2 大时,在幂迭代法中将其重塑为 2D 以获取谱
规范。这是通过一个钩子实现的,该钩子计算谱范数
在每次调用:meth:`~Module.forward`之前重新缩放权重。
请参阅《用于生成对抗网络的谱归一化》_ 。
参考链接:https://arxiv.org/abs/1802.05957
Args:
模块(nn.Module):包含模块。
名称(str,可选):权重参数的名称
n_power_iterations(int,可选):幂迭代次数。
计算谱范数
eps (float, 可选): ε用于数值稳定性
计算范数
dim (int, 可选): 对应输出数量的维度,
默认为 ``0``,除非是实例化的模块,
ConvTranspose{1,2,3}d,当其为``1``时
返回:
原始模块带有谱范数钩子
.. 注意::
此函数已被重新实现为
:func:`torch.nn.utils.parametrizations.spectral_norm` 使用新的
参数化功能
:func:`torch.nn.utils.parametrize.register_parametrization`. 请使用
新版本。此功能将在未来的版本中弃用
的 PyTorch。
示例::
>>> m = spectral_norm(nn.Linear(20, 40))
>>> m
Linear(in_features=20, out_features=40, bias=True)
>>> m.weight_u.size()
torch.Size([40])
"源代码"
如果
维度
是
无:
如果 isinstance(
模块,
(
火炬.nn.ConvTranspose1d,
火炬.nn.ConvTranspose2d,
火炬.nn.ConvTranspose3d,
),
):
维度 = 1
否则:
维度 = 0
频谱归一化.
应用(
模块,
名称,
n 次幂迭代次数,
暗淡, eps)
返回
模块
[文档]def remove_spectral_norm(module: T_module, name: str = "weight") -> T_module:
r"""从模块中移除频谱归一化的重新参数化。
Args:
模块 (Module): 包含的模块
名称 (str, 可选): 权重参数的名称
示例:
>>> m = spectral_norm(nn.Linear(40, 10))
>>> remove_spectral_norm(m)
"""
for k, hook in module._forward_pre_hooks.items():
如果 isinstance(hook, SpectralNorm) 且 hook.name 等于 name:
hook.remove(module)
del module._forward_pre_hooks[k]
break
else:
抛出 ValueError 异常(f"spectral_norm of '{name}' not found in {module} ")
for k, hook in module._state_dict_hooks.items():
if isinstance(hook, SpectralNormStateDictHook) and hook.fn.name == name:
删除 module._state_dict_hooks[k]
终止循环
for k, hook in module._load_state_dict_pre_hooks.items():
如果 isinstance(hook, SpectralNormLoadStateDictPreHook) 且 hook.fn.name == name:
del module._load_state_dict_pre_hooks[k]
break
return module