什么是函数?如何描述函数?与神经网络一样,函数也可以用多种方法描述,但没有一种方法能完整地描绘它。与其尝试给出简单的一句话描述,不如像盲人摸象那样,依次根据每个维度来了解函数。
下面是两个用数学符号描述的函数示例。
以上有两个函数,分别为 和 ,当输入数字 时,第一个函数将其转换为 ,第二个函数则将其转换为 。
下面是一种描绘函数的方法。
这种利用坐标的方法是法国哲学家勒内 · 笛卡儿发明的,它在许多数学领域非常有用,特别是微积分领域。图 1-1 展示了以上两个函数的示意图。
图 1-1:两个连续、基本可微的函数
然而,还有另一种描绘函数的方法,这种方法在学习微积分时并没有那么有用,但是对于思考深度学习模型非常有帮助。可以把函数看作接收数字(输入)并生成数字(输出)的盒子,就像小型工厂一样,它们对输入的处理有自己的内部规则。图 1-2 通过一般规则和具体的输入描绘了以上两个函数。
图 1-2:另一种描绘函数的方法
最后,可以使用代码描述这两个函数。在开始之前,先介绍一下 NumPy 这个 Python 库,下面会基于该库编写函数。
NumPy 是一个广泛使用的 Python 库,用于快速数值计算,其内部大部分使用 C 语言编写。简单地说,在神经网络中处理的数据将始终保存在一个
多维数组
中,主要是一维数组、二维数组、三维数组或四维数组,尤其以二维数组或三维数组居多。NumPy 库中的
ndarray
类能够让我们以直观且快速的方式计算这些数组。举一个最简单的例子,如果将数据存储在 Python 列表或列表的嵌套列表中,则无法使用常规语法实现数据对位相加或相乘,但
ndarray
类可以实现:
print("Python list operations:")
a = [1,2,3]
b = [4,5,6]
print("a+b:", a+b)
try:
print(a*b)
except TypeError:
print("a*b has no meaning for Python lists")
print()
print("numpy array operations:")
a = np.array([1,2,3])
b = np.array([4,5,6])
print("a+b:", a+b)
print("a*b:", a*b)
Python list operations:
a+b: [1, 2, 3, 4, 5, 6]
a*b has no meaning for Python lists
numpy array operations:
a+b: [5 7 9]
a*b: [ 4 10 18]
ndarray
还具备
维数组所具有的多个特性:每个
ndarray
都具有
个轴(从 0 开始索引),第一个轴为轴 0,第二个轴为轴 1,以此类推。另外,由于二维
ndarray
较为常见,因此可以将轴 0 视为行,将轴 1 视为列,如图 1-3 所示。
图 1-3:一个二维
ndarray
,其中轴 0 为行,轴 1 为列
NumPy 库的
ndarray
类还支持以直观的方式对这些轴应用函数。例如,沿轴 0(二维数组的
行
)求和本质上就是沿该轴“折叠数组”,返回的数组比原始数组少一个维度。对二维数组来说,这相当于对每一列进行求和:
print('a:')
print(a)
print('a.sum(axis=0):', a.sum(axis=0))
print('a.sum(axis=1):', a.sum(axis=1))
a:
[[1 2]
[3 4]]
a.sum(axis=0): [4 6]
a.sum(axis=1): [3 7]
ndarray
类支持将一维数组添加到最后一个轴上。对一个
行
列的二维数组
a
而言,这意味着可以添加长度为
的一维数组
b
。NumPy 将以直观的方式进行加法运算,并将元素添加到
a
的每一行中
。
a = np.array([[1,2,3],
[4,5,6]])
b = np.array([10,20,30])
print("a+b:\n", a+b)
a+b:
[[11 22 33]
[14 25 36]]
如前所述,本书代码的主要目标是确保概念描述的准确性和清晰性。随着本书内容的展开,这将变得更具挑战性,后文涉及编写带有许多参数的函数,这些参数是复杂类的一部分。为了解决这个问题,本书将在整个过程中使用带有类型签名的函数。例如,在第3章中,我们将使用如下方式初始化神经网络:
def __init__(self,
layers: List[Layer],
loss: Loss,
learning_rate: float = 0.01) -> None:
仅通过类型签名,就能了解该类的用途。与此相对,考虑以下 可用于定义运算 的类型签名:
def operation(x1, x2):
这个类型签名本身并没有给出任何提示。只有打印出每个对象的类型,查看对每个对象执行的运算,或者根据名称
x1
和
x2
进行猜测,才能够理解该函数的功能。这里可以改为定义具有类型签名的函数,如下所示:
def operation(x1: ndarray, x2: ndarray) -> ndarray:
很明显,这是接受两个
ndarray
的函数,可以用某种方式将它们组合在一起,并输出该组合的结果。由于它们读起来更为清楚,因此本书将使用经过类型检查的函数。
了解前面的内容后,现在来编写之前通过 NumPy 库定义的函数:
def square(x: ndarray) -> ndarray:
'''
将输入ndarray中的每个元素进行平方运算。
'''
return np.power(x, 2)
def leaky_relu(x: ndarray) -> ndarray:
'''
将Leaky ReLU函数应用于ndarray中的每个元素。
'''
return np.maximum(0.2 * x, x)
NumPy 库有一个奇怪的地方,那就是可以通过使用
np.
function_name
(ndarray)
或
ndarray.
function_name
将许多函数应用于
ndarray
。例如,前面的 ReLU 函数可以编写成
x.clip(min=0)
。本书将尽量保持一致,在整个过程中遵循
np.
function_name
(ndarray)
约定,尤其会避免使用
ndarray
.T
之类的技巧来转置二维
ndarray
,而会使用
np.transpose(
ndarray
, (1, 0))
。
如果能通过数学、示意图和代码这 3 个维度来表达相同的基本概念,就说明你已经初步拥有了真正理解深度学习所需的灵活思维。