今日(きょう)(さわ)がしく(たわむ)れ生きる人々の漫画映画(まんがえいが)

Dive Into Deep Learning

Dive Into Deep Learning 这本书的笔记。

安装与环境配置

我的系统是 Arch Linux,所以利用 AUR 可以很便利地安装相关软件。另外,方便起见也可以考虑使用 Arch Linux 中文社区仓库 来安装相关的预编译的包。

书中对应的安装教程页面是 https://d2l.ai/chapter_installation/index.html 。

Python 环境

书中使用 Miniconda 来管理 Python 环境。AUR 里有 miniconda3 ,ArchLinuxCN 源里有 anaconda。因为 Miniconda 其实就是 Anaconda 的简化版本,所以我就直接从源里安装了 Anaconda。

我使用 ArchCN 的 conda,首先进入第一层环境,然后创建新的 Python 环境:(如果不是 ArchCN 的 conda 的话还请跟着官网走)

$ source /opt/anaconda/bin/activate root
(base) $ conda create --name d2l python=3.8 -y
Collecting package metadata (current_repodata.json): done
Solving environment: done
...
Preparing transaction: done
Verifying transaction: done
Executing transaction: done
>#
># To activate this environment, use
>#
>#     $ conda activate d2l
>#
># To deactivate an active environment, use
>#
>#     $ conda deactivate

跟着官网把 d2l-en.zip 代码压缩包下载下来,解压(这些命令不抄了,请看书),然后正式进入我们的新环境:

$ conda activate d2l
(d2l) $

我是 MXNet 和 PyTorch 都想学一学,所以两个都装了:(你 (因为我的笔记本双显卡配置 CUDA 可能比较麻烦,所以就不安装 GPU 的版本了。如果想要 GPU 版本的话,请把书先翻到下面的 GPU Support 一节看一看再说。MXNet 的安装需要把 CPU 版本的先卸载掉,就不要先把 CPU 版本的装上做无用功了吧。)

(d2l) $ pip install mxnet==1.7.0.post1
Collecting mxnet==1.7.0.post1
  Downloading mxnet-1.7.0.post1-py2.py3-none-manylinux2014_x86_64.whl (55.0 MB)
...
Installing collected packages: urllib3, idna, chardet, requests, numpy, graphviz, mxnet
Successfully installed chardet-4.0.0 graphviz-0.8.4 idna-2.10 mxnet-1.7.0.post1 numpy-1.21.0 requests-2.25.1 urllib3-1.26.6
(d2l) $ pip install torch torchvision
Collecting torch
  Downloading torch-1.9.0-cp38-cp38-manylinux1_x86_64.whl (831.4 MB)
...
Installing collected packages: typing-extensions, torch, pillow, torchvision
Successfully installed pillow-8.3.0 torch-1.9.0 torchvision-0.10.0 typing-extensions-3.10.0.0

CUDA 配置

(可是它们性能给的实在是太多了。)

(但是显卡好贵啊。祝愿矿难(显卡挖矿那种意义上的)越早到来越好。现在买二手都开始害怕了。)

Arch 的 CUDA 版本太新了(11.3)。本着对 CUDA 以及 MXNet 向后兼容的信赖,我决定装 MXNet 1.8.0 的 CUDA 11.0 版本。(注意书里一直用的是 MXNet 1.7.0 版本。最好还是跟着书来,我这里先踩踩坑。)

踩完坑,但是还是不知道踩到哪里的坑了,CUDA 用回了 10.2 版本。但现在想一想,似乎用 11.0 版本还是可行的,但懒了。

总之,先装上 CUDA。Arch 上的 CUDA 版本太太新了(11.3),所以要手动装旧的 CUDA。还好 AUR 上有旧版本的 CUDA:手动把 gcc8 , cuda-10.2 , cudnn7-cuda10.2 装上就可以了(CUDNN 可能不需要装,因为 conda 环境里是有的)。用 makepkg 手动安装的话,可以在安装 gcc8 时使用 --asdeps 的选项来把软件依赖关系保持得规整一些。

其它系统的话请各位各自大显神通。

然后在 conda 的 d2l 环境里面安装相关的包:(见 某个 MXNet GitHub Issue )

(d2l) $ conda install -c conda-forge cudnn nccl
(d2l) $ pip uninstall mxnet
(d2l) $ pip install mxnet-cu102

安装 d2l 包

(d2l) $ pip install -U d2l

pandas

预处理数据用的。

(d2l) $ pip install pandas

似乎已经装上了。

测试安装

MXNet

详见 Validate Your MXNet Installation 。

(d2l) $ python
Python 3.8.10 (default, Jun 4 2021, 15:09:15)
[GCC 7.5.0] :: Anaconda, Inc. on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import mxnet as mx
>>> a = mx.nd.ones((2, 3))
>>> b = a * 2 + 1
>>> b.asnumpy()
array([[3., 3., 3.],
       [3., 3., 3.]], dtype=float32)
>>> # 以下为 CUDA 版本的测试,若没有安装 CUDA 即可忽略
>>> a = mx.nd.ones((2, 3), mx.gpu())
>>> b = a * 2 + 1
>>> b.asnumpy()
array([[3., 3., 3.],
       [3., 3., 3.]], dtype=float32)
PyTorch

下面是测试 CUDA 版本的,CPU 版本的真的需要测试么?(非要的话可以把下面的 cuda 相关 , dev = torch.device("cuda") , .to(dev) 部分删掉。)

(d2l) $ python
Python 3.8.10 (default, Jun 4 2021, 15:09:15)
[GCC 7.5.0] :: Anaconda, Inc. on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import torch
>>> print(torch.__version__)
1.9.0+cu102 (你的 PyTorch 版本,不一定一致)
>>> print(torch.cuda.is_available()) # 这个必须 True
True
>>> torch.cuda.device_count() # 这个(至少?)是 1
1
>>> torch.cuda.get_device_name(0)
'这里会显示你的显卡名字'
>>> dev = torch.device("cuda")
>>> a = torch.ones((2, 3)).to(dev)
>>> b = a * 2 + 1
>>> print(b)
tensor([[3., 3., 3.],
        [3., 3., 3.]], device='cuda:0')

First Steps

数据操作

MXNet

from mxnet import np, npx

npx.set_np()

x = np.arange(12)
x
x.shape
x.size
# 可以有一个 -1 ,意为从其它维数中推断
X = x.reshape(3, 4)
np.zeros((2, 3, 4))
np.ones((2, 3, 4))
np.random.normal(0, 1, size=(3, 4))
np.array([[2, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]])

(之前试的时候 npx.set_up() 总是报错,为什么呢?因为是 set_np() 而不是 set_up() 。)

不抄了,element-wise 的操作有 x + y , x - y , x * y , x / y , x ** y , x == y 。另外还有 np 里的一些函数。

另外的一些操作 np.concatenate([X, Y], axis=0), np.concatenate([X, Y], axis=1)

x.sum() 也可以指定 axis。

符合直觉:

X[-1], X[1:3]
X[1, 2] = 9
X[0:2, :] = 12
# 实际上,这赋值可以使用 broadcasting,如:
a = np.arange(12).reshape(3, 4)
a[:, :] = np.arange(3).reshape(3, 1)
print(a)
# [[0. 0. 0. 0.]
#  [1. 1. 1. 1.]
#  [2. 2. 2. 2.]]

PyTorch

基本同上,要 import torch ,然后把 torch 当作 np 使。

一些不同

MXNet

PyTorch

x.size

x.numel()

x.reshape((3, 4))

x.reshape(3, 4)

np.random.normal(0, 1, size=(3, 4))

torch.randn(3, 4)

np.array(...)

torch.tensor(...)

np.concatenate([X, Y], axis=0)

torch.cat((X, Y), dim=0)

np.concatenate([X, Y], axis=1)

torch.cat((X, Y), dim=1)

X.asnumpy()

X.numpy()

X.copy()

X.clone()

读入以及预处理数据

使用 pandas 库: import pandas as pd

一些基本操作

目标

操作

读入数据

可以使用 read_csv , read_excel 一系列函数。 .. note:: 在读入时, pandas 会把看起来像是 N/A (not available)的数据作为 N/A 值读入:

By default the following values are interpreted as NaN: ‘’, ‘#N/A’, ‘#N/A N/A’, ‘#NA’, ‘-1.#IND’, ‘-1.#QNAN’, ‘-NaN’, ‘-nan’, ‘1.#IND’, ‘1.#QNAN’, ‘<NA>’, ‘N/A’, ‘NA’, ‘NULL’, ‘NaN’, ‘n/a’, ‘nan

如果不想要让这些值被当作 N/A 的话,可以使用 na_values 来指定 N/A 值(可以统一指定,也可以每一列分别指定。还请看 文档 。

对 N/A 值处理

按列分割数据

提取部分列的数据可以用 iloc (integer-location based indexing):

X, Y = data.iloc[:, 0:2], data.iloc[:, 2]

非数字列转哑变量

把类似 enum 的列替换为多个哑变量列(dummy variables):

data = pd.get_dummies(data, dummy_na=True)

转化为 tensor

# mxnet
from mxnet import np
X, y = np.array(inputs.values), np.array(outputs.values)
# pytorch
import torch
X, y = torch.tensor(inputs.values), torch.tensor(outputs.values)

线性代数相关知识

不多说。不说明的话 MXNet 和 PyTorch 相同。

对应操作

概念

操作

转置

A.T

Element-wise 加减乘除

A + / - / * / / B

求和

A.sum() , A.sum(axis=0) , A.sum(axis=[0, 1], keepdims=True)

累积求和

A.cumsum(axis=0) (一行一行加下去,除第一行外每行都变了)

均值

A.mean() 也可以指定 axiskeepdims

点乘

MXNet: np.dot(A, B); PyTorch: torch.dot(A, B)

\(L_2\) 模以及 Frobenius 矩阵模: MXNet: np.linalg.norm(v); PyTorch: torch.norm(v)

另一种是 \(L_1\)np / torch.abs(v).sum() 但少用。

微积分

这个真不用多说了吧。

本节只是数学部分, autograd 之类的在下一节。

自动微分

后面的全部叫做 autograd 了,省事。代码片段看个感觉:

from mxnet import autograd, np, npx
npx.set_np()
x = np.arange(4.0)

x.attach_grad()
with autograd.record():
    y = 2 * np.dot(x, x)
y.backward()
x.grad

MXNet 基本上就是把要进行的操作放在 with autograd.record:

import torch
x = torch.arange(4.0)
x.requires_grad_(True)
y = 2 * torch.dot(x, x)
y.backward()
x.grad

PyTorch 就是算就完事了。

在 PyTorch 中,autograd 只能用于 size 为 1 的 y。MXNet 里可以用作 vector 的 y 上,但是其实也就是 y = y.sum() 之后再 autograd 而已。(因为机器学习的 cost function 常常就是有这么一步操作。)

y.detach 从计算图中切断 y 与原来的变量( .attach_grad() 了的那个)的联系,将 y 仅仅看作一个常量。

对任意一个从最开始的 x 出发的中间变量均可以进行 .backward 操作。MXNet 默认每一次 backward 均会覆盖上一次的(可在 attach_grad 里改),但 PyTorch 默认会加到前一次的结果之上,所以不想要的话要手动 x.grad.zero_() 清零。

计算原理大概是记录一个计算图,所以就算历经一大堆 if 语句 for 语句也是可以计算微分的。(想象分段函数,通过初等函数表达的每一段都是可以微分的。)(而且这边充其量也只是一阶近似。)

概率论

多项分布:

np.random.multinomial(10, fair_probs, size=500)

torch.distributions.multinomial.Multinomial(10, fair_probs).sample((500,))

从这个例子可以大概看出之后找随机分布生成器要去哪里找。(但从之后可以看出,PyTorch 的正态分布是在 torch.normal(0, 1, (num_examples, len(w))) 的。)

Three Easy Pieces

本章是线性神经网络。

SGD 是 stochastic gradient descent 的意思。

两个例子其一:手动实现

lr = 0.03
num_epochs = 3
net = linreg
loss = squared_loss

for epoch in range(num_epochs):
    for X, y in data_iter(batch_size, features, labels):
        with autograd.record():
            l = loss(net(X, w, b), y)
        l.backward()
        sgd([w, b], lr, batch_size)
    train_l = loss(net(features, w, b), labels)
    print(f'epoch {epoch + 1}, loss {float(train_l.mean()):f}')
lr = 0.03
num_epochs = 3
net = linreg
loss = squared_loss

for epoch in range(num_epochs):
    for X, y in data_iter(batch_size, features, labels):
        l = loss(net(X, w, b), y)
        l.sum().backward()
        sgd([w, b], lr, batch_size)
    with torch.no_grad():
        train_l = loss(net(features, w, b), labels)
        print(f'epoch {epoch + 1}, loss {float(train_l.mean()):f}')

基本流程就是:

  1. 随机初始化参数;

  2. 大堆训练数据(随机)分成块;

  3. 对每块依次先(一边记录计算图一边)从自由变量推算出 \(\hat{y}\)

  4. \(\hat{y}\) 反向推算出参数的梯度;

  5. 向梯度方向移动;

  6. 重复直至耗尽数据,此为一个周期;

  7. 重复以上步骤。

两个例子其二:各种工具库

from mxnet import autograd, gluon, np, npx

# ...

dataset = gluon.data.ArrayDataset(features, labels)
data_iter = gluon.data.DataLoader(dataset, batch_size, shuffle=True)

from mxnet.gluon import nn
net = nn.Sequential()
# Gluon 会自动根据之后的数据调整网络大小
# 因为我们数据是 [x1, x2]
# 所以之后 Gluon 模型里参数就是 [w1, w2, b]
# 下面的 1 表示输出的节点数,我们只有一个 y
net.add(nn.Dense(1))

from mxnet import init
# 这里的初始化实际上被留到了知道网络实际大小之后再进行
net.initialize(init.Normal(sigma=0.01))

loss = gluon.loss.L2Loss()
trainer = gluon.Trainer(net.collect_params(), 'sgd', {'learning_rate': 0.03})
num_epochs = 3
for epoch in range(num_epochs):
    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(f'epoch {epoch + 1}, loss {l.mean().asnumpy():f}')

# 获取参数
w = net[0].weight.data()
b = net[0].bias.data()
import torch
from torch.utils import data

# ...

dataset = data.TensorDataset(features, labels)
data_iter = data.DataLoader(dataset, batch_size, shuffle=True)

from torch import nn
net = nn.Sequential(nn.Linear(2, 1))

net[0].weight.data.normal_(0, 0.01) # [w1, w2]
net[0].bias.data.fill_(0)           # [b]

loss = nn.MSELoss()
trainer = torch.optim.SGD(net.parameters(), lr=0.03)
num_epochs = 3
for epoch in range(num_epochs):
    for X, y in data_iter:
        l = loss(net(X), y)
        trainer.zero_grad()
        l.backward()
        trainer.step()
    l = loss(net(features), labels)
    print(f'epoch {epoch + 1}, loss {l:f}')

# 获取参数
w = net[0].weight.data()
b = net[0].bias.data()

Softmax 回归

分类的处理。

这里就用到我们之前的哑变量处理了。有时这种互斥的哑变量组合也叫做 one-hot (只有一个能为 1)。

如果输出是多个哑变量,那么我们希望输出会服从一些基本的概率性质(至少非负、归一),从而让我们可以将其解读为概率。我们会用到 softmax 函数来进行这样的归化:

\begin{equation*} \hat{\mathbf{y}} = \mathrm{softmax}(\mathbf{o})\quad \text{where}\quad \hat{y}_j = \frac{\exp(o_j)}{\sum_k \exp(o_k)}. \end{equation*}

另外,得到概率之后我们需要一个类似模的运算来测量两个概率向量之间的距离,从而计算得 loss。这个衡量概率距离的我们常用 Cross-Entropy Loss ,因为它可以在求导过程中消去前一层的 softmax 函数的一些指数操作:

\begin{equation*} l(\mathbf{y}, \hat{\mathbf{y}}) = - \sum_{j=1}^q y_j \log \hat{y}_j. \end{equation*}

似乎就是这样,在网上找了找没有什么推导的,应该只是方便。这个函数本身来自于 信息论 。

一个实例:图像分类

数据读入

MXNet Gluon 以及 torchvision 自带一些数据集的下载机制。

mnist_train = gluon.data.vision.FashionMNIST(train=True)
mnist_test = gluon.data.vision.FashionMNIST(train=False)
transformer = gluon.data.vision.transforms.ToTensor()
train_iter = gluon.data.DataLoader(mnist_train.transform_first(transformer),
                                   batch_size, shuffle=True,
                                   num_workers=(0 if is_windows() else 4))
for X, y in train_iter:
    continue
trans = transforms.ToTensor()
mnist_train = torchvision.datasets.FashionMNIST(root="../data", train=True,
                                                transform=trans,
                                                download=True)
mnist_test = torchvision.datasets.FashionMNIST(root="../data", train=False,
                                               transform=trans, download=True)
train_iter = data.DataLoader(mnist_train, batch_size, shuffle=True,
                             num_workers=get_dataloader_workers())
for X, y in train_iter:
    continue
模型与初始化与 loss function
net = nn.Sequential()
net.add(nn.Dense(10))
net.initialize(init.Normal(sigma=0.01))
loss = gluon.loss.SoftmaxCrossEntropyLoss()
net = nn.Sequential(nn.Flatten(), nn.Linear(784, 10))
def init_weights(m):
    if type(m) == nn.Linear:
        nn.init.normal_(m.weight, std=0.01)
net.apply(init_weights);
loss = nn.CrossEntropyLoss()

其实我们的模型理论上应该再包含一层 softmax 层,但是从数学上:

全连接层 -> softmax -> cross-entropy -> loss

我们只是想要从 loss 反向传播得到 X 的梯度,那么其实把 softmax 直接放到 loss function 里也是同样效果。 而将 softmax 与 loss function 结合,我们将可以简化实际的运算,同时也可以避免先 exp 再 log 造成的精度损失。 MXNet 的 SoftmaxCrossEntropyLoss 这个命名也许更能说明其中的含义。

优化算法
# MXNet
trainer = gluon.Trainer(net.collect_params(), 'sgd', {'learning_rate': 0.1})
# PyTorch
trainer = torch.optim.SGD(net.parameters(), lr=0.1)
训练

摘录点代码。

# 一个周期
if isinstance(updater, gluon.Trainer):
    updater = updater.step
# for ...
    with autograd.record():
        y_hat = net(X)
        l = loss(y_hat, y)
    l.backward()
    # X.shape[0] is just batch_size
    updater(X.shape[0])
if isinstance(net, torch.nn.Module):
    net.train()
for X, y in train_iter:
    y_hat = net(X)
    l = loss(y_hat, y)
    if isinstance(updater, torch.optim.Optimizer):
        updater.zero_grad()
        l.backward()
        updater.step()

There and Back Again

本章开始叠层了。还有一些优化的技巧或常用手段。

多层叫做:MLP(multilayer perceptron)

Activations

因为(全连接)每一层其实就是一个矩阵点乘操作,如果没有什么处理的话,就可以直接化简:

\begin{equation*} \mathbf{W}^{(1)} \cdot \mathbf{W}^{(2)} \dots \mathbf{W}^{(m)} = \mathbf{W} \end{equation*}

最后化简为单层的。所以每一层之间我们要进行一点非线性的额外操作,称为 activations。

常用的

名称

介绍

ReLU (rectified linear unit)

\(\operatorname{ReLU}(x) = \max(x, 0)\) 虽然一阶导有断点,但是数值求解能用就行,性能好

Sigmoid

\(\operatorname{sigmoid}(x) = \frac{1}{1 + \exp(-x)}\) 之前常用(我半途而废时候的似乎就是这个)。可以当作 softmax 函数的特例。似乎也还有用。

Tanh (hyperbolic tangent)

\(\operatorname{tanh}(x) = \frac{1 - \exp(-2x)}{1 + \exp(-2x)}.\) \(\operatorname{tanh}(x) + 1 = 2 \operatorname{sigmoid}(2x)\)

简单的模型

net = nn.Sequential()
net.add(nn.Dense(256, activation='relu'), nn.Dense(10))
net.initialize(init.Normal(sigma=0.01))
net = nn.Sequential(nn.Flatten(), nn.Linear(784, 256), nn.ReLU(),
                    nn.Linear(256, 10))

def init_weights(m):
    if type(m) == nn.Linear:
        nn.init.normal_(m.weight, std=0.01)

net.apply(init_weights);

似乎 PyTorch 的操作更能让人看出数据经历的操作:

Flatten -> Linear -> ReLU -> Linear -> (softmax + loss)

而 MXNet 的 nn.Dense 则是在此层后加上 ReLU 的处理。(其实似乎 MXNet 也有个专门的 Activation 层,可以设置为 relu , sigmoid 等。)

更多的概念

概念

过拟合 (overfitting)

如其名,只见树木,不见森林。模型只是记住了数据,而不是提取数据特征。

模型复杂度 (model complexity)

(难直接衡量)

自由度 (degrees of freedom)

(基本上)可调参数的数量

校验数据集 (validation dataset)

把所用数据分为三份:训练用、测试(test)用、校验(validation)用。但是 validation 和 test 界限很多时候比较模糊。本书据称之后都是训练用加 validation 用。

K-fold 交叉检验

(数据很少时)把数据分为 K 份,训练时取一份作检验,其余作训练,训练 K 次,每次轮换。(这个的目的似乎并不是评估训练出来的特定模型,而是评估训练本身的那个方法。如选择 hyperparametres (e.g. 步进速率、训练循环数等))

欠拟合 (underfitting)

如其名。可能是模型参数量不够。

Weight Decay

改善 overfitting 的一种方法。从优化 loss function \(\operatorname{L}(\mathbf{w},b)\) 到优化:

\begin{equation*} \operatorname{L}(\mathbf{w}, b) + \frac{\lambda}{2} \|\mathbf{w}\|^2 \end{equation*}

后面那项准确叫做 \(\mathbf{w}\)\(L_2\) 模(乘个系数),但也有使用 \(L_1\) 模的。前者主要限制大的参数,后者似乎会把一些小的参数尽量归零(叫 feauture selection)(可能是这样的? \(\dots + \lambda |\mathbf{w}|\) )。

# MXNet
trainer = gluon.Trainer(net.collect_params(), 'sgd', {
    'learning_rate': lr,
    'wd': wd})
# 取消常数项的 weight decay
net.collect_params('.*bias').setattr('wd_mult', 0)
# PyTorch
# 只给 wight 添加 weight decay
trainer = torch.optim.SGD([
    {
        "params": net[0].weight,
        'weight_decay': wd
    }, {
        "params": net[0].bias
    }], lr=lr)

Dropout

在模型 forward 的途中随机添加一些噪声(随机失活节点(数据置零?)或者是随机浮动)。

似乎常用的是:

\begin{equation*} \begin{split}\begin{aligned} h' = \begin{cases} 0 & \text{ with probability } p \\ \frac{h}{1-p} & \text{ otherwise} \end{cases} \end{aligned}\end{split} \end{equation*}

Dropout 的手动实现:

# MXNet
mask = np.random.uniform(0, 1, X.shape) > dropout
return mask.astype(np.float32) * X / (1.0 - dropout)
# PyTorch
mask = (torch.rand(X.shape) > dropout).float()
return mask * X / (1.0 - dropout)

MXNet 有个新函数 autograd.is_training()

工具库调用:

# MXNet
net = nn.Sequential()
net.add(
    nn.Dense(256, activation="relu"),
    nn.Dropout(dropout1), nn.Dense(256, activation="relu"),
    nn.Dropout(dropout2), nn.Dense(10))
net.initialize(init.Normal(sigma=0.01))

# PyTorch
net = nn.Sequential(
    nn.Flatten(), nn.Linear(784, 256), nn.ReLU(),
    nn.Dropout(dropout1), nn.Linear(256, 256), nn.ReLU(),
    nn.Dropout(dropout2), nn.Linear(256, 10))
def init_weights(m):
    if type(m) == nn.Linear:
        nn.init.normal_(m.weight, std=0.01)
net.apply(init_weights);

Forward Propagation, Backward Propagation, and Computational Graphs

本节原理揭示。还请务必看一下。

Numerical Stability and Initialization

在训练时梯度如果不太稳定可能会出现问题。

一是梯度过小,训练过慢,无法在时间内稳定至接近于 optimum;一是梯度过大,可能直接毁掉模型。

前者似乎是不选用 sigmoid 函数的原因之一(因为 sigmoid 的导数在两端都很小)。

另外一点是从最开始,如果没有其它条件的话,全连接层里的各个参数都是完全等价的——如果全部初始化为一样的值,那么训练时它们都会发生相同的变动——所以我们必须把不同参数初始化得不同一些:

  1. 之前的例子是标准正态分布初始化;

  2. 也可 Xavier 初始化:尽量保持传递过程中的方差变化不太大: \(\frac{n_{\text{in}} + n_{\text{out}}}{2} \sigma^2 = 1\)

Environment and Distribution Shift

模型的部署可能影响新的现实数据。

数据的各种偏移会影响效果。

还挺有趣,可以读一读。

有一些校正算法。

有现今机器学习的一些分类。

縱列風

本章开始把各种层组合起来了。

MXNet 实现一个层(或一个块)需要继承 nn.Block ,PyTorch 的是 nn.Module 。MXNet 的 Block.add 对应的是 PyTorch 的 Module.add_module 。两者都需要实现个 self.forward(X) 的方法。

自定义块有可能在数据操纵中必须用到 CPU 的功能(Python 控制流),而影响使用 GPU 的训练效率。在十二章似乎有优化说明。

模型的参数

访问

# MXNet
print(net[1].params)
print(net[1].bias)
print(net[1].bias.data())
net[1].weight.grad()
print(net[0].collect_params())
print(net.collect_params())
net.collect_params()['dense1_bias'].data()
# PyTorch
# 多了一层 ReLU,所以 index 变成了 2
print(net[2].state_dict())
print(net[2].bias)
print(net[2].bias.data)
net[2].weight.grad == None
net[0].named_parameters()
net.state_dict()['2.bias'].data

初始化

MXNet 的就是之前用过的 Block.initialize 可以在 init 参数里加上 init.Xavier() , init.Constant(1) 等初始化方法。这种自动的方法还挺方便,如果想要自定义的初始化初始化方法那就需要手动继承 init.Initializer 并覆盖 _init_weight 方法(也有个 _init_bias 方法)。

PyTorch 似乎必须自己写一个函数并且使用 Module.apply(your_init_func) 来初始化。可以在函数里调用 nn.init.normal_(m.weight) , nn.init.zeros_(m.bias) 这种来初始化。

(下面有个初始化成 42 的例子可还行……)

MXNet 要注意自动微分过程里乱访问更改可能会有问题,似乎有一个 set_data 方法之后可以了解一下。

共用参数

# MXNet
shared = nn.Dense(8, activation='relu')
another = nn.Dense(8, activation='relu', params=shared.params)
# PyTorch
shared = nn.Linear(8, 8)
another = shared # 是的

延迟初始化

PyTorch 没有。

自定义层的参数

自定义层如果需要参数的话,要在 __init__ 里:

# MXNet
self.weight = self.params.get('weight', shape=(in_units, units))
self.bias = self.params.get('bias', shape=(units,))
# PyTorch
self.weight = nn.Parameter(torch.randn(in_units, units))
self.bias = nn.Parameter(torch.randn(units,))

保存至文件

对 tensor:( X 也可以是 [X, Y]{ 'x' : X, 'y' : Y }

npx.save(filename, X), X = npx.load(filename) 以及 torch.save(X, filename), X = torch.load(filename).

对整个模型:

# MXNet
net.save_parameters('mlp.params')
clone = YourModel()
clone.load_parameters('mlp.params')
# PyTorch
torch.save(net.state_dict(), 'mlp.params')
clone = YourModel()
clone.load_state_dict(torch.load('mlp.params'))
clone.eval()

GPU

# MXNet
X = np.ones((2, 3), ctx=npx.gpu(0))
X.copyto(npx.cpu())
X.as_in_ctx(npx.gpu(0)) # 若已处于设备则不复制
# PyTorch
X = torch.ones(2, 3, device=torch.device('cuda'))
Z = X.cpu()
X.cuda(0) # 若已处于设备则不复制

为什么不自动复制?因为复制很慢,所以需要手动控制。一个隐含的复制是 print 以及转换为 numpy 数据的时候。

库里的层:

# MXNet
net.initialize(ctx=npx.gpu())
# PyTorch
net = net.to(device=torch.cuda())

当然,处理的数据也需要预先传到 GPU

Like a Rolling Stone

卷积神经网络(CNN)。

从全连接到卷积

因为全连接层所有节点位置都是等价的(也没有边缘效应),所以用于处理一些具有位置信息的数据(如图片(x, y 轴)、音频(时间轴))的数据就会失掉相邻的位置信息。

另外,很多时候这种带有位置信息的数据都是具有平移不变性的:提取的信息在与不在与对应数据块的位置无关,只要数据块存在在某处即可。(一张图片中人脸在哪里都算是有人脸了。)

数学公式看起来难懂,结合图理解起来可能好一些:

../images/correlation.svg

参数有卷积核 kernel,padding,stride。

padding: 避免图像边缘信息损失:

../images/conv-pad.svg

stride: 为了计算效率或就是降低精度:

../images/conv-stride.svg

库的使用是:

# MXNet
# nn.Conv2D(out_channel_count, kernel_size, ...)
conv2d = nn.Conv2D(1, kernel_size=3, padding=(2, 1))
# PyTorch
# nn.Conv2d(in_channel_count, out_channel_count, ...)
conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1)

二者接受数据的维度均为 (batch_size, channel_count, width, height)

../images/conv-1x1.svg

上面是 \(1\times 1\) 的核的基本用法之一。也可以看到,每一个输出 chennel 会对应一组的核,输入 channel 数乘输出 channel 数才为核的总数。(如果我对“一个核”的概念没理解错的话。)

Pooling(池化层)

与卷积类似,但是没有核,只是在移动的窗口里取一些固定操作,如取最大值或是平均值。

(据本节说,卷积操作对位置较为敏感,而池化可以对此减弱。)

# MXNet
# 还有 nn.AvgPool2D
nn.MaxPool2D(pool_size, strides=2, padding=(1, 2))
# PyTorch
# 还有 nn.AvgPool2d
nn.MaxPool2d(pool_size, stride=2, padding=(1, 2))

对输入的数据维度与卷积一样要求: X.shape = (batch_size, channel_count, width, height).

输出与输入相同维度。

小例子大例子

本节以及下一章都是有名模型的例子。(然而大多数我的小笔记本直接跑不起来……)

  • LeNet : (-> conv -> pool) * 2 (-> dense)* 3

  • AlexNet : (-> conv -> pool) * 2 (-> conv) * 3 -> pool (-> dense) * 3

  • VGG : ((-> conv) * M -> pool) * M (-> dense) * 3

    In their VGG paper, Simonyan and Ziserman experimented with various architectures. In particular, they found that several layers of deep and narrow convolutions (i.e., 3×3) were more effective than fewer layers of wider convolutions.

  • NiN : (-> conv -> 1*1 conv -> 1*1 conv) * N -> pool

  • GoogLeNet : 有并行的数据流(有一点感动,因为 2G 显卡终于跑得起来了)。

  • Batch Normalization : 在 denseactivation 层之间或 convactivation 层之间的一层归化处理,防止数据发生大的数量级偏移。似乎可以加大学习率。因为是两层之间的,所以 MXNet 必须分开两层了 net.add(nn.Conv2D(6, kernel_size=5), nn.BatchNorm(), nn.Activation('sigmoid')) .

  • ResNet :

  • DenseNet :

(略)

MXNet 的 Dense 层会自动把输入的 shape 从 (batch_size, s1, s2, ...) 转为 (batch_size, total)

海螺小姐

循环神经网络。

  • Autoregressive Models: 以特定长度的时间序列为输入

    • Autoregressive Models: ~

    • Latent Regressive Models: 附加一块输入,该输入由上一次时间序列处理过的输出的一部分提供

  • Markov Models: 老朋友。认为下一个事件只与短时间内的历史有关,所以只需要这段时间内的输入即可。

  • 因果关系:不是模型吧。因为时间序列里面因果关系只能是单向的,所以输入的顺序也有影响?

数据预处理

  • 数据每一单位应转化为数字,如自然语言处理把每一个字/词转变为一个对应的编号。有时这种编号转变,为了减少计算量和模型复杂度,可以把词频较低的词去除,换为统一的一个已删除的编号。

  • 从长序列生成训练数据:

    • 随机采样:

    • 顺序分割(sequential partitioning)

评论