深度学习的前置知识——动手学AI笔记(Chapter 1 & 2)

jkm Lv3

0. 笔记前言

其实在这个时代,在这个llm与agent百花齐放的时代,对于ai从业者来说,到底还有没有必要从基础学起,已经是个问题了。我提出这个疑问是基于我的亲身经历,在大二下我接触了第一个深度学习项目,而在此之前我只用三天时间囫囵吞枣地看完了黑马程序员的机器学习网课,最后这个项目还算成功,我们发了论文,也发了专利软著。而从接触这个项目到毕业,我完全秉持着“干中学”的思想,哪里不会就查哪里,完全没有系统地学习过深度学习。

而这种看法似乎正在成为大势所趋。何恺明在一次MIT的讲座中,指出下一代编程语言很可能不再是cpp或python,而就是自然语言。可能这个说法现在看来尚且比较大胆,但事实就是当今的ai从业者,似乎越来越没有必要“懂ai”。我们现在有gpt、deepseek帮我们读代码,有cursor帮我们写代码,人类的角色似乎就是一个架构师的角色,给出方案,交由ai执行。与朋友的交流印证了这个想法,据说朋友在实习期间看见了一个学机械的大哥,git、docker都不会用,但却手握好几篇论文,可以说是vibe coding的大师级人物了。

所以说回来,我其实目前也说不好啃这种ai基础书籍的意义还剩多少,可能是出于计算机科班的一些幽默的“尊严”、对“调包侠”名号的鄙夷(其实我觉得调包侠也没什么的,都是为了吃饭,不寒碜),可能是对“夯实基础”的一些迷信,也可能是硕士的课程与理论直接相关,还有可能是我现在实在是太闲了。不过不管怎么说,我还是决定来啃一下这本书。大概在一年前我曾试图读过这本书,当时心态还比较浮躁,没啃下来,希望这次不会半途而废。

这篇笔记记录于2025.7.20,距离去新加坡还有一礼拜,希望在硕士课程开始之前我可以学完,写完会放到博客上。笔记我会尽量写的通俗易懂,所以我会加入很多自己的理解,不一定全对。


1. 引言

机器学习的关键组件:

  • data
  • model
  • objetive function
  • algorithm(用来调整模型参数以优化目标函数的算法,如梯度下降)

机器学习的问题类别:

  • 监督学习
    • 回归
    • 分类
    • 标记:这个问题类别还是第一次见。与分类的区别是标记问题是学习预测不相互排斥的类别,比如一个实际问题是给博客或者文献打上tag。一篇博客可能涵盖多个tag,并不像分类问题一样是非此即彼的。
    • 搜索:根据query在信息库里检索answer,并给出可靠的排序。
    • 推荐系统:为“用户”和“物品”的匹配性打分
    • 序列学习:以上所有问题有一个特点就是输入的特征都是完全孤立的,并且形状固定。但如果输入是连续的,就需要模型考虑上下文,这就是序列学习。序列学习的输入输出size不固定。
  • 无监督学习
  • 强化学习:以下是强化学习的一些Overview
    • 与环境交互:不管是有监督与无监督学习,他们都是不与环境交互的,也就是off-line的。但强化学习是与环境实时交互的。
    • 框架

      在强化学习问题中,智能体(agent)在一系列的时间步骤上与环境交互。 在每个特定时间点,智能体从环境接收一些观察(observation),并且必须选择一个动作(action),然后通过某种机制(有时称为执行器)将其传输回环境,最后智能体从环境中获得奖励(reward)。

    • 强化学习与监督学习的异同:
      • 联系:任何监督学习问题可以转化为强化学习问题。例如“预测”可以看成是一个“动作”,“损失函数”可以看作“奖励“
      • 区别:强化学习不会直接告诉模型正确答案(label),而是引导模型总结哪些行为会获得奖励。

机器学习、表示学习与深度学习这几个概念之间的关联:

  • 机器学习肯定是范围最广的,涵盖了后两个
  • 表示学习研究的重点是如何自动找到合适的数据表示方式
  • 深度学习首先要深度,有很多线性与非线性层的叠加。其也被称为多层表示学习,通过学习多层次的转换来进行的多层次的表示学习。

2. 预备知识

借此机会好好补一下numpy的基本操作

2.1 数据操作

2.1.1 创建tensor

从创建tensor开始。tensor其实就是多维数组,深度学习框架里叫tensor,numpy里叫ndarray,区别是前者支持gpu与自动微分,后者只支持cpu。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import torch 

### 创建行向量
x = torch.arrange(12)
# 输出:tensor([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])

### 查看形状
x.shape
# 输出:torch.Size([12])

### 改变形状
# ps: 可以自动计算,下面代码等价于 x.reshape(-1,4) 或者 x.reshape(3,-1)
X = x.reshape(3, 4)

### 各种初始化
# 0初始化
torch.zeros((2, 3, 4))
# 1初始化
torch.ones((2, 3, 4))
# 随机初始化
torch.randn(3, 4)
# 列表初始化,可以手动指定元素
torch.tensor([[2, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]])

2.1.2 运算符

以下运算符都是逐元素操作。可以看到,这个例子中,如果有一个元素是float,那所有结果就会自动为float。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
x = torch.tensor([1.0, 2, 4, 8])
y = torch.tensor([2, 2, 2, 2])

### 加法
x + y
# 输出:tensor([ 3., 4., 6., 10.])

### 减、乘、除都一样,以此类推
x - y, x * y, x / y

### 幂运算
x ** y
# 输出:tensor([ 1., 4., 16., 64.])

### e的指数
torch.exp(x)

把多个tensor进行拼接操作:

1
2
3
4
5
6
7
8
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)

### 按列拼接
torch.cat((X, Y), dim=1)

通过逻辑运算符构建二元张量:

1
X == Y

对张量中的所有元素进行求和:

1
X.sum()

2.1.3 广播机制

这块之前不熟,写的细一点
对两个形状不同的tensor进行逐元素运算。有两个步骤:

  • 先通过复制元素来扩展一个或两个数组,保证形状一样
  • 逐元素运算
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    a = torch.arange(3).reshape((3, 1))
    b = torch.arange(2).reshape((1, 2))
    '''
    此时,a:
    tensor([[0],
    [1],
    [2]]
    b:
    tensor([[0, 1]]
    '''
    a + b
    '''
    输出:
    tensor([[0, 1],
    [1, 2],
    [2, 3]]
    '''
    上述例子中,就是把a复制列,b复制行。总而言之就是把维度小的往大的复制

2.1.4 索引和切片

与python的列表一样,省略

2.1.5 节省内存

如果直接执行以下代码,会发现结果为False

1
2
3
before = id(Y)
Y = Y + X
id(Y) == before

这里应该是python语言本身的问题,python认为用户新建了一个Y对象,所以导致如果这段代码在一个循环中,那么内存会极大浪费。
解决方法:

1
2
3
4
5
6
### 方法1:创建一个新变量,**使用索引逐元素赋值**保证地址不变。
Z = torch.zeros_like(Y)
Z[:] = X + Y

### 方法2:使用+=
X += Y

2.1.6 转换为其他Python对象

ndarray和tensor互转:

1
2
A = X.numpy()
B = torch.tensor(A)

2.2 数据预处理

运用pandas处理数据,这部分比较熟了

1
2
3
4
5
6
7
8
9
10
11
12
import pandas

# 读取数据并划分输入输出
data = pd.read_csv(data_file)
inputs, outputs = data.iloc[:, 0:2], data.iloc[:, 2]

# 用平均值填补缺失值,也可以删除,dropna()
inputs = inputs.fillna(inputs.mean())

# 转化为tensor
X = torch.tensor(inputs.to_numpy(dtype=float))
y = torch.tensor(outputs.to_numpy(dtype=float))

2.3 线性代数

对单独一个矩阵进行操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 创建矩阵:arrange/ones_like + reshape
A = torch.arange(20).reshape(5, 4)

# 转置矩阵
A.T

# 求和
A.sum()
# 按某一维度求和
A_sum_axis1 = A.sum(axis=1)

# 求平均
A.mean()
# 按某一维度求平均
A.mean(axis=0)

对矩阵之间进行操作(以下操作均需遵循线性代数的维度要求):

1
2
3
4
5
6
7
8
9
### 点积
# 向量 * 向量
torch.dot(x, y) # 等同于 torch.sum(x * y)

# 矩阵 * 向量
torch.mv(A, x)

# 矩阵 * 矩阵
torch.mm(A, B)

范数

  • 概念:是向量空间中度量大小的一种方式
  • 假设有一个计算向量范数的函数,给定向量,需要满足以下性质:
  1. 放缩向量的所有元素, 其范数也会按相同常数因子的绝对值缩放:
  2. 三角不等式:
  3. 非负性:

    当且仅当向量全为0,等号成立
  • 各种范数
    • 范数:
      $$ |\mathbf{x}|1 = \sum{i=1}^n |x_i| $$
    • 范数:
      $$ |\mathbf{x}|2 = \sqrt{\sum{i=1}^n x_i^2} $$
    • 范数:
      $$ |\mathbf{x}|p = \left( \sum{i=1}^n |x_i|^p \right)^{1/p} $$
1
2
3
4
5
6
7
8
9
u = torch.tensor([3.0, -4.0])

# L2 范数
torch.norm(u)
# 输出:5

# L1 范数
torch.abs(u).sum()
# 输出:7

2.4 微积分

这一章简单回顾了一下微积分几个重要的公式与概念,和高数重合度很高,就简单记记。

  • 导数:
    • 定义:
    • 求导法则,这里就不写了
  • 偏导数:
    • 定义:
  • 梯度:
    • 定义:

2.5 自动微分

上一小节是理论补充,这一小节是代码实现,为后面的梯度下降做准备。

2.5.1 标量变量的反向传播

首先是标量变量的反向传播,例子:
最终输出的结果与 一致。注意这个式子中只有一个未知数,不需要求偏导,相当于求一次导带入不同值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import torch

x = torch.arange(4.0)
x.requires_grad_(True) # 保存梯度

y = 2 * torch.dot(x, x)
# output: 28

y.backward()
x.grad
# output: tensor([ 0., 4., 8., 12.])

# 在默认情况下,PyTorch会累积梯度,我们需要清除之前的值
x.grad.zero_()

2.5.2 非标量变量的反向传播

原文中有这么一句话,没太看懂,所以需要先做一下补充:

当y不是标量时,向量y关于向量x的导数的最自然解释是一个矩阵。 对于高阶和高维的y和x,求导的结果可以是一个高阶张量。

这句话意思是说,当 是一个向量时,它关于另一个向量 的导数会得到一个矩阵,称为 雅可比矩阵(Jacobian Matrix),例如:

  • 是一个 m 维向量,且每个 都是关于 的函数。
  • 那么 是一个 的矩阵, 的列数
  • 公式为:
  • 更高维:如果 是一个 的矩阵, 是一个 的矩阵,那么雅可比矩阵是一个四维张量。

然而,在深度学习中,并不需要计算如此庞大的雅可比矩阵,因为损失函数通常是标量(比如均方误差、交叉熵等),而我们需要计算的是这个标量损失函数对模型参数的梯度,以便进行参数更新(如梯度下降)。

所以在这一小节的代码中,整体思想就是将非标量进行求和,然后进行反向传播。

1
2
3
4
5
6
7
8
# 对非标量调用backward需要传入一个gradient参数,该参数指定微分函数关于self的梯度。
# 本例只想求偏导数的和,所以传递一个1的梯度是合适的
x.grad.zero_()
y = x * x
# 等价于y.backward(torch.ones(len(x)))
y.sum().backward()
x.grad
# output: tensor([0., 2., 4., 6.])

2.5.3 分离计算

如果希望将一个大函数的某一部分禁用梯度计算,就用detach。

1
2
3
4
5
6
7
y = x * x
u = y.detach()
z = u * x

z.sum().backward()
x.grad == u
# output: tensor([True, True, True, True])

这样就保证了 中,这里的u是常数,求导就是u,而非

2.6 概率

概率同样也是在学考研数学的时候复习过的了,简单记记。

  • 基本概率论:有一句话描述的比较有意思

    笼统来说,可以把分布(distribution)看作对事件的概率分配

  • 联合概率:
  • 条件概率:
  • Bayes定理:
  • 边缘概率:
  • 期望:
    • 随机变量 的期望:
    • 函数 的期望,x是从分布 中抽取的随机变量:
  • 方差:
    • 随机变量 的方差:平方的期望减期望的平方
    • 随机变量函数

      随机变量函数的方差衡量的是:当从该随机变量分布中采样不同值时,函数值偏离该函数的期望的程度

  • 标题: 深度学习的前置知识——动手学AI笔记(Chapter 1 & 2)
  • 作者: jkm
  • 创建于 : 2025-08-09 16:31:13
  • 更新于 : 2025-08-09 16:45:29
  • 链接: https://goldenkm.github.io/2025/08/09/D2L-Note-Chapter1-2/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论