建议点赞收藏关注!持续更新至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…n维
标量(Scalar):0阶张量
向量(Vector):1阶张量
矩阵(Matrix):2阶张量 -
有8个属性
data
dtype:有很多数据类型
shape : 如 (6,6,6,6)
device:所在设备cpu/gpu
grad:data的梯度
grad_fn:创建tensor 的function,是自动求导的关键,加法还是乘法
requires_grad:指示是否需要梯度,不一定所有的张量都需要设置梯度
is_leaf:指示是否是叶子节点(张量),计算图中再介绍。 -
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 包含常用图像预处理方法
数据增强使得训练集变换,使模型更具泛化能力
- transforms 包含常用图像预处理方法
- 数据中心化
- 数据标准化:可以加快模型的收敛
即代码中的bias变大,使得整体数据向上向右偏,导致iteration要变大才可能收敛到之前相同的loss
transforms.Normalize(norm_mean, norm_std) - 缩放
transforms.Resize - 裁剪 5种
transforms.CenterCrop
transforms.RandomCrop
transforms.RandomResizedCrop
transforms.FiveCrop
transforms.TenCrop - 旋转 &翻转 3种
transforms.RandomHorizontalFlip
transforms.RandomVerticalFlip
transforms.RandomRotation - 填充
transforms.Pad - 噪声增加
transforms.RandomErasing(p=0.5, scale=(0.02, 0.33), ratio=(0.3, 3.3), value=0, inplace=False) - 灰度变换
transforms.Grayscale(num_output_channels)
transforms.RandomGrayscale(num_output_channels, p=0.1) - 线性变换
torchvision.transforms.LinearTransformation(transformation_matrix, mean_vector):对tensor进行矩阵变换后减去一个向量 - 仿射变换
transforms.RandomAffine(degrees, translate=None, scale=None, shear=None, resample=False, fillcolor=0) - 亮度、饱和度及对比度变换
transforms.ColorJitter(brightness=0, contrast=0, saturation=0, hue=0) - 自定义
transforms.Lambda(lambd)
transforms.RandomChoice([transforms1, transforms2, transforms3])
transforms.RandomApply([transforms1, transforms2, transforms3], p=0.5)
transforms.RandomOrder([transforms1, transforms2, transforms3]) - 格式转换
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要素
- 仅接收一个参数,返回一个参数
- 注意上下游的输入与输出的衔接、类型对接
- 如果需要实现多参数传入,通过类来实现
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/