项目总结SVD Training(终焉)

Posted 囚生CY

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了项目总结SVD Training(终焉)相关的知识,希望对你有一定的参考价值。

0 0 0 序言

最终笔者决定抽空把最近的工作做一个回顾总结。本文是对以下两篇博客工作的进一步延申探讨,也是一篇总结文,接下来重心要转移到其他工作上了:

  1. 【数值分析×机器学习】以SVD的分解形式进行深度神经网络的训练
  2. 【论文实现】以SVD的分解形式进行深度神经网络的训练(PyTorch)

前面的工作笔者不再赘述,有兴趣地可以仔细阅读以上两文,其中第一篇是论文笔注,第二篇则是对该文的一个初步实现,后续的工作基本围绕第二篇博客中的实现展开,完整代码已上传 GitHub@SVDtraining \\text{GitHub@SVDtraining} GitHub@SVDtraining,由于目前原论文作者仍然没有提供其论文实现的源码,因此笔者在实现上可能仍然存在一些问题,详细的实现思路在第二篇博客中已经阐述,本文将不再重述代码细节。

本文第 1 , 2 , 4 1,2,4 1,2,4节中是对笔者在使用 PyTorch \\text{PyTorch} PyTorch对论文实现中遇到的一些小 trick \\text{trick} trick做一个记录,笔者觉得这些技巧应该还是对初学者比较有帮助的。本文的重点内容在第 3 3 3,在这一部分中,将着重于解析笔者在原文基础上引出的在线剪枝方法的实现,以及基于笔者实验的结果,提出一些可能算是创新点的愚见,权当抛砖引玉,愿诸君不吝赐教。



1 1 1 P y T o r c h \\rm PyTorch PyTorch模型参数分组

1.1 1.1 1.1 为什么需要模型参数分组?

可能一些朋友还不太理解为什么需要对神经网络模型中的参数进行分组,不妨思考以下几种场景:

  1. 需要对不同的参数使用不同的优化策略,如可以为不同参数设置不同的学习率(步长),典型例子是对敏感性较高的模型参数可以赋予较小的学习率(步长),如在 SVD training \\text{SVD training} SVD training中的因为左(右)奇异向量矩阵 left(right) singular vector matrix \\text{left(right) singular vector matrix} left(right) singular vector matrix)理论上要求严格满足正交约束,这类参数的敏感性就非常高,需要较小的学习率(步长)进行微调。
  2. 需要对不同的参数应用不同的损失函数,典型的例子是损失函数中带有与参数相关的正则项,而不同参数的正则项的权重是不相同。尤其当这个正则项并非传统的 L1 \\text{L1} L1 L2 \\text{L2} L2正则项(因为这些正则项的权重可以直接在 PyTorch \\text{PyTorch} PyTorch优化器中设置参数 weight_decay \\text{weight\\_decay} weight_decay即可),而是自定义的正则项(此时需要重写损失函数),如原论文中的奇异向量的正交正则器 Singular vectors orthogonality regularizer \\text{Singular vectors orthogonality regularizer} Singular vectors orthogonality regularizer)与奇异值稀疏导出正则器 Singular values sparsity-inducing regularizer \\text{Singular values sparsity-inducing regularizer} Singular values sparsity-inducing regularizer)。
  3. 在模型训练过程中可能需要基于模型参数的实际情况,引入一些自定义的逻辑对不同类型的模型参数进行人工调整。这就是本文第 3 3 3节中在线剪枝中所提出的方案。

O K \\rm OK OK,至此应该对模型参数分组已经有了一个初步的认识,接下来通过举例说明上述第 1 1 1点(优化器中的参数分组)与第 2 2 2点(损失函数中的参数分组)。

1.2 1.2 1.2 P y T o r c h \\rm PyTorch PyTorch优化器中的模型参数分组

关于 P y T o r c h \\rm PyTorch PyTorch优化器中的模型参数分组可以参考官方文档,以torch.optim.SGD随机梯度下降优化器为例,我们首先可以通过调用model.named_parameters()生成器来获取一个 P y T o r c h \\rm PyTorch PyTorch模型中的所有参数及其名称。比如模型可以选取torchvision.models.resnet模块中自带的 ResNet18 \\text{ResNet18} ResNet18

from torchvision.models import resnet

model = resnet.resnet18()
for name, parameter in model.named_parameters():
    print(name, parameter.shape)

输出结果:

conv1.weight torch.Size([64, 3, 7, 7])
bn1.weight torch.Size([64])
bn1.bias torch.Size([64])
layer1.0.conv1.weight torch.Size([64, 64, 3, 3])
layer1.0.bn1.weight torch.Size([64])
layer1.0.bn1.bias torch.Size([64])
layer1.0.conv2.weight torch.Size([64, 64, 3, 3])
layer1.0.bn2.weight torch.Size([64])
layer1.0.bn2.bias torch.Size([64])
layer1.1.conv1.weight torch.Size([64, 64, 3, 3])
layer1.1.bn1.weight torch.Size([64])
layer1.1.bn1.bias torch.Size([64])
layer1.1.conv2.weight torch.Size([64, 64, 3, 3])
layer1.1.bn2.weight torch.Size([64])
layer1.1.bn2.bias torch.Size([64])
layer2.0.conv1.weight torch.Size([128, 64, 3, 3])
layer2.0.bn1.weight torch.Size([128])
layer2.0.bn1.bias torch.Size([128])
layer2.0.conv2.weight torch.Size([128, 128, 3, 3])
layer2.0.bn2.weight torch.Size([128])
layer2.0.bn2.bias torch.Size([128])
layer2.0.downsample.0.weight torch.Size([128, 64, 1, 1])
layer2.0.downsample.1.weight torch.Size([128])
layer2.0.downsample.1.bias torch.Size([128])
layer2.1.conv1.weight torch.Size([128, 128, 3, 3])
layer2.1.bn1.weight torch.Size([128])
layer2.1.bn1.bias torch.Size([128])
layer2.1.conv2.weight torch.Size([128, 128, 3, 3])
layer2.1.bn2.weight torch.Size([128])
layer2.1.bn2.bias torch.Size([128])
layer3.0.conv1.weight torch.Size([256, 128, 3, 3])
layer3.0.bn1.weight torch.Size([256])
layer3.0.bn1.bias torch.Size([256])
layer3.0.conv2.weight torch.Size([256, 256, 3, 3])
layer3.0.bn2.weight torch.Size([256])
layer3.0.bn2.bias torch.Size([256])
layer3.0.downsample.0.weight torch.Size([256, 128, 1, 1])
layer3.0.downsample.1.weight torch.Size([256])
layer3.0.downsample.1.bias torch.Size([256])
layer3.1.conv1.weight torch.Size([256, 256, 3, 3])
layer3.1.bn1.weight torch.Size([256])
layer3.1.bn1.bias torch.Size([256])
layer3.1.conv2.weight torch.Size([256, 256, 3, 3])
layer3.1.bn2.weight torch.Size([256])
layer3.1.bn2.bias torch.Size([256])
layer4.0.conv1.weight torch.Size([512, 256, 3, 3])
layer4.0.bn1.weight torch.Size([512])
layer4.0.bn1.bias torch.Size([512])
layer4.0.conv2.weight torch.Size([512, 512, 3, 3])
layer4.0.bn2.weight torch.Size([512])
layer4.0.bn2.bias torch.Size([512])
layer4.0.downsample.0.weight torch.Size([512, 256, 1, 1])
layer4.0.downsample.1.weight torch.Size([512])
layer4.0.downsample.1.bias torch.Size([512])
layer4.1.conv1.weight torch.Size([512, 512, 3, 3])
layer4.1.bn1.weight torch.Size([512])
layer4.1.bn1.bias torch.Size([512])
layer4.1.conv2.weight torch.Size([512, 512, 3, 3])
layer4.1.bn2.weight torch.Size([512])
layer4.1.bn2.bias torch.Size([512])
fc.weight torch.Size([1000, 512])
fc.bias torch.Size([1000])

可以看到确实是有许多参数,现在笔者想将上述这些参数按照名称以weight结尾或bias结尾分为两组,然后赋予他们不同的随机梯度下降优化的策略:

from torch import optim
from torchvision.models import resnet

weight_params = []
bias_params = []

model = resnet.resnet18()
for name, parameter in model.named_parameters():
    suffix = name.split('.')[-1]
    if suffix == 'weight':
        weight_params.append(parameter)
    elif suffix == 'bias':
        bias_params.append(parameter)
        
optimizer = optim.SGD([{'params': weight_params, 'lr': 1e-2, 'momentum':.9}, 
                       {'params': bias_params, 'lr': 1e-3, 'weight_decay':.1}],
                      lr=1e-2,
                      momentum=.5,
                      weight_decay=1e-2)

注意在optim.SGD中除了为每个组别的模型参数赋予了不同的学习率lr,动量因子momentum,以及权重衰减weight_decay外,仍然设置学习率lr=1e-2,动量因子momentum=.5,以及权重衰减weight_decay=1e-2。这些可以理解为缺省值:

  • weight_params参数组没有设置weight_decay,则默认为1e-2

  • bias_params参数组没有设置momentum,则默认为.5

  • 倘若weight_paramsbias_params未能覆盖所有模型参数(当然上例中模型参数不存在未分组的情形),剩余模型参数的优化默认值即lr=1e-2, momentum=.5, weight_decay=1e-2

其实上述这些参数名称其实都是 PyTorch \\text{PyTorch} PyTorch中默认给出的,比如:

  1. conv1.weight对应模块model.conv1.weight1

  2. layer1.0.conv1.weight对应模块model.layer1[0].conv1.weight

  3. layer3.0.downsample.0.weight对应模块model.layer3[0].downsample[0].weight

根据这种默认的命名规则,当然可以写一个函数来根据参数名称直接调用到对应的参数变量(详见第 3 3 3),可能有人会疑惑使用for name, parameter in model.named_parameters(): ...时,不就已经可以通过parameter调用参数名称为name的变量了嘛,为什么不能这样调用的原因笔者在第 3 3 3节中将具体说明。

笔者想吐槽的问题是,为什么 P y T o r c h \\rm PyTorch PyTorch没有开放给模型参数自主命名,然后通过自主命名直接调用到模型参数的方法呢?明明 PyTorch \\text{PyTorch} PyTorch里都可以对torch.tensor的张量型变量命名,实话说有点无语。

本小节告一段落,接下来让我们来看看 PyTorch \\text{PyTorch} PyTorch损失函数中的模型参数分组。

1.3 1.3 1.3 PyTorch \\text{PyTorch} PyTorch损失函数中的模型参数分组

PyTorch \\text{PyTorch} PyTorch损失函数中的模型参数分组仍然可以用相同的方法,即通过model.named_parameters()生成器来实现,以这里以 SVD training \\text{SVD training} SVD training实现中的带奇异向量的正交正则器 Singular vectors orthogonality regularizer \\text{Singular vectors orthogonality regularizer} Singular vectors orthogonality regularizer)与奇异值稀疏导出正则器 Singular values sparsity-inducing regularizer \\text{Singular values sparsity-inducing regularizer} Singular values sparsity-inducing regularizer)的交叉熵损失函数代码为例,第一版的代码实现如下:

# -*- coding: utf-8 -*-
# @author: caoyang
# @email: caoyang@163.sufe.edu.cn

import torch
from torch.nn import functional as F

class CrossEntropyLossSVD(torch.nn.CrossEntropyLoss):

	def __init__(self, weight=None, size_average=None, ignore_index=-100, reduce=None, reduction='mean'):
		super(CrossEntropyLossSVD, self).__init__(
			weight=weight,
			size_average=size_average,
			ignore_index=ignore_index,
			reduce=reduce,
			reduction=reduction,
		)

	def forward(self, input, target, model=None, regularizer_weights=[1, 1], orthogonal_suffix='svd_weight_matrix', sparse_suffix='svd_weight_vector', mode='lh') -> torch.FloatTensor:
		cross_entropy_loss = F.cross_entropy(input, target, weight=self.weight, ignore_index=self.ignore_index, reduction=self.reduction)
		if model is None:
			return cross_entropy_loss
		
		# 正交正则器
		def _orthogonality_regularizer(x):		 		 				 # x应当是一个2D张量(矩阵)且高大于宽
			return torch.norm(torch.mm(x.t(), x) - torch.eye(x.shape[1]).cuda(), p='fro') / x.shape[1] / x.shape[1]
		
		# 稀疏导出正则器
		def _sparsity_inducing_regularizer(x, mode='lh'):				 # x应当是一个1D张量(向量)
			mode = mode.lower()
			if mode == 'lh':
				return torch.norm(x, 1) / torch.norm(x, 2)	
			elif model == 'l1':
				return torch.norm(x, 1)
			raise Exception(f'Unknown mode: {mode}')
	
		regularizer = torch.zeros(1, ).cuda()
		for name, parameter in model.named_parameters():
			lastname = name.split('.')[-1]
			if lastname.startswith(orthogonal_suffix):					 # 奇异向量矩阵参数:添加正交正则项
				regularizer += _orthogonality_regularizer(parameter) * regularizer_weights[0]
			elif lastname.startswith(sparse_suffix):					 # 奇异值向量参数:添加稀疏导出正则项
				regularizer += _sparsity_inducing_regularizer(parameter, mode) * regularizer_weights[1]
		return cross_entropy_loss + regularizer

可以看到笔者是在forward函数中进行模型参数分组的,并将model作为参数传入了forward函数。然后笔者就发现这样写会导致损失函数的计算特别的慢,后来发现其实与优化器相同,也可以直接将两类参数存成list传入,这样就不需要将model作为参数传入且无需每次计算损失函数时都执行一次模型参数分组逻辑。

改良后的写法如下所示:

# -*- coding: utf-8 -*-
# @author: caoyang
# @email: caoyang@163.sufe.edu.cn

import torch
from torch import nn
from torch.nn import functional as F

class CrossEntropyLossSVD(nn.CrossEntropyLoss<

以上是关于项目总结SVD Training(终焉)的主要内容,如果未能解决你的问题,请参考以下文章

SVD原理及代码实现

机器学习中的矩阵分解LU分解QR分解SVD分解

回归 | js实用代码片段的封装与总结(持续更新中...)

基于用户、基于项目和SVD的协同过滤Python代码

项目开发收尾总结(片段)

VsCode 代码片段-提升研发效率