仅有几层的全连接网络只能做很多事情-为了接近图像分类的最新结果,有必要做得更深一些。换句话说,网络中需要更多的层。但是,通过添加许多其他层,我们遇到了一些问题。首先,我们会遇到消失的梯度问题。 –但是,可以通过使用合理的激活功能(例如ReLU激活系列)在一定程度上解决这一问题。深度完全连接网络的另一个问题是模型中可训练参数的数量(即权重)会快速增长。这意味着训练速度变慢或几乎变得不可能,并且使模型过度拟合。那么有什么解决方案?
卷积神经网络试图通过利用图像(或时间序列)中相邻输入之间的相关性来解决第二个问题。例如,在猫和狗的图像中,靠近猫眼的像素更有可能与附近的显示猫的鼻子的像素相关,而不是代表图像另一侧的像素的像素。狗的鼻子。这意味着并非网络中的每个节点都需要连接到下一层中的每个其他节点–从而减少了模型中需要训练的权重参数的数量。卷积神经网络还具有其他一些可以改善训练的技巧,但我们将在下一节中介绍这些技巧。
卷积神经网络中要了解的第一件事是实际的卷积部分。 这是一个花哨的数学词,它表示所研究图像上的移动窗口或滤镜实质上是什么。 该移动窗口适用于节点的特定邻域,如下所示–在这里,应用的过滤器为(0.5 x节点值):
上图中仅显示了两个输出,其中每个输出节点都是2 x 2输入正方形的映射。 如前所述,每个输入正方形的映射权重在所有四个输入中均为0.5。 因此输出可以计算为:
在神经网络的卷积部分,我们可以想象这个2 x 2移动滤波器在输入图像中的所有可用节点/像素上滑动。 也可以使用标准的神经网络节点图来说明此操作:
移动过滤器连接的第一个位置由蓝色连接表示,第二个位置由绿线显示。如前所述,每个连接的权重为0.5。
这个卷积步骤中有几件事可以通过减少参数/权重来改善训练:
与完全连接的神经网络相比,卷积神经网络的这两个属性可以大大减少需要训练的参数数量。
卷积神经网络结构的下一步是通过非线性激活函数(通常是ReLU激活函数的某些版本)传递卷积运算的输出。这提供了神经网络众所周知的标准非线性行为。
卷积块中涉及的过程通常称为特征映射-这是指可以训练每个卷积滤波器以“搜索”图像中的不同特征,然后将其用于分类的想法。在进入卷积神经网络的下一个主要功能(称为池化)之前,我们将在下一部分中研究这种特征映射和通道的思想。
如前所述,由于各个过滤器的权重在应用于输入节点时保持不变,因此可以训练它们从输入数据中选择某些特征。在图像的情况下,它可以学习识别常见的几何对象,例如构成对象的线,边和其他形状。这就是名称特征映射的来源。因此,任何卷积层都需要多个经过训练以检测不同特征的滤波器。因此,需要更新之前的移动过滤器图表以看起来像这样:
现在你可以在上面图表的右侧看到卷积操作有多个堆叠输出。这是因为有多个训练过的滤波器会产生自己的2D输出(对于2D图像)。这些多个过滤器通常在深度学习中称为通道。这些通道中的每一个最终都将被训练以检测图像中的某些关键特征。因此,对于像MNIST数据集这样的灰度图像,卷积层的输出实际上将具有3维-每个通道2D,然后是不同通道数量的另一维。
如果输入本身是多通道的,就像彩色RGB图像(每个R-G-B一个通道)的情况一样,输出实际上是4D。幸运的是,任何值得盐的深度学习库,包括PyTorch,将能够为您轻松处理所有这些映射。最后,不要忘记卷积操作的输出将通过每个节点的激活。
现在,卷积神经网络的下一个至关重要的部分是一个称为池化的概念。
在卷积神经网络中池化有两个主要好处。这些是:
那么什么是池化?这是另一种滑动窗口类型技术,但是它不是应用可以训练的权重,而是在窗口的内容上应用某种类型的统计函数。最常见的池类型称为最大池,它在窗口内容上应用max()函数。还有其他变体,例如均值池(采用内容的统计均值),在某些情况下也会使用。在本教程中,我们将专注于最大池化。下图显示了最大池操作的一个示例:
我们将讨论与上图有关的许多要点:
在上图中,您可以观察到最大池化生效。对于第一个窗口(蓝色),您可以看到最大池输出3.0,这是2×2窗口中的最大节点值。同样,对于绿色的2×2窗口,它输出的最大值为5.0,对于红色窗口为最大值7.0。这很简单。
在上面的池图中,您会注意到池窗口每次向右移2个位置。这被称为步幅2。在上图中,步幅仅在x方向上显示,但是,如果目标是防止合并窗口重叠,则步幅在y方向上也必须为2。换句话说,步幅实际上被指定为[2,2]。需要注意的重要一件事是,如果在合并过程中跨度大于1,则输出大小将减小。如上所述,将5 x 5输入减少为3 x 3输出。这是一件好事–这称为下采样,它减少了模型中可训练参数的数量。
上面的池图中要注意的另一件事是,在5 x 5输入中添加了额外的列和行–这使池空间的有效大小等于6 x6。这是为了确保2 x 2合并窗口可以以[2,2]的步幅正常运行,称为填充。这些节点基本上是虚拟节点-因为这些虚拟节点的值为0,所以它们对于最大池操作基本上是不可见的。在PyTorch中构建卷积神经网络时,需要考虑填充。
好的,现在我们了解了池在卷积神经网络中的工作原理,以及它在执行降采样方面的作用,但是它还有什么作用呢?为什么最大池使用如此频繁?
除了下采样功能外,在卷积神经网络中还使用池化功能,以使某些特征的检测在一定程度上不会因比例和方向变化而变化。 思考池做什么的另一种方法是,它概括了较低级别,更复杂的信息。 假设有一个卷积滤波器,在训练过程中,该算法学会在输入图像中的各个方向上检测数字“ 9”。 为了使卷积神经网络正确地对图像中“ 9”的外观进行分类,无论尺寸或方向如何,只要在图像中的任何位置找到“ 9”,它都需要以某种方式“激活” 该数字是(即看起来为“ 6”时除外)。 池可以帮助进行更高级别的通用功能选择,如下图所示:
该图是池化操作的风格化表示。如果我们认为输入图像的一小部分区域中有一个数字“ 9”(绿色框),并假设我们试图检测图像中的这样一个数字,那么将发生什么,如果我们有一些卷积滤波器,他们将在图像中“看到”“ 9”(即返回较大的输出)时学会(通过ReLU)激活。但是,它们会或多或少地强烈激活,具体取决于“ 9”的方向。我们希望网络检测到图像中的“ 9”,而不管方向是什么,而这正是池的来源。只要这些滤波器中的任何一个具有较高的激活度,它都会“查看”这三个滤波器的输出并给出较高的输出。
因此,池化充当较低级别数据的泛化器,因此在某种程度上使网络能够从高分辨率数据移至较低分辨率信息。换句话说,结合卷积滤波器的池尝试检测图像中的对象。
下图来自Wikipedia,显示了完整开发的卷积神经网络的结构:
如果您从左到右处理上面的图像,我们首先会看到机器人的图像。然后“扫描”该图像是一系列卷积滤镜或特征图。然后,通过合并操作对这些过滤器的输出进行二次采样。此后,在第一个卷积合并操作的输出上还有另一组卷积和池化。最后,在输出处“附加”一个完全连接的层。网络输出中此完全连接层的目的需要一些解释。
如前所述,卷积神经网络获取高分辨率数据并将其有效解析为对象表示。因此,可以将完全连接的层视为将标准分类器附加到网络的信息丰富的输出上,以“解释”结果并最终产生分类结果。为了将此完全连接的层连接到网络,卷积神经网络的输出尺寸需要扁平化。
考虑前面的图-在输出处,我们有x x y个矩阵/张量的多个通道。这些通道需要展平为单个(N X 1)张量。考虑一个例子–假设我们有100个2 x 2矩阵的通道,代表了网络最终合并操作的输出。因此,需要将其展平为2 x 2 x 100 = 400行。可以在PyTorch中轻松执行此操作,如下所示。现在已经介绍了卷积神经网络的基础知识,是时候展示如何在PyTorch中实现它们了。
任何值得一提的深度学习框架都将能够轻松处理卷积神经网络操作。 PyTorch就是这样一个框架。 在本节中,我将逐步向您展示如何在PyTorch中创建卷积神经网络。 理想情况下,您已经对PyTorch有了一些基本概念(如果没有,您可以查看我的PyTorch入门教程)–否则,欢迎您使用它。 我们将要建立的网络将执行MNIST数字分类。 教程的完整代码可以在该站点的Github存储库中找到。
下图显示了我们将要构建的卷积神经网络体系结构:
首先,我们可以看到输入图像将是28 x 28像素的数字灰度表示。第一层将由32个5 x 5卷积滤波器+ ReLU激活通道组成,随后是2 x 2的最大合并下采样,步幅为2(这将提供14 x 14的输出)。在下一层中,我们再次使用64个5 x 5卷积滤波器通道扫描第1层的14 x 14输出,并最终进行2 x 2 max pooling(stride = 2)下采样,以产生7 x 7输出第二层
在网络的卷积部分之后,将进行扁平化操作,该操作将创建7 x 7 x 64 = 3164个节点,一个包含1000个完全连接节点的中间层,以及在10个输出节点上进行softmax运算以产生类概率。这些层代表输出分类器。
PyTorch具有集成的MNIST数据集(在torchvision软件包中),我们可以通过DataLoader功能使用它。在本小节中,我将介绍如何为MNIST数据集设置数据加载器。但首先,需要定义一些初步变量:
# Hyperparameters
num_epochs = 5
num_classes = 10
batch_size = 100
learning_rate = 0.001
DATA_PATH = 'C:\\Users\Andy\PycharmProjects\MNISTData'
MODEL_STORE_PATH = 'C:\\Users\Andy\PycharmProjects\pytorch_models\\'
首先,我们建立了一些训练超参数。 接下来–提供一些用于存储MNIST数据集的本地驱动器文件夹的规范(PyTorch会自动将数据集下载到该文件夹中,供您使用),并且在训练完成后还提供了训练后的模型参数的位置。
接下来,我们设置一个转换以应用于MNIST数据以及数据集变量:
# transforms to apply to the data
trans = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,))])
# MNIST dataset
train_dataset = torchvision.datasets.MNIST(root=DATA_PATH, train=True, transform=trans, download=True)
test_dataset = torchvision.datasets.MNIST(root=DATA_PATH, train=False, transform=trans)
上面要注意的第一件事是transforms.Compose()函数。此功能来自torchvision软件包。它允许开发人员在指定的数据集上设置各种操作。可以使用Compose()函数将多个变换链接在一起,形成一个列表。在这种情况下,首先我们指定一个将输入数据集转换为PyTorch张量的变换。 PyTorch张量是PyTorch中使用的一种特定数据类型,用于网络内的所有各种数据和权重操作。从本质上讲,它只是一个多维矩阵。无论如何,PyTorch要求将数据集转换为张量,以便可以在网络的训练和测试中使用它。
Compose()列表中的下一个参数是规范化转换。当输入数据被归一化时,神经网络的训练效果更好,因此数据范围为-1到1或0到1。要通过PyTorch Normalize变换做到这一点,我们需要提供MNIST数据集的均值和标准差,这种情况分别是0.1307和0.3081。请注意,对于每个输入通道,必须提供均值和标准差–在MNIST情况下,输入数据仅是单通道的,但是对于CIFAR数据集之类的东西,它具有3个通道(RGB中每种颜色对应一个)频谱),则需要为每个通道提供平均值和标准偏差。
接下来,需要创建train_dataset和test_dataset对象。 这些将随后传递到数据加载器。 为了从MNIST数据创建这些数据集,我们需要提供一些参数。 首先,root参数指定train.pt和test.pt数据文件所在的文件夹。 train参数是一个布尔值,它通知数据集以选择train.pt数据文件或test.pt数据文件。 下一个参数transform是我们提供创建的任何要应用于数据集的变换对象的位置-这里我们提供了先前创建的trans对象。 最后,download参数告诉MNIST数据集功能从在线来源下载数据(如果需要)。
现在已经创建了训练和测试数据集,是时候将它们加载到数据加载器中了:
train_loader = DataLoader(dataset=train_dataset, batch_size=batch_size, shuffle=True)
test_loader = DataLoader(dataset=test_dataset, batch_size=batch_size, shuffle=False)
PyTorch中的数据加载器对象提供了许多功能,这些功能在使用训练数据时非常有用-轻松整理数据的能力,轻松批处理数据的能力以及最终通过加载数据的能力来提高数据使用效率 使用多处理并行处理。 可以看到,提供了三个简单的参数–首先是您希望加载的数据集,其次是您想要的批处理大小,最后是是否希望随机地对数据进行随机排序。 数据加载器可以用作迭代器-因此要提取数据,我们可以只使用标准的Python迭代器(例如枚举)。 这将在本教程稍后的实践中显示。
接下来,我们需要设置我们的nn.Module类,该类将定义我们将要训练的卷积神经网络:
class ConvNet(nn.Module):
def __init__(self):
super(ConvNet, self).__init__()
self.layer1 = nn.Sequential(
nn.Conv2d(1, 32, kernel_size=5, stride=1, padding=2),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2))
self.layer2 = nn.Sequential(
nn.Conv2d(32, 64, kernel_size=5, stride=1, padding=2),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2))
self.drop_out = nn.Dropout()
self.fc1 = nn.Linear(7 * 7 * 64, 1000)
self.fc2 = nn.Linear(1000, 10)
好的–这就是模型定义的地方。在PyTorch中创建神经网络结构最直接的方法是创建一个类,该类继承自PyTorch中的nn.Module超级类。 nn.Module是一个非常有用的PyTorch类,其中包含构建典型的深度学习网络所需的全部内容。它还具有方便的功能,例如将变量和操作移至GPU或移回CPU的方法,对类中所有属性应用递归函数的方法(即重置所有权重变量),创建简化的界面进行训练等。值得在这里检查所有可用方法。
第一步是在_init_类中创建一些顺序图层对象。首先,我们通过创建nn.Sequential对象来创建第1层(self.layer1)。这种方法使我们可以在网络中创建顺序排列的层,并且是创建卷积+ ReLU +池化序列的便捷方法。可以看出,顺序定义中的第一个元素是Conv2d nn.Module方法-此方法创建了一组卷积滤波器。第一个参数是输入通道的数量–在这种情况下,它是我们的单通道灰度MNIST图像,因此参数是1。Conv2d的第二个参数是输出通道的数量–如上面的模型架构图所示,第一卷积滤波器层包含32个通道,因此这是我们第二个自变量的值。
kernel_size参数是卷积过滤器的大小–在这种情况下,我们需要5 x 5大小的卷积过滤器–因此该参数为5。如果要在x和y方向上使用大小不同的形状的过滤器,则需要提供一个元组 (x尺寸,y尺寸)。 最后,我们要指定padding参数。 这需要更多的思考。 卷积滤波或池化操作的任何维度的输出大小可以通过以下公式计算:
其中Win是输入的宽度,F是滤波器的大小,P是填充,S是跨度。相同的公式适用于高度计算,但是由于我们的图像和滤镜是对称的,因此相同的公式适用于两者。如果我们希望保持输入和输出尺寸相同,过滤器大小为5,步幅为1,则从以上公式可以看出,我们需要填充2。因此,Conv2d中填充的参数为2 。
序列中的下一个元素是简单的ReLU激活。在self.layer1的顺序定义中添加的最后一个元素是最大池操作。第一个参数是池大小,即2 x 2,因此参数是2。第二个–我们想通过将有效图像大小减小2倍来对数据进行下采样。为此,请使用上面的公式,我们将跨步设置为2,将填充设置为零。因此,stride参数等于2。如果我们未指定padding参数的默认值为0,那么这就是上面代码中的操作。通过这些计算,我们现在知道self.layer1的输出将是32个14 x 14“图像”的通道。
接下来,以与第一层相同的方式定义第二层self.layer2。 唯一的区别是Conv2d函数的输入现在是32个通道,而输出是64个通道。 使用相同的逻辑,并进行池下采样,self.layer2的输出为64个7 x 7图像通道。
接下来,我们指定一个退出层,以避免模型过度拟合。 最后,创建两个两个完全连接的层。 第一层的大小为7 x 7 x 64节点,并将连接到第二层的1000个节点。 要在PyTorch中创建完全连接的图层,我们使用nn.Linear方法。 此方法的第一个参数是该层中的节点数,第二个参数是下一层的节点数。
使用此_init_定义,现在已经创建了图层定义。 下一步是定义在执行通过网络的正向传递时数据如何流经这些层:
def forward(self, x):
out = self.layer1(x)
out = self.layer2(out)
out = out.reshape(out.size(0), -1)
out = self.drop_out(out)
out = self.fc1(out)
out = self.fc2(out)
return out
重要的是将此函数称为“ forward”,因为它将覆盖nn.Module中的基本正向函数,并允许所有nn.Module功能正常工作。 可以看出,它采用一个输入参数x,它是要通过模型传递的数据(即一批数据)。 我们将此数据传递到第一层(self.layer1),并将输出返回为“ out”。 然后将此输出馈送到下一层,依此类推。 请注意,在self.layer2之后,我们对out应用了整形函数,该函数将数据尺寸从7 x 7 x 64展平为3164 x1。接下来,应用了dropout,接着是两个完全连接的层,最终输出为 从函数返回。
好的-现在我们定义了卷积神经网络及其运行方式。 现在该训练模型了。
在训练模型之前,我们必须首先创建ConvNet类的实例,然后定义损失函数和优化器:
model = ConvNet()
# Loss and optimizer
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
首先,创建一个称为“模型”的ConvNet()实例。 接下来,我们定义将用于计算损失的损失操作。 在这种情况下,我们使用PyTorch的CrossEntropyLoss()函数。 您可能已经注意到,我们尚未为最终分类层定义SoftMax激活。 这是因为CrossEntropyLoss函数在同一函数中将SoftMax激活和交叉熵损失函数结合在一起,即获胜。 接下来,我们定义一个Adam优化器。 传递给此函数的第一个参数是我们希望优化器训练的参数。 通过ConvNet派生的nn.Module类可以轻松实现–我们要做的就是将model.parameters()传递给函数,PyTorch会跟踪模型中需要训练的所有参数。 最后,提供学习率。
下一步–创建训练循环:
# Train the model
total_step = len(train_loader)
loss_list = []
acc_list = []
for epoch in range(num_epochs):
for i, (images, labels) in enumerate(train_loader):
# Run the forward pass
outputs = model(images)
loss = criterion(outputs, labels)
loss_list.append(loss.item())
# Backprop and perform Adam optimisation
optimizer.zero_grad()
loss.backward()
optimizer.step()
# Track the accuracy
total = labels.size(0)
_, predicted = torch.max(outputs.data, 1)
correct = (predicted == labels).sum().item()
acc_list.append(correct / total)
if (i + 1) % 100 == 0:
print('Epoch [{}/{}], Step [{}/{}], Loss: {:.4f}, Accuracy: {:.2f}%'
.format(epoch + 1, num_epochs, i + 1, total_step, loss.item(),
(correct / total) * 100))
开始时最重要的部分是两个循环-首先,循环数个时期,在这个循环中,我们使用enumerate遍历train_loader。在此内部循环中,首先通过将图像(这是来自train_loader的一批归一化MNIST图像)传递到模型来计算模型的前向输出。注意,我们不必像nn那样调用model.forward(images).Module知道在执行model(images)时需要调用forward。
下一步是将模型输出和真实图像标签传递到我们的CrossEntropyLoss函数(定义为标准)。损失会附加到列表中,稍后将用于绘制培训进度。下一步是执行反向传播和优化的训练步骤。首先,必须将梯度归零,这可以通过在优化器上调用zero_grad()轻松完成。接下来,我们在loss变量上调用.backward()来执行向后传播。最后,既然已经在反向传播中计算了梯度,我们只需调用optimizer.step()即可执行Adam优化器训练步骤。 PyTorch使训练模型非常容易和直观。
下一组步骤涉及跟踪训练集的准确性。可以使用torch.max()函数确定模型的预测,该函数返回张量中最大值的索引。此函数的第一个参数是要检查的张量,第二个参数是确定最大值索引的轴。模型的输出张量将为大小(batch_size,10)。为了确定模型预测,对于批次中的每个样本,我们需要找到10个输出节点上的最大值。这些中的每一个将对应于手写数字之一(即输出2将对应于数字“ 2”,依此类推)。具有最高值的输出节点将是模型的预测。因此,我们需要将torch.max()函数的第二个参数设置为1 –这指向max函数以检查输出节点轴(axis = 0对应于batch_size维度)。
这将返回模型中的预测整数列表–下一行将预测与真实标签(预测==标签)进行比较,并对它们求和以确定有多少正确的预测。注意sum()的输出仍然是张量,因此要访问它的值,您需要调用.item()。我们用正确的预测数除以batch_size(等于labels.size(0))来获得准确性。最终,在训练过程中,每隔100次内循环迭代,就会打印进度。
训练输出将如下所示:
Epoch [1/6], Step [100/600], Loss: 0.2183, Accuracy: 95.00%
Epoch [1/6], Step [200/600], Loss: 0.1637, Accuracy: 95.00%
Epoch [1/6], Step [300/600], Loss: 0.0848, Accuracy: 98.00%
Epoch [1/6], Step [400/600], Loss: 0.1241, Accuracy: 97.00%
Epoch [1/6], Step [500/600], Loss: 0.2433, Accuracy: 95.00%
Epoch [1/6], Step [600/600], Loss: 0.0473, Accuracy: 98.00%
Epoch [2/6], Step [100/600], Loss: 0.1195, Accuracy: 97.00%
接下来,让我们创建一些代码来确定测试集上的模型准确性。
为了测试模型,我们使用以下代码:
# Test the model
model.eval()
with torch.no_grad():
correct = 0
total = 0
for images, labels in test_loader:
outputs = model(images)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
print('Test Accuracy of the model on the 10000 test images: {} %'.format((correct / total) * 100))
# Save the model and plot
torch.save(model.state_dict(), MODEL_STORE_PATH + 'conv_net_model.ckpt')
第一步,我们通过运行model.eval()将模型设置为评估模式。 这是一个方便的功能,它可以禁用模型中的任何退出或批处理规范化层,从而使模型评估/测试更加混乱。 torch.no_grad()语句禁用了模型中的autograd功能(请参见此处以获取更多详细信息),因为在模型测试/评估中不需要此功能,这将加快计算速度。 其余与训练期间的准确性计算相同,除了在这种情况下,代码通过test_loader进行迭代。
最后,将结果输出到控制台,并使用torch.save()函数保存模型。
在Github存储库的代码的最后部分,我使用Bokeh绘图库对损失和准确性跟踪进行了一些绘图。 最终结果如下所示:
该模型在10000张测试图像上的测试准确性:99.03%
可以观察到,网络在训练集上相当快地达到了很高的准确度,并且在6个星期后,测试集的准确度达到了99%–不错! 肯定比基本的完全连接的神经网络所达到的精度要好。
总结:在本教程中,您已经了解了卷积神经网络的优点和结构以及它们如何工作。 您还学习了如何在超赞的PyTorch深度学习框架中实现它们,我认为该框架有很大的发展前景。 我希望它是有用的–在您的深度学习之旅中玩得开心!