为了给读者提供一个使用PyTorch进行深度学习的总体印象,这里准备了一个实战案例,向读者演示进行深度学习任务所需要的整体流程,读者可能不熟悉这里的程序设计和编写,但是只要求了解每个过程需要做的内容以及涉及的步骤即可。
HelloWorld是任何一门编程语言入门的基础程序,读者在开始学习编程时,打印的第一句话往往就是HelloWorld。在前面的章节中,我们也带领读者学习了PyCharm打印出来的第一个程序HelloWorld。
在深度学习编程中也有其特有的HelloWorld,其编程对象是一个图片数据集MNIST,要求对数据集中的图片进行分类,因此难度比较大。
对于好奇的读者来说,一定有一个疑问:MNIST究竟是什么?
实际上,MNIST是一个手写数字的图片数据库,它有60000个训练样本集和10000个测试样本集。打开来看,MNIST数据集如图2-23所示。
图2-23 MNIST数据集
读者可直接使用本书源码库提供的MNIST数据集,文件在dataset文件夹中,如图2-24所示。
图2-24 dataset文件夹
之后使用NumPy工具库进行数据读取,代码如下:
import numpy as np x_train = np.load("./dataset/mnist/x_train.npy") y_train_label = np.load("./dataset/mnist/y_train_label.npy")
读者也可以在百度搜索MNIST的下载地址,直接下载train-images-idx3-ubyte.gz、train-labels-idx1-ubyte.gz等,如图2-25所示。
图2-25 下载页面
下载4个文件,分别是训练图片集、训练标签集、测试图片集、测试标签集,这些文件都是压缩文件,解压后,可以发现这些文件并不是标准的图像格式,而是二进制文件,其中训练图片集的部分内容如图2-26所示。
图2-26 训练图片集的部分内容
MNIST训练集内部的文件结构如图2-27所示。
图2-27 训练集内部的文件结构
训练集中有60000个实例,也就是说这个文件包含60000个标签内容,每一个标签的值为一个0~9的数。这里先解析文件中每一个属性的含义。首先,该数据是以二进制格式存储的,我们读取的时候要以rb方式读取;其次,真正的数据只有[value]这一项,其他[type]之类的项只是用来描述的,并不是真正放在数据文件里面的信息。
也就是说,在读取真实数据之前,要读取4个32位integer。由[offset]可以看出,真正的pixel是从0016开始的,一个int 32位,所以在读取pixel之前要读取4个32位integer,也就是magic number、number of images、number of rows、number of columns。
继续对图片进行分析。在MNIST图片集中,所有的图片都是28×28的,也就是每幅图片都有28×28个像素。如图2-28所示,在train-images-idx3-ubyte文件中偏移量为0字节处,有一个4字节的数为0000 0803,表示魔数。这里补充一下什么是魔数,其实它就是一个校验数,用来判断这个文件是不是MNIST里面的train-images-idx3-ubyte文件。
图2-28 魔数
接下来是0000 ea60,值为60000,代表容量;从第8字节开始有一个4字节数,值为28,也就是0000 001c,表示每幅图片的行数;从第12字节开始有一个4字节数,值也为28,也就是0000 001c,表示每幅图片的列数;从第16字节开始才是我们的像素值。
这里使用每784字节代表一幅图片。
前面向读者介绍了两种不同的MNIST数据集的获取方式,在这里推荐使用本书配套源码中的MNIST数据集进行数据读取,代码如下:
import numpy as np x_train = np.load("./dataset/mnist/x_train.npy") y_train_label = np.load("./dataset/mnist/y_train_label.npy")
这里numpy函数会根据输入的地址对数据进行处理,并自动将其分解成训练集和验证集。打印训练集的维度如下:
(60000, 28, 28) (60000,)
这是使用数据处理的第一个步骤,有兴趣的读者可以进一步完成数据的训练集和测试集的划分。
回到MNIST数据集,每个MNIST实例数据单元也是由两部分构成的,包括一幅包含手写数字的图片和一个与其对应的标签。可以将其中的标签特征设置成y,而图片特征矩阵以x来代替,所有的训练集和测试集中都包含x和y。
图2-29用更为一般化的形式解释了MNIST数据实例的展开形式。在这里,图片数据被展开成矩阵的形式,矩阵的大小为28×28。至于如何处理这个矩阵,常用的方法是将其展开,而展开的方式和顺序并不重要,只需要将其按同样的方式展开即可。
图2-29 MNIST数据实例的展开形式
下面回到对数据的读取,前面已经介绍了,MNIST数据集实际上就是一个包含着60000幅图片的60000×28×28大小的矩阵张量[60000,28,28]。
矩阵中行数指的是图片的索引,用以对图片进行提取。而后面的28×28个向量用以对图片特征进行标注。实际上,这些特征向量就是图片中的像素点,每幅手写图片的大小是[28,28],每个像素转换为一个0~1的浮点数,构成矩阵,如图2-30所示。
图2-30 手写图片示例
对于使用PyTorch进行深度学习的项目来说,一个非常重要的内容是模型的设计,模型决定了深度学习在项目中采用哪种方式达到目标的主体设计。在本例中,我们的目的是输入一个图像之后对其进行去噪处理。
对于模型的选择,一个非常简单的思路是,图像输出的大小应该就是输入的大小,在这里我们选择使用Unet作为设计的主要模型。
注意: 对于模型的选择,读者现在不需要考虑,随着对本书学习的深入,见识到更多处理问题的手段后,对模型的选择自然会心领神会。
我们可以整体看一下Unet的结构(读者目前只需要知道Unet输入和输出大小是同样的维度即可),如图2-31所示。
图2-31 Unet的结构
可以看到对于整体模型架构来说,其通过若干个“模块”(block)与“直连”(residual)进行数据处理。这部分内容我们在后面的章节中会讲到,目前读者只需要知道模型有这种结构即可。Unet的模型整体代码如下:
import torch import einops.layers.torch as elt class Unet(torch.nn.Module): def __init__(self): super(Unet, self).__init__() #模块化结构,这也是后面常用到的模型结构 self.first_block_down = torch.nn.Sequential( torch.nn.Conv2d(in_channels=1,out_channels=32,kernel_size=3, padding=1),torch.nn.GELU(), torch.nn.MaxPool2d(kernel_size=2,stride=2) ) self.second_block_down = torch.nn.Sequential( torch.nn.Conv2d(in_channels=32,out_channels=64,kernel_size=3, padding=1),torch.nn.GELU(), torch.nn.MaxPool2d(kernel_size=2,stride=2) ) self.latent_space_block = torch.nn.Sequential( torch.nn.Conv2d(in_channels=64,out_channels=128,kernel_size=3, padding=1),torch.nn.GELU(), ) self.second_block_up = torch.nn.Sequential( torch.nn.Upsample(scale_factor=2), torch.nn.Conv2d(in_channels=128, out_channels=64, kernel_size=3, padding=1), torch.nn.GELU(), ) self.first_block_up = torch.nn.Sequential( torch.nn.Upsample(scale_factor=2), torch.nn.Conv2d(in_channels=64, out_channels=32, kernel_size=3, padding=1), torch.nn.GELU(), ) self.convUP_end = torch.nn.Sequential( torch.nn.Conv2d(in_channels=32,out_channels=1,kernel_size=3, padding=1), torch.nn.Tanh() ) def forward(self,img_tensor): image = img_tensor image = self.first_block_down(image) #print(image.shape) #torch.Size([5, 32, 14, 14]) image = self.second_block_down(image) #print(image.shape) #torch.Size([5, 16, 7, 7]) image = self.latent_space_block(image) #print(image.shape) #torch.Size([5, 8, 7, 7]) image = self.second_block_up(image) #print(image.shape) #torch.Size([5, 16, 14, 14]) image = self.first_block_up(image) #print(image.shape) #torch.Size([5, 32, 28, 28]) image = self.convUP_end(image) #print(image.shape) #torch.Size([5, 32, 28, 28]) return image if __name__ == '__main__': #main是Python进行单文件测试的技巧,请读者记住这种写法 image = torch.randn(size=(5,1,28,28)) Unet()(image)
在这里通过一个main架构标识了可以在单个文件中对文件进行测试,请读者记住这种写法。
除了深度学习模型外,完成一个深度学习项目设定模型的损失函数与优化函数也很重要。这两部分内容对于初学者来说可能并不是很熟悉,在这里读者只需要知道有这部分内容即可。
(1)对损失函数的选择,在这里选用MSELoss作为损失函数,MSELoss损失函数的中文名字为均方损失函数(Mean Squared Error Loss)。
MSELoss的作用是计算预测值和真实值之间的欧式距离。预测值和真实值越接近,两者的均方差就越小,均方差函数常用于线性回归模型的计算。在PyTorch中使用MSELoss的代码如下:
loss = torch.nn.MSELoss(reduction="sum")(pred, y_batch)
(2)优化函数的设定,在这里我们采用Adam优化器,对于Adam优化函数,请读者自行学习,在这里只提供使用Adam优化器的代码,如下所示:
optimizer = torch.optim.Adam(model.parameters(), lr=2e-5)
在介绍了深度学习的数据准备、模型以及损失函数和优化函数后,下面使用PyTorch训练一个可以实现去噪性能的深度学习整理模型,完整代码如下(本代码参看配套资源的第2章,读者可以直接在PyCharm中打开文件运行并查看结果):
import os os.environ['CUDA_VISIBLE_DEVICES'] = '0' #指定GPU编码 import torch import numpy as np import unet import matplotlib.pyplot as plt from tqdm import tqdm batch_size = 320 #设定每次训练的批次数 epochs = 1024 #设定训练次数 #device = "cpu" #PyTorch的特性,需要指定计算的硬件,如果没有GPU,就使用CPU进行计算 device = "cuda" #在这里默认使用GPU,如果读者运行出现问题,可以将其改成CPU模式 model = unet.Unet() #导入Unet模型 model = model.to(device) #将计算模型传入GPU硬件等待计算 model = torch.compile(model) #PyTorch 2.0的特性,加速计算速度 optimizer = torch.optim.Adam(model.parameters(), lr=2e-5) #设定优化函数 #载入数据 x_train = np.load("../dataset/mnist/x_train.npy") y_train_label = np.load("../dataset/mnist/y_train_label.npy") x_train_batch = [] for i in range(len(y_train_label)): if y_train_label[i] < 2: #为了加速演示,这里只运行数据集中小于2的数字, 也就是0和1,读者可以自行增加训练个数 x_train_batch.append(x_train[i]) x_train = np.reshape(x_train_batch, [-1, 1, 28, 28]) #修正数据输入维度: ([30596, 28, 28]) x_train /= 512. train_length = len(x_train) * 20 #增加数据的单词循环次数 for epoch in range(epochs): train_num = train_length // batch_size #计算有多少批次数 train_loss = 0 #用于损失函数的统计 for i in tqdm(range(train_num)): #开始循环训练 x_imgs_batch = [] #创建数据的临时存储位置 x_step_batch = [] y_batch = [] # 对每个批次内的数据进行处理 for b in range(batch_size): img = x_train[np.random.randint(x_train.shape[0])]#提取单幅图片内容 x = img y = img x_imgs_batch.append(x) y_batch.append(y) #将批次数据转换为PyTorch对应的tensor格式并将其传入GPU中 x_imgs_batch = torch.tensor(x_imgs_batch).float().to(device) y_batch = torch.tensor(y_batch).float().to(device) pred = model(x_imgs_batch) #对模型进行正向计算 loss = torch.nn.MSELoss(reduction=True)(pred, y_batch)/batch_size # 使用损失函数进行计算 #这里读者记住下面就是固定格式,一般这样使用即可 optimizer.zero_grad() #对结果进行优化计算 loss.backward() #损失值的反向传播 optimizer.step() #对参数进行更新 train_loss += loss.item() #记录每个批次的损失值 #计算并打印损失值 train_loss /= train_num print("train_loss:", train_loss) #下面对数据进行打印 image = x_train[np.random.randint(x_train.shape[0])]#随机挑选一条数据计算 image = np.reshape(image,[1,1,28,28]) #修正数据维度 image = torch.tensor(image).float().to(device) #挑选的数据传入硬件中等待计算 image = model(image) #使用模型对数据进行计算 image = torch.reshape(image, shape=[28,28]) #修正模型输出结果 image = image.detach().cpu().numpy() #将计算结果导入CPU中进行后续计算或者展示 #展示或存储数据结果 plt.imshow(image) plt.savefig(f"./img/img_{epoch}.jpg")
在代码中展示了完整的模型训练过程。首先传入数据,然后使用模型对数据进行计算,计算结果与真实值的误差被回传到模型中,之后PyTorch框架根据回传的误差对整体模型参数进行修正。训练结果如图2-32所示。
图2-32 训练结果
从图2-32中可以很清楚地看到,随着训练过程的进行,模型逐渐能够学会对输入的数据进行整形和输出,此时模型的输出结果表示已经能够很好地对输入的图形细节进行修正,读者可以自行完成这部分代码的运行。