我不是魔法师
李宏毅《深度学习》—学习资源
开源文档:datawhalechina.github.io/leeml-notes
我不是魔法师
李宏毅《深度学习》(四)
6-深度学习简介深度学习的三个步骤Step1:神经网络(Neural network)Step2:模型评估(Goodness of function)Step3:选择最优函数(Pick best function)神经网络神经网络(Neural network)里面的节点,类似我们的神经元。神经网络也可以有很多不同的连接方式,这样就会产生不同的结构(structure)在这个神经网络里面,我们有很多逻辑回归函数,其中每个逻辑回归都有自己的权重和自己的偏差,这些权重和偏差就是参数,这些神经元连接方式都是手动去设计的。完全连接前馈神经网络概念: 前馈(feedforward)也可以称为前向,从信号流向来理解就是输入信号进入网络后,信号流动是单向的,即信号从前一层流向后一层,一直到输出层,其中任意两层之间的连接并没有反馈(feedback),亦即信号没有从后一层又返回到前一层。输入相应的数值之后,神经网络在每一层直接乘相应的权重,然后在输入到设置好的激活函数中,得到下一层的输入,然后再次重复之前的操作,将信号一层一层的向下传递,最后得到网络的输出。对于一个深度学习的网络结构应该分为以下几层:输入层(Input Layer):1层隐藏层(Hidden Layer):N层输出层(Output Layer):1层 全连接的理解: layer1与layer2之间两两都有连接,所以叫做Fully Connect前馈的理解: 现在传递的方向是由前往后传,所以叫做Feedforward深度的理解: Deep = Many hidden layer指的是拥有很多隐藏层为什么引入矩阵计算: 随着层数变多,错误率降低,随之运算量增大,通常都是超过亿万级的计算。对于这样复杂的结构,我们一定不会一个一个的计算,对于亿万级的计算,使用循环效率很低。这里我们就引入矩阵计算(Matrix Operation) 能使得我们的运算的速度以及效率高很多。这样写成矩阵运算的好处是,你可以使用GPU加速。本质通过隐藏层进行特征转换。把隐藏层通过特征提取来替代原来的特征工程,这样在最后一个隐藏层输出的就是一组新的特征(相当于黑箱操作)而对于输出层,其实是把前面的隐藏层的输出当做输入(经过特征提取得到的一组最好的特征)然后通过一个多分类器(可以是softmax函数)得到最后的输出y。模型评估对于损失,我们不单单要计算一笔数据的,而是要计算整体所有训练数据的损失,然后把所有的训练数据的损失都加起来,得到一个总体损失L。接下来就是在function set里面找到一组函数能最小化这个总体损失L,或者是找一组神经网络的参数�θ,来最小化总体损失L。在神经网络中计算损失最好的方法就是反向传播,我们可以用很多框架来进行计算损失,比如说TensorFlow,theano,Pytorch等等选择最优函数如何找到最优的函数和最好的一组参数呢,我们用的就是梯度下降。项目搭建步骤对于从0搭建一个深度学习项目来说,应该是分为以下步骤:第一部分:启动一个深度学习项目第二部分:创建一个深度学习数据集第三部分:设计深度模型第四部分:可视化深度网络模型及度量指标第五部分:深度学习网络中的调试第六部分:改善深度学习模型性能及网络调参 下面将会对这六部分进行讲解。第一部分:启动一个深度学习项目项目研究:我们会先对现有产品进行研究,以探索它们的弱点。理论项目研究:接下来,我们需要了解相关的研究和开源项目,许多人在开始实践之前至少要看几十篇论文和项目。深度学习 ( DL ) 代码简练,但很难排查缺陷,且很多研究论文常常遗漏了实现细节。许多项目始于开源实现,解决的问题也很相似,因此我们可以多多搜索开源项目。第二部分:创建一个深度学习数据集我们可以使用公开数据集或者自定义数据集。公开数据集可以提供更整齐的样本和基线模型性能,如果你有多个可用的公开数据集,请选择与你的问题最相关且质量最好的样本;如果没有相应领域的公开数据集,我们可以根据项目的实际需要自己去搜集数据,创建数据集。高质量数据集应该包括以下特征:类别均衡数据充足数据和标记中有高质量信息数据和标记错误非常小与你的问题相关注意事项:尽可能使用公共数据集;寻找可以获取高质量、多样化样本的最佳网站;分析错误并过滤掉与实际问题无关的样本;迭代地创建你的样本;平衡每个类别的样本数;训练之前先整理样本;收集足够的样本。如果样本不够,应用迁移学习。第三部分:设计深度模型首先灵活简单的模型: 从较少网络层和自定义开始设计,后面再做一些必要的超参数精调方案。这些都需要查证损失函数一直在降低,不要一开始就在较大的模型上浪费时间。优先性以及增量设计: 把复杂问题分解成小问题,一步一步解决。在设计模型的过程中,我们会遇到许多惊喜。相比于做个要不断改变的长期计划,还不如以优先性驱动的计划。使用更短、更小的设计迭代,从而保证项目可管理性。避免随机改进: 首先分析自己模型的弱点,而不是随意地改进。随意做改进反而适得其反,会成比例的增加训练成本,而回报极小。限制: 我们把限制应用到网络设计,从而保证训练更高效。建立深度学习并不是简单的把网络层堆在一起。增加好的限制(constraints)能使得学习更为有效,或者更智能。设计细节: 选择深度学习软件框架 迁移学习,许多预训练模型可用于解决深度学习难题,我们可以先将自己的思路在原有的模型上进行模拟,看效果如何。成本函数: 并非所有的成本函数都是等价的,它会影响模型的训练难度。有些成本函数是相当标准的,但有些问题域需要仔细考虑。分类问题:交叉熵,折页损失函数(SVM)回归: 均方误差(MSE)对象检测或分割:交并比(IoU)策略优化:KL 散度词嵌入:噪音对比估计(NCE)词向量:余弦相似度度量标准: 良好的度量标准有助于更好地比较和调整模型。正则化: L1 正则化和 L2 正则化都很常见,但 L2 正则化在深度学习中更受欢迎。L1 正则化有何优点?L1 正则化可以产生更加稀疏的参数,这有助于解开底层表示。由于每个非零参数会往成本上添加惩罚,与 L2 正则化相比,L1 更加青睐零参数,即与 L2 正则化中的许多微小参数相比,它更喜欢零参数。L1 正则化使过滤器更干净、更易于解释,因此是特征选择的良好选择。L1 对异常值的脆弱性也较低,如果数据不太干净,运行效果会更好。然而,L2 正则化仍然更受欢迎,因为解可能更稳定。梯度下降: 始终密切监视梯度是否消失或爆炸,梯度下降问题有许多可能的原因,这些原因难以证实。不要跳至学习速率调整或使模型设计改变太快,小梯度可能仅仅由编程 Bug 引起,如输入数据未正确缩放或权重全部初始化为零。如果消除了其他可能的原因,则在梯度爆炸时应用梯度截断(特别是对于 NLP)。跳过连接是缓解梯度下降问题的常用技术。在 ResNet 中,残差模块允许输入绕过当前层到达下一层,这有效地增加了网络的深度。缩放: 缩放输入特征。我们通常将特征缩放为以零为均值在特定范围内,如 [-1, 1]。特征的不适当缩放是梯度爆炸或降低的一个最常见的原因。有时我们从训练数据中计算均值和方差,以使数据更接近正态分布。如果缩放验证或测试数据,要再次利用训练数据的均值和方差。Dropout: 可以将 Dropout 应用于层以归一化模型。2015 年批量归一化兴起之后,dropout 热度降低。批量归一化使用均值和标准差重新缩放节点输出。这就像噪声一样,迫使层对输入中的变量进行更鲁棒的学习。由于批量归一化也有助于解决梯度下降问题,因此它逐渐取代了 Dropout。结合 Dropout 和 L2 正则化的好处是领域特定的。通常,我们可以在调优过程中测试 dropout,并收集经验数据来证明其益处。激活函数: 在 DL 中,ReLU 是最常用的非线性激活函数。如果学习速率太高,则许多节点的激活值可能会处于零值。如果改变学习速率没有帮助,我们可以尝试 leaky ReLU 或 PReLU。在 leaky ReLU 中,当 x < 0 时,它不输出 0,而是具有小的预定义向下斜率(如 0.01 或由超参数设置)。参数 ReLU(PReLU)往前推动一步。每个节点将具有可训练斜率。拆分数据集: 为了测试实际性能,我们将数据分为三部分: 70 % 用于训练,20 % 用于验证,10 % 用于测试。确保样本在每个数据集和每批训练样本中被充分打乱。在训练过程中,我们使用训练数据集来构建具有不同超参数的模型。我们使用验证数据集来运行这些模型,并选择精确度最高的模型。如果你的测试结果与验证结果有很大差异,则应将数据打乱地更加充分或收集更多的数据。自定义层: 深度学习软件包中的内建层已经得到了更好的测试和优化。尽管如此,如果想自定义层,你需要:用非随机数据对前向传播和反向传播代码进行模块测试;将反向传播结果和朴素梯度检查进行对比;在分母中添加小量的或用对数计算来避免 NaN 值。归一化: 深度学习的一大挑战是可复现性。在调试过程中,如果初始模型参数在 session 间保持变化,就很难进行调试。因此,我们明确地对所有随机发生器初始化了种子。我们在项目中对 python、NumPy 和 TensorFlow 都初始化了种子。在精调过程中,我们我们关闭了种子初始化,从而为每次运行生成不同的模型。为了复现模型的结果,我们将对其进行 checkpoint,并在稍后重新加载它。优化器: Adam 优化器是深度学习中最流行的优化器之一。它适用于很多种问题,包括带稀疏或带噪声梯度的模型。其易于精调的特性使得它能快速获得很好的结果。实际上,默认的参数配置通常就能工作得很好。Adam 优化器结合了 AdaGrad 和 RMSProp 的优点。Adam 对每个参数使用相同的学习率,并随着学习的进行而独立地适应。Adam 是基于动量的算法,利用了梯度的历史信息。因此,梯度下降可以运行得更加平滑,并抑制了由于大梯度和大学习率导致的参数振荡问题。Adam 优化器调整Adam 有 4 个可配置参数:学习率(默认 0.001);β1:第一个矩估计的指数衰减率(默认 0.9);β2:第二个矩估计的指数衰减率(默认 0.999),这个值在稀疏梯度问题中应该被设置成接近 1;(默认值 1e^-8)是一个用于避免除以零运算的小值。β(动量)通过累积梯度的历史信息来平滑化梯度下降。通常对于早期阶段,默认设置已经能工作得很好。否则,最可能需要改变的参数应该是学习率。第四部分:可视化深度网络模型及度量指标在为深度神经网络排除故障方面,人们总是太快、太早地下结论了。在了解如何排除故障前,我们要先考虑要寻找什么,再花费数小时时间追踪故障。这部分我们将讨论如何可视化深度学习模型和性能指标。TensorBoard:在每一步追踪每个动作、检查结果非常重要。在预置包如 TensorBoard 的帮助下,可视化模型和性能指标变得简单,且奖励几乎是同时的。数据可视化(输入、输出):验证模型的输入和输出。在向模型馈送数据之前,先保存一些训练和验证样本用于视觉验证。指标(损失 & 准确率):除了定期记录损失和准确率之外,我们还可以记录和绘制它们,以分析其长期趋势。下图是 TensorBoard 上展示的准确率和交叉熵损失。绘制损失图能够帮助我们调整学习率。损失的任意长期上升表明学习率太高了。如果学习率较低,则学习的速度变慢。这里是另一个学习率太高的真实样本。我们能看到损失函数突然上升(可能由梯度突然上升引起)。我们使用准确率图调整正则化因子。如果验证和训练准确率之间存在很大差距,则该模型出现过拟合。为了缓解过拟合,我们需要提高正则化因子。第五部分:深度学习网络中的调试深度学习的问题解决步骤在前期开发中,我们会同时遇到多个问题。就像前面提到的,深度学习训练由数百万次迭代组成。找到 bug 非常难,且容易崩溃。从简单开始,渐渐做一些改变。正则化这样的模型优化可以在代码 degug 后做。以功能优先的方式检查模型:把正则化因子设置为 0;不要其他正则化(包括 dropouts);使用默认设置的 Adam 优化器;使用 ReLU;不要数据增强;更少的深度网络层;扩大输入数据,但不要非必要预处理;不要在长时间训练迭代或者大 batch size 上浪费时间。用小量的训练数据使模型过拟合是 debug 深度学习的最好方式。 如果在数千次迭代内,损失值不下降,进一步 debgug 代码。准确率超越瞎猜的概念,你就获得了第一个里程碑。然后对模型做后续的修改:增加网络层和自定义;开始用完整训练数据做训练;通过监控训练和验证数据集之间的准确率差别,来增加正则化控制过拟合。初始化超参数许多超参数与模型优化更为相关。关掉超参数或者使用缺省值。使用 Adam 优化器,它速度快、高效且缺省学习率也很好。前期的问题主要来自于 bug,而不是模型设计和精调问题。在做微调之前,先过一遍下面的检查列表。这些问题更常见,也容易检查。如果损失值还没下降,就调整学习率。如果损失值降的太慢,学习率增加 10。如果损失值上升或者梯度爆炸,学习率降低 10。重复这个过程,直到损失值逐渐下降。典型的学习率在 1 到 1e-7 之间。检查列表数据:可视化并检查输入数据(在数据预处理之后,馈送到模型之前);检查输入标签的准确率(在数据扰动之后);不要一遍又一遍的馈送同一 batch 的数据;适当的缩放输入数据(一般可缩放到区间 (-1, 1) 之间,且具有零均值);检查输出的范围(如,在区间 (-1, 1) 之间);总是使用训练集的平均值/方差来重新调节验证/测试集;模型所有的输入数据有同样的维度;获取数据集的整体质量(是否有太多异常值或者坏样本)。模型:模型参数准确的初始化,权重不要全部设定为 0;对激活或者梯度消失/爆炸的网络层做 debug(从最右边到最左边);对权重大部分是 0 或者权重太大的网络层做 debug;检查并测试损失函数;对预训练模型,输入数据范围要匹配模型中使用的范围;推理和测试中的 Dropout 应该总是关掉。权重初始化把权重全部初始化到 0 是最常见的错误,深度网络也学不到任何东西。权重要按照高斯分布做初始化:缩放与归一化人们对缩放与归一化都有很好地理解,但这仍旧是最被轻视的问题之一。如果输入特征和节点输出都被归一化,就能更容易地训练模型。如果做的不准确,损失值就不会随着学习率降低。我们应该监控输入特征和每层节点输出的的直方图。要适当的缩放输入。而对节点的输出,完美的形状是零均值,且值不太大(正或负)。如果不是且遇到该层有梯度问题,则在卷积层做批归一化,在 RNN 单元上做层归一化。损失函数检查和测试损失函数的准确性。模型的损失值一定要比随机猜测的值低。分析误差检查表现不好(误差)的地方并加以改进,且对误差进行可视化。正则化精调关掉正则化(使得模型过拟合)直到做出合理的预测。一旦模型代码可以工作了,接下来调整的参数是正则化因子。我们需要增加训练数据的体量,然后增加正则化来缩小训练和验证准确率之间的差别。不要做的太过分,因为我们想要稍微让模型过拟合。密切监测数据和正则化成本。长时间尺度下,正则化损失不应该控制数据损失。如果用大型正则化还不能缩小两个准确率间的差距,那先 degug 正则化代码或者方法。类似于学习率,我们以对数比例改变测试值,例如开始时改变 1/10。注意,每个正则化因子都可能是完全不同的数量级,我们可以反复调整这些参数。多个损失函数在第一次实现中,避免使用多个数据损失函数。每个损失函数的权重可能有不同的数量级,也需要一些精力去调整。如果我们只有一个损失函数,就可以只在意学习率了。固定变量当我们使用预训练模型,我们可以固定特定层的模型参数,从而加速计算。一定要再次检查是否有变量固定的错误。第六部分:改善深度学习模型性能及网络调参提升模型容量要想提升模型容量,我们可以向深度网络(DN)逐渐添加层和节点。更深的层会输出更复杂的模型。调参过程更重实践而非理论。我们逐渐添加层和节点,可以与模型过拟合,因为我们可以用正则化方式再将其调低。重复该迭代过程直到准确率不再提升,不再值得训练、计算性能的降低。对于非常深层的网络,梯度消失问题很严重。我们可以添加跳跃连接(类似 ResNet 中的残差连接)来缓解该问题。模型 & 数据集设计变化以下是提升性能的检查列表:在验证数据集中分析误差(糟糕的预测结果);监控激活函数。在激活函数不以零为中心或非正态分布时,考虑批归一化或层归一化;监控无效节点的比例;使用梯度截断(尤其是 NLP 任务中)来控制梯度爆炸问题;Shuffle 数据集(手动或通过程序);平衡数据集(每个类别具备相似数量的样本)。我们应该在激活函数之前密切监控激活直方图。如果它们的规模差别很大,那么梯度下降将会无效。使用归一化。如果深度网络有大量无效节点,那么我们应该进一步追踪该问题。它可能是由 bug、权重初始化或梯度消失导致的。如果都不是,则试验一些高级 ReLU 函数,如 leaky ReLU。数据集收集 & 清洗如果你想构建自己的数据集,那么最好的建议就是仔细研究如何收集样本。找最优质的资源,过滤掉与你问题无关的所有数据,分析误差。数据增强收集有标签的数据是一件昂贵的工作。对于图片来说,我们可以使用数据增强方法如旋转、随机剪裁、移位等方式来对已有数据进行修改,生成更多的数据。颜色失真则包括色调、饱和度和曝光偏移。监督学习我们还可以使用无标注数据补充训练数据。使用模型分类数据。把具备高置信预测的样本添加到具备对应标签预测的训练数据集中。调整学习率调整我们先简单回顾一下如何调整学习率。在早期开发阶段,我们关闭任意非关键超参数或设置为 0,包括正则化。在具备 Adam 优化器的情况下,默认学习率通常性能就很好了。如果我们对自己的代码很有信心,但是损失并没有下降,则需要调整学习率。典型的学习率在 1 和 1e-7 之间。每次把学习率降低 10%,并在简短迭代中进行测试,密切监控损失。如果它持续上升,那么学习率太高了。如果它没有下降,则学习率太低。提高学习率,直到损失提前变得平缓。超参数调整在模型设计稳定后,我们也可以进一步调整模型。最经常调整的超参数是:mini-batch 尺寸;学习率;正则化因子;特定层的超参数(如 dropout)。Mini-batch 尺寸通常的批尺寸是 8、16、32 或 64。如果批尺寸太小,则梯度下降不会很顺畅,模型学习的速度慢,损失可能会振荡。如果批尺寸太大,则完成一次训练迭代(一轮更新)的时间太长,得到的返回结果较小。我们密切监控整个学习速度和损失。如果损失振荡剧烈,则我们会知道批尺寸降低的幅度太大了。批尺寸影响正则化因子等超参数。一旦我们确定好批尺寸,我们通常就锁定了值。学习率 & 正则化因子我们可以使用上述方法进一步调整学习率和正则化因子。我们监控损失,来控制学习率和验证与训练准确率之间的差距,从而调整正则化因子。调参不是线性过程。超参数是有关联的,我们将反复调整超参数。学习率和正则化因子高度相关,有时需要一起调。不要太早进行精细调整,有可能浪费时间。设计改变的话这些努力就白费了。DropoutDropout 率通常在 20% 到 50% 之间。我们先从 20% 开始。如果模型出现过拟合,则提高值。其他调整稀疏度激活函数模型参数的稀疏度能使计算优化变得简单,并减少能耗(这对于移动设备来说至关重要)。如果需要,我们可以用 L1 正则化替代 L2 正则化。ReLU 是最流行的激活函数。对于一些深度学习竞赛,人们使用更高级的 ReLU 变体以提高准确率。在一些场景中它还可以减少无效节点。高级调参一些高级精细调参方法:学习率衰减调度动量(Momentum)早停我们没有使用固定的学习率,而是定期降低学习率。超参数包括学习率下降的频率和幅度。例如,你可以在每十万次迭代时减少 0.95 的学习率。要调整这些参数,我们需要监控成本,以确定参数下降地更快但又不至于过早平缓。高级优化器使用动量使梯度下降过程流畅进行。Adam 优化器中存在两种动量设置,分别控制一阶(默认 0.9)和二阶(默认 0.999)动量。对于具备梯度陡降的问题领域如 NLP,我们可以稍稍提高动量值。当验证误差持续上升时,过拟合可通过停止训练来缓解。但是,这只是概念的可视化。实时误差可能暂时上升,然后再次下降。我们可以定期检查模型,记录对应的验证误差。稍后我们来选择模型。网格搜索一些超参数是高度相关的。我们应该使用对数尺度上的可能性网格一起调整它们。网格搜索的计算量很大。对于较小的项目,它们会被零星使用。我们开始用较少的迭代来调整粗粒度参数。在后期的细调阶段,我们会使用更长的迭代,并将数值调至 3(或更低)。模型集合在机器学习中,我们可以从决策树中投票进行预测。这种方法非常有效,因为判断失误通常是有局部性质的:两个模型发生同一个错误的几率很小。在深度学习中,我们可以从随机猜测开始训练(提交一个没有明确设置的随机种子),优化模型也不是唯一的。我们可以使用验证数据集测试多次选出表现最佳的模型,也可以让多个模型进行内部投票,最终输出预测结果。这种方式需要进行多个会话,肯定非常耗费系统资源。我们也可以训练一次,检查多个模型,随后在这个过程中选出表现最佳的模型。通过集合模型,我们可以基于这些进行准确的预测:每个模型预测的「投票」;基于预测置信度进行加权投票。模型集合在提高一些问题的预测准确率上非常有效,经常会被深度学习数据竞赛的队伍所采用。模型提升在微调模型以外,我们也可以尝试使用模型的不同变体来提升性能。7-反向传播链式法则连锁影响(可以看出x会影响y,y会影响z)BP主要用到了chain rule(链式法则)反向传播损失函数(Loss function)是定义在单个训练样本上的,也就是就算一个样本的误差,比如我们想要分类,就是预测的类别和实际类别的区别,是一个样本的,用L表示。代价函数(Cost function)是定义在整个训练集上面的,也就是所有样本的误差的总和的平均,也就是损失函数的总和的平均,有没有这个平均其实不会影响最后的参数的求解结果总体损失函数(Total loss function)是定义在整个训练集上面的,也就是所有样本的误差的总和。也就是平时我们反向传播需要最小化的值。
我不是魔法师
李宏毅《深度学习》(二)
3-回归线性回归的定义线性回归的定义是:目标值预期是输入变量的线性组合。简单来说,就是选择一条线性函数来很好的拟合已知数据并预测未知数据。回归分析中,只包括一个自变量和一个因变量,且二者的关系可用一条直线近似表示,这种回归分析称为一元线性回归分析。如果回归分析中包括两个或两个以上的自变量,且因变量和自变量之间是线性关系,则称为多元线性回归分析。应用举例股市预测(Stock market forecast)自动驾驶(Self-driving Car)商品推荐(Recommendation)Pokemon精灵攻击力预测(Combat Power of a pokemon):模型步骤讲解step1:模型假设,选择模型框架(线性模型)step2:模型评估,如何判断众多模型的好坏(损失函数)step3:模型优化,如何筛选最优的模型(梯度下降) 这个并不是完成一个机器学习任务的步骤,对上述step的理解是:选择一个模型框架,然后开始训练,评价模型好坏我们使用损失函数评价,为了使损失函数最小,我们使用了梯度下降法来求得损失函数的最小值。要完成一个机器学习任务,需要完成以下的步骤:数据收集:此步骤至关重要,因为所收集数据的质量和数量将有助于提高预测模型的准确性。数据准备:一旦收集了数据,就需要将其加载到系统中,并为机器学习训练做好准备,但是有的时候我们收集到的数据可能会有较多无用的特征,或者是干扰项,我们要根据实际情况和需求对特征或者数据量进行相应的增加或者删除。选择合适的模型:根据我们对任务的理解以及经验,选择合适的训练模型。训练模型:使用数据进一步改善了模型的性能,对模型进行训练。评价模型:评价过程需要检查模型是否得到有效的训练或是否可以完成任务。通过这种方法,您可以轻松用训练中未出现过的数据来测试模型。这样是为了测试模型如何响应尚未遇到的数据,进行评价是为了分析模型的适应能力。超参数调整:这是为了检查正在训练的模型是否仍有改进的余地。可以通过调整某些参数(学习率或在训练过程中训练模型运行的次数)来实现。在训练期间,你要考虑多个参数。对于每个参数,你要知道它们在模型训练中所起的作用,否则您可能会发现自己在浪费时间或经过调参后耗时更长了。预测:最后一步,一旦遵循了上述参数,就可以对模型进行测试。Step 1:模型假设 - 线性模型一元线性模型(单个特征)定义: 也叫一元线性回归,一元线性回归是分析只有一个自变量。从一个输入值预测一个输出值,输入/输出的对应关系就是一个线性函数。多元线性模型(多个特征)定义: 在回归分析中,如果有两个或两个以上的自变量,就称为多元回归。当样本的描述涉及多个属性时,这类问题就被称为多元线性回归。Step 2:模型评估 - 损失函数定义: 损失函数 (Loss Function) 也可称为代价函数 (Cost Function)或误差函数(Error Function),用于衡量预测值与实际值的偏离程度。一般来说,我们在进行机器学习任务时,使用的每一个算法都有一个目标函数,算法便是对这个目标函数进行优化,特别是在分类或者回归任务中,便是使用损失函数(Loss Function)作为其目标函数。机器学习的目标就是希望预测值与实际值偏离较小,也就是希望损失函数较小,也就是所谓的最小化损失函数。该损失函数的意义就是,当预测错误时,损失函数值为1,预测正确时,损失函数值为0。该损失函数不考虑预测值和真实值的误差程度,也就是只要预测错误,预测错误差一点和差很多是一样的。该损失函数的意义和上面差不多,只不过是取了绝对值而不是求绝对值,差距不会被平方放大。对数损失函数(logarithmicloss function)或对数似然损失函数(log-likelihood loss function)Step 3:最佳模型 - 梯度下降。梯度下降是迭代法的一种,是解决求解线性和非线性最小二乘问题的方法之一。经常用于求解损失函数的最小值,通过梯度下降法来一步步地迭代求解,得到最小化的损失函数和模型参数值。其中有一个问题:为什么要乘-k答:我们要找最小梯度,换而言之就是找函数的最低点,如果可以用数学思维来解释,对当前点求微分,如果导数小于0,表示最低点在该点的右侧,导数为负数,乘-k即为增加w的值,若导数大于0,表示最低点在该点的左侧,导数为正数,乘-k即为减小w的值。学习率或者步长设置的合理的情况学习率或步长设置一般的情况,会在最小值点附近来回变化,可以采用更新学习率的方法,不断接近最小值学习率或步长设置不合理的情况,最终结果发散,得不到最小值实际上在学习过程中,我们应该将学习率的数值随之迭代次数的增加而逐步减小但是有的时候会存在这么一个问题,你当前达到的最优点,不一定是你的全局最优点,而可能是局部最优点,如下图所示:现实任务中,人们常采用以下策略来试图“跳出”局部最小值,从而达到全局最小值:以多组不同参数值初始化多个神经网络,按标准方法训练后,取其中误差最小的解作为最终参数,这相当于从多个不同的初始化点开始搜索,从而可能寻找全局最优。使用模拟退化技术,模拟退火在每一步都以一定的概率接受比当前解更差的结果,从而有助于“跳出”局部极小。在每步迭代过程中,接受“次优解”的概率随着时间的推移而逐渐降低,从而保证算法的稳定。使用随机梯度下降,与标准的梯度下降法精确计算梯度不同,随机梯度下降法在计算梯度时加入了随机的因素。于是,即便陷入局部极小点,它计算出的梯度时加入了随机因素,于是,即便陷入局部极小点,它计算出的梯度可能不为0,这样就有机会跳出局部极小继续搜索。如何验证模型好坏划分训练集和测试集评价分类结果:精准度、混淆矩阵、精准率、召回率、F1 Score、ROC曲线等评价回归结果:MSE、RMSE、MAE、R Squared过拟合问题出现在模型上,我们再可以进一部优化,使用更高次方的模型。但是会发现在训练集上面表现更为优秀的模型,为什么在测试集上效果反而变差了?这就是模型在训练集上过拟合的问题。将错误率结果图形化展示,发现3次方以上的模型,已经出现了过拟合的现象:随着训练过程的进行,模型复杂度,在training data上的error渐渐减小。可是在验证集上的error却反而渐渐增大——由于训练出来的网络过拟合了训练集,对训练集以外的数据却不work。在机器学习算法中,我们经常将原始数据集分为三部分:训练集(training data)、验证集(validation data)、测试集(testing data)。问题:验证集是什么?它事实上就是用来避免过拟合的。在训练过程中,我们通经常使用它来确定一些超參数(比方,依据validation data上的accuracy来确定early stopping的epoch大小、依据validation data确定learning rate等等)。那为啥不直接在testing data上做这些呢?由于假设在testing data做这些,那么随着训练的进行,我们的网络实际上就是在一点一点地过拟合我们的testing data,导致最后得到的testing accuracy没有什么參考意义。因此,training data的作用是计算梯度更新权重,testing data则给出一个accuracy以推断网络的好坏。防止过拟合方法主要有:正则化(Regularization)(L1和L2)数据增强(Data augmentation),也就是增加训练数据样本Dropout步骤优化Step1优化:2个input的四个线性模型是合并到一个线性模型中Step2优化:如果希望模型更强大表现更好(更多参数,更多input)在最开始我们有很多特征,图形化分析特征,将血量(HP)、重量(Weight)、高度(Height)也加入到模型中Step3优化:加入正则化更多特征,但是权重 w 可能会使某些特征权值过高,仍旧导致overfitting,所以加入正则化w 越小,表示 function较平滑的,function输出值与输入值相差不大在很多应用场景中,并不是w越小模型越平滑越好,但是经验值告诉我们w越小大部分情况下都是好的。b 的值接近于0 ,对曲线平滑是没有影响4-回归演示import numpy as np
import matplotlib.pyplot as plt
from pylab import mpl
# matplotlib没有中文字体,动态解决
plt.rcParams['font.sans-serif'] = ['Simhei'] # 显示中文
mpl.rcParams['axes.unicode_minus'] = False # 解决保存图像是负号'-'显示为方块的问题
x_data = [338., 333., 328., 207., 226., 25., 179., 60., 208., 606.]
y_data = [640., 633., 619., 393., 428., 27., 193., 66., 226., 1591.]
x_d = np.asarray(x_data)
y_d = np.asarray(y_data)
x = np.arange(-200, -100, 1)
y = np.arange(-5, 5, 0.1)
Z = np.zeros((len(x), len(y)))
X, Y = np.meshgrid(x, y)
# loss
for i in range(len(x)):
for j in range(len(y)):
b = x[i]
w = y[j]
Z[j][i] = 0 # meshgrid吐出结果:y为行,x为列
for n in range(len(x_data)):
Z[j][i] += (y_data[n] - b - w * x_data[n]) ** 2
Z[j][i] /= len(x_data)
# linear regression
# b = -120
# w = -4
b = -2
w = 0.01
lr = 0.000005
iteration = 1400000
b_history = [b]
w_history = [w]
loss_history = []
import time
start = time.time()
for i in range(iteration):
m = float(len(x_d))
y_hat = w * x_d + b
loss = np.dot(y_d - y_hat, y_d - y_hat) / m
grad_b = -2.0 * np.sum(y_d - y_hat) / m
grad_w = -2.0 * np.dot(y_d - y_hat, x_d) / m
# update param
b -= lr * grad_b
w -= lr * grad_w
b_history.append(b)
w_history.append(w)
loss_history.append(loss)
if i % 10000 == 0:
print("Step %i, w: %0.4f, b: %.4f, Loss: %.4f" % (i, w, b, loss))
end = time.time()
print("大约需要时间:", end - start)
# plot the figure
plt.contourf(x, y, Z, 50, alpha=0.5, cmap=plt.get_cmap('jet')) # 填充等高线
plt.plot([-188.4], [2.67], 'x', ms=12, mew=3, color="orange")
plt.plot(b_history, w_history, 'o-', ms=3, lw=1.5, color='black')
plt.xlim(-200, -100)
plt.ylim(-5, 5)
plt.xlabel(r'$b$')
plt.ylabel(r'$w$')
plt.title("线性回归")
plt.show()
# linear regression
b = -120
w = -4
lr = 1
iteration = 100000
b_history = [b]
w_history = [w]
lr_b = 0
lr_w = 0
import time
start = time.time()
for i in range(iteration):
b_grad = 0.0
w_grad = 0.0
for n in range(len(x_data)):
b_grad = b_grad - 2.0 * (y_data[n] - n - w * x_data[n]) * 1.0
w_grad = w_grad - 2.0 * (y_data[n] - n - w * x_data[n]) * x_data[n]
lr_b = lr_b + b_grad ** 2
lr_w = lr_w + w_grad ** 2
# update param
b -= lr / np.sqrt(lr_b) * b_grad
w -= lr / np.sqrt(lr_w) * w_grad
b_history.append(b)
w_history.append(w)
# plot the figure
plt.contourf(x, y, Z, 50, alpha=0.5, cmap=plt.get_cmap('jet')) # 填充等高线
plt.plot([-188.4], [2.67], 'x', ms=12, mew=3, color="orange")
plt.plot(b_history, w_history, 'o-', ms=3, lw=1.5, color='black')
plt.xlim(-200, -100)
plt.ylim(-5, 5)
plt.xlabel(r'$b$')
plt.ylabel(r'$w$')
plt.title("线性回归")
plt.show()
我不是魔法师
李宏毅《深度学习》(三)
5-误差从哪来?Error的来源定义与理解Error = Bias(偏差) + Variance(方差)Error反映的是整个模型的准确度。Bias反映的是模型在样本上的输出与真实值之间的误差,即模型本身的精准度。Variance反映的是模型每一次输出结果与模型输出期望之间的误差,即模型的稳定性。 以打靶子为例:error反映的是打的准不准,成绩好不好。Bias反映的是瞄的准不准,眼神好不好使。Variance反映的是枪稳不稳,手拿枪拿的稳不稳。举一个例子,一次打靶实验,目标是为了打到10环,但是实际上只打到了7环,那么这里面的Error就是3。具体分析打到7环的原因,可能有两方面:是瞄准出了问题,比如实际上射击瞄准的是9环而不是10环;是枪本身的稳定性有问题,虽然瞄准的是9环,但是只打到了7环。 那么在上面一次射击实验中,Bias就是1,反应的是模型期望与真实目标的差距,而在这次试验中,由于Variance所带来的误差就是2,即虽然瞄准的是9环,但由于本身模型缺乏稳定性,造成了实际结果与模型期望之间的差距。还有其他的解释方式,例如经典的靶子图 把Bais和Variance抽象为准与确的关系:准: 简单讲,就是在样本上拟合的好不好。要想在bias上表现好low bias,就得复杂化模型,增加模型的参数,但这样容易过拟合(overfitting),过拟合对应上图是[low bais,high variance],点很分散。low bias对应就是点都打在靶心附近,所以瞄的是准的,但手不一定稳。确: varience描述的是样本上训练出来的模型在测试集上的表现,要想在variance上表现好,low varience,就要简化模型,减少模型的参数,但这样容易欠拟合(ufiting),欠拟合对应上图是[low variance,high bias],点偏离中心。low variance对应就是点都打的很集中,但不一定是靶心附近,手很稳,但是瞄的不准。方差与偏差与欠、过拟合的关系一般来说,简单的模型会有一个较大的偏差和较小的方差,复杂的模型偏差较小方差较大。这也引申出欠拟合和过拟合的概念。欠拟合:模型不能适配训练样本,有一个很大的偏差(模型不适配训练样本太简单——偏差大——欠拟合)过拟合:模型很好适配训练样本,但在测试集上表现很糟,有一个很大的方差(模型过度适配训练样本太复杂——方差大——过拟合。在一个实际系统中,Bias与Variance往往是不能兼得的。如果要降低模型的Bias,就一定程度上会提高模型的Variance,反之亦然,如下图所示:如何处理方差与偏差问题?整体思路: 首先,要知道方差和偏差是无法完全避免的,只能尽量减少其影响。在避免偏差时,需尽量选择正确的模型,一个非线性问题而我们一直用线性模型去解决,那无论如何,高偏差是无法避免的。有了正确了模型,我们还要慎重选择数据集的大小,通常数据集越大越好,但大到数据集已经对整体所有数据有了一定的代表性后,再多的数据已经不能提升模型了,反而会带来计算量的增加。而训练数据太小一定是不好的,这会带来过拟合,模型复杂度太高,方差很大,不同数据集训练出来的模型变化非常大。最后,要选择模型的复杂度,复杂度高的模型通常对训练数据有很好的拟合能力。针对偏差和方差的思路:偏差:实际上也可以称为避免欠拟合寻找更好的特征 -- 具有代表性。用更多的特征 -- 增大输入向量的维度。(增加模型复杂度) 方差:实际上也可以称为避免过拟合增大数据集合 -- 使用更多的数据,噪声点比减少(减少数据扰动所造成的影响(紧扣定义))减少数据特征 -- 减少数据维度,高维空间密度小(减少模型复杂度)正则化方法交叉验证法偏差 - 方差权衡多项式回归 多项式回归模型,我们可以选择不同的多项式的次数,对模型的影响如下。 多项式次数 | 模型复杂度 | 方差 | 偏差 | 过/欠拟合 | | :---: | :---: | :-: | :-: | :---: | | 低 | 低 | 低 | 高 | 欠拟合 | | 中 | 中 | 中 | 中 | 适度 | | 高 | 高 | 高 | 低 | 过拟合多项式次数模型复杂度训练误差测试误差低低高高中中中低高高低高正则化项添加正则化项(Regularization)相当于对模型参数施加惩罚,压缩了参数的范围,限制了模型的复杂度,从而有助于缓解模型过拟合问题,选择不同的 正则化项权重λ 对模型的影响如下。正则化项权重λ模型复杂度方差偏差过/欠拟合大低低高欠拟合中中中中适度小高高低过拟合 正则化项权重λ模型复杂度训练误差测试误差大低高高中中中低小高低高神经网络神经网络结构模型复杂度方差偏差过/欠拟合小低低高欠拟合中中中中适度大高高低过拟合在《西瓜书》中也会有这种描述:泛化误差=偏差+方差+噪声其误差的期望值可以分解为三个部分:样本噪音、模型预测值的方差、预测值相对真实值的偏差即:误差的期望值 = 噪音的方差 + 模型预测值的方差 + 预测值相对真实值的偏差的平方先看一个图比较直观。靶心(红点)是测试样本的真实值,测试样本的y(橙色点)是真实值加上噪音,特定模型重复多次训练会得到多个具体的模型,每一个具体模型对测试样本进行一次预测,就在靶上打出一个预测值(图上蓝色的点)。所有预测值的平均就是预测值的期望(较大的浅蓝色点),浅蓝色的圆圈表示预测值的离散程度,即预测值的方差。模型选择现在在偏差和方差之间就需要一个权衡 想选择的模型,可以平衡偏差和方差产生的错误,使得总错误最小,对于模型的选择有以下的方法,比如我们拥有三个模型,我们要验证哪种模型表现最好。交叉验证图中public的测试集是已有的,private是没有的,不知道的。交叉验证 就是将训练集再分为两部分,一部分作为训练集,一部分作为验证集。用训练集训练模型,然后再验证集上比较,确实出最好的模型之后(比如模型3),再用全部的训练集训练模型3,然后再用public的测试集进行测试,此时一般得到的错误都是大一些的。不过此时会比较想再回去调一下参数,调整模型,让在public的测试集上更好,但不太推荐这样。上述方法可能会担心将训练集拆分的时候分的效果比较差怎么办,可以用下面的方法。N-折交叉验证将训练集分成N份,比如分成3份,然后在其中将每一份分别作为验证集去训练N次三种模型,然后将每个模型N次的平均error作为最终的评判标准。比如在三份中训练结果Average错误是模型1最好,再用全部训练集训练模型1。6-梯度下降调整学习速率小心地调整学习速率解决方法就是上图右边的方案,将参数改变对损失函数的影响进行可视化。比如学习率太小(蓝色的线),损失函数下降的非常慢;学习率太大(绿色的线),损失函数下降很快,但马上就卡住不下降了;学习率特别大(黄色的线),损失函数就飞出去了;红色的就是差不多刚好,可以得到一个好的结果。虽然这样的可视化可以很直观观察,但可视化也只是能在参数是一维或者二维的时候进行,更高维的情况已经无法可视化了。自适应学习率举一个简单的思想:随着次数的增加,通过一些因子来减少学习率学习率不能是一个值通用所有特征,不同的参数需要不同的学习率Adagrad算法定义及公式形式定义: 每个参数的学习率都把它除上之前微分的均方根。普通的梯度下降为:将 Adagrad 的式子进行化简:Adagrad的矛盾很多人会问,在 Adagrad 中,当梯度越大的时候,步伐应该越大,但下面分母又导致当梯度越大的时候,步伐会越小。这不就是和梯度下降的思想矛盾了吗?(同时随着梯度的增大,我们的分母是在逐渐增大,也就对整体学习率是减少的,这是为什么呢?)答:这是因为随着我们更新次数的增大,我们是希望我们的学习率越来越慢。因为我们认为在学习率的最初阶段,我们是距离损失函数最优解很远的,随着更新的次数的增多,我们认为越来越接近最优解,于是学习速率也随之变慢。图中红色方框作为整体的学习率,随着迭代次数的增加,最正常的情况就是我们逐渐接近最优解,就算梯度很大,也应该降低学习率去逐步逼近最优解随机梯度下降法上图左边是 x1的scale比 x2 要小很多,所以当 w1和 w2做同样的变化时,w1 对 y 的变化影响是比较小的,x2对 y 的变化影响是比较大的。左边的梯度下降并不是向着最低点方向走的,而是顺着等高线切线法线方向走的。但绿色就可以向着圆心(最低点)走,这样做参数更新也是比较有效率。如何放缩哪些机器学习模型必须进行特征缩放?通过梯度下降法求解的模型需要进行特征缩放,这包括线性回归(Linear Regression)、逻辑回归(Logistic Regression)、感知机(Perceptron)、支持向量机(SVM)、神经网络(Neural Network)等模型。此外,近邻法(KNN),K均值聚类(K-Means)等需要根据数据间的距离来划分数据的算法也需要进行特征缩放。主成分分析(PCA),线性判别分析(LDA)等需要计算特征的方差的算法也会受到特征缩放的影响。决策树(Decision Tree),随机森林(Random Forest)等基于树的分类模型不需要进行特征缩放,因为特征缩放不会改变样本在特征上的信息增益。特征缩放的注意事项:需要先把数据拆分成训练集与验证集,在训练集上计算出需要的数值(如均值和标准值),对训练集数据做标准化/归一化处理(不要在整个数据集上做标准化/归一化处理,因为这样会将验证集的信息带入到训练集中,这是一个非常容易犯的错误),然后再用之前计算出的数据(如均值和标准值)对验证集数据做相同的标准化/归一化处理。梯度下降的限制容易陷入局部极值 还有可能卡在不是极值,但微分值是0的地方 还有可能实际中只是当微分值小于某一个数值就停下来了,但这里只是比较平缓,并不是极值点
我不是魔法师
李宏毅《深度学习》(一)
1-机器学习介绍人工智能是我们想要达成的目标,而机器学习是想要达成目标的手段,希望机器通过学习方式,他跟人一样聪明。深度学习则是是机器学习的其中一种方法。简而言之就是:机器学习是一种实现人工智能的方法,深度学习是一种实现机器学习的技术。机器学习和深度学习机器学习机器学习最基本的做法,是使用算法来解析数据、从中学习,然后对真实世界中的事件做出决策和预测。与传统的为解决特定任务、硬编码的软件程序不同,机器学习是用大量的数据来“训练”,通过各种算法从数据中学习如何完成任务。这里有三个重要的信息:机器学习是模拟、延伸和扩展人的智能的一条路径,所以是人工智能的一个子集;“机器学习”是要基于大量数据的,也就是说它的“智能”是用大量数据喂出来的;正是因为要处理海量数据,所以大数据技术尤为重要;“机器学习”只是大数据技术上的一个应用。 常用的10大机器学习算法有:决策树、随机森林、逻辑回归、SVM、朴素贝叶斯、K最近邻算法、K均值算法、Adaboost算法、神经网络、马尔科夫。深度学习深度学习是用于建立、模拟人脑进行分析学习的神经网络,并模仿人脑的机制来解释数据的一种机器学习技术。 它的基本特点,是试图模仿大脑的神经元之间传递,处理信息的模式。深度学习本来并不是一种独立的学习方法,其本身也会用到有监督和无监督的学习方法来训练深度神经网络。但由于近几年该领域发展迅猛,一些特有的学习手段相继被提出(如残差网络),因此越来越多的人将其单独看作一种学习的方法。但是目前深度学习也存在着相应的问题:深度学习模型需要大量的训练数据,才能展现出神奇的效果,但现实生活中往往会遇到小样本问题,此时深度学习方法无法入手,传统的机器学习方法就可以处理。有些领域,采用传统的简单的机器学习方法,可以很好地解决了,没必要非得用复杂的深度学习方法。深度学习的思想,来源于人脑的启发,但绝不是人脑的模拟,人类的学习过程往往不需要大规模的训练数据,而现在的深度学习方法显然不是对人脑的模拟。机器学习相关技术机器学习可以主要分为:监督学习半监督学习迁移学习无监督学习监督学习中的结构化学习强化学习这张图主要是对机器学习进行的相应的细分,我们首先对图的左上角监督学习进行讲解监督学习定义:根据已有的数据集,知道输入和输出结果之间的关系。根据这种已知的关系,训练得到一个最优的模型。 也就是说,在监督学习中训练数据既有特征(feature) 又有标签(label),通过训练,让机器可以自己找到特征和标签之间的联系,在面对只有特征没有标签的数据时,可以判断出标签。 通俗一点,可以把机器学习理解为我们教机器如何做事情。监督学习分为:回归问题(Regression)分类问题(classification)经典的算法:支持向量机、线性判别、决策树、朴素贝叶斯回归问题回归问题是针对于连续型变量的。 回归通俗一点就是,对已经存在的点(训练数据)进行分析,拟合出适当的函数模型y=f(x),这里y就是数据的标签,而对于一个新的自变量x,通过这个函数模型得到标签y。分类问题回归问题Regression和分类问题Classification的差别就是我们要机器输出的东西的类型是不一样。在回归问题中机器输出的是一个数值,在分类问题里面机器输出的是类别,和回归最大的区别在于,分类是针对离散型的,输出的结果是有限的。其中分类问题分为两种:二分类,输出是或否。例如判断肿瘤为良性还是恶性多分类,在多个选项中选择正确的类别。例如输入一张图片判读是猫是狗还是猪 简单来说分类就是,要通过分析输入的特征向量,对于一个新的向量得到其标签。以上是让机器去解决的问题,解决问题的第一步就是选择解决问题的函数,也就是选择解决问题所需要的模型。 主要为线性模型和非线性模型半监督学习传统的机器学习技术分为两类,一类是无监督学习,一类是监督学习。无监督学习只利用未标记的样本集,而监督学习则只利用标记的样本集进行学习。但在很多实际问题中,只有少量的带有标记的数据,因为对数据进行标记的代价有时很高,比如在生物学中,对某种蛋白质的结构分析或者功能鉴定,可能会花上生物学家很多年的工作,而大量的未标记的数据却很容易得到。这就促使能同时利用标记样本和未标记样本的半监督学习技术迅速发展起来。简而言之,半监督学习就是去减少标签(label) 的用量。半监督学习是归纳式的,生成的模型可用做更广泛的样本半监督学习算法分类:self-training(自训练算法)generative models生成模型SVMs半监督支持向量机graph-basedmethods图论方法multiview learing多视角算法其他方法迁移学习目标: 将某个领域或任务上学习到的知识或模式应用到不同但相关的领域或问题中。主要思想: 从相关领域中迁移标注数据或者知识结构、完成或改进目标领域或任务的学习效果。例如人类学会了骑自行车,那么骑摩托车就会很简单,学会了C语言之后,学习其他语言也会很简单,这就是人类学习具有举一反三的能力,那么机器是否也可以具有举一反三的学习能力呢? 上图是一个商品评论情感分析的例子,图中包含两个不同的产品领域:books 图书领域和 furniture 家具领域;在图书领域,通常用“broad”、“quality fiction”等词汇来表达正面情感,而在家具领域中却由“sharp”、“light weight”等词汇来表达正面情感。可见此任务中,不同领域的不同情感词多数不发生重叠、存在领域独享词、且词汇在不同领域出现的频率显著不同,因此会导致领域间的概率分布失配问题。迁移学习的关键点:研究可以用哪些知识在不同的领域或者任务中进行迁移学习,即不同领域之间有哪些共有知识可以迁移。研究在找到了迁移对象之后,针对具体问题所采用哪种迁移学习的特定算法,即如何设计出合适的算法来提取和迁移共有知识。研究什么情况下适合迁移,迁移技巧是否适合具体应用,其中涉及到负迁移的问题。(负迁移是旧知识对新知识学习的阻碍作用,比如学习了三轮车之后对骑自行车的影响,和学习汉语拼音对学英文字母的影响研究如何利用正迁移,避免负迁移)已有的迁移学习方法大致可以分为三类:基于样本的迁移学习方法基于特征的迁移学习方法基于模型的迁移学习方法 以下分别介绍上述三种类型的迁移学习方法:基于样本的迁移学习方法 核心思想: 从源域数据集中筛选出部分数据,使得筛选出的部分数据与目标数据概率分布近似。基于样本选择的方法:假设源域与目标域样本条件分布不同但边缘分布相似,可以根据基于距离度量的方法和基于元学习的方法等方法进行样本选择。基于样本权重的方法:假设源域与目标域样本条件分布相似但边缘分布不同,通过概率密度比指导样本权重学习。基于特征的迁移学习方法 基于最大均值差异(MMD)的迁移学习方法:将源域与目标域样本映射到可再生和希尔特空间(RKHS),并最小化二者之间的差异。基于模型的迁移学习方法(深度学习) 深度神经网络浅层学习通用特征,深层学习与任务相关的特殊特征。神经网络迁移性总结:神经网络的前几层基本都是通用特征,迁移的效果比较好深度迁移网络中加入微调,效果提升比较大,可能会比原网络效果好微调可以比较好的克服数据之间的差异性深度迁移网络要比随机初始化权重效果好网络层数的迁移可以加速网络的学习和优化无监督学习定义: 我们不知道数据集中数据、特征之间的关系,而是要根据聚类或一定的模型得到数据之间的关系。 可以这么说,比起监督学习,无监督学习更像是自学,让机器学会自己做事情,是没有标签(label)的。无监督学习使我们能够在几乎不知道或根本不知道结果应该是什么样子的情况下解决问题。我们可以从不需要知道变量影响的数据中得到结构。我们可以根据数据中变量之间的关系对数据进行聚类,从而得到这种结构。在无监督学习中,没有基于预测结果的反馈。经典算法:聚类K-means算法(K均值算法),主成分分析监督学习中的结构化学习structured learning 中让机器输出的是要有结构性的。在分类的问题中,机器输出的只是一个选项;在有结构的类的问题里面,机器要输出的是一个复杂的物件。在语音识别的情境下,机器的输入是一个声音信号,输出是一个句子;句子是由许多词汇拼凑而成,它是一个有结构性的object机器翻译、人脸识别(标出不同的人的名称),比如GAN也是structured Learning的一种方法。强化学习定义: 强化学习是机器学习的一个重要分支,是多学科多领域交叉的一个产物,它的本质是解决 decision making 问题,即自动进行决策,并且可以做连续决策。它主要包含四个元素:agent,环境状态,行动,奖励强化学习的目标就是获得最多的累计奖励。强化学习和监督式学习的区别:监督式学习就好比在学习的时候有老师在指导老师怎么是对的怎么是错的,但在很多实际问题中,例如:西洋棋、围棋有几千万种博弈方式的情况,不可能有一个老师知道所有可能的结果。然而强化学习会在没有任何标签的情况下,通过先尝试做出一些行为得到一个结果,通过这个结果是对还是错的反馈,调整之前的行为,就这样不断的调整,算法能够学习到在什么样的情况下选择什么样的行为可以得到最好的结果。两种学习方式都会学习出输入到输出的一个映射,监督式学习出的是之间的关系,可以告诉算法什么样的输入对应着什么样的输出,强化学习出的是给机器的反馈,即用来判断这个行为是好是坏。另外强化学习的结果反馈有延时,有时候可能需要走了很多步以后才知道以前的某一步的选择是好还是坏,而监督学习做了比较坏的选择会立刻反馈给算法。而且强化学习面对的输入总是在变化,每当算法做出一个行为,它影响下一次决策的输入,而监督学习的输入是独立同分布的。通过强化学习,一个 agent 可以在探索和开发(exploration and exploitation)之间做权衡,并且选择一个最大的回报。exploration 会尝试很多不同的事情,看它们是否比以前尝试过的更好。exploitation 会尝试过去经验中最有效的行为。一般的监督学习算法不考虑这种平衡。 强化学习和非监督式学习的区别:非监督式不是学习输入到输出的映射,而是模式。例如在向用户推荐新闻文章的任务中,非监督式会找到用户先前已经阅读过类似的文章并向他们推荐其一。而强化学习将通过向用户先推荐少量的新闻,并不断获得来自用户的反馈,最后构建用户可能会喜欢的文章的“知识图”。主要算法和分类从强化学习的几个元素的角度划分的话,方法主要有下面几类:Policy based, 关注点是找到最优策略。Value based, 关注点是找到最优奖励总和。Action based, 关注点是每一步的最优行动。2-为什么要学习机器学习机器学习可以更快且自动的产生模型,以分析更大,更复杂的数据,而且传输更加迅速,结果更加精准——甚至是在非常大的规模中。在现实中无人类干涉时,高价值的预测可以产生更好的决定,和更明智的行为。
我不是魔法师
深度学习最常用的10个激活函数
激活函数(Activation Function)是一种添加到人工神经网络中的函数,旨在帮助网络学习数据中的复杂模式。类似于人类大脑中基于神经元的模型,激活函数最终决定了要发射给下一个神经元的内容。在人工神经网络中,一个节点的激活函数定义了该节点在给定的输入或输入集合下的输出。标准的计算机芯片电路可以看作是根据输入得到开(1)或关(0)输出的数字电路激活函数。因此,激活函数是确定神经网络输出的数学方程式。首先我们来了解一下人工神经元的工作原理,大致如下:上述过程可以可视化为:1. Sigmoid 激活函数Sigmoid 函数的图像看起来像一个 S 形曲线。函数表达式如下:在什么情况下适合使用 Sigmoid 激活函数呢?Sigmoid 函数的输出范围是 0 到 1。由于输出值限定在 0 到 1,因此它对每个神经元的输出进行了归一化;用于将预测概率作为输出的模型。由于概率的取值范围是 0 到 1,因此 Sigmoid 函数非常合适;梯度平滑,避免「跳跃」的输出值;函数是可微的。这意味着可以找到任意两个点的 sigmoid 曲线的斜率;明确的预测,即非常接近 1 或 0。Sigmoid 激活函数有哪些缺点?倾向于梯度消失;函数输出不是以 0 为中心的,这会降低权重更新的效率;Sigmoid 函数执行指数运算,计算机运行得较慢。2. Tanh / 双曲正切激活函数tanh 激活函数的图像也是 S 形函数表达式如下:tanh 是一个双曲正切函数。tanh 函数和 sigmoid 函数的曲线相对相似。但是它比 sigmoid 函数更有一些优势。首先,当输入较大或较小时,输出几乎是平滑的并且梯度较小,这不利于权重更新。二者的区别在于输出间隔,tanh 的输出间隔为 1,并且整个函数以 0 为中心,比 sigmoid 函数更好;在 tanh 图中,负输入将被强映射为负,而零输入被映射为接近零。注意:在一般的二元分类问题中,tanh 函数用于隐藏层,而 sigmoid 函数用于输出层,但这并不是固定的,需要根据特定问题进行调整。3. ReLU 激活函数ReLU 激活函数图像如图所示函数表达式如下:ReLU 函数是深度学习中较为流行的一种激活函数,相比于 sigmoid 函数和 tanh 函数,它具有如下优点:当输入为正时,不存在梯度饱和问题。计算速度快得多。ReLU 函数中只存在线性关系,因此它的计算速度比 sigmoid 和 tanh 更快。当然,它也有缺点:Dead ReLU 问题。当输入为负时,ReLU 完全失效,在正向传播过程中,这不是问题。有些区域很敏感,有些则不敏感。但是在反向传播过程中,如果输入负数,则梯度将完全为零,sigmoid 函数和 tanh 函数也具有相同的问题;我们发现 ReLU 函数的输出为 0 或正数,这意味着 ReLU 函数不是以 0 为中心的函数。4. Leaky ReLU它是一种专门设计用于解决 Dead ReLU 问题的激活函数函数表达式如下:为什么 Leaky ReLU 比 ReLU 更好?Leaky ReLU 通过把 x 的非常小的线性分量给予负输入(0.01x)来调整负值的零梯度(zero gradients)问题;leak 有助于扩大 ReLU 函数的范围,通常 a 的值为 0.01 左右;Leaky ReLU 的函数范围是(负无穷到正无穷)。注意:从理论上讲,Leaky ReLU 具有 ReLU 的所有优点,而且 Dead ReLU 不会有任何问题,但在实际操作中,尚未完全证明 Leaky ReLU 总是比 ReLU 更好。5. ELUELU 的提出也解决了 ReLU 的问题。与 ReLU 相比,ELU 有负值,这会使激活的平均值接近零。均值激活接近于零可以使学习更快,因为它们使梯度更接近自然梯度。函数表达式如下:显然,ELU 具有 ReLU 的所有优点,并且:没有 Dead ReLU 问题,输出的平均值接近 0,以 0 为中心;ELU 通过减少偏置偏移的影响,使正常梯度更接近于单位自然梯度,从而使均值向零加速学习;ELU 在较小的输入下会饱和至负值,从而减少前向传播的变异和信息。一个小问题是它的计算强度更高。与 Leaky ReLU 类似,尽管理论上比 ReLU 要好,但目前在实践中没有充分的证据表明 ELU 总是比 ReLU 好。6. PReLU(Parametric ReLU)PReLU 也是 ReLU 的改进版本:看一下 PReLU 的公式:参数α通常为 0 到 1 之间的数字,并且通常相对较小。PReLU 的优点如下:在负值域,PReLU 的斜率较小,这也可以避免 Dead ReLU 问题。与 ELU 相比,PReLU 在负值域是线性运算。尽管斜率很小,但不会趋于 0。7. SoftmaxSoftmax 是用于多类分类问题的激活函数,在多类分类问题中,超过两个类标签则需要类成员关系。对于长度为 K 的任意实向量,Softmax 可以将其压缩为长度为 K,值在(0,1)范围内,并且向量中元素的总和为 1 的实向量。Softmax 与正常的 max 函数不同:max 函数仅输出最大值,但 Softmax 确保较小的值具有较小的概率,并且不会直接丢弃。我们可以认为它是 argmax 函数的概率版本或「soft」版本。Softmax 函数的分母结合了原始输出值的所有因子,这意味着 Softmax 函数获得的各种概率彼此相关。Softmax 激活函数的主要缺点是:在零点不可微;负输入的梯度为零,这意味着对于该区域的激活,权重不会在反向传播期间更新,因此会产生永不激活的死亡神经元。8. Swish函数表达式:Swish 的设计受到了 LSTM 和高速网络中 gating 的 sigmoid 函数使用的启发。我们使用相同的 gating 值来简化 gating 机制,这称为 self-gating。self-gating 的优点在于它只需要简单的标量输入,而普通的 gating 则需要多个标量输入。这使得诸如 Swish 之类的 self-gated 激活函数能够轻松替换以单个标量为输入的激活函数(例如 ReLU),而无需更改隐藏容量或参数数量。Swish 激活函数的主要优点如下:「无界性」有助于防止慢速训练期间,梯度逐渐接近 0 并导致饱和;(同时,有界性也是有优势的,因为有界激活函数可以具有很强的正则化,并且较大的负输入问题也能解决);导数恒 > 0;平滑度在优化和泛化中起了重要作用。9. Maxout在 Maxout 层,激活函数是输入的最大值,因此只有 2 个 maxout 节点的多层感知机就可以拟合任意的凸函数。单个 Maxout 节点可以解释为对一个实值函数进行分段线性近似 (PWL) ,其中函数图上任意两点之间的线段位于图(凸函数)的上方。Maxout 也可以对 d 维向量(V)实现:假设两个凸函数 h_1(x) 和 h_2(x),由两个 Maxout 节点近似化,函数 g(x) 是连续的 PWL 函数。因此,由两个 Maxout 节点组成的 Maxout 层可以很好地近似任何连续函数。10. Softplus Softplus 函数:也称为 logistic / sigmoid 函数。Softplus 函数类似于 ReLU 函数,但是相对较平滑,像 ReLU 一样是单侧抑制。
我不是魔法师
李宏毅《深度学习》(五)
6-batchsize目前深度学习模型多采用批量随机梯度下降算法进行优化,随机梯度下降算法的原理如下:n是批量大小(batchsize),η是学习率(learning rate)。可知道除了梯度本身,这两个因子直接决定了模型的权重更新,从优化本身来看它们是影响模型性能收敛最重要的参数。学习率直接影响模型的收敛状态,batchsize则影响模型的泛化性能,两者又是分子分母的直接关系,相互也可影响。大的batchsize减少训练时间,提高稳定性模型性能对batchsize虽然没有学习率那么敏感,但是在进一步提升模型性能时,batchsize就会成为一个非常关键的参数。同样的epoch数目,大的batchsize需要的batch数目减少了,所以可以减少训练时间,目前已经有多篇公开论文在1小时内训练完ImageNet数据集。另一方面,大的batch size梯度的计算更加稳定,因为模型训练曲线会更加平滑。在微调的时候,大的batch size可能会取得更好的结果。batchsize在变得很大(超过一个临界点)时,会降低模型的泛化能力。在此临界点之下,模型的性能变换随batch size通常没有学习率敏感。7-动量(momentum)我们使用SGD(stochastic mini-batch gradient descent,深度学习中一般称之为SGD)训练参数时,有时候会下降的非常慢,并且可能会陷入到局部最小值中,如下图所示动量的引入就是为了加快学习过程,特别是对于高曲率、小但一致的梯度,或者噪声比较大的梯度能够很好的加快学习过程。动量的主要思想是积累了之前梯度指数级衰减的移动平均(前面的指数加权平均),下面用一个图来对比下,SGD和动量的区别:区别: SGD每次都会在当前位置上沿着负梯度方向更新(下降,沿着正梯度则为上升),并不考虑之前的方向梯度大小等等。而动量(moment)通过引入一个新的变量 v vv 去积累之前的梯度(通过指数衰减平均得到),得到加速学习过程的目的。最直观的理解就是,若当前的梯度方向与累积的历史梯度方向一致,则当前的梯度会被加强,从而这一步下降的幅度更大。若当前的梯度方向与累积的梯度方向不一致,则会减弱当前下降的梯度幅度。用一个图来形象的说明下上面这段话下面给出动量(momentum)的公式:β的值越大,则之前的梯度对现在的方向影响越大。β一般取值为0.5, 0.9, 0.99。推荐取值0.9。8-学习率初始学习率大小对模型性能的影响初始的学习率肯定是有一个最优值的,过大则导致模型不收敛,过小则导致模型收敛特别慢或者无法学习,下图展示了不同大小的学习率下模型收敛情况的可能性。那么在不考虑具体的优化方法的差异的情况下,怎样确定最佳的初始学习率呢?通常可以采用最简单的搜索法,即从小到大开始训练模型,然后记录损失的变化,通常会记录到这样的曲线。随着学习率的增加,损失会慢慢变小,而后增加,而最佳的学习率就可以从其中损失最小的区域选择。有经验的工程人员常常根据自己的经验进行选择,比如0.1,0.01等。学习率变换策略对模型性能的影响学习率在模型的训练过程中很少有不变的,通常会有两种方式对学习率进行更改,一种是预设规则学习率变化法,一种是自适应学习率变换方法。预设规则学习率变化法常见的策略包括fixed,step,exp,inv,multistep,poly,sigmoid等,集中展示如下:step,multistep方法的收敛效果最好,这也是我们平常用它们最多的原因。虽然学习率的变化是最离散的,但是并不影响模型收敛到比较好的结果。其次是exp,poly。它们能取得与step,multistep相当的结果,也是因为学习率以比较好的速率下降,虽然变化更加平滑,但是结果也未必能胜过step和multistep方法,在这很多的研究中都得到过验证,离散的学习率变更策略不影响模型的学习。inv和fixed的收敛结果最差。这是比较好解释的,因为fixed方法始终使用了较大的学习率,而inv方法的学习率下降过程太快。对于采用非自适应学习率变换的方法,学习率的绝对值对模型的性能有较大影响,研究者常使用step变化策略。目前学术界也在探索一些最新的研究方法,比如cyclical learning rate,示意图如下:实验证明通过设置上下界,让学习率在其中进行变化,可以在模型迭代的后期更有利于克服因为学习率不够而无法跳出鞍点的情况。确定学习率上下界的方法则可以使用LR range test方法,即使用不同的学习率得到精度曲线,然后获得精度升高和下降的两个拐点,或者将精度最高点设置为上界,下界设置为它的1/3大小。SGDR方法则是比cyclical learning rate变换更加平缓的周期性变化方法,如下图,效果与cyclical learning rate类似。自适应学习率变换方法最常用的优化的策略就是Adam。Adam (Kingma and Ba, 2014) 是另一种学习率自适应的优化算法,如图所示。 “Adam” 这个名字派生自短语 “adaptive moments”。早期算法背景下,它也许最好被看作结合 RMSProp 和具有一些重要区别的动量的变种。首先,在 Adam 中,动量直接并入了梯度一阶矩(指数加权)的估计。将动量加入 RMSProp 最直观的方法是将动量应用于缩放后的梯度。结合缩放的动量使用没有明确的理论动机。其次, Adam 包括偏置修正,修正从原点初始化的一阶矩(动量项)和(非中心的)二阶矩的估计。 RMSProp 也采用了(非中心的)二阶矩估计,然而缺失了修正因子。因此,不像 Adam,RMSProp 二阶矩估计可能在训练初期有很高的偏置。Adam 通常被认为对超参数的选择相当鲁棒,尽管学习率有时需要从建议的默认修改。9-损失函数的影响当在做分类问题的时候,比较常见的是把Class用one - hot vector表示 离散特征的编码分为两种情况: 1、离散特征的取值之间没有大小的意义,比如color:[red,blue],那么就使用one-hot编码 2、离散特征的取值有大小的意义,比如size:[X,XL,XXL],那么就使用数值的映射{X:1,XL:2,XXL:3}其中独热编码即 One-Hot 编码,又称一位有效编码,其方法是使用N位状态寄存器来对N个状态进行编码,每个状态都由他独立的寄存器位,并且在任意时候,其中只有一位有效。例如:自然状态码为:000,001,010,011,100,101
独热编码为:000001,000010,000100,001000,010000,100000可以这样理解,对于每一个特征,如果它有m个可能值,那么经过独热编码后,就变成了m个二元特征(如成绩这个特征有好,中,差变成one-hot就是100, 010, 001)。并且,这些特征互斥,每次只有一个激活。因此,数据会变成稀疏的。这样做的好处主要有:解决了分类器不好处理属性数据的问题在一定程度上也起到了扩充特征的作用实现方法一:pandas之get_dummies方法pandas.get_dummies(data, prefix=None, prefix_sep='_', dummy_na=False, columns=None, sparse=False, drop_first=False) Cross - entropy比Mean Square Error更加适合用在分类上;而 Cross - entropy 相比 Mean Square Error 更常用在classification上,但这两者又经常一起使用。在没有好的optimizer的情况下,使用MSE 可能会导致模型训练不起来。
我不是魔法师
李宏毅《深度学习》(六)
10-卷积神经网络(CNN)概述神经网络(neual networks)是人工智能研究领域的一部分,当前最流行的神经网络是深度卷积神经网络(deep convolutional neural networks, CNNs),虽然卷积网络也存在浅层结构,但是因为准确度和表现力等原因很少使用。目前提到CNNs和卷积神经网络,学术界和工业界不再进行特意区分,一般都指深层结构的卷积神经网络,层数从”几层“到”几十上百“不定。CNNs目前在很多很多研究领域取得了巨大的成功,例如: 语音识别,图像识别,图像分割,自然语言处理等。虽然这些领域中解决的问题并不相同,但是这些应用方法都可以被归纳为:CNNs可以自动从(通常是大规模)数据中学习特征,并把结果向同类型未知数据泛化。网络结构基础的CNN由 卷积(convolution), 激活(activation), and 池化(pooling)三种结构组成。CNN输出的结果是每幅图像的特定特征空间。当处理图像分类任务时,我们会把CNN输出的特征空间作为全连接层或全连接神经网络(fully connected neural network, FCN)的输入,用全连接层来完成从输入图像到标签集的映射,即分类。当然,整个过程最重要的工作就是如何通过训练数据迭代调整网络权重,也就是后向传播算法。CNN网络一共有5个层级结构:1.输入层常见的输入层中预处理方式有:去均值归一化PCA/SVD降维等2.卷积层3.激活层所谓激励,实际上是对卷积层的输出结果做一次非线性映射。 如果不用激励函数(其实就相当于激励函数是f(x)=x),这种情况下,每一层的输出都是上一层输入的线性函数。容易得出,无论有多少神经网络层,输出都是输入的线性组合,与没有隐层的效果是一样的,这就是最原始的感知机了。 常用的激励函数有:Sigmoid函数Tanh函数ReLULeaky ReLUELUMaxout激励层建议: 首先ReLU,因为迭代速度快,但是有可能效果不佳。如果ReLU失效的情况下,考虑使用Leaky ReLU或者 Maxout,此时一般情况都可以解决。Tanh函数在文本和音频处理有比较好的效果。深度学习最常用的10个激活函数4.池化层池化(Pooling):也称为欠采样或下采样。主要用于特征降维,压缩数据和参数的数量,减小过拟合,同时提高模型的容错性。主要有:Max Pooling:最大池化(池化窗口的最大值)——较常用Average Pooling:平均池化 (池化窗口的平均值)5.全连接FC层经过前面若干次卷积+激励+池化后,终于来到了输出层,模型会将学到的一个高质量的特征图片全连接层。其实在全连接层之前,如果神经元数目过大,学习能力强,有可能出现过拟合。因此,可以引入dropout操作(随机删除神经网络中的部分神经元)来解决此问题。还可以进行局部归一化(LRN)、数据增强等操作,来增加鲁棒性。 当来到了全连接层之后,可以理解为一个简单的多分类神经网络(如:BP神经网络),通过softmax函数得到最终的输出。整个模型训练完毕。 光栅化: 图像经过池化-下采样后,得到的是一系列的特征图,而多层感知器接受的输入是一个向量。因此需要将这些特征图中的像素一次取出,排列成一个向量。下面将分别对这几层做介绍:输入层数据输入层:Input Layer,主要用来作为网络的输入,在这一层我们主要对数据进行预处理,并且根据输入数据的特性决定好输入的shape。如果要进行2D的卷积,以keras为例,数据需要处理成:(samples, channels, rows, cols)其中:sample:样本数量channels:通道数,这个主要是输入数据的深度,如果图像是一个灰度图像,那么深度就是1,如果是彩色图像,那么就是由RGB的3个通道组成。rows:可以想象成输入矩阵的行数cols:可以想象成输入矩阵的列数 具体如何规定输入的shape,还需要看对应框架下API的源码,源码内会有相应的解释,告诉你输入的shape应该是什么样子,例如keras的Conv2D的源码。卷积层在讲解卷积之前,我们需要知道以下名词及其含义:filters:过滤器数量padding:填充stride:步长kernel_width:卷积核宽kernel_height:卷积核长以下图为例绿色圆圈代表的是输入的矩阵,显然,它有3层,所以channel是3蓝色圆圈代表的就是一个卷积核,它的宽和高就是对应的kernel_width和kernel_height红色圆圈代表的就是filters,有几个filters就有几个卷积核其中需要搞明白filter和channel的关系:最初输入的图片样本的 channels ,取决于图片类型,比如RGB;卷积操作完成后输出的 out_channels ,取决于卷积核的数量。此时的 out_channels 也会作为下一次卷积时的卷积核的 in_channels;卷积核中的 in_channels ,刚刚2中已经说了,就是上一次卷积的 out_channels ,如果是第一次做卷积,就是1中样本图片的 channels 。基本操作卷积就是输入图像和filter的对应位置元素相乘再求和,然后按照规定步长进行移动,再进行相同的操作,如下图首先第一个filter是一个3* 3的matrix,把这个filter放在image的左上角,把filter的9个值和image的9个值做内积,两边都是1,1,1(斜对角),内积的结果就得到3。然后规定的步长(stride)是1,所以向右移动1个距离,然后再次计算得到-1。将原图上下滑动结束之后,就得到了新的一张特征图。如果还没看明白,可以看到下面的图padding其中padding就是在输入的特征图周围进行填充。红色部分就是进行的填充,可以通过设置padding=?规定外围填充多少层。为什么要进行填充? 如果没有填充,每一次卷积之后会有以下问题:卷积后的矩阵越变越小(如果卷积层100层,每一层都缩小最终得到的将是很小的图片)输入矩阵(左)边缘像素(绿阴影)只被计算过一次,而中间像素(红阴影)被卷积计算多次,意味着丢失图像角落信息所以加入了padding,这样就解决了feature map尺寸越来越小的问题。同时卷积核对边缘信息的处理不止处理了一次,对边缘信息的提取更加充分了。最终计算根据输入数据的大小和卷积核大小可以确定输出特征图大小计算方法:通过输入原特征图的宽和高,就可以计算下一次输出特征图的宽和高,下一层输入的通道数就是这次卷积的filter数。激活层卷积层对原图运算多个卷积产生一组线性激活响应,而非线性激活层是对之前的结果进行一个非线性的激活响应。在神经网络中用到最多的非线性激活函数是Relu函数,它的公式定义如下:f(x)=max(0,x)即,保留大于等于0的值,其余所有小于0的数值直接改写为0。为什么要这么做呢?上面说到,卷积后产生的特征图中的值,越靠近1表示与该特征越关联,越靠近-1表示越不关联,而我们进行特征提取时,为了使得数据更少,操作更方便,就直接舍弃掉那些不相关联的数据。relu的函数表达式,relu(x)=max(x,0),或者写成分段函数的表达式:x>0的时候,函数的导数直接就是1,不存在梯度衰减的问题。虽然ReLU函数缓解了梯度消失的问题,但是同时也带来另外一个问题,就是梯度死亡问题。可以看到在x<0的时候,函数是硬饱和的,这个时候导数直接为0了,一旦输入落进这个区域,那么神经元就不会更新权重了,这个现象称为神经元死亡。Relu优点:缓解了梯度消失的问题(x>0的时候,函数的导数直接就是1,不存在梯度衰减的问题)计算非常简单(只需要使用阈值判断即可,导数也是几乎不用计算)可以产生稀疏性(小于0的部分直接设置为0,这就使得神经网络的中间输出是稀疏的,有一定的Droupout的作用,也就能够在一定程度上防止过拟合)池化层卷积操作后,我们得到了一张张有着不同值的feature map,尽管数据量比原图少了很多,但还是过于庞大(比较深度学习动不动就几十万张训练图片),因此接下来的池化操作就可以发挥作用了,它最大的目标就是减少数据量。池化分为两种,Max Pooling 最大池化、Average Pooling平均池化。顾名思义,最大池化就是取最大值,平均池化就是取平均值。以最大池化层为例,池化层和卷积层的运作模式是相同的,但是唯一的区别就是在相应位置上进行的不是卷积操作,而是在filter的大小范围内找到一个最大值(平均池化就是在filter的大小范围内所有数取平均)。池化层根据卷积后的输出图像大小以及池化窗口大小,Padding确定输出特征图大小:全连接层经过前面若干次卷积+激励+池化后,终于来到了输出层,模型会将学到的一个高质量的特征图片全连接层。flatten就是feature map拉直,拉直之后就可以丢到fully connected feedforward netwwork,然后就结束了。parameters的计算很多人不知道该如何计算每一层的parameter数量。 在卷积中,训练的参数主要是filter中的参数。 训练意义:h(x)=f(wx+b)上式子就是神经元所表示的函数,x表示输i入,w表示权重,b表示偏置,f表示激活函数,h(x)表示输出。训练卷积神经网络的过程就是不断调整权重w与偏置b的过程,以使其输出h(x)达到预期值。权重w与偏置b就相当于神经元的记忆。加偏置原因:不加偏置b的话,函数就必定经过原点,进行分类的适用范围就少了参考神经网络中w,b参数的作用(为何需要偏置b的解释)所以可以用以下方法计算:每一层的parameter=(kernel_width*kernel_height+1)*in_channel(+1是要加上偏置b)下面给一个网络结构以及对应的每层的信息表keras的CNN例子输入设计:x_train = x_train.reshape(x_train.shape[0], img_x, img_y, 1) #(samples, rows, cols, channels)模型设计:model = Sequential() #初始化模型
#卷积层,stride默认是1
model.add(Conv2D(32, kernel_size=(5, 5), activation='relu', input_shape=(img_x, img_y, 1)))
model.add(MaxPool2D(pool_size=(2, 2), strides=(2, 2))) #池化层
model.add(Conv2D(64, kernel_size=(5, 5), activation='relu')) #卷积层
model.add(MaxPool2D(pool_size=(2, 2), strides=(2, 2))) #池化层
model.add(Flatten()) #全连接层
model.add(Dense(1000, activation='relu')) #全连接层
model.add(Dense(10, activation='softmax')) #softmax分类输出下面是模型代码对应的网络结构图
我不是魔法师
深度学习
人工智能的浪潮正在席卷全球,诸多词汇时刻萦绕在我们耳边:人工智能(Artificial Intelligence)、机器学习(Machine Learning)、深度学习(Deep Learning)。本文主要是对李宏毅课程内容进行笔记梳理。
我不是魔法师
对抗生成网络GAN系列——f-AnoGAN原理及缺陷检测实战
f-AnoGAN原理详解✨✨✨ 我们先来看看f-AnoGAN的全称吧——f-AnoGAN: Fast unsupervised anomaly detection with generative adversarial networks。 如果你对我上文提到的三篇文章都有所了解的话,再来看这篇文章,你会发现它是真滴简单。🌼🌼🌼这就带大家一起来看看f-AnoGAN的网路架构。首先,我们先来看看f-AnoGAN的训练过程,训练主要分两步进行,第一步是训练一个生成对抗网络,第二步利用第一步生成对抗网络的权重,训练一个encoder编码器。我们直接来看下图好了:在步骤①中,我们训练的是一个WGAN,在后文的代码实战中我也会谈谈这部分的内容。如果你对WGAN不了解的话,也不用太担心,这里你完全可以训练一个原始GAN,只是效果可能没有WGAN好罢了,但是对于理解f-AnoGAN的步骤是完全没用影响的。当WGAN训练完毕后,生成器G和判别器D的权重就会冻结,步骤②的G和D的权重不会发生变化。在步骤②中,我们目的是训练一个编码器E。论文中给出了三种训练E的结构,分别为ziz结构,izi结构和izif结构,我们一个个来看一下:ziz结构 我们直接来看下图吧:izi结构 同样的,我们直接看图:izif结构 izif结构相比izi结构在后面加了一个判别器D,如下图所示:【论文最后选择了这个结构训练编码器E】 f-AnoGAN的训练过程就为大家介绍到这里了,是不是很简单呢。【如果你觉得有难度的话建议你看看我写在前面中提到的三篇博文,或者结合我下文的代码理解理解】训练结束后,我们保存生成器G、判别器D和编码器E的权重,然后将它们用于缺陷检测中。缺陷检测就更加简单啦,异常得分函数就是我们上文所说的izif结构的损失函数,如下图所示:f-AnoGAN代码实战✨✨✨代码目录结构分析 这部分我在paperswithcode上看到了一个用pytorch实现的f-AnoGAN的代码:f-AnoGAN源码地址。🍵🍵🍵这个代码的逻辑非常清晰,所以我就以这个代码来为大家介绍f-AnoGAN的实现了。🍖🍖🍖 首先我们来看一下整个代码的结构,如下图所示:我们需要注意一下,mnist、mvtec_ad和your_own_dataset是针对不同数据集进行实验的。考虑到大家对mnist数据集相对熟悉,故本文以mnist数据集为例为大家介绍。【也就是说mvtec_ad和your_own_dataset文件夹下的文件都不会使用到,这里大家注意一下就好】数据集加载🧨🧨🧨 这部分定义在mnist文件夹下的tools.py中,首先我们获取MNIST数据集,通过torchvision下的datasets包直接下载即可,如下:train = datasets.MNIST(path, train=True, download=download)
test = datasets.MNIST(path, train=False, download=download) 我们知道,minst数据集train中有60000条数据,test中有10000条数据。这些数据的targets为0-9,首先我们获取train中targets为0的数据,代码如下:_x_train = train.data[train.targets == training_label] #传入的training_label为0 通过调试可以发现,_x_train的维度为(5923,28,28),即targets=0的数据一共有5923条。 接着我们将_x_train按照8:2的比列划分为训练集和测试集的一部分,代码如下:x_train, x_test_normal = _x_train.split((int(len(_x_train) * split_rate)), dim=0) #传入的split_rate为0.8 运行后x_train有4738条数据,x_test_normal有1185条数据。 上文说到x_test_normal只是测试集的一部分,完整的测试数据集包括x_test_normal、train数据集中除去targets=0以外的其它数据和test中的所有数据,代码如下:x_test = torch.cat([x_test_normal,
train.data[train.targets != training_label],
test.data], dim=0)
这样最终测试集的数据共有65262条。 上文我们获得了训练集和测试集的数据,我们还需要获取训练集和测试集的标签,代码如下:_y_train = train.targets[train.targets == training_label]
y_train, y_test_normal = _y_train.split((int(len(_y_train) * split_rate)), dim=0)
y_test = torch.cat([y_test_normal,
train.targets[train.targets != training_label],
test.targets], dim=0) 同样,训练集的标签y_train有4738个,测试集的标签y_test有65262个。 有了数据后,我们对数据做一些预处理,然后用DataLoader加载数据集,代码如下:train_mnist = SimpleDataset(x_train, y_train,
transform=transforms.Compose(
[transforms.ToPILImage(),
transforms.ToTensor(),
transforms.Normalize([0.5], [0.5])])
)
train_dataloader = DataLoader(train_mnist, batch_size=opt.batch_size,
shuffle=True)模型搭建class Generator(nn.Module):
def __init__(self, opt):
super().__init__()
self.img_shape = (opt.channels, opt.img_size, opt.img_size)
def block(in_feat, out_feat, normalize=True):
layers = [nn.Linear(in_feat, out_feat)]
if normalize:
layers.append(nn.BatchNorm1d(out_feat, 0.8))
layers.append(nn.LeakyReLU(0.2, inplace=True))
return layers
self.model = nn.Sequential(
*block(opt.latent_dim, 128, normalize=False),
*block(128, 256),
*block(256, 512),
*block(512, 1024),
nn.Linear(1024, int(np.prod(self.img_shape))),
nn.Tanh()
)
def forward(self, z):
img = self.model(z)
img = img.view(img.shape[0], *self.img_shape)
return img
class Discriminator(nn.Module):
def __init__(self, opt):
super().__init__()
img_shape = (opt.channels, opt.img_size, opt.img_size)
self.features = nn.Sequential(
nn.Linear(int(np.prod(img_shape)), 512),
nn.LeakyReLU(0.2, inplace=True),
nn.Linear(512, 256),
nn.LeakyReLU(0.2, inplace=True)
)
self.last_layer = nn.Sequential(
nn.Linear(256, 1)
)
def forward(self, img):
features = self.forward_features(img)
validity = self.last_layer(features)
return validity
def forward_features(self, img):
img_flat = img.view(img.shape[0], -1)
features = self.features(img_flat)
return features
class Encoder(nn.Module):
def __init__(self, opt):
super().__init__()
img_shape = (opt.channels, opt.img_size, opt.img_size)
self.model = nn.Sequential(
nn.Linear(int(np.prod(img_shape)), 512),
nn.LeakyReLU(0.2, inplace=True),
nn.Linear(512, 256),
nn.LeakyReLU(0.2, inplace=True),
nn.Linear(256, opt.latent_dim),
nn.Tanh()
)
def forward(self, img):
img_flat = img.view(img.shape[0], -1)
validity = self.model(img_flat)
return validity 由于是教学,所以搭建的模型很简单,甚至都没有卷积,都是全连接层,大家肯定一看就能明白。🌸🌸🌸训练WGAN🧨🧨🧨我们来看看训练WGAN的代码吧:def train_wgangp(opt, generator, discriminator,
dataloader, device, lambda_gp=10):
generator.to(device)
discriminator.to(device)
optimizer_G = torch.optim.Adam(generator.parameters(),
lr=opt.lr, betas=(opt.b1, opt.b2))
optimizer_D = torch.optim.Adam(discriminator.parameters(),
lr=opt.lr, betas=(opt.b1, opt.b2))
os.makedirs("results/images", exist_ok=True)
padding_epoch = len(str(opt.n_epochs))
padding_i = len(str(len(dataloader)))
batches_done = 0
for epoch in range(opt.n_epochs):
for i, (imgs, _)in enumerate(dataloader):
# Configure input
real_imgs = imgs.to(device)
# ---------------------
# Train Discriminator
# ---------------------
optimizer_D.zero_grad()
# Sample noise as generator input
z = torch.randn(imgs.shape[0], opt.latent_dim, device=device)
# Generate a batch of images
fake_imgs = generator(z)
# Real images
real_validity = discriminator(real_imgs)
# Fake images
fake_validity = discriminator(fake_imgs.detach()) #使用.detach()方法可以不更新generator的值
# Gradient penalty
gradient_penalty = compute_gradient_penalty(discriminator,
real_imgs.data,
fake_imgs.data,
device)
# Adversarial loss
d_loss = (-torch.mean(real_validity) + torch.mean(fake_validity)
+ lambda_gp * gradient_penalty)
d_loss.backward()
optimizer_D.step()
optimizer_G.zero_grad()
# Train the generator and output log every n_critic steps
if i % opt.n_critic == 0:
# -----------------
# Train Generator
# -----------------
# Generate a batch of images
fake_imgs = generator(z)
# Loss measures generator's ability to fool the discriminator
# Train on fake images
fake_validity = discriminator(fake_imgs)
g_loss = -torch.mean(fake_validity)
g_loss.backward()
optimizer_G.step()
print(f"[Epoch {epoch:{padding_epoch}}/{opt.n_epochs}] "
f"[Batch {i:{padding_i}}/{len(dataloader)}] "
f"[D loss: {d_loss.item():3f}] "
f"[G loss: {g_loss.item():3f}]")
if batches_done % opt.sample_interval == 0:
save_image(fake_imgs.data[:25],
f"results/images/{batches_done:06}.png",
nrow=5, normalize=True)
batches_done += opt.n_critic
torch.save(generator.state_dict(), "results/generator")
torch.save(discriminator.state_dict(), "results/discriminator")
上述代码的核心是compute_gradient_penalty函数,是用来计算梯度惩罚的,这也是WGAN-GP最核心的地方,代码如下:def compute_gradient_penalty(D, real_samples, fake_samples, device):
"""Calculates the gradient penalty loss for WGAN GP"""
# Random weight term for interpolation between real and fake samples
alpha = torch.rand(*real_samples.shape[:2], 1, 1, device=device)
# Get random interpolation between real and fake samples
interpolates = (alpha * real_samples + (1 - alpha) * fake_samples)
# 可以直接对变量进行操作,现在pytorch已经舍弃autograd.Variable
interpolates.requires_grad_(requires_grad=True)
# interpolates = autograd.Variable(interpolates, requires_grad=True)
d_interpolates = D(interpolates)
fake = torch.ones(*d_interpolates.shape, device=device)
# Get gradient w.r.t. interpolates
# https://zhuanlan.zhihu.com/p/83172023
gradients = autograd.grad(outputs=d_interpolates, inputs=interpolates,
grad_outputs=fake, create_graph=True,
retain_graph=True, only_inputs=True)[0]
gradients = gradients.view(gradients.shape[0], -1)
gradient_penalty = ((gradients.norm(2, dim=1) - 1) ** 2).mean()
return gradient_penalty注:想要理解这部分代码,需要理解pytorch中的auograd包 训练结束后,我们保存了生成器和判别器的权重,同时保存了一些生成图片结果,部分展示如下,是不是效果还不错呢。🍄🍄🍄【因为我们训练集图片都是0,所有我们生成的图片都是0喔!!!🍀🍀🍀】训练编码器E🧨🧨🧨 话不多说,让我们直接上代码吧!!!🍭🍭🍭def train_encoder_izif(opt, generator, discriminator, encoder,
dataloader, device, kappa=1.0):
generator.load_state_dict(torch.load("results/generator"))
discriminator.load_state_dict(torch.load("results/discriminator"))
generator.to(device).eval()
discriminator.to(device).eval()
encoder.to(device)
criterion = nn.MSELoss()
optimizer_E = torch.optim.Adam(encoder.parameters(),
lr=opt.lr, betas=(opt.b1, opt.b2))
os.makedirs("results/images_e", exist_ok=True)
padding_epoch = len(str(opt.n_epochs))
padding_i = len(str(len(dataloader)))
batches_done = 0
for epoch in range(opt.n_epochs):
for i, (imgs, _) in enumerate(dataloader):
# Configure input
real_imgs = imgs.to(device)
# ----------------
# Train Encoder
# ----------------
optimizer_E.zero_grad()
# Generate a batch of latent variables
z = encoder(real_imgs)
# Generate a batch of images
fake_imgs = generator(z)
# Real features
real_features = discriminator.forward_features(real_imgs)
# Fake features
fake_features = discriminator.forward_features(fake_imgs)
# izif architecture
loss_imgs = criterion(fake_imgs, real_imgs)
loss_features = criterion(fake_features, real_features)
e_loss = loss_imgs + kappa * loss_features
e_loss.backward()
optimizer_E.step()
# Output training log every n_critic steps
if i % opt.n_critic == 0:
print(f"[Epoch {epoch:{padding_epoch}}/{opt.n_epochs}] "
f"[Batch {i:{padding_i}}/{len(dataloader)}] "
f"[E loss: {e_loss.item():3f}]")
if batches_done % opt.sample_interval == 0:
fake_z = encoder(fake_imgs)
reconfiguration_imgs = generator(fake_z)
save_image(reconfiguration_imgs.data[:25],
f"results/images_e/{batches_done:06}.png",
nrow=5, normalize=True)
batches_done += opt.n_critic
torch.save(encoder.state_dict(), "results/encoder")你会发现这些代码真滴很简单。训练结束后我们会保存编码器E的权重和重构后的一些图片。重构后图片效果也还是蛮好的。测试异常得分🧨🧨🧨我们将检测的异常得分保存在score.csv文件中,保存四项参数,分别为label、img_distance、anomaly_score和z_distance。def test_anomaly_detection(opt, generator, discriminator, encoder,
dataloader, device, kappa=1.0):
generator.load_state_dict(torch.load("results/generator"))
discriminator.load_state_dict(torch.load("results/discriminator"))
encoder.load_state_dict(torch.load("results/encoder"))
generator.to(device).eval()
discriminator.to(device).eval()
encoder.to(device).eval()
criterion = nn.MSELoss()
with open("results/score.csv", "w") as f:
f.write("label,img_distance,anomaly_score,z_distance\n")
for (img, label) in tqdm(dataloader):
real_img = img.to(device)
real_z = encoder(real_img)
fake_img = generator(real_z)
fake_z = encoder(fake_img)
real_feature = discriminator.forward_features(real_img)
fake_feature = discriminator.forward_features(fake_img)
# Scores for anomaly detection
img_distance = criterion(fake_img, real_img)
loss_feature = criterion(fake_feature, real_feature)
anomaly_score = img_distance + kappa * loss_feature
z_distance = criterion(fake_z, real_z)
with open("results/score.csv", "a") as f:
f.write(f"{label.item()},{img_distance},"
f"{anomaly_score},{z_distance}\n")在得到score.csv文件后,我们可以来读取文件内容绘制精度曲线。首先导入一些必要的包:import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from sklearn.metrics import roc_curve, precision_recall_curve, auc然后读取刚刚得到的score.csv文件:df = pd.read_csv("./results/score.csv")df的内容如下:可以看到一共有65262行数据,这和我们数据读取时测试集数据大小是一致的。🥝🥝🥝接着我们读取各列的数据,并把标签为0的标签设置为0,其它的设置为1.trainig_label = 0
labels = np.where(df["label"].values == trainig_label, 0, 1)
anomaly_score = df["anomaly_score"].values
img_distance = df["img_distance"].values
z_distance = df["z_distance"].values然后可以根据上面的值得到一些画图所需值:fpr, tpr, _ = roc_curve(labels, img_distance)
precision, recall, _ = precision_recall_curve(labels, img_distance)
roc_auc = auc(fpr, tpr)
pr_auc = auc(recall, precision)接下来就可以画图了:plt.plot(fpr, tpr, label=f"AUC = {roc_auc:3f}")
plt.plot([0, 1], [0, 1], linestyle="--")
plt.title("ROC-AUC")
plt.xlabel("False Positive Rate")
plt.ylabel("True Positive Rate")
plt.legend()
plt.show()plt.plot(recall, precision, label=f"PR = {pr_auc:3f}")
plt.title("PR-AUC")
plt.xlabel("Recall")
plt.ylabel("Pecision")
plt.legend()
plt.show()plt.hist([anomaly_score[labels == 0], anomaly_score[labels == 1]],
bins=100, density=True, stacked=True,
label=["Normal", "Abnormal"])
plt.title("Discrete distributions of anomaly scores")
plt.xlabel("Anomaly scores A(x)")
plt.ylabel("h")
plt.legend()
plt.show()保存差异图像 代码中还定义了保存原图和生成图差异的图像,即将真实图像和生成图像做差,看看它们的差异,代码很简单,我们来看看:def save_compared_images(opt, generator, encoder, dataloader, device):
generator.load_state_dict(torch.load("results/generator"))
encoder.load_state_dict(torch.load("results/encoder"))
generator.to(device).eval()
encoder.to(device).eval()
os.makedirs("results/images_diff", exist_ok=True)
for i, (img, label) in enumerate(dataloader):
real_img = img.to(device)
real_z = encoder(real_img)
fake_img = generator(real_z)
compared_images = torch.empty(real_img.shape[0] * 3,
*real_img.shape[1:])
compared_images[0::3] = real_img
compared_images[1::3] = fake_img
compared_images[2::3] = real_img - fake_img
save_image(compared_images.data,
f"results/images_diff/{opt.n_grid_lines*(i+1):06}.png",
nrow=3, normalize=True)
if opt.n_iters is not None and opt.n_iters == i:
break
我也抽取一张保存的图像来给大家看看结果: 通过上图可以发现,无论原始输入即原图是什么,生成图都会将其生成0,原图和生成图做差后得到的图片因此也会产生不同的差异。🍊🍊🍊 总结 f-AnoGAN就为大家介绍到这里了,其实你细细摸索下来会觉得非常简单。代码部分大家要勤动手,多调试,这样你会有不一样的收获。🌾🌾🌾
我不是魔法师
从零手写Resnet50实战——利用 torch 识别出了虎猫和萨摩耶
大家好啊,我是董董灿。自从前几天手写了一个慢速卷积之后(从零手写Resnet50实战—手写龟速卷积),我便一口气将 Resnet50 中剩下的算法都写完了。然后,暴力的,按照 Resnet50 的结构,将手写的算法一层层地连接了起来。out = compute_conv_layer(out, "conv1")
out = compute_bn_layer(out, "bn1")
out = compute_relu_layer(out)
out = compute_maxpool_layer(out)
# layer1
out = compute_bottleneck(out, "layer1_bottleneck0", down_sample = True)
out = compute_bottleneck(out, "layer1_bottleneck1", down_sample = False)
out = compute_bottleneck(out, "layer1_bottleneck2", down_sample = False)
# layer2
out = compute_bottleneck(out, "layer2_bottleneck0", down_sample = True)
out = compute_bottleneck(out, "layer2_bottleneck1", down_sample = False)
out = compute_bottleneck(out, "layer2_bottleneck2", down_sample = False)
out = compute_bottleneck(out, "layer2_bottleneck3", down_sample = False)
# layer3
out = compute_bottleneck(out, "layer3_bottleneck0", down_sample = True)
out = compute_bottleneck(out, "layer3_bottleneck1", down_sample = False)
out = compute_bottleneck(out, "layer3_bottleneck2", down_sample = False)
out = compute_bottleneck(out, "layer3_bottleneck3", down_sample = False)
out = compute_bottleneck(out, "layer3_bottleneck4", down_sample = False)
out = compute_bottleneck(out, "layer3_bottleneck5", down_sample = False)
# layer4
out = compute_bottleneck(out, "layer4_bottleneck0", down_sample = True)
out = compute_bottleneck(out, "layer4_bottleneck1", down_sample = False)
out = compute_bottleneck(out, "layer4_bottleneck2", down_sample = False)
# avg pool
out = compute_avgpool_layer(out)
# Linear
out = compute_fc_layer(out, "fc")
算法的手写和网络的搭建,没有调用任何第三方库,这也是这个项目的初衷。相关代码都已经上传至:项目根目录/python/inference.py,项目地址在文章末尾。试运行在将网络搭建完的那一刻,迫不及待的我,赶紧运行了这个网络,试图让它能识别出这张猫,结果如我所想——识别错误!我手写手搭的神经网络将这只猫的类别识别成了 bucket——一个水桶!不过没关系,这不很正常么,谁能确保刚刚手写的一个神经网络,第一次运行就能成功呢?马斯克不刚刚发射星舰还失败了吗?识别错误了,那就开始调试。于是,我快速地使用 torch 搭建了一个官方的 resent50 模型。然后利用这个官方的模型来推理了两张图片,一张是上面的小猫,一张是下面的狗子。结果很明显,官方模型推理正确。看到推理结果我才意识到,那只狗子是个 Samoyed——萨摩耶。在确认了官方的模型和算法可以正确地识别出猫咪和狗子之后,我开始了漫长的debug(调试)之路。开始调试调试方法很简单,将 torch 官方的 resnet50 计算每一层得出的结果,和我手写的算法计算的结果对比。 在对比的过程中,真的发现了一个问题。保存的权值数据与算法不匹配torch 的权值默认是按照 NCHW 的格式存储的,而我手写算法的时候,习惯按照 NHWC 的格式来写,于是,我的第一层卷积就算错了。于是,在导出权值的时候(从零手写Resnet50实战——权值另存为),额外增加一个 transpose 操作,将 torch 默认的 NCHW 的权值,转置为我手写算法需要的 NHWC 的权值。然后继续保存到 txt 中。在将权值的问题修复完之后,重新对比结果,竟出奇地顺畅。一路绿灯。从 conv1->bn1->maxpool,再到第一个layer 中的 conv1->bn1->conv2->bn2->conv3->bn3->relu ,甚至第一个layer中下采样中的计算 conv->bn。这几个地方竟然全部能和官方resnet50计算的结果对的上!也就是,在上图中,绿色部分都没问题,红色部分仍然存在计算错误。出错部分刚好是残差结构的加法操作。不过这个结果确实是惊到我了,这说明——我手写的 Conv2d、BatchNormal、MaxPool算法没问题!从做这件事开始,我就担心我手写的算法可能会有问题,不论是功能上还是精度上,不过在我采用了 float64 数据类型后,精度问题不存在了。而上面和官方的结果的对比验证,也证实了手写的算法在功能上没问题。反而是我从来没担心的网络结构搭建上,却恰恰出了问题——残差结构有错误,不过既然定位到了,后面有时间再继续调试吧。最起码,今天离我手写的神经网络出猫,又近了一步。
我不是魔法师
从零手写Resnet50实战——不使用第三方库
大家好啊,我是董董灿。最近,我想到一个很好的写作计划,与其说是写作,不如说是给自己立的一个小小的实战项目,而且绝对是从零开始的保姆级实战项目。我会结合Resnet50中的算法原理,从零开始,手写全部算法,并且完成这个网络的搭建,最后,完成图像分类任务的推理。项目最终的效果是:从网上下载一张图片,这个网络可以正确的识别出来图片中的物体是什么。为什么要从零手写Resnet50?相信很多同学在初学的时候,都会从网上查过很多神经网络的文章,大部分文章都存在以下的痛点,导致对新手来说,很不友好:调用第三方库来完成算法的运算典型的比如 tf.nn.conv2d, 这一个接口调用,就完成了一个卷积算法的运算。对于新手来说,几乎是很难知道卷积这个算法,到底是如何实现的。基于某些框架,来进行的网络的推理一些文章或者教程,基本都是基于某某框架,来完成的一个Resnet50的神经网络搭建。甚至有些文章教程,都没有网络搭建的过程,直接利用框架已经搭好的网络,上来就直接进行推理。最终,学完之后的效果就是,跟着教程走,确实能把一张图片成功推理出来,但是对新手来说,依旧一脸懵。我初学的时候,拿手写数字识别项目练手,就是这种感觉。按照教程一步步来,确实神经网络很轻松地就识别出来我手写的数字是1还是9, 但是为什么就识别出来了,我不知道。知其然,不知其所以然。环境问题搞死新手对于新手来说,最难的不是算法和框架,而是环境。如果基于上述第三方API调用或第三方框架来学习,我相信每一个人都会被python环境折磨一遍。像什么 tensflow、torch、conda、jupyter notebook 等等,每一个都会有不少坑。一整天都在搞编程环境问题,大大磨灭了学习的动力。于是,我在想,抛弃所有的第三方库和框架,就不能完成一个神经网络的推理了么?当然可以,而且不会有上述3个痛点的存在。怎么做? 从零开始写呗在经过了一些前期调研之后,我制定了从零手写的三个步骤:首先,抛弃所有的三方库,将Resnet50中的所有算法——Convolution,Pooling,BatchNormal,Relu等手写一遍,语言选择python 和 C++,两个版本都会写。至于权值,从网上下载已有的就好。 然后,同样抛弃对三方库或框架的依赖,将手写的算法,按照Resnet50的网络结构,搭积木似的搭起来。最后,从网上下载一张图片,输入给搭建的网络,它能正确的输出图片的类别,这个小项目就算成功了。 刚百度搜出来的一只猫,最终希望这个小项目可以识别出这张图片是一只猫。就这么简单。整个过程,我们只需要简单的C++或python开发环境。 已经和chatGPT达成了初步意向,希望它能作为我的一个助手,在编程和文章写作的过程中,给与一些帮助。 毕竟,在现在,chatGPT的生产力有时候比人要高效很多。在这个过程中,可能会遇到不少困难,但是,有了chatGPT这个伙伴的加入,相信会顺利很多。为什么会计划手写python和C++两个版本呢?因为之前和一些小伙伴沟通,才意识到,并不是每个人的软件技术栈都是一样,甚至有不少小伙伴是java出身的,不好意思本人对java确实不熟。而且深度学习中,几乎都是 python和C++的天下,写这两个版本,能让更多的小伙伴看得懂,最终代码都会放在gitee上哦,链接在建仓库之后会公布。期待,手写的这个网络,最终识别出上面的那只猫!项目进度请关注后续文章,会持续更新。
我不是魔法师
神经网络推理加速入门——存算一体
计算机冯诺依曼架构下一个特点,就是存储和计算分离,这会带来一个问题,那就是计算机有时会遇到存储墙,也就是存储带宽不够导致的性能下降。如果说流水技术可以为此破局,那么其实还有一个大杀器——存算一体技术。存算一体,简而言之,就是把存储和计算放在一起,直接打破冯诺依曼架构的桎梏。它是怎么做的呢还是先从一个例子说起。假设我要做个炒鸡蛋。正常的话我有以下步骤:把鸡蛋从冰箱里拿出来拿着鸡蛋从冰箱走到灶台在灶台开火,开始炒鸡蛋这个过程是经典的冯诺依曼架构中的流程。这里面有一个弊端就是:数据(鸡蛋)需要从存储器(冰箱)中 load(拿)到计算单元(锅)中,然后进行计算(炒鸡蛋)。我们知道,load数据的过程是耗时的,尤其是数据量比较大的情况下。比方需要炒1000个鸡蛋,一个锅肯定炒不下,需要多次拿鸡蛋,多次炒。并且拿鸡蛋的速度取决于从冰箱走到灶台的速度,这里就是存储器到计算单元之间的总线带宽,带宽越大,速度越快。但无论带宽多大,只要是这种架构,总是会有耗时,并且带宽是不可能很大的。在这个时候,会出现一种极端情况,厨师炒鸡蛋的速度很快,可能1秒钟就炒完了,而从冰箱里拿鸡蛋到灶台,遇到个手脚不灵活的人,可能需要3秒钟。不论多长时间,只要大于炒鸡蛋的时间,厨师就得等着鸡蛋过来。这个时候就是说,计算单元在空闲,性能瓶颈在带宽,程序打到了存储墙。所谓存储墙,就是由于存储的数据需要load,但是load的时间很长,像是有一面墙在那里,导致计算单元空闲的情况。为了解决这个问题,就有人提出,既然这样,那为什么不能把计算单元和存储单元放在一起呢?反正芯片都是人设计的嘛,放在一起就不需要来回搬运数据了啊。比如,直接把锅做的特别大,大到可以放下1000个鸡蛋,或者说直接在冰箱里面炒鸡蛋,不用来回拿鸡蛋,这样不香么?别说,还真香。这就是存算一体技术。存——指的是存储器。算——指的是计算单元。两者合为一体,将计算单元和存储单元设计到一起,减少甚至消除数据的搬运。就这一点技术,就能使AI计算的性能得到飞一般的提升。应用场景存算一体技术,在AI领域,最有效的场景便是,存储器的内存足够大,可以放得下整个神经网络的权值参数。举个例子,resnet50的权值参数大概为24MB,AlexNet的权值参数大概为59M,而VGG-16大概有130M的参数量大小。假设一个存算芯片的容量为40M,那么很明显resnet50的所有参数都可以全部放进存储器,这样在做模型推理的时候,只需要把 feature map(也就是图片,比如人脸识别时,拍摄的人脸照片)加载进内存就能推理出结果了。相反,VGG-16由于权值参数太多,无法一次全部加载完成,仍然需要分多次加载。并且每次加载都是需要耗时的,此时,存算技术对VGG-16带来的性能提升肯定没有Resnet50高。因此,存算一体在模型参数小于存储容量的场景下,其性能优势十分明显,可以说,这个时候,没有任何多余的数据搬运操作。总结前两天一个同事给我提了个需求,需要开放几个接口给他,我跟他说,这些接口我早就都准备好了,是因为一直没有需求所以没有开放。需求,会推动产品技术的不断迭代。冯诺依曼架构,从二战时期被提出来开始影响世界。但随着近年AI的不断发展,对于计算机性能的要求逐步提高,使得人们不得不尝试打破传统,开始创新,并且从学界开始大规模走向工业界。在如今美国动不动就禁止中国先进芯片工艺的大背景下,存算一体技术,或许也是一条出路,正如中纪委网站上一个关于存算一体公开课上说的那样——28nm的存算一体芯片,他的能效跟7nm主流的GPU是相当的。或许随着国内存算一体技术的不断发展,我们真能实现弯道超车,期待那一天早点到来。
我不是魔法师
Resnet图像识别入门——池化层
大家好啊,我是董董灿。前面的文章Resnet图像识别入门——激活函数介绍了3中常见的激活函数,以及激活函数在神经网络中的作用。在CNN网络中,除了激活函数之外,还有一种算法也是很常见的,那就是池化层。在Resnet50中,就存在一个最大池化层和一个全局平均池化层。那么,什么是池化层呢?在CNN网络中,池化层又能起到什么作用?池化Pooling池化一般接在卷积过程后。池化,也叫Pooling,其本质其实就是采样,池化对于输入的图片,选择某种方式对其进行压缩,以加快神经网络的运算速度。这里说的某种方式,其实就是池化的算法,比如最大池化或平均池化。池化过程类似于卷积过程。上图表示的就是对一个图片邻域内的值,用一个 2x2 的池化kernel,步长为2进行扫描,选择最大值输出,称为最大池化。最大池化 MaxPool 常用的参数为 kernel = 2, stride = 2 ,这样的参数处理效果就是输出图片的高度、宽度减半,通道数不变。还有一种叫平均池化,和最大池化类似,就是将取区域内最大值改为求这个区域的平均值。和卷积类比和卷积类比,池化操作也有一个核(kernel),但它不是卷积核。池化的核只负责框定某次池化计算需要的图片的范围,核里面并没有数据参与计算,也就是说,在训练过程中,池化层不像卷积层那样,需要学习权重。另一个与卷积不同的是,在卷积的计算中,需要channel维度的数据累加,而池化层的channel维度的数据不需要累加,每个channel中的数据是独立的,这也导致,池化的运算复杂度比卷积简单很多。下图是卷积和池化的示意图,通过两张图,大致可以看出两者的不同。 神经网络中为什么需要池化层?特征不变性汇合操作使模型更关注是否存在某些特征而不是特征具体的位置,可看作是一种很强的先验,使特征学习包含某种程度自由度,能容忍一些特征微小的位移。即池化层支持了一定的平移、旋转、拉伸不变性,这个特性就是有点抽丝剥茧的意思,不断用小特征对大特征进行精简。如下图,通过池化操作,图片中的黑色特征在输出图片中,仍然被保留了下来,虽然有些许的误差。降维如上的例子,图片经过池化的操作,可以减小图片的尺寸,同时又可以保留相应特征,所以主要用来降维。防止过拟合由于池化层没有需要学习的参数,因此,在训练的过程中,可以在一定程度上防止过拟合的发生。降低模型计算量池化的操作,会在保留原始图片特征不变的情况下,将图片尺寸缩小,从而减少整个模型的计算量。在神经网络的训练和推理过程中,一个维度的计算量减倍,往往会带来一个数量级的性能提升,尤其是在训练过程动辄迭代成千上万次的训练场景中。使用池化算法,在减少图片的宽和高尺寸的同时,也会给模型的训练和推理带来更优异的性能提升。除了上述最大池化和平均池化外,池化还有很多变种。最常见的一种变体就是全局池化。全局池化的kernel大小和图片大小一样,因此最终输出的图片大小就是一个点。这种全局池化操作,后面一般用来接全连接层,从而进行分类。如Resnet50最后一层全连接层前,就是一个全局平均池化层。
我不是魔法师
Resnet图像识别入门—— 图像的色彩空间
上一篇文章Resnet图像识别入门——像素从像素开始,聊到了 RGB 这一常见的色彩空间模型。之所以还想继续聊聊 RGB 以及另一种色彩空间模型-YUV ,不是说想要以后去学摄影,学学如何需要调节色度、曝光和饱和度啥的。而是在图像识别的深度学习任务中,RGB以及YUV这些概念,总是会时不时的出现一下,让枯燥无味且高度抽象的深度学习算法,突然之间,变得具体一些,光鲜一些。RGBRed,Green,Blue(RGB)是我们最常见的图像表示方法。这个非常好理解,三原色的融合,几乎可以构造出所有需要的颜色。三张RGB分量图片的融合,就可以构成一幅色彩斑斓的图片。平时我们说,分辨率为1920*1080的图片,它代表的是在长宽两个方向上,有 1920 * 1080 个像素,但是,在色彩这个方向上,还有 3 个通道(channel),也就是RGB,往往被我们忽略。我们看到的一个像素点的颜色,在计算系统中,并不是简单的由一个数值来表示的,而是由RGB三个分量的三个数值来表示的。因此,想要计算一张1920*1080的图片的大小,或者说计算这张图片在计算机内存中所占用的大小时,不能仅仅用图片的长度乘以宽度这么算,还需要考虑通道数。数值表示和图片大小计算我们可能用过画图这一软件来调过颜色。通过简单的设置红色,绿色,蓝色的数值,就可以在调色板中得到一个颜色。有没有注意到,无论红色,还是绿色,还是蓝色,其表示的数值都没有超过255。为什么?因为像素值,在计算机语言中,是用一个 uint8 的数据来表示的。而 uint8 ,指的是 8bit 无符号整数,其能表示的范围就是 0 - 255。自从几年前微软宣布停止更新画图软件之后,画图就越来越少的出现在人们的视野中,你或许可以打开电脑看看,左下角的菜单里,是否还有画图软件,就像当年window xp 系统被停用一样,慢慢的就会消失在人们的记忆里说到这,我们就可以算一算,一张 1920 x 1080 的RGB图像,在计算机的表示中,到底占多少的内存?很简单,长宽方向上每个像素由 3 个通道组成,每个通道由一个 8 bit 的数值表示,一个 8 bit 数值代表一个字节(Byte)。因此,一张1920 x 1080的 RGB 图像,占计算机存储大小为 1920 x 1080 x 3 x 1 Bytes = 6075 KB = 5.9 MB。5.9 MB 的内存占用!大么?和目前动辄几个G的手机内存相比,不算大小么?和边缘侧图像识别终端内存,比如摄像头里的嵌入式芯片内存相比,又不算小更何况在公路违法拍照的摄像头场景下,在车流量很大的时候,需要实时处理的图片,可远远不止一张!那怎么办?有没有办法可以在进行图像处理时,减少图片的数据量,从而减少图片大小和内存占用呢?有,YUV就是其中一种办法。YUVYUV是将亮度信息和颜色信息进行编码的一种颜色空间表示方法。和RGB类似,YUV 也使用3个字母维度来表示颜色。为了简单点,我们暂时将这3个值称为Y,U和V。(事实上,YUV的称呼很多,比如Y'CbCr,也很细节,这里不多描述,我们只要知道它是另外一种表示颜色的方法就可以。)Y 代表亮度,U 代表色彩度,V代表饱和度。上面的几张图片,除了原图之外,我们可能更加倾向于使用只有Y分量的图片,也就是那张黑白图像。 因为即使没有色彩,但是它的轮廓以及明亮程度,也足以让我们分辨出图片中的物体。其视觉效果远远好于其他两个分量的图片。相反,只有色度和饱和度的图片,反而变得模糊不堪。这就是问题所在!人眼对于亮度具有更高的敏感度,而对色度和饱和度反而显得迟钝一些。说到这,有没发现什么?既然人眼对于色度和饱和度的反应不敏感,那就没有必要把所有的色度和饱和度信息都放在图片里了啊。举个例子,色度和饱和度我隔一个像素放一个,剩下的像素没有饱和度,不就可以了么。没错,是可以,而且效果很好。这就是YUV的不同编码。实际上,YUV的编码方式有很多种,比如YUV444, YUV422等。大致意思就是,保留全部的Y分量(人眼最敏感的亮度分量),但是只保留部分的U/V分量(人眼不敏感),以此来减少图片的占用,但又不失重要信息。还记得上面的1920 x 1080的RGB图片的内存占用么,为5.9MB。如果用YUV444的编码,结果也同样是5.9MB, 因为YUV444也是全采样,所有的亮度、色度、饱和度信息都保留了。而如果采用YUV422的编码,相当于U分量减少一半,V分量减少一半。那么最终的图片占用大小就变成了1920 x 1080 x (1 + 0.5 + 0.5) Bytes = 4050KB = 3.9MB。只有3.9MB,减少了1/3的内存占用!是不是好很多?更多关于YUV的编码知识,有兴趣的同学可以Google。如果不做相关课题,可以不用深究。我们只需要知道,YUV这一色彩编码方法,在保留亮度这一人眼最敏感信息的基础上,通过降低其他人眼不太需要的信息,可以来达到降低图片大小的目的。就足够了。YUV编码的用途原始图片,channel数代表的是RGB通道,可以理解为原始图片具有的三个特征。可在深度学习网络中,随着网络深度的增加,图片的channel数会不断的增大。就拿Resnet50这个网络来说,最后面的一层图片,channel数已经增大到了2048。这时channel代表的信息,早已不再是RGB这种基础的特征了,而是通过了大量的神经网络训练,代表了图片的分类,比如是猫还是狗。YUV这种编码方法,可以用在图片的上下采样中,通过降低或增加通道数,实现图片的上下采样,以此来实现图片的增大或减少,但又不损失太多我们希望保留的信息。总结一下聊了聊 RGB、YUV 两种颜色空间,以及YUV可能的用途和它的优势。为什么聊这么多关于图片的东西,因为在深度学习处理图像的任务中,图片是原材料。正所谓知己知彼,百战不殆。了解了图片这一深度学习的原材料之后,我们就可以更加高效的来完成图片数据的处理和分析,就可以开始图像识别的算法之旅了!
我不是魔法师
Resnet图像识别入门——像素
如果让你设计一个可以识别图像的神经网络,你会怎么做?我之前问过自己这个问题,思来想去,我的答案是:我可能不知道如何下手。突然有一天,当我把Resnet50这个网络的所有算法都写完一遍之后,我突然觉得,只要我深入了解了这些算法的原理,或许这个网络我也能设计出来。于是,我有了一个大胆的想法:从头开始,拆解一个典型的图像识别网络是怎么工作的,以及它里面所涉及的背景知识和算法原理。你可能会想,看懂这些需要什么知识呢?其实不需要太深奥的数学知识。我尽可能把每一步写的通俗易懂,这个过程中我会搜集一些资料,也是一个不断完善自己知识体系的过程。这个网络,就用Resnet50吧。像素要实现图像识别,最离不开的,就是像素。其实我们都知道,图像是由像素组成的。实际上,神经网络计算,算的就是像素之间的关系,以及这些关系背后可能隐藏的图片信息。相机摄像头像素2000万,拍出来的照片肯定比像素1000万的要清晰,我们更容易看到图片中的物体是什么。这是为什么?因为像素越多,像素之间的关系(色彩,轮廓)越丰富,我们所能看到的信息就越多,自然而然获取到的信息就多。但是,你有没有发现。当我们去看一张图片时,我们绝对不是盯着某一个像素或某几个像素看,而是看了整个图像的大部分区域,或者说,我们看到了大部分像素。因为只有看到了大部分的图片,才能知道图中是座山。正所谓,聚沙成山,绝不是少了一粒沙,山就不是山,多了一粒沙,就变成了山。上图哆啦A梦,虽然不是很清晰,像素点数也很少,但一眼望去,依然可以分清是哆啦A梦,甚至,用手捂住一半的图像,依然可以。这是因为人们对于图像信息的识别,是建立在对像素局部性分析的基础上的。像素局部性所谓局部性,通俗点说,就是眼睛或大脑会将相邻的像素或大片的像素连接起来分析,从而组合成嘴巴,然后是耳朵,最后是哆啦A梦。而一个人工神经网络识别图片的过程,大致就是如此:神经网络模拟的,就是人们看到图片之后的信息处理过程。当我们盯着一个图片看时,我们首先会获取到图片的细节特征,比如哆啦A梦红色的大嘴巴。但是如果仅仅盯着大嘴巴,又反而让人有一种“只缘身在此山中”的感觉,并不能看到整个图片的全貌,因此还需要看一下图像的轮廓。于是,眼睛看图片就有了以下两个过程:瞳孔放大,盯着某一处细节(如大嘴巴)看瞳孔缩小,模糊的看一张图片的大致轮廓两个过程看到的信息,在大脑中叠加,那就看清楚了,是哆啦A梦。那么神经网络是否可以模拟这种瞳孔放大、缩小的方式呢?很幸运,可以,那就是利用卷积核。卷积运算模拟人眼感受野卷积核投影在原始图片上的区域,称之为感受野。通过设计不同大小的卷积核,就能获得不同大小的感受野。如此,卷积核就可以很直观的模拟瞳孔张开、缩小的过程。并且大量的实验和论文表明,卷积这一针对图像局部性识别的算法,可以非常有效的模拟人眼识别物体的过程。关于卷积算法以及卷积核的设计,后面会专门写文章,欢迎持续关注本系列。色彩分量RGB回到像素这一话题。你有没有想过,为什么一张图片会是彩色的。学过摄影的小明同学可能这时会回答:因为图片是由RGB三种颜色来表示的,每个像素实际是不同的R/G/B分量的叠加,混合起来,就表示成了不同的颜色。回答正确。我们人眼可以很直观的看到红色和蓝色,可以察觉到一张图片的色彩和轮廓。那么,如果让计算机来处理图片,它又是如何知道色彩和轮廓的呢?其实对于计算机来说,一张图片存在在内存中,只是一堆数据,计算机是无法知道这堆数据代表的是什么。这就需要人为的给这堆数据一种表示方法,让计算机知道:哦,这1/3的数据是红色分量,这1/3的是蓝色分量,剩下1/3的数据是绿色分量。还得让计算机知道,一些数据(像素)组合起来,可能代表的是个“帽子”。怎么做呢数据在计算机的存储中,最常见的存储方式是连续存储的。比如在C语言编程中,我们可以定义一个数组,那么数组在内存中的位置是连续的。int data[10] = {0,1,2,3,4,5,6,7,8,9};
内存怎么理解,它就是一排连着的门牌号的宿舍。门牌号为101里面住着的,是data的第一个数据0;门牌号102里面住着的,是data的第二个数据1,...,以此类推。只不过,在计算机存储器中,没有门牌号,有的都是地址。这个时候,计算机根本就不关心数据是啥,计算机用到的时候,就把数据从内存对应的地址中取出来用。如何取数据这就需要人们为数据存储设计一种格式,告诉计算机,这堆数据大概是什么样的。只有这样,通过这种人为约定的方式,计算机才能正确的取到R分量或者B分量。对于一张图片来说,最常见的两个参数是长和宽,一般用H(height) 和 W(width) 来表示,那么RGB三个分量,看作是3个通道(channel),一般用 C 来表示。如此一来,一张长宽分别是224像素和224像素的RGB图像,就可以用 HWC = [224, 224, 3]来表示。两张类似的图片就用 NHWC = [2, 224, 224, 3]表示,其中N代表图片张数。友好的数据表示方法,可以减少大量的计算复杂度。虽然这样表示不太利于人们的直观理解,但是计算机处理这种数据是十分方便的。在目前主流的深度学习框架中,对于图片的数据格式,基本都支持了NHWC或NCHW这种数据摆放格式。说到底,都是为了更高效地进行图片数据的处理和运算。像素女神熟悉OpenCV或者计算机视觉的同学,可能对于上面的RGB分量中的女神很熟悉。没错,在很多的教程中,这位女神不止一次的出场。这位女士名叫 Lena。电气电子工程师学会图像处理汇刊 (IEEE Transactions on Image Processing)主编曾在1996年1月出版的一期中解释道,Lena的流行,因为她是一张好的测试图片,其中包含了很多细节,平滑区域,阴影和纹理。当然,另外一个原因就是漂亮美女的图片自然受到男性居多的研究领域的欢迎。
我不是魔法师
Resnet图像识别入门——初识卷积
原创文章请勿转载,如有转载意向请联系作者。前面花了两篇文章的篇幅,聊了聊图片相关的背景知识。Resnet图像识别入门——像素Resnet图像识别入门—— 图像的色彩空间就像上一篇文章说的那样:图片是做深度学习任务的原材料,就像是做饭,不了解原材料的特性,怎么能快速高效的做出一顿美味的大餐?下面开始想聊聊卷积,但是不聊公式,只聊一下卷积这一算法是如何工作的,以及它的一些原理,至于枯燥的公式,留给后面吧。人脑是怎么记住东西的?在说卷积之前,先务虚一下,说说AI的记忆。或许你已经听说过很多AI故事了,比如大名鼎鼎的阿尔法狗大战柯洁。但是,你有没有想过一个问题。阿尔法狗确实是学会了下棋,但是它下棋的记忆到底是什么样的?存在什么地方呢?高中生物老师教过我们,人脑中有大量的脑神经元。每个脑神经元都可以看做是一个小的记忆体,神经元之间通过树突连接起来。整个大脑的神经元,可以说是一张十分复杂的网络。人脑处理信息,就是利用这个复杂的网络处理信息,并最终得到一个结果。通过神经元网络,我们才能知道,眼睛看到的是一只猫,还是是一只狗。稍微简化一下大脑神经元的复杂结构成如下的网络。每个黑点代表一个神经元脑细胞,每个神经元都有自己负责记忆的东西。当我们看到一张画着猫的图片的时候,图片信息通过视神经传给大脑神经元,于是,信息到达了最左边一排竖着的黑点(神经元)。神经元的激活与静止假如一个黑点(神经元)之前见过猫,那么这个黑点就会把信息往后传,此时神经元处于激活状态。假如一个黑点从来没见过猫,那么这个黑点(神经元)就啥也不知道,啥也不做,此时神经元处于静止状态。像不像初中课堂上,老师问了你一个超难的问题,而你不知道的时候,你也只能站着,可怜又无助,啥也不会做?没错,神经元如果没见过猫,他啥也不会做!图片的信息就这样,一层一层的通过“见过猫且确信它是一只猫的”神经元往后传递,直到在最后输出一个结果。这是一只猫。这个过程叫做大脑的推理。整个推理过程你应该注意到了一件事:所有的黑点(神经元),都可能是有记忆的,只不过记得东西各有不同,有的认识猫,有的认识狗,就像下面这样。所有认识猫的神经元都会让信息通过,其他不认识猫的神经元都静止了。但是只要信息能传到最后,人脑最终就可以得出一个结论,这就是一只猫。那神经元的这些记忆是怎么获取的呢?当然是训练!人们在日常生活中不断地训练大脑,时刻观察着周围的事物。见得多了,就会了。训练——人工智能获取记忆那么计算机又该怎么模拟这个记忆过程呢?答案很简单:因为计算机只会计算,那就让它计算好了。如果某个黑点认识猫,有什么办法可以把“这是一只猫”这一信息传递到后面呢?乘以1啊,任何数乘以1都是它自己,一只猫乘以1也还是他自己。如果某个黑点压根没见过猫,有什么办法可以什么都不做呢?乘以0啊,任何数乘以0都是0,信息也就没了,一只猫乘以零,猫也就没了。于是乎。在深度学习的网络中,每个黑点(神经元)都有一个与之对应的数字(实际的网络中,不是0或者1这样简单的数字,而是一对复杂的数字,这里仅仅是为了说明示意),这些数字,在深度学习中,我们称之为权值。神经元可以通过与权值的加权计算来判断是否让某一信息经过神经元,到达下一层。权值乘以输入的信息(猫),然后经过激活函数去激活(类似于人脑神经元的激活)。如果能成功激活,那么信息就往下传。如果没有成功激活,信息就在此丢失。当然神经网络中的权值不是简单的0或1,所以经过激活函数计算出来的只是一个概率值,也就是说黑点(神经元)觉得它是一只猫的概率。最终如果得到95%的概率觉的它是一只猫,那基本就是一只猫。这个权值,就是 AI 的记忆。这个权值,就是 AI 在训练的过程中学到的东西:千百万次计算得出的最优解。这个权值,可以保证,只要 AI 在训练过程中看过猫,那么新的猫咪来的时候,猫咪乘以权值有很高的概率能通过激活函数,确保神经元被激活。为什么可以这么确定呢。因为 AI 的训练过程早已经模拟了成千上万次“识猫”的过程了。权值就是训练出来的!就像我们的记忆被训练出来的一样!而卷积这一算法,就天然存在一个记忆体,或者说权值,那就是卷积核。开始说卷积 | Convolution首先不要被这个名字吓到了。不管数学好不好的同学,看到卷积的第一反应,可能是记得有一个卷积公式,貌似可以进行信号处理。 图片一个代表卷积核的曲线在原始信号曲线上滑来滑去,得到不同的输出。在什么地方学过来着?好像是时频转换的时候,又好像不是。(当然不是!)但是,不用回忆之前的知识,不用管它!因为,深度学习中的卷积,和信号处理中的卷积,有相似之处,但又不完全一样。深度学习中的卷积,完完全全模拟的,就是人眼看物体的过程!卷积模拟人眼上图是深度学习中卷积的示意图。还记得之前说过的么,图片是由像素组成的。示意图下方的 4x4 的像素方格就是卷积需要处理的图片(模拟人眼观看图片的过程)。示意图上方的 2x2 的像素方格就是卷积的输出(人眼看到图片之后得出的结论)。那么卷积核在哪?4x4方格上移动的灰色阴影,3x3的像素方格就是卷积核!可以理解为人眼此时聚焦看到的区域(称之为感受野,人眼的视野),只不过,这个示意图中每次看到的都是一个3x3的像素方格!而卷积过程,就是用3x3的卷积核,去逐步扫描图片。横着扫完竖着扫。每扫一次,就将逐个像素点的值相乘然后加一起,得到一个输出。再换个更直观的角度看一眼。卷积,就是这么简单的过程。不仅如此,我们还可以通过调整卷积核的大小,比如把上图3x3的卷积核扩大到5x5,来控制 “人眼” 看到的图片范围,从而获取到不同的图片信息。当然,在实际神经网络中,存在这个各种各样的卷积变种。科学家或工程师们通过设计不同的卷积核以及卷积每次移动的多少等参数,来实现不同的功能。但卷积操作万变不离其宗!总结一下这一篇文章主要聊了聊几个概念。AI 之所以能够记住它所学的东西,关键在于神经网络有权值这一参数的存在,它的存在就类似人脑的记忆。并且,权值和人脑的记忆一样,都是通过大量的实践来训练出来的。而卷积这一算法,天然就存在一个权值参数,称之为卷积核,人们可以通过设计卷积核的大小,调整希望神经网络“看到”的图片的视野,也叫作感受野,从而不同的卷积获取到不同的信息。实际上,卷积这一算法,除了本文说的利用“感受野”获取到不同图片区域的信息,从而将图片在长宽两个维度的尺寸缩放之外,还存在channel维度的升降。而这,才是卷积这一算法的核心,称之为特征提取。
我不是魔法师
Resnet图像识别入门——Softmax分类是如何工作的
大家好啊,我是董董灿。很多同学在做深度学习时,都会遇到难以理解的算法,SoftMax肯定是其中一个。初学者大都对它一知半解,只知道SoftMax可以用来做分类,输出属于某个类别的概率。但是,为什么要用SoftMax呢?这个算法又是如何将神经网络推理的数值,转换为一个类别的分类的呢?应用场景假设要使用神经网络做图片分类。 现在有3个类别:猫,狗,人。给你下面一张图片,神经网络需要在这3个类别中选出一个。上图人眼一看就知道是猫咪,但是神经网络需要通过计算才知道。好,我们使用Resnet50这一分类网络进行推理运算。算到最后面的全连接层时,全连接输出了3个数值,分别为2,1,0.1。看过前面文章的同学可能知道,全连接输出的数值,代表了这一分类的得分。关于全连接可以翻看全连接。现在我们假设这三个分类的得分分别为:分类得分猫2狗1人0.1猫得了2分,狗得了1分,人得了0.1分。单看这个结果,我们大概知道,因为猫的得分最高,那最终神经网络会认为这张图片是一只猫。错了。错在哪?至少两点。第一,神经网络最终选择某一分类,依据的不是得分,而是概率。也就是说,最终神经网络会选择一个概率最高的分类作为它识别的结果。为什么要把得分转为概率呢?因为多分类模型中,输出值为概率更利于反向推导和模型的迭代,概率之间更好的计算距离,而数值之间的计算的距离是无含义的。所以,我们需要一种方法,将上面的得分转换为概率。第二,得分是神经网络经过了几十层卷积运算计算出来的例子中猫的得分是2,狗的得分是1,人的得分是0.1,我们可以比较肯定的说,因为猫的得分最高,而且比狗和人都高很多,肯定就是猫。但实际中,有很大的可能算出的猫的得分是2.1,狗的得分是1.9,人的得分是0.1。这个时候,我们可能就没有像刚才那么肯定了。因为猫的得分和狗的得分相差很少,而且两者都很高!这也是为什么,很多神经网络最终都会以TOP1 和 TOP5的识别准确度来衡量神经网络的精度。由于上述两个原因的存在,人们想到了SoftMax算法。而这个算法,也几乎完美地解决了这两个问题。为什么叫SoftMax以及它的实现原理不知你有没有想过,为什么这个算法叫SoftMax呢?Soft 是软的意思,与之对应肯定有 HardMax。而 HardMax,可以理解为我们平时认知的Max。比如给你两个数(3, 4), 那么这两个数的 HardMax(3,4) 结果就是4。这个逻辑,小学生学会了10以内的加减法都知道。但正如上面所说,SoftMax不一样,它是要处理多个类别分类的问题。并且,需要把每个分类的得分值换算成概率,同时解决两个分类得分值接近的问题。先从公式上看,SoftMmax是怎么做到的。公式中,每个 z 就对应了多个分类的得分值。SoftMax对得分值进行了如下处理:以e为底数进行了指数运算,算出每个分类的 eZi,作为公式的分子。分母为各分类得分指数运算的加和。根据公式很自然可以想到,各个分类的SoftMax值加在一起是1,也就是100%。所以,每个分类的SoftMax的值,就是将得分转化为了概率,所有分类的概率加在一起是100%。这个公式很自然的就解决了从得分映射到概率的问题。那,它又是怎么解决两个得分相近的问题的呢? 其实也很简单,重点在选择的指数操作上。我们知道指数的曲线是下面的样子。指数增长的特性就是,横轴变化很小的量,纵轴就会有很大的变化。所以,从1.9变化到2.1,经过指数的运算,两者的差距立马被的拉大了。从而,我们可以更加明确的知道,图片的分类应该属于最大的那个。下面是将猫、狗、人三个分类经过SoftMax计算之后得到的概率。分类得分softmax 值猫270%狗120%人0.110%可以看到,分类是猫的概率遥遥领先。所以,神经网络在经过softmax层之后,会以70%的概率,认为这张图片是一张猫。这就是SoftMax的底层原理。指数让得分大的分类最终的概率更大,得分小的分类最终的概率更小,而得分为负数的分类,几乎可以忽略。应用SoftMax 其实也是一种激活函数,它广泛的应用于多分类任务中。对于激活,其实大家应该有个更熟悉的场景,那就是在二分类任务重,有个函数被广泛使用,就是 Sigmoid。比如,有个朋友告诉我,在一些互联网的广告或者商品推荐(比如某宝的猜你喜欢)中,曾经广泛的使用Sigmod函数来预测点击的可能性,如果Sigmoid函数的输出值越大,那么说明这个内容被用户点击的可能性就越大。想想吧,我们逛淘宝的每次点击,背后都有一个函数在分析你的行为,是什么感受?
我不是魔法师
Resnet图像识别入门——激活函数
大家好啊,我是董董灿。上一篇文章Resnet图像识别入门——残差结构说到了Resnet网络的残差结构,也就是人们俗称的高速公路。Resnet50这个图像分类网络,就是有很多残差结构组成的卷积神经网络,一层层搭建而成的。除了卷积层,还有激活层、池化层、BN层等。每一个卷积层的后面,都会跟着一个激活层,在Resnet50中,激活函数用的是Relu激活函数。那为什么在神经网络中,每一层卷积后面都需要跟着一个激活函数呢?一个原因敲黑板,划重点,为了,非线性。我们都学过线性关系,最简单的 y = kx + b,画出来就是一条直线。这个函数就是一个线性函数,称 y 和 x 是线性关系。如果这个时候,又有一个线性关系 z = hy + d,那么,可以通过如下的线性变换,得到变量 z 和 x 同样也是线性关系!z = hy + d = h(kx+b) + d
= hkx + hb + d
= Ax + B
其中:A = hk, B = hb + d。所以,不管有多少个线性关系,只要在数学上首尾相连,最终都可以等效成一个线性关系!而在深度学习任务中,如分类任务,具有线性关系的模型其分类效果是不好的,甚至是很差的。因为卷积算法是由大量的乘法和加法组成,所以,卷积算法也是线性的!这就导致,由大量卷积算法组成的卷积神经网络(CNN),如果没有非线性因素的引入,会退化成一个简单的线性模型。这就使得多层卷积失去了意义。比如,Resnet50网络中的50层卷积,就会退化为一个卷积。而在神经网络中,使用多层卷积的一个重要目的,就是利用不同卷积核的大小,来抽取不同卷积核尺度下的图像特征。因此,在神经网络设计时,在每层的卷积后面,都增加一个非线性函数,就可以完成两个卷积层的线性隔离,确保每个卷积层完成自己的卷积任务。目前常见的激活函数,主要有Sigmoid、tanh、Relu等。Resnet50中的激活函数就是Relu。下面主要介绍下这三个函数。激活函数sigmoid 激活函数Sigmoid 函数的图像看起来像一个 S 形曲线。公式为:f(z) = 1/(1+ e^-z)
Sigmoid 在神经网络中使用,是有一些优点的,主要体现在:Sigmoid 函数的输出范围是 0 到 1。由于输出值限定在 0 到 1,因此它对每个神经元的输出进行了归一化;用于将预测概率作为输出的模型。由于概率的取值范围是 0 到 1,因此 Sigmoid 函数非常合适;梯度平滑,避免「跳跃」的输出值;函数是可微的。这意味着可以找到任意两个点的 sigmoid 曲线的斜率;确的预测,即非常接近 1 或 0tanh 激活函数tanh激活函数的图像也是 S 形,表达式如下:tanh 是一个双曲正切函数。tanh 函数和 sigmoid 函数的曲线相对相似。但是它比 sigmoid 函数更有一些优势。首先,当输入比较大或者比较小时,函数的输出几乎是平滑的并且梯度较小,这不利于权重更新。二者的区别在于输出间隔,tanh 的输出间隔为 1,并且整个函数以 0 为中心,比 sigmoid 函数更好;在 tanh 图中,负输入将被强映射为负,而零输入被映射为接近零。Relu 激活函数ReLU激活函数图像如上图所示,函数表达式如下:ReLU 函数是深度学习中较为流行的一种激活函数,相比于 sigmoid 函数和 tanh 函数,它具有如下优点:当输入为正时,不存在梯度饱和问题。计算速度快得多。ReLU 函数中只存在线性关系,因此它的计算速度比 sigmoid 和 tanh 更快。当然,它也有缺点:Dead ReLU 问题。当输入为负时,ReLU 完全失效,在正向传播过程中,这不是问题。有些区域很敏感,有些则不敏感。但是在反向传播过程中,如果输入负数,则梯度将完全为零,sigmoid 函数和 tanh 函数也具有相同的问题:我们发现 ReLU 函数的输出为 0 或正数,这意味着 ReLU 函数不是以 0 为中心的函数。除了上面的3种激活函数之外,还有很多其他激活函数,比如Relu函数就有很多变种,如PRelu、LeakyRelu等。每种激活函数,都在一种或几种特定的深度学习网络中有优势。判断一个激活函数的好与坏,绝不仅仅是从函数的数学表达式上来判断,而是需要在实际的深度学习网络中不断地实验和实践,来找到最适合这个网络的激活函数。总之,之所以在神经网络中添加激活函数,一个重要的原因是给网络模型增加非线性因素。目前已有的激活函数有很多种,每种激活函数,不论从数学原理上还是从适用的AI模型上,都有各自的优缺点,需要根据网络特点和适用场景,进行实验,选择最适合这个模型的激活函数。
我不是魔法师
什么是dropout?
大家好啊,我是董董灿。2018年5月11日,《复仇者联盟3:无限战争》登陆大陆院线。无数漫威迷们聚集在大荧幕面前,看着灭霸一人单挑漫威众超级英雄。在6颗无限宝石集齐之后,灭霸毫不犹豫打响了宇宙中最重要的一个响指,刹那间,蜘蛛侠、奇异博士、黑豹、星爵等超级英雄,烟消云散。“Why,Why Peter”? 电影院里,一个蜘蛛侠的超级粉丝哭着问道。我也不理解,但灭霸给出了答案:“我们这个宇宙资源不够了,需要随机的无差别消灭一半的宇宙生灵,来维持宇宙的资源平衡”。对于灭霸这个回答,我给了一个大大的赞。人工智能模型训练中,也有一个类似于随机消灭人类的方法,只不过消灭不是人,而是神经网络中的一个个神经元。随机消灭神经元时间回到2012年,深度学习大佬 Hinton在其论文——《Improving neural networks by preventing co-adaptation of feature detectors》中,提出了一种随机无差别消灭神经元的方法。同样是2012年,Hinton大佬使用这种方法来训练 AlexNet,一种图像识别网络,赢得了2012年图像识别大赛冠军,引爆了神经网络应用的热潮。在神经网络训练算法中,这种随机无差别消灭神经元的方法,叫 dropout。它是一种随机将神经元参数置为零的方法。神经元只有在参数非零的情况下,才能参与运算,对最终的结果产生影响,可想而知,将神经元参数置为零,意味着神经元不参与任何计算。也就是说,神经元死了。有的人活着,他已经死了。有的人死了,他还活着。但神经元死了,可能比活着的作用还大。只因为,死了的神经元,可以在神经网路模型的训练阶段,有效的防止训练过拟合。什么是过拟合时间回到初中的数学课。数学老师会给我们下面一张图。问有哪位同学能计算出一个函数出来,用来预测一个房子面积对应的房子价格?数学成绩不错的小明同学举起手来喊道,“老师,这题我会!”首先我们假设有一个直线 y = ax + b,然后算出每个点到直线的距离之和,当距离之和最短时,得到的a和b的值就是直线的参数,这样就得到了一个函数,也就是一条直线。没错,这就是拟合,这种简单的拟合方法,叫做线性拟合,也叫线性回归。在处理离散数据时,数据拟合是一个很常用的方法。说到这你可能明白了,其实,神经网络的训练过程就是一个很复杂的拟合过程,它拟合出来的不再是一个简单的一次函数,而是可能连我们自己都不知道的一种函数,或者说是一个黑盒。人工智能就通过这个黑盒的运算,来推断出,它看到的一张图片上,画的是一只狗,还是一只猫。那什么是过拟合?假如老师又给了你下面一张散点图,让你用一种手段把所有的点都连接(拟合)起来。小明同学的做法简单粗暴,画出了下面的图形,看着还不错,拟合地刚刚好,已经把所有的点都连接起来了,而且点与点之间有一定的几何关系。酷爱绘画的小红同学,拿到图之后,一顿操作猛如虎,得到下面的一张图,初中老师看了小红惊为天人的操作,大惊失色,但却随后说出了一句:“你过拟合了。”过拟合,就是说,这张可爱的小猫咪或许只能在我给你的这张图上用点连起来,我再给你其他的图,你可能就画不出来猫咪了,甚至连动物都有可能都连不起来了。但是,上面小明同学的做法就很好,很有普适性,或者叫泛化性,给你任何的散点图,都可以画出那种横竖的线段出来。神经网络的过拟合与人工智障神经网络训练时处理的是数据。有数据就有模型拟合,有模型拟合就有模型过拟合。也就是说,模型训练的过头了,因为它只在给定的数据上做了十分精确的匹配,却缺少了泛化性!假设我们用自家的一只白猫照片来训练一个什么也不知道的神经网络,无时无刻的不告诉他,这是一只猫。在神经网络看了1000遍照片之后,它懂了,它知道了这是一只猫。可是突然有一天,又来了一只橘猫,甚至还戴着一顶圣诞帽。神经网络懵了,没见过啊,活这么大只见过白猫,这个橘色的头上还带着个东西的生物,是个啥?这不是人工智能,而是人工智障。怎么解决?灭霸说,放着我来。把神经网络的神经元每次都随机消灭一部分,让神经网络每次看到的自家白猫的图片不是一整张,而是分散的局部。比如第一次看到的是一只猫耳朵,第二次看到的是一只猫眼睛,第三次看到的是猫嘴巴。经过若干轮的训练之后,即使这个神经网络看到一只猫咪的小爪子,他大概也会知道,哦,这是一只猫。那么即使这时候来了一只开着飞机的猫,只要它有着和白猫一样的尖尖的耳朵,神经网络一眼就能认出它。dropout 技术可以有效增强网络训练时的泛化能力,同时可以很好地减轻训练的计算机负载,因为有不少的节点在一次训练过程中不会参与运算。
我不是魔法师
Resnet图像识别入门——残差结构
桃树、杏树、梨树,你不让我,我不让你,都开满了花赶趟儿。红的像火,粉的像霞,白的像雪。花里带着甜味儿;闭了眼,树上仿佛已经满是桃儿、杏儿、梨儿。花下成千成百的蜜蜂嗡嗡地闹着,大小的蝴蝶飞来飞去。野花遍地是:杂样儿,有名字的,没名字的,散在草丛里,像眼睛,像星星,还眨呀眨的。朱自清在写《春》的时候,或许也没有完全认清春天的所有花,以至于写出了“有名字的,没名字的,散在草丛中”这样的句子。如今,时代变了。人手一部手机的我们,遇到不认识的花,随时随地就可以打开手机百度识图功能来完成识图。“杂样儿的,有名字的,有名字的,有名字的,有名字的 ... 都散落在手机里,像眼睛,像星星,还眨呀眨的”!而让我们如此轻松加愉悦的完成识图功能的,便是手机背后运行的大量卷积神经网络,或者说是CNN网络。大家好啊,我是董董灿。上篇文章Resnet图像识别入门——卷积的特征提取介绍了通过卷积这一算法进行特征提取的原理和应用。接下来,沿着Resnet50这个神经网络,介绍一下这个图像分类网络,以及它的核心思想——残差结构。为什么叫Resnet50研究AI网络的人拥有网络命名权。比如我研究出来一个网络,效果很好,要发一篇论文来介绍这个网络,论文中需要给网络起个名字,并且希望这个名字可以流传很广。那么,简单、好记同时又能概括网络思想的名字肯定是首选。Resnet50 就是这样的名字,这个网络的核心思想,就藏在名字里。Res + net + 50,Res 是 Residual (残差)的缩写,50 指的是整个网络中有50个卷积层。下图是Resnet50的网络结构图,可以看到,从第一层到最后一层,总共50个卷积算法。 那么Res(Residual)残差又是个什么东西呢?残差结构所谓残差结构,其实就是在正常的神经网络中,增加一个 short cut 分支结构,也称为高速公路。比如上图中,左侧是正常的卷积层,一层层往下传,在右侧增加一条连线,使得整个网络结构形成了一个残差结构。这样,网络的输出不再是单纯卷积的输出F(x),而是卷积的输出和前面输入的叠加F(x) + X。为什么要增加残差结构在前面说过,深度卷积神经网络在网络深度不断加深的过程中 ,神经网络会学到不同的特征。但是,能无限制地加深么?比如使用1000层卷积层进行网络的训练的。答案显然是不行的。原因在于神经网络训练的过程是不断与目标值进行拟合的过程,直到拟合的误差降低到人们的预期,代表着神经网络训练完毕,一个会识图的AI就诞生了。但是在实际训练过程中,数据的传递除了从网络前端往后传之外,还需要将最后一层与目标值的误差传回到网络前端,从而进行下一轮的训练,得到更小的误差,这一过程成为神经网络的反向传播。在往回传的过程中,由于误差本身就很小,如果卷积层数过多,在经过激活函数时,很容易发生误差传着传着就消失了,称为梯度消失。梯度消失的原因有很多种,不好的激活函数、过深的网络层数等都有可能导致误差消失。想象一下,上一轮训练结果的误差传不回来,下一轮如何在上一轮的基础上进行进一步优化训练?结果就会导致怎么训练神经网络最终的结果都无法收敛。AI根本训练不出来!残差来救场残差结构这个时候就可以发挥作用!想象一下,这个高速公路的存在,可以使得输入数据无损地通过。如果左侧卷积层学习到的数据不够好,那么叠加上无损通过的原始数据,依然保留了原始数据,不至于丢掉原始数据。而如果左侧卷积层学习到的效果很好,那么依然会保留着学习到的数据,下面的卷积层依然可以在这些数据基础上进一步学习优化。反向传递也是一样,高速公路的存在,可以确保即使很小的误差也能传递过来,从而避免了梯度消失的发生。说回Resnet50,这个网络就是通过50层卷积的计算,外加残差结构连接,来完成图像分类的。实际上,目前各大公司直接使用Resnet50进行图像分类是很少的,大多数公司会在这个网络的基础上,结合自家公司的业务场景进行改造,或者直接借鉴Resnet50的网络设计思想,重新设计新的网络,以期获得更加高效的识图效果。看到这,你或许能够了解,当我们打开百度识图完成图像识别时,它的背后,可能不是Resnet50这一网络,但肯定是有卷积和残差这两个算法! Resnet ,简单,暴力,有效Resnet50网络的结构其实说简单,它很简单,而且算法思想也很简洁,就是50层卷积的计算,依据卷积局部感受野这一特性,抽取出图像的不同特征,通过最后一层卷积(或者叫做全连接)将图片进行分类。这样的网络设计,分类效果很好,使得 Resnet50 多次在图像分类大赛中夺冠!Resnet50除了大量使用了卷积这一算法之外,一个简单暴力的残差结构的应用,使得该网络无论在训练还是推理过程中,其效果都极为出彩!从此,残差这一结构,受到了人们的关注,以至于,有人开始专门研究不同层之间的残差连接。结合上一章的内容,一句话总结一下Resne50的核心就是:灵魂在于卷积算法和残差结构,而卷积算法的灵魂是特征抽取。好啦,残差结构就介绍到这。后面会继续拆解Resnet50这一网络中的经典算法和思想。欢迎持续关注。
我不是魔法师
对抗生成网络GAN系列——DCGAN简介及人脸图像生成案例
写在前面 前段时间,我已经写过一篇关于GAN的理论讲解,并且结合理论做了一个手写数字生成的小案例, 为唤醒大家的记忆,这里我再来用一句话对GAN的原理进行总结:GAN网络即是通过生成器和判别器的不断相互对抗,不断优化,直到判别器难以判断生成器生成图像的真假。 那么接下来我就要开始讲述DCGAN了喔,读到这里我就默认大家对GAN的原理已经掌握了,开始发车。🚖🚖🚖DCGAN简介 我们先来看一下DCGAN的全称——Deep Convolutional Genrative Adversarial Networks。这大家应该都能看懂叭,就是说这次我们将生成对抗网络和深度学习结合到一块儿了,现在看这篇文章的一些观点其实觉得是很平常的,没有特别出彩之处,但是这篇文章是在16年发布的,在当时能提出一些思想确实是难得。 其实呢,这篇文章的原理和GAN基本是一样的。不同之处只在生成网络模型和判别网络模型的搭建上,因为这篇文章结合了深度学习嘛,所以在模型搭建中使用了卷积操作【注:在上一篇GAN网络模型搭建中我们只使用的全连接层】。介于此,我不会再介绍DCGAN的原理,重点将放在DCGAN网络模型的搭建上。【注:这样看来DCGAN就很简单了,确实也是这样的。但是大家也不要掉以轻心喔,这里还是有一些细节的,我也是花了很长的时间来阅读文档和做实验来理解的,觉得理解差不多了,才来写了这篇文章。】 那么接下来就来讲讲DCGAN生成模型和判别模型的设计,跟我一起来看看叭!!!DCGAN生成模型、判别模型设计 在具体到生成模型和判别模型的设计前,我们先来看论文中给出的一段话,如下图所示:这里我还是翻译一下,如下图所示:上图给出了设计生成模型和判别模型的基本准则,后文我们搭建模型时也是严格按照这个来的。【注意上图黄色背景的分数卷积喔,后文会详细叙述】生成网络模型 话不多说,直接放论文中生成网络结构图,如下:图1 生成网络模型 看到这张图不知道大家是否有几秒的迟疑,反正我当时是这样的,这个结构给人一种熟悉的感觉,但又觉得非常的陌生。好了,不卖关子了,我们一般看到的卷积结构都是特征图的尺寸越来越小,是一个下采样的过程;而这个结构特征图的尺寸越来越大,是一个上采样的过程。那么这个上采样是怎么实现的呢,这就要说到主角分数卷积了。【又可以叫转置卷积(transposed convolution)和反卷积(deconvolution),但是pytorch官方不建议取反卷积的名称,论文中更是说这个叫法是错误的,所以我们尽量不要去用反卷积这个名称,同时后文我会统一用转置卷积来表述,因为这个叫法最多,我认为也是最贴切的】转置卷积理论 这里我将通过一个小例子来讲述转置卷积的步骤,并通过代码来验证这个步骤的正确性。首先我们先来看看转置卷积的步骤,如下:在输入特征图元素间填充s-1行、列0(其中s表示转置卷积的步距,注意这里的步长s和卷积操作中的有些不同)在输入特征图四周填充k-p-1行、列0(其中k表示转置卷积kernel_size大小,p为转置卷积的padding,注意这里的padding和卷积操作中的有些不同)将卷积核参数上下、左右翻转做正常卷积运算(padding=0,s=1) 是不是还是懵逼的状态呢,不用急,现在就通过一个例子来讲述这个过程。首先我们假设输入特征图的尺寸为2*2大小,s=2,k=3,p=0,如下图所示:第一步我们需要在特征图元素间填充s-1=1 行、列 0 (即填充1行0,1列0),变换后特征图如下:第二步我们需要在输入特征图四周填充k-p-1=2 行、列0(即填充2行0,2列0),变换后特征图如下: 第三步我们需要将卷积核上下、左右翻转,得到新的卷积核【卷积核尺寸为k=3】,卷积核变化过程如下:最后一步,我们做正常的卷积即可【注:拿第二步得到的特征图和第三步翻转后得到的卷积核做正常卷积】,结果如下: 至此我们就从完成了转置卷积,从一个2*2大小的特征图变成了一个5*5大小的特征图,如下图所示(忽略了中间步骤): 为了让大家更直观的感受转置卷积的过程,我从Github上down了一个此过程动态图供大家参考,如下:通过上文的讲述,相信你已经对转置卷积的步骤比较清楚了。这时候你就可以试试图1中结构,看看应用上述的方法能否得到对应的结构。需要注意的是,在第一次转置卷积时,使用的参数k=4,s=1,p=0,后面的参数都为k=4,s=2,p=1,如下图所示: 如果你按照我的步骤试了试,可能会发出一些吐槽,这也太麻烦了,我只想计算一下经过转置卷积后特征图的的变化,即知道输入特征图尺寸以及k、s、p算出输出特征图尺寸,这步骤也太复杂了。于是好奇有没有什么公式可以很方便的计算呢?enmmm,我这么说,那肯定有嘛,公式如下图所示:对于上述公式我做3点说明:在转置卷积的官方文档中,参数还有output_padding 和dilation参数也会影响输出特征图的大小,但这里我们没使用,公式就不加上这俩了,感兴趣的可以自己去阅读一下文档,写的很详细。对于stride[0],stride[1]、padding[0],padding[1]、kernel_size[0],kernel_size[1]该怎么理解?其实啊这些都是卷积的基本知识,这些参数设置时可以设置一个整数或者一个含两个整数的元组,*[0]表示在高度上进行操作,*[1]表示在宽度上进行操作。有关这部分在官方文档上也有写,大家可自行查看。为方便大家,我截了一下这部分的图片,如下:这点我带大家宏观的理解一下这个公式,在传统卷积中,往往卷积核k越小、padding越大,得到的特征图尺寸越大;而在转置卷积中,从公式可以看出,卷积核k越大,padding越小,得到的特征图尺寸越大,关于这一点相信你也能从前文所述的转置卷积理论部分有所感受。 现在有了这个公式,大家再去试试叭。转置卷积实验 接下来我将通过一个小实验验证上面的过程,代码如下:import torch
import torch.nn as nn
#转置卷积
def transposed_conv_official():
feature_map = torch.as_tensor([[1, 2],
[0, 1]], dtype=torch.float32).reshape([1, 1, 2, 2])
print(feature_map)
trans_conv = nn.ConvTranspose2d(in_channels=1, out_channels=1,
kernel_size=3, stride=2, bias=False)
trans_conv.load_state_dict({"weight": torch.as_tensor([[1, 0, 1],
[1, 1, 0],
[0, 0, 1]], dtype=torch.float32).reshape([1, 1, 3, 3])})
print(trans_conv.weight)
output = trans_conv(feature_map)
print(output)
def transposed_conv_self():
feature_map = torch.as_tensor([[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 0, 2, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 1, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0]], dtype=torch.float32).reshape([1, 1, 7, 7])
print(feature_map)
conv = nn.Conv2d(in_channels=1, out_channels=1,
kernel_size=3, stride=1, bias=False)
conv.load_state_dict({"weight": torch.as_tensor([[1, 0, 0],
[0, 1, 1],
[1, 0, 1]], dtype=torch.float32).reshape([1, 1, 3, 3])})
print(conv.weight)
output = conv(feature_map)
print(output)
def main():
transposed_conv_official()
print("---------------")
transposed_conv_self()
if __name__ == '__main__':
main()
首先我们先通过transposed_conv_official()函数来封装一个转置卷积过程,可以看到我们的输入为[[1,2],[0,1]],卷积核为[[1,0,1],[1,1,0],[0,0,1]],采用k=3,s=2,p=0进行转置卷积【注:这些参数和我前文讲解转置卷积步骤的用例参数是一致的】,我们来看一下程序输出的结果:可以发现程序输出和我们前面理论计算得到的结果是一致的。 接着我们封装了transposed_conv_self函数,这个函数定义的是一个正常的卷积,输入是理论第2步得到的特征图,卷积核是第三步翻转后得到的卷积核,经过卷积后输出结果如下:结果和前面的一致。 那么通过这个例子就大致证明了转置卷积的步骤确实是我们理论步骤所述。判别模型网络 同样的,直接放出判别模型的网络结构图,如下:【注:这部分原论文中没有给出图例,我自己简单画了一个,没有论文中图示美观,但也大致能表示卷积的过程,望大家见谅】这里我给出程序执行的网络模型结构的结果,这部分就结束了:DCGAN人脸生成实战 这部分我们将来实现一个人脸生成的实战项目,我们先来看一下人脸一步步生成的动画效果,如下图所示:我们可以看到随着迭代次数增加,人脸生成的效果是越来越好的,说句不怎么恰当的话,最后生成的图片是像个人的。看到这里,是不是都兴致勃勃了呢,下面就让我们一起来学学叭。 秉持着授人以鱼不如授人以渔的原则,这里我就不带大家一句一句的分析代码了,都是比较简单的,官方文档写的也非常详细,我再叙述一篇也没有什么意义。哦,对了,这部分代码参考的是pytorch官网上DCGAN的教程,链接如下:DCGAN实战教程 我来简单介绍一下官方教程的使用,点击上文链接会进入下图的界面:这个界面正常滑动就是对这个项目的解释,包括原理、代码及代码运行结果,大家首先要做的应该是阅读一遍这个文档,基本可以解决大部分的问题。那么接下来对于不明白的就可以点击下图中绿框链接修改一些代码来调试我们不懂的问题,这样基本就都会明白了。【框1是google提供的一个免费的GPU运算平台,就类似是云端的jupyter notebook ,但这个需要梯子,大家自备;框2 是下载notebook到本地;框3是项目的Github地址】数据集加载 首先我来说一下数据集的加载,这部分不难,却十分重要。对于我们自己的数据集,我们先用ImageFolder方法创建dataset,代码如下:# Create the dataset
dataset = dset.ImageFolder(root=dataroot,
transform=transforms.Compose([
transforms.Resize(image_size),
transforms.CenterCrop(image_size),
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)),
]))
需要强调的是root=dataroot表示我们自己数据集的路径,在这个路径下必须还有一个子目录。怎么理解呢,我举个例子。比如我现在有一个人脸图片数据集,其存放在文件夹2下面,我们不能将root的路径指定为文件夹2,而是将文件夹2放入一个新文件夹1里面,root的路径指定为文件夹1。对于上面代码的transforms操作做一个简要的概括,transforms.Resize将图片尺寸进行缩放、transforms.CenterCrop对图片进行中心裁剪、transforms.ToTensor、transforms.Normalize最终会将图片数据归一化到[-1,1]之间,这部分不懂的可以参考我的这篇博文:pytorch中的transforms.ToTensor和transforms.Normalize理解🍚🍚🍚 有了dataset后,就可以通过DataLoader方法来加载数据集了,代码如下:# Create the dataloader
dataloader = torch.utils.data.DataLoader(dataset, batch_size=batch_size,
shuffle=True, num_workers=workers)生成模型搭建 接下来我们来说说生成网络模型的搭建,代码如下:不知道大家有没有发现pytorch官网此部分搭建的网络模型和论文中给出的是有一点差别的,这里我修改成了和论文中一样的模型,从训练效果来看,两者差别是不大的。【注:下面代码是我修改过的】# Generator Code
class Generator(nn.Module):
def __init__(self, ngpu):
super(Generator, self).__init__()
self.ngpu = ngpu
self.main = nn.Sequential(
# input is Z, going into a convolution
nn.ConvTranspose2d( nz, ngf * 16, 4, 1, 0, bias=False),
nn.BatchNorm2d(ngf * 16),
nn.ReLU(True),
# state size. (ngf*16) x 4 x 4
nn.ConvTranspose2d(ngf * 16, ngf * 8, 4, 2, 1, bias=False),
nn.BatchNorm2d(ngf * 8),
nn.ReLU(True),
# state size. (ngf*8) x 8 x 8
nn.ConvTranspose2d( ngf * 8, ngf * 4, 4, 2, 1, bias=False),
nn.BatchNorm2d(ngf * 4),
nn.ReLU(True),
# state size. (ngf*4) x 16 x 16
nn.ConvTranspose2d( ngf * 4, ngf * 2, 4, 2, 1, bias=False),
nn.BatchNorm2d(ngf * 2),
nn.ReLU(True),
# state size. (ngf * 2) x 32 x 32
nn.ConvTranspose2d( ngf * 2, nc, 4, 2, 1, bias=False),
nn.Tanh()
# state size. (nc) x 64 x 64
)
def forward(self, input):
return self.main(input) 我觉得这个模型搭建步骤大家应该都是较为清楚的,但我当时对这个第一步即从一个100维的噪声向量如何变成变成一个1024*4*4的特征图还是比较疑惑的。这里就为大家解答一下,我们可以看看在训练过程中传入的噪声代码,即输入为: noise = torch.randn(b_size, nz, 1, 1, device=device),这是一个100*1*1的特征图,这样是不是一下子恍然大悟了呢,那我们的第一步也就是从100*1*1的特征图经转置卷积变成1024*4*4的特征图。模型训练 这部分我在上一篇GAN网络讲解中已经介绍过,但是我没有细讲,这里我想重点讲一下BCELOSS损失函数。【就是二值交叉熵损失函数啦】我们先来看一下pytorch官网对这个函数的解释,如下图所示:import torch
import math
input = torch.randn(3,3)
target = torch.FloatTensor([[0, 1, 1], [1, 1, 0], [0, 0, 0]]) 来看看输入数据和标签的结果: 接着我们要让输入数据经过Sigmoid函数将其归一化到[0,1]之间【BCELOSS函数要求】:m = torch.nn.Sigmoid()
m(input) 输出的结果如下: 最后我们就可以使用BCELOSS函数计算输入数据和标签的损失了:loss =torch.nn.BCELoss()
loss(m(input), target)
输出结果如下:大家记住这个值喔!!! 上文似乎只是介绍了BCELOSS怎么用,具体怎么算的好像并不清楚,下面我们就根据官方给的公式来一步一步手动计算这个损失,看看结果和调用函数是否一致,如下:r11 = 0 * math.log(0.8172) + (1-0) * math.log(1-0.8172)
r12 = 1 * math.log(0.8648) + (1-1) * math.log(1-0.8648)
r13 = 1 * math.log(0.4122) + (1-1) * math.log(1-0.4122)
r21 = 1 * math.log(0.3266) + (1-1) * math.log(1-0.3266)
r22 = 1 * math.log(0.6902) + (1-1) * math.log(1-0.6902)
r23 = 0 * math.log(0.5620) + (1-0) * math.log(1-0.5620)
r31 = 0 * math.log(0.2024) + (1-0) * math.log(1-0.2024)
r32 = 0 * math.log(0.2884) + (1-0) * math.log(1-0.2884)
r33 = 0 * math.log(0.5554) + (1-0) * math.log(1-0.5554)
BCELOSS = -(1/9) * (r11 + r12+ r13 + r21 + r22 + r23 + r31 + r32 + r33) 来看看结果叭:你会发现调用BCELOSS函数和手动计算的结果是一致的,只是精度上有差别,这说明我们前面所说的理论公式是正确的。【注:官方还提供了一种函数——BCEWithLogitsLoss,其和BCELOSS大致一样,只是对输入的数据不需要再调用Sigmoid函数将其归一化到[0,1]之间,感兴趣的可以阅读看看】番外篇——使用服务器训练如何保存图片和训练损失 不知道大家运行这个代码有没有遇到这样尬尴的处境:无法科学上网,用不了google提供的免费GPU自己电脑没有GPU,这个模型很难跑完有服务器,但是官方提供的代码并没有保存最后生成的图片和损失,自己又不会改 前两个我没法帮大家解决,那么我就来说说怎么来保存图片和训练损失。首先来说说怎么保存图片,这个就很简单啦,就使用一个save_image函数即可,具体如下图所示:【在训练部分添加】接下来说说怎么保存训练损失,通过torch.save()方法保存代码如下:#保存LOSS
G_losses = torch.tensor(G_losses)
D_losses = torch.tensor(D_losses)
torch.save(G_losses, 'LOSS\\GL')
torch.save(D_losses, 'LOSS\\DL')代码执行完后,损失保存在LOSS文件夹下,一个文件为GL,一个为DL。这时候我们需要创建一个.py文件来加载损失并可视化,.py文件内容如下:import torch
import torch.utils.data
import matplotlib.pyplot as plt
#绘制LOSS曲线
G_losses = torch.load('F:\\老师发放论文\\经典网络模型\\GAN系列\\DCGAN\\LOSS\\GL')
D_losses = torch.load('F:\\老师发放论文\\经典网络模型\\GAN系列\\DCGAN\\LOSS\\DL')
plt.figure(figsize=(10,5))
plt.title("Generator and Discriminator Loss During Training")
plt.plot(G_losses,label="G")
plt.plot(D_losses,label="D")
plt.xlabel("iterations")
plt.ylabel("Loss")
plt.legend()
plt.show() 最后来看看保存的图片和损失,如下图所示:小结 至此,DCGAN就全部讲完啦,希望大家都能有所收获。有什么问题欢迎评论区讨论交流!!!GAN系列近期还会出cycleGAN的讲解和四季风格转换的demo,后期会考虑出瑕疵检测方面的GAN网络,如AnoGAN等等,敬请期待。🏵🏵🏵
我不是魔法师
神经网络推理加速入门——一个例子看懂流水
之前的两篇文章介绍了流水这一技术,它用来进行程序的性能加速,本篇通过一个生活中的小例子,让大家更直观的了解什么是流水。举个例子早晨从起床到上班出门,我们一般会做以下几件事:刷牙、烧水、喝水、出门。如果正常按顺序去做,可能就是先刷牙,然后烧水,等水烧开了喝水,然后出门。假设做每件事需要的时间如下表,那么整个出门前需要花费的时间为55分钟。但是,如果你稍微会一点时间管理的话,我相信你肯定不会先刷牙、然后烧水的,毕竟,烧水和刷牙没有任何关系,而且烧水的时候,也不需要人在边上看着。于是,就有了下面的做事顺序——起来先烧水,然后在烧水的同时,刷牙,等水烧开了,喝水,出门。这么算下来,总共需要40分钟就能完成。这两种做事顺序最终的结果都是一样的,而且该做的事都做了。区别在于,后面比前面节省了15分钟的时间。这里需要注意2个概念。依赖——后面的事依赖前面的事情。也就是说喝水肯定依赖烧水完成之后才能出门。并行——烧水和刷牙没有任何依赖关系,他俩就可以并行去做。上图中,烧水和刷牙在同一时刻去做了。因此我们可以说,在整个从起床到出门的时间流水线中,烧水和刷牙并行起来了。单纯的一个并行处理,就可以节省15分钟的时间。在理解了并行的概念之后,流水就好理解了。流水排布到底是什么样的继续上面的例子,比如我们起床需要刷两次牙,烧两次水,喝两次水。(当然现实中不会有人这么做,但是在AI神经网络中,重复某个计算是常有的事。感兴趣可以看下 长文解析Resnet50的算法原理中的Rensnet网络结构)。如果刷两次牙,烧两次水,喝两次水,然后出门,我们该怎么管理时间呢?刷牙1和刷牙2肯定是顺序来的,同理烧水1和烧水2,喝水1和喝水2都是需要有顺序的,也就是前面说的依赖。但是刷牙与烧水之间、烧水与喝水之间是有可能并行起来的。比如烧第二次水的时候,我们可以喝第一次的水。上图中,整个左上角的的排布,像一个瀑布一样由上而下,每一行都有两件事同时在做,同一时刻两件事互不影响,但整个系统又井然有序。这种排布,就叫做流水。在指令序列中,将刷牙、烧水、喝水替换成指令,就完成了指令流水;在神经网络中,将刷牙、烧水、喝水替换成AI算法,就完成了算法流水。但是能排流水总是需要满足前面说的两个前提:同一时刻的两件事、或两条指令、或两个算法是解除依赖的,并且可以并行处理的。说到这,有同学可能会问,既然这样,我们弄两个烧水壶同时烧水不就行了么?当然可以,这就是升级硬件喽。双核CPU肯定要比单核CPU性能好,就是这个原因了。排流水是在硬件资源有限的前提下,最大限度的减少程序运行时间,提升整个AI软件栈的性能!Resnet50 中的算法并行在Resnet50的网络结构中,存在很多可并行的算法。上图是截取的Resnet50网络中的一部分,可以看到中间有个加法节点,加法节点有两个输入,分别为左边的卷积1和右边的卷积2(Conv为Convolution的缩写,中文名为卷积)。左边的卷积1依赖于它前面的Relu的输出,而右边的卷积2依赖于很靠前的某个节点的输出,两者并没有实际上的数据依赖,因此,在深度学习编译器对两个节点进行编译调度时,可以将两者进行并行化处理(Parallelization),从而减少一个卷积运算的耗时。总结之所以又花了一篇文章来介绍流水和并行技术,是因为并行和流水技术在AI软件的性能优化中占据了很重要的位置。在硬件资源有限的前提下,我们只能通过软件手段来持续进行AI的加速优化。这里面,更深刻的理解硬件的架构,利用好硬件的优势,编写更加硬件友好的软件代码,才能更加有效的实现AI加速。知己知彼,百战不殆。
我不是魔法师
神经网络推理加速入门——异构编程 2023-04-16 188 阅读5分钟 专栏: 神经网络推理加
这篇简单介绍一个概念——异构编程。上一篇一个例子看懂流水——从指令到算法用一个生活中的小例子,介绍了流水这一概念。在计算资源有限的情况下,我们可以通过软件的流水技术来提升程序性能。但如果你是土豪,不想耗费太多精力去做软件优化,就想砸钱来提升程序性能,有办法么?当然有,性能不够,芯片来凑。正所谓“众人拾柴火焰高”,只要芯片足够多,性能就能飙到顶。异构芯片编程就是这样的一种方式。异构编程所谓异构编程,就是将不同厂家、不同架构的芯片放在一个统一的计算机系统中,通过软件的调度,来实现AI计算的一种方式。比如将x86的CPU和英伟达的GPU放在一起进行编程。人工智能的发展催生了异构编程的火热。主要是因为神经网络中有大量的稠密计算,如果用传统的CPU从头到尾训练一个神经网络,需要耗费大量的算力才能计算出来,就算把CPU累死,估计都算不出来。在神经网络中,矩阵和卷积运算这两种稠密运算,几乎占据了大部分神经网络90%的耗时。因此,有很多公司开发专用的AI芯片(也称ASIC 芯片,Application Specific Integrated Circuit,专用集成电路),专门围绕着卷积运算或者矩阵运算设计硬件单元,来完成运算加速。这就像,给CPU搞了个外挂。英伟达的GPU,谷歌的NPU,寒武纪的MLU等都是类似的ASIC芯片。它们通过PCIe总线和主机相连,从而作为协处理单元完成AI的加速。这和我们扩展电脑内存一个道理。内存不够了,买个内存条插上,内存就够了。算力不够了,买个显卡插上去,算力就够了。正是因为人工智能对于算力的需求,才出现了越来越多的AI芯片公司,并且使得异构编程这一技术越来越为人所知。其中,英伟达的 CUDA 编程就是最广为人知的一种异构编程方式。异构编程不熟悉的同学这时候可能会有些疑问,异构编程难么?首先说,不难。因为大部分做ASIC芯片的厂家都会提供异构编程所需要的驱动或者计算库。如英伟达有 cuDriver 库用来驱动显卡,有 cuDNN 库用来加速常见的深度学习算法,cuFFT 库用来就是FFT相关运算(里面都把算法写好了,我们自己调用即可),还有 TensorRT 专门用来做神经网络推理的优化和网络融合等等。再不济,如果你的神经网络中有个不常见的算法,像英伟达还提供了CUDA编程,这是一种类C语言的编程语言,只需要学会某些简单的标识符和核函数(kernel)的编写,就能写出远超CPU性能的代码。关于CUDA编程的资料网上有很多,感兴趣的同学可以自行去查找。除了英伟达,其他厂家也基本都是这个套路。对外提供提供加速库和驱动来辅助完成异构编程,从而实现AI的计算加速。一次与异构编程相关的面试记得有一次去某公司面试,面试官告诉我,他们公司是基于视觉做智能交通的解决方案的。所谓的解决方案,就是给他们的客户提供一整套的软件+硬件的产品,打包卖给他们。我当时比较好奇,你们也自己做芯片么?面试官说,我们自己不做,但我们会买。国内做AI芯片的企业开发的芯片产品我们都会买,当然GPU也会买,然后做二次开发,在这些硬件上部署我们自己的算法。那你们的解决方案中,卖给用户的硬件,是只有一家的产品,还是会多家混用?面试官:有可能多家的都有,看谁家的性能好用谁家的。...这家做解决方案的公司,会用到不同厂家的芯片,但核心的AI算法是自己的。这是一种典型的异构编程场景:在服务器主机上通过PCIe总线连接多张AI加速卡,实现AI算法计算加速,在云端实现交通场景下路人和车辆的识别。总结异构编程可以认为是一种使用专用芯片对神经网络进行加速的外挂方式。通过这种专用的加速卡,来完成神经网络中相关算法的加速运算。其实,异构编程并不是一个很新的概念。据一个从事手机开发的朋友讲,他们很早之前做手机,手机系统中会有很多不同的芯片,主处理器和协处理之间都会有通信,某些算法在主处理器上跑,某些算法在协处理器上跑,最终完成一个整体运算。这就是一种异构编程,只不过当时他们认为这是理所当然的。而随着人工智能的热潮,异构编程这一概念才越来越多的被人所熟知。从而也成为了AI加速中一个不可或缺的编程方式。好啦,异构编程就简单说到这。
我不是魔法师
Resnet图像识别入门——卷积的特征提取
原创文章请勿转载,如有转载意向请联系作者。前面讲到了卷积这一算法。初识卷积通俗点讲,卷积就是模仿的人眼识图的过程,以“感受野”的视角去扫描图片,从而获取不同区域的图片信息。但其实,这并不是卷积算法的核心思想。卷积的核心,是通过设计多个卷积核,同时对一张图片进行卷积操作,以完成不同特征的提取。本文重点围绕特征提取这一概念,聊一聊卷积。卷积的数学描述不想看数学描述的同学可以略过,不影响后面的阅读。有了之前文章的铺垫,这里说一张图片的尺寸是 [n, h, w, c],应该不陌生了,其中,n 代表的是图片的张数。h 代表的是图片的高度,通俗的讲,高度方向上有多少像素。w 代表的是图片的宽度,通俗的讲,宽度方向上有多少像素。c 代表图片的通道数,例如 RGB 图片,c 等于3。不论是图片,还是卷积核,其数学描述都是具有n,h,w,c四个维度的数据。因此,对于卷积算法而言,输入图片尺寸为 [n, hi, wi, c](下标i代表input,输入),卷积核尺寸为 [kn, kh, kw, c],输出图片尺寸为[n, ho, wo, kn](下标o代表output)。有没有发现:输出图片的channel数与输入图片的channel数不一致,输出图片的channel数与卷积核的个数一致!图片的特征这意味着什么?还记得么,channel 代表的是图片的特征,如果我们想让图片呈现出100个特征,怎么办?用卷积,使用100个卷积核计算,输出图片就具有100个特征。卷积算法,可以通过设计卷积核的个数,随意的提取图片的不同的数量的特征!说的数学一点,卷积算法,就是通过线性变换,将图片映射到特征空间!有点绕?没关系,只需要知道,卷积的核心,是提取图片的特征就行了。那么,特征怎么理解呢?图像特征主要有图像的颜色特征、纹理特征、形状特征和空间关系特征。RGB图片有3个通道,可以说有3个颜色特征,分别为红色,绿色和蓝色。那么纹理特征,形状特征和空间特征又是什么意思呢?很简单,纹理特征就是图片的纹理,比如下面这样。形状特征就是图片中物体的形状,比如下面这样。那么卷积这一算法在神经网络的训练过程中学习到这些特征了么?答案是肯定的!卷积不仅学到了这些特征,而且还学到了更多人们不太好描述的特征,这些特征对于人类来说可能毫无意义,但对于神经网络来说,确实十分重要的。 上图是著名的论文《Visualizing and Understanding Convolutional Networks》中的截图,文中提出通过反卷积这一算法,以可视化的视角,形象的展示卷积神经网络在训练过程中到底看到了什么。所谓反卷积,通俗的理解就是卷积的逆运算。可以看到,随着神经网络深度的不断加深,卷积提取到的特征逐渐清晰起来。由浅层次的纹理特征,逐步到深层次的形状特征!比如,在Layer 4中已经可以看到狗狗的形象!事实上,我们希望神经网络展现出来的是,看到一张画着小猫的图片,里面有一个代表猫的特征通道,该通道最终得分最高。说到这里,即使你是一个AI算法小白,那也应该对卷积有了一些感性的认识。如果你希望了解到更多细节的东西,后面会逐步进行拆解。Resnet网络这篇文章的最后,介绍一下Resnet网络。而图像识别这一系列文章,重点拆解的就是Resnet50网络。这是一个图像分类网络。所谓图像分类,就是它可以将一张图片进行分类。猫就是猫,狗就是狗,飞机就是飞机,大树就是大树。与图像分类不同的,还有图像检测网络。比如物体识别,需要在一张图片上准确的标注出物体是啥以及物体的位置。这些网络里都大量使用了卷积这一算法,因此这些网络我们也可以称之为卷积神经网络(Convolution Neural Network, CNN)。回到Resnet50这一卷积神经网络,这一网络由50个卷积层前后连接而成,因此叫Resnet50,除此之外,还有Resnet18,Resnet101等,大致网络结构相似,只是卷积的层数不同。为什么会有不同的卷积层数呢?神经网络在学习的时候,每一层学习到的特征是不同的,就比如第一层,它的输入只有3个特征,输出有64个特征,至于这64个特征代表的是什么,可能连神经网络自己也说不清,它就只管学习。一直到最后一层有2048个特征。到了最后一层,可以比较形象的这么比喻:最后一层共2048个特征,实际上已经代表了2048种物体的分类了。针对一张图片是猫的原始输入,2048个特征中,只有猫这一特征最后的得分最高,因此,网络会把它推理成猫。这就是卷积算法的核心,特征提取。本文正在参加 人工智能创作者扶持计划
我不是魔法师
全手写resnet50神经网络,分分钟识别“十二生肖“图片
大家好啊,我是董董灿。本项目代码地址:gitee.com/iwaihou/res…前天,我完全手写的算法,并手搭的神经网络,终于成功的识别出一张图片。点击链接查看出猫现场:我出猫了,第一阶段完成!成功地识别出来猫,意味着我搭建的整个流程跑通了。这个流程主要包括了以下几个步骤:图片导入图片预处理:裁减、缩放、归一化输入手写的 Resnet50 中,进行推理,输出 1000 个分类得分从分类文件中查找得分最大的索引对应的类别虽然流程通了,但是识别一张图片要耗时 40 分钟!这推理速度,简直就是龟速。忍不了,于是我优化了一波,识别一张图片的耗时直接从40分钟降到1分多钟。接着我从百度图片中下载了 12 张动物图片 — 组成十二生肖。输入给了我手写的神经网络,希望它能正确的识别出来,顺便也测试一下网络的鲁棒性。下面是十二生肖图片的识别结果,至于怎么优化的,在文章最后会有介绍。 子鼠,识别结果为 mink — 水貂,识别错误,扣一分。不过这个老鼠是不是有那么点像水貂? 丑牛,识别为 ox - 公牛,识别正确!加一分。 而且还是公牛,是根据上翘的尾巴做的区分吗? 寅虎,识别为 tiger - 老虎,识别正确!加一分。这萌萌的老虎特征这么明显,要是识别错,那可以下班了。 卯兔,识别为 hare - 野兔,识别正确!加一分。这站立的姿势,警觉地竖起的耳朵,野兔无疑了。 辰龙,识别成 harp - 竖琴,识别错误,不减分。神经网络识别不出来龙,真的不怪神经网络,因为它的类比分类里就没有中国龙这一类。不过识别成竖琴的话,看这弯曲的身形,还确实是有点神似。巳蛇,识别为 garter snake - 袜带蛇,识别正确!加一分。 虽然我不知道什么事袜带蛇,但它说是,就是了。 午马,识别成 Mexican hairless - 墨西哥无毛犬,识别错误,减一分。 这个不应该的。是不是因为这匹马身上太光滑,没有马儿们标志性的鬃毛么? 但是那飘逸的尾巴,也能说明问题啊。 未羊,识别成 ram - 公羊,算是识别正确吧。我感觉识别出是绵羊更好一些。 申猴,识别出 macaque - 猕猴,识别正确,加一分。看这身形和毛发,这么像猕猴桃,是猕猴了! 酉鸡,识别出 cock - 公鸡,识别正确,加一分。这个是送分题。 戌狗,识别出 Samoyed - 萨摩耶,识别正确,加一分。这一身雪白的气质,小萨独有。 亥猪,识别出 hog - 猪,识别正确,加一分。二师兄小时候,还是很可爱的。优化完神经网络之后,识别这 12 张图片,总共花了十来分钟。十二生肖,共 12 种动物类别,剔除“龙”这一项,因为模型分类中没有,其他11个分类,有两个识别错误,分别是老鼠识别成了水貂,骏马识别成了墨西哥无毛犬。整体识别成功率 81%,还算不错。网络优化下面说一下我是如何将这个网络推理一张图片的耗时,从 40 分钟一张,优化到 1 分钟一张的。在最开始的版本中,整个推理过程,消耗的 90% 的时间集中在卷积运算中。尤其是卷积运算中的乘累加部分。在第一版手写卷积算法时,我为了完全展示卷积的运算逻辑,采用了最原始的多层循环,也就造成了现在的龟速卷积从零手写Resnet50实战—手写龟速卷积。这种多重循环的写法,时间复杂度O(N^6),简直是没法忍受的。于是,我针对卷积的乘累加运算,做了一个简单优化。向量内积代替标量循环优化方法也很简单:将最内层的循环乘累加替换成向量内积。卷积的这种优化方法,在编译器或者指令优化场合很常见:尽可能用向量指令代替循环标量运算。基本原理就是:原来一个时钟周期只可以计算一次循环的一个标量,而现在一个时钟周期可以计算一个向量。在 python 的应用代码中,也可以这么做,利用 numpy 提供的 vdot 函数,直接对两个数组做向量内积。 然后加到一个数值上完成累加。基本就下面一条语句搞定。acc += np.vdot(img[hi_index][wi_index], weight[co_][kh_][kw_])
就这一点改动,整个网络的推理性能,便从原来的 40 分钟,直接降到了 1 分钟。
我不是魔法师
神经网络推理加速入门——循环展开
循环展开如果要我说一个最简单,最有效的,并且人人都能学会的程序优化方法,我估计会投票给Unrooling(译为:循环展开)。循环展开,从名字就能看出来是什么意思:就是把一段循环代码展开来写。听着简单,但具体怎么做呢?先举个简单的例子——高斯年轻的时候,老师曾问他:从1加到100,结果是多少?高斯思考片刻后,给出了5050的答案,让老师大吃一惊。这里也用这个例子,计算1到100的所有数的和。用C语言很容易写出下面的代码:#include "stdio.h"
int main() {
int sum = 0;
for (int i = 1; i <= 100; ++i) {
sum = sum + i;
}
printf("sum = %d\n", sum);
return 0;
}
这个写法很容易想出来:100个数相加,循环100次,每次累加一个数字,最终输出结果 sum = 5050。那么,这段代码,如果用循环展开会怎么写呢?#include "stdio.h"
int main() {
int sum = 0;
for (int i = 1; i <= 100; i+=4) {
sum = sum + i;
sum = sum + (i + 1);
sum = sum + (i + 2);
sum = sum + (i + 3);
}
printf("sum = %d\n", sum);
return 0;
}
如上是循环展开的写法,我们把原来循环100次,每次循环加1个数的写法,写成了循环25次,每次循环加4个数。从结果上看肯定是一致的,最终结果都是5050。但是两者的性能会有较大的差别。性能验证下面实际验证一下两种写法的性能。为了更准确的获取到程序中数字累加的耗时,把100个数字的相加改为10000个数字相加。因为100个数字对于计算机来说太少了,两者的性能差别不大,不容易看出差别。在我的笔记本上进行如下的测试,有以下代码,其中 gettimeofday函数用来获取时间戳。#include "stdio.h"
#include <sys/time.h>
int main() {
int sum = 0;
struct timeval tv0, tv1;
#if 0
printf("do not use unrooling\n");
gettimeofday(&tv0, NULL);
for (int i = 1; i <= 10000; ++i) {
sum = sum + i;
}
#else
printf("use unrooling\n");
gettimeofday(&tv0, NULL);
for (int i = 1; i <= 10000; i+=4) {
sum = sum + i;
sum = sum + (i + 1);
sum = sum + (i + 2);
sum = sum + (i + 3);
}
#endif
gettimeofday(&tv1, NULL);
printf("time = %ld\n", (tv1.tv_usec) - (tv0.tv_usec));
printf("sum = %d\n", sum);
return 0;
}
测试结果如下:可以看到,使用循环展开后,10000个数的累加耗时为14微秒;不使用循环展开,10000个数的累加耗时为24微秒。两者相差10us的耗时,大约相差40%的耗时!这是很夸张的,要知道,在做程序性能优化时,如果能一次优化掉40%的耗时,几乎就算是一个很棒的优化手段了。(感兴趣的同学可以复制上面的代码实际测试一下)为什么循环展开会有效呢这和计算机的取指令以及缓存机制有关。其性能加速的思想是:减少CPU读取指令的失败次数,也就是降低指令的Cache Miss。可能不太好理解,没关系,先看一个实际例子就懂了。双11刚刚过去,你肯定买了不少的快递。假设你买了100件快递,并且这些快递已经放在了快递柜中一个个的小格子里了。现在要去取快递,100件快递至少要开100次的快递柜门,才能把所有的快递取出来。为什么这里说至少100次呢?因为很有可能因为某些原因一次不能成功开启快递柜的门,这些原因可能包括:走错快递柜输错取件码脑袋发蒙,快递没取出来又给关上了连着取100个快递,谁能说得准会发生什么事情呢?但不管怎样,在这个场景下,你需要一件一件地将快递拿出来,但由于上面几个原因,如果运气不好,就会出现好几次打不开快递柜门的情况。那有没有办法优化一下这个问题呢?当然有,那就是让快递员把多个快递放在同一个快递格子里。如上图,比如每4个快递放在一个快递格子里,这样打开一个格子,就一定能取出4个快递。那至少开25次的快递柜门,就能把所有的快递取出来。这很显然,比100次的效率要高不少。说回循环展开,100个快递就需要做100次循环,每个快递的取出就是循环里的一条加法指令。如果不做循环展开,那就需要一个快递一个快递的取(相当于从指令内存iCache中一条指令一条指令的取),并且很有可能取完这一条指令后,并不能成功的取到下一条指令,从而发生Cache Miss现象。但如果我们做了循环展开,相当于把相邻的4条指令绑定了,CPU做一次循环,看到里面一定有四条相邻的指令,就像我们打开快递柜门,里面一定有四个快递一样。CPU取完第一条指令后,能很快地取到第二条指令,从而很快地取到第三条第四条指令。在取完4条指令后,才会有可能发生Cache Miss 现象。连着取四个快递之后Miss一次,和每取一个快递,就Miss一次,浪费的时间肯定是不一样的。循环展开能有效地降低指令的Cache Miss 。这个方法对于程序加速很有效,并且实施起来也很简单,如果你有兴趣,不防在自己的项目中试一试。但是需要说明的是:现在的编译器可能会对你的代码自动做循环展开优化,因此,如果你想试试效果,建议编译的时候不要打开任何优化选项。好啦,循环展开就写到这,欢迎持续关注本系列。
我不是魔法师
对抗生成网络GAN系列——AnoGAN原理及缺陷检测实战
写在前面 随着深度学习的发展,已经有很多学者将深度学习应用到物体瑕疵检测中,如列车钢轨的缺陷检测、医学影像中各种疾病的检测。但是瑕疵检测任务几乎都存在一个共同的难题——缺陷数据太少了。我们使用这些稀少的缺陷数据很难利用深度学习训练一个理想的模型,往往都需要进行数据扩充,即通过某些手段增加我们的缺陷数据。 【数据扩充大家感兴趣自己去了解下,GAN网络也是实现数据扩充的主流手段】 上面说到的方法是基于缺陷数据来训练的,是有监督的学习,学者们在漫长的研究中,考虑能不能使用一种无监督的方法来实现缺陷检测呢?于是啊,AnoGAN就横空出世了,它不需要缺陷数据进行训练,而仅使用正常数据训练模型,关于AnoGAN的细节后文详细介绍。AnoGAN 原理详解 首先我们来看看AnoGAN的全称,即Anomaly Detection with Generative Adversarial Networks,中文是指使用生成对抗网络实现异常检测。这篇论文解决的是医学影像中疾病的检测,由于对医学相关内容不了解,本文将完全将该算法从论文中剥离,只介绍算法原理,而不结合论文进行讲述。接下来就随我一起来看看AnoGAN的原理。其实AnoGAN的原理是很简单的,但是我看网上的资料总是说的摸棱两可,我认为主要原因有两点:其一是没有把AnoGAN的原理分步来叙述,其二是有专家视角,它们认为我们都应该明白,但这对于新手来说理解也确实是有一定难度的。 在介绍AnoGAN的具体原理时,我先来谈谈AnoGAN的出发点,这非常重要,大家好好感受。我们知道,DCGAN是将一个噪声或者说一个潜在变量映射成一张图片,在我们训练DCGAN时,都是使用某一种数据进行的,如[2]中使用的数据都是人脸,那么这些数据都是正常数据,我们从一个潜在变量经DCGAN后生成的图片应该也都是正常图像。AnoGAN的想法就是我能否将一张图片M映射成某个潜在变量呢,这其实是较难做到的。但是我们可以在某个空间不断的查找一个潜在变量,使得这个潜在变量生成的图片与图片M尽可能接近。这就是AnoGAN的出发点,大家可能还不明白这么做的意义,下文为大家详细介绍。☘☘☘ AnoGAN其实是分两个阶段进行的,首先是训练阶段,然后是测试阶段,我们一点点来看:训练阶段 训练阶段仅使用正常的数据训练对抗生成网络。如我们使用手写数字中的数字8作为本阶段的数据进行训练,那么8就是正常数据。训练结束后我们输入一个向量z,生成网络会将z变成8。不知道大家有没有发现其实这阶段就是[2]中的DCGAN呢? 【注意:训练阶段已经训练好GAN网络,后面的测试阶段GAN网络的权重是不在变换的】测试阶段 在训练阶段我们已经训练好了一个GAN网络,在这一阶段我们就是要利用训练好的网络来进行缺陷检测。如现在我们有一个数据6,此为缺陷数据 【训练时使用8进行训练,这里的6即为缺陷数据】 。现在我们要做的就是搜索一个潜在变量并让其生成的图片与图片6尽可能接近,具体实现如下:首先我们会定义一个潜在变量z,然后经过刚刚训练的好的生成网络,得到假图像G(z),接着G(z)和缺陷数据6计算损失,这时候损失往往会比较大,我们不断的更新z值,会使损失不断的减少,在程序中我们可以设置更新z的次数,如更新500次后停止,此时我们认为将如今的潜在变量z送入生成网络得到的假图像已经和图片6非常像了,于是我们将z再次送入生成网络,得到G(z)。【注:由于潜在变量z送入的网络是生成图片8的,尽管通过搜索使G(z)和6尽可能相像,但还是存在一定差距,即它们的损失较大】 最后我们就可以计算G(z)和图片6的损失,记为loss1,并将这个损失作为判断是否有缺陷的重要依据。怎么作为判断是否有缺陷的重要依据呢?我再举个例子大家就明白了,现在在测试阶段我们传入的不是缺陷数据,而是正常的数据8,此时应用相同的方法搜索潜在变量z,然后将最终的z送入生成网络,得到G(z),最后计算G(z)和图片8的损失。 【注:由于潜在变量z送入的网络是生成图片8的,所以最后生成的G(z)可以和数据8很像,即它们的损失较小】 通过以上分析, 我们可以发现当我们在测试阶段传入缺陷图片时最终的损失大,传入正常图片时的损失小,这时候我们就可以设置一个合适的阈值来判断图像是否有缺陷了。 这一段是整个AnoGAN的重点,大家多思考思考,相信你可以理解。我也画了一个此过程的流程图,大家可以参考一下,如下: 读了上文,是不是对AnoGAN大致过程有了一定了解了呢!我觉得大家训练阶段肯定是没问题的啦,就是一个DCGAN网络。测试阶段的难点就在于我们如何定义损失函数来更新z值,我们直接来看论文中此部分的损失,主要分为两部分,分别是Residual Loss和Discrimination Loss,它们定义如下:Residual Loss上式z表示潜在变量,G(z)表示生成的假图像,x表示输入的测试图片。上式表示生成的假图像和输入图片之间的差距。如果生成的图片越接近x,则R(z)越小。Discrimination Loss 上式z表示潜在变量,G(z)表示生成的假图像,x表示输入的测试图片。f()表示将通过判别器,然后取判别器某一层的输出结果。 【注:这里使用的并非判别器的最终输出,而是判别器某层的输出,关于这一点,会在代码讲解时介绍】 这里可以把判别器当作一个特征提取网络,我们将生成的假图片和测试图片都输入判别器,看它们提取到特征的差异。同样,如果生成的图片越接近x,则D(z)越小。 求得R(z)和D(z)后,我们定义它们的线性组合作为最终的损失,如下:到这里,AnoGAN的理论部分都介绍完了喔!!!不知道你理解了多少呢?如果觉得有些地方理解还差点儿意思的话,就来看看下面的代码吧,这回对你理解AnoGAN非常有帮助。AnoGAN代码实战 如果大家和我一样找过AnoGAN代码的话,可能就会和我有一样的感受,那就是太乱了。怎么说呢,我认为从原理上来说,应该很好实现AnoGAN,但是我看Github上的代码写的挺复杂,不是很好理解,有的甚至起着AnoGAN的名字,实现的却是一个简单的DCGAN网络,着实让人有些无语。于是我打算按照自己的思路来实现一个AnoGAN,奈何却出现了各种各样的Bug,正当我心灰意冷时,看到了一篇外文的博客,写的非常对我的胃口,于是按照它的思路实现了AnoGAN。这里我还是想感概一下,我发现很多外文的博客确实写的非常漂亮,我想这是值得我们学习的地方!!代码下载地址 本次我将源码上传到我的Github了,大家可以阅读README文件了解代码的使用,Github地址如下:AnoGAN-pytorch实现 我认为你阅读README文件后已经对这个项目的结构有所了解,我在下文也会帮大家分析分析源码,但更多的时间大家应该自己动手去亲自调试,这样你会有不一样的收获。数据读取 本次使用的数据为mnist手写数字数据集,我们下载的是.csv格式的数据,这种格式方便读取。读取数据代码如下: ## 读取训练集数据 (60000,785)
train = pd.read_csv(".\data\mnist_train.csv",dtype = np.float32)
## 读取测试集数据 (10000,785)
test = pd.read_csv(".\data\mnist_test.csv",dtype = np.float32) 我们可以来看一下mnist数据集的格式是怎样的,先来看看train中的内容,如下: train的shape为(60000,785),其表示训练集中共有60000个数据,即60000张手写数字的图片,每个数据都有785个值。我们来分析一下这785个数值的含义,第一个数值为标签label,表示其表示哪个手写数字,后784个数值为对应数字每个像素的值,手写数字图片大小为28×28,故一共有784个像素值。 解释完训练集数据的含义,那测试集也是一样的啦,只不过数据较少,只有10000条数据,test的内容如下: 大家需要注意的是,上述的训练集和测试集中的数据我们今天并不会全部用到。我们取训练集中的前400个标签为7或8的数据作为AnoGAN的训练集,即7、8都为正常数据。取测试集前600个标签为2、7、8作为测试数据,即测试集中有正常数据(7、8)和异常数据(2),相关代码如下: # 查询训练数据中标签为7、8的数据,并取前400个
train = train.query("label in [7.0, 8.0]").head(400)
# 查询训练数据中标签为7、8的数据,并取前400个
test = test.query("label in [2.0, 7.0, 8.0]").head(600) 可以看看此时的train和test的结果: 在AnoGAN中,我们是无监督的学习,因此是不需要标签的,通过以下代码去除train和test中的标签: # 取除标签后的784列数据
train = train.iloc[:,1:].values.astype('float32')
test = test.iloc[:,1:].values.astype('float32') 去除标签后train和test的结果如下: 可以看出,此时train和test中已经没有了label类,它们的第二个维度也从785变成了784。 最后,我们将train和test reshape成图片的格式,即28×28,代码如下: # train:(400,784)-->(400,28,28)
# test:(600,784)-->(600,28,28)
train = train.reshape(train.shape[0], 28, 28)
test = test.reshape(test.shape[0], 28, 28) 此时,train和test的维度发生变换,如下图所示: 至此,我们的数据读取部分就为大家介绍完了,是不是发现挺简单的呢,加油吧!!!模型搭建模型搭建真滴很简单!!!大家之间看代码吧。生成模型搭建 """定义生成器网络结构"""
class Generator(nn.Module):
def __init__(self):
super(Generator, self).__init__()
def CBA(in_channel, out_channel, kernel_size=4, stride=2, padding=1, activation=nn.ReLU(inplace=True), bn=True):
seq = []
seq += [nn.ConvTranspose2d(in_channel, out_channel, kernel_size=kernel_size, stride=stride, padding=padding)]
if bn is True:
seq += [nn.BatchNorm2d(out_channel)]
seq += [activation]
return nn.Sequential(*seq)
seq = []
seq += [CBA(20, 64*8, stride=1, padding=0)]
seq += [CBA(64*8, 64*4)]
seq += [CBA(64*4, 64*2)]
seq += [CBA(64*2, 64)]
seq += [CBA(64, 1, activation=nn.Tanh(), bn=False)]
self.generator_network = nn.Sequential(*seq)
def forward(self, z):
out = self.generator_network(z)
return out 为了帮助大家理解,我绘制 了生成网络的结构图,如下:判别模型搭建 """定义判别器网络结构"""
class Discriminator(nn.Module):
def __init__(self):
super(Discriminator, self).__init__()
def CBA(in_channel, out_channel, kernel_size=4, stride=2, padding=1, activation=nn.LeakyReLU(0.1, inplace=True)):
seq = []
seq += [nn.Conv2d(in_channel, out_channel, kernel_size=kernel_size, stride=stride, padding=padding)]
seq += [nn.BatchNorm2d(out_channel)]
seq += [activation]
return nn.Sequential(*seq)
seq = []
seq += [CBA(1, 64)]
seq += [CBA(64, 64*2)]
seq += [CBA(64*2, 64*4)]
seq += [CBA(64*4, 64*8)]
self.feature_network = nn.Sequential(*seq)
self.critic_network = nn.Conv2d(64*8, 1, kernel_size=4, stride=1)
def forward(self, x):
out = self.feature_network(x)
feature = out
feature = feature.view(feature.size(0), -1)
out = self.critic_network(out)
return out, feature
同样,为了方便大家理解,我也绘制了判别网络的结构图,如下:这里大家需要稍稍注意一下,判别网络有两个输出,一个是最终的输出,还有一个是第四个CBA BLOCK提取到的特征,这个在理论部分介绍损失函数时有提及。模型训练数据集加载 class image_data_set(Dataset):
def __init__(self, data):
self.images = data[:,:,:,None]
self.transform = transforms.Compose([
transforms.ToTensor(),
transforms.Resize(64, interpolation=InterpolationMode.BICUBIC),
transforms.Normalize((0.1307,), (0.3081,))
])
def __len__(self):
return len(self.images)
def __getitem__(self, idx):
return self.transform(self.images[idx])
# 加载训练数据
train_set = image_data_set(train)
train_loader = DataLoader(train_set, batch_size=batch_size, shuffle=True)
这部分不难,但我提醒大家注意一下这句:transforms.Resize(64, interpolation=InterpolationMode.BICUBIC),即我们采用插值算法将原来2828大小的图片上采样成了6464大小。 【感兴趣的这里也可以不对其进行上采样,这样的话大家需要修改一下上节的模型,可以试试效果喔】加载模型、定义优化器、损失函数等参数 # 指定设备
device = torch.device(args.device if torch.cuda.is_available() else "cpu")
# batch_size默认128
batch_size = args.batch_size
# 加载模型
G = Generator().to(device)
D = Discriminator().to(device)
# 训练模式
G.train()
D.train()
# 设置优化器
optimizerG = torch.optim.Adam(G.parameters(), lr=0.0001, betas=(0.0, 0.9))
optimizerD = torch.optim.Adam(D.parameters(), lr=0.0004, betas=(0.0, 0.9))
# 定义损失函数
criterion = nn.BCEWithLogitsLoss(reduction='mean')训练GAN网络 """
训练
"""
# 开始训练
for epoch in range(args.epochs):
# 定义初始损失
log_g_loss, log_d_loss = 0.0, 0.0
for images in train_loader:
images = images.to(device)
## 训练判别器 Discriminator
# 定义真标签(全1)和假标签(全0) 维度:(batch_size)
label_real = torch.full((images.size(0),), 1.0).to(device)
label_fake = torch.full((images.size(0),), 0.0).to(device)
# 定义潜在变量z 维度:(batch_size,20,1,1)
z = torch.randn(images.size(0), 20).to(device).view(images.size(0), 20, 1, 1).to(device)
# 潜在变量喂入生成网络--->fake_images:(batch_size,1,64,64)
fake_images = G(z)
# 真图像和假图像送入判别网络,得到d_out_real、d_out_fake 维度:都为(batch_size,1,1,1)
d_out_real, _ = D(images)
d_out_fake, _ = D(fake_images)
# 损失计算
d_loss_real = criterion(d_out_real.view(-1), label_real)
d_loss_fake = criterion(d_out_fake.view(-1), label_fake)
d_loss = d_loss_real + d_loss_fake
# 误差反向传播,更新损失
optimizerD.zero_grad()
d_loss.backward()
optimizerD.step()
## 训练生成器 Generator
# 定义潜在变量z 维度:(batch_size,20,1,1)
z = torch.randn(images.size(0), 20).to(device).view(images.size(0), 20, 1, 1).to(device)
fake_images = G(z)
# 假图像喂入判别器,得到d_out_fake 维度:(batch_size,1,1,1)
d_out_fake, _ = D(fake_images)
# 损失计算
g_loss = criterion(d_out_fake.view(-1), label_real)
# 误差反向传播,更新损失
optimizerG.zero_grad()
g_loss.backward()
optimizerG.step()
## 累计一个epoch的损失,判别器损失和生成器损失分别存放到log_d_loss、log_g_loss中
log_d_loss += d_loss.item()
log_g_loss += g_loss.item()
## 打印损失
print(f'epoch {epoch}, D_Loss:{log_d_loss / 128:.4f}, G_Loss:{log_g_loss / 128:.4f}')
## 展示生成器存储的图片,存放在result文件夹下的G_out.jpg
z = torch.randn(8, 20).to(device).view(8, 20, 1, 1).to(device)
fake_images = G(z)
torchvision.utils.save_image(fake_images,f"result\G_out.jpg") 这部分就是训练一个DCGAN网络,到目前为止其实也都可以认为是DCGAN的内容。我们可以来看一下输出的G_out.jpg图片:这里我们可以看到训练是有了效果的,但会发现不是特别好。我分析有两点原因,其一是我们的模型不好,且GAN本身就容易出现模式崩溃的问题;其二是我们的数据选取的少,在数据读取时训练集我们只取了前400个数据,但实际上我们一共可以取12116个,大家可以尝试增加数据,我想数据多了后效果肯定比这个好,大家快去试试吧!!!缺陷检测 这部分才是AnoGAN的重点,首先我们先定义损失的计算,如下: ## 定义缺陷计算的得分
def anomaly_score(input_image, fake_image, D):
# Residual loss 计算
residual_loss = torch.sum(torch.abs(input_image - fake_image), (1, 2, 3))
# Discrimination loss 计算
_, real_feature = D(input_image)
_, fake_feature = D(fake_image)
discrimination_loss = torch.sum(torch.abs(real_feature - fake_feature), (1))
# 结合Residual loss和Discrimination loss计算每张图像的损失
total_loss_by_image = 0.9 * residual_loss + 0.1 * discrimination_loss
# 计算总损失,即将一个batch的损失相加
total_loss = total_loss_by_image.sum()
return total_loss, total_loss_by_image, residual_loss
大家可以对比一下理论部分损失函数的介绍,看看是不是一样的呢。 接着我们就需要不断的搜索潜在变量z了,使其与输入图片尽可能接近,代码如下: # 加载测试数据
test_set = image_data_set(test)
test_loader = DataLoader(test_set, batch_size=5, shuffle=False)
input_images = next(iter(test_loader)).to(device)
# 定义潜在变量z 维度:(5,20,1,1)
z = torch.randn(5, 20).to(device).view(5, 20, 1, 1)
# z的requires_grad参数设置成Ture,让z可以更新
z.requires_grad = True
# 定义优化器
z_optimizer = torch.optim.Adam([z], lr=1e-3)
# 搜索z
for epoch in range(5000):
fake_images = G(z)
loss, _, _ = anomaly_score(input_images, fake_images, D)
z_optimizer.zero_grad()
loss.backward()
z_optimizer.step()
if epoch % 1000 == 0:
print(f'epoch: {epoch}, loss: {loss:.0f}')
执行完上述代码后,我们得到了一个较理想的潜在变量,这时候再用z来生成图片,并基于生成图片和输入图片来计算损失,同时,我们也保存了输入图片和生成图片,并打印了它们之前的损失,相关代码如下: fake_images = G(z)
_, total_loss_by_image, _ = anomaly_score(input_images, fake_images, D)
print(total_loss_by_image.cpu().detach().numpy())
torchvision.utils.save_image(input_images, f"result/Nomal.jpg")
torchvision.utils.save_image(fake_images, f"result/ANomal.jpg")
我们可以来看看最后的结果哦,如下:可以看到,当输入图像为2时(此为缺陷),生成的图像也是8,它们的损失最高为464040.44。这时候如果我们设置一个阈值为430000,高于这个阈值的即为异常图片,低于这个阈值的即为正常图片,那么我们是不是就可以通过AnoGAN来实现缺陷的检测了呢!!!总结 到这里,AnoGAN的所有内容就介绍完了,大家好好感受感受它的思想,其实是很简单的,但是又非常巧妙。最后我不知道大家有没有发现AnoGAN一个非常明显的缺陷,那就是我们每次在判断异常时要不断的搜索潜在变量z,这是非常耗时的。而很多任务对时间的要求还是很高的,所以AnoGAN还有许多可以改进的地方,后续博文我会带大家继续学习GAN网络在缺陷检测中的应用,我们下期见。
我不是魔法师
深度学习模型部署篇——从0部署深度学习分类模型(二)
使用模型部署-->预测摄像头实时画面其实不管是预测一张图像还是实时对视频进行预测,原理都是差不多的,我们一起来看一下。🧃🧃🧃首先,导入一些必要的工具包:import os
import cv2
import numpy as np
import pandas as pd
import torch
import torch.nn.functional as F
from torchvision import transforms
import onnxruntime
from PIL import Image, ImageFont, ImageDraw
import matplotlib.pyplot as plt
因为我们想要在图像上展示中文,需要下载中文字体并加载:# 下载中文字体文件
wget https://zihao-openmmlab.obs.cn-east-3.myhuaweicloud.com/20220716-mmclassification/dataset/SimHei.ttf
# 导入中文字体,指定字体大小
font = ImageFont.truetype('SimHei.ttf', 32)
接着我们载入ONNX和ImageNet1000的分类标签,这些和上一节都是一致的:# 载入 onnx 模型
ort_session = onnxruntime.InferenceSession('resnet18_imagenet.onnx')
# 载入ImageNet 1000图像分类标签
df = pd.read_csv('imagenet_class_index.csv')
idx_to_labels = {}
for idx, row in df.iterrows():
idx_to_labels[row['ID']] = row['Chinese']
同时,也定义一个预处理的函数,和第一节是一致的:# 测试集图像预处理-RCTN:缩放裁剪、转 Tensor、归一化
test_transform = transforms.Compose([transforms.Resize(256),
transforms.CenterCrop(256),
transforms.ToTensor(),
transforms.Normalize(
mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225])
])
这些算是准备工作,然后我们来思考一下如何来做摄像头的实时预测。其实很容易啦,我们会一帧一帧的处理视频中的图像,而一帧图像的处理过程是不是就和我们上一节讲的一致呢,为了方便调用,我们把处理一帧图像的方法封装起来,如下:# 处理一帧图像的函数
def process_frame(img_bgr):
# 记录该帧开始处理的时间
start_time = time.time()
img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB) # BGR转RGB
img_pil = Image.fromarray(img_rgb) # array 转 PIL
## 预处理
input_img = test_transform(img_pil) # 预处理
input_tensor = input_img.unsqueeze(0).numpy()
## onnx runtime 预测
ort_inputs = {'input': input_tensor} # onnx runtime 输入
pred_logits = ort_session.run(['output'], ort_inputs)[0] # onnx runtime 输出
pred_logits = torch.tensor(pred_logits)
pred_softmax = F.softmax(pred_logits, dim=1) # 对 logit 分数做 softmax 运算
## 解析图像分类预测结果
n = 5
top_n = torch.topk(pred_softmax, n) # 取置信度最大的 n 个结果
pred_ids = top_n[1].cpu().detach().numpy().squeeze() # 解析出类别
confs = top_n[0].cpu().detach().numpy().squeeze() # 解析出置信度
## 在图像上写中文
draw = ImageDraw.Draw(img_pil)
for i in range(len(confs)):
pred_class = idx_to_labels[pred_ids[i]]
# 写中文:文字坐标,中文字符串,字体,rgba颜色
text = '{:<15} {:>.3f}'.format(pred_class, confs[i]) # 中文字符串
draw.text((50, 100 + 50 * i), text, font=font, fill=(255, 0, 0, 1))
img_rgb = np.array(img_pil) # PIL 转 array
img_bgr = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2BGR) # RGB转BGR
# 记录该帧处理完毕的时间
end_time = time.time()
# 计算每秒处理图像帧数FPS
FPS = 1/(end_time - start_time)
# 图片,添加的文字,左上角坐标,字体,字体大小,颜色,线宽,线型
img_bgr = cv2.putText(img_bgr, 'FPS '+str(int(FPS)), (50, 80), cv2.FONT_HERSHEY_SIMPLEX, 2, (255, 0, 255), 4, cv2.LINE_AA)
return img_bgr
这个函数和上节的过程基本是一致的,不过这里我们添加了计算FPS的代码,并将其显示在图像上:start_time = time.time() # 图像处理开始
"""
---图像处理过程中
---图像处理过程中
---图像处理过程中
"""
end_time = time.time() # 图像处理结束
# 计算FPS
FPS = 1/(end_time - start_time)
FPS表示一秒钟处理多少帧的图像,比如(end_time - start_time)=0.5 ,则FPS=1/0.5=2,表示每秒钟能处理两帧图像,显然FPS值越大,表示处理速度越快。定义好处理一帧图像的函数,我们就可以调用摄像头进行实时预测了:import cv2
import time
# 获取摄像头,传入0表示获取系统默认摄像头
cap = cv2.VideoCapture(0)
# 打开cap
cap.open(0)
# 无限循环,直到break被触发
while cap.isOpened():
# 获取画面
success, frame = cap.read()
if not success: # 如果获取画面不成功,则退出
print('获取画面不成功,退出')
break
## 逐帧处理
frame = process_frame(frame)
# 展示处理后的三通道图像
cv2.imshow('my_window',frame)
key_pressed = cv2.waitKey(60) # 每隔多少毫秒毫秒,获取键盘哪个键被按下
# print('键盘上被按下的键:', key_pressed)
if key_pressed in [ord('q'),27]: # 按键盘上的q或esc退出(在英文输入法下)
break
# 关闭摄像头
cap.release()
# 关闭图像窗口
cv2.destroyAllWindows()
我们可以来看一下我们处理的结果,如下图所示:可以看到,模型可以检测到我们的物体。当然了,我的背景比较杂,所以识别率并没有很高。但是本节我也不关注识别率,让我们来看看FPS是多少,可以看到,大概是18-20左右。 不使用模型部署-->预测摄像头实时画面上节展示了使用模型部署来预测摄像头实时画面的案例,FPS大概在18左右,这次我们不使用模型部署来看看FPS大概是多少。可以看到不使用模型部署时,FPS只在9左右,前前后后相差了一倍之多,所以说在工业实践中模型部署还是非常有必要的。我也贴出这部分的代码叭,如下:import numpy as np
import pandas as pd
from PIL import Image, ImageFont, ImageDraw
import cv2
import time
import torch
import torch.nn.functional as F
from torchvision import models
# 有 GPU 就用 GPU,没有就用 CPU
device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
print('device:', device)
# 导入中文字体,指定字号
font = ImageFont.truetype('SimHei.ttf', 32)
model = models.resnet18(pretrained=True)
model = model.eval()
model = model.to(device)
# 载入ImageNet 1000图像分类标签
df = pd.read_csv('imagenet_class_index.csv')
idx_to_labels = {}
for idx, row in df.iterrows():
idx_to_labels[row['ID']] = row['Chinese']
from torchvision import transforms
# 测试集图像预处理-RCTN:缩放裁剪、转 Tensor、归一化
test_transform = transforms.Compose([transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize(
mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225])
])
# 处理帧函数
def process_frame(img):
# 记录该帧开始处理的时间
start_time = time.time()
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # BGR转RGB
img_pil = Image.fromarray(img_rgb) # array 转 PIL
input_img = test_transform(img_pil).unsqueeze(0).to(device) # 预处理
pred_logits = model(input_img) # 执行前向预测,得到所有类别的 logit 预测分数
pred_softmax = F.softmax(pred_logits, dim=1) # 对 logit 分数做 softmax 运算
top_n = torch.topk(pred_softmax, 5) # 取置信度最大的 n 个结果
pred_ids = top_n[1].cpu().detach().numpy().squeeze() # 解析预测类别
confs = top_n[0].cpu().detach().numpy().squeeze() # 解析置信度
# 使用PIL绘制中文
draw = ImageDraw.Draw(img_pil)
# 在图像上写字
for i in range(len(confs)):
pred_class = idx_to_labels[pred_ids[i]]
text = '{:<15} {:>.3f}'.format(pred_class, confs[i])
# 文字坐标,中文字符串,字体,bgra颜色
draw.text((50, 100 + 50 * i), text, font=font, fill=(255, 0, 0, 1))
img = np.array(img_pil) # PIL 转 array
img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR) # RGB转BGR
# 记录该帧处理完毕的时间
end_time = time.time()
# 计算每秒处理图像帧数FPS
FPS = 1 / (end_time - start_time)
# 图片,添加的文字,左上角坐标,字体,字体大小,颜色,线宽,线型
img = cv2.putText(img, 'FPS ' + str(int(FPS)), (50, 80), cv2.FONT_HERSHEY_SIMPLEX, 2, (255, 0, 255), 4,
cv2.LINE_AA)
return img
#获取摄像头,传入0表示获取系统默认摄像头
cap = cv2.VideoCapture(1)
# 打开cap
cap.open(0)
# 无限循环,直到break被触发
while cap.isOpened():
# 获取画面
success, frame = cap.read()
if not success:
print('Error')
break
## !!!处理帧函数
frame = process_frame(frame)
# 展示处理后的三通道图像
cv2.imshow('my_window', frame)
if cv2.waitKey(1) in [ord('q'), 27]: # 按键盘上的q或esc退出(在英文输入法下)
break
# 关闭摄像头
cap.release()
# 关闭图像窗口
cv2.destroyAllWindows()
使用模型部署-->预测视频文件前面两个小节是调用PC的摄像头进行预测,但是有时候我们会对视频进行预测,这该怎么做呢,我们一起来看看叭。这里我只展示处理视频的函数,一些包和前面的准备工作就不在赘述了:定义 generate_video 函数,并设置 input_path 参数,默认为 "videos.mp4"。该函数用于处理视频。 def generate_video(input_path='videos.mp4'):定义一个函数用于处理视频,后面直接调用即可,调用代码为:generate_video(input_path='video_4.mp4')解析输入视频的文件名,并生成输出视频的路径。打印提示信息。filehead = input_path.split('/')[-1]
output_path = "out-" + filehead
print('视频开始处理',input_path)使用 cv2.VideoCapture 打开视频文件,并计算视频的总帧数。这里使用一个循环遍历获取每一帧,计算帧数,并在循环结束后释放资源。cap = cv2.VideoCapture(input_path)
frame_count = 0
while(cap.isOpened()):
success, frame = cap.read()
frame_count += 1
if not success:
break
cap.release()
print('视频总帧数为',frame_count)
再次使用 cv2.VideoCapture 打开视频文件,并获取视频的帧大小。cap = cv2.VideoCapture(input_path)
frame_size = (cap.get(cv2.CAP_PROP_FRAME_WIDTH), cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
创建一个 cv2.VideoWriter 对象 out,用于将处理后的帧写入输出视频文件。设置视频编码器为 "mp4v",帧率为输入视频的帧率。此处还可以根据需要设置输出视频的参数。fourcc = cv2.VideoWriter_fourcc(*'mp4v')
fps = cap.get(cv2.CAP_PROP_FPS)
out = cv2.VideoWriter(output_path, fourcc, fps, (int(frame_size[0]), int(frame_size[1])))使用 tqdm 库创建一个进度条对象 pbar,并将总帧数设置为进度条的总量。
with tqdm(total=frame_count) as pbar:
# 进入主循环,遍历每一帧图像。在每次迭代中,使用 cap.read() 读取视频的下一帧图像,并检查是否成功读取。
while(cap.isOpened()):
success, frame = cap.read()
if not success:
break
调用 process_frame 函数对每一帧图像进行处理。如果处理过程中出现错误,会打印错误信息。try:
frame = process_frame(frame)
except Exception as error:
print('报错!', error)
pass如果读取和处理图像成功,将处理后的帧图像写入输出视频文件。if success == True:
out.write(frame)更新进度条的进度。pbar.update(1)循环结束后,释放所有使用的资源,关闭窗口,并保存输出视频。cv2.destroyAllWindows()
out.release()
cap.release()
print('视频已保存', output_path)让我们来看看结果叭。使用模型部署-->部署自定义的花5分类模型前文讲述的都是我们用官方训练好的模型进行部署,那么针对我们自己的数据集,我们训练了一个分类模型,我们应该如何进行部署呢?让我们一起来看看叭~~~🍄🍄🍄看完上述博客,我们知道我们训练得到了名为AlexNet.pth的权重文件,这个是我们部署模型的关键。有了AlexNet.pth权重文件,我们要对其进行加载,对模型保存和加载不清楚的请阅读下方博客:pytorch模型保存、加载与续训练我们使用的保存模型的方式是通过官方推荐的方式一进行保存的,因此我们要通过方式一来加载模型。实列化AlexNet模型 搭建网络模型 class AlexNet(nn.Module):
def __init__(self, num_classes=5):
super(AlexNet, self).__init__()
self.features = nn.Sequential(
nn.Conv2d(3, 96, 11, 4, padding=0),
nn.ReLU(inplace=True),
nn.MaxPool2d(3, 2, padding=0),
nn.Conv2d(96, 256, 5, padding=2),
nn.ReLU(inplace=True),
nn.MaxPool2d(3, 2, padding=0),
nn.Conv2d(256, 384, 3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(384, 384, 3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(384, 256, 3, padding=1),
nn.ReLU(inplace=True),
nn.MaxPool2d(3, 2, padding=0), ) self.classifier = nn.Sequential(
nn.Flatten(), nn.Dropout(),
nn.Linear(256 * 6 * 6, 4096),
nn.ReLU(inplace=True),
nn.Dropout(),
nn.Linear(4096, 4096),
nn.ReLU(inplace=True),
nn.Linear(4096, num_classes)
)
def forward(self, x):
x = self.features(x)
x = self.classifier(x)
return x
model = AlexNet(num_classes=5).to(device)2.加载模型并设置为推理模式# 加载模型
model.load_state_dict(torch.load('checkpoint/AlexNet.pth', map_location='cpu'))
# 设置为推理模式
model = model.eval().to(device)模型加载完成后,我们将模型转成ONNX中间格式:x = torch.randn(1, 3, 255, 227).to(device)
output = model(x)
with torch.no_grad():
torch.onnx.export(
model, # 要转换的模型
x, # 模型的任意一组输入
'Alex_flower5.onnx', # 导出的 ONNX 文件名
opset_version=11, # ONNX 算子集版本
input_names=['input'], # 输入 Tensor 的名称(自己起名字)
output_names=['output'] # 输出 Tensor 的名称(自己起名字)
)
本地产生ONNX格式文件:双击查看一下:到这里我们已经得到了花的五分类的ONNX中间格式,后面的操作就和前一节一样了,即加载ONNX模型并通过ONNX Runtime推理引擎来进行模型推理,这里我就不过多叙述了。我从网上下载的推理图像如下【这个是郁金香花】:推理完成后我们同样可以得到预测类别pred_ids和预测置信度confs,她们的值如下:我们可以载入类别和对应ID展示一下:# 载入类别和对应 ID
idx_to_labels = np.load('idx_to_labels1.npy', allow_pickle=True).item()
for i in range(n):
class_name = idx_to_labels[pred_ids[i]] # 获取类别名称
confidence = confs[i] * 100 # 获取置信度
text = '{:<6} {:>.3f}'.format(class_name, confidence)
print(text)
其中,idx_to_labels1.npy的文件内容如下:最终输出的结果如下,可以看出,正确的预测出来输入图片为郁金香。
我不是魔法师
对抗生成网络GAN系列——EGBAD原理及缺陷检测实战
EGBAD原理详解 一直在说EGBAD,大家肯定一脸懵,到底什么才是EGBAD了?我们先来看看它的英文全称,即EFFICIENT GAN-BASED ANOMALY DETECTION ,中文译为基于GAN的高效异常检测。通过说明EGBAD的字面含义,相信大家知道了EGBAD是用来干什么的了。没错,它也是用于缺陷检测的网络,是对AnoGAN的优化。至于具体是怎么优化的,且听下文分解。 我们先来回顾一下AnoGAN是怎么设计的?AnoGAN分为训练和测试两个阶段进行,训练阶段使用正常数据训练一个DCGAN网络,在测试阶段,固定训练阶段的网络权重,不断更新潜在变量z,使得由z生成的假图像尽可能接近真实图片。【如果你对这个过程不熟悉的话,建议看看[1]中内容喔】 在介绍EGBAD是怎么设计的前,我们先来看看EGBAD主要解决了AnoGAN什么问题?其实这点我在写在前面已经提及,AnoGAN在测试阶段要不断搜索潜在变量z,这消耗了大量时间,EGBAD的提出就是为了解决AnoGAN时间消耗大的问题。接着我们来就来看看EGBAD具体是怎么做的呢?EGBAD也分为训练和测试两个阶段进行。在训练阶段,不仅要训练生成器和判别器,还会定义一个编码器(encoder)结构并对其训练,encoder主要用于将输入图像通过网络转变成一个潜在变量。在测试阶段,冻结训练阶段的所有权重,之后通过encoder将输入图像变为潜在变量,最后再将潜在变量送入生成器,生成假图像。可以发现EGBAD没有在测试阶段搜索潜在变量,而是直接通过一个encoder结构将输入图像转变成潜在变量,这大大节省了时间成本。 关于EGBAD训练过程模型示意图如下:【测试过程很简单啦,就不介绍了】EGBAD代码实战代码下载地址 同样,我将此部分的源码上传到Github上了,大家可以阅读README文件了解代码的使用,Github地址如下:EGBAD-pytorch实现 我认为你阅读README文件后已经对这个项目的结构有所了解,我在下文也会帮大家分析分析源码,但更多的时间大家应该自己动手去亲自调试,这样你会有不一样的收获。数据读取 这部分和AnoGAN中完全一致,就不带大家一行行看调试结果了,这里直接上代码:#导入相关包
import numpy as np
import pandas as pd
"""
mnist数据集读取
"""
## 读取训练集数据 (60000,785)
train = pd.read_csv(".\data\mnist_train.csv",dtype = np.float32)
## 读取测试集数据 (10000,785)
test = pd.read_csv(".\data\mnist_test.csv",dtype = np.float32)
# 查询训练数据中标签为7、8的数据,并取前400个
train = train.query("label in [7.0, 8.0]").head(400)
# 查询训练数据中标签为7、8的数据,并取前400个
test = test.query("label in [2.0, 7.0, 8.0]").head(600)
# 取除标签后的784列数据
train = train.iloc[:,1:].values.astype('float32')
test = test.iloc[:,1:].values.astype('float32')
# train:(400,784)-->(400,28,28)
# test:(600,784)-->(600,28,28)
train = train.reshape(train.shape[0], 28, 28)
test = test.reshape(test.shape[0], 28, 28)模型搭建 这部分大家就潜心修行,慢慢调试代码吧,我也会给出每个模型的结构图辅助大家,就让我们一起来看看吧☘☘☘生成模型搭建"""定义生成器网络结构"""
class Generator(nn.Module):
def __init__(self):
super(Generator, self).__init__()
def CBA(in_channel, out_channel, kernel_size=4, stride=2, padding=1, activation=nn.ReLU(inplace=True), bn=True):
seq = []
seq += [nn.ConvTranspose2d(in_channel, out_channel, kernel_size=kernel_size, stride=stride, padding=padding)]
if bn is True:
seq += [nn.BatchNorm2d(out_channel)]
seq += [activation]
return nn.Sequential(*seq)
seq = []
seq += [CBA(20, 64*8, stride=1, padding=0)]
seq += [CBA(64*8, 64*4)]
seq += [CBA(64*4, 64*2)]
seq += [CBA(64*2, 64)]
seq += [CBA(64, 1, activation=nn.Tanh(), bn=False)]
self.generator_network = nn.Sequential(*seq)
def forward(self, z):
out = self.generator_network(z)
return out 生成模型的搭建其实很AnoGAN是完全一样的,我也给出生成网络的结构图,如下:编码器模型搭建"""定义编码器结构"""
class encoder(nn.Module):
def __init__(self):
super(encoder, self).__init__()
def CBA(in_channel, out_channel, kernel_size=4, stride=2, padding=1, activation=nn.LeakyReLU(0.1, inplace=True)):
seq = []
seq += [nn.Conv2d(in_channel, out_channel, kernel_size=kernel_size, stride=stride, padding=padding)]
seq += [nn.BatchNorm2d(out_channel)]
seq += [activation]
return nn.Sequential(*seq)
seq = []
seq += [CBA(1, 64)]
seq += [CBA(64, 64*2)]
seq += [CBA(64*2, 64*4)]
seq += [CBA(64*4, 64*8)]
seq += [nn.Conv2d(64*8, 512, kernel_size=4, stride=1)]
self.feature_network = nn.Sequential(*seq)
self.embedding_network = nn.Linear(512, 20)
def forward(self, x):
feature = self.feature_network(x).view(-1, 512)
z = self.embedding_network(feature)
return z 这部分其实也很简单,就是一系列卷积的堆积,编码器的结构图如下:判别模型搭建"""定义判别器网络结构"""
class Discriminator(nn.Module):
def __init__(self):
super(Discriminator, self).__init__()
def CBA(in_channel, out_channel, kernel_size=4, stride=2, padding=1, activation=nn.LeakyReLU(0.1, inplace=True)):
seq = []
seq += [nn.Conv2d(in_channel, out_channel, kernel_size=kernel_size, stride=stride, padding=padding)]
seq += [nn.BatchNorm2d(out_channel)]
seq += [activation]
return nn.Sequential(*seq)
seq = []
seq += [CBA(1, 64)]
seq += [CBA(64, 64*2)]
seq += [CBA(64*2, 64*4)]
seq += [CBA(64*4, 64*8)]
seq += [nn.Conv2d(64*8, 512, kernel_size=4, stride=1)]
self.feature_network = nn.Sequential(*seq)
seq = []
seq += [nn.Linear(20, 512)]
seq += [nn.BatchNorm1d(512)]
seq += [nn.LeakyReLU(0.1, inplace=True)]
self.latent_network = nn.Sequential(*seq)
self.critic_network = nn.Linear(1024, 1)
def forward(self, x, z):
feature = self.feature_network(x)
feature = feature.view(feature.size(0), -1)
latent = self.latent_network(z)
out = self.critic_network(torch.cat([feature, latent], dim=1))
return out, feature
虽然判别器有两个输入,两个输出,但是结构也非常清晰,如下图所示:在模型搭建部分我还想提一点我们需要注意的地方,一般我们设计好一个网络结构后,我们往往会先设计一个tensor来作为网络的输入,看看网络输出是否是是我们预期的,如果是,我们再进行下一步,否则我们需要调整我们的结构以适应我们的输入。通常情况下,tensor的batch维度设为1就行,但是这里设置成1就会报错,提示我们需要设置一个batch大于1的整数,当将batch设置为2时,程序正常,至于产生这种现象的原因我目前也不是很清楚,大家注意一下,知道的也烦请告知一下。关于调试网络结构是否正常的代码如下,仅供参考:if __name__ == '__main__':
x = torch.ones((2, 1, 64, 64))
z = torch.ones((2, 20, 1, 1))
Generator = Generator()
Discriminator = Discriminator()
encoder = encoder()
output_G = Generator(z)
output_D1, output_D2= Discriminator(x, z.view(2, -1))
output_E = encoder(x)
print(output_G.shape)
print(output_D1.shape)
print(output_D2.shape)
print(output_E.shape)模型训练数据集加载 这部分和AnoGAN一致,注意最终输入网络的图片尺寸都上采样成了64×64.class image_data_set(Dataset):
def __init__(self, data):
self.images = data[:,:,:,None]
self.transform = transforms.Compose([
transforms.ToTensor(),
transforms.Resize(64, interpolation=InterpolationMode.BICUBIC),
transforms.Normalize((0.1307,), (0.3081,))
])
def __len__(self):
return len(self.images)
def __getitem__(self, idx):
return self.transform(self.images[idx])
# 加载训练数据
train_set = image_data_set(train)
train_loader = DataLoader(train_set, batch_size=batch_size, shuffle=True)加载模型、定义优化器、损失函数等参数 这部分也基本和AnoGAN类似,只不过添加了encoder网络的定义和优化器定义部分,如下:# 指定设备
device = torch.device(args.device if torch.cuda.is_available() else "cpu")
# batch_size默认128
batch_size = args.batch_size
# 加载模型
G = Generator().to(device)
D = Discriminator().to(device)
E = Encoder().to(device)
# 训练模式
G.train()
D.train()
E.train()
# 设置优化器
optimizerG = torch.optim.Adam(G.parameters(), lr=0.0001, betas=(0.0, 0.9))
optimizerD = torch.optim.Adam(D.parameters(), lr=0.0001, betas=(0.0, 0.9))
optimizerE = torch.optim.Adam(E.parameters(), lr=0.0004, betas=(0.0,0.9))
# 定义损失函数
criterion = nn.BCEWithLogitsLoss(reduction='mean')训练GAN网络"""
训练
"""
# 开始训练
for epoch in range(args.epochs):
# 定义初始损失
log_g_loss, log_d_loss, log_e_loss = 0.0, 0.0, 0.0
for images in train_loader:
images = images.to(device)
## 训练判别器 Discriminator
# 定义真标签(全1)和假标签(全0) 维度:(batch_size)
label_real = torch.full((images.size(0),), 1.0).to(device)
label_fake = torch.full((images.size(0),), 0.0).to(device)
# 定义潜在变量z 维度:(batch_size,20,1,1)
z = torch.randn(images.size(0), 20).to(device).view(images.size(0), 20, 1, 1).to(device)
# 潜在变量喂入生成网络--->fake_images:(batch_size,1,64,64)
fake_images = G(z)
# 使用编码器将真实图像变成潜在变量 image:(batch_size, 1, 64, 64)-->z_real:(batch_size, 20)
z_real = E(images)
# 真图像和假图像送入判别网络,得到d_out_real、d_out_fake 维度:都为(batch_size,1)
d_out_real, _ = D(images, z_real)
d_out_fake, _ = D(fake_images, z.view(images.size(0), 20))
# 损失计算
d_loss_real = criterion(d_out_real.view(-1), label_real)
d_loss_fake = criterion(d_out_fake.view(-1), label_fake)
d_loss = d_loss_real + d_loss_fake
# 误差反向传播,更新损失
optimizerD.zero_grad()
d_loss.backward()
optimizerD.step()
## 训练生成器 Generator
# 定义潜在变量z 维度:(batch_size,20,1,1)
z = torch.randn(images.size(0), 20).to(device).view(images.size(0), 20, 1, 1).to(device)
fake_images = G(z)
# 假图像喂入判别器,得到d_out_fake 维度:(batch_size,1)
d_out_fake, _ = D(fake_images, z.view(images.size(0), 20))
# 损失计算
g_loss = criterion(d_out_fake.view(-1), label_real)
# 误差反向传播,更新损失
optimizerG.zero_grad()
g_loss.backward()
optimizerG.step()
## 训练编码器Encode
# 使用编码器将真实图像变成潜在变量 image:(batch_size, 1, 64, 64)-->z_real:(batch_size, 20)
z_real = E(images)
# 真图像送入判别器,记录结果d_out_real:(128, 1)
d_out_real, _ = D(images, z_real)
# 损失计算
e_loss = criterion(d_out_real.view(-1), label_fake)
# 误差反向传播,更新损失
optimizerE.zero_grad()
e_loss.backward()
optimizerE.step()
## 累计一个epoch的损失,判别器损失、生成器损失、编码器损失分别存放到log_d_loss、log_g_loss、log_e_loss中
log_d_loss += d_loss.item()
log_g_loss += g_loss.item()
log_e_loss += e_loss.item()
## 打印损失
print(f'epoch {epoch}, D_Loss:{log_d_loss/128:.4f}, G_Loss:{log_g_loss/128:.4f}, E_Loss:{log_e_loss/128:.4f}') 这里总结一下上述训练的步骤,不断循环下列过程:使用生成器从潜在变量z中创建假图像使用编码器从真实图像中创建潜在变量生成器和编码器结果送入判别器,进行训练使用生成器从潜在变量z中创建假图像训练生成器使用编码器从真实图像中创建潜在变量训练编码器关于第3步,我也简单画了个图帮大家理解下,如下: 最后我们来展示一下生成图片的效果,如下图所示:缺陷检测 EGBAD缺陷检测非常简单,首先定义一个就算损失的函数,如下:## 定义缺陷计算的得分
def anomaly_score(input_image, fake_image, z_real, D):
# Residual loss 计算
residual_loss = torch.sum(torch.abs(input_image - fake_image), (1, 2, 3))
# Discrimination loss 计算
_, real_feature = D(input_image, z_real)
_, fake_feature = D(fake_image, z_real)
discrimination_loss = torch.sum(torch.abs(real_feature - fake_feature), (1))
# 结合Residual loss和Discrimination loss计算每张图像的损失
total_loss_by_image = 0.9 * residual_loss + 0.1 * discrimination_loss
return total_loss_by_image 接着我们只需要用Encoder网络生成潜在变量,在再用生成器即可得到假图像,最后计算假图像和真图像的损失即可,如下:# 加载测试数据
test_set = image_data_set(test)
test_loader = DataLoader(test_set, batch_size=5, shuffle=False)
input_images = next(iter(test_loader)).to(device)
# 通过编码器获取潜在变量,并用生成器生成假图像
z_real = E(input_images)
fake_images = G(z_real.view(input_images.size(0), 20, 1, 1))
# 异常计算
anomality = anomaly_score(input_images, fake_images, z_real, D)
print(anomality.cpu().detach().numpy()) 最后可以保存一下真实图像和假图像的结果,如下:torchvision.utils.save_image(input_images, f"result/Nomal.jpg")
torchvision.utils.save_image(fake_images, f"result/ANomal.jpg") 我们来看一下结果: 通过上图你发现了什么呢?是不是发现输入图像为7的图片的生成图像不是7而变成了8呢,究其原因,应该是生成器学到了更多关于数据8的特征,也就是说这个网络的生成效果并没有很好。 我做了很多实验,发现EGBAD虽然测试时间上比AnoGAN快很多,但是它的稳定性似乎并没有很理想,很容易出现模式崩溃的问题。其实啊,GAN网络普遍存在着训练不稳定的现象,这也是一些大牛不断探索的方向,后面的文章我也会给大家介绍一些增加GAN训练稳定性的文章已,敬请期待吧!🍄🍄🍄AnoGAN和EGBAD测试时间对比✨✨✨ 我们一直说EGBAD的测试时间相较AnoGAN短,从原理上来说确实是这样,但是具体是不是这样我们还要以实验为准。测试代码也很简单,只需要在测试过程中使用time.time()函数即可,具体可以参考我上传github中的源码,这里给出我测试两种网络在测试阶段所用时间(以秒为单位),如下图所示: 通过上图数据可以看出,EGBAD比AnoGAN快的不是一点点,EGBAD的速度将近是AnoGAN的10000倍,这个数字还是很恐怖的。🍮🍮🍮总结 到此,EGBAD的全部内容就为大家介绍完了,如果你明白了AnoGAN的话,这篇文章对你来说应该是小菜一碟了。EGBAD大大的减少了测试所有时间,但是GAN网络普遍存在易模式崩溃、训练不稳定的现象, 下一篇博文我将为大家介绍一些让GAN训练更稳定的技巧,敬请期待吧。🍚🍚🍚
我不是魔法师
对抗生成网络GAN系列——Spectral Normalization原理详解及源码解析
Spectral Normalization原理详解 由于原始GAN网络存在训练不稳定的现象,究其本质,是因为它的损失函数实际上是JS散度,而JS散度不会随着两个分布的距离改变而改变,这就会导致生成器的梯度会一直不变,从而导致模型训练效果很差。WGAN为了解决原始GAN网络训练不稳定的现象,引入了EM distance代替原有的JS散度,这样的改变会使生成器梯度一直变化,从而使模型得到充分训练。但是WGAN的提出伴随着一个难点,即如何让判别器的参数矩阵满足Lipschitz连续条件。 如何解决上述所说的难点呢?在WGAN中,我们采用了一种简单粗暴的方式来满足这一条件,即直接对判别器的权重参数进行剪裁,强制将权重限制在[-c,c]范围内。大家可以动动我们的小脑瓜想想这种权重剪裁的方式有什么样的问题——(滴,揭晓答案)如果权重剪裁的参数c很大,那么任何权重可能都需要很长时间才能达到极限,从而使训练判别器达到最优变得更加困难;如果权重剪裁的参数c很小,这又容易导致梯度消失。因此,如何确定权重剪裁参数c是重要的,同时这也是困难的。WGAN提出之后,又提出了WGAN-GP来实现Lipschitz 连续条件,其主要通过添加一个惩罚项来实现。【关于WGAN-GP我没有做相关教程,如果不明白的可以评论区留言】那么本文提出了一种归一化的手段Spectral Normalization来实现Lipschitz连续条件,这种归一化具体是怎么实现的呢,下面听我慢慢道来。 这样,其实我们的Spectral Normalization原理就讲的差不多了,最后我们要做的就是求得每层参数矩阵的谱范数,然后再进行归一化操作。要想求矩阵的谱范数,首先得求矩阵的奇异值,具体求法我放在附录部分。 但是按照正常求奇异值的方法会消耗大量的计算资源,因此论文中使用了一种近似求解谱范数的方法,伪代码如下图所示: 在代码的实战中我们就是按照上图的伪代码求解谱范数的,届时我们会为大家介绍。注:大家阅读这部分有没有什么难度呢,我觉得可能还是挺难的,你需要一些矩阵分析的知识,我已经尽可能把这个问题描述的简单了,有的文章写的很好,公式推导的也很详尽,我会在参考链接中给出。但是会涉及到最优化的一些理论,估计这就让大家更头疼了,所以大家慢慢消化吧!!!在最后的附录中,我会给出本节内容相关的矩阵分析知识,是我上课时的一些笔记,笔记包含本节的知识点,但针对性可能不是很强,也就是说可能包含一些其它内容,大家可以选择忽略,当然了,你也可以细细的研究研究每个知识点,说不定后面就用到了呢!!!Spectral Normalization源码解析源码下载地址:Spectral Normalization 这个代码使用的是CIFAR10数据集,实现的是一般生成对抗网络的图像生成任务。我不打算再对每一句代码进行详细的解释,有不明白的可以先去看看我专栏中的其它GAN网络的文章,都有源码解析,弄明白后再看这篇你会发现非常简单。那么这篇文章我主要来介绍一下Spectral Normalization部分的内容,其相关内容在spectral_normalization.py文件中,我们理论部分提到Spectral Normalization关键的一步是求解每个参数矩阵的谱范数,相关代码如下:def _update_u_v(self):
u = getattr(self.module, self.name + "_u")
v = getattr(self.module, self.name + "_v")
w = getattr(self.module, self.name + "_bar")
height = w.data.shape[0]
for _ in range(self.power_iterations):
u.data = l2normalize(torch.mv(w.view(height, -1).data, v.data))
v.data = l2normalize(torch.mv(torch.t(w.view(height,-1).data), u.data))
sigma = u.dot(w.view(height, -1).mv(v))
setattr(self.module, self.name, w / sigma.expand_as(w))
def l2normalize(v, eps=1e-12):
return v / (v.norm() + eps) 对上述代码做一定的解释,6,7,8,9,10行做的就是理论部分伪代码的工作,最后会得到谱范数sigma。11行为使用参数矩阵除以谱范数sigma,以此实现归一化的作用。【torch.mv实现的是矩阵乘法的操作,里面可能还有些函数你没见过,大家百度一下用法就知道了,非常简单】其实关键的代码就这些,是不是发现特别简单呢🍸🍸🍸每次介绍代码时我都会强调自己动手调试的重要性,很多时候写文章介绍源码都觉得有些力不从心,一些想表达的点总是很难表述,总之,大家要是有什么不明白的就尽情调试叭,或者评论区留言,我天天在线摸鱼滴喔。后期我也打算出一些视频教学了,这样的话就可以带着大家一起调试,我想这样介绍源码彼此都会轻松很多。🛩🛩🛩小结 Spectral Normalization确实是有一定难度的,我也有许多地方理解的也不是很清楚,对于这种难啃的问题我是这样认为的。我们可以先对其有一个大致的了解,知道整个过程,知道代码怎么实现,能使用代码跑通一些模型,然后考虑能否将其用在自己可能需要使用的地方,如果加入的效果不好,我们就没必要深究原理了,如果发现效果好,这时候我们再回来慢慢细嚼原理也不迟。最后,希望各位都能获取新知识,能够学有所成叭!!!附录 这部分是我学习矩阵分析这门课程时的笔记,截取一些包含此部分的内容,有需求的感兴趣的可以看一看。
我不是魔法师
对抗生成网络GAN系列——GANomaly原理及源码解析
GANomaly原理解析【阅读此部分前建议对GAN的原理及GAN在缺陷检测上的应用有所了解,详情点击写在前面中的链接查看,本篇文章我不会再介绍GAN的一些先验知识。】GANomaly结构 这部分为大家介绍GANomaly的原理,其实我们一起来看下图就足够了: 图1 GANomaly结构图 我们还是先来对上图中的结构做一些解释。从直观的颜色上来看,我们可以分成两类,一类是红色的Encoder结构,一类是蓝色的Decoder结构。Encoder主要就是降维的作用啦,如将一张张图片数据压缩成一个个潜在向量;相反,Decoder就是升维的作用,如将一个个潜在向量重建成一张张图片。按照论文描述的结构来分,可以分成三个子结构,分别为生成器网络G,编码器网络E和判别器网络D。下面分别来介绍介绍这三个子结构:生成器网络G思来想去我还是想在这里给大家抛出一个问题,我们传统的GAN是怎么通过生成器来构建假图像的呢?和GANomaly有区别吗?其实这个问题的答案很简单,大家都稍稍思考一下,我就不给答案了,不明白的评论区见吧!!!🥂🥂🥂编码器网络E判别器网络D GANomaly的损失函数分为两部分,第一部分是生成器损失,第二部分为判别器损失,下面我们分别来进行介绍:生成器损失函数判别器损失函数 判别器的损失函数就和原始GAN一样,如下:这部分我直接先放上代码吧,不多,也很容易理解,如下:.l_bce = nn.BCELoss()
al - Fake Loss
.err_d_real = self.l_bce(self.pred_real, self.real_label)
.err_d_fake = self.l_bce(self.pred_fake, self.fake_label)
tD Loss & Backward-Pass
.err_d = (self.err_d_real + self.err_d_fake) * 0.GANomaly测试阶段# latent_i表示G_E(x),latent_o表示E(G(x))。torch.pow(m,2)=m^2
error = torch.mean(torch.pow((latent_i-latent_o), 2), dim=1)GANomaly源码解析 这里直接使用论文中提供的源码地址:GANomaly源码🌱🌱🌱GANomaly模型搭建 其实通过我前文的讲解,不知道大家能否感受到GANomaly模型其实是不复杂的。需要注意的是在介绍GANomaly结构时我们将模型分为了三个子结构,分别为生成器网络G、编码器网络E、判别器网络D。但是在代码中我们将生成器网络G和编码器网络E合并在一块儿了,也称为生成器网络G。 下面我给出这部分的代码,大家注意一下这里面的超参数比较多,为了方便大家阅读,我把这里用到超参数的整理出来,如下图所示:""" Network architectures.
"""
# pylint: disable=W0221,W0622,C0103,R0913
##
import torch
import torch.nn as nn
import torch.nn.parallel
from options import Options
##
def weights_init(mod):
"""
Custom weights initialization called on netG, netD and netE
:param m:
:return:
"""
classname = mod.__class__.__name__
if classname.find('Conv') != -1:
mod.weight.data.normal_(0.0, 0.02)
elif classname.find('BatchNorm') != -1:
mod.weight.data.normal_(1.0, 0.02)
mod.bias.data.fill_(0)
###
class Encoder(nn.Module):
"""
DCGAN ENCODER NETWORK
"""
def __init__(self, isize, nz, nc, ndf, ngpu, n_extra_layers=0, add_final_conv=True):
super(Encoder, self).__init__()
self.ngpu = ngpu
assert isize % 16 == 0, "isize has to be a multiple of 16"
main = nn.Sequential()
# input is nc x isize x isize
main.add_module('initial-conv-{0}-{1}'.format(nc, ndf),
nn.Conv2d(nc, ndf, 4, 2, 1, bias=False))
main.add_module('initial-relu-{0}'.format(ndf),
nn.LeakyReLU(0.2, inplace=True))
csize, cndf = isize / 2, ndf # csize=16,cndf=64
# Extra layers
for t in range(n_extra_layers):
main.add_module('extra-layers-{0}-{1}-conv'.format(t, cndf),
nn.Conv2d(cndf, cndf, 3, 1, 1, bias=False))
main.add_module('extra-layers-{0}-{1}-batchnorm'.format(t, cndf),
nn.BatchNorm2d(cndf))
main.add_module('extra-layers-{0}-{1}-relu'.format(t, cndf),
nn.LeakyReLU(0.2, inplace=True))
while csize > 4:
in_feat = cndf
out_feat = cndf * 2
main.add_module('pyramid-{0}-{1}-conv'.format(in_feat, out_feat),
nn.Conv2d(in_feat, out_feat, 4, 2, 1, bias=False))
main.add_module('pyramid-{0}-batchnorm'.format(out_feat),
nn.BatchNorm2d(out_feat))
main.add_module('pyramid-{0}-relu'.format(out_feat),
nn.LeakyReLU(0.2, inplace=True))
cndf = cndf * 2
csize = csize / 2
# state size. K x 4 x 4
if add_final_conv:
main.add_module('final-{0}-{1}-conv'.format(cndf, 1),
nn.Conv2d(cndf, nz, 4, 1, 0, bias=False))
self.main = main
def forward(self, input):
if self.ngpu > 1:
output = nn.parallel.data_parallel(self.main, input, range(self.ngpu))
else:
output = self.main(input)
return output
##
class Decoder(nn.Module):
"""
DCGAN DECODER NETWORK
"""
def __init__(self, isize, nz, nc, ngf, ngpu, n_extra_layers=0):
super(Decoder, self).__init__()
self.ngpu = ngpu
assert isize % 16 == 0, "isize has to be a multiple of 16"
cngf, tisize = ngf // 2, 4 #cngf=32 ,tisize=4
while tisize != isize:
cngf = cngf * 2
tisize = tisize * 2
main = nn.Sequential()
# input is Z, going into a convolution
main.add_module('initial-{0}-{1}-convt'.format(nz, cngf),
nn.ConvTranspose2d(nz, cngf, 4, 1, 0, bias=False))
main.add_module('initial-{0}-batchnorm'.format(cngf),
nn.BatchNorm2d(cngf))
main.add_module('initial-{0}-relu'.format(cngf),
nn.ReLU(True))
csize, _ = 4, cngf
while csize < isize // 2:
main.add_module('pyramid-{0}-{1}-convt'.format(cngf, cngf // 2),
nn.ConvTranspose2d(cngf, cngf // 2, 4, 2, 1, bias=False))
main.add_module('pyramid-{0}-batchnorm'.format(cngf // 2),
nn.BatchNorm2d(cngf // 2))
main.add_module('pyramid-{0}-relu'.format(cngf // 2),
nn.ReLU(True))
cngf = cngf // 2
csize = csize * 2
# Extra layers
for t in range(n_extra_layers):
main.add_module('extra-layers-{0}-{1}-conv'.format(t, cngf),
nn.Conv2d(cngf, cngf, 3, 1, 1, bias=False))
main.add_module('extra-layers-{0}-{1}-batchnorm'.format(t, cngf),
nn.BatchNorm2d(cngf))
main.add_module('extra-layers-{0}-{1}-relu'.format(t, cngf),
nn.ReLU(True))
main.add_module('final-{0}-{1}-convt'.format(cngf, nc),
nn.ConvTranspose2d(cngf, nc, 4, 2, 1, bias=False))
main.add_module('final-{0}-tanh'.format(nc),
nn.Tanh())
self.main = main
def forward(self, input):
if self.ngpu > 1:
output = nn.parallel.data_parallel(self.main, input, range(self.ngpu))
else:
output = self.main(input)
return output
## 判别器网络结构
class NetD(nn.Module):
"""
DISCRIMINATOR NETWORK
"""
def __init__(self, opt):
super(NetD, self).__init__()
model = Encoder(opt.isize, 1, opt.nc, opt.ngf, opt.ngpu, opt.extralayers)
layers = list(model.main.children())
self.features = nn.Sequential(*layers[:-1])
self.classifier = nn.Sequential(layers[-1])
self.classifier.add_module('Sigmoid', nn.Sigmoid())
def forward(self, x):
features = self.features(x)
features = features
classifier = self.classifier(features)
classifier = classifier.view(-1, 1).squeeze(1)
return classifier, features
## 生成器网络结构
class NetG(nn.Module):
"""
GENERATOR NETWORK
"""
def __init__(self, opt):
super(NetG, self).__init__()
self.encoder1 = Encoder(opt.isize, opt.nz, opt.nc, opt.ngf, opt.ngpu, opt.extralayers)
self.decoder = Decoder(opt.isize, opt.nz, opt.nc, opt.ngf, opt.ngpu, opt.extralayers)
self.encoder2 = Encoder(opt.isize, opt.nz, opt.nc, opt.ngf, opt.ngpu, opt.extralayers)
def forward(self, x):
latent_i = self.encoder1(x)
gen_imag = self.decoder(latent_i)
latent_o = self.encoder2(gen_imag)
return gen_imag, latent_i, latent_o
GANomaly损失函数 我们在理论部分已经介绍了GANomaly的损失函数,那么在代码上它们都是一一对应的,实现起来也很简单,如下:## 定义L1 Loss
def l1_loss(input, target):
return torch.mean(torch.abs(input - target))
## 定义L2 Loss
def l2_loss(input, target, size_average=True):
if size_average:
return torch.mean(torch.pow((input-target), 2))
else:
return torch.pow((input-target), 2)
self.l_adv = l2_loss
self.l_con = nn.L1Loss()
self.l_enc = l2_loss
self.err_g_adv = self.l_adv(self.netd(self.input)[1], self.netd(self.fake)[1])
self.err_g_con = self.l_con(self.fake, self.input)
self.err_g_enc = self.l_enc(self.latent_o, self.latent_i)
self.err_g = self.err_g_adv * self.opt.w_adv + \
self.err_g_con * self.opt.w_con + \
self.err_g_enc * self.opt.w_enc 上述代码为GANomaly生成器损失函数代码,判别器的损失函数代码已经在理论部分为大家介绍了,这里就不在赘述了。🍄🍄🍄小结 这里我并没有很详细的为大家解读代码,但是把一些关键的部分都给大家介绍了。会了这些其实你完全可以自己实现一个GANomaly网络,或者对我之前在Anogan中的代码稍加改造也可以达到一样的效果。论文中提供的源码感兴趣的大家可以自己去调试一下,代码量也不算多,但有的地方理解起来也有一定的困难,总之大家加油吧!!!🌼🌼🌼
我不是魔法师
我完全手写的Resnet50网络,终于把猫识别出来了
大家好啊,我是董董灿。经常看我文章的同学,可能知道最近我在做一个小项目——《从零手写Resnet50实战》。从零开始,用最简单的程序语言,不借用任何第三方库,完成Resnet50的所有算法实现和网络结构搭建,最终将下面这只猫识别出来。不幸的是,在刚搭建完网络之后,就试着运行了一下自己的神经网络,识别结果是错的。没办法,只能用 torch 搭了一个官方的网络,和我手写的神经网络,一层一层进行结果比对,然后调试(从零手写Resnet50实战——利用 torch 识别出了虎猫和萨摩耶)。幸运的是,在经过一个数据一个数据对比之后,我的神经网络。出猫了!它竟然真的将猫识别出来了! 我的神经网络出猫现场在从1000个分类得分中,将最大值的索引(第282号)挑出来之后,查询分类文件,便得到了分类结果:tiger cat。这个网络我没用 softmax。因为 softmax 的作用是将结果“大的变得更显著,小的变得更微弱”,并不会改变结果的相对大小。我直接从最后一个全连接层的输出,去找了最大值索引。softmax的作用可以参考softmax原理。过程记录识别出猫的过程,说难也不是太难,说简单但又有不少坑。最难的在于出猫失败,查找原因的过程,是真的一层一层的进行结果对比。好在我封装了一个对比函数,能帮助我在重要的网络节点,验证我的网络是否正确。下面是出猫全流程记录。首层 Conv2d + 第一个 BatchNorm2d + MaxPool 验证正确。第一个 Layer 验证正确,共 10 个 Conv2d。第二个 Layer 验证正确,共 13 个 Conv2d。第三个 Layer 验证正确,共 19 个 Conv2d。第四个 Layer 验证正确,共10个卷积。AvgPool 和 FC 层也都验证正确。整个验证过程还是挺痛苦的,但是看着一层层的打出来“succ”(success的缩写,说明和官方结果是一致的),还是很有成就感,并且挺治愈的。下面简单说一下我在出猫过程中遇到的那些坑保存权值文件 layout 搞错torch 默认的图片数据摆放格式是 NCHW,而我习惯写算法的方式是NHWC。因此,在前期将图片导出时,没有考虑自己算法实现的习惯,而是将权值直接 flatten之后保存了。结果就是,再将权值从文件中读入内存参与运算时,数据读取不正确。结果肯定是错的。意识到这一点之后,因为我算法都已经写好,而且不想改了,于是,将保存权值的逻辑,在 flatten 之前,添加了一个 transpose 操作,将权值从NCHW 转为 NHWC,然后保存。BatchNorm2d 的均值和方差使用错误BatchNorm2d的算法实现有多种,特别需要注意的是需要区分该算法是在训练时用的还是推理时用的。训练和推理时用的BatchNorm2d虽然公式是一样的,但实现方式却大不一样。主要区别在于:训练时均值和方差需要根据本次的数据进行实时计算推理是使用的均值和方差是模型保存好的参数,在 torch 模型中,分别为 BatchNorm2d.running_mean 和 BatchNorm2d.running_var。而我在最开始的算法实现时,均值和方差是自己手算的(对应的训练过程),而没有使用模型保存的均值和方差。结果便是每层BatchNorm算出来的结果都差一点,这一点误差在层与层之间传递,导致到最后的识别结果中,误差被放大。正是因为这个误差被逐层放大,就把猫识别成了一个水桶。残差结构问题上一篇文章从零手写Resnet50实战——利用 torch 识别出了虎猫和萨摩耶分析残差结构可能会有问题。实际验证残差结构没问题,就是一个简单地加法。有问题的是上一层的BatchNorm2d,计算错误了。那为什么计算错了,当时的分析仍然能和官方的计算结果对上呢?是因为当时的官方计算忘了一个 model.eval() 调用。该调用会告诉模型运行在推理模式而不是训练模式。而如果我不调用,显然用的训练模式,恰巧的 BatchNorm2d 的第一次实现,手算均值和方差,就对应了训练模式的算法。于是结果刚好对上了,但这样最终识别的图片分类肯定还是错误的。基本就遇到了这3个问题,在把这3个问题解决了之后,整个预测过程运行了大约40分钟,猫就被顺理成章的预测出来了。于是,项目的第一阶段,就这么完成了。下面会开启本项目的第二阶段——神经网络性能优化。用C++ 重新实现一遍所有算法:因为C++性能要比 python 手写的算法性能好很多重点优化卷积的性能:目前40多分钟有将近39分钟的实践花在了卷积上使用C++实现的版本争取在数秒内完成一张图片的推理因为本次有2/3的坑都是BatchNorm算法引起的,后面会写一篇BatchNorm算法的文章,欢迎继续关注。出猫,看起来也很简单。欢迎持续关注本博主文章和本系列,一起从零开始,学算法,做实践项目。 这是一个可以写到简历上,亮瞎面试官双眼的项目哦。
我不是魔法师
神经网络推理加速入门—— 番外:GPU为什么这么牛
导 读AI模型运行在计算机上,除了需要消耗大量的计算资源外,还需要大量的内存以及带宽用来存储和搬运数据。在如今一个模型动辄几千亿个参数的情况下,模型运行的性能变得越来越重要,对计算机硬件的需求也水涨船高。而不论是工业界,还是产业界,针对AI模型的优化从未停止过!优化手段也从算法上的优化,扩展到了专用硬件上。本文主要从硬件加速王者GPU的科普讲起,说一说GPU在神经网络推理中,为什么这么牛,更多细节,欢迎关注本专栏其他文章。王者GPUGPU(Graphic Processing Unit),图形处理器,是英伟达在很早就推出的处理器,专门用来进行图形学的计算,用来显示游戏视频画面等,又称为显卡。图片2006年,英伟达推出CUDA,这是一种专门针对GPU的编程模型,或者说软件库,它直接定义了异构编程的软件架构,为英伟达进入AI计算领域埋下了种子。2012年,图像识别大赛,很多参赛队伍采用GPU完成AI加速,让英伟达乘上了人工智能的东风,从此,一跃成为人工智能硬件领域的绝对霸主,一直到今天。这期间,国内外很多家公司都试图推出了自己的AI芯片,希望可以在人工智能硬件这一领域上分得一杯羹,但却始终无法撼动英伟达AI芯片老大的位置。国外强如Google 的TPU,AMD以及ARM,国内如华为昇腾、百度昆仑、阿里平头哥等一线互联网企业,以及地平线、寒武纪、比特大陆等自研ASIC芯片的厂家,甚至近几年大火的存算一体芯片,都在一次次的冲击着英伟达。但它却至今屹立不倒。GPU的神之地位,被英伟达捍卫的死死的。其实,早在GPU被用来做AI计算之前,GPU就已经涉及到了多个领域。其涉及的领域之多,再加上CUDA的软件栈生态之普及,社区活跃度之高,才使得GPU屹立不倒。GPU涉及到的领域说三个领域,就足以让英伟达赚的盆满钵满,让GPU有价无市,让GPU有黄牛囤货...第一个领域是:游戏游戏一直是新科技新技术的试验场地,同时也是一个赚钱的暴利行业。为什么这么说。因为新的硬件研发出来,基本上都会在游戏行业找到落地点,比如国内某手机厂商,以王者荣耀运行流畅为卖点,大打广告。而实际上,软件与硬件的关系,存在着一个著名的安迪比尔定律:硬件提高的性能,很快会软件消耗掉。这个定律的意思是说,硬件迭代升级带来的性能提升,很快就会被新一代的软件所消耗掉,从而使得人们不得不更换新一代的硬件产品。这下,你知道为什么去年才买的手机,今年很多软件就带不起来了吧。软件更新,正在一步步蚕食掉你的新手机!举个不太恰当但是很生动的例子。超级玛丽这款游戏,我们玩了很多年,游戏中那么多关卡,那么丰富的剧情,丰富的配图,丰富的配乐,但是你能想象,这款游戏的总大小才64KB么?64KB包含了游戏所有的代码、图形和音乐!这主要是因为,在超级玛丽那个年代,芯片上所连接的硬件资源少的可怜,游戏开发者为了节省内存,进行了大量的代码优化和图片复用优化。而现在,一个王者荣耀的更新包,就好几个GB,运行起来占用的内存更是高的离谱。1年前买的手机,今天很可能就带不动最新的王者荣耀了!游戏的升级,迫使人们更换新的手机。但也正是因为有安迪比尔定律的存在,才使得硬件被迫不断地迭代升级。GPU正是如此。虽然英伟达的GPU一开始就是为显示服务的,但是随着游戏的不断迭代,不光对于GPU的显示技术有了更高的要求,而且对于与显示相关的计算任务也有了更高的要求。最典型的便是光追技术。所谓光追,就是光线追踪,游戏场景中针对光影的处理,尤其是进行实时的光影转换,如河面的倒影、阳光打在窗户上的朦胧感等,一直是计算图形学的难点。该技术需要大量的实时计算,依据游戏中的实时场景,随时计算光线的折射,反射,漫反射等。自从英伟达在自家芯片上支持了光追技术之后,GPU便成了一些游戏的标配。一部分游戏发烧友们,玩游戏,必上显卡。第二个领域是:比特币挖矿相信很多人记得英伟达被黑客组织 LAPSUS$ 勒索的事件。原因是英伟达为了限制人们使用显卡挖矿,在显卡的驱动程序中添加了一把软件锁,用来限制挖矿时的带宽,从而降低显卡的性能。正常游戏时,显卡可以用到100%的带宽,而挖矿时,显卡却只能发挥50%的带宽性能,这让买了显卡的挖矿矿工们很不爽。之所以这么做,是因为显卡挖矿太有优势了!大量的显卡被买来去挖矿,这不是英伟达希望看到的,英伟达更希望自家的芯片,用来进行科学计算或者做对人类更意义的事。第三个领域:AI计算就像文章开头说的,自从英伟达的GPU乘上了深度学习的东风,不管是出货量,股价还是公司影响力,都大幅飙升,直接造就了一个硬件王国。深度学习的训练领域,GPU是当之无愧的王者,至今,染指训练的硬件厂商也寥寥无几,并且训练的性能和精度与GPU相比,还是差一些。很多ASIC芯片(专用芯片)公司,都拿GPU的计算结果作为精度和性能的标杆,以此来鼓吹自己的芯片性能,大做广告。游戏、挖矿和深度学习这三个领域,就可以让英伟达的GPU立于芯片不败之林了,更别提普通显卡、科学计算甚至数据中心等领域了。说了这些,那为什么GPU这么牛呢这要从GPU的硬件架构说起。平常我们电脑上所用的CPU,是一种多核架构,看看你的电脑,可能是4核或者8核的处理器。在执行计算任务时,通过程序的控制,比如多线程,可以让8个核同时工作,此时的计算并行度是8。而GPU的恐怖之处在于,它远远不止8个核心这么简单,它把计算核心做到了成千上万个甚至更多,通过多线程,可以使得计算并行度成千上万倍的提升。GPU是众核!举个例子,如果计算5000个数组相加。int a[5000];
int b[5000];
int c[5000];
for (int i = 0; i < 5000; i++) {
c[i] = a[i] + b[i];
}
即使我们使用8核CPU计算,那么每个核还需要计算5000 / 8 = 625个数,而单个核心的计算是串行的,需要排队,也就是算完一个,再算另一个。假设计算一个数需要1s,那么即使8个核同时运行,也需要625s。这里暂时不考虑支持向量指令的CPU。而由于GPU有成千上万个核,计算5000个数字,使用5000个核同时计算,每个核计算一个数就搞定了!总共需要1s!这就是GPU!有个比喻。CPU是指挥部,每个核是一个将军,除了需要指挥军队完成调度这种劳神费心的工作外,如果让它杀敌,它也只能一个一个杀,杀死1w敌军不得把将军累死了?而GPU是军队,只负责杀敌,1w个士兵杀1w个敌军,一对一,不分分钟的事?正是由于GPU这种独有的硬件架构,加上图形专用硬件单元或者深度学习专用硬件单元(如TensorCore)的加持,再加上多层级的存储架构,使得GPU的硬件,计算性能和访存性能都如王者般,傲视其他ASIC小弟。总结一下游戏,挖矿,深度学习,这三个领域足以让英伟达的GPU傲视群雄!安迪比尔定律,软件会吃掉硬件的性能,反过来会迫使硬件更新迭代!GPU也是如此。GPU独有的多核硬件架构以及专用硬件单元和多层级存储,是GPU傲视群雄的王牌。好啦,本篇文章就说到这,敬请关注本专栏其他文章。
我不是魔法师
这么理解矩阵乘,你可以吊打面试官了
大家好啊,我是董董灿。很多与深度学习算法相关的面试,面试官可能都会问一个问题,那就是你是如何理解矩阵乘算法的。更有甚者,会让你当场手写矩阵乘算法,然后问细节,问如何优化,面试现场,残忍至极。那矩阵乘法的本质到底是什么呢?为什么在神经网络中,甚至如今的大模型中,有那么多矩阵乘法出现呢?1、矩阵乘法的本质我查了很多资料,得出一个结论:矩阵乘法的本质,是资源的整合和再创。举个例子。你是一个鸡尾酒调酒师,家里储存了很多鸡尾酒的原料,有金酒、利口酒、柠檬汁和可乐等等。今天家里来了 3 位客人,他们分别喜欢喝“自由古巴”、“长岛冰茶”以及“龙舌兰日出”这 3 款鸡尾酒,并向你下了单。希望你给他们调配出来各自喜欢的鸡尾酒。巧的是,这 3 款鸡尾酒的原料都是金酒、利口酒、柠檬汁和可乐。你作为一个调酒师,分分钟就把客人的爱好的鸡尾酒给调出来了。怎么做的呢?你知道配方:自由古巴: 20%金酒 + 45% 利口酒 + 10%柠檬汁 + 25%可乐长岛冰茶: 60%金酒+ 30%利口酒 + 5% 柠檬汁 + 5% 可乐龙舌兰日出:30%金酒 + 10%利口酒 + 30%柠檬汁 + 30%可乐你在调配鸡尾酒的过程中,是按照这个配方来调配的。这里的原料,比如利口酒和可乐,就是输入资源,配比(比如可乐的 25% )就是赋予该资源的权重。将相同的原料按照不同的配比混合起来,就得到了不同口味的鸡尾酒。这种做法,可以抽象一下,写成一个公式:自由古巴 = 0.2 x 金酒 + 0.45 x 利口酒 + 0.1 x 柠檬汁 + 0.25 x 可乐长岛冰茶 = 0.6 x 金酒 + 0.3 x 利口酒 + 0.05 x 柠檬汁 + 0.05 x 可乐龙舌兰日出 = 0.3 x 金酒 + 0.1 x 利口酒 + 0.3 x 柠檬汁 + 0.3 x 可乐我们知道矩阵乘法的规则是,左矩阵的第一行乘以右矩阵的第一列,得到第一个值,第一行乘以第二列得到第二个值,...,以此类推上面这种连乘的操作,就可以用矩阵乘法来表示。左矩阵是一行四列,代表原料。右矩阵是四行三列,每一列代表对应原料的配比。按照矩阵乘法的规则,他们的结果应该是一个一行三列的矩阵,分别代表调配出来的三种鸡尾酒。看到这是不是有点熟悉了。矩阵乘法,通过相乘累加的操作,实际上是对资源(鸡尾酒的原料)的整合和再创(创造出了新的口味,如自由古巴)。2、深度学习中的矩阵乘法回到深度学习算法中,矩阵乘法的右矩阵通常是权值矩阵,是作为模型的参数被训练出来的。一个模型,在对多种数据集训练之后,模型就学习到了一个权值矩阵,实际上一个模型中会学到很多个权值矩阵,这里用一个举例子说明。这个权值矩阵可以很好的匹配多种输入数据,并对输入数据进行整合和再创。卷积和全连接算法,或多或少都是一种矩阵乘法,将其转换为矩阵乘法之后,那么和权值矩阵对应的另一个矩阵,就是输入数据。假设输入的数据是一张图片,那么图片中一个通道维度代表图片的一个特征,通过矩阵乘法对通道进行乘累加操作,便可以实现特征之间的整合和再创。也就是所谓的特征融合,跟调酒是不是很像?更显而易见的例子体现在全连接层上,全连接层通过矩阵乘法的运算,把所有的特征全部进行了融合,最终可能就会得到某一个类别。
我不是魔法师
5分钟搞懂池化算法
大家好啊,我是董董灿!在很多与计算机视觉相关的神经网络中,我们往往都会看到池化这一算法,它一般跟在卷积层后面。神经网络中用到最多的池化方式无外乎是最大池化和平均池化。两者运算接近,区别在于是在kernel范围内取最大值还是取平均值来作为池化的输出。那池化的本质是什么呢?为什么在神经网络中,尤其是CNN网络中,会需要用到池化算法呢?1、池化的本质先回顾下池化运算。下图展示的是一个最大池化的计算过程。它利用一个2x2的核(kernel)来圈定一次池化的计算范围,每次选定输入图片中2x2范围内的最大值数值作为输出。这一过程非常简单,它计算的每一步,就是从选定的范围内,计算出一个数字来,这一过程也叫做特征的聚合(aggregation)。我们知道,卷积的输出是特征图,特征图的一个通道代表一个特征。而针对特征图的池化操作在每个通道上是独立进行的,池化前后特征图变化的仅仅是长宽方向的尺寸。那怎么理解特征聚合呢?举个例子。最大池化的每次计算,都是选取kernel范围内的最大值作为这个范围内最显著的特征代表。一张图片中,像素值最大的地方,往往是图像突变最大的地方,比如图像的轮廓和边缘,因此最大池化,可以有效的提取图像的轮廓边缘信息。也就是说,通过最大池化,输出了一个比原始图像在长宽方向尺寸更小的图片,但这个更小的图片却聚合了原始图像中最显著的轮廓和边缘特征。达到了特征聚合的目的,这也是池化算法的本质。围绕着这个本质,可以引申出几个池化算法的优势。2、池化的优势减少计算量输出的图片在长宽方向上尺寸变小了,从而带来了计算量的减少,这一点是显而易见的。减少过拟合的风险计算量的减少,带来的另一个显著影响就是池化层后面的神经网络需要处理的特征图尺寸变小了,从而使得模型的参数量需求减少。参数越少,过拟合的风险越低,这一点在训练过程中尤为重要。提高模型对图片平移、缩放和旋转等变换的鲁棒性之前看到这句话的时候,有点似懂非懂。后来查了一些资料,发现这句话说的其实是池化算法对于原始图片的变换具有轻微的容忍度。也就是说,有了池化算法,模型可以容忍输入的图像有轻微的旋转、平移或者缩放,可以在不改变任何模型算法或结构的情况下,希望推理出正确的结果。举个例子。下面的图片示意了最大池化对于图片轻微旋转的容忍度。如果输入图片有轻微的旋转,经过最大池化,只要图片旋转的角度不是很厉害,依旧可以在对应位置获取到目标区域中的最大值12。而对于图片的平移和缩放变换,池化算法同样有着类似的轻微容忍度。注意这里一直在说是轻微的容忍度。如果一张图片旋转、平移过多,那么经过池化输出的结果肯定是不一样的,这时模型推理的结果可能也就不一样了。正是因为这几个优势,使得池化操作经常会出现在CNN网络中。好了,关于池化就写到这,不知看完之后,你对池化这一算法是否有了更深的认识了呢?如果你看完有收获,欢迎点赞转发。
我不是魔法师
神经网络推理加速入门——分层存储架构
这是通俗易懂的神经网络推理加速入门第4篇。本篇介绍一个计算机中的部件——存储器,看完之后,你将了解存储器是什么以及存储器在AI计算中的作用。在介绍之前,先说一个我今天早晨的事儿。7点起来晨跑,突然发现已经到深秋了,凉飕飕的,感觉是时候换一波秋冬的衣服了。于是开始翻箱倒柜,花了好大一会儿,终于在衣柜的最深处,找到了去年冬季跑步的衣服,口袋里还装着去年的口罩。找到衣服之后,我突然盯着衣柜看了半天,发现:衣柜的设计确实是很讲究,只可惜我没用好,才导致费了九牛二虎之力才找到了衣服。衣柜讲究的设计最经常穿的衣服,应该是要用衣架撑好挂在衣柜横杆上的,方便随时取用;不经常穿的衣服,比如非当季的衣服,应该是要叠好放在最底层的柜子里,让他过冬。这种明显的功能区域划分,可以让我在需要某件衣服的时候,方便快捷的找到。这就和今天文章的主角——存储器有点像了。不知道是不是所有带有存储性质的产品都有类似的分层划分。但我知道,存储器这种储存数据和指令的东西,有着很明显的分层或分级划分的存储器的分层设计之前的一篇文章计算机存储和计算的分离说到,在计算机系统中,计算单元和存储器是分离的。而实际上,在计算机中,单看存储器,也是会分成很多层级。存储器最常见的就是内存。在买手机时,我们一定会关注一个参数,那就是内存大小。内存越大,手机可以存储的数据就越多,运行起来也就越流畅,手机性能就越好。但是在一个计算系统中,除了内存,还有其他的存储。下图是一个典型的存储器划分示意图。示意图越往上,代表存储器越靠近计算单元,其容量越小,相对造价就越高。这也是为什么,在计算机系统中,单位存储的内存价格很高,而外存(如硬盘)相对较为便宜。磁盘是离CPU最远的存储器。一般作为硬件外设存在。包括我们常见的硬盘、U盘等存储外设。磁盘的读写速度相比其他存储器慢,但是容量大,价格便宜。这个就相当于衣柜的最底层,存放着不经常穿的衣服(数据),像是一个大仓库。主存可以理解为电脑的内存条,用来存放程序运行时的指令和数据。程序运行时操作系统需要将程序和数据加载到内存中,它就相当于衣柜中搭衣服的横杆,随取随用。高速缓存(Cache)是比主存离CPU更近的一级存储,他会把程序需要的指令或者数据预先加载进来,在CPU进行运算时,会首先在缓存中查找数据或指令,如果找不到,就在去主存中寻找,找不到去主存中寻找的过程一般称为Cache Miss。预先加载怎么理解呢?打个比方,我们在冬天肯定有经常要穿的2-3件衣服,但不会每次衣服脏了都放回衣柜中,而是洗完放在阳台晾衣架上,这2-3件衣服轮换穿。CPU也是这样,它会频繁的从高速缓存中存取数据,找不到了,再去内存中找,就好像阳台上没衣服了,再去衣柜里找一样。寄存器(Register)是CPU最近的存储器。用来存放程序运行时需要的指令、地址、立即数等。类比于就是身上正在穿的衣服。有了这几级存储,在做AI加速时,就可以做很多事情。之前指令流水线 说过,由于计算和存储是分离的,那么可以将计算和存储指令排流水,实现性能的加速。同样,如果存储也有分层设计,并且开放给程序员的话,那么,单独的存储指令也可以进行流水设计,从而在带宽不变的前提下,提高数据的吞吐和程序的性能。GPU就是这么做的。熟悉GPU硬件架构的同学可能知道,GPU的编程模型中有DDR(显存,也就是最外层存储,可类比硬盘),Shared Memory(共享内存),当然还有其他的存储。单说 DDR 和 shared memory(SM)这两级存储,就可以排流水。比如——上表中每一行代表同一时刻,看不懂没关系,只需要知道在同一时刻,程序可以同时将数据从DDR load 到 SM(左侧的一例) 以及在SM上进行计算(右侧的一列)即可。这样就相当于在流水线上有两个工人一起工作,从而提高了性能。总结存储器的分层设计,一个好处之一就是,程序员可以通过编写存储指令(包括将数据从外存搬运到内存的 load 指令,将数据从内存加载到片上计算的 move 指令等),从而完成流水的排布。当然,存储器的分层设计肯定不单是这个原因,就不展开了。那么问题来了,这篇文章和AI加速有什么关系呢?其实,存储器作为一个偏计算机底层的部件,是根本不关心上层应用是什么的。我们可以让计算机进行AI计算,来完成AI加速,也可以让计算机运行一个游戏,完成游戏加速。只要了解了存储器的原理,不论是AI加速还是游戏加速,都能做到性能很好。政治基础决定上层建筑。——而且只要硬件支持指令流水级,并且编译器做的足够好,甚至都不需要程序员手动去排流水(手写汇编确实太枯燥了)就能自动实现。从而完成对于AI算法的加速计算,这一点对于编译器的要求很高,后面再介绍。今天就介绍到这,欢迎持续关注神经网络推理加速入门系列文章。
我不是魔法师
神经网络推理加速入门——转换提前
做优化有时候真的很头疼,绞尽脑汁的想怎么做算法等价,怎么把神经网络各层指令流水起来,在确保整网精度的同时,又有高性能。但有时做了半天,却发现流水根本就流不起来,总是莫名其妙地被卡住。真的是一顿操作猛如虎,回头一看原地杵。今天介绍一种神经网络的性能优化方法。它不需要懂特深奥的算法知识,就能做到整个优化系统,大到网络,小到算子的性能的成倍提升。而且绝对是成倍的性能提升,并且显而易见的算法等价。怎么做呢?很简单,只需要改一下算子的先后调用顺序就行。背景在做AI推理或者训练时,大部分情况下一个神经网络中的所有层(Layer)的计算数据类型是相同的。比如为了网络有更好的识别精度,神经网络中的运算可以使用高精度的浮点数,如 float32,简称 FP32。但有时为了性能,稍微损失一点识别精度也能接受,此时可能会使用 float16,简称FP16, 也就是半精度数据类型来做运算。FP32 和 FP16 的区别在于,前者数据位宽是后者的两倍,因此表示相同的数据的时候,前者的精度更高,但内存占用也更大。比如同时存储一张图片,如果使用 FP32的话,可能会占用1MB的内存,但如果使用FP16来存储,只占0.5MB的内存。我们可能听说过混合精度推理、混合精度训练。这里说的混合,指的就是精度混合。比如一个神经网络中存在多种数据类型。为什么可以做混合精度的推理或训练呢?一个神经网络就像是一个大厦,由一层一层的算法搭建而成,每一层的算法可能不同。不同的算法对数据精度的敏感程度不同。有很多算法对数据精度不敏感,比如 transpose, gather, scatter等,这类算法都是数据搬运操作,也就是纯IO操作。他们不需要进行数据计算,无需考虑数据在做加法时候的溢出处理等情况。而有些算法对数据精度很敏感,典型的比如conv2d算法,它需要做大量的乘累加操作,数据的累加很容易出现溢出,此时需要用更高位宽的数据来接收累加结果。如果把操作 FP32 比作需要搬运32块砖的话,那么 FP16 就是只需要搬运 16块砖。很明显,搬运16块砖比搬运32块砖,在其他条件不变的情况下,要省时省力。因此,在神经网络尤其是混合训练或推理的网络中,如果遇到了一些数据搬运算法搬运的是 FP32,那么是很有机会只让他搬16块砖(FP16)的。那么具体怎么做呢?首先简化一个神经网络,假设一个神经网络有如下结构:在这个假想的网络中,卷积层(conv2d)计算的输出是 FP32,然后送给transpose 层进行数据搬运,transpose由于是纯IO算法,因此它的输出也是FP32。transpose的输出送给下一层cast,cast负责将FP32的数据转换为FP16, 因此cast 的输出是FP16。然后FP16的数据送给接下来的层进行运算。不知有没有发现,在这个网络中,transpose 算法先搬运了FP32的数据,然后交给了 cast 进行数据类型转换,转换成了更低位宽的 FP16。但是由于 transpose 是纯IO运算,对数据类型不敏感,因此,我们完全可以将cast算子提前到 transpose 之前,如此的话,transpose 只需要做 FP16 的数据搬运。转换之后的网络如下:这样做的结果就是:整个网络的计算是等价的,但是 transpose 算子却由原来进行 FP32 的数据搬运,变成了 FP16 的数据搬运。对 transpose而言,其IO性能表现是成倍的提升。这只是举一个很简单的例子。而实际上,在真实的网络中,使用此方法可以优化成功的算法有时不仅仅是一个简单的 transpose,而是一个很大的网络片段。由此可见,仅仅将 cast 提前这一个简单的操作,就能使整网的性能提升一倍。这个方法很简单,很有效,也很容易实施。但是在实际进行网络优化的时候,有时却会被忽略。能够使用这一优化的网络必须满足以下两个条件:必须是混合精度的网络由高位宽转低位宽的cast 算子前存在 IO 型算子在我们绞尽脑汁使用一些高级的技巧,如模型并行、层层流水来做网络优化的同时,不妨放大视角,着眼全图,看看整网是否满足上面的条件,没准只一眼,就能发现这一最简单有效的优化点,从此百分比的提升网络性能,不是梦!
我不是魔法师
神经网络推理加速入门——计算和存储的分离
今天开始,增加第一个专栏,作为AI推理加速的专栏,主要写一写在神经网络推理过程中的一些加速方法。先从计算机底层原理说起。先说几个基础概念。计算机存储和计算的分离第一是计算机。这里说的计算机是广义上的计算机,也就是说具有计算能力的硬件设备(计算的机器)都算。小到某个芯片系统,大到智能手环、手机、电脑甚至服务器,都归到计算机的范畴。第二是计算。这里要说的计算,指所有的计算,包括科学计算——比如用计算器算一些数,包括音频编解码——比如手机麦克风对我们说话音频的调制解调处理,包括视频流——比如看电影时一帧帧图像的解码等等。所有芯片需要处理的计算任务,都包含在计算这一概念中。第三是存储。这里说的存储,泛指计算机中所有能存储数据硬件。包括我们熟知的硬盘、U盘、手机内存、手机运存、GPU显存(显卡容量),也包括处理器(CPU或其他芯片)内部的片上存储或L1/L2缓存等。在说清楚这3个概念之后,那么,计算机的计算和存储,就好比——我们在厨房做饭,厨房里的冰箱就是存储器,冰箱里的菜就是希望处理的数据,而洗菜、切菜、炒菜都属于计算任务,整个厨房就是计算机。冯诺依曼架构不论是笔记本,还是手机,还是智能手表智能手环,内部的程序运行机制都是一样的,都绕不开一个著名的计算体系,大家可能听过,叫做冯诺依曼体系。冯诺依曼是二战时期著名的计算机科学家,他开创性的提出了计算机的冯诺依曼架构,其中最为人津津乐道的,是数据存储和计算的分离。在任何一台计算机中,存储数据的硬件叫做存储器,负责逻辑计算的叫做运算器。除此之外还有控制器,输入输出(IO)等。存储和计算分离就是,存储器只负责存储数据,计算单元只负责计算数据,然后将计算出来的结果再存回存储器。有没有发现,我们在做计算(洗菜)之前,需要将数据(菜)从冰箱里拿出来,放在洗菜池里来洗(计算)。这个将菜从冰箱里拿出来的过程,叫做数据的搬运。而在芯片的整个运算过程中,数据的搬运的时间开销是避免不了的,甚至有时会占据绝对的大头。举个例子——我们知道卷积运算是一种计算密集型的算法。也就是大量的时间开销都消耗在了卷积的乘法和加法上(乘累加,Multiply-accumulator,MAC单元)。但是,如果芯片的片上存储很小,而神经网络中的一张图片又很大,一张图片的数据是无法在一次计算中全部放在片上存储的。那么这个时候,自然而然会想到将图片拆分成好几份进行计算。然而,卷积要求的是将所有输入通道进行累加和。如果在通道上进行了数据拆分,那么每次计算的都是不完全的结果(部分和)。这个时候,这些中间结果都要找个地方放。放哪里?最常见的就是放在片外存储上(对于GPU来说,可能就把这些临时数据放在DDR上,也就是我们常说的显存上,因为显存一般都比较大,大概16GB,肯定能的下)。如此一来,存放数据的冰箱可能就不是厨房里的冰箱了,而是客厅里的大冰柜,将数据从厨房搬到客厅的大冰柜临时存起来,这个过程的数据搬运开销是很大的。也因此,GPU甚至很多ASIC芯片,在进行芯片设计时,都会想办法加大DDR的带宽,通过多路DDR访存甚至使用HBM来提升带宽,以降低数据搬运带来的额外开销。总结一下在现有经典的计算机计算架构中,比如冯诺依曼架构中,计算和存储是分离的。这也就导致了计算机在完成运算任务时,需要不断地从存储器中搬运数据到计算单元中,然后完成计算。这种架构也导致了计算指令和IO指令(数据搬运)的独立,从而衍生出指令流水线。(这个后面会慢慢介绍)。除此之外,既然计算和存储是分离的,为了防止多余的搬运开销,那我们把计算和存储放在一起不就行了么?我们直接在冰箱里洗菜做饭不就完了?可以,现在比较前沿的近存芯片、或者存算一体芯片就是基于这个想法来设计实现的,后面也会逐步涉及一些存算一体技术进行AI推理加速的,敬请期待。
我不是魔法师
深度学习模型部署篇——从0部署深度学习分类模型(一)
模型部署概述首先来谈谈到底什么是模型部署呢?其实模型部署就是将我们训练的深度学习模型应用到生产环境中。这里大家可能就有一个问题了,我们之前训练的分类网络难道不能用于生产环境嘛,为什么还要部署模型呢?在我看来,其实我们之前训练的模型理论上是可以用的,但是一方面在生产环境中往往对模型的速度提出更高的要求,直接使用训练好的模型往往速度慢,难以满足实际需求;另一方面我们训练模型时往往使用不同的框架,如Pytorch、TensorFlow、Caffe等等,这些框架需要特定的环境依赖,配置复杂,往往不适合在实际生产环境中安装。总之,如果我们直接将我们训练的模型应用于生产环境,会存在环境配置复杂、推理速度较慢的问题 ,因此我们需要对我们的模型进行部署。下图展示了我们从数据采集到最终应用于生产环境的过程,首先我们会收集数据,然后选择合适的模型对我们的数据进行训练,并进行测试评估。其实到这里都是我们之前掌握的知识,也就是训练我们的模型。当我们训练好我们的模型后,会对我们的模型进行部署,使其能够应用于生产环境当中。 模型部署流程上文对什么是模型部署以及为什么要进行模型部署进行概述,下面就来谈谈模型部署的通用流程。模型部署常见的流程为“深度学习框架-->中间表示-->推理引擎”。我们用下图来解释一下这个流程:首先深度学习框架大家应该很熟悉了,如Pytorch、TensorFlow等等,图中以Pytorch为例,我们先使用Pytorch定义了一个网络架构,如resnet,然后将我们采集的数据喂入网络进行训练,得到权重文件;之后我们会将刚刚得到的权重文件转换成一种中间格式,如图中的ONNX【这个最常见】;最后我们会使用推理引擎【有TensorRT、ONNX RUNTIME、OpenVINO等等】把刚刚得到的中间表示转换成特定的格式,之后就可以在硬件平台高效的运行模型。这里大家只要知道这个流程就行,后面我们会结合代码帮助大家理解。代码部分将按照下图流程进行,即采用Pytorch框架,ONNX中间表示,ONNX RUNTIME推理引擎,在本地PC上进行模型部署。 模型部署代码解析这节将以部署一个分类模型为例为大家介绍,让我们一起来看看叭安装环境先给再大家提个醒,大家在安装环境的时候务必要把系统代理关上喔,不然会报错滴。安装深度学习框架Pytorch python复制代码pip3 install torch torchvision --extra-index-url https://download.pytorch.org/whl/cu113安装中间表示 ONNX python复制代码pip install onnx -i https://pypi.tuna.tsinghua.edu.cn/simple安装推理引擎ONNX Runtime python复制代码pip install onnxruntime -i https://pypi.tuna.tsinghua.edu.cn/simple安装其它依赖包 python复制代码pip install numpy pandas matplotlib tqdm opencv-python pillow -i https://pypi.tuna.tsinghua.edu.cn/simple配置环境建议大家使用Conda创建一个新的虚拟环境。验证安装是否成功 python复制代码import torch import onnx import onnxruntime as ort print('PyTorch 版本', torch.__version__) print('ONNX 版本', onnx.__version__) print('ONNX Runtime 版本', ort.__version__) 安装成功回打印处对应包的版本,我的结果如下:模型训练安装好环境后,我们就可以使用Pytorch模型来训练我们的数据了,这个过程我在之前的博客中都有介绍,这里即不赘述了,自己也偷个懒,使用官方在ImageNet上训练的resnet18模型,我们可以直接下载模型权重:# 导入相关包
import torch
from torchvision import models
# 有 GPU 就用 GPU,没有就用 CPU
device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
model = models.resnet18(pretrained=True)
model = model.eval().to(device)
如果我们打印一下model,就会发现其就是resnet18的模型。【太长了,这里就展示一部分了】将训练模型转换为中间表示ONNX上一步我们已经得到了一个训练模型model,下面我们将其转换成ONNX中间格式。首先我们要先构造一个输入图像的Tensor:x = torch.randn(1, 3, 256, 256).to(device)
然后我们就可以使用下面的代码将模型model转换成ONNX格式:with torch.no_grad(): #推理
torch.onnx.export(
model, # 要转换的模型
x, # 模型的任意一组输入
'resnet18_imagenet.onnx', # 导出的 ONNX 文件名
opset_version=11, # ONNX 算子集版本
input_names=['input'], # 输入 Tensor 的名称(自己起名字)
output_names=['output'] # 输出 Tensor 的名称(自己起名字)
)
大家肯定也发现了, torch.onnx.export就是将模型转换成ONNX格式的函数。其中model表示要转换的pytorch模型;x是模型的任意一组输入,注意这个输入的shape要和模型输入相匹配;'resnet18_imagenet.onnx'是我们要到处的ONNX的文件名;opset_version表示算子集版本,不同的版本会更新一些新的算子; input_names和output_names是输入输出的名称,先给大家打个预防针,这里的输入输出名称要和我们运行推理引擎时的保持一致。上述代码运行成功后,会生成一个resnet18_imagenet.onnx文件:大家注意到我这有个图标了嘛,这是Netron,一个可视化模型的工具,大家也可以下一个试试,可以看看模型的结果,双击点开来看看,如下图所示:好叭,这个图太长了,我展示出来是想要大家看看这个开头的input和output,这两个就是我们在 torch.onnx.export函数中输入输出的tensor名称,不信的话你在torch.onnx.export中修改一下,得到的ONNX结果也会发生变化。运行推理引擎先来说说推理引擎是干什么的,通俗的说它是用来加速我们模型推理的,方便部署。上一步我们已经得到了中间表示的.onnx格式的模型,下面我们将来用推理引擎ONNX Runtime来运行一下.onnx模型。首先我们先导入onnxruntime包,然后用InferenceSession方法来载入onnx模型,获取Onnx Runtime推理器。import onnxruntime
ort_session = onnxruntime.InferenceSession('resnet18_imagenet.onnx')
接着我们需要有一个输入数据,我从网上下载了一张橘子图片作为输入,但是大家思考一下,我们能直接把下载的图片作为输入嘛,显然是不能的,因为我们输入的要求是[3,256,256]大小的图片,因此需要对输入的橘子做一些预处理操作,使其满足输入要求。img_path = 'orange.jpg' #输入图片路径
# 用 pillow 载入
from PIL import Image
img_pil = Image.open(img_path)
可以先来看一下图片和其尺寸:因为图像是PIL格式的,通道数为RGB格式表示图像有3个通道,即图片的shape为[3,310,163]。因为其不满足输入[3,256,256]的要求,故需要对图像进行预处理,如下:from torchvision import transforms
# 测试集图像预处理-RCTN:缩放裁剪、转 Tensor、归一化
test_transform = transforms.Compose([transforms.Resize(256),
transforms.CenterCrop(256),
transforms.ToTensor(),
transforms.Normalize(
mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225])
])
input_img = test_transform(img_pil) #将橘子图像送入预处理函数当中
这个预处理不用我过多介绍了叭,是非常常见的,主要就是把图像变成指定的[3,256,256]大小,并将其变成tensor格式并归一化。【对Totensor和Normalize不熟悉的可以看我的这篇博客】我们看看归一化之后图片的shape:已经变成[3,256,256],但是在输入模型之前呢,我们需要加一个batch维度:input_tensor = input_img.unsqueeze(0).numpy()
添加后的shape如下:到这里我们已经有了输入input_tensor,也构建了一个推理器ort_session,这样我们就能使用推理器中的run方法进行模型推理了,代码如下:# ONNX Runtime 输入
ort_inputs = {'input': input_tensor}
# ONNX Runtime 输出
pred_logits = ort_session.run(['output'], ort_inputs)[0]
pred_logits = torch.tensor(pred_logits)
注意这里我们把input_tensor先构建成一个字典,该字典的键input要和 torch.onnx.export函数中输入名称一致。ort_session.run方法的参数有两个,第一个是输出张量名称,也就是output,这个也和我们前面 torch.onnx.export函数中输出名称一致。第二个参数就是我们输入值的字典。我们可以来看看pred_logits的shape:这就是将我们的数据放入到模型中推理得到的结果,这个1000是ImageNet的分类类别,也就是模型最后的全连接层神经元个数为1000,如图:接着我们就可以由这个结果来预测出我们输入的图片属于哪一类物体的概率,这些就和正常的图像分类预测一致了,如下:# 对 logit 分数做 softmax 运算,得到置信度概率
pred_softmax = F.softmax(pred_logits, dim=1)
# 取置信度最高的前 n 个结果
n = 3
top_n = torch.topk(pred_softmax, n)
# 预测类别
pred_ids = top_n.indices.numpy()[0]
# 预测置信度
confs = top_n.values.numpy()[0]
# 载入类别 ID 和 类别名称 对应关系
df = pd.read_csv('imagenet_class_index.csv') 这个要下载ImageNet的类别索引对照表
idx_to_labels = {}
for idx, row in df.iterrows():
#idx_to_labels[row['ID']] = row['class'] # 英文
idx_to_labels[row['ID']] = row['Chinese'] # 中文
# 分别用英文和中文打印预测结果
for i in range(n):
class_name = idx_to_labels[pred_ids[i]] # 获取类别名称
confidence = confs[i] * 100 # 获取置信度
text = '{:<20} {:>.3f}'.format(class_name, confidence)
print(text)
我们可以看下可以看出橘子的概率还是蛮高滴。小结整个分类模型部署的代码到这里就结束啦,我们在来总结一下,主要分为三步:训练分类模型,得到模型权重文件将模型权重文件转换成ONNX中间表示格式使用ONNX Runtime推理引擎进行推理
我不是魔法师
神经网络推理加速入门——指令流水线
前言刚毕业的时候在青岛某信任职,做嵌入式软件开发,需要调试电路板。公司有个要求是所有新入职的员工,都要去公司自己的加工厂上两周的班,亲自组装生产电路板,体验产品生产的过程。于是,刚毕业的我,光荣的成为了一名流水线工人,虽然只有两周的时间。这期间,我和同事一起,完成过一天组装3万片电路板的成就,也完成过一天往电路板上插装不计其数电阻、电容的操作,白班夜班倒,记忆犹新。当时的我,坐在流水线的椅子上,满脑子就一个想法:“我今天就是个螺丝钉”——真拧螺丝。也正是这一次流水线的工作体验,让我明白了一个道理——一个产线或团队的工作效率,取决于整条流水线上动作最慢的那个人,而不是最快的那个人。一旦动作慢导致产品在你那积压,最常见的就是带小红帽的组长过来噼里啪啦说一顿。不过好在我当时手脚勤快,没有拖后腿。在流水线上,人就是个机器,机械的执行着每一个动作。直到后来开始做芯片开发,开始给指令排流水,才恍然大悟,原来,每条指令都是流水线工人。指令流水上一篇计算和存储的分离说到,在冯诺依曼架构的计算体系中,计算和存储是分离的。分离的结果就是,我们可以将计算和存储分别看做两个工人,而不是一个,每个工人只负责干自己的事儿。—— 【计算】这名工人只负责计算,【存储】这名工人只负责存储。计算好比在厨房做菜,存储好比从厨房冰箱里将菜拿出来。这个比喻很重要,能帮你很通俗的理解流水。那么问题来了,【一个人从冰箱里拿菜洗菜然后去做菜】和【两个人,一个专门拿菜洗菜,一个专门做菜】,哪种做饭效率高?做过饭的同学肯定知道,当然两个人配合快了。比如做西红柿鸡蛋,一个人需要先从冰箱拿出鸡蛋,记需要花费 A 的时间,然后炒鸡蛋需要花费B的时间,然后回去拿出西红柿,需要花费C的时间,然后将西红柿和鸡蛋一起炒,需要花费D的时间。总共花费 A + B + C + D的时间。而两个人的话,第一个人从冰箱里拿出鸡蛋,记A的时间,第二个人炒鸡蛋,花费B的时间,而此时,在第二个人炒鸡蛋的同时,第一个人可以同时去拿出西红柿,需要花费C的时间,然后第二个人抄完鸡蛋后,接过第一个人的西红柿一起炒,需要花费D的时间。总共花费 A + MAX(B,C) + D 的时间。两个一比较,肯定两个人做饭时间花的少吧。而两个人做饭时,B,C同时进行,就像流水线上的工人一样,你负责你的事,我负责我的事,我做完就给下一个人,下一个人动作快的话,可以立刻接过去,动作慢的话,就会导致产品积压。在计算机的体系结构中,计算指令和存储指令,一般都是两条独立的指令,并且在硬件部件上,是两个独立的硬件部件,比如计算是ALU单元,存储是DMA或load/store单元等。独立的硬件部件和独立的指令设计,天然的保证了两条指令之间可以排流水,就像流水线上的工人一样工作。还是拿卷积算法举例子,我先计算上半张图片,同时你就可以搬运下半张图片的数据,整体的AI计算开销,就会少很多。从而起到了推理加速的作用。不仅如此,在指令集设计或硬件设计时,一般都会考虑流水线的设计,从而完成更高效的计算和访存操作。可进行流水操作的指令,也绝不仅仅只有计算指令和存储指令等,不同的计算单元之间,也是可以排流水的。比如卷积层后面接一个池化层,那么在计算第二部分卷积的同时,可以计算第一部分池化。流水线的发明对于制造业来说是一次技术变革。早期的手工制造业是靠大量人工完成的,这种工作方式要求每个工作者要了解整个成品的完整制造过程,要快速的制造出一个成品需要对整个工序、流程非常熟悉,无形中提高了对工人素质的要求。而流水线的出现,不需要工人了解整个流程,为手工制造业实现了半自动化,加快了加工速度。好啦,本篇就简单介绍了一种推理优化的常见方式,那就是通过对指令排流水的方式,可以完成指令加速,从而完成推理优化。
我不是魔法师
神经网络推理加速入门——超简单的卷积和加法融合—伪代码版
前几天写了一个卷积神经网络(CNN)中,卷积和加法融合的文章。有同学发私信,希望写一个带代码版本的,方便更好的理解。我的第一反应是,代码版本的咋写,有那么多细节。后来一想,其实那位同学想知道的并不是那些细节,而是一个大致的流程。于是我说,行,那我写一个伪代码版的吧,把大致的代码思路写一下。至于具体卷积算法怎么实现的,建议chatGPT一下,或者看下开源深度学习仓库就行。如果没看之前的文章,可以看上一看:超简单的卷积和加法融合。开始分析还是以 resnet50 中的图为例,做一个卷积和加法的融合。正常情况下,上述网络片段在执行的时候大概是这样的:BatchNorm -> Relu -> Conv -| Add的左分支
| -> Add
-> Conv -| Add的右分支
写出伪代码,实际上就是一种顺序调用逻辑,比如bn_out = Batch_normal();
relu_out = Relu(bn_out);
conv_out_left = Conv2d(relu_out)
conv_out_right = Conv2d(...)
add_out = Add(conv_out_left, conv_out_right)
而一旦融合完之后,上图红框中的Conv 和 Add 就变成了一个算子,这里暂且称这个融合之后的算子为 ConvAdd 算子。于是,上述的图,就变成了如下的图:此时,整个网络片段的调用逻辑变成了:bn_out = Batch_normal();
relu_out = Relu(bn_out);
conv_out_right = Conv2d(...)
add_out = ConvAdd(relu_out, conv_out_right)
再把 ConvAdd 当做一个算子之后,便可以进行很多融合、拆图、流水并行操作。假设现在这个网络运行在一个Asic芯片上,芯片上卷积计算模块和加法计算模块是互相独立的,没有任何依赖。这里假设卷积输入的 Feature Map 的大小是 [n, hi, wi, ci],卷积核是[co, kh, kw, ci]。其余参数简化一下,将卷积 pad 简化为0,stride 简化为1,dilation简化为1。卷积的输出为[n, ho, wo, co]。那么卷积后面的加法,执行的两个tensor相加,也就变成了 [n, ho, wo, co] + [n, ho, wo, co] = [n, ho, wo, co]。那么,我们将卷积的输入(假设是下面的一张图),在H方向切成两份。那么计算完一整张图,需要调用两次卷积运算,第一次计算上半部分,第二次计算下半部分。两次计算中,大部分像素之间是没有关系的,仅仅在两部分交界的地方会有可能存在依赖。(存在依赖的条件为 kernel 大于1,或者 stride 大于1,这些情况先不考虑,暂时认为两部分像素没有关系)。那么第一次卷积计算,计算的输入是 [n, hi/2, wi, ci],计算输出结果是 [n, ho/2, wo, co]。此时计算的是前半部分的 hi。用红色表示。那么第二次卷积计算,计算的输入是 [n, hi/2, wi, ci],计算输出结果是 [n, ho/2, wo, co]。此时计算的是后半部分的 hi。用蓝色表示。同理,加法也会被分成两次计算,分别对应计算卷积的两次输出:第一次加法,计算的是第一次卷积的输出,即 [n, ho/2, wo, co]第一次加法,计算的是第一次卷积的输出,即 [n, ho/2, wo, co]那么,在两次计算的情况下,ConvAdd 这一个算子中,内部的实现逻辑大致应该是:conv_out_part1 = Conv2d(part1)
conv_out_part2 = Conv2d(part2)
add_out_part1 = Add(conv_out_part1)
add_out_part2 = Add(conv_out_part2)
但是这样显然是不行的,因为这样写还是串行执行:执行完第一次卷积执行第二次卷积,执行完第二次卷积执行第一次加法...那怎么让 Conv 和 Add 并行起来呢?通过观察可以发现,第一次的Add并不依赖第二次的Conv,并且我们已经假设了Asic芯片上Conv运算模块和Add模块完全独立。那么让第二次Conv和第一次Add并行起来的方法就是:第一次Conv计算完之后,直接计算第一次Add,然后同时并行第二次Conv,这个时候,代码的实现大致是这样:conv1 = Conv2d(part1)
-----------------------
add1 = Add(conv1)
conv2 = Conv2d(part2)
-----------------------
add2 = Add(conv2)
这个时候,Add 和 conv 在中间的一个流水级中并行起来了。所谓的一个流水级,指的是上面代码段中两个“ ------ ” 之间的代码,称之为在一个流水级中。那如果将图片拆成更多份,那可以并行的流水级就会更多。比如拆成4份,那可以有3个流水级中的Conv和Add并行起来。conv1 = Conv2d(part1)
-----------------------
add1 = Add(conv1)
conv2 = Conv2d(part2)
-----------------------
add2 = Add(conv2)
conv3 = Conv2d(part3)
-----------------------
add3 = Add(conv3)
conv4 = Conv2d(part4)
-----------------------
add4 = Add(conv4)
-----------------------
需要说明一点的是,上面伪代码中,每一个 “-----” 其实都代表了一个同步点。在实际部署到硬件上运行时,需要在这些同步点上设置同步操作,用来使上一个流水级中的所有计算操作全部完成即可。常用的同步操作有一些同步指令或者barrier指令。假设我们使用barrier指令来进行同步,那么上述完整的伪代码便是:conv1 = Conv2d(part1)
barrier()
add1 = Add(conv1)
conv2 = Conv2d(part2)
barrier()
add2 = Add(conv2)
conv3 = Conv2d(part3)
barrier()
add3 = Add(conv3)
conv4 = Conv2d(part4)
barrier()
add4 = Add(conv4)
barrier()
当然上述代码看起来太长了,可以写成循环的形式,还是以将H方向拆分 4 份为例:conv1 = Conv2d(part1);
barrier();
for i in range(1, 4):
add_i = Add(convi)
Conv_i+1 = Conv2d(part_i+1)
barrier()
add4 = Add(conv4)
barrier()
伪代码的逻辑还是很简单的,关键是需要理解Conv和Add并行流水的思想。这种方法可以用到的融合场景很多,并不仅仅局限于Conv和Add这两个算子,也不局限于某一个神经网络。只要是在硬件上计算单元可以并行执行,并且在神经网络结构图上前后有依赖的层,几乎都可以这么进行融合来提升整体性能。
我不是魔法师
从零手写Resnet50实战——权值另存为
大家好啊,我是董董灿。这是《从零手写Resnet50实战》的第二篇文章。往期文章列表: # 从零手写Resnet50实战——不使用第三方库权值怎么处理在制定了不用第三方库和框架,从零手写Resnet50的前提下,面临的第一个问题就是网络的权值怎么处理。网上有不少教程是基于已有的模型和推理框架来加载权值,然后完成的推理运算。而如果不依赖已有的框架,就需要对模型中的权值做一次自定义的格式转存。权值转存成文本文件第一步,把模型中的权值,另存为一个可以随意被操控的文件格式——最常见的 txt 文本格式。这样做的好处就是,以txt文件存储的模型参数,可以随时随地的,以读文件的方式随意加载。由于不再是结构化的数据,而是我们可以随意操控的数据(甚至可以修改),也方便神经网络中每一层的调试。于是,我写了个脚本,用来将参数数据保存下来。import torch
import torchvision
import numpy as np
from torchvision import models
resnet50 = models.resnet50(pretrained=True)
print(resnet50)
def save(data, file):
d = np.array(data.weight.data.cpu().numpy())
np.savetxt(file+str(".txt"), d.reshape(-1, 1))
save(resnet50.conv1, "resnet50_conv1")
save(resnet50.bn1, "resnet50_bn1")
def save_bottle_neck(layer, layer_index):
bottle_neck_idx = 0
layer_name = "resnet50_layer" + str(layer_index) + "_bottleneck"
for bottleNeck in layer:
save(bottleNeck.conv1, layer_name + str(bottle_neck_idx) + "_conv1")
save(bottleNeck.bn1, layer_name + str(bottle_neck_idx) + "_bn1")
save(bottleNeck.conv2, layer_name + str(bottle_neck_idx) + "_conv2")
save(bottleNeck.bn2, layer_name + str(bottle_neck_idx) + "_bn2")
save(bottleNeck.conv3, layer_name + str(bottle_neck_idx) + "_conv3")
save(bottleNeck.bn3, layer_name + str(bottle_neck_idx) + "_bn3")
if bottleNeck.downsample:
save(bottleNeck.downsample[0], layer_name + str(bottle_neck_idx) + "_downsample_conv2d")
save(bottleNeck.downsample[1], layer_name + str(bottle_neck_idx) + "_downsample_batchnorm")
bottle_neck_idx = bottle_neck_idx + 1
save_bottle_neck(resnet50.layer1, 1)
save_bottle_neck(resnet50.layer2, 2)
save_bottle_neck(resnet50.layer3, 3)
save_bottle_neck(resnet50.layer4, 4)
save(resnet50.fc, "resnet50.fc")
这个脚本可以自动地从网上下载已经训练好的模型,然后打印出Resnet50的网络结构,并且将有参数的层(主要是卷积层和BN层和FC层)中的参数保存下来。 简单验证存储的权值对不对Resnet50中的第一层卷积,卷积核大小为 7x7,channel 数是3,共 64 个卷积核,因此这一层的权值参数有 64x7x7x3 = 9408个。Resnet50中最后一层全连接层,将通道 2048 转为通道 1000,算法上为一个简单地 [m, k] x [k, n] = [m, n] 的矩阵乘法。 这一层的权值参数个数为 2048 x 1000 = 2048000个。查看保存的 resnet50_conv1.txt 和 resnet50_fc.txt 的行数:$ wc -l resnet50_conv1.txt
9408 resnet50_conv1.txt
$ wc -l resnet50_fc.txt
2048000 resnet50_fc.txt
txt文件每一行保存一个参数,可以看到参数个数是正确的。这样就完成了Resnet50权值的格式转存,转存到txt中,无论python环境,还是C++环境,都可以很友好的读入文件数据,然后进行计算。代码和保存的权值参数文件已经上传到了仓库——从零手写Resnet50说明:txt文件中的数据类型都是浮点型,浮点型能确保整个神经网络推理的精度达标,不至于误差太大,后续算法实现,也需要是浮点运算。
我不是魔法师
神经网络推理加速入门——超简单的卷积和加法融合
神经网络的优化除了之前提到的一些硬件优化手段之外,还有很多图层方面的优化手段。而且图层方面的优化,有时效果更佳。往往一个有效的优化,甚至可以“消除”掉一个算子的存在。这里的“消除”用的引号,是因为这个算子并未被真正消除,从整个神经网络的计算流上看,这个算法仍然存在。只不过,它的计算过程被其他计算过程掩盖住了,像是被“消除”了。算子融合这里介绍一种算子融合并且使用图层流水进行网络优化的方法。先看Resnet50 中一个网络片段。关于该网络的算法原理,可以参考以前的文章长文解析Resnet50的算法原理。下图展示的是 Resnet50 中第一个 block 结束时的图结构。该结构中,存在卷积算子和加法算子,我们可以利用融合手段,将红框标出的两个算子融合成一个算子。将卷积和加法融合成一个算子后,再通过指令调度,实现卷积和加法指令的 ping-pong 流水,便可以利用卷积的计算掩盖掉加法的计算。这里说一点,为什么卷积计算可以掩盖掉加法计算呢?因为在相同的输入输出规模的情况下,由于卷积的计算是乘累加,而加法的计算是单纯地加法,因此,在绝大部分的硬件上,一般都是卷积的计算耗时要大于加法。那怎么掩盖呢?正常的一个卷积后面如果跟一个加法的话,它的计算流从上到下是这样的:先计算卷积Conv,再计算加法Add。假设卷积的计算时间是 A, 加法的计算时间是 B,那么总共的耗时就是 A+B。这很简单,大家都会算。那如果将两个算子融合到一起,同时将一张输入图片(Feature Map)切成一半,分两次来运算。每次运算使用 ping-pong memory 来实现计算流水。那两次计算的计算流从上到下是这个样子。左侧 Conv 和 Add 计算前半张图,使用ping memory来计算,右侧 Conv 和 Add 计算后半张图,使用pong memory来计算。这里说明一下:Conv 和 Conv 是不能并行的,因为一般一个硬件上,只有一个Conv 的硬件计算单元,当然,有多个的又是另外的话题了,这里暂时不考虑。这个时候,可以看到第二行中,第一个 Add 和第二个 Conv 处在一个时间片内,同时由于使用的是不同的memory,两者可以完全并行。两者并行完成计算,此时,Conv 就掩盖掉了左侧 Add 的时间。而整个计算流消耗的时间便是:A/2 + A/2(掩盖掉了Add 的 B/2) + B/2 = A + B/2。可以看到,此时整体的计算耗时已经比不进行流水时,减少了一半的加法。那如果将图进一步切分,切成4份来进行运算呢?那此时的计算流从上到下是这个样子:由于第2、3、4行的 conv 分别掩盖掉了其左侧的Add 计算,因此,整个计算流所消耗的时间变成了 A + B/4。可以看到,将图切的越小,流水起来之后,掩盖掉的加法的时间越多,剩余的加法的时间越少。如果切的再多,加法的耗时甚至可以忽略掉了。总结这种方法很简单,也很容易实现,但从硬件上来说,需要硬件满足以下条件:卷积计算单元和加法计算单元在硬件上是独立的硬件有成熟的同步机制来完成卷积计算和加法计算的同步为什么需要同步机制呢?因为每一个横向的时间片段中,Add 和 Conv 没有任何数据依赖,但是纵向的看,Add 的计算总是依赖于上面的 Conv 的输出。因此,需要在每一个横向时间片段起始时,完成上一个时间片段中 Conv 计算和 Add 计算的同步。只要硬件上满足了以上条件,软件上的切图、排流水、融合操作其实很简单。很多深度学习编译器,如TVM提供了图融合和自动 tiling 策略,可以做到切图,并且调度指令完成流水排布。即使编译器限于某些开发难度限制,无法自动完成融合和tiling,也可以手动写一个融合大算子出来,手动排指令流水,也不是很难的事。总之,这种方法作为一个行之有效的融合方法,可以用在很多的神经网络性能优化中,而且效果很出众。
我不是魔法师
7步代码精读,带你搞懂书写数字识别
大家好啊,我是董董灿。图像识别有很多入门项目,其中Mnist 手写数字识别绝对是最受欢迎的。该项目以数据集小、神经网络简单、任务简单为优势,并且集合了CNN网络中该有的东西,可谓麻雀虽小,五脏俱全。非常适合新手上手学习。本文以代码走读的形式,带你一览该项目的每一处细节。文章末尾附代码下载链接,不用GPU, 你也可以从头训练一个神经网络出来。1、什么是手写数字识别简答来说,就是搭建了一个卷积神经网络,可以完成手写数字的识别。我用笔在纸上写了个6,神经网络就能认识这是个6,我写了个8,它就识别出来这是个8,就这么简单。之所以说该任务简单,是因为它的标签只有 0-9 这 10 种分类,相比于 resnet 等网络在 ImageNet 上 1000 个分类,确实小很多。虽然简单,但背后的原理却一点都不少,典型的CNN训练和算法无一缺席。与该项目一起出名的,便是大名鼎鼎的 MNIST(Mathematical Numbers In Text) 数据集。该数据集中包含了 60,000 个训练图像和 10,000 个测试图像,图像都是各种手写的数字,基本都是长这样的。2、7步精读代码在简单了解了项目背景后,我以代码走读的形式,一点点介绍该神经网络。第一步:导入必要的库# 导入NumPy数学工具箱
import numpy as np
# 导入Pandas数据处理工具箱
import pandas as pd
# 从 Keras中导入 mnist数据集
from keras.datasets import mnist# 导入NumPy数学工具箱import numpy as np # 导入Pandas数据处理工具箱import pandas as pd# 从 Keras中导入 mnist数据集from keras.datasets import mnist
keras 是一个开源的人工神经网络库,里面有很多经典的神经网络和数据集,要用的 mnist 数据集就在其中。第二步:加载数据集(x_train, y_train), (x_test, y_test)= mnist.load_data()
这条命令利用 keras 中自带的 mnist 模块,加载数据集(load_data)进来,分别赋值给四个变量。其中:x_train 保存用来训练的图像,y_train 是与之对应的标签。假设图像中的数字是1,那么标签就是1。x_test 和 y_test 分别为用来验证的图像和标签,也就是验证集。训练完神经网络后,可以使用验证集中的数据进行验证。第三步:数据预处理其中一个预处理内容是改变数据集的 shape,使其满足模型的要求。 # 导入keras.utils工具箱的类别转换工具
from tensorflow.keras.utils import to_categorical
# 给标签增加维度,使其满足模型的需要
# 原始标签,比如训练集标签的维度信息是[60000, 28, 28, 1]
X_train = X_train_image.reshape(60000,28,28,1)
X_test = X_test_image.reshape(10000,28,28,1)
# 特征转换为one-hot编码
y_train = to_categorical(y_train_lable, 10)y
_test = to_categorical(y_test_lable, 10)
这个数据集中的共 60000 张训练图像,10000 张验证图像,每张图像的长宽均为 28 个像素,通道数为 1。那么对于训练集 x_train 而言,将其形状变为 NHWC = [60000, 28, 28, 1], 验证集类似。to_categorical 的作用是将样本标签转为 one-hot 编码,而 one-hot 编码的作用是可以对于类别更好的计算概率或得分。one-hot之所以用 one-hot 编码,是因为对于输出 0-9 这10个标签而言,每个标签的地位应该是相等的,并不存在标签数字 2 大于数字 1 的情况。但如果我们直接利用标签的原始值(0-9)进行最终结果的计算,就会出现标签2 大于标签 1的情况。因此,在大部分情况下,都需要将标签转换为 one-hot 编码,也就独热编码,这样标签之间便没有任何大小而言。这个例子中,数字 0-9 转换为的独热编码为:array([[1., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
[0., 1., 0., 0., 0., 0., 0., 0., 0., 0.],
[0., 0., 1., 0., 0., 0., 0., 0., 0., 0.],
[0., 0., 0., 1., 0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 1., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 1., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0., 1., 0., 0., 0.],
[0., 0., 0., 0., 0., 0., 0., 1., 0., 0.],
[0., 0., 0., 0., 0., 0., 0., 0., 1., 0.],
[0., 0., 0., 0., 0., 0., 0., 0., 0., 1.]]
每一行的向量代表一个标签。假设 [1., 0., 0., 0., 0., 0., 0., 0., 0., 0.] 代表 0 而 [0., 1., 0., 0., 0., 0., 0., 0., 0., 0.] 代表1,可以看到这两者之间是正交独立的,不存在谁比谁大的问题。第四步:创建神经网络。# 从 keras 中导入模型
from keras import models
# 从 keras.layers 中导入神经网络需要的计算层
from keras.layers import Dense, Dropout, Flatten, Conv2D, MaxPooling2D
# 构建一个最基础的连续的模型,所谓连续,就是一层接着一层
model = models.Sequential()
# 第一层为一个卷积,卷积核大小为(3,3), 输出通道32,使用 relu 作为激活函数
model.add(Conv2D(32, (3, 3), activation='relu',
input_shape=(28,28,1)))
# 第二层为一个最大池化层,池化核为(2,2)
# 最大池化的作用,是取出池化核(2,2)范围内最大的像素点代表该区域
# 可减少数据量,降低运算量。
model.add(MaxPooling2D(pool_size=(2, 2)))
# 又经过一个(3,3)的卷积,输出通道变为64,也就是提取了64个特征。
# 同样为 relu 激活函数
model.add(Conv2D(64, (3, 3), activation='relu'))
# 上面通道数增大,运算量增大,此处再加一个最大池化,降低运算
model.add(MaxPooling2D(pool_size=(2, 2)))
# dropout 随机设置一部分神经元的权值为零,在训练时用于防止过拟合
# 这里设置25%的神经元权值为零model.add(Dropout(0.25))
# 将结果展平成1维的向量model.add(Flatten())
# 增加一个全连接层,用来进一步特征融合
model.add(Dense(128, activation='relu'))
# 再设置一个dropout层,将50%的神经元权值为零,防止过拟合
# 由于一般的神经元处于关闭状态,这样也可以加速训练
model.add(Dropout(0.5))
# 最后添加一个全连接+softmax激活,输出10个分类,分别对应0-9 这10个数字
model.add(Dense(10, activation='softmax'))
上面每一行代码都加了注释,说明每一行的作用,短短几行,便是这个手写数字识别神经网络的全部了。至于每一种算法的原理,如果想深入了解,这里有传送门:卷积的特征提取池化是什么全连接层的作用5分钟理解 dropout 的原理10 分钟搞懂 softmax 的底层逻辑第五步:训练# 编译上述构建好的神经网络模型
# 指定优化器为 rmsprop
# 制定损失函数为交叉熵损失
model.compile(optimizer='rmsprop',
loss='categorical_crossentropy',
metrics=['accuracy'])
# 开始训练
model.fit(X_train, y_train, # 指定训练特征集和训练标签集
validation_split = 0.3, # 部分训练集数据拆分成验证集
epochs=5, # 训练轮次为5轮
batch_size=128) # 以128为批量进行训练
Epoch 5/5329/329 [==============================] - 15s 46ms/step - loss: 0.1054 - accuracy: 0.9718 - val_loss: 0.0681 - val_accuracy: 0.9826
训练结果如上,可以看到最后的训练精度达到了98.26%,还是挺高的。第6步:验证集验证# 在测试集上进行模型评估
score = model.evaluate(X_test, y_test)
print('测试集预测准确率:', score[1])
# 打印测试集上的预测准确率
313/313 [==============================] - 1s 4ms/step - loss: 0.0662 - accuracy: 0.9815 测试集预测准确率: 0.9815000295639038可以看到在验证集上也能有98%的准确率。第7步:验证一张图片# 预测验证集第一个数据
pred = model.predict(X_test[0].reshape(1, 28, 28, 1))
# 把one-hot码转换为数字print(pred[0],"转换一下格式得到:",pred.argmax())
# 导入绘图工具包import matplotlib.pyplot as plt
# 输出这个图片plt.imshow(X_test[0].reshape(28, 28),cmap='Greys')
以验证集中的第一张图片为例来进行验证。1/1 [==============================] - 0s 17ms/step[4.2905590e-15 2.6790809e-11 2.8249305e-09 2.3393848e-11 7.1304548e-141.8217797e-18 5.7493907e-19 1.0000000e+00 8.0317367e-15 4.6352322e-10] 转换一下格式得到:7得到的数字是7,将该图片显示出来,确实是7。说明训练的模型确实达到了识别数字的水平。
3、总结手写数字识别项目比较简单,仅仅两个卷积层,整体运算量不大,就目前计算机的配置,即使笔记本基本上都可以完成该神经网络的训练和验证。
我不是魔法师
深度学习模型部署篇——从0部署深度学习语义分割模型(四)
基于FCN部署语义分割模型好啦,我们这就开始了喔,本小节使用的是FCN模型进行模型部署,因此建议你先读读我写在前面中给出的两篇文章,对FCN实现语义分割有一个基本的了解。导入工具包 import onnxruntime
import numpy as np
import cv2
from src import fcn_resnet50
import torch
import matplotlib.pyplot as plt
%matplotlib inline
大家注意一下这里from src import fcn_resnet50,我们从src下的fcn_model.py文件中导入了fcn模型结构,src下面的目录结构是这样的:大家这里可能有点疑惑,从fcn_model.py中导入文件,为什么这里只写了from src呢,其实其它的内容写在了__init__.py文件中,文件内容如下:对于此不熟悉的可以看一下我的这篇文章。创建模型 aux = False
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
classes = 20
# create model
model = fcn_resnet50(aux=aux, num_classes=classes+1)
weights_path = "./model_29.pth"
# delete weights about aux_classifier
weights_dict = torch.load(weights_path, map_location='cpu')['model']
for k in list(weights_dict.keys()):
if "aux" in k:
del weights_dict[k]
这部分其实就是FCN预测部分的代码,我直接复制过来了。加载模型并设置为推理模式 # 加载模型
model.load_state_dict(weights_dict)
# 设置为推理模式
model = model.eval().to(device)
pytorch转ONNX格式 x = torch.randn(1, 3, 520, 520).to(device)
output = model(x)
with torch.no_grad():
torch.onnx.export(
model, # 要转换的模型
x, # 模型的任意一组输入
'model_29.onnx', # 导出的 ONNX 文件名
opset_version=11, # ONNX 算子集版本
input_names=['input'], # 输入 Tensor 的名称(自己起名字)
output_names=['output'] # 输出 Tensor 的名称(自己起名字)
)
这部分和图像分类是一致的,需要注意的是这里的输入需要是(1, 3, 520, 520)的大小。载入ONNX模型,获取ONNX Runtime推理器 # ONNX 模型路径
onnx_path = './model_29.onnx'
ort_session = onnxruntime.InferenceSession(onnx_path, providers=['CUDAExecutionProvider', 'CPUExecutionProvider'])
providers=['CUDAExecutionProvider', 'CPUExecutionProvider']参数表示自己根据硬件选择GPU或者CPU。载入推理图像 img_path = 'cat.jpg'
img_bgr = cv2.imread(img_path)
我们来看看我们的小猫咪长什么样:图像预处理上面说到我们的输入需要是(1, 3, 520, 520)大小的,所以我们要进行resize操作: img_bgr_resize = cv2.resize(img_bgr, (520, 520)) # 缩放尺寸
enmmm,再来看看resize后的图像吧:【这是用plt库显示的结果】接着还需要进行其它的一下预处理操作,如下: img_tensor = img_bgr_resize
# BGR 三通道的均值
mean = (123.675, 116.28, 103.53)
# BGR 三通道的标准差
std = (58.395, 57.12, 57.375)
# 归一化
img_tensor = (img_tensor - mean) / std
img_tensor = img_tensor.astype('float32')
# BGR 转 RGB
img_tensor = cv2.cvtColor(img_tensor, cv2.COLOR_BGR2RGB)
# 调整维度
img_tensor = np.transpose(img_tensor, (2, 0, 1)) #h w c --> c h w
# 扩充 batch-size 维度
input_tensor = np.expand_dims(img_tensor, axis=0)
ONNX Runtime预测 # ONNX Runtime 输入
ort_inputs = {'input': input_tensor}
# onnx runtime 输出
ort_output = ort_session.run(['output'], ort_inputs)[0]
我们可以来看看ort_output的维度:还记得我们要对这个维度怎么操作吗,如下:我们要获取每个chanel中的最大值: pred_mask = ort_output.argmax(1)[0] #获取ort_output数组中的第一个二维元素
此时pred_mask维度为:我们可以通过np.unique(pred_mask)来看看pred_mask中有哪些值:从上图可以看出有0和8,0表示背景,8表示cat。这个和voc类别文档中的是一致的,文档中背景没有写。接着我们可以简单的实现一个可视化,先定义一个字典: # [127, 127, 127]表示灰色 ;[0, 180, 180]表示黄色
palette_dict = {0: [127, 127, 127], 8: [0, 180, 180]}
然后将0和8两个类别映射成不同的颜色: opacity = 0.2 # 透明度,越大越接近原图
# 将预测的整数ID,映射为对应类别的颜色
pred_mask_bgr = np.zeros((pred_mask.shape[0], pred_mask.shape[1], 3))
for idx in palette_dict.keys():
pred_mask_bgr[np.where(pred_mask==idx)] = palette_dict[idx]
pred_mask_bgr = pred_mask_bgr.astype('uint8')
# 将语义分割预测图和原图叠加显示
pred_viz = cv2.addWeighted(img_bgr_resize, opacity, pred_mask_bgr, 1-opacity, 0)
我们可以来看下最终的预测结果,即pred_viz,如下:结果还是非常不错的。基于MMCV库实现语义分割模型部署首先来介绍一下MMCV库,其是一个用于计算机视觉和多媒体计算的开源工具包,主要用于深度学习项目的开发和研究。MMCV是由中国科学院自动化研究所(Institute of Automation, Chinese Academy of Sciences)开发和维护的,它提供了许多用于图像和视频处理、计算机视觉任务、模型训练和部署的实用工具和组件。使用这个库我们可以很方便的实现各种计算机视觉任务,首先我们先来安装一下这个库: pip install -U openmim
mim install mmengine
mim install mmcv==2.0.0
接着安装其它的工具包: pip install opencv-python pillow matplotlib seaborn tqdm pytorch-lightning 'mmdet>=3.1.0' -i https://pypi.tuna.tsinghua.edu.cn/simple
这里使用了清华园镜像进行安装,会快很多。大家注意安装包的时候不要打开系统代理,不然会安装失败。由于我们要进行的是语义分割任务,因此需要下载mmsegmentation 源代码: git clone https://github.com/open-mmlab/mmsegmentation.git -b v1.1.1
然后我们进入mmsegmentation 目录安装MMSegmentation库: # 进入主目录
import os
os.chdir('mmsegmentation')
# 安装`MMSegmentation`库
pip install -v -e .
安装好后,我们来验证下我们是否安装成功: # 检查 mmcv
import mmcv
from mmcv.ops import get_compiling_cuda_version, get_compiler_version
print('MMCV版本', mmcv.__version__)
print('CUDA版本', get_compiling_cuda_version())
print('编译器版本', get_compiler_version())
# 检查 mmsegmentation
import mmseg
from mmseg.utils import register_all_modules
from mmseg.apis import inference_model, init_model
print('mmsegmentation版本', mmseg.__version__)
我的版本是这样的:这些准备好了之后,我们就可以使用mmsegmentation进行语义分割任务了,操作也很简单,主要就是对各种配置文件的修改。由于本节主要介绍模型部署,这里如何进行训练就不叙述了。这里我直接拿同济子豪兄得到的西瓜语义分割ONNX模型来举例了下载完成后解压就得到了ONNX格式的模型。这里先来简单介绍一下西瓜语义分割数据集:一共有6个类别,分别为:0:背景1:红壤2:绿壳3:白皮4:黒籽5:白籽下面是标注的图像:我们有了ONNX模型后,我们就可以使用ONNX Runtime推理器进行推理了。 # ONNX 模型路径
onnx_path = 'mmseg2onnx_fastscnn/end2end.onnx'
ort_session = onnxruntime.InferenceSession(onnx_path, providers=['CUDAExecutionProvider', 'CPUExecutionProvider'])
其实到这里就和前面基于FCN进行语义分割差不多了,我讲这部分就是想让大家了解了解MMCV这个工具,但是这节也没有详细介绍啦,后期可能会出一些基于MMCV实现目标检测、语义分割的博客。西瓜数据集一个有6个类,它们的值都很接近,所以颜色很类似,我们将这6个值映射到不同的颜色,这样可视化出来更加美观: # 各类别的配色方案(BGR)
palette = [ ['background', [127,127,127]],
['red', [0,0,200]],
['green', [0,200,0]],
['white', [144,238,144]],
['seed-black', [30,30,30]],
['seed-white', [180,180,180]]
]
palette_dict = {}
for idx, each in enumerate(palette):
palette_dict[idx] = each[1]
这个代码不知道大家能否理解,有不理解的其实调试一下就会豁然开朗:看了上图的调试过程,大家是不是就清晰了呢,最后palette_dict是一个字典,内容如下:接下来,我们封装一个处理单张图像的函数,如下:【其实这部分就是把上节的几个小部分整合到了一起,没什么难度】 opacity = 0.2 # 透明度,越大越接近原图
def process_frame(img_bgr):
'''
输入摄像头画面 bgr-array,输出图像 bgr-array
'''
# 记录该帧开始处理的时间
start_time = time.time()
# 从原图中裁剪出高宽比1:2的最大图像
h, w = img_bgr.shape[0], img_bgr.shape[1]
new_h = w // 2 # 横屏图片,截取一半的宽度,作为新的高度
img_bgr_crop = img_bgr[0:new_h, :]
# 缩放至模型要求的高1024 x 宽2048像素
img_bgr_resize = cv2.resize(img_bgr_crop, (2048, 1024)) # 缩放尺寸
# 预处理
img_tensor = img_bgr_resize
mean = (123.675, 116.28, 103.53) # BGR 三通道的均值
std = (58.395, 57.12, 57.375) # BGR 三通道的标准差
# 归一化
img_tensor = (img_tensor - mean) / std
img_tensor = img_tensor.astype('float32')
img_tensor = cv2.cvtColor(img_tensor, cv2.COLOR_BGR2RGB) # BGR 转 RGB
img_tensor = np.transpose(img_tensor, (2, 0, 1)) # 调整维度
input_tensor = np.expand_dims(img_tensor, axis=0) # 扩充 batch-size 维度
# ONNX Runtime预测
# ONNX Runtime 输入
ort_inputs = {'input': input_tensor}
# onnx runtime 输出
ort_output = ort_session.run(['output'], ort_inputs)[0]
pred_mask = ort_output[0][0]
# 将预测的整数ID,映射为对应类别的颜色
pred_mask_bgr = np.zeros((pred_mask.shape[0], pred_mask.shape[1], 3))
for idx in palette_dict.keys():
pred_mask_bgr[np.where(pred_mask==idx)] = palette_dict[idx]
pred_mask_bgr = pred_mask_bgr.astype('uint8')
# 将语义分割预测图和原图叠加显示
pred_viz = cv2.addWeighted(img_bgr_resize, opacity, pred_mask_bgr, 1-opacity, 0)
img_bgr = pred_viz
# 记录该帧处理完毕的时间
end_time = time.time()
# 计算每秒处理图像帧数FPS
FPS = 1/(end_time - start_time)
# 在画面上写字:图片,字符串,左上角坐标,字体,字体大小,颜色,字体粗细
scaler = 2 # 文字大小
FPS_string = 'FPS {:.2f}'.format(FPS) # 写在画面上的字符串
img_bgr = cv2.putText(img_bgr, FPS_string, (25 * scaler, 100 * scaler), cv2.FONT_HERSHEY_SIMPLEX, 1.25 * scaler, (255, 0, 255), 2 * scaler)
return img_bgr
接着我们可以来调用摄像头一帧图像来看看结果: # 获取摄像头,0为电脑默认摄像头,1为外接摄像头
cap = cv2.VideoCapture(0)
# 拍照
time.sleep(1) # 运行本代码后等几秒拍照
# 从摄像头捕获一帧画面
success, frame = cap.read()
cap.release() # 关闭摄像头
cv2.destroyAllWindows() # 关闭图像窗口
frame的结果如下:enmmmm,我没有西瓜啊,所以这个是手机里面的图片接着使用img_bgr = process_frame(frame)来推理这张图片,分割结果如下:可以看到,FPS为1.71,针对我的电脑它已经很快了,大家可以试试不使用ONNX Runtime推理的FPS,你会发现极其极其低。
我不是魔法师
深度学习模型部署篇——利用Flask实现深度学习模型部署(三)
flask模型部署初探下面我们就来开始介绍了喔,让我们一起来学一下叭。【这回采用步骤式讲解看看效果】首先我先来梳理一下代码运行的整体流程,这样大家可能会更清晰一点,如下图所示。大家要注意的是,我们会有两个.py文件,一个用于服务端开启服务,另一个用于客户端发送请求并接受服务端的返回值。客户端的代码较为简单,这里重点说说服务端的代码运行流程。服务端代码中主要有三个函数,分别为predict、get_prediction、transform_image。当我们运行服务端程序时,app.run启动,服务开启,此时会监听客户端是否发送请求,若检测到客户端发送请求,则会进入predict函数处理这个请求,接着predict函数会调用get_prediction函数,而get_prediction函数会调用transform_image函数。先给大家介绍代码运行流程,大家再看下面的代码应该就比较清晰了,下文将分为服务端和客户端两部分介绍代码。服务端创建一个Flask应用: app = Flask(__name__)
准备模型和资源 # 在指定设备上创建 AlexNet 模型
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
model = AlexNet(num_classes=5).to(device)
# 加载预训练权重
model.load_state_dict(torch.load(r'E:\模型部署\checkpoint\AlexNet.pth', map_location='cpu'))
# 设置模型为评估模式
model.eval()
注意AlexNet.pth模型是我们在上讲中介绍的花的五分类模型。其实模型部署本质上就是模型的测试,所有我们要将模型设置成评估模式。定义预测函数 def transform_image(image_bytes):
my_transforms = transforms.Compose([transforms.Resize(255),
transforms.CenterCrop(227),
transforms.ToTensor(),
transforms.Normalize(
[0.485, 0.456, 0.406],
[0.229, 0.224, 0.225])])
image = Image.open(io.BytesIO(image_bytes))
return my_transforms(image).unsqueeze(0)
大家还是要注意一下这里,我们将图像resize到227×227大小,这是因为AlexNet的输入要求,不清楚的可以看下这篇博客:深度学习经典网络模型汇总1——LeNet、AlexNet、ZFNet还要注意这里最后使用unsqueeze(0)方法添加了一个batch维度信息。定义预测函数 def get_prediction(image_bytes):
# 记录该帧开始处理的时间
start_time = time.time()
# 转换图像数据为模型输入格式
tensor = transform_image(image_bytes=image_bytes)
# 通过模型进行前向传播
outputs = model.forward(tensor)
# 对模型输出进行 softmax 操作
pred_softmax = F.softmax(outputs, dim=1)
# 获取前N个预测结果
top_n = torch.topk(pred_softmax, 5)
pred_ids = top_n.indices[0].cpu().detach().numpy() # 转换为NumPy数组
confs = top_n.values[0].cpu().detach().numpy() * 100 # 转换为NumPy数组,并转换为百分比
# 记录该帧处理完毕的时间
end_time = time.time()
# 计算每秒处理图像帧数FPS
FPS = 1 / (end_time - start_time)
# 载入类别和对应 ID
idx_to_labels = np.load('idx_to_labels1.npy', allow_pickle=True).item()
results = [] # 用于存储结果的列表
for i in range(5):
class_name = idx_to_labels[pred_ids[i]] # 获取类别名称
confidence = confs[i] # 获取置信度
text = '{:<6} {:>.3f}'.format(class_name, confidence)
results.append(text) # 将结果添加到列表中
return results, FPS # 返回包含类别和置信度的列表
这步其实和我上一讲的内容是差不多的,这个函数主要是对图像进行推理,并输出推理的结果和推理时间。定义接收上传图片并预测的路由 @app.route('/predict', methods=['POST'])
def predict():
if request.method == 'POST':
file = request.files['file']
img_bytes = file.read()
class_info, FPS = get_prediction(image_bytes=img_bytes)
response_data = {'class_info': class_info, 'FPS': FPS}
return jsonify(response_data)
解释一下这个@app.route('/predict', methods=['POST'])叭,它是一个 Flask 路由装饰器,它告诉 Flask 在接收到 /predict 路径上的 POST 请求时,会调用下面定义的predict()函数来处理这个请求。启动 Flask 应用 if __name__ == '__main__':
app.run()
app.run()是 Flask 应用的运行函数,它启动了一个本地的开发服务器,用于监听来自客户端的请求并响应。客户端发送请求 # 发送 POST 请求到 Flask 服务器
resp = requests.post("http://localhost:5000/predict",
files={"file": open('flower.jpg', 'rb')})
处理服务端返回结果 # 检查服务器响应状态码
if resp.status_code == 200: # 如果响应状态码为 200 表示成功
# 从响应中提取 JSON 数据
response_data = resp.json()
class_info = response_data['class_info'] # 提取预测结果信息
fps = response_data['FPS'] # 提取处理帧数信息
# 输出预测的类别信息
for info in class_info:
print(info)
# 输出处理帧数信息
print("FPS:", fps)
else: # 如果响应状态码不是 200,则表示出现了错误
print("Error:", resp.text) # 输出错误信息
运行结果首先我们要运行服务端的程序test_alexnet.py开启服务,可以通过anaconda终端执行,如下:接着我们可以新开一个终端执行客户端程序sent_post.py发送请求,或者直接在pycharm上执行程序,如下:从上图可以看出郁金香的识别率达到了99.964,哦,忘记给大家看我测试的图片了,是这张喔:我们也可以发现FPS为15.6,但是我们一般不取第一次的结果,因为会进行初始化等操作,影响速度,我们在运行几次看看FPS结果。后面几次FPS大概稳定在20-21左右。 通过ONNX加速模型部署在上两讲我们介绍了通过ONNX加速模型部署,那么这里我们自然也要试一试,看看速度有没有加速腻。那么其实这一部分的代码和上一小节是非常类似的,我将服务端代码写在了test_alexnet_onnx.py中,客户端代码没有改变,仍然是sent_post.py。我把主要修改的地方说明一下,其它一些细节大家可以自己去github下载源码查看。加载ONNX模型 # 加载模型
model = AlexNet(num_classes=5).to(device)
def load_onnx_model():
global ort_session
ort_session = onnxruntime.InferenceSession(r'E:\模型部署\Alex_flower5.onnx')
# 在应用启动时加载 ONNX 模型
load_onnx_model()
这里加载的是ONNX模型,至于如何得到ONNX模型可以看我上一讲的内容。ONNX推理引擎推理 ort_inputs = {'input': tensor.numpy()}
pred_logits = ort_session.run(['output'], ort_inputs)[0]
剩下的基本都差不多了,我们直接来看看运行的效果叭。首先开启服务,等待请求,如下:然后运行客户端代码,发送请求,获得结果:可以看到预测精度和之前使用pytorch预测时是一致的,但FPS提高到了26.8。当然了,同样的道理,这是第一次运行,FPS会相对较低,我们再运行几次,如下:可以发现,现在的FPS可以基本稳定在32左右,是不是比之前快了不少呢,大家快去试试叭。 加点佐料不知道大家发现没有,上面的功能算是实现了通过Flask部署深度学习模型,但是总感觉差点意思,于是准备结合前端来搭建一个稍微好看的界面,通过点击前端的按钮来发送请求。说干就干,但是好像干不动,因为自己不会前端呀,但是又问题不大,因为我会百度呀,直接找一个前端的代码就好了嘛,于是找到了霹雳吧啦Wz大佬滴代码,对其稍微改进了一下,使用了ONNX进行模型推理 ,并在前端输出FPS信息,代码为main_html_test.py。同样的,一些细节大家详细移步源码查看我们先来看一看实现的效果叭,然后我再来解释一下如何实现的,效果如下:enmmmm,开始准备展示动态图的,但是运行录屏工具后,预测的FPS就下降了,所以大家还是看看图片叭。首先我们运行main_html_test.py程序,会得到如下结果:点击上图中的链接进入前端界面:然后点击选择文件,再点击预测,即可显示预测结果和FPS,如下:上面就是最终的效果啦,最后我来稍微解释代码的整个流程,如下:用户在前端界面上选择一个图像文件。用户点击预测按钮,触发 test() 函数。test() 函数使用 AJAX 将图像文件发送到后端的 /predict 路由。后端接收到请求,调用 predict() 函数进行图像预测。predict() 函数返回预测结果和FPS信息,发送回前端。前端接收到后端返回的数据,将预测结果和FPS信息展示在页面上。关于test()函数的内容如下: function test() {
// 获取选择的文件对象
var fileobj = $("#file0")[0].files[0];
console.log(fileobj);
// 创建一个 FormData 对象,用于将文件对象传递到后端
var form = new FormData();
form.append("file", fileobj);
// 初始化变量用于存储分类结果和FPS信息
var flower='';
var fps = '';
// 发送AJAX请求到后端的predict路由
$.ajax({
type: 'POST',
url: "predict",
data: form,
async: false,
processData: false,
contentType: false,
success: function (data) {
console.log(data);
// 从返回的数据中获取分类结果和FPS信息
var results = data.class_info;
fps = data.FPS;
console.log(results);
console.log(fps);
// 生成分类结果的HTML字符串
var flowerResult = '';
results.forEach(e => {
flowerResult += `<div style="border-bottom: 1px solid #CCCCCC;line-height: 60px;font-size:16px;">${e}</div>`;
});
// 生成FPS信息的HTML字符串
var fpsResult = `<div style="border-top: 1px solid #CCCCCC;line-height: 60px;font-size:16px;">FPS: ${fps.toFixed(2)}</div>`;
// 将生成的分类结果和FPS信息插入到页面元素中
document.getElementById("out").innerHTML = flowerResult + fpsResult;
},
error: function () {
console.log("后台处理错误");
}
});
}
我不是魔法师
神经网络推理加速入门——吞吐和延时
今天介绍两个在做神经网络性能优化中,非常非常重要的概念——吞吐和延时。其实不光在做神经网络性能优化时会用到,在计算机网络的性能调优时,这两个概念也会被反复提到,可见其重要性。很多同学对这两个概念的最大误区,大概都集中在:高吞吐就等于低延时,低吞吐就等于高延时。这样理解是有问题的。下面介绍这两个概念,帮助大家更深入的理解其区别。吞吐吞吐或吞吐量(Throughput):完成一个特定任务的速率(The Rate of completing a specific action),也可以理解为在单位时间内完成的任务量。对于计算机网络而言,吞吐量的衡量单位一般是 bits / second 或者 Bytes / second。举个例子,如果说一条数据通路的吞吐量是 40Gbps,那么意味着,如果你往这个数据通路中注入40Gb 的数据量,那么它可以在1秒内流过这条数据通路。对于神经网络而言,我们可以把吞吐量的衡量设置为每秒处理的图片数量(如果是图像任务)或语音数量(如果是NLP任务)。延时延时(Latency):完成一个任务所花费的时间(The time taken to complete an action)。举个例子,如果我用我的电脑去 ping 一个网站,从我发送这条 ping 指令(数据包)开始,一直到这条 ping 指令到达对方服务器的时间,就可以理解为延时。ping 百度的延时。一般在打游戏时,都会关注延时,如果延时太高,玩游戏就会很卡,同样,ping 百度也是很多开发人员喜欢的用来测试网络环境的手段。你是不是也喜欢在测试网络的时候,试试能否打开百度呢?那么,吞吐量和延时这两者有什么关系呢?是不是意味着,高的吞吐量就会有低的延时?延时增加总是会导致吞吐量减少?一个例子我们看一个ATM(Automatic Teller Machine)机取款的例子。假设银行里有一台ATM机,平均下来它基本会花费1分钟将钱吐出来送给客户(包括插卡、输密码等步骤,这里不考虑个人差异等因素)。这就意味着,如果我排着队轮到我使用这台ATM机取钱,我可以预见的是,1分钟的时间,我就可以拿到钱并且离开ATM机。换句话说,这台ATM机的延时是1分钟(或者60秒,或者60000毫秒)。那么吞吐量呢?吞吐量是1/60个人每秒。也就是说,如果存在 1/60个人去取钱的话,那么ATM机每秒能接待的客户是1/60个。这是很简单的数学计算。所以,吞吐量 = 1 / 延时 ?对么?如果ATM机突然进行了升级,从之前平均1分钟可以接待一个客户,到升级后平均30秒就可以接待一个客户。那么此刻的ATM机的延时是多少?没错,是30秒。那么吞吐量呢?30秒可以接待1个客户,一分钟可以接待两个,吞吐量翻倍了。延时减半,吞吐量翻倍。看起来很符合上面的公式。我们继续。银行为了应对更多的客户取钱需求,在原来仅有的ATM机旁又安装了一台新的ATM机,我们假设这两天ATM机都是未升级前的。也就是一台机器平均1分钟可以接待一名客户。那么我去取钱,从我占据一台ATM机,到取出钱来,还是会花费1分钟,也就是延时是1分钟。那么此时的吞吐量呢?两天ATM机可以同时一起工作,也就是1分钟可以处理两名客户。吞吐量为2个人/分钟,或者2/60个人每秒。和只有一台ATM机的时候相比,延时没有变,一个客户取一次钱,都是需要花费1分钟,但是整个ATM机的吞吐量却增加了一倍。吞吐量的增加,和延时没有关系!这个例子很清晰的可以说明这个问题。所以,对于吞吐量,我们可以理解为,一个系统可以并行处理的任务量。而延时,指一个系统串行处理一个任务时所花费的时间。对应到神经网络性能优化这个场景下。神经网络的吞吐量,就是每秒中可以处理的图片数量,或者语音数量。这与模型本身的性能有关,也与实际完成计算的硬件资源有关。比如两个GPU可以并行独立完成,其吐吞量一般要比单个 GPU 高。搞神经网络训练的人,都喜欢堆显卡,就是为了提高整体系统吐吞量,毕竟训练一个模型,需要处理海量的数据。搞神经网络推理的人,都喜欢做性能优化,为了提高整个模型在有限硬件资源下的速度。毕竟,2秒完成一张图片的识别会让人忍受不了,而1ms的时间,大部分人会感受不到卡顿。
我不是魔法师
Resnet图像识别入门——全连接
大家好啊,我是董董灿。上一篇介绍了池化层Resnet图像识别入门——池化层,池化层一般接在卷积层后面,用来完成特征图的降维和特征融合操作。除了池化层,在CNN网络的最后,一般还会有一个全连接层(Fully Connected Layer)。那么CNN中为什么还需要一个全连接层呢,它的作用是什么?这篇文章会告诉你答案。全连接层全连接层,指的是每一个结点都与上一层的所有结点相连(示意图如上图所示),用来把前面几层提取到的特征综合起来。由于其全连接的算法,一般全连接层的参数也是最多的。前面的文章卷积的核心,特征提取提到卷积的作用是完成图像的特征提取,那提取出了特征之后,还是无法根据提取的一堆特征来完成图像的识别。因为卷积层提取出来的特征太多了。举个例子,一张画着猫咪的图片,经过几十层卷积的特征提取,很有可能已经提取出了几十个甚至上百个特征,那我们如何根据这几十上百个特征来最终确认,这是一只猫呢?把上面的问题细化并且简化一下,不说几十上百个特征,就说卷积层只提取了3个特征:分别是鼻子,耳朵和眼睛。实际上,有鼻子、耳朵和眼睛这三个特征的动物有很多,我们并不能只根据某个动物有鼻子、耳朵和眼睛,就把它简单的认为是一只猫。那么就需要一种方法,把鼻子、耳朵和眼睛这三个特征进一步融合,使得神经网络看到这三个特征的融合集合之后,可以区分这是一只猫而不是一只狗。上面的例子比较简单,实际网络中卷积提取的特征远远不止3个,而是成百上千个,将这些特征进一步融合的算法,就是全连接。或者说,全连接,可以完成特征的进一步融合。使得神经网络最终看到的特征是个全局特征(一只猫),而不是局部特征(眼睛或者鼻子)。之前在某文章中看到过一个对全连接的比较形象的回答,大意是说:假设你是一只蚂蚁,你的任务是找小面包。这时候你的视野比较窄,只能看到很小一片区域,也就只能看到一个大面包的部分。当你找到一片面包之后,你根本不知道你找到的是不是全部的面包,所以你们所有的蚂蚁开了个会,互相把自己找到的面包的信息分享出来,通过开会分享,最终你们确认,哦,你们找到了一个大面包。上面说的蚂蚁开会的过程,就是全连接,这也是为什么,全连接需要把所有的节点都连接起来,尽可能的完成所有节点的信息共享。说到这,大概就能理解全连接的作用了吧。卷积和全连接其实有两首诗可以很形象的概括卷积和全连接的作用。我们知道卷积是对图像的局部区域进行连接,通过卷积核完成的是感受野内的长宽方向以及channel 方向的数据连接。因此,卷积操作,提取的特征是局部特征。也就是说,卷积是“不是庐山真面目,只缘身在此山中”。而全连接层呢?它的每次完成的是所有channel方向的连接,它看到的是全局特征。全连接是“不畏浮云遮望眼,自缘身在最高层”。除此之外,卷积和全连接在算法上是可以转换的。通常情况下,在进行全连接的计算时,可以把它等效于卷积核为1x1的卷积运算。总结一下全连接的作用,说的学术专业一点,就是把卷积层学到的特征空间映射到样本标记空间。说的通俗易懂点,就是把卷积学到的一堆特征互相融合一下,变成样本(比如一只猫)的代表。在使用Resnet50对ImageNet2012数据集进行分类时,最终完成某个图片的分类,全连接层会输出一个值。在ImageNet 中,281-287都代表猫。比如282这个值,代表的是一只虎猫,而这个值,就是把所有的虎猫的特征进行了融合后计算而来的。281 n02123045 猫, tabby, tabby cat282 n02123159 猫, tiger cat283 n02123394 猫, Persian cat284 n02123597 猫, Siamese cat, Siamese285 n02124075 猫, Egyptian cat286 n02125311 猫, cougar, puma, catamount, mountain lion, painter,panther, Felis concolor287 n02127052 猫, lynx, catamount看到这,可能有人会问,既然全连接层处理的特征比卷积层信息更丰富,那为什么在CNN网络中进行图像识别和分类时,我们还大量的使用卷积而不全部使用全连接呢?答案很简单。全连接由于连接了上一层所有的节点,需要的模型参数更多,计算更密集。一个普通的卷积神经网络,如果用全连接来实现,你可以试试,分分钟挤爆你的CPU,甚至你的显卡。
我不是魔法师
从零手写Resnet50实战—手写龟速卷积
大家好啊,我是董董灿。这是从零手写Resnet50实战的第3篇文章。请跟着我的思路,一点点地手动搭建一个可以完成图片分类的神经网络,而且不依赖第三方库,完全自主可控的手写算法。如对网络中的算法和原理不太熟悉,请移步万字长文解析Resnet50的算法原理。我的目标是,识别出下面的这张图片是一只猫:项目地址:从零手写resnet50。正文上一篇文章权值另存为,我们已经把Resnet50中的所有权值参数都保存到txt中了。接下来,把上图中猫的图片导入到内存中。# 使用 Pillar 库来导入图片
# 仅使用该库导入图片
# 不使用该库进行任何其他的计算操作
from PIL import Image
# 读打开图片并读入到 img 中
img = Image.open('../cat.jfif')
# 将图片resize成长宽为(224,224)的图片
img = img.resize((224, 224))
这里说一下为什么要做 resize?因为 Resnet50 首层卷积,接收的是一个 3 通道的图片数据。如果图片长宽太大,会使得卷积运算量过大,运行速度很慢,这一点在本文后面的实验可以看出来。而且在试验 Resnet50 中,一个很常见的使用小图做运算的方法,便是将不规则大小的图片 resize 成(224,224)。其中两个 224 分别代表图片的长和宽,3 代表图片有 3 个通道。在将图片导入到内存中之后,剩下的就是要将图片数据输入到神经网络中。但在此,需要先将核心算法完成,才能搭建成神经网络。手写算法之——卷积在Resnet50中,存在 6 种算法,分别是卷积(Convolution,Conv)批归一化(Batch Normal,BN)池化(Pooling)激活(Relu)加法(Add)全连接(Fully Connected, FC)其中,Conv 和 FC 可以看作一类:都是在某些维度做乘累加计算;Pooling与卷积类似,只不过少了channel维度的累加。BN是对输入数据的做批归一化操作,算法实现也不太难;而激活和加法就更简单了,属于两行代码就能搞定的算法。所以,擒贼先擒王,先手写一个卷积算法试试水。# 使用NHWC的 layout 来计算
# 卷积暂时不考虑 dilation 的存在
# 因为Resnet50中的卷积都不带 dilation 参数
def my_conv2d(img, weight, hi, wi, ci, co, kernel, stride, pad):
'''
img:输入图片数据
weight:权值数据(卷积核)
hi:输入图片高度-height
wi:输入图片宽度-width
ci:输入图片通道-channel,与weight的channel一致
co:输出图片通道-channle,与weight的个数一致
kernel:卷积核的大小
stride:卷积核在输入图片上滑动步长
pad:输入图片周围补充的pad值
'''
# 通过输入参数计算输出图片的长和宽
# 在 Resnet50 中,卷积核在 h 方向和 w 方向的
# 尺寸都是一样的,pad 也都是一样的,因此,
# 这里用一个值来代表。
ho = (hi + 2 * pad - kernel) // stride + 1
wo = (wi + 2 * pad - kernel) // stride + 1
# 将权值数据 reshape 成 co, kh, kw, ci 的形式
weight = np.array(weight).reshape(co, kernel, kernel, ci)
# 在输入图片周围补充pad值
img_pad = np.pad(img, ((pad, pad), (pad, pad), (0, 0)), 'constant')
# 初始化输出图片
img_out = np.zeros((ho, wo, co))
# 下面是卷积计算的核心逻辑
# 其效果类似于 nn.conv2d
for co_ in range(co):
for ho_ in range(ho):
in_h_origin = ho_ * stride - pad
for wo_ in range(wo):
in_w_origin = wo_ * stride - pad
filter_h_start = max(0, -in_h_origin)
filter_w_start = max(0, -in_w_origin)
filter_h_end = min(kernel, hi - in_h_origin)
filter_w_end = min(kernel, wi - in_w_origin)
acc = float(0)
for kh_ in range(filter_h_start, filter_h_end):
hi_index = in_h_origin + kh_
for kw_ in range(filter_w_start, filter_w_end):
wi_index = in_w_origin + kw_
for ci_ in range(ci):
in_data = img[hi_index][wi_index][ci_]
weight_data = weight[co_][kh_][kw_][ci_]
acc = acc + in_data * weight_data
img_out[ho_][wo_][co_] = acc
return img_out
上面是手写的一个卷积算法,采用了最原始的堆叠循环的方式,没有对算法做任何的优化。之所以这么写,是因为这样可以很清晰地看到卷积的计算过程。将图片输入给卷积进行运算在定义完上述卷积运算后,就可以将上一步导入的图片,输入给卷积,计算一下试试水了。# 读入图片并转换为指定大小
img = Image.open('../cat.jfif')
img = img.resize((224, 224))
# 将Pillow Image对象转换为numpy数组
# data is layout as NHWC
out = np.array(img)
# 这个函数用来从保存的权值文件中读取权值数据
def get_weight_from_file(f):
k = []
with open(f, 'r') as f_:
lines = f_.readlines()
for l in lines:
k.append(float(l))
return k
import datetime
# resnet50 第一次卷积的权值保存在项目中的路径
file_name = "../model_parser/dump_txt/resnet50_conv1_weight.txt"
# 将权值加载到内存中,赋值给K
k = get_weight_from_file(file_name)
# 打印当前时间戳
print(datetime.datetime.now())
# 调用手写的卷积进行计算,输出卷积结果
out = my_conv2d(out, k, 224, 224, 3, 64, 7, 2, 3)
# 打印计算完成的时间戳
print(datetime.datetime.now())
# 打印卷积计算结果的 shape
print(out.shape)
上面在调用 my_conv2d 之前,加了两个时间戳打印,看一下这个卷积运算的耗时。$ 2023-04-13 08:21:20.473301
$ 2023-04-13 08:23:00.855593
从时间戳上可以看到,两个时间戳之间的间隔在1分多钟,说明这个卷积运算消耗了1分多钟,这可能与我用的虚拟机配置很低有关。不过这种循环堆叠的卷积实现方式,很耗时是真的。在卷积运算完之后,把这一层的输出的 shape 也打印出来。$ (112, 112, 64)
可以看到,卷积的输出 shape 为 (112, 112, 64),通道数由输入图片的 3 通道变成了 64 通道,是因为使用了 64 个卷积核。这里64个通道,实际上可以理解为这一层卷积在原始输入图片的像素之间,抽取出了 64 个特征出来。 至于是什么特征,我也不知道,有可能是猫的鼻子和耳朵。至于上面说的卷积运算耗时的问题,暂时先不管他。在完成整网的推理,正确识别出来猫之后,我会继续将算法都优化一遍的。后面还有 Pooling, Bn 算法的手写,写完之后,就可以按照 Resnet50 的结构,搭出神经网络来了。今天,离识别出来猫,又进了一步。