Pytorch核心基础

Pytorch核心基础

文章目录

  • Pytorch核心基础
      • 一、自定义数据集类(★★★)
        • 1、使用`torchvision`内置数据集类
          • 1)CIFAR10
          • 2)MNIST
        • 2、继承torch.utils.data.Dataset自定义数据类
          • 1)自定义图像数据集类("PIL image / opencv")
          • 2)自定义信号数据集类("df")
        • 3、小总结(“init数组,getitem格式转换”)
        • 4、运行报错汇总(”积少成多“)
      • 二、自定义神经网络(★★★★)
        • 1、继承`nn.Module`构建网络(”重写init,实现forward“)
        • 2、神经网络参数
        • 3、小总结
          • 1)自定义神经网络小总结("init初始化nn网络,forward处理inut(使用init的Net或F.xx)")
          • 2)神经网络参数小总结("Net参数的三种存储形式")
        • 4、运行报错汇总(”积少成多“)
      • 三、自定义损失函数
        • 1、内置损失函数和优化器
        • 2、自定义损失函数(“继承nn.Module,实现forward / 直接函数”)
          • 1)损失函数类
          • 2)损失函数
        • 3、小总结("loss反向,激活优化器修改网络参数")
      • 四、关于反向传播的梯度计算(★★★★★)
        • 1、标量的反向传播
        • 2、非标量反向传播(”对向量中的每一个标量进行逐一求导“)
        • 3、小总结
          • 1)tensor自动求导遵循规则(”浮点,requires_grad=True,标量求导“)
          • 2)计算图对哪些tensor求梯度(“叶子,requires_grad=True")
        • 4、运行报错汇总
      • 五、网络训练(★★★★★)
        • 1、网络训练和评估(”train,eval“)
        • 2、网络可视化(”SummaryWriter“)
          • 1)损失函数可视化
          • 2)特征图可视化
        • 3、网络权重文件保存 & 加载
          • 1)权重保存和深拷贝("torch.save & module.state_dict")
          • 2)权重加载(“module.load_state_dict & torch.load")
          • 3)部分权重加载(适合模型模块对接)
        • 4、小总结(“熟悉流程,再抠细节”)
          • 1)模型训练(“根据损失,修改数据集类的输出类型和格式”)
          • 2)模型评估(“train,eval,no_grad” ★★★★★)
          • 3)模型可视化(“在init中使用Sequential构建组件块”)
        • 5、运行错误汇总
      • 六、常见问题 & 经验小结
        • 1、conv层如何接上全连接层(”conv + flatten + linear“)
        • 2、有序容器Sequential的使用(”依次调用组件对象forward“)
          • 2)可以在Sequential使用OrderDict来封装组件实例(待验证)
        • 3、tqdm显示模型运行进度条

用pytorch构建一个神经网络,跑一个程序一般需要如下这些流程(以MNIST数据集为例):

  • 1)利用PyTorch内置函数mnist下载数据。
  • 2)利用torchvision对数据进行预处理,调用torch.utils建立一个数据迭代器。
  • 3)可视化源数据。
  • 4)利用nn工具箱构建神经网络模型。
  • 5)实例化模型,并定义损失函数及优化器。
  • 6)训练模型。
  • 7)可视化结果。

更通用的一个流程为:参考十分钟掌握Pytorch搭建神经网络的流程

1.先定义网络:写网络Net的Class,声明网络的实例net=Net(),

2.定义优化器optimizer=optim.xxx(net.parameters(),lr=xxx),

3.再定义损失函数(自己写class或者直接用官方的,compute_loss=nn.MSELoss()或者其他。

4.在定义完之后,开始一次一次的循环:

  • ①先清空优化器里的梯度信息,optimizer.zero_grad();
  • ②再将input传入,output=net(input) ,正向传播
  • ③算损失,loss=compute_loss(output, target) ##这里target就是参考标准值GT,需要自己准备,和之前传入的input一一对应
  • ④误差反向传播,loss.backward()
  • ⑤更新参数,optimizer.step()

一、自定义数据集类(★★★)

1、使用torchvision内置数据集类
1)CIFAR10
import glob
import os

import torch
# 导入 PyTorch 内置的 mnist 数据
#导入预处理模块
from torch.utils.data import DataLoader
#导入nn及优化器
import torch.optim as optim
from torch import nn
from torch.utils.data import TensorDataset
from unittest import TestCase
import torch
from torch.utils import data
import torchvision
import torchvision.transforms as transforms
from tqdm import tqdm
import time
import matplotlib.pyplot as plt
import numpy as np
from PIL import Image
import pandas as pd

class image_DatasetTest(TestCase):

    ''''''
    '''显示图像'''
    def imshow(self,img):
        img = img / 2 + 0.5  # unnormalize
        npimg = img.numpy()
        plt.imshow(np.transpose(npimg, (1, 2, 0)))
        plt.show()

    '''测试对CIFAR10数据集的封装'''
    def test_CIFAR10(self):
        transform = transforms.Compose(
            [transforms.ToTensor(),
             transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])

        trainset = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
        trainloader = torch.utils.data.DataLoader(trainset, batch_size=4, shuffle=True, num_workers=0)

        testset = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transform)
        testloader = torch.utils.data.DataLoader(testset, batch_size=4, shuffle=False, num_workers=0)

        classes = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck')

        '''随机获取部分训练数据'''
        dataiter = iter(trainloader)
        images, labels = dataiter.next()
        # 显示图像
        self.imshow(torchvision.utils.make_grid(images))
Pytorch核心基础_第1张图片
2)MNIST
#其他代码同上
class image_DatasetTest(TestCase):
    ...
	'''测试对MINST数据集的封装'''
    def test_MINST(self):
        train_batch_size = 4
        test_batch_size = 4
        transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize([0.5], [0.5])])
        # 下载数据,并对数据进行预处理
        train_dataset = torchvision.datasets.MNIST('./data', train=True, transform=transform, download=True)
        test_dataset = torchvision.datasets.MNIST('./data', train=False, transform=transform)

        # dataloader是一个可迭代对象,可以使用迭代器一样使用。
        train_loader = DataLoader(train_dataset, batch_size=train_batch_size, shuffle=True)
        test_loader = DataLoader(test_dataset, batch_size=test_batch_size, shuffle=False)

        '''随机获取部分训练数据'''
        dataiter = iter(train_loader)
        images, labels = dataiter.next()
        # 显示图像
        self.imshow(torchvision.utils.make_grid(images))
Pytorch核心基础_第2张图片
2、继承torch.utils.data.Dataset自定义数据类
1)自定义图像数据集类(“PIL image / opencv”)

假设数据目录树如下:

└─code0011_initialDataset
    └─data
        ├─cifar-10-batches-py
        ├─MNIST
        │  └─raw
        ├─myBlinks
        │  ├─test
        │  └─train
        └─myHands
            ├─test
            	0001.png
                0002.png
                0003.png
                0004.png
                0005.png
                ...
            └─train
                0001.png
                0002.png
                0003.png
                0004.png
                0005.png
                0006.png
                0007.png
                0008.png
                0009.png
                0010.png
                ...
    └─code00111_initial_Image_Dataset.py

使用myHands里的图片来构建自定义数据集类

参考PyTorch自定义数据集

#其他代码同上
class image_DatasetTest(TestCase):
    ...
    '''测试对自定义图片数据集的封装'''
    def test_myImageDataset(self):

        image_transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize([0.5], [0.5])])
        train_dataset = HandDataset("data/myHands",train=True,transform=image_transform)
        test_dataset = HandDataset("data/myHands",transform=image_transform)

        train_loader = DataLoader(train_dataset, batch_size=4, shuffle=True)
        test_loader = DataLoader(test_dataset, batch_size=4, shuffle=False)

        '''随机获取部分训练数据'''
        dataiter = iter(train_loader)
        images, labels = dataiter.next()
        # 显示图像
        self.imshow(torchvision.utils.make_grid(images))
        
        
 '''自定义图像数据集'''
class HandDataset(data.Dataset): #继承Dataset

    ''''''
    '''初始化只需读取文件路径即可,无需对sample进行transform'''
    def __init__(self, root_dir, train=True, transform=None):
        """
        Args:
            root_dir (string): Directory with all the images.
            transform (callable, optional): Optional transform to be applied on a sample.
        """
        self.root_dir = root_dir
        # self.img_path = os.listdir(self.root_dir)   #显示单签文件夹下所有子文件夹
        if train:
            # self.img_path = list(filter(lambda x: int(x.split('.')[1]) < 10000, self.img_path))  # 划分训练集和验证集
            # 获取train文件夹下所有图片路径
            self.img_path = glob.glob(os.path.join(root_dir + "/train","*.png"))  #将param1,param2拼接成字符串
        else:
            # 获取test文件夹下所有图片路径
            self.img_path = glob.glob(os.path.join(root_dir + "/test","*.png"))
        self.transform = transform

    def __len__(self):
        return len(self.img_path)

    '''Dataloader执行时调用该函数'''
    def __getitem__(self, idx):
        # image = Image.open(os.path.join(self.root_dir, self.img_path[idx]))
        image = Image.open(os.path.join(self.img_path[idx]))

        '''@Error1 '''
        '''需要对image的size进行一致化,否则dataloader会报错: RuntimeError: stack expects each tensor to be equal size, but got [4, 584, 891] at entry 0 and [4, 581, 877] at entry 1'''
        #参考 
        image = image.convert("RGB")
        # print image.mode
        image = image.resize((600,900))
        '''@Error1 '''

        label = 1
        if self.transform:
            '''使用torchvision的transform 将 PIL image转tensor'''
            image = self.transform(image)   
        label = torch.from_numpy(np.array([label]))
        return image, label
Pytorch核心基础_第3张图片

注意在使用image = self.transform(image)之后,PIL image变成了tensor类型,且是归一化后的图片,要想正常显示图片,需要去归一化(因为transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize([0.5], [0.5])])),正常显示图片的函数

def imshow(img):
    img = img / 2 + 0.5  # unnormalize
    npimg = img.numpy()
    plt.imshow(np.transpose(npimg, (1, 2, 0)))
    plt.show()
2)自定义信号数据集类(“df”)

假设数据目录树如下:

└─code0011_initialDataset
    └─data
        ├─cifar-10-batches-py
        ├─MNIST
        │  └─raw
        ├─myBlinks
        │  ├─test
        │  └─train
        └─myHands
            ├─test
            	blinkSeqs_02_0.csv
            └─train
                blinkSeqs_01_0.csv
                blinkSeqs_01_10.csv
                blinkSeqs_01_5.csv
           
    └─code00111_initial_Image_Dataset.py

其中每个csv文件内容部分如下:

ID,duration,amplitude,EOV,Perclos,non_blink_EAR,KSS
01,7,0.12115102639296188,0.02991202346041056,0.14583333333333334,0.20047914795602176,0
01,2,0.043560606060606064,0.08712121212121213,0.041666666666666664,0.236442149705705,0
01,4,0.05268817204301075,0.04301075268817204,0.08333333333333333,0.2108885902031063,0
01,3,0.033333333333333326,0.033333333333333326,0.0625,0.21044477761119434,0
01,3,0.049999999999999996,0.049999999999999996,0.0625,0.20445301542776995,0
01,4,0.08010752688172043,0.053405017921146956,0.08333333333333333,0.21444892473118277,0
01,5,0.08440860215053764,0.044802867383512544,0.1875,0.21444892473118277,0

使用myBlinks里的图片来构建自定义数据集类,

#其他代码同上
class image_DatasetTest(TestCase):
    ...	
    '''测试对自定义信号数据集的封装'''
    def test_mySignalDataset(self):

        train_dataset = SignalDataset("data/myBlinks", train=True)
        test_dataset = SignalDataset("data/myBlinks")

        train_loader = DataLoader(train_dataset, batch_size=4, shuffle=True)
        test_loader = DataLoader(test_dataset, batch_size=4, shuffle=False)

        '''随机获取部分训练数据'''
        dataiter = iter(train_loader)
        images, labels = dataiter.next()
        # 显示图像
        self.imshow(torchvision.utils.make_grid(images))

        signal,label = train_dataset.__getitem__(0)
        print(f"signal = {signal},label = {label}")
   

'''读取当前文件夹下所有csv,并合并成一个df并返回'''
def get_mergeDf_from_Dir(read_Dir):
    total_path = glob.glob(os.path.join(read_Dir, '*.csv'))
    df = []
    for path in total_path:
        if(len(df) == 0):
            df = pd.read_csv(path)
        else:
            df = df.append(pd.read_csv(path))
    return df

'''从df中获取特征、标签,并以ndarray返回'''
def get_features_label_from_df():
    pass

'''自定义信号数据集'''
class SignalDataset(data.Dataset): #继承Dataset
    def __init__(self, root_dir, train=True):
        """
        Args:
            root_dir (string): Directory with all the images.
            transform (callable, optional): Optional transform to be applied on a sample.
        """
        self.root_dir = root_dir

        if(train):
            self.df = get_mergeDf_from_Dir(read_Dir=root_dir + "/train")
            '''提取特征、标签,并转换成ndarray'''
            self.signals = self.df[['duration','amplitude','EOV','Perclos','non_blink_EAR']].values
            self.labels = self.df['KSS'].values
        else:
            self.df = get_mergeDf_from_Dir(read_Dir=root_dir + "/test")
            '''提取特征、标签,并转换成ndarray'''
            self.signals = self.df[['duration', 'amplitude', 'EOV', 'Perclos', 'non_blink_EAR']].values
            self.labels = self.df['KSS'].values

    def __len__(self):
        return len(self.signals)

    '''Dataloader执行时调用该函数,根据索引获取数据'''
    def __getitem__(self, idx):

        '''返回指定形式的signal和label即可'''
        signal = torch.tensor(self.signals[idx])
        label = torch.tensor([self.labels[idx]])

        return signal,label  
---
signal = tensor([7.0000, 0.1212, 0.0299, 0.1458, 0.2005], dtype=torch.float64),label = tensor([0])
Pytorch核心基础_第4张图片
3、小总结(“init数组,getitem格式转换”)

该小节主要是用torch.data.Dataset来封装自定义数据集(图片/信号),并且使用Dataloader,按照某种采样方式来采样训练集,测试集样本

Note: 自定义数据集需要继承data.Dataset,并且自定义构造函数__init__(不用给父类的Dataset进行初始化), 重写__getitem__方法

  • 1)__init__方法主要是为了在创建Dataset()实例时,为对象封装一个数据集数组,这个数据集数组可以是数据,也可以是文件路径,但是必须是数组为的是通过__getitem__传入idx来获取相应行的数据
  • 2) __getitem__(idx)方法主要是通过__init__初始化的数组和索引idx,获取指定训练样本,并且通过一定的格式转换(ndarray->tensor, PIL image->tensor),完成__getitem__()接口的输出(Dataset的__getitem__()输出是**T_co,即一个泛型**)
    无需考虑原数据集的存储形式(csv,png,tar…),只需完成一定的数据格式转换,满足接口输出即可,这样Dataloader才能对该数据集进行随机采样
4、运行报错汇总(”积少成多“)

Q1:RuntimeError: stack expects each tensor to be equal size, but got [4, 584, 891] at entry 0 and [4, 581, 877] at entry 1

A1:DataLoader输入的数据集,其中每个数据的大小必须是一致的,因此需要对image的size进行一致化,否则dataloader会报错

#参考 
image = image.convert("RGB")
# print image.mode
image = image.resize((600,900))
...

二、自定义神经网络(★★★★)

1、继承nn.Module构建网络(”重写init,实现forward“)

参考

  • 十分钟掌握Pytorch搭建神经网络的流程
  • nn.Model中的forward()前向传播不调用
class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()  # 先初始化父类Module,对Module实例化(Module里有很多参数,需要先实例化父类,子类才可以用它)
        self.conv1 = nn.Conv2d(1,6,5)  #先初始化一个输入通道1,输出通道6, 5*5的卷积核实例
        self.conv2 = nn.Conv2d(6,16,5)
        self.myRelu = nn.ReLU()

    '''如果没有实现forward方法,会在_forward_unimplemented中报raise NotImplementedError,这也说明forward方法是实现,而不是重写'''
    def forward(self,x):
        ''''''
        '''nn.Relu是层结构,需要装载容器中,不是调用函数  TypeError: conv2d() received an invalid combination of arguments - got (MaxPool2d, Parameter, Parameter, tuple, tuple, tuple, int), but expected one of:'''
        # x = nn.ReLU(self.conv1(x))  #conv继承Module,实现forward方法(不是重写,其实是Module通过某种机制,将其子类的forward方法放在__call__方法中,使其子类实例都是可调用的对象,即self.conv(x) = self.conv.forward(x)
        # x = nn.MaxPool2d(x,2)
        # x = nn.ReLU(self.conv2(x))
        # x = nn.MaxPool2d(x, 2)

        # x = F.relu(self.conv1(x))  # conv继承Module,实现forward方法(不是重写,其实是Module通过某种机制,将其子类的forward方法放在__call__方法中,使其子类实例都是可调用的对象,即self.conv(x) = self.conv.forward(x)
        x = self.myRelu(self.conv1(x))  #等价于x = F.relu(self.conv1(x))
        x = F.max_pool2d(x, 2)
        x = F.relu(self.conv2(x))
        x = F.max_pool2d(x, 2)
        return x

net = Net()
'''Error1 conv输入的是一个批次的图片,而不是一张图片,需要进行扩维 RuntimeError: Expected 4-dimensional input for 4-dimensional weight [6, 1, 5, 5], but got 3-dimensional input of size [1, 5, 5] instead'''
# input = np.expand_dims(np.arange(0,25).reshape((1,5,5)),axis=0)  #图片大点,否则会报错
input = np.expand_dims(np.arange(0,784).reshape((1,28,28)),axis=0)  #图片大点,否则会报错:RuntimeError: Given input size: (6x1x1). Calculated output size: (6x0x0). Output size is too small
'''Error2 RuntimeError: expected scalar type Int but found Float'''
input = torch.tensor(input)
input = input.to(torch.float32)
'''Error2'''
'''Error1 '''

res = net(input)
print(res)
---
tensor([[[[  0.0000,   0.0000,   0.0000,   0.0000],
          [  0.0000,   0.0000,   0.0000,   0.0000],
          [  0.0000,   0.0000,   0.0000,   0.0000],
          [  0.0000,   0.0000,   0.0000,   0.0000]],

         [[  0.0000,   0.0000,   0.0000,   0.0000],
          [  0.0000,   0.0000,   0.0000,   0.0000],
          [  0.0000,   0.0000,   0.0000,   0.0000],
          [  0.0000,   0.0000,   0.0000,   0.0000]],

         [[  0.0000,   0.0000,   0.0000,   0.0000],
          [  0.0000,   0.0000,   0.0000,   0.0000],
          [  0.0000,   0.0000,   0.0000,   0.0000],
          [  0.0000,   0.0000,   0.0000,   0.0000]],

         [[  0.0000,   0.0000,   0.0000,   0.0000],
          [  0.0000,   0.0000,   0.0000,   0.0000],
          [  0.0000,   0.0000,   0.0000,   0.0000],
          [  0.0000,   0.0000,   0.0000,   0.0000]],

         [[ 72.2763,  73.0320,  73.7877,  74.5434],
          [ 92.9591,  93.6892,  94.4193,  95.1495],
          [113.4025, 114.1326, 114.8627, 115.5928],
          [133.8458, 134.5760, 135.3061, 136.0362]],

         [[  0.0000,   0.0000,   0.0000,   0.0000],
          [  0.0000,   0.0000,   0.0000,   0.0000],
          [  0.0000,   0.0000,   0.0000,   0.0000],
          [  0.0000,   0.0000,   0.0000,   0.0000]],

         [[  0.0000,   0.0000,   0.0000,   0.0000],
          [  0.0000,   0.0000,   0.0000,   0.0000],
          [  0.0000,   0.0000,   0.0000,   0.0000],
          [  0.0000,   0.0000,   0.0000,   0.0000]],

         [[  0.4722,   0.3624,   0.2525,   0.1427],
          [  2.4232,   2.5544,   2.6856,   2.8168],
          [  6.0973,   6.2285,   6.3597,   6.4909],
          [  9.7714,   9.9026,  10.0338,  10.1650]],

         [[  0.0000,   0.0000,   0.0000,   0.0000],
          [  0.0000,   0.0000,   0.0000,   0.0000],
          [  0.0000,   0.0000,   0.0000,   0.0000],
          [  0.0000,   0.0000,   0.0000,   0.0000]],

         [[  0.0000,   0.0000,   0.0000,   0.0000],
          [  0.0000,   0.0000,   0.0000,   0.0000],
          [  0.0000,   0.0000,   0.0000,   0.0000],
          [  0.0000,   0.0000,   0.0000,   0.0000]],

         [[  0.0000,   0.0000,   0.0000,   0.0000],
          [  0.0000,   0.0000,   0.0000,   0.0000],
          [  0.0000,   0.0000,   0.0000,   0.0000],
          [  0.0000,   0.0000,   0.0000,   0.0000]],

         [[ 56.3407,  57.0218,  57.7029,  58.3841],
          [ 73.6562,  74.2513,  74.8464,  75.4415],
          [ 90.3185,  90.9136,  91.5087,  92.1038],
          [106.9808, 107.5759, 108.1710, 108.7661]],

         [[ 69.5583,  70.0699,  70.5816,  71.0932],
          [ 86.3591,  86.9912,  87.6234,  88.2556],
          [104.0596, 104.6918, 105.3239, 105.9561],
          [121.7600, 122.3922, 123.0244, 123.6565]],

         [[  0.0000,   0.0000,   0.0000,   0.0000],
          [  0.0000,   0.0000,   0.0000,   0.0000],
          [  0.0000,   0.0000,   0.0000,   0.0000],
          [  0.0000,   0.0000,   0.0000,   0.0000]],

         [[  0.0000,   0.0000,   0.0000,   0.0000],
          [  0.0000,   0.0000,   0.0000,   0.0000],
          [  0.0000,   0.0000,   0.0000,   0.0000],
          [  0.0000,   0.0000,   0.0000,   0.0000]],

         [[  0.0000,   0.0000,   0.0000,   0.0000],
          [  0.0000,   0.0000,   0.0000,   0.0000],
          [  0.0000,   0.0000,   0.0000,   0.0000],
          [  0.0000,   0.0000,   0.0000,   0.0000]]]],
       grad_fn=<MaxPool2DWithIndicesBackward>)
2、神经网络参数

参考

  • Pytorch(六)(模型参数的遍历)——net.parameters() & net.named_parameters() & net.state_dict()
  • Pytorch net.parameters() net.named_parameters()
  • 【PyTorch】state_dict详解

调用上面自定义的网络,比较net.named_parameters()net.parameters(), net.state_dict的区别

import torch
from code001_basicNet.code0012_initalNet.code00121_constructNet import *

net = Net()

print("\n")

'''1) net.named_parameters()是一个generator,既包含参数,又包含属性信息'''
print("######################### net.named_parameters() ############################")
print(net.named_parameters())
print(list(net.named_parameters()),end="\n\n")
#'conv1.weight', Parameter containing:~~ 超大一个矩阵
#'conv1.bias', Parameter containing:~~ 超大一个矩阵
#'conv2.weight', Parameter containing:~~ 超大一个矩阵
#'conv2.bias', Parameter containing:~~ 超大一个矩阵
#会发现maxPool2d,Relu并不是网络中的参数,即使定义了myRelu模块,也没有在params中
for param in net.named_parameters():
    print(param,  "\t", type(param))  #param是类型,可以通过param[0],param[1]获取

print(end="\n\n")


'''2) net.parameters() 是一个生成器,只包含参数'''
print("######################### net.parameters() ############################")
print(net.parameters())
print(list(net.parameters()),end="\n\n")
for param in net.parameters():
    print(param, "\t", type(param))  #param是类型

print(end="\n\n")

'''3) net.state_dict()'''
print("######################### net.state_dict() ############################")
print(net.state_dict())
print(list(net.state_dict()),end="\n\n")
for param_tensor in net.state_dict():
    # 打印 key value字典
    print(param_tensor, '\t', net.state_dict()[param_tensor].size())  #param是字符串,而state_dict是字典类型
3、小总结
1)自定义神经网络小总结(“init初始化nn网络,forward处理inut(使用init的Net或F.xx)”)
  • 1)在自定义一个神经网络时,需要继承Module类,在子类的__init__方法中,先对父类的Module实例化,因为Module里有很多参数,需要先实例化父类,子类才可以用它。
  • 2)在子类的__init__方法中,需要通过对nn中其他类(conv,Relu)进行实例化,来构建自己的网络(这里补充一点,__init__中,用nn模块(如Relu)中的类进行实例化,而不是使用nn.functional.relu()进行调用;而在**forward中正好相反**,对于__init__中未定义的模块,只能使用F.xx()来调用)
  • 3)在forward方法中,需要对input进行处理, 如果用到了__init__中没有的模块,则使用nn.functional.xx来调用。而且要注意的是Module通过某种机制,将其子类(Conv,Relu,MSELoss,MyNet)的forward方法放在__call__方法中,使其子类实例都是可调用的对象(即实例化后的conv1对象经过conv1() == conv1.forward())
  • 4)对于conv2d,它处理的是一个批次的图片,而不是一张图片,即使是一张,也需要对tensor进行扩维处理
2)神经网络参数小总结(“Net参数的三种存储形式”)
  • 1) net.named_parameters()是一个generator, 既包含参数,又包含属性信息, 在对其进行迭代输出时,每个元素param为类型,可以通过param[0]获取属性信息, param[1]获取参数值

  • 2)net.parameters() 是一个generator,只包含参数, 不包含属性信息,在对其进行迭代输出时,每个元素param为类型

  • 3)net.state_dict是字典类型,在对其进行迭代输出时,每个元素param是一个key,是一个字符串

conv1.weight 	 torch.Size([6, 1, 5, 5])
conv1.bias 	 torch.Size([6])
conv2.weight 	 torch.Size([16, 6, 5, 5])
conv2.bias 	 torch.Size([16])
#即使定义了myRelu模块,也没有在params中
4、运行报错汇总(”积少成多“)

Q1:在nn.Module的_forward_unimplemented方法中报错:raise NotImplementedError

A1:实现自定义神经网络时,在继承Module类时要实现forward方法检查forward方法是否拼写对。

Q2:RuntimeError: Expected 4-dimensional input for 4-dimensional weight [6, 1, 5, 5], but got 3-dimensional input of size [1, 5, 5] instead

A2conv输入的是一个批次的图片,而不是一张图片,需要np.expand_dims(img,axis=0)扩展维数 (参考https://blog.csdn.net/weixin_43899249/article/details/105691765)

input = np.expand_dims(np.arange(0,25).reshape((1,5,5)),axis=0)

Q3:RuntimeError: expected scalar type Int but found Float

A3:将tensor的int类型转化成torch.float32即可

input = torch.tensor(input)
input = input.to(torch.float32)

Q4:RuntimeError: Given input size: (6x1x1). Calculated output size: (6x0x0). Output size is too small

A4:输入的图片太小了,调整输入的tensor的大小即可

input = np.expand_dims(np.arange(0,25).reshape((1,5,5)),axis=0)  #图片大点,否则会报错
input = np.expand_dims(np.arange(0,784).reshape((1,28,28)),axis=0)  #图片大点,否则会报错:RuntimeError: Given input size: (6x1x1). 

Q5TypeError: conv2d() received an invalid combination of arguments - got (MaxPool2d, Parameter, Parameter, tuple, tuple, tuple, int), but expected one of:

A5:将nn.Relu, nn.MaxPool2d分别修改成nn.functional.relu, nn.functional.max_pool2d即可
主要原因是:nn.ReLU作为一个层结构,必须添加到nn.Module容器中(在__init__方法中初始化)才能使用,而F.ReLU则作为一个函数调用,看上去作为一个函数调用更方便更简洁。具体使用哪种方式,取决于编程风格。(参考https://blog.csdn.net/u011501388/article/details/86602275)

class Net(nn.Module):	
    def __init__(self):
       super(Net, self).__init__()  
       self.conv1 = nn.Conv2d(1,6,5) 
       self.conv2 = nn.Conv2d(6,16,5)
       self.myRelu = nn.ReLU()  #省去x = F.relu(self.conv1(x)) 
       
   def forward(self,x):
       ''''''
       '''nn.Relu是层结构,需要装载容器中,不是调用函数  TypeError: conv2d() received an invalid combination of arguments - got (MaxPool2d, Parameter, Parameter, tuple, tuple, tuple, int), but expected one of:'''
       # x = nn.ReLU(self.conv1(x))  #conv继承Module,实现forward方法(不是重写,其实是Module通过某种机制,将其子类的forward方法放在__call__方法中,使其子类实例都是可调用的对象,即self.conv(x) = self.conv.forward(x)
       # x = nn.MaxPool2d(x,2)
       # x = nn.ReLU(self.conv2(x))
       # x = nn.MaxPool2d(x, 2)

       # x = F.relu(self.conv1(x)) 
       x = self.myRelu(self.conv1(x))  #等价于x = F.relu(self.conv1(x))
       x = F.max_pool2d(x, 2)
       x = F.relu(self.conv2(x))
       x = F.max_pool2d(x, 2)
       return x

三、自定义损失函数

参考

  • pytorch自定义网络层以及损失函数
  • 十分钟掌握Pytorch搭建神经网络的流程
1、内置损失函数和优化器

调用上面自定义的网络,使用内置的nn.MSELoss()

from code001_basicNet.code0012_initialNet.code00121_constructNet import *
import torch
from torch import optim

print("\n")
'''参考'''
net = Net()

input = np.expand_dims((np.arange(0,784).reshape((1,28,28))),axis=0)
input = torch.tensor(input)
input = input.to(torch.float32)
print(f"input requires_grad = {input.requires_grad}")  #input requires_grad = False  

res = net(input)   #res requires_grad = True
print(res)
print(f"res requires_grad() = {res.requires_grad}")

'''定义损失函数'''
mse_loss = nn.MSELoss()  #MSELoss继承了Module父类,实现了forward方法,因此其实例对象也是可调用对象
target = torch.full_like(res,4)

'''定义优化器'''
optimizer = optim.SGD(net.parameters(),lr=0.001,momentum=0.9)  #传入网络参数后,SGD先进行默认参数处理(判断参数是否输入正确,封装dampening=dampening,weight_decay=weight_decay, nesterov=nesterov到default中,在最后才通过param,和SGD的默认参数创建优化器实例,使得优化器可以对网络参数进行控制和修改

#前向传播
loss = mse_loss(res,target)  #不知道mse_loss如何传参,点进去看forward函数即可;而且返回的loss是一个tensor标量,tensor(4844.6704, grad_fn=) 如果想获得float类型数值,使用loss.item()
print(loss)
#后向传播
optimizer.zero_grad()
loss.backward()  #注意是loss(torch.Tensor)进行反向传播时才能激活optimizer对参数进行修改(手动optimizer.step()),而不是optimizer进行反向传播,optimizer只是个参数"控制器",为参数更新设置了某种策略。
#loss执行backward之后计算图会自动清空
optimizer.step()
#记录误差
loss_val = loss.item()
---
input requires_grad = False
tensor([[[[  0.0000,   0.0000,   0.0000,   0.0000],
          [  0.0000,   0.0000,   0.0000,   0.0000],
          [  0.0000,   0.0000,   0.0000,   0.0000],
          [  0.0000,   0.0000,   0.0000,   0.0000]],

         [[  0.0000,   0.0000,   0.0000,   0.0000],
          [  0.0000,   0.0000,   0.0000,   0.0000],
          [  0.0000,   0.0000,   0.0000,   0.0000],
          [  0.0000,   0.0000,   0.0000,   0.0000]],

         [[ 47.1162,  47.9328,  48.7493,  49.5659],
          [ 69.9797,  70.7963,  71.6128,  72.4294],
          [ 92.8432,  93.6598,  94.4763,  95.2929],
          [115.7067, 116.5233, 117.3398, 118.1564]],

         [[ 57.0466,  58.4608,  59.8750,  61.2892],
          [ 96.6441,  98.0583,  99.4725, 100.8867],
          [136.2415, 137.6557, 139.0699, 140.4842],
          [175.8390, 177.2532, 178.6674, 180.0816]],

         [[ 12.6389,  12.5621,  12.4852,  12.4084],
          [  9.1513,   9.0099,   8.8685,   8.7271],
          [  5.1918,   5.0504,   4.9089,   4.7676],
          [  1.2322,   1.0908,   0.9494,   0.8080]],

         [[  0.0000,   0.0000,   0.0000,   0.0000],
          [  0.0000,   0.0000,   0.0000,   0.0000],
          [  0.0000,   0.0000,   0.0000,   0.0000],
          [  0.0000,   0.0000,   0.0000,   0.0000]],

         [[ 35.9857,  36.4011,  36.8166,  37.2320],
          [ 47.6175,  48.0329,  48.4483,  48.8638],
          [ 59.2493,  59.6647,  60.0801,  60.4955],
          [ 70.8810,  71.2965,  71.7119,  72.1273]],

         [[ 15.1310,  15.2257,  15.3204,  15.4150],
          [ 17.7819,  17.8766,  17.9713,  18.0660],
          [ 20.4328,  20.5275,  20.6222,  20.7169],
          [ 23.0838,  23.1784,  23.2731,  23.3678]],

         [[ 66.1469,  66.8394,  67.5317,  68.2241],
          [ 85.5342,  86.2266,  86.9190,  87.6114],
          [104.9214, 105.6137, 106.3062, 106.9986],
          [124.3086, 125.0010, 125.6934, 126.3857]],

         [[ 24.2711,  24.5046,  24.7382,  24.9718],
          [ 30.8107,  31.0443,  31.2778,  31.5114],
          [ 37.3503,  37.5839,  37.8175,  38.0510],
          [ 43.8900,  44.1235,  44.3571,  44.5907]],

         [[ 46.0051,  46.4567,  46.9083,  47.3599],
          [ 58.6496,  59.1012,  59.5528,  60.0043],
          [ 71.2940,  71.7456,  72.1972,  72.6487],
          [ 83.9384,  84.3900,  84.8416,  85.2932]],

         [[ 46.3952,  47.5693,  48.7434,  49.9174],
          [ 79.2691,  80.4432,  81.6173,  82.7913],
          [112.1431, 113.3171, 114.4912, 115.6652],
          [145.0170, 146.1911, 147.3651, 148.5392]],

         [[ 55.0889,  56.1313,  57.1737,  58.2161],
          [ 84.2756,  85.3180,  86.3604,  87.4028],
          [113.4623, 114.5047, 115.5470, 116.5895],
          [142.6490, 143.6914, 144.7337, 145.7761]],

         [[ 28.2521,  28.5004,  28.7487,  28.9970],
          [ 35.2039,  35.4522,  35.7004,  35.9487],
          [ 42.1557,  42.4039,  42.6522,  42.9005],
          [ 49.1074,  49.3557,  49.6040,  49.8523]],

         [[ 47.9018,  48.6425,  49.3832,  50.1239],
          [ 68.6414,  69.3821,  70.1228,  70.8635],
          [ 89.3810,  90.1217,  90.8624,  91.6031],
          [110.1207, 110.8613, 111.6020, 112.3427]],

         [[  0.0000,   0.0000,   0.0000,   0.0000],
          [  0.0000,   0.0000,   0.0000,   0.0000],
          [  0.0000,   0.0000,   0.0000,   0.0000],
          [  0.0000,   0.0000,   0.0000,   0.0000]]]],
       grad_fn=<MaxPool2DWithIndicesBackward>)
res requires_grad() = True
tensor(4185.7754, grad_fn=<MeanBackward0>)
2、自定义损失函数(“继承nn.Module,实现forward / 直接函数”)
1)损失函数类
'''自定义损失函数类'''
class My_loss(nn.Module):
    def __init__(self):
        super().__init__()
        print("使用自定义的MSE_loss类实例")

    '''自定义MSE损失函数'''
    def forward(self,x,y):
        return torch.mean(torch.pow(x - y,2))

...

#调用时
mse_loss = My_loss()   #使用自定义损失函数类
loss = mse_loss(res,target)
loss.backward()
print(loss)
2)损失函数
'''自定义损失函数(不需要维护参数,梯度等信息,但必须保证所有的数学操作使用tensor完成)'''
#'''对于自定义类中,其实最终是调用forward来实现,同时nn.Module还要维护一些其他变量和状态。不如直接定义loss函数来实现'''
def mse_loss_func(x,y):
    print("使用自定义的MSE_loss函数")
    return torch.mean(torch.pow(x - y, 2))

...

#调用时
loss = mse_loss_func(res,target)  #使用自定义损失函数
loss.backward()
print(loss)
3、小总结(“loss反向,激活优化器修改网络参数”)
  • 1)通常情况下,在初始化网络之后,先定义优化器,让优化器拥有网络参数的控制权,接着再定义损失函数。
  • 2)传入网络参数后,SGD先进行默认参数处理(判断参数是否输入正确,封装dampening=dampening,weight_decay=weight_decay, nesterov=nesterov到default中,在最后才通过param,和SGD的默认参数创建优化器(Optimizer父类)实例,使得优化器可以对网络参数进行控制和修改
  • 3)MSELoss继承了Module父类实现了forward方法,因此其实例对象mse_loss也是可调用对象,可以通过mse_loss(input,target)直接计算损失。
  • 4)loss是torch.Tensor,在进行反向传播时才能激活optimizer对参数进行修改(手动optimizer.step()),而不是optimizer进行反向传播,optimizer只是个参数"控制器",为参数更新设置了某种策略
  • 5)自定义损失函数类和自定义网络一样,需要继承nn.module父类,并实现forward方法。 参考https://www.cnblogs.com/rainsoul/p/11376180.html
  • 6)自定义损失函数:对于自定义类中,其实最终是调用forward来实现,同时**nn.Module还要维护一些其他变量和状态**。不如直接定义loss函数来实现。自定义损失函数不需要维护参数,梯度等信息,但必须保证所有的数学操作使用tensor完成。
  • 7)经过Net计算得到的tensor的requires_grad=True,即使输入的input的requires_grad=False(自定义的tensor的requires_grad默认为false,而神经网络层的权值w的tensor的requires_grad属性默认为True

四、关于反向传播的梯度计算(★★★★★)

参考

  • requires_grad_()与requires_grad的区别,同时pytorch的自动求导(AutoGrad)
  • 叶子节点和tensor的requires_grad参数
1、标量的反向传播
import torch
'''######################################## 对input测试 - 自动求导 ########################################################'''

'''1、标量反向传播:'''
print("################################## 标量反向传播  ###############################################")
'''Error1'''
# input.backward()  #RuntimeError: element 0 of tensors does not require grad and does not have a grad_fn
'''Error1'''

'''Error2'''
# RuntimeError: grad can be implicitly created only for scalar outputs
# input.requires_grad = True  #input.requires_grad_(True)报同样错
# input.backward()
'''Error2'''

'''Error3'''
# input = torch.tensor([2],requires_grad=True)   #RuntimeError: Only Tensors of floating point and complex dtype can require gradients
'''Error3'''
input = torch.tensor([2],requires_grad=True,dtype=torch.float32)  #浮点标量在设置requires_grad属性后才可以计算梯度(无需传入任何参数值,自动求导)
w = torch.full_like(input,6, requires_grad = True)
# input.backward()
y = torch.mul(input,w) #grad_fn = 
y.backward()
print(f"input.grad = {input.grad}",end="\n\n")

---
################################## 标量反向传播  ###############################################
input.grad = tensor([6.])
2、非标量反向传播(”对向量中的每一个标量进行逐一求导“)

参考B站李沐深度学习

import torch
'''######################################## 对input测试 - 自动求导 ########################################################'''

'''2、非标量反向传播'''
print("################################## 非标量反向传播  ###############################################")
# input = torch.tensor([[5,6,7]],dtype = torch.float32)  #反向传播后,input.grad = None
input = torch.tensor([[5,6,7]],dtype = torch.float32,requires_grad=True)  #反向传播后,input.grad = tensor
w = torch.full_like(input,2, requires_grad = True)
bias = torch.tensor([0.5], requires_grad = True)
y = torch.mul(input,w) #grad_fn = 
z = torch.add(y,bias)  #grad_fn = 

print(f"input.is_leaf = {input.is_leaf},w.is_leaf = {w.is_leaf},bias.is_leaf = {bias.is_leaf}")
print(f"y.is_leaf = {y.is_leaf},z.is_leaf = {z.is_leaf}")

# output.requires_grad = True
print(z)
'''Error2'''
print(f"y requires_grad = {y.requires_grad}, z requires_grad = {z.requires_grad}")
# z.backward()  # RuntimeError: grad can be implicitly created only for scalar outputs
'''Error2'''
# 方法1: 参考
# z.backward(z.clone().detach())  #detach之后,修改叶子节点的位置,之前的非叶子计算节点和叶子节点断开连接,因此对非叶子节点计算的梯度,input的梯度均为None
# # print(f"y.grad = {y.grad}")  #y.grad = None # UserWarning: The .grad attribute of a Tensor that is not a leaf Tensor is being accessed. Its .grad attribute won't be populated during autograd.backward(). If you indeed want the gradient for a non-leaf Tensor, use .retain_grad() on the non-leaf Tensor. If you access the non-leaf Tensor by mistake, make sure you access the leaf Tensor instead. See github.com/pytorch/pytorch/pull/30531 for more information.
# print(f"input.grad = {input.grad}")  #input.grad = None

# 方法2:
'''Z为[1,3],在Jacobian矩阵为[3,3]'''
W = torch.zeros(3,3)
B = torch.zeros(3,3)
z.backward(gradient=torch.tensor([[1,0,0]]),retain_graph=True)
print(f"w.grad = {w.grad}, bias.grad = {bias.grad}")
print(f"input.grad = {input.grad}, y.grad = {y.grad}",end="\n\n")
W[0],B[0] = w.grad, bias.grad  #赋值,最后输出W,B,为最终向量对w,b的求导结果
# 梯度是累加的,故需要对前面的所有计算节点的梯度清零
w.grad = torch.zeros_like(w.grad)
bias.grad = torch.zeros_like(bias.grad)
# y.grad = torch.zeros_like(y.grad)   # grad为None,报错
# input.grad = torch.zeros_like(input.grad)  # input.grad为None,报错

z.backward(gradient=torch.tensor([[0,1,0]]),retain_graph=True)
print(f"w.grad = {w.grad}, bias.grad = {bias.grad}")
print(f"input.grad = {input.grad}, y.grad = {y.grad}",end="\n\n")
W[1],B[1] = w.grad, bias.grad  #赋值,最后输出W,B,为最终向量对w,b的求导结果
# 梯度是累加的,故需要对前面的所有计算节点的梯度清零
w.grad = torch.zeros_like(w.grad)
bias.grad = torch.zeros_like(bias.grad)

z.backward(gradient=torch.tensor([[0,0,1]]),retain_graph=True)
print(f"w.grad = {w.grad}, bias.grad = {bias.grad}")
print(f"input.grad = {input.grad}, y.grad = {y.grad}",end="\n\n")
W[2],B[2] = w.grad, bias.grad  #赋值,最后输出W,B,为最终向量对w,b的求导结果
# 梯度是累加的,故需要对前面的所有计算节点的梯度清零
w.grad = torch.zeros_like(w.grad)
bias.grad = torch.zeros_like(bias.grad)

print(f"W = {W} \n B = {B}")
---
################################## 非标量反向传播  ###############################################
input.is_leaf = True,w.is_leaf = True,bias.is_leaf = True
y.is_leaf = False,z.is_leaf = False
tensor([[10.5000, 12.5000, 14.5000]], grad_fn=<AddBackward0>)
y requires_grad = True, z requires_grad = True
w.grad = tensor([[5., 0., 0.]]), bias.grad = tensor([1.])
input.grad = tensor([[2., 0., 0.]]), y.grad = None

w.grad = tensor([[0., 6., 0.]]), bias.grad = tensor([1.])
input.grad = tensor([[2., 2., 0.]]), y.grad = None

w.grad = tensor([[0., 0., 7.]]), bias.grad = tensor([1.])
input.grad = tensor([[2., 2., 2.]]), y.grad = None

W = tensor([[5., 0., 0.],
        [0., 6., 0.],
        [0., 0., 7.]]) 
 B = tensor([[1., 1., 1.],
        [1., 1., 1.],
        [1., 1., 1.]])
3、小总结
1)tensor自动求导遵循规则(”浮点,requires_grad=True,标量求导“)

对于tensor,如果想在反向传播时自动求导,要遵循如下规则: 参考https://www.cnblogs.com/kevin-red-heart/p/11340944.html

  • 1)该tensor的自动求导属性(requires_grad)为True,否则会报RuntimeError: element 0 of tensors does not require grad and does not have a grad_fn
  • 2)浮点型;否则会报RuntimeError: Only Tensors of floating point and complex dtype can require gradients
  • 3)只支持【标量】对标量,或【标量】对向量/矩阵求导
  • 4)向量也可以backward()函数求导,但是需要相应处理,通常情况下,对向量中的每一个标量进行逐一求导,接着将每次计算的变量的梯度保存在Jaccobian矩阵中,如果不处理会报 RuntimeError: grad can be implicitly created only for scalar outputs
  • 5)一般情况下,一个计算图只能backward一次,如果想backward多次,需要retain_graph=True避免1次backward就将各节点的值清除
2)计算图对哪些tensor求梯度(“叶子,requires_grad=True")

关于对自动求导过程的理解,一定要理解计算图的定义, 以及反向传播时是对哪些tensor求梯度: 参考https://zhuanlan.zhihu.com/p/85506092

  • 1)可以使用tensor.is_leaf判断该节点是否为叶子节点

  • 2)使用backward()函数反向传播计算tensor的梯度时,并不计算所有tensor的梯度,而是只计算满足这几个条件的tensor的梯度:

  • 类型为叶子节点

  • requires_grad=True

  • 依赖该tensor的所有tensor的requires_grad=True

Pytorch核心基础_第5张图片
  • 3)使用detach()函数可以将某一个非叶子节点剥离成为叶子节点,该新的叶子节点无论requires_grad属性为何值,原先的叶子节点求导通路中断,便无法获得梯度数值了

detach()前:

Pytorch核心基础_第6张图片

detach()后:

Pytorch核心基础_第7张图片
4、运行报错汇总

Q1:RuntimeError: element 0 of tensors does not require grad and does not have a grad_fn
A1:原因是该tensor不具备自动求导的属性,需要通过input.requires_grad = True设置

Q2: RuntimeError: grad can be implicitly created only for scalar outputs

A2:主要原因是输出的z是向量不是标量,即使z的**grad_fn**是xxxBackward0 object,也不能进行求导

Q3:RuntimeError: Only Tensors of floating point and complex dtype can require gradients

A3:原因是只有浮点型才能计算梯度

Q4:警告 - UserWarning: The .grad attribute of a Tensor that is not a leaf Tensor is being accessed. Its .grad attribute won’t be populated during autograd.backward(). If you indeed want the gradient for a non-leaf Tensor, use .retain_grad() on the non-leaf Tensor. If you access the non-leaf Tensor by mistake, make sure you access the leaf Tensor instead. See github.com/pytorch/pytorch/pull/30531 for more information.

A4对于非叶子计算节点访问梯度值为有 警告,而对于非叶子节点则不会报该警告

Q5:对于上面非标量的反向传播,究竟z是对谁求导,x,y,input? 还是w, bias? 为什么前者grad为None, 后者有值?

A5:在backward()时,只对tensor.is_leaf的节点计算梯度,x,y都是非叶子节点,input, w,bias都是叶子节点
但是input的require_grad=False, input.grad=None,如果input.require_grad=True,则input.grad = tensor

五、网络训练(★★★★★)

  • 对于网络的训练,在熟悉前面知识点基础上(自定义数据集类,自定义网络,自定义损失函数以及优化器的使用),一定要对网络训练的具体步骤很是熟悉,这样才能把前面的知识点流畅地串起来。
  • 神经网络可玩性很高,而且对于前面某一个知识点的深入理解,更是需要我们对网络训练步骤进行把握,否则会陷入某一局部细节,忽视对整个网络训练轮廓的认知
1、网络训练和评估(”train,eval“)

Net_with_fc网络如下:

class Net_with_fc(nn.Module):
    def __init__(self,num_classes):
        super(Net_with_fc, self).__init__()  # 先初始化父类Module,对Module实例化(Module里有很多参数,需要先实例化父类,子类才可以用它)
        self.num_classes = num_classes
        self.conv1 = nn.Conv2d(3, 6, 5)  #先初始化一个输入通道3(RGB通道),输出通道6, 5*5的卷积核实例
        self.conv2 = nn.Conv2d(6, 16, 7)
        self.conv3 = nn.Conv2d(16, 32, 9)
        self.conv4 = nn.Conv2d(32, 64, 11)
        self.myRelu = nn.ReLU()
        self.myMaxPool2d = nn.MaxPool2d(2)
        self.myFlatten = nn.Flatten()
        #三层全连接
        self.fc1 = nn.Linear(1 * 64 * 22 * 29, 2048)
        self.fc2 = nn.Linear(2048, 2048)
        self.fc3 = nn.Linear(2048, self.num_classes)
        #softmax
        self.mySoftmax = nn.Softmax()

    '''如果没有实现forward方法,会在_forward_unimplemented中报raise NotImplementedError,这也说明forward方法是实现,而不是重写'''
    def forward(self,x):
        ''''''
        '''nn.Relu是层结构,需要装载容器中,不是调用函数  TypeError: conv2d() received an invalid combination of arguments - got (MaxPool2d, Parameter, Parameter, tuple, tuple, tuple, int), but expected one of:'''
        x = self.myRelu(self.conv1(x))  #等价于x = F.relu(self.conv1(x))
        x = self.myMaxPool2d(x)   #output = [1, 6, 238, 298]
        x = self.myRelu(self.conv2(x))
        x = self.myMaxPool2d(x)  #output = [1, 16, 116, 146]
        x = self.myRelu(self.conv3(x))
        x = self.myMaxPool2d(x)   #output = [1, 32, 54, 69]
        x = self.myRelu(self.conv4(x))
        # x = F.max_pool2d(x, 32)  #直接用KernelSize = 32进行最大池化,将size=[1,3,400,600]图片变成[1, 64, 1, 1]
        x = self.myMaxPool2d(x)   #output = [1, 64, 22, 29]
        x = self.myFlatten(x)   #output = [1, 40832]

        # 使用3层全连接,转成标量
        x = self.fc1(x)   #output = [1,2048]
        x = self.fc2(x)   #output = [1,2048]
        x = self.fc3(x)  #output = [1,2]
        x = self.mySoftmax(x)
        return x

网络训练和评估如下:

from code001_basicNet.code0011_initialDataset.code00111_initial_Image_Dataset import *
from code001_basicNet.code0012_initialNet.code00121_constructNet1_withLinear import *
from torch.optim import SGD
from torch.nn import MSELoss
from tensorboardX import SummaryWriter
import torchvision.utils as vutils
''''''

'''初始化参数'''
lr = 0.0001
momentum = 0.9
epoches = 100
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

'''加载自定义Dataset'''
image_transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize([0.5], [0.5])])
train_dataset = HandDataset("../code0011_initialDataset/data/myHands",train=True,transform=image_transform)
test_dataset = HandDataset("../code0011_initialDataset/data/myHands",transform=image_transform)

train_loader = DataLoader(train_dataset, batch_size=4, shuffle=True)  #DataLoader是个迭代器
test_loader = DataLoader(test_dataset, batch_size=4, shuffle=False)

'''随机获取部分训练数据'''
dataiter = iter(train_loader)
images, labels = dataiter.next()
# 显示图像
imshow(torchvision.utils.make_grid(images))

'''初始化自定义神经网络'''
model = Net_with_fc(num_classes=2)  #输入图片大小为480 * 600
model = model.to(device)

#模型参数量
print("net_gvp have {} paramerters in total".format(sum(x.numel() for x in model.parameters())))

'''初始化优化器'''
optimizer = SGD(model.parameters(),lr=lr,momentum=momentum)
'''初始化损失函数'''
# mse_loss = MSELoss()  #回归损失函数
crossEntropy_loss = nn.CrossEntropyLoss()  #分类损失函数


for epoch in range(epoches):

    train_loss = 0
    train_acc = 0

    '''通过train_dataloader随机获取训练数据集'''
    for img,label in train_loader:
        # 将img
        img = img.to(device)
        label = label.to(device)

        #前向传播
        out = model(img)
        # loss = mse_loss(out,label)  #这里的label是one-hot编码的,out是一个1维数组([0,1]),向量可以和向量计算损失 参考
        # loss = mse_loss(out,label)  #这里的label是标量(0,1),而out是一个1维数组([0,1]),标量可以和向量计算损失
        loss = crossEntropy_loss(out,label)  #这里的label是标量(0,1),而out是一个1维数组([0,1]),标量可以和向量计算损失
        #反向传播
        optimizer.zero_grad() #每次反向传播前,先将所有节点的梯度置为0
        loss.backward()
        optimizer.step()

        #累积误差
        train_loss += loss.item()
        #计算分类准确率
        pred,pred_index = out.max(axis=1)
        num_correct = (pred_index == label).sum().item()  #如果label是标量,则方便通过pred_index + 1 == label,进行pred和label的比较(如果label是one-hot编码就不太方便)
        train_acc += num_correct / img.shape[0]  #每个批次求平均的acc

        #需要对损失进行平均化,否则计算的损失会随着累加不断增加
        # print(f"epoch :{epoch}, train_loss: {train_loss}, train_acc: {train_acc}")
    '''每完成一次epoch,就打印一次训练损失,acc结果'''
    print(f"epoch :{epoch}, train_loss: {train_loss / len(train_loader)}, train_acc: {train_acc / len(train_loader)}",end=",")

    '''模型评估,无需修改模型权值'''
    model.eval()
    eval_loss = 0
    eval_acc = 0
    for img,label in test_loader:
        img = img.to(device)
        label = label.to(device)

        out = model(img)
        loss = crossEntropy_loss(out,label)

        #累加eval_loss
        eval_loss += loss.item()
        #累加eval_acc
        pred,pred_index = out.max(axis=1)
        count = (pred_index == label).sum().item()
        eval_acc += count / img.shape[0]

    '''每完成一次epoch,就打印一次验证损失,acc结果'''
    print(f"epoch :{epoch}, eval_loss: {eval_loss / len(test_loader)}, eval_acc: {eval_acc / len(test_loader)}")
---
...
epoch :11, train_loss: 0.6921057825287183, train_acc: 0.5520833333333334,epoch :11, eval_loss: 0.6923125063379606, eval_acc: 0.5208333333333334
epoch :12, train_loss: 0.6918440063794454, train_acc: 0.5520833333333334,epoch :12, eval_loss: 0.6922148441274961, eval_acc: 0.5208333333333334
epoch :13, train_loss: 0.6917069454987844, train_acc: 0.5520833333333334,epoch :13, eval_loss: 0.6921395535270373, eval_acc: 0.5208333333333334
epoch :14, train_loss: 0.692189114789168, train_acc: 0.5208333333333334,epoch :14, eval_loss: 0.692050389945507, eval_acc: 0.5208333333333334
epoch :15, train_loss: 0.6920397082964579, train_acc: 0.5208333333333334,epoch :15, eval_loss: 0.6919525017340978, eval_acc: 0.5208333333333334
epoch :16, train_loss: 0.6920238385597864, train_acc: 0.5208333333333334,epoch :16, eval_loss: 0.691834976275762, eval_acc: 0.5208333333333334
2、网络可视化(”SummaryWriter“)
1)损失函数可视化
model = Net_with_fc(num_classes=2)  #输入图片大小为480 * 600
...
'''实例化SummaryWriter对象'''
linear_writer = SummaryWriter(log_dir='logs',comment='Linear')  #绘制损失函数

for epoch in epoches:
    train()
    
    # 保存loss的数据与epoch数值
	linear_writer.add_scalar('训练损失值', train_loss / len(train_loader), epoch)

    test()
2)特征图可视化
model = Net_with_fc(num_classes=2)  #输入图片大小为480 * 600
...
featureMap_writer = SummaryWriter(log_dir='logs',comment='feature map')   #绘制特征图
for epoch in epoches:
    train()
   
    test()
    
    '''绘制特征图'''
    x_image,x_label = test_dataset.__getitem__(0)
    x = torch.unsqueeze(x_image, dim=0)  #x_image扩充一维,变成一个批次的图片
    x = x.to(device)
    for name, layer in model._modules.items():  #遍历输出网络的每一层

        # 打印每一层的名称name
        x = layer(x)  #x逐层传递下去(conv1->conv2->conv3->conv4->myRelu->myMaxPool2d)...
        print(f'{name}')

        # 查看卷积层的特征图
        if 'conv' in name:
            x1 = x.transpose(0, 1)  # C,B, H, W  ---> B,C, H, W 将Channel与Batch进行交换
            img_grid = vutils.make_grid(x1, normalize=True, scale_each=True, nrow=4)  # normalize进行归一化处理
            featureMap_writer.add_image(f'{name}_feature_maps', img_grid, global_step=0)  # 用tensofboard可视化特征图

    featureMap_writer.flush()  # 将实例化的SummaryWriter类刷新,清除缓存
    featureMap_writer.close()  # 关闭
---
conv1
conv2
conv3
conv4
myRelu
myMaxPool2d
myFlatten
fc1
fc2
fc3
mySoftmax

Pytorch核心基础_第8张图片

Pytorch核心基础_第9张图片

这里要注意的是在逐层x = layer(x)时,按照遍历顺序conv1后接着conv2,并没有接着ReluMaxPool2d,会导致图片中还能看得出人物,如果用了MaxPool2d,图片就面目全非了。

如果使用Sequential封装的网络([Net_with_fc_Seq自定义网络](#1、conv层如何接上全连接层(“conv + flatten + linear”)))

model = Net_with_fc_Seq(num_classes=2)
...
featureMap_writer = SummaryWriter(log_dir='logs',comment='feature map')   #绘制特征图
for epoch in epoches:
    train()
   
    test()
    
    '''绘制特征图'''
    x_image,x_label = test_dataset.__getitem__(0)
    x = torch.unsqueeze(x_image, dim=0)  #x_image扩充一维,变成一个批次的图片
    x = x.to(device)
    for name, layer in model._modules.items():  #遍历输出网络的每一层

        # 打印每一层的名称name
        x = layer(x)  #x逐层传递下去(conv_layer->fc_layer->conv3->classifier)...
        print(f'{name}')

        # 查看卷积层的特征图
        if 'conv' in name:
            x1 = x.transpose(0, 1)  # C,B, H, W  ---> B,C, H, W 将Channel与Batch进行交换
            img_grid = vutils.make_grid(x1, normalize=True, scale_each=True, nrow=4)  # normalize进行归一化处理
            featureMap_writer.add_image(f'{name}_feature_maps', img_grid, global_step=0)  # 用tensofboard可视化特征图

    featureMap_writer.flush()  # 将实例化的SummaryWriter类刷新,清除缓存
    featureMap_writer.close()  # 关闭
---
conv_layer
fc_layer
classifier

Pytorch核心基础_第10张图片

可以发现,conv_layer1输出64通道的特征图,其中有很多特征图都是黑色的,说明这些卷积核在处理该图片时没有作用。参考Pytorch可视化特征图(虽然不是用tensorBoardX绘制的)

3、网络权重文件保存 & 加载
1)权重保存和深拷贝(“torch.save & module.state_dict”)

模型权重文件保存:

path = 'myNet.pth'

for epoch in epoches:
    train()
    test()

print("finish")
'''保存权重文件'''
torch.save(model.state_dict(), path) # nothing else here

模型权重深拷贝:

from copy import deepcopy

'''加载之前深拷贝的权重变量:weight_before'''
print("加载之前深拷贝的权重变量")
weight_before = deepcopy(model1.state_dict())
model2 = Net_with_fc_Seq(2)
model2.load_state_dict(weight_before)

这一步主要在基于MAML优化器下,模型参数更新时会用到。

2)权重加载(“module.load_state_dict & torch.load")

先初始化模型model,接着model.load_state_dict(torch.load(path))进行参数的加载即可

from code001_basicNet.code0012_initialNet.code00121_constructNet1_withLinear import Net_with_fc_Seq
from code001_basicNet.code0011_initialDataset.code00111_initial_Image_Dataset import *
import cv2


'''显示图像'''
def imshow(img):
    img = img / 2 + 0.5  # unnormalize
    npimg = img.numpy()
    plt.imshow(np.transpose(npimg, (1, 2, 0)))
    plt.show()

image_transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize([0.5], [0.5])])
test_dataset = HandDataset("../code0011_initialDataset/data/myHands",transform=image_transform)
path = "myNet.pth"

'''加载权重文件'''
print("加载权重文件")
model1 = Net_with_fc_Seq(2)
model1.load_state_dict(torch.load(path))

image,_ = test_dataset.__getitem__(0)
imshow(image)

image = torch.unsqueeze(image,dim=0)  #得到的是归一化后的图片,扩维才能喂给conv层
res = model1(image)  #二维数组
pred_index = torch.max(res,axis=1)[1].item()
if pred_index == 0 :
    label = "hands"
else: label = "wheel"
print(label)
3)部分权重加载(适合模型模块对接)

参考Pytorch载入部分参数并冻结

举个栗子:假设我使用MobileNetV3对人脸身份进行分类,在得到准确率较高的权重文件之后,我并不想要最后的分类层(Linear4),我更想要倒数第二层的输出,用于特征的提取。这样在进行人脸身份识别时,通过比较当前输入特征和数据库中特征的相似度,来判断这个人的身份。

具体步骤是:

  • 获取现模型的参数字典(没有LIinear4的MobileNetV3)
  • 在原模型训练的权重文件中(原参数字典),找到与现模型一样的一系列参数名并完成相应赋值,得到加载后的现模型参数字典
  • 通过新的参数字典加载现模型
import torch
import os
import pandas as pd
from model.mobilenetv3 import MobileNetV3_Large
import cv2
import numpy as np
from PIL import Image

identities_names = pd.read_csv("../data/identity.txt")
num_classes = len(identities_names)

model = MobileNetV3_Large(num_classes=num_classes)

# 参考 https://blog.csdn.net/happyeveryday62/article/details/104550948/
# 加载引入的网络模型
model_path = "mobileNetV3.pth"
checkpoint = torch.load(os.path.join(model_path), map_location=torch.device('cpu'))

'''在原mobileNetV3基础上去掉最后一层全连接层,用于特征提取: 先获取现模型的参数字典,接着在原模型中找到与现模型一样的一系列参数名并完成赋值,接着完成模型的加载'''
model_dict =  model.state_dict()  # 获取现有模型的参数字典
# 获取两个模型相同网络层的参数字典
state_dict = {k:v for k,v in checkpoint.items() if k in model_dict.keys()}
model_dict.update(state_dict)  # update必不可少,实现相同key的value同步
model.load_state_dict(model_dict)  # 加载模型部分参数

model.eval()  #模型评估

image = cv2.imread("../data/id_dataset/00000.jpg")
image = cv2.resize(image,(224,224),interpolation=cv2.INTER_CUBIC)
w, h, c = image.shape
image = np.resize(image, (c, w, h))
img_tensor = torch.tensor(image, dtype=torch.float32)
img_tensor = img_tensor.unsqueeze(0)
output = model(img_tensor)
print(output.detach().numpy().tolist())

Note:这种方法同样适用于在同一个模型下用其他数据集训练的权重文件进行模型的初始化,只不过在加载权重时,需要去除掉最后一层全连接层,因为两个数据集训练的模型输出的维数不同(num_classes不同)eg:

state_dict = {k: v for k, v in checkpoint.items() if k in model_dict.keys() and "linear4" not in k}

Note:简单理解下dict的update操作,起始就是“取并集”。

dict_1 = {"apple":1,"banana":2}  #原模型
dict_2 = {"apple":4,"pear":3}  #去掉全连接层的模型
dict_2.update(dict_1)
print(dict_2)
---
{'apple': 1, 'pear': 3, 'banana': 2}
4、小总结(“熟悉流程,再抠细节”)
1)模型训练(“根据损失,修改数据集类的输出类型和格式”)
  • 1)train_acc的计算: 由于网络输出的tensor是一个长度为num_classes的一维数组通过out.max(axis=1)获取下标和label进行比较(所以不建议对label进行one—hot编码,这样比较又要进行更多处理), 统计每一批次分类正确的数目,进而求得每一批次的平均acc
  • 2)需要根据实际使用的损失函数,修改自定义数据集类的label输出类型(float32/int64),否则会报RuntimeError: Expected object of scalar type Long but got scalar type Float for argument #2 'target' in call to _thnn_nll_loss_forward有时需要修改输出格式 (是以[num],还是以num输出),否则会报RuntimeError: multi-target not supported at C:/w/b/windows/pytorch/aten/src\THCUNN/generic/ ClassNLLCriterion.cu:15
  • 3)MSE_loss:label是否要进行one-hot编码,对于MSELoss没什么问题
    CrossEntropyLoss:但是CrossEntropyLoss要求target不能是one-hot编码,要求target=数字,否则会报RuntimeError: multi-target not supported at C:/w/b/windows/pytorch/aten/src\THCUNN/generic/ClassNLLCriterion.cu:15
  • 4)可以通过sum(x.numel() for x in model.parameters())获取网络的总参数量
  • 5)注意每次在反向传播前,先将所有节点的梯度置为0 (optimizer.zero_grad()),避免当前的梯度在前一次backward下累加计算
  • 6)使用dataloader,每次加载批量的训练集时,就不用torch.unsqueeze(image,dim=0)来扩充图片维数了。
2)模型评估(“train,eval,no_grad” ★★★★★)

参考https://blog.csdn.net/qq_38410428/article/details/101102075,https://www.cnblogs.com/ashiqiaozhoujiaz0712/p/14592690.html

  • model.train()

启用 Batch Normalization 和 Dropout。
如果模型中有BN层(Batch Normalization)和Dropout,需要在训练时添加model.train()。model.train()保证BN层能够用到每一批数据的均值和方差。对于Dropout,model.train()随机取一部分网络连接来训练更新参数

  • model.eval()
    不启用 Batch Normalization 和 Dropout。
    如果模型中有BN层(Batch Normalization)和Dropout,在测试时添加model.eval()。model.eval()是保证BN层能够用全部训练数据的均值和方差,即测试过程中要保证BN层的均值和方差不变。对于Dropout,model.eval()利用到了所有网络连接,即不进行随机舍弃神经元

  • torch.no_grad()

eval()模式不会影响各层的gradient计算行为,即gradient计算和存储与training模式一样,只是不进行反向传播(back probagation)(其实model.eval()阶段,loss.backward不会报错,也能正常训练,但是torch.no_grad阶段,loss.backward()会出错RuntimeError: element 0 of tensors does not require grad and does not have a grad_fn)。

with torch.no_grad():则主要是用于停止autograd模块的工作,以起到加速和节省显存的作用。它的作用是将该with语句包裹起来的部分停止梯度的更新,从而节省了GPU算力和显存,但是并不会影响dropout和BN层的行为

如果不在意显存大小和计算时间的话,仅仅使用model.eval()已足够得到正确的validation/test的结果;而with torch.no_grad()则是更进一步加速和节省gpu空间(因为不用计算和存储梯度),从而可以更快计算,也可以跑更大的batch来测试。

  • 假设代码如下(MAML框架)
 '''update inner model params '''
    #训练
    model_support.train()
    for sample, y in support_dataloader:  # 一次循环over

        sample = sample.to(device)
        y = y.to(device)

        optimizer.zero_grad()
        out = model_support(sample)
        loss = crossEntropy_loss(out, y)
        loss.backward(retain_graph=True)

        # 使用SGD更新inner model参数
        optimizer.step()
	
    #验证
    model_support.eval()
    for sample, y in query_dataloader:  # 一次循环over

         sample = sample.to(device)
            y = y.to(device)

            # 计算测试损失
            out = model_support(sample)

            loss = crossEntropy_loss(out, y)
            validate_loss = loss.item()
            pred, pred_index = out.max(axis=1)
            count = (pred_index == y).sum().item()
            acc = count / sample.shape[0]
            print(
                f"train_epoch = {i}, validate_loss = {validate_loss}, validate_acc = {acc}, ",
                end="")
            
   '''calcualate loss from test'''
    #测试
    model_support.train()
    for sample, y in test_dataloader:  # 一次循环over
        
          sample = sample.to(device)
          y = y.to(device)

          out = model_support(sample)
          loss = crossEntropy_loss(out, y)

          pred, pred_index = out.max(axis=1)
          count = (pred_index == y).sum().item()
          acc = count / sample.shape[0]
          print(f"test_loss = {loss.item()}, test_acc = {acc}")

          # test_loss += loss
          # #https://blog.csdn.net/ncc1995/article/details/99542594
          test_loss = test_loss.add_(loss)

          test_temp = test_dataset
          model_list.append(model_support)  # 保存当前模型

在验证和测试阶段,并没用通过反向传播更新参数,因此通过model_support.state_dict()查询参数如下(使用的是MobileNetV3):

但是对于同一个任务池中抽取的查询集和测试集(样本基本相同)来说,在计算损失时却有差别:train阶段启用BN和Dropout来计算损失,而**eval阶段不启用BN和Dropout进行损失值计算**,因此输出结果如下:

meta-iter = 2 
 ---------------------------------
train_epoch = 0, validate_loss = 6.879605770111084, validate_acc = 0.3333333333333333, test_loss = 3.652918577194214, test_acc = 0.6666666666666666
train_epoch = 1, validate_loss = 6.877546787261963, validate_acc = 0.3333333333333333, test_loss = 4.150152683258057, test_acc = 0.7222222222222222
train_epoch = 2, validate_loss = 6.875402450561523, validate_acc = 0.3333333333333333, test_loss = 3.6878652572631836, test_acc = 0.6666666666666666
train_epoch = 3, validate_loss = 6.872812271118164, validate_acc = 0.3333333333333333, test_loss = 3.6603708267211914, test_acc = 0.6666666666666666
train_epoch = 4, validate_loss = 6.869955539703369, validate_acc = 0.3333333333333333, test_loss = 3.8159589767456055, test_acc = 0.6666666666666666
train_epoch = 5, validate_loss = 6.866775035858154, validate_acc = 0.3333333333333333, test_loss = 3.7374703884124756, test_acc = 0.6666666666666666
outer model params
test_final_loss = 6.879781246185303, test_final_acc = 0.3333333333333333
meta-iter = 3 
 ---------------------------------
train_epoch = 0, validate_loss = 6.878757953643799, validate_acc = 0.3333333333333333, test_loss = 3.8334012031555176, test_acc = 0.6666666666666666
train_epoch = 1, validate_loss = 6.876657962799072, validate_acc = 0.3333333333333333, test_loss = 3.873514413833618, test_acc = 0.6666666666666666
train_epoch = 2, validate_loss = 6.874414443969727, validate_acc = 0.3333333333333333, test_loss = 3.8009045124053955, test_acc = 0.7222222222222222
train_epoch = 3, validate_loss = 6.871823787689209, validate_acc = 0.3333333333333333, test_loss = 3.596531867980957, test_acc = 0.6666666666666666
train_epoch = 4, validate_loss = 6.868765354156494, validate_acc = 0.3333333333333333, test_loss = 3.57517147064209, test_acc = 0.6666666666666666
train_epoch = 5, validate_loss = 6.865607738494873, validate_acc = 0.3333333333333333, test_loss = 3.589280843734741, test_acc = 0.6666666666666666
outer model params
test_final_loss = 6.87879753112793, test_final_acc = 0.3333333333333333

要想保证两个集合误差基本一致,要么同时使用model.train(),要么同时使用model.eval(),通常情况下**model.train()model.eval()更适合进行模型训练,而model.eval()适合进行模型评估**,在相同模型下,model.train()计算的损失值比model.eval()小,计算的准确度比model.eval()

3)模型可视化(“在init中使用Sequential构建组件块”)

损失函数可视化很简单,这里主要对特征图可视化进做个小结

特征图可视化:参考https://blog.csdn.net/qq_45445740/article/details/106622013,Pytorch可视化特征图

  • 1)pytorch输出网络结构各层 参考https://blog.csdn.net/hxxjxw/article/details/107734140

  • model._modules.items() 是一个迭代器,可以遍历输出每一层的定义的名称(conv1)和具体层参数信息(Conv2d(3, 6, kernel_size=(5, 5), stride=(1, 1))),而且注意该参数信息是用一个可调用的组件实例保存的。

  • model.modules() 返回网络模型里的所有组成元素,包括不同级别的子元素(逐层拆包)(假设list = [1,2,[3,4]], 则返回[1,2,[3,4]],1,2,[3,4],3,4]

  • model.children() 返回的是最外层的元素(只对第一层拆包)(假设list = [1,2,[3,4]], 则返回 1,2,[3,4]

  • 2)这里注意的是:输出的网络结构是在Net的__init__方法中定义好的实例化组件,如果在forward中使用nn.functional.max_Pool2d,在调用以上方法时,是获取不到max_Pool2d方法的。如果想通过model._modules.items()逐层绘制featureMap,建议将所有的组件都进行实例化(即所有组件在__init__中进行初始化,而不是使用函数形式,这样会导致在逐层传递输入x时,输出的tensor.shape和网络构建的输出不一致,最终会导致全连接层输入参数报错

  • 3)上面观点 2)不完全正确:因为model._modules.items()仅保存实例化的组件,name为key,value为组件对象,但是仅通过遍历所有的name来进行Net的重组,需要保证在forward中该组件只使用过一次,因此在可视化每一个层,或者每一个块时(conv块,fc块等),建议用有序容器(sequential)装起来,保证在forward中是根据初始化的组件依次搭起网络。对于原来未使用Sequential的自定义"conv + 全连接"网络如下Net_with_fc 自定义网络,model._modules.items()输出如下,会发现即使forward中多次使用了该组件,但是遍历时对同一个组件只显示一次

#model._modules.items()输出
---
odict_items([('conv1', Conv2d(3, 6, kernel_size=(5, 5), stride=(1, 1))), ('conv2', Conv2d(6, 16, kernel_size=(7, 7), stride=(1, 1))), ('conv3', Conv2d(16, 32, kernel_size=(9, 9), stride=(1, 1))), ('conv4', Conv2d(32, 64, kernel_size=(11, 11), stride=(1, 1))), ('myRelu', ReLU()), ('myMaxPool2d', MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)), ('myFlatten', Flatten(start_dim=1, end_dim=-1)), ('fc1', Linear(in_features=40832, out_features=2048, bias=True)), ('fc2', Linear(in_features=2048, out_features=2048, bias=True)), ('fc3', Linear(in_features=2048, out_features=2, bias=True)), ('mySoftmax', Softmax(dim=None))])
  • 4)建议Sequential内的组件尽可能少点,方便查看特征图的变换过程(是显示边缘,还是显示细节…)

  • 5)进入tensorboard页面查看:进入logs目录所在的同级目录,输入如下命令:tensorboard --logdir=logs --port 6006,访问http://localhost:6006/

5、运行错误汇总

Q1RuntimeError: Input type (torch.cuda.FloatTensor) and weight type (torch.FloatTensor) should be the same

A1:原因是传入的input是GPU类型的,而权重是CPU类型的,解决方法是将Net放到GPU上 参考https://www.cnblogs.com/expttt/p/13047451.html

Q1RuntimeError: Expected object of scalar type Long but got scalar type Float for argument #2 ‘target’ in call to _thnn_nll_loss_forward

A1:原因是对于crossEntropy_loss(out,label)要求out.dtype为float32,而target.dtype,也就是label.dtype是int64。

Q1RuntimeError: multi-target not supported at C:/w/b/windows/pytorch/aten/src\THCUNN/generic/ClassNLLCriterion.cu:15

A1:原因是 CrossEntropyLoss does not expect a one-hot encoded vector as the target, but class indices,pytorch 中在计算交叉熵损失函数时, 输入的正确 label (target)不能是 one-hot 格式。所以只需要输入数字 4 就行,不需要输入 one hot 格式的 [ 0 0 0 0 1]。函数内部会自己处理成 one hot 格式。 参考https://blog.csdn.net/weixin_42419002/article/details/100988897

六、常见问题 & 经验小结

1、conv层如何接上全连接层(”conv + flatten + linear“)
class Net_with_fc_Seq(nn.Module):
    def __init__(self,num_classes):
        super(Net_with_fc_Seq, self).__init__()  # 先初始化父类Module,对Module实例化(Module里有很多参数,需要先实例化父类,子类才可以用它)
        self.num_classes = num_classes

        self.conv_layer = nn.Sequential(
            nn.Conv2d(3, 6, 5),
            nn.ReLU(),
            nn.MaxPool2d(2),

            nn.Conv2d(6, 16, 7),
            nn.ReLU(),
            nn.MaxPool2d(2),

            nn.Conv2d(16, 32, 9),
            nn.ReLU(),
            nn.MaxPool2d(2),

            nn.Conv2d(32, 64, 11),
            nn.ReLU(),
            nn.MaxPool2d(2) #output = [4, 64, 22, 29]
        )

        self.fc_layer = nn.Sequential(
            nn.Flatten(),  #conv接上fc之前,先对tensor铺平 [4, 64 * 22 *29] 
            nn.Linear(1 * 64 * 22 * 29, 2048),
            nn.Linear(2048, 2048),
            nn.Linear(2048, self.num_classes)
        )

        self.classifier = nn.Softmax()

    '''如果没有实现forward方法,会在_forward_unimplemented中报raise NotImplementedError,这也说明forward方法是实现,而不是重写'''
    def forward(self,x):
        x = self.conv_layer(x)
        x = self.fc_layer(x)
        x = self.classifier(x)
        return x
2、有序容器Sequential的使用(”依次调用组件对象forward“)

Sequential有序容器的使用 参考 https://blog.csdn.net/dss_dssssd/article/details/82980222

  • 1)forward方法如下:

    def forward(self, input):
        for module in self:
            input = module(input)
            return input
    

    对其有序封装的组件实例进行依次遍历,遍历过程中依次调用对应组件实例的forward方法,对x进行处理,完成数据的传递

  • 2)可以在Sequential使用OrderDict来封装组件实例(待验证)
    layer1 = nn.Sequential(OrderedDict([
        ('conv1', nn.Conv2d(1,20,5)),
        ('relu1', nn.ReLU()),
        ('conv2', nn.Conv2d(20,64,5)),
        ('relu2', nn.ReLU())
    ]))
    

    这里得验证一下,在使用model._modules.items(),会不会遍历seq里面的组件实例,如果可以就不用考虑把Sequential内的组件尽可能写少点,就可以查看特征图在不同卷积层,池化层的变化过程。

3、tqdm显示模型运行进度条

必要的输出和可视化结果可以帮助我们实时了解模型的运行情况,其中进度条是跑深度学习模型,尤其是CV项目必备的一个可视化工具(一开始没有用这个工具,让我在AutoDL上跑模型时心急如焚)。

   from tqdm import tqdm
	for epoch in range(0,epoches):
        model.train()
        train_loss = 0
        train_acc = 0
        
        n_train = train_dataset.__len__() #train集的数量
        with tqdm(total=n_train, desc=f'Epoch {epoch + 1}/{epoches}', unit='img', ncols=50) as pbar:     #进度条显示
            for sample,y in train_dataloader:
                sample = sample.to(device)
                y = y.to(device)
                ...
                pbar.update(sample.shape[0])  #进度条滚动控制

            print(f"epoch = {epoch}, train_loss = {train_loss / len(train_dataloader)}, train_acc = {train_acc / len(train_dataloader)}",end=",")
        '''保存权重文件'''
        torch.save(model.state_dict(), "mobileNetV3.pth")  # nothing else here
        print(f"checkpoint{epoch + 1} is saved")

你可能感兴趣的:(#,计算机视觉,机器学习,#,python,pytorch,深度学习,python)