Pytorch深度学习入门 | 系列(一)
☀️教程:B站我是土堆
☀️链接:https://www.bilibili.com/video/BV1hE411t7RN?p=1
1 Python学习中的两大法宝函数
理解package结构以及dir()和help()函数
Pytorch可以比喻为一个工具箱,里面有很多分区,每个分区的工具有不同的作用
dir()函数的作用:查看对象内的所有的属性和方法,返回包含查询对象的所有属性和方法名称的列表。(能让我们知道工具包以及工具包中的分隔区有什么东西)
help()函数的作用:help()函数帮助我们了解模块、类型、对象、方法、属性的详细信息,例如告诉我们这个函数的作用等等。(能让我们知道每个工具是如何使用的,即工具的使用方法)
举例:查看torch包下的is_available()函数用法
注:在使用help查询函数用法的时候,输入函数名即可,不用带(),否则查询的是这个函数的返回值的信息
2 Pycharm与Jupyter使用对比
如果代码是以块为一个整体运行的话
Pycharm中的Python文件:Python文件的块是文件中的所有行的代码
- 优点:通用,传播方便适用于大型项目
- 缺点:如果报错则需要从头开始运行
Pycharm中的控制台:是以任意行为块运行的,适合调试代码
- 优点:可以显示每个变量的属性
- 缺点:不利于代码的修改,如果出错的话,报的错不会消失,影响代码的可读性
Jupyter:以任意行为块运行的
- 优点:利于代码的阅读和修改,适用于小型项目
- 缺点:环境需要配置
3 Pytorch中如何加载数据
3.1 Dataset类
假设我们拥有的数据是一堆“垃圾”,Dataset类可以提供一种方式去获取“垃圾”中的有用的数据及其lable,整理到一起。
该类解决的两个问题:
- 如何获取每一个数据及其label
- 告诉我们一共有多少个数据
Dataset类是一个抽象类,我们需要编写自己的Dataset类并继承它,主要需要重写其中的两个方法:get_item()(必须重写)和 len() (选择重写)
实战代码演示:
当文件名就是对应的label时,代码可以这么写
from torch.utils.data import Dataset
from PIL import Image
import os
class MyData(Dataset):
"""
该数据集的文件结构如下:
-------该项目
------dataset文件夹
-----train文件夹
-----ants文件夹
-----bees文件夹
-----val文件夹
-----ants文件夹
-----bees文件夹
"""
def __init__(self, root_dir, label_dir):
# 根目录 对应到train文件夹
self.root_dir = root_dir
# 标签目录就是数据文件名
self.label_dir = label_dir
# 数据地址就是根目录 + 标签目录(数据文件),也就是对应的ants文件夹或bees文件夹
self.path = os.path.join(self.root_dir, self.label_dir)
# 图片地址就是数据文件下的所有文件名 (返回一个列表)
self.img_path = os.listdir(self.path)
# 重写__getitem__()函数
def __getitem__(self, idx):
imag_name = self.img_path[idx]
img_item_path = os.path.join(self.path, imag_name)
img = Image.open(img_item_path)
label = self.label_dir
return img, label
# 重写 __len__()函数:获取数据集的数量
def __len__(self):
return len(self.img_path)
# 测试
root_dir = "hymenoptera_data/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)
train_dataset = ants_dataset + bees_dataset
img, label = train_dataset[0]
img.show()
print(label)
当label放在一个专属的label文件夹中,label文件夹中存放着和数据文件夹中文件名字相对应的txt文件时,代码可以这么写,读出对应图片文件的label
from torch.utils.data import Dataset
from PIL import Image
import os
class MyData(Dataset):
"""
该数据集的文件结构如下:
-------该项目
------dataset文件夹
-----train文件夹
-----ants_image文件夹
-----ants_label文件夹(其中存放的文件的文件名与ants_image文件夹中的文件名一一对应)
-----bees_image文件夹
-----bees_label文件夹
-----val文件夹
-----ants_image文件夹
-----ants_label文件夹(其中存放的文件的文件名与ants_image文件夹中的文件名一一对应)
-----bees_image文件夹
-----bees_label文件夹
"""
def __init__(self, root_dir, label):
# 根目录 对应到train文件夹
self.root_dir = root_dir
# 标签目录,对应到对应的标签文件夹
self.label_dir = os.path.join(root_dir, label + "_label")
# 数据地址就是根目录 + 标签目录(数据文件),也就是对应的ants文件夹或bees文件夹
self.path = os.path.join(self.root_dir, label + "_image")
# 图片地址就是数据文件下的所有文件名 (返回一个列表)
self.img_path = os.listdir(self.path)
# 重写__getitem__()函数
def __getitem__(self, idx):
# 对应图片文件的名字 使用于数据文件夹和标签文件夹
imag_name = self.img_path[idx]
# 图片的地址
img_item_path = os.path.join(self.path, imag_name)
# 读出图片
img = Image.open(img_item_path)
# 读出该图片标签文件中的标签
with open(os.path.join(self.label_dir, self.img_path[idx].split('.')[0] + ".txt"), 'r') as f:
label = f.read()
return img, label
# 重写 __len__()函数:获取数据集的数量
def __len__(self):
return len(self.img_path)
# 测试
root_dir = "dataset/train"
ants_label = "ants"
bees_label = "bees"
ants_dataset = MyData(root_dir, ants_label)
bees_dataset = MyData(root_dir, bees_label)
train_dataset = ants_dataset + bees_dataset
img, label = train_dataset[200]
img.show()
print(label)
至此,以上编写的类的作用,就是可以读取数据集中的数据并读出该数据对应的标签,实际上就是提供一个接口可以去访问我们的数据集。
3.2 Dataloader类
此节可在学习完第六章后返回来看
由于每个神经网络接收的数据形式不同,该类可以为后面的网络提供不同的数据形式,也就是将数据包装加载到神经网络中。例如,神经网络读数据时一次读多少,随机读还是顺序读等等。
torch.utils.data.DataLoader(dataset, batch_size, shuffle, sampler, num_workers, drop_last, .......)
- dataset:我们使用的数据集
- batch_size:每个批次加载多少个数据
- shuffle:每轮读数据是否打乱上一轮读的顺序,这里的一轮指的是epoch,也就是将dataloader完整的读取一遍
- num_workers:多少个子进程一起加载数据,默认为0代表只使用主进程
- drop_last:如果数据集大小不能被批处理大小整除,是否舍弃掉最后不够batch_size大小的数据
这里介绍一下batch_size和epoch:
每次从总的数据集中随机抽取出小部分数据来代表整体,基于这部分数据计算梯度和损失来更新参数,这种方法被称作随机梯度下降法(Stochastic Gradient Descent,SGD),核心概念如下:
- minibatch:每次迭代时抽取出来的一批数据被称为一个minibatch。
- batch size:每个minibatch所包含的样本数目称为batch size。
- epoch:当程序迭代的时候,按minibatch逐渐抽取出样本,当把整个数据集都遍历到了的时候,则完成了一轮训练,也叫一个Epoch(轮次)。启动训练时,可以将训练的轮数
num_epochs
和batch_size
作为参数传入。
代码示例:
import torchvision.datasets
from torch.utils.data import DataLoader
# 准备测试的数据集
test_data = torchvision.datasets.CIFAR10("./dataset", train=False, transform=torchvision.transforms.ToTensor())
test_loader = DataLoader(dataset=test_data, batch_size=4, shuffle=True, num_workers=0, drop_last=False)
# 测试数据集中第一张图片及target
img, target = test_data[0]
print(img.shape)
print(target)
# 像上面那样,dataloader每次会按照batch_size大小取出相应数量的img和target并合并组合成imgs和targets返回
for data in test_loader:
imgs, targets = data
print(imgs.shape)
print(targets)
test_loader是一个可迭代对象,用for循环取数据的时候,每次取的数据量是batch_size决定的,运行结果如下:
可以看到,imgs中img的数量为4,每个img都是(3, 32, 32)
的,targets的数量同样也是4
接下来我们修改代码,使用TensorBoard来观察:
import torch.utils.tensorboard
import torchvision.datasets
from torch.utils.data import DataLoader
# 准备测试的数据集
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)
writer = torch.utils.tensorboard.SummaryWriter("logs_dataloader")
step = 0
# 像上面那样,dataloader每次会按照batch_size大小取出相应数量的img和target并合并组合成imgs和targets返回
for data in test_loader:
imgs, targets = data
# print(imgs.shape)
# print(targets)
# 添加一组图片可以使用add_images()
writer.add_images("test_data", imgs, step)
step = step + 1
writer.close()
打开TensorBoard可以观察到结果:
每次取数据的时候,取了64张图片
因为我们把shuffle设置为True,所以我们每轮读dataloader的时候,顺序是打乱的而不是和上一轮一样的顺序来读:
# 将dataloader完整的读两轮
for epoch in range(2):
# 像上面那样,dataloader每次会按照batch_size大小取出相应数量的img和target并合并组合成imgs和targets返回
for data in test_loader:
imgs, targets = data
writer.add_images("Epoch: {}".format(epoch), imgs, step)
step = step + 1
打开TensorBoard可以观察到结果:
可以看到,读的顺序完全不同,一般情况下,我们都会设置shuffle为True
在平时我们使用Dataloader时,我们每次读出的imgs就可以作为神经网络的输入
4 TensorBoard的使用
TensorBoard 是Google开发的一个机器学习可视化工具。其主要用于记录机器学习过程,例如:
- 记录损失变化、准确率变化等
- 记录图片变化、语音变化、文本变化等,例如在做GAN时,可以过一段时间记录一张生成的图片
- 绘制模型
TensorBoard主要提供三个API:
SummaryWriter
:这个用来创建一个log文件,TensorBoard面板查看时,也是需要选择查看那个log文件。add_something
: 向log文件里面增添数据。例如可以通过add_scalar
增添折线图数据,add_image
可以增添图片。close
:当训练结束后,我们可以通过close
方法结束log写入。
4.1 TensorBoard的安装
一开始可能没有装tensorboard
,我们直接pip install tensorboard
装的话可能会出现setuptools
版本过高而Pytorch的版本太低的情况,所以我们需要pip uninstall setuptools
并重新安装版本低的conda install setuptools==58.0.4
即可解决
4.2 SummaryWriter类的介绍
SummaryWriter
类提供了一个高级 API,将条目直接写入 log_dir 中的事件文件以供 TensorBoard 使用,用于在给定目录中创建事件文件,并向其中添加摘要和事件。 该类异步更新文件内容。 这允许训练程序调用方法以直接从训练循环将数据添加到文件中,而不会减慢训练速度。
代码示例:
from torch.utils.tensorboard import SummaryWriter
writer = SummaryWriter("logs") # logs代表要穿件的目录的名字
# write.add_image()
# y = x
for i in range(100):
writer.add_scalar("y = x", i, i) # scalar_value是纵坐标y轴上的数据 global_step是横坐标x轴上的数据
writer.close()
运行代码之后,会在项目下创建一个名叫logs的文件夹,打开文件夹会看到刚刚代码生成的事件文件:
如果要用tensorboard解析该文件并打开,可以在命令行输入:
# 打开名叫logs的文件目录 未指定端口时 端口默认为6006
tensorboard --logdir=logs
# 打开名叫logs的文件目录 指定端口 避免和其他人的端口冲突
tensorboard --logdir=logs --port=6007
点击输出的网址就可以打开了:
可以看到,TensorBoard已经解析了该事件文件并生成了一个折线图。
如果我们重新运行程序并且没有更改tag
,就会出现这种情况:
所以当我们想重新写入的时候,可以将事件文件删掉重新执行程序
4.3 add_scalar()的使用:观察值
上面的代码示例展示了如何使用该函数,一般我们可以使用add_scalar()
来实现每隔多少轮次记录train/val loss等值,绘制折线图等等
4.4 add_image()的使用:观察结果
该函数常用来观察训练的结果,也就是可以将每一(或指定)轮次(步)训练的训练结果(图片)添加到事件文件中,在TensorBoard中查看,示例代码如下:
from torch.utils.tensorboard import SummaryWriter
from PIL import Image
import numpy as np
writer = SummaryWriter("logs")
img_path = "hymenoptera_data/train/ants/0013035.jpg"
img_PIL = Image.open(img_path)
# 由于add_image()函数传递的image类型必须是tensor或numpy类型 所以我们进行转换
img_array = np.array(img_PIL)
# 由于此img_array是numpy类型但为(H,W,C)格式的,所以我们还需要加个参数指定一下dataformats
writer.add_image("test", img_array, 1, dataformats='HWC')
# y = x
for i in range(100):
writer.add_scalar("y = 2x", 2 * i, i) # scalar_value是纵坐标y轴上的数据 global_step是横坐标x轴上的数据
writer.close()
注意:从PIL到numpy,需要在add_image()
中指定shape中每一个数字/维表示的含义
打开TensorBoard查看结果:
可以看到图片已经插入到里面了
一般我们可以使用这个来查看我们每一轮次训练的结果,做一个对比分析。
5 torchvision中的transforms
5.1 transforms的结构及作用
主要是用于对图片进行一些变换
transforms是一个工具箱,实际上就是一个py文件,里面有很多不同的class文件代表不同的工具,
进入transforms文件后按快捷键Alt+7
查看该文件的结构,可以看到有很多的类:
所以,transforms的作用看下面这个图可以更直观的理解:
5.2 transforms.ToTensor
通过了解transforms.ToTensor,我们可以解决两个问题:
- 如何使用transforms
- 为什么需要Tensor这个数据类型
我们在使用transforms时,选择要使用的工具类,创建其对象实例,直接将图片传入对象中即可(其会自动调用call函数来完成相应的操作),使用transforms的实例代码如下:
from PIL import Image
from torchvision import transforms
import cv2
img_path = "hymenoptera_data/train/ants/0013035.jpg"
img_PIL = Image.open(img_path)
img_cv2 = cv2.imread(img_path)
# 实例化ToTensor工具
trans_totensor = transforms.ToTensor()
# PIL to Tensor
img_tensor = trans_totensor(img_PIL)
print(img_tensor)
# numpy to Tensor
img_tensor = trans_totensor(img_cv2)
print(img_tensor)
Tensor数据类型:包装了我们神经网络要用到的一些参数,例如梯度等,将数据转换为Tensor类型,便于后面神经网络的训练,是专门针对GPU来设计的,可以运行在GPU上来加快计算效率
5.3 transforms.Normalize
Normalize归一化,该类可以归一化Tensor类型的图片
transforms.Normalize(mean, std)
输入(channel,height,width)形式的tensor,并输入每个channel对应的均值和标准差作为参数,函数会利用这两个参数分别将每层标准化**(使数据均值为0,方差为1)**后输出。
- mean:(list类型)长度与输入的通道数相同,代表每个通道上所有数值的平均值。
- std:(list类型)长度与输入的通道数相同,代表每个通道上所有数值的标准差。
计算公式为:
按理说,我们在设置参数的时候,应该是根据输入数据来计算出每个通道的上数据的平均值和标准差,但是当数据量过大的时候,我们再计算会很困难,这时候就可以将两个参数都设置为0.5并与transforms.ToTensor()
一起使用可以使将数据强制缩放到[-1,1]区间上。(标准化只能保证大部分数据在0附近——3σ原则)
原理是tensor的值的范围为[0, 1]
的话,将mean和std都设置为0.5
,那么上面的公式就变为 2 * input - 1
,就会将范围归到[1, -1]
之间。代码举例:
from PIL import Image
from torchvision import transforms
from torch.utils.tensorboard import SummaryWriter
writer = SummaryWriter("logs")
img_PIL = Image.open("images/HuGe.jpg")
# ToTensor
trans_totensor = transforms.ToTensor()
img_tensor = trans_totensor(img_PIL)
writer.add_image("img_tensor", img_tensor, 1)
print(img_tensor[0][0][0])
# Normalize(mean, std)
# 由于是三通道,所以每个参数要写三个值
trans_normalize = transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5])
img_norm = trans_normalize(img_tensor)
writer.add_image("img_norm", img_norm, 1)
print(img_norm[0][0][0])
writer.close()
输出控制台可以看到:
在归一化之前,img_tensor[0][0][0]
的值为0.4078
,归一化之后变为了-0.1843
,这正是 0.4078 * 2 - 1
的结果
打开TensorBoard,观察归一化结果:
5.4 transforms.Resize
用于将输入的PIL图片转换为给出的size
transforms.Resize(size)
- size:可以是(h,w)也可以是 单个值,当输入为(h,w)时,图片会被Resize为指定的大小,当输入是单个值时,会根据最小的边来等比缩放(将最小的边设置为指定值的同时,另一个边也会跟着等比缩放)
# Resize
trans_resize = transforms.Resize((512, 512))
# 注意 输入的是PIL的Image 输出同样也是PIL的Image
img_resize = trans_resize(img_PIL)
img_resize = trans_totensor(img_resize)
writer.add_image("img_resize", img_resize, 1)
5.5 transforms.Composes
Composes several transforms together.Compose类将transforms里的工具类都集成到了一起。
一般处理图像我们用Compose把多个步骤整合到一起:
transforms.Compose([transforms参数1, transforms参数2, ...])
这里的参数指的各个transforms工具的实例对象,代码举例:
# Compose - Resize - 2
trans_totensor = transforms.ToTensor()
trans_resize_2 = transforms.Resize(512)
trans_compose = transforms.Compose([trans_resize_2, trans_totensor])
img_resize_2 = trans_compose(img_PIL)
writer.add_image("img_resize", img_resize_2, 2)
可以看到,compose的作用就是将多个步骤整合到一起执行,TensorBoard的结果为:
可以观察到,Resize输入单个值,图片是等比缩放的
注意:Compose的参数列表中是要执行的操作步骤,每一个步骤的输入和输出一定要相互匹配
5.6 transforms.RandomCrop
对PIL图片进行随机裁剪
transforms.RandomCrop(size)
- size:可以是(h,w)也可以是 单个值,当输入为(h,w)时,图片会被Resize为指定的大小,当输入是单个值时,会默认为(size,size)
代码举例:
# RandomCrop
trans_random = transforms.RandomCrop(512)
trans_compose_2 = transforms.Compose([trans_random, trans_totensor])
img_random = trans_compose_2(img_PIL)
writer.add_image("img_resize", img_random, 3)
TensorBoard的结果为:
可以看到,RandomCrop将我们的图片随机的裁剪了一块指定大小的区域
5.7 transforms使用总结
- 在使用transforms中的工具时,首先关注输入和输出类型
- 多看官方文档
- 关注方法需要什么参数
6 torchvision中的datasets
6.1 数据集的下载和使用
torchvision中提供了很多经典的数据集供我们使用,可以参考官方文档,里面有各个数据集的介绍:
https://pytorch.org/vision/0.9/datasets.html
以CIFAR10数据集举例,CIFAR10数据集共有60000个样本,每个样本都是一张32*32像素的RGB图像(彩色图像),每个RGB图像又必定分为3个通道(R通道、G通道、B通道)。这60000个样本被分成了50000个训练样本和10000个测试样本。
torchvision.datasets.CIFAR10(root, train, transform, target_transform, download)
- root:数据集存放的位置
- train:是否认定为训练集,True将认定为训练集 否则为测试集
- transform:对数据集中的数据做怎样的transform
- download:此数据集是否需要下载
如何获得这个数据集,代码示范如下:
import torchvision
# root:设置数据集存放的位置 train:为真则认定为训练集 否则为测试集 download:需不需要下载
train_set = torchvision.datasets.CIFAR10(root="./dataset", train=True, download=True)
test_set = torchvision.datasets.CIFAR10(root="./dataset", train=False, download=True)
执行代码之后,就会自动下载该数据集,但速度较慢,我们可以复制给出的下载地址到迅雷下载下来。
如果是在python环境中下载完成后,会自动解压到该压缩包所在位置,如果是迅雷下载的压缩包,那么将压缩包放到dataset文件夹下,重新运行程序,就可以解压了
接下来,可以对这个数据集进行访问:
# 访问这个dataset的方法和之前自定义dataset的方法一样
# 索引访问返回一个元组 第一个是图片数据 第二个数该图片的类别在类别列表中的索引
img, target = test_set[0]
print(test_set.classes[target])
img.show()
得到的结果是cat,表明该图片是猫的图片
6.2 数据集结合transforms使用
有时,我们需要对数据集中的数据做transform操作,我们可以在读取数据集的操作和transform的操作结合,也就是设置transform的参数,示例代码如下:
import torchvision
from torch.utils.tensorboard import SummaryWriter
from torchvision import transforms
# 创建一个专门对该数据集进行操作的transform
# 也就是将数据集中的图片转换为tensor类型的
dataset_transform = transforms.Compose([
transforms.ToTensor()
])
# root:设置数据集存放的位置 train:为真则认定为训练集 否则为测试集 download:需不需要下载
train_set = torchvision.datasets.CIFAR10(root="./dataset", train=True, transform=dataset_transform, download=True)
test_set = torchvision.datasets.CIFAR10(root="./dataset", train=False, transform=dataset_transform, download=True)
writer = SummaryWriter("logs_cifar10")
for i in range(10):
img, target = test_set[i]
writer.add_image("test_set", img, i)
writer.close()
由于我们希望将图片添加到TensorBoard进行查看,所以我们需要把数据转换为Tensor数据类型,这就涉及到了transform操作中的ToTensor,我们定义好该数据集的transform操作,并赋值给transform参数即可,最后在TensorBoard中可以看到,我们取出了该数据集中的前十张图片:
注意:每个数据集的加载参数都有可能不同,在使用数据集的时候注意关注官方文档
我们这里看到的是pytorch给我们提供的现成的数据集,一开始我们讲的Dataset是说我们可以自定义数据集,这里现成的数据集只不过是给我们定义好了,都是继承了Dataset类。