打造Fashion-MNIST CNN,PyTorch风格

  • 2019 年 10 月 28 日
  • 笔记

作者 | Lee 来源 | Medium

编辑 | 代码医生团队

关于技术框架,一个有趣的事情是,从一开始,似乎总是被各种选择。但是随着时间的推移,比赛将演变为只剩下两个强有力的竞争者。例如“ 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 课程有四个主要目的。

  1. 计算并记录每个时期和运行的持续时间。
  2. 计算每个时期和跑步的训练损失和准确性。
  3. 记录每个时期的训练数据(例如,损失,准确性,权重,梯度,计算图等)并运行,然后将其导出到Tensor Board中进行进一步分析。
  4. 保存所有训练结果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构建卷积神经网络并以结构化方式对其进行训练,因此我并未完成整个训练时期,并且准确性也不是最佳的。可以自己尝试一下,看看模型的性能如何。