fast.ai 机器学习笔记(三)(2)https://developer.aliyun.com/article/1482653
数据加载器[29:05]
那么我们实际上在这里构建了什么?嗯,正如我所说的,我们实际上构建的是可以像常规函数一样运行的东西。所以我想向你展示如何实际上将其作为一个函数调用。为了能够将其作为一个函数调用,我们需要能够向其传递数据。为了能够向其传递数据,我需要获取一个 MNIST 图像的小批量。为了方便起见,我们使用了 Fast AI 的ImageClassifierData.from_arrays
方法,它会为我们创建一个 PyTorch DataLoader。PyTorch DataLoader 是一种获取几张图像并将它们放入一个小批量并使其可用的东西。你基本上可以说给我另一个小批量,给我另一个小批量,给我另一个小批量。所以在 Python 中,我们称这些东西为生成器。生成器是一种东西,你基本上可以说我想要另一个,我想要另一个,我想要另一个。迭代器和生成器之间有非常紧密的联系,我现在不打算担心它们之间的区别。但你会看到,为了获得我们可以说请给我另一个的东西,为了获取我们可以用来生成小批量的东西,我们必须取出我们的数据加载器,这样你就可以从我们的模型数据对象中请求训练数据。你会看到有很多不同的数据加载器可以请求:测试数据加载器、训练数据加载器、验证数据加载器、增强图像数据加载器等等。
dl = iter(md.trn_dl)
所以我们要获取为我们创建的训练数据加载器。这是一个标准的 PyTorch 数据加载器,稍微被我们优化了一下,但是思路是一样的。然后你可以说这个(iter
)是一个标准的 Python 东西,我们可以说将其转换为一个迭代器,即我们可以一次从中获取另一个的东西。一旦你这样做了,我们就有了一个可以迭代的东西。你可以使用标准的 Python next
函数从生成器中获取一个更多的东西。
xmb,ymb = next(dl)
所以这是从一个小批量返回 x 和 y。在 Python 中,您可以使用for
循环来使用生成器和迭代器。我也可以说 x 小批量逗号 y 小批量在数据加载器中,然后做一些事情:
所以当你这样做时,实际上是在幕后,它基本上是调用next
很多次的语法糖。所以这都是标准的 Python 东西。
所以它返回了一个大小为 64 乘以 784 的张量,这是我们所期望的。我们使用的 Fast AI 库默认使用小批量大小为 64,这就是为什么它这么长。这些都是背景零像素,但它们实际上并不是零。在这种情况下,为什么它们不是零呢?因为它们被标准化了。所以我们减去了平均值,除以标准差。
xmb ''' -0.4245 -0.4245 -0.4245 ... -0.4245 -0.4245 -0.4245 -0.4245 -0.4245 -0.4245 ... -0.4245 -0.4245 -0.4245 -0.4245 -0.4245 -0.4245 ... -0.4245 -0.4245 -0.4245 ... ⋱ ... -0.4245 -0.4245 -0.4245 ... -0.4245 -0.4245 -0.4245 -0.4245 -0.4245 -0.4245 ... -0.4245 -0.4245 -0.4245 -0.4245 -0.4245 -0.4245 ... -0.4245 -0.4245 -0.4245 [torch.FloatTensor of size 64x784 (GPU 0)] '''
现在我们要做的是将其传递给我们的逻辑回归。所以我们可能会这样做,我们将使用vxmb
(变量 x 小批量),我可以取出我的 x 小批量,我可以将其移动到 GPU 上,因为记住我的net2
对象在 GPU 上,所以我们的数据也必须在 GPU 上。然后我要做的第二件事是,我必须将其包装在Variable
中。那么变量是做什么的呢?这是我们免费获得自动微分的方式。PyTorch 可以自动微分几乎任何张量。但这需要内存和时间,所以它不会总是跟踪。要进行自动微分,它必须跟踪确切的计算方式。我们将这些东西相加,我们将其乘以那个,然后我们取了这个的符号等等。你必须知道所有的步骤,因为然后要进行自动微分,它必须使用链式法则对每个步骤求导,然后将它们相乘。所以这是缓慢和占用内存的。所以我们必须选择说“好的,这个特定的东西,我们以后会对其进行求导,所以请为我们跟踪所有这些操作。”我们选择的方式是将一个张量包装在Variable
中。这就是我们的做法。
你会发现它看起来几乎和一个张量一样,但现在它说“包含这个张量的变量”。所以在 PyTorch 中,一个变量的 API 与张量完全相同,或者更具体地说,是张量 API 的超集。我们对张量可以做的任何事情,我们也可以对变量做。但它会跟踪我们做了什么,以便我们以后可以求导。
vxmb = Variable(xmb.cuda()) vxmb ''' Variable containing: -0.4245 -0.4245 -0.4245 ... -0.4245 -0.4245 -0.4245 -0.4245 -0.4245 -0.4245 ... -0.4245 -0.4245 -0.4245 -0.4245 -0.4245 -0.4245 ... -0.4245 -0.4245 -0.4245 ... ⋱ ... -0.4245 -0.4245 -0.4245 ... -0.4245 -0.4245 -0.4245 -0.4245 -0.4245 -0.4245 ... -0.4245 -0.4245 -0.4245 -0.4245 -0.4245 -0.4245 ... -0.4245 -0.4245 -0.4245 [torch.cuda.FloatTensor of size 64x784 (GPU 0)] '''
所以我们现在可以将其传递给我们的net2
对象。记住我说过你可以将其视为函数。所以请注意,我们没有调用.forward()
,我们只是将其视为函数。然后记住,我们取了对数,为了撤销这个操作,我正在使用.exp()
,这将给我概率。所以这是我的概率,它返回的大小是 64 乘以 10,所以对于小批量中的每个图像,我们有 10 个概率。你会看到,大多数概率都非常接近零。而其中一些则要大得多,这正是我们所希望的。就像好吧,它不是零,不是 1,不是 2,它是3,不是 4 等等。
preds = net2(vxmb).exp(); preds[:3] ''' Variable containing: Columns 0 to 5 1.6740e-03 1.0416e-05 2.5454e-05 1.9119e-02 6.5026e-05 9.7470e-01 3.4048e-02 1.8530e-04 6.6637e-01 3.5073e-02 1.5283e-01 6.4995e-05 3.0505e-08 4.3947e-08 1.0115e-05 2.0978e-04 9.9374e-01 6.3731e-05 Columns 6 to 9 2.1126e-06 1.7638e-04 3.9351e-03 2.9154e-04 1.1891e-03 3.2172e-02 1.4597e-02 6.3474e-02 8.9568e-06 9.7507e-06 7.8676e-04 5.1684e-03 [torch.cuda.FloatTensor of size 3x10 (GPU 0)] '''
我们可以调用net2.forward(vxmb)
,它会做完全相同的事情。但这并不是 PyTorch 的所有机制实际上是如何工作的。他们实际上将其称为函数。这实际上是一个非常重要的想法,因为这意味着当我们定义自己的架构或其他内容时,任何你想要放入函数的地方,你都可以放入一个层;任何你想要放入一个层的地方,你都可以放入一个神经网络;任何你想要放入一个神经网络的地方,你都可以放入一个函数。因为就 PyTorch 而言,它们都只是它将调用的东西,就像它们是函数一样。所以它们是可以互换的,这是非常重要的,因为这就是我们通过混合和匹配许多部分并将它们组合在一起来创建非常好的神经网络的方式。
让我举个例子。这是我的逻辑回归,准确率达到了 91%多一点。我现在要把它转换成一个带有一个隐藏层的神经网络。
我要做的是创建更多的层。我要改变这个,使其输出 100 而不是 10,这意味着这个输入将是 100 而不是 10。现在这样还不能让事情变得更好。为什么这肯定不会比之前更好呢?因为两个线性层的组合只是一个线性层,但参数不同。
所以我们有两个线性层,这只是一个线性层。为了使事情变得有趣,我将用零替换第一层中的所有负数。因为这是一个非线性转换,这个非线性转换被称为修正线性单元(ReLU)。
所以nn.Sequential
简单地会依次调用每个层对每个小批量进行操作。所以做一个线性层,用零替换所有负数,再做一个线性层,最后做一个 softmax。这现在是一个有一个隐藏层的神经网络。所以让我们尝试训练这个。准确率现在已经提高到 96%。
所以这个想法是,我们在这节课中学习的基本技术在你开始将它们堆叠在一起时变得强大。
问题:为什么你选择了 100?没有原因。输入一个额外的零更容易。神经网络层中应该有多少激活是深度学习从业者的规模问题,我们在深度学习课程中讨论过,而不是在这门课程中。
问题:在添加额外的层时,如果你做了两个 softmax,这会有影响吗,或者这是你不能做的事情?你绝对可以在那里使用 softmax。但这可能不会给你想要的结果。原因是 softmax 倾向于将大部分激活推向零。激活,只是为了明确,因为在深度学习课程中我收到了很多关于什么是激活的问题,激活是在一个层中计算出来的值。这就是一个激活:
这不是一个权重。权重不是一个激活。它是你从一个层计算出来的值。所以 softmax 会倾向于使大部分激活接近于零,这与你想要的相反。通常你希望你的激活尽可能丰富、多样且被使用。所以没有什么能阻止你这样做,但它可能不会工作得很好。基本上,你的所有层几乎都会跟随非线性激活函数,通常是 ReLU,除了最后一层。
问题:在做多层时,比如说 2 或 3 层,你想要改变这些激活层吗?不。所以如果我想要更深,我会直接这样做。
现在这是一个两个隐藏层的网络。
问题:所以我想我听到你说有几种不同的激活函数,比如修正线性单元。有一些例子,为什么会使用每一个呢?是的,很好的问题。所以基本上当你添加更多的线性层时,你的输入进来,你把它通过一个线性层然后一个非线性层,线性层,非线性层,线性层和最终的非线性层。最终的非线性层正如我们讨论过的,如果它是多类别分类但你只选择其中一个,你会使用 softmax。如果是二元分类或多标签分类,你会使用 sigmoid。如果是回归,通常你根本不会有,尽管我们在昨晚的深度学习课程中学到有时你也可以在那里使用 sigmoid。所以它们基本上是最终层的主要选项。对于隐藏层,你几乎总是使用 ReLU,但还有另一个你可以选择的,有点有趣,叫做 leaky ReLU。基本上如果它大于零,它是y = x,如果小于零,它就像y = 0.1x。所以它与 ReLU 非常相似,但不是等于 0,而是接近于 0。所以它们是主要的两种:ReLU 和 Leaky ReLU。
还有其他一些,但它们有点像那样。例如,有一种叫做 ELU 的东西相当受欢迎,但细节并不太重要。像 ELU 这样的东西有点像 ReLU,但在中间稍微弯曲一些。它通常不是基于数据集选择的东西。随着时间的推移,我们发现了更好的激活函数。所以两三年前,每个人都使用 ReLU。一年前,几乎每个人都使用 Leaky ReLU。今天,我想大多数人开始转向 ELU。但老实说,激活函数的选择实际上并不太重要。人们实际上已经表明,你可以使用相当任意的非线性激活函数,甚至是正弦波,它仍然有效。
所以尽管今天我们要做的是展示如何创建这个没有隐藏层的网络,将其转变为下面这个网络(下面)准确率为 96%左右将会很简单。这是你可能应该在这一周尝试做的事情,创建这个版本。
现在我们有了一个可以传递我们的变量并得到一些预测的网络,这基本上就是我们调用fit
时发生的事情。所以我们将看看这种方法如何用于创建这种随机梯度下降。需要注意的一件事是将预测的概率转换为预测的数字是,我们需要使用 argmax。不幸的是,PyTorch 并不称之为 argmax。相反,PyTorch 只是称之为 max,并且 max 返回两个东西:它返回给定轴上的实际最大值(所以max(1)
将返回列的最大值),它返回的第二件事是该最大值的索引。所以 argmax 的等价物是调用 max 然后获取第一个索引的东西:
这就是我们的预测。如果这是 numpy,我们将使用np.argmax()
。
preds = predict(net2, md.val_dl).argmax(1) plots(x_imgs[:8], titles=preds[:8])
所以这是我们手动创建的逻辑回归的预测,在这种情况下,看起来我们几乎全部正确。
接下来我们要尝试摆脱使用库的是我们将尝试避免使用矩阵乘法运算符。相反,我们将尝试手动编写。
广播[46:58]
因此,接下来,我们将学习一些似乎是一个小的编程概念的东西。但实际上,至少在我看来,这将是我们在这门课程中教授的最重要的编程概念,也可能是你需要构建机器学习算法的所有重要编程概念。这就是广播的概念。我将通过示例展示这个概念。
如果我们创建一个数组 10、6、-4 和一个数组 2、8、7,然后将它们相加,它会依次添加这两个数组的每个分量——我们称之为“逐元素”。
a = np.array([10, 6, -4]) b = np.array([2, 8, 7]) a + b ''' array([12, 14, 3]) '''
换句话说,我们不必编写循环。在过去,我们必须循环遍历每一个并将它们相加,然后将它们连接在一起。今天我们不必这样做。它会自动为我们发生。因此,在 numpy 中,我们自动获得逐元素操作。我们可以用 PyTorch 做同样的事情。在 Fast AI 中,我们只需添加一个大写 T 来将某物转换为 PyTorch 张量。如果我们将它们相加,结果完全相同。
因此,这些库中的逐元素操作在这种情况下是相当标准的。有趣的不仅仅是因为我们不必编写 for 循环,而且实际上更有趣的是由于这里发生的性能问题。
性能
第一个是如果我们在 Python 中进行 for 循环,那将会发生。即使你使用 PyTorch,它仍然在 Python 中执行 for 循环。它没有优化 for 循环的方法。因此,在 Python 中,for 循环的速度大约比在 C 中慢 10,000 倍。这是你的第一个问题。我记不清是 1,000 还是 10,000。
然后,第二个问题是,你不仅希望它在 C 中得到优化,而且你希望 C 利用你所有 CPU 所做的事情,这被称为 SIMD,单指令多数据。你的 CPU 能够一次处理 8 个向量中的 8 个元素,并将它们相加到另一个包含 8 个元素的向量中,在一个 CPU 指令中。因此,如果你能利用 SIMD,你立即就会快 8 倍。这取决于数据类型有多大,可能是 4,可能是 8。
你的计算机中还有多个进程(多个核心)。因此,如果向量相加发生在一个核心中,你可能有大约 4 个核心。因此,如果你使用 SIMD,你会快 8 倍,如果你可以使用多个核心,那么你会快 32 倍。然后如果你在 C 中这样做,你可能会快 32k 倍。
所以好处是当我们执行a + b
时,它利用了所有这些东西。
更好的是,如果你在 PyTorch 中执行这个操作,并且你的数据是用.cuda()
创建的,然后将其放在 GPU 上,那么你的 GPU 可以一次执行大约 10,000 个操作。因此,这将比 C 快 100 倍。因此,这对于获得良好的性能至关重要。你必须学会如何通过利用这些逐元素操作来编写无循环的代码。这不仅仅是加号(+
)。我还可以使用小于号(<
),这将返回 0,1,1。
或者如果我们回到 numpy,False,True,True。
因此,你可以使用这个来做各种事情而不需要循环。例如,我现在可以将其乘以a,这里是所有小于b的a的值:
或者我们可以取平均值:
(a < b).mean() ''' 0.66666666666666663 '''
这是a中小于b的值的百分比。因此,你可以用这个简单的想法做很多事情。
进一步
但要进一步,要进一步超越这种逐元素操作,我们将不得不走到下一步,到一种称为广播的东西。让我们从看一个广播的例子开始。
a ''' array([10, 6, -4]) '''
a 是一个具有一维的数组,也称为秩 1 张量,也称为向量。我们可以说a
大于零:
a > 0 ''' array([ True, True, False], dtype=bool) '''
这里,我们有一个秩 1 张量(a
)和一个秩 0 张量(0
)。秩 0 张量也称为标量,秩 1 张量也称为向量。我们之间有一个操作:
现在你可能已经做了一千次,甚至没有注意到这有点奇怪。你有不同等级和不同大小的这些东西。那么它实际上在做什么呢?它实际上是将那个标量复制 3 次(即[0, 0, 0]),并实际上逐个元素地给我们三个答案。这就是所谓的广播。广播意味着复制我的张量的一个或多个轴,以使其与另一个张量的形状相同。但它实际上并没有复制。它实际上是存储了一种内部指示器,告诉它假装这是一个三个零的向量,但实际上它不是去下一行或下一个标量,而是回到它来的地方。如果你对这个特别感兴趣,它们将该轴上的步幅设置为零。这对于那些好奇的人来说是一个较为高级的概念。
所以我们可以做 a + 1[54:52]。它将广播标量 1 为[1, 1, 1],然后进行逐元素加法。
a + 1 ''' array([11, 7, -3]) '''
我们可以对一个矩阵做同样的操作。这是我们的矩阵。
m = np.array([[1, 2, 3], [4,5,6], [7,8,9]]); m ''' array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) '''
那个矩阵的 2 倍将广播 2 为[[2, 2, 2],[2,2,2],[2,2,2]],然后进行逐元素乘法。这就是我们广播的最简单版本。
2*m ''' array([[ 2, 4, 6], [ 8, 10, 12], [14, 16, 18]]) '''
将向量广播到矩阵[55:27]
这是广播的一个稍微复杂的版本。这里有一个名为c
的数组。这是一个秩 1 张量。
c = np.array([10,20,30]); c ''' array([10, 20, 30]) '''
这是之前的矩阵m
——秩 2 张量。我们可以添加m + c
。那么这里发生了什么?
m + c ''' array([[11, 22, 33], [14, 25, 36], [17, 28, 39]]) '''
你可以看到它所做的是将[10, 20, 30]添加到每一行。
所以我们可以想象它似乎做了与广播标量相同类型的想法,就像复制了它。然后将这些视为秩 2 矩阵。现在我们可以进行逐元素加法。
问题:通过查看这个例子,它将其复制下来生成新的行。如果我们想要获得新的列,我们应该如何做[56:50]?我很高兴你问了。所以我们会这样做:
现在将其视为我们的矩阵。要让 numpy 这样做,我们需要传入一个矩阵而不是一个向量,而是传入一个具有一列的矩阵(即秩 2 张量)。基本上,numpy 会将这个秩 1 张量视为秩 2 张量,表示一行。换句话说,它是 1 乘 3。所以我们想要创建一个 3 乘 1 的张量。有几种方法可以做到这一点。一种方法是使用np.expand_dims(c,1)
,如果你传入这个参数,它会说“请在这里插入一个长度为 1 的轴”。所以在我们的情况下,我们想将其转换为 3 乘 1,所以如果我们说expand_dims(c,1)
,它会将形状改变为(3, 1)。所以如果我们看看它是什么样子的,它看起来像一列。
np.expand_dims(c,1).shape ''' (3, 1) ''' np.expand_dims(c,1) ''' array([[10], [20], [30]]) '''
所以如果我们现在加上m
,你可以看到它确实做了我们希望它做的事情,即将 10、20、30 添加到列[58:50]:
m + np.expand_dims(c,1) ''' array([[11, 12, 13], [24, 25, 26], [37, 38, 39]]) '''
现在由于单位轴的位置是如此重要,所以通过实验创建这些额外的单位轴并知道如何轻松地做到这一点是非常有帮助的。在我看来,np.expand_dims
并不是最容易的方法。最简单的方法是使用一个特殊的索引None
来索引张量。None
的作用是在那个位置创建一个长度为 1 的新轴。因此,这将在开始添加一个新的长度为 1 的轴。
c[None] ''' array([[10, 20, 30]]) ''' c[None].shape ''' (1, 3) '''
这将在末尾添加一个新的长度为 1 的轴。
c[:,None] ''' array([[10], [20], [30]]) ''' c[:,None].shape ''' (3, 1) '''
或者为什么不两者都做呢
c[None,:,None].shape ''' (1, 3, 1) '''
所以如果你考虑一下,一个张量中有 3 个元素,可以是任何你喜欢的阶数,你可以随意添加单位轴。这样,我们可以决定我们希望广播的方式。所以在 numpy 中有一个非常方便的东西叫做broadcast_to
,它的作用是将我们的向量广播到那个形状,并展示给我们看看那会是什么样子。
np.broadcast_to(c, (3,3)) ''' array([[10, 20, 30], [10, 20, 30], [10, 20, 30]]) '''
所以如果你对某个广播操作中发生的事情感到不确定,你可以使用broadcast_to
。例如,在这里,我们可以说,而不是(3,3),我们可以说m.shape
,看看将会发生什么。
np.broadcast_to(c, m.shape) ''' array([[10, 20, 30], [10, 20, 30], [10, 20, 30]]) '''
这就是在我们将其添加到m
之前会发生的事情。所以如果我们说将其转换为列,那就是它的样子:
np.broadcast_to(c[:,None], m.shape) ''' array([[10, 10, 10], [20, 20, 20], [30, 30, 30]]) '''
所以这就是广播的直观定义。现在希望我们可以回到那个 numpy 文档并理解它的含义。
广播这个术语描述了 numpy 在算术运算期间如何处理具有不同形状的数组。在一定的约束条件下,较小的数组(较低秩的张量)被“广播”到较大的数组上,以便它们具有兼容的形状。广播提供了一种向量化数组操作的方法,使循环发生在 C 而不是 Python 中。它可以在不进行不必要的数据复制的情况下实现这一点,并通常导致高效的算法实现。
“向量化”通常意味着使用 SIMD 等技术,以便多个操作同时进行。它实际上并不会进行不必要的数据复制,只是表现得好像进行了复制。所以这就是我们的定义。
现在在深度学习中,你经常处理 4 阶或更高阶的张量,并且经常将它们与 1 阶或 2 阶的张量结合在一起,仅凭直觉正确地执行这些操作几乎是不可能的。所以你真的需要了解规则。
这里是m.shape
和c.shape
[1:02:45]。所以规则是我们将逐个元素地比较我们两个张量的形状。我们将一次查看一个,并且我们将从末尾开始向前移动。当这两个条件之一为真时,两个维度将是兼容的。所以让我们检查一下我们的m
和c
是否兼容。所以我们将从末尾开始(首先是尾部维度)并检查“它们是否兼容?”如果维度相等,则它们是兼容的。让我们继续下一个。哦,我们缺少了。c
缺少一些东西。如果有东西缺失会发生什么,我们会插入一个 1。这就是规则。所以现在让我们检查一下——这些是否兼容?其中一个是 1,是的,它们是兼容的。所以现在你可以看到为什么 numpy 将一维数组视为一个代表行的 2 阶张量。这是因为我们基本上在前面插入了一个 1。这就是规则。
在对两个数组进行操作时,Numpy/PyTorch 会逐个元素地比较它们的形状。它从尾部维度开始,逐步向前推进。当两个维度兼容时
- 它们是相等的,或者
- 其中之一是 1
数组不需要具有相同数量的维度。例如,如果你有一个256*256*3
的 RGB 值数组,并且你想要按不同的值缩放图像中的每种颜色,你可以将图像乘以一个具有 3 个值的一维数组。根据广播规则对齐这些数组的尾部轴的大小,显示它们是兼容的:
Image (3d array): 256 x 256 x 3 Scale (1d array): 3 Result (3d array): 256 x 256 x 3
例如,上面是你经常需要做的事情,即你从一幅图像开始,256 像素乘以 256 像素乘以 3 个通道。你想要减去每个通道的平均值。所以你有 256 乘以 256 乘以 3,你想要减去长度为 3 的东西。是的,你可以做到。绝对可以。因为 3 和 3 是兼容的,因为它们是相同的。256 和空是兼容的,因为它会插入一个 1。256 和空是兼容的,因为它会插入一个 1。所以你最终会得到这个(每个通道的平均值)会在所有这个轴(从右边数第二个)上广播,然后整个东西将在这个最左边的轴上广播,所以我们最终会得到一个 256 乘以 256 乘以 3 的有效张量。
有趣的是,数据科学或机器学习社区中很少有人理解广播,大多数时候,例如,当我看到人们为计算机视觉进行预处理时,比如减去平均值,他们总是在通道上写循环。我认为不必这样做非常方便,而且通常不必这样做速度更快。所以如果你擅长广播,你将拥有这种非常少数人拥有的超级有用的技能。这是一种古老的技能。它可以追溯到 APL 的时代。APL 是上世纪 50 年代的,代表着 A Programming Language,肯尼斯·艾弗森写了一篇名为“符号作为思维工具”的论文,他在其中提出了一种新的数学符号。他提出,如果我们使用这种新的数学符号,它会给我们提供新的思维工具,让我们能够思考以前无法思考的事情。他的一个想法就是广播,不是作为计算机编程工具,而是作为数学符号的一部分。因此,他最终将这种符号实现为一种思维工具,作为一种名为 APL 的编程语言。他的儿子继续将其进一步发展为一种名为 J 的软件,这基本上是当你将 60 年来非常聪明的人们致力于这个想法时得到的结果。通过这种编程语言,你可以用一两行代码表达非常复杂的数学思想。我们有 J 是很棒的,但更棒的是这些想法已经进入我们所有人使用的语言中,比如在 Python 中的 numpy 和 PyTorch 库。这些不仅仅是一些小众的想法,它们是思考数学和进行编程的基本方式。
让我举个例子,这种符号作为思维工具的例子。这里我们有 c:
这里我们有 c[None]
:
注意现在有两个方括号。这有点像一个一行向量张量。这里是一个小列:
这将会做什么呢?
所以从广播规则的角度来看,我们基本上是将这个列(维度为(3,1))和这个行(维度为(1,3))进行操作。为了使这些符合我们的广播规则,列必须被复制 3 次,因为它需要匹配 3。行必须被复制 3 次以匹配 3。所以现在我有两个矩阵可以进行逐元素乘积。
所以正如你所说,这是我们的外积。
现在有趣的是,突然间这不再是一个特殊的数学案例,而只是广播这个一般概念的一个特定版本,我们可以像外加一样:
或者外大于:
或者其他。所以突然间我们有了这个概念,我们可以用它来构建新的想法,然后我们可以开始尝试这些新的想法。
有趣的是,numpy 有时会使用这个方法。例如,如果你想创建一个网格,numpy 就是这样做的:
实际上返回的是 0、1、2、3、4;一个作为列,一个作为行。所以我们可以说好的,这是 x 网格(xg
)逗号 y 网格(yg
),现在你可以做类似这样的事情:
所以突然间我们把它扩展成了一个网格。所以有趣的是一些简单的概念是如何被不断地建立和发展的。所以如果你熟悉 APL 或 J,这是一个由许多层层叠加而成的整个环境。虽然在 numpy 中我们没有这样深层次的环境,但你肯定可以看到这种广播的想法在简单的事情中体现出来,比如我们如何在 numpy 中创建一个网格。
实现矩阵乘法[1:12:30]
这就是广播,现在我们可以使用这个来实现矩阵乘法。那么为什么我们要这样做呢?显然我们不需要。矩阵乘法已经被我们的库完美地处理了。但是很多时候你会发现在各种领域,特别是在深度学习中,会有一些特定类型的线性函数,你想要做的事情并没有完全为你做好。例如,有一个叫做张量回归和张量分解的领域,目前正在得到很大的发展,它们讨论的是如何将高阶张量转化为行、列和面的组合。事实证明,当你这样做时,你基本上可以处理非常高维的数据结构,而不需要太多的内存和计算时间。例如,有一个非常棒的库叫做TensorLy,它为你做了很多这样的事情。所以这是一个非常重要的领域。它涵盖了所有的深度学习,还有很多现代机器学习。所以即使你不会定义矩阵乘法,你很可能会想要找到一些其他略有不同的张量积。所以了解如何做这个是非常有用的。
让我们回过头来看看我们的二维数组和一维数组,秩为 2 的张量和秩为 1 的张量[1:14:27]。
m, c ''' (array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]), array([10, 20, 30])) '''
记住,我们可以使用@符号或旧方法np.matmul
进行矩阵乘法。当我们这样做时,实际上我们基本上是在说1*10 + 2*20 + 3*30 = 140
,所以我们对每一行都这样做,然后我们可以继续对下一行做同样的事情,依此类推以获得我们的结果。
m @ c # np.matmul(m, c) ''' array([140, 320, 500]) '''
你也可以在 PyTorch 中这样做
T(m) @ T(c) ''' 140 320 500 [torch.LongTensor of size 3] '''
但是(m * c
)这不是矩阵乘法。那是什么?逐元素广播。但请注意,它创建的数字[10, 40, 90]正是我在做矩阵乘法的第一部分时需要计算的三个确切数字(1*10 + 2*20 + 3*30
)。
m * c ''' array([[ 10, 40, 90], [ 40, 100, 180], [ 70, 160, 270]]) '''
换句话说,如果我们对列求和,即轴等于 1,我们就得到了矩阵向量乘积:
(m * c).sum(axis=1) ''' array([140, 320, 500]) '''
所以我们可以在不依赖库的特殊帮助下做这些事情。现在让我们将这扩展到矩阵矩阵乘积。
矩阵矩阵乘积看起来是这样的。有一个很棒的网站叫做matrixmultiplication.xyz,它向我们展示了当我们将两个矩阵相乘时会发生什么。从操作的角度来看,这就是矩阵乘法。换句话说,我们刚刚做的是首先取第一列和第一行相乘得到 15:
然后我们取第二列和第一行得到 27:
所以我们基本上是在做我们刚刚做的事情,矩阵向量乘积,我们只是做了两次。一次用这一列(左边),一次用那一列(右边),然后我们把这两个连接在一起。所以我们现在可以继续这样做:
(m * n[:,0]).sum(axis=1) ''' array([140, 320, 500]) ''' (m * n[:,1]).sum(axis=1) ''' array([ 25, 130, 235]) '''
这就是我们矩阵乘法的两列。
我不想让我们的代码太乱,所以我不打算真的使用那个,但是现在我们有了它,如果我们想要的话。我们不再需要使用 torch 或 numpy 矩阵乘法。我们有自己的方法可以使用,只使用逐元素操作、广播和求和。
这是我们从头开始的逻辑回归类[1:18:37]。我只是把它复制到这里。
class LogReg(nn.Module): def __init__(self): super().__init__() self.l1_w = get_weights(28*28, 10) # Layer 1 weights self.l1_b = get_weights(10) # Layer 1 bias def forward(self, x): x = x.view(x.size(0), -1) x = x @ self.l1_w + self.l1_b return torch.log(softmax(x))
这是我们实例化对象的地方,复制到 GPU。我们创建一个优化器,我们将在稍后学习。然后我们调用 fit。
net2 = LogReg().cuda() opt=optim.Adam(net2.parameters()) fit(net2, md, n_epochs=1, crit=loss, opt=opt, metrics=metrics) ''' [ 0\. 0.31102 0.28004 0.92406] '''
编写我们自己的训练循环[1:18:53]
所以目标是现在重复这一过程,而无需调用 fit。为此,我们需要一个循环,每次抓取一个小批量的数据。对于每个小批量的数据,我们需要将其传递给优化器,并说“请尝试为这个小批量提供稍微更好的预测”。
正如我们学到的,为了一次抓取训练集的一个小批次,我们必须向模型数据对象请求训练数据加载器。我们必须将其包装在iter
中以创建一个迭代器或生成器。这样就给我们了我们的数据加载器。所以 PyTorch 将其称为数据加载器。我们实际上编写了自己的 Fast AI 数据加载器,但基本上是相同的思路。
dl = iter(md.trn_dl) # Data loader
接下来我们要做的是获取 x 和 y 张量,从我们的数据加载器中获取下一个。将其包装在Variable
中以表明我需要能够对使用此计算的导数进行求导。因为如果我不能求导,那么我就无法得到梯度,也无法更新权重。而且我需要将其放在 GPU 上,因为我的模块在 GPU 上(net2 = LogReg().cuda()
)。所以现在我们可以将该变量传递给我们实例化的对象(即我们的逻辑回归)。记住,我们的模块,我们可以将其用作函数,因为这就是 PyTorch 的工作原理。这给我们提供了一组预测,就像我们以前看到的那样。
xt, yt = next(dl) y_pred = net2(Variable(xt).cuda())
现在我们可以检查损失[1:20:41]。我们定义损失为负对数似然损失对象。我们将在下一课中学习如何计算它,现在,只需将其视为分类问题的均方根误差。所以我们也可以像调用函数一样调用它。所以你可以看到这在 PyTorch 中是一个非常普遍的想法,将一切都理想地视为函数。在这种情况下,我们有一个负对数似然损失对象,我们可以将其视为函数。我们传入我们的预测和实际值。同样,实际值需要转换为变量并放在 GPU 上,因为损失是我们实际想要求导的东西。这给我们带来了我们的损失,就是这样。
l = loss(y_pred, Variable(yt).cuda()) print(l) ''' Variable containing: 2.4352 [torch.cuda.FloatTensor of size 1 (GPU 0)] '''
这是我们的损失 2.43。所以它是一个变量,因为它是一个变量,它知道它是如何计算的。它知道它是用这个损失函数(loss
)计算的。它知道预测是用这个网络(net2
)计算的。它知道这个网络由这些操作组成:
所以我们可以自动获取梯度。要获取梯度,我们调用l.backward()
。记住l
是包含我们损失的东西。所以l.backward()
是添加到任何变量的东西。然后调用.backward()
,这表示请计算梯度。这样就计算了梯度并将其存储在内部,基本上对于用于计算的每个权重/参数,现在都存储在.grad
中,我们稍后会看到,但基本上存储了梯度。然后我们可以调用optimizer.step()
,我们很快将手动执行这一步。这部分表示请让权重变得更好一点。
optimizer.step [1:22:49]
因此,optimizer.step()
正在做的是,如果你有一个非常简单的函数像这样,优化器所做的就是说好的,让我们选择一个起始点,计算损失的值,计算导数告诉我们哪个方向是向下的。因此,它告诉我们我们需要朝那个方向走。然后我们迈出一小步。
然后我们再次取导数,采取一个小步骤,并重复,直到最终我们采取的步骤如此之小以至于停止。
这就是梯度下降的作用。小步骤有多大?基本上在这里取导数,所以让我们说导数是 8。然后我们乘以一个小数,比如 0.01,这告诉我们要采取什么步骤大小。这里的这个小数被称为学习率,它是设置的最重要的超参数。如果你选择的学习率太小,那么你的下降步骤将会很小,而且会花费很长时间。学习率太大,你会跳得太远,然后你会跳得太远,最终会发散而不是收敛。
在这节课中我们不会讨论如何选择学习率,但在深度学习课程中,我们实际上向你展示了一种非常可靠地选择一个非常好的学习率的特定技术。
因此,基本上正在发生的是,我们计算导数,我们调用执行step
的优化器,换句话说,根据梯度和学习率更新权重。
希望在这样做之后,我们的损失比之前更好。因此,我刚刚重新运行了这个,得到了一个 4.16 的损失。
一步之后,现在是 4.03。
所以它按照我们希望的方式运行,基于这个小批量,它更新了我们网络中的所有权重,使它们比之前更好。因此,我们的损失下降了。
训练循环
因此,让我们将其转化为一个训练循环。我们将进行一百步:
- 从数据加载器中获取另一个小批量数据
- 从我们的网络中计算预测
- 从预测和实际值计算我们的损失
- 每 10 次,我们将打印出准确率,只需取平均值,看它们是否相等。
- 一个 PyTorch 特定的事情,你必须将梯度清零。基本上,你可以有许多不同的损失函数的网络,你可能想要将所有的梯度加在一起。因此,你必须告诉 PyTorch 何时将梯度设置为零。因此,这只是说将所有的梯度设置为零。
- 计算梯度,这被称为反向传播
- 然后进行一步优化器,使用梯度和学习率更新权重
for t in range(100): xt, yt = next(dl) y_pred = net2(Variable(xt).cuda()) l = loss(y_pred, Variable(yt).cuda()) if t % 10 == 0: accuracy = np.mean(to_np(y_pred).argmax(axis=1) == to_np(yt)) print("loss: ", l.data[0], "\t accuracy: ", accuracy) optimizer.zero_grad() l.backward() optimizer.step() ''' loss: 2.2104923725128174 accuracy: 0.234375 loss: 1.3094730377197266 accuracy: 0.625 loss: 1.0296542644500732 accuracy: 0.78125 loss: 0.8841525316238403 accuracy: 0.71875 loss: 0.6643403768539429 accuracy: 0.8125 loss: 0.5525785088539124 accuracy: 0.875 loss: 0.43296846747398376 accuracy: 0.890625 loss: 0.4388267695903778 accuracy: 0.90625 loss: 0.39874207973480225 accuracy: 0.890625 loss: 0.4848807752132416 accuracy: 0.875 '''
一旦我们运行它,你会看到损失下降,准确率上升。所以这是基本的方法。下一课,我们将看到optimizer.step()
做了什么。我们将详细看一下。我们不会深入研究l.backward()
,因为我说过我们基本上会将导数的计算视为给定的。但基本上,在任何深度网络中,你有一个类似于线性函数的函数,然后将其输出传递到另一个可能类似于 ReLU 的函数中。然后将其输出传递到可能是另一个线性层的函数中,依此类推:
i( h( g( f(x) ) ) )
因此,这些深度网络只是函数的函数的函数。因此,你可以用数学方式写出它们。因此,反向传播所做的就是说(让我们将其简化为深度为二的版本),我们可以说:
g( f(x) ) u = f(x)
因此,我们可以用链式法则计算*g(f(x))*的导数:
g'(u) f'(x)
所以你可以看到,我们可以对函数的函数的函数做同样的事情。因此,当你将一个函数应用于一个函数的函数时,你可以通过将这些层的导数的乘积来计算导数。在神经网络中,我们称之为反向传播。因此,当你听到反向传播时,它只是意味着使用链式法则来计算导数。
所以当你看到一个神经网络像这样定义时:
如果按顺序定义,字面上,所有这意味着将这个函数应用于输入,将这个函数应用于那个,将这个函数应用于那个,依此类推。因此,这只是定义了一个函数到一个函数到一个函数到一个函数的组合。因此,虽然我们不打算自己计算梯度,但现在你可以看到为什么它可以这样做,只要它内部知道幂函数的导数是什么,正弦函数的导数是什么,加法的导数是什么,依此类推。然后我们在这里的 Python 代码只是将这些东西组合在一起。因此,它只需要知道如何用链式法则将它们组合在一起,然后就可以运行了。
所以我认为我们现在可以把它留在这里,在下一堂课中,我们将看看如何编写我们自己的优化器,然后我们将自己从头解决 MNIST 问题。到时见!
机器学习 1:第 10 课
原文:
medium.com/@hiromi_suenaga/machine-learning-1-lesson-10-6ff502b2db45
译者:飞龙
来自机器学习课程的个人笔记。随着我继续复习课程以“真正”理解它,这些笔记将继续更新和改进。非常感谢 Jeremy 和 Rachel 给了我这个学习的机会。
pip 上的 Fast AI [0:00]
欢迎回到机器学习!这周最令人兴奋的事情当然是 Fast AI 现在在 pip 上了,所以你可以pip install fastai
:
[## fastai
fastai 使得使用 PyTorch 进行深度学习更快、更准确、更容易
pypi.org](https://pypi.org/project/fastai/?source=post_page-----6ff502b2db45--------------------------------)
最简单的方法可能仍然是执行conda env update
,但有几个地方更方便的是执行pip install fastai
,如果你在笔记本之外的地方工作,那么这将使你在任何地方都可以访问 Fast AI。他们还向 Kaggle 提交了一个拉取请求,试图将其添加到 Kaggle 内核中。所以希望你很快就能在 Kaggle 内核上使用它。你可以在工作中或其他地方使用它,所以这很令人兴奋。我不会说它已经正式发布了。显然,现在还很早,我们仍在添加(你也在帮助添加)文档和所有这些东西。但很高兴现在有这个。
Kaggle 内核 [1:22]
这周有几个来自 USF 学生的很酷的内核。我想强调两个都来自文本规范化竞赛的内核,该竞赛旨在尝试将标准英语文本转换为文本,还有一个俄语的。你要尝试识别可能是“第一,第二,第三”之类的东西,并说这是一个基数,或者这是一个电话号码或其他什么。我快速搜索了一下,发现学术界曾尝试使用深度学习来做这个,但他们没有取得太多进展,实际上我注意到Alvira 的内核在这里得到了 0.992 的排名,我认为是前 20 名。这完全是启发式的,是特征工程的一个很好的例子。在这种情况下,整个事情基本上完全是特征工程。基本上是通过查看和使用大量正则表达式来弄清楚每个标记是什么。我认为她在这里做得很好,清楚地列出了所有不同的部分以及它们如何相互配合。她提到她也许希望将这个变成一个库,我认为这将是很好的。你可以使用它来提取文本中的所有部分。这是自然语言处理社区希望能够做到的事情,而不需要像这样大量手写代码。但目前,我很感兴趣看看获胜者到底做了什么,但我还没有看到机器学习被用来做这个特别好。也许最好的方法是将这种特征工程与一些机器学习结合起来。但我认为这是一个有效特征工程的很好例子。
这位是另一位 USF 的学生,她做了类似的事情,得到了类似的分数,但使用了自己不同的规则。同样,这也会让你在排行榜上获得一个不错的位置。所以我觉得看到我们的一些学生参加比赛并通过基本的手写启发式方法获得前 20 名结果的例子很有趣。这就是,例如,六年前的计算机视觉仍然是这样。基本上最好的方法是大量仔细手写的启发式方法,通常结合一些简单的机器学习。所以我认为随着时间的推移,这个领域肯定在努力向更多自动化方向发展。
fast.ai 机器学习笔记(三)(4)https://developer.aliyun.com/article/1482657