李宏毅2021HW3中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
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+2padding−dilation(kernerl_size−1)−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+2padding−dilation(kernel_size−1)−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,表示需要更新存储的的均值和方差
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()
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")