ML Naive

线性回归 (Linear Regression)

约定: 初始矩阵大小是样本数$\times$特征数,权重矩阵大小是输入次特征数$\times$输出维度数
$$L(\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$$

1
2
3
4
5
6
for epoch in range(num_epochs): # train several rounds
for X, y in train_iter: # iterate through batched samples
l = loss(net(X), y) # target: minimize loss
updater.zero_grad()
l.backward() # calculate direction
updater.step() # optimize one step

优化方法:SGD (Stochastic Gradient Descent)

1
2
3
4
5
def sgd(params, lr, batch_size):
with torch.no_grad():
for param in params:
param -= lr * param.grad / batch_size
param.grad.zero_()

多层感知机(MultiLayer Perceptron)

$$H^{(1)}=\sigma(XW^{(1)}+b^{(1)})$$

$$H^{(2)}=\sigma(H^{(1)}W^{(2)}+b^{(2)})$$

$$ O=H^{(2)}W^{(3)}+b^{(3)}$$

卷积神经网络 (Convolutional Neural Network, CNN)

对于图像 $H_{n\times m}$,常规的线性变换采用公式

$$H_{i, j} = U_{i, j} + \sum_k \sum_lW_{i, j, k, l} X_{k, l}$$

基于平移不变性的假设将其改进为

$$ H_{i, j} = u + \sum_a\sum_b V_{a, b} X_{i+a, j+b} $$

即放弃了 $\mathbf{V}$ 关于起始位置的依赖性,将四维降成两维。$\mathbf{V}$ 被称为 卷积核(convolution kernel)滤波器(filter)

感受野 (receptive field)

某一层的任意元素 $x$,其感受野是指在前向传播期间可能影响 $x$ 计算的所有元素(来自所有先前层)。

填充 (padding)

在应用多层卷积时,我们常常丢失边缘像素。卷积核越多、应用的卷积层越多,累积丢失的像素数越多。
通过填充——在输入图像的边界填充元素(通常是0)——来解决这个问题。
一般左右上下填充相同数量,使用 padding 参数向四个方向填充同样的行列数。

步幅 (stride)

卷积窗口从输入张量的左上角开始,向下和向右滑动。有时候为了高效计算或是缩减采样次数,卷积窗口可以跳过中间位置,每次滑动多个元素。使用 stride 参数。

$1\times 1$ 卷积

窗口大小取为 $1\times 1$,对通道进行操作。

池化层 (pooling)

目的:降低卷积层对位置的敏感性,同时降低对空间采样表示的敏感性。
实现:池化窗口从左到右、从上到下地在输入张量内滑动,并在每个位置,计算该窗口框出的张量的最大值(或平均值)。
若输入有多个通道,池化层将操作依次作用到各个通道上。

激活函数 $\sigma$

  • $\text{ReLU}(x)=\max(x,0)$,封装函数torch.relu(x)
    默认 $x=0$ 处导数为 $0$
    $$\frac{\text{d}}{\text{d}x}\text{ReLU}(x)=[x>1]$$
  • $\text{sigmoid}(x)=\frac{1}{1+e^{-x}}$
    当输入较大时,导数非常小,优化缓慢
    $$\frac{\text{d}}{\text{d}x}\text{sigmoid}(x)=\text{sigmoid}(x)(1-\text{sigmoid}(x))$$
  • $\tanh(x)=\frac{1-e^{-2x}}{1+e^{-2x}}$
    $$\frac{\text{d}}{\text{d}x}\tanh(x)=1-\tanh^2(x)$$

过拟合与欠拟合

  • 训练误差:模型在训练数据集上的误差
  • 泛化误差:模型在测试数据集上的误差,理想的泛化误差是在无限大的随机的数据集上的误差
  • 模型容量:模型拟合各种函数的能力,由变元数量以及变元的取值范围决定
  • 随着模型容量增大,训练误差减小,而泛化误差先减小后增大,泛化误差$-$训练误差被认为是泛化程度
  • 欠拟合是指模型无法继续减小训练误差。过拟合是指训练误差远小于泛化误差。
  • VC维:对于一个分类模型,存在一个数据集,不论如何进行标号,都存在一组参数能完美分类,称这些数据集中最大的一个的大小为VC维

权重衰减 (weight decay)

$$L(\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 + \frac{\lambda}{2} |\mathbf{w}|^2$$
也称为 L2 正则化,这项技术通过函数与零的距离来衡量变换的复杂度。L2范数对权重向量的大分量施加了巨大的惩罚。这使得算法偏向于选择均匀分布权重的模型。
torch implementation

1
2
3
4
5
6
net = nn.Sequential(nn.Linear(num_inputs, 1))
for param in net.parameters():
param.data.normal_()
trainer = torch.optim.SGD([
{"params":net[0].weight,'weight_decay': wd},
{"params":net[0].bias}], lr=lr)

暂退法 (dropout)

暂退法在前向传播过程中,计算每一内部层的同时注入噪声,这已经成为训练神经网络的常用技术。在整个训练过程的每一次迭代中,标准暂退法包括在计算下一层之前将当前层中的一些节点置零。
解释:神经网络过拟合与 每一层都依赖于前一层激活值 相关,暂退法也许可以破坏这种“共适应性”
在标准暂退法正则化中,每个中间活性值 $h$ 以暂退概率 $p$ 由随机变量 $h’$ 替换
$$h’=\begin{cases}0,&p\\frac{h}{1-p},&1-p \end{cases}$$
$E[h’]=h$,被隐藏的节点相当于不参与到计算中,也不被反向传播

Xavier初始化

  • 对于线性节点,其权重矩阵初始取关于 $\mathcal{N}(0, \frac{2}{n_{\text{in}}+n_{\text{out}}})$ 的独立同分布,此时可以尽可能避免梯度爆炸。(实际效果较好,收敛显著变快,且精度提高)
  • 激活函数选用 $\text{ReLU}()$ 而非 $\text{Sigmoid}()$ 可以避免在中间值较大时,出现 $\sigma’(z)$ 趋近于 $0$,导致收敛极慢的现象

前向传播计算图

forward propagate

环境和分布偏移

经验风险(empirical risk)

神经网络的优化目标是

$$\mathop{\mathrm{minimize}}\limits_f \frac{1}{n} \sum_{i=1}^n l(f(x_i), y_i)$$

这一项称为经验风险,是为了近似真实风险(true risk),整个训练数据上的平均损失,即从其真实分布 $p(x,y)$ 中抽取的所有数据的总体损失的期望值

$$E_{p(\mathbf{x}, y)} [l(f(\mathbf{x}), y)] = \iint l(f(\mathbf{x}), y) p(\mathbf{x}, y) \ \text{d}\mathbf{x}\text{d}y$$

协变量漂移(covariate shift)

在不同分布偏移中,协变量偏移可能是最为广泛研究的。 这里我们假设:虽然输入的分布可能随时间(或者随训练集与测试集)而改变,但标签函数(即条件分布 $P(y|x)$)没有改变(或在训练集与测试集中都相等)。统计学家称之为协变量偏移,这个问题是由于协变量(特征)分布的变化而产生的。

PyTorch Basics

network layer with Module

1
2
3
4
5
6
7
8
9
10
11
12
class MySequential(nn.Module):
def __init__(self, *args):
super().__init__()
for idx, module in enumerate(args):
# _module:OrderedDict
self._modules[str(idx)] = module

def forward(self, X):
# OrderedDict保证了按照成员添加的顺序遍历它们
for block in self._modules.values():
X = block(X)
return X

_module: 在模块的参数初始化过程中,系统知道在_modules字典中查找需要初始化参数的子块。

torch.nn.parameter.Parameter

参数是复合的对象,包含值、梯度和额外信息。

1
2
self.weight = nn.Parameter(torch.randn(in_units, units))
self.bias = nn.Parameter(torch.randn(units,))

Parameter storage

1
2
3
4
net = MODEL()
torch.save(net.state_dict(), 'file.name')
clone = MODEL()
clone.load_state_dict(torch.load('file.name'))

GPU

如果有多个GPU,我们使⽤ torch.cuda.device(f'cuda:{i}') 来表示第 $i$ 块GPU(i从0开始),cuda:0cuda 是等价的。


PyTorch APIs

torch.Tensor.attributes

  • Tensor.device属性是torch.device类,表示该变量的储存位置(cpu or cuda)
  • Tensor.dtype属性是torch.dtype类,主要有torch.int64,torch.float32
  • Tensor.shape属性是torch.Size类,描述各维度大小

torch.Tensor.methods

  • 以上属性是不可写属性,只能新建变量进行转化。函数Tensor.to()能够返回指定类型的Tensor,如

    1
    data.to(dtype=torch.float64, device=(torch.device(type='cuda')))
  • torch.Tensor([1,2]) 调用torch.Tensor的构造函数,构造的Tensor中元素为torch.float32

  • torch.tensor([1,2]) 拷贝提供的类型中的元素,dtype 保留它们的数据类型

  • torch.size() 返回Tensor.Size类型,表示张量的各维度大小

  • x[0:2,:] 取第 0、1 行的引用

  • Tensor.numpy() 返回numpy.array类型

  • x.item() 若x为单元素的 Tensor,则可以导出为 Python scalar

  • Tensor.abs(), Tensor.tan() 等 torch 中的定义的数学运算

  • Tensor.dim() 返回 Tensor 的维数

  • Tensor.unsqueeze(dim) 向 Tensor 中插入一个大小为 1 的维度,使其为第dim维

  • Tensor.cumsum(axis=dim) 沿着 dim 求累和

  • Tensor.norm() 计算 $\sqrt{\sum x_i^2}$

  • Tensor.requires_grad_(True) 设置 Tensor.requires_grad 属性为 True

  • Tensor.backward() 进行反向传播计算,只能在 Tensor 为标量时调用。Tensor不是标量的时候可以用Tensor.sum().backward()。反向传播只计算到各个叶子节点,非叶子节点没有结果

  • Tensor.grad() 返回计算得的导数

  • Tensor.grad.zero_() 将上次的导数计算结果清零

  • Tensor.argmax(axis) 求第 axis 维上最大元素对应的下标,压缩第 axis 维

  • Tensor.reshape(), Tensor.view() reshape 产生额外空间开销,view 共享内存

  • torch.cat((X,Y),dim)拼接X与Y,其中两者的维度dim被相加,其他维度要求完全相等。前一个输入只要使Tuple of Tensor即可
  • torch.cat((X,Y),dim,out=Z)要求Z为Tensor,将结果储存在Z中

torch.nn

Module

1
2
torch.nn.Module.cuda(
self: ~T, device: Union[int, torch.device, NoneType] = None)

将 Module 的所有参数与缓存区移动到指定的设别上
torch.nn.Module.parameters()
返回一个遍历当前 module 的 Parameter 的迭代器

nn.init

  • nn.init.normal_(tensor, mean=0, std=1) 依照正态分布N(mean, std)填充输入的张量或变量
  • nn.Module.apply(func) 将初始化函数应用到网络的各层上
  • nn.init.xavier_uniform_() 使用均匀分布 $\mathcal{U}(-a,a)$ 填充 Tensor
    1
    nn.init.xavier_uniform_(tensor:torch.Tensor, gain:float=1.0)

$$a = \text{gain} \times \sqrt{\frac{6}{\text{fan_in} + \text{fan_out}}} $$

  • nn.init.xavier_normal() 使用正态分布 $\mathcal{N}(0,\text{std}^2)$ 填充 Tensor

$$ \text{std} = \text{gain} \times \sqrt{\frac{2}{\text{fan_in} + \text{fan_out}}} $$

Loss Function

nn.MSELoss

均方方差 $loss(\hat{y}_i,y_i)=(\hat{y}_i-y_i)^2$

  • reduce,default 为 True,表示将所有样本的误差求和返回;若设为 false,则直接返回 Tensor
  • loss,仅当 reduce 为 True 时有效,default 为 True,将结果除以样本数

nn.CrossEntrophyLoss()

Layers

nn.Sequential(args) (container)

1
2
net = nn.Sequential(nn.Linear(2,1), nn.ReLU())
net = nn.Sequential(); net.add_module("tag", nn.ReLU())

nn.Conv2d(args)

1
2
3
4
5
6
7
8
9
torch.nn.Conv2d(
in_channels: int, # eg. 3 for RGB
out_channels: int,
kernel_size: Union[int, Tuple[int, int]],
stride: Union[int, Tuple[int, int]] = 1,
padding: Union[str, int, Tuple[int, int]] = 0,
... )
# allow Tuple[int, int]: support 2D specification, (Dim_0, Dim_1)
conv2d = nn.Conv2d(1, 1, kernel_size=(3, 5), padding=(0, 1), stride=(3, 4))

nn.MaxPool2d(args)

1
2
3
4
5
6
torch.nn.MaxPool2d(
kernel_size: Union[int, Tuple[int, ...]],
stride: Union[int, Tuple[int, ...], NoneType] = None,
padding: Union[int, Tuple[int, ...]] = 0,
... )
pool2d = nn.MaxPool2d((2, 3), padding=1, stride=2)

nn.LSTM(args)

1
2
3
4
5
6
7
8
9
10

torch.nn.LSTM(
input_size,
hidden_size,
num_layers,
bias=True, # enable bias or not
batch_first=True, # put batch at first dim or not
dropout=0,
bidirectional=False,
... )

nn.BatchNorm1d(feature_size:int)

nn.Flatten()

nn.Linear(in_features:int, out_features:int)

访问 Params:

  • net[0].weight.data->torch.Tensor
  • net[0].bias.data->torch.Tensor

nn.Dropout(dropout)

dropout 是抛弃率

手写网络

1
2
3
4
5
class Net(nn.Module):
def __init__(self, ...):
...
def forward(self, X): # 输入特征向量,返回预测结果
...

torch.optim.Optimizer

workflow:

  1. 创建Optimizer对象:传入网络模型的参数,设置学习率等优化方法的参数lr
  2. 使用函数zero_grad将梯度置为零
  3. 调用函数backward来进行反向传播计算梯度
  4. 使用step函数更新参数
    1
    2
    3
    4
    5
    6
    for input, target in dataset:
    optimizer.zero_grad()
    output = model(input)
    loss = loss_fn(output, target)
    loss.backward()
    optimizer.step() # a single step of optimization
  • torch.optim.SGD(model.parameters(), lr)
  • torch.optim.AdamW(model.parameters(), lr)

torch.utils.data

该包需要单独导入

torch.utils.data.Dataset

若数据集组织形式为从键值到样本的映射,则可以继承该类。它的子类应当重载__getitem__()方法,为给定的键值返回相应的样本;子类可以选择重载__len__()方法,返回数据集的大小(供Sampler与DataLoader使用)。

torch.utils.data.IterableDataset

若数据集组织形式是iterable,则继承该类。

torch.utils.data.Sampler

是所有采样器的基类,其子类应当重载__iter__()方法,提供迭代访问全部数据的方法;子类应当重载__len__()方法,提供被返回的迭代器的长度。

torch.utils.data.TensorDataset(*tensors)

将第一维的大小相同的Tensor合成为一个torch.utils.data.dataset.TensorDataset类,使用索引[l:r]访问时返回一个tuple,依次为各个Tensor在[l:r]的切片(仍然为一个Tensor)。

torch.utils.data.DataLoader

1
2
3
4
5
6
7
8
9
dataset = torch.utils.data.TensorDataset(train_data, train_label)
torch.utils.data.DataLoader(
dataset, # Dataset
batch_size=1, # int
shuffle=False, # bool
sampler=None, # Sampler or Iterable
batch_sampler=None, # Sampler or Iterable
num_workers=0, # int
)

将一个数据集与一个采样器结合,返回一个对应的可迭代对象。若 sampler 已经给定,则 shuffle 必须。默认使用的 sampler 按照 index 进行采样,需要使用连续整数索引。


Python basics

iterable – 可迭代对象

能够逐一返回其成员项的对象。包括所有序列类型 (例如 list, str 和 tuple) 以及某些非序列类型例如 dict, 文件对象 以及定义了 __iter__() 方法或是实现了序列语义的__getitem__() 方法的任意自定义类对象

可迭代对象被可用于 for 循环以及许多其他需要一个序列的地方(zip()、map() …)。当一个可迭代对象作为参数传给内置函数 iter() 时,它会返回该对象的迭代器。这种迭代器适用于对值集合的一次性遍历。

iterator – 迭代器

表示一连串数据流的对象。重复调用迭代器的 __next__() 方法(或将其传给内置函数 next())将逐个返回流中的项。当没有数据可用时则将引发 StopIteration 异常。迭代器必须具有 iter() 方法用来返回该迭代器对象自身,因此迭代器必定也是可迭代对象,可被用于其他可迭代对象适用的大部分场合。一个显著的例外是那些会多次重复访问迭代项的代码。容器对象(例如 list)在你每次向其传入 iter() 函数或是在 for 循环中使用它时都会产生一个新的迭代器。如果在此情况下你尝试用迭代器则会返回在之前迭代过程中被耗尽的同一迭代器对象,使其看起来就像是一个空容器。

import

单独import某个包名称时,不会导入该包中所包含的所有子模块。

list construction with for loop

1
2
3
4
>>> [i+j for i in range(2) for j in range(3)]
>>> [0, 1, 2, 1, 2, 3]
>>> print([i + j for i,j in zip(range(0, 3), range(10, 40, 10))])
>>> [10, 21, 32]

super

重写父类的函数的时候,如果要调用父类的函数,则用super().function(args)。若B继承了A,也可以在B中用A.function(args)来调用。

@property

当它被加在类的一个方法前时,@property装饰器会将方法转换为相同名称的属性,调用该方法时不需要也不能再加括号

dict

1
2
>>> a = dict(x = 1, y = 2)
>>> {'x':1, 'y':2}

I/O

  • readline() 读一整行,包括\n,若EOF则返回空字符串
  • map(function,iterable)对序列中的每个元素调用function,返回由结果构成的list

lambda

1
2
3
4
5
>>> def f(x):
return x * x
>>> g = lambda x: f(x * x)
>>> g(10)
>>> 10000

*args**kwargs

两者都是 python 中的可变参数,区别在于

  • *args 表示任何多个无名参数,它本质是一个 tuple
  • **kwargs 表示关键字参数,它本质上是一个 dict
    如果同时使用 *args 和 **kwargs 时,必须 *args 参数列要在 **kwargs 之前。
    1
    2
    3
    4
    5
    6
    >>> def fun(*args, **kwargs):
    ... pass
    ...
    >>> fun(1,2,3,4,A='a',B='b',C='c')
    args = (1,2,3,4)
    kwargs = ('A':'a','B':'b','C':'c')
    用于将序列与字典拆开:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    >>> def fun(data1, data2, data3):
    ... print("data1: ", data1)
    ... print("data2: ", data2)
    ... print("data3: ", data3)
    ...
    >>> args = ("one", 2, 3)
    >>> fun(*args)
    data1: one
    data2: 2
    data3: 3
    >>> kwargs = {"data3": "one", "data2": 2, "data1": 3}
    >>> fun(**kwargs)
    data1: 3
    data2: 2
    data3: one