Deep Learning and Pytorch

Pytorch

1. 预备知识

1.1 数据操作

1.1.1 Tensor

函数 功能
Tensor(size) 创建一个tensor
tensor(data) 将其他数据类型转为torch.Tensor
ones(size) 创建全1的Tensor
zeros(size) 创建全0的Tensor
eye(size) 创建对角线为1其余为0的Tensor
arange(s, e, step) 创建从s到e-1 步长尾step的1维 Tensor
linspace(s, e, points) 从s到e均匀取出points个点
rand/randn(size) 均匀/标准分布
normal(mean, std)/uniform(from, to) 正态/均匀分布

可以指定数据类型dtype和存放device(cpu/gpu)

1.1.2 操作

加法:“x + y” or torch.add(x, y) or inplace操作(不开辟新的内存)x.add_(y)

索引:与正常列表索引方法一致,注意索引结果与原Tensor共享内存

深复制:x_cp = x.clone()

线性代数:

函数功能trace求矩阵的迹diag取出对角线元素t取转置dot/cross内积/外积inverse求逆svd奇异值分解\begin{array}{|c|c|} \hline 函数 & 功能\\ \hline trace & 求矩阵的迹\\ \hline diag & 取出对角线元素\\ \hline t & 取转置\\ \hline dot/cross & 内积/外积\\ \hline inverse & 求逆\\ \hline svd & 奇异值分解\\ \hline \end{array}

1.1.3 广播机制

当两个形状不同的Tensor相加时,Tensor中的元素会被广播,实际上就是填充,使两个矩阵形状相同再相加,但似乎只有Tensor是一维时才能广播

1.1.4 Numpy转换

  • torch.from_numpy(np.array)共享内存
  • torch.tensor(np.array)不共享内存

1.2 自动求梯度

1.2.1 Tensor

创建Tensor对象时,可以将属性.requires_grad设置为True,这样可以开始跟踪每一步操作,并将操作结果反映在.grad_fn属性上,如果是初始Tensor,.grad_fn为None

也可以通过.requires_grad_( )来改变Tensor的.requires_grad属性

1.2.2 梯度

scalar.backward( )可以对所有张量链式法则求导,得到梯度

注意这里必须为标量scalar,不允许张量对张量求导,当求导对象是张量时,要在.backward( )中传递一个和张量同型的张量,通过对两个张量*乘求和得到标量再求梯度

反向传播的梯度结果会累加,因此每次反向传播前需要用x.grad.data.zero_( )将梯度清零

当中间变量用torch.no_grad( )包裹时,它是不会被计入最终表达式进行梯度传播的

1
2
3
with torch.no_grad():
y = x ** 3
z = x + y # 在backward()时不算y

可以用tensor.data对变量数据进行修改而不影响梯度

2. 深度学习基础

2.1 线性回归

线性回归的过程已经非常熟悉了,无非建模型、获取数据、计算损失函数、优化

这里主要学习如何用pytorch来实现

过程中学到了函数用于画散点图

1
2
3
4
5
6
7
8
9
10
11
def use_svg_display():
# 用矢量图表示
display.set_matplotlib_format('svg')

def set_figsize(figsize = (3.5, 2.5)):
use_svg_display()
# 设置图的尺寸
plt.rcParams['figure.figsize'] = figsize

set_figsize()
plt.scatter(features[:, 0].numpy(), label.numpy()) # 以第一个特征和label的关系为例

2.1.1 读取数据

1
2
3
4
5
6
7
def data_iter(batch_size, features, labels):
num_example = len(features)
indices = list(range(num_examples))
random.shuffle(indices)
for i in range(0, num_examples, batch_size):
j = torch.LongTensor(indices[i: min(i + batch_size, num_examples)])
yield features.index_select(0, j), labels.index_select(0, j)

这里的tensor.index_select(0, j)是一种取样方式,其中参数0表示对二维tensor横着一行一行取,可以改为参数1就是竖着一列一列取

而j是一个LongTensor类型的tensor,其中为要取出元素的索引,最后得到的data_iter可以看成每个batch为一组,训练时一组一组来训

2.1.2 训练

训练需要设置epoch数,每次epoch要将所有样本遍历一遍,遍历样本时,通过随机梯度下降,选取一个batch的样本计算损失函数和梯度来更新参数,每次更新之前需要把参数的梯度清零

2.1.3 简便实现

1
2
3
4
import torch.utils.data as Data

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

起到了和之前的data_iter( )函数同样的效果

还可以通过继承torch.nn.Module来直接构建神经网络

1
2
3
4
5
6
7
8
class LinearNet(nn.Module):
def __init__(self, n_feature):
super(LinearNet, self).__init__()
self.linear == nn.linear(n.feature, 1) # 参数分别为输入和输出维度
# 定义前向传播
def forward(self, x):
y = self.linear(x)
return y

损失函数可以使用nn.MSEloss计算

优化算法可以使用torch.optim中的SGD和Adam等,参数需要传递模型权重和学习率

2.2 softmax回归

2.2.1 softmax函数

yi^=exp(oi)i=1Sexp(oi)oi=xjwji\hat{y_i} = \frac{exp(o_i)}{\sum_{i=1}^{S}exp(o_i)}\\ o_i=\sum x_j w_{ji}

2.2.2 交叉熵损失函数

可以更好地衡量两个概率分布的差异

H(y,y^)=jqyjlogyj^H(y,\hat{y})=-\sum_j^q y_j log \hat{y_j}

则损失函数为

l(Θ)=1ninH(y(i),y^(i))l(\Theta)=\frac{1}{n}\sum_i^n H(y^{(i)},\hat{y}^{(i)})

2.2.3 实现

1
2
3
4
def softmax(X):
X_exp = X.exp()
partition = X_exp.sum(dim=1, keepdim=True) # dim=1表示对tensor的行做操作,keepdim表示保留列的维度
return X_exp / partition # 应用了广播机制
1
2
def cross_entropy(y_hat, y):
return - torch.log(y_hat.gather(1, y.view(-1, 1)))

实际上这里对交叉熵损失函数做了简化,因为在交叉熵损失函数中,非标签类别对应的预测概率权重是0,标签类别对应权重是1,因此只需要知道每个样本对标签类的预测概率就好,这里的gather就是这个作用

2.3 多层感知机

实现了多层神经网络,中间层也叫隐藏层,但如果所有层都是线性层的话,最终的神经网络实现的功能和一个线性层无异,因此需要引入非线性变换

2.3.1 激活函数

ReLU函数

ReLU(x)=max(x,0)ReLU(x)=max(x,0)

只保留正数,将负元素清零

Sigmoid函数

sigmoid(x)=11+exp(x)sigmoid(x)=\frac{1}{1+exp(-x)}

由0到1递增,呈S型

tanh函数

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

由-1到1递增,呈S型

2.3.2 多层感知机

至少含一个隐藏层,每个隐藏层输出通过激活函数变换,输出规则如下:

H=ϕ(XWh+bh)O=HWo+boH=\phi(XW_h+b_h)\\ O=HW_o+b_o

2.3.3 实现

1
2
3
4
5
6
7
8
net = nn.Sequential(
nn.Linear(num_inputs, num_hiddens),
nn.ReLU(),
nn.Linear(num_hiddens, num_outputs),
)

for params in net.parameters():
init.normal_(params, means, std=0.01) # 参数初始化

2.4 欠拟合、过拟合和模型选择

欠拟合:模型不够复杂,使训练误差和泛化误差都比较大

过拟合:模型过于复杂,使训练误差趋近于0而泛化误差越过最低点继续增大

模型选择:有奥卡姆剃刀原则,就是要“正好”,模型不大也不小,使泛化误差最小,训练误差也小但不是最小

2.5 权重衰减

对模型的过拟合问题,权重衰减是一个解决办法,等价于L2L_2范数正则化,即在损失函数中添加权重的L2L_2范数

ldecay(w,b)=l(w,b)+w2l_{decay}(w,b)=l(w,b)+||w||^2

这种方法可以防止权重过大,在设置优化器时可以设置weight_decay参数来使用权重衰减

1
optimizer = torch.optim.SGD([w, b], lr=lr, weight_decay=wd)

2.6 丢弃法

2.6.1 方法

丢弃法同样是为了应对过拟合问题,对隐藏层使用丢弃法,随机的丢弃隐藏层的单元,从而使输出无法过度依赖隐藏层中的任何一个,从而起到了正则化作用来应对过拟合

设丢弃一个单元的概率为pp,随机变量ξi\xi_i为0和1的概率分别为pp1p1-p,丢弃后隐藏单元表达式为

hi=ξi1phih_i'=\frac{\xi_i}{1-p}h_i

但丢弃法不改变输入的期望值

E(hi)=E(ξi)1phi=hiE(h_i')=\frac{E(\xi_i)}{1-p}h_i=h_i

但是在测试时为了保证结果的确定性,一般不使用丢弃法

2.6.2 实现

一种方式:

1
2
3
4
5
6
7
8
9
10
def dropout(X, drop_prob):
X = X.float()
assert 0 <= drop_prob <= 1
keep_prob = 1 - drop_prob
# drop out all when drpo_prob == 1
if keep_prob == 0:
return torch.zero_like(X)
mask = (torch.rand(X.shape) < keep_prob).float

return mask * X / keep_prob

简介实现,在每层激活函数后添加Dropout层:

1
2
3
4
5
6
7
8
9
net = nn.Sequential(
nn.Linear(num_inputs, num_hiddens1),
nn.ReLU(),
nn.Dropout(drop_prob1),
nn.Linear(num_hiddens1, num_hiddens2),
nn.ReLU(),
nn.Dropout(drop_prob2),
nn.Linear(num_hiddens2, num_outputs),
)

2.7 数值稳定性和模型初始化

2.7.1 衰减和爆炸

随着网络层数增多,变量计算次数也增加,就可能出现最终结果过小(衰减)或过大(爆炸)的问题,同时梯度也可能出现同样的衰减和爆炸

2.7.2 随机初始化模型参数

当初始化模型参数时将其设置为全部相等,那么正向传播时得到的结果相等,反向传播得到的梯度也相等,梯度下降最终得到的结果也相等,这样就导致几个隐藏层单元起到的作用是一样的,因此需要随机初始化

Pytorch默认初始化:采用正态分布

Xavier随机初始化:输入维度为aa,输出维度为bb,该层每个权重采样于如下均匀分布U(6a+b,6a+b)U(-\sqrt{\frac{6}{a+b}},\sqrt{\frac{6}{a+b}})

3. 深度学习计算

3.1 模型构造

3.1.1 继承Module类来构造模型

通过继承torch.nn.Module来自由组建模型,以一个具有一层隐藏层的多层感知机为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
import torch
from torch import nn

class MLP(nn.Module):
def __init__(self, **kwargs):
super(MLP, self).__init__(**kwargs):
self.hidden = nn.Linear(784, 256)
self.act = nn.ReLU()
self.output = nn.Linear(256, 10)

def forward(self, X):
a = self.act(self.hidden(X))
return self.output(a)

其中无需定义反向传播函数,系统会自动求梯度生成backward( )函数

1
2
net = MLP()
net(X)

net(X)会调用Module类的_ _ call _ _函数,这个函数会调用forward函数来完成前向传播

3.1.2 Module的子类

Sequential类

当将网络的每层传入Sequential类时,模型会调用add_module方法将module添进_modules中,组织成一个OrderedDict,当调用模型时,会逐层计算

ModuleList类

将一个模块的列表作为输入,还可以通过append方法来添加,内部仍组织成一个OrderedDict,但与Sequential不同的是,列表内部各模块之间没有什么顺序联系,forward( )函数需要自己定义,Module的参数会自动添加到整个网络

ModuleDict类

将一个模块的字典作为输入,也可以像字典那样添加和访问,同样forward( )函数需要自己定义,Module的参数会自动添加到整个网络

3.2 模型参数

3.2.1 访问

可以通过Module类的parameters( )和named_parameters来访问模型参数,后者会返回模型参数的名字

3.2.2 初始化

可以通过torch.nn.init来初始化参数

1
2
init.normal_(weight, mean=0, std=0.01)
init.constant_(bias, val=0)

3.2.3 共享

forward调用时调用同一个层,或者构建模型时用同一个Module,可以实现参数共享

3.3 自定义层

通过继承Module类和forward函数来实现一个层,当模型含有参数时,要把模型的参数设置成Parameter类,或者用ParameterList或ParameterDict组织起来,然后在forward函数中通过对输入和参数操作来实现计算

3.4 读取和存储

3.4.1 读写Tensor

通过save和load来存储到disk以及读入内存

1
2
3
4
5
6
7
torch.save(x, 'x.pt')												# tensor
torch.save([x, y], 'xy_list.pt') # list of tensors
torch.save({'x': x, 'y': y}, 'xy_dict.pt') # dict of tensors

x = torch.load('x.pt')
xy_list = torch.load('xy_list.pt')
xy_dict = torch.load('xy_dict.pt')

3.4.2 读写模型

state_dict保存了模型的可学习参数,每层的参数都组织成了一个tensor字典

保存

1
torch.save(model.state_dict(), PATH)

加载

1
model.load_state_dict(torch.load(PATH))

保存和加载时的对象大概率来自同一个网络的类,因为这样可以使参数和网络的规模完美契合

3.5 GPU计算

3.5.1 计算设备

查看GPU是否可用

1
torch.cuda.is_available()	# True or False

查看GPU数量

1
torch.cuda.device_count()

查看当前GPU索引号

1
torch.cuda.current_device()

根据索引查看GPU名字

1
torch.cuda.get_device_name(0)

3.5.2 Tensor的GPU计算

1
2
x = torch.tensor([1, 2, 3])
x = x.cuda(0) # 转移到0号GPU上

查看tensor所在设备

1
x.device

创建时指定设备

1
2
3
4
5
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

x = torch.tensor([1, 2, 3], device=device)
# or
x = torch.tensor([1, 2, 3]).to(device)

创建在不同设备上的数据是不能直接计算的,包括gpu和cpu、不同的gpu

3.5.3 模型的GPU计算

1
net.cuda()

直接将模型转移到GPU上,之后要保证输入也在GPU上

4. 卷积神经网络

4.1 二维卷积层

4.1.1 二维互相关运算

将输入数组与二维卷积核做互相关运算,卷积窗口从左到右、从上到下按元素相乘并求和

1
2
3
4
5
6
7
def corr2d(X, K):
h, w = K.shape
Y = torch.zeros((X.shape[0] - h + 1, X.shape[1] - w + 1))
for i in range(Y.shape[0]):
for j in range(Y.shape[1]):
Y[i, j] = (X[i: i + h, j: j + w] * K).sum()
return Y

将卷积核左右翻转、上下翻转,再做互相关运算实际上就是卷积运算,在实际使用中卷积运算和互相关运算没有区别,因为卷积核参数都是学出来的,对最终的预测结果没有影响,只不过卷积核左右上下翻转

4.1.2 特征图和感受野

特征图:经过卷积运算的输出可以看作是输入在空间维度上的表征

感受野:输出的某一单元,对应输入的一个卷积核范围称为感受野,当网络更深时,输出的一个单元可以对应输入的更大范围,因此可以使单个单元的感受野更加广阔,捕捉输入的更大尺寸特征

4.2 填充和步幅

一般来说,假设输入的形状是nh×nwn_h \times n_w,卷积核窗口的形状是kh×kwk_h \times k_w,输出的形状则是

(nhkh+1)×(nwkw+1)(n_h - k_h + 1)\times (n_w - k_w + 1)

4.2.1 填充

当输入的上下填充共ph=kh1p_h=k_h-1行,左右填充共pw=kw1p_w=k_w-1列,可以使输入与输出形状相同

一般卷积核的高宽都是奇数,因此输入上下、左右填充数相等,使输出Y[i,j]Y[i,j]对应以X[i,j]X[i,j]为中心的窗口卷积运算得到的

1
conv2d = nn.Conv2d(in_channels=1, out_channels=1, kernel_size=(5, 3), padding=(2, 1))

4.2.2 步幅

卷积核窗口移动的行数和列数称为步幅,设上下步幅为shs_h,左右步幅为sws_w,则输出形状为

(nhkh+ph+sh)/sh×(nwkw+pw+sw)/sw\lfloor(n_h-k_h+p_h+s_h)/s_h\rfloor \times \lfloor(n_w-k_w+p_w+s_w)/s_w\rfloor

4.3 多输入通道和多输出通道

4.3.1 多输入通道

输入数据增加一个输入通道的维度,同时卷积核也增加了这一维度且数量相等,实际上就每个通道各做一次二维相关运算,最后把结果相加就行

4.3.2 多输出通道

将卷积核创建为co×ci×kh×kwc_o\times c_i\times k_h\times k_w,每个输出通道对应一个4.3.1的多输入通道的运算

4.3.3 1×11\times1卷积层

将输入通道数看成特征数,整个单元数看成数据样本数,所以实际上就能将输入看成是nhnw×cin_h*n_w \times c_i的二维矩阵,将卷积核看成是ci×coc_i\times c_o的矩阵,矩阵乘法得到输出nhnw×con_h*n_w \times c_o

4.4 池化层

池化层可以缓解卷积层对位置的过度敏感性,有最大池化和平均池化

和卷积层一样,有池化窗口,有填充和步幅,有多通道输入,输出通道数与输入通道数相等

1
pool2d = nn.MaxPool2d((2, 4), padding=(1, 2), stride=(2, 3))

4.5 卷积神经网络

卷积神经网络识别图像可以防止同一列邻近像素距离过远和模型参数规模过大的问题

4.5.1 LeNet

分为卷积层块和全连接层块两个部分

卷积层块:卷积层后Sigmoid激活,然后最大池化

卷积层块传入全连接层块时会将每个样本变平(flatten),向量长度为通道 X 高 X 宽

4.5.2 AlexNet

8层变换,5层卷积层、2层全连接隐藏层、1层全连接输出层

激活函数选用ReLU,防止Sigmoid在比较小的正区域梯度几乎为0的问题

全连接层还使用了dropout

4.5.3 VGG

VGG块:连续使用数个相同的填充为1、窗口形状为3×33\times3的卷积层后接上一个步幅为2、窗口形状为2×22\times2的最大池化层

用小卷积核优于大卷积核,这样可以增加网络的深度保证复杂的学习模式,参数也更少

4.5.4 NiN

网络中的网络:串联多个由卷积层和全连接层构成的小网络来构建深层网络

NiN块:一个卷积层加两个充当全连接层的1×11\times1卷积层

4.5.5 GoogLeNet

Inception块:

5.9_inception

有4条并行的线路,中间两个线路的1×11\times1用来减少通道数,通道合并层做的是将输出的所有通道相加

4.6 批量归一化

批量归一化可以维持深度网络的中间输出的数值稳定性

4.6.1 全连接层批量归一化

归一化设置在仿射变换和激活函数之间,设输入为uu,仿射变换输出为

x=Wu+bx=Wu+b

batch大小为mm,归一化结果为

y(i)=BN(x(i))y^{(i)}=BN(x^{(i)})

μ=1mi=1mx(i)σ2=1mi=1m(x(i)μ)2x^(i)=x(i)μσ2+ϵ\mu=\frac{1}{m}\sum^m_{i=1}x^{(i)}\\ \sigma^2=\frac{1}{m}\sum^m_{i=1}(x^{(i)}-\mu)^2\\ \hat{x}^{(i)}=\frac{x^{(i)}-\mu}{\sqrt{\sigma^2+\epsilon}}

这里ϵ>0\epsilon>0是一个很小的常数保证分母大于0

y(i)=γx^(i)+βy^{(i)}=\gamma⊙\hat{x}^{(i)}+\beta

⊙表示元素相乘,γ\gammaβ\beta是两个可学习参数,如果最终学习结果使γ=σ2+ϵ\gamma=\sqrt{\sigma^2+\epsilon}β=μ\beta=\mu,可以认为这里不需要归一化

4.6.2 卷积层批量归一化

对输出单通道上的mm个样本做同时归一化

4.6.3 实现

1
2
nn.BatchNorm1d(num_features)	# 全连接层
nn.BatchNorm2d(num_channels) # 卷积层

4.7 残差网络(ResNet)

添加新的层有助于降低训练误差,可以添加一个恒等映射,但实际中误差不降反升,因此提出残差网络

5.11_residual-block

左侧要学习的是理想映射f(x)f(x),右侧要学习的是残差映射f(x)xf(x)-x,残差映射更容易优化

4.8 稠密连接网络(DenseNet)

与ResNet不同的是,不是输入与输出相加,而是将输入与输出在通道维上连结

5. 循环神经网络

5.1 循环神经网络

每一个时间点的输出由隐藏状态决定,该时间点的隐藏状态由该时间点的输入和上一个时间点的隐藏状态决定

Ot=HtWhq+bqHt=ϕ(XtWxh+Ht1Whh+bh)O_t=H_tW_{hq}+b_q\\ H_t=\phi(X_tW_{xh}+H_{t-1}W_{hh}+b_h)

3d55590162be0cc924c3ce29f98b

5.2 实现

5.2.1 one-hot向量

根据每个字符对应的索引,将字符转换为索引位置为1其余位置为0的向量

输入为(batch_size, num_steps)的二维矩阵,转换过后是(num_steps, batch_size, vocab_size)的三维矩阵

5.2.2 梯度裁剪

为了防止梯度爆炸,裁剪后梯度为

min(θg,1)gmin(\frac{\theta}{||g||},1)g

5.2.3 困惑度

困惑度是交叉熵损失函数取指数后的值

  • 最佳情况下,模型总是把标签类别的概率预测为1,此时困惑度为1;
  • 最坏情况下,模型总是把标签类别的概率预测为0,此时困惑度为正无穷;
  • 基线情况下,模型总是预测所有类别的概率都相同,此时困惑度为类别个数。

显然,任何一个有效模型的困惑度必须小于类别个数。在本例中,困惑度必须小于词典大小vocab_size

5.2.4 简洁实现

1
rnn_layer = nn.RNN(input_size=vocab_size, hidden_size=num_hiddens)

该层的输入形状为(num_steps, batch_size, vocab_size),但输出并不是真正的输出,而是隐藏单元的输出,形状为(num_steps, batch_size, num_hiddens),所以想要得到最终输出后面还需要接一个线性层

5.3 门控循环单元(GRU)

t时间点输入与上一时间点隐藏状态生成重置门Rt=σ(XtWxr+Ht1Whr+br)R_t=\sigma(X_tW_{xr}+H_{t-1}W_{hr}+b_r)

t时间点输入与上一时间点隐藏状态生成更新门Zt=σ(XtWxz+Ht1Whz+bz)Z_t=\sigma(X_tW_{xz}+H_{t-1}W_{hz}+b_z)

由更新门、输入和上一时间点隐藏状态生成候选隐藏状态H~t=tanh(XtWxh+(RtHt1)Whh+bh)\widetilde{H}_t=tanh(X_tW_{xh}+(R_t⊙H_{t-1})W_{hh}+b_h)

再由上一隐藏状态、候选隐藏状态和更新门生成该时间点隐藏状态Ht=ZtHt1+(1Zt)H~tH_t=Z_t⊙H_{t-1}+(1-Z_t)⊙\widetilde{H}_t

6.7_gru_3

5.4 长短期记忆(LSTM)

首先由上一时间步隐藏状态和输入生成遗忘门、输入门、输出门和候选记忆细胞

  • 遗忘门Ft=σ(XtWxf+Ht1Whf+bf)F_t=\sigma(X_tW_{xf}+H_{t-1}W_{hf}+b_f)
  • 输入门It=σ(XtWxi+Ht1Whi+bi)I_t=\sigma(X_tW_{xi}+H_{t-1}W_{hi}+b_i)
  • 输出门Ot=σ(XtWxo+Ht1Who+bo)O_t=\sigma(X_tW_{xo}+H_{t-1}W_{ho}+b_o)
  • 候选记忆细胞C~t=tanh(XtWxc+Ht1Whc+bc)\widetilde{C}_t=tanh(X_tW_{xc}+H_{t-1}W_{hc}+b_c)

然后由上一时间步记忆细胞经过遗忘门,候选记忆细胞经过输出门得到该时间步记忆细胞

  • 记忆细胞Ct=FtCt1+ItC~tC_t=F_t⊙C_{t-1}+I_t⊙\widetilde{C}_t

然后由当前时间步的记忆细胞经过输出门得到隐藏状态

  • 隐藏状态Ht=Ottanh(Ct)H_t=O_t⊙tanh(C_t)

6.8_lstm_3

6. 优化算法

由于梯度下降、随机梯度下降部分已经比较熟悉,此部分内容略

6.1 动量法

动量法创建初始速度变量v0=0v_0=0,接下来每次迭代为

vt=γvt1+ηtgtxt=xt1vtv_t=\gamma v_{t-1}+\eta_t g_t\\ x_t=x_{t-1}-v_t

超参数γ\gamma满足0γ10\le \gamma \le 1

由指数加权移动平均,每次对11γ\frac{1}{1-\gamma}个时间步的梯度做指数加权平均来更新自变量

6.2 AdaGrad算法

当梯度较大时,使用大的学习率容易发散,当梯度较小时,使用小的学习率收敛过慢,因此使用统一的学习率可能会在不同的方向上出问题,该算法尝试使用不同的学习率来更新

st=st1+gtgtxt=xt1ηst+ϵgts_t=s_{t-1}+g_t⊙g_t\\ x_t=x_{t-1}-\frac{\eta}{\sqrt{s_t+\epsilon}}⊙g_t

该算法会使学习率逐渐变小,因此当早期学习率下降较快且还没到收敛时,可能会由于学习率过小导致找不到最优解

6.3 RMSProp算法

为了解决AdaGrad算法的问题,引入了指数加权移动平均

st=γst1+(1γ)gtgtxt=xt1ηst+ϵgts_t=\gamma s_{t-1}+(1-\gamma)g_t⊙g_t\\ x_t=x_{t-1}-\frac{\eta}{\sqrt{s_t+\epsilon}}⊙g_t

这样可以防止学习率一直下降的问题

6.4 AdaDelta算法

同样为了解决AdaGrad的问题,特别的是,这个算法没有学习率,但引入了额外的状态变量表示自变量的变化Δx\Delta x

st=ρst1+(1ρ)gtgtgt=Δxt1+ϵst+ϵgtxt=xt1gtΔxt=ρΔxt1+(1ρ)gtgts_t=\rho s_{t-1}+(1-\rho)g_t⊙g_t\\ g_t'=\sqrt{\frac{\Delta x_{t-1}+\epsilon}{s_t+\epsilon}}⊙g_t\\ x_t=x_{t-1}-g_t'\\ \Delta x_t=\rho \Delta x_{t-1}+(1-\rho)g_t'⊙g_t'

6.5 Adam算法

结合RMSProp和动量法,使用指数加权移动平均

vt=β1vt1+(1β1)gtst=β2st1+(1β2)gtgtv_t=\beta_1 v_{t-1}+(1-\beta_1)g_t\\ s_t=\beta_2 s_{t-1}+(1-\beta_2)g_t⊙g_t

偏差修正

v^t=vt1β1ts^t=st1β2t\hat{v}_t=\frac{v_t}{1-\beta^t_1}\\ \hat{s}_t=\frac{s_t}{1-\beta^t_2}

然后更新自变量

gt=ηv^ts^t+ϵxt=xt1gtg_t'=\frac{\eta \hat{v}_t}{\sqrt{\hat{s}_t}+\epsilon}\\ x_t=x_{t-1}-g_t'

7. 自然语言处理

7.1 词嵌入

7.1.1 为何不用one-hot

one-hot难以表达相似词之间的相似度,例如不同词之间的余弦相似度都为0

7.1.2 跳字模型

每个词可以作为中心词或背景词,中心词向量表示为viv_i,背景词向量表示为uiu_i,当以wcw_c为中心词wow_o为背景词时的概率为

P(wowc)=exp(uoTvc)iVexp(uiTvc)P(w_o|w_c)=\frac{exp(u_o^Tv_c)}{\sum_{i\in V}exp(u_i^Tv_c)}

训练模型(竟然每个向量表示都是一个参数)时,最小化似然函数(背景窗口大小为m)

t=1Tmjm,j0P(w(t+j)w(t))\prod_{t=1}^T \prod_{-m\le j\le m,j\neq 0}P(w^{(t+j)}|w^{(t)})

7.1.3 连续词袋模型

该模型与跳字模型相反,是由背景词来预测中心词

Wo=wo1,...,wo2mW_o={w_{o1},...,w_{o2m}},且uˉo=(uo1+...+uo2m)/2m\bar{u}_o=(u_{o1}+...+u_{o2m})/2m,那么

P(wcWo)=exp(vcTuˉo)iVexp(viTuˉo)P(w_c|W_o)=\frac{exp(v_c^T\bar{u}_o)}{\sum_{i\in V}exp(v_i^T\bar{u}_o)}

似然函数也相似

7.2 近似训练

由于似然函数涉及词典大小项的累加,计算开销可能过大,为降低计算复杂度,使用两种近似方法

7.2.1 负采样

加入噪声词,计算开销与噪声词数量线性相关

7.2.2 层序softmax

使用二叉树,计算开销与logVlogV相关

7.3 编码器-解码器(seq2seq)

7.3.1 编码器

利用输入得到各个时间步的隐藏状态ht=f(xt,ht1)h_t=f(x_t,h_{t-1}),然后将各个时间步隐藏状态整合成背景变量c=q(h1,...,hT)c=q(h_1,...,h_T)

7.3.2 解码器

tt'时间步概率分布基于背景向量cc,之前的输出和状态变量st=g(yt1,c,st1)s_{t'}=g(y_{t'-1},c,s_{t'-1})来计算

7.4 束搜索

7.4.1 贪婪搜索

在时间步tt',选择条件概率最大的那个词作为该时间步的输出,但得到的序列不一定是最优的

7.4.2 穷举搜索

列举所有可能的序列,找到概率最大的序列,但是时间复杂度太大了

7.4.3 束搜索

设一个束宽超参数kk,时间步1选取kk个最大概率作为输出,然后以这kk个词作为候选首词,生成kVk|V|个子序列,在子序列中再选kk个概率最大的作为候选序列,然后依次生成。最后,选出每个时间步的候选序列组成集合,按照一下公式计算分数选出分数最高的序列

1LαlogP(y1,...,yL)\frac{1}{L^{\alpha}}logP(y_1,...,y_L)

LL为输出序列的长度,α\alpha一般选0.75

7.5 注意力机制

不同于之前的编码器-解码器,这里的解码器每个时间步的背景状态是不一样的,隐藏状态计算公式也发生了改变

st=g(yt1,ct,st1)s_{t'}=g(y_{t'-1},c_{t'},s_{t'-1})

主要是在计算背景变量时,通过对编码器各时间步的隐藏状态分配不同的注意力来实现的

7.5.1 背景变量计算

要计算解码器tt'的背景变量,要通过解码器t1t'-1的隐藏变量和编码器各时间步的隐藏变量做函数aa(长度相同一般为内积)的运算,然后得到的值做softmax运算得到权重,再分别对编码器的隐藏变量加权平均

矢量计算

softmax(QKT)Vsoftmax(QK^T)V

深刻理解!

7.5.2 发展

重磅Transformer的出现,抛弃了循环神经网络和卷积神经网络的架构,计算效率更高,能力更强


Deep Learning and Pytorch
https://markouv.github.io/2023/08/11/NLP/Pytorch /
作者
Kov
发布于
2023年8月11日
许可协议