打造Fashion-MNIST CNN,PyTorch风格
- 2019 年 10 月 28 日
- 笔记
编辑 | 代码医生团队
关于技术框架,一个有趣的事情是,从一开始,似乎总是被各种选择。但是随着时间的推移,比赛将演变为只剩下两个强有力的竞争者。例如“ PC vs Mac”,“ iOS vs Android”,“ React.js vs Vue.js”等。现在,在机器学习中拥有“ PyTorch vs TensorFlow”。
由Google支持的TensorFlow无疑是这里的领先者。它于2015年作为开放源代码的机器学习框架发布,迅速获得了广泛的关注和认可,尤其是在生产准备和部署至关重要的行业中。PyTorch于2017年在Facebook上推出的很晚,但由于其动态的计算图和“ pythonic ''风格而很快赢得了从业者和研究人员的广泛喜爱。

图片来自渐变
The Gradient的最新研究表明,PyTorch在研究人员方面做得很好,而TensorFlow在行业界占主导地位:
在2019年,机器学习框架之战还有两个主要竞争者:PyTorch和TensorFlow。我的分析表明,研究人员正在放弃TensorFlow并大量涌向PyTorch。同时,在行业中,Tensorflow当前是首选平台,但长期以来可能并非如此。— 渐变
PyTorch 1.3的最新版本引入了PyTorch Mobile,量化和其他功能,它们都在正确的方向上缩小了差距。如果对神经网络基础有所了解,但想尝试使用PyTorch作为其他样式,请继续阅读。将尝试说明如何使用PyTorch从头开始为Fashion-MNIST数据集构建卷积神经网络分类器。如果没有强大的本地环境,则可以在Google Colab和Tensor Board上使用此处的代码。事不宜迟开始吧。可以在下面找到Google Colab Notebook和GitHub链接:
Co Google Colab笔记本
https://colab.research.google.com/drive/1YWzAjpAnLI23irBQtLvDTYT1A94uCloM
GitHub上
https://github.com/wayofnumbers/SideProjects/blob/master/PyTorch_Tutorial_Basic_v1.ipynb
Import
首先,导入必要的模块。
# import standard PyTorch modules import torch import torch.nn as nn import torch.nn.functional as F import torch.optim as optim from torch.utils.tensorboard import SummaryWriter # TensorBoard support # import torchvision module to handle image manipulation import torchvision import torchvision.transforms as transforms # calculate train time, writing train data to files etc. import time import pandas as pd import json from IPython.display import clear_output torch.set_printoptions(linewidth=120) torch.set_grad_enabled(True) # On by default, leave it here for clarity
PyTorch模块非常简单。
Torch
torch是包含Tensor计算所需的所有内容的主要模块。可以单独使用Tensor计算来构建功能齐全的神经网络,但这不是本文的目的。将利用更强大和便捷torch.nn,torch.optim而torchvision类快速构建CNN。
torch.nn和torch.nn.functional

Alphacolor在Unsplash上拍摄的照片
该torch.nn模块提供了许多类和函数来构建神经网络。可以将其视为神经网络的基本构建块:模型,各种层,激活函数,参数类等。它可以像将一些LEGO集放在一起一样构建模型。
Torch优化
torch.optim 提供了SGD,ADAM等所有优化程序,因此无需从头开始编写。
Torch视觉
torchvision包含许多用于计算机视觉的流行数据集,模型架构和常见图像转换。我们从中获取Fashion MNIST数据集,并使用其变换。
SummaryWriter(张量板)
SummaryWriter使PyTorch可以为Tensor Board生成报告。将使用Tensor Board查看训练数据,比较结果并获得直觉。Tensor Board曾经是TensorFlow相对于PyTorch的最大优势,但是现在从v1.2开始,PyTorch正式支持它。
也引进了一些其他实用模块,如time,json,pandas,等。
数据集
torchvision已经具有Fashion MNIST数据集。如果不熟悉Fashion MNIST数据集:
Fashion-MNIST是Zalando文章图像的数据集-包含60,000个示例的训练集和10,000个示例的测试集。每个示例都是一个28×28灰度图像,与来自10个类别的标签相关联。我们打算Fashion-MNIST直接替代原始MNIST数据集,以对机器学习算法进行基准测试。它具有相同的图像大小以及训练和测试分割的结构。— 来自Github
https://github.com/zalandoresearch/fashion-mnist

Fashion-MNIST数据集— 来自GitHub
# Use standard FashionMNIST dataset train_set = torchvision.datasets.FashionMNIST( root = './data/FashionMNIST', train = True, download = True, transform = transforms.Compose([ transforms.ToTensor() ]) )
这不需要太多解释。指定了根目录来存储数据集,获取训练数据,允许将其下载(如果本地计算机上不存在的话),然后应用transforms.ToTensor将图像转换为Tensor,以便可以在网络中直接使用它。数据集存储在dataset名为train_set.
网络
在PyTorch中建立实际的神经网络既有趣又容易。假设对卷积神经网络的工作原理有一些基本概念。如果没有,可以参考Deeplizard的以下视频:
Fashion MNIST的尺寸仅为28×28像素,因此实际上不需要非常复杂的网络。可以像这样构建一

CNN拓扑
有两个卷积层,每个都有5×5内核。在每个卷积层之后,都有一个最大步距为2的最大合并层。这能够从图像中提取必要的特征。然后,将张量展平并放入密集层中,通过多层感知器(MLP)来完成10类分类的任务。
现在已经了解了网络的结构,看看如何使用PyTorch来构建它:
# Build the neural network, expand on top of nn.Module class Network(nn.Module): def __init__(self): super().__init__() # define layers self.conv1 = nn.Conv2d(in_channels=1, out_channels=6, kernel_size=5) self.conv2 = nn.Conv2d(in_channels=6, out_channels=12, kernel_size=5) self.fc1 = nn.Linear(in_features=12*4*4, out_features=120) self.fc2 = nn.Linear(in_features=120, out_features=60) self.out = nn.Linear(in_features=60, out_features=10) # define forward function def forward(self, t): # conv 1 t = self.conv1(t) t = F.relu(t) t = F.max_pool2d(t, kernel_size=2, stride=2) # conv 2 t = self.conv2(t) t = F.relu(t) t = F.max_pool2d(t, kernel_size=2, stride=2) # fc1 t = t.reshape(-1, 12*4*4) t = self.fc1(t) t = F.relu(t) # fc2 t = self.fc2(t) t = F.relu(t) # output t = self.out(t) # don't need softmax here since we'll use cross-entropy as activation. return t
首先,PyTorch中的所有网络类都在基类上扩展nn.Module。它包含了所有基础知识:权重,偏差,正向方法,以及一些实用程序属性和方法,例如.parameters()以及.zero_grad()将使用的方法。
网络结构在__init__dunder函数中定义。
def __init__(self): super().__init__() # define layers self.conv1 = nn.Conv2d(in_channels=1, out_channels=6, kernel_size=5) self.conv2 = nn.Conv2d(in_channels=6, out_channels=12, kernel_size=5) self.fc1 = nn.Linear(in_features=12*4*4, out_features=120) self.fc2 = nn.Linear(in_features=120, out_features=60) self.out = nn.Linear(in_features=60, out_features=10)
nn.Conv2d并且nn.Linear是内限定两个标准PyTorch层torch.nn模块。这些是不言而喻的。需要注意的一件事是,仅在此处定义了实际的图层。激活和最大池操作包含在下面说明的正向功能中。
# define forward function def forward(self, t): # conv 1 t = self.conv1(t) t = F.relu(t) t = F.max_pool2d(t, kernel_size=2, stride=2) # conv 2 t = self.conv2(t) t = F.relu(t) t = F.max_pool2d(t, kernel_size=2, stride=2) # fc1 t = t.reshape(-1, 12*4*4) t = self.fc1(t) t = F.relu(t) # fc2 t = self.fc2(t) t = F.relu(t) # output t = self.out(t) # don't need softmax here since we'll use cross-entropy as activation. return t
一旦定义了层,就可以使用层本身来计算每个层的前向结果,再加上激活函数(ReLu)和最大池操作,可以轻松地编写上述网络的前向函数。请注意,在fc1(完全连接层1)上,使用了PyTorch的张量操作t.reshape来拉平张量,以便随后可以将其传递到密集层。另外,没有在输出层添加softmax激活函数,因为PyTorch的CrossEntropy函数将解决这个问题。
超参数
可以精选一组超参数和做一些实验和他们在一起。在这个例子中,想通过引入一些结构来做更多的事情。将构建一个系统来生成不同的超参数组合,并使用它们进行训练“运行”。每个“运行”使用一组超参数组合。将每次运行的训练数据/结果导出到Tensor Board,以便可以直接比较并查看哪个超参数集表现最佳。
将所有超参数存储在OrderedDict中:
# put all hyper params into a OrderedDict, easily expandable params = OrderedDict( lr = [.01, .001], batch_size = [100, 1000], shuffle = [True, False] ) epochs = 3
lr:学习率。想为模型尝试0.01和0.001。
batch_size:批次大小以加快训练过程。将使用100和1000。
shuffle:随机切换,是否在训练之前对批次进行随机混合。
一旦参数关闭。使用两个帮助程序类:RunBuilder和RunManager管理超参数和训练过程。
运行构建器
该类的主要目的RunBuilder是提供一个静态方法get_runs。它以OrderedDict(所有超参数都存储在其中)为参数,并生成一个命名元组Run,每个的元素run表示超参数的一种可能组合。此命名的元组稍后由训练循环使用。该代码很容易理解。
# import modules to build RunBuilder and RunManager helper classes from collections import OrderedDict from collections import namedtuple from itertools import product # Read in the hyper-parameters and return a Run namedtuple containing all the # combinations of hyper-parameters class RunBuilder(): @staticmethod def get_runs(params): Run = namedtuple('Run', params.keys()) runs = [] for v in product(*params.values()): runs.append(Run(*v)) return runs
运行管理器
本RunManager 课程有四个主要目的。
- 计算并记录每个时期和运行的持续时间。
- 计算每个时期和跑步的训练损失和准确性。
- 记录每个时期的训练数据(例如,损失,准确性,权重,梯度,计算图等)并运行,然后将其导出到Tensor Board中进行进一步分析。
- 保存所有训练结果csv,json以备将来参考或提取API。
如您所见,它可以帮助处理物流,这对于成功训练模型也很重要。看一下代码。它有点长,所以请忍受:
# Helper class, help track loss, accuracy, epoch time, run time, # hyper-parameters etc. Also record to TensorBoard and write into csv, json class RunManager(): def __init__(self): # tracking every epoch count, loss, accuracy, time self.epoch_count = 0 self.epoch_loss = 0 self.epoch_num_correct = 0 self.epoch_start_time = None # tracking every run count, run data, hyper-params used, time self.run_params = None self.run_count = 0 self.run_data = [] self.run_start_time = None # record model, loader and TensorBoard self.network = None self.loader = None self.tb = None # record the count, hyper-param, model, loader of each run # record sample images and network graph to TensorBoard def begin_run(self, run, network, loader): self.run_start_time = time.time() self.run_params = run self.run_count += 1 self.network = network self.loader = loader self.tb = SummaryWriter(comment=f'-{run}') images, labels = next(iter(self.loader)) grid = torchvision.utils.make_grid(images) self.tb.add_image('images', grid) self.tb.add_graph(self.network, images) # when run ends, close TensorBoard, zero epoch count def end_run(self): self.tb.close() self.epoch_count = 0 # zero epoch count, loss, accuracy, def begin_epoch(self): self.epoch_start_time = time.time() self.epoch_count += 1 self.epoch_loss = 0 self.epoch_num_correct = 0 # def end_epoch(self): # calculate epoch duration and run duration(accumulate) epoch_duration = time.time() - self.epoch_start_time run_duration = time.time() - self.run_start_time # record epoch loss and accuracy loss = self.epoch_loss / len(self.loader.dataset) accuracy = self.epoch_num_correct / len(self.loader.dataset) # Record epoch loss and accuracy to TensorBoard self.tb.add_scalar('Loss', loss, self.epoch_count) self.tb.add_scalar('Accuracy', accuracy, self.epoch_count) # Record params to TensorBoard for name, param in self.network.named_parameters(): self.tb.add_histogram(name, param, self.epoch_count) self.tb.add_histogram(f'{name}.grad', param.grad, self.epoch_count) # Write into 'results' (OrderedDict) for all run related data results = OrderedDict() results["run"] = self.run_count results["epoch"] = self.epoch_count results["loss"] = loss results["accuracy"] = accuracy results["epoch duration"] = epoch_duration results["run duration"] = run_duration # Record hyper-params into 'results' for k,v in self.run_params._asdict().items(): results[k] = v self.run_data.append(results) df = pd.DataFrame.from_dict(self.run_data, orient = 'columns') # display epoch information and show progress clear_output(wait=True) display(df) # accumulate loss of batch into entire epoch loss def track_loss(self, loss): # multiply batch size so variety of batch sizes can be compared self.epoch_loss += loss.item() * self.loader.batch_size # accumulate number of corrects of batch into entire epoch num_correct def track_num_correct(self, preds, labels): self.epoch_num_correct += self._get_num_correct(preds, labels) @torch.no_grad() def _get_num_correct(self, preds, labels): return preds.argmax(dim=1).eq(labels).sum().item() # save end results of all runs into csv, json for further analysis def save(self, fileName): pd.DataFrame.from_dict( self.run_data, orient = 'columns', ).to_csv(f'{fileName}.csv') with open(f'{fileName}.json', 'w', encoding='utf-8') as f: json.dump(self.run_data, f, ensure_ascii=False, indent=4)
__init__:初始化必要的属性,例如计数,损失,正确预测的数量,开始时间等。
begin_run:记录运行的开始时间,以便在运行结束时可以计算出运行的持续时间。创建一个SummaryWriter对象以存储我们想要在运行期间导出到Tensor Board中的所有内容。将网络图和样本图像写入SummaryWriter对象。
end_run:运行完成后,关闭SummaryWriter对象,并将纪元计数重置为0(为下一次运行做好准备)。
begin_epoch:记录纪元开始时间,以便纪元结束时可以计算纪元持续时间。重置epoch_loss并epoch_num_correct。
end_epoch:大多数情况下都会发生此功能。当一个纪元结束时,将计算该纪元持续时间和运行持续时间(直到该纪元,除非最终的运行纪元,否则不是最终的运行持续时间)。将计算该时期的总损失和准确性,然后将记录的损失,准确性,权重/偏差,梯度导出到Tensor Board中。为了便于在Jupyter Notebook中进行跟踪,还创建了一个OrderedDict对象results,并将所有运行数据(损耗,准确性,运行计数,时期计数,运行持续时间,时期持续时间,所有超参数)放入其中。然后,将使用Pandas读取它并以整洁的表格格式显示它。
track_loss,track_num_correct,_get_num_correct:这些是实用功能以累积损耗,每批所以历元损失和准确性可以在以后计算的正确预测的数目。
save:保存所有运行数据(名单results OrderedDict所有实验对象)到csv和json作进一步的分析或API访问的格式。
这RunManager堂课有很多内容。恭喜到此为止!最困难的部分已经在身后。
训练
准备做一些训练!在RunBuilder 和RunManager的帮助下,训练过程变得轻而易举:
m = RunManager() # get all runs from params using RunBuilder class for run in RunBuilder.get_runs(params): # if params changes, following line of code should reflect the changes too network = Network() loader = torch.utils.data.DataLoader(train_set, batch_size = run.batch_size) optimizer = optim.Adam(network.parameters(), lr=run.lr) m.begin_run(run, network, loader) for epoch in range(epochs): m.begin_epoch() for batch in loader: images = batch[0] labels = batch[1] preds = network(images) loss = F.cross_entropy(preds, labels) optimizer.zero_grad() loss.backward() optimizer.step() m.track_loss(loss) m.track_num_correct(preds, labels) m.end_epoch() m.end_run() # when all runs are done, save results to files m.save('results')
首先,用于RunBuilder创建超参数的迭代器,然后循环遍历每种超参数组合以进行训练:
for run in RunBuilder.get_runs(params):
然后,network从Network上面定义的类创建对象。network = Network()。该network物体支撑着我们需要训练的所有重量/偏向。
还需要创建一个DataLoader 对象。这是一个保存训练/验证/测试数据集的PyTorch类,它将迭代该数据集,并以与batch_size指定数量相同的批次提供训练数据。
loader = torch.utils.data.DataLoader(train_set, batch_size = run.batch_size)
之后,将使用torch.optim类创建优化器。该optim课程将网络参数和学习率作为输入,将帮助逐步完成训练过程并更新梯度等。在这里,将使用Adam作为优化算法。
optimizer = optim.Adam(network.parameters(), lr=run.lr)
现在已经创建了网络,准备了数据加载器并选择了优化器。开始训练吧!
将循环遍历所有想要训练的纪元(此处为3),因此将所有内容包装在“纪元”循环中。还使用班级的begin_run方法RunManager来开始跟踪跑步训练数据。
m.begin_run(run, network, loader) for epoch in range(epochs):
对于每个时期,将遍历每批图像以进行训练。
m.begin_epoch() for batch in loader: images = batch[0] labels = batch[1] preds = network(images) loss = F.cross_entropy(preds, labels) optimizer.zero_grad() loss.backward() optimizer.step() m.track_loss(loss) m.track_num_correct(preds, labels)
上面的代码是进行实际训练的地方。从批处理中读取图像和标签,使用network类进行正向传播(还记得forward上面的方法吗?)并获得预测。通过预测,可以使用cross_entropy函数计算该批次的损失。一旦计算出损失,就用重置梯度(否则PyTorch将积累不想要的梯度).zero_grad(),执行一种反向传播使用loss.backward()方法来计算权重/偏差的所有梯度。然后,使用上面定义的优化程序来更新权重/偏差。现在,针对当前批次更新了网络,将计算损失和正确预测的数量,并使用类的track_loss和track_num_correct方法进行累积/跟踪RunManager。
完成所有操作后,将使用将结果保存到文件中m.save('results')。

张量板

图片来自Tensorboard.org
Tensor Board是一个TensorFlow可视化工具,现在也PyTorch支持。已经采取了将所有内容导出到'./runs'文件夹的工作,Tensor Board将在其中查找要使用的记录。现在需要做的只是启动张量板并检查。由于在Google Colab上运行此模型,因此将使用一种称为的服务ngrok来代理和访问在Colab虚拟机上运行的Tensor Board。ngrok 首先安装:
!wget https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-linux-amd64.zip !unzip ngrok-stable-linux-amd64.zip
然后,指定要从中运行Tensor Board的文件夹并启动Tensor Board Web界面(./runs为默认值):
LOG_DIR = './runs' get_ipython().system_raw( 'tensorboard --logdir {} --host 0.0.0.0 --port 6006 &' .format(LOG_DIR) )
启动ngrok代理:
get_ipython().system_raw('./ngrok http 6006 &')
生成一个URL,以便可以从Jupyter Notebook中访问Tensor Board:
! curl -s http://localhost:4040/api/tunnels | python3 -c "import sys, json; print(json.load(sys.stdin)['tunnels'][0]['public_url'])"
如下所示,TensorBoard是一个非常方便的可视化工具,可深入了解训练,并可以极大地帮助调整超参数。可以轻松地找出哪个超参数comp表现最佳,然后使用它来进行真正的训练。



结论
如您所见,PyTorch作为一种机器学习框架是灵活,强大和富于表现力的。只需编写Python代码。由于本文的主要重点是展示如何使用PyTorch构建卷积神经网络并以结构化方式对其进行训练,因此我并未完成整个训练时期,并且准确性也不是最佳的。可以自己尝试一下,看看模型的性能如何。