这里关于迁移学习与微调的内容不再细说,有关概念可以参考:https://blog.csdn.net/weixin_44751294/article/details/116844391
1. 迁移学习
这里介绍迁移学习调用模型的我用过的方法,关键步骤是pretrained=True,使用预训练的参数。
1.1 使用list列表直接截取
这种方法直接使用了list截取了前面部分的网络结构,然后用Flatten()对数据进行处理,再添加最后一层全连接层来实现分类
trained_model = resnet18(pretrained=True) model = nn.Sequential(*list(trained_model.children())[:-1], # torch.Size([32, 512, 1, 1]) Flatten(), # torch.Size([32, 512]) nn.Linear(512, 5) # torch.Size([32, 5]) )
其中,Flatten的代码如下:
# 打平操作 class Flatten(nn.Module): def __init__(self): super(Flatten, self).__init__() def forward(self, x): shape = torch.prod(torch.tensor(x.shape[1:])).item() return x.view(-1, shape)
1.2 直接对结构进行修改
同样是使用迁移学习,这种方法不需要使用什么特别的函数,直接用自定义的全连接层替换原本resnet的全连接层即可,比较方便。
finetune_net = torchvision.models.resnet18(pretrained=True) finetune_net.fc = nn.Linear(finetune_net.fc.in_features, 2)
直接打印模型print(finetune_net),或者通过finetune_net.modules即可输出修改后的网络结构,如下图所示:
比如:
# 定义网络结构 class Net(nn.Module): def __init__(self): super(Net, self).__init__() self.conv1 = nn.Conv2d(3, 6, 5) self.pool1 = nn.MaxPool2d(2, 2) self.conv2 = nn.Conv2d(6, 16, 5) self.pool2 = nn.MaxPool2d(2, 2) self.fc1 = nn.Linear(16 * 5 * 5, 120) self.fc2 = nn.Linear(120, 84) self.fc3 = nn.Linear(84, 10) self.initialize_weights() # for m in self.modules(): # print(m) def forward(self, x): x = self.pool1(F.relu(self.conv1(x))) x = self.pool2(F.relu(self.conv2(x))) x = x.view(-1, 16 * 5 * 5) x = F.relu(self.fc1(x)) x = F.relu(self.fc2(x)) x = self.fc3(x) return x # 定义权值初始化 def initialize_weights(self): # 其中self.modules()展示网络的层结构 for m in self.modules(): # 对nn.Conv2d层进行处理 if isinstance(m, nn.Conv2d): # 采用 torch.nn.init.xavier_normal 方法对该层的 weight 进行初始化 torch.nn.init.xavier_normal_(m.weight.data) # 并判断是否存在偏置(bias),若存在,将 bias 初始化为全 0 if m.bias is not None: m.bias.data.zero_() # 对BatchNorm2d层进行处理 elif isinstance(m, nn.BatchNorm2d): # 对于BatchNorm2d,其是不需要bias的,所以对应的bias设置为0;其他设置为1,以下是两种设置方法,功能是一样的 # 方法1: m.weight.data.fill_(1) m.bias.data.zero_() # 方法2:使用常数初始化 # nn.init.constant_(m.weight, 1) # nn.init.constant_(m.bias, 0) # 对Linear层进行处理 elif isinstance(m, nn.Linear): # 正态分布初始化,使值服从正态分布 N(mean, std) torch.nn.init.normal_(m.weight.data, 0, 0.01) m.bias.data.zero_() net = Net() # self.modules()结果展示如下 net.modules
<bound method Module.modules of Net( (conv1): Conv2d(3, 6, kernel_size=(5, 5), stride=(1, 1)) (pool1): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False) (conv2): Conv2d(6, 16, kernel_size=(5, 5), stride=(1, 1)) (pool2): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False) (fc1): Linear(in_features=400, out_features=120, bias=True) (fc2): Linear(in_features=120, out_features=84, bias=True) (fc3): Linear(in_features=84, out_features=10, bias=True) )>
2. 微调
对于加载预训练的参数,我也使用过两种方法,这里记录一下。
在此之前,需要注意,新增加的全连接层的参数是随机的,所以需要对其进行初始化操作,这里使用Xavier均匀分布(avier 初始化方法中服从均匀分布 U(−a,a)):
nn.init.xavier_uniform_(finetune_net.fc.weight)
输出:
Parameter containing: tensor([[ 0.1061, -0.0440, 0.0247, ..., 0.0166, 0.0326, -0.0786], [-0.0324, -0.0513, -0.0037, ..., -0.0276, -0.0958, 0.0679]], requires_grad=True)
其他的初始化方法可以参考博文:pytorch中的权值初始化方法,这里不再做详细介绍。
2.1 冻结层
由于上诉例子,只是修改了最后的一层全连接层,对于该层之外的全部层可以冻结其参数,只训练最后的全连接层,做法如下。
# 冻结除最后一层外的全部参数 for name, param in finetune_net.named_parameters(): if name not in ["fc.weight", "fc.bias"]: # print(name) param.requires_grad=False # 关键一步,设置为False之后,优化器溜不会对参数进行更新
当param.requires_grad=False的时候,表示该参数没有梯度信息,也就是不会反向传播更新参数,也就是相当于冻结了,数值保持不变。
查看效果:
for name, param in finetune_net.named_parameters(): print(name) print(param)
可以看见,除了最后的一层全脸曾的权值与偏置,其他的requires_grad=False,全部被冻结。
那么,现在可以把全部参数送入优化器,优化器也只会对全连接层的数据进行处理。
# 这个优化器只对最后一层的参数进行更新,可以加快训练速度 optimizer = torch.optim.SGD(finetune_net.fc.parameters(), lr=1e-2, momentum=0.9) # 或者是这一条,个人感觉这两句的效果是一致的(如有错误恳请指出) # optimizer = torch.optim.SGD(finetune_net.parameters(), lr=learning_rate, weight_decay=0.001) print(optimizer)
输出:
SGD ( Parameter Group 0 dampening: 0 lr: 0.01 momentum: 0.9 nesterov: False weight_decay: 0 )
2.2 分组设置参数组
由于这里我们只需要对最后一层修改后的全连接层进行训练,而对其他层不需要怎么训练,根据这种特性,可以分为两个参数组丢进优化器中。
learning_rate = 1e-3 # 获取除了最后一层全连接层的全部层参数, 因为这部分数据参数只需要微调 params_1x = [param for name, param in finetune_net.named_parameters() if name not in ["fc.weight", "fc.bias"]] # 设置了两个参数组 # 其中学习率是不同的,对于其他组的学习率会低点,全连接组的学习率会高点 trainer = torch.optim.SGD([{'params': params_1x}, # 其他组 {'params': finetune_net.fc.parameters(),'lr': learning_rate * 10}], # 全连接组 lr=learning_rate, weight_decay=0.001) print(trainer)
输出:
# 这里也分别对应了两组参数 SGD ( Parameter Group 0 dampening: 0 lr: 0.001 momentum: 0 nesterov: False weight_decay: 0.001 Parameter Group 1 dampening: 0 lr: 0.01 momentum: 0 nesterov: False weight_decay: 0.001 )
- 这里进一步说明一下设置两组的意图:
在利用 pre-trained model 的参数做初始化之后,我们可能想让 fc 层更新相对快一些,而希望前面的权值更新小一些,这就可以通过为不同的层设置不同的学习率来达到此目的。为不同层设置不同的学习率,主要通过优化器对多个参数组进行设置不同的参数。所以,只需要将原始的参数组,划分成两个,甚至更多的参数组,然后分别进行设置学习率。这里将原始参数“切分”成 fc3 层参数和其余参数,为 fc3 层设置更大的学习率。
对参数分组进行训练同样也是一种方法,但是时间可能比冻结需要的时间要长点。
在这一节中涉及到了设置学习率的问题,在之后我会再对如何动态的调整学习率做一个笔记总结。之后再接触到其他方法再进行补充。
3. 优化器基类:Optimizer
对于参数组的设置,其实是优化器基类的一个参数,这里补充一下这方面的笔记。
当数据、模型和损失函数确定,任务的数学模型就已经确定,接着就要选择一个合适的优化器(Optimizer)对该模型进行优化。
PyTorch 中所有的优化器(如:optim.Adadelta、optim.SGD、optim.RMSprop 等)均是Optimizer 的子类,Optimizer 中定义了一些常用的方法,有 zero_grad()、step(closure)、state_dict()、load_state_dict(state_dict)和add_param_group(param_group)
1. param_groups
认识 Optimizer 的方法之前,需要了解一个概念,叫做参数组(param_groups)。在finetune,某层定制学习率,某层学习率置零操作中,都会设计参数组的概念,因此首先了解参数组的概念非常有必要。
optimizer 对参数的管理是基于组的概念,可以为每一组参数配置特定的lr,momentum,weight_decay 等等。
参数组在 optimizer 中表现为一个 list(self.param_groups),其中每个元素是dict,表示一个参数及其相应配置,在 dict 中包含’params’、‘weight_decay’、‘lr’ 、'momentum’等字段。
import torch import torch.optim as optim w1 = torch.randn(2, 2) w1.requires_grad = True w2 = torch.randn(2, 2) w2.requires_grad = True w3 = torch.randn(2, 2) w3.requires_grad = True # 一个参数组 optimizer_1 = optim.SGD([w1, w3], lr=0.1) print('len(optimizer.param_groups): ', len(optimizer_1.param_groups)) # print(optimizer_1.param_groups, '\n') # 两个参数组 optimizer_2 = optim.SGD([{'params': w1, 'lr': 0.1}, {'params': w2, 'lr': 0.001}]) print('len(optimizer.param_groups): ', len(optimizer_2.param_groups)) # print(optimizer_2.param_groups)
len(optimizer.param_groups): 1 len(optimizer.param_groups): 2
# 一组参数 optimizer_1.param_groups
[{'params': [tensor([[-0.3037, 1.4797], [ 0.5660, 0.0942]], requires_grad=True), tensor([[0.0423, 0.2110], [1.4629, 0.7293]], requires_grad=True)], 'lr': 0.1, 'momentum': 0, 'dampening': 0, 'weight_decay': 0, 'nesterov': False}]
# 两组参数 optimizer_2.param_groups
[{'params': [tensor([[-0.3037, 1.4797], [ 0.5660, 0.0942]], requires_grad=True)], 'lr': 0.1, 'momentum': 0, 'dampening': 0, 'weight_decay': 0, 'nesterov': False}, {'params': [tensor([[ 0.5763, -0.2417], [ 0.9620, -0.2617]], requires_grad=True)], 'lr': 0.001, 'momentum': 0, 'dampening': 0, 'weight_decay': 0, 'nesterov': False}]
2. zero_grad
功能:将梯度清零
w1 = torch.randn(2, 2) w1.requires_grad = True w2 = torch.randn(2, 2) w2.requires_grad = True optimizer = optim.SGD([w1, w2], lr=0.001, momentum=0.9) optimizer.param_groups[0]['params'][0].grad = torch.randn(2, 2) print('参数w1的梯度:') print(optimizer.param_groups[0]['params'][0].grad, '\n') # 参数组,第一个参数(w1)的梯度 optimizer.zero_grad() print('执行zero_grad()之后,参数w1的梯度:') print(optimizer.param_groups[0]['params'][0].grad) # 参数组,第一个参数(w1)的梯度
参数w1的梯度: tensor([[ 2.0855, -1.7181], [ 1.4635, 0.1929]]) 执行zero_grad()之后,参数w1的梯度: tensor([[0., 0.], [0., 0.]])
optimizer.param_groups
[{'params': [tensor([[ 0.7282, -1.6298], [-0.8011, -0.4588]], requires_grad=True), tensor([[-1.9988, -0.2675], [ 0.4767, 0.2058]], requires_grad=True)], 'lr': 0.001, 'momentum': 0.9, 'dampening': 0, 'weight_decay': 0, 'nesterov': False}]
3. state_dict
功能:获取模型当前的参数,以一个有序字典形式返回。这个有序字典中,key 是各层参数名,value 就是参数。
class Net(nn.Module): def __init__(self): super(Net, self).__init__() self.conv1 = nn.Conv2d(3, 1, 3) self.pool = nn.MaxPool2d(2, 2) self.fc1 = nn.Linear(1 * 3 * 3, 2) def forward(self, x): x = self.pool(F.relu(self.conv1(x))) x = x.view(-1, 1 * 3 * 3) x = F.relu(self.fc1(x)) return x net = Net() # 获取网络当前参数 net_state_dict = net.state_dict() net_state_dict
OrderedDict([('conv1.weight', tensor([[[[ 0.1543, -0.1407, 0.0830], [-0.1273, -0.1226, -0.0813], [ 0.0063, 0.0947, 0.0870]], [[-0.0373, -0.0405, -0.1581], [-0.1434, -0.0394, -0.0907], [ 0.1879, 0.0017, 0.1906]], [[-0.0318, -0.1629, -0.0959], [ 0.1870, 0.0410, -0.0414], [ 0.1877, -0.0737, 0.0832]]]])), ('conv1.bias', tensor([0.0677])), ('fc1.weight', tensor([[-0.0222, -0.0308, 0.3086, -0.0744, 0.1465, 0.2873, 0.1144, -0.1305, 0.1582], [-0.1610, 0.2409, 0.0661, -0.1861, -0.2027, 0.1601, -0.2494, 0.1504, 0.1627]])), ('fc1.bias', tensor([-0.2310, 0.2416]))])
4. load_state_dict
功能:将 state_dict 中的参数加载到当前网络,常用于 finetune。
class Net(nn.Module): def __init__(self): super(Net, self).__init__() self.conv1 = nn.Conv2d(3, 1, 3) self.pool = nn.MaxPool2d(2, 2) self.fc1 = nn.Linear(1 * 3 * 3, 2) def forward(self, x): x = self.pool(F.relu(self.conv1(x))) x = x.view(-1, 1 * 3 * 3) x = F.relu(self.fc1(x)) return x def zero_param(self): for m in self.modules(): if isinstance(m, nn.Conv2d): torch.nn.init.constant_(m.weight.data, 0) if m.bias is not None: m.bias.data.zero_() elif isinstance(m, nn.Linear): torch.nn.init.constant_(m.weight.data, 0) m.bias.data.zero_() net = Net() # 保存,并加载模型参数(仅保存模型参数) torch.save(net.state_dict(), 'net_params.pkl') # 假设训练好了一个模型net pretrained_dict = torch.load('net_params.pkl') # 将net的参数全部置0,方便对比 net.zero_param() net_state_dict = net.state_dict() print('conv1层的权值为:\n', net_state_dict['conv1.weight'], '\n') # 通过load_state_dict 加载参数 net.load_state_dict(pretrained_dict) print('加载之后,conv1层的权值变为:\n', net_state_dict['conv1.weight'])
conv1层的权值为: tensor([[[[0., 0., 0.], [0., 0., 0.], [0., 0., 0.]], [[0., 0., 0.], [0., 0., 0.], [0., 0., 0.]], [[0., 0., 0.], [0., 0., 0.], [0., 0., 0.]]]]) 加载之后,conv1层的权值变为: tensor([[[[ 0.1342, 0.0739, 0.1349], [-0.0289, -0.0936, 0.1227], [-0.0100, -0.1250, -0.1766]], [[ 0.1367, 0.0436, 0.1686], [ 0.1190, -0.1689, -0.0090], [-0.0925, 0.1353, -0.0834]], [[ 0.1444, -0.1853, 0.0623], [ 0.1150, 0.1841, 0.0029], [ 0.1390, 0.1746, -0.0154]]]])
pretrained_dict
OrderedDict([('conv1.weight', tensor([[[[ 0.1342, 0.0739, 0.1349], [-0.0289, -0.0936, 0.1227], [-0.0100, -0.1250, -0.1766]], [[ 0.1367, 0.0436, 0.1686], [ 0.1190, -0.1689, -0.0090], [-0.0925, 0.1353, -0.0834]], [[ 0.1444, -0.1853, 0.0623], [ 0.1150, 0.1841, 0.0029], [ 0.1390, 0.1746, -0.0154]]]])), ('conv1.bias', tensor([-0.0964])), ('fc1.weight', tensor([[ 0.0834, 0.3215, -0.2080, 0.1315, 0.0505, -0.2244, 0.1805, 0.1946, 0.0444], [-0.3096, 0.3098, 0.2564, -0.0232, 0.3014, 0.1928, 0.1730, 0.0521, 0.0925]])), ('fc1.bias', tensor([ 0.3252, -0.1319]))])
5. add_param_group
功能:给 optimizer 管理的参数组中增加一组参数,可为该组参数 定制 lr, momentum, weight_decay 等
w1 = torch.randn(2, 2) w1.requires_grad = True w2 = torch.randn(2, 2) w2.requires_grad = True w3 = torch.randn(2, 2) w3.requires_grad = True # 一个参数组 optimizer_1 = optim.SGD([w1, w2], lr=0.1) optimizer_1.param_groups
[{'params': [tensor([[-0.5223, -0.5817], [-3.5183, 1.2317]], requires_grad=True), tensor([[ 0.5794, -1.6020], [-0.5890, 1.3211]], requires_grad=True)], 'lr': 0.1, 'momentum': 0, 'dampening': 0, 'weight_decay': 0, 'nesterov': False}]
# 增加一组参数 optimizer_1.add_param_group({'params': w3, 'lr': 0.001, 'momentum': 0.8}) optimizer_1.param_groups
[{'params': [tensor([[-0.5223, -0.5817], [-3.5183, 1.2317]], requires_grad=True), tensor([[ 0.5794, -1.6020], [-0.5890, 1.3211]], requires_grad=True)], 'lr': 0.1, 'momentum': 0, 'dampening': 0, 'weight_decay': 0, 'nesterov': False}, {'params': [tensor([[0.6763, 1.1298], [0.8228, 0.5904]], requires_grad=True)], 'lr': 0.001, 'momentum': 0.8, 'dampening': 0, 'weight_decay': 0, 'nesterov': False}]
具体的优化器使用方法之后再补充