动手学深度学习

该笔记是看李沐视频学习的。
内容大概分为几部分

  • 深度学习基础
    • 线性模型
    • CNN
    • RNN
    • 求导、训练
    • Gluon最佳实践
      • hybridize
      • 自动并行
      • 多卡训练
    • 计算机视觉
      • Resnet
      • SSD
    • 自然语言处理
      • Word embedding
      • seq2seq
    • 增强学习
    • 对抗网络
    • 推荐系统

《动手学深度学习》是面向中文读者的能运行、可讨论的深度学习教科书。也是视频的参考书籍,可以通过访问 https://zh.d2l.ai 获取最新的内容 更新。希望通过一个月的时间学习加练习,来加强自己对深度学习的理解,为将来的论文和工作打基础。

配置Jupyter

以下命令都是进入⽂件夹“d2l-zh”的窗口来打开cmd
激活环境

1
2
3
conda activate gluon # 若conda版本低于4.4,使⽤命令activate gluon 
#需退出虚拟环境
conda deactivate

打开Jupyter记事本

1
jupyter notebook

更新代码的步骤
第⼀步是重新下载最新的包含本书全部代码的压缩包。下载地址为https://zh.d2l.ai/d2l-zh.zip。 解压后进⼊⽂件夹“d2l-zh”。

第⼆步是使⽤下⾯的命令更新运⾏环境:

1
conda env update -f environment.yml

使⽤GPU版的MXNet

第⼀步是卸载CPU版本MXNet。如果没有安装虚拟环境,可以跳过此步。如果已安装虚拟环境, 需要先激活该环境,再卸载CPU版本的MXNet。

1
pip uninstall mxnet

第⼆步是更新依赖为GPU版本的MXNet。使⽤⽂本编辑器打开本书的代码所在根⽬录下的⽂ 件environment.yml,将⾥⾯的字符串“mxnet”替换成对应的GPU版本。例如,如果计算机 上装的是8.0版本的CUDA,将该⽂件中的字符串“mxnet”改为“mxnet-cu80”。如果计算机上 安装了其他版本的CUDA(如7.5、9.0、9.2等),对该⽂件中的字符串“mxnet”做类似修改(如 改为“mxnet-cu75”“mxnet-cu90”“mxnet-cu92”等)。保存⽂件后退出。

第三步是更新虚拟环境,执⾏命令

1
conda env update -f environment.yml

之后,我们只需要再激活安装环境就可以使⽤GPU版的MXNet运⾏本书中的代码了。需要提醒的 是,如果之后下载了新代码,那么还需要重复这3步操作以使⽤GPU版的MXNet。

数学基础概念

一般像这里指的维度0和维度1是指矩阵的行和列

我们使用符号$\odot$表示两个矩阵按元素做乘法的运算:


范数

设$n$维向量$\boldsymbol{x}$中的元素为$x_1, \ldots, x_n$。向量$\boldsymbol{x}$的$L_p$范数为

例如,$\boldsymbol{x}$的$L_1$范数是该向量元素绝对值之和:

而$\boldsymbol{x}$的$L_2$范数是该向量元素平方和的平方根:

我们通常用$|\boldsymbol{x}|$指代$|\boldsymbol{x}|_2$。

设$\boldsymbol{X}$是一个$m$行$n$列矩阵。矩阵$\boldsymbol{X}$的Frobenius范数为该矩阵元素平方和的平方根:

其中$x_{ij}$为矩阵$\boldsymbol{X}$在第$i$行第$j$列的元素。

特征向量和特征值

对于一个$n$行$n$列的矩阵$\boldsymbol{A}$,假设有标量$\lambda$和非零的$n$维向量$\boldsymbol{v}$使

那么$\boldsymbol{v}$是矩阵$\boldsymbol{A}$的一个特征向量,标量$\lambda$是$\boldsymbol{v}$对应的特征值。

梯度

假设函数$f: \mathbb{R}^n \rightarrow \mathbb{R}$的输入是一个$n$维向量$\boldsymbol{x} = [x_1, x_2, \ldots, x_n]^\top$,输出是标量。函数$f(\boldsymbol{x})$有关$\boldsymbol{x}$的梯度是一个由$n$个偏导数组成的向量:

为表示简洁,我们有时用$\nabla f(\boldsymbol{x})$代替$\nabla_{\boldsymbol{x}} f(\boldsymbol{x})$。

假设$\boldsymbol{x}$是一个向量,常见的梯度演算包括

类似地,假设$\boldsymbol{X}$是一个矩阵,那么

海森矩阵

假设函数$f: \mathbb{R}^n \rightarrow \mathbb{R}$的输入是一个$n$维向量$\boldsymbol{x} = [x_1, x_2, \ldots, x_n]^\top$,输出是标量。假定函数$f$所有的二阶偏导数都存在,$f$的海森矩阵$\boldsymbol{H}$是一个$n$行$n$列的矩阵:

其中二阶偏导数

运算内存开销

在Python中操作经常会开心的内存来存储运算结果。
即使像Y=X+Y这样的运算,也会开新的内存,然后将Y指向新的内存。

1
2
3
4
5
6
输入
before = id(Y)
Y = Y + X
id(Y) == before
输出
False

如果想把结果保存到指定内存,可以先通过zeros_like创建和Y形状相同且元素为0的NDArray,记为Z。接下来,我们把X+Y的结果通过[:]写进Z对应的的内存中。

1
2
3
4
5
6
7
输入
Z = Y.zeros_like()
before = id(Z)
Z[:] = X + Y
id(Z) == before
输出
True

但是实际上,这里还是为X+Y开了临时内存来存储计算结果,再复制到Z对应的内存。如果想避免这个临时内存开销,我们可以使用运算符全名函数中的out参数。

1
2
3
4
5
输入
nd.elemwise_add(X, Y, out=Z)
id(Z) == before
输出
True

如果X的值在之后程序中不会复用,可以用 X[:] = X + Y 或者 X += Y 来减少运算的内存开销。

1
2
3
4
5
6
输入
before = id(X)
X += Y
id(X) == before
输出
True

从上手到多类分类

线性回归

何为线性回归?

就是给定一个数据点的集合以及对应的目标Y, 然后线性模型就会找一根线,其由向量w和位置b组成,来最好的近似每个样本的x[i]和y[i],用数学符号来表示就是将学w和b来预测。

并且最小化所有数据点上的平方误差。

创建数据集

使用人工创建数据集,方便我们知道真实的模型是怎么样子的,采用以下方法来生成:

1
2
y[i] = 2 * X[i][0] - 3.4 * X[i][1] + 4.2+noise
其中noise是噪音,服从均值0和方差为0.1的正态分布

注意到X的每一行是一个长度为2的向量,而y的每一行是一个长度为1的向量(标量)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from mxnet import ndarray as nd
from mxnet import autograd

num_inputs = 2 # X 的维度是2
num_examples= 1000 # 生成1000个样本

true_w = [2 , -3.4] # w的值为2 和 -3.4
true_b = 4.2 # b的值为4.2

X = nd.random.normal(shape=(num_examples,num_inputs)) # 这里是指生成1000行,2列
y = true_w[0] * X[:,0] + true_w[1] * X[:,1] + true_b
y += 0.01 * nd.random.normal(shape=y.shape)

print( X[0:10] , y[0:10])
#打印X的1到10行,y的前10个值

有个小知识点,关于转型的问题

上面出现了,数组+浮点数 得到了 数组,该数组是每一个值都加一次浮点数

如果数组加布尔型,也是可以的,数组里的每一个值都把该布尔型转换成float,再加一次。

数据读取

定义一个函数,每次返回batch_size个随机的样本和对应的目标。通过Python的yield来构造一个迭代器。

1
2
3
4
5
6
7
8
9
10
import random
batch_size= 10
def data_iter():
#产生一个随机索引
idx = list(range(num_examples))
random.shuffle(idx)
for i in range(0, num_examples , batch_size):
j = nd.array(idx[i:min(i + batch_size , num_examples)])
yield nd.take(x , j), nd.take(y , j)
# take(a,indices,axis=None,out=None,mode="raise"),其中a是指定数组,indices是指定索引的位置。axis决定是返回数组或者矩阵

下面代码读取第一个随机数据块

1
2
3
for data, label in data_iter():
print(data , label)
break

初始化模型

1
2
3
w = nd.random.normal(shape=(num_inputs,1))
b = nz.zeros((1,))
params = [w,b]

因为之后训练时需要对这些参数求导来更新它们的值,所以需要创建它们的梯度。

1
2
for param in params:
param.attach_grad()

定义模型

1
2
def net(x):
return nd.dot(X , w) + b

损失函数

使用常见的平方误差来衡量预测目标和真实目标之间的差距

1
2
3
def square_loss( yhat , y):
#注意这里把y变形成yhat的形状来避免自动光法
return (yhat - y.reshape(yhat.shape)) **2

优化

虽然线性回归有显式解,但绝大部分模型没有。所以通过随机梯度下降来求解。每一步都将模型参数沿着梯度的反方向走特定的距离lr。这个距离一般叫学习率。之后又会一直用这个函数,保存到utils.py里

1
2
3
def SGD(params , lr):
for param in params:
param[ : ] = param - lr *param.grad

训练

训练通常需要迭代数据次数,一次迭代里,每次随机读取固定数个数据点,计算梯度并且更新模型参数。

1
2
3
4
5
6
7
8
9
10
11
12
epochs = 5
learning_rate = 0.001
for e in range(epochs):
total_loss = 0
for data , label in data_iter():
with autograd.record():
output = net(data)
loss = square_loss(output, label)
loss.backward()
SGD(params, learning_rate)
total_loss += nd.sum(loss).asscalar()
print("Epoch %d, average loss: %f" % (e, total_loss/num_examples))

这时候就可以跑出结果了

1
2
3
4
5
6
输出:
epoch 1, loss 0.000048
epoch 2, loss 0.000048
epoch 3, loss 0.000049
epoch 4, loss 0.000048
epoch 5, loss 0.000048

训练后,用学习到的参数和真实参数进行比较

1
2
3
4
5
输入
true_w,w
输出
([2, -3.4],
[[ 1.9999499],[-3.4000692]]
1
2
3
true_b, b
(4.2,
[4.1994824]

可以看得出来数据是非常接近的。

使用gluon的线性回归

同样的模型,使用高层抽象包gluon

创建数据集

1
2
3
4
5
6
7
8
from mxnet import autograd, nd
num_inputs = 2
num_examples = 1000
true_w = [2, -3.4]
true_b = 4.2
features = nd.random.normal(scale=1, shape=(num_examples, num_inputs))
labels = true_w[0] * features[:, 0] + true_w[1] * features[:, 1] + true_b
labels += nd.random.normal(scale=0.01, shape=labels.shape)

数据读取

这里使用data模块来读取数据
但是由于data经常用作变量名,我们将导入的data模块用添加gluon的首字母的假名gdata代替

1
2
3
4
5
6
7
8
9
from mxnet.gluon import data as gdata

batch_size = 10

# 将训练数据的特征和标签组合
dataset = gdata.ArrayDataset(features, labels)

# 随机读取⼩批量
data_iter = gdata.DataLoader(dataset, batch_size, shuffle=True)

这里的data_iter的使用跟上一部分的一样,让我们读取并打印第一个小批量的数据样本

1
2
3
for x , y in data_iter:
print(x, y)
break

定义模型

先导入nn(neural networks缩写)模块,里面定义了大量神经网络的层。
我们先定义一个sequential实例,在gluon中,sequential实例可以看作是一个串联各个层的容器。在构造模型时,在该容易依次添加层。当给定输入数据时,容器每一层将依次计算将输出作为下一层的输入。

1
2
3
from mxnet.gluon import nn

net = nn.sequential()

线性回归在神经网络图中,作为一个单层神经网络,线性回归输出层中的神经元和输入层中各个输入完全连接。
因此,线性回归的输出层又叫全连接层。在gluon中,全连接层是一个dense实例,我们定义该层输出个数为1.

1
net.add(nn.Dense(1))

在gluon中,我们无需定义每一层输入的形状,比如线性回归的输入个数。当模型得到数据时,在后面执行next(X)时,模型将自动腿短出每一层的输入个数。

初始化模型参数

在使用net之前,需要初始化模型参数,比如线性回归的权重和偏差。

从MXNet中导入inti模块,该模块提供模型初始化的各种方法。

1
2
3
4
5
from mxnet import init
net.initialize(init.Normal(sigma = 0.01))
# 通过init.Normal(sigma= 0.01)指定权重参数每个元素将在初始化时
# 随机采样于均值为0,标准差为0.01的正态分布。
# 偏差参数默认会初始化为零

定义损失函数

在gluon中,loss模块定义了各种损失函数,用假名gloss代替导入的loss模块,直接使用它提供的平方损失作为模型的损失函数。

1
2
from mxnet.gluon import loss as gloss
loss = gloss.L2Loss() #平方损失函数又称为L2范数损失

定义优化算法

同样,我们也无需实现小批量的随机梯度下降。导入gluon后,创建一个Trainer实例,并指定学习率为0.03的小批量随机梯度下降(sgd)为优化算法。该优化算法用来迭代net实例所有通过add函数嵌套的层所包含的全部参数。
这些参数可以通过collect_params函数获取。

1
2
from mxnet import gluon
trainer = gluon.Trainer(net.collect_params(), 'sgd' , {learning_rate' : 0.03})

训练模型

在使用gluon训练模型时,通过调用Trainer实例的step函数来迭代模型参数。

由于变量 l 是长度为batch_size的以为NDArray,执行 l.backward()等价于执行 l.sum().backward().

按照小批量随机梯度下降的定义,我们在step函数中指明批量大小,从面对批量中样本梯度求平均。

1
2
3
4
5
6
7
8
9
num_epochs =3
for epoch in range(1 ,num_epochs + 1):
for x , y in data_iter:
with autograd.record():
l = loss( net(X) , y)
l.backward()
trainer.step(batch_size)
l = loss(net(features),labels)
print('epoch %d ,loss: %f '%(epoch, l.mean().asnumpy()))

输出

1
2
3
epoch 1, loss: 0.040645 
epoch 2, loss: 0.000157
epoch 3, loss: 0.000051

然后分别比较学到的模型参数和真实的模型参数。
我们从net获得需要的层,并访问起权重(weight)和偏差(bias)。学到的参数和真实的参数很接近。

1
2
dense = net[0]
true_w , dens.weight.data()

输出

1
2
([2, -3.4], [[ 1.9996208 -3.399555 ]] 
<NDArray 1x2 @cpu(0)>)

1
true_b , dense.bias.data()

输出

1
2
(4.2, [4.199315] 
<NDArray 1 @cpu(0)>)

当遇到关于mxnet不懂的地方,可以尝试使用

1
2
3
4
5
6
想访问weight的梯度,但是不知道怎么做,可以这样做:
dense.weight?
里面会列出详细的文档
或者
dense.weight??
甚至有实现的方法

多类逻辑回归(softmax回归)

线性回归模型适用于输出为连续值的情况,但是碰到模型输出是一个像图像类别的离散值,我们可以使用诸如softmax回归在内的分类模型来进行离散值预测。

和线性回归不同,softmax回归的输出单元从一个变成了多个,且引入了softmax运算,使运算更适合离散值的预测和训练。

分类问题

Just for fun!
------------- 文章已经到尾 -------------