《动手学深度学习》笔记(一)

EliorFoy Lv3

第一讲 杂谈+学前准备

1. 关于深度学习

​ 本人是一名大学生,很不幸运在大学之前对深度学习一无所知。自从ChatGPT爆火全球,我才逐渐关注到原来“智能”与深度学习息息相关。本书《动手学深度学习》其实有另一套理论版《DeepLearning》(书的封面是一片花海故被称作“花书”),也是一套很经典的教程,但是由于整篇比较注重数学的推算可以说是很难啃;而本书由书名就可以知道案例比较丰富,“动手学”才能让抽象的变形象。本书第一章没有很多内容,大致是介绍一下深度学习到底是什么。个人觉得重要的是知道“监督学习”、“无监督学习”、“与环境互动”以及“强化学习”这些概念。按照我的理解,监督学习是需要为数据打上标签,自发学习就是无监督学习,与环境互动中有反馈是强化学习。

2. 环境配置

​ 本书配备有完整的电子教程 (其实电子教程已经涵盖了实体书的所有内容,所以对于想要学习DL的同志,这些资料都是开源的)。由于我之前有学过Python,对Python的虚拟环境算是比较熟悉,我没有使用书上的教程进行环境配置,书上的Python环境使用的是Miniconda,Miniconda是一个轻量级的Python发行版本,创建和管理Python环境比较方便所以应用于数据科学方面比较广泛。

2.1 安装Jupyter

​ 我个人使用的是Python3.12,可以手动创建venv虚拟环境然后执行pip install jupyter就能安装jupyter了,至于jupyter是什么 可以自己去搜一下就明白为什么有这个东西了。jupyter notebook有插件可以安装,但是似乎在7.x版本的notebook中不再支持nbextensions插件,需要手动把notebook的版本降为6.x。插件不是必要的,安装完成后输入jupyter notebook即可以启动webui界面。

2.2 安装CUDA、cuDNN和Pytorch

​ 深度学习一般是矩阵计算,而GPU比CPU更适合做这些计算,要调用GPU就需要安装驱动,NVIDIA系列的显卡有CUDA工具包方便我们调用,而cuDNN 是 NVIDIA 的 CUDA 深度神经网络库,可以显著提高训练速度和推断速度,需要匹配对应的cuda版本。而安装Pytorch框架也需要匹配对应的cuda版本,等我安装pytorch的时候发现我装的cuda版本是12.6而pytorch匹配的最新版本是12.4,所以在此告诉大家安装的时候一定要看清所有匹配的版本再安装。CPU也不是不能够用…等到算不够的情况下再换cuda吧。

2.3 云端环境

​ 对于没有显卡的小伙伴,网上有很多免费的GPU资源(转载) 可以使用。

第二讲 预备知识

​ 本讲主要介绍的就是一些数学基础以及Python常用操作。

1.数据操作

1
2
3
4
5
6
7
8
9
10
import torch  # 后续操作几乎都用此库
x = torch.arange(12) # 0-11的张量
len(x) # 几行
x.shape # 几行几列
x.numel() # 总个数
x.reshape(3,4) # 变成三行四列,reshape(-1,4)输入-1会自动推算
torch.zeros(2,3,4)
torch.ones(2,3,4)
torch.randn(3,4) # 分别创建均为0,均为1,形状为(3,4)满足均值为0方差为1的张量
torch.tensor(l:list) # 输入数组创建张量

2.数据运算

1
2
3
4
5
6
# + - * / ** 都是对每个元素进行运算
torch.exp(x) # 对x的每个元素进行e^x
torch.cat(X,Y,dim=0) # 张量连接,dim为0是行连接,dim为1是列连接
X == Y # 返回对应元素相等对应位置上的元素为True的张量
x.sum() # 求和 可以指定axis=0是所有列分别相加形成的一行(沿着列),为1是所有行分别相加形成的一行,这个时候通过指定keepdims=True保持轴数不变还是一列
A.cumsum(axis=0) # 不会降低维度,相当于把x.sum()追加到了最后一行

广播机制:形状不同的张量进行操作的时候会进行自动复制扩充以匹配运算。

索引和切片:张量特性和Python数组特性类似有索引和切片

内存节省: X = X + YX += Y的区别,在Python中如果X是简单数据类型(如整数、浮点数、字符等)那么这两者没有区别,如果是复杂数据类型前者可能会重新创建一个对象并赋给X后者将在原有内存上进行赋值,所以后者是更加节省内存的(或者写作X[:] = X + Y)。

对象转换:将torch的张量转换为numpy的张量(ndarray),这两其实看上去都是Python的数组,转换也就理所当然了。转换后是共享底层内存的,所以对变量的操作将会相互影响。

1
2
3
A = X.numpy()
B = torch.tensor(A)
B.item() # 只有一个元素的张量(长度为1)可以直接转换为Python数据类型,也可以直接int(B),float(B)

3.数据预处理

​ 数据预处理是将原始数据处理成张量的过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 通过Python的pandas库读取csv文件的数据
import pandas as pd
data = pd.read_csv(data_file)
data.iloc # 位置索引相当于切片

data = {
'NumRooms': [None, 2.0, 4.0, None],
'Alley': ['Pave', None, None, None],
'Price': [127500, 106000, 178100, 140000]
}
data = pd.DataFrame(data)
inputs, outputs = data.iloc[:, 0:2], data.iloc[:, 2]
inputs = inputs.fillna(inputs.mean(numeric_only=True)) # 以平均值填充NaN(缺失的数据)
inputs = pd.get_dummies(inputs, dummy_na=True)
# pd.get_dummies() 函数将分类变量的每个类别转换为一个二进制列(0 或 1),其中 1 表示观察值属于该类别,0 表示不属于.
# dummy_na参数表示将NaN值单独作为一个类别
x = torch.tensor(inputs.to_numpy(dtype=float)) # 将pandas数据转换为pytorch的张量

4.数学基础

4.1 线代

1
2
3
4
5
6
7
A.T  # 矩阵A的转置
torch.dot(x,y) # x,y两个向量的点积
torch.mv(A,x) # 矩阵-向量积(matrix-vector product)
torch.mm(A,B) # 矩阵-矩阵乘法(matrix-matrix multiplication)
torch.norm(u) # L2范数,也就是求向量元素平方和的平方根
torch.abs(u).sum # 另一种L1范数,即绝对值求和
# L1范数和L2范数都是更一般的范数Lp(Frobenius范数)的特例,也是使用torch.norm方法

4.2 微积分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
%matplotlib inline
# 这是jupyter魔术命令将图像直接显示嵌入
import numpy as np
from matplotlib_inline import backend_inline
from matplotlib import pyplot as plt
def f(x):
return 3 * x ** 2 - 4 * x

# 以下就是一些绘图的代码,后续需要绘图可以重用,这部分代码已经包含在本书配套软件包d2l中
def use_svg_display(): #@save
"""使用svg格式在Jupyter中显示绘图"""
backend_inline.set_matplotlib_formats('svg')

def set_figsize(figsize=(3.5, 2.5)): #@save
"""设置matplotlib的图表大小"""
use_svg_display()
plt.rcParams['figure.figsize'] = figsize

#@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()

#@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()

#@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 plt.gca()

# 如果X有一个轴,输出True
def 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), X
elif 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)

x = np.arange(0, 3, 0.1)
plot(x, [f(x), 2 * x - 3], 'x', 'f(x)', legend=['f(x)', 'Tangent line (x=1)'])

非代码部分的偏导数以及梯度等等概念高数书中有详细介绍不在此赘述。

4.3 自动微分

​ 框架提供自动微分来加快求导,自动微分使系统能够随后反向传播梯度。这里,反向传播(backpropagate)意味着跟踪整个计算图,填充关于每个参数的偏导数。书中这句话说明了自动求导的意义,现在还不是很懂,后续遇到应该会有所了解。

  • x.requires_grad_(True) # 等价于x=torch.arange(4.0,requires_grad=True)

​ 这意味着在计算图中,x 将被视为一个需要计算梯度的节点。当你对这个张量执行操作并最终计算损失函数时,PyTorch 会自动跟踪所有对 x 的操作,并在反向传播时计算关于 x 的梯度。

1
2
3
4
5
6
x.grad  # 默认会是None
y = 2 * torch.dot(x, x) # 点积
y.backward()
x.grad # 通过调用反向传播函数来自动计算y关于x每个分量的梯度
x.grad.zero_() # 在默认情况下,PyTorch会累积梯度,我们需要清除之前的值

​ ==感觉这里需要补一下矩阵微分(向量微分) 矩阵梯度 以及反向传播的知识。(后续再补吧,头大)==

​ 以上的微分简而言之是下面这个公式:

xf(x)=[f(x)x1,f(x)x2,,f(x)xn]\nabla_{\mathbf{x}} f(\mathbf{x})=\left[\frac{\partial f(\mathbf{x})}{\partial x_1}, \frac{\partial f(\mathbf{x})}{\partial x_2}, \ldots, \frac{\partial f(\mathbf{x})}{\partial x_n}\right]^{\top}

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

​ ==到这里只是似懂非懂了…:cry:==

4.4 概率

1
2
3
4
5
6
7
8
9
%matplotlib inline
import torch
from torch.distributions import multinomial

# 模拟掷骰子的过程
fair_probs = torch.ones([6]) / 6 # 这个向量表示了一个公平的六面骰子每个面出现的概率。
multinomial.Multinomial(1, fair_probs).sample() # 创建一个多项式分布对象,其中1表示我们只抽取一个样本,fair_probs是我们刚才创建的公平概率向量。.sample()方法用于从这个分布中抽取一个样本。
multinomial.Multinomial(10, fair_probs).sample()
# tensor([0., 2., 1., 2., 4., 1.]) 可能结果10次独立的抽样,分别得到了上述的索引值,每个索引值对应于一个特定的结果。

第三讲 线性神经网络

​ 主要包含两个部分:线性回归和softmax回归。

3.1 线性回归

3.1.1 线性模型

​ 即线性拟合,这与高中所学过的概率知识契合,不做过多叙述(其实这里用矩阵描述的)。

3.1.2 损失函数

L(w,b)=1ni=1nl(i)(w,b)=1ni=1n12(wx(i)+by(i))2.L(\mathbf{w}, b)=\frac{1}{n} \sum_{i=1}^n l^{(i)}(\mathbf{w}, b)=\frac{1}{n} \sum_{i=1}^n \frac{1}{2}\left(\mathbf{w}^{\top} \mathbf{x}^{(i)}+b-y^{(i)}\right)^2 .

​ 损失函数就是描述估计值和观测值之间的误差的函数。在训练模型时,我们希望寻找一组参数(w,b)\left(\mathbf{w}^*, b^*\right), 这组参数能最小化在所有训练样本上的总损失。

w,b=argminw,bL(w,b).\mathbf{w}^*, b^*=\underset{\mathbf{w}, b}{\operatorname{argmin}} L(\mathbf{w}, b) .

3.1.3 解析解

yXw2\|\mathbf{y}-\mathbf{X} \mathbf{w}\|^2最小化(也就是让损失关于w\mathbf{w}的导数设为0)得到解析解:

​ ==这里是怎么推算的也不是很清楚:anguished:后续补上==

w=(XX)1Xy\mathbf{w}^*=\left(\mathbf{X}^{\top} \mathbf{X}\right)^{-1} \mathbf{X}^{\top} \mathbf{y}

​ 线性回归存在解析解,但不是所有问题存在解析解,所以无法广泛应用在DL。

3.1.4 随机梯度下降

​ 无法得到解析解的情况下,可以通过梯度下降的方法通过不断地在损失函数递减的方向上更新参数来降低误差。对于平方损失和仿射变换就是以下形式:

wwηBiBwl(i)(w,b)=wηBiBx(i)(wx(i)+by(i)),bbηBiBbl(i)(w,b)=bηBiB(wx(i)+by(i)).\begin{aligned} \mathbf{w} & \leftarrow \mathbf{w}-\frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \partial_{\mathbf{w}} l^{(i)}(\mathbf{w}, b)=\mathbf{w}-\frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \mathbf{x}^{(i)}\left(\mathbf{w}^{\top} \mathbf{x}^{(i)}+b-y^{(i)}\right), \\ b & \leftarrow b-\frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \partial_b l^{(i)}(\mathbf{w}, b)=b-\frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}}\left(\mathbf{w}^{\top} \mathbf{x}^{(i)}+b-y^{(i)}\right) . \end{aligned}

B|\mathcal{B}|表示每个小批量中的样本数,η\eta表示学习率,批量大小和学习率的值通常是手动预先指定(称为超参数),而不是通过模型训练得到的。 训练集上的损失达到最小并没有太大作用,难的是找到一组参数,这组参数能够在我们从未见过的数据上实现较低的损失(也称为泛化)。

3.1.5 矢量化加速

​ 使用循环遍历向量的效率是不如向量直接相加的,大概是因为这些库对运算已经做好了优化吧。

3.1.6 正态分布与平方损失

p(x)=12πσ2exp(12σ2(xμ)2).p(x)=\frac1{\sqrt{2\pi\sigma^2}}\mathrm{exp}\left(-\frac1{2\sigma^2}(x-\mu)^2\right).

1
2
3
4
5
6
7
8
# 正态函数
def normal(x, mu, sigma):
p = 1 / math.sqrt(2 * math.pi * sigma**2)
return p * np.exp(-0.5 / sigma**2 * (x - mu)**2)

# 绘制三个不同的正态分布
params = [(0, 1), (0, 2), (3, 1)]
d2l.plot(x,[normal(x,mu,sigma) for mu,sigma in params],xlabel='x',ylabel='p(x)',figsize=(4.5,2.5),legend=[f'mean {mu}, std {sigma}' for mu, sigma in params]) # figsize控制图形的显示大小,legend指定图例

​ 最小化负对数似然:

logP(yX)=i=1n12log(2πσ2)+12σ2(y(i)wx(i)b)2.-\log P(\mathbf{y}\mid\mathbf{X})=\sum_{i=1}^n\frac12\mathrm{log}(2\pi\sigma^2)+\frac1{2\sigma^2}\left(y^{(i)}-\mathbf{w}^\top\mathbf{x}^{(i)}-b\right)^2.

3.2 线性回归的从零开始实现

3.2.1 生成数据集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def synthetic_data(w, b, num_examples):  #@save
"""生成y=Xw+b+噪声"""
X = torch.normal(0, 1, (num_examples, len(w)))
y = torch.matmul(X, w) + b

# torch.matmul函数的行为取决于输入张量的形状:
# 如果两个输入都是1维张量(向量),它将执行点积(dot product)。
# 如果至少一个输入是2维张量(矩阵),它将执行矩阵乘法。
# 如果输入的维度超过2维,它将执行批量矩阵乘法。
y += torch.normal(0, 0.01, y.shape) # 将标准差设为0.01
return X, y.reshape((-1, 1))

true_w = torch.tensor([2, -3.4])
true_b = 4.2
features, labels = synthetic_data(true_w, true_b, 1000)
d2l.set_figsize()
d2l.plt.scatter(features[:, (1)].detach().numpy(), labels.detach().numpy(), 1)
# detach()方法用于创建一个新的张量,这个新张量与原始张量共享数据,但不包含梯度信息
# 需要注意的是,.numpy()方法只能用于CPU张量。如果张量在GPU上(即它的设备是CUDA),你需要先将其移动到CPU上,然后才能转换为NumPy数组。

后续越来越不知道是在干啥了,感觉还是需要配合《深度学习》(花书)把概念先过一遍才好理解,那就先改变学习计划吧,算是在计划之外了,高估了自己的基础…

  • 标题: 《动手学深度学习》笔记(一)
  • 作者: EliorFoy
  • 创建于 : 2024-10-12 17:21:47
  • 更新于 : 2024-10-12 17:22:22
  • 链接: https://eliorfoy.github.io/2024/10/12/大三上/《动手学深度学习》(一)/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。