跟着李沐老师学习深度学习(一)
深度学习基础
学习视频:https://space.bilibili.com/1567748478/channel/seriesdetail?sid=358497 教学教材:https://zh.d2l.ai/ 加油!我来啦!我支持你!
深度学习相关介绍
应用场景包括图像分类、物体检测与分割、风格迁移、面部合成以及文字生成图像等技术领域;广告点击案例分析中,在关键词触发下进行点击率预估,并进行排序影响因素分析以确定排序依据。
安装
- 使用conda/ miniconda环境
我这里是在Windows下,使用conda环境,如下命令可以创建一个新的环境:
conda create --name d2l python=3.9 -y
激活 d2l 环境:
conda activate d2l
- 安装需要的包
pip install torch==1.12.0
pip install torchvision==0.13.0
pip install d2l==0.17.6
- 接下来,需要下载这本书的代码
进入自己的代码工作目录
d:
cd D:\code\d2l
下载代码:
mkdir d2l-zh && cd d2l-zh
curl https://zh-v2.d2l.ai/d2l-zh-2.0.0.zip -o d2l-zh.zip
下载完成后无需额外操作即可在指定目录进行解压缩操作;完成解压缩后,请前往目标工作区;然后按照以下步骤执行命令:
jupyter notebook
目前能够在Web浏览器中打开指定地址:http://localhost:8888(该地址通常会默认启动)。基于此,我们能够执行每章节的代码操作,并参考下图中的示例。

由于课程中的课件是以Jupyter幻灯片文件开发的,在Jupyter环境中无法直接打开这些文件,默认情况下会提示缺少相应的软件包。
pip install rise
数据操作+数据预处理
N维数组样例
- N维数组是机器学习和神经网络的主要数据结构


创建数组
- 数组必须满足特定要求
- 该数组的维度结构应包含如3×4矩阵的例子
- 每个元素的数据格式为32位浮点型
- 每个元素的值应全部为零或为随机数值
访问元素
- 单一元素:[1, 2]
- 单行:[1, :]
- 单列:[., :]
- 子区域:[\text{从索引}1\text{到}3\text{(不包含}3\text{)以及从索引}.\text{开始的所有后续元素}]
- 子区域:[\text{每隔两行及每隔两列访问一次}]

数组相关练习:
1. 张量创建函数:
torch.arange(start=0, end, step=1, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False):基于 start 生成一个一维张量序列,在 end 处终止(不包含该端点),并设置步长为 step。
* 例如 `x = torch.arange(12)` 创建了一个包含 0 到 11 的一维张量。
torch.zeros(size, out parameters can be set to None and are optional for other parameters like dtype and layout): # creates a zero tensor of specified size with default values for other arguments as None
* 例如 `torch.zeros((2, 3, 4))` 创建一个形状为 (2, 3, 4) 的全零张量。
torch.ones(size, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False):生成一个指定大小为 size 的填充1的张量。
* 例如 `torch.ones((2, 3, 4))` 创建一个形状为 (2, 3, 4) 的全一张量。
torch.tensor(data, data_type=None, device=device.default(), requires_grad=False, pin_memory=False):生成基于现有数据源(例如基于Python的列表或嵌套列表)的张量对象。
* 例如 z = torch.tensor([[2, 1, 3, 4], [1, 2, 3, 4], [4, 3, 1, 2]])。
2. 张量属性和元素信息:
-
tensor.shape:返回张量的形状- 例如
x.shape可以查看张量 x 的形状。
- 例如
-
tensor.numel():返回张量中元素的总数。- 例如
x.numel()会返回 x 中元素的数量。
- 例如
-
tensor.dtype:获取张量的数据类型 -
例如,在
x = torch.arange(12, dtype=torch.float32)中通过指定dtype参数将数据类型设置为float32。
3. 张量形状变换函数:
- tensor.reshape(*shape) 能够重新排列张量的元素而不影响其值。
- 如 X = x.reshape(3, 4),这会将x转换为一个形状为 (3, 4) 的新张量。
4. 张量的元素级运算:
标准算子(+, -, *, / 和 **): 这些算子对 tensor 执行按元素加法、减法、乘法、除法以及幂运算。这些操作会逐个元素地处理输入 tensor 并生成输出 tensor。如 x + y 所示,则是按元素相加;x ** y 则是计算幂。
该函数用于计算输入张量中每个元素的指数值,并通过指定out参数可选地将结果存储到指定位置。例如,在调用此函数时,默认情况下它会返回一个新的张量包含所有输入值对应的指数值。
5. 张量连接函数:
torch.cat(tensors, dim=0):沿指定维度对多个张量进行连接。- 例如,在沿指定维度进行操作时使用
torch.cat()函数,在第 0 维合并 x 和 y 张量时会调用torch.cat((x, y), dim=0)。当沿第 1 维合并 x 和 y 张量时,则会调用torch.cat((x, y), dim=1)。
6. 张量的比较运算:
- 执行元素级的等值比较操作
tensor1 == tensor2,将会得到一个二进制张量 。 - 比如
x == y将创建一个与 x 和 y 形状相同的张量,在其中位置上表示 x 和 y 对应位置上的元素是否相等。
7. 张量的元素访问和修改:
-
使用
tensor[index]可以实现对张量元素的直接获取。 -
示例
x[-1]能够获取向量x的最后一个元素。 -
此外,在
x[1:3]中可以通过范围索引来获取第二行至第三行的元素。 -
利用索引实现对张量元素的修改
-
具体而言,在矩阵中使用索引进行操作时:
- 在矩阵x中使用索引x[1,2]赋值9,则表示在矩阵x中取第(下标从0开始)第(下标从0开始)位置上的元素并将其设置为数值9。
- 使用切片操作取矩阵x的第一至第二行的所有列并赋值为数值12。
- 使用切片操作取矩阵x的第一至第二行以及第二至第三列并将其各元素均设置为数值88。
8. 张量的内存操作:
id(tensor):该函数生成唯一的标识符以区分不同的张量实例。- 例如,在代码中使用
before = id(y)可以记录变量 y 的初始标识符值;随后通过y = y + x这样的操作可能会导致分配新的内存空间(实际应用中应尽量避免此类情况)。
in-place operation:为了实现这一目标,请考虑使用 x[:] = x + y 或者采用更为简洁的方式如 x += y 来完成运算。其中 x += y 将直接将结果存储在变量中,并且无需额外的空间开销。
9. 张量和 numpy 数组的转换:
-
tensor.numpy()实现了张量到 numpy 数组的转换功能。 -
例如
A = x.numpy()可用于将张量 x 转换为 numpy 数组 A。 -
使用
torch.tensor(array)来实现numpy数组转为张量。 -
例如通过
B = torch.tensor(A)完成从numpy数组A到张量B的转换。
张量与标量之间的转换在实际应用中非常常见,在操作过程中我们常常需要对它们进行相应的处理和计算。 -
tensor.item():单元素张量会返回Python标量值。 -
例如
a.item()将单元素张量a转换为Python标量值,并可进一步获取不同数据类型的Python标量。
10. 相关练习代码如下:
import torch
# 张量表示一个数值组成的数组,这个数组可能有多个维度
x = torch.arange(12)
x
# 可以通过张量的shape属性来访问张量的 形状 和 张量中的元素的总数
x.shape
x.numel() # 元素的总数:永远是个标量,这里是12
# 要改变一个张量的形状而不改变元素数量和元素值,可以调用reshape函数
X = x.reshape(3, 4)
X
# 使用全0、全1、其他常量或者从特定分布中随机采样的数字
torch.zeros((2, 3, 4))
torch.ones((2, 3, 4))
# 通过提供包含数值的python列表(或嵌套列表)来为所需张量中的每个元素赋予确定值
z = torch.tensor([[2, 1, 3, 4], [1, 2, 3, 4], [4, 3, 1, 2]])
z.shape
# 常见的标准算术运算符(+、-、*、/和**)都可以被升级为按元素运算
x = torch.tensor([1.0, 2, 4 ,8])
y = torch.tensor([2, 2, 2, 2])
x + y, x - y, x * y, x / y, x**y # ** 是求幂运算
# 按元素方式应用更多的计算
torch.exp(x)
# 可以把多个张量连结在一起
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]])
torch.cat((x, y), dim=0) # 在第0维合并
torch.cat((x, y), dim=1) #
x == y
# 对张量中的所有元素进行求和会产生一个只有一个元素的张量
x.sum()
# 即使形状不同,可以通过调用 广播机制(broadcasting mechanism)来执行按元素操作
a = torch.arange(3).reshape((3, 1))
b = torch.arange(2).reshape((1, 2))
a, b
a + b
# 可以用-1 选择最后一个元素,可以用[1:3]选择第二个和第三个元素
x[-1], x[1:3] # x[-1]: 最后一行; x[1:3]:第一行和第二行
# 除了读取之外,还可以通过指定索引来将元素写入矩阵
x[1, 2] = 9
x
# 为多个元素赋值相同的值,只需要索引所有元素,然后为它们赋值
x[0:2, :] = 12
x
x[0:2, 1:3] = 88
x
# 运行一些操作可能会导致为新结果分配内存
before = id(y) # 将y的id存起来
y = y + x
id(y) == before
# 执行原地操作
z = torch.zeros_like(y)
print('id(z):', id(z))
z[:] = x + y
print('id(z):', id(z))
# 如果在后续计算中没有重复使用x,也可以使用x[:] = x + y 或 x += y 来减少操作的内存开销
before = id(x)
x += y
id(x) == before
# 转为numpy张量
A = x.numpy()
B = torch.tensor(A)
type(A), type(B)
# 将大小为1 的张量转换为python标量
a = torch.tensor([5.3])
a, a.item(), float(a), int(a)
数据预处理:
1. 文件操作相关函数:
os.makedirs(path, exist_ok=False):生成目录 。通过调用 os.path.join('..', 'data') 来构建路径。当 exist_ok=True 时,在目录已存在的情况下也不会触发异常。
* 例如 `os.makedirs(os.path.join('..', 'data'), exist_ok=True)` 创建 `../data` 目录。
open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None):打开指定文件 ,‘w’ 模式用于实现文字的写入功能。
* 例如 `with open(data_file, 'w') as f` 以写入模式打开文件。
file.write(str):向文件中写入字符串 。
* 例如 `f.write('NumRooms,Alley,Price\n')` 向文件中写入列名和数据样本。
2. 数据读取和处理:
pd.read_csv():从 CSV 文件读取数据为 DataFrame。
* 例如 `data = pd.read_csv(data_file)` 从 CSV 文件 data_file 读取数据。
DataFrame.iloc[rows, columns]:基于整数位置进行数据选取 。
比如,在数据集中
DataFrame.fillna(value=None, method=None, axis=None, inplace=False, limit=None, downcast=None):该函数采用指定的方式或策略来填补缺失的数据。
比如 inputs = inputs.fillna(inputs.mean()) 采用 inputs 列的平均值替代 inputs 中的缺失值。
pd.get_dummies(data, prefix=None, prefix_sep='_', dummy_na=False, columns=None, sparse=False, drop_first=False, dtype=None,):将输入数据中的分类变量转换为虚拟/指示变量,并通过指定的参数对其进行配置,默认情况下会生成一个稀疏矩阵形式的结果矩阵。
例如 inputs = pd.get_dummies(inputs, dummy_na=True) 将输入数据中的分类变量(包括缺失值)转化为虚拟编码。
3. 张量操作相关函数:
torch.tensor(data, dtype=None, device=None, requires_grad=False, pin_memory=False):用于生成一个张量。- 例如
X, Y = torch.tensor(inputs.values), torch.tensor(outputs.values)表示将 inputs 和 outputs 的值映射为相应的张量。(比较少用)
4. 相关代码练习
# 数据预处理
# 创建一个人工数据集,并存储在csv(逗号分隔值)文件
import os
os.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')
# 从创建的csv 文件中加载原始数据集
import pandas as pd
data = pd.read_csv(data_file)
print(data)
data
# 为了处理缺失的数据,典型的方法是 插值、删除,这里,将考虑前者
inputs, outputs = data.iloc[:, 0:2], data.iloc[:, 2] # 将前三列放在inputs里面,将最后一列放在outputs里面
inputs = inputs.fillna(inputs.mean())
inputs
# 对于inputs中的类别值或离散值,将“NaN”视为一个类别
inputs = pd.get_dummies(inputs, dummy_na=True)
print(inputs)
# 现在inputs和outputs中的所有条目都是数值类型,它们可以转换为张量格式,深度学习一般使用float32的类型进行数据处理
import torch
X,Y = torch.tensor(inputs.values), torch.tensor(outputs.values)
X,Y
线性代数
1. 张量的基本操作:
-
标量 :单元素数组可通过
x = torch.tensor([3.0])实现,在支持基本算术运算的基础上还包括幂运算。 -
向量 :可视为由标量构成的一维列表结构,默认使用
torch.arange()方法初始化,并通过索引访问各元素值。 -
矩阵 :通过结合
torch.arange()与.reshape()方法创建任意维度数组,默认构造形如(5,4)二维数组后可执行转置操作.T并判断其对称性特征`. -
高维张量 :多轴数据结构可通过
.reshape(2, 3, 4)等方式灵活构建。 -
张量运算:
- 形状一致的多维数组可实现按对应位置执行的基本算术操作。
- 按维度求取总和值时需注意结果维度的变化特点。
- 计算平均值时可选择性地保持中间计算结果的维度特征。
- 累积求和操作需明确输出结果的空间分布特性。
-
特殊运算包括:
-
点积:可利用 torch.dot 函数或 torch.sum 函数来计算两个向量的点积。
-
矩阵与向量的乘法:采用 torch.mv 函数来完成矩阵与向量相乘的操作。
-
矩阵之间的乘法:可以通过 torch.mm 方法实现两个矩阵之间的相乘运算。
-
范数计算:
-
L2范数(也称欧几里得范数)是衡量向量大小的一种方式;它等于向量各分量数值平方之和后取其平方根;
-
L1范数(也称曼哈顿距离)则是向量各个分量绝对值之和;
-
F范数(Frobenius范式)用于衡量矩阵大小;它是将矩阵所有元素先取其各自数值的绝对值再取其平均或其他统计方法;
2. 矩阵计算(针对导数)
-
标量导数
-
比如说,在某些情况下(例如变量a不依赖于x时),其标量导数值为零;例如sin x的标量导数值即为其对应的余弦函数cos x… 众所周知,在微积分学中标量函数在某一点处的标量导数值代表了曲线在该点处的切线斜率;对于复合函数y = f(u),其中u = g(x),根据链式法则可得其总微分为dy = f’(u) du;因此,在这种情况下求解复合函数的总微分时需要应用链式法则来计算各部分的变化率之积。
-
亚导数(将导数)
- 将导数拓展到不可微的函数,例如y = |x|
-
梯度
-
将导数拓展到向量

情况1:y是一个标量,x是一个向量

梯度一定指向的是 值变化最大的方向
情况2:y是一个向量,x是一个标量

情况3:y是一个向量,x是一个向量

- 将输入拓展到矩阵
3 相关代码练习
# 线性代数
# 标量由只有一个元素的张量表示
import torch
x = torch.tensor([3.0])
y = torch.tensor([2.0])
x + y, x * y, x / y, x ** y
# 向量: 可以看作由标量组成的列表
x = torch.arange(4)
x
# 通过张量的索引来访问任一元素
x[2], x[3]
# 访问张量的长度
len(x)
# 只有一个轴的张量,形状只有一个元素
x.shape
#通过指定两个分量m 和 n 来创建一个形状为m * n的矩阵
A = torch.arange(20).reshape(5, 4)
A
# 矩阵的转置
A.T
# 对称矩阵(symmetric matrix) A 等于其转置:A = A^T
B = torch.tensor([[1, 2, 3], [2, 0, 4], [3, 4, 5]])
B
B == B.T
# 就像是 向量是标量的推广, 矩阵是向量的推广一样,我们可以构建具有更多轴的数据结构
x = torch.arange(24).reshape(2, 3, 4)
x
x = torch.arange(24).reshape(4, 3, 2)
x
# 给定具有相同形状的任何两个张量,任何按元素二元运算的结果都将是相同形状的张量
A = torch.arange(20, dtype=torch.float32).reshape(5, 4)
B = A.clone() # 通过分配新内存,将A的一个副本分配给B
A, A + B
# 两个矩阵的按元素乘法称为:哈达玛积(hadamard product)
A * B
a = 2
X = torch.arange(24).reshape(2, 3, 4)
a + X, (a * X).shape
# 计算其元素的和
x = torch.arange(4, dtype=torch.float32)
x, x.sum()
# 表示任意形状张量的元素和
A = torch.arange(20).reshape(5, 4)
A.shape, A.sum()
A = torch.arange(20 * 2, dtype=torch.float32).reshape(2, 5, 4)
A, A.shape, A.sum()
# 指定求和汇总张量的轴
A_sum_axis0 = A.sum(axis=0)
A_sum_axis0, A_sum_axis0.shape
A_sum_axis1 = A.sum(axis=1)
A_sum_axis1, A_sum_axis1.shape
A.sum(axis=[0, 1]) # 和A.sum()类似
A.sum(axis=[0, 1]).shape
# 一个与求和相关的量是 **平均值** (mean或average)
A.mean(), A.sum() / A.numel()
A.mean(axis=0), A.sum(axis=0)/A.shape[0]
# 计算总和或均值时保持轴数不变
sum_A = A.sum(axis=1, keepdims=True)
A, sum_A
# 通过广播将A 除以sum_A (使用广播的前提:两个维度必须一致)
A / sum_A
# 某个轴计算A元素的累积总和
A.cumsum(axis=0)
# 点积是相同位置的按元素乘积的和
y = torch.ones(4, dtype=torch.float32)
x, y, torch.dot(x, y)
# 可以通过执行按元素乘法,然后进行求和来表示两个向量的点积
torch.sum(x * y)
# 矩阵向量积Ax是一个长度为m的列向量,其第i个元素是点积(ai)^T x
A = torch.arange(20, dtype=torch.float32).reshape(5, 4)
A.shape, x.shape, torch.mv(A, x)
#可以将矩阵-矩阵乘法AB看作是简单地执行m次矩阵-向量积,并将结果拼接在一起,形成了一个n * m 矩阵
B = torch.ones(4, 3)
torch.mm(A, B)
# L2范数是向量元素平方和的平方根
u = torch.tensor([3.0, -4.0])
torch.norm(u)
# L1 范数:表示为向量元素的绝对值之和
torch.abs(u).sum()
# 矩阵的F 范数:是矩阵元素的平方和的平方根
torch.ones((4, 9)), torch.norm(torch.ones((4, 9)))
# 关于sum的补充
import torch
a = torch.arange(20*2, dtype=torch.float32).reshape((2, 5, 4))
a.sum(axis=1), a.sum(axis=1).shape # [2, 4]
a.sum(axis=0), a.sum(axis=0).shape # [5, 4]
a.sum(axis=[0, 2]), a.sum(axis=[0, 2]).shape # [5]
a.sum(axis=1, keepdims=True), a.sum(axis=1, keepdims=True).shape # [2, 1, 4]
a.sum(axis=[0, 2], keepdims=True), a.sum(axis=[0, 2], keepdims=True).shape # [1, 5, 1]
向量链式法则

自动求导
自动求导计算一个函数在指定值上的导数
计算图
* 将代码分解成操作子
* 将计算表示成一个无环图

自动求导的两种模式

反向累积总结:
- 构建计算图
- 前向:运行图并记录中间结果
- 逆序:从逆序方向运行图
- 剪枝:去除不必要的分支

反向复杂度:
* 计算复杂度:O(n), n是操作子个数
* 通常正向和反向的代价类似
- 空间复杂度:O(n),由于所有中间状态的存储需求
- 跟正向累积比较:
- 计算单个变量梯度所需的时间复杂度为O(n)
- 内存占用维持在O(1)水平
自动求导的实现
深度学习架构通过利用自动微分技术(automatic differentiation)来实现高效的梯度计算。
在实际应用中,在现实场景下(computational graph)被用来建立一个用于追踪数据是如何通过运算结合生成输出的过程的结构。该结构被称为计算图(computational graph)。自动生成导数过程(autodiff)使得系统能够随后实现梯度反向传播(backpropagation of gradients)。
这里指出反向传播(backpropagation)是一种用于计算并填充所有参数相对于损失函数的梯度的方法,在深度学习中被广泛采用以优化模型权重。
- 自动求导 例子:
假设想对函数 y = 2(x^T) * x,关于列向量x求导
import torch
x = torch.arange(4.0)
在计算y 关于x 的梯度之前,需要一个地方来存储梯度
x.requires_grad_(True) # 等价于 x = torch.arange(4.0, requires_gard=True)
x.grad # 默认值是None
计算y
y = 2 * torch.dot(x, x)
通过调用反向传播函数来自动计算y关于x每个分量的梯度
y.backward() # 求导
x.grad # 访问导数
x.grad == 4 * x
在默认情况下,pytorch会累积梯度,需要清除之前的值
x.grad.zero_() # 将所有梯度清零
y = x.sum()
y.backward()
x.grad
在深度学习中,并非目标是计算单个样本的微分矩阵;而是每个样本分别计算其偏导数之和
x.grad.zero_() # 将所有梯度清零
y = x * x # 当y也是一个向量的时候,在数学中,得到的是一个矩阵,但在深度学习中,会先将y进行求和在调用backward
# 等价于y.backward(torch.ones(len(x)))
y.sum().backward()
x.grad
有时候需要对某些计算进行分离处理:将特定运算从记录式计算图中剥离出来 ,例如我们可以设定变量y为仅依赖于变量x的结果 ,而变量z则由变量y和变量x共同决定 。设想我们要对z关于x求导数 ,但由于特殊需求希望将中间结果y视为固定值 ,并专注于后续步骤中与x相关的运算影响
我们可以从y中分离出一个新变量u(通过 detach() 方法);这个新变量与原变量y具有完全相同的数值属性,并且能够避免在反向传播过程中使用构建计算图时所依赖的关于如何计算原变量y的具体操作信息。换句话说,在深度学习模型的反向传播过程中,梯度并不会流向这个被断开的新变量u进而影响原始输入变量x的变化量
在此基础上,该反向传播函数计算∂z/∂x时使用乘积公式,并将u视为与x无关的常数值。这与直接计算∂(x³)/∂x的方式不同。
x.grad.zero_()
y = x * x
u = y.detach() # 常数,不计算梯度
z = u * x
z.sum().backward()
x.grad == u, u
x.grad.zero_()
y.sum().backward()
x.grad == 2 * x
即使在搭建函数时需依赖Python中的流程控制(例如条件判断、循环结构或其他任意函数调用),也能成功求得变量梯度
def f(a):
b = a
while b.norm() < 1000:
b = b
if b.sum() > 0:
c = b
else:
c = 100 * b
return c
# 分析f(a),对于任何a,存在某个常量标量k,使得f(a)=k*a,其中k的值取决于输入a,因此可以用d/a验证梯度是否正确。
a = torch.randn(size=(), requires_grad=True) # 创建一个形状为空的张量 a,并且将其设为需要计算梯度
d = f(a)
d.backward()
a.grad == d / a
概述:深度学习能够自动生成导数的过程:首先是将梯度附加到想要对其求偏导的那个变量上;接着记录目标值的运算过程,并调用反向传播函数来获取相关的梯度信息。
