动手学深度学习第二章 2.1~2.3 节学习

希望您们先看过书,再来看本 blog,否则可能会看不懂,原因是我只写书上没写或者很新奇的东西。

先引个 torch 包

import torch

2.1 数据操作

2.1.1 入门

x.numel() 会返回张量中元素的总数。

torch.tensor()torch.Tensor() 的区别

  • torch.tensor() 是(当你未指定 dtype 的类型时)将 data 转化为 torch.FloatTensortorch.LongTensortorch.DoubleTensor 等类型,转化类型依据于 data 的类型或者 dtype 的值

    • 直接使用 vec = torch.tensor() 会报错,可以考虑如下方案修改:

      • vec = torch.Tensor()

      • vec = torch.tensor([])

  • torch.Tensor()torch.FloatTensor() 的别名

# vec = torch.tensor() 
# TypeError: tensor() missing 1 required positional arguments: "data"
vec = torch.Tensor()
vec
tensor([])
vec = torch.tensor([])
vec
tensor([])

从下面代码可以看出,两种 tensor 函数都是开辟了新的内存空间建立张量。而且 Tensor() 函数直接转化为了浮点数。

a = [1, 2]
ida = id(a)
va = torch.Tensor(a)
idva = id(va)
va2 = torch.tensor(a)
idva2 = id(va2)
a, va, va2, ida, idva, idva2
([1, 2],
 tensor([1., 2.]),
 tensor([1, 2]),
 140242979567568,
 140242979895440,
 140242979896976)

torch.randn((n, m)) 会创建一个形状为 \(n \times m\) 的张量,其中每个元素都从均值为 0,标准差为 1 的标准高斯分布(正态分布)中随机采样。

torch.randn((3, 4))
tensor([[ 0.2106,  1.6204,  2.3458, -0.0705],
        [-0.2921, -0.1707,  0.1801, -1.0608],
        [ 0.0082, -1.4975,  0.2525, -0.2153]])

2.1.2 运算符

torch.cat() 函数中,\(dim=0\) 是按照行作为轴拼接,\(dim=1\) 是按照列作为轴拼接。可以简单地将其想象为吸附在轴上。

这里可以写成 (X, Y) 或者 [X, Y] 都可以,不过官方文档以及 d2l 书中都写的 (X, Y)

X = torch.arange(12, dtype=torch.float32).reshape((3, 4))
Y = torch.tensor([[2.0, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]])
X, Y, torch.cat((X, Y), dim=0), torch.cat((X, Y), dim=1)
(tensor([[ 0.,  1.,  2.,  3.],
         [ 4.,  5.,  6.,  7.],
         [ 8.,  9., 10., 11.]]),
 tensor([[2., 1., 4., 3.],
         [1., 2., 3., 4.],
         [4., 3., 2., 1.]]),
 tensor([[ 0.,  1.,  2.,  3.],
         [ 4.,  5.,  6.,  7.],
         [ 8.,  9., 10., 11.],
         [ 2.,  1.,  4.,  3.],
         [ 1.,  2.,  3.,  4.],
         [ 4.,  3.,  2.,  1.]]),
 tensor([[ 0.,  1.,  2.,  3.,  2.,  1.,  4.,  3.],
         [ 4.,  5.,  6.,  7.,  1.,  2.,  3.,  4.],
         [ 8.,  9., 10., 11.,  4.,  3.,  2.,  1.]]))

2.1.3 广播机制

  • 首先,两个张量的维度必须相等(张量的维度可以理解为张量轴的个数),对于维度没有要求,除了要 \(\ge 2\)

  • 如果两个某一维的长度不相等,那么必定存在一个张量这一维大小等于 1.

  • 然后复制后,进行广播

2.1.4 索引和切片

两个有趣的写法:

  • X[1,2] = 9 使得 \(x_{1,2} = 9\)。梦回 pascal 时代。

  • X[0:2, :] = 12 使得前两行的所有列都赋值为 12.

2.1.5 节省内存

如果调用语句 Y = X + Y,那么将取消引用 Y 指向的张量,而是指向新分配的内存处的张量。

本书作者认为这种元素的操作改变内存地址不可取,原因有二:

  1. 不想总是不必要地分配内存

  2. 如果不原地更新,其他引用仍然会指向旧的内存位置,这样某些代码可能会无意中引用旧的参数。

    2.1 例如如果做 Z = Y 这样地操作,那么仅仅只是将 Z 指向的位置改到和 Y 一样,而没有对 Y 的内容进行复制。

执行原地操作的方法也有两种:

  1. Y[:] = <expression>

  2. Y += X

另外,reshape() 也修改了内存地址。

2.1.6 转化

a = torch.tensor([3.5])
a, a.item(), float(a), int(a)
(tensor([3.5000]), 3.5, 3.5, 3)

2.2 数据预处理

这一节主要教我们怎么用 pandas 包。但是我总觉得没啥用啊

一些有趣的函数

  • inputs, outputs = data.iloc[:, 0:2], data.iloc[:, 2],通过位置索引获得 DataFrame 实例的值。

  • inputs = inputs.fillna(inputs.mean()),把 inputs 中的所有 NaN 替换为 inputs 的平均值。

  • inputs = pd.get_dummies(inputs, dummy_na=True),这个函数将字符串类型的都改为 one-hot 类型,最后一个形参的意思是是否 NaN 也计算在内的意思。

  • newdata = data.drop(data.isna().sum().idxmax(), axis=1),这个语句可以删除缺失值最多的列。

2.3 线性代数

2.3.1 标量

跳过(233)

2.3.2 向量

向量用小写粗体表示,而元素是一个标量,因此最好不要用粗体表示元素。

这里维度这个词的明确很好:向量或轴的维度被用来表示向量或轴的长度,即向量或轴的元素数量。而张量的维度用来表示张量具有的轴数。在这个意义上,张量的某个轴的维数就是这个轴的长度。

2.3.3 矩阵

矩阵用大写粗体 \(\boldsymbol{A}\) 表示。其中的元素有两种表示方法:

  1. \([\boldsymbol{A}]_{ij}\),在不清楚 \([\boldsymbol{A}]_{ij}\) 是否为标量时可以使用。

  2. \(a_{ij}\),当已知 \([\boldsymbol{A}]_{ij}\) 为标量时可以使用。

另附 markdown 加粗倾斜指令 \boldsymbol{}

2.3.4 张量

张量用特殊字体的大写字母表示,如 \(\sf{X, Y, Z}\),另附 markdown 等线体指令 \sf{},表示张量元素的机制和矩阵类似。

不妨假设有一个张量 \(\sf{X}\),它的形状为 \((3,4,5)\),那么,调用 len(X) 将会返回 3,原因是它只会返回第一维的长度,而 X.shape 则会返回其整体的形状。

2.3.5 张量算法的基本性质

给定具有相同形状的任意两个张量,任意按元素二元运算的结果都将是相同形状的张量。

另外,作者举了一个有趣的例子,两个矩阵的按元素乘法称为哈达玛积(Hadamard product)(数学符号为 \(\odot\))。

2.3.6 降维

降维求和

某些函数除了自己的主要作用外,还具有降维的第二作用。假如现在有个张量 A = torch.arange(20, dtype=torch.float32).reshape(5,4),所以它有几个例子函数具有降维功能:

  • A.sum()

  • A.mean()

其中,它们都有一个参数 axis,表示消除哪一个轴,沿着哪一个轴做操作。

也有神奇的用法比如 A.sum(axis=[0,1]),该操作与 A.sum() 等价。

非降维求和

可以在做操作时保持轴数不变,sum_A = A.sum(axis=1, keepdims=True),这样可以接着做操作,比如广播。

如果想沿某个轴计算其元素的累积综合,可以调用 cumsum() 函数。

A = torch.arange(20, dtype=torch.float32).reshape(5,4)
A.cumsum(axis=0)
tensor([[ 0.,  1.,  2.,  3.],
        [ 4.,  6.,  8., 10.],
        [12., 15., 18., 21.],
        [24., 28., 32., 36.],
        [40., 45., 50., 55.]])

2.3.7 点积

两种方式:

  1. torch.dot(x, y)

  2. torch.sum(x * y)

2.3.8 矩阵-向量积

函数为:torch.mv(A, x)。很有趣,这里给出了简写很棒啊,而且也更加安全一些,因为这个函数没有广播机制。

2.3.9 矩阵-矩阵乘法

函数为:torch.mm(A, B),这个函数同样没有广播机制。

另附一下 torch.matmul(input, other, *, out=None) 的内容:

  • 如果两个张量都是一维的,将返回点乘(标量)。

  • 如果两个参数都是二维的,则返回矩阵-矩阵乘积。

  • 如果第一个参数是一维的,第二个参数是二维的,为了进行矩阵乘法,会在其维度上预加一个1。矩阵乘法后,预置的维度将被移除。

  • 如果第一个参数是二维的,第二个参数是一维的,则返回矩阵-向量乘积。

  • 如果两个参数都是至少一维的,并且至少有一个参数是 N 维的(其中 N>2),那么将返回一个分批的矩阵乘法。如果第一个参数是一维的,为了进行分批矩阵乘法,会在其维度上预加一个 1,然后删除。如果第二个参数是一维的,那么在它的维度上加一个 1,以便进行分批矩阵乘法,之后再删除。非矩阵(例如批)的维度是广播的(因此一定是可广播的)。例如,如果 input 是一个 \((j×1×n×n)\) 张量,other 是一个 \((k×n×n)\) 张量,out 将是一个 \((j×k×n×n)\) 张量。

    请注意,广播逻辑在确定输入是否可广播时只看批维度,而不看矩阵维度。例如,如果 input 是一个 \((j×1×n×m)\) 张量,other 是一个 \((k×m×p)\) 张量,这些输入对于广播是有效的,即使最后两个维度(即矩阵维度)不同,输出将是一个 \((j×k×n×p)\) 张量。这个操作对具有稀疏布局的参数有支持。特别是矩阵-矩阵(两个参数都是2维的)支持稀疏的参数,其限制与 torch.mm() 相同

2.3.10 范数

范数有如下三个性质,不妨设此时有一个向量 \(\boldsymbol{x}\),它的范数为 \(f(\boldsymbol{x})\)

  1. \(f(\alpha \boldsymbol{x}) = |\alpha|f(\boldsymbol{x})\)

  2. \(f(\boldsymbol{x} + \boldsymbol{y}) \le f(\boldsymbol{x}) + f(\boldsymbol{y}))\)

  3. \(f(\boldsymbol{x}) \ge 0\)

于是原书中提到几种范数:

  1. \(L_1\) 范数:\(||\boldsymbol{x}||_1 = \sum_{i=1}^n |x_i|\),相应的代码为 torch.abs(x).sum()

  2. \(L_2\) 范数:\(||\boldsymbol{x}||_2 = \sqrt{\sum_{i=1}^n x_i^2}\),相应的代码为 torch.norm(x)

  3. 弗罗贝尼乌斯范数(Frobenius norm):这是一个矩阵 \(\boldsymbol{X}\) 的范数 \(||\boldsymbol{X}||_F = \sqrt{\sum_{i=1}^m \sum_{j=1}^n x_{ij}^2}\),它是矩阵所有元素的平方和的平方根。

    直接用 torch.norm(X) 也可以求这个值,这个函数也可以指定以哪一位做范数,参数为 dim。