【AI】Pytorch_数据&预处理

建议点赞收藏关注!持续更新至pytorch大部分内容更完。
本文已达到10w字,故按模块拆开,详见目录导航。

整体框架如下
数据及预处理
模型及其构建
损失函数及优化器


框架代码原理分析方法

在这里插入图片描述
在这里插入图片描述
这个path->torchvision->models可以查看各模型源码

torchsummary

安装这个就可以打印出网络结构
在这里插入图片描述
一般第一次对原图片卷积,kernel_size设置较大,会有大的感受野。

卷积通过stride=2将特征图长宽减半。

output shape里面 list[0]代表batchsize 电脑性能越好,可以输入更多batchsize, list[1]通道数,list[2]、list[3]宽、高 即特征图大小。
当特征图缩小时,则卷积核数增倍,通道数就会增多。

通常一层中有卷积三大件(卷积、BN batch normalization、RELU,BN batch normalization)使得以比较大的学习率训练模型,可以很快的收敛。

out = self.conv1(x)
out = F.relu(self.bn1(out))

卷积向全联接(Dropout在前面 防止过拟合,Linear)过渡的中间层中一般会用AdaptiveAvgPool/AdaptiveMaxPool,减少了网络参数个数;正则化;图片输入大小更自由input size 即

from torchsummary import summary
summary(net, input_size=(3, 512, 512))
#不会像传统LeNet 只能输入input_size=(3, 32, 32) 32x32

'''
还有一个防止过拟合的地方在于weight_decay
Weight Decay是一个正则化技术,作用是抑制模型的过拟合,以此来提高模型的泛化性。
它是通过给损失函数增加模型权重L2范数的惩罚(penalty)来让模型权重不要太大,以此来减小模型的复杂度,从而抑制模型的过拟合。

'''
# ============================ step 4/5 优化器 ============================
optimizer = optim.Adam(net.parameters(), lr=LR, betas=(0.9, 0.999), eps=1e-08, weight_decay=1e-1)                        # 选择优化器
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.1)     # 设置学习率下降策略

在这里插入图片描述
比如说原图是左边的这个形状,128x14x14,这两个池化会分别将黄色平面压扁,各自形成128长度的向量,然后将他们cat 即链接起来,得到128x2=256长度的向量

gpu加速

打开jupyter lab,打开一个file,右上角选择kernel为torchgpuprivate
要在 Mac M1的GPU 上运行 PyTorch 代码,使用命令 torch.device(“mps”)来指定。
在这里插入图片描述
在这里插入图片描述
之后就只需要引用gpu_a,gpu_b,其他与cpu_a…的操作没有区别。

数据

getitem一定要高效,数据预处理要提前做好,数据预处理不要放到getitem中,getitem中就是指定路径读取处理后的数据,这样训练模型最高效,即空间换时间。

数据结构

张量Tensor

  1. ==数组概念,1…n维
    标量(Scalar):0阶张量
    向量(Vector):1阶张量
    矩阵(Matrix):2阶张量

  2. 有8个属性
    data
    dtype:有很多数据类型
    shape : 如 (6,6,6,6)
    device:所在设备cpu/gpu
    grad:data的梯度
    grad_fn:创建tensor 的function,是自动求导的关键,加法还是乘法
    requires_grad:指示是否需要梯度,不一定所有的张量都需要设置梯度
    is_leaf:指示是否是叶子节点(张量),计算图中再介绍。

  3. api

device=torch.device("mps")
torch.tensor(
			data, #list or numpy
			dtype=None, # 默认==data的类型
			device=None,
			requires_grad=False,
			pin_memory=False # 是否存于锁页内存 一般默认
			)

#创建
arr=np.ones((3,3))
t=torch.tensor(arr,device=device) 
# t.dtype == arr.dtype==float64

torch.from_numpy(ndarray) #从numpy创建tensor
#从torch.from_numpy创建的tensor与原来的ndarray共享内存,一改跟着改

#打印地址查看 :修改tensor值后 id也会变化
id(arr) 

torch.zeros(*size,#形状 如(3,3)
			out=None,#输出的张量 相当于赋值
			dtype=None,#内存中的布局形式
			layout=torch.strided,#通常默认,除非稀疏矩阵 用sparse_coo
			device=None,
			requires_grad=False)
torch.zeros_like(input,#根据Input的形状创建
				dtype=None,
				layout=None,
				device=None,
				requires_grad=False)

zeros 换成 ones

torch.full(size,#(3,3)
		   fill_value,#张量的值 如10
		   out=None,
		   dtype=None,
		   layout=torch.strided,
		   device=None,
		   requires_grad=False)

torch.arange(start=0, #创建等差1维张量
			end, #左闭右开
			step=1,#公差 默认1
			out=None,
		   dtype=None,
		   layout=torch.strided,
		   device=None,
		   requires_grad=False)

torch.linspace(start=0, #创建均分1维张量
			end, #左闭右闭
			steps=1,#数列长度
			#步长=(end-start)/step -1
			out=None,
		   dtype=None,
		   layout=torch.strided,
		   device=None,
		   requires_grad=False)

torch.logspace(base=...底默认10,...) #对数均分

torch.eye(n,#行数  #默认方阵
		  m,#列数
		  ...
		) #创建单位对角矩阵,2维张量,

torch.normal(mean,std,out=None)
#正态分布,mean均值,std方差
#mean std都可以分别是标量 or张量

#都是张量的情况的normal如何计算?
mean=[1,2,3,4]
std=[1,2,3,4]
t_normal=torch.normal(mean,std) #得到[1.66,2.53,3.18,6.48]
#1.66是通过mean=1,std=1的正态分布采样得到

torch.randn(*size, #标准正态分布  
			out=None,
			dtype=None,
			layout=torch.strided,
			device=None,
		   requires_grad=False)
torch.randn_like()

torch.rand() 
#生成在区间 [0, 1) 内均匀分布的随机数。
#size 参数是一个表示所需张量形状的元组或整数。可以生成任何形状的随机张量。
torch.rand(4) #shape :(1,4)
torch.rand(2,3)#shape:(2,3)

torch.randint(low=0, #均匀分布 前闭后开
			high,
			size, 
			out=None,
			dtype=None,
			layout=torch.strided,
			device=None,
		   requires_grad=False) 

torch.randint_like()

torch.randperm(n,#张量长度
			out=None,
			dtype=None,
			layout=torch.strided,
			device=None,
		   requires_grad=False) 
#生成0-n-1随机排列

torch.bernoulli(input,
				*,
				generator=None,
				out=None)
 #以input为概率,生成伯努利分布,即0-1分布,两点分布

#张量拼接
torch.cat(tensors,dim=0,out=None)#张量拼接:按维度dim拼接,不会扩张维度
torch.stack(tensors,dim=0,out=None)
#在新创建的维度dim上拼接,会扩张,如果已经在这个维度上有值 则会往后移 例如2,3->2,2,3

#例如
t=torch.ones((2,3))
t_0=torch.cat([t,t],dim=0) #shape 2,3->4,3
t_1=torch.cat([t,t],dim=1)#2,3->2,6
t_2=torch.stack([t,t],dim=2)#2,3->2,3,2

#也就是说 dim=0 在shape中 第一个数值变化,dim=1是第二个数值变化,dim=2则是第三个数值改变 (1,2,3,....)

#张量切分
torch.chunk(input,chunks,dim=0)#按dim维度平均切分,若不能整除,最后一份张量小于其他张量
#chunks要切分的份数

a=torch.ones((2,5))
list_of_tensors=torch.chunk(a,dim=1,chunks=2)
#切分成(2,3),(2,2)
#同理,假如是(2,7),chunks=3,切分成(2,3),(2,3),(2,1) 除法向上取整

torch.split(tensor,split_size_or_sections,dim=0)
#split_size_or_sections:int型时表示每份长度,list时按list元素切分

#张量索引
torch.index_select(input,dim,index,out=None) 
#在维度dim上,按index索引数据,index指要索引数据的序号,dtype需为long

t=torch.randint(0,9,size=(3,3))
#t: [[4,5,0],[5,7,1],[2,5,8]]
idx=torch.tensor([0,2],type=torch.long)
t_select=torch.index_select(t,dim=0,index=idx)
#t_select:[[4,5,0],[2,5,8]]

torch.masked_select(input,mask,out=None)#根据mask中的true进行索引,返回一维张量
#mask是与input同形状的布尔类型张量
t=torch.randint(0,9,size=(3,3))
mask=t.ge(5) #greater than or equal 5 来生成布尔 还有le,lt 小于等于,小于
t_select=torch.masked_select(t,mask)
#t: [[4,5,0],[5,7,1],[2,5,8]]
#mask=[[false,true,false],[true,true,false],[false,true,true]]

#张量变换
torch.reshape(input,shape)
#张量在内存中连续时,新张量与input共享数据内存,及映射实际地址是一样的,一个改变另一个跟着动
#shape新张量形状

t=torch.randperm(8)
t_reshape=torch.reshape(t,(-1,4)) 
#-1表示这个维度先不管,等其他维度计算完之后自动得到剩下的个数
#8 reshape(-1,4) 则8/4=2 ->(2,4)
#8 reshape(-1,2,2) 则8/2/2=2 ->(2,2,2)

torch.transpose(input,dim0,dim1)#交换张量的两个维度
#即[2,3,4]->[2,4,3]
torch.t(input)#2维张量转置,等价于对矩阵进行torch.transpose(input,0,1)

torch.squeeze(input,dim=None,out=None)
#压缩长度为1的维度(轴),dim==None移除所有长度为1的轴;指定维度则当且仅当该维度长度为1时被移除

torch.unsqueeze(input,dim=None,out=None)#依据dim利用现有元素延展维度

    t = torch.rand((1, 2, 3, 1))
    t_sq = torch.squeeze(t)
    t_0 = torch.squeeze(t, dim=0)
    t_1 = torch.squeeze(t, dim=1)
    print(t.shape)#[1,2,3,1]
    print(t_sq.shape)#[2,3]
    print(t_0.shape)#[2,3,1]
    print(t_1.shape)#[1,2,3,1]

x = torch.tensor([1, 2, 3, 4])
y = torch.unsqueeze(x, 0)#在第0维扩展,第0维大小为1
y,y.shape
#输出结果如下:(tensor([[1, 2, 3, 4]]), torch.Size([1, 4]))

y = torch.unsqueeze(x, 1)#在第1维扩展,第1维大小为1 
y,y.shape
#输出结果如下:
(tensor([[1],
         [2],
         [3],
         [4]]),
 torch.Size([4, 1]))

y = torch.unsqueeze(x, -1)#在第最后一维扩展,最后一维大小为1
y,y.shape
#输出结果跟dim=1相同

#张量的数学运算
1.加减乘除
torch.add(input,alpha=1,other,out=None)
	#alpha乘项因子 input+alpha*other 不只可以做加法
	#other第二个张量 
	
torch.addcdiv(input,value=1,tensor1,tensor2,out=None)
	#结合除法 out=input+value*tensor1/tensor2 常用于优化过程
torch.addcmul()
	#结合乘法out=input+value*tensor1*tensor2
torch.addcmul(t, tensor1=t1, tensor2=t2, value=x)#tensor1与 tensor2的shape相同

torch.sub()
torch.div()
torch.mul()

2.对数、指数、幂函数
torch.log(input,out=None)
torch.log10(input,out=None)
torch.log2(input,out=None)
torch.exp(input,out=None)
torch.pow()

3.三角函数
torch.abs(input,out=None)
torch.acos(input,out=None)
torch.cosh(input,out=None)
torch.cos(input,out=None)
torch.asin(input,out=None)
torch.atan(input,out=None)
torch.atan2(input,other,out=None)

torch.autograd自动求导系统

#自动求梯度
torch.autograd.backward(tensors,#用于求导的张量
						grad_tensors=None,#设置多层梯度权重
						retain_graph=None,#保存计算图,否则会释放
						create_graph=False)#创建导数计算图,用于高阶求导
#retain_graph=True时就可以继续反向传播,使用该计算图

注意:
1.梯度不自动清0,会叠加上去
    w = torch.tensor([1.], requires_grad=True)
    x = torch.tensor([2.], requires_grad=True)

    for i in range(4):
        a = torch.add(w, x)
        b = torch.add(w, 1)
        y = torch.mul(a, b)

        y.backward()
        print(w.grad) #不断循环n次 得到n*5 叠加起来了

        w.grad.zero_()#梯度清0 zero后面有_下划线,表示是原地操作
2. 依赖叶子节点的节点,其requires_grad默认为True
3. 叶子节点不可执行in-place操作,即原地操作,也就是在原始内存中改变这个数据,改变数据时id会改变
为什么叶子节点不能执行in-place操作?因为不断前向传播时,上面的节点是依赖底层叶子节点的值进行计算的,如dy/da=w+1,w是叶子节点,所以w的这个原始数据不可以被改变,否则会出现计算错误



    w = torch.tensor([1.], requires_grad=True)
    x = torch.tensor([2.], requires_grad=True)

    a = torch.add(w, x)     # retain_grad()
    b = torch.add(w, 1)

    y0 = torch.mul(a, b)    # y0 = (x+w) * (w+1)
    y1 = torch.add(a, b)    # y1 = (x+w) + (w+1)    dy1/dw = 2

    loss = torch.cat([y0, y1], dim=0)       # [y0, y1]
    grad_tensors = torch.tensor([1., 2.])#1,2为权重

    loss.backward(gradient=grad_tensors)    # gradient 传入 torch.autograd.backward()中的grad_tensors

    print(w.grad)#得到9 怎么算的 y0对应的梯度*权重1+y1对应的梯度*权重2

torch.autograd.grad(outputs,inputs,
					grad_tensors=None,#设置多层梯度权重
					retain_graph=None,#保存计算图,否则会释放
					create_graph=False)
#outputs用于求导的张量 如loss, inputs需要梯度的张量

    x = torch.tensor([3.], requires_grad=True)
    y = torch.pow(x, 2)     # y = x**2

    grad_1 = torch.autograd.grad(y, x, create_graph=True)   # grad_1 = dy/dx = 2x = 2 * 3 = 6
    #create_graph=True用来创建导数的计算图,用于高阶求导
    print(grad_1)

    grad_2 = torch.autograd.grad(grad_1[0], x)              #求二阶导数 ,grad_2 = d(dy/dx)/dx = d(2x)/dx = 2
    print(grad_2)

Variable

已经合并到tensor 但是对于理解张量有帮助。variable是torch.autograd中的数据类型,用于封装tensor进行自动求导,有五个属性
data:被包装的tensor
grad:data的梯度
grad_fn:创建tensor 的function,是自动求导的关键,加法还是乘法
requires_grad:指示是否需要梯度,不一定所有的张量都需要设置梯度
is_leaf:指示是否是叶子节点(张量),计算图中再介绍。

计算图 动态图 静态图

计算图是用来描述运算的有向无环图,便于求导
计算图有两个主要元素:结点(Node)和边(Edge)
结点表示数据,如向量,矩阵,张量
边表示运算,如加减乘除卷积等

这个表示方式跟数据结构中的有向无环图,用最少的节点表达表达式异曲同工。
例如:y=(x+w)(w+1) ,a=x+w, b=w+1, y=ab,从下向上执行
在这里插入图片描述

为什么计算图便于求导?因为这个图也相当于是求导里面的链式法则。
dy/dw=dy/da * da/dw+ dy/db * db/dw = b+a=x+w+w+1=2w+x+1 代入具体数值
dy/da=b
da/dw=1
dy/db=a
db/dw=1

import torch

w = torch.tensor([1.], requires_grad=True)
x = torch.tensor([2.], requires_grad=True)

a = torch.add(w, x)     # retain_grad()
b = torch.add(w, 1)
y = torch.mul(a, b)

y.backward()
print(w.grad)

# 查看叶子结点
# print("is_leaf:\n", w.is_leaf, x.is_leaf, a.is_leaf, b.is_leaf, y.is_leaf)
#只有w,x是叶子节点 为True

# 查看梯度 非叶子节点的梯度为None
# print("gradient:\n", w.grad, x.grad, a.grad, b.grad, y.grad)
#若想要查看非叶子节点的梯度,需要在反向传播之前,添加a.retain_grad()

# 查看 grad_fn  创建该张量时所用的方法 mul or add ...
print("grad_fn:\n", w.grad_fn, x.grad_fn, a.grad_fn, b.grad_fn, y.grad_fn) 
#w,x的grad_fn是None 因为他们是直接创建的张量

叶子节点:用户创建的节点即叶子节点,如x与w

动态图 vs静态图
动态图:运算与搭建同时进行 灵活 该用到什么的时候再写那步骤的代码
静态图:先搭建图,后运算 不灵活 已经都建立好了步骤 如tensorflow 传入tensor之后就无法改变其流动方向

Pytorch 是动态图,每一次训练,都会销毁图并重新创建,这样做花销很大,但是更加灵活。
而 Tensorflow 是静态图,一旦定义训练时就不能修改。

预处理、数据增强

function_下划线() 表示这个是function的原地操作,直接在这个变量上修改,而非深拷贝的修改。
常出现的inplace 也是原地操作的意思

  • 数据收集:分成img,label

  • 数据划分:分成train,valid,test集,而valid集是用来测是否过拟合并挑选模型的

  • 数据读取:DataLoader,分成[sampler输出index,生成选取一个batchsize 的index,打包成list],[DataSet data_dir读取图片和标签],[Dataset中的getitem读出img,label]
    为什么是这样?通过调试,ctrl并点击对应的函数名跳转至对应的类看到的
    DataLoader->DataLoaderIter(单进程还是多进程)->Sampler获取index->DatasetFetcher->Dataset->(根据Index )getitem->Img,Label->collate_fn->BatchData

  • 数据预处理:transforms
    这就要用到安装时所提及的torchvision(计算机视觉工具包)
    模块如下
    torchvision.transforms:常用的图像预处理方法
    torchvision.datasets:常用数据集的dataset实现,MNIST,CIFAR-10,ImageNet等
    torchvision.model:常用的预训练模型,AlexNet,VGG,ResNet,GoogleLeNet等

    • transforms 包含常用图像预处理方法
      数据增强使得训练集变换,使模型更具泛化能力
  1. 数据中心化
  2. 数据标准化:可以加快模型的收敛
    即代码中的bias变大,使得整体数据向上向右偏,导致iteration要变大才可能收敛到之前相同的loss
    transforms.Normalize(norm_mean, norm_std)
  3. 缩放
    transforms.Resize
  4. 裁剪 5种
    transforms.CenterCrop
    transforms.RandomCrop
    transforms.RandomResizedCrop
    transforms.FiveCrop
    transforms.TenCrop
  5. 旋转 &翻转 3种
    transforms.RandomHorizontalFlip
    transforms.RandomVerticalFlip
    transforms.RandomRotation
  6. 填充
    transforms.Pad
  7. 噪声增加
    transforms.RandomErasing(p=0.5, scale=(0.02, 0.33), ratio=(0.3, 3.3), value=0, inplace=False)
  8. 灰度变换
    transforms.Grayscale(num_output_channels)
    transforms.RandomGrayscale(num_output_channels, p=0.1)
  9. 线性变换
    torchvision.transforms.LinearTransformation(transformation_matrix, mean_vector):对tensor进行矩阵变换后减去一个向量
  10. 仿射变换
    transforms.RandomAffine(degrees, translate=None, scale=None, shear=None, resample=False, fillcolor=0)
  11. 亮度、饱和度及对比度变换
    transforms.ColorJitter(brightness=0, contrast=0, saturation=0, hue=0)
  12. 自定义
    transforms.Lambda(lambd)
    transforms.RandomChoice([transforms1, transforms2, transforms3])
    transforms.RandomApply([transforms1, transforms2, transforms3], p=0.5)
    transforms.RandomOrder([transforms1, transforms2, transforms3])
  13. 格式转换
    transforms.ToTensor()

数据增强为了观察测试集,使训练集更接近测试集。

#数据读取
torch.utils.data.DataLoader(dataset,#Dataset类,决定数据从哪读取及如何读取
							batch_size=1,#批大小
							shuffle=False,#每个epoch是否乱序
							sampler=None,
							batch_sampler=None,
							num_workers=0,#是否多进程读取数据
							collate_fn=None,
							pin_memory=False,
							drop_last=False,#当样本数不能被batchsize整除,是否舍弃最后一批数据
							timeout=0,
							worker_init_fn=None,
							multiprocessing_context=None)
'''
epoch:所有训练样本都已输入到模型中,称为一个epoch
iteration:训练一批就是一次iteration
batch 一批样本
batchsize:批大小,决定一个epoch里有多少个iteration

例如,样本总数80,batchsize 8, 1epoch=10 iteration
样本总数87,batchsize 8, 1epoch=10 iteration (if drop_last=True)
样本总数87,batchsize 8, 1epoch=11 iteration (if drop_last=False)

torch.utils.data.Dataset 即
class Dataset(object):
	def __getitem__(self,index):
		raise NotImplementedError
	def __add__(self,other):
		return ConcatDataset([self,other])

Dataset抽象类,所有自定义的Dataset需要继承它,并且复写__getitem__()
getitem作用是接收一个索引,返回一个样本
'''
#数据标准化
transforms.Normalize(mean,std,inplace=False)
'''
逐channel对图像进行标准化
output=(input-mean)/std
inplace:是否原地操作
'''

#从图像中心裁剪图片
transforms.CenterCrop(size)
#size: 若为int,则尺寸为size*size; 若为(h,w),则尺寸为h*w.多余的用黑色表示。

#裁剪
transforms.RandomCrop(size, padding=None, pad_if_needed=False, fill=0, padding_mode='constant')
'''
size: 若为int,则尺寸为size*size; 若为(h,w),则尺寸为h*w;
padding: 设置填充大小:
I. 当padding为a时,左右上下均填充a个像素;
II. 当padding为(a,b)时,左右填充a个像素,上下填充b个像素;
III. 当padding为(a,b,c,d)时,左、上、右、下分别填充a、b、c、d;
pad_if_need:若设定的size大于原图像尺寸,则填充;
padding_mode:填充模式,有4种模式:
I. constant:像素值由fill设定;
II. edge:像素值由图像边缘的像素值颜色决定;
III. reflect:镜像填充,最后一个像素不镜像,eg. [1,2,3,4] --> [3,2,1,2,3,4,3,2];
向左:由于1不会镜像,所以左边镜像2、3;
向右:由于4不会镜像,所以右边镜像3、2;
IV. symmetric:镜像填充,最后一个像素镜像,eg. [1,2,3,4] --> [2,1,1,2,3,4,4,3];
向左:1、2镜像;
向右:4、3镜像;
fill:当padding_mode='constant'时,用于设置填充的像素值颜色;
'''
train_transform = transforms.Compose([
    transforms.Resize((224, 224)), #图片统一缩放到244*244

    # 2 RandomCrop
    transforms.RandomCrop(224, padding=16),
    transforms.RandomCrop(224, padding=(16, 64)),
    transforms.RandomCrop(224, padding=16, fill=(255, 0, 0)), #fill=(255, 0, 0)RGB颜色
    
    #当size大于图片尺寸,即512大于244,pad_if_needed必须设置为True,否则会报错,其他区域会填充黑色(0,0,0)
    transforms.RandomCrop(512, pad_if_needed=True),   # pad_if_needed=True
    transforms.RandomCrop(224, padding=64, padding_mode='edge'), #边缘
    transforms.RandomCrop(224, padding=64, padding_mode='reflect'), #镜像
    transforms.RandomCrop(1024, padding=1024, padding_mode='symmetric'), #镜像

    transforms.ToTensor(),
    transforms.Normalize(norm_mean, norm_std),
])

transforms.RandomResizedCrop(size, scale=(0.08, 1.0), ratio=(3/4, 4/3), interpolation=<InterpolationMode.BILINEAR: 'bilinear'>)
'''
size: 裁剪图片尺寸,若为int,则尺寸为size*size; 若为(h,w),则尺寸为h*w,size是最后图片的尺寸;
scale: 随机裁剪面积比例,默认区间(0.08,1),scale默认是随机选取0.08-1之间的一个数
ratio: 随机长宽比,默认区间(3/4,4/3),ratio默认是随机选取3/4-4/3之间的一个数
interpolation: 插值方法,裁剪后的图片尺寸可能小于size,这时需要插值。
PIL. Image. NEAREST 最近邻
PIL. Image. BILINEAR 双线性插值
PIL. Image. BICUBIC 双三次差值
(3)步骤:
随机确定scale和ratio,然后对原始图片进行裁剪,再将选取的片段缩放到size大小;
'''

transforms.FiveCrop(size)
transforms.TenCrop(size, vertical_flip=False)
'''
均返回turple形式
在图像的左上、右上、左下、右下、中心随机剪裁出尺寸为size的5张图片,然后再对这5张照片进行水平或者垂直镜像来获得总共10张图片;
size: 裁剪图片尺寸,若为int,则尺寸为size*size; 若为(h,w),则尺寸为h*w;
vertical_flip: 是否垂直翻转,默认为False代表进行水平翻转;
'''
train_transform = transforms.Compose([
    transforms.Resize((224, 224)), #图片统一缩放到244*244
    # 4 FiveCrop
    transforms.FiveCrop(112), #单独使用错误,直接使用transforms.FiveCrop(112)会报错,需要跟下一行一起使用 tencrop同理
    #lamda的冒号之前是函数的输入(crops),冒号之后是函数的返回值
    transforms.Lambda(lambda crops: torch.stack([(transforms.ToTensor()(crop)) for crop in crops])), #这里需要将PIL image->tensor/nparray才不会报错。
])
'''
使用FiveCrop时需要使用五维可视化,这是因为inputs为五维(batch_size*ncrops*channel*图像宽*图像高)
'''

#用户自定义lambda方法 匿名函数
transforms.Lambda(lambd)
lambda [arg1 [, arg2, ..., argn]]: expression

transforms.FiveCrop(112), #单独使用错误,直接使用transforms.FiveCrop(112)会报错,需要跟下一行一起使用
#因为Five/TenCrop都是返回tuple形式的image,而transforms需要PIL image或者Tensor形式
#lamda的冒号之前是上一个函数的输出(crops,长度为10的turple),冒号之后是函数的返回值
transforms.Lambda(lambda crops: torch.stack([(transforms.ToTensor()(crop)) for crop in crops])), #这里进行了ToTensor(),后面不需要执行Totensor()和Normalize
#torch.stack()之前提过,张量拼接,默认在第一个维度 行 进行拼接

# 构建MyDataset实例
train_data = RMBDataset(data_dir=train_dir, transform=train_transform)
valid_data = RMBDataset(data_dir=valid_dir, transform=valid_transform)

# 构建DataLoder
train_loader = DataLoader(dataset=train_data, batch_size=BATCH_SIZE, shuffle=True)
valid_loader = DataLoader(dataset=valid_data, batch_size=BATCH_SIZE)

# 训练
for epoch in range(MAX_EPOCH):
    for i, data in enumerate(train_loader):
        inputs, labels = data   
        #五维可视化
        #使用FiveCrop时inputs为五维:batch_size*ncrops*chanel*图像宽*图像高,此时ncrops=5
        bs, ncrops, c, h, w = inputs.shape
        for n in range(ncrops):
            img_tensor = inputs[0, ...]  # C H W
            img = transform_invert(img_tensor, train_transform)
            #逆变换 tensor->PIL image 可以可视化了
            
            plt.imshow(img)
            plt.show()
            plt.pause(1)


#transform逆操作 tensor->PIL image
transform_invert(img,transform_train)

#翻转
transforms.RandomHorizontalFlip(p=0.5)#水平翻转 p: 反转概率
transforms.RandomVerticalFlip(p=0.5)#垂直翻转
#实例
train_transform = transforms.Compose([
    transforms.Resize((224, 224)), #图片统一缩放到244*244

    # 1 Horizontal Flip
    transforms.RandomHorizontalFlip(p=1), #执行水平翻转的概率为1
	# 2 Vertical Flip
    transforms.RandomVerticalFlip(p=0.5), #执行垂直翻转的概率为0.5
    
    transforms.ToTensor(),
    transforms.Normalize(norm_mean, norm_std),
])

#旋转
transforms.RandomRotation(degrees, expand=False, center=None, fill=0, resample=None)
'''
degrees: 旋转角度;
I. 当degrees为a时,在区间(-a,a)之间随机选择旋转角度;
II. 当degrees为(a,b)时,在区间(a,b)之间随机选择旋转角度;
resample: 重采样方法;一般默认即可。
expand: 是否扩大图片以保持原图信息,因为旋转后可能有些信息被遮挡了而丢失,如果扩大尺寸则可以显示完整图片信息;注意如果shape变了,在组合成batch_size data即构建dataloader之前要resize
center: 旋转点设置,默认沿着中心旋转;

'''
#填充
transforms.Pad(padding, fill=0, padding_mode='constant')
'''
padding: 设置填充大小:
I. 当padding为a时,左右上下均填充a个像素;
II. 当padding为(a,b)时,左右填充a个像素,上下填充b个像素;
III. 当padding为(a,b,c,d)时,左、上、右、下分别填充a、b、c、d;
padding_mode:填充模式,有4种模式:
I. constant:像素值由fill设定;
II. edge:像素值由图像边缘的像素值决定;
III. reflect:镜像填充,最后一个像素不镜像,eg. [1,2,3,4] --> [3,2,1,2,3,4,3,2];
IV. symmetric:镜像填充,最后一个像素镜像,eg. [1,2,3,4] --> [2,1,1,2,3,4,4,3];

fill:当padding_mode='constant'时,用于设置填充的像素值,(R,G,B) or (Gray);
'''
train_transform = transforms.Compose([
    transforms.Resize((224, 224)),

    # 1 Pad
    transforms.Pad(padding=32, fill=(255, 0, 0), padding_mode='constant'),
    transforms.Pad(padding=(8, 64), fill=(255, 0, 0), padding_mode='constant'),
    transforms.Pad(padding=(8, 16, 32, 64), fill=(255, 0, 0), padding_mode='constant'),
    transforms.Pad(padding=(8, 16, 32, 64), fill=(255, 0, 0), padding_mode='symmetric'),

    transforms.ToTensor(),
    transforms.Normalize(norm_mean, norm_std),
])

#调整图片的亮度、对比度、饱和度和色相;
transforms.ColorJitter(brightness=0, contrast=0, saturation=0, hue=0)
'''
brightness: 亮度调整因子,brightness > 1会更亮,brightness < 1会更暗;
I. 当brightness为a时,从区间[max(0,1-a),1+a]中随机选择;
II. 当brightness为(a,b)时,从区间[a,b]中随机选择;
contrast: 对比度参数,同brightness,对比度越低,图像越灰;
saturation: 饱和度参数,同brightness,饱和度越低,图像越暗淡;
hue: 色相参数;
I. 当hue为a时,从[-a,a]中随机选择参数,注意a的区间是0 ≤ a ≤ 0.5;
II. 当hue为(a,b)时,从[a,b]区间中随机选择参数,注意-0.5 ≤ a ≤ b ≤ 0.5;
'''
train_transform = transforms.Compose([
    transforms.Resize((224, 224)),

    # 2 ColorJitter
    transforms.ColorJitter(brightness=0.5),
    transforms.ColorJitter(contrast=0.5),
    transforms.ColorJitter(saturation=0.5),
    transforms.ColorJitter(hue=0.3),

    transforms.ToTensor(),
    transforms.Normalize(norm_mean, norm_std),
])

#转化为灰度图
transforms.Grayscale(num_output_channels)
'''
num_output_channels: 输出通道数,只能设置为1或3;
'''
transforms.RandomGrayscale(num_output_channels, p=0.1)
'''
根据概率将图片转换为灰度图;
'''
train_transform = transforms.Compose([
    transforms.Resize((224, 224)),

    # 3 Grayscale
    transforms.Grayscale(num_output_channels=3),

    transforms.ToTensor(), 
    transforms.Normalize(norm_mean, norm_std),
])

#仿射变换
transforms.RandomAffine(degrees, translate=None, scale=None, shear=None, resample=False, fillcolor=0)
'''
仿射变换是二维的线性变换,由五种基本原子变换构成,分别是旋转、平移、缩放、错切和翻转;
degrees: 旋转角度;
degrees旋转是中心旋转,degrees参数必须设置,不想旋转的话设置degrees=0;
I. 当degrees为a时,在区间(-a,a)之间随机选择旋转角度;
II. 当degrees为(a,b)时,在区间(a,b)之间随机选择旋转角度;
translate: 平移区间设置,如果为(a,b),a设置宽width,b设置高height,图像在宽维度平移的区间为-img_width * a < dx < img_width * a,在高维度平移的区间为-img_height * a < dy < img_height *a;
scale: 缩放比例(以面积为单位),scale区间范围是[0,1];
fill_color: 填充颜色设置
shear: 错切角度设置,有水平错切和垂直错切;哪个轴平行就是沿着哪个轴错切
I. 若shear为a,则仅在x轴错切,错切角度在区间(-a,a)之间随机选择;
II. 若shear为(a,b),则a设置x轴错切角度,即区间(-a,a)之间随机选择,b设置y轴错切角度,即区间(-b,b)之间随机选择;
若shear为(a,b,c,d)则a,b设置x轴角度,c,d,设置y轴角度
resample: 重采样方式,有NEAREST、BILINEAR、BICUBIC;
'''

#随机遮挡
'''
RandomErasing接受的是张量,所以需要先进行ToTensor()操作;
p: 概率值,图像被遮挡的概率;
scale: 遮挡区域的比例(以面积为单位);
ratio: 遮挡区域的长宽比;
value: 设置遮挡区域的像素值,eg. (R,G,B) or (Gray);还可以设置random
'''
transforms.RandomErasing(p=0.5, scale=(0.02, 0.33), ratio=(0.3, 3.3), value=0, inplace=False)

train_transform = transforms.Compose([
    transforms.Resize((224, 224)),

    # 5 Erasing
    # RandomErasing接受的是张量,所以需要先进行ToTensor()操作
    transforms.ToTensor(),
    transforms.RandomErasing(p=1, scale=(0.02, 0.33), ratio=(0.3, 3.3), value=(254/255, 0, 0)), #value=(254/255, 0, 0),此时为张量,需要进行归一化,除以255变换到0-1范围
    transforms.RandomErasing(p=1, scale=(0.02, 0.33), ratio=(0.3, 3.3), value='1234'), #value='1234'value为任意字符串时,则填充的为随机彩色像素值
    transforms.Normalize(norm_mean, norm_std),
])

transforms的选择操作(常用于数据增强)

#从一系列transforms方法中随机选择一个执行; 执行一个
transforms.RandomChoice([transforms1, transforms2, transforms3])

train_transform = transforms.Compose([
    transforms.Resize((224, 224)),

    # 1 RandomChoice
    transforms.RandomChoice([transforms.RandomVerticalFlip(p=1), transforms.RandomHorizontalFlip(p=1)]),

    transforms.ToTensor(),
    transforms.Normalize(norm_mean, norm_std),
])

#根据概率执行该组transforms; 执行一组
transforms.RandomApply([transforms1, transforms2, transforms3], p=0.5)

train_transform = transforms.Compose([
    transforms.Resize((224, 224)),

    # 2 RandomApply
    transforms.RandomApply([transforms.RandomAffine(degrees=0, shear=45, fillcolor=(255, 0, 0)), 
                            transforms.Grayscale(num_output_channels=3)], p=0.5),

    transforms.ToTensor(), 
    transforms.Normalize(norm_mean, norm_std),
])

#对一组transforms打乱顺序并执行这组
transforms.RandomOrder([transforms1, transforms2, transforms3])

train_transform = transforms.Compose([
    transforms.Resize((224, 224)),

    # 3 RandomOrder
    transforms.RandomOrder([transforms.RandomRotation(15),
                            transforms.Pad(padding=32),
                            transforms.RandomAffine(degrees=0, translate=(0.01, 0.1), scale=(0.9, 1.1))]),

    transforms.ToTensor(),
    transforms.Normalize(norm_mean, norm_std),
])

在这里插入图片描述

自定义transforms要素

  1. 仅接收一个参数,返回一个参数
  2. 注意上下游的输入与输出的衔接、类型对接
  3. 如果需要实现多参数传入,通过类来实现
class your_own_transforms(object):
	def __init__(self,...):
		...
	def __call__(self,img):#调用时执行
		...
		return img 
'''
椒盐噪声
白色为盐噪声,黑色为椒噪声
信噪比(Signal-to-Noise Ratio, SNR)=图像像素个数/整个像素个数之比,通常用分贝(dB)表示,用来衡量噪声比例
'''

numpy.random.choice(a, size=None, replace=True, p=None)
'''
从a(只要是ndarray都可以,但必须是一维的)中随机抽取数字,并组成指定大小(size)的数组
replace:True表示可以取相同数字,False表示不可以取相同数字
数组p:与数组a相对应,表示取数组a中每个元素的概率,默认为选取每个元素的概率相同。
size也跟下面讲的axis同理,表示从最外面括号开始数的元素个数。
'''
mask = np.random.choice([0, 1, 2],size=(3,3,1), p=[0.5, 0.25, 0.25])
print(mask)
mask = np.repeat(mask, 3, axis=2)
print(mask)
'''
输出
[[[0]
  [0]
  [2]]

 [[0]
  [1]
  [2]]

 [[0]
  [1]
  [1]]]
[[[0 0 0]
  [0 0 0]
  [2 2 2]]

 [[0 0 0]
  [1 1 1]
  [2 2 2]]

 [[0 0 0]
  [1 1 1]
  [1 1 1]]]
'''

numpy.repeat(a,repeats,axis=None)
'''
axis翻译过来就是轴的意思。
numpy数组中:
一维数组拥有一个轴:axis=0;
二维数组拥有两个轴:axis=0,axis=1;
三维数组拥有三个轴:axis=0,axis=1,axis=2。
四维数组拥有三个轴:axis=0,axis=1,axis=2,axis=3。

axis=None,会flatten当前矩阵,实际上就是变成了一个行向量
axis=0,增加行数,列数不变
axis=1,增加列数,行数不变
repeats 复制次数或者按照特定方式复制
'''
c = np.array(([1,2],[3,4],[4,5]))
d=np.repeat(c,[2,3],axis=1)#对行操作,增加的是列,一行2个元素,[2,3]就是2个元素(行不变,列变:第一列复制2次,第二列复制3次)
print(d)
print('d形状:',d.shape)
'''
[[1 1 2 2 2]
 [3 3 4 4 4]
 [4 4 5 5 5]]
d形状: (3, 5)
'''
e=np.repeat(c,[2,3,4],axis=0)#对列操作增加的是行,一列3个元素,[2,3,4]就是3个元素(列不变,行变:第一行复制2次,第二行复制3次,第三行复制3次)
print(e)
print('e形状:',e.shape)
'''
[[1 2]
 [1 2]
 [3 4]
 [3 4]
 [3 4]
 [4 5]
 [4 5]
 [4 5]
 [4 5]]
e形状: (9, 2)
'''
# @file name  : my_transforms.py
# @brief      : 自定义一个transforms方法

import os
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
import numpy as np
import torch
import random
import torchvision.transforms as transforms
from PIL import Image
from matplotlib import pyplot as plt
from torch.utils.data import DataLoader

path_lenet = os.path.abspath(os.path.join(BASE_DIR, "..", "..", "model", "lenet.py"))
path_tools = os.path.abspath(os.path.join(BASE_DIR, "..", "..", "tools", "common_tools.py"))
assert os.path.exists(path_lenet), "{}不存在,请将lenet.py文件放到 {}".format(path_lenet, os.path.dirname(path_lenet))
assert os.path.exists(path_tools), "{}不存在,请将common_tools.py文件放到 {}".format(path_tools, os.path.dirname(path_tools))

import sys
hello_pytorch_DIR = os.path.abspath(os.path.dirname(__file__)+os.path.sep+".."+os.path.sep+"..")
sys.path.append(hello_pytorch_DIR)

from tools.my_dataset import RMBDataset
from tools.common_tools import set_seed, transform_invert

set_seed(1)  # 设置随机种子

# 参数设置
MAX_EPOCH = 10
BATCH_SIZE = 1
LR = 0.01
log_interval = 10
val_interval = 1
rmb_label = {"1": 0, "100": 1}


class AddPepperNoise(object):
    """增加椒盐噪声
    Args:
        snr (float): Signal Noise Rate
        p (float): 概率值,依概率执行该操作
    """

    def __init__(self, snr, p=0.9):
        assert isinstance(snr, float) and (isinstance(p, float))    # 2020 07 26 or --> and
        self.snr = snr
        self.p = p

    def __call__(self, img):
        """
        Args:
            img (PIL Image): PIL Image
        Returns:
            PIL Image: PIL image.
        """
        if random.uniform(0, 1) < self.p:
            img_ = np.array(img).copy()
            h, w, c = img_.shape
            signal_pct = self.snr
            noise_pct = (1 - self.snr)
            mask = np.random.choice((0, 1, 2), size=(h, w, 1), p=[signal_pct, noise_pct/2., noise_pct/2.])
            mask = np.repeat(mask, c, axis=2)
            img_[mask == 1] = 255   # 盐噪声
            img_[mask == 2] = 0     # 椒噪声
            return Image.fromarray(img_.astype('uint8')).convert('RGB')
        else:
            return img


# ============================ step 1/5 数据 ============================
split_dir = os.path.abspath(os.path.join(BASE_DIR, "..", "..", "data", "rmb_split"))
if not os.path.exists(split_dir):
    raise Exception(r"数据 {} 不存在, 回到lesson-06\1_split_dataset.py生成数据".format(split_dir))
train_dir = os.path.join(split_dir, "train")
valid_dir = os.path.join(split_dir, "valid")

norm_mean = [0.485, 0.456, 0.406]
norm_std = [0.229, 0.224, 0.225]


train_transform = transforms.Compose([
    transforms.Resize((224, 224)),
    AddPepperNoise(0.9, p=0.5),
    transforms.ToTensor(),
    transforms.Normalize(norm_mean, norm_std),
])

valid_transform = transforms.Compose([
    transforms.Resize((224, 224)),
    transforms.ToTensor(),
    transforms.Normalize(norm_mean, norm_std)
])

# 构建MyDataset实例
train_data = RMBDataset(data_dir=train_dir, transform=train_transform)
valid_data = RMBDataset(data_dir=valid_dir, transform=valid_transform)

# 构建DataLoder
train_loader = DataLoader(dataset=train_data, batch_size=BATCH_SIZE, shuffle=True)
valid_loader = DataLoader(dataset=valid_data, batch_size=BATCH_SIZE)


# ============================ step 5/5 训练 ============================
for epoch in range(MAX_EPOCH):
    for i, data in enumerate(train_loader):
        inputs, labels = data   # B C H W
        img_tensor = inputs[0, ...]     # C H W
        img = transform_invert(img_tensor, train_transform)
        plt.imshow(img)
        plt.show()
        plt.pause(0.5)
        plt.close()





实战:RMB分类识别


#####RMB分类识别 split_dataset.py#####
##划分数据,制作train,valid,test集
# -*- coding: utf-8 -*-
import os
import random
import shutil
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
#__file__是Python中内置的变量,它表示当前文件的文件名
#os.path.abspath(__file__)用于获取文件或目录的绝对路径的函数
#os.path.dirname当前路径文件夹名
def makedir(new_dir):
    if not os.path.exists(new_dir):
        os.makedirs(new_dir) #若不存在则创建


if __name__ == '__main__':

    dataset_dir = os.path.abspath(os.path.join(BASE_DIR, "..", "..", "data", "RMB_data"))#..表示上一个文件夹
    split_dir = os.path.abspath(os.path.join(BASE_DIR, "..", "..", "data", "rmb_split"))
    
    #rmb_split 下创建子文件夹
    train_dir = os.path.join(split_dir, "train")
    valid_dir = os.path.join(split_dir, "valid")
    test_dir = os.path.join(split_dir, "test")

    if not os.path.exists(dataset_dir):#文件夹不存在
        raise Exception("\n{} 不存在,请下载 02-01-数据-RMB_data.rar 放到\n{} 下,并解压即可".format(
            dataset_dir, os.path.dirname(dataset_dir)))

#设置分割比例
    train_pct = 0.8
    valid_pct = 0.1
    test_pct = 0.1



'''
os.walk()用于遍历目录和其子目录,并返回一个三元组(root, dirs, files)的生成器。
root:Prints out directories only from what you specified.
dirs:Prints out sub-directories from root.
files:Prints out all files from root and directories.

filter(function,iterable) 返回判断为True的元素
function -> 判断条件
iterable -> 可迭代对象

'''

    for root, dirs, files in os.walk(dataset_dir):
        for sub_dir in dirs:

            imgs = os.listdir(os.path.join(root, sub_dir))
            imgs = list(filter(lambda x: x.endswith('.jpg'), imgs))
            random.shuffle(imgs)
            img_count = len(imgs)

            train_point = int(img_count * train_pct)
            valid_point = int(img_count * (train_pct + valid_pct))

            if img_count == 0:
                print("{}目录下,无图片,请检查".format(os.path.join(root, sub_dir)))
                import sys
                sys.exit(0)
            for i in range(img_count):
                if i < train_point: #各集的文件夹
                    out_dir = os.path.join(train_dir, sub_dir)
                elif i < valid_point:
                    out_dir = os.path.join(valid_dir, sub_dir)
                else:
                    out_dir = os.path.join(test_dir, sub_dir)

                makedir(out_dir)

                target_path = os.path.join(out_dir, imgs[i])
                src_path = os.path.join(dataset_dir, sub_dir, imgs[i])

                shutil.copy(src_path, target_path)#将src图片复制到target地址

            print('Class:{}, train:{}, valid:{}, test:{}'.format(sub_dir, train_point, valid_point-train_point,
                                                                 img_count-valid_point))
            print("已在 {} 创建划分好的数据\n".format(out_dir))


# -*- coding: utf-8 -*-
"""
# @file name  : train_lenet.py
# @brief      : 人民币分类模型训练
"""
import os
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
import numpy as np
import torch
import torch.nn as nn
from torch.utils.data import DataLoader
import torchvision.transforms as transforms
import torch.optim as optim
from matplotlib import pyplot as plt

path_lenet = os.path.abspath(os.path.join(BASE_DIR, "..", "..", "model", "lenet.py"))
path_tools = os.path.abspath(os.path.join(BASE_DIR, "..", "..", "tools", "common_tools.py"))
assert os.path.exists(path_lenet), "{}不存在,请将lenet.py文件放到 {}".format(path_lenet, os.path.dirname(path_lenet))
assert os.path.exists(path_tools), "{}不存在,请将common_tools.py文件放到 {}".format(path_tools, os.path.dirname(path_tools))

#assert 判断条件,为False的处理

import sys
hello_pytorch_DIR = os.path.abspath(os.path.dirname(__file__)+os.path.sep+".."+os.path.sep+"..")
sys.path.append(hello_pytorch_DIR)#添加到导入模块的搜索路径

from model.lenet import LeNet
from tools.my_dataset import RMBDataset
from tools.common_tools import set_seed


set_seed()  # 设置随机种子
rmb_label = {"1": 0, "100": 1}

# 参数设置
MAX_EPOCH = 10
BATCH_SIZE = 16
LR = 0.01
log_interval = 10
val_interval = 1

# ============================ step 1/5 数据 ============================
split_dir = os.path.abspath(os.path.join(BASE_DIR, "..", "..", "data", "rmb_split"))
if not os.path.exists(split_dir):
    raise Exception(r"数据 {} 不存在, 回到lesson-06\1_split_dataset.py生成数据".format(split_dir))
train_dir = os.path.join(split_dir, "train")
valid_dir = os.path.join(split_dir, "valid")

#设置数据标准化的均值、方差
norm_mean = [0.485, 0.456, 0.406]
norm_std = [0.229, 0.224, 0.225]

#compose是将一系列方法组合起来
train_transform = transforms.Compose([
    transforms.Resize((32, 32)),#32*32
    transforms.RandomCrop(32, padding=4),#裁剪    transforms.ToTensor(),
    transforms.Normalize(norm_mean, norm_std),
])

valid_transform = transforms.Compose([
    transforms.Resize((32, 32)),
    transforms.ToTensor(),#这里跟上面不一样的是 验证的时候不需要再做数据增强
    transforms.Normalize(norm_mean, norm_std),
])

# 构建MyDataset实例
train_data = RMBDataset(data_dir=train_dir, transform=train_transform)#transform指数据预处理
valid_data = RMBDataset(data_dir=valid_dir, transform=valid_transform)

# 构建DataLoder
train_loader = DataLoader(dataset=train_data, batch_size=BATCH_SIZE, shuffle=True)
valid_loader = DataLoader(dataset=valid_data, batch_size=BATCH_SIZE)

# ============================ step 2/5 模型 ============================

net = LeNet(classes=2)#卷积神经网络 分类能力强
net.initialize_weights()

# ============================ step 3/5 损失函数 ============================
criterion = nn.CrossEntropyLoss()   #交叉熵损失                                                # 选择损失函数

# ============================ step 4/5 优化器 ============================
optimizer = optim.SGD(net.parameters(), lr=LR, momentum=0.9)                        # 选择优化器 :随机梯度下降
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.1)     # 设置学习率下降策略

# ============================ step 5/5 训练 ============================
train_curve = list()
valid_curve = list()

for epoch in range(MAX_EPOCH):

    loss_mean = 0.
    correct = 0.
    total = 0.

    net.train()
    for i, data in enumerate(train_loader):#iteration 每次取batchsize

        # forward
        inputs, labels = data
        outputs = net(inputs)

        # backward
        optimizer.zero_grad()
        loss = criterion(outputs, labels)
        loss.backward()

        # update weights
        optimizer.step()

        # 统计分类情况
        _, predicted = torch.max(outputs.data, 1)
        total += labels.size(0)
        correct += (predicted == labels).squeeze().sum().numpy()

        # 打印训练信息
        loss_mean += loss.item()
        train_curve.append(loss.item())
        if (i+1) % log_interval == 0:
            loss_mean = loss_mean / log_interval
            print("Training:Epoch[{:0>3}/{:0>3}] Iteration[{:0>3}/{:0>3}] Loss: {:.4f} Acc:{:.2%}".format(
                epoch, MAX_EPOCH, i+1, len(train_loader), loss_mean, correct / total))
            loss_mean = 0.

    scheduler.step()  # 更新学习率

    # validate the model
    if (epoch+1) % val_interval == 0:

        correct_val = 0.
        total_val = 0.
        loss_val = 0.
        net.eval()
        with torch.no_grad():
            for j, data in enumerate(valid_loader):
                inputs, labels = data
                outputs = net(inputs)
                loss = criterion(outputs, labels)

                _, predicted = torch.max(outputs.data, 1)
                total_val += labels.size(0)
                correct_val += (predicted == labels).squeeze().sum().numpy()

                loss_val += loss.item()

            loss_val_epoch = loss_val / len(valid_loader)
            valid_curve.append(loss_val_epoch)
            print("Valid:\t Epoch[{:0>3}/{:0>3}] Iteration[{:0>3}/{:0>3}] Loss: {:.4f} Acc:{:.2%}".format(
                epoch, MAX_EPOCH, j+1, len(valid_loader), loss_val_epoch, correct_val / total_val))


train_x = range(len(train_curve))
train_y = train_curve

train_iters = len(train_loader)
valid_x = np.arange(1, len(valid_curve)+1) * train_iters*val_interval - 1  # 由于valid中记录的是epochloss,需要对记录点进行转换到iterations
valid_y = valid_curve

plt.plot(train_x, train_y, label='Train')
plt.plot(valid_x, valid_y, label='Valid')

plt.legend(loc='upper right')
plt.ylabel('loss value')
plt.xlabel('Iteration')
plt.show()

# ============================ inference ============================

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
test_dir = os.path.join(BASE_DIR, "test_data")

test_data = RMBDataset(data_dir=test_dir, transform=valid_transform)
valid_loader = DataLoader(dataset=test_data, batch_size=1)

for i, data in enumerate(valid_loader):
    # forward
    inputs, labels = data
    outputs = net(inputs)
    _, predicted = torch.max(outputs.data, 1)

    rmb = 1 if predicted.numpy()[0] == 0 else 100
    print("模型获得{}元".format(rmb))

实战:数据增强

训练集是第四套100元人民币,测试第五套100元人民币

"""
# @file name  : RMB_data_augmentation.py
# @brief      : 人民币分类模型数据增强实验
"""
import os
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
import numpy as np
import torch
import torch.nn as nn
from torch.utils.data import DataLoader
import torchvision.transforms as transforms
import torch.optim as optim
from matplotlib import pyplot as plt

path_lenet = os.path.abspath(os.path.join(BASE_DIR, "..", "..", "model", "lenet.py"))
path_tools = os.path.abspath(os.path.join(BASE_DIR, "..", "..", "tools", "common_tools.py"))
assert os.path.exists(path_lenet), "{}不存在,请将lenet.py文件放到 {}".format(path_lenet, os.path.dirname(path_lenet))
assert os.path.exists(path_tools), "{}不存在,请将common_tools.py文件放到 {}".format(path_tools, os.path.dirname(path_tools))

import sys
hello_pytorch_DIR = os.path.abspath(os.path.dirname(__file__)+os.path.sep+".."+os.path.sep+"..")
sys.path.append(hello_pytorch_DIR)

from model.lenet import LeNet
from tools.my_dataset import RMBDataset
from tools.common_tools import transform_invert, set_seed

set_seed()  # 设置随机种子
rmb_label = {"1": 0, "100": 1}

# 参数设置
MAX_EPOCH = 10
BATCH_SIZE = 16
LR = 0.01
log_interval = 10
val_interval = 1

# ============================ step 1/5 数据 ============================
split_dir = os.path.abspath(os.path.join(BASE_DIR, "..", "..", "data", "rmb_split"))
if not os.path.exists(split_dir):
    raise Exception(r"数据 {} 不存在, 回到lesson-06\1_split_dataset.py生成数据".format(split_dir))
train_dir = os.path.join(split_dir, "train")
valid_dir = os.path.join(split_dir, "valid")

norm_mean = [0.485, 0.456, 0.406]
norm_std = [0.229, 0.224, 0.225]

train_transform = transforms.Compose([
    transforms.Resize((32, 32)),
    transforms.RandomCrop(32, padding=4),
    transforms.RandomGrayscale(p=0.9),
    #去掉这个灰度处理,就无法通过蓝色的第四套来正确识别红色的第五套
    transforms.ToTensor(),
    transforms.Normalize(norm_mean, norm_std),
])


valid_transform = transforms.Compose([
    transforms.Resize((32, 32)),
    transforms.ToTensor(),
    transforms.Normalize(norm_mean, norm_std),
])

# 构建MyDataset实例
train_data = RMBDataset(data_dir=train_dir, transform=train_transform)
valid_data = RMBDataset(data_dir=valid_dir, transform=valid_transform)

# 构建DataLoder
train_loader = DataLoader(dataset=train_data, batch_size=BATCH_SIZE, shuffle=True)
valid_loader = DataLoader(dataset=valid_data, batch_size=BATCH_SIZE)

# ============================ step 2/5 模型 ============================

net = LeNet(classes=2)
net.initialize_weights()

# ============================ step 3/5 损失函数 ============================
criterion = nn.CrossEntropyLoss()                                                   # 选择损失函数

# ============================ step 4/5 优化器 ============================
optimizer = optim.SGD(net.parameters(), lr=LR, momentum=0.9)                        # 选择优化器
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.1)     # 设置学习率下降策略

# ============================ step 5/5 训练 ============================
train_curve = list()
valid_curve = list()

for epoch in range(MAX_EPOCH):

    loss_mean = 0.
    correct = 0.
    total = 0.

    net.train()
    for i, data in enumerate(train_loader):

        # forward
        inputs, labels = data
        outputs = net(inputs)

        # backward
        optimizer.zero_grad()
        loss = criterion(outputs, labels)
        loss.backward()

        # update weights
        optimizer.step()

        # 统计分类情况
        _, predicted = torch.max(outputs.data, 1)
        total += labels.size(0)
        correct += (predicted == labels).squeeze().sum().numpy()

        # 打印训练信息
        loss_mean += loss.item()
        train_curve.append(loss.item())
        if (i+1) % log_interval == 0:
            loss_mean = loss_mean / log_interval
            print("Training:Epoch[{:0>3}/{:0>3}] Iteration[{:0>3}/{:0>3}] Loss: {:.4f} Acc:{:.2%}".format(
                epoch, MAX_EPOCH, i+1, len(train_loader), loss_mean, correct / total))
            loss_mean = 0.

    scheduler.step()  # 更新学习率

    # validate the model
    if (epoch+1) % val_interval == 0:

        correct_val = 0.
        total_val = 0.
        loss_val = 0.
        net.eval()
        with torch.no_grad():
            for j, data in enumerate(valid_loader):
                inputs, labels = data
                outputs = net(inputs)
                loss = criterion(outputs, labels)

                _, predicted = torch.max(outputs.data, 1)
                total_val += labels.size(0)
                correct_val += (predicted == labels).squeeze().sum().numpy()

                loss_val += loss.item()

            valid_curve.append(loss_val)
            print("Valid:\t Epoch[{:0>3}/{:0>3}] Iteration[{:0>3}/{:0>3}] Loss: {:.4f} Acc:{:.2%}".format(
                epoch, MAX_EPOCH, j+1, len(valid_loader), loss_val, correct / total))


train_x = range(len(train_curve))
train_y = train_curve

train_iters = len(train_loader)
valid_x = np.arange(1, len(valid_curve)+1) * train_iters*val_interval # 由于valid中记录的是epochloss,需要对记录点进行转换到iterations
valid_y = valid_curve

plt.plot(train_x, train_y, label='Train')
plt.plot(valid_x, valid_y, label='Valid')

plt.legend(loc='upper right')
plt.ylabel('loss value')
plt.xlabel('Iteration')
plt.show()

# ============================ inference ============================

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
test_dir = os.path.join(BASE_DIR, "test_data")

test_data = RMBDataset(data_dir=test_dir, transform=valid_transform)
valid_loader = DataLoader(dataset=test_data, batch_size=1)

for i, data in enumerate(valid_loader):
    # forward
    inputs, labels = data
    outputs = net(inputs)
    _, predicted = torch.max(outputs.data, 1)

    rmb = 1 if predicted.numpy()[0] == 0 else 100

    img_tensor = inputs[0, ...]  # C H W
    img = transform_invert(img_tensor, valid_transform)
    plt.imshow(img)
    plt.title("LeNet got {} Yuan".format(rmb))
    plt.show()
    plt.pause(0.5)
    plt.close()

实战:猫狗分类

# -*- coding: utf-8 -*-
"""
# @file name  : dataset.py
# @brief      : 各数据集的Dataset定义
"""

import os
import random
from PIL import Image
from torch.utils.data import Dataset

random.seed(1)
DogCat_label = {"dog": 0, "cat": 1}# 与rmb实战的区别不多,这里需要改动一下


class DogCatDataset(Dataset):
    def __init__(self, data_dir, transform=None):
        """
        rmb面额分类任务的Dataset
        :param data_dir: str, 数据集所在路径
        :param transform: torch.transform,数据预处理
        """
        self.label_name = {"1": 0, "100": 1}
        self.data_info = self.get_img_info(data_dir)  # data_info存储所有图片路径和标签,在DataLoader中通过index读取样本
        self.transform = transform

    def __getitem__(self, index):
        path_img, label = self.data_info[index]
        img = Image.open(path_img).convert('RGB')     # 0~255

        if self.transform is not None:
            img = self.transform(img)   # 在这里做transform,转为tensor等等

        return img, label

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

    @staticmethod
    def get_img_info(data_dir):
        data_info = list()
        for root, dirs, _ in os.walk(data_dir):
            # 遍历类别
            for sub_dir in dirs:
                img_names = os.listdir(os.path.join(root, sub_dir))
                img_names = list(filter(lambda x: x.endswith('.jpg'), img_names))

                # 遍历图片
                for i in range(len(img_names)):
                    img_name = img_names[i]
                    path_img = os.path.join(root, sub_dir, img_name)
                    label_name = img_name.split(".")[0]     # 新增代码
                    label = DogCat_label[label_name]        # 新增代码
                    # label = DogCat_label[sub_dir]           # 原代码
                    data_info.append((path_img, int(label)))

        return data_info

# -*- coding: utf-8 -*-
"""
# @file name  : lenet.py
# @brief    : lenet模型定义
"""
import torch.nn as nn
import torch.nn.functional as F
import torch

def conv_input(ni, nf): return nn.Conv2d(ni, nf, kernel_size=7, stride=1, padding=3)
def conv(ni, nf): return nn.Conv2d(ni, nf, kernel_size=3, stride=1, padding=1)
def conv_half_reduce(ni, nf): return nn.Conv2d(ni, nf, kernel_size=3, stride=2, padding=1)

class MyNet(nn.Module):#加了很多层
    def __init__(self, classes):
        super(MyNet, self).__init__()
        self.conv1 = conv_input(3, 16)
        self.bn1 = nn.BatchNorm2d(16)
        self.conv2_1 = conv_half_reduce(16, 32)
        self.bn2_1 = nn.BatchNorm2d(32)
        self.conv2_2 = conv(32, 32)
        self.bn2_2 = nn.BatchNorm2d(32)
        self.conv3_1 = conv_half_reduce(32, 64)
        self.bn3_1 = nn.BatchNorm2d(64)
        self.conv3_2 = conv(64, 64)
        self.bn3_2 = nn.BatchNorm2d(64)
        self.conv4_1 = conv_half_reduce(64, 128)
        self.bn4_1 = nn.BatchNorm2d(128)
        self.conv4_2 = conv(128, 128)
        self.bn4_2 = nn.BatchNorm2d(128)

        self.avg_pool = nn.AdaptiveAvgPool2d((1,1))
        self.bn5_avg = nn.BatchNorm2d(128)

        self.max_pool = nn.AdaptiveMaxPool2d((1,1))
        self.bn5_max = nn.BatchNorm2d(128)

        self.dropout1 = nn.Dropout(0.25)
        self.fc1 = nn.Linear(256, 128)
        self.dropout2 = nn.Dropout(0.5)
        self.fc2 = nn.Linear(128, classes)


    def forward(self, x):
        out = self.conv1(x)
        out = F.relu(self.bn1(out)) #添加bn层,即上面出现过的BatchNorm
        out = self.conv2_1(out)
        out = F.relu(self.bn2_1(out))
        out = self.conv2_2(out)
        out = F.relu(self.bn2_2(out))
        out = self.conv3_1(out)
        out = F.relu(self.bn3_1(out))
        out = self.conv3_2(out)
        out = F.relu(self.bn3_2(out))
        out = self.conv4_1(out)
        out = F.relu(self.bn4_1(out))
        out = self.conv4_2(out)
        out = F.relu(self.bn4_2(out))

        out1 = self.avg_pool(out)
        out1 = self.bn5_avg(out1)

        out2 = self.max_pool(out)
        out2 = self.bn5_max(out2)

        out = torch.cat((out1, out2), dim=1)
        out = out.view(out.size(0), -1)

        out = self.dropout1(out)
        out = F.relu(self.fc1(out))
        out = self.dropout2(out)
        out = self.fc2(out)


        return out

    def initialize_weights(self):
        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                nn.init.xavier_normal_(m.weight.data)
                if m.bias is not None:
                    m.bias.data.zero_()
            elif isinstance(m, nn.BatchNorm2d):
                m.weight.data.fill_(1)
                m.bias.data.zero_()
            elif isinstance(m, nn.Linear):
                nn.init.normal_(m.weight.data, 0, 0.1)
                m.bias.data.zero_()

class LeNet(nn.Module):#原网络
    def __init__(self, classes):
        super(LeNet, self).__init__()
        self.conv1 = nn.Conv2d(3, 6, 5)
        self.conv2 = nn.Conv2d(6, 16, 5)
        self.fc1 = nn.Linear(16*5*5, 120)
        self.fc2 = nn.Linear(120, 84)
        self.fc3 = nn.Linear(84, classes)

    def forward(self, x):
        out = F.relu(self.conv1(x))
        out = F.max_pool2d(out, 2)
        out = F.relu(self.conv2(out))
        out = F.max_pool2d(out, 2)
        out = out.view(out.size(0), -1)
        out = F.relu(self.fc1(out))
        out = F.relu(self.fc2(out))
        out = self.fc3(out)
        return out

    def initialize_weights(self):
        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                nn.init.xavier_normal_(m.weight.data)
                if m.bias is not None:
                    m.bias.data.zero_()
            elif isinstance(m, nn.BatchNorm2d):
                m.weight.data.fill_(1)
                m.bias.data.zero_()
            elif isinstance(m, nn.Linear):
                nn.init.normal_(m.weight.data, 0, 0.1)
                m.bias.data.zero_()


class LeNet2(nn.Module):
    def __init__(self, classes):
        super(LeNet2, self).__init__()
        self.features = nn.Sequential(
            nn.Conv2d(3, 6, 5),
            nn.ReLU(),
            nn.MaxPool2d(2, 2),
            nn.Conv2d(6, 16, 5),
            nn.ReLU(),
            nn.MaxPool2d(2, 2)
        )
        self.classifier = nn.Sequential(
            nn.Linear(16*5*5, 120),
            nn.ReLU(),
            nn.Linear(120, 84),
            nn.ReLU(),
            nn.Linear(84, classes)
        )

    def forward(self, x):
        x = self.features(x)
        x = x.view(x.size()[0], -1)
        x = self.classifier(x)
        return x

参考资料

https://github.com/greebear/pytorch-learning/
https://github.com/JansonYuan/Pytorch-Camp/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值