2.1 数据操作
N维数组样例
N维数组是机器学习和神经网络的主要数据结构
张量表示一个由数值组成的数组,这个数组可能有多个维度。 具有一个轴的张量对应数学上的向量(vector); 具有两个轴的张量对应数学上的矩阵(matrix); 具有两个轴以上的张量没有特殊的数学名称。
创建数组
访问数组
左开右闭
一个冒号是表示所有
最后一个例子是表示跳着访问,::3
表示从第0行开始,每3行一跳,即0、3、…
::2
表示从第0列开始,每2列一跳,即0、2、…
2.1 数据操作实现
首先,我们介绍n维数组,也称为张量(tensor)。 使用过Python中NumPy计算包的读者会对本部分很熟悉。 无论使用哪个深度学习框架,它的张量类(在MXNet中为ndarray, 在PyTorch和TensorFlow中为Tensor)都与Numpy的ndarray类似。 但深度学习框架又比Numpy的ndarray多一些重要功能: 首先,GPU很好地支持加速计算,而NumPy仅支持CPU计算; 其次,张量类支持自动微分。 这些功能使得张量类更适合深度学习。 如果没有特殊说明,本书中所说的张量均指的是张量类的实例。
入门
1.导入
首先,我们导入torch。请注意,虽然它被称为PyTorch,但是代码中使用torch而不是pytorch。
import torch
2.创建张量
张量表示一个由数值组成的数组,这个数组可能有多个维度。 具有一个轴的张量对应数学上的向量(vector); 具有两个轴的张量对应数学上的矩阵(matrix); 具有两个轴以上的张量没有特殊的数学名称。
首先,我们可以使用 arange 创建一个行向量 x。这个行向量包含以0开始的前12个整数,它们默认创建为整数。也可指定创建类型为浮点数。张量中的每个值都称为张量的 元素(element)。例如,张量 x 中有 12 个元素。除非额外指定,新的张量将存储在内存中,并采用基于CPU的计算。
3.shape && numel()
可以通过张量的shape属性来访问张量**(沿每个轴的长度)的形状** 。
因为x是一个向量,因此只有一个维度,且这个维度长为12
如果只想知道张量中元素的总数,即形状的所有元素乘积,可以检查它的大小(size)。 因为这里在处理的是一个向量,所以它的shape与它的size相同。
numel = number of elements
4.reshape
要想改变一个张量的形状而不改变元素数量和元素值,可以调用reshape函数。 例如,可以把张量x从形状为(12,)的行向量转换为形状为(3,4)的矩阵。 这个新的张量包含与转换前相同的值,但是它被看成一个3行4列的矩阵。 要重点说明一下,虽然张量的形状发生了改变,但其元素值并没有变。 注意,通过改变张量的形状,张量的大小不会改变。
我们不需要通过手动指定每个维度来改变形状。 也就是说,如果我们的目标形状是(高度,宽度), 那么在知道宽度后,高度会被自动计算得出,不必我们自己做除法。 在上面的例子中,为了获得一个3行的矩阵,我们手动指定了它有3行和4列。 幸运的是,我们可以通过**-1来调用此自动计算出维度**的功能。 即我们可以用x.reshape(-1,4)或x.reshape(3,-1)来取代x.reshape(3,4)。
5.创建特殊张量
有时,我们希望使用全0、全1、其他常量,或者从特定分布中随机采样的数字来初始化矩阵。 我们可以创建一个形状为(2,3,4)的张量,其中所有元素都设置为0。代码如下:
同样,我们可以创建一个形状为(2,3,4)的张量,其中所有元素都设置为1。代码如下:
有时我们想通过从某个特定的概率分布中随机采样来得到张量中每个元素的值。 例如,当我们构造数组来作为神经网络中的参数时,我们通常会随机初始化参数的值。 以下代码创建一个形状为(3,4)的张量。 其中的每个元素都从均值为0、标准差为1的标准高斯分布(正态分布)中随机采样。
我们还可以通过提供包含数值的Python列表(或嵌套列表),来为所需张量中的每个元素赋予确定值。 在这里,最外层的列表对应于轴0,内层的列表对应于轴1。
创建一个二维的数组
创建一个三维的数组
运算符
1.按元素计算
我们的兴趣不仅限于读取数据和写入数据。 我们想在这些数据上执行数学运算,其中最简单且最有用的操作是按元素(elementwise)运算。 它们将标准标量运算符应用于数组的每个元素。 对于将两个数组作为输入的函数,按元素运算将二元运算符应用于两个数组中的每对位置对应的元素。 我们可以基于任何从标量到标量的函数来创建按元素函数。
对于任意具有相同形状的张量, 常见的标准算术运算符(+、-、* 、/和 **
)都可以被升级为按元素运算。 我们可以在同一形状的任意两个张量上调用按元素操作。 在下面的例子中,我们使用逗号来表示一个具有5个元素的元组,其中每个元素都是按元素操作的结果。
1->整数
1.0->浮点
“按元素”方式可以应用更多的计算,包括像求幂这样的一元运算符。
2.线性代数运算
除了按元素计算外,我们还可以执行线性代数运算,包括向量点积和矩阵乘法。 我们将在 2.3节中解释线性代数的重点内容。
3.张量连结
我们也可以把多个张量连结(concatenate)在一起, 把它们端对端地叠起来形成一个更大的张量。 我们只需要提供张量列表,并给出沿哪个轴连结。 下面的例子分别演示了当我们沿行(轴-0,形状的第一个元素) 和**按列(轴-1,形状的第二个元素)**连结两个矩阵时,会发生什么情况。 我们可以看到,第一个输出张量的轴-0长度(6)是两个输入张量轴-0长度的总和(3+3); 第二个输出张量的轴-1长度(8)是两个输入张量轴-1长度的总和(4+4)。
torch,cat((X, Y), dim=0) 把这两个元素合并在一起,且在第0维合并,也就是按行合并,即堆起来
torch,cat((X, Y), dim=1) 把这两个元素合并在一起,且在第1维合并,也就是按列合并
4.通过逻辑运算符构建二元张量
有时,我们想通过逻辑运算符构建二元张量。 以X == Y为例: 对于每个位置,如果X和Y在该位置相等,则新张量中相应项的值为1。 这意味着逻辑语句 X == Y在该位置处为真,否则该位置为0。
5.求和
对张量中的所有元素进行求和,会产生一个单元素张量。
广播机制
在上面的部分中,我们看到了如何在相同形状的两个张量上执行按元素操作。 在某些情况下,即使形状不同,我们仍然可以通过调用 广播机制(broadcasting mechanism)来执行按元素操作。 这种机制的工作方式如下:
- 通过适当复制元素来扩展一个或两个数组,以便在转换之后,两个张量具有相同的形状;
- 对生成的数组执行按元素操作。
在大多数情况下,我们将沿着数组中长度为1的轴进行广播,如下例子:
由于a和b分别是31和12矩阵,如果让它们相加,它们的形状不匹配。 我们将两个矩阵广播为一个更大的3*2矩阵,如下所示:矩阵a将复制列, 矩阵b将复制行,然后再按元素相加。
为什么可以广播机制?首先a和b维度是一样的(如果维度不一样就没戏了),然后 矩阵a将复制列, 矩阵b将复制行,然后再按元素相加
索引和切片
就像在任何其他Python数组中一样,张量中的元素可以通过索引访问。 与任何Python数组一样:第一个元素的索引是0,最后一个元素索引是-1; 可以指定范围以包含第一个元素和最后一个之前的元素。
如下所示,我们可以用[-1]选择最后一个元素,可以用[1:3]选择第二个和第三个元素:
X[-1]访问了最后一行
除读取外,我们还可以通过指定索引来将元素写入矩阵。
如果我们想为多个元素赋值相同的值,我们只需要索引所有元素,然后为它们赋值。 例如,[0:2, :]访问第1行和第2行,其中“:”代表沿轴1(列)的所有元素。 虽然我们讨论的是矩阵的索引,但这也适用于向量和超过2个维度的张量。
节省内存
运行一些操作可能会导致为新结果分配内存。 例如,如果我们用Y = X + Y,我们将取消引用Y指向的张量,而是指向新分配的内存处的张量。
在下面的例子中,我们用Python的id()函数演示了这一点, 它给我们提供了内存中引用对象的确切地址。 运行Y = Y + X后,我们会发现id(Y)指向另一个位置。 这是因为Python首先计算Y + X,为结果分配新的内存,然后使Y指向内存中的这个新位置。
这可能是不可取的,原因有两个:
- 首先,我们不想总是不必要地分配内存。在机器学习中,我们可能有数百兆的参数,并且在一秒内多次更新所有参数。通常情况下,我们希望原地执行这些更新;
- 如果我们不原地更新,其他引用仍然会指向旧的内存位置,这样我们的某些代码可能会无意中引用旧的参数。
在 Python 中,Y = Y + X 这样的操作可能会导致“其他引用仍然会指向旧的内存位置”,这是由于 Python 的可变对象与不可变对象的行为差异所导致的。具体来说,Python 对象在内存中的管理机制和赋值操作的行为决定了这一点。
- 不可变对象的内存管理
int、float、tuple、str 等是不可变对象。当你对它们执行操作(如 Y = Y + X),Python 实际上会创建一个新对象,并将 Y 指向该新对象的内存地址。
(1)在不可变对象中,Y = Y + X 不会修改原来的 Y。它创建了一个新对象,将 Y 引用的地址更新为新的对象,而原来的内存地址和对象保持不变。
(2)问题出现的原因:如果有其他变量也引用了原来的 Y,这些变量将继续指向原始的内存地址,而不会指向新的内存位置。
a = 10
b = a # 现在a和b指向相同的内存地址
a = a + 5 # a现在指向一个新的对象,而b仍然指向原来的对象
print(a) # 输出:15
print(b) # 输出:10
a = (1, 2)
b = a # b 和 a 指向相同的元组
a = a + (3,) # a 被重新赋值,指向一个新元组
print(a) # 输出:(1, 2, 3)
print(b) # 输出:(1, 2),b 仍然指向旧的元组
- 可变对象的情况
对于可变对象(如 list、dict、set 等),操作会直接在原对象上进行修改,因此引用这些对象的变量都会“看到”相同的更改。例如:
在这个例子中,lst1 和 lst2 都引用了相同的列表对象,因此对 lst1 的修改也影响了 lst2,因为它们共享同一个内存地址。
lst1 = [1, 2, 3]
lst2 = lst1 # 现在lst1和lst2指向相同的内存地址
lst1.append(4) # 修改原对象
print(lst1) # 输出:[1, 2, 3, 4]
print(lst2) # 输出:[1, 2, 3, 4],lst2也被修改了
幸运的是,执行原地操作非常简单。 我们可以使用切片表示法将操作的结果分配给先前分配的数组,例如Y[:] = <expression>
。 为了说明这一点,我们首先创建一个新的矩阵Z,其形状与另一个Y相同, 使用zeros_like来分配一个全0的块。
如果在后续计算中没有重复使用X, 我们也可以使用X[:] = X + Y或X += Y来减少操作的内存开销。
GPT:x=x+y无论x是否为可变对象都会创建一个新对象,x+=y在x为可变对象(如列表等)时不会创建新对象,即原地执行
转换为其他Python对象
将深度学习框架定义的张量转换为NumPy张量(ndarray)很容易,反之也同样容易。 torch张量和numpy数组将共享它们的底层内存,就地操作更改一个张量也会同时更改另一个张量。
要将大小为1的张量转换为Python标量,我们可以调用item函数或Python的内置函数。
2.2 数据预处理实现
为了能用深度学习来解决现实世界的问题,我们经常从预处理原始数据开始, 而不是从那些准备好的张量格式数据开始。 在Python中常用的数据分析工具中,我们通常使用pandas软件包。 像庞大的Python生态系统中的许多其他扩展包一样,pandas可以与张量兼容。 本节我们将简要介绍使用pandas预处理原始数据,并将原始数据转换为张量格式的步骤。
读取数据集
举一个例子,我们首先创建一个人工数据集,并存储在CSV(逗号分隔值)文件 …/data/house_tiny.csv中。 以其他格式存储的数据也可以通过类似的方式进行处理。 下面我们将数据集按行写入CSV文件中。
# 纯净版
import osos.makedirs(os.path.join('..', 'data'), exist_ok=True)
data_file = os.path.join('..', 'data', 'house_tiny.csv')
with open(data_file, 'w') as f:f.write('NumRooms,Alley,Price\n') # 列名f.write('NA,Pave,127500\n') # 每行表示一个数据样本f.write('2,NA,106000\n')f.write('4,NA,178100\n')f.write('NA,NA,140000\n')
# 注释版
import os# os.makedirs:用于递归地创建目录。如果上级目录不存在,它会自动创建所有必要的上级目录
# os.path.join 将两个路径 '..' 和 'data' 拼接成一个完整的路径。'..' 表示上一级目录,'data' 是要创建的目录
# os.path.join('..', 'data') 会在上一级目录(..)下创建名为 data 的文件夹
# exist_ok=True:这个参数表示如果指定的目录已经存在,不会抛出错误,而是继续执行。
os.makedirs(os.path.join('..', 'data'), exist_ok=True)# 表示文件将被创建在上一级目录的 data 文件夹中,文件名为 house_tiny.csv
# 创建 CSV 文件的路径,并存储在变量 data_file 中
data_file = os.path.join('..', 'data', 'house_tiny.csv')# open(data_file, 'w'):以写模式 ('w') 打开 data_file 指向的文件。如果文件已经存在,它会被覆盖;如果文件不存在,则会创建一个新文件
# with 语句:使用 with 可以确保文件在完成操作后自动关闭,不需要显式调用 f.close()
with open(data_file, 'w') as f:# 接下来的几行使用 f.write() 向 CSV 文件写入数据,每一行表示一个记录# 写入 CSV 文件的表头,表示这三个列分别为房间数量(NumRooms)、小巷类型(Alley)和房价(Price)。\n 表示换行f.write('NumRooms,Alley,Price\n') # 列名# NA 通常表示缺失值(Not Available)。# 每行数据之间用逗号(,)分隔,这符合 CSV(Comma-Separated Values,逗号分隔值)文件的格式f.write('NA,Pave,127500\n') # 每行表示一个数据样本f.write('2,NA,106000\n')f.write('4,NA,178100\n')f.write('NA,NA,140000\n')
数据内容:
NumRooms,Alley,Price
NA,Pave,127500
2,NA,106000
4,NA,178100
NA,NA,140000
这段代码的作用是:
- 确保在上级目录中创建 data 文件夹。
- 在 data 文件夹中创建或覆盖一个名为 house_tiny.csv 的文件。
- 向文件中写入房屋数据,包括列名(房间数量、小巷类型、价格)以及四条房屋信息样本。
要从创建的CSV文件中加载原始数据集,我们导入pandas包并调用read_csv函数。该数据集有四行三列。其中每行描述了房间数量(“NumRooms”)、巷子类型(“Alley”)和房屋价格(“Price”)。
import pandas as pddata = pd.read_csv(data_file)
print(data)
处理缺失值
注意,“NaN”项代表缺失值。 为了处理缺失的数据,典型的方法包括插值法和删除法, 其中插值法用一个替代值弥补缺失值,而删除法则直接忽略缺失值。 在这里,我们将考虑插值法。
通过位置索引iloc,我们将data分成inputs和outputs, 其中前者为data的前两列,而后者为data的最后一列。 对于inputs中缺少的数值,我们用同一列的均值替换“NaN”项。
iloc 是 pandas 中的一种索引方式,基于整数位置索引。[:, 0:2] 表示选取所有行,并选取第 0 列和第 1 列(即前两列)。
iloc = index location
# data.iloc[:, 0:2]:使用 iloc 按位置选择 data 数据框的第 0 列到第 1 列(不包含第 2 列)的数据,表示输入特征。
# data.iloc[:, 2]:同样使用 iloc 按位置选择 data 数据框的第 2 列,作为输出目标。
# 这行代码将输入特征赋值给 inputs,将输出目标赋值给 outputs。
inputs, outputs = data.iloc[:, 0:2], data.iloc[:, 2]# 这行代码用于填补 inputs 数据框中的缺失值,采用每列的平均值进行填补:
# inputs.fillna(inputs.mean()):fillna() 用于填补缺失值。这里 inputs.mean() 计算 inputs 数据框中每列的均值,然后 fillna() 将缺失值(NaN)替换为相应列的均值。
# 对于 NumRooms 列,mean() 会计算该列的均值:因此,NaN 将被替换为 3.0。对于 Alley 列,由于它是非数值型数据,mean() 不适用,它会被忽略,fillna() 无法填充该列的 NaN。
inputs = inputs.fillna(inputs.mean())
print(inputs)
对于inputs中的类别值或离散值,我们将“NaN”视为一个类别。 由于“巷子类型”(“Alley”)列只接受两种类型的类别值“Pave”和“NaN”, pandas可以自动将此列转换为两列“Alley_Pave”和“Alley_nan”。 巷子类型为“Pave”的行会将“Alley_Pave”的值设置为1,“Alley_nan”的值设置为0。 缺少巷子类型的行会将“Alley_Pave”和“Alley_nan”分别设置为0和1。
inputs = pd.get_dummies(inputs, dummy_na=True)
print(inputs)
pd.get_dummies():这个函数将分类数据转换为独热编码(One-Hot Encoding)。独热编码是一种将分类变量转换为多个二进制列的技术,每个类别(或者值)会被表示为一列,值为 1 或 0。
dummy_na=True:这个参数告诉 get_dummies 函数将缺失值 (NaN) 也视为一种类别,生成一列对应的二进制特征,表示原数据中某行是否为缺失值。如果 dummy_na=False(默认设置),缺失值会被忽略。
转换为张量格式
现在inputs和outputs中的所有条目都是数值类型,它们可以转换为张量格式。 当数据采用张量格式后,可以通过在 2.1节中引入的那些张量函数来进一步操作。
import torchX = torch.tensor(inputs.to_numpy(dtype=float))
y = torch.tensor(outputs.to_numpy(dtype=float))
X, y
inputs 是一个 pandas 数据框,可能包含不同的数据类型(如整数、浮点数、字符串等)。
inputs.to_numpy(dtype=float):这部分代码将 inputs 数据框转换为 NumPy 数组,并强制数组的数据类型为 float(浮点数)。这样可以确保数据在后续的计算中具有统一的数值类型。
torch.tensor():这个函数将 NumPy 数组转换为 PyTorch 张量。PyTorch 张量类似于 NumPy 数组,但它可以在 GPU 上运行,从而支持深度学习中的高效计算。将 inputs 转换为张量后,便可以使用 PyTorch 的各类操作函数,进行后续的深度学习模型训练和推理。
传统python默认使用float64,但是64位浮点数一般计算比较慢,因此深度学习中我们通常使用32位浮点数
2.3 线性代数
矩阵乘法从直观上来说是一个扭曲的空间,一个向量通过一个矩阵乘法变成了另外一个向量,这个矩阵把空间进行了扭曲
c和b都是向量,那么c的范数就会小于等于A的范数乘上b的范数
F范数就是等价于把矩阵拉成一条向量,然后做向量的范数
标量
标量由只有一个元素的张量表示。 下面的代码将实例化两个标量,并执行一些熟悉的算术运算,即加法、乘法、除法和指数。
import torchx = torch.tensor(3.0)
y = torch.tensor(2.0)x + y, x * y, x / y, x**y
向量
向量可以被视为标量值组成的列表。 这些标量值被称为向量的元素(element)或分量(component)。
人们通过一维张量表示向量。一般来说,张量可以具有任意长度,取决于机器的内存限制。
x = torch.arange(4)
x
我们可以使用下标来引用向量的任一元素,例如可以通过x_i来引用第i个元素。 注意,元素x_i是一个标量,所以我们在引用它时不会加粗。 大量文献认为列向量是向量的默认方向,在本书中也是如此。
在代码中,我们通过张量的索引来访问任一元素。
x[3]
1.长度、维度和形状
在数学表示法中,如果我们想说一个向量x由n个实值标量组成, 可以将其表示为
。 向量的长度通常称为向量的维度(dimension)。
与普通的Python数组一样,我们可以通过调用Python的内置len()函数来访问张量的长度。
len(x)
当用张量表示一个向量(只有一个轴)时,我们也可以通过.shape属性访问向量的长度。 形状(shape)是一个元素组,列出了张量沿每个轴的长度(维数)。 对于只有一个轴的张量,形状只有一个元素。
x.shape
请注意,维度(dimension)这个词在不同上下文时往往会有不同的含义,这经常会使人感到困惑。 为了清楚起见,我们在此明确一下: 向量或轴的维度被用来表示向量或轴的长度,即向量或轴的元素数量。 然而,张量的维度用来表示张量具有的轴数。 在这个意义上,张量的某个轴的维数就是这个轴的长度。
矩阵
数学表示法使用来表示矩阵A,其由m行和n列的实值标量组成。
当调用函数来实例化张量时, 我们可以通过指定两个分量m和n来创建一个形状为
m*n的矩阵。
A = torch.arange(20).reshape(5, 4)
A
现在在代码中访问矩阵的转置。
A.T
B = torch.tensor([[1, 2, 3], [2, 0, 4], [3, 4, 5]])
B
B == B.T
尽管单个向量的默认方向是列向量,但在表示表格数据集的矩阵中, 将每个数据样本作为矩阵中的行向量更为常见。 后面的章节将讲到这点,这种约定将支持常见的深度学习实践。 例如,沿着张量的最外轴,我们可以访问或遍历小批量的数据样本。
张量
当我们开始处理图像时,张量将变得更加重要,图像以n维数组形式出现, 其中3个轴对应于高度、宽度,以及一个通道(channel)轴, 用于表示颜色通道(红色、绿色和蓝色)。 现在先将高阶张量暂放一边,而是专注学习其基础知识。
X = torch.arange(24).reshape(2, 3, 4)
X
在1个三维张量中,有2个二维矩阵,每个二维矩阵有3个向量,每个向量有4个标量
张量算法的基本性质
标量、向量、矩阵和任意数量轴的张量(本小节中的“张量”指代数对象)有一些实用的属性。 例如,从按元素操作的定义中可以注意到,任何按元素的一元运算都不会改变其操作数的形状。 同样,给定具有相同形状的任意两个张量,任何按元素二元运算的结果都将是相同形状的张量。 例如,将两个相同形状的矩阵相加,会在这两个矩阵上执行元素加法。
A = torch.arange(20, dtype=torch.float32).reshape(5, 4)
B = A.clone() # 通过分配新内存,将A的一个副本分配给B
A, A + B
如果用B = A
,不会进行任何新的内存的分配,只是把它的索引给你
而B = A.clone()
通过分配新内存,将A的一个副本分配给B
具体而言,两个矩阵的按元素乘法称为Hadamard积(Hadamard product)(数学符号
)。 对于矩阵
, 其中第i行和第j列的元素是b_ij。 矩阵A(在 (2.3.2)中定义)和B的Hadamard积为:
A * B
将张量乘以或加上一个标量不会改变张量的形状,其中张量的每个元素都将与标量相加或相乘。
a = 2
X = torch.arange(24).reshape(2, 3, 4)
a + X, (a * X).shape
a + X:X所有元素都加上a
降维
我们可以对任意张量进行的一个有用的操作是计算其元素的和。 数学表示法使用
符号表示求和。 在代码中可以调用计算求和的函数:
x = torch.arange(4, dtype=torch.float32)
x, x.sum()
我们可以表示任意形状张量的元素和。
A.shape, A.sum()
默认情况下,调用求和函数会沿所有的轴降低张量的维度,使它变为一个标量。 我们还可以指定张量沿哪一个轴来通过求和降低维度。 以矩阵为例,为了通过求和所有行的元素来降维(轴0),可以在调用函数时指定axis=0。 由于输入矩阵沿0轴降维以生成输出向量,因此输入轴0的维数在输出形状中消失。
A_sum_axis0 = A.sum(axis=0)
A_sum_axis0, A_sum_axis0.shape
指定axis=1将通过汇总所有列的元素降维(轴1)。因此,输入轴1的维数在输出形状中消失。
A_sum_axis1 = A.sum(axis=1)
A_sum_axis1, A_sum_axis1.shape
axis等于几相当于把第几维给消去。这里axis=0,相当于把【5,4】的“5”给消去,剩下的就是“4”
沿着行和列对矩阵求和,等价于对矩阵的所有元素进行求和。
A.sum(axis=[0, 1]) # 结果和A.sum()相同
一个与求和相关的量是平均值(mean或average)。 我们通过将总和除以元素总数来计算平均值。 在代码中,我们可以调用函数来计算任意形状张量的平均值。
A.mean(), A.sum() / A.numel()
同样,计算平均值的函数也可以沿指定轴降低张量的维度。
A.mean(axis=0), A.sum(axis=0) / A.shape[0]
1.非降维求和
但是,有时在调用函数来计算总和或均值时保持轴数不变会很有用。
为了使用广播机制
sum_A = A.sum(axis=1, keepdims=True)
sum_A
A.sum(axis=1, keepdims=True) 的作用是对数组或张量 A 进行按行的求和,并保留结果的维度
keepdims=True:表示在求和操作后,保留原始的维度。这样,即使某一维度被缩减,结果仍会保持相同的维度数目,只是那些求和后的维度值变为 1。
如果不设置 keepdims=True,求和后的结果会少一个维度。例如,在二维数组上进行按行求和,结果会变为一维数组。如果 keepdims=True,结果将保持二维数组的形式,只是列数变为 1。
例如,由于sum_A在对每行进行求和后仍保持两个轴,我们可以通过广播将A除以sum_A。
A / sum_A
如果我们想沿某个轴计算A元素的累积总和, 比如axis=0(按行计算),可以调用cumsum函数。 此函数不会沿任何轴降低输入张量的维度。
A.cumsum(axis=0)
点积(Dot Product)
我们已经学习了按元素操作、求和及平均值。 另一个最基本的操作之一是点积。 给定两个向量, 它们的点积(dot product)
(或) 是相同位置的按元素乘积的和:。
torch.dot 只能对一维向量做乘积
y = torch.ones(4, dtype = torch.float32)
x, y, torch.dot(x, y)
注意,我们可以通过执行按元素乘法,然后进行求和来表示两个向量的点积:
torch.sum(x * y)
矩阵-向量积
在代码中使用张量表示矩阵-向量积,我们使用mv函数。 当我们为矩阵A和向量x调用torch.mv(A, x)时,会执行矩阵-向量积。 注意,A的列维数(沿轴1的长度)必须与x的维数(其长度)相同。
A.shape, x.shape, torch.mv(A, x)
矩阵-矩阵乘法
B = torch.ones(4, 3)
torch.mm(A, B)
矩阵-矩阵乘法可以简单地称为矩阵乘法,不应与“Hadamard积”混淆。
范数
线性代数中最有用的一些运算符是范数(norm)。 非正式地说,向量的范数是表示一个向量有多大。 这里考虑的大小(size)概念不涉及维度,而是分量的大小。
范数听起来很像距离的度量。 欧几里得距离和毕达哥拉斯定理中的非负性概念和三角不等式可能会给出一些启发。 事实上,欧几里得距离是一个L_2范数: 假设n维向量x中的元素是x_1,…,x_n,其L_2范数是向量元素平方和的平方根:
其中,在L_2范数中常常省略下标2,也就是说等同于。 在代码中,我们可以按如下方式计算向量的L_2范数。
u = torch.tensor([3.0, -4.0])
torch.norm(u)
深度学习中更经常地使用L_2范数的平方,也会经常遇到L_1范数,它表示为向量元素的绝对值之和:
与L_2范数相比,L_1范数受异常值的影响较小。 为了计算L_1范数,我们将绝对值函数和按元素求和组合起来。
torch.abs(u).sum()
L_2范数和L_1范数都是更一般的L_p范数的特例:
类似于向量的L_2范数,矩阵的Frobenius范数(Frobenius norm)是矩阵元素平方和的平方根:
Frobenius范数满足向量范数的所有性质,它就像是矩阵形向量的L_2范数。 调用以下函数将计算矩阵的Frobenius范数。
torch.norm(torch.ones((4, 9)))
1.范数和目标
在深度学习中,我们经常试图解决优化问题: 最大化分配给观测数据的概率; 最小化预测和真实观测之间的距离。 用向量表示物品(如单词、产品或新闻文章),以便最小化相似项目之间的距离,最大化不同项目之间的距离。 目标,或许是深度学习算法最重要的组成部分(除了数据),通常被表达为范数。
矩阵计算
标量导数
亚导数
当不一定存在导数时,
梯度
将导数扩展到向量,通常称为梯度
矩阵求导之分子布局与分母布局
标量对标量求导是标量
向量对标量求导、标量对向量求导 都是向量
向量对向量求导 是矩阵
梯度与等高线相切,指向的是值变化最大的方向
因为内积<u,v>就定义为uTv。内积,就是点乘,但是一般情况两个向量内积,写成转置形式
简单记一下就是标量对列向量求导 结果是行向量(通常是用列向量表示的向量 所以加T 就是行向量了)
由于偏导计算中无关行列空间属性,这里为了方便计算,通过转置y使得X^T A=A^T X
,从而dy/dx=d(X A^T)/dX=A^T
2.4 微积分
导数和微分
注意,注释#@save是一个特殊的标记,会将对应的函数、类或语句保存在d2l包中。 因此,以后无须重新定义就可以直接调用它们(例如,d2l.use_svg_display())。
%matplotlib inline
import numpy as np
from matplotlib_inline import backend_inline
from d2l import torch as d2ldef f(x):return 3 * x ** 2 - 4 * x
def use_svg_display(): #@save"""使用svg格式在Jupyter中显示绘图"""backend_inline.set_matplotlib_formats('svg')
我们定义set_figsize函数来设置图表大小。 注意,这里可以直接使用d2l.plt,因为导入语句 from matplotlib import pyplot as plt已标记为保存到d2l包中。
def set_figsize(figsize=(3.5, 2.5)): #@save"""设置matplotlib的图表大小"""use_svg_display()d2l.plt.rcParams['figure.figsize'] = figsize
下面的set_axes函数用于设置由matplotlib生成图表的轴的属性。
#@save
def set_axes(axes, xlabel, ylabel, xlim, ylim, xscale, yscale, legend):"""设置matplotlib的轴"""axes.set_xlabel(xlabel)axes.set_ylabel(ylabel)axes.set_xscale(xscale)axes.set_yscale(yscale)axes.set_xlim(xlim)axes.set_ylim(ylim)if legend:axes.legend(legend)axes.grid()
通过这三个用于图形配置的函数,定义一个plot函数来简洁地绘制多条曲线, 因为我们需要在整个书中可视化许多曲线。
#@save
def plot(X, Y=None, xlabel=None, ylabel=None, legend=None, xlim=None,ylim=None, xscale='linear', yscale='linear',fmts=('-', 'm--', 'g-.', 'r:'), figsize=(3.5, 2.5), axes=None):"""绘制数据点"""if legend is None:legend = []set_figsize(figsize)axes = axes if axes else d2l.plt.gca()# 如果X有一个轴,输出Truedef has_one_axis(X):return (hasattr(X, "ndim") and X.ndim == 1 or isinstance(X, list)and not hasattr(X[0], "__len__"))if has_one_axis(X):X = [X]if Y is None:X, Y = [[]] * len(X), Xelif has_one_axis(Y):Y = [Y]if len(X) != len(Y):X = X * len(Y)axes.cla()for x, y, fmt in zip(X, Y, fmts):if len(x):axes.plot(x, y, fmt)else:axes.plot(y, fmt)set_axes(axes, xlabel, ylabel, xlim, ylim, xscale, yscale, legend)
现在我们可以绘制函数u=f(x)及其在x=1处的切线y=2x-3, 其中系数2是切线的斜率。
x = np.arange(0, 3, 0.1)
plot(x, [f(x), 2 * x - 3], 'x', 'f(x)', legend=['f(x)', 'Tangent line (x=1)'])
偏导数
到目前为止,我们只讨论了仅含一个变量的函数的微分。 在深度学习中,函数通常依赖于许多变量。 因此,我们需要将微分的思想推广到多元函数(multivariate function)上。
为了计算, 我们可以简单地将x_1,…,x_i-1,x_i+1,…,x_n看作常数, 并计算y关于x_i的导数
梯度
我们可以连结一个多元函数对其所有变量的偏导数,以得到该函数的梯度(gradient)向量
梯度是一个向量,其分量是多变量函数相对于其所有变量的偏导数。
x是一个n维向量,函数f(x)相对于x的梯度是一个包含n个偏导数的向量:
链式法则
然而,上面方法可能很难找到梯度。 这是因为在深度学习中,多元函数通常是复合(composite)的, 所以难以应用上述任何规则来微分这些函数。 幸运的是,链式法则可以被用来微分复合函数。
现在考虑一个更一般的场景,即函数具有任意数量的变量的情况。 假设可微分函数y有变量u_1,…,u_m,其中每个可微分函数u_i都有变量x_1,…,x_n。 注意,y是x_1,…,x_n的函数。 对于任意i=1,2,…,n,链式法则给出:
2.5 自动微分
正如 2.4节中所说,求导是几乎所有深度学习优化算法的关键步骤。 虽然求导的计算很简单,只需要一些基本的微积分。 但对于复杂的模型,手工进行更新是一件很痛苦的事情(而且经常容易出错)。
深度学习框架通过自动计算导数,即自动微分(automatic differentiation)来加快求导。 实际中,根据设计好的模型,系统会构建一个计算图(computational graph), 来跟踪计算是哪些数据通过哪些操作组合起来产生输出。 自动微分使系统能够随后反向传播梯度。 这里,反向传播(backpropagate)意味着跟踪整个计算图,填充关于每个参数的偏导数。
向量链式法则
例子1
z是标量,因此是标量对向量求导
例子2
相比上一个例子,涉及到矩阵
z是标量,因此是标量对向量求导
向量b的 L2范数的平方对向量b求导实质就是标量对nx1的向量求导,一般标量对向量求导是采用分母布局,就是结果与分母一样也是nx1的向量,但这里是分子布局所以再转置一下,结果是2bT
自动求导
计算图
计算图本质上等价于刚才我们使用链式法则的过程
自动求导的两种模式
反向累积
所以如果采用前向,需要把中间的值存下来;如果采用反向,就是沿着反方向执行,需要把中间结果拿过来用
(正向中子节点比父节点先计算,因此也无法像反向那样把本节点的计算结果传给每一个子节点)
反向从根节点向下扫,可以保证每个节点只扫一次;正向从叶节点向上扫,会导致上层节点可能需要被重复扫多次
自动求导实现
1.一个简单的例子
作为一个演示例子,假设我们想对函数y=2x^Tx
关于列向量x求导。 首先,我们创建变量x并为其分配一个初始值。
(标量对向量求导 -> 向量)
import torchx = torch.arange(4.0)
x
在我们计算y关于x的梯度之前,需要一个地方来存储梯度。 重要的是,我们不会在每次对一个参数求导时都分配新的内存。 因为我们经常会成千上万次地更新相同的参数,每次都分配新的内存可能很快就会将内存耗尽。 注意,一个标量函数关于向量x的梯度是向量,并且与x具有相同的形状。
x.requires_grad_(True) # 等价于x=torch.arange(4.0,requires_grad=True)
x.grad # 默认值是None,y关于x的导数是放在这个地方的
现在计算y。
y = 2 * torch.dot(x, x)
y
dot(点积运算)是指对应位置相乘再相加,dot(x,x)的运算结果和xTx的运算结果一致,也就是说两者是等价的
x是一个长度为4的向量,计算x和x的点积,得到了我们赋值给y的标量输出。 接下来,通过调用反向传播函数来自动计算y关于x每个分量的梯度,并打印这些梯度。
y.backward()
x.grad
在 PyTorch 中,每个张量都有一个 grad 属性,用来存储梯度(即损失函数对这个张量的导数)。
函数y=2x^Tx
关于x的梯度应为4x。 让我们快速验证这个梯度是否计算正确。
x.grad == 4 * x
现在计算x的另一个函数。
# 在默认情况下,PyTorch会累积梯度,我们需要清除之前的值
x.grad.zero_()
y = x.sum() # 等价于 y = x_1 + ... + x_n
y.backward()
x.grad
在 PyTorch 中,默认情况下,梯度不会自动清除,每次调用 backward() 时,计算得到的梯度会累积到之前的梯度中。因此,如果在一次反向传播后不清除梯度,梯度值会叠加。x.grad.zero_():这个方法将 x.grad 中存储的梯度清零,避免累积。zero_() 是一个就地操作(in-place operation),即直接对现有的 grad 张量进行操作,而不是创建一个新的张量
y = x.sum():这个操作对张量 x 的所有元素求和,生成一个新的标量张量 y。求和操作是一个可微分的操作,因此 PyTorch 可以自动计算它相对于输入张量 x 的梯度。
y.backward():这是 PyTorch 中用来计算反向传播的函数。它会从 y 开始,沿着计算图向后计算每个张量的梯度。在这个例子中,y 是通过 x.sum() 得到的,因此 PyTorch 会自动计算出 y 对 x 的梯度,由于 y = x.sum(),求和的梯度对每个元素的导数都是 1,因此对于 x 的每个元素,梯度都会是 1
2.非标量变量的反向传播
当y不是标量时,向量y关于向量x的导数的最自然解释是一个矩阵。 对于高阶和高维的y和x,求导的结果可以是一个高阶张量。
然而,虽然这些更奇特的对象确实出现在高级机器学习中(包括深度学习中), 但当调用向量的反向计算时,我们通常会试图计算一批训练样本中每个组成部分的损失函数的导数。 这里,我们的目的不是计算微分矩阵,而是单独计算批量中每个样本的偏导数之和。
# 对非标量调用backward需要传入一个gradient参数,该参数指定微分函数关于self的梯度。
# 本例只想求偏导数的和,所以传递一个1的梯度是合适的
x.grad.zero_()
y = x * x # 这里的操作 x * x 是逐元素相乘,结果y 是与 x 相同形状的张量,每个元素是 x 对应元素的平方
# 等价于y.backward(torch.ones(len(x)))
y.sum().backward()
x.grad
两个一维张量相乘是对应元素相乘得到一个一维张量,所以这里的y是一个向量
最后计算得y对x的梯度为2x
3.分离计算
有时,我们希望将某些计算移动到记录的计算图之外。 例如,假设y是作为x的函数计算的,而z则是作为y和x的函数计算的。 想象一下,我们想计算z关于x的梯度,但由于某种原因,希望将y视为一个常数, 并且只考虑到x在y被计算后发挥的作用。
这里可以分离y来返回一个新变量u,该变量与y具有相同的值, 但丢弃计算图中如何计算y的任何信息。 换句话说,梯度不会向后流经u到x。 因此,下面的反向传播函数计算z=ux关于x的偏导数,**同时将u作为常数处理, 而不是z=xx*x关于x的偏导数**。
x.grad.zero_()
y = x * x
u = y.detach() # 把y当作常数而不是一个关于x的函数,因此对系统而言u不再是一个关于x的函数,只是值等于。这一操作会创建一个新的张量 u,它的值和 y 是相同的,但这个新张量将不再与计算图相关联。换句话说,u 是从计算图中分离出来的,它不会参与梯度计算。
# 即 u = [x_1^2, x_2^2, ..., x_n^2],但 u 不会计算梯度。z = u * x # z 是 u 和 x 的逐元素相乘,即 z = [u_1 * x_1, u_2 * x_2, ..., u_n * x_n]
# 对系统而言,由于分离计算,此时z关于x的导数是 一个常数乘以x
# 这里的 u 是已经从计算图中分离出来的,因此在计算反向传播时,u 不会影响梯度的计算z.sum().backward() # 对 z 求和,得到一个标量张量;对这个标量调用backward(),将计算 z.sum() 对 x 的梯度;对系统而言,由于分离计算,梯度应该为ux.grad == u
由于记录了y的计算结果,我们可以随后在y上调用反向传播, 得到y=xx关于的x的导数,即2x。
x.grad.zero_()
y.sum().backward()
x.grad == 2 * x
(这里使用.sum()的原因是为了把y从一个向量变成一个标量,如果是向量对向量求导结果是矩阵,标量对向量求导结果是向量)
4.Python控制流的梯度计算
使用自动微分的一个好处是: 即使构建函数的计算图需要通过Python控制流(例如,条件、循环或任意函数调用),我们仍然可以计算得到的变量的梯度。 在下面的代码中,while循环的迭代次数和if语句的结果都取决于输入a的值。
def f(a):b = a * 2while b.norm() < 1000:b = b * 2if b.sum() > 0:c = belse:c = 100 * breturn c
norm()是求欧几里得范数,欧几里得范数指得就是通常意义上的距离范数。例如在欧式空间里,它表示两点间的距离(向量x的模长)。
让我们计算梯度。
# 这行代码创建了一个标量 a,它是从标准正态分布中生成的随机数。由于 size=(),a 是一个标量张量(形状为空,即没有维度)。
# requires_grad=True 表明 PyTorch 需要跟踪 a 的计算图,以便稍后能够计算它的梯度。
a = torch.randn(size=(), requires_grad=True)# 将输入张量 a 不断放大,直到其范数(对于标量张量,范数即绝对值)大于或等于1000
# 由于a是标量,因此经过操作后,b也是标量,故d也是一个标量张量
d = f(a)# d.backward() 触发反向传播,计算 d 对输入张量 a 的梯度
d.backward()
我们现在可以分析上面定义的f函数。 请注意,它在其输入a中是分段线性的。 换言之,对于任何a,存在某个常量标量k,使得f(a)=k*a,其中k的值取决于输入a,因此可以用d/a验证梯度是否正确。
a.grad == d / a