编辑 | 代码医生团队
关于技术框架,一个有趣的事情是,从一开始,似乎总是被各种选择。但是随着时间的推移,比赛将演变为只剩下两个强有力的竞争者。例如“ 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个示例的测试集。每个示例都是一个28x28灰度图像,与来自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的尺寸仅为28x28像素,因此实际上不需要非常复杂的网络。可以像这样构建一
CNN拓扑
有两个卷积层,每个都有5x5内核。在每个卷积层之后,都有一个最大步距为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 课程有四个主要目的。
如您所见,它可以帮助处理物流,这对于成功训练模型也很重要。看一下代码。它有点长,所以请忍受:
# 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构建卷积神经网络并以结构化方式对其进行训练,因此我并未完成整个训练时期,并且准确性也不是最佳的。可以自己尝试一下,看看模型的性能如何。