动手深度学习
数据操作 + 数据预处理
N维数组
N维数组是机器学习和神经网络的主要数据结构。
0维 标量
1.0
表示一个类别
维 向量
[1.0, 2.7, 3.4]
表示一个特征向量
2维 矩阵
[[1.0, 2.7, 3.4]
[5.0, 0.2, 4.6]
[4.3, 8.5, 0.2]]
表示一个样本—特征矩阵
3维
[ [ [0.1, 2.7, 3.4]
[5.0, 0.2, 4.6]
[4.3, 8.5, 0.2] ]
[ [3.2, 5.7, 3.4]
[5.4, 6.2, 3.2]
[4.1, 3.5, 6.2] ] ]
如RGB图片(w * H * channels )
4维
一个RGB图片的批量 (批量大小 * 宽 * 高 * 通道)
5维
一个视频批量 (批量大小 * 时间 * 宽 * 高 * 通道)
数据操作
1 | import torch |
张量(tensor)表示一个数值组成的数组,这个数据可能有多个维度。
shape
属性:访问张量的形状。numel()
函数:访问张量中元素的总数。reshape()
函数:改变张量的形状而不改变元素数量和元素值。
1 | x = torch.arange(12) |
torch.zeros()
使用全0填充指定形状的张量torch.ones()
使用全1填充指定形状的张量torch.tensor()
为张量中的每个元素赋予确定值
1 | torch.zeros((2, 3, 4)) |
常见的标准运算符(
+
、-
、*
、/
和求幂**
)都可以被升级为按元素运算。1
2
3x = 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.cat()
连结张量
通过逻辑运算符构建二元张量
x.sum()
对张量中所有元素进行求和,会产生只有一个元素的张量。即使形状不同,仍然可以通过调用广播机制(维度的尺寸要么相等,要么其中一个维度为1)来执行按元素操作。
可以用
[-1]
选择最后一个元素可以用
[1:3]
选择第二个和第三个元素可以指定索引将元素写入矩阵
为多个元素赋值相同的元素,只需要索引所有元素,为其赋值。
将Numpy类型的张量转换为pytorch类型的张量
1
2A = X.numpy()
B = torch.tensor(A)将大小为1的张量转换为Python标量
1
2a = torch.tensor([3.5])
a.item(), float(a), int(a)
数据预处理
创建人工数据集,并存储在csv文件
1
2
3
4
5
6
7
8
9
10
11
12import os
# 创建 ../data文件夹
os.makedirs(os.path.join('..', 'data'), exist_ok=True)
# CSV 文件的路径 ../data/house_tiny.csv
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文件中加载原始数据集
为了处理缺失的数据,典型的方法包括插值和删除。
下例通过插入数值,消除缺失的数值。
1
2
3
4inputs, outputs = data.iloc[:, 0:2], data.iloc[:, 2]
# 用 非空数据的平均值 填充NaN
inputs = inputs.fillna(inputs.mean(numeric_only=True))
inputs对于类别值或离散值,将
NaN
视为一个类别。1
2
3
4
5
6
7# 独热编码(将分类变量转换为二进制向量)
# 每个分类变量的每个可能取值都被编码为一个二进制特征
# 当分类变量有多个不同取值时,会生成更多的列。这可能会导致维度灾难(curse of dimensionality)的问题
inputs = pd.get_dummies(inputs, dummy_na=True)
# 将inputs中的所有值转换为数值类型(True/False --> 1/0)
inputs *= 1
inputs将dataframe转换为张量
1
2
3
4
5
6
7import torch
# 把所有条目都是数值类型的 dataframe格式的inputs和outputs
# 转换为张量格式
X, y = torch.tensor(inputs.values), torch.tensor(outputs.values)
X, y
线性代数
标量
- 简单操作
- $c = a + b$
- $c = a \cdot b$
- $c = \sin a$
- 长度
- $|a|=\begin{cases} a, & \text{if } a > 0 \ -a, & \text{if } x \leq 0 \end{cases}$
- $|a+b| \leq |a| + |b|$
- $|a\cdot b|=|a| \cdot |b|$
标量由只有一个元素的张量表示。
1 | import torch |
向量
简单操作
- $c = a + b \text{ where } c_i = a_i + b_i$
- $c = \alpha \cdot b \text{ where } c_i = \alpha b_i$ (α是标量)
- $c = \sin a \text{ where } c_i = \sin a_i$
长度
- $||a||2=\sqrt{\sum\limits{i=1}^ma_i^2}$
- $||a||\geq 0 \text{ for all } a$
- $||a+b|| \leq ||a|| + ||b||$
- $||\alpha\cdot b||=|\alpha| \cdot ||b||$ (α是标量)
点乘
$a^Tb = \sum\limits_ia_ib_i$
如果a与b正交,则$a^Tb = 0$
1 | # 可以把向量视为标量值组成的列表 |
矩阵
简单操作
- $C = A + B \text{ where }C_{ij}=A_{ij}+B_{ij}$
- $C=\alpha\cdot B \text{ where } C_{ij}=\alpha B_{ij}$
- $C=\sin A \text{ where } C_{ij} = \sin A_{ij}$
乘法(矩阵 x 向量)
$C = Ab \text{ where } C_i=\sum\limits_jA_{ij}b_j$ (矩阵A的每一行和向量b做内积)
乘法(矩阵 x 矩阵)
$C = AB \text{ where } C_{ik}=\sum\limits_jA_{ij}B_{jk}$
范数(norm)
F范数:把所有元素的模取平方和,再开方。
$||A||2=\sqrt{|A{11}|^2+… + |A_{nn}|^2}=\sqrt{\sum\limits_{i,j}|a_{ij}|^2}$
1
torch.norm(torch.ones((4, 9)))
平方范数$L_2$,
特殊矩阵
对称和反对称矩阵
- $A_{ij}=A_{ji}$
- $A_{ij}=-A_{ji}$
正定矩阵
$x$为任意向量,$x^TAx \geq 0$ ,就称A为正定矩阵。
正交矩阵
- 所有行/列都是单位向量,且两两正交
- 可以写成$AA^T=E$ (对角线全为1的单位矩阵)
置换矩阵
特征向量
不被矩阵改变方向的向量,称为特征向量。对称矩阵总能找到特征向量。
$Ax = \lambda x$
代码举例
通过指定两个分量$m$和$n$来创建一个形状为$m\times n$的矩阵。
1 | A = torch.arange(20).reshape(5, 4) |
哈达玛积 $\odot$
两个矩阵的按元素乘法称为哈达玛积$\odot$,不常用。
1 | import torch |
1 | a = 2 |
按指定轴求和
1 | A = torch.arange(20 * 2).reshape(2, 5, 4) |
针对第1个维度求和
1
2A_sum_axis0 = A.sum(axis = 0)
A.shape, A_sum_axis0.shape, A_sum_axis0针对第2个维度求和
1
2A_sum_axis1 = A.sum(axis = 1)
A.shape, A_sum_axis1.shape, A_sum_axis1针对1、2两个维度求和
1
A.sum(axis = [0, 1])
针对第1个维度计算累加和
1
A.cumsum(axis = 0)
平均值
A.mean()
A.mean(axis=0)
计算总和或均值时 保持轴数不变
1 | A = torch.arange(20 * 2.).reshape(2, 5, 4) |
点积 $\cdot$ (点乘、内积)
点积是相同位置的按元素乘积的和
1 | x = torch.arange(4.) |
可以通过执行按元素乘法(哈达玛积$\odot$),然后求和来表示两个向量的点积。
1 | torch.sum(x * y) |
矩阵向量积
1 | A.shape, x.shape, torch.mv(A, x) |
矩阵乘法
1 | B = torch.ones(4, 3) |
梯度
梯度是导数在向量上的拓展,指向值变化最大的方向。
y是标量,x是向量
x是列向量,求导之后,会变成行向量。
y是向量,x是标量
y是列向量,求导之后,还是列向量。
y和x都是向量
求导之后,是一个矩阵。
自动求导
自动求导计算一个函数在指定值上的导数。自动求导有两种模式:
正向累积
$\frac{\partial y}{\partial x}=\frac{\partial y}{\partial u_n}(\frac{\partial u_n}{\partial u_{n-1}}(\dots(\frac{\partial u_2}{\partial u_1}\frac{\partial u_1}{\partial x})))$
反向累积(反向传递)
$\frac{\partial y}{\partial x}=(((\frac{\partial y}{\partial u_n}\frac{\partial u_n}{\partial u_{n-1}})\dots)\frac{\partial u_2}{\partial u_1})\frac{\partial u_1}{\partial x}$
反向累积求导的复杂度
时间复杂度 $O(n)$,n是操作子个数
通常和正向累积代价类似。
空间复杂度$O(n)$
需要存储正向计算的所有中间结果。正向累积的空间复杂度是$O(1)$
对函数$y = 2x^Tx$ 关于列向量x求导
1 | import torch |
重新计算梯度时,要清除之前的值
如继续对函数$y = \sum\limits_{i=1}^nx_i$关于列向量x求导。
1 | # 默认情况下,pytorch会累积梯度,需要清除之前的值 |
将某些计算移动到记录的计算图之外
1 | x.grad.zero_() |
1 | x.grad.zero_() |
即使构建函数的计算图需要通过Python控制流,仍然可以计算得到变量的梯度
1 | def f(a): |
线性回归 + 基础优化算法
线性回归模型
给定n维输入$\mathbf{x} = [x_1, x_2, \dots, x_n]^T$
线性回归模型有一个n维权重$\mathbf{w}$和一个标量偏差b
$\mathbf{w}=[w_1, w_2, \dots, w_n]^T, \ b$
输出是输入的加权和
$y = w_1x_1 +w_2x_2 +\dots+w_nx_n+b$
向量版本:$y=\langle\mathbf{w},\mathbf{x}\rangle+b$
线性回归模型可以看做单层神经网络,有显式解。
衡量预估质量
使用平方损失(均方损失)衡量预测值和真实值的差异。
$\mathscr{l}(y,\hat{y})=\frac{1}{2}(y-\hat{y})^2$ ($\frac{1}{2}$是为了求导时,方便消去)
参数学习
训练损失
$\ell(\mathbf{X},\mathbf{y},\mathbf{w},b)=\frac{1}{2n}\sum\limits_{i=1}^{n}(y_i-\langle\mathbf{x}_i,\mathbf{w}\rangle-b)^2$
通过最小化损失函数来学习参数
$\mathbf{w}^*,\mathbf{b}^*=\arg\min\limits_{\mathbf{w},b}\ell(\mathbf{X},\mathbf{y},\mathbf{w},b)$
均方误差(Mean Squared Error, MSE)是一种常用的损失函数,用于衡量预测值与真实值之间的差异:
$\text{MSE} = \frac{1}{n}\sum\limits_{i=1}^n(y_i-\hat{y})^2$
1 | from torch import nn |
基础优化方法
梯度下降
当一个模型没有显式解时,如何操作?
挑选一个初始值$\mathbf{w}_0$
重复迭代参数 t = 1, 2, 3
$\mathbf{w}t= \mathbf{w}{t-1} -\eta \frac{\partial \ell}{\partial \mathbf{w}_{t-1}}$
$\frac{\partial \ell}{\partial \mathbf{w}{t-1}}$:损失函数$\ell$关于$\mathbf{w}{t-1}$的梯度
- 沿梯度方向将增加损失函数值
- 学习率$\eta$:步长的超参数(需要人为指定的值)。学习率不能太小,也不能太大。
在实际中,很少直接使用梯度下降,最长使用的形式是小批量随机梯度下降。
在整个训练集上算梯度成本高昂
一个深度神经网络模型可能需要数分钟至数小时
可以随机采样 b 个样本$i_1, i_2, …, i_b$来近似损失
$\frac{1}{b}\sum\limits_{i\in I_b}\ell(\mathbf{x}_i, y_i, \mathbf{w})$
b是批量大小,另一个重要的超参数。
- 梯度下降通过不断沿着反梯度方向更新参数求解
- 小批量随机梯度下降是深度学习默认的求解算法
- 两个重要的超参数是批量大小和学习率
pytorch实现
1 | import numpy as np |
调用框架中现有的api来读取数据
1 | def load_array(data_arrays, batch_size, is_train=True): |
使用框架的预定义好的层
1 | from torch import nn |
初始化模型参数
1 | # normal_ 对权重参数的数据进行正态分布初始化,均值为 0,标准差为 0.01 |
计算均方误差使用MSELoss
类
$\text{MSE} = \frac{1}{n}\sum\limits_{i=1}^n(y_i-\hat{y})^2$
1 | loss = nn.MSELoss() |
实例化SGD实例
随机梯度下降(SGD, Stochastic Gradient Descent)
1 | # torch.optim.SGD是一个优化器类 |
训练
1 | num_epochs = 3 |
Softmax回归
Softmax回归其实是一个分类问题。
- 回归估计一个连续值
- 跟真实值的区别作为损失
- 分类预测一个离散类别
- 通常多个输出
- 输出i是预测为第i类的置信度
Softmax函数
$softmax(o)=\hat{y}$
$softmax(o)_i = \hat{y}_i=\frac{\exp(o_i)}{\sum_j\exp(o_j)}$
- $softmax(o)_i$ 是模型输出
o
经过 softmax 函数后的概率分布的第i
个元素。 - $\sum_j\exp(o_j)$:所有向量指数的和
使用Softmax函数得到每个类的预测置信度。
交叉熵
交叉熵常用来衡量两个概率的区别:$H(p,q)=\sum\limits_i -p_i\log (q_i)$
将它作为损失:
$\ell(y,\hat{y})= -\sum\limits_i y_i\log \hat{y}_i = -\log \hat{y}_y$
将每个类别的真实标签的概率 $y_i$ 乘以预测概率 $\hat{y}_i$ 的负对数,并将所有类别的结果求和。交叉熵衡量了预测概率分布 $\hat{y}_i$ 在真实概率分布 $y_i$ 上的不确定性和差异。
交叉熵损失函数对模型输出 o
求梯度,即真实概率和预测概率的区别
$\partial_{o_i}\ell(y, \hat{y}) = softmax(o)_i - y_i$
损失函数
均方损失 $L_2$ Loss
$\ell(y,y^{‘})=\frac{1}{2}(y-y^{‘})^2$
绝对值损失 $L_1$ Loss
$\ell(y, y^{‘})=|y-y^{‘}|$
优化到末期时,就不那么稳定
Softmax回归的从零开始实现
1 | import torch |
- softmax的输入需要是一个向量,展平每个图像,将它们视为长度为784(1*28*28)的向量。
- 因为我们的数据集有10个类别,所以网络输出维度为10
1 | num_inputs = 1 * 28 * 28 |
实现Softmax回归模型
对于一个矩阵来说,就是按行来做softmax。
$softmax(X){ij}=\frac{\exp (X{ij})}{\sum_{k}\exp (X_{ik})}$
1 | def softmax(X): |
实现交叉熵损失函数
1 | def cross_entropy(y_hat, y): |
将预测类别与真实y
元素进行比较
1 | def accuracy(y_hat, y): |
评估准确率
1 | class Accumulator: |
训练的一个迭代周期
1 | # 训练模型一个迭代周期 |
定义一个在动画中绘制数据的类
1 | from matplotlib_inline.backend_inline import set_matplotlib_formats |
训练函数
1 | def train(net, train_iter, test_iter, loss, num_epochs, updater): |
小批量随机梯度下降来优化模型的损失函数
1 | def sgd(params, lr, batch_size): |
训练10个迭代周期,对图像进行分类预测
1 | num_epochs = 10 |
多层感知机
感知机
给定输入$\mathbf{x}$,权重$\mathbf{w}$,偏移$b$,感知机输出:
$o=\sigma(\langle\mathbf{w},\mathbf{x}\rangle+b)$
$\sigma(x)=\begin{cases}1 \ \ \text{if}\ x > 0\0 \ \ \text{otherwise}\end{cases}$
感知机其实就是二分类的问题。
- 和线性回归相比,线性回归输出实数。
- 和Softmax回归相比,Softmax是多分类。
模型选择
模型容量
模型容量指的是拟合各种函数的能力。
- 低容量的模型难以拟合训练数据
- 高容量的模型可以记住所有的训练数据。
网络中的网络 NiN
全连接层的问题
卷积层只需要较少的参数,但卷积层之后的第一个全连接层需要的参数量很大。
- 卷积层的参数量: $c_i\times c_o \times k^2$ (输入通道数 x 输出通道数 x 卷积核的高 x 卷积核的宽)
- 全连接层的参数量:$c_i\times w_i \times h_i \times c_o \times w_o \times h_i$ (输入通道数 x 输入宽度 x 输入高度 x 输出通道数 x 输出宽度 x 输出高度)
参数量大,会占用很多内存、计算带宽、容易过拟合。
NiN块
1个卷积层后跟2个1 * 1的卷积层,对每个像素增加了非线性:
- 步幅1,无填充,输出形状跟卷积层输出一样
- 起到全连接层的作用
NiN架构
- 无全连接层
- 交替使用NiN块和步幅为2的最大池化层,逐步减小宽高、增大通道数。
- 最后使用全局平均池化层得到输出,不容易过拟合,更少的参数个数。
- 输入通道数是类别数
含并行连结的网络 GoogLeNet / Inception V3
Inception块用4条有不同超参数的卷积层和池化层的路来抽取不同的信息,它的一个主要优点是模型参数小,计算复杂度低。
GoogLeNet使用了9个lnception块,是第一个达到上百层的网络,后续有一系列改进。
批量归一化
损失出现在最后,后面的层训练较快。
数据在最底部
- 底部的层训练较慢
- 底部层一变化,所有都得跟着变
- 最后的那些层需要重新学习多次
- 导致收敛变慢
我们可以在学习底部层的时候避免变化顶部层吗?
固定小批量B里的样本x的均值
$\mu_B=\frac {1}{|B|}\sum\limits_{x\in B}x_i$
固定小批量B里的样本x的方差
$\sigma^2_B=\frac{1}{|B|}\sum\limits_{i\in B}(x_i-\mu_B)^2+\epsilon$
$\epsilon$是为了防止方差为0的一个很小的数。
批量归一化就是通过可学习的参数$\gamma、\beta$得到的结果:
$x_{i+1}=\gamma\frac{x_i-\mu_B}{\sigma_B}+\beta$
批量归一化层
- 含有可学习的参数为$\gamma$和$\beta$
- 作用范围:
- 全连接层和卷积层输出上,激活函数前
- 全连接层和卷积层输入上
- 对全连接层,作用在特征维
- 对于卷积层,作用在通道维
批量归一化在做什么
后续有论文指出它可能就是通过在每个小批量里加入噪音来控制模型复杂度,因此没必要跟丢弃法混合使用。
$x_{i+1}=\gamma\frac{x_i-\hat{\mu}_B}{\hat{\sigma}_B}+\beta$
把$\hat{\mu}_B$和$\hat{\sigma}_B$看作随机偏移和随机缩放。
批量归一化固定小批量中的均值和方差,然后学习出适合的偏移和缩放。可以加速收敛速度,但一般不改变模型精度。
残差网络 ResNet
残差块
- 串联一个层改变函数类,希望能扩大函数类。
- 残差块加入快速通道,来得到$f(x)=x+g(x)$的结构
有很多调整变形的排列组合。
有两种残差块:
- 高宽减半、通道数翻倍的残差块(步幅stride=2)
- 接多个高宽不变的残差块(步幅stride=1)
残差块使得很深的网络更加容易训练,甚至可以训练一千层的网络。
数据增广
数据增强就是在一个已有的数据集中,通过变形数据,使得数据集有更多的多样性,模型繁华性能更好。例如:
- 在语言里加入各种不同的背景噪音
- 改变图片的形状和颜色
常见的图片的数据增强的形式:
- 翻转
- 切割
- 变色
微调
- 微调通过使用在大数据上得到的预训练好的模型,来初始化模型权重,来完成提升精度。
- 预训练模型质量很重要。
- 微调通常速度更快、精度更高。
序列模型
- 时序模型中,当前数据跟之前观察到的数据相关
- 自回归模型使用自身过去数据来预测未来
- 马尔科夫模型假设当前只跟最近少数数据相关,从而简化模型
- 潜变量模型使用潜变量来概括历史信息