HYLOVEYOURSELF

小洋宝的进步史

导航

< 2025年2月 >
26 27 28 29 30 31 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 1
2 3 4 5 6 7 8

统计

Datawhale组队学习 深入浅出Pytorch Task 3🔊

Datawhale组队学习

Pytorch模型定义

作者:博客园-岁月月宝贝

参考视频:Pytorch模型定义与训练技巧_哔哩哔哩_bilibili

代码来源:[thorough-pytorch/notebook/第六章 PyTorch进阶训练技巧/PyTorch模型定义与进阶训练技巧.ipynb at main · datawhalechina/thorough-pytorch](https://github.com/datawhalechina/thorough-pytorch/blob/main/notebook/第六章 PyTorch进阶训练技巧/PyTorch模型定义与进阶训练技巧.ipynb)

讲解内容:thorough-pytorch/source/第五章 at main · datawhalechina/thorough-pytorch

结论——本笔记荟萃天地精华🐶

导引:本节重点如下:

  • 模型定义方式
  • 利用模型快速搭建复杂网络
  • 模型修改
  • 模型保存与读取
  • 自定义损失函数
  • 动态调整学习率
  • 模型微调
  • 半精度训练😘

本章将结合U-Net模型来探索PyTorch的模型定义方式和进阶训练技巧。下方每个“Point”对应于教程中每一节的内容😊。

预备工作:导包~

import os
import numpy as np
import collections
import torch
import torch.nn as nn
import torch.nn.functional as F
import torchvision

下面直接进入第一个要点:

Point 1:模型定义方式

PyTorch中自定义模型主要通过以下三种方式:

  • Sequential
  • ModuleList
  • ModuleDict

下面我们依次来看!

先看大哥Sequential !

Sequential

Sequential也分为两种类别,一个是Direct list ,另一个是Ordered Dict❤

1️⃣ Direct list

方式:前后列开就好啦~

import torch.nn as nn
net1 = nn.Sequential(
nn.Linear(784, 256),#输入节点数:784(手写数字拉长版);隐层节点数:256
nn.ReLU(),#激活下
nn.Linear(256, 10), #输出节点数为10
)
print(net1)

输出:

image-20250216161411902

可见net1包含3层,两个Linear的映射层,一个Relu的激活层

2️⃣Ordered Dict

方式:带顺序的字典(Python字典原来无序~),因为我们层与层之间需要顺序

试试下面的代码:

import collections
import torch.nn as nn
net2 = nn.Sequential(collections.OrderedDict([
('fc1', nn.Linear(784, 256)),
('relu1', nn.ReLU()),
('fc2', nn.Linear(256, 10))
]))
print(net2)

咳咳:

image-20250216161828327

你会发现输出是相同的!唯一不一样的地方在于标号放在了层的前面,比如fc1,就是第一个fully connect层~

Ordered Dict为我们实现了模型层的命名,更有利于我们实现各个层的功能!

👌现在两个层形状相同,那么我们输入一个东西,输出会相同嘛?

a = torch.rand(4,784)#4表示batchsize,784代表输入维度
out1 = net1(a)
out2 = net2(a)
print(out1.shape==out2.shape, out1.shape)

运行下:

image-20250216162515870

两个输出的维度是相等哒~还可以看到输出结果的batchsize是4,维度是10

😥那两个网络完全一样嘛?

答:不能保证,因为初始化两个网络时,网络中的参数是随机产生的

了解了大哥(他有逐层定义这个唯一缺点),现在我们来看二哥ModuleList——

ModuleList

这边对你的层里有很多层结构是一样的模型的状况比较友好(见代码第一行)

然后因为是list,所以可以使用append增加里面的元素(见代码),还可以用索引看每层是什么——

net3 = nn.ModuleList([nn.Linear(784, 256), nn.ReLU()])#list还可以使用自动化添加方法:[nn.Linear(x,x), for i in range(5)]
net3.append(nn.Linear(256, 10)) # 类似List的append操作
print(net3[-1]) # 类似List的索引访问
print(net3)

(可以发现这个结构排布和上面网络的结构是一样的)

image-20250216163926424

-1也帮我们索引到了模型-1层

但是注意⭐ModuleList 并没有定义一个网络,它只是将不同的模块储存在一起。

out3 = net3(a)

会发现报错:

image-20250216164156749

就是你没有实现这个网络排布,模型不知道前向传播的顺序😋

但是ModuleList可以写在初始化函数中,我们利用初始化函数,可以把模型定义好;接着,我们可以使用forward函数,把模型从前一层传到后一层的顺序定义好:(下面我们还输入了我们定义好的张量a)

class Net3(nn.Module):#继承nn.Module
def __init__(self):
super().__init__()#实例化
self.modulelist = nn.ModuleList([nn.Linear(784, 256), nn.ReLU()])
self.modulelist.append(nn.Linear(256, 10))
def forward(self, x):
for layer in self.modulelist:
x = layer(x)
return x
net3_ = Net3()
out3_ = net3_(a)
print(out3_.shape)

输出:

image-20250216164728048

形状与前面两个torch相同!

了解了对于层复用友好的二哥,最后我们来介绍弟弟ModuleDict~

ModuleDict

他和他大哥的分身Sequential里面的Ordered Dict很像,都可以指定层和层的名称,以字典的形式(还可通过索引访问!)但是缺点又和他二哥很像,它并没有把网络连起来(没有定义一个网络),只是把这些结构组织在了一起:

我们先构成网络,用索引访问下:

net4 = nn.ModuleDict({
'linear': nn.Linear(784, 256),
'act': nn.ReLU(),
})
net4['output'] = nn.Linear(256, 10) # 添加
print(net4['linear']) # 索引1
print(net4.output)#索引2

输出:

image-20250216172512284

“没有定义一个网络”的证明:

out4 = net4(a)

错误警告:

image-20250216173039574

那么,正确的方式是什么呢?

先用init定义好层名+层,然后用forward“遍历”-----这里的遍历当然不如上面的list方便,需要你一个个写层名来遍历⭐

以上我们就讲完了本章的第一个点~下面我们来看如何用模型块快速搭建网络😊

Point 2:利用模型块快速搭建复杂网络

下面我们开始探索如何利用模型块,快速构建U-Net网络

why U-Net?

network architecture

U-Net是一种非常典型的模块化网络,“模块化”意味着你只需要定义好网络的几个主要模块,然后再用一个模型(总模型)把这些模块串起来。这些模块通过不同的实例还可以进行复用,我们得到这些模块之间的连接关系后就能更加快速简便地去构建这个网络😊

  1. max-pool理解为降维会更简单一些哦
  2. up-conv上卷积,这样尺寸会增大,但是通道数会更少
  3. copy and crop:灰色线,copy过来,crop到一样的大小(这里还堆叠起来,一起作卷积),还叫跳连接(skip connection)

总结下U-Net的主要模块(四个模块-一个操作):

两次卷积(double conv)→ 下采样(max pooling)→ 上采样(up sampling)→skip connection是一个数据连接操作,但我们在实现时,有自己独特的实现方式,所以我们不把它作为一个模块来看待 → output是最后一个模块,但它只是一个单一的模块,它不复用

下面代码主要参考:https://github.com/milesial/Pytorch-UNet

导入必要包

import os
import numpy as np
import collections
import torch
import torch.nn as nn
import torch.nn.functional as F
import torchvision

双次卷积

class DoubleConv(nn.Module):
"""(convolution => [BN] => ReLU) * 2"""
def __init__(self, in_channels, out_channels, mid_channels=None):#mid_channels:第一次卷积的输出&第二次卷积的输入
super().__init__()
if not mid_channels:
mid_channels = out_channels
self.double_conv = nn.Sequential(#可见我们这边是用大哥Sequential的方式作的定义
nn.Conv2d(in_channels, mid_channels, kernel_size=3, padding=1, bias=False),#in_channels到mid_channels的卷积
nn.BatchNorm2d(mid_channels),
nn.ReLU(inplace=True),#激活
nn.Conv2d(mid_channels, out_channels, kernel_size=3, padding=1, bias=False),#mid_channels到out_channels的卷积
nn.BatchNorm2d(out_channels),
nn.ReLU(inplace=True)
)
def forward(self, x):
return self.double_conv(x)#我们的Sequential

下采样

class Down(nn.Module):
"""Downscaling with maxpool then double conv"""
def __init__(self, in_channels, out_channels):
super().__init__()
self.maxpool_conv = nn.Sequential(#仍然为我们的大哥方式
nn.MaxPool2d(2),#先降低尺寸
DoubleConv(in_channels, out_channels)#复用了前面的DoubleConv模块
)
def forward(self, x):
return self.maxpool_conv(x)#我们的Sequential

上采样

其中也作了skip connection

class Up(nn.Module):
"""Upscaling then double conv"""
def __init__(self, in_channels, out_channels, bilinear=True):
super().__init__()
# if bilinear, use the normal convolutions to reduce the number of channels
if bilinear:#上采样的插值方式
self.up = nn.Upsample(scale_factor=2, mode='bilinear', align_corners=True)#上采样
self.conv = DoubleConv(in_channels, out_channels, in_channels // 2)#双卷积
else:
self.up = nn.ConvTranspose2d(in_channels, in_channels // 2, kernel_size=2, stride=2)
self.conv = DoubleConv(in_channels, out_channels)
def forward(self, x1, x2):#这个特殊一点,因为里面包含了skip connection
x1 = self.up(x1)
# input is CHW
diffY = x2.size()[2] - x1.size()[2]
diffX = x2.size()[3] - x1.size()[3]
x1 = F.pad(x1, [diffX // 2, diffX - diffX // 2,
diffY // 2, diffY - diffY // 2])
# if you have padding issues, see
# https://github.com/HaiyongJiang/U-Net-Pytorch-Unstructured-Buggy/commit/0e854509c2cea854e247a9c615f175f76fbb2e3a
# https://github.com/xiaopeng-liao/Pytorch-UNet/commit/8ebac70e633bac59fc22bb5195e513d5832fb3bd
x = torch.cat([x2, x1], dim=1)#精髓点,cat完成了叠加
return self.conv(x)

Out模块

使得channel数目达到最后的channel数量;关于最后的channel数量:如果你作的是二分类,最后的channel数就等于1或2(取决于你的损失函数如何定义),如果channel数大于1,那么你的channel数最后就是更大的数。

class OutConv(nn.Module):
def __init__(self, in_channels, out_channels):
super(OutConv, self).__init__()
self.conv = nn.Conv2d(in_channels, out_channels, kernel_size=1)#只有卷积操作
def forward(self, x):
return self.conv(x)

组装

利用模型块,构建UNet网络

UNet只需要两个输入:n_channels表示输入通道数,比如RGB就是三通道;n_classes需要输出几个类别,这边就输入几。

class UNet(nn.Module):
def __init__(self, n_channels, n_classes, bilinear=True):
super(UNet, self).__init__()
self.n_channels = n_channels
self.n_classes = n_classes
self.bilinear = bilinear
#下面我们在定义模型参数
self.inc = DoubleConv(n_channels, 64)#你会发现参数连续
self.down1 = Down(64, 128)
self.down2 = Down(128, 256)
self.down3 = Down(256, 512)
factor = 2 if bilinear else 1#factor取决于插值方式 bilinear是双线性插值,如果是双线性插值factor设为2
self.down4 = Down(512, 1024 // factor)
self.up1 = Up(1024, 512 // factor, bilinear)#上采样的通道数在逐渐减小
self.up2 = Up(512, 256 // factor, bilinear)
self.up3 = Up(256, 128 // factor, bilinear)
self.up4 = Up(128, 64, bilinear)
self.outc = OutConv(64, n_classes)
def forward(self, x):#优美数据流
x1 = self.inc(x)#x输入进来,过inchannel,作第一次的DoubleConv
x2 = self.down1(x1)#down里面包含了DoubleConv(所以不用再次调用
x3 = self.down2(x2)
x4 = self.down3(x3)
x5 = self.down4(x4)#下采样后上采样
x = self.up1(x5, x4)#上采用有两个输入,因为有“灰色箭头”
x = self.up2(x, x3)
x = self.up3(x, x2)
x = self.up4(x, x1)
logits = self.outc(x)#上采用输入outputcov得到logits
return logits#是作sigmoid激活前的一个数

运行

下面适用于黑白的二分类问题

unet = UNet(3,1)
print(unet)

输出大家可以慢慢看

image-20250216210127642 image-20250216210152852

我们现在已经实现了UNet操作,下面我们来进行下模型后面的操作😀

大家有没有一个疑惑,前面针对的是单分类问题,那么如果我们那天需要多分类该怎么办?(虽然可以在最后“UNet(3,1)”直接改那个1,但是我们是不是还能深入一下?)

Point 3:模型修改

这里我们假设最后的分割是多类别的(即mask不止0和1,还有2,3,4等值代表其他目标),需要对模型特定层进行修改。
此外还有两种情况的模型修改方式,这里也做演示:

  • 添加额外输入
  • 添加额外输出

(这边还会着重讲下灰色箭头是怎么cat的,为什么batchsize可以随意增加)

修改特定层

import copy
unet1 = copy.deepcopy(unet)#unet1模型结构和unet会完全一样
print(unet1.outc)#这里是看下outputconv层

输出:

image-20250216211100177

那么我们有没有办法修改outc层?把64到1改为64到2?等等等等一个我想要的数字?

下面我们先创造一个噪声图片(3个channel batchsize是1 大小是224)

b = torch.rand(1,3,224,224)
out_unet1 = unet1(b)
print(out_unet1.shape)

输出:

image-20250216211701638

可见输出channel仍然为1,

但是我们想输出5个channel 每个channel分别代表我们想要的5种类别的概率——

现在大家是不是忘了什么东西?我们的Unet网络outConv好像是索引到的:

image-20250216212023257

那我们是不是可以为它重新赋值?见下(别忘了OutConv是我们前面定义过的,现在属于unet1的模块哦)

unet1.outc = OutConv(64, 5)
print(unet1.outc)

大家可以发现输出通道数变啦:

image-20250216212456670

为了再次验证输出通道数变为5这个好消息,我们再次把噪声图像输入试一下:

out_unet1 = unet1(b)
print(out_unet1.shape)

输出通道数变成5啦!

image-20250216212747411

(batchsize和channel数都没变)

然后midchannel数也能改,举一反三

添加额外输入

这里我们创一个新模型unet2:

class UNet2(nn.Module):
def __init__(self, n_channels, n_classes, bilinear=True):#这边也是模块的堆叠
super(UNet2, self).__init__()
self.n_channels = n_channels
self.n_classes = n_classes
self.bilinear = bilinear
self.inc = DoubleConv(n_channels, 64)
self.down1 = Down(64, 128)#B
self.down2 = Down(128, 256)#C
self.down3 = Down(256, 512)
factor = 2 if bilinear else 1
self.down4 = Down(512, 1024 // factor)
self.up1 = Up(1024, 512 // factor, bilinear)
self.up2 = Up(512, 256 // factor, bilinear)
self.up3 = Up(256, 128 // factor, bilinear)
self.up4 = Up(128, 64, bilinear)
self.outc = OutConv(64, n_classes)
#那么我们怎么增加额外输入呢?一个是在forward里面加一个add_variable
def forward(self, x, add_variable):
#接着你可以在forward下面的流程中把这个新加的变量用起来
x1 = self.inc(x)
x2 = self.down1(x1)#A
x3 = self.down2(x2)
x4 = self.down3(x3)
x5 = self.down4(x4)
x = self.up1(x5, x4)
x = self.up2(x, x3)
x = self.up3(x, x2)
x = self.up4(x, x1)
x = x + add_variable #修改点(如果add_variable是常数,可以把x的每一场都加上这个数,如果add_variable是和x一样大小的一个mask,你同样可以给x都加上这样一个add_variable)
logits = self.outc(x)
return logits
unet2 = UNet2(3,1)
c = torch.rand(1,1,224,224)#因为我们加了 add_variable,所以这边我们还能再添加一个输入
out_unet2 = unet2(b, c)
print(out_unet2.shape)

好啦,我们看输出:

image-20250216224340568

出于好玩的目的x想加哪层加哪层,但是你在def forward里面改了后别忘了修改def __init__,比如A处加了一个节点,B和C处的128都应该变成129

总之,上面模型块的定义要和下面的forward流进行匹配

添加额外输出

同样,我们复制上上面未加输入变量的unet2,把它修改为unet3

有些论文的模型作unet分割时想从底层(bottom neck)引出作一个分类的监督——那怎么从中间层输出呢?那我们就去改

class UNet3(nn.Module):
def __init__(self, n_channels, n_classes, bilinear=True):
super(UNet3, self).__init__()
self.n_channels = n_channels
self.n_classes = n_classes
self.bilinear = bilinear
self.inc = DoubleConv(n_channels, 64)
self.down1 = Down(64, 128)
self.down2 = Down(128, 256)
self.down3 = Down(256, 512)
factor = 2 if bilinear else 1
self.down4 = Down(512, 1024 // factor)
self.up1 = Up(1024, 512 // factor, bilinear)
self.up2 = Up(512, 256 // factor, bilinear)
self.up3 = Up(256, 128 // factor, bilinear)
self.up4 = Up(128, 64, bilinear)
self.outc = OutConv(64, n_classes)
def forward(self, x):#改return就OK
x1 = self.inc(x)
x2 = self.down1(x1)
x3 = self.down2(x2)
x4 = self.down3(x3)
x5 = self.down4(x4)#比如这里x5是最下面这一层
x = self.up1(x5, x4)
x = self.up2(x, x3)
x = self.up3(x, x2)
x = self.up4(x, x1)
logits = self.outc(x)
return logits, x5 # 修改点就是把x5来return出就好
unet3 = UNet3(3,1)
#c = torch.rand(1,1,224,224)
out_unet3, mid_out = unet3(b)#输出时别忘了设几个变量接着!
print(out_unet3.shape, mid_out.shape)#mid_out.shape就是x5也就是bottom neck的一个维度;out_unet3.shape应该与原来(未改时)一致

输出:

image-20250216225510978

成功!

Point 4:模型保存与读取

这里用我们原始的unet模型(输入channel是3,输出channel是1),注意前面我们改输入输出只改了unet1和2

❤注意我们要看两种训练模式(单卡和多卡并行情况)下的两种保存类型(指保存整个模型or保存模型权重)——so下面我们将进行四分类讨论

首先我们先了解下模型权重是啥?

print(unet.state_dict())

输出:

image-20250216230604702 image-20250216230624027

可见为字典的格式的好长一堆——这,就是权重!

知道我们要保存的权重是什么东西后,第二个就是要知道我们要保存的这个权重为什么格式:

模型格式其实有三种,一种是pt,一种是pth,一种是pkl(但是ljq说这3种其实没什么区别)

下面我们开始四分类讨论!

CPU或单卡:保存&读取整个模型

这是四种方法里最简单的方法,我们叫它“百事通大姐”

torch.save(unet, "./unet_example.pth")#“”里面是保存的路径
loaded_unet = torch.load("./unet_example.pth")
print(loaded_unet.state_dict())#和print(unet.state_dict())会完全一样

输出里面出了和上面完全一样的部分外还多了一个.pth文件:

image-20250216231602725

下面是二姐,和大姐超级像:

CPU或单卡:保存&读取模型权重

保存模型权重流程和大姐很像,只是加载模型权重复原模型时略有区别

torch.save(unet.state_dict(), "./unet_weight_example.pth")
loaded_unet_weights = torch.load("./unet_weight_example.pth")
unet.load_state_dict(loaded_unet_weights)#加载权重
print(unet.state_dict())

输出内容unet.state_dict()部分和上面不能说是完全相同,只能说是毫无差别,unet_weight_example.pth文件和上面大姐的.pth文件也基本一样大……所以下次遇到CPU或单卡情况,大家无脑保存模型就OK了(虽说权重涉及信息也少不了多少)

目录里面多出的小东西:

image-20250216232538564

下面再看三姐:

⭐多卡的情况就完全不一样了哦

因为单卡保存的模型是cuda0 ,而多卡保存的模型实际是分布在多个GPU上,比如cuda1和cuda2,这就导致如果你模型保存了再在换了显卡或者是改变了显卡数量的情况下想把模型加载回来,就很难把现在的模型与你之前的模型加载回来。

但是如果你使用的显卡固定,且不想把模型传Github(读取后训练环境可能不同!),像三姐这样就OK

多卡:保存&读取整个模型

❤下面这四行代码只要使用多卡就必须加

os.environ['CUDA_VISIBLE_DEVICES'] = '2,3'
unet_mul = copy.deepcopy(unet)
unet_mul = nn.DataParallel(unet_mul).cuda()#DataParallel用来进行一个多卡的分布
print(unet_mul)

输出(作者的输出,因为我只有4060):

image-20250216234459190

可以看到最大的区别是模型前面多了一个module,也是它的存在,导致多卡保存和加载有所不同。

下面来保存和加载下:

torch.save(unet_mul, "./unet_mul_example.pth")
loaded_unet_mul = torch.load("./unet_mul_example.pth")
print(loaded_unet_mul)

这边因为卡没有换,所以输出和“print(unet_mul)”的输出相同:

image-20250216234935489

也就是正常加载了,但三姐方法不保险,我们再看妹妹:

多卡:保存&读取模型权重

权重保存,加载和使用和二姐会很像

另外注意这里是多卡保存,多卡读取

torch.save(unet_mul.state_dict(), "./unet_weight_mul_example.pth")
loaded_unet_weights_mul = torch.load("./unet_weight_mul_example.pth")
unet_mul.load_state_dict(loaded_unet_weights_mul)#参数加载回来
unet_mul = nn.DataParallel(unet_mul).cuda()#还要分布再不同的卡上
print(unet_mul.state_dict())

但是多卡保存,单卡读取的话,因为单卡不含module,所以上面第一行需要改为“ torch.save(unet_mul.module.state_dict(), "./unet_weight_mul_example.pth")”。

PS:教程还有load dict和使用load dict赋值的方式对模型进行加载,见[thorough-pytorch/source/第五章/5.4 PyTorh模型保存与读取.md at main · datawhalechina/thorough-pytorch](https://github.com/datawhalechina/thorough-pytorch/blob/main/source/第五章/5.4 PyTorh模型保存与读取.md)

输出:

image-20250217000029918

可以发现加载后是一样的~

另外,如果你复现别人论文遇到别人就给的是整个模型,怎么办?那就先加载整个模型&提取权重,再以权重的方式构建本地模型

unet_mul.state_dict = loaded_unet_mul.state_dict
unet_mul = nn.DataParallel(unet_mul).cuda()
print(unet_mul.state_dict())

输出:

image-20250217000625940

看到这里的助教姐姐/妹妹们辛苦啦!😘你们对我的支持是我写笔记莫大的鼓励!

附:https://www.cnblogs.com/HYLOVEYOURSELF/p/18716442 为Task2(加了很多复现时的易错点处理)

https://www.cnblogs.com/HYLOVEYOURSELF/p/18710835 为Task1

请家人们多多点赞收藏+关注!

posted on   岁月月宝贝  阅读(15)  评论(0编辑  收藏  举报

相关博文:
阅读排行:
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· spring官宣接入deepseek,真的太香了~
点击右上角即可分享
微信分享提示