深度学习基础
神经网络的数学基础
神经网络的“齿轮”:张量运算
-
所有计算机程序最终都可以简化为二进制输入上的一些二进制运算(AND、OR、NOR等),与此类似,深度神经网络学到的所有变换也都可以简化为数值数据张量上的一些张量运算(tensor operation),例如加上张量、乘以张量等。
-
在最开始的例子中,我们通过叠加 Dense 层来构建网络:
keras.layers.Dense(512, activation='relu')
- 这个层可以理解为一个函数,输入一个 2D 张量,返回另一个 2D 张量,即输入张量的新表示。
- 具体而言,这个函数如下所示(其中 W 是一个 2D 张量,b 是一个向量,二者都是该层的属性):
output = relu(dot(W, input) + b)
- 这里有三个张量运算:输入张量和张量 W 之间的点积运算(dot)、得到的 2D 张量与向量 b 之间的加法运算(+)、最后的 relu 运算。
- relu(x) 是 max(x, 0)。
逐元素运算
-
relu 运算和加法都是逐元素(element-wise)的运算,即该运算独立地应用于张量中的每个元素,也就是说,这些运算非常适合大规模并行实现(向量化实现)。
-
如果你想对逐元素运算编写简单的 Python 实现,那么可以用 for 循环。
# 对逐元素 relu 运算的简单实现。 def naive_relu(x): assert len(x.shape) == 2 # x 是一个 Numpy 的 2D 张量 x = x.copy() # 避免覆盖输入张量 for i in range(x.shape[0]): for j in range(x.shape[1]): x[i, j] = max(x[i, j], 0) return x # 对逐元素加法运算的简单实现。 def naive_add(x, y): assert len(x.shape) == 2 # x 和 y 是 Numpy 的 2D 张量 assert x.shape == y.shape x = x.copy() for i in range(x.shape[0]): for j in range(x.shape[1]): x[i, j] += y[i, j] return x # 根据同样的方法,可以实现逐元素的乘法、减法等。
-
在实践中处理 Numpy 数组时,这些运算都是优化好的 Numpy 内置函数,因此,在 Numpy 中可以直接进行下列逐元素运算,速度非常快。
import numpy as np z = x + y # 逐元素的相加 z = np.maximum(z, 0.) # 逐元素的 relu
广播
-
如果将两个形状不同的张量相加,如果没有歧义的话,较小的张量会被广播(broadcast),以匹配较大张量的形状。
-
广播包含以下两步。
- 向较小的张量添加轴(叫作广播轴),使其 ndim 与较大的张量相同。
- 将较小的张量沿着新轴重复,使其形状与较大的张量相同。
-
假设 X 的形状是(32, 10),y 的形状是(10, )。
- 首先,我们给 y 添加空的第一个轴,这样 y 的形状变为(1, 10)。
- 然后,我们将 y 沿着新轴重复32次,这样得到的张量 Y 的形状为(32, 10),并且
Y[i, :] == y for i in range(0, 32)
。 - 现在,我们可以将 X 和 Y 相加,因为它们的形状相同。
-
在实际的实现过程中并不会创建新的 2D 张量,因为那样做非常低效。重复的操作完全是虚拟的,它只出现在算法中,而没有发生在内存中。
# 简单实现矩阵与向量的加法 def naive_add_matrix_and_vector(x, y): assert len(x.shape) == 2 # x 是一个 Numpy 的 2D 张量 assert len(y.shape) == 1 # y 是一个 Numpy 向量 assert x.shape[1] == y.shape[0] x = x.copy() # 避免覆盖输入张量 for i in range(x.shape[0]): for j in range(x.shape[1]): x[i, j] += y[j] return x
-
如果一个张量的形状是 (a, b, … n, n+1, … m),另一个张量的形状是 (n, n+1, … m),那么你通常可以利用广播对它们做两个张量之间的逐元素运算。广播操作会自动应用于从 a 到 n-1 的轴。
# 利用广播将逐元素的 maximum 运算应用于两个形状不同的张量 import numpy as np x = np.random.random((64, 3, 32, 10)) # x 是形状为 (64, 3, 32, 10) 的随机张量 y = np.random.random((32, 10)) # y 是形状为 (32, 10) 的随机张量 z = np.maximum(x, y) # 输出 z 的形状是 (64, 3, 32, 10),与 x 相同
张量点积
-
点积运算,也叫张量积(tensor product),是最常见也最有用的张量运算。
-
与逐元素的运算不同,它将输入张量的元素合并在一起。
-
在 Numpy、Keras、Theano 和 TensorFlow 中,都是用 * 实现逐元素乘积。
-
TensorFlow 中的点积使用了不同的语法,但在 Numpy 和 Keras 中,都是用标准的 dot 运算符来实现点积。
import numpy as np z = np.dot(x, y) # 两个向量 x 和 y 的点积。其计算过程如下。 def naive_vector_dot(x, y): # x 和 y 都是 Numpy 向量 assert len(x.shape) == 1 assert len(y.shape) == 1 assert x.shape[0] == y.shape[0] z = 0. for i in range(x.shape[0]): z += x[i] * y[i] return z
-
两个向量之间的点积是一个标量,而且只有元素个数相同的向量之间才能做点积。
-
你还可以对一个矩阵 x 和一个向量 y 做点积,返回值是一个向量,其中每个元素是 y 和 x 的每一行之间的点积。
import numpy as np def naive_matrix_vector_dot(x, y): assert len(x.shape) == 2 # x 是一个 Numpy 矩阵 assert len(y.shape) == 1 # y 是一个 Numpy 向量 assert x.shape[1] == y.shape[0] # x 的第1维和 y 的第0维大小必须相同 z = np.zeros(x.shape[0]) # 这个运算返回一个全是0的向量,其形状与 x.shape[0] 相同 for i in range(x.shape[0]): for j in range(x.shape[1]): z[i] += x[i, j] * y[j] return z
-
如果两个张量中有一个的 ndim 大于1,那么 dot 运算就不再是对称的,也就是说,dot(x, y) 不等于 dot(y, x)。
-
-
点积可以推广到具有任意个轴的张量。
-
最常见的应用可能就是两个矩阵之间的点积。
-
对于两个矩阵 x 和 y,当且仅当 x.shape[1] == y.shape[0] 时,你才可以对它们做点积(dot(x, y))。
-
得到的结果是一个形状为 (x.shape[0], y.shape[1]) 的矩阵,其元素为 x 的行与 y 的列之间的点积。
# 矩阵乘法的实现 def naive_matrix_dot(x, y): # x 和 y 都是 Numpy 矩阵 assert len(x.shape) == 2 assert len(y.shape) == 2 assert x.shape[1] == y.shape[0] # x 的第1维和 y 的第0维大小必须相同 z = np.zeros((x.shape[0], y.shape[1])) # 这个运算返回特定形状的零矩阵 for i in range(x.shape[0]): # 遍历 x 的所有行 for j in range(y.shape[1]): # 然后遍历 y 的所有列 row_x = x[i, :] column_y = y[:, j] z[i, j] = naive_vector_dot(row_x, column_y) return z
-