基本数学计算
本章讨论如何使用 Python 代码进行典型的科学计算。我们将重点说明数值数据在数学上如何表示,如何被结构化或组织,如何存储、操作、处理以及以高效方式计算。还将讨论这些操作在 Python 中的一些细节。本教材中的所有代码,特别是本章中的代码,可以在 https://sohoware.ru/SohoBook/ 找到。我们的讨论将从向量、矩阵和张量结构数据上的一些线性代数基本运算开始。 线性代数对于任何与大数据相关的计算都极为重要,机器学习也不例外。我们计划借助 Python 编程以及 Python 社区中已经广泛开发的模块,简要回顾线性代数的基本运算。我们将依次说明基本概念、数学记号、数据结构以及计算过程。已经熟悉线性代数基础计算的读者可以跳过本章。我们的讨论从数据结构开始。首先导入必要的模块和函数。...
关键思想
- 3.1 线性代数
- 3.2 插值
- 3.3 单项式分解
- 3.4 主成分分析
- 3.5 数值求根
- 3.6 机器学习语境中的数值积分
实践任务
选取一个与“基本数学计算”相关的小例子,说明机器学习、特征在其中如何发挥作用,并用一句话解释结果。
基本数学计算
本章讨论如何使用 Python 代码进行典型的科学计算。我们将重点说明数值数据在数学上如何表示,如何被结构化或组织,如何存储、操作、处理以及以高效方式计算。还将讨论这些操作在 Python 中的一些细节。本教材中的所有代码,特别是本章中的代码,可以在 https://sohoware.ru/SohoBook/ 找到。我们的讨论将从向量、矩阵和张量结构数据上的一些线性代数基本运算开始。
3.1 线性代数
线性代数对于任何与大数据相关的计算都极为重要,机器学习也不例外。我们计划借助 Python 编程以及 Python 社区中已经广泛开发的模块,简要回顾线性代数的基本运算。我们将依次说明基本概念、数学记号、数据结构以及计算过程。已经熟悉线性代数基础计算的读者可以跳过本章。我们的讨论从数据结构开始。首先导入必要的模块和函数。
import numpy as np # 导入 NumPy 包,并将其别名设为 np
# 后面还将使用 TensorFlow 包。
# 如果尚未安装,可以使用命令 pip install tensorflow 安装 TensorFlow。
# 安装后导入 TensorFlow。
import tensorflow as tf # 导入 TensorFlow,并将其别名设为 tf3.1.1 标量数
在上一章中,我们已经在数学计算的语境下接触过标量数。这些数主要分为三类:整数、实数和复数。在 Python 编程语言中,每个这样的数都会被赋予唯一的名称和内存地址。这使我们能够通过名称访问这个数,更新它的值,并将它作为函数的参数使用;这些函数可以是内置函数、用户在代码中定义的函数,也可以是导入模块中的函数。
Python 中的数值运算被设计得直观而直接。不过,在计算中经常会出现与数值允许范围有关的问题,这可能导致溢出(overflow)或下溢、精度损失(underflow)。也就是说,某个数对于当前数值类型和计算机体系结构的限制而言可能过大或过小,从而无法被正确处理。
在 Python 中,通常认为数值能够覆盖整个实数范围,但其精度受到计算机特性的限制。这一点很重要,因为在计算中,精度以及处理大数值范围的能力可能会显著影响模型的效率和准确性。
3.1.2 向量
向量是按一维顺序排列的一组数。这些数称为向量的分量。在物理语境中,向量的每个分量可以表示某一方向上的量。例如,三维空间中的力向量包含三个分量,每个分量对应沿相应坐标轴方向作用的力。
在机器学习中,向量用于表示特征集合,其中向量的每个元素对应一个特征。这样的向量可能非常大,尤其是在使用有限元方法等技术时,对象会被离散化为大量单元和节点,从而产生包含数百万个分量的向量。
下面借助 Python 及其科学计算库 TensorFlow 和 NumPy,来看向量的表示方法。
在 TensorFlow 中创建向量的示例:
import tensorflow as tf # 导入 TensorFlow,并将其别名设为 tf
p = 15 # 向量长度,表示分量的数量
# 创建一个由从 0 到 p-1 的连续数字构成的一维张量
x = tf.range(p) # 使用 TensorFlow 库的 range 函数
# 输出张量及其形状
print(x) # 张量 x 现在包含 15 个分量
print(x.shape) # Python 中张量的形状表示为 (p,)tf.Tensor([ 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14], shape=(15,), dtype=int32)
(15,)结果得到长度为 15 的向量 x。在数学中,这样的向量常常写成列向量,但在 Python 中,默认表示更接近行形式的一维数组。
如果希望转置这个向量,也就是把行向量转换为列向量,需要先改变它的形状:
import tensorflow as tf # 导入 TensorFlow,并将其别名设为 tf
# 改变张量形状,使其成为二维张量(列向量)
x_reshaped = tf.reshape(x, (p, 1))
# 转置二维张量
x_transposed = tf.transpose(x_reshaped)
# 输出转置后的张量及其形状
print(x_transposed) # 转置后的向量
print(x_transposed.shape) # 转置后向量的形状tf.Tensor([[ 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14]], shape=(1, 15), dtype=int32)
(1, 15)在 TensorFlow 中,向量转置不一定会导致数据在内存中发生物理移动,因此这一操作是高效的。不过,TensorFlow 可能不会为原始张量和转置张量保留相同的内存地址,特别是在使用改变张量形状的操作时。
3.1.3 矩阵
矩阵是一种由数字组成的数据结构,这些数字按照两个维度排列:行和列。矩阵是向量概念向二维及更高维结构的扩展。矩阵中的每一列都可以看作一个向量。在这种情况下,矩阵可以被理解为多个向量并排堆叠而成的集合。矩阵用于表示和处理数据集,其中行对应单独的观测对象,例如不同图像;列对应不同特征,例如像素。
在 Python 编程中,使用 TensorFlow 库可以如下执行矩阵操作:
import tensorflow as tf # 导入 TensorFlow,并将其别名设为 tf
p = 15 # 向量长度,表示分量的数量
# 创建一个由从 0 到 p-1 的连续数字构成的一维张量
x = tf.range(p) # 使用 TensorFlow 库的 range 函数
# 假设 x 是一维张量,改变其形状,
# 得到一个 3 行 5 列的二维张量
A = tf.reshape(x, (3, 5))
# 输出张量 A 及其转置版本
print('@' * 50)
print("A =", A)
print("A.T =", tf.transpose(A))
# 查看张量 A 及其转置版本的形状
print('@' * 50)
print(A.shape)
print(tf.transpose(A).shape)
# 可以通过索引和切片访问张量元素:
print('@' * 50)
# 访问第一行第二列位置上的元素
print('A[0, 1] =', A[0, 1])
print('第 2 行', A[1, :]) # 获取第二行的所有元素
print('第 1 列', A[:, 1]) # 获取第二列的所有元素@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
A = tf.Tensor(
[[ 0 1 2 3 4]
[ 5 6 7 8 9]
[10 11 12 13 14]], shape=(3, 5), dtype=int32)
A.T = tf.Tensor(
[[ 0 5 10]
[ 1 6 11]
[ 2 7 12]
[ 3 8 13]
[ 4 9 14]], shape=(5, 3), dtype=int32)
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
(3, 5)
(5, 3)
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
A[0, 1] = tf.Tensor(1, shape=(), dtype=int32)
第 2 行 tf.Tensor([5 6 7 8 9], shape=(5,), dtype=int32)
第 1 列 tf.Tensor([ 1 6 11], shape=(3,), dtype=int32)3.1.4 张量
在数学和物理中,张量是一组结构化数据,在坐标系变化时会按照特定规则变换。张量可以具有不同的阶数:标量(单个数)是零阶张量,向量是一阶张量,矩阵是二阶张量,随着维度数量增加还可以得到更高阶张量。
不过,在机器学习(ML)的语境中,“张量”通常指任意维度超过二的数据数组。在这一领域,张量常常与需要以高维形式组织的大数据相关联。例如,RGB 格式图像就是一个三维张量,其轴分别对应高度、宽度和颜色通道。在 Python 的 NumPy 库中,张量以多维数组的形式表示。
机器学习通常不使用物理学中的张量变换规则,因此在本教材中,我们将使用“张量”一词来表示多维数组,而不考虑严格数学意义上的张量变换规则。
可以使用如下代码在 TensorFlow 库中创建张量:
import tensorflow as tf # 导入 TensorFlow,并将其别名设为 tf
# 先用 tf.range 创建三维张量的数据,再改变其形状
X = tf.range(24)
X = tf.reshape(X, (2, 3, 4))
# 输出张量的形状以及张量本身
print('X.shape =', X.shape)
print('X =', X)X.shape = (2, 3, 4)
X = tf.Tensor(
[[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]]
[[12 13 14 15]
[16 17 18 19]
[20 21 22 23]]], shape=(2, 3, 4), dtype=int32)结果是一个形状为 (2, 3, 4) 的三维张量 X,它表示两个数据块,每个数据块包含三行和四列。
在机器学习中,张量常用于逐元素执行的操作,如下面的示例所示:
import tensorflow as tf # 导入 TensorFlow,并将其别名设为 tf
# 创建二维张量 A,以及形状相同、所有元素为 8 的张量 B
A = tf.reshape(tf.range(8), (2, 4))
B = tf.ones_like(A) * 8
# 对张量 A 和 B 执行逐元素加法与乘法
print('A =', A, '\nB =', B)
print('A + B =', A + B, '\nA * B =', A * B)A = tf.Tensor(
[[0 1 2 3]
[4 5 6 7]], shape=(2, 4), dtype=int32)
B = tf.Tensor(
[[8 8 8 8]
[8 8 8 8]], shape=(2, 4), dtype=int32)
A + B = tf.Tensor(
[[ 8 9 10 11]
[12 13 14 15]], shape=(2, 4), dtype=int32)
A * B = tf.Tensor(
[[ 0 8 16 24]
[32 40 48 56]], shape=(2, 4), dtype=int32)这里,张量 A 由从 0 到 7 的数字组成,而张量 B 的每个元素都是重复的数字 8。加法和乘法都按元素执行,结果是形状相同的新张量。
在机器学习中,经常需要计算张量中元素的总和或平均值。在这种情况下,张量可以被看作多维数据数组。对张量进行求和、求平均等操作,可以得到数据的汇总特征。
下面说明如何在 Python 中使用 TensorFlow 库实际执行这些操作:
import tensorflow as tf # 导入 TensorFlow,并将其别名设为 tf
# 使用 range 函数初始化一维张量(向量)
x = tf.range(5)
print(x) # 输出向量
print(tf.reduce_sum(x)) # 计算向量元素之和tf.Tensor([0 1 2 3 4], shape=(5,), dtype=int32)
tf.Tensor(10, shape=(), dtype=int32)这段代码创建了一个一维张量(向量)x,其取值为 0 到 4。print(x) 命令显示这个向量。print(tf.reduce_sum(x)) 返回向量 x 中所有元素的总和,也就是 0 + 1 + 2 + 3 + 4,结果为 10。
接下来,创建一个二维张量(矩阵)并执行相关运算:
import tensorflow as tf # 导入 TensorFlow,并将其别名设为 tf
# 创建一个由 1 填充的二维张量
X = tf.ones((3, 5))
print(X) # 输出矩阵
print(tf.reduce_sum(X)) # 计算矩阵元素之和
# 用两种方式计算矩阵元素的平均值
print(tf.reduce_mean(X), tf.reduce_sum(X) / tf.size(X).numpy())tf.Tensor(
[[1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1.]], shape=(3, 5), dtype=float32)
tf.Tensor(15.0, shape=(), dtype=float32)
tf.Tensor(1.0, shape=(), dtype=float32) tf.Tensor(1.0, shape=(), dtype=float32)这段代码创建了一个大小为 3×5 的矩阵 X,其中每个元素都等于 1。print(X) 命令显示该矩阵。print(tf.reduce_sum(X)) 计算矩阵 X 中所有元素的总和;由于矩阵中有 15 个 1,因此结果为 15。这里,tf.reduce_mean(X) 计算张量 X 中所有元素的平均值;由于所有元素都等于 1,所以平均值为 1。通过将元素总和除以张量中的元素数量,即 tf.reduce_sum(X) / tf.size(X).numpy(),也可以得到相同结果。
这些操作是数据分析的基础,它们可以把高维信息压缩为更易管理、也更易解释的统计量。
3.1.5 两个向量的点积
点积是机器学习中最常用的运算之一。本节将详细讨论它在向量中的使用方式。向量可能采用不同的数据结构,这会带来一些细节问题。
给定两个向量 a 和 b。在线性代数中,它们的点积通常写作 a · b。本质上,它只是对应元素乘积之和,结果是一个标量。这意味着向量 a 和 b 的形状必须兼容:它们必须具有相同长度。下面看几个示例。
import numpy as np # 导入 NumPy 包,并将其别名设为 np
import tensorflow as tf # 导入 TensorFlow,并将其别名设为 tf
# 初始化两个向量 a 和 b
a = tf.range(5)
b = tf.ones_like(a) * 2 # 这可以保证它们兼容
# 输出向量及其形状
print('--------------------------------------')
print(f"a={a}, a.shape={a.shape}\nb={b}, b.shape={b.shape}")
# 使用 TensorFlow 库计算向量点积
print('--------------------------------------')
print('tf.tensordot(a, b, axes=1)', tf.tensordot(a, b, axes=1))
# 输出点积结果的形状
print('--------------------------------------')
print('tf.tensordot(a, b, axes=1).shape', tf.tensordot(a, b, axes=1).shape)
# 使用 NumPy 库计算向量点积
print('--------------------------------------')
print(f"np.dot(a, b)={np.dot(a.numpy(), b.numpy())}")--------------------------------------
a=[0 1 2 3 4], a.shape=(5,)
b=[2 2 2 2 2], b.shape=(5,)
--------------------------------------
tf.tensordot(a, b, axes=1) tf.Tensor(20, shape=(), dtype=int32)
--------------------------------------
tf.tensordot(a, b, axes=1).shape ()
--------------------------------------
np.dot(a, b)=20在 TensorFlow 和 NumPy 中,向量 a 与 b 的点积给出相同的标量结果。
转置是一种数学运算,它把矩阵的行变为列,把列变为行。这一操作经常用于数学和计算机计算,尤其是在矩阵处理中。
不过,当涉及 NumPy 中的一维数组(向量)时,情况略有不同。在 NumPy 中,一维数组或向量的形状为 (n,),其中 n 是数组中元素的数量。这里仅指定一个维度,对应向量的长度。由于不存在第二个维度,转置操作并没有通常意义上的效果,因为没有第二条轴可以与第一条轴“交换”。转置假定存在二维结构,而一维数组已经处于最简单的形式。
更一般地说,当我们转置 NumPy 中的二维数组时,会交换它的轴。如果有一个形状为 (m, n) 的数组,其中 m 是行数,n 是列数,那么转置会把它的形状变为 (n, m)。这意味着转置前位于第 i 行、第 j 列的元素,转置后将位于第 j 行、第 i 列。
对于一维数组,无法执行这样的轴交换,因为它只有一个维度。NumPy 中的 .T 属性或 transpose() 方法会返回原始的一维数组而不改变它,因为不存在可交换的轴。
下面用一个简单示例来说明:
import numpy as np
# 在 NumPy 中创建一维数组
a = np.array([1, 2, 3])
# 尝试转置一维数组
a_transposed = a.T
# 转置结果仍然是同一个一维数组
print(a_transposed) # 输出 [1 2 3][1 2 3]可以看到,一维数组转置后的结果仍然是同一个一维数组,因为它只有一条轴,因此没有另一条轴可以与之交换。NumPy 的一维数组形状为 (n,),并不被视为矩阵。
当 b 是列向量(二维数组的一种特殊情形)时,它与矩阵一样具有两条轴。从结果标量值的角度看,点积 a · b 与矩阵乘积 ab 相同,其中 a 被定义为行向量,b 被定义为列向量。因此,在本教材的说明中,我们在数学上通常不区分二者,并经常使用如下等式:a · b = ab。
下面给出一些代码示例,用于说明上述细节:
import numpy as np # 导入 NumPy 包,并将其别名设为 np
import tensorflow as tf # 导入 TensorFlow,并将其别名设为 tf
# 初始化两个向量 a 和 b
a = tf.range(5)
b = tf.fill([5], 2) # 这可以保证它们兼容
# 将一维数组 b 转换为列向量
b_c = tf.reshape(b, [-1, 1])
# 输出列向量及其形状
print('-----------------------------------')
print(b_c, 'b_c.shape=', b_c.shape)
# 在 NumPy 中计算点积(结果是标量)
print('-----------------------------------')
print('np.dot(a, b)', np.dot(a.numpy(), b.numpy()))
# 计算一维数组与列向量的点积(结果是数组)
print('-----------------------------------')
print('np.dot(a, b_c)', np.dot(a.numpy(), b_c.numpy()))
# 输出 TensorFlow 中点积运算的结果
print('-----------------------------------')
print('tf.tensordot(a, b, axes=1)', tf.tensordot(a, b, axes=1))
print('-----------------------------------')
print('tf.tensordot(a, b_c, axes=1)', tf.tensordot(a, b_c, axes=1))
print('-----------------------------------')
print('tf.tensordot(a, b_c, axes=1).shape', tf.tensordot(a, b_c, axes=1).shape)-----------------------------------
tf.Tensor(
[[2]
[2]
[2]
[2]
[2]], shape=(5, 1), dtype=int32) b_c.shape= (5, 1)
-----------------------------------
np.dot(a, b) 20
-----------------------------------
np.dot(a, b_c) [20]
-----------------------------------
tf.tensordot(a, b, axes=1) tf.Tensor(20, shape=(), dtype=int32)
-----------------------------------
tf.tensordot(a, b_c, axes=1) tf.Tensor([20], shape=(1,), dtype=int32)
-----------------------------------
tf.tensordot(a, b_c, axes=1).shape (1,)可以看到,所有这些操作都给出相同的标量数值。
点积是线性代数中的标准运算。当我们以列向量形式处理向量时,它具有特别重要的意义。在线性代数中,向量通常表示为列向量;在这种语境中,两个列向量的点积会得到一个标量。不过,在数学记号中,这一乘积通常使用其中一个向量的转置来书写,以表明我们正在把两个向量的对应元素相乘并求和。
假设有两个向量 a 和 b,它们都具有列向量形式,并且包含相同数量的元素。这些向量的点积可以记作 a^T b 或 b^T a,其中符号 T 表示转置操作。对向量 a 进行转置,会把它从列向量变为行向量,然后对向量 a 与向量 b 的对应元素乘积求和。这个乘积给出一个标量,也就是一个单独的数。
需要注意的是,虽然结果是标量值,但在 NumPy 和 TensorFlow 等库中,结果可能以包含一个元素的二维数组或矩阵形式表示,这从技术上看是一个大小为 1×1 的矩阵,而不只是普通数字。这是因为这些库用数组来表示向量和矩阵,对它们执行的运算遵循数组规则,即使最终结果只是一个单值。
import numpy as np # 导入 NumPy 包,并将其别名设为 np
import tensorflow as tf # 导入 TensorFlow,并将其别名设为 tf
# 初始化两个向量 a 和 b
a = tf.range(5)
b = tf.fill([5], 2) # 这可以保证它们兼容
# 将一维数组 b 转换为列向量
b_c = tf.reshape(b, [-1, 1])
# 将一维数组 a 转换为列向量
a_c = tf.reshape(a, [-1, 1])
# 计算转置后的列向量 a 与列向量 b 的点积
# 结果是二维数组中的一个标量
print('-------------------------------------')
print('tf.matmul(a_c, b_c, transpose_a=True)', tf.matmul(a_c, b_c, transpose_a=True))
# 要从形状为 (1, 1) 的二维数组中取得标量值,可以使用:
print('-------------------------------------')
print('scalar value', tf.reshape(tf.matmul(a_c, b_c, transpose_a=True), []).numpy())
# 使用 reshape 将列向量重新转换为一维向量
print('-------------------------------------')
print('tf.tensordot(a_c, b_c, axes=1)', tf.tensordot(tf.reshape(a_c, [-1]), tf.reshape(b_c, [-1]), axes=1))
# 要从一维数组形状的结果中取得标量值,可以使用:
print('-------------------------------------')
print('scalar value', tf.tensordot(tf.reshape(a_c, [-1]), tf.reshape(b_c, [-1]), axes=1).numpy())-------------------------------------
tf.matmul(a_c, b_c, transpose_a=True) tf.Tensor([[20]], shape=(1, 1), dtype=int32)
-------------------------------------
scalar value 20
-------------------------------------
tf.tensordot(a_c, b_c, axes=1) tf.Tensor(20, shape=(), dtype=int32)
-------------------------------------
scalar value 20因此,上述代码展示了如何在 TensorFlow 和 NumPy 中执行点积运算,以及如何处理这些运算在不同数据结构中产生的结果。
3.1.6 两个向量的外积:定义与应用
两个向量 a 和 b 的外积通常会得到一个矩阵。该运算无论向量 a 和 b 的具体形状如何,总可以在适当解释后执行。
其工作方式如下:矩阵中位置 ij 的元素表示 a_i b_j 的乘积。例如,如果有一个元素为 [0, 1, 2] 的向量,以及一个元素为 [2, 2, 2, 2, 2] 的向量,那么外积的结果就是一个矩阵,其中第一个向量的每个元素都与第二个向量的每个元素相乘。
下面使用 NumPy 库在 Python 中给出示例:
import numpy as np # 导入 NumPy 包,并将其别名设为 np
# 定义向量 a 和 b
a = np.arange(3) # 创建向量 [0, 1, 2]
b = np.ones(5) * 2 # 创建向量 [2, 2, 2, 2, 2]
# 输出向量 a 和 b
print('-------------------------------')
print(a, b)
# 计算 a 和 b 的外积
# np.outer() 函数接收两个向量,并返回它们的外积
outer_product = np.outer(a, b)
print('-------------------------------')
print('np.outer=\n', outer_product)-------------------------------
[0 1 2] [2. 2. 2. 2. 2.]
-------------------------------
np.outer=
[[0. 0. 0. 0. 0.]
[2. 2. 2. 2. 2.]
[4. 4. 4. 4. 4.]]也可以通过 @ 运算符得到类似结果:先把向量 a 变换为列向量(形状为 (n, 1)),再把向量 b 变换为行向量(形状为 (1, m))。
import numpy as np # 导入 NumPy 包,并将其别名设为 np
# 定义向量 a 和 b
a = np.arange(3) # 创建向量 [0, 1, 2]
b = np.ones(5) * 2 # 创建向量 [2, 2, 2, 2, 2]
# 将一维数组 a 转换为列向量
a_c = a.reshape(-1, 1)
# 将一维数组 b 转换为列向量
b_c = b.reshape(-1, 1)
# 对列向量 b_c 转置,得到行向量
b_transposed = b_c.T
# 输出向量 a_c 和 b_transposed
print('-------------------------------')
print(a_c, b_transposed)
# 计算 a 和 b 的外积
result = a_c @ b_transposed
print('-------------------------------')
print('np.outer=\n', result)-------------------------------
[[0]
[1]
[2]] [[2. 2. 2. 2. 2.]]
-------------------------------
np.outer=
[[0. 0. 0. 0. 0.]
[2. 2. 2. 2. 2.]
[4. 4. 4. 4. 4.]]不过,通常更建议使用内置函数 np.outer(),因为它运行更快,也不需要额外的形状变换操作。
3.1.7 矩阵与向量的乘积
矩阵与向量相乘是线性代数中的标准运算,当矩阵和向量的维度兼容时即可执行。该操作可以通过把数据乘以由向量表示的特征权重来完成数据变换。
下面看一下如何在 Python 中使用 TensorFlow 库实际实现这一运算:
import tensorflow as tf # 导入 TensorFlow,并将其别名设为 tf
# 初始化一个 3×5 矩阵 A35,其中包含从 0 到 14 的数字
A35 = tf.reshape(tf.range(15), (3, 5))
# 初始化长度为 5、由 1 组成的向量 d5
d5 = tf.cast(tf.ones((A35.shape[1],)), tf.int32) # 将 d5 转换为 int32
# 输出矩阵 A35 及其形状、向量 d5 及其形状
print('--------------------')
print(A35.numpy(), A35.shape, d5.numpy(), d5.shape)
# 计算矩阵 A35 与向量 d5 的乘积
f = tf.matmul(A35, tf.reshape(d5, [-1, 1]))
# 形状兼容:[3, 5] × [5, 1] -> 长度为 3 的向量
print('--------------------')
print(f.numpy(), f.shape)--------------------
[[ 0 1 2 3 4]
[ 5 6 7 8 9]
[10 11 12 13 14]] (3, 5) [1 1 1 1 1] (5,)
--------------------
[[10]
[35]
[60]] (3, 1)在这个例子中,我们把形状为 3×5 的矩阵 A35 与长度为 5 的向量 d5 相乘。由于矩阵 A35 的列数与向量 d5 的长度一致,乘法运算可以执行,结果是一个长度为 3 的向量。
然而,如果尝试按相反顺序执行运算,也就是用向量 d5 乘以矩阵 A35,则会得到错误,因为这种情况下维度并不兼容。
替换变量 f 时,下面的操作将导致错误:
# 下面的操作会因形状不兼容而出错
# f = tf.matmul(d5, A35) # 这一行会引发错误如果希望从左侧用向量乘以矩阵,那么该向量的长度必须等于矩阵的行数:
import tensorflow as tf # 导入 TensorFlow,并将其别名设为 tf
# 初始化一个 3×5 矩阵 A35,其中包含从 0 到 14 的数字
A35 = tf.reshape(tf.range(15), (3, 5))
# 初始化长度为 3、由 1 组成的向量 d3
# 使用 A35 形状中的第一个元素来确定向量长度
d3 = tf.cast(tf.ones((A35.shape[0],)), tf.int32)
# 输出矩阵 A35 及其形状、向量 d3 及其形状
print('--------------------')
print(d3, d3.shape, A35, A35.shape)
# 计算向量 d3 与矩阵 A35 的乘积
f = tf.matmul(tf.reshape(d3, [1, -1]), A35)
# 形状兼容:[1, 3] × [3, 5] -> 长度为 5 的向量
print('--------------------')
print(f, f.shape)--------------------
tf.Tensor([1 1 1], shape=(3,), dtype=int32) (3,) tf.Tensor(
[[ 0 1 2 3 4]
[ 5 6 7 8 9]
[10 11 12 13 14]], shape=(3, 5), dtype=int32) (3, 5)
--------------------
tf.Tensor([[15 18 21 24 27]], shape=(1, 5), dtype=int32) (1, 5)在这种情况下,结果是一个长度为 5 的向量,它是通过对矩阵 A35 每一列的元素求和得到的。
3.1.8 矩阵与矩阵相乘
为了执行两个矩阵的乘法,它们的形状必须兼容。也就是说,第一个矩阵的列数必须等于第二个矩阵的行数。
下面使用 Python 中的 TensorFlow 库给出矩阵乘法示例:
import tensorflow as tf # 导入 TensorFlow,并将其别名设为 tf
# 初始化两个形状兼容的矩阵 A23 和 B35
A23 = tf.ones(shape=(2, 3))
B35 = tf.ones(shape=(3, 5))
print('-' * 20)
# 输出两个矩阵
print(A23.numpy(), B35.numpy())
# 执行矩阵与矩阵相乘
C25 = tf.matmul(A23, B35)
# 形状兼容:[2, 3] × [3, 5] -> 结果形状为 [2, 5]
print('-' * 20)
print(C25.numpy())--------------------
[[1. 1. 1.]
[1. 1. 1.]] [[1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1.]]
--------------------
[[3. 3. 3. 3. 3.]
[3. 3. 3. 3. 3.]]在这段代码中,矩阵 A23 的形状为 2×3,矩阵 B35 的形状为 3×5。相乘后得到形状为 2×5 的矩阵 C25,其中每个元素都是第一矩阵对应行元素与第二矩阵对应列元素乘积之和。在本例中,由于两个矩阵都由 1 填充,因此结果矩阵 C25 中每个元素都等于 3。
同样的操作在 NumPy 中也可以类似完成:
import numpy as np # 导入 NumPy 包,并将其别名设为 np
# 初始化两个形状兼容的矩阵 A23 和 B35
A23 = np.ones(shape=(2, 3))
B35 = np.ones(shape=(3, 5))
print('-' * 20)
# 输出两个矩阵
print(A23, '\n', B35)
print('-' * 20)
# 使用 np.dot() 函数进行矩阵乘法
print('np.dot():\n', np.dot(A23, B35))
print('-' * 20)
# 使用 NumPy 中的 @ 运算符进行矩阵乘法
print('numpy @ operator:\n', A23 @ B35)--------------------
[[1. 1. 1.]
[1. 1. 1.]]
[[1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1.]]
--------------------
np.dot():
[[3. 3. 3. 3. 3.]
[3. 3. 3. 3. 3.]]
--------------------
numpy @ operator:
[[3. 3. 3. 3. 3.]
[3. 3. 3. 3. 3.]]NumPy 中的两种写法都会得到与 TensorFlow 中相同的结果。
3.1.9 范数
在线性代数和数据分析中,范数是一种函数;它为向量空间中的每个向量指定一个非负的长度或大小,而零向量的长度为零。在 Python 编程和机器学习的语境中,范数用于确定向量和矩阵的“大小”。它们有助于判断向量或矩阵中的元素相对于零或相对于彼此究竟有多大。
存在多种不同类型的范数,每一种都有自己的应用场景。
• L2 范数(也称为欧几里得范数):向量 L2 范数等于其元素平方和的平方根。这是最常用的范数类型之一,尤其常用于机器学习,因为它与点之间的欧几里得距离相关。对于矩阵,L2 范数也常与 Frobenius 范数联系起来;Frobenius 范数可以看作矩阵数据上的 L2 范数类比。
• L1 范数:向量 L1 范数等于其元素绝对值之和。该范数对异常值不那么敏感,常用于统计学和优化问题。对于矩阵,L1 范数可以定义为矩阵各列 L1 范数中的最大值。
下面使用 TensorFlow 给出 Python 中 L1 范数和 L2 范数的示例:
import tensorflow as tf # 导入 TensorFlow,并将其别名设为 tf
import numpy as np # 导入 NumPy 包,并将其别名设为 np
# 创建一个由 1 组成的张量(向量)
d = tf.ones(9)
print('-' * 20)
# 输出向量以及其元素之和
print(d.numpy(), tf.reduce_sum(d).numpy())
print('-' * 20)
# 使用 TensorFlow 计算向量的 L2 范数
print('tf.norm(d)', tf.norm(d).numpy())
print('-' * 20)
# 使用 NumPy 计算向量的 L2 范数
print('np.linalg.norm(d)', np.linalg.norm(d.numpy()))
print('-' * 20)
# 使用 TensorFlow 计算向量的 L1 范数
print('tf.norm(d, ord=1)', tf.norm(d, ord=1).numpy())
print('-' * 20)
# 使用 NumPy 计算向量的 L1 范数
print('np.linalg.norm(d, 1)', np.linalg.norm(d.numpy(), 1))--------------------
[1. 1. 1. 1. 1. 1. 1. 1. 1.] 9.0
--------------------
tf.norm(d) 3.0
--------------------
np.linalg.norm(d) 3.0
--------------------
tf.norm(d, ord=1) 9.0
--------------------
np.linalg.norm(d, 1) 9.0范数用于正则化。正则化通过限制模型参数的大小来防止机器学习模型过拟合。例如,L1 正则化可能得到稀疏解,使模型的某些参数变为零,这对特征选择很有用。L2 正则化则倾向于同时减小模型的所有参数,有助于避免过大的权重并降低模型复杂度。
3.1.10 线性代数方程组的求解
在数学和机器学习中,经常会遇到求解线性方程组的问题。这样的方程组可以表示为矩阵方程:
K D = F这里,K 是方阵,D 是需要求解的变量向量,F 是结果向量或已知值向量。
矩阵 K 应当是正定的,也就是说它的所有特征值都大于零。这个条件对于保证解的唯一性是必要的。在有限元方法(FEM)的语境中,刚度矩阵 K 通常是对称且正定的,这可以保证系统方程对应于物理上真实且稳定的模型。
numpy.linalg.solve 函数用于求解这类方程组。它接收两个参数:矩阵 K 和向量 F,并返回满足原始方程的向量 D。
来看一个例子。假设我们有一个方程组,其中矩阵 K 描述系统的某些物理特性,而 F 表示作用在系统上的外力。我们的目标是求出 D。
import numpy as np # 导入 NumPy 包,并将其别名设为 np
# 定义新的矩阵 K
K = np.array([[3, 1], [1, 2]])
# 定义向量 F
F = np.array([9, 3])
# 求解方程组并得到 D
D = np.linalg.solve(K, F)
# 将结果输出到屏幕
print("矩阵 K:\n", K)
print("向量 F:", F)
print("向量 D:", D)刚度矩阵 K:
[[3 1]
[1 2]]
外部载荷向量 F: [9 3]
向量 D: [3. 0.]在这段代码中,numpy.linalg.solve 高效地求解了方程组。如果矩阵 K 不是方阵,或者系统是超定的(即方程数量多于未知量数量),那么可以使用 numpy.linalg.lstsq,通过最小二乘法寻找解,使数据与模型预测之间的平方误差之和最小。这个方法在机器学习的回归任务中经常使用。
需要注意的是,大型方程组的求解可能会消耗大量计算资源。现代数值算法和计算技术的发展使得这类问题可以被高效处理,尤其是在使用迭代方法和基于梯度下降的方法时,这些方法会把解的误差降低到可接受的水平。它们与机器学习中用于优化损失函数的方法有相似之处。
3.1.11 矩阵求逆
矩阵求逆是寻找矩阵 K 的逆矩阵 K^{-1} 的过程。逆矩阵对于给定矩阵 K 是唯一的,并满足条件:
K × K^{-1} = I
K^{-1} × K = I其中,I 是相应大小的单位矩阵。
在前面的章节中,我们讨论了线性代数方程的求解。现在更详细地看一下矩阵求逆。考虑方程 K D = F,其解可以写为:
D = K^{-1} F其中 K^{-1} 是矩阵 K 的逆矩阵。因此,如果能够计算出 K^{-1},求解问题就转化为矩阵与向量的乘法。对于小型系统,这种方法确实有效,并且被广泛使用。为了计算逆矩阵,可以使用 numpy.linalg.inv() 函数:
import numpy as np # 导入 NumPy 包,并将其别名设为 np
from numpy.linalg import inv
# 定义矩阵 K
K = np.array([[3, 1], [1, 2]])
# 计算 K 的逆矩阵
Kinv = inv(K)
print('-' * 20)
print(Kinv)
print('-' * 20)
# 检查 K 与 Kinv 的乘积是否得到单位矩阵
print(np.allclose(np.dot(K, Kinv), np.eye(2)))
print(np.allclose(np.dot(Kinv, K), np.eye(2)))--------------------
[[ 0.4 -0.2]
[-0.2 0.6]]
--------------------
True
True这里使用 np.allclose 来检查 K 与 Kinv 的乘积是否近似等于单位矩阵,这是逆矩阵计算正确的标志。
方程的解 D 可以按如下方式得到:
import numpy as np # 导入 NumPy 包,并将其别名设为 np
from numpy.linalg import inv
# 定义矩阵 K
K = np.array([[3, 1], [1, 2]])
# 定义向量 F
F = np.array([9, 3])
# 计算 K 的逆矩阵
Kinv = inv(K)
# 将结果 D 输出到屏幕
D = np.dot(Kinv, F)
print('D:', D)D: [3. 0.]这个解与前面得到的结果一致。一个有趣的地方是,也可以同时对多个矩阵求逆:
import numpy as np # 导入 NumPy 包,并将其别名设为 np
from numpy.linalg import inv
a = np.array([[[1., 2.], [3., 4.]], [[5, 6], [7, 8]]])
print('-' * 20)
print(a)
print('-' * 20)
print(inv(a))--------------------
[[[1. 2.]
[3. 4.]]
[[5. 6.]
[7. 8.]]]
--------------------
[[[-2. 1. ]
[ 1.5 -0.5]]
[[-4. 3. ]
[ 3.5 -2.5]]]大型矩阵求逆在计算资源方面代价很高,因此通常更倾向于直接求解代数方程组。在机器学习相关计算中,也可能遇到奇异矩阵。奇异矩阵没有逆矩阵,会导致计算失败。很多时候,矩阵可能是近似奇异的,这可能让计算继续进行,但会带来严重误差,并表现为意外或奇怪的行为。当观察到这种现象时,很可能系统矩阵是“病态”的;这时应检查数据或建模过程是否存在错误,因为这些错误可能导致系统矩阵接近奇异。如果问题来自数据本身,就可能需要进行数据清洗或检查数据错误。之后可以使用数学方法处理,其中一种方法是使用奇异值分解(SVD)从数据中提取尽可能好的信息。
这里要强调的核心点是:决定代数方程组能否求得高质量解的最重要因素,是系统矩阵的性质(或特征、条件)。特征值(如果存在)及其对应的特征向量,是矩阵的重要特征。
3.1.12 矩阵的特征值与特征向量分解
分解基础
矩阵的特征值与特征向量分解,是把可对角化矩阵表示为特征向量矩阵、特征值对角矩阵以及特征向量矩阵转置的乘积的过程。这个方法对于实对称矩阵尤其有效,因为此时特征值是实数,特征向量可以是正交归一的。
特征向量矩阵(V):矩阵的特征向量表示这样一些方向:矩阵在这些方向上的作用可以表示为简单的缩放。如果矩阵 A 有特征向量 v,那么当 A 乘以 v 时,结果会等于 v 乘以某个标量。这个标量称为特征值。V 由矩阵 A 的特征向量组成,这些特征向量作为 V 的列。对于对称矩阵,这些向量是正交归一的,也就是说它们长度为 1,并且彼此正交。
特征值对角矩阵(Λ):该分解中的这个组成部分是一个对角矩阵,主对角线上的每个元素都是与矩阵 V 中某个特征向量对应的特征值。特征值反映的是特征向量在乘以矩阵 A 后被放大的倍数。对于对称矩阵,所有特征值都是实数。
特征向量矩阵的转置(V^T):分解的最后一个组成部分是矩阵 V 的转置。矩阵转置意味着把行变为列(或把列变为行)。对于对称矩阵 A 的正交归一矩阵 V,转置等价于求逆。这是因为 V 与 V^T 相乘(或反过来相乘)会得到单位矩阵。
对于正定对称矩阵 A,分解形式如下:
A = V Λ V^T其中:
• V 是特征向量矩阵;
• Λ 是特征值对角矩阵;
• V^T 是矩阵 V 的转置。
由于 V 是正交归一的,因此有:
V^T V = I所以矩阵 V 的逆矩阵等于它的转置:
V^{-1} = V^T逆矩阵的计算
完成矩阵分解之后,逆矩阵的计算就变得简单。根据逆矩阵的定义 A^{-1} A = I,并使用 A = V Λ V^T 与 V^{-1} = V^T,可以得到:
A^{-1} = V Λ^{-1} V^T对角矩阵 Λ 的逆矩阵由其主对角线元素的倒数组成。
下面看一下如何在 Python 中计算特征值和特征向量:
import numpy as np # 导入 NumPy 库
from numpy import linalg as lg # 将 NumPy 中的 linalg 模块导入,并设别名为 lg
A = np.array([[1, 0.4, 0.8], [0.4, 1, 0.5], [0.8, 0.5, 1]]) # 创建 3×3 矩阵
# 调用 linalg 模块中的 eig 函数,计算矩阵 A 的特征值 e 和特征向量 v
e, v = lg.eig(A)
print('-' * 20)
# 输出矩阵 A
print('矩阵 A:\n', A)
print('-' * 20)
# 输出矩阵 A 的特征值
print('特征值:', e)
print('-' * 20)
# 输出矩阵 A 的特征向量
print('特征向量:\n', v)--------------------
矩阵 A:
[[1. 0.4 0.8]
[0.4 1. 0.5]
[0.8 0.5 1. ]]
--------------------
特征值: [2.15226471 0.19102751 0.65670778]
--------------------
特征向量:
[[-0.60615713 -0.66545116 0.43560106]
[-0.48421405 -0.12572504 -0.86586949]
[-0.63095982 0.73577712 0.24601167]]这里得到了三个实特征值和一组正交归一的特征向量。
利用这些特征值和特征向量,可以重构原始矩阵:
import numpy as np # 导入 NumPy 库
from numpy import linalg as lg # 将 NumPy 中的 linalg 模块导入,并设别名为 lg
A = np.array([[1, 0.4, 0.8], [0.4, 1, 0.5], [0.8, 0.5, 1]]) # 创建 3×3 矩阵
# 调用 linalg 模块中的 eig 函数,计算矩阵 A 的特征值 e 和特征向量 v
e, v = lg.eig(A)
# 根据特征值 e 创建对角矩阵 lamd。lamd 的元素位于主对角线上,其他元素为零。
lamd = np.diag(e)
# 使用特征值与特征向量分解重构原始矩阵 A。
# 将特征向量矩阵 v、特征值对角矩阵 lamd 以及特征向量矩阵的转置 v.T 相乘。
# 运算符 @ 表示矩阵乘法。
A_recovered = v @ lamd @ v.T
# 输出重构后的矩阵 A
print('重构后的矩阵 A:\n', A_recovered)重构后的矩阵 A:
[[1. 0.4 0.8]
[0.4 1. 0.5]
[0.8 0.5 1. ]]这说明利用这种方式重构出的矩阵与原始矩阵一致。
该方法也可以用于非对称矩阵,但在这种情况下,特征值可能是复数。
特征值与特征向量分解是一种强大的工具,尤其适用于矩阵的分析与变换。它是矩阵分解的一种形式,对对称矩阵和正定矩阵具有特别重要的意义。
3.1.13 矩阵的条件数
条件数是一种度量,用来评估方程组的结果对输入数据微小变化的“敏感程度”。在矩阵语境中,它表示如果对方程系数(也就是矩阵元素)做出小幅改变,方程组的解会发生多大变化。在实际计算中,经常会出现舍入误差和数据不精确。条件数有助于理解这些小误差会在多大程度上影响最终结果。较小的条件数意味着即便存在误差,结果也会相当准确;较大的条件数则表明方程组或数学问题在数值上不稳定。这意味着输入数据中的微小变化可能导致输出结果发生显著变化。
可以把矩阵想象成一组线性方程。如果有一个线性方程组,就可以用矩阵表示并求解它。在求解过程中,我们期望得到确定的变量值。如果矩阵的条件数较低,那么系数(矩阵元素)的小变化只会导致解的小变化。例如,如果某个矩阵的条件数为 2,那么输入数据改变 1% 时,结果大约会改变 2%。反过来,如果矩阵的条件数很高,那么系数中的微小变化可能导致解发生非常大且难以预测的变化。例如,若矩阵的条件数为 10000,那么输入数据变化 1% 时,结果可能变化 10000%。
下面给出使用 NumPy 的 Python 示例:
import numpy as np # 导入 NumPy 包,并将其别名设为 np
from numpy import linalg as lg
A = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]])
print('-' * 20)
# 调用 linalg 模块中的 eig 函数,计算矩阵 A 的特征值 e 和特征向量 v
e, v = lg.eig(A)
print('特征值:', e)
print('-' * 20)
# 单位矩阵显然有 3 个特征值,均为 1.0
print(A, '\n矩阵 A 的条件数=', lg.cond(A))--------------------
特征值: [1. 1. 1.]
--------------------
[[1 0 0]
[0 1 0]
[0 0 1]]
矩阵 A 的条件数= 1.0由于矩阵 A 是单位矩阵,因此它的条件数为 1,这符合预期。再看另一个例子:
import numpy as np # 导入 NumPy 包,并将其别名设为 np
from numpy import linalg as lg
A = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 0]])
print('-' * 20)
# 调用 linalg 模块中的 eig 函数,计算矩阵 A 的特征值 e 和特征向量 v
e, v = lg.eig(A)
print('特征值:', e)
print('-' * 20)
print(A, '\n矩阵 A 的条件数=', lg.cond(A))--------------------
特征值: [1. 1. 0.]
--------------------
[[1 0 0]
[0 1 0]
[0 0 0]]
矩阵 A 的条件数= inf由于矩阵 A 是奇异矩阵,它的条件数为 inf,在 NumPy 中这表示无穷大,也符合预期。
import numpy as np # 导入 NumPy 包,并将其别名设为 np
from numpy import linalg as lg
A = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 10]])
print('-' * 20)
# 调用 linalg 模块中的 eig 函数,计算矩阵 A 的特征值 e 和特征向量 v
e, v = lg.eig(A)
print('特征值:', e)
print('-' * 20)
# 该矩阵的三个特征值分别为 1.0、1.0 和 10.0
print(A, '\n矩阵 A 的条件数=', lg.cond(A))--------------------
特征值: [ 1. 1. 10.]
--------------------
[[ 1 0 0]
[ 0 1 0]
[ 0 0 10]]
矩阵 A 的条件数= 10.0这个矩阵 A 的条件数为 10.0,对应于 10.0 / 1.0。也就是说,条件数是最大特征值与最小特征值之间的比值。数值 10 表明该矩阵不如单位矩阵稳定,但它并不是奇异矩阵。由此可以得出结论:如果矩阵的最大特征值非常大,或者最小特征值非常小,那么矩阵很可能是特殊的(奇异的),这取决于二者的比值。
这一结论意味着,矩阵归一化(机器学习中经常使用)在理论上不会改变其条件数。但它可以帮助减少有效数字的损失,而有效数字损失通常来自计算机硬件中浮点数表示的限制。
3.1.14 矩阵的秩
矩阵的秩是一种度量,用来确定矩阵中线性无关的行或列的最大数量。线性无关的行或列,是指它们不能表示为其他行或列的线性组合。如果一个方阵具有满秩,就意味着它的所有列(或行)彼此线性无关。这样的矩阵不是奇异矩阵。对于奇异矩阵(即没有逆矩阵的矩阵;在线性代数语境中,这意味着矩阵的行或列不是线性无关的),它的秩一定小于满秩,这称为秩亏。设想一个 3×3 矩阵,其中只有两行或两列是线性无关的。那么该矩阵的秩就是 2。由于 3×3 方阵的满秩应为 3,而矩阵的实际秩为 2,因此秩亏为 3 - 2 = 1。
对于非方阵,满秩等于其列数或行数中的较小者。同样地,如果它的秩小于满秩,也会出现秩亏。
现在用 NumPy 更详细地来看这一点。
import numpy as np # 导入 NumPy 包,并将其别名设为 np
from numpy.linalg import matrix_rank, eig
A = np.eye(4) # 单位矩阵
rank = matrix_rank(A) # 计算秩
print('单位矩阵的秩=', rank)单位矩阵的秩= 4可以看到,单位矩阵的大小为 4 × 4,并且具有满秩。
import numpy as np # 导入 NumPy 包,并将其别名设为 np
from numpy.linalg import matrix_rank, eig
A = np.array([[1, -0.2, 0], [0.1, 1, -0.5], [0.1, 1, -0.5]]) # 奇异矩阵
rank = matrix_rank(A) # 计算秩
print('奇异矩阵的秩=', rank)奇异矩阵的秩= 2这个奇异矩阵有两个线性无关的列,因此秩为 2。它的秩亏为 1。因此,它也应当有一个零特征值,如下所示。如果矩阵的秩亏为 n,那么它就会有 n 个零特征值。这一点很容易用 NumPy 验证。
import numpy as np # 导入 NumPy 包,并将其别名设为 np
from numpy.linalg import matrix_rank
from numpy import linalg as lg
# 奇异矩阵
A = np.array([[1, -0.2, 0], [0.1, 1, -0.5], [0.1, 1, -0.5]])
# 计算秩
rank = matrix_rank(A)
# 计算特征值
e, v = lg.eig(A)
# 输出四舍五入到小数点后 4 位的数值
formatted_eigenvalues = np.around(e, 4)
print('矩阵 A 的特征值:', formatted_eigenvalues)
print('奇异矩阵的秩=', rank)矩阵 A 的特征值: [ 0.9562 0.5438 -0. ]
奇异矩阵的秩= 2非方阵示例:
import numpy as np # 导入 NumPy 包,并将其别名设为 np
from numpy.linalg import matrix_rank
# 非方阵
A = np.array([[1, -0.2, 2], [0.1, 1, -0.5]])
# 计算秩
rank = matrix_rank(A)
print('非方阵的秩=', rank)非方阵的秩= 2这个矩阵只有两行,秩为 2。它具有满秩。
3.1.15 旋转矩阵
在二维坐标系中,旋转矩阵用于改变点或向量的位置。例如,在旋转图像或图形时,这个过程非常重要。旋转矩阵 T 可以表示为:
T = [[cos(θ), -sin(θ)],
[sin(θ), cos(θ)]]这里,θ 表示旋转角。这个角度决定了我们希望对象(例如向量)旋转多少,以及向哪个方向旋转。矩阵由该角度的余弦和正弦组成,并按特定方式排列,使它能够在二维空间中把向量旋转指定角度。
我们希望旋转的向量 d 在坐标系中由两个分量表示:
d = [u, v]现在来看如何使用 NumPy 在 Python 中实现它。
import numpy as np # 导入 NumPy 包,并将其别名设为 np
theta = 45 # 旋转角度,单位为度。这里是 45 度。
# 将角度从度转换为弧度,
# 因为 NumPy 中的 cos 和 sin 函数使用弧度。
theta_rad = np.deg2rad(theta)
# 计算矩阵元素所需的余弦值和正弦值。
cos, sin = np.cos(theta_rad), np.sin(theta_rad)
# 使用余弦和正弦创建旋转矩阵。
T = np.array([[cos, -sin], [sin, cos]])
print('-' * 20)
print('变换矩阵 T:\n', T)
d = np.array([1, 0]) # 沿 X 轴方向的原始向量。
print('-' * 20)
print('原始向量 d:', d)
print('-' * 20)
K = T @ d # 将向量 d 旋转角度 theta。
print('向量 d 旋转角度 theta 后\n', K)
print('-' * 20)
K = T @ (T @ d) # 将向量 d 旋转角度 2*theta。
print('向量 d 旋转角度 2*theta 后\n', K)
print('-' * 20)
T_2 = T @ T # 组合两个旋转矩阵
K = T_2 @ d # 等价写法:T @ (T @ d)
print('向量 d 旋转角度 2*theta 后(第二种方式)\n', K)--------------------
变换矩阵 T:
[[ 0.70710678 -0.70710678]
[ 0.70710678 0.70710678]]
--------------------
原始向量 d: [1 0]
--------------------
向量 d 旋转角度 theta 后
[0.70710678 0.70710678]
--------------------
向量 d 旋转角度 2*theta 后
[0. 1.]
--------------------
向量 d 旋转角度 2*theta 后(第二种方式)
[0. 1.]这段代码展示了连续应用旋转矩阵如何导致旋转角度增加。在这个例子中,两次应用旋转矩阵会把向量旋转到原始角度两倍的位置。
不过,一个有趣的点是:如果角度 θ 等于 45 度,那么旋转 8 × 45 = 360 度(即多次连续应用同一个旋转矩阵)实际上会让向量回到原始位置,因为 360 度旋转不会改变对象的位置。
这些代码示例说明了如何使用旋转矩阵改变二维空间中向量的方向。这个工具可用于机器学习和计算机视觉,在这些领域中,此类变换可用于数据准备与数据增强,也可用于改善图像的感知和分析。
3.2 插值
插值是一种常用的数值技术,它允许我们根据已知数据得到近似值。从某种意义上说,机器学习与插值有相似之处。本节使用 NumPy 库讨论与插值相关的一般问题。插值也常被称为曲线拟合。这里将展示若干函数插值和近似的示例,其中函数值只在空间中的离散点上给定。
第一个示例使用 numpy.interp。
import numpy as np # 导入 NumPy 包,并将其别名设为 np
# 已知函数值的数据点
x_known = [1, 2, 3, 4, 5]
y_known = [2.1, 2.9, 3.8, 5.1, 7.2]
# 我们希望进行插值的新点
x_new = [1.5, 2.5, 3.5]
# 使用 numpy.interp 函数进行插值
y_new = np.interp(x_new, x_known, y_known)
print(y_new)
[2.5 3.35 4.45]在这段代码中,x_known 和 y_known 表示已知数据。在本例中,它们给出一个函数在离散点上的已知取值。x_new 是新的点,我们希望借助插值计算这些点上的函数值。np.interp 函数接收新点 x_new,以及数组 x_known 和 y_known,从而计算插值后的值 y_new。
这个简单示例展示了插值的基本概念:根据若干已知点上的函数值,求出新点上的函数值。它常用于机器学习和数据分析,因为在这些领域中经常需要处理不完整或离散的数据。

3.2.1 分段线性插值
分段线性插值是数学和数值分析中的一种方法,用于根据一组离散的已知点求出新点。在一维情形下,这意味着我们在一条直线上有若干已知点,并希望估计中间点处的函数值。
import numpy as np # 导入 NumPy 包,并将其别名设为 np
# 可用数据
xn = [1, 15, 50] # 数据:给定的 x 坐标
fn = [1, 20, 40] # 数据:在 x 点处给定的函数值
# 在新的点 x 处查询/预测 f
x = 25
f = np.interp(x, xn, fn) # 得到 x 点处的近似值
print(f'f({x:.3f})≈{f:.3f}') # 输出结果
print('-' * 20)
x_2 = [11, 21, 31, 19, 10]
f_2 = np.interp(x_2, xn, fn) # 在更多点上进行查询
print(f_2)f(25.000)≈25.714
--------------------
[14.57142857 23.42857143 29.14285714 22.28571429 13.21428571]在实践中,我们知道插值可能是一项相当有风险的操作,因此需要特别谨慎,尤其是在进行外推时。
外推是把函数估计扩展到已知值范围之外的过程。与插值不同,外推意味着预测给定数据点范围之外的函数值。
为了避免外推,或者至少在外推发生时能够察觉,可以在插值超出数据覆盖区域时设置警告。
import numpy as np # 导入 NumPy 包,并将其别名设为 np
# 可用数据
xn = [1, 15, 50] # 数据:给定的 x 坐标
fn = [1, 20, 40] # 数据:在 x 点处给定的函数值
out_of_domain = -1111111.1 # 使用一个数字作为警告值
print(np.interp(29, xn, fn, right=out_of_domain)) # 正常插值
print(np.interp(60, xn, fn, right=out_of_domain)) # 超出右边界时给出警告值28.0
-1111111.1使用更高阶多项式进行插值可能更准确,但也可能带来更大的问题。在拥有“密集数据”时,分段线性近似通常更安全,也可能非常有效。下面给出一个使用分段线性插值近似正弦函数的示例。

import numpy as np # 导入 NumPy 包,并将其别名设为 np
import matplotlib.pyplot as plt # 用于显示结果的模块
x = np.linspace(0, 4 * np.pi, 40) # 数据:x 的取值
y = np.sin(x) # 数据:x 点处的函数值
xvals = np.linspace(0, 4 * np.pi, 50) # 生成更密集的 x 数据,函数值通过插值得到
yinterp = np.interp(xvals, x, y)
plt.plot(x, y, 'o') # 显示原始数据点
plt.plot(xvals, yinterp, '-x') # 显示插值后的数据点
plt.show() # 显示图形这里,x 是从 0 到 4*pi、被分成 40 个等距部分的数组。y 是这些点上的正弦函数值。随后生成更密集的一组点 xvals,并用 np.interp 在这些新点上插值得到正弦值。插值完成后,结果用 matplotlib 进行可视化。原始数据点以圆点('o')显示,插值点以叉号('x')显示。
在下一个图中,我们将使用少一半的 x 值,也就是把代码中的这一行:
x = np.linspace(0, 4 * np.pi, 40) # 数据:x 的取值改为:
x = np.linspace(0, 4 * np.pi, 20) # 数据:x 的取值这样可以直观地看到插值如何近似原始函数。
3.2.2 用最小二乘法进行近似求解
一维情形下的最小二乘近似求解,是把线性模型拟合到数据的过程。在本例中,我们将在 x-y 平面上用一条直线近似一个数据集。
y = wx + b这里,y 是我们试图预测的值,x 是自变量,w 是直线的梯度(或斜率),b 是偏置(或与 Y 轴的交点)。

我们将使用数据对来确定梯度 w 和偏置 b。在这个示例中,方程可以重写为:y = X · w。
在这个方程中,X 表示数据矩阵,其中包含 [x, 1](其中 1 是用于考虑偏置 b 的常数项)。此时 w 表示参数向量,包含梯度和偏置。现在可以使用 np.linalg.lstsq 来求解 w。
import numpy as np # 导入 NumPy 包,并将其别名设为 np
import matplotlib.pyplot as plt
w_true, b_true = 2.0, 0.0 # 设置梯度和偏置的真实值
x = np.array([0, 1, 2, 3, 4, 5]) # 创建 x 值数组
X = np.vstack([x, np.ones(len(x))]).T # 构造矩阵 X
y = w_true * x + b_true + np.random.rand(len(x)) / 0.1 # 生成 y 值并加入随机噪声
w, b = np.linalg.lstsq(X, y, rcond=None)[0] # 计算参数 w 和 b
plt.plot(x, y, 'o', label='原始数据', markersize=10)
plt.plot(x, w * x + b, 'r', label='近似直线')
plt.legend()
plt.show()得到的模型是最简单的机器学习形式之一,即线性回归。它展示了如何用线性函数近似变量之间的依赖关系。这种方法在机器学习中用于预测数值,或用于理解变量之间的关系。
3.2.3 使用 interp1d 进行一维插值
一维插值是一个简单但强大的工具。下面考察 SciPy 库中的 interp1d 插值方法。
interp1d 函数允许对一维数据进行插值。它可以是线性插值,即通过用直线连接相邻数据点来生成新点;也可以是更复杂的形式,例如三次插值,其中使用三次多项式平滑地连接各个点。
考虑一个示例:


import numpy as np # 导入 NumPy 包,并将其别名设为 np
import matplotlib.pyplot as plt # 导入 Matplotlib 库以可视化数据
from scipy.interpolate import interp1d # 导入 interp1d 插值函数
x0, xL = 0, 20 # 设置 x 范围的起始值 (x0) 和终止值 (xL)
# 在 x0 和 xL 之间生成 11 个均匀分布的点
x = np.linspace(x0, xL, num=11, endpoint=True)
# 计算 y 值:x 的三次方取负后除以 8,再取余弦
y = np.cos(-x**3 / 8.0)
print('数据 x:', x)
print('数据 y:', y)
print('x.shape:', x.shape, 'y.shape:', y.shape) # 输出 x 和 y 数组的维度
f = interp1d(x, y) # 创建线性插值函数
f2 = interp1d(x, y, kind='cubic') # 创建三次插值函数
# 生成用于预测的新点
xnew = np.linspace(x0, xL, num=41, endpoint=True)
# 可视化原始数据和插值结果
plt.plot(x, y, 'o', xnew, f(xnew), '-', xnew, f2(xnew), '-')
plt.legend(['数据', '线性', '三次'], loc='best') # 添加图例
plt.show() # 显示图形数据 x: [ 0. 2. 4. 6. 8. 10. 12. 14. 16. 18. 20.]
数据 y: [ 1. 0.54030231 -0.14550003 -0.29213881 0.39185723 0.78771451
-0.71798508 -0.84383778 -0.99683339 0.98869558 0.56237908]
x.shape: (11,) y.shape: (11,)3.2.4 使用 bisplrep 表示二维样条
二维样条是一种用二维数据集生成光滑曲面的工具。二维坐标通常记为 x 和 y,二维空间中的每个点都对应一个值,通常记为 z。样条用于构造经过这些点、或近似这些点分布的连续光滑函数。
bisplrep 函数(B-spline representation,B 样条表示)用于构建 B 样条,它是一种描述光滑曲线和曲面的数学模型。B 样条在处理数据时兼具灵活性和准确性,并允许通过一组参数控制近似形状。
考虑一个示例:
# 导入必要的库
import numpy as np # 用于处理数组和数学函数
import matplotlib.pyplot as plt # 用于数据可视化
from scipy import interpolate # 用于插值和样条处理
# 创建二维坐标网格
# np.mgrid 在 -2 到 2 的范围内创建二维网格,每个轴上有 20 个点
grid_x, grid_y = np.mgrid[-2:2:20j, -2:2:20j]
# 按高斯函数计算取值
# 对网格中的每个点应用高斯函数
gaussian_z = np.exp(-0.5 * (grid_x**2 + grid_y**2))
# 可视化原始数据
plt.figure() # 创建新的图形窗口
plt.pcolor(grid_x, grid_y, gaussian_z, shading='auto') # 绘制高斯函数原始数据的色彩图
plt.colorbar() # 添加颜色条以理解数值
plt.title('离散点上的高斯函数') # 设置图形标题
plt.show() # 显示图形
# 创建新的、更密集的网格用于插值
fine_grid_x, fine_grid_y = np.mgrid[-2:2:100j, -2:2:100j]
# 使用 bisplrep 函数创建 B 样条
# bisplrep 根据原始数据创建 B 样条参数
spline_tck = interpolate.bisplrep(grid_x, grid_y, gaussian_z, s=0)
# 在新网格上计算样条值
# bisplev 在新网格上计算样条值
interpolated_z = interpolate.bisplev(fine_grid_x[:, 0], fine_grid_y[0, :], spline_tck)
# 可视化插值后的数据
plt.figure() # 创建另一个图形窗口
plt.pcolor(fine_grid_x, fine_grid_y, interpolated_z, shading='auto') # 绘制插值数据的色彩图
plt.colorbar() # 添加颜色条
plt.title('插值后的高斯函数') # 设置图形标题
plt.show() # 显示图形上述代码中使用的高斯函数定义如下:
def gaussian_function(x, y):
return np.exp(-0.5 * (x**2 + y**2))在这个函数中,x 和 y 是用于计算高斯函数值的坐标。高斯函数通常呈钟形或穹顶形,中心位于 (0, 0),并且随着到中心距离的增加而减小。
在这个具体情形中,所使用的公式表示一个没有位移、且两个坐标轴方向标准差相同的二维正态分布,其中心位于 (0, 0)。指数中的 0.5 决定函数“穹顶”的宽度;较大的值会使穹顶更“尖”、更高,而较小的值会使它更“平”、更宽。
可以对代码进行实验,这将帮助你更好地理解材料,并在编写代码时更有信心。
3.2.5 用于平滑和插值的径向基函数
为了理解径向基函数(RBF)的工作原理,我们先分析 RBF 的几个主要方面。
用于近似的基函数。 RBF 用于函数近似,也就是寻找与给定数据集近似匹配的函数。当我们有一组数据点,并希望找到一条经过这些点或靠近这些点的光滑函数时,这一点尤其有用。
距离函数。 RBF 的基础是距离概念。这类函数依赖于到中心点的距离,因此具有径向对称性。这个性质使 RBF 在处理分布不均匀的数据,或复杂的多维空间中的数据时非常灵活。
抵抗过拟合。 与一些其他近似方法相比,RBF 通常不太容易过拟合。这与它们的光滑性以及泛化能力有关。
SciPy 库提供了多种 RBF 类型,每种类型都有自己的特点:
"multiquadric": sqrt((r/self.epsilon)**2 + 1)
这是一种通用函数,可以提供光滑近似。它能够处理不同类型的数据,并且经常被用作默认选项。
"inverse": 1.0/sqrt((r/self.epsilon)**2 + 1)
反多二次函数会生成光滑曲面,非常适合近似光滑函数。
"gaussian": exp(-(r/self.epsilon)**2)
高斯函数提供非常光滑的近似,经常用于统计分析和机器学习。
"linear": r
线性函数很简单,在某些基础近似任务中可能很有用。
"cubic": r**3
三次函数与线性函数或高斯函数相比,提供更陡峭的近似。
"quintic": r**5

五次函数提供更高程度的光滑性。
"thin_plate": r**2 * log(r)
“薄板”函数特别适用于空间数据插值,并经常用于地统计学和计算机图形学。
在许多 RBF(例如多二次函数或高斯函数)中,都使用参数 epsilon 来控制函数的“宽度”。通过改变它的取值,可以控制近似的光滑程度或局部化程度。选择合适的 RBF 类型并调整参数,可以在近似精度和抗过拟合稳定性之间取得最佳平衡。
先看一维示例。
import numpy as np
from scipy.interpolate import Rbf, InterpolatedUnivariateSpline
import matplotlib.pyplot as plt
# 生成数据
np.set_printoptions(formatter={'float': '{: 0.3f}'.format})
# 创建从 -10 到 10 的点数组,共 11 个点
sample_points = np.linspace(-10, 10, 11)
# 将 sample_points 的余弦值作为样本值
sample_values = np.cos(sample_points)
# 创建更细的网格,以显示插值后的数据
interpolation_points = np.linspace(-10, 10, 100)
# 使用样条进行插值
spline_interpolator = InterpolatedUnivariateSpline(sample_points, sample_values)
spline_values = spline_interpolator(interpolation_points)
plt.subplot(2, 1, 1) # 准备同时显示 2 个图
plt.plot(sample_points, sample_values, 'bo') # 用蓝色点显示原始数据
plt.plot(interpolation_points, np.cos(interpolation_points), 'r') # 用红线显示原始余弦函数
plt.plot(interpolation_points, spline_values, 'g') # 用绿线显示样条插值值
plt.title('使用一维样条进行插值')
# 使用 RBF 进行插值
rbf_interpolator = Rbf(sample_points, sample_values) # 创建 RBF 插值器
rbf_values = rbf_interpolator(interpolation_points) # 计算 RBF 插值值
plt.subplot(2, 1, 2) # 准备第二个图
plt.plot(sample_points, sample_values, 'bo') # 用蓝色点显示原始数据
plt.plot(interpolation_points, np.cos(interpolation_points), 'r') # 用红线显示原始余弦函数
plt.plot(interpolation_points, rbf_values, 'g') # 用绿线显示 RBF 插值值
plt.title('使用 RBF 进行插值')
plt.legend(['原始数据', '原始函数', '插值值'],
loc='upper center', bbox_to_anchor=(0.5, -0.1),
fancybox=True, shadow=True, ncol=4)
plt.show()
现在考虑二维示例:
import numpy as np
from scipy.interpolate import Rbf
import matplotlib.pyplot as plt
from matplotlib import cm
# 生成 2D 测试数据
num_points = 10 # 点的数量
x = np.random.rand(num_points) * 6 - 3 # [-3, 3] 范围内的随机 x
y = np.random.rand(num_points) * 6 - 3 # [-3, 3] 范围内的随机 y
z = np.sin(np.sqrt(x**2 + y**2)) # z 是到坐标原点距离的正弦
# 创建插值网格
grid_size = 100
xi = np.linspace(-3, 3, grid_size)
yi = np.linspace(-3, 3, grid_size)
XI, YI = np.meshgrid(xi, yi) # 创建网格
# 使用 RBF 进行插值
rbf_interpolator = Rbf(x, y, z, function='multiquadric', epsilon=0.5) # 创建 RBF 插值器
ZI = rbf_interpolator(XI, YI) # 插值
# 可视化结果
plt.figure(figsize=(8, 6))
plt.pcolor(XI, YI, ZI, cmap=cm.viridis, shading='auto') # 显示插值数据
plt.scatter(x, y, 30, z, cmap=cm.viridis, edgecolor='k') # 显示原始数据点
plt.title('2D RBF 插值 - multiquadric')
plt.xlim(-3, 3)
plt.ylim(-3, 3)
plt.colorbar()
plt.show()再看一个三维数据的示例:
import numpy as np
from scipy.interpolate import Rbf
import matplotlib.pyplot as plt
# 生成随机 3D 数据
x, y, z = np.random.rand(3, 30) # 每个维度中有 30 个 [0, 1] 范围内的随机点
d = np.sin(x) + np.cos(y) + np.tan(z) # 根据 x、y 和 z 的函数计算 d 值
# 创建 RBF 插值器
rbf_interpolator = Rbf(x, y, z, d, function='multiquadric')
# 创建插值网格
xi, yi, zi = np.meshgrid(
np.linspace(0, 1, 10),
np.linspace(0, 1, 10),
np.linspace(0, 1, 10)
)
# 插值
di = rbf_interpolator(xi, yi, zi)
# 可视化原始数据和插值数据
fig = plt.figure(figsize=(12, 6))
# 可视化原始数据
ax1 = fig.add_subplot(121, projection='3d')
ax1.scatter(x, y, z, c=d, cmap='viridis')
ax1.set_title('原始数据')
ax1.set_xlabel('X')
ax1.set_ylabel('Y')
ax1.set_zlabel('Z')
# 可视化插值数据
ax2 = fig.add_subplot(122, projection='3d')
ax2.scatter(xi.flatten(), yi.flatten(), zi.flatten(), c=di.flatten(), cmap='viridis')
ax2.set_title('插值数据')
ax2.set_xlabel('X')
ax2.set_ylabel('Y')
ax2.set_zlabel('Z')
plt.show()
3.3 单项式分解
单项式分解常用于近似表示变量之间的复杂依赖关系。单项式分解的方法可以从经典多项式扩展到更复杂的基函数。本节涵盖分解的理论方面,例如基函数的选择和系数优化,也讨论这种方法在机器学习实践任务中的应用,例如回归以及复杂函数的近似。
3.3.1 SVD 的表述
SVD(奇异值分解)是一种矩阵分解方法,广泛应用于信号处理、统计学和机器学习等领域。SVD 的核心思想是把一个矩阵分解为三个矩阵;这种方法既适用于方阵,也适用于矩形矩阵。
对于大小为 m × p 的矩阵 A,SVD 分解可表示为:
A = UΣV*其中:
U 是大小为 m × m 的酉矩阵。
Σ(sigma)是大小为 m × p 的矩形对角矩阵,其对角线上为非负数。这些数称为矩阵 A 的奇异值。
V* 是大小为 p × p 的酉矩阵的共轭转置。
酉矩阵 U 和 V 具有保持长度和角度的性质,因此非常适合描述空间中的旋转和反射。在 SVD 的语境中,这些矩阵为矩阵 A 的输入空间和输出空间提供基。
Σ 中的奇异值指出矩阵 A 在哪些方向上对空间的“拉伸”最强。例如,如果某个奇异值远大于其他奇异值,这说明矩阵的大部分“能量”或“信息”集中在对应的奇异向量方向上。
左奇异向量(U 的列)表示原空间中的方向,这些方向经矩阵 A 变换后会对应到右奇异向量(V 的列)。这些向量在各自对应的空间中形成正交归一基。
SVD 是数值分析中的强大工具,它使我们能够把矩阵“拆解”为若干组成部分,从而理解矩阵不同部分之间如何相互作用。
3.3.2 SVD 的算法
计算 SVD 的常用方法之一基于 QR 分解。QR 分解是把矩阵分解为一个正交矩阵 Q 和一个上三角矩阵 R 的过程。这种方法适合处理方阵和非方阵。
另一种计算 SVD 的方法基于特征值分解。该方法在理论上较简单,但由于数值稳定性问题,在大矩阵的实际计算中很少使用。不过,对于本书中的理论分析和公式推导而言,这不是主要问题,因此我们将考虑这种方法。为了使用这种简单方法,需要对矩阵 A 施加若干条件。
假设有一个由实数组成的矩阵 A,其大小为 m × p,满足 m > p,并且秩等于 p。
构造正规矩阵 B:该矩阵是对称的方阵,大小为 p × p。
B = A^T A随后对矩阵 B 做特征值分解,得到:
B = V_e Λ V_e^T其中:
V_e 是由特征向量组成的正交归一矩阵;
Λ 是由特征值组成的对角矩阵。
已知 A 具有 SVD 分解,即:
A = UΣV^T由于矩阵 A 的秩为 p,所以 Σ 中的奇异值都是正实数。由公式 A = UΣV^T 可得:
A^T A = VΣ^T U^T UΣV^T因为 U^T U = I,且 Σ 为对角矩阵,所以与前面的公式 B = V_e Λ V_e^T 比较可得:
V = V_e并且:
Σ = sqrt(Λ)现在使用 A = UΣV^T 以及正交归一性质 V^T V = I,可得:
AV = UΣ由于 Σ 的对角元素均为正数,最终得到:
U = AVΣ^{-1}U 为酉矩阵这一结论来自于:A 被表示为奇异值分解,其中 U 和 V 是酉矩阵。最后,如果 A 未充分确定,即 rank(A) < p,则矩阵 B 会有零特征值。在这种情况下,我们只需丢弃所有零特征值及其对应的特征向量。这样仍然可以得到截断形式的 SVD,并且上述过程依然有效。
这种方法的主要问题在于数值稳定性,尤其是在处理大矩阵时。问题来自构造矩阵 B = A^T A 的过程中,条件数会被平方。
虽然这种方法有助于从理论上理解 SVD,并证明任意矩阵都具有 SVD,但它在实际计算中很少用于求解 SVD。在实践中,更常用 QR 分解等其他方法,因为这些方法可以避免构造矩阵 B 所带来的问题。
3.3.3 示例
下面来看一个使用 Python 和 NumPy 库进行 SVD(Singular Value Decomposition,奇异值分解)的示例。
import numpy as np # 导入 NumPy 库
# 创建一个大小为 4x5、元素为随机整数的矩阵 my_matrix
my_matrix = np.random.randint(0, 100, size=(4, 5))
print('-' * 20)
print("原始矩阵:\n", my_matrix)
print('-' * 20)
# 对矩阵 my_matrix 执行奇异值分解(SVD)
# U:左奇异向量
# sigma:奇异值
# Vt:右奇异向量(转置后)
U, sigma, Vt = np.linalg.svd(my_matrix, full_matrices=False)
# 输出矩阵 U、sigma 和 Vt 的形状以进行检查
print("\n形状:U, sigma, Vt:", U.shape, sigma.shape, Vt.shape)
print('-' * 20)
# 输出矩阵 U、sigma 和 Vt
print("\n左奇异向量 U:\n", U)
print("\n奇异值 Sigma:", sigma)
print("\n右奇异向量 Vt:\n", Vt)
print('-' * 20)
# 将奇异值向量 sigma 转换为对角矩阵 sigma_mat
sigma_mat = np.diag(sigma)
print("\n对角矩阵 sigma:\n", sigma_mat)
print('-' * 20)
# 检查是否可以恢复原始矩阵
reconstructed_matrix = np.dot(U, np.dot(sigma_mat, Vt))
print("\n是否恢复了原始矩阵?", np.allclose(my_matrix, reconstructed_matrix))
print('-' * 20)
# 输出原始矩阵和恢复矩阵进行比较
print("\n原始矩阵:\n", my_matrix)
print("\n恢复矩阵:\n", reconstructed_matrix)--------------------
原始矩阵:
[[58 83 22 94 36]
[22 54 45 82 91]
[10 34 62 75 24]
[55 26 46 52 68]]
--------------------
形状:U, sigma, Vt: (4, 4) (4,) (4, 5)
--------------------
左奇异向量 U:
[[ 0.56147956 0.79253797 0.237659 -0.01193577]
[ 0.57205124 -0.43106941 0.05107003 -0.69593706]
[ 0.40034304 -0.01747367 -0.87370638 0.27578483]
[ 0.44409856 -0.43099582 0.42136351 0.66292675]]
奇异值 Sigma: [242.50494188 59.13289951 48.44113155 33.06221611]
右奇异向量 Vt:
[[ 0.30341564 0.42329775 0.34368231 0.63011555 0.46216321]
[ 0.21314985 0.51921871 -0.38678068 0.26091348 -0.68359633]
[ 0.60580112 0.07706081 -0.56275368 -0.35278672 0.43118031]
[ 0.70218915 -0.36169658 0.4843455 -0.09172851 -0.36483051]]
--------------------
对角矩阵 sigma:
[[242.50494188 0. 0. 0. ]
[ 0. 59.13289951 0. 0. ]
[ 0. 0. 48.44113155 0. ]
[ 0. 0. 0. 33.06221611]]
--------------------
是否恢复了原始矩阵? True
--------------------
原始矩阵:
[[58 83 22 94 36]
[22 54 45 82 91]
[10 34 62 75 24]
[55 26 46 52 68]]
恢复矩阵:
[[58. 83. 22. 94. 36.]
[22. 54. 45. 82. 91.]
[10. 34. 62. 75. 24.]
[55. 26. 46. 52. 68.]]这个代码示例及其中的注释有助于更好地理解 SVD 的工作方式,以及如何把它应用于实际任务。

3.3.4 用 SVD 进行数据压缩
使用 SVD 压缩图像:在这个示例中,我们将使用从 PIL(Python Imaging Library)库加载的图像,并对其执行 SVD 以进行数据压缩。
import numpy as np
from PIL import Image
import matplotlib.pyplot as plt
# 加载图像
# 将 'path_to_image.jpg' 替换为你的图像路径
original_image = Image.open('path_to_image.jpg')
# 将图像转换为黑白格式,以简化处理
gray_image = original_image.convert('L')
# 将 PIL 图像转换为 NumPy 数组,以便进一步处理
gray_array = np.array(gray_image)
print('从图像得到的数据数组\n', gray_array)
print('-' * 20)
print("原始图像数组的形状:", gray_array.shape)
# 创建用于显示图像的窗口
plt.figure(figsize=(12, 6))
# 显示原始图像
plt.subplot(1, 2, 1) # 1 行,2 列,位置 1
plt.imshow(gray_array, cmap='gray')
plt.title("原始图像")
# 对图像矩阵执行 SVD
U, S, Vt = np.linalg.svd(gray_array, full_matrices=False)
print('-' * 20)
# 输出原始图像的奇异值数量
print("原始图像中的奇异值数量:", len(S))
# 确定要使用的奇异值数量
k = 112 # 可以尝试不同的值,以获得不同的压缩程度
print('-' * 20)
print("使用的奇异值数量:", k)
# 构建近似图像
# 使用 k 个奇异值通过矩阵乘法重建图像
approximated_image = np.dot(U[:, :k], np.dot(np.diag(S[:k]), Vt[:k, :]))
# 显示重建图像
plt.subplot(1, 2, 2) # 1 行,2 列,位置 2
plt.imshow(approximated_image, cmap='gray')
plt.title(f"使用 {k} 个奇异值的压缩图像")
plt.show()从图像得到的数据数组
[[40 40 40 ... 74 74 73]
[40 40 40 ... 74 74 74]
[41 41 41 ... 74 74 75]
...
[53 53 53 ... 61 61 61]
[53 53 53 ... 61 61 61]
[53 53 53 ... 61 61 61]]
--------------------
原始图像数组的形状: (1125, 2000)
--------------------
原始图像中的奇异值数量: 1125
--------------------
使用的奇异值数量: 112变量 k 指定用于近似重建图像的奇异值数量。它是影响图像质量和压缩程度的关键参数。表达式:
np.dot(U[:, :k], np.dot(np.diag(S[:k]), Vt[:k, :]))通过只使用 k 个奇异值来相乘矩阵 U、S 和 Vt,从而重建图像。这样得到的是原始图像的压缩版本。
这个示例展示了如何使用 SVD 进行图像压缩,从而减少存储和传输图像所需的数据量。需要注意的是,k 的取值,即使用的奇异值数量,是图像质量与压缩程度之间的折中。过小的 k 可能导致细节大量丢失,而过大的 k 则可能只带来很小的压缩效果。通过尝试不同的 k 值,可以为具体任务找到合适的平衡。
3.4 主成分分析
主成分分析(Principal Component Analysis,PCA)是一种统计方法,在机器学习中常用于降低数据维度,同时尽可能保留原始数据中的信息。PCA 最早由卡尔·皮尔逊于 1901 年提出,它可以把一个包含大量变量、且这些变量之间常常彼此相关的数据集,转换为较少数量的互不相关变量;这些新变量称为主成分。
PCA 从由 p 个变量(特征)组成的原始数据集开始。其目标是对数据进行变换,使新的变量(主成分)在线性意义下互不相关。这些主成分按解释总变异程度的大小排序:第一主成分解释数据中最大的一部分总体变异,第二主成分解释次大的一部分,依此类推。
PCA 使用的变换是正交变换。这意味着在多维特征空间中,各主成分相互正交,也就是彼此不相关。正交变换保证每个主成分都表示数据的一个独特方面,而不会重复携带同一部分信息。
主成分按照对数据变异性的解释程度进行排序。第一主成分具有最高变异性,第二主成分较低,依此类推。这种排序使我们能够决定需要保留多少个主成分,才能在降低原始特征空间维度的同时,仍然充分表示数据。
PCA 在机器学习中的一个主要用途是数据降维。当处理具有大量特征的数据集时,这一点尤其有用,因为降维可以加快模型训练,并降低过拟合风险。
使用 PCA 时需要记住:PCA 对变量的尺度很敏感。不同特征尺度的差异可能显著影响分析结果,因此在应用 PCA 前通常建议对数据进行标准化。此外,PCA 基于变量之间的线性关系,因此对于发现数据中的非线性结构可能并不有效。
3.4.1 PCA 的表述
考虑一个由实数组成的通用矩阵 A,其大小为 m × p,其中 m > p。首先构造矩阵:
B = AᵀA这会得到一个大小为 p × p 的对称方阵。该矩阵至少是半正定的,且在许多情况下是正定矩阵(SPD)。随后对矩阵 B 进行特征值分解:
B = VΣVᵀ其中:
• V 是大小为 p × p 的正交归一矩阵,由矩阵 B 的 p 个特征向量组成;
• Σ 是大小为 p × p 的对角矩阵,其中包含非负实数特征值。
于是 PCA 可定义为:
C = AV也就是说,我们把原始数据 A 投影到一组正交归一的特征向量上。所得矩阵 C 保持与原始矩阵 A 相同的行数,并在使用全部特征向量时保持 m × p 的形式。
使用全部特征向量时,可以恢复原始矩阵 A:
A = CVᵀ = AVVᵀ这是因为特征向量是正交归一的。然而,在实际中常常只使用按特征值排序后的前若干个特征向量,因为它们包含了原始矩阵 A 中的大部分信息。使用 k 个特征向量进行部分恢复的例子如下:
A_k = C[0:m, 0:k] V[0:p, 0:k]ᵀ
= A[0:m, 0:p] V[0:p, 0:k] V[0:p, 0:k]ᵀ ≈ A一般而言,这不等于原始的 A,但通常可以非常接近它。在这种情况下,需要存储的数据量为 m × k + k × p,这可能远小于原始大小 m × p。
如果矩阵 A 的维度满足 m < p,则可以使用其转置并采用类似过程。在这种情况下构造矩阵:
B = AAᵀ这会产生一个大小为 m × m 的对称方阵。对该矩阵进行特征值分解后得到:
B = VΣVᵀ其中 V 和 Σ 的含义与前一种情况相似,但维度为 m × m。
此时也可以定义 PCA 投影,并用全部特征向量恢复原始数据。使用较少数量的特征向量时,也可以得到对矩阵 A 的近似恢复。例如,如果使用 k ≤ m 个特征向量,则保留的部分可以写成:
[0:m, 0:k] [0:k, 0:p]对于大型系统,通常不会直接构造矩阵 B 并进行特征值分解,而是使用更稳定的算法,例如前文提到过的 QR 变换。
3.4.2 示例
示例 1

我们来看一个 PCA 示例,用于分析身高和体重测量数据集。这个示例有助于说明 PCA 如何用于发现数据中主要的变化方向。
假设我们有一个数据集,包含一组人的身高(厘米)和体重(千克)测量值。我们的目标是使用 PCA 找出这两个变量中的主要变化方向。
import numpy as np
import matplotlib.pyplot as plt
# 原始数据
height = np.array([170, 168, 177, 181, 172, 171, 169, 175, 174, 178])
weight = np.array([70, 66, 78, 85, 72, 68, 65, 74, 71, 80])
# 创建数据矩阵
A = np.vstack((height, weight)).T
PCA 从数据中心化开始,即从每个特征值中减去该特征的平均值。
# 数据中心化
mean_height = np.mean(height)
mean_weight = np.mean(weight)
A_centered = A - np.array([mean_height, mean_weight])下一步是计算协方差矩阵,它表示变量如何共同变化。
# 计算协方差矩阵
cov_matrix = np.cov(A_centered.T)现在可以通过求协方差矩阵的特征值和特征向量来执行 PCA。
# 执行 PCA
eigenvalues, eigenvectors = np.linalg.eig(cov_matrix)
# 对特征值和特征向量排序
index = eigenvalues.argsort()[::-1]
eigenvalues = eigenvalues[index]
eigenvectors = eigenvectors[:, index]特征值表示每个主成分解释了多少变异。特征向量表示主成分在原始特征空间中的方向。
# 第一主成分
first_component = eigenvectors[:, 0]
# 可视化
plt.scatter(A_centered[:, 0], A_centered[:, 1])
plt.quiver(
0, 0,
first_component[0], first_component[1],
scale=5,
color='r'
)
plt.xlabel('身高')
plt.ylabel('体重')
plt.title('身高和体重的 PCA')
plt.axis('equal')
plt.show()在这个示例中,第一主成分(用红色箭头表示)显示了数据中最大变化的方向。如果身高和体重高度相关,那么该主成分会指向这两个量一起增加的方向。例如,它可能表示随着身高增加体重也增加的一般趋势。
确定主成分后,我们可以把原始数据投影到这些主成分上,用于降维或进一步分析。

# 将数据投影到主成分上
projected_data = np.dot(A_centered, eigenvectors)
# 可视化投影
plt.scatter(projected_data[:, 0], projected_data[:, 1])
plt.xlabel('第一主成分')
plt.ylabel('第二主成分')
plt.title('身高和体重数据在主成分上的投影')
plt.axis('equal')
plt.show()在这个图中,数据点现在表示在由主成分定义的坐标系中。第一轴(x 轴)对应第一主成分,并捕捉数据中最大的变异;第二轴(y 轴)对应第二主成分,并捕捉剩余的变异。

这个示例说明了 PCA 如何用于发现数据中的主要变化方向。当变量很多、并且需要找出最重要的变化方向以简化分析或为机器学习准备数据时,PCA 尤其有用。
示例 2
下面编写一个截断 PCA(Principal Component Analysis)代码,可用于图像压缩。在这个示例中,我们加载图像,将其转换为灰度图,应用 PCA,然后用有限数量的主成分重建图像。
import numpy as np
import matplotlib.pyplot as plt
from PIL import Image
# 定义执行 PCA 的函数
def princomp(A, numpc=0):
# 减去每个特征的平均值以进行数据中心化
A = A - np.mean(A, axis=0)
# 计算数据协方差矩阵
cov_matrix = np.dot(A.T, A) / (A.shape[0] - 1)
# 求协方差矩阵的特征值和特征向量
eigenvalues, eigenvectors = np.linalg.eig(cov_matrix)
# 将特征值和特征向量转换为实数
eigenvalues = np.real(eigenvalues)
eigenvectors = np.real(eigenvectors)
# 按降序对特征值及其对应特征向量排序
idx = np.argsort(eigenvalues)[::-1]
eigenvalues = eigenvalues[idx]
eigenvectors = eigenvectors[:, idx]
# 将主成分数量截断到指定的 numpc
if numpc < A.shape[1] and numpc >= 0:
eigenvectors = eigenvectors[:, :numpc]
# 将数据投影到主成分上以得到新的特征
score = np.dot(A, eigenvectors)
return eigenvectors, score, eigenvalues
# 从指定路径加载图像
image = Image.open('path_to_image.jpg') # 请替换为图像路径
# 将图像转换为灰度图
image = image.convert('L')
# 将图像转换为二维 NumPy 数组
image_array = np.array(image)
# 对图像应用 PCA,并将主成分数量限制为 50
eigenvectors, score, eigenvalues = princomp(image_array, numpc=50)
# 从压缩表示中恢复图像
reconstructed_image = np.dot(score, eigenvectors.T) + np.mean(image_array, axis=0)
# 对恢复图像只使用实数部分
reconstructed_image = np.real(reconstructed_image)
# 创建新图形以显示图像
plt.figure(figsize=(8, 4))
# 显示原始图像
plt.subplot(1, 2, 1)
plt.imshow(image_array, cmap='gray')
plt.title('原始图像')
plt.axis('off')
# 显示灰度恢复图像
plt.subplot(1, 2, 2)
plt.imshow(reconstructed_image, cmap='gray')
plt.title('恢复图像')
plt.axis('off')
plt.show()在这段代码中,我们首先定义函数 princomp,它对输入矩阵 A 执行 PCA。然后加载图像,将其转换为灰度图,并对其应用 PCA。PCA 的结果包括特征向量(eigenvectors)、数据在主成分上的投影(score)和特征值(eigenvalues)。最后,我们从压缩表示中重建图像,并可视化原始图像与恢复后的图像。
可以看到,使用 50 个主成分时,相比原始图像,恢复图像仍能保持相当好的质量。但如果只取 5 个主成分,则虽然仍可得到可辨认的图像,但质量会更差。
作为学习主成分分析(PCA)的一个重要环节,强烈建议读者自行对代码进行实验。这样不仅可以加深对该方法的理论理解,还能培养实际应用技能。通过尝试不同代码变体并把它们改用于不同任务,可以获得宝贵经验,从而形成更深入、更直观的理解,也能更好地应对真实数据分析场景。代码实验还为结果的可视化和解释提供了机会。
3.5 数值求根
在机器学习领域,尤其是在优化算法中,经常需要求解非线性方程的根。Python 中的 scipy.optimize 模块提供了 fsolve() 函数,用于寻找定义为 f(x) = 0 的一组非线性方程的根。当已知根的大致位置时,该函数尤其有用。
fsolve() 函数是 MINPACK 中相关算法的封装,这些算法使用牛顿迭代法。该方法首先选择一个根的初始近似值,然后通过对函数进行局部线性化来逐步修正该近似值。
牛顿法的应用可以通过一个示例直观地展示。考虑一个单变量函数:
fromscipy.optimizeimportfsolvedefstress_strain_curve(strain):
# 示例函数
returnstrain**3-0.2*strain**2+0.1*strain-0.05
# 寻找函数的根
strain_root=fsolve(stress_strain_curve, 0.1)
print('Кореньуравнения:', strain_root)方程的根:
[0.33940705]类似地,fsolve() 也可以用于多变量方程组,这在多维问题中经常出现:
fromscipy.optimizeimportfsolvedefmarket_equilibrium(x):
# 两个函数,表示市场上的需求与供给
demand=10-x[0] +0.5*x[1]
supply=x[0] -5-0.25*x[1]
return [demand, supply]
# 价格和数量的初始猜测
initial_guess= [2, 2]
price_quantity=fsolve(market_equilibrium, initial_guess)
print('Равновесие на рынке:', price_quantity)市场均衡:
[-4.03896783e-27 -2.00000000e+01]需要强调的是,在机器学习中处理多项式或代数方程时,即使方程系数都是实数,也可能出现复数根。这是因为复数空间在几何上是封闭的,而实数空间并非如此。因此,n 次多项式应当有 n 个根,这些根既可能位于实数空间,也可能位于复数空间。
3.6 机器学习语境中的数值积分
数值积分研究的是对函数定积分进行估计的问题。其基本思想是:用函数在若干指定点处的取值乘以小区间长度,再把这些乘积相加,以此近似积分值。
梯形法。 这是数值积分中使用的基本方法之一。该方法把函数图像下方的区域划分为许多小梯形,然后计算这些梯形面积之和。对于变化不太剧烈的函数,这是一个简单而有效的方法。
辛普森法。 该方法基于在小区间上用抛物线来近似函数,并计算这些抛物线下方的面积。与梯形法相比,辛普森法通常能提供更高精度,尤其适合行为变化更复杂的函数。

NumPy 库提供了实现这些方法的便捷工具。例如,可以使用 numpy.trapz() 函数应用梯形法。
使用示例:
# 需要积分的函数
deffunction(x):
returnnp.sin(x)
# 生成点和函数值
x=np.linspace(0, np.pi, 100)
y=function(x)
# 应用梯形法
integral=np.trapz(y, x)
print("Значение интеграла:", integral)积分值:
1.99983216389399273.6.1 梯形法则
更详细地考察梯形法则:它是一种数值积分方法,用于近似计算函数的定积分。梯形法则的公式如下。
为了在实践中演示梯形法则,我们定义一个简单的多项式函数,并在有限区间 [a, b] 上选取其若干个均匀分布点处的函数值。
下面给出一个 Python 代码示例,演示梯形法则的使用:
# 导入必要的库
importnumpyasnp # 导入 NumPy 库用于数值计算
importmatplotlib.pyplotasplt # 导入 Matplotlib 库
fromscipy.integrateimportquad # 从 SciPy 导入 quad 函数
# 设置输出格式
# 设置 NumPy 中浮点数的输出格式
np.set_printoptions(formatter={'float': '{: 0.3f}'.format})
# 定义多项式函数
deff(x): # 定义函数 f(x)
return5*x**3-6*x**2-7*x+6
# 对函数值进行采样
# 设置起点 (a)、终点 (b) 和点数 (n)
a, b, n=-1., 2, 400
# 在 a 和 b 之间创建由 n 个均匀分布点组成的数组
x=np.linspace(a, b, n)
y=f(x) # 计算数组 x 中每个点处的函数 f(x) 值
# 对函数进行积分
ns=6 # 设置梯形法使用的点数
# 创建用于积分的 ns 个均匀分布点
xint=np.linspace(a, b, ns)
yint=f(xint) # 计算 xint 中每个点处的函数值
# 可视化结果
plt.plot(x, y, lw=2) # 绘制函数 f(x) 的图像
# 填充 xint 与 yint 所形成的图像下方区域
plt.fill_between(xint, 0, yint, facecolor='gray', alpha=0.4)
# 在图上添加积分文本
plt.text((a+b)/2, 12, r"$\int_a^b f(x)dx$", horizontalalignment='center', fontsize=15)
plt.show() # 显示图形
# 计算积分
# 计算积分的精确值和误差
integral, error=quad(f, a, b)
# 用梯形法计算积分的近似值
integral_trapezoid=sum((xint[1:]-xint[:-1])*(yint[1:]+yint[:-1]))/2
# 输出结果
# 输出积分的精确值及其误差
print("Точный результат:", integral, "+/-", error)
# 输出使用梯形法并基于给定点数得到的近似结果
print("Результат приближения трапецией с", len(xint), "точками:", integral_trapezoid)精确结果:
8.25 +/- 1.2419279516758014e-13使用 6 个点进行梯形近似的结果:
8.520000000000001从这个例子可以看出,梯形法则得到的是近似结果。当采样点数量较少时,计算误差可能较大。梯形法则结果的精度直接取决于所使用的区间数量。区间越多,近似越准确,但区间数量的增加也会提高计算复杂度。
梯形法则是一种简单而有效的数值积分方法。该方法能够得到积分的近似值,而在机器学习的实际任务中,这种近似通常已经足够。不过,始终需要注意潜在的近似误差,并在计算精度与计算复杂度之间取得平衡。
3.6.2 高斯积分
高斯积分,也称为高斯求积法,是一种高效的数值积分技术。该方法基于在专门选择的点,即高斯点,处计算函数值,并使用相应权重对这些函数值求和。该方法能提供较高计算精度,尤其适合处理多项式形式的被积函数,因为高斯点对应于区间 [-1, 1] 上勒让德多项式的根。
高斯积分可用于贝叶斯统计方法中的积分计算,也可用于优化问题的求解。
考虑一个示例,在该示例中,我们使用高斯积分计算某个函数的积分。这里的函数可以表示,例如,分类器的错误概率。
importnumpyasnp # 导入 NumPy 库
fromscipy.integrateimportquad # 从 SciPy 导入 quad 函数
deferror_function(x):
""" 分类器错误函数的表示 """
# 返回 e 的 (-x^2) 次幂,即指数函数
returnnp.exp(-x**2)
# 设置积分区间
# a 和 b 分别确定积分区间的下界与上界
a, b=0, 1
# 在给定区间上对函数进行积分
# quad 函数用于数值积分
integral, error=quad(error_function, a, b)
# error_function 从 a 到 b 的积分
# 返回积分值和估计误差
# 输出积分值
print(f"Интеграл функции ошибки классификатора на интервале [{a}, {b}]:", integral)
# 输出积分计算误差估计
print("Оценочная ошибка вычислений:", error)区间 [0, 1] 上分类器错误函数的积分:
0.7468241328124271计算误差估计:
8.291413475940725e-15对于一般的复杂被积函数,高斯积分未必给出精确解。不过,与梯形法则或矩形法则相比,它的精度通常仍然高得多。换句话说,在获得相近精度的解时,高斯积分所需的采样点数量更少。
这种积分方法可用于评估分类算法中的错误概率,也可用于对贝叶斯方法中出现的函数进行积分。高斯积分的高效性能够降低计算成本,在处理大规模数据或复杂模型时尤其有价值。
3.7 数据的初步处理
数据预处理是为分析和机器学习模型训练做准备的关键阶段。本节将介绍在模型训练开始之前对数据使用的主要方法和技术。
数据通常表示为数据集 X。这里,m 表示数据点或观测值的数量,p 表示特征或变量的数量。在实际问题中,数据值可能差异很大,这会在模型训练过程中导致数值稳定性问题。
为了保证稳定性并提升机器学习模型的性能,通常需要对数据进行预处理,包括归一化或缩放。数据归一化主要有两种方法:最小—最大缩放和标准缩放。
3.7.1 最小—最大缩放
最小—最大缩放是机器学习数据预处理中的一个步骤。该方法可以标准化特征的取值范围,对于对特征尺度敏感的算法尤其重要,例如梯度下降。
缩放公式如下:
其中,min 和 max 分别表示特征的最小值和最大值。该方法会把每个特征的所有取值转换到 0 到 1 的范围内。该方法也可以推广到把取值转换到任意区间 [a, b]:
这使得缩放可以根据具体需要进行调整。
一旦对训练数据集执行了这种缩放变换,就可以使用相同的 min 和 max 对测试数据集执行完全相同的变换,以保证预测时的一致性和正确性。
下面是一个执行最小—最大缩放的简单代码。
importnumpyasnp
# 设置输出参数以提高可读性
np.set_printoptions(precision=4)
# 用于示例的修改后数据集
data_set= [
[4, -1, 2],
[9, 1, 5],
[14, 2, 8],
[19, 4, 11]
]
print('-'*20)
print(f"Исходный обучающий набор данных:\n{data_set}")
print('-'*20)
# 将列表转换为 NumPy 数组,便于计算
data_array=np.array(data_set)
# 执行最小—最大缩放
data_min=data_array.min(axis=0)
data_max=data_array.max(axis=0)
data_normalized= (data_array-data_min) / (data_max-data_min)
print(f"Масштабированный обучающий набор данных:\n{data_normalized}")
print(f"Максимальные значения для каждого признака:\n{data_max}")
print(f"Минимальные значения для каждого признака:\n{data_min}")
print('-'*20)
# 修改后的测试数据集
test_data=np.array([[0, 4, 6], [7, 5, 6]])
test_data_normalized= (test_data-data_min) / (data_max-data_min)
print("Масштабированный тестовый набор данных:\n", test_data_normalized)
print('-'*20)
# 对训练数据集执行逆变换
data_original=data_normalized* (data_max-data_min) +data_min
print("Возвращенный обратно обучающий набор данных:\n", data_original)
print('-'*20)
# 对测试数据集执行逆变换
test_data_original=test_data_normalized* (data_max-data_min) +data_min
print("Возвращенный обратно тестовый набор данных:\n", test_data_original)--------------------原始训练数据集:
[[4, -1, 2], [9, 1, 5], [14, 2, 8], [19, 4, 11]]--------------------缩放后的训练数据集:
[[0. 0. 0. ]
[0.3333 0.4 0.3333]
[0.6667 0.6 0.6667]
[1. 1. 1. ]]每个特征的最大值:
[19 4 11]每个特征的最小值:
[ 4 -1 2]--------------------缩放后的测试数据集:
[[-0.2667 1. 0.4444]
[ 0.2 1.2 0.4444]]--------------------逆变换恢复后的训练数据集:
[[ 4. -1. 2.]
[ 9. 1. 5.]
[14. 2. 8.]
[19. 4. 11.]]--------------------逆变换恢复后的测试数据集:
[[0. 4. 6.]
[7. 5. 6.]]可以清楚地看到,最小—最大缩放不会破坏数据集。必要时,所有数据都可以恢复到原始形式。
3.7.2 “One-hot”编码
许多机器学习数据集中都使用类别特征。例如,变量“颜色”可以取“红色”“绿色”和“蓝色”等值。为了构建机器学习模型,需要把这些类别值转换为数值。考虑一个一维特征向量,其初始形式为 [[绿色], [红色], [0], [蓝色]]。一种编码方式是为每种颜色分配一个唯一数字,例如 [[1], [2], [0], [3]]。
然而,这种直接编码可能会隐含地在类别之间引入顺序关系,而这并不总是合适的。例如,模型可能把“蓝色”(3)解释为比“绿色”(1)“更大”或“更重要”,这可能是不正确的。
为了解决这个问题,常常使用“one-hot”编码方法。在这种方法中,每个类别都会被转换为一个单独的二进制特征。对此类数据进行缩放通常不会改变它们,因为取值已经以 0 和 1 的形式表示。逆变换则可以恢复原始类别值。
“One-hot”编码是处理类别数据的有效方案。它可以避免把类别变量错误地解释为具有顺序关系的数值变量,否则在数据分析中可能导致不正确的结论。根据数据性质和任务要求正确选择预处理方法非常重要。“One-hot”编码会增加特征空间的维度,但能够更准确、更合理地处理数据,并保证每个类别对模型具有同等影响。这对于依赖数据点之间距离的算法尤其重要,例如 k 近邻或聚类算法。
3.7.3 标准缩放
当数据集的分布接近正态分布时,可以使用标准缩放。标准缩放的公式如下。
下面给出一个执行标准缩放的简单代码。
importnumpyasnp
X= [[3, 5, 8], # 假定的训练数据集
[2, 6, 4.5],
[3, 6, -6],
[11, 5, 5]]
print('-'*20)
print(f"Исходный обучающий набор данных X:\n{X}")
print('-'*20)
X=np.array(X)
X_scaled= (X-X.mean(axis=0)) /X.std(axis=0)
print(f"Стандартно масштабированный обучающий набор данных X:\n{X_scaled}")
print(f"Среднее значение для каждого признака:\n{X.mean(axis=0)}")
print(f"Стандартное отклонение для каждого признака:\n{X.std(axis=0)}")
print('-'*20)
Xtest= [[-15, 9, 2], # 假定的测试数据集
[4, 4, 4]]
Xt_scaled= (Xtest-X.mean(axis=0)) /X.std(axis=0)
print(f"Стандартно масштабированный тестовый набор данных Xtest:\n{Xt_scaled}")
print('-'*20)
X_back=X_scaled*X.std(axis=0) +X.mean(axis=0)
print(f"Возвращенный обратно обучающий набор данных:\n{X_back}")
print('-'*20)
Xt_back=Xt_scaled*X.std(axis=0) +X.mean(axis=0)
print(f"Возвращенный обратно тестовый набор данных Xtest:\n{Xt_back}")--------------------原始训练数据集 X:
[[3, 5, 8], [2, 6, 4.5], [3, 6, -6], [11, 5, 5]]--------------------标准缩放后的训练数据集 X:
[[-0.48189987 -1. 0.96772426]
[-0.75727123 1. 0.3068394 ]
[-0.48189987 1. -1.67581519]
[ 1.72107098 -1. 0.40125152]]每个特征的均值:
[4.75 5.5 2.875]每个特征的标准差:
[3.63145976 0.5 5.29593004]--------------------标准缩放后的测试数据集 Xtest:
[[-5.43858429 7. -0.16522122]
[-0.20652852 -3. 0.21242728]]--------------------逆变换恢复后的训练数据集:
[[ 3. 5. 8. ]
[ 2. 6. 4.5]
[ 3. 6. -6. ]
[11. 5. 5. ]]--------------------逆变换恢复后的测试数据集 Xtest:
[[-15. 9. 2.]
[ 4. 4. 4.]]需要注意的是,应用于训练数据集特征的缩放也可以应用于标签或目标变量。当标签是数值而不是类别或概率分布时,这一点尤其重要。例如,如果训练数据集的标签表示重量或价格等数值,就可以对其进行缩放,以提高模型训练的性能与稳定性。
不过,在测试模型或使用模型进行预测时,应将标签恢复到原始尺度。这样才能保证结果解释的正确性,因为缩放后的标签可能难以理解,甚至可能产生误导。
自测
哪一项最能概括“基本数学计算”这一章的重点?
在机器学习中,理论定义需要通过数值示例和可视化进行检验。
哪些做法有助于巩固本章内容?
参加测试