目录
三种运行方式(PyCharm、PyCharm的Python控制台、Jupyter Notebook)对比:
PyTorch 读取数据涉及两个类:Dataset & Dataloader
2.Tensor数据类型与其他图片数据类型有什么区别?为什么需要Tensor数据类型
如何把数据集(多张图片)和 transforms 结合在一起
15. 神经网络 - 搭建小实战和 Sequential 的使用
用 Sequential 搭建,实现上图 CIFAR10 model 的代码,并引入 tensorboard 可视化模型结构
在之前写的神经网络中用到 Loss Function(损失函数)
方式2:网络模型的参数保存为字典,不保存网络模型的结构(官方推荐的保存方式,用的空间小)
1.Python学习中的两大法宝函数
pytorch就像一个工具箱,有很多不同的分隔区,每个分隔区里有不同的工具或者更多分割区
探索工具箱,使用两个重要的函数:
- dir() 函数:能让我们知道工具箱以及工具箱中的分隔区有什么东西(打开,看见)
- help() 函数:能让我们知道每个工具是如何使用的,工具的使用方法(官方说明书)
例如上图中,输入dir(pytorch),就能看到输出1、2、3、4等分隔区;想继续探索第3个分隔区里有什么的话,输入dir(pytorch.3),输出会是a,b,c(3号分隔区里有a,b,c等工具)输入help(pytorch.3.a) 输出此工具功能:将此扳手放在特定地方,然后拧动
打开Pycharm,在python console中输入:
dir(torch)
可以看到输出了大量的分隔区(或理解为大量更小的工具箱),输入ctrl+F找到cuda
想看cuda分隔区里有什么,输入
dir(torch.cuda)
可以看到又输出了大量的分隔区
继续输入:
dir(torch.cuda.is_available)
可以看到输出的前后都带有双下划线__,这是一种规范,说明这些变量不容许篡改,即它不再是一个分隔区,说明is_available是一个确确实实的函数(相当于工具,可以用help()函数查看该函数的具体说明)
对工具使用:
help(torch.cuda.is_available)
注意此处 is_available 不带括号
2. Pycharm及Jupyter使用及对比
如何在PyCharm中新建项目?
(1)新建文件夹或者打开现有文件夹
(2)新建 Python文件,命名 first_demo
(3)右键运行程序:
如何在Jupyter中新建项目?
首先在开始菜单打开anaconda的命令行,进入pytorch的conda环境,再打开Jupyter
以每一个块为一个运行整体
三种运行方式(PyCharm、PyCharm的Python控制台、Jupyter Notebook)对比:
代码是以块为一个整体运行的,Python文件的块是所有行的代码,即在PyCharm运行时出现错误时,修改后要从头开始运行
PyCharm的Python控制台:一行一行代码运行的,即以每一行为一个块来运行的(也可以以任意行为块运行,按Shift+回车,输入下一行代码,再按回车运行
Jupyter以任意行为块运行
3. PyTorch加载数据初认识
PyTorch 读取数据涉及两个类:Dataset & Dataloader
Dataset:提供一种方式,获取其中需要的数据及其对应的真实的 label 值,并完成编号。
函数主要实现以下两个功能:
- 怎么去获取每一个数据及其label
- 告诉我们总共有多少的数据
Dataloader:打包(batch_size),为后面的神经网络提供不同的数据形式
数据集的几种组织形式
上诉train 里有两个文件夹:ants 和 bees,其中分别都是一些蚂蚁和蜜蜂的图片
还有一种标记方式是images是一个文件夹,labels是另一个文件夹,记事本中直接标注图片所属类别.
Dataset类
from torch.utils.data import Dataset
查看Dataset类的介绍:
help(Dataset)
Dataset 是一个抽象类,所有数据集都需要继承这个类,所有子类都需要重写 __getitem__ 的方法,这个方法主要是获取每个数据集及其对应 label,还可以选择性重写长度类 __len__
4. Dataset类代码实战
如何读取图片
# 法1 from PIL import Image img_path = "xxx" img = Image.open(img_path) img.show() # 法2:利用opencv读取图片,获得numpy型图片数据 import cv2 #先在控制台安装pip install opencv-python cv_img=cv2.imread(img_path)
绝对路径和相对路径:
数据格式一
注意:绝对路径引号前加 r 可以防止转义
完整代码:
from torch.utils.data import Dataset #从torch的常用工具区引入Dataset from PIL import Image import os #想要获取所有图片地址(通过索引),需要os库 class MyData(Dataset): #创建一个MyData的类,需要继承Dataset def __init__(self,root_dir,label_dir): #初始化函数---可以理解为C++中的构造函数 self.root_dir = root_dir #创建时给类的变量可以成为类的全局变量,方便后续的使用 self.label_dir = label_dir self.path =os.path.join(self.root_dir,self.label_dir) # 把两个路径拼接在一起 self.img_path = os.listdir(self.path) # 将path路径下所有地址以列表的形式返回 def __getitem__(self, idx): # idx为编号 # 获取每一个图片 img_name = self.img_path[idx] # 名称 img_item_path = os.path.join(self.root_dir, self.label_dir, img_name) # 获取每张图片的相对路径 img = Image.open(img_item_path) # 读取对应idx图片 label = self.label_dir #图片对应标签 return img, label def __len__(self): # 数据集的长度 return len(self.img_path) # 用类创建实例 root_dir = "dataset/train" ants_label_dir = "ants" bees_label_dir = "bees" ants_dataset = MyData(root_dir, ants_label_dir) bees_dataset = MyData(root_dir, bees_label_dir) img, label = ants_dataset[0] img.show() # 可视化第一张图片 # 将ants(124张)和bees(121张)两个数据集进行拼接 train_dataset = ants_dataset + bees_dataset
数据格式二
当label比较复杂,存储数据比较多时,不可能以文件夹命名的方式,而是以每张图片对应一个txt文件,txt里存储label信息的方式
完整代码:
from torch.utils.data import Dataset from PIL import Image import os class MyData(Dataset): def __init__(self,root_dir,img_dir,lable_dir): self.root_dir = root_dir self.img_dir = img_dir self.lable_dir = lable_dir self.img_item_path= os.path.join(self.root_dir,self.img_dir) self.img_lable_path = os.path.join(self.root_dir, self.lable_dir) #将标签所在的文件夹路径拼接起来 self.img_list = os.listdir(self.img_item_path) self.lable_list = os.listdir(self.img_lable_path)# 将存放标签的文件夹转换成list def __getitem__(self, item): img_name = self.img_list[item] img_name_path = os.path.join(self.img_item_path,img_name) img = Image.open(img_name_path) lable_name = self.lable_list[item] #对应的标签名字 lable_name_path = os.path.join(self.img_lable_path,lable_name) #标签的路径 lable = open(lable_name_path, 'r') # 打开对应的txt标签文件 return img,lable def __len__(self): return len(self.img_list) root_dir = "dataset2/train" ants_dir = "ants_image" lable_dir="ants_label" ants = MyData(root_dir,ants_dir,lable_dir) img,lable = ants[0] img.show()
5. TensorBoard的使用
transforms 在 Dataset 类中很常用,对图像进行变换,如图像要统一到某一个尺寸,或图像中的每一个数据进行类的转换
TensorBoard 探究:
- 训练过程中loss是如何变化的
- 模型在不同阶段的输出
SummaryWriter类
from torch.utils.tensorboard import SummaryWriter
查看一个类或方法如何使用:在PyCharm中,按住Ctrl键,把鼠标移到类上,点击进去
是一个直接向 log_dir 文件夹写入的事件文件,可以被 TensorBoard 进行解析
# 实例化SummaryWriter类 writer = SummaryWriter("logs") # 把对应的事件文件存储到logs文件夹下
主要用到两个方法:
writer.add_image() writer.add_scalar() writer.close()
直接在 PyCharm 的 Terminal 的 pytorch 环境中安装
输入:
pip install tensorboard
即可安装成功
快捷键注释方法:Ctrl + /
add_scalar() 方法的使用
def add_scalar( self, tag, scalar_value, global_step=None, walltime=None, new_style=False, double_precision=False, ):
添加一个标量数据到 Summary 当中,需要参数
- tag:Data指定方式,类似于图表的title
- scalar_value:需要保存的数值(y轴)
- global_step:训练到多少步(x轴)
from torch.utils.tensorboard import SummaryWriter #导入SummaryWriter类 #创建实例 writer=SummaryWriter("logs") #把对应的事件文件存储到logs文件夹下 #两个方法 # writer.add_image() # y=x for i in range(100): writer.add_scalar("y=x",i,i) writer.close()
运行后多了一个logs文件夹,下面是TensorBoard的一些事件文件:
如何打开事件文件?
在 Terminal 里输入:
tensorboard --logdir=logs # logdir=事件文件所在文件夹名
结果如图:
为了防止和别人冲突(一台服务器上有好几个人训练,默认打开的都是6006端口),也可以指定端口,命令如下:
tensorboard --logdir=logs --port=6007
例:y=2x:
from torch.utils.tensorboard import SummaryWriter #导入SummaryWriter类 #创建实例 writer=SummaryWriter("logs") #把对应的事件文件存储到logs文件夹下 #两个方法 # writer.add_image() # y=2x for i in range(100): writer.add_scalar("y=2x",2*i,i) # 标题、y轴、x轴 writer.close()
title 和 y 不一致的情况:
每向 writer 中写入新的事件,也记录了上一个事件
解决:
把logs文件夹下的所有文件删掉,重新运行代码,在 Terminal 里按 Ctrl+C ,再输入命令:
tensorboard --logdir=logs 就可以出现名字为y=2x,但实际纵坐标是y=3x数值的图像
add_image() 的使用
- tag:对应图像的title
- img_tensor:图像的数据类型,只能是torch.Tensor、numpy.array,string/blobname
- global_step:训练步骤,int 类型
PIL类型不满足要求:
利用numpy.array(),对PIL图片进行转换
在Python控制台,把PIL类型的img变量转换为numpy类型(add_image() 函数所需要的图像的数据类型),重新赋值给img_array:
import numpy as np img_array=np.array(img) print(type(img_array)) # numpy.ndarray
从PIL到numpy,需要在add_image()中dataformats参数中指定shape中每一个数字/维表示的含义
step1:蚂蚁为例
from torch.utils.tensorboard import SummaryWriter #导入SummaryWriter类 import numpy as np from PIL import Image #创建实例 writer=SummaryWriter("logs") #把对应的事件文件存储到logs文件夹下 image_path="data/train/ants_image/0013035.jpg" #传入相对位置或绝对位置皆可 img_PIL=Image.open(image_path) #打开图片,此时格式为PIL类型 img_array=np.array(img_PIL) #利用numpy进行转换 print(type(img_array)) print(img_array.shape) #(512,768,3) 即(H,W,C)(高度,宽度,通道) writer.add_image("test",img_array,1, dataformats='HWC') # 切记需要指定维度 writer.close()
结果:
以蜜蜂为例:
from torch.utils.tensorboard import SummaryWriter #导入SummaryWriter类 import numpy as np from PIL import Image #创建实例 writer=SummaryWriter("logs") #把对应的事件文件存储到logs文件夹下 image_path="data/train/bees_image/16838648_415acd9e3f.jpg" img_PIL=Image.open(image_path) img_array=np.array(img_PIL) print(type(img_array)) print(img_array.shape) #(512,768,3) 即(H,W,C)(高度,宽度,通道) writer.add_image("test",img_array,2, dataformats='HWC') # 第2步 writer.close()
在一个title下,通过滑块显示每一步的图形,可以直观地观察训练中给model提供了哪些数据,或者想对model进行测试时,可以看到每个阶段的输出结果
如果想要单独显示,重命名一下title即可,即 writer.add_image() 的第一个字符串类型的参数
6. torchvision中transforms的使用
transforms的结构及用法
from torchvision import transforms
结构
按住Ctrl,看 transforms.py文件(工具箱),它定义了很多 class文件(工具)
一些类
Compose类:结合不同的transforms
- ToTensor类:把一个PIL的Image或者numpy数据类型的图片转换成 tensor 的数据类型
- ToPILImage类:把一个图片转换成PIL Image
- Normalize类:归一化
- Resize类:尺寸变换
- CenterCrop类:中心裁剪
1.Transforms该如何使用:
从transforms中选择一个class,对它进行创建,对创建的对象传入图片,即可返回出结果 例如:ToTensor将一个 PIL Image 或 numpy.ndarray 转换为 tensor的数据类型
Ctrl+P可以提示函数里需要填什么参数
# 1、Transforms该如何使用 tensor_trans = transforms.ToTensor() #从工具箱transforms里取出ToTensor类,返回tensor_trans对象 tensor_img=tensor_trans(img) #创建出tensor_trans后,传入其需要的参数,即可返回结果 print(tensor_img)
__call__魔术方法,把实例好的对象当作方法(函数),直接加括号就可以调用
魔术方法(也称为双下划线方法)是在对象导向编程中用于定义或重写的特殊方法。在Python中,魔术方法通常以两个下划线开头和结尾,例如
__init__()
或__str__()
。它们允许类以特定方式与内置操作交互。例如,__init__()
方法用于初始化对象,而__str__()
方法被用于返回对象的字符串表示。可参考博客:理解Python_魔法方法__call___call魔法方法-CSDN博客
https://blog.csdn.net/xie_0723/article/details/79505131
2.Tensor数据类型与其他图片数据类型有什么区别?为什么需要Tensor数据类型
在Python Console输入:
from PIL import Image from torchvision import transforms img_path= "data/train/ants_image/0013035.jpg" img = Image.open(img_path) tensor_trans = transforms.ToTensor() tensor_img = tensor_trans(img)
打开img,即用Python内置的函数读取的图片,具有的参数有:
再打开tensor_img,看一下它有哪些参数:
为什么需要使用Tensor数据类型:Tensor 数据类型包装了反向神经网络所需要的一些理论基础必须的参数,如:_backward_hooks、_grad等(先转换成Tensor数据类型,再训练)
两种读取图片的方式
1.PIL Image
from PIL import Image img_path = "xxx" img = Image.open(img_path) img.show()
2.numpy.ndarray(通过opencv)
import cv2 cv_img=cv2.imread(img_path)
7. 常见的Transforms的使用
图片有不同的格式,打开方式也不同
Compose 的使用
把不同的 transforms 结合在一起,后面接一个数组,里面是不同的transforms
Example:图片首先要经过中心裁剪,再转换成Tensor数据类型 >>> transforms.Compose([ >>> transforms.CenterCrop(10), >>> transforms.PILToTensor(), >>> transforms.ConvertImageDtype(torch.float), >>> ])
Python中 __call__ 的用法
内置函数 __call__ ,不用.的方式调用方法,可以直接拿对象名,加上需要的参数,即可调用方法
class Person: def __call__(self, name): #下划线__表示为内置函数 print("__call__"+"Hello "+name) def hello(self,name): print("hello"+name) person = Person() person("zhangsan") person.hello("lisi")
输出结果如下:
__call__Hello zhangsan hellolisi
ToTensor 的使用
把 PIL Image 或 numpy.ndarray 类型转换为 tensor 类型(TensorBoard 必须是 tensor 的数据类型
from PIL import Image from torchvision import transforms from torch.utils.tensorboard import SummaryWriter image_path = r"F:\learn_pytorch\pythonProject\image\bbf69b4c9b3437cf78986a9c8b8fe315.jpg" image = Image.open(image_path) img_trans = transforms.ToTensor() img_tensor = img_trans(image) print(img_tensor) writer = SummaryWriter("log") writer.add_image("tensor",img_tensor,1) #图片类型默认是CHW,故不需要显式设置参数dataformats
(运行前要先把之前的logs进行删除)运行后,在 Terminal 里输入:
tensorboard --logdir=logs
进入网站后可以看到图片:
Normalize 的使用
图片RGB三个信道,将每个信道中的输入进行标准化
output[channel] = (input[channel] - mean[channel]) / std[channel]
设置 mean 和 std 都为0.5,则 output= 2*input -1。如果 input 图片像素值为0~1范围内,那么结果就是 -1~1之间img_norm = transforms.Normalize([0.5,0.5,0.5],[0.5,0.5,0.5]) img_n = img_norm(img_tensor) print(img_n) writer = SummaryWriter("log") writer.add_image("tensor",img_n,2) writer.close()
刷新网页:
更改mean和std值后刷新网页:
Resize() 的使用
输入:PIL或者Tensor 将输入转变到给定尺寸
- 序列:(h,w)高度,宽度
- 一个整数:不改变高和宽的比例,只单纯改变最小边和最长边之间的大小关系。之前图里最小的边将会匹配这个数(等比缩放)
PyCharm小技巧设置:忽略大小写,进行提示匹配
一般情况下,你需要输入R,才能提示出Resize
我们想设置,即便你输入的是r,也能提示出Resize,也就是忽略了大小写进行匹配提示
File—> Settings—> 搜索case—> Editor-General-Code Completion-去掉Match case前的√—>Apply—>OK
#Resize # Resize的使用 print(image.size) # 输入是PIL.Image trans_resize = transforms.Resize((512, 512)) # img:PIL --> resize --> img_resize:PIL 输入是PIL图片,输出还是PIL图片 img_resize = trans_resize(image) # 输出还是PIL Image # img_resize:PIL --> totensor --> img_resize:tensor img_resize = img_trans(img_resize) writer = SummaryWriter("file") writer.add_image("Resize", img_resize, 0) print(img_resize)
Compose() 的使用
Compose() 中的参数需要是一个列表,Python中列表的表示形式为[数据1,数据2,...]
在Compose中,数据需要是transforms类型,所以得到Compose([transforms参数1,transforms参数2,...])
#Compose的使用(将输出类型从PIL变为tensor类型,第二种方法) trans_resize_2 = transforms.Resize(512) # 将图片短边缩放至512,长宽比保持不变 # PIL --> resize --> PIL --> totensor --> tensor #compose()就是把两个参数功能整合,第一个参数是改变图像大小,第二个参数是转换类型,前者的输出类型与后者的输入类型必须匹配 trans_compose = transforms.Compose([trans_resize_2,trans_totensor]) img_resize_2 = trans_compose(img) # 输入需要是PIL Image writer.add_image("Resize",img_resize_2,1)
RandomCrop() 的使用
参数size:
- sequence:(h,w) 高,宽
- int:裁剪一个该整数×该整数的图像
(1)以 int 为例
#RandomCrop()的使用 trans_random = transforms.RandomCrop(512) trans_compose_2 = transforms.Compose([trans_random,trans_totensor]) writer = SummaryWriter("file1") for i in range(10): #裁剪10个 img_crop = trans_compose_2(image) # 输入需要是PIL Image writer.add_image("RandomCrop",img_crop,i)
输出结果:
(2)以 sequence 为例:
#RandomCrop()的使用 trans_random = transforms.RandomCrop((500,1000)) trans_compose_2 = transforms.Compose([trans_random,trans_totensor]) writer = SummaryWriter("file1") for i in range(10): #裁剪10个 img_crop = trans_compose_2(image) writer.add_image("RandomCropHW",img_crop,i)
输出结果:
总结使用方法
1.关注transforms中参数输入和输出类型
2.多看官方文档
3.关注方法需要什么参数:参数如果设置了默认值,保留默认值即可,没有默认值的需要指定(看一下要求传入什么类型的参数)
4.不知道变量的输出类型可以:直接print该变量,print(type()),看结果里显示什么类型
最后要 totensor,在 tensorboard 看一下结果(tensorboard需要tensor数据类型进行显示)
8. torchvision 中的数据集使用
CIFAR10数据集
数据集下载过慢时:
获得下载链接后,把下载链接放到迅雷中,会首先下载压缩文件tar.gz,之后会对该压缩文件进行解压,里面会有相应的数据集
采用迅雷下载完毕后,在PyCharm里新建directory,名字也叫dataset,再将下载好的压缩包复制进去,download依然为True,运行后,会自动解压该数据
没有显示下载地址时:
按住Ctrl键,查看数据集的源代码,若其中有 url地址,可直接复制到迅雷中进行下载
如何把数据集(多张图片)和 transforms 结合在一起
CIFAR10数据集原始图片是PIL Image,如果要给pytorch使用,需要转为tensor数据类型(转成tensor后,就可以用tensorboard了)transforms 更多地是用在 datasets 里 transform 的选项中
import torchvision from torch.utils.tensorboard import SummaryWriter # 把dataset_transform运用到数据集中的每一张图片,都转为tensor数据类型 dataset_transform = torchvision.transforms.Compose([ torchvision.transforms.ToTensor() ]) train_set = torchvision.datasets.CIFAR10(root="./dataset", train=True, transform=dataset_transform, download=True) # root使用相对路径,会在该.py所在位置创建一个叫dataset的文件夹,同时把数据保存进去 test_set = torchvision.datasets.CIFAR10(root="./dataset", train=False, transform=dataset_transform, download=True) # print(test_set[0]) writer = SummaryWriter("p10") # 显示测试数据集中的前10张图片 for i in range(10): img, target = test_set[i] writer.add_image("test_set", img, i) # img已经转成了tensor类型 writer.close()
运行结果:
9. DataLoader 的使用
- dataset:告诉程序中数据集的位置,数据集中索引,数据集中有多少数据(想象成一叠扑克牌)
- dataloader:将数据加载到神经网络中,每次从dataset中取数据,通过dataloader中的参数可以设置如何取数据(想象成抓的一组牌)
参数:
batch_size:每次抓牌抓几张
shuffle:打乱与否,值为True的话两次打牌时牌的顺序是不一样。默认为False,但一般用True
num_workers:加载数据时采用单个进程还是多个进程,多进程的话速度相对较快,默认为0(主进程加载)。Windows系统下该值>0会有问题(报错提示:BrokenPipeError)
drop_last:100张牌每次取3张,最后会余下1张,这时剩下的这张牌是舍去还是不舍去。值为True代表舍去这张牌、不取出,False代表要取出该张牌示例:
dataset
__getitem()__:return img,target
dataloader(batch_size=4):从dataset中取4个数据img0,target0 = dataset[0]
img1,target1 = dataset[1]
img2,target2 = dataset[2]
img3,target3 = dataset[3]把 img 0-3 进行打包,记为imgs;target 0-3 进行打包,记为targets;作为dataloader中的返回
for data in test_loader: imgs,targets = data print(imgs.shape) print(targets)
输出结果:
torch.Size([4, 3, 32, 32]) #4张图片,三通道,32×32 tensor([0, 4, 4, 8]) #4个target进行一个打包
数据是随机取的(断点debug一下,可以看到采样器sampler是随机采样的),所以两次的 target 0 并不一样
# 用上节课torchvision提供的自定义的数据集 # CIFAR10原本是PIL Image,需要转换成tensor import torchvision.datasets from torch.utils.data import DataLoader from torch.utils.tensorboard import SummaryWriter # 准备的测试数据集 test_data = torchvision.datasets.CIFAR10("./dataset",train=False,transform=torchvision.transforms.ToTensor()) # 加载测试集 test_loader = DataLoader(dataset=test_data,batch_size=64,shuffle=True,num_workers=0,drop_last=False) #batch_size=4,意味着每次从test_data中取4个数据进行打包 writer = SummaryWriter("dataloader") step=0 for data in test_loader: imgs,targets = data #imgs是tensor数据类型 writer.add_images("test_data",imgs,step) step=step+1 writer.close()
运行结果如图,滑动滑块即是每一次取数据时的batch_size张图片:
由于 drop_last 设置为 False,所以最后16张图片(没有凑齐64张)显示如下:
drop_last
若将 drop_last 设置为 True,最后16张图片(step 156)会被舍去,结果如图:
shuffle
一个 for data in test_loader 循环,就意味着打完一轮牌(抓完一轮数据),在下一轮再进行抓取时,第二次数据是否与第一次数据一样。值为True的话,会重新洗牌(一般都设置为True)shuffle为False的话两轮取的图片是一样的
在外面再套一层 for epoch in range(2) 的循环
结果如下:
可以看出两次 step 155 的图片不一样
10. 神经网络的基本骨架 - nn.Module 的使用
其中最常用的是 Module 模块(为所有神经网络提供基本骨架)
模板:
import torch.nn as nn import torch.nn.functional as F class Model(nn.Module): #搭建的神经网络 Model继承了 Module类(父类) def __init__(self): #初始化函数 super(Model, self).__init__() #必须要这一步,调用父类的初始化函数 self.conv1 = nn.Conv2d(1, 20, 5) self.conv2 = nn.Conv2d(20, 20, 5) def forward(self, x): #前向传播(为输入和输出中间的处理过程),x为输入 x = F.relu(self.conv1(x)) #conv为卷积,relu为非线性处理 return F.relu(self.conv2(x))
前向传播 forward(在所有子类中进行重写)
11. 神经网络 - 卷积层
图像为二维矩阵,常使用二维卷积
参数:
动图:
蓝色的为输入图像,绿色的为输出图像,黑色阴影部分为卷积核
kernel_size:
定义了一个卷积核的大小,若为3则生成一个3×3的卷积核卷积核的参数是从一些分布中进行采样得到的,实际训练过程中,卷积核中的值会不断进行调整
in_channels & out_channels
in_channels:输入图片的channel数(彩色图像 in_channels 值为3)
out_channels:输出图片的channel数
in_channels 和 out_channels 都为 1 时,拿一个卷积核在输入图像中进行卷积out_channels 为 2 时,卷积层会生成两个卷积核(两个卷积核不一定一样),得到两个输出
CIFAR10数据集实例
# CIFAR10数据集 import torch import torchvision from torch import nn from torch.nn import Conv2d from torch.utils.data import DataLoader from torch.utils.tensorboard import SummaryWriter dataset = torchvision.datasets.CIFAR10("./dataset", train=False, transform=torchvision.transforms.ToTensor(), download=True) # 这里用测试数据集,因为训练数据集太大了 dataloader = DataLoader(dataset, batch_size=64) # 搭建神经网络Tudui class Tudui(nn.Module): def __init__(self): super(Tudui, self).__init__() # 因为是彩色图片,所以in_channels=3 self.conv1 = Conv2d(in_channels=3, out_channels=6, kernel_size=3, stride=1, padding=0) # 卷积层conv1 def forward(self, x): # 输出为x x = self.conv1(x) return x tudui = Tudui() # 初始化网络 # 打印一下网络结构 print(tudui) # Tudui((conv1): Conv2d(3, 6, kernel_size=(3, 3), stride=(1, 1))) writer = SummaryWriter("conv") step = 0 for data in dataloader: imgs, targets = data # 经过ToTensor转换,成为tensor数据类型,可以直接送到网络中 output = tudui(imgs) print(imgs.shape) # 输入大小 torch.Size([64, 3, 32, 32]) batch_size=64,in_channels=3(彩色图像),每张图片是32×32的 print(output.shape) # 经过卷积后的输出大小 torch.Size([64, 6, 30, 30]) 卷积后变成6个channels,但原始图像减小,所以是30×30的 writer.add_images("input", imgs, step) # 6个channel无法显示出来,所以需要经过转换reshape # torch.Size([64, 6, 30, 30]) ——> [xxx,3,30,30] 第一个值不知道为多少时写-1,会根据后面值的大小进行计算 output = torch.reshape(output, (-1, 3, 30, 30)) writer.add_images("output", output, step) step = step + 1 writer.close()
运行后的结果:
卷积前后维度计算公式:
很多论文中padding和stride为给出,需要通过公式去进行计算
比如VGG16模型:第一层的卷积进行了padding填充是经过公式计算得出
12. 神经网络 - 最大池化的使用
torch.nn — PyTorch 2.4 documentation
- MaxPool:最大池化(下采样)(最常用)
- MaxUnpool:上采样
- AvgPool:平均池化
- AdaptiveMaxPool2d:自适应最大池化
参数:
注意,卷积中stride默认为1,而池化中stride默认为kernel_size
注意:黑色阴影部分为卷积核
ceil_mode参数
Ceil_mode 默认情况下为 False,对于最大池化一般只需设置 kernel_size 即可
代码实现
要求的 input 必须是四维的,参数依次是:batch_size、channel、高、宽
上述图用代码实现:(以 Ceil_mode = True 为例)
import torch from torch import nn from torch.nn import MaxPool2d input = torch.tensor([[1,2,0,3,1], [0,1,2,3,1], [1,2,1,0,0], [5,2,3,1,1], [2,1,0,1,1]],dtype=torch.float32) #最大池化无法对long数据类型进行实现,将input变成浮点数的tensor数据类型 input = torch.reshape(input,(-1,1,5,5)) #-1表示torch计算batch_size print(input.shape) # 搭建神经网络 class Tudui(nn.Module): def __init__(self): super(Tudui, self).__init__() self.maxpool1 = MaxPool2d(kernel_size=3,ceil_mode=True) def forward(self,input): output = self.maxpool1(input) return output # 创建神经网络 tudui = Tudui() output = tudui(input) print(output)
运行结果:
为什么要进行最大池化?最大池化的作用是什么?
最大池化的目的是保留输入的特征,同时把数据量减小(数据维度变小),对于整个网络来说,进行计算的参数变少,会训练地更快如上面案例中输入是5x5的,但输出是3x3的,甚至可以是1x1的
类比:1080p的视频为输入图像,经过池化可以得到720p,也能满足绝大多数需求,传达视频内容的同时,文件尺寸会大大缩小用数据集 CIFAR10 实现最大池化
import torch import torchvision from torch import nn from torch.nn import MaxPool2d from torch.utils.data import DataLoader from torch.utils.tensorboard import SummaryWriter dataset = torchvision.datasets.CIFAR10("./dataset", train=False, download=True, transform=torchvision.transforms.ToTensor()) dataloader = DataLoader(dataset, batch_size=64) # 搭建神经网络 class Tudui(nn.Module): def __init__(self): super(Tudui, self).__init__() self.maxpool1 = MaxPool2d(kernel_size=3, ceil_mode=True) def forward(self, input): output = self.maxpool1(input) return output # 创建神经网络 tudui = Tudui() writer = SummaryWriter("maxpool") step = 0 for data in dataloader: imgs, targets = data writer.add_images("input", imgs, step) output = tudui(imgs) # output尺寸池化后不会有多个channel,原来是3维的图片,经过最大池化后还是3维的,不需要像卷积一样还要reshape操作(影响通道数的是卷积核个数) writer.add_images("output", output, step) step = step + 1 writer.close()
运行结果:(经过池化操作后,可以发现图片清晰度降低,像是打了马赛克一样的效果,但是图片的细节信息一样还是可以保持)
13. 神经网络 - 非线性激活
非线性激活:给神经网络引入一些非线性的特征
非线性越多,才能训练出符合各种曲线或特征的模型(提高泛化能力)
最常见:RELU
(输入:(N,*) N 为 batch_size,*不限制)
代码举例:
import torch from torch import nn from torch.nn import ReLU input = torch.tensor([[1,-0.5], [-1,3]]) input = torch.reshape(input,(-1,1,2,2)) #input必须要指定batch_size,-1表示batch_size自己算,1表示是1维的 print(input.shape) #torch.Size([1, 1, 2, 2]) # 搭建神经网络 class Tudui(nn.Module): def __init__(self): super(Tudui, self).__init__() self.relu1 = ReLU() #inplace默认为False def forward(self,input): output = self.relu1(input) return output # 创建网络 tudui = Tudui() output = tudui(input) print(output)
运行结果:
关于inplace:
Sigmoid
输入:(N,*) N 为 batch_size,*不限制
代码举例:Sigmoid(数据集CIFAR10)
import torch import torchvision.datasets from torch import nn from torch.nn import Sigmoid from torch.utils.data import DataLoader from torch.utils.tensorboard import SummaryWriter trans = torchvision.transforms.Compose([ torchvision.transforms.ToTensor() ]) dataset = torchvision.datasets.CIFAR10("./dataset", train=False, download=True, transform=trans) dataloader = DataLoader(dataset, batch_size=64) # 搭建神经网络 class Tudui(nn.Module): def __init__(self): super(Tudui, self).__init__() self.sigmoid1 = Sigmoid() # inplace默认为False def forward(self, input): output = self.sigmoid1(input) return output # 创建网络 tudui = Tudui() writer = SummaryWriter("sigmoid") step = 0 for data in dataloader: imgs, targets = data writer.add_images("input", imgs, global_step=step) output = tudui(imgs) writer.add_images("output", output, step) step = step + 1 writer.close()
运行结果:
14. 神经网络 - 线性层及其他层介绍
Linear Layers
import torch import torchvision.datasets from torch import nn from torch.nn import Linear from torch.utils.data import DataLoader dataset = torchvision.datasets.CIFAR10("./dataset", train=False, transform=torchvision.transforms.ToTensor(), download=True) dataloader = DataLoader(dataset, batch_size=64, drop_last=True) class Tudui(nn.Module): def __init__(self): super(Tudui, self).__init__() self.linear1 = Linear(196608, 10) def forward(self, input): output = self.linear1(input) return output tudui = Tudui() for data in dataloader: imgs, targets = data print(imgs.shape) # torch.Size([64, 3, 32, 32]) # output = torch.reshape(imgs,(1,1,1,-1)) # reshape按照指定维度把图片展平 # print(output.shape) # torch.Size([1, 1, 1, 196608]) # output = tudui(output) # print(output.shape) # torch.Size([1, 1, 1, 10]) output = torch.flatten(imgs) # flatten摊平成一行 print(output.shape) # torch.Size([196608]) output = tudui(output) print(output.shape) # torch.Size([10])
运行结果:
pytorch提供的一些网络模型
- 图片相关:torchvision torchvision.models — Torchvision 0.11.0 documentation
https://pytorch.org/vision/stable/models.html
分类、语义分割、目标检测、实例分割、人体关键节点识别(姿态估计)等等- 文本相关:torchtext 无
- 语音相关:torchaudio torchaudio.models — Torchaudio 2.4.0 documentation
https://pytorch.org/audio/stable/models.html
15. 神经网络 - 搭建小实战和 Sequential 的使用
好处:代码简洁易懂
对 CIFAR10 进行分类的简单神经网络
CIFAR 10:根据图片内容,识别其究竟属于哪一类(10代表有10个类别)
第一次卷积:首先加了几圈 padding(图像大小不变,还是32x32),然后卷积了32次
这里是假定stride=1,然后再求出padding.如何假定stride=2,padding过大,显然不合理.
直接搭建,实现上图 CIFAR10 model 的代码
from torch import nn from torch.nn import Conv2d,MaxPool2d,Flatten,Linear import torch class yyh(nn.Module): def __init__(self): super(yyh, self).__init__() self.conv1 = Conv2d(3,32,5,stride=1,padding=2) self.maxpool1 = MaxPool2d(2) self.conv2 = Conv2d(32,32,5,stride=1,padding=2)#维持尺寸不变,所以padding仍为2 self.maxpool2 = MaxPool2d(2) self.conv3 = Conv2d(32,64,5,stride=1,padding=2) self.maxpool3 = MaxPool2d(2) self.flatten = Flatten() #经过两个线性层:第一个线性层(1024为in_features,64为out_features) # 第二个线性层(64为in_features,10为out_features) self.Linear1 = Linear(1024,64) self.Linear2 = Linear(64,10) def forward(self,input): input = self.conv1(input) input = self.maxpool1(input) input = self.conv2(input) input = self.maxpool2(input) input = self.conv3(input) input = self.maxpool3(input) input = self.flatten(input) input = self.Linear1(input) input = self.Linear2(input) return input Yyh = yyh() print(Yyh) input = torch.ones((64,3,32,32))##全是1,batch_size=64(64张图片),3通道,32x32 output = Yyh(input) print(output.shape)
用 Sequential 搭建,实现上图 CIFAR10 model 的代码,并引入 tensorboard 可视化模型结构
作用:代码更加简洁
from torch import nn from torch.nn import Conv2d,MaxPool2d,Flatten,Linear,Sequential import torch from torch.utils.tensorboard import SummaryWriter class yyh(nn.Module): def __init__(self): super(yyh, self).__init__() self.model = Sequential( Conv2d(3, 32, 5, stride=1, padding=2), MaxPool2d(2), Conv2d(32, 32, 5, stride=1, padding=2), MaxPool2d(2), Conv2d(32, 64, 5, stride=1, padding=2), MaxPool2d(2), Flatten(), Linear(1024, 64), Linear(64, 10) ) def forward(self,input): input = self.model(input) return input Yyh = yyh() print(Yyh) input = torch.ones((64,3,32,32))##全是1,batch_size=64(64张图片),3通道,32x32 output = Yyh(input) print(output.shape) writer = SummaryWriter('seqs') writer.add_graph(Yyh,input) writer.close()
运行结果:
可视化结果:
16. 损失函数与反向传播
torch.nn 里的 loss function 衡量误差,在使用过程中根据需求使用,注意输入形状和输出形状即可loss 衡量实际神经网络输出 output 与真实想要结果 target 的差距,越小越好
作用:
计算实际输出和目标之间的差距
为我们更新输出提供一定的依据(反向传播):给每一个卷积核中的参数提供了梯度 grad,采用反向传播时,每一个要更新的参数都会计算出对应的梯度,优化过程中根据梯度对参数进行优化,最终达到整个 loss 进行降低的目的L1LOSS
torch.nn.L1Loss(size_average=None, reduce=None, reduction='mean')
input:(N,*) N是batch_size,即有多少个数据;*可以是任意维度
取平均的代码:
import torch from torch.nn import L1Loss # 实际数据或网络默认情况下就是float类型,不写测试案例的话一般不需要加dtype inputs = torch.tensor([1,2,3],dtype=torch.float32) # 计算时要求数据类型为浮点数,不能是整型的long targets = torch.tensor([1,2,5],dtype=torch.float32) inputs = torch.reshape(inputs,(1,1,1,3)) # 1 batch_size, 1 channel, 1行3列 targets = torch.reshape(targets,(1,1,1,3)) loss = L1Loss() result = loss(inputs,targets) print(result)
运行结果:
求和的代码:(只需要修改其中一处代码)
loss = L1Loss(reduction='sum')
运行结果:
MSELOSS(均方误差)
input:(N,*) N是batch_size,即有多少个数据;*可以是任意维度
求和的代码:
求平均的代码:
手动计算示例:
CROSSENTROPYLOSS(交叉熵)
适用于训练分类问题,有C个类别
input 为没有进行处理过的对每一类的得分
在之前写的神经网络中用到 Loss Function(损失函数)
import torchvision.datasets from torch import nn from torch.nn import Conv2d, MaxPool2d, Flatten, Linear, Sequential from torch.utils.data import DataLoader dataset = torchvision.datasets.CIFAR10("./dataset", train=False, transform=torchvision.transforms.ToTensor(), download=True) dataloader = DataLoader(dataset, batch_size=1) class Tudui(nn.Module): def __init__(self): super(Tudui, self).__init__() self.model1 = Sequential( Conv2d(3, 32, 5, padding=2), MaxPool2d(2), Conv2d(32, 32, 5, padding=2), MaxPool2d(2), Conv2d(32, 64, 5, padding=2), MaxPool2d(2), Flatten(), Linear(1024, 64), Linear(64, 10) ) def forward(self, x): # x为input x = self.model1(x) return x loss = nn.CrossEntropyLoss() tudui = Tudui() for data in dataloader: imgs, targets = data # imgs为输入,放入神经网络中 outputs = tudui(imgs) # outputs为输入通过神经网络得到的输出,targets为实际输出 result_loss = loss(outputs, targets) print(result_loss) # 神经网络输出与真实输出的误差
结果:
backward 反向传播
计算出每一个节点参数的梯度
在上述代码后加一行:
import torchvision.datasets from torch import nn from torch.nn import Conv2d, MaxPool2d, Flatten, Linear, Sequential from torch.utils.data import DataLoader dataset = torchvision.datasets.CIFAR10("./dataset", train=False, transform=torchvision.transforms.ToTensor(), download=True) dataloader = DataLoader(dataset, batch_size=1) class Tudui(nn.Module): def __init__(self): super(Tudui, self).__init__() self.model1 = Sequential( Conv2d(3, 32, 5, padding=2), MaxPool2d(2), Conv2d(32, 32, 5, padding=2), MaxPool2d(2), Conv2d(32, 64, 5, padding=2), MaxPool2d(2), Flatten(), Linear(1024, 64), Linear(64, 10) ) def forward(self, x): # x为input x = self.model1(x) return x loss = nn.CrossEntropyLoss() tudui = Tudui() for data in dataloader: imgs, targets = data # imgs为输入,放入神经网络中 outputs = tudui(imgs) # outputs为输入通过神经网络得到的输出,targets为实际输出 result_loss = loss(outputs, targets) print(result_loss) # 神经网络输出与真实输出的误差 result_loss.backward() # backward反向传播,是对result_loss,而不是对loss
在加上的这一句代码前打上断点(运行到该行代码的前一行,该行不运行),debug 后:
tudui ——> model 1 ——> Protected Attributes ——> _modules ——> '0' ——> bias / weight——> grad(是None)
点击Step into My Code,运行完该行后,可以发现刚刚的None有值了(损失函数一定要经过 .backward() 后才能反向传播,才能有每个需要调节的参数的grad的值)
17. 优化器
当使用损失函数时,可以调用损失函数的 backward,得到反向传播,反向传播可以求出每个需要调节的参数对应的梯度,有了梯度就可以利用优化器,优化器根据梯度对参数进行调整,以达到整体误差降低的目的
torch.optim — PyTorch 2.4 documentation
https://pytorch.org/docs/stable/optim.html
如何使用优化器?
(1)构造
(2)调用优化器的step方法
利用之前得到的梯度对参数进行更新
for input, target in dataset: optimizer.zero_grad() #把上一步训练的每个参数的梯度清零 output = model(input) loss = loss_fn(output, target) # 输出跟真实的target计算loss loss.backward() #调用反向传播得到每个要更新参数的梯度 optimizer.step() #每个参数根据上一步得到的梯度进行优化
算法
如Adadelta、Adagrad、Adam、RMSProp、SGD等等,不同算法前两个参数params、lr 都是一致的,后面的参数不同
学习速率不能太大(太大模型训练不稳定)也不能太小(太小模型训练慢),一般建议先采用较大学习速率,后采用较小学习速率
完整代码
在 data 循环外又套一层 epoch 循环,一次 data 循环相当于对数据训练一次,加了 epoch 循环相当于对数据训练 20 次
import torch import torchvision.datasets from torch import nn from torch.nn import Conv2d, MaxPool2d, Flatten, Linear, Sequential from torch.utils.data import DataLoader # 加载数据集并转为tensor数据类型 dataset = torchvision.datasets.CIFAR10("./dataset", train=False, transform=torchvision.transforms.ToTensor(), download=True) dataloader = DataLoader(dataset, batch_size=1) # 创建网络名叫Tudui class Tudui(nn.Module): def __init__(self): super(Tudui, self).__init__() self.model1 = Sequential( Conv2d(3, 32, 5, padding=2), MaxPool2d(2), Conv2d(32, 32, 5, padding=2), MaxPool2d(2), Conv2d(32, 64, 5, padding=2), MaxPool2d(2), Flatten(), Linear(1024, 64), Linear(64, 10) ) def forward(self, x): # x为input,forward前向传播 x = self.model1(x) return x # 计算loss loss = nn.CrossEntropyLoss() # 搭建网络 tudui = Tudui() # 设置优化器 optim = torch.optim.SGD(tudui.parameters(), lr=0.01) # SGD随机梯度下降法 for epoch in range(20): running_loss = 0.0 # 在每一轮开始前将loss设置为0 for data in dataloader: # 该循环相当于只对数据进行了一轮学习 imgs, targets = data # imgs为输入,放入神经网络中 outputs = tudui(imgs) # outputs为输入通过神经网络得到的输出,targets为实际输出 result_loss = loss(outputs, targets) optim.zero_grad() # 把网络模型中每一个可以调节的参数对应梯度设置为0 result_loss.backward() # backward反向传播求出每一个节点的梯度,是对result_loss,而不是对loss optim.step() # 对每个参数进行调优 running_loss = running_loss + result_loss # 每一轮所有loss的和 print(running_loss)
部分运行结果:
优化器对模型参数不断进行优化,每一轮的 loss 在不断减小
实际过程中模型在整个数据集上的训练次数(即最外层的循环)都是成百上千/万的,本例仅以 20 次举例
18. 现有网络模型的使用及修改
- 本节讲述:
- 如何加载现有的一些 pytorch 提供的网络模型
- 如何对网络模型中的结构进行修改,包括添加自己想要的一些网络模型结构
本节主要以Classification 里的 VGG 模型为例,数据集仍为 CIFAR10 数据集(主要用于分类)
注意:
./xxx表示当前路径下,../xxx表示返回上一级目录
多行注释快捷键:ctrl+/
VGG16 模型
- weights为 None 的情况下,只是加载网络模型,参数都为默认参数,不需要下载
- 为VGG16_Weights时需要从网络中下载,卷积层、池化层对应的参数等等(在ImageNet数据集中训练好的)
vgg16 网络架构
import torchvision.models from torchvision.models import VGG16_Weights vgg16_false = torchvision.models.vgg16(weights=None) # 另一个参数progress显示进度条,默认为True vgg16_true = torchvision.models.vgg16(weights=VGG16_Weights.IMAGENET1K_V1) print(vgg16_true)
输出:
VGG( (features): Sequential( (0): Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (1): ReLU(inplace=True) (2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (3): ReLU(inplace=True) (4): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False) (5): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (6): ReLU(inplace=True) (7): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (8): ReLU(inplace=True) (9): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False) (10): Conv2d(128, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (11): ReLU(inplace=True) (12): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (13): ReLU(inplace=True) (14): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (15): ReLU(inplace=True) (16): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False) (17): Conv2d(256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (18): ReLU(inplace=True) (19): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (20): ReLU(inplace=True) (21): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (22): ReLU(inplace=True) (23): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False) (24): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (25): ReLU(inplace=True) (26): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (27): ReLU(inplace=True) (28): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (29): ReLU(inplace=True) (30): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False) ) (avgpool): AdaptiveAvgPool2d(output_size=(7, 7)) (classifier): Sequential( (0): Linear(in_features=25088, out_features=4096, bias=True) (1): ReLU(inplace=True) (2): Dropout(p=0.5, inplace=False) (3): Linear(in_features=4096, out_features=4096, bias=True) (4): ReLU(inplace=True) (5): Dropout(p=0.5, inplace=False) (6): Linear(in_features=4096, out_features=1000, bias=True) ) ) Process finished with exit code 0
如何利用现有网络去改动它的结构?
CIFAR10 把数据分成了10类,而 vgg16 模型把数据分成了 1000 类,如何应用这个网络模型呢?
- 方式一:把最后线性层的 out_features 从1000改为10
- 方式二:在最后的线性层下面再加一层,in_features为1000,out_features为10
利用现有网络去改动它的结构,避免写 vgg16
很多框架会把 vgg16 当做前置的网络结构,提取一些特殊的特征,再在后面加一些网络结构,实现功能。
方式一:修改
以 vgg16_false 为例:
想将最后一层 Linear 的 out_features 改为10:
import torchvision.models from torch import nn from torchvision.models import VGG16_Weights vgg16_false = torchvision.models.vgg16(weights=None) # 另一个参数progress显示进度条,默认为True vgg16_true = torchvision.models.vgg16(weights=VGG16_Weights.IMAGENET1K_V1) print(vgg16_false) vgg16_false.classifier[6] = nn.Linear(4096,10) print(vgg16_false)
结果如下:
方式二:添加
以 vgg16_true 为例:
# 给 vgg16 添加一个线性层,输入1000个类别,输出10个类别 vgg16_true.add_module('add_linear',nn.Linear(in_features=1000,out_features=10)) print(vgg16_true)
结果如图:
如果想将 module 添加至 classifier 里:
# 给 vgg16 添加一个线性层,输入1000个类别,输出10个类别 vgg16_true.classifier.add_module('7',nn.Linear(in_features=1000,out_features=10)) print(vgg16_true)
结果如图:
19. 网络模型的保存与读取
两种方式保存模型
方式1:不仅保存了网络模型的结构,也保存了网络模型的参数
import torch import torchvision.models from torch import nn vgg16 = torchvision.models.vgg16() print(vgg16) # 保存方式1:模型结构+模型参数 torch.save(vgg16,"vgg16_method1.pth")
运行结果:
方式2:网络模型的参数保存为字典,不保存网络模型的结构(官方推荐的保存方式,用的空间小)
import torch import torchvision.models from torch import nn vgg16 = torchvision.models.vgg16() print(vgg16) # 保存方式2:模型参数(官方推荐) torch.save(vgg16.state_dict(),"vgg16_method2.pth") # 把vgg16的状态保存为字典形式(Python中的一种数据格式)
运行结果:
两种方式加载模型
方式1:对应保存方式1,打印出的是网络模型的结构
import torch import torchvision.models from torch import nn vgg16 = torchvision.models.vgg16() print(vgg16) torch.save(vgg16,"vgg16_method1.pth") # 方式1 对应 保存方式1,加载模型 model = torch.load("vgg16_method1.pth",) print(model) # 打印出的只是模型的结构,其实它的参数也被保存下来了
运行结果:
方式2:对应保存方式2,打印出的是参数的字典形式
import torch import torchvision.models from torch import nn vgg16 = torchvision.models.vgg16() torch.save(vgg16.state_dict(),"vgg16_method2.pth") # 方式2 加载模型 model = torch.load("vgg16_method2.pth") print(model) # 打印出的只是模型的结构,其实它的参数也被保存下来了
运行结果:
如何恢复网络模型结构?寻
import torch import torchvision.models from torch import nn vgg16 = torchvision.models.vgg16() torch.save(vgg16.state_dict(),"vgg16_method2.pth") vgg16.load_state_dict(torch.load("vgg16_method2.pth")) print(vgg16)
20. 完整的模型训练套路
在语句后面按 Ctrl + d 可以复制这条语句
训练过程说明几个点:
1.测试过程中不需要对模型进行调优,利用现有模型进行测试,所以有以下命令:
with torch.no_grad():
2.为了使测试的 loss 结果易找,在 train.py 中添加了一句 if 的代码,使train每训练100轮才打印1次:
if total_train_step % 100 ==0: # 逢百才打印记录 print("训练次数:{},loss:{}".format(total_train_step,loss.item()))
3.实际写项目过程中,模型直接定义在一个单独的文件中(如model.py),再在 训练文件中导入:
from model import *
4.与tensorboard结合可视化训练结果:
writer.add_scalar("train_loss",loss.item(),total_train_step) writer.add_scalar("test_loss", total_test_loss, total_test_step)
5.保存每一轮的训练结果:
torch.save(tudui, "tudui_{}.pth".format(i)) # 每一轮保存一个结果 print("模型已保存")
6.即便得到整体测试集上的 loss,也不能很好说明在测试集上的表现效果,在分类问题中可以用正确率表示:
accuracy = (outputs.argmax(1) == targets).sum() #argmax取出指定方向的最大值 # 1代表:横向比较。==(逻辑比较):True或False,sum:计算True或False个数 total_accuracy = total_accuracy + accuracy #计算总的精确个数
7.model.train() 和 model.eval()的作用
训练步骤开始之前会把网络模型(我们这里的网络模型叫 tudui)设置为train,并不是说把网络设置为训练模型它才能够开始训练
测试网络前写 网络.eval(),并不是说需要这一行才能把网络设置成 eval 状态,才能进行网络测试
作用:
这两句不写网络依然可以运行,它们的作用是:
本节写的案例没有 Dropout 层或 BatchNorm 层,所以有没有这两行无所谓
如果有这些特殊层,一定要调用
8.利用GPU训练:
第一种方式:(不常用)
第二种方式:
9.比较CPU/GPU训练时间
为了比较时间,引入 time 这个 package
import time
完整的代码示例:
model.py
import torch from torch import nn # 搭建神经网络(10分类网络) class Tudui(nn.Module): def __init__(self): super(Tudui, self).__init__() # 把网络放到序列中 self.model = nn.Sequential( nn.Conv2d(in_channels=3, out_channels=32, kernel_size=5, stride=1, padding=2), # 输入是32x32的,输出还是32x32的(padding经计算为2) nn.MaxPool2d(kernel_size=2), nn.Conv2d(in_channels=32, out_channels=32, kernel_size=5, stride=1, padding=2), # 输入输出都是16x16的(同理padding经计算为2) nn.MaxPool2d(kernel_size=2), nn.Conv2d(in_channels=32, out_channels=64, kernel_size=5, stride=1, padding=2), nn.MaxPool2d(kernel_size=2), nn.Flatten(), # 展平 nn.Linear(in_features=64 * 4 * 4, out_features=64), nn.Linear(in_features=64, out_features=10) ) def forward(self, x): x = self.model(x) return x if __name__ == '__main__':#为了让这段代码在此脚本文件执行时才运行 # 测试网络的验证正确性 tudui = Tudui() input = torch.ones((64, 3, 32, 32)) # batch_size=64(代表64张图片),3通道,32x32 output = tudui(input) print(output.shape)
train_cpu.py
import torchvision.datasets from torch.utils.tensorboard import SummaryWriter from model import * from torch import nn from torch.utils.data import DataLoader import time # 准备数据集,CIFAR10 数据集是PIL Image,要转换为tensor数据类型 train_data = torchvision.datasets.CIFAR10(root="../dataset", train=True, transform=torchvision.transforms.ToTensor(), download=True) test_data = torchvision.datasets.CIFAR10(root="../dataset", train=False, transform=torchvision.transforms.ToTensor(), download=True) # 看一下训练数据集和测试数据集都有多少张(如何获得数据集的长度) train_data_size = len(train_data) # length 长度 test_data_size = len(test_data) # 如果train_data_size=10,那么打印出的字符串为:训练数据集的长度为:10 print("训练数据集的长度为:{}".format(train_data_size)) # 字符串格式化,把format中的变量替换{} print("测试数据集的长度为:{}".format(test_data_size)) # 利用 DataLoader 来加载数据集 train_dataloader = DataLoader(train_data, batch_size=64) test_dataloader = DataLoader(test_data, batch_size=64) # 创建网络模型 tudui = Tudui() # 创建损失函数 loss_fn = nn.CrossEntropyLoss() # 分类问题可以用交叉熵 # 定义优化器 learning_rate = 0.01 # 另一写法:1e-2,即1x 10^(-2)=0.01 optimizer = torch.optim.SGD(tudui.parameters(), lr=learning_rate) # SGD 随机梯度下降 # 设置训练网络的一些参数 total_train_step = 0 # 记录训练次数 total_test_step = 0 # 记录测试次数 epoch = 10 # 训练轮数 # 添加tensorboard writer = SummaryWriter("../logs_train") start_time = time.time() for i in range(epoch): print("----------第{}轮训练开始-----------".format(i + 1)) # i从0-9 # 训练步骤开始 tudui.train() for data in train_dataloader: imgs, targets = data outputs = tudui(imgs) loss = loss_fn(outputs, targets) # 优化器优化模型 optimizer.zero_grad() # 首先要梯度清零 loss.backward() # 反向传播得到每一个参数节点的梯度 optimizer.step() # 对参数进行优化 total_train_step += 1 if total_train_step % 100 == 0: # 逢百才打印记录 end_time = time.time() print(end_time-start_time) print("训练次数:{},loss:{}".format(total_train_step, loss.item())) writer.add_scalar("train_loss", loss.item(), total_train_step) # 测试步骤开始 tudui.eval() total_test_loss = 0 total_accuracy = 0 with torch.no_grad(): # 无梯度,不进行调优 for data in test_dataloader: imgs, targets = data outputs = tudui(imgs) loss = loss_fn(outputs, targets) # 该loss为部分数据在网络模型上的损失,为tensor数据类型 # 求整体测试数据集上的误差或正确率 total_test_loss = total_test_loss + loss.item() # loss为tensor数据类型,而total_test_loss为普通数字 # 求整体测试数据集上的误差或正确率 accuracy = (outputs.argmax(1) == targets).sum() #argmax取出指定方向的最大值 # 1代表:横向比较。==(逻辑比较):True或False,sum:计算True或False个数 total_accuracy = total_accuracy + accuracy #计算总的精确个数 print("整体测试集上的Loss:{}".format(total_test_loss)) print("整体测试集上的正确率:{}".format(total_accuracy / test_data_size)) writer.add_scalar("test_loss", total_test_loss, total_test_step) writer.add_scalar("test_accuracy", total_accuracy / test_data_size, total_test_step) total_test_step += 1 torch.save(tudui, "tudui_{}.pth".format(i)) # 每一轮保存一个结果 print("模型已保存") writer.close()
train_gpu1.py
import torchvision.datasets from torch.utils.tensorboard import SummaryWriter from model import * from torch import nn from torch.utils.data import DataLoader import time # 准备数据集,CIFAR10 数据集是PIL Image,要转换为tensor数据类型 train_data = torchvision.datasets.CIFAR10(root="../dataset", train=True, transform=torchvision.transforms.ToTensor(), download=True) test_data = torchvision.datasets.CIFAR10(root="../dataset", train=False, transform=torchvision.transforms.ToTensor(), download=True) # 看一下训练数据集和测试数据集都有多少张(如何获得数据集的长度) train_data_size = len(train_data) # length 长度 test_data_size = len(test_data) # 如果train_data_size=10,那么打印出的字符串为:训练数据集的长度为:10 print("训练数据集的长度为:{}".format(train_data_size)) # 字符串格式化,把format中的变量替换{} print("测试数据集的长度为:{}".format(test_data_size)) # 利用 DataLoader 来加载数据集 train_dataloader = DataLoader(train_data, batch_size=64) test_dataloader = DataLoader(test_data, batch_size=64) # 创建网络模型 tudui = Tudui() tudui = tudui.cuda() # 创建损失函数 loss_fn = nn.CrossEntropyLoss() # 分类问题可以用交叉熵 loss_fn = loss_fn.cuda() # 定义优化器 learning_rate = 0.01 # 另一写法:1e-2,即1x 10^(-2)=0.01 optimizer = torch.optim.SGD(tudui.parameters(), lr=learning_rate) # SGD 随机梯度下降 # 设置训练网络的一些参数 total_train_step = 0 # 记录训练次数 total_test_step = 0 # 记录测试次数 epoch = 10 # 训练轮数 # 添加tensorboard writer = SummaryWriter("../logs_train_gpu") start_time = time.time() for i in range(epoch): print("----------第{}轮训练开始-----------".format(i + 1)) # i从0-9 # 训练步骤开始 for data in train_dataloader: imgs, targets = data imgs = imgs.cuda() targets = targets.cuda() outputs = tudui(imgs) loss = loss_fn(outputs, targets) # 优化器优化模型 optimizer.zero_grad() # 首先要梯度清零 loss.backward() # 反向传播得到每一个参数节点的梯度 optimizer.step() # 对参数进行优化 total_train_step += 1 if total_train_step % 100 == 0: # 逢百才打印记录 end_time = time.time() print(end_time-start_time) print("训练次数:{},loss:{}".format(total_train_step, loss.item())) writer.add_scalar("train_loss", loss.item(), total_train_step) # 测试步骤开始 total_test_loss = 0 total_accuracy = 0 with torch.no_grad(): # 无梯度,不进行调优 for data in test_dataloader: imgs, targets = data imgs = imgs.cuda() targets = targets.cuda() outputs = tudui(imgs) loss = loss_fn(outputs, targets) # 该loss为部分数据在网络模型上的损失,为tensor数据类型 # 求整体测试数据集上的误差或正确率 total_test_loss = total_test_loss + loss.item() # loss为tensor数据类型,而total_test_loss为普通数字 # 求整体测试数据集上的误差或正确率 accuracy = (outputs.argmax(1) == targets).sum() # 1:横向比较,==:True或False,sum:计算True或False个数 total_accuracy = total_accuracy + accuracy print("整体测试集上的Loss:{}".format(total_test_loss)) print("整体测试集上的正确率:{}".format(total_accuracy / test_data_size)) writer.add_scalar("test_loss", total_test_loss, total_test_step) writer.add_scalar("test_accuracy", total_accuracy / test_data_size, total_test_step) total_test_step += 1 torch.save(tudui, "tudui_{}.pth".format(i)) # 每一轮保存一个结果 print("模型已保存") writer.close()
train.gpu2.py(用第二张显卡跑)
import torchvision.datasets from torch.utils.tensorboard import SummaryWriter from model import * from torch import nn from torch.utils.data import DataLoader import time #定义训练的设备 device = torch.device("cuda:1") print(device) # 准备数据集,CIFAR10 数据集是PIL Image,要转换为tensor数据类型 train_data = torchvision.datasets.CIFAR10(root="../dataset", train=True, transform=torchvision.transforms.ToTensor(), download=True) test_data = torchvision.datasets.CIFAR10(root="../dataset", train=False, transform=torchvision.transforms.ToTensor(), download=True) # 看一下训练数据集和测试数据集都有多少张(如何获得数据集的长度) train_data_size = len(train_data) # length 长度 test_data_size = len(test_data) # 如果train_data_size=10,那么打印出的字符串为:训练数据集的长度为:10 print("训练数据集的长度为:{}".format(train_data_size)) # 字符串格式化,把format中的变量替换{} print("测试数据集的长度为:{}".format(test_data_size)) # 利用 DataLoader 来加载数据集 train_dataloader = DataLoader(train_data, batch_size=64) test_dataloader = DataLoader(test_data, batch_size=64) # 创建网络模型 tudui = Tudui() tudui = tudui.to(device) # 创建损失函数 loss_fn = nn.CrossEntropyLoss() # 分类问题可以用交叉熵 loss_fn = loss_fn.to(device) # 定义优化器 learning_rate = 0.01 # 另一写法:1e-2,即1x 10^(-2)=0.01 optimizer = torch.optim.SGD(tudui.parameters(), lr=learning_rate) # SGD 随机梯度下降 # 设置训练网络的一些参数 total_train_step = 0 # 记录训练次数 total_test_step = 0 # 记录测试次数 epoch = 10 # 训练轮数 # 添加tensorboard writer = SummaryWriter("../logs_train_gpu") start_time = time.time() for i in range(epoch): print("----------第{}轮训练开始-----------".format(i + 1)) # i从0-9 # 训练步骤开始 for data in train_dataloader: imgs, targets = data imgs = imgs.to(device) targets = targets.to(device) outputs = tudui(imgs) loss = loss_fn(outputs, targets) # 优化器优化模型 optimizer.zero_grad() # 首先要梯度清零 loss.backward() # 反向传播得到每一个参数节点的梯度 optimizer.step() # 对参数进行优化 total_train_step += 1 if total_train_step % 100 == 0: # 逢百才打印记录 end_time = time.time() print(end_time-start_time) print("训练次数:{},loss:{}".format(total_train_step, loss.item())) writer.add_scalar("train_loss", loss.item(), total_train_step) # 测试步骤开始 total_test_loss = 0 total_accuracy = 0 with torch.no_grad(): # 无梯度,不进行调优 for data in test_dataloader: imgs, targets = data imgs = imgs.to(device) targets = targets.to(device) outputs = tudui(imgs) loss = loss_fn(outputs, targets) # 该loss为部分数据在网络模型上的损失,为tensor数据类型 # 求整体测试数据集上的误差或正确率 total_test_loss = total_test_loss + loss.item() # loss为tensor数据类型,而total_test_loss为普通数字 # 求整体测试数据集上的误差或正确率 accuracy = (outputs.argmax(1) == targets).sum() # 1:横向比较,==:True或False,sum:计算True或False个数 total_accuracy = total_accuracy + accuracy print("整体测试集上的Loss:{}".format(total_test_loss)) print("整体测试集上的正确率:{}".format(total_accuracy / test_data_size)) writer.add_scalar("test_loss", total_test_loss, total_test_step) writer.add_scalar("test_accuracy", total_accuracy / test_data_size, total_test_step) total_test_step += 1 torch.save(tudui, "tudui_{}.pth".format(i)) # 每一轮保存一个结果 print("模型已保存") writer.close()
21. 完整的模型验证(测试、demo)套路
核心:利用已经训练好的模型,给它提供输入进行测试
以VGG16为例,进行验证
测试过程细节说明:
1.训练数据的标签数据
2.如果训练数据是随便在网络上找图片,需要通过 Resize() 使图片符合模型
3.网络要求是四维的输入 [batch_size,channel,length,width],但是获得的图片是三维的,图片没有指定 batch_size(网络训练过程中是需要 batch_size 的,而图片输入是三维的,需要reshape() 一下。torch.reshape() 方法
4.如果挑选的测试图片预测错误,原因可能是训练次数不够多,将训练轮数 epoch 修改大一点,并且加载更后面的训练文件
5.使用三张随机图片进行测试:
完整测试代码(以bird为例):
test.py
验证图像:
代码:
# 如何从test.py文件去找到dog文件(相对路径) import torch import torchvision.transforms from PIL import Image from torch import nn image_path = "../imgs/bird.png" # 或右键-> Copy Path-> Absolute Path(绝对路径) # 读取图片(PIL Image),再用ToTensor进行转换 image = Image.open(image_path) # 现在的image是PIL类型 print(image) # <PIL.PngImagePlugin.PngImageFile image mode=RGB size=430x247 at 0x1DF29D33AF0> # image = image.convert('RGB') # 因为png格式是四通道,除了RGB三通道外,还有一个透明度通道,所以要调用上述语句保留其颜色通道 # 当然,如果图片本来就是三颜色通道,经过此操作,不变 # 加上这一步后,可以适应 png jpg 各种格式的图片,不同截图软件保存的图片通道数不同,也许不加这行代码并不会报错 # 该image大小为260x180,网络模型的输入只能是32x32,进行一个Resize() # Compose():把transforms几个变换联立在一起 transform = torchvision.transforms.Compose([torchvision.transforms.Resize((32, 32)), # 32x32大小的PIL Image torchvision.transforms.ToTensor()]) # 转为Tensor数据类型 image = transform(image) print(image.shape) # torch.Size([3, 32, 32]) # 然后加载网络模型 model = torch.load("tudui_29_gpu1.pth") # 因为test.py和tudui_29_gpu1.pth在同一个层级下,所以地址可以直接写 print(model) image = torch.reshape(image, (1, 3, 32, 32)) image = image.cuda() #这一步很容易错,采用GPU训练得到的,在验证时一定也要将输入转换到cuda上面 model.eval() # 模型转化为测试类型 with torch.no_grad():# 节约内存和性能 output = model(image) print(output) train_data = torchvision.datasets.CIFAR10(root="../dataset", train=True, transform=torchvision.transforms.ToTensor(), download=True) print(train_data.classes[output.argmax(1)]) #为了输出具体类别
输出结果: