Pytorch快速指南

一、一个简单完整的CNN网络模型

该章节用于展示一个CNN网络模型的搭建、数据管道设计、模型训练、模型测试整个过程。形成对深度学习模型的整个知识架构的初步认知,以加快相关工作的展开。最主要的原因是,时间稍微一长就忘记了一些深度学习工程实现的一些具体细节,所以通过下面的内容,用于快速回顾。

1.1 数据管道

import torch
from torchvision import datasets, transforms

# 数据预处理和增强
transform = transforms.Compose([
    transforms.Resize((224, 224)),
    transforms.ToTensor(),
    transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))  # 归一化
])

# 加载训练集和验证集
train_dataset = datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
val_dataset = datasets.CIFAR10(root='./data', train=False, download=True, transform=transform)

# 创建数据加载器
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=64, shuffle=True)
val_loader = torch.utils.data.DataLoader(val_dataset, batch_size=64, shuffle=False)

1.2 模型构建

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

class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(3, 16, kernel_size=3, stride=1, padding=1)
        self.pool = nn.MaxPool2d(kernel_size=2, stride=2)
        self.conv2 = nn.Conv2d(16, 32, kernel_size=3, stride=1, padding=1)
        self.fc1 = nn.Linear(32 * 56 * 56, 256)
        self.fc2 = nn.Linear(256, 10)

    def forward(self, x):
        x = self.pool(F.relu(self.conv1(x)))
        x = self.pool(F.relu(self.conv2(x)))
        x = x.view(-1, 32 * 56 * 56)
        x = F.relu(self.fc1(x))
        x = self.fc2(x)
        return x

model = Net()

1.3 模型训练

import torch.optim as optim

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)

criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9)

num_epochs = 10
train_losses = []
val_losses = []

for epoch in range(num_epochs):
    train_loss = 0.0
    val_loss = 0.0
    model.train()
    for images, labels in train_loader:
        images, labels = images.to(device), labels.to(device)
        optimizer.zero_grad()
        outputs = model(images)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()
        train_loss += loss.item() * images.size(0)

    model.eval()
    for images, labels in val_loader:
        images, labels = images.to(device), labels.to(device)
        outputs = model(images)
        loss = criterion(outputs, labels)
        val_loss += loss.item() * images.size(0)

    train_loss /= len(train_loader.dataset)
    val_loss /= len(val_loader.dataset)

    train_losses.append(train_loss)
    val_losses.append(val_loss)

    print(f"Epoch {
      
      epoch+1}/{
      
      num_epochs}, Train Loss: {
      
      train_loss:.4f}, Val Loss: {
      
      val_loss:.4f}")

print("Training finished.")

补充:model.eval()的作用
调用model.eval()方法用于将模型设置为评估模式。这对模型在进行推理或评估阶段非常重要,具有以下几个方面的意义:

  1. 批规范化和Dropout的行为:

    • 批规范化(Batch Normalization):在训练阶段和评估阶段,批规范化的计算方式是不同的。在评估阶段,我们希望使用固定的均值和方差进行计算,以保持一致性。通过将模型设置为评估模式,可以确保批规范化层使用评估模式下的统计信息进行计算。
    • Dropout:在评估阶段,我们不希望应用Dropout操作,因为它只在训练阶段起作用。通过将模型设置为评估模式,可以关闭Dropout层,从而避免对评估结果的不确定性。
  2. 影响模型中某些层的行为:

    • 某些层(例如,BatchNorm、Dropout和任何有随机性的层)在训练和评估阶段的行为是不同的。设置模型为评估模式确保模型在评估阶段保持一致的行为。
  3. 冻结BatchNorm的移动平均值和方差:

    • 在训练阶段,BatchNorm层会根据每个批次的统计信息更新移动平均值和方差。但在评估阶段,我们希望使用训练期间学到的统计信息进行推断。通过将模型设置为评估模式,可以冻结BatchNorm层的移动平均值和方差,确保在评估阶段使用的是训练期间的统计信息。

综上所述,调用model.eval()方法是为了将模型设置为评估模式,确保模型在进行推理或评估时的一致性行为,包括批规范化、Dropout和其他层的适当处理。

1.4 训练结果可视化

import matplotlib.pyplot as plt

plt.plot(range(1, num_epochs+1), train_losses, label='Train Loss')
plt.plot(range(1, num_epochs+1), val_losses, label='Val Loss')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.title('Training Curve')
plt.legend()
plt.show()

1.5 模型评估

correct = 0
total = 0
model.eval()
with torch.no_grad():
    for images, labels in val_loader:
        images, labels = images.to(device), labels.to(device)
        outputs = model(images)
        _, predicted = torch.max(outputs.data, 1)
        total += labels.size(0)
        correct += (predicted == labels).sum().item()

accuracy = 100 * correct / total
print(f"Accuracy on validation set: {
      
      accuracy:.2f}%")

1.6 导出为onnx模型

torch.onnx.export(model, torch.randn(1, 3, 224, 224).to(device), "model.onnx")

1.7 使用TensorRT推理加速

import tensorrt as trt

# 创建TensorRT引擎
TRT_LOGGER = trt.Logger(trt.Logger.WARNING)
with trt.Builder(TRT_LOGGER) as builder, builder.create_network() as network, \
        trt.OnnxParser(network, TRT_LOGGER) as parser:
    builder.max_workspace_size = 1 << 30  # 设置工作空间大小
    builder.max_batch_size = 1  # 设置批大小

    with open("model.onnx", 'rb') as model_file:
        if not parser.parse(model_file.read()):
            for error in range(parser.num_errors):
                print(parser.get_error(error))
            raise ValueError("Failed to parse the ONNX file")

    engine = builder.build_cuda_engine(network)

# 创建TensorRT上下文
with engine.create_execution_context() as context:
    # 输入和输出分配内存
    inputs, outputs, bindings = [], [], []
    for binding in engine:
        size = trt.volume(engine.get_binding_shape(binding)) * engine.max_batch_size
        dtype = trt.nptype(engine.get_binding_dtype(binding))
        # 分配CPU内存
        host_mem = cuda.pagelocked_empty(size, dtype)
        # 分配GPU内存
        device_mem = cuda.mem_alloc(host_mem.nbytes)
        # 将CPU内存绑定到GPU内存
        bindings.append(int(device_mem))
        # 创建对应的numpy数组,方便后续操作
        inputs.append(host_mem)
        outputs.append(host_mem)

    # 加载图像并进行推理
    image = Image.open('test_image.jpg')
    preprocessed_image = preprocess_image(image)
    np.copyto(inputs[0], preprocessed_image.ravel())

    # 执行推理
    [cuda.memcpy_htod_async(inp.device, inp) for inp in inputs]
    context.execute_async(bindings=bindings, stream_handle=stream.handle)
    [cuda.memcpy_dtoh_async(out.device, out) for out in outputs]
    stream.synchronize()

    # 解析输出
    output_data = outputs[0]

    # 对输出进行后处理
    predicted_class = postprocess_output(output_data)
    print(f"Predicted class: {
      
      predicted_class}")

二、 快速构建模型

本章节展示了几种模型构建的方法,具体如下所示:

2.1 通过继承Module类构造模型

import torch
from torch import nn

class MLP(nn.Module):
    def __init__(self, **kwargs):
        # 调用MLP父类Module的构造函数来进行必要的初始化。这样在构造实例时还可以指定其他函数
        # 参数,如“模型参数的访问、初始化和共享”一节将介绍的模型参数params
        super(MLP, self).__init__(**kwargs)
        self.hidden = nn.Linear(784, 256) # 隐藏层
        self.act = nn.ReLU()
        self.output = nn.Linear(256, 10)  # 输出层

    # 定义模型的前向计算,即如何根据输入x计算返回所需要的模型输出
    def forward(self, x):
        a = self.act(self.hidden(x))
        return self.output(a)

Module类是nn模块里提供的一个模型构造类,是构建神经网络模型的基类。后面的几种创建模型的方法也是通过继承该类实现的。

2.2 通过Sequential类

net = Sequential(
        nn.Linear(784, 256),
        nn.ReLU(),
        nn.Linear(256, 10), 
        )
# 使用上面的定义的网络进行前向计算,其中X是输入张量,比如X = torch.rand(2, 784)
net(X)  

该方法适用于简单的串联式网络模型,通常也可以作为nn.Module中_init_函数中的子模块使用。

2.3 使用ModuleList类

ModuleList类接收一个子模块的列表作为输入,但是不像Sequential类中模块的顺序就是计算的顺序,这里只是把这些模块添加进来了,这些模块的顺序不是计算顺序,所以,ModuleList类没有默认实现forward方法(Sequential类就默认实现了,因为Sequential中模块计算顺序是确定的)。也可以类似List那样进行append和extend操作:

net = nn.ModuleList([nn.Linear(784, 256), nn.ReLU()])  #跟前面的不同就是把模块放到一个列表里了
net.append(nn.Linear(256, 10))  # 类似List的append操作,append一个模块
print(net[-1])  # 类似List的索引访问,访问最后一个模块
print(net)
# net(torch.zeros(1, 784)) # 会报NotImplementedError

2.4 使用ModuleDict类

net = nn.ModuleDict({
    
    
    'linear': nn.Linear(784, 256),
    'act': nn.ReLU(),
})
net['output'] = nn.Linear(256, 10) # 添加
print(net['linear']) # 访问
print(net.output)
print(net)
# net(torch.zeros(1, 784)) # 会报NotImplementedError

2.5 使用上面多种方法复合创建模型

class FancyMLP(nn.Module):
    def __init__(self, **kwargs):
        super(FancyMLP, self).__init__(**kwargs)

        self.rand_weight = torch.rand((20, 20), requires_grad=False) # 不可训练参数(常数参数)
        self.linear = nn.Linear(20, 20)

    def forward(self, x):
        x = self.linear(x)
        x = nn.functional.relu(torch.mm(x, self.rand_weight.data) + 1)
        # 复用全连接层。等价于两个全连接层共享参数
        x = self.linear(x)
        # 控制流,这里我们需要调用item函数来返回标量进行比较
        while x.norm().item() > 1:
            x /= 2
        if x.norm().item() < 0.8:
            x *= 10
        return x.sum()

class NestMLP(nn.Module):
    def __init__(self, **kwargs):
        super(NestMLP, self).__init__(**kwargs)
        self.net = nn.Sequential(nn.Linear(40, 30), nn.ReLU()) 

    def forward(self, x):
        return self.net(x)

net = nn.Sequential(NestMLP(), nn.Linear(30, 20), FancyMLP())

X = torch.rand(2, 40)
print(net)
net(X)

三、 获取预训练模型的任意中间层的输出

3.1 遍历子模块方法—适用于简单模型

import torchvision
import torch
net = torchvision.models.resnet18(weights=None)
print("model ", net)
out = []
x = torch.randn(1, 3, 224, 224)
return_layer = "maxpool"
for name, module in net.named_children():
    print(name)
    # print(module)
    x = module(x)
    print(x.shape)
    if name == return_layer:
        out.append(x.data)
        break
print(out[0].shape)
resnet18的所有名称为:
name conv1
name bn1
name relu
name maxpool
name layer1
name layer2
name layer3
name layer4
name avgpool
name fc

缺点: 只能得到其子模块的输出,而无法获取使用nn.Sequential包含多层的模块内部的输出。

3.2 使用torchvision提供的内置方法----缺点与上面的方法一致

注意事项: 必须保证注册到模型中的顺序必须与被使用的顺序保持一致。所以必须保证在forward计算过程中,单个模型不能被重复传入两次。其返回值为: (Dict[name, new_name]), 该字典以layer的名称作为键值,以别名作为值。其实核心的原理基本和上面的遍历方法保持一致。

from collections import OrderedDict
import torch
from torch import nn

class IntermediateLayerGetter(nn.ModuleDict):
    def __init__(self, model, return_layers):
        if not set(return_layers).issubset([name for name, _ in model.named_children()]):
            raise ValueError("return_layers are not present in model")
 
        orig_return_layers = return_layers
        return_layers = {
    
    k: v for k, v in return_layers.items()}
        layers = OrderedDict()
        for name, module in model.named_children():
            layers[name] = module
            if name in return_layers:
                del return_layers[name]
            if not return_layers:
                break
 
        super(IntermediateLayerGetter, self).__init__(layers)
        self.return_layers = orig_return_layers
 
    def forward(self, x):
        out = OrderedDict()
        for name, module in self.named_children():
            x = module(x)
            if name in self.return_layers:
                out_name = self.return_layers[name]
                out[out_name] = x
        return out
 
# example
m = torchvision.models.resnet18(weights=models.ResNet18_Weights.DEFAULT)
# 提取layer1和layer3的输出,并赋值别名为feat1和feat2;
new_m = torchvision.models._utils.IntermediateLayerGetter(m,{
    
    'layer1': 'feat1', 'layer3': 'feat2'})
out = new_m(torch.rand(1, 3, 224, 224))
print([(k, v.shape) for k, v in out.items()])
# 输出的结果为:
[('feat1', torch.Size([1, 64, 56, 56])), ('feat2', torch.Size([1, 256, 14, 14]))]
  • 上面方法的简便替代----使用create_feature_extractor方法,并且能够弥补IntermediateLayerGetter方法的缺陷,实现能够访问sequential内部的中间层。
# 方案1: 提取res18中间层
# Feature extraction with resnet
from torchvision.models.feature_extraction import create_feature_extractor
model = torchvision.models.resnet18()
# extract layer1 and layer3, giving as names `feat1` and feat2`
model = create_feature_extractor(
model, {
    
    'layer1': 'feat1', 'layer3': 'feat2'})
out = model(torch.rand(1, 3, 224, 224))
print([(k, v.shape) for k, v in out.items()])
# 案例二:提取vgg16模型
# vgg16
# 对于vgg16如果使用遍历的话:features,avgpool,classifier;其中features和classifier是Sequential实现的,所以要访问内部的输出,

import torchvision
import torch
from torchvision import models
from torchvision.models.feature_extraction import create_feature_extractor

backbone = torchvision.models.vgg16_bn(weights=models.VGG16_BN_Weights.IMAGENET1K_V1)
print(backbone)
backbone = create_feature_extractor(backbone, return_nodes={
    
    "features.42": "0"})  #“0”字典的key, 其中42表示访问features模块的第42层
out = backbone(torch.rand(1, 3, 224, 224))
print(out["0"].shape)  # torch.Size([1, 512, 14, 14])
VGG16网络的features模块的组成,如果要访问Sequential中的指定层, 需要将return_nodes={
    
    "features.42": "0"}
中的features.42中的42改为目标层的索引。
VGG(
  (features): Sequential(
    (0): Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (2): ReLU(inplace=True)
    (3): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (4): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (5): ReLU(inplace=True)
    (6): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (7): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (8): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (9): ReLU(inplace=True)
    (10): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (11): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (12): ReLU(inplace=True)
    (13): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (14): Conv2d(128, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (15): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (16): ReLU(inplace=True)
    (17): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (18): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (19): ReLU(inplace=True)
    (20): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (21): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (22): ReLU(inplace=True)
    (23): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (24): Conv2d(256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (25): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (26): ReLU(inplace=True)
    (27): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (28): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (29): ReLU(inplace=True)
    (30): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (31): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (32): ReLU(inplace=True)
    (33): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (34): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (35): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (36): ReLU(inplace=True)
    (37): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (38): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (39): ReLU(inplace=True)
    (40): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (41): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (42): ReLU(inplace=True)
    (43): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  )

3.3 使用hook函数获取任意层的输出(推荐使用)

import torchvision
import torch
from torch import nn
from torchvision.models import resnet50, resnet18
 
 
resnet = resnet18()
print(resnet)
 
features_in_hook = []
features_out_hook = []
# 使用 hook 函数
def hook(module, fea_in, fea_out):      # hook函数的定义必须指定这三个参数,不能更改。
    features_in_hook.append(fea_in[0].data)         # 存储的是指定层的输入
    # 只取前向传播的数值
    features_out_hook.append(fea_out.data)      # 存储的是指定层的输出
    return None
 
layer_name = 'avgpool'    # 模型最后输出层非全连接层
for (name, module) in resnet.named_modules():
    print(name)
    if name == layer_name:
        module.register_forward_hook(hook=hook)
# 测试
x = torch.randn(1, 3, 224, 224)
resnet(x)

print(features_in_hook[0].shape)  # 指定层的输入
print(features_out_hook[0].shape)  # 指定层的输出  # 1, 64, 56, 56

特别注意: def hook(module, fea_in, fea_out) 中的fea_in是tuple类型, fea_out是Tensor类型

通过register_forward_hook方法获取到的每层的名称就更加详细,具体如下:
conv1
bn1
relu
maxpool
layer1
layer1.0
layer1.0.conv1
layer1.0.bn1
layer1.0.relu
layer1.0.conv2
.......
.......
layer4.1.conv2
layer4.1.bn2
avgpool
fc

四、修改预训练模型方法汇总

4.1 只修改输入输出层的参数

#1、只修改输入输出的类别数,即某些网络层的参数(常见的是修改通道数)
model = torchvision.models.resnet50(weights=True)
# 修改最后线性层的输出通道数
model.fc = nn.Linear(2048,10)
print(model.fc)

4.2 替换整个backbone或预训练模型的某一部分

#2、替换整个backbone或预训练模型的某一部分
#以下是替换骨干网的示例
import torchvision
from torchvision.models.detection import FasterRCNN
from torchvision.models.detection.rpn import AnchorGenerator
 
# 先加载一个预训练模型的特征
# only the features:其实就是不包含最后的classifier的两层
backbone = torchvision.models.mobilenet_v2(weights=True).features
# 需要知道backbone的输出通道数,这样才能在替换时保证通道数的一致性
backbone.out_channels = 1280
 
# 设置RPN的anchor生成器,下面的anchor_generator可以在每个空间位置生成5x3的anchors,即5个不同尺寸×3个不同的比例
anchor_generator = AnchorGenerator(sizes=((32, 64, 128, 256, 512),),
                                   aspect_ratios=((0.5, 1.0, 2.0),))
 
# 定义roipool模块
roi_pooler = torchvision.ops.MultiScaleRoIAlign(featmap_names=[0],
                                                output_size=7,
                                                sampling_ratio=2)
 
# 将以上模块集成到FasterRCNN中
model = FasterRCNN(backbone,
                   num_classes=4,
                   rpn_anchor_generator=anchor_generator,
                   box_roi_pool=roi_pooler)

4.3 修改网络中间层的结构

#3、修改网络中间层的结构(一般是重写部分中间层再整体替换)
#以下是在ResNet50的基础上修改的,大部分是复制的源码,但后面新增+修改了一些层
class CNN(nn.Module):
 
    def __init__(self, block, layers, num_classes=9):
        self.inplanes = 64
        super(CNN, self).__init__()
        self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3,
                               bias=False)
        self.bn1 = nn.BatchNorm2d(64)
        self.relu = nn.ReLU(inplace=True)
        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
        self.layer1 = self._make_layer(block, 64, layers[0])
        self.layer2 = self._make_layer(block, 128, layers[1], stride=2)
        self.layer3 = self._make_layer(block, 256, layers[2], stride=2)
        self.layer4 = self._make_layer(block, 512, layers[3], stride=2)
        self.avgpool = nn.AvgPool2d(7, stride=1)
        #新增一个反卷积层
        self.convtranspose1 = nn.ConvTranspose2d(2048, 2048, kernel_size=3, stride=1, padding=1, output_padding=0, groups=1, bias=False, dilation=1)
        #新增一个最大池化层
        self.maxpool2 = nn.MaxPool2d(kernel_size=3, stride=1, padding=1)
        #去掉原来的fc层,新增一个fclass层
        self.fclass = nn.Linear(2048, num_classes)
 
        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels
                m.weight.data.normal_(0, math.sqrt(2. / n))
            elif isinstance(m, nn.BatchNorm2d):
                m.weight.data.fill_(1)
                m.bias.data.zero_()
 
    def _make_layer(self, block, planes, blocks, stride=1):
        downsample = None
        if stride != 1 or self.inplanes != planes * block.expansion:
            downsample = nn.Sequential(
                nn.Conv2d(self.inplanes, planes * block.expansion,
                          kernel_size=1, stride=stride, bias=False),
                nn.BatchNorm2d(planes * block.expansion),
            )
 
        layers = []
        layers.append(block(self.inplanes, planes, stride, downsample))
        self.inplanes = planes * block.expansion
        for i in range(1, blocks):
            layers.append(block(self.inplanes, planes))
 
        return nn.Sequential(*layers)
 
    def forward(self, x):
        x = self.conv1(x)
        x = self.bn1(x)
        x = self.relu(x)
        x = self.maxpool(x)
 
        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)
 
        x = self.avgpool(x)
        #新加层的forward
        x = x.view(x.size(0), -1)
        x = self.convtranspose1(x)
        x = self.maxpool2(x)
        x = x.view(x.size(0), -1)
        x = self.fclass(x)
 
        return x
 
#加载model
resnet50 = torchvision.models.resnet50(pretrained=True)
cnn = CNN(Bottleneck, [3, 4, 6, 3])

#读取参数
pretrained_dict = resnet50.state_dict()
model_dict = cnn.state_dict()

# 将pretrained_dict里不属于model_dict的键剔除掉
pretrained_dict =  {
    
    k: v for k, v in pretrained_dict.items() if k in model_dict}

# 更新现有的model_dict
model_dict.update(pretrained_dict)

# 加载我们真正需要的state_dict
cnn.load_state_dict(model_dict)
# print(resnet50)
print(cnn)

4.4 快速去除预训练模型本身的层,并添加新的层

#4、快速去除预训练模型本身的网络层并添加新的层
#先将模型的网络层列表化,每一层对应列表一个元素,bottleneck对应一个序列层
net_structure = list(model.children())
print(net_structure)
#去除最后两层得到新的网络
resnet_modified = nn.Sequential(*net_structure[:-2])
 
#去除后两层后构建新网络
class Net(nn.Module):
    def __init__(self , model):
        super(Net, self).__init__()
        #取掉model的后两层
        self.resnet_layer = nn.Sequential(*list(model.children())[:-2])
        
        self.transion_layer = nn.ConvTranspose2d(2048, 2048, kernel_size=14, stride=3)
        self.pool_layer = nn.MaxPool2d(32)  
        self.Linear_layer = nn.Linear(2048, 8)
        
    def forward(self, x):
        x = self.resnet_layer(x)
 
        x = self.transion_layer(x)
 
        x = self.pool_layer(x)
 
        x = x.view(x.size(0), -1) 
 
        x = self.Linear_layer(x)
        
        return x

五、 模型权重关键属性及其作用

5.1 模型的常将属性和作用

在PyTorch中,模型(Model)是一个包含层(Layer)和参数(Parameter)的对象,它具有许多有用的属性和方法。以下是一些常见的模型属性及其作用:

  1. parameters(): 返回模型中所有需要学习的参数的迭代器。可以使用它来访问和操作模型的参数。

  2. named_parameters(): 返回一个迭代器,以元组的形式返回参数名称和参数本身。可以用来获取具有名称的参数,并在训练过程中对它们进行操作。

  3. modules(): 返回模型中所有子模块的迭代器。可以使用它来访问模型的各个子模块,并对它们进行操作。

  4. children(): 返回模型中直接子模块的迭代器。与modules()类似,但只返回直接子模块,而不包括孙子模块。

  5. to(device): 将模型移动到指定的设备(如CPU或GPU)。可以使用它来切换模型的设备。

  6. eval(): 将模型设置为评估模式。在评估阶段使用,会关闭一些特定于训练的操作,如Dropout。

  7. train(): 将模型设置为训练模式。在训练阶段使用,会打开一些特定于训练的操作,如Dropout。

  8. state_dict(): 返回一个包含模型当前状态的字典。它包含模型的参数和缓冲区,可用于保存和加载模型的权重。

这些属性提供了对模型的访问和控制能力。你可以使用它们来检查和修改模型的参数、层次结构,将模型移动到不同的设备,以及在训练和评估阶段切换模式等。通过熟悉这些属性,你可以更好地理解和操作PyTorch模型。

5.2 模型训练过程中的权重操作

在PyTorch中,常见的权重操作包括权重初始化、权重正则化和权重更新。

  1. 权重初始化:

    • 使用预训练模型的权重进行初始化:

      import torchvision.models as models
      
      # 加载预训练的ResNet模型
      model = models.resnet18(pretrained=True)      # 不同的pytorch版本参数有些差异
      

      注意部分:
      案例1: 如果只是想使用预训练模型的部分权重参数,可以使用如下方法:

      # 1. 加载预训练模型
      import torch.nn as nn
      import torchvision.models as models
      
      model = models.resnet50(pretrained=True)
      
      # 2. 冻结模型的全部参数
      for param in model.parameters():
          param.requires_grad = False
       
      # 3. 修改特定层的权重参数:
      num_classes = 10
      model.fc = nn.Linear(2048, num_classes)  # 替换最后一层全连接层
      nn.init.xavier_uniform_(model.fc.weight)  # 使用Xavier初始化方法初始化新的权重
      nn.init.zeros_(model.fc.bias)  # 初始化偏置为零
      model.fc.requires_grad = True  # 设置新的全连接层的参数为可学习
      
      

      案例2: 修改预训练模型,并使用之前的模型参数

      import torch
      import torch.nn as nn
      import torchvision.models as models
      
      # 加载预训练模型
      pretrained_model = models.resnet50(pretrained=True)
      
      # 提取模型的特征提取部分(不包括全连接层)
      feature_extractor = nn.Sequential(*list(pretrained_model.children())[:-1])
      
      # 添加自定义的中间层
      custom_layers = nn.Sequential(
          nn.Linear(2048, 1024),
          nn.ReLU(),
          nn.Linear(1024, 512),
          nn.ReLU()
      )
      
      # 构建新的模型
      model = nn.Sequential(
          feature_extractor,
          custom_layers
      )
      
      # 冻结预训练模型的参数
      for param in feature_extractor.parameters():
          param.requires_grad = False
      
      # 打印模型结构
      print(model)
      
      # 验证模型是否可以使用之前的预训练模型参数
      input_tensor = torch.randn(1, 3, 224, 224)
      output = model(input_tensor)
      print("Output shape:", output.shape)
      
      
    • 使用特定方法初始化权重:

      import torch.nn as nn
      
      # 定义自定义模型
      class MyModel(nn.Module):
          def __init__(self):
              super(MyModel, self).__init__()
              self.conv1 = nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1)
              self.conv2 = nn.Conv2d(64, 64, kernel_size=3, stride=1, padding=1)
              
              # 使用正态分布初始化权重
              nn.init.normal_(self.conv1.weight, mean=0, std=0.01)
              
              # 使用常数初始化偏置
              nn.init.constant_(self.conv2.bias, 0.1)
      
  2. 权重正则化:

    • L2正则化(权重衰减):
      optimizer = torch.optim.SGD(model.parameters(), lr=0.001, weight_decay=0.0001)
      
  3. 权重更新:

    • 通过反向传播和优化器更新权重:

      optimizer.zero_grad()  # 梯度清零
      loss.backward()  # 反向传播计算梯度
      optimizer.step()  # 优化器更新权重
      
    • 手动更新权重:

      with torch.no_grad():
          model.weight -= learning_rate * model.weight.grad
          model.bias -= learning_rate * model.bias.grad
          model.weight.grad.zero_()
          model.bias.grad.zero_()
      
    • 冻结部分权重的更新:

      for param in model.parameters():
          if param.requires_grad:
              # 更新可学习的参数
              optimizer.step()
          else:
              # 冻结的参数不更新
              param.grad = None
      

5.3 模型冻结训练要在PyTorch中冻结模型的某些层或参数,以防止它们在训练过程中被更新,你可以采取以下几种方法:

  1. 设置requires_grad属性:将模型的参数的requires_grad属性设置为False可以冻结它们,使其在反向传播时不会更新。例如:

    for param in model.parameters():
        param.requires_grad = False
    

    也可以选择性地冻结特定层的参数,例如:

    for name, param in model.named_parameters():
        if name.startswith('conv'):  # 冻结以'conv'开头的层的参数
            param.requires_grad = False
    
  2. 使用torch.no_grad()上下文管理器:使用torch.no_grad()上下文管理器可以在一段代码块内禁用梯度计算,从而冻结所有参数。例如:

    with torch.no_grad():
        # 冻结的代码块
        output = model(input)
        loss = criterion(output, target)
        # ...
    

    注意,在这种情况下,虽然模型的参数仍然具有requires_grad=True,但在计算损失和梯度时,梯度不会被计算或更新,从而达到冻结的效果。
    这些方法可以根据需要冻结模型的不同层或参数。冻结模型的目的通常是在迁移学习或微调过程中保持预训练的权重固定,只更新特定层或参数。

    # 基本案例演示:
    import torch
    import torch.nn as nn
    from torchvision.models import resnet18
    
    # 加载预训练的ResNet-18模型
    model = resnet18(pretrained=True)
    
    # 冻结模型的前两个卷积层
    for name, param in model.named_parameters():
        if name.startswith('conv1') or name.startswith('layer1'):
            param.requires_grad = False
    
    # 替换模型的全连接层
    num_classes = 10
    model.fc = nn.Linear(512, num_classes)
    
    # 定义损失函数和优化器
    criterion = nn.CrossEntropyLoss()
    optimizer = torch.optim.SGD(model.parameters(), lr=0.001, momentum=0.9)
    
    # 加载数据并进行训练
    # ...
    
    # 在训练过程中,前两个卷积层的参数将不会被更新
    # 只有全连接层的参数会被更新
    
    

六、常用的Tensor操作

张量操作类型概览:
创建、数据类型转换、基本运算与形状修改、设备选择、分片、索引、压缩、扩充、维度交换、拼接、切割、变形、张量的属性。

6.1 张量的创建

import torch

# 从列表创建张量
tensor_from_list = torch.tensor([1, 2, 3])

# 从多维列表创建张量
tensor_from_2d_list = torch.tensor([[1, 2], [3, 4]])

# 从数组创建张量
import numpy as np
array = np.array([1, 2, 3])
tensor_from_array = torch.tensor(array)
# 创建全零张量
zeros_tensor = torch.zeros(3, 4)  # 创建一个 3x4 的全零张量

# 创建全一张量
ones_tensor = torch.ones(2, 2)  # 创建一个 2x2 的全一张量

# 创建指定值的张量
constant_tensor = torch.full((3, 3), 5)  # 创建一个 3x3 的元素全为 5 的张量
# 创建服从均匀分布的随机张量(范围为[0, 1))
random_uniform = torch.rand(2, 2)  # 创建一个 2x2 的随机张量

# 创建服从标准正态分布的随机张量
random_normal = torch.randn(3, 3)  # 创建一个 3x3 的随机张量

# 创建在指定范围内的随机整数张量
random_integers = torch.randint(0, 10, (2, 2))  # 创建一个 2x2 的随机整数张量,范围在 0 到 10 之间
# 创建等差序列张量
arange_tensor = torch.arange(0, 10, 2)  # 创建一个从 0 到 10(不包括 10),步长为 2 的等差序列张量

# 创建均匀间隔序列张量
linspace_tensor = torch.linspace(0, 1, 5)  # 创建一个从 0 到 1(包括 1)之间,均匀间隔的序列张量,共包含 5 个元素

6.2 张量变形与维度操作

以下是PyTorch中常用的Tensor变形和维度操作:

1. `view()`: 改变Tensor的形状,但要求保持原有Tensor中元素的总数不变。
2. `reshape()`: 改变Tensor的形状,不要求保持元素总数不变。
3. `unsqueeze()`: 在指定位置添加一个新的维度。
4. `squeeze()`: 删除维度为1的维度。
5. `expand()`: 在指定维度上扩展Tensor的大小。
6. `expand_as()`: 将Tensor的大小扩展为与指定的Tensor相同。
7. `repeat()`: 在指定维度上复制Tensor的内容。
8. `transpose()`: 交换Tensor的维度顺序。
9. `t()`: 转置Tensor的两个维度。
10. `permute()`: 重新排列Tensor的维度顺序。
11. `flatten()`: 将Tensor展平为一维。
12. `chunk()`: 将Tensor按指定维度切分为多个子块。
13. `split()`: 将Tensor按指定维度切分为多个子Tensor。
14. `cat()`: 沿指定维度将多个Tensor连接起来。
15. `stack()`: 沿新的维度将多个Tensor堆叠起来。
16. `unbind()`: 移除指定维度上的Tensor,并返回一个元组。
17. `narrow()`: 在指定维度上选择Tensor的一个子区域。
18. `gather()`: 根据索引从指定维度上收集Tensor的元素。
这些方法提供了广泛的功能,可以用于在PyTorch中执行各种Tensor的变形和维度操作。
# 简单案例
tensor1 = torch.tensor([[1, 2], [3, 4]])
tensor2 = torch.tensor([[5, 6]])

# Concatenate tensors along a specified dimension
concatenated_tensor = torch.cat((tensor1, tensor2), dim=0)  # Concatenate along the 0th dimension

# Split tensor into multiple tensors along a specified dimension
split_tensors = torch.split(concatenated_tensor, split_size_or_sections=2, dim=1)  # Split into tensors of size 2 along the 1st dimension

6.3 张量的切片和索引

在PyTorch中,可以使用切片和索引操作来访问和修改张量中的元素。下面是一些常用的切片和索引操作:

  1. 切片操作(Slicing):

    tensor[start:end]  # 从索引start到end-1的元素(不包括end)
    tensor[start:]     # 从索引start到最后的元素
    tensor[:end]       # 从索引0到end-1的元素(不包括end)
    tensor[:]          # 所有元素
    tensor[start:end:step]  # 从索引start到end-1的元素,步长为step
    
  2. 通过索引获取单个元素:

    tensor[index]      # 获取指定索引位置的元素
    
  3. 通过索引修改单个元素:

    tensor[index] = value  # 将指定索引位置的元素修改为value
    
  4. 通过整数数组索引获取多个元素:

    tensor[indices]     # 获取索引数组indices对应位置的多个元素
    
  5. 通过整数数组索引修改多个元素:

    tensor[indices] = values  # 将索引数组indices对应位置的多个元素修改为values
    
  6. 通过布尔索引获取多个元素:

    tensor[boolean_mask]     # 根据布尔掩码boolean_mask获取满足条件的多个元素
    
  7. 通过布尔索引修改多个元素:

    tensor[boolean_mask] = values  # 根据布尔掩码boolean_mask将满足条件的多个元素修改为values
    

6.4 张量的数学运算

在PyTorch中,张量(Tensor)支持各种数学运算。下面是一些常用的数学运算操作:

  1. 加法和减法:

    result = tensor1 + tensor2  # 加法
    result = tensor1 - tensor2  # 减法
    
  2. 乘法和除法:

    result = tensor1 * tensor2  # 乘法
    result = tensor1 / tensor2  # 除法
    
  3. 幂运算:

    result = tensor ** exponent  # 幂运算
    
  4. 绝对值:

    result = torch.abs(tensor)  # 绝对值
    
  5. 平方和平方根:

    result = torch.square(tensor)  # 平方
    result = torch.sqrt(tensor)   # 平方根
    
  6. 指数和对数运算:

    result = torch.exp(tensor)    # 指数运算
    result = torch.log(tensor)    # 自然对数
    result = torch.log10(tensor)  # 以10为底的对数
    
  7. 三角函数运算:

    result = torch.sin(tensor)    # 正弦函数
    result = torch.cos(tensor)    # 余弦函数
    result = torch.tan(tensor)    # 正切函数
    
  8. 矩阵乘法:

    result = torch.matmul(tensor1, tensor2)  # 矩阵乘法
    
  9. 平均值和求和:

    result = torch.mean(tensor)  # 平均值
    result = torch.sum(tensor)   # 求和
    
  10. 最大值和最小值:

    result = torch.max(tensor)  # 最大值
    result = torch.min(tensor)  # 最小值
    

这只是一些常见的数学运算,PyTorch还提供了更多的数学函数和操作。你可以根据需要查阅PyTorch的文档,了解更多可用的数学运算。

6.5 广播计算

# 自动广播
tensor1 = torch.tensor([[1, 2], [3, 4]])
scalar = 2
broadcasted_tensor = tensor1 * scalar

# 手动广播
tensor2 = torch.tensor([[5], [6]])
manually_broadcasted_tensor = tensor1 + tensor2

6.6 归一化和标准化

# 归一化
tensor = torch.tensor([1, 2, 3, 4, 5], dtype=torch.float)
normalized_tensor = torch.nn.functional.normalize(tensor)

# 标准化
tensor = torch.tensor([1, 2, 3, 4, 5], dtype=torch.float)
mean = tensor.mean()
std = tensor.std()
standardized_tensor = (tensor - mean) / std

6.7 改变数据类型

在PyTorch中,可以使用不同的方法来改变张量的数据类型。下面是一些常用的方法:

  1. to(): 使用to()方法可以将张量转换为指定的数据类型,并可选择将其移动到指定的设备(如CPU或GPU)。例如:

    tensor = tensor.to(torch.float)  # 转换为float类型
    tensor = tensor.to(torch.int64)  # 转换为int64类型
    tensor = tensor.to(device)       # 移动到指定设备
    
  2. type(): 使用type()方法可以将张量转换为指定的数据类型。该方法返回一个新的张量,原始张量不变。例如:

    new_tensor = tensor.type(torch.float)  # 转换为float类型
    new_tensor = tensor.type(torch.int64)  # 转换为int64类型
    
  3. float(): 使用float()方法可以将张量转换为浮点类型。该方法返回一个新的浮点型张量。例如:

    float_tensor = tensor.float()  # 转换为float类型
    
  4. double(): 使用double()方法可以将张量转换为双精度浮点类型。该方法返回一个新的双精度浮点型张量。例如:

    double_tensor = tensor.double()  # 转换为double类型
    
  5. long(): 使用long()方法可以将张量转换为长整型。该方法返回一个新的长整型张量。例如:

    long_tensor = tensor.long()  # 转换为long类型
    

这些方法可以用于将张量从一种数据类型转换为另一种数据类型。请注意,这些方法返回的是一个新的张量,原始张量不会被修改。

6.8 GPU支持

在PyTorch中,可以使用不同的方法来改变张量的设备属性,即将张量从一个设备移动到另一个设备。下面是一些常用的方法:

  1. to(): 使用to()方法可以将张量移动到指定的设备。你可以传递一个设备对象(如torch.device('cuda'))或设备字符串(如'cuda''cpu')。例如:

    tensor = tensor.to(device)  # 将张量移动到指定设备
    
  2. cuda(): 使用cuda()方法将张量移动到GPU设备。如果没有可用的GPU,它将引发一个错误。例如:

    tensor = tensor.cuda()  # 将张量移动到GPU设备
    
  3. cpu(): 使用cpu()方法将张量移动到CPU设备。例如:

    tensor = tensor.cpu()  # 将张量移动到CPU设备
    

这些方法可以在不同设备之间移动张量。请注意,在移动张量到不同设备时,PyTorch会复制数据并在目标设备上创建新的张量。原始张量保持不变。

此外,你还可以使用上述方法来将模型中的张量移动到特定设备。通过将模型和数据都移动到适当的设备上,你可以利用GPU等加速计算资源来进行模型训练和推理。

6.9 并行计算

# 多个 Tensor 并行计算
tensor1 = torch.tensor([1, 2, 3])
tensor2 = torch.tensor([4, 5, 6])
result = torch.nn.functional.parallel.parallel_apply([tensor1, tensor2], lambda x: x * 2)

6.10 梯度计算

tensor = torch.tensor([1, 2, 3], dtype=torch.float, requires_grad=True)
loss = (tensor ** 2).sum()
loss.backward()  # 计算梯度

6.11 随机数生成

# 生成服从均匀分布的随机数
random_uniform = torch.rand(size=(3, 3))

# 生成服从标准正态分布的随机数
random_normal = torch.randn(size=(2, 2))

# 生成在指定范围内的随机整数
random_integers = torch.randint(low=0, high=10, size=(3, 3))

6.12 累计操作

tensor = torch.tensor([[1, 2, 3], [4, 5, 6]])

# 按维度计算累加和
sum_along_dim = tensor.sum(dim=0)  # 沿第 0 维度计算累加和

# 按维度计算累积乘积
cumprod_along_dim = tensor.cumprod(dim=1)  # 沿第 1 维度计算累积乘积

# 求累计最大值和累计最小值
cummax = tensor.cummax(dim=0)  # 沿第 0 维度计算累计最大值
cummin = tensor.cummin(dim=1)  # 沿第 1 维度计算累计最小值

6.13 比较逻辑操作

tensor1 = torch.tensor([[1, 2], [3, 4]])
tensor2 = torch.tensor([[2, 1], [4, 3]])

# 比较运算
greater_than = tensor1 > tensor2
less_than_equal = tensor1 <= tensor2

# 逻辑运算
logical_and = torch.logical_and(tensor1 > 0, tensor2 < 3)
logical_or = torch.logical_or(tensor1 > 0, tensor2 < 3)
logical_not = torch.logical_not(tensor1 > 0)

猜你喜欢

转载自blog.csdn.net/xiaochuideai/article/details/131312636
今日推荐