CNN学习笔记

1 DatasetFolder

from torchvision.datasets import DatasetFolder
DatasetFolder(root: str, 
              loader: Callable[[str], Any],
              extensions: Optional[Tuple[str, ...]] = None,
              transform: Optional[Callable] = None, 
              target_transform: Optional[Callable] = None,
              is_valid_file: Optional[Callable[[str], bool]] = None)
Dataset=DatasetFold(...)

  #root:根目录路径,改环境下的文件夹名就是类别名,能自动创建标签target0,1,2……
  #loader:加载数据路径的一个函数,默认cv2.imread
  #extensions:允许的数据后缀列表,包括(‘.jpg’, ‘.jpeg’, ‘.png’, ‘.ppm’, ‘.bmp’, ‘.pgm’, ‘.tif’, ‘.tiff’, ‘.webp’),默认值None
  #transform:图像数据预处理,默认值None
  #target_transform:对图片类别进行预处理的操作,输入为 target,输出对其的转换。如果不传该参数,即对 target 不做任何转换,返回的顺序索引 0,1, 2…
  #is_valid_file:判断每条数据的路径是否有效,默认值None(不能与extensions同时出现

Dataset.class  #查看类别名
Dataset.class_to_idx  #查看类别名和标签
Dataset.samples  #查看全部图片属性:地址,标签

1.1 loader详解

通过lambda定义函数输入图片

from PIL import Image  #图像处理库
loader=lambda x: Image.open(x)

1.2 transform详解

通过 torchvision.transforms对图像进行预处理

import torchvision.transforms as transforms #使用transform 对图像进行预处理
train_tfm = transforms.Compose([
    transforms.Resize((128, 128)),  #重构图片的大小,128*128
    transforms.RandomHorizontalFlip(), # 随机将图片水平翻转
    transforms.RandomRotation(15), # 随机旋转图片
    transforms.ToTensor(), # 将图片(Image)转成Tensor,归一化至[0, 1]
])
transform=train_tfm

更多操作见Pytorch_transforms

1.3 target_transform详解

官网做法(没看懂):
  我们定义的函数可以将一个整数转换成one-hot编码张量,它首先创建一个尺寸为10的零值张量,然后调用scatter__将labely数值对应索引处的0改为1。

target_transform = Lambda(lambda y: torch.zeros(
    10, dtype=torch.float).scatter_(dim=0, index=torch.tensor(y), value=1))

1.4 is_valid_file详解

读取数据集时忽略特定文件:
  参数类型为可调用的函数,该函数传入一个str参数,返回一个bool值。当返回值为True时保留该文件,否则忽略。
  例如,读取时想要忽略所有文件名带‘invalid’的文件。

import platform
from torchvision.datasets import DatasetFolder

class Check(object):
    def __init__(self,
                 key_word: str):
        self.key_word = key_word
        self.separator = '\\' if platform.system() == 'Windows' else '/'

    def __call__(self, 
                 file_name: str) -> bool:
        folders = file_name.split(self.separator)
        return folders[-1].find(self.key_word) < 0
        
 dataset = DatasetFolder('./data', is_valid_file=Check('invalid'))

2 ImageFolder

与DatasetFolder用法基本一致,值得注意的是:
  1.该项中无extensions参数,默认读取所有图片格式
  2.loader为可选项,默认读取为RGB格式的PIL Image对象

from torchvision.datasets import ImageFolder
ImageFolder(root: str, 
            loader: Callable[[str], Any]= default_loader,
            transform: Optional[Callable] = None, 
            target_transform: Optional[Callable] = None,
            is_valid_file: Optional[Callable[[str], bool]] = None)

样例代码:

from torchvision.datasets import ImageFolder
import matplotlib.pyplot as plt

dataset=ImageFolder("./data") #获取路径,返回的是所有图的data、label
print(dataset.classes)    #查看类别名
print(dataset.class_to_idx) #查看类别名,及对应的标签。
print(dataset.samples)  #查看路径里所有的图片,及对应的标签;此处samples=imgs
print(dataset.targets)  #查看图片对应类别标签
#dataset[][]  第一维表示第几张图,第二维表示属性(0:图片数据;1:标签)
print(dataset[0][0])   #打印第一张图的数据
print(dataset[0][1])   #打印第一张图的标签
plt.imshow(dataset[0][0])  #展示第一张图
plt.axis('off') #不显示网格
plt.show()   #显示图片
['00', '01']
{'00': 0, '01': 1}
[('./data\\00\\0_0.jpg', 0), ('./data\\00\\0_1.jpg', 0), ('./data\\00\\0_2.jpg', 0), ('./data\\00\\0_3.jpg', 0), 
('./data\\01\\1_0.jpg', 1), ('./data\\01\\1_1.jpg', 1), ('./data\\01\\1_10.jpg', 1), ('./data\\01\\1_2.jpg', 1), ('./data\\01\\1_3.jpg', 1)]
[0, 0, 0, 0, 1, 1, 1, 1, 1]
<PIL.Image.Image image mode=RGB size=512x512 at 0x1A2A596E588>
0
图0_0, 0

3 DataLoader

3.1 Dataset和DataLoader的区别

  torch.utils.data.Dataset是代表这一数据的抽象类(也就是基类)。我们可以通过继承和重写这个抽象类实现自己的数据类,只需要定义__len__和__getitem__这个两个函数。
  DataLoader是Pytorch中用来处理模型输入数据的一个工具类。组合了数据集(dataset) + 采样器(sampler),并在数据集上提供单线程或多线程(num_workers )的可迭代对象。在DataLoader中有多个参数,这些参数中重要的几个参数的含义说明如下:

 1. epoch:所有的训练样本输入到模型中称为一个epoch; 
 2. iteration:一批样本输入到模型中,成为一个Iteration;
 3. batchszie:批大小,决定一个epoch有多少个Iteration;
 4. 迭代次数(iteration):=样本总数(samples)/批尺寸(batchszie)
 5. dataset (Dataset) :决定数据从哪读取或者从何读取;
 6. batch_size (python:int, optional) :批尺寸(每次训练样本个数,默认为1)
 7. shuffle (bool, optional) :每一个epoch是否为乱序 (default: False)8. num_workers (python:int, optional) :是否多进程读取数据(默认为0);
 9. drop_last (bool, optional) :当样本数不能被batchsize整除时,最后一批数据是否舍弃(default: False)
 10. pin_memory(bool, optional) :如果为True会将数据放置到GPU上去(默认为false)

3.2 DataLoader基本用法

from torch.utils.data import DataLoader
DataLoader(dataset: Dataset[T_co]
           batch_size: Optional[int]
    	   num_workers: int
    	   pin_memory: bool
    	   shuffle:bool
    	   drop_last: bool
    	   timeout: float
    	   sampler: Sampler
    	   prefetch_factor: int
    	   _iterator : Optional['_BaseDataLoaderIter']
    	   __initialized = False
    	   )

  #dataset: 从哪里加载数据集
  #batch_size: 每次批处理的样本数量
  #num_workers: 有多少个子进程读取数据,默认值0
  #pin_memory: 是否返回张量前将数据存到GPU上,默认值False
  #shuffle: 是否在每一轮重新整理数据,默认值False

train_loader = DataLoader(train_set, batch_size=64, shuffle=True, num_workers=0, pin_memory=True)
valid_loader = DataLoader(valid_set, batch_size=64, shuffle=True, num_workers=0, pin_memory=True)
test_loader  = DataLoader(test_set,  batch_size=64, shuffle=False)

更多内容见torch.utils.data.DataLoader中文版

4 torch.nn

4.1 Module

  所有神经网络模块的基类,自定义模型也应该继承这个类,Modules还可以包含其他模块,允许将它们嵌套在树结构中。子模块分配为常规属性的两种方法:

import torch.nn as nn

class Model(nn.Module):
    def __init__(self):
        super(Model, self).__init__()
        self.conv1 = nn.Conv2d(1, 20, 5)# submodule: Conv2d
        self.conv2 = nn.Conv2d(20, 20, 5)
model = Model()
print(model.conv1)

使用给定的名称作为属性访问

import torch.nn as nn
class Model(nn.Module):
    def __init__(self):
        super(Model, self).__init__()
        self.add_module("conv", nn.Conv2d(10, 20, 4))
        #self.conv = nn.Conv2d(10, 20, 4) 和上面这个增加module的方式等价
model = Model()
print(model.conv)

4.2 Sequential

  一个时序容器,Modules会以它传入的顺序被添加到容器中。

1.最常用的简单序贯模型

import torch.nn as nn
model = nn.Sequential(
                  nn.Conv2d(1,20,5),
                  nn.ReLU(),
                  nn.Conv2d(20,64,5),
                  nn.ReLU()
                )
 
print(model)
print(model[2]) # 通过索引获取第几个层
'''运行结果为:
Sequential(
  (0): Conv2d(1, 20, kernel_size=(5, 5), stride=(1, 1))
  (1): ReLU()
  (2): Conv2d(20, 64, kernel_size=(5, 5), stride=(1, 1))
  (3): ReLU()
)
Conv2d(20, 64, kernel_size=(5, 5), stride=(1, 1))
#每一个层是没有名称,默认的是以0、1、2、3来命名
'''

2.给每一个层添加名称

import torch.nn as nn
from collections import OrderedDict
model = nn.Sequential(OrderedDict([
                  ('conv1', nn.Conv2d(1,20,5)),
                  ('relu1', nn.ReLU()),
                  ('conv2', nn.Conv2d(20,64,5)),
                  ('relu2', nn.ReLU())
                ]))
 
print(model)
print(model[2]) # 通过索引获取第几个层
#print(model.conv2) 同上
'''运行结果为:
Sequential(
  (conv1): Conv2d(1, 20, kernel_size=(5, 5), stride=(1, 1))
  (relu1): ReLU()
  (conv2): Conv2d(20, 64, kernel_size=(5, 5), stride=(1, 1))
  (relu2): ReLU()
)
Conv2d(20, 64, kernel_size=(5, 5), stride=(1, 1))
'''

4.3 卷积层

4.3.1 Conv1d

  一维卷积层,输入的尺度是(N, C_in,L_in),输出尺度( N,C_out,L_out)

class torch.nn.Conv1d(in_channels,    #(int)输入信号的通道
					  out_channels,   #(int)卷积产生的通道 
					  kernel_size,    #(int or tuple)卷积核的尺寸
					  stride=1, 	  #(int or tuple, optional)卷积步长
					  padding=0,      #(int or tuple, optional)输入的每一条边补充0的层数
					  dilation=1,     #(int or tuple, `optional``)卷积核元素之间的间距
					  groups=1,       #(int, optional)从输入通道到输出通道的阻塞连接数,默认值1全输出
					  bias=True)      #(bool, optional)如果bias=True,添加偏置

shape:
输入: (N,C_in,L_in)
输出: (N,C_out,L_out)
输入输出的计算方式:
L o u t = f l o o r ( ( L i n + 2 p a d d i n g − d i l a t i o n ( k e r n e r l _ s i z e − 1 ) − 1 ) / s t r i d e + 1 ) L_{out}=floor((L_{in}+2padding-dilation(kernerl\_size-1)-1)/stride+1) Lout=floor((Lin+2paddingdilation(kernerl_size1)1)/stride+1)
变量:
#weight(tensor) - 卷积的权重,大小是(out_channels, in_channels, kernel_size)
#bias(tensor) - 卷积的偏置系数,大小是(out_channel)

4.3.2 Conv2d

  二维卷积层, 输入的尺度是(N, C_in,H_in,W_in),输出尺度(N,C_out,H_out,W_out)

class torch.nn.Conv2d(in_channels,    #(int)输入信号的通道
					  out_channels,   #(int)卷积产生的通道 
					  kernel_size,    #(int or tuple)卷积核的尺寸
					  stride=1, 	  #(int or tuple, optional)卷积步长
					  padding=0,      #(int or tuple, optional)输入的每一条边补充0的层数
					  dilation=1,     #(int or tuple, `optional``)卷积核元素之间的间距
					  groups=1,       #(int, optional)从输入通道到输出通道的阻塞连接数
					  bias=True)      #(bool, optional)如果bias=True,添加偏置

  参数kernel_size,stride,padding,dilation也可以是一个int的数据,此时卷积height和width值相同;也可以是一个tuple数组,tuple的第一维度表示height的数值,tuple的第二维度表示width的数值。
shape:
输入: (N,C_in,H_in,W_in)
输出: (N,C_out,H_out,Wout)
输入输出的计算方式:
H o u t = f l o o r ( ( H i n + 2 p a d d i n g [ 0 ] − d i l a t i o n [ 0 ] ( k e r n e r l _ s i z e [ 0 ] − 1 ) − 1 ) / s t r i d e [ 0 ] + 1 ) H_{out}=floor((H_{in}+2padding[0]-dilation[0](kernerl\_size[0]-1)-1)/stride[0]+1) Hout=floor((Hin+2padding[0]dilation[0](kernerl_size[0]1)1)/stride[0]+1)

W o u t = f l o o r ( ( W i n + 2 p a d d i n g [ 1 ] − d i l a t i o n [ 1 ] ( k e r n e r l _ s i z e [ 1 ] − 1 ) − 1 ) / s t r i d e [ 1 ] + 1 ) W_{out}=floor((W_{in}+2padding[1]-dilation[1](kernerl\_size[1]-1)-1)/stride[1]+1) Wout=floor((Win+2padding[1]dilation[1](kernerl_size[1]1)1)/stride[1]+1)

变量:
weight(tensor) - 卷积的权重,大小是(out_channels, in_channels,kernel_size)
bias(tensor) - 卷积的偏置系数,大小是(out_channel)

4.4 池化层

4.4.1 MaxPool1d

  一维最大池化(max pooling)

class torch.nn.MaxPool1d(kernel_size,  #(int or tuple) - max pooling的窗口大小
					     stride=None,  #(int or tuple, optional) - max pooling的窗口移动的步长。默认值是kernel_size
					     padding=0,    #(int or tuple, optional) - 输入的每一条边补充0的层数
					     dilation=1,   #(int or tuple, optional) – 一个控制窗口中元素步幅的参数
					     return_indices=False, #如果等于True,会返回输出最大值的序号,对于上采样操作会有帮助
					     ceil_mode=False)  #如果等于True,计算输出信号大小的时候,会使用向上取整,代替默认的向下取整的操作

shape:
输入: (N,C_in,L_in)
输出: (N,C_out,Lout)
输入输出的计算方式:
L o u t = f l o o r ( ( L i n + 2 p a d d i n g − d i l a t i o n ( k e r n e l _ s i z e − 1 ) − 1 ) / s t r i d e + 1 ) L_{out}=floor((L_{in} + 2padding - dilation(kernel\_size - 1) - 1)/stride + 1) Lout=floor((Lin+2paddingdilation(kernel_size1)1)/stride+1)

4.4.2 MaxPool2d

  二维最大池化(max pooling)

class torch.nn.MaxPool1d(kernel_size,  #(int or tuple) - max pooling的窗口大小
					     stride=None,  #(int or tuple, optional) - max pooling的窗口移动的步长。默认值是kernel_size
					     padding=0,    #(int or tuple, optional) - 输入的每一条边补充0的层数
					     dilation=1,   #(int or tuple, optional) – 一个控制窗口中元素步幅的参数
					     return_indices=False, #如果等于True,会返回输出最大值的序号,对于上采样操作会有帮助
					     ceil_mode=False)  #如果等于True,计算输出信号大小的时候,会使用向上取整,代替默认的向下取整的操作

shape:
输入: (N,C,H_{in},W_in)
输出: (N,C,H_out,Wout)
输入输出的计算方式:
H o u t = f l o o r ( ( H i n + 2 p a d d i n g [ 0 ] − d i l a t i o n [ 0 ] ( k e r n e l _ s i z e [ 0 ] − 1 ) − 1 ) / s t r i d e [ 0 ] + 1 ) H_{out}=floor((H_{in} + 2padding[0] - dilation[0](kernel\_size[0] - 1) - 1)/stride[0] + 1) Hout=floor((Hin+2padding[0]dilation[0](kernel_size[0]1)1)/stride[0]+1)

W o u t = f l o o r ( ( W i n + 2 p a d d i n g [ 1 ] − d i l a t i o n [ 1 ] ( k e r n e l _ s i z e [ 1 ] − 1 ) − 1 ) / s t r i d e [ 1 ] + 1 ) W_{out}=floor((W_{in} + 2padding[1] - dilation[1](kernel\_size[1] - 1) - 1)/stride[1] + 1) Wout=floor((Win+2padding[1]dilation[1](kernel_size[1]1)1)/stride[1]+1)

4.4.3 其他操作

  除了常用的最大值池化外,还有平均池化和最小值池化的方法。当然torch.nn还提供最大值池化的逆过程(不完全池化,因为有些数据已经丢失),class torch.nn.MaxUnpool2d(kernel_size, stride=None, padding=0)。
详情请见pytorch中文网

4.5 BatchNorm2d

  在卷积神经网络的卷积层之后总会添加BatchNorm2d进行数据的归一化处理,这使得数据在进行Relu之前不会因为数据过大而导致网络性能的不稳定,BatchNorm2d()函数数学原理如下:
在这里插入图片描述

class torch.nn.BatchNorm2d(
	num_features: int,#一般情况下输入的数据格式为batch_size * num_features * height * width,即为特征数,channel数
	eps: float = 1e-5,#分母中添加的一个值,目的是为了计算的稳定性,默认为:1e-5
	momentum: float = 0.1,#用于存储运行过程中均值和方差的一个估计参数
	affine: bool = True,#当设为true时,会给定可以学习的系数矩阵gamma和beta,默认值gamma=1,beta=0
	track_running_stats: bool = True,#若为True,表示需要更新存储的的均值和方差

详细解读nn.BatchNorm2d

4.6 ReLU

  激活函数
R e L U ( x ) = ( x ) + = m a x ( 0 , x ) ReLU(x)=(x)^+=max(0,x) ReLU(x)=(x)+=max(0,x)
在这里插入图片描述

class torch.nn.ReLU(inplace: bool=False) #默认值False

#inplace为True,将会改变输入的数据,否则不会改变原输入,只会产生新的输出

4.7 Linear layers

  PyTorch的nn.Linear()是用于设置网络中的全连接层的,需要注意在二维图像处理的任务中,全连接层的输入与输出一般都设置为二维张量,形状通常为[batch_size, size],不同于卷积层要求输入输出是四维张量。

class torch.nn.Linear(in_features: int, #每个输入样本的大小
                      out_features:int, #每个输出样本的大小
                      bias:bool=True)   #是否学习偏置,默认值Ture

样例代码:

import torch as t
from torch import nn

# in_features由输入张量的形状决定,out_features则决定了输出张量的形状 
connected_layer = nn.Linear(in_features = 64*64*3, out_features = 1)

# 假定输入的图像形状为[64,64,3]
input = t.randn(1,64,64,3)

# 将四维张量转换为二维张量之后,才能作为全连接层的输入
input = input.view(1,64*64*3) #view()函数用来改变张量维度
print(input.shape)
output = connected_layer(input) # 调用全连接层
print(output.shape)

输出结果:

input shape is %s torch.Size([1, 12288])
output shape is %s torch.Size([1, 1])

5 to(device)

  是否启动GPU加速

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

6 Softmax函数

  softmax函数,又称归一化指数函数。它是二分类函数sigmoid在多分类上的推广,目的是将多分类的结果以概率的形式展现出来。
  下图可以很清晰的看出softmax的计算过程:
在这里插入图片描述
(图片来自网络)
假设有一个数组 W W W W y W_{y} Wy表示 W W W中的第 y y y个元素,那么这个元素的softmax值为:
在这里插入图片描述
总结一下softmax函数如何将多分类输出问题转换为概率问题:

  1)分子:通过指数函数,将实数输出映射到零到正无穷。

  2)分母:将所有结果相加,进行归一化。

7 Cross-Entropy Loss

#对于分类任务, 我们使用cross-entropy(交叉熵)作为性能的度量值。
criterion = nn.CrossEntropyLoss()
loss = criterion(logits, labels.to(device))

交叉熵公式表示为,其中 P ( x ) P(x) P(x)表示真实值概率, Q ( x ) Q(x) Q(x)表示预测概率:
在这里插入图片描述
一个batch的 l o s s loss loss为:
在这里插入图片描述

交叉熵损失函数原理详解

8 backward

  反向传播

loss.backward()

pytorch中backward()函数详解

9 源代码

  半监督学习部分还没完善

# 导入必要的软件包
import numpy as np    #大量的数学函数库
import torch
import torch.nn as nn
import torchvision.transforms as transforms
#使用transform 对图像进行预处理
from PIL import Image  #图像处理库
# 当你在做半监督学习时,"ConcatDataset" 和"Subset" 可能会用到
from torch.utils.data import ConcatDataset, DataLoader, Subset
from torchvision.datasets import DatasetFolder
from torch.utils.data import _utils

#进程条
from tqdm.auto import tqdm

# 在训练中进行数据增强是很重要的
# 然而不是所有的增强都是有用的
# 思考哪种类型的数据增强有利于food recognition.
train_tfm = transforms.Compose([
    transforms.Resize((128, 128)),  #重构图片的大小,128*128
    transforms.RandomHorizontalFlip(), # 随机将图片水平翻转
    transforms.RandomRotation(15), # 随机旋转图片
    # 你可以在这里进行一些变换
    # ToTensor()必须是最后一步变换
    transforms.ToTensor(),
    # 将图片(Image)转成Tensor,归一化至[0, 1]
])

# 在测试和验证中我们不需要进行数据增强
# 我们在这里需要的只是调整PIL图像并将其转换为张量
test_tfm = transforms.Compose([
    transforms.Resize((128, 128)),
    transforms.ToTensor(),
])

# 批量尺寸用于训练、验证和测试
# 较大的批量尺寸通常会提供更稳定的梯度
# 但是GPU内存有限,因此请仔细调整
batch_size = 64
# 采用分批次训练(加快参数更新速度),决定一个epoch有多少个迭代次数Iteration=样本总数/批次数

# 构建数据集。
# 参数“ DataSetFolder”讲述了Torchvision如何读取数据
#class paddle.vision.datasets.DatasetFolder(root, loader=None, extensions=None, transform=None, is_valid_file=None)
#root:根目录路径,改环境下的文件夹名就是类别名,能自动创建标签0,1,2、、、  class_to_idx查看类别名和标签,imgs查看全部图片属性
#loader:加载数据路径的一个函数
#extensions:允许的数据后缀列表,包括('.jpg', '.jpeg', '.png', '.ppm', '.bmp', '.pgm', '.tif', '.tiff', '.webp'),默认None
#transform:(可选)图像数据预处理,默认值None
#is_valid_file:(可选)判断每条数据的路径是否有效,默认值None。不可与extensions共存
train_set = DatasetFolder("food-11/training/labeled", loader=lambda x: Image.open(x), extensions="jpg", transform=train_tfm)
valid_set = DatasetFolder("food-11/validation", loader=lambda x: Image.open(x), extensions="jpg", transform=test_tfm)
unlabeled_set = DatasetFolder("food-11/training/unlabeled", loader=lambda x: Image.open(x), extensions="jpg", transform=train_tfm)
test_set = DatasetFolder("food-11/testing", loader=lambda x: Image.open(x), extensions="jpg", transform=test_tfm)

# 构建数据加载程序
'''
DataLoader(dataset, batch_size=1, shuffle=False, sampler=None,
           batch_sampler=None, num_workers=0, collate_fn=None,
           pin_memory=False, drop_last=False, timeout=0,
           worker_init_fn=None)
'''
#dataset: 从哪里加载数据集
#batch_size:每次批处理的样本数量
#num_workers:有多少个子进程读取数据,默认值0
#pin_memory: 是否返回张量前将数据存到GPU上,默认值False
#shuffle: 是否在每一轮重新整理数据,默认值False
train_loader = DataLoader(train_set, batch_size=batch_size, shuffle=True, num_workers=0, pin_memory=True)
valid_loader = DataLoader(valid_set, batch_size=batch_size, shuffle=True, num_workers=0, pin_memory=True)
test_loader = DataLoader(test_set, batch_size=batch_size, shuffle=False)

class Classifier(nn.Module):
    def __init__(self):
        super(Classifier, self).__init__()
        # The arguments for commonly used modules:
        # torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride, padding)  二维卷积层
        # torch.nn.MaxPool2d(kernel_size, stride, padding)                          二维最大池化
        # torch.nn.Linear(in_features: int, out_features:int,bias:bool=True)        全连接层

        # input image size: [3, 128, 128]
        self.cnn = nn.Sequential(  #顺序连接模型
            nn.Conv2d(3, 64, 3, 1, 1),  # [64, 128, 128]  卷积
            nn.BatchNorm2d(64),   #标准化
            nn.ReLU(),  #激活函数,增加非线性表达能力
            nn.MaxPool2d(2, 2, 0),  # [64, 64, 64]  最大值池化

            nn.Conv2d(64, 128, 3, 1, 1),  # [128, 64, 64]
            nn.BatchNorm2d(128),
            nn.ReLU(),
            nn.MaxPool2d(2, 2, 0),  # [128, 32, 32]

            nn.Conv2d(128, 256, 3, 1, 1),  # [256, 32, 32]
            nn.BatchNorm2d(256),
            nn.ReLU(),
            nn.MaxPool2d(2, 2, 0),  # [256, 16, 16]

            nn.Conv2d(256, 512, 3, 1, 1),  # [512, 16, 16]
            nn.BatchNorm2d(512),
            nn.ReLU(),
            nn.MaxPool2d(2, 2, 0),  # [512, 8, 8]

            nn.Conv2d(512, 512, 3, 1, 1),  # [512, 8, 8]
            nn.BatchNorm2d(512),
            nn.ReLU(),
            nn.MaxPool2d(2, 2, 0),  # [512, 4, 4]
        )
        self.fc = nn.Sequential(
            nn.Linear(512 * 4 * 4, 1024),  #全连接层
            nn.ReLU(),     
            nn.Linear(1024, 512),
            nn.ReLU(),
            nn.Linear(512, 11)
        )

    def forward(self, x):
        out = self.cnn(x)
        out = out.view(out.size()[0], -1) # 转为二维张量 size()[0]表示输出行数
        return self.fc(out)

def get_pseudo_labels(dataset, model, threshold=0.75):
    # 此函数使用给定模型生成数据集的伪标签。
    # 它返回 DatasetFolder 的一个实例,其中包含预测置信度超过给定阈值的图像。

    device = "cuda" if torch.cuda.is_available() else "cpu"   #是否启动GPU加速

    # 确保模型处于eval模式.
    model.eval()
    # 定义SoftMax功能。
    softmax = nn.Softmax(dim=-1)

    # 通过批次在数据集上迭代。
    for batch in tqdm(dataset): 
        img, _ = batch

        # 转发数据
        # 使用torch.no_grad()加速进程前进
        with torch.no_grad():
            logits = model(img.to(device))

        # 通过在logits上应用SoftMax来获得概率分布
        probs = softmax(logits)

        # ---------- TODO ----------
        # 过滤数据并构建一个新数据集

    # # 关闭eval模式.
    model.train()
    return dataset

#"cuda" 仅在GPU可用的时候.
device = "cuda" if torch.cuda.is_available() else "cpu"  #启动GPU加速

# 初始化模型,并将其放在指定的设备上。
model = Classifier().to(device)
model.device = device

# 对于分类任务, 我们使用cross-entropy(交叉熵)作为性能的度量值。
criterion = nn.CrossEntropyLoss()

# 初始化optimizer(优化器), 你可以自行微调一些超参数,例如学习率。
optimizer = torch.optim.Adam(model.parameters(), lr=0.0003, weight_decay=1e-5)

# 训练期数
n_epochs = 50

# 是否做半监督学习
do_semi = False

# 训练
for epoch in range(n_epochs):
    # ---------- TODO ----------
    # 在每一个时期,为半监督学习重新标记未标签的数据集
    # 然后你可以将标记的数据集和伪标记的数据集组合起来进行训练
    if do_semi:
        # 使用训练后的模型获得未标记数据的伪标签
        pseudo_set = get_pseudo_labels(unlabeled_set, model)

        # 构建一个新的数据集和一个数据加载程序用于训练
        # 这个仅在半监督学习时使用
        concat_dataset = ConcatDataset([train_set, pseudo_set])
        train_loader = DataLoader(concat_dataset, batch_size=batch_size, shuffle=True, num_workers=0, pin_memory=True)

    # ---------- Training ----------
    # 在训练之前,请确保模型处于train模式
    model.train()

    # 用于记录训练中的信息
    train_loss = []
    train_accs = []

    # 在每一个批次中迭代训练集
    for batch in tqdm(train_loader):

        # 一个批次由图像数据和相应的标签组成
        imgs, labels = batch

        # 发送数据 (确保数据和模型在相同的设备上)
        logits = model(imgs.to(device))

        # 计算交叉熵损失函数
        # 在计算交叉熵时候我们不需要进行softmax,因为它会自动计算
        loss = criterion(logits, labels.to(device))

        # 在上一步中储存的参数梯度应该首先被清除
        optimizer.zero_grad()

        # 计算参数的梯度
        loss.backward()

        # 缩减梯度范围以进行稳定训练
        grad_norm = nn.utils.clip_grad_norm_(model.parameters(), max_norm=10)

        # 用计算的梯度更新参数
        optimizer.step()

        # 计算当前批次的准确性
        acc = (logits.argmax(dim=-1) == labels.to(device)).float().mean()

        # 记录损失和准确性
        train_loss.append(loss.item())  #.item()用来将tensor格式转化为python的数据类型格式
        train_accs.append(acc)

    # 训练集的平均损失和准确性是记录值的平均值
    train_loss = sum(train_loss) / len(train_loss)
    train_acc = sum(train_accs) / len(train_accs)

    # 打印信息
    print(f"[ Train | {epoch + 1:03d}/{n_epochs:03d} ] loss = {train_loss:.5f}, acc = {train_acc:.5f}")

    # ---------- 验证集处理Validation ----------
    # 确保模型处于eval模式以便一些模块例如dropout被禁用并且正常工作
    model.eval()

    # 用于记录验证中的信息
    valid_loss = []
    valid_accs = []

    # 在每一个批次中迭代验证集
    for batch in tqdm(valid_loader):

        # 一个批次由图像数据和相应的标签组成
        imgs, labels = batch

        # 在验证中我们不需要梯度
        # 使用torch.no_grad()加速前进进程
        with torch.no_grad():
          logits = model(imgs.to(device))

        # 我们仍然可以计算损失(但不能计算梯度)
        loss = criterion(logits, labels.to(device))

        # 计算当前批次的准确性
        acc = (logits.argmax(dim=-1) == labels.to(device)).float().mean()

        # 记录损失和准确性
        valid_loss.append(loss.item())
        valid_accs.append(acc)

    # 验证集的平均损失和准确性是记录值的平均值
    valid_loss = sum(valid_loss) / len(valid_loss)
    valid_acc = sum(valid_accs) / len(valid_accs)

    # 打印信息
    print(f"[ Valid | {epoch + 1:03d}/{n_epochs:03d} ] loss = {valid_loss:.5f}, acc = {valid_acc:.5f}")

# 确保模型处于eval模式
# 如果模型处于训练模式,则某些模块(例如Dropout或BatchNorm)会影响。
model.eval()

# 初始化列表以存储预测。
predictions = []

# 在每一个批次中迭代测试集
for batch in tqdm(test_loader):
    # 一个批次由图像数据和相应的标签组成
    # 但是在这里,变量“标签”是没有用的,因为我们没有基础真相
    # 如果打印出标签,你会发现它始终是0
    # 这是因为包装器(DataSetFolder)返回每个批次的图像和标签,
    # 因此,我们必须创建假标签以使其正常工作。
    imgs, labels = batch

    # 在测试时,我们不需要梯度甚至没有标签来计算损失
    # 使用torch.no_grad()加速进程前进
    with torch.no_grad():
        logits = model(imgs.to(device))

    # 以最大的logit作为预测,并记录下来。
    predictions.extend(logits.argmax(dim=-1).cpu().numpy().tolist())

# 将预测保存到文件中
with open("predict.csv", "w") as f:

    # 第一行必须是 "Id, Category"
    f.write("Id,Category\n")

    # 对于其余的行,每个图像ID对应于预测的类。
    for i, pred in  enumerate(predictions):  #枚举
         f.write(f"{i},{pred}\n")
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值