1. 向量化 (Vectorization)
在深度学习的世界中,数据量经常是巨大的。在这种背景下,使用for循环来处理数据显得效率低下。一个更为高效的方法就是向量化。
所谓向量化,指的是利用矩阵运算来代替传统的循环,从而极大地提高代码执行的速度。让我们通过一个Python示例来直观地感受向量化和非向量化代码之间的差异。
import numpy as np
import time
a = np.random.rand(1000000)
b = np.random.rand(1000000)
tic = time.time()
c = np.dot(a,b)
toc = time.time()
print(c)
print("向量化版本运行时间:" + str(1000*(toc-tic)) + "ms")
c = 0
tic = time.time()
for i in range(1000000):
c += a[i]*b[i]
toc = time.time()
print(c)
print("for循环版本运行时间:" + str(1000*(toc-tic)) + "ms")
可能的输出为:
250286.989866
向量化版本运行时间:1.5027523040771484ms
250286.989866
for循环版本运行时间:474.29513931274414ms
显然,使用for循环的时间是向量化版本的大约300倍。这突显了在深度学习计算中,利用向量化可以大大提高效率。
不仅如此,为了进一步提速,我们还可以使用GPU,它的计算能力远超于CPU。事实上,GPU和CPU都有并行计算的能力,这得益于它们都支持Single Instruction Multiple Data(SIMD)。简单地说,SIMDSIMDSIMD允许单一的指令同时处理多个数据点,从而大幅度提高了运算速度。特别是在做矩阵相关的运算时,GPU的性能由于其更多的并行处理单元而远超CPU。Python的numpy库的很多函数已经优化过,它们内部利用了SIMD指令,这也是为什么numpy函数如此高效的原因。
总之,无论是向量化还是利用GPU加速,目的都是为了让深度学习算法更快、更高效。
2. 更多的向量化示例 (More Vectorization Examples)
在我们之前的讨论中,已经明确提到:在计算时,应该尽量避免使用for循环,而更多地选择向量化矩阵运算,这样可以大大提高效率。如果你用过Python的numpy库,你可能已经知道我们通常使用np.dot()
函数来进行这种矩阵运算。
当我们将这种向量化的方法应用于逻辑回归算法时,可以极大地减少for循环的使用,取而代之的是直接采用矩阵运算。但这里需要注意的是,算法的最外层循环(例如训练迭代)是不能被替换的。不过,我们可以在每次迭代中直接使用矩阵运算来计算J、dw和b。
3. 向量化逻辑回归 (Vectorizing Logistic Regression)
回想一下我们在《神经网络与深度学习》的课程笔记(2)中所讨论的,整个训练数据集构成的输入矩阵 X X X的维度是 ( n x , m ) (nx, m) (nx,m),权重矩阵 w w w的维度是 ( n x , 1 ) (nx, 1) (nx,1), b b b是一个常数,输出矩阵 Y Y Y的维度是 ( 1 , m ) (1, m) (1,m)。有了向量化的思想,我们可以使用矩阵表示法来计算所有 m m m个样本的线性输出 Z Z Z:
Z = w T X + b Z = w^TX + b Z=wTX+b
在Python的numpy库中,这可以轻易地用以下方式表示:
Z = np.dot(w.T, X) + b
A = sigmoid(Z)
其中, w T w^T wT表示 w w w的转置。
所以,通过利用向量化矩阵运算,我们可以同时对所有 m m m个样本进行计算,从而极大地提高运算速度,而无需依赖for循环。
4. 向量化逻辑回归的梯度输出 (Vectorizing Logistic Regression’s Gradient Output)
当我们谈论机器学习,特别是逻辑回归时,效率是关键。逻辑回归中的梯度下降是一个关键的优化方法,但使用传统的循环来计算它可能会很慢。那么,我们怎么提高它的速度呢?答案是“向量化”。
向量化是一种用数组或矩阵替代传统循环的技术,可以显著提高计算速度,特别是当我们处理大数据集时。
首先,让我们考虑逻辑回归的梯度。对于所有的样本m
,我们有梯度dZ
,它的维度是 (1, m)。这个梯度可以简单地表示为:
d
Z
=
A
−
Y
dZ = A - Y
dZ=A−Y
接下来,我们需要计算另一个重要的参数db
,它代表了b
的梯度。它是dZ
所有元素的平均值,可以这样表示:
d
b
=
1
m
∑
i
=
1
m
d
z
(
i
)
db = \frac{1}{m} \sum_{i=1}^{m} dz(i)
db=m1i=1∑mdz(i)
在Python中,我们可以使用以下代码来实现它:
db = 1/m * np.sum(dZ)
然后,我们有dw
,它代表了w
的梯度,可以这样表示:
dw=1mX⋅dZTdw = \frac{1}{m} X \cdot dZ^Tdw=m1X⋅dZT
在Python中,我们可以使用以下代码来实现它:
dw = 1/m * np.dot(X, dZ.T)
所以,通过向量化,我们可以避免使用传统的for循环来计算逻辑回归中的梯度。一个完整的单次迭代的梯度下降算法可以这样表示:
Z = np.dot(w.T, X) + b
A = sigmoid(Z)
dZ = A - Y
dw = 1/m * np.dot(X, dZ.T)
db = 1/m * np.sum(dZ)
w = w - alpha * dw
b = b - alpha * db
这里,alpha
是学习率,它决定了w
和b
更新的速度。请注意,上述代码只表示一个单独的训练更新。在真实的场景中,为了达到最佳的结果,我们通常会在外部添加一个for循环来多次迭代此过程。
总之,通过使用向量化技术,我们不仅提高了代码的效率,而且使它更简洁易读。
注意: "sigmoid"是逻辑函数的一种常用实现,它将任何数字映射到0和1之间。
5. Broadcasting in Python (Python中的广播)
当我们在Python中使用NumPy这样的库来处理数组时,会遇到一个非常有用的技术叫做“广播”。广播允许我们用很直观的方式处理不同维度的数组。它背后的原理可能初看起来有点复杂,但一旦你掌握了,就会觉得非常强大和实用。
广播机制可以通过以下四点来理解:
-
首先,比较所有输入数组的维度。不足的部分会在其维度的前面加1,从而和最长的那个维度保持一致。
举个例子,假设我们有一个形状为 ( 3 , ) (3,) (3,)的数组和一个形状为 ( 1 , 3 ) (1,3) (1,3)的数组,当它们进行运算时,第一个数组会被看作形状为 ( 1 , 3 ) (1,3) (1,3)。 -
输出数组的维度是输入数组各个维度上的最大值。
如果我们把一个形状为 ( 3 , 1 ) (3,1) (3,1)的数组和一个形状为 ( 3 , 4 ) (3,4) (3,4)的数组相加,那么输出数组的形状就会是 ( 3 , 4 ) (3,4) (3,4)。 -
只有当输入数组在某个维度上的长度与输出数组相同,或者该维度的长度为1时,该输入数组才能用于计算。否则,计算会报错。
这意味着,一个形状为 ( 3 , 4 ) (3,4) (3,4)的数组和一个形状为 ( 4 , 3 ) (4,3) (4,3)的数组是不能进行元素级别运算的,因为它们的维度不兼容。 -
当输入数组的某个维度长度为1时,它会沿着这个维度复制其内容,使其与输出数组在该维度上的长度一致。
如果你用一个形状为 ( 3 , 1 ) (3,1) (3,1)的数组加上一个形状为 ( 3 , 4 ) (3,4) (3,4)的数组,那么第一个数组会沿着其第二个维度复制内容,好像它本来就是 ( 3 , 4 ) (3,4) (3,4)那样。
为了更好地理解,你可以想象广播像是一种让不同维度的数组能够和谐共存、一起工作的魔法。并且,通过使用reshape()
函数,你可以轻松地调整数组的形状,确保它们可以正常地进行广播操作。
注意: 当你在编写代码时,尤其是涉及多维数组的计算时,最好明确你的数组的形状,以避免可能出现的错误或不可预见的结果。
6. 关于Python/Numpy向量的小提示
我们接下来要探讨Python中一些关于向量的细节,帮助你避免编码中的小陷阱。
在Python的Numpy库中,当你使用以下方式创建一个向量:
a = np.random.randn(5)
这样产生的向量a
的形状是(5,)。这样的向量很特别,它既不是行向量,也不是列向量,我们称它为"rank 1 array"。这种向量在实际操作中可能会带来一些麻烦。比如,你试图转置这个向量,但结果还是它本身。为了避免这样的问题,当你需要创建一个(5,1)的列向量或一个(1,5)的行向量时,建议使用以下更规范的方式:
a = np.random.randn(5,1)
b = np.random.randn(1,5)
为了确保向量或数组的形状与你预期的相符,你可以使用assert
语句来进行检查:
assert(a.shape == (5,1))
这个assert
语句会检查a
的形状是否为(5,1)。如果不是,程序会立刻报错并停止。养成使用assert
语句的习惯,可以帮你及时捕捉潜在的错误,确保代码的准确性。
最后,如果需要,你还可以使用reshape
函数来调整数组的形状,使其符合你的要求:
a.reshape((5,1))
希望这些小提示能帮你更好地在Python中处理向量,避免不必要的错误!
7. Jupyter/iPython笔记本快速指南 (Quick tour of Jupyter/iPython Notebooks)
Jupyter notebook(也被称为IPython notebook)是一个能够实现编程、数学、绘图和文本全方位互动的工具,它支持超过40种编程语言,包括最受欢迎的Python。在这个课程中,我们会使用Python语言,并且所有的编程练习都会在Jupyter notebook上完成。
8. 逻辑回归成本函数的解释 (Explanation of logistic regression cost function - optional)
在之前的课程中,我们已经初步讲解了逻辑回归的成本函数。现在,我们来更深入地理解这个成本函数是如何产生的。
首先,我们有预测输出 y ^ \hat{y} y^,其公式为:
y ^ = σ ( w T x + b ) \hat{y} = \sigma(w^T x + b) y^=σ(wTx+b)
这里, σ ( z ) = 1 1 + exp ( − z ) \sigma(z) = \frac{1}{1 + \exp(-z)} σ(z)=1+exp(−z)1 表示sigmoid函数。我们可以将 y ^ \hat{y} y^理解为给定输入x时,输出为正类(y=1)的预测概率:
y ^ = P ( y = 1 ∣ x ) \hat{y} = P(y=1 | x) y^=P(y=1∣x)
如果真实的y=1,预测概率为:
p ( y ∣ x ) = y ^ p(y|x) = \hat{y} p(y∣x)=y^
反之,如果y=0:
p ( y ∣ x ) = 1 − y ^ p(y|x) = 1 - \hat{y} p(y∣x)=1−y^
我们可以结合上面两个方程得到:
P ( y ∣ x ) = y ^ y ( 1 − y ^ ) ( 1 − y ) P(y|x) = \hat{y}^y (1 - \hat{y})^{(1-y)} P(y∣x)=y^y(1−y^)(1−y)
为了简化问题,我们可以对上述概率应用对数函数。得到:
log P ( y ∣ x ) = y log y ^ + ( 1 − y ) log ( 1 − y ^ ) \log P(y|x) = y \log \hat{y} + (1 - y) \log (1 - \hat{y}) logP(y∣x)=ylogy^+(1−y)log(1−y^)
我们的目标是让上述的概率尽可能大,所以,将其乘以-1,我们就得到了单个样本的损失函数,即:
L = − ( y log y ^ + ( 1 − y ) log ( 1 − y ^ ) ) L = - (y \log \hat{y} + (1 - y) \log (1 - \hat{y})) L=−(ylogy^+(1−y)log(1−y^))
考虑到所有的m个训练样本,如果假设所有的样本都是独立同分布的,我们的目标是让整体的概率尽可能地大。通过引入对数函数并乘以-1,我们可以得到整体的成本函数:
J ( w , b ) = − 1 m ∑ i = 1 m [ y ( i ) log y ^ ( i ) + ( 1 − y ( i ) ) log ( 1 − y ^ ( i ) ) ] J(w, b) = - \frac{1}{m} \sum_{i=1}^m [y^{(i)} \log \hat{y}^{(i)} + (1 - y^{(i)}) \log (1 - \hat{y}^{(i)})] J(w,b)=−m1i=1∑m[y(i)logy^(i)+(1−y(i))log(1−y^(i))]
在这里, 1 m \frac{1}{m} m1 是一个正则化因子,用于对所有样本的成本函数取平均。
9. 总结 (Summary)
在这一节,我们探讨了神经网络的基础:Python和向量化。当涉及深度学习程序时,利用向量化和矩阵运算可以极大地提高执行速度,节省宝贵的时间。通过逻辑回归为例,我们学习了如何将算法流程和梯度下降转换为向量化形式。此外,我们还简要介绍了Python编程的相关方法和策略。