《动手学深度学习》随笔 part1 —— pytorch 基本操作

本文最后更新于:2023年10月10日 下午

数值操作

模块导入:

import torch

torch.arange() 创建向量:

>>> torch.arange(12)
tensor([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11])

参数列表:torch.arange(start=0, end, step=1, \*, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) → Tensor

start end step :起始值,结束值和步长。

dtype :指定数据类型。

out :输出张量。

layout :布局方式,一般有以下两种:

  • torch.strided :密集布局,张量元素按一定步幅排列在内存中,相邻元素间地址差距连续,但元素不一定连续存储。
  • torch.sparse_coo :稀疏布局。只存储非零元素的索引和值,节省内存。访问 Link 获取更多信息。

device :设备。例如可选 cpucuda

requires_grad :是否为张量启用梯度计算。

torch.reshape() 改变张量形状:

>>> x = torch.arange(12).reshape((2, 3, 2))
>>> x
tensor([[[ 0,  1],
         [ 2,  3],
         [ 4,  5]],

        [[ 6,  7],
         [ 8,  9],
         [10, 11]]])

注:可以用 \(-1\) 表示自动填充某一个轴的大小。

x = torch.arange(12).reshape((-1, 3, 2)) 就和以上语句等价。

shape 获取沿所有轴的元素个数:

>>> x.shape
torch.Size([2, 3, 2])

如果只对张量的第一个维度感兴趣,可以用 len(x)

torch.zeros(),torch.ones() 获得全0 / 全1 张量:

>>> torch.zeros(2, 3)
tensor([[0., 0., 0.],
        [0., 0., 0.]])
>>> torch.ones(2, 3)
tensor([[1., 1., 1.],
        [1., 1., 1.]])

torch.randn() 随机采样( 会生成均值为 0,标准差为 1 的正态分布):

>>> torch.randn(2, 3, 4)
tensor([[[ 0.2692,  0.0791,  0.0039, -0.1389],
         [-0.1045, -0.3420,  0.2542,  1.4940],
         [ 1.5095,  1.1909, -0.5695,  1.4376]],

        [[ 0.5032, -0.1839, -0.0568,  0.1740],
         [-0.2951,  2.4619,  1.2984,  0.0647],
         [-0.5046,  0.9516, -0.0810, -0.3269]]])

更一般地,有库函数 torch.normal(mean, std, size, out=None)

mean :正态分布的均值。

std :正态分布的标准差。

size :生成的张量形状。可以是整数(一维向量),也可以是元组或列表(多维张量)。

out :如果提供了一个输出张量,随机生成的值将存储在这个张量中,而不是创建一个新的张量。

torch.randn(2, 3, 4) 等效于:

>>> torch.normal(0.0, 1.0, (2, 3, 4))

指定值:

>>> x = torch.tensor([[1, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3]])
>>> x
tensor([[1, 1, 1, 1],
        [2, 2, 2, 2],
        [3, 3, 3, 3]])

sum() 求和:

>>> x.sum()
tensor(24)

axis=0 指定沿列求和:

>>> x.sum(axis=0)
tensor([6, 6, 6, 6])

axis=1 指定沿行求和:

>>> x.sum(axis=1)
tensor([4, 8, 12])

keepdims=True 非降维求和(保持原有行/列的形状):

>>> x.sum(axis=1, keepdims=True)
tensor([[ 4],
        [ 8],
        [12]])

numel() 获取总元素个数:

>>> x.numel()
12

四则运算:形状相同,按元素操作

>>> x = torch.tensor([1, 2, 4, 8])
>>> y = torch.tensor([2, 2, 2, 2])
>>> x + y, x - y, x * y, x / y, x ** y
( tensor([ 3,  4,  6, 10]), 
  tensor([-1,  0,  2,  6]), 
  tensor([ 2,  4,  8, 16]), 
  tensor([0.5000, 1.0000, 2.0000, 4.0000]), 
  tensor([ 1,  4, 16, 64]) )

其中,单纯两个矩阵中每个值按元素相乘,称为 Hadamard积

x.exp() \(\to\) \(e^x\)

>>> x.exp()
tensor([2.7183e+00, 7.3891e+00, 5.4598e+01, 2.9810e+03])

dtype 指定元素类型:

>>> x = torch.arange(12, dtype=torch.float32).reshape((3, 4))
>>> x
tensor([[ 0.,  1.,  2.,  3.],
        [ 4.,  5.,  6.,  7.],
        [ 8.,  9., 10., 11.]])

cat 沿行(轴 0)拼接张量:

>>> y = torch.tensor([[1.0, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3]])
tensor([[1., 1., 1., 1.],
        [2., 2., 2., 2.],
        [3., 3., 3., 3.]])
>>> torch.cat((x, y), dim=0)
tensor([[ 0.,  1.,  2.,  3.],
        [ 4.,  5.,  6.,  7.],
        [ 8.,  9., 10., 11.],
        [ 1.,  1.,  1.,  1.],
        [ 2.,  2.,  2.,  2.],
        [ 3.,  3.,  3.,  3.]])

cat 沿列(轴 1)拼接张量:

>>> torch.cat((x, y), dim=1)
tensor([[ 0.,  1.,  2.,  3.,  1.,  1.,  1.,  1.],
        [ 4.,  5.,  6.,  7.,  2.,  2.,  2.,  2.],
        [ 8.,  9., 10., 11.,  3.,  3.,  3.,  3.]])

按元素比较:

>>> x == y
tensor([[False,  True, False, False],
        [False, False, False, False],
        [False, False, False, False]])

利用广播机制使得不同形状的张量执行按元素操作,tensor 会自动扩充维度。

两个“可广播的” tensor 满足以下条件:

  • 每个 tensor 至少一个维度。
  • 从末尾遍历 tensor 所有维度时,出现以下情况:
    • 维度相等。
    • 维度不等 && 其中一个维度为 \(1\)
    • 维度不等 && 其中一个维度不存在。

满足规则,将小的扩展成大的 tensor

>>> x = torch.arange(2).reshape((2, 1))
>>> y = torch.arange(4).reshape((1, 4))
>>> x, y
(tensor([[0],
        [1]]), tensor([[0, 1, 2, 3]]))
>>> x + y
tensor([[0, 1, 2, 3],
        [1, 2, 3, 4]])

这时候就体现非降维求和的优势了:

>>> x = torch.arange(12).reshape((3, 4))
>>> x
tensor([[ 0,  1,  2,  3],
        [ 4,  5,  6,  7],
        [ 8,  9, 10, 11]])
>>> sum_x = x.sum(axis=1, keepdim=True)
>>> sum_x
tensor([[ 6],
        [22],
        [38]])
>>> x / sum_x
tensor([[0.0000, 0.1667, 0.3333, 0.5000],
        [0.1818, 0.2273, 0.2727, 0.3182],
        [0.2105, 0.2368, 0.2632, 0.2895]])

可以看到对每一行/列求了平均,而不会因维度对不上而报错。

与 python 字符串类似地进行索引。

>>> x = torch.arange(12).reshape((3, 4))
>>> x
tensor([[ 0,  1,  2,  3],
        [ 4,  5,  6,  7],
        [ 8,  9, 10, 11]])
>>> x[-1]
tensor([ 8,  9, 10, 11])
>>> x[1:3]
tensor([[ 4,  5,  6,  7],
        [ 8,  9, 10, 11]])

将指定元素写入:

>>> x[2, 2] = 114514
tensor([[     0,      1,      2,      3],
        [     4,      5,      6,      7],
        [     8,      9, 114514,     11]])

多元素赋值,先 0 轴后 1 轴。

>>> x[0:2, 0:3] = 1919810
tensor([[1919810, 1919810, 1919810,       3],
        [1919810, 1919810, 1919810,       7],
        [      8,       9,  114514,      11]])

以分配新内存的方式分配 \(x\) 的副本给 \(y\)

>>> y = x.clone()

mean() 求所有均值(注意必须是浮点型):

>>> x = torch.arange(12, dtype=torch.float32).reshape((3, 4))
>>> x
tensor([[ 0.,  1.,  2.,  3.],
        [ 4.,  5.,  6.,  7.],
        [ 8.,  9., 10., 11.]])
>>> x.mean()
tensor(5.5000)

x.mean(axis=0) 等价于 x.sum(axis=0) / x.shape[0]

cumsum 沿着某个维度计算累计总和:

>>> x.cumsum(axis=0)
tensor([[ 0.,  1.,  2.,  3.],
        [ 4.,  6.,  8., 10.],
        [12., 15., 18., 21.]])

torch.unsqueeze() 增加数据维度:

>>> x = torch.arange(4)  # tensor([0, 1, 2, 3])
>>> x = torch.unequeeze(x, 0)
>>> x
tensor([[0, 1, 2, 3]])

可以看到最外层增加了一维。

参数列表:new_tensor = torch.unsqueeze(input, dim)

input:需要操作的张量。

dim:要在哪个维度上增加一个维度。

返回值是一个新的张量。

线性代数操作

矩阵转置:

>>> A = torch.arange(20, dtype=torch.float32).reshape(4, 5)
>>> A
tensor([[ 0.,  1.,  2.,  3.,  4.],
        [ 5.,  6.,  7.,  8.,  9.],
        [10., 11., 12., 13., 14.],
        [15., 16., 17., 18., 19.]])
>>> A.T
tensor([[ 0.,  5., 10., 15.],
        [ 1.,  6., 11., 16.],
        [ 2.,  7., 12., 17.],
        [ 3.,  8., 13., 18.],
        [ 4.,  9., 14., 19.]])

点积(Dot Product) \(\textbf{x}^\top \textbf{y} = \sum x_iy_i\)

>>> x = torch.arange(5, dtype=torch.float32)
>>> y = torch.ones(5, dtype = torch.float32)
>>> x, y
(tensor([0., 1., 2., 3., 4.]), tensor([1., 1., 1., 1., 1.]))
>>> torch.dot(x, y)
tensor(10.)

等效表达 torch.sum(x * y)

矩阵-向量积:

对于一个矩阵 $^{mn} $ ,和向量 \(\textbf{x}\in \mathbb{R}^{n}\)

\(\textbf{Ax} = \begin{bmatrix} a_{11} & a_{12} & \ldots & a_{1n} \\ a_{21} & a_{22} & \ldots & a_{2n} \\ \vdots & \vdots & \ddots & \vdots \\ a_{m1} & a_{m2} & \ldots & a_{mn} \end{bmatrix}\)\(\begin{bmatrix} x_1 \\ x_2 \\ \vdots \\ x_m \end{bmatrix}\) \(= \begin{bmatrix} \left\langle a_1x\right\rangle \\ \left\langle a_2x\right\rangle \\ \vdots \\ \left\langle a_mx\right\rangle \end{bmatrix}\)

\(\left\langle a_ix \right\rangle\) 表示矩阵的第 \(i\) 行构成的行向量和向量 \(x\) 的点积。

利用 mv(A, x) 进行矩阵-向量积:

>>> A, x
(tensor([[ 0.,  1.,  2.,  3.,  4.],
        [ 5.,  6.,  7.,  8.,  9.],
        [10., 11., 12., 13., 14.],
        [15., 16., 17., 18., 19.]]), tensor([0., 1., 2., 3., 4.]))
>>> torch.mv(A, x)
tensor([ 30.,  80., 130., 180.])

利用 mm(A, B) 矩阵乘法:

>>> B = torch.ones(5, 4)
tensor([[1., 1., 1., 1., 1.],
        [1., 1., 1., 1., 1.],
        [1., 1., 1., 1., 1.],
        [1., 1., 1., 1., 1.]])
>>> torch.mm(A, B)
tensor([[10., 10., 10., 10.],
        [35., 35., 35., 35.],
        [60., 60., 60., 60.],
        [85., 85., 85., 85.]])

更加通用地,有库函数 torch.matmul(input, other, out=None) 执行矩阵相乘。

input :要进行矩阵相乘的第一个张量(或标量)。

other :要进行矩阵相乘的第二个张量(或标量)。

out :如果提供了输出张量,结果将存储在这个张量中,而不是创建一个新的张量。

torch.normal() 的行为取决于输入张量的维度。

  1. 若两个张量均一维,执行内积(点积)操作,返回一个标量。
  2. 若两个张量均二维,它执行矩阵乘法,返回一个二维矩阵。
  3. 若至少一个张量高维,它执行广义矩阵乘法操作,根据广播规则计算结果。

利用 norm(x)\(L_2\) 范数:

\[ \| \mathbf{v} \|_2 = \sqrt{v_1^2 + v_2^2 + \ldots + v_n^2} \]

>>> v = torch.tensor([3.0,-4.0])
>>> torch.norm(v)
tensor(5.)

\(L_1\) 范数:

>>> torch.abs(v).sum()
tensor(7.)

矩阵的 \(L_2\) 范数:(Frobenius范数)

\[\|A\|_2=\sqrt{\sum_{i=1}^{m}\sum_{j=1}^{n}|a_{ij}|^2}\]

>>> torch.norm(torch.ones((4, 9)))
tensor(6.)

微分操作

requires_grad = True 为张量启动梯度计算:

>>> x = torch.arange(4.0, requires_grad = True) 
>>> x
tensor([0., 1., 2., 3.])

另一种写法:

>>> x.requires_grad_(True)

利用 backward() 计算梯度:

>>> y = torch.dot(x, x)
>>> y.backward()

注意,使用点积而不是乘法,因为 pytorch 只能对标量求梯度。

不过可以使用 y.sum().backward()

效果和 y.backward(torch.ones_like(y)) 等同。

利用 .grad 显示梯度:

>>> x.grad
tensor([0., 2., 4., 6.])

\(\nabla \textbf{x}^\top\textbf{x}=2\textbf{x}\) 得知答案正确,也可以利用程序验证。

>>> x.grad == 2 * x
tensor([True, True, True, True])

重置梯度值为 0 ,以便后续计算其它梯度:

>>> x.grad.zero_()

利用 .detach() 停止梯度传播:对比两个例子:

>>> x = torch.arange(4.0, requires_grad = True) # [0, 1, 2, 3]
>>> y = x * x
>>> y1 = y * x
>>> y1.sum().backward()
>>> x.grad
tensor([0., 3., 12., 27.])
>>> x = torch.arange(4.0, requires_grad = True) # [0, 1, 2, 3]
>>> y = x * x
>>> y2 = y.detach() * x
>>> y2.sum().backward()
>>> x.grad
tensor([0., 1., 4., 9.])

可以看到前者计算的是 \(y1=x\times x \times x\) 的偏导数为 \(3x^2\)

而后者则是 \(y2=u\times x\)\(u\) 看作常量,数值等于 \(y\) )的偏导数,即为 \(u=x^2\)

此外,还可以用一个上下文管理器 torch.no_grad() 来禁止梯度传播。

例如以下是第二个例子的等价形式:

>>> x = torch.arange(4.0, requires_grad = True) # [0, 1, 2, 3]

>>> with torch.no_grad():
        y = x * x;
    
>>> y2 = y * x;
>>> y2.sum().backward()
>>> x.grad
tensor([0., 1., 4., 9.])

利用 retain_graph=True 保留计算图,以便再次 backward()

>>> x = torch.arange(4.0, requires_grad=True)
>>> y = torch.dot(x, x)
>>> y.backward(retain_graph=True)
>>> y.backward()

若第三行换成 y.backward() 则会报错。

线性回归

模块导入

from torch import nn

nn.Linear 定义一个线性层:

>>> model = nn.Linear(2, 1) # 输入特征数为2,输出特征数为1(标量)

参数列表:torch.nn.Linear(in_features, out_features, bias=True)

in_features :输入神经元个数,即输入特征数。

out_features :输出神经元个数,即输出特征数。

bias :是否包含偏置。

本质是执行了一个线性变换: \[\textbf{Y}_{n\times out} = \textbf{X}_{n\times in}\textbf{W}_{in\times out}+\textbf{b}\] 其中 \(n\) 是样本数量,或者说 batch_size\(in,out\) 为输入和输出的特征维度,\(\textbf{b}\)\(out\) 维的向量偏置,使用了广播机制。

nn.Sequential 定义神经网络容器:

>>> net = nn.Sequential(model)

作用是按顺序组织一系列神经网络的层(layer),例如:

>>> model = nn.Sequential(
        nn.Conv2d(1, 20, 5),
        nn.ReLU(),
        nn.Conv2d(20, 64, 5),
        nn.ReLU()
        )

我们可以通过下标访问元素,如:

>>> net = nn.Sequential(
        nn.Linear(2, 1)
        	# other
    )
>>> net[0].weight.data.normal_(0, 0.01)
>>> net[0].bias.data.fill_(0)

通过 net[0] 去访问了 Linear 类中的函数,使模型参数初始化。

其中 weightbias 指明要访问权值还是偏置数据,normal_fill_ 则是 pytorch 的两个张量方法。

tensor.normal_(mean=0, std=1) 指定随机抽样的正态分布均值和标准差。

tensor.fill_(value) 则直接填充 value 值。

调用损失函数:

>>> nn.MSELoss() # 均方误差,或称平方L2范数

torch.optim.SGD 执行小批量随机梯度下降算法并更新:

>>> trainer = torch.optim.SGD(net.parameters(), lr=0.03)
>>> trainer.step()

parameters() 用于自动读取参数,lr 为学习率(LearningRate)。

完整实例(《动手学深度学习》章节3.3):

import numpy as np
import torch
from torch.utils import data
from d2l import torch as d2l
from torch import nn

true_w = torch.tensor([2, -3.4])
true_b = 4.2

# 生成多个噪声数据
features, labels = d2l.synthetic_data(true_w, true_b, 1000)

# 构建PyTorch数据迭代器
def load_array(data_arrays, batch_size, is_train=True):  #@save
    dataset = data.TensorDataset(*data_arrays)
    return data.DataLoader(dataset, batch_size, shuffle=is_train)

batch_size = 10
data_iter = load_array((features, labels), batch_size)

next(iter(data_iter))

# 创建神经网络
net = nn.Sequential(nn.Linear(2, 1))

# 初始化参数
net[0].weight.data.normal_(0, 0.01)
net[0].bias.data.fill_(0)

loss = nn.MSELoss()

trainer = torch.optim.SGD(net.parameters(), lr=0.03)

num_epochs = 3
for epoch in range(num_epochs):
    for X, y in data_iter:
        l = loss(net(X) ,y)
        trainer.zero_grad()
        l.backward()
        trainer.step()
    l = loss(net(features), labels)
    print(f'epoch {epoch + 1}, loss {l:f}')


w = net[0].weight.data
print('w的估计误差:', true_w - w.reshape(true_w.shape))
b = net[0].bias.data
print('b的估计误差:', true_b - b)

# 访问线性回归的梯度
w_grad = net[0].weight.grad
print('w的梯度:', w_grad)
b_grad = net[0].bias.grad
print('b的梯度:', b_grad)
epoch 1, loss 0.000232
epoch 2, loss 0.000101
epoch 3, loss 0.000100
w的估计误差: tensor([ 0.0002, -0.0003])
b的估计误差: tensor([-0.0003])
w的梯度: tensor([[-0.0041, -0.0147]])
b的梯度: tensor([0.0073])

激活函数

  1. \(\text{ReLU}(x)\) 函数

\[ \text{ReLU}(x)=\max(x,0) \]

>>> x = torch.arange(-3.0, 3.0, 0.5, requires_grad=True)
>>> y = torch.relu(x)
>>> x, y
>>> y.backward(torch.ones_like(x), retain_graph=True)
(tensor([-3.0000, -2.5000, -2.0000, -1.5000, -1.0000, -0.5000,  0.0000,  0.5000,
          1.0000,  1.5000,  2.0000,  2.5000], requires_grad=True),
 tensor([0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.5000, 1.0000,
         1.5000, 2.0000, 2.5000], grad_fn=<ReluBackward0>))

一个显著的性质是对 \(\text{ReLU}(x)\) 求导后非 \(0\)\(1\) ,即要么让参数消失,要么让参数通过。

用许多个 \(\text{ReLU}(x)\) 函数构成连续的分段线性函数:

变体:(即使参数是负的,某些信息仍然可以通过) \[ \text{pReLU}(x)=\max(0,x) + \alpha\min(0,x) \]

  1. \(\text{sigmoid}(x)\) 函数(“S”型函数)

\[ \text{sigmoid}(x)=\dfrac{1}{1+e^{-x}} \]

>>> y = torch.sigmoid(x)
>>> x.grad.zero_()
>>> y.backward(torch.ones_like(x), retain_graph=True)

\[ \dfrac{d}{dx}\text{sigmoid}(x)=\dfrac{e^{-x}}{(1+e^{-x})^2}=\text{sigmoid}(x)(1-\text{sigmoid}(x)) \]

易知 \(x=0\) 时有导数最大值 \(\dfrac{1}{4}\)


《动手学深度学习》随笔 part1 —— pytorch 基本操作
https://kisuraop.github.io/posts/a3c53bfd.html
作者
KisuraOP
发布于
2023年10月7日
许可协议