扩展内容

结构化预测

中文分词 Chinese Word Segmentation

Parsing与Recursive Neural Networks

图神经网络

Data to Text 文本生成

知识图谱相关问题

XLNet

Transformer-XL: Attentive Language Models Beyond a Fixed-Length Context

https://arxiv.org/pdf/1901.02860.pdf

相较于传统transformer decoder,引入两个新模块

  • segment-level recurrence mechanism

img

  • a novel positional encoding scheme

  • 考虑我们在attention机制中如何使用positional encoding

(E_{x_i}^T+U_i^T)W_q^TW_kE_{x_j}U_j

img

  • R他们采用的是transformer当中的positional encoding

  • u和v是需要训练的模型参数

最终Transformer XL模型

img

代码

https://github.com/kimiyoung/transformer-xl

XLNet: Generalized Autoregressive Pretraining for Language Understanding

https://arxiv.org/pdf/1906.08237.pdf

背景知识

  • 自回归语言模型(Autoregressive Language Model):采用从左往右或从右往左的语言模型,根据上文预测下文。

  • 缺点:只利用了预测单词左边或右边的信息,无法同时利用两边的信息。ELMo在一定程度上解决了这个问题。

  • img

  • 自编码模型(Denoising Auto Encoder, DAE):在输入中随机mask一些单词,利用上下文来预测被mask掉的单词。BERT采用了这一思路。

  • img

两个模型的问题

img

XLNet的目标是融合以上两种模型的优点,解决它们各自存在的问题。

XLNet模型:Permutation Language Modeling

img

Two-Stream Self-Attention

img

img

参考资料

https://zhuanlan.zhihu.com/p/70257427

代码

https://github.com/zihangdai/xlnet

PyTorch

什么是PyTorch?

PyTorch是一个基于Python的科学计算库,它有以下特点:

  • 类似于NumPy,但是它可以使用GPU
  • 可以用它定义深度学习模型,可以灵活地进行深度学习模型的训练和使用

Tensors

Tensor类似与NumPy的ndarray,唯一的区别是Tensor可以在GPU上加速运算。

In [2]:

1
import torch

构造一个未初始化的5x3矩阵:

In [4]:

1
2
x = torch.empty(5,3)
x

Out[4]:

1
2
3
4
5
tensor([[ 0.0000e+00, -8.5899e+09,  0.0000e+00],
[-8.5899e+09, nan, 0.0000e+00],
[ 2.7002e-06, 1.8119e+02, 1.2141e+01],
[ 7.8503e+02, 6.7504e-07, 6.5200e-10],
[ 2.9537e-06, 1.7186e-04, nan]])

构建一个随机初始化的矩阵:

In [5]:

1
2
x = torch.rand(5,3)
x

Out[5]:

1
2
3
4
5
tensor([[0.4628, 0.7432, 0.9785],
[0.2068, 0.4441, 0.9176],
[0.1027, 0.5275, 0.3884],
[0.9380, 0.2113, 0.2839],
[0.0094, 0.4001, 0.6483]])

构建一个全部为0,类型为long的矩阵:

In [8]:

1
2
x = torch.zeros(5,3,dtype=torch.long)
x

Out[8]:

1
2
3
4
5
tensor([[0, 0, 0],
[0, 0, 0],
[0, 0, 0],
[0, 0, 0],
[0, 0, 0]])

In [11]:

1
2
x = torch.zeros(5,3).long()
x.dtype

Out[11]:

1
torch.int64

从数据直接直接构建tensor:

In [12]:

1
2
x = torch.tensor([5.5,3])
x

Out[12]:

1
tensor([5.5000, 3.0000])

也可以从一个已有的tensor构建一个tensor。这些方法会重用原来tensor的特征,例如,数据类型,除非提供新的数据。

In [16]:

1
2
x = x.new_ones(5,3, dtype=torch.double)
x

Out[16]:

1
2
3
4
5
tensor([[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.]], dtype=torch.float64)

In [17]:

1
2
x = torch.randn_like(x, dtype=torch.float)
x

Out[17]:

1
2
3
4
5
tensor([[ 0.2411, -0.3961, -0.9206],
[-0.0508, 0.2653, 0.4685],
[ 0.5368, -0.3606, -0.0073],
[ 0.3383, 0.6826, 1.7368],
[-0.0811, -0.6957, -0.4566]])

得到tensor的形状:

In [20]:

1
x.shape

Out[20]:

1
torch.Size([5, 3])

注意

torch.Size 返回的是一个tuple

Operations

有很多种tensor运算。我们先介绍加法运算。

In [21]:

1
2
y = torch.rand(5,3)
y

Out[21]:

1
2
3
4
5
tensor([[0.9456, 0.3996, 0.1981],
[0.8728, 0.7097, 0.3721],
[0.7489, 0.9502, 0.6241],
[0.5176, 0.0200, 0.5130],
[0.3552, 0.2710, 0.7392]])

In [23]:

1
x + y

Out[23]:

1
2
3
4
5
tensor([[ 1.1866,  0.0035, -0.7225],
[ 0.8220, 0.9750, 0.8406],
[ 1.2857, 0.5896, 0.6168],
[ 0.8559, 0.7026, 2.2498],
[ 0.2741, -0.4248, 0.2826]])

另一种着加法的写法

In [24]:

1
torch.add(x, y)

Out[24]:

1
2
3
4
5
tensor([[ 1.1866,  0.0035, -0.7225],
[ 0.8220, 0.9750, 0.8406],
[ 1.2857, 0.5896, 0.6168],
[ 0.8559, 0.7026, 2.2498],
[ 0.2741, -0.4248, 0.2826]])

加法:把输出作为一个变量

In [26]:

1
2
3
4
result = torch.empty(5,3)
torch.add(x, y, out=result)
# result = x + y
result

Out[26]:

1
2
3
4
5
tensor([[ 1.1866,  0.0035, -0.7225],
[ 0.8220, 0.9750, 0.8406],
[ 1.2857, 0.5896, 0.6168],
[ 0.8559, 0.7026, 2.2498],
[ 0.2741, -0.4248, 0.2826]])

in-place加法

In [28]:

1
2
y.add_(x)
y

Out[28]:

1
2
3
4
5
tensor([[ 1.1866,  0.0035, -0.7225],
[ 0.8220, 0.9750, 0.8406],
[ 1.2857, 0.5896, 0.6168],
[ 0.8559, 0.7026, 2.2498],
[ 0.2741, -0.4248, 0.2826]])

注意

任何in-place的运算都会以_结尾。 举例来说:x.copy_(y), x.t_(), 会改变 x

各种类似NumPy的indexing都可以在PyTorch tensor上面使用。

In [31]:

1
x[1:, 1:]

Out[31]:

1
2
3
4
tensor([[ 0.2653,  0.4685],
[-0.3606, -0.0073],
[ 0.6826, 1.7368],
[-0.6957, -0.4566]])

Resizing: 如果你希望resize/reshape一个tensor,可以使用torch.view

In [39]:

1
2
3
4
x = torch.randn(4,4)
y = x.view(16)
z = x.view(-1,8)
z

Out[39]:

1
2
tensor([[-0.5683,  1.3885, -2.0829, -0.7613, -1.9115,  0.3732, -0.2055, -1.2300],
[-0.2612, -0.4682, -1.0596, 0.7447, 0.7603, -0.4281, 0.5495, 0.1025]])

如果你有一个只有一个元素的tensor,使用.item()方法可以把里面的value变成Python数值。

In [40]:

1
2
x = torch.randn(1)
x

Out[40]:

1
tensor([-1.1493])

In [44]:

1
x.item()

Out[44]:

1
-1.1493233442306519

In [48]:

1
z.transpose(1,0)

Out[48]:

1
2
3
4
5
6
7
8
tensor([[-0.5683, -0.2612],
[ 1.3885, -0.4682],
[-2.0829, -1.0596],
[-0.7613, 0.7447],
[-1.9115, 0.7603],
[ 0.3732, -0.4281],
[-0.2055, 0.5495],
[-1.2300, 0.1025]])

更多阅读

各种Tensor operations, 包括transposing, indexing, slicing, mathematical operations, linear algebra, random numbers在<https://pytorch.org/docs/torch>.

Numpy和Tensor之间的转化

在Torch Tensor和NumPy array之间相互转化非常容易。

Torch Tensor和NumPy array会共享内存,所以改变其中一项也会改变另一项。

把Torch Tensor转变成NumPy Array

In [49]:

1
2
a = torch.ones(5)
a

Out[49]:

1
tensor([1., 1., 1., 1., 1.])

In [50]:

1
2
b = a.numpy()
b

Out[50]:

1
array([1., 1., 1., 1., 1.], dtype=float32)

改变numpy array里面的值。

In [51]:

1
2
b[1] = 2
b

Out[51]:

1
array([1., 2., 1., 1., 1.], dtype=float32)

In [52]:

1
a

Out[52]:

1
tensor([1., 2., 1., 1., 1.])

把NumPy ndarray转成Torch Tensor

In [54]:

1
import numpy as np

In [55]:

1
2
3
4
a = np.ones(5)
b = torch.from_numpy(a)
np.add(a, 1, out=a)
print(a)
1
[2. 2. 2. 2. 2.]

In [56]:

1
b

Out[56]:

1
tensor([2., 2., 2., 2., 2.], dtype=torch.float64)

所有CPU上的Tensor都支持转成numpy或者从numpy转成Tensor。

CUDA Tensors

使用.to方法,Tensor可以被移动到别的device上。

In [60]:

1
2
3
4
5
6
7
if torch.cuda.is_available():
device = torch.device("cuda")
y = torch.ones_like(x, device=device)
x = x.to(device)
z = x + y
print(z)
print(z.to("cpu", torch.double))

Out[60]:

1
False

In [ ]:

1
2
y.to("cpu").data.numpy()
y.cpu().data.numpy()

In [ ]:

1
model = model.cuda()

热身: 用numpy实现两层神经网络

一个全连接ReLU神经网络,一个隐藏层,没有bias。用来从x预测y,使用L2 Loss。

  • ℎ=𝑊1𝑋h=W1X
  • 𝑎=𝑚𝑎𝑥(0,ℎ)a=max(0,h)
  • 𝑦ℎ𝑎𝑡=𝑊2𝑎yhat=W2a

这一实现完全使用numpy来计算前向神经网络,loss,和反向传播。

  • forward pass
  • loss
  • backward pass

numpy ndarray是一个普通的n维array。它不知道任何关于深度学习或者梯度(gradient)的知识,也不知道计算图(computation graph),只是一种用来计算数学运算的数据结构。

In [ ]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
N, D_in, H, D_out = 64, 1000, 100, 10

# 随机创建一些训练数据
x = np.random.randn(N, D_in)
y = np.random.randn(N, D_out)

w1 = np.random.randn(D_in, H)
w2 = np.random.randn(H, D_out)

learning_rate = 1e-6
for it in range(500):
# Forward pass
h = x.dot(w1) # N * H
h_relu = np.maximum(h, 0) # N * H
y_pred = h_relu.dot(w2) # N * D_out

# compute loss
loss = np.square(y_pred - y).sum()
print(it, loss)

# Backward pass
# compute the gradient
grad_y_pred = 2.0 * (y_pred - y)
grad_w2 = h_relu.T.dot(grad_y_pred)
grad_h_relu = grad_y_pred.dot(w2.T)
grad_h = grad_h_relu.copy()
grad_h[h<0] = 0
grad_w1 = x.T.dot(grad_h)

# update weights of w1 and w2
w1 -= learning_rate * grad_w1
w2 -= learning_rate * grad_w2

PyTorch: Tensors

这次我们使用PyTorch tensors来创建前向神经网络,计算损失,以及反向传播。

一个PyTorch Tensor很像一个numpy的ndarray。但是它和numpy ndarray最大的区别是,PyTorch Tensor可以在CPU或者GPU上运算。如果想要在GPU上运算,就需要把Tensor换成cuda类型。

In [ ]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
N, D_in, H, D_out = 64, 1000, 100, 10

# 随机创建一些训练数据
x = torch.randn(N, D_in)
y = torch.randn(N, D_out)

w1 = torch.randn(D_in, H)
w2 = torch.randn(H, D_out)

learning_rate = 1e-6
for it in range(500):
# Forward pass
h = x.mm(w1) # N * H
h_relu = h.clamp(min=0) # N * H
y_pred = h_relu.mm(w2) # N * D_out

# compute loss
loss = (y_pred - y).pow(2).sum().item()
print(it, loss)

# Backward pass
# compute the gradient
grad_y_pred = 2.0 * (y_pred - y)
grad_w2 = h_relu.t().mm(grad_y_pred)
grad_h_relu = grad_y_pred.mm(w2.t())
grad_h = grad_h_relu.clone()
grad_h[h<0] = 0
grad_w1 = x.t().mm(grad_h)

# update weights of w1 and w2
w1 -= learning_rate * grad_w1
w2 -= learning_rate * grad_w2

简单的autograd

In [72]:

1
2
3
4
5
6
7
8
9
10
11
12
x = torch.tensor(1., requires_grad=True)
w = torch.tensor(2., requires_grad=True)
b = torch.tensor(3., requires_grad=True)

y = w*x + b # y = 2*1+3

y.backward()

# dy / dw = x
print(w.grad)
print(x.grad)
print(b.grad)
1
2
3
tensor(1.)
tensor(2.)
tensor(1.)

PyTorch: Tensor和autograd

PyTorch的一个重要功能就是autograd,也就是说只要定义了forward pass(前向神经网络),计算了loss之后,PyTorch可以自动求导计算模型所有参数的梯度。

一个PyTorch的Tensor表示计算图中的一个节点。如果x是一个Tensor并且x.requires_grad=True那么x.grad是另一个储存着x当前梯度(相对于一个scalar,常常是loss)的向量。

In [ ]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
N, D_in, H, D_out = 64, 1000, 100, 10

# 随机创建一些训练数据
x = torch.randn(N, D_in)
y = torch.randn(N, D_out)

w1 = torch.randn(D_in, H, requires_grad=True)
w2 = torch.randn(H, D_out, requires_grad=True)

learning_rate = 1e-6
for it in range(500):
# Forward pass
y_pred = x.mm(w1).clamp(min=0).mm(w2)

# compute loss
loss = (y_pred - y).pow(2).sum() # computation graph
print(it, loss.item())

# Backward pass
loss.backward()

# update weights of w1 and w2
with torch.no_grad():
w1 -= learning_rate * w1.grad
w2 -= learning_rate * w2.grad
w1.grad.zero_()
w2.grad.zero_()

PyTorch: nn

这次我们使用PyTorch中nn这个库来构建网络。 用PyTorch autograd来构建计算图和计算gradients, 然后PyTorch会帮我们自动计算gradient。

In [ ]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import torch.nn as nn

N, D_in, H, D_out = 64, 1000, 100, 10

# 随机创建一些训练数据
x = torch.randn(N, D_in)
y = torch.randn(N, D_out)

model = torch.nn.Sequential(
torch.nn.Linear(D_in, H, bias=False), # w_1 * x + b_1
torch.nn.ReLU(),
torch.nn.Linear(H, D_out, bias=False),
)

torch.nn.init.normal_(model[0].weight)
torch.nn.init.normal_(model[2].weight)

# model = model.cuda()

loss_fn = nn.MSELoss(reduction='sum')

learning_rate = 1e-6
for it in range(500):
# Forward pass
y_pred = model(x) # model.forward()

# compute loss
loss = loss_fn(y_pred, y) # computation graph
print(it, loss.item())

# Backward pass
loss.backward()

# update weights of w1 and w2
with torch.no_grad():
for param in model.parameters(): # param (tensor, grad)
param -= learning_rate * param.grad

model.zero_grad()

In [113]:

1
model[0].weight

Out[113]:

1
2
3
4
5
6
7
8
9
Parameter containing:
tensor([[-0.0218, 0.0212, 0.0243, ..., 0.0230, 0.0247, 0.0168],
[-0.0144, 0.0177, -0.0221, ..., 0.0161, 0.0098, -0.0172],
[ 0.0086, -0.0122, -0.0298, ..., -0.0236, -0.0187, 0.0295],
...,
[ 0.0266, -0.0008, -0.0141, ..., 0.0018, 0.0319, -0.0129],
[ 0.0296, -0.0005, 0.0115, ..., 0.0141, -0.0088, -0.0106],
[ 0.0289, -0.0077, 0.0239, ..., -0.0166, -0.0156, -0.0235]],
requires_grad=True)

PyTorch: optim

这一次我们不再手动更新模型的weights,而是使用optim这个包来帮助我们更新参数。 optim这个package提供了各种不同的模型优化方法,包括SGD+momentum, RMSProp, Adam等等。

In [ ]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import torch.nn as nn

N, D_in, H, D_out = 64, 1000, 100, 10

# 随机创建一些训练数据
x = torch.randn(N, D_in)
y = torch.randn(N, D_out)

model = torch.nn.Sequential(
torch.nn.Linear(D_in, H, bias=False), # w_1 * x + b_1
torch.nn.ReLU(),
torch.nn.Linear(H, D_out, bias=False),
)

torch.nn.init.normal_(model[0].weight)
torch.nn.init.normal_(model[2].weight)

# model = model.cuda()

loss_fn = nn.MSELoss(reduction='sum')
# learning_rate = 1e-4
# optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)

learning_rate = 1e-6
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)

for it in range(500):
# Forward pass
y_pred = model(x) # model.forward()

# compute loss
loss = loss_fn(y_pred, y) # computation graph
print(it, loss.item())

optimizer.zero_grad()
# Backward pass
loss.backward()

# update model parameters
optimizer.step()

PyTorch: 自定义 nn Modules

我们可以定义一个模型,这个模型继承自nn.Module类。如果需要定义一个比Sequential模型更加复杂的模型,就需要定义nn.Module模型。

In [ ]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import torch.nn as nn

N, D_in, H, D_out = 64, 1000, 100, 10

# 随机创建一些训练数据
x = torch.randn(N, D_in)
y = torch.randn(N, D_out)

class TwoLayerNet(torch.nn.Module):
def __init__(self, D_in, H, D_out):
super(TwoLayerNet, self).__init__()
# define the model architecture
self.linear1 = torch.nn.Linear(D_in, H, bias=False)
self.linear2 = torch.nn.Linear(H, D_out, bias=False)

def forward(self, x):
y_pred = self.linear2(self.linear1(x).clamp(min=0))
return y_pred

model = TwoLayerNet(D_in, H, D_out)
loss_fn = nn.MSELoss(reduction='sum')
learning_rate = 1e-4
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)

for it in range(500):
# Forward pass
y_pred = model(x) # model.forward()

# compute loss
loss = loss_fn(y_pred, y) # computation graph
print(it, loss.item())

optimizer.zero_grad()
# Backward pass
loss.backward()

# update model parameters
optimizer.step()

In [ ]:

1
 

朴素贝叶斯

朴素贝叶斯

1. 引言

贝叶斯方法是一个历史悠久,有着坚实的理论基础的方法,同时处理很多问题时直接而又高效,很多高级自然语言处理模型也可以从它演化而来。因此,学习贝叶斯方法,是研究自然语言处理问题的一个非常好的切入口。

2. 贝叶斯公式

贝叶斯公式就一行:

$$
P(Y|X)=P(X|Y)P(Y)/P(X)
$$

而它其实是由以下的联合概率公式推导出来:
$$
P(Y,X)=P(Y|X)P(X)=P(X|Y)P(Y)
$$
其中P(Y)叫做先验概率,P(Y|X)叫做后验概率,P(Y,X)叫做联合概率。

没了,贝叶斯最核心的公式就这么些。

3. 用机器学习的视角理解贝叶斯公式

在机器学习的视角下,我们把X理解成“具有某特征”,把Y理解成“类别标签”(一般机器学习为题中都是X=>特征, Y=>结果对吧)。在最简单的二分类问题(判定)下,我们将Y理解成“属于某类”的标签。于是贝叶斯公式就变形成了下面的样子:

P(“属于某类”|“具有某特征”)=P(“具有某特征”|“属于某类”)P(“属于某类”)P(“具有某特征”)

我们简化解释一下上述公式:

P(“属于某类”|“具有某特征”)=在已知某样本“具有某特征”的条件下,该样本“属于某类”的概率。所以叫做『后验概率』
P(“具有某特征”|“属于某类”)=在已知某样本“属于某类”的条件下,该样本“具有某特征”的概率。
P(“属于某类”)=(在未知某样本具有该“具有某特征”的条件下,)该样本“属于某类”的概率。所以叫做『先验概率』
P(“具有某特征”)=(在未知某样本“属于某类”的条件下,)该样本“具有某特征”的概率。

而我们二分类问题的最终目的就是要判断P(“属于某类”|“具有某特征”)是否大于1/2就够了。贝叶斯方法把计算“具有某特征的条件下属于某类”的概率转换成需要计算“属于某类的条件下具有某特征”的概率,而后者获取方法就简单多了,我们只需要找到一些包含已知特征标签的样本,即可进行训练。而样本的类别标签都是明确的,所以贝叶斯方法在机器学习里属于有监督学习方法。

这里再补充一下,一般『先验概率』、『后验概率』是相对出现的,比如P(Y)与P(Y|X)是关于Y的先验概率与后验概率,P(X)与P(X|Y)是关于X的先验概率与后验概率。

4. 垃圾邮件识别

举个例子好啦,我们现在要对邮件进行分类,识别垃圾邮件和普通邮件,如果我们选择使用朴素贝叶斯分类器,那目标就是判断P(“垃圾邮件”|“具有某特征”)是否大于1/2。现在假设我们有垃圾邮件和正常邮件各1万封作为训练集。需要判断以下这个邮件是否属于垃圾邮件:

“我司可办理正规发票(保真)17%增值税发票点数优惠!”

也就是判断概率P(“垃圾邮件”|“我司可办理正规发票(保真)17%增值税发票点数优惠!”)是否大于1/2

咳咳,有木有发现,转换成的这个概率,计算的方法:就是写个计数器,然后+1 +1 +1统计出所有垃圾邮件和正常邮件中出现这句话的次数啊!!!好,具体点说:

P(“垃圾邮件”|“我司可办理正规发票(保真)17%增值税发票点数优惠!”) =垃圾邮件中出现这句话的次数垃圾邮件中出现这句话的次数+正常邮件中出现这句话的次数

5. 分词

一个很悲哀但是很现实的结论: 训练集是有限的,而句子的可能性则是无限的。所以覆盖所有句子可能性的训练集是不存在的。

所以解决方法是? 句子的可能性无限,但是词语就那么些!!汉语常用字2500个,常用词语也就56000个(你终于明白小学语文老师的用心良苦了)。按人们的经验理解,两句话意思相近并不强求非得每个字、词语都一样。比如“我司可办理正规发票,17%增值税发票点数优惠!”,这句话就比之前那句话少了“(保真)”这个词,但是意思基本一样。如果把这些情况也考虑进来,那样本数量就会增加,这就方便我们计算了。

于是,我们可以不拿句子作为特征,而是拿句子里面的词语(组合)作为特征去考虑。比如“正规发票”可以作为一个单独的词语,“增值税”也可以作为一个单独的词语等等。

句子“我司可办理正规发票,17%增值税发票点数优惠!”就可以变成(“我”,“司”,“可”,“办理”,“正规发票”,“保真”,“增值税”,“发票”,“点数”,“优惠”))

于是你接触到了中文NLP中,最最最重要的技术之一:分词!!!也就是把一整句话拆分成更细粒度的词语来进行表示。另外,分词之后去除标点符号、数字甚至无关成分(停用词)是特征预处理中的一项技术

中文分词是一个专门的技术领域(我不会告诉你某搜索引擎厂码砖工有专门做分词的!!!),上过之前课程的同学都知道python有一个非常方便的分词工具jieba,假定我们已经完成分词工作:

我们观察(“我”,“司”,“可”,“办理”,“正规发票”,“保真”,“增值税”,“发票”,“点数”,“优惠”),这可以理解成一个向量:向量的每一维度都表示着该特征词在文本中的特定位置存在。这种将特征拆分成更小的单元,依据这些更灵活、更细粒度的特征进行判断的思维方式,在自然语言处理与机器学习中都是非常常见又有效的。

因此贝叶斯公式就变成了:

P(“垃圾邮件”|(“我”,“司”,“可”,“办理”,“正规发票”,“保真”,“增值税”,“发票”,“点数”,“优惠”)) =P((“我”,“司”,“可”,“办理”,“正规发票”,“保真”,“增值税”,“发票”,“点数”,“优惠”)|”垃圾邮件”)P(“垃圾邮件”)P((“我”,“司”,“可”,“办理”,“正规发票”,“保真”,“增值税”,“发票”,“点数”,“优惠”))

P(“正常邮件”|(“我”,“司”,“可”,“办理”,“正规发票”,“保真”,“增值税”,“发票”,“点数”,“优惠”)) =P((“我”,“司”,“可”,“办理”,“正规发票”,“保真”,“增值税”,“发票”,“点数”,“优惠”)|”正常邮件”)P(“正常邮件”)P((“我”,“司”,“可”,“办理”,“正规发票”,“保真”,“增值税”,“发票”,“点数”,“优惠”))

6. 条件独立假设

下面我们马上会看到一个非常简单粗暴的假设。

概率P((“我”,“司”,“可”,“办理”,“正规发票”,“保真”,“增值税”,“发票”,“点数”,“优惠”)|”垃圾邮件”)依旧不够好求,我们引进一个很朴素的近似。为了让公式显得更加紧凑,我们令字母S表示“垃圾邮件”,令字母H表示“正常邮件”。近似公式如下:

P((“我”,“司”,“可”,“办理”,“正规发票”,“保真”,“增值税”,“发票”,“点数”,“优惠”)|S)
=P(“我”|S)×P(“司”|S)×P(“可”|S)×P(“办理”|S)×P(“正规发票”|S) ×P(“保真”|S)×P(“增值税”|S)×P(“发票”|S)×P(“点数”|S)×P(“优惠”|S)

这就是传说中的条件独立假设。基于“正常邮件”的条件独立假设的式子与上式类似,此处省去。接着,将条件独立假设代入上面两个相反事件的贝叶斯公式。

于是我们就只需要比较以下两个式子的大小:

C=P(“我”|S)P(“司”|S)P(“可”|S)P(“办理”|S)P(“正规发票”|S) ×P(“保真”|S)P(“增值税”|S)P(“发票”|S)P(“点数”|S)P(“优惠”|S)P(“垃圾邮件”) C⎯⎯⎯⎯=P(“我”|H)P(“司”|H)P(“可”|H)P(“办理”|H)P(“正规发票”|H) ×P(“保真”|H)P(“增值税”|H)P(“发票”|H)P(“点数”|H)P(“优惠”|H)P(“正常邮件”)

厉(wo)害(cao)!酱紫处理后式子中的每一项都特别好求!只需要分别统计各类邮件中该关键词出现的概率就可以了!!!比如:

P(“发票”|S)=垃圾邮件中所有“发票”的次数垃圾邮件中所有词语的次数

统计次数非常方便,而且样本数量足够大,算出来的概率比较接近真实。于是垃圾邮件识别的问题就可解了。

7. 朴素贝叶斯(Naive Bayes),“Naive”在何处?

加上条件独立假设的贝叶斯方法就是朴素贝叶斯方法(Naive Bayes)。 Naive的发音是“乃一污”,意思是“朴素的”、“幼稚的”、“蠢蠢的”。咳咳,也就是说,大神们取名说该方法是一种比较萌蠢的方法,为啥?

将句子(“我”,“司”,“可”,“办理”,“正规发票”) 中的 (“我”,“司”)与(“正规发票”)调换一下顺序,就变成了一个新的句子(“正规发票”,“可”,“办理”, “我”, “司”)。新句子与旧句子的意思完全不同。但由于乘法交换律,朴素贝叶斯方法中算出来二者的条件概率完全一样!计算过程如下:

P((“我”,“司”,“可”,“办理”,“正规发票”)|S) =P(“我”|S)P(“司”|S)P(“可”|S)P(“办理”|S)P(“正规发票”|S) =P(“正规发票”|S)P(“可”|S)P(“办理”|S)P(“我”|S)P(“司”|S) =P((“正规发票”,“可”,“办理”,“我”,“司”)|S)

也就是说,在朴素贝叶斯眼里,“我司可办理正规发票”与“正规发票可办理我司”完全相同。朴素贝叶斯失去了词语之间的顺序信息。这就相当于把所有的词汇扔进到一个袋子里随便搅和,贝叶斯都认为它们一样。因此这种情况也称作词袋子模型(bag of words)

词袋子配图

词袋子模型与人们的日常经验完全不同。比如,在条件独立假设的情况下,“武松打死了老虎”与“老虎打死了武松”被它认作一个意思了。恩,朴素贝叶斯就是这么单纯和直接,对比于其他分类器,好像是显得有那么点萌蠢。

8. 简单高效,吊丝逆袭

虽然说朴素贝叶斯方法萌蠢萌蠢的,但实践证明在垃圾邮件识别的应用还令人诧异地好。Paul Graham先生自己简单做了一个朴素贝叶斯分类器,“1000封垃圾邮件能够被过滤掉995封,并且没有一个误判”。(Paul Graham《黑客与画家》)

那个…效果为啥好呢?

“有人对此提出了一个理论解释,并且建立了什么时候朴素贝叶斯的效果能够等价于非朴素贝叶斯的充要条件,这个解释的核心就是:有些独立假设在各个分类之间的分布都是均匀的所以对于似然的相对大小不产生影响;即便不是如此,也有很大的可能性各个独立假设所产生的消极影响或积极影响互相抵消,最终导致结果受到的影响不大。具体的数学公式请参考这篇 paper。”(刘未鹏《:平凡而又神奇的贝叶斯方法》)

恩,这个分类器中最简单直接看似萌蠢的小盆友『朴素贝叶斯』,实际上却是简单、实用、且强大的。

9. 处理重复词语的三种方式

我们之前的垃圾邮件向量(“我”,“司”,“可”,“办理”,“正规发票”,“保真”,“增值税”,“发票”,“点数”,“优惠”),其中每个词都不重复。而这在现实中其实很少见。因为如果文本长度增加,或者分词方法改变,必然会有许多词重复出现,因此需要对这种情况进行进一步探讨。比如以下这段邮件:

“代开发票。增值税发票,正规发票。” 分词后为向量: (“代开”,“发票”,“增值税”,“发票”,“正规”,“发票”)

其中“发票”重复了三次。

9.1 多项式模型:

如果我们考虑重复词语的情况,也就是说,重复的词语我们视为其出现多次,直接按条件独立假设的方式推导,则有

P((“代开”,“发票”,“增值税”,“发票”,“正规”,“发票”)|S) =P(“代开””|S)P(“发票”|S)P(“增值税”|S)P(“发票”|S)P(“正规”|S)P(“发票”|S)=P(“代开””|S)P3(“发票”|S)P(“增值税”|S)P(“正规”|S) 注意这一项:P3(“发票”|S)。

在统计计算P(“发票”|S)时,每个被统计的垃圾邮件样本中重复的词语也统计多次。

P(“发票”|S)=每封垃圾邮件中出现“发票”的次数的总和每封垃圾邮件中所有词出现次数(计算重复次数)的总和

你看这个多次出现的结果,出现在概率的指数/次方上,因此这样的模型叫作多项式模型

9.2 伯努利模型

另一种更加简化的方法是将重复的词语都视为其只出现1次

P((“代开”,“发票”,“增值税”,“发票”,“正规”,“发票”)|S) =P(“发票”|S)P(“代开””|S)P(“增值税”|S)P(“正规”|S)

统计计算P(“词语”|S)时也是如此。

P(“发票”|S)=出现“发票”的垃圾邮件的封数每封垃圾邮件中所有词出现次数(出现了只计算一次)的总和

这样的模型叫作伯努利模型(又称为二项独立模型)。这种方式更加简化与方便。当然它丢失了词频的信息,因此效果可能会差一些。

9.3 混合模型

第三种方式是在计算句子概率时,不考虑重复词语出现的次数,但是在统计计算词语的概率P(“词语”|S)时,却考虑重复词语的出现次数,这样的模型可以叫作混合模型

我们通过下图展示三种模型的关系。

三种形态

具体实践中采用那种模型,关键看具体的业务场景,一个简单经验是,对于垃圾邮件识别,混合模型更好些

10. 去除停用词与选择关键词

我们继续观察(“我”,“司”,“可”,“办理”,“正规发票”,“保真”,“增值税”,“发票”,“点数”,“优惠”) 这句话。其实,像“我”、“可”之类词其实非常中性,无论其是否出现在垃圾邮件中都无法帮助判断的有用信息。所以可以直接不考虑这些典型的词。这些无助于我们分类的词语叫作“停用词”(Stop Words)。这样可以减少我们训练模型、判断分类的时间。 于是之前的句子就变成了(“司”,“办理”,“正规发票”,“保真”,“增值税”,“发票”,“点数”,“优惠”)

我们进一步分析。以人类的经验,其实“正规发票”、“发票”这类的词如果出现的话,邮件作为垃圾邮件的概率非常大,可以作为我们区分垃圾邮件的“关键词”。而像“司”、“办理”、“优惠”这类的词则有点鸡肋,可能有助于分类,但又不那么强烈。如果想省事做个简单的分类器的话,则可以直接采用“关键词”进行统计与判断,剩下的词就可以先不管了。于是之前的垃圾邮件句子就变成了(“正规发票”,“发票”) 。这样就更加减少了我们训练模型、判断分类的时间,速度非常快。

“停用词”和“关键词”一般都可以提前靠人工经验指定。不同的“停用词”和“关键词”训练出来的分类器的效果也会有些差异。

11. 浅谈平滑技术

我们来说个问题(中文NLP里问题超级多,哭瞎T_T),比如在计算以下独立条件假设的概率:

P((“我”,“司”,“可”,“办理”,“正规发票”)|S) =P(“我”|S)P(“司”|S)P(“可”|S)P(“办理”|S)P(“正规发票”|S)

我们扫描一下训练集,发现“正规发票”这个词从出现过!!!*,于是P(“正规发票”|S)=0…问题严重了,整个概率都变成0了!!!朴素贝叶斯方法面对一堆0,很凄惨地失效了…更残酷的是这种情况其实很常见,因为哪怕训练集再大,也可能有覆盖不到的词语。本质上还是样本数量太少,不满足大数定律,计算出来的概率失真**。为了解决这样的问题,一种分析思路就是直接不考虑这样的词语,但这种方法就相当于默认给P(“正规发票”|S)赋值为1。其实效果不太好,大量的统计信息给浪费掉了。我们进一步分析,既然可以默认赋值为1,为什么不能默认赋值为一个很小的数?这就是平滑技术的基本思路,依旧保持着一贯的作风,朴实/土但是直接而有效

对于伯努利模型,P(“正规发票”|S)的一种平滑算法是:

P(“正规发票”|S)=出现“正规发票”的垃圾邮件的封数+1每封垃圾邮件中所有词出现次数(出现了只计算一次)的总和+2

对于多项式模型,P(“正规发票”| S)的一种平滑算法是:

P(“发票”|S)=每封垃圾邮件中出现“发票”的次数的总和+1每封垃圾邮件中所有词出现次数(计算重复次数)的总和+被统计的词表的词语数量

说起来,平滑技术的种类其实非常多,有兴趣的话回头我们专门拉个专题讲讲好了。这里只提一点,就是所有的平滑技术都是给未出现在训练集中的词语一个估计的概率,而相应地调低其他已经出现的词语的概率

平滑技术是因为数据集太小而产生的现实需求。如果数据集足够大,平滑技术对结果的影响将会变小。

12. 内容小结

我们找了个最简单常见的例子:垃圾邮件识别,说明了一下朴素贝叶斯进行文本分类的思路过程。基本思路是先区分好训练集与测试集,对文本集合进行分词、去除标点符号等特征预处理的操作,然后使用条件独立假设,将原概率转换成词概率乘积,再进行后续的处理。

贝叶斯公式 + 条件独立假设 = 朴素贝叶斯方法

基于对重复词语在训练阶段与判断(测试)阶段的三种不同处理方式,我们相应的有伯努利模型、多项式模型和混合模型。在训练阶段,如果样本集合太小导致某些词语并未出现,我们可以采用平滑技术对其概率给一个估计值。而且并不是所有的词语都需要统计,我们可以按相应的“停用词”和“关键词”对模型进行进一步简化,提高训练和判断速度。

13. 为什么不直接匹配关键词来识别垃圾邮件?

有同学可能会问:“何必费这么大劲算那么多词的概率?直接看邮件中有没有‘代开发票’、‘转售发票’之类的关键词不就得了?如果关键词比较多就认为是垃圾邮件呗。”

其实关键词匹配的方法如果有效的话真不必用朴素贝叶斯。毕竟这种方法简单嘛,就是一个字符串匹配。从历史来看,之前没有贝叶斯方法的时候主要也是用关键词匹配。但是这种方法准确率太低。我们在工作项目中也尝试过用关键词匹配的方法去进行文本分类,发现大量误报。感觉就像扔到垃圾箱的邮件99%都是正常的!这样的效果不忍直视。而加一个朴素贝叶斯方法就可能把误报率拉低近一个数量级,体验好得不要不要的。

另一个原因是词语会随着时间不断变化。发垃圾邮件的人也不傻,当他们发现自己的邮件被大量屏蔽之后,也会考虑采用新的方式,如变换文字、词语、句式、颜色等方式来绕过反垃圾邮件系统。比如对于垃圾邮件“我司可办理正规发票,17%增值税发票点数优惠”,他们采用火星文:“涐司岢办理㊣規髮票,17%增値稅髮票嚸數優蕙”,那么字符串匹配的方法又要重新找出这些火星文,一个一个找出关键词,重新写一些匹配规则。更可怕的是,这些规则可能相互之间的耦合关系异常复杂,要把它们梳理清楚又是大一个数量级的工作量。等这些规则失效了又要手动更新新的规则……无穷无尽猫鼠游戏最终会把猫给累死

而朴素贝叶斯方法却显示出无比的优势。因为它是基于统计方法的,只要训练样本中有更新的垃圾邮件的新词语,哪怕它们是火星文,都能自动地把哪些更敏感的词语(如“髮”、“㊣”等)给凸显出来,并根据统计意义上的敏感性给他们分配适当的权重 ,这样就不需要什么人工了,非常省事。你只需要时不时地拿一些最新的样本扔到训练集中,重新训练一次即可

小补充一下,对于火星文、同音字等替代语言,一般的分词技术可能会分得不准,最终可能只把一个一个字给分出来,成为“分字”。效果可能不会太好。也可以用过n-gram之类的语言模型,拿到最常见短语。当然,对于英文等天生自带空格来间隔单词的语言,分词则不是什么问题,使用朴素贝叶斯方法将会更加顺畅。

14.实际工程的tricks

应用朴素贝叶斯方法的过程中,一些tricks能显著帮助工程解决问题。我们毕竟经验有限,无法将它们全都罗列出来,只能就所知的一点点经验与大家分享,欢迎批评指正。

14.1 trick1:取对数

我们提到用来识别垃圾邮件的方法是比较以下两个概率的大小(字母S表示“垃圾邮件”,字母H表示“正常邮件”):

C=P(“我”|S)P(“司”|S)P(“可”|S)P(“办理”|S)P(“正规发票”|S)

×P(“保真”|S)P(“增值税”|S)P(“发票”|S)P(“点数”|S)P(“优惠”|S)P(“垃圾邮件”)

C⎯⎯⎯⎯=P(“我”|H)P(“司”|H)P(“可”|H)P(“办理”|H)P(“正规发票”|H)

×P(“保真”|H)P(“增值税”|H)P(“发票”|H)P(“点数”|H)P(“优惠”|H)P(“正常邮件”)

但这里进行了很多乘法运算,计算的时间开销比较大。尤其是对于篇幅比较长的邮件,几万个数相乘起来还是非常花时间的。如果能把这些乘法变成加法则方便得多。刚好数学中的对数函数log就可以实现这样的功能。两边同时取对数(本文统一取底数为2),则上面的公式变为:

logC=logP(“我”|S)+logP(“司”|S)+logP(“可”|S)+logP(“办理”|S)+logP(“正规发票”|S)

+logP(“保真”|S)+logP(“增值税”|S)+logP(“发票”|S)+logP(“点数”|S)+logP(“优惠”|S)+logP(“垃圾邮件”)

logC⎯⎯⎯⎯=logP(“我”|H)+logP(“司”|H)+logP(“可”|H)+logP(“办理”|H)+logP(“正规发票”|H)

+logP(“保真”|H)+logP(“增值税”|H)+logP(“发票”|H)+logP(“点数”|H)+logP(“优惠”|H)+logP(“正常邮件”)

有同学可能要叫了:“做对数运算岂不会也很花时间?”的确如此,但是可以在训练阶段直接计算 logP ,然后把他们存在一张大的hash表里。在判断的时候直接提取hash表中已经计算好的对数概率,然后相加即可。这样使得判断所需要的计算时间被转移到了训练阶段,实时运行的时候速度就比之前快得多,这可不止几个数量级的提升。

14.2 trick2:转换为权重

对于二分类,我们还可以继续提高判断的速度。既然要比较logC 和logC⎯⎯⎯⎯ 的大小,那就可以直接将上下两式相减,并继续化简:

logCC⎯⎯⎯⎯⎯=logP(“我”|S)P(“我”|H)+logP(“司”|S)P(“司”|H)+logP(“可”|S)P(“可”|H)+logP(“办理”|S)P(“办理”|H)+logP(“正规发票”|S)P(“正规发票”|H)

+logP(“保真”|S)P(“保真”|H)+logP(“增值税”|S)P(“增值税”|H)+logP(“发票”|S)P(“发票”|H)+logP(“点数”|S)P(“点数”|H)+logP(“优惠”|S)P(“优惠”|H)+logP(“正常邮件”|S)P(“正常邮件”)

logCC⎯⎯⎯⎯⎯ 如果大于0则属于垃圾邮件。我们可以把其中每一项作为其对应词语的权重,比如logP(“发票”|S)P(“发票”|H) 就可以作为词语“发票”的权重,权重越大就越说明“发票”更可能是与“垃圾邮件”相关的特征。这样可以根据权重的大小来评估和筛选显著的特征,比如关键词。而这些权重值可以直接提前计算好而存在hash表中 。判断的时候直接将权重求和即可。

关键词hash表的样子如下,左列是权重,右列是其对应的词语,权重越高的说明越“关键”:

hash

14.3 trick3:选取topk的关键词

前文说过可以通过提前选取关键词来提高判断的速度。有一种方法可以省略提前选取关键词的步骤,就是直接选取一段文本中权重最高的K个词语,将其权重进行加和。比如Paul Graham 在《黑客与画家》中是选取邮件中权重最高的15个词语计算的。

通过权重hash表可知,如果是所有词语的权重,则权重有正有负。如果只选择权重最高的K个词语,则它们的权重基本都是正的。所以就不能像之前那样判断logCC⎯⎯⎯⎯⎯ 是否大于0来区分邮件了。而这需要依靠经验选定一个正数的阈值(门槛值) ,依据logCC⎯⎯⎯⎯⎯ 与该门槛值的大小来识别垃圾邮件。

如下图所示,蓝色点代表垃圾邮件,绿色点代表正常邮件,横坐标为计算出来的logCC⎯⎯⎯⎯⎯ 值,中间的红线代表阈值。

权重

14.4 trick4:分割样本

选取topk个词语的方法对于篇幅变动不大的邮件样本比较有效。但是对篇幅过大或者过小的邮件则会有判断误差。

比如这个垃圾邮件的例子:(“我”,“司”,“可”,“办理”,“正规发票”,“保真”,“增值税”,“发票”,“点数”,“优惠”)。分词出了10个词语,其中有“正规发票”、“发票”2个关键词。关键词的密度还是蛮大的,应该算是敏感邮件。但因为采用最高15个词语的权重求和,并且相应的阈值是基于15个词的情况有效,可能算出来的结果还小于之前的阈值,这就造成漏判了。

类似的,如果一封税务主题的邮件有1000个词语,其中只有“正规发票”、“发票”、“避税方法”3个权重比较大的词语,它们只是在正文表述中顺带提到的内容。关键词的密度被较长的篇幅稀释了,应该算是正常邮件。但是却被阈值判断成敏感邮件,造成误判了。

这两种情况都说明topk关键词的方法需要考虑篇幅的影响。这里有许多种处理方式,它们的基本思想都是选取词语的个数及对应的阈值要与篇幅的大小成正比,本文只介绍其中一种方方法:

  • 对于长篇幅邮件,按一定的大小,比如每500字,将其分割成小的文本段落,再对小文本段落采用topk关键词的方法。只要其中有一个小文本段落超过阈值就判断整封邮件是垃圾邮件。
  • 对于超短篇幅邮件,比如50字,可以按篇幅与标准比较篇幅的比例来选取topk,以确定应该匹配关键词语的个数。比如选取 50500×15≈2 个词语进行匹配,相应的阈值可以是之前阈值的 215 。以此来判断则更合理。

14.5 trick5:位置权重

到目前为止,我们对词语权重求和的过程都没有考虑邮件篇章结构的因素。比如“正规发票”如果出现在标题中应该比它出现在正文中对判断整个邮件的影响更大;而出现在段首句中又比其出现在段落正文中对判断整个邮件的影响更大。所以可以根据词语出现的位置,对其权重再乘以一个放大系数,以扩大其对整封邮件的影响,提高识别准确度

比如一封邮件其标题是“正规发票”(假设标题的放大系数为2),段首句是“发票”,“点数”,“优惠”(假设段首的放大系数为1.5),剩下的句子是(“我”,“司”,“可”,“办理”,“保真”)。则计算logCC⎯⎯⎯⎯⎯ 时的公式就可以调整为:

logCC⎯⎯⎯⎯⎯=2×logP(“正规发票”|S)P(“正规发票”|H)+1.5×logP(“发票”|S)P(“发票”|H)+1.5×logP(“点数”|S)P(“点数”|H)+1.5×logP(“优惠”|S)P(“优惠”|H)

+logP(“我”|S)P(“我”|H)+logP(“司”|S)P(“司”|H)+logP(“可”|S)P(“可”|H)+logP(“办理”|S)P(“办理”|H)+logP(“保真”|S)P(“保真”|H)+logP(“正常邮件”|S)P(“正常邮件”)

14.6 trick6:蜜罐

我们通过辛辛苦苦的统计与计算,好不容易得到了不同词语的权重。然而这并不是一劳永逸的。我们我们之前交代过,词语及其权重会随着时间不断变化,需要时不时地用最新的样本来训练以更新词语及其权重

而搜集最新垃圾邮件有一个技巧,就是随便注册一些邮箱,然后将它们公布在各大论坛上。接下来就坐等一个月,到时候收到的邮件就绝大部分都是垃圾邮件了(好奸诈)。再找一些正常的邮件,基本就能够训练了。这些用于自动搜集垃圾邮件的邮箱叫做“蜜罐”。“蜜罐”是网络安全领域常用的手段,因其原理类似诱捕昆虫的装有蜜的罐子而得名。比如杀毒软件公司会利用蜜罐来监视或获得计算机网络中的病毒样本、攻击行为等。

15. 贝叶斯方法的思维方式

讲了这么多tricks,但这些手段都是建立在贝叶斯方法基础之上的。因此有必要探讨一下贝叶斯方法的思维方式,以便更好地应用这种方法解决实际问题。

15.1 逆概问题

我们重新看一眼贝叶斯公式:

P(Y|X)=P(X|Y)P(Y)P(X)

先不考虑先验概率P(Y)与P(X),观察两个后验概率P(Y|X)与P(X|Y),可见贝叶斯公式能够揭示两个相反方向的条件概率之间的转换关系

从贝叶斯公式的发现历史来看,其就是为了处理所谓“逆概”问题而诞生的。比如P(Y|X) 不能通过直接观测来得到结果,而P(X|Y) 却容易通过直接观测得到结果,就可以通过贝叶斯公式从间接地观测对象去推断不可直接观测的对象的情况

好吧,我们说人话。基于邮件的文本内容判断其属于垃圾邮件的概率不好求(不可通过直接观测、统计得到),但是基于已经搜集好的垃圾邮件样本,去统计(直接观测)其文本内部各个词语的概率却非常方便。这就可以用贝叶斯方法。

引申一步,基于样本特征去判断其所属标签的概率不好求,但是基于已经搜集好的打上标签的样本(有监督),却可以直接统计属于同一标签的样本内部各个特征的概率分布。因此贝叶斯方法的理论视角适用于一切分类问题的求解。

15.2 处理多分类问题

前面我们一直在探讨二分类(判断题)问题,现在可以引申到多分类(单选题)问题了。

还是用邮件分类的例子,这是现在不只要判断垃圾邮件,还要将正常邮件细分为私人邮件、工作邮件。现在有这3类邮件各1万封作为样本。需要训练出一个贝叶斯分类器。这里依次用Y1,Y2,Y3表示这三类邮件,用X表示被判断的邮件。套用贝叶斯公式有:

P(Y1|X)=P(X|Y1)P(Y1)P(X)

P(Y2|X)=P(X|Y2)P(Y2)P(X)

P(Y3|X)=P(X|Y3)P(Y3)P(X)

通过比较3个概率值的大小即可得到X所属的分类。发现三个式子的分母P(X) 一样,比较大小时可以忽略不计,于是就可以用下面这一个式子表达上面3式:

P(Yi|X)∝P(X|Yi)P(Yi);i=1,2,3

其中 ∝ 表示“正比于”。而P(X|Yi) 则有个特别高逼格的名字叫做“似然函数”。我们上大学的时候也被这个名字搞得晕晕乎乎的,其实它也是个概率,直接理解成“P(Yi|X) 的逆反条件概率” 就方便了。

这里只是以垃圾邮件3分类问题举了个例子,对于任意多分类的问题都可以用这样的思路去理解。比如新闻分类、情感喜怒哀乐分类等等

15.3 先验概率的问题

在垃圾邮件的例子中,先验概率都相等,P(Y1)=P(Y2)=P(Y3)=10000/30000=1/3,所以上面是式子又可以进一步化简:

P(Yi|X)∝P(X|Yi);i=1,2,3

只需比较右边式子(也就是“似然函数”)的大小就可以了。这种方法就是传说中的最大似然法:不考虑先验概率而直接比较似然函数。

关于选出最佳分类Yi是否要考虑先验概率P(Yi)的问题,曾经在频率学派和贝叶斯学派之间产生了激烈的教派冲突。统计学家(频率学派)说:我们让数据自己说话。言下之意就是要摒弃先验概率。而贝叶斯学派支持者则说:数据会有各种各样的偏差,而一个靠谱的先验概率则可以对这些随机噪音做到健壮。对此有兴趣的同学可以找更多资料进行了解,本文在此不做更多的引申,只基于垃圾邮件识别的例子进行探讨。

比如我们在采集垃圾邮件样本的时候,不小心delete掉了一半的数据,就剩下5000封邮件。则计算出来的先验概率为:

P(Y1)=5000/25000=1/5,

P(Y2)=P(Y3)=10000/25000=2/5

如果还用贝叶斯方法,就要在似然函数后面乘上先验概率。比如之前用最大似然法算出Y1 垃圾邮件的概率大,但是因为P(Y1)特别小,用贝叶斯方法得出的结果是Y2 私人邮件的概率大。那相信哪个呢?其实,我们删掉了部分带标签的样本,从计算结果看P(Y1),P(Y2),P(Y3)的概率分布变化了,但实际上这三个类别的真实分布应该是一个客观的状态,不应该因为我们的计算方法而发生变化。所以是我们计算出来的先验概率失真,应该放弃这样计算出来的先验概率,而用最大似然法。但即便我们不删掉一半垃圾邮件,这三类邮件的分布就真的是1:1:1那样平均吗?那也未必。我们只是按1:1:1这样的方式进行了抽样而已,真正在邮箱里收到的这三类邮件的分布可能并不是这样。也就是说,在我们对于先验概率一无所知时,只能假设每种猜测的先验概率是均等的(其实这也是人类经验的结果),这个时候就只有用最大似然了。在现实运用过程中如果发现最大似然法有偏差,可以考虑对不同的似然函数设定一些系数或者阈值,使其接近真实情况。

但是,如果我们有足够的自信,训练集中这三类的样本分布的确很接近真实的情况,这时就应该用贝叶斯方法。难怪前面的贝叶斯学派强调的是“靠谱的先验概率”。所以说贝叶斯学派的适用范围更广,关键要先验概率靠谱,而频率学派有效的前提也是他们的先验概率同样是经验统计的结果

16. (朴素)贝叶斯方法的常见应用

说了这么多理论的问题,咱们就可以探讨一下(朴素)贝叶斯方法在自然语言处理中的一些常见应用了。以下只是从原理上进行探讨,对于具体的技术细节顾及不多。

16.1 褒贬分析

一个比较常见的应用场景是情感褒贬分析。比如你要统计微博上人们对一个新上映电影的褒贬程度评价:好片还是烂片。但是一条一条地看微博是根本看不过来,只能用自动化的方法。我们可以有一个很粗略的思路:

  • 首先是用爬虫将微博上提到这个电影名字的微博全都抓取下来,比如有10万条。
  • 然后用训练好的朴素贝叶斯分类器分别判断这些微博对电影是好评还是差评。
  • 最后统计出这些好评的影评占所有样本中的比例,就能形成微博网友对这个电影综合评价的大致估计。

接下来的核心问题就是训练出一个靠谱的分类器。首先需要有打好标签的文本。这个好找,豆瓣影评上就有大量网友对之前电影的评价,并且对电影进行1星到5星的评价。我们可以认为3星以上的评论都是好评,3星以下的评论都是差评。这样就分别得到了好评差评两类的语料样本。剩下就可以用朴素贝叶斯方法进行训练了。基本思路如下:

  • 训练与测试样本:豆瓣影评的网友评论,用爬虫抓取下100万条。
  • 标签:3星以上的是好评,3星以下的是差评。
  • 特征:豆瓣评论分词后的词语。一个简单的方法是只选择其中的形容词,网上有大量的情绪词库可以为我们所用。
  • 然后再用常规的朴素贝叶斯方法进行训练。

但是由于自然语言的特点,在提取特征的过程当中,有一些tricks需要注意:

  • 对否定句进行特别的处理。比如这句话“我不是很喜欢部电影,因为它让我开心不起来。”其中两个形容词“喜欢”、“开心”都是褒义词,但是因为句子的否定句,所以整体是贬义的。有一种比较简单粗暴的处理方式,就是“对否定词(“不”、“非”、“没”等)与句尾标点之间的所有形容词都采用其否定形式” 。则这句话中提取出来的形容词就应该是“不喜欢”和“不开心”。
  • 一般说来,最相关的情感词在一些文本片段中仅仅出现一次,词频模型起得作用有限,甚至是负作用,则使用伯努利模型代替多项式模型。这种情况在微博这样的小篇幅文本中似乎不太明显,但是在博客、空间、论坛之类允许长篇幅文本出现的平台中需要注意。
  • 其实,副词对情感的评价有一定影响。“不很喜欢”与“很不喜欢”的程度就有很大差异。但如果是朴素贝叶斯方法的话比较难处理这样的情况。我们可以考虑用语言模型或者加入词性标注的信息进行综合判断。这些内容我们将在之后的文章进行探讨。

当然经过以上的处理,情感分析还是会有一部分误判。这里涉及到许多问题,都是情感分析的难点:

  • 情绪表达的含蓄微妙:“导演你出来,我保证不打死你。”你让机器怎么判断是褒还是贬?
  • 转折性表达:“我非常喜欢这些大牌演员,非常崇拜这个导演,非常赞赏这个剧本,非常欣赏他们的预告片,我甚至为了这部影片整整期待了一年,最后进了电影院发现这是个噩梦。” 五个褒义的形容词、副词对一个不那么贬义的词。机器自然判断成褒义,但这句话是妥妥的贬义。

16.2 拼写纠错

拼写纠错本质上也是一个分类问题。但按照错误类型不同,又分为两种情况:

  • 非词错误(Non-word Errors):指那些拼写错误后的词本身就不合法,如将“wifi”写成“wify”;
  • 真词错误(Real-word Errors):指那些拼写错误后的词仍然是合法的情况,如将“wifi”写成“wife”。

真词错误复杂一些,我们将在接下来的文章中进行探讨。而对于非词错误,就可以直接采用贝叶斯方法,其基本思路如下:

  • 标签:通过计算错误词语的最小编辑距离(之前咱们提到过的),获取最相似的候选词,每个候选词作为一个分类。
  • 特征:拼写错误的词本身。因为它就一个特征,所以没有什么条件独立性假设、朴素贝叶斯啥的。它就是纯而又纯的贝叶斯方法。
  • 判别公式:

P(候选词i|错误词)∝P(错误词|候选词i)P(候选词i);i=1,2,3,…

  • 训练样本1:该场景下的正常用词语料库,用于计算P(候选词i)。

P(候选词i)=候选词出现的次数所有词出现的次数

  • 训练样本2:该场景下错误词与正确词对应关系的语料库,用于计算P(错误词|候选词i)

P(错误词|候选词i)=候选词被拼写成该“错误词”的次数候选词出现的次数

由于自然语言的特点,有一些tricks需要注意:

  • 据统计,80%的拼写错误编辑距离为1,几乎所有的拼写错误编辑距离小于等于2。我们只选择编辑距离为1或2的词作为候选词,这样就可以减少大量不必要的计算
  • 由于我们只选择编辑距离为1或2的词,其差别只是一两个字母级别差别。因此计算似然函数的时候,可以只统计字母层面的编辑错误,这样搜集的样本更多,更满足大数定律,也更简单。对于编辑距离为1的似然函数计算公式可以进化为:

P(错误词|候选词i)=⎧⎩⎨⎪⎪⎪⎪⎪⎪字母“xy”被拼写成“y”的次数字母“xy”出现的次数,字母“x”被拼写成“xy”的次数字母“x”出现的次数,字母“x”被拼写成“y”的次数字母“x”出现的次数,字母“xy”被拼写成“yx的次数字母“xy”出现的次数,

  • 键盘上临近的按键更容易拼写错误,据此可以对上面这个条件概率进行加权

\[键盘\]

17. 内容小结

从前面大家基本可以看出,工程应用不同于学术理论,有许多tricks需要考虑,而理论本质就是翻来倒去折腾贝叶斯公式,都快玩出花来了。

In [ ]:

1
 

GPT模型

文本分类:

数据集

THUCNews 数据集子集,链接: https://pan.baidu.com/s/1hugrfRu 密码: qfud

Language Understanding

  • intent classification

  • dialogue state tracking

  • sentiment classification

Language Generation

  • information, structured, sentiment –> language

必读论文

GPT

Radford et. al., Improving Language Understanding by Generative Pre-Training

https://s3-us-west-2.amazonaws.com/openai-assets/research-covers/language-unsupervised/language_understanding_paper.pdf

这篇文章推出了generative pre-training + discriminative fine-tuning的方法,后来也被BERT沿用。task-aware input transformation也是BERT借用的一个点。当年这篇文章刚出来的时候刷榜一波,不过离BERT太近,导致后来大家都不怎么关心这篇文章了。

a b c d e f

| | | | | | |

a b c d e f

1 0 0 0 0 0 0

1 1 0 0 0 0 0

1 1 1 0 0 0 0

1 1 1 1 0 0 0

1 1 1 1 1 0 0

1 1 1 1 1 1 0

预训练

语言模型objective

img

Transformer Decoder

img

训练使用BooksCorpus数据集,7000本书。

模型参数:

  • 12层transformer decoder

  • 768 hidden states, 12 attention heads

  • FFN层有3072维度inner states

Fine tuning

使用最后一层最后一个token的representation来做task specific的模型fine tuning

img

依然使用Log loss

img

作者发现在fine tuning的时候继续使用语言模型的loss也有好处

img

Task-specific Input Transformations

四种问题有四种不同的文本表示方法

img

Natural Language Inference

  • 判断两句话的关系,entailment 承接关系,contradiction 矛盾关系,neutral 中立关系

  • 在几个NLI任务上都有不小的提升

Question Answering and Common Sense Reasoning

Semantic Similarity 语义相似度

  • Microsoft Paraphrase Corpus

  • Quora Question Pairs

分类问题

  • Corpus of Lingustic Acceptability,判断一句话的语法是不是正确。

  • Stanford Sentiment Treebank 情感分类

GPT2

Radford et. al., Language Models are Unsupervised Multitask Learners

https://d4mucfpksywv.cloudfront.net/better-language-models/language_models_are_unsupervised_multitask_learners.pdf

比GPT更大的训练数据集

  • Common Crawl来自网页爬取,删除了Wikipedia,总共40GB数据。

evaluation任务

关于文本生成

https://arxiv.org/pdf/1904.09751.pdf

代码解读

我对代码添加了一些注释

https://github.com/ZeweiChu/gpt-2/blob/master/src/model.py

huggingface代码

https://github.com/huggingface/transformers/blob/master/src/transformers/modeling_gpt2.py

自动检测

def attention_mask(nd, ns, *, dtype):

​ “””1’s in the lower triangle, counting from the lower right corner.

​ 左下角的三角形都是1,其余是0,用于生成mask。

​ Same as tf.matrix_band_part(tf.ones([nd, ns]), -1, ns-nd), but doesn’t produce garbage on TPUs.

​ “””

​ i = tf.range(nd)[:,None]

​ j = tf.range(ns)

​ m = i >= j - ns + nd

​ return tf.cast(m, dtype)

0 0 0 0 0

1 1 1 1 1

2 2 2 2 2

3 3 3 3 3

4 4 4 4 4

0 1 2 3 4

1 0 0 0 0

1 1 0 0 0

1 1 1 0 0

1 1 1 1 0

1 1 1 1 1

w00 w01-inf w02-inf w03-inf w04-inf

w10 w11 w12-inf w13-inf w14-inf

w20 w21 w22 w23-inf w24-inf

w30 w31 w32 w33 w34-inf

w40 w41 w42 w43 w44

阅读GPT2代码:https://github.com/ZeweiChu/gpt-2/blob/master/src/model.py

Google T5: Transformer预训练模型大总结

论文地址:https://arxiv.org/pdf/1910.10683.pdf

代码+预训练模型:https://github.com/google-research/text-to-text-transfer-transformer

BERT系列预训练模型

BERT:Masked Language Modeling预训练模型

论文地址:https://arxiv.org/pdf/1810.04805.pdf

中文翻译:https://zhuanlan.zhihu.com/p/59775981

Language modeling预训练任务

Masked Language Model

完形填空

I study at Julyedu .

80% I study at [MASK] .

10% I study at Apple .

10% I study at Julyedu .

[CLS] I study at [MASK] . [SEP] I love [MASK] language processing . [SEP]

–> transformer encoder

o1, o2, o3, o4, o5, …., o_n

o5 –> Julyedu cross entropy

o10 –> natural cross entropy

o1 –> True cross entropy

BERT说:“我要用 transformer 的 encoders”

Ernie不屑道:“呵呵,你不能像Bi-Lstm一样考虑文章”

BERT自信回答道:“我们会用masks”

解释一下Mask:

语言模型会根据前面单词来预测下一个单词,但是self-attention的注意力只会放在自己身上,那么这样100%预测到自己,毫无意义,所以用Mask,把需要预测的词给挡住。

如下图:

img

Two-sentence Tasks

我们回顾一下OpenAI transformer处理不同任务的输入转换,你会发现在某些任务上我们需要2个句子作为输入,并做一些更为智能的判断,比如是否相似,比如 给出一个维基百科的内容作为输入,同时在放入一条针对该条目的问题,那么我们的算法模型能够处理这个问题吗?

为了使BERT更好的处理2个句子之间的关系,预训练的过程还有一个额外的任务:给定2个句子(A和B),A与B是否相似?(0或者1)

特殊NLP任务

BERT的论文为我们介绍了几种BERT可以处理的NLP任务:

  1. 短文本相似

  2. 文本分类

  3. QA机器人

  4. 语义标注

img

BERT用做特征提取

微调方法并不是使用BERT的唯一方法,就像ELMo一样,你可以使用预选训练好的BERT来创建语境化词嵌入。然后你可以将这些嵌入提供给现有的模型。

img

哪个向量最适合作为上下文嵌入? 我认为这取决于任务。 本文考察了六种选择(与微调模型相比,得分为96.4):

img

  • Feature Extraction:特征提取

  • Finetune:微调

如何使用BERT

BERT源码

查看BERT仓库中的代码:

  1. 该模型在modeling.py(BertModel类)中构建,与vanilla Transformer编码器完全相同。

  2. run_classifier.py是微调过程的一个示例。它还构建了监督模型的分类层。如果要构建自己的分类器,请查看该文件中的create_model()方法。

  3. 可以下载几种预先训练的模型。涵盖102种语言的多语言模型,这些语言都是在维基百科的数据基础上训练而成的。

  4. BERT不会将单词视为tokens。相反,它注重WordPieces。 tokenization.py是将你的单词转换为适合BERT的wordPieces的tokensizer。

可以查看BERT的PyTorch实现 (https://github.com/huggingface/transformers)。

BERT模型的使用

BERT升级版

RoBERTa:更强大的BERT

论文地址:https://arxiv.org/pdf/1907.11692.pdf

  • 加大训练数据 16GB -> 160GB,更大的batch size,训练时间加长

  • 不需要NSP Loss: natural inference

  • 使用更长的训练 Sequence

  • Static vs. Dynamic Masking

  • 模型训练成本在6万美金以上(估算)

ALBERT:参数更少的BERT

论文地址:https://arxiv.org/pdf/1909.11942.pdf

  • 一个轻量级的BERT模型

  • 核心思想:

  • 共享层与层之间的参数 (减少模型参数)

  • 增加单层向量维度

DistilBERT:轻量版BERT

MNIST

0, 1, 2, 3, …, 9

logits: [0.1, 0.6, …, 0.01] q

label: 2 [0, 0, 1, …, 0] p

loss: cross entropy loss -\sum_{i=1}^10 p_i*log q_i

loss: -log q_{label}

训练一个Student network,mimic the behavior of the teacher network

teacher network: [0.1, 0.6, …, 0.01] t

student network: [s_1, s_2, .., s_10]

cross entropy loss: -sum_{i=1}^10 t_i * log s_i

4, 7

https://arxiv.org/pdf/1910.01108.pdf

  • MLM, NSP

  • MLM: cross entropy loss: -\sum_{i=1}^k p_i log (q_i) = - log (q_{label})

  • teacher (MLM) = distribution

  • student: 学习distribution: -\sum_{i=1}^k p_teacher_i log (q_student_i)

Patient Distillation

https://arxiv.org/abs/1908.09355

img

参考阅读资料

BERT Distillation

对于BERT模型压缩感兴趣的同学可以参考以下资料

关于BERT模型压缩的一套方法

ELECTRA

https://openreview.net/pdf?id=r1xMH1BtvB

使用GAN训练BERT模型

img

  • Generator针对[MASK]位置生成单词,Discriminator判断这些单词是由Generator (从[MASK]) 生成的还是原本就存在的。

  • Discriminator被用于downstream task finetuning。

XLNet

我在上一期NLP就业班中介绍了XLNet,不过由于近些日子BERT的各种加强版层出不穷,XLNet显得并不特别突出。感兴趣的同学可以参考上一期的课件:https://shimo.im/docs/PHqcpWtYjJjW3yH3

XLNet的代码和预训练模型也可以在Huggingface的版本中找到。

NLP预训练模型串讲

我之前在七月在线的公开课中使用的PPT

NLP预训练模型.pdf1.9MB

参考阅读:The Illustrated BERT, ELMo, and co.

https://shimo.im/docs/Y6q3gX8yGGjpWqXx

阅读理解

NLP当中的阅读理解(Reading Comprehension, Question Answering)任务主要是以下形式:给定一些背景知识,主要是一篇文章,有时候也可能是一些结构化的知识图谱,然后回答与该背景知识的相关问题。

常见的问题和答案形式有:

  • 完形填空:在文章中给定一个空位和一些候选答案,我们需要把一个候选答案填充进去。

  • 简答题:给定一篇文章和一个问题,我们需要从文章中去找答案,且这个答案一定在文章中出现过。SQuAD

  • 选择题:给定一篇文章,一个问题和一些候选答案,选择一个正确答案。

还有一些在上述问答任务基础上的拓展情况,例如有的任务需要在多篇文章的基础上作答,有的QA任务需要我们自己来推理和撰写答案 (open domain),无法直接从文中找到答案。

整个QA领域的发展主要都是依靠一些数据集的提出和解决来推动的。往往是有人制作了一个数据集和一个QA任务,然后大家开始比赛谁能更好地解决它。

我认为最好的学习方法是去了解这些QA数据集(http://nlpprogress.com/english/question_answering.html),针对自己感兴趣的数据集去寻找相应的解决方案,在这个过程中了解QA的解决方法。将来如果在实际的应用场景中遇到类似的QA任务(例如一些客服机器人等),我们就可以寻找到比较相关的QA数据集,使用在这些数据集上最好的解决方案来解决自己的任务。

一些有名的阅读理解数据集和模型

SQuAD 1.0/2.0

从文章中找答案

https://aclweb.org/anthology/D16-1264

img

BiDAF模型

Bi-Directional Attention Fflow for Machine Comprehension

https://arxiv.org/pdf/1611.01603.pdf

2017年的模型,用于解决SQuAD之类的问题。后来的很多模型都参考了该模型的设计思想

img

预测: start_pos, end_pos

其他相关模型

Document Reader (single model)

r-net (single model)

QANet (single)

https://github.com/allenai/bi-att-flow

MCTest

https://www.microsoft.com/en-us/research/wp-content/uploads/2016/11/MCTest_EMNLP2013.pdf

BERT模型

模型 https://arxiv.org/pdf/1810.04805.pdf

代码 https://github.com/huggingface/transformers/blob/master/src/transformers/modeling_bert.py#L1402

https://github.com/huggingface/transformers/blob/master/examples/run_squad.py

CNN/Daily Mail

完形填空类问题

Teaching Machines to Read and Comprehend

https://arxiv.org/pdf/1506.03340.pdf

img

Attention Sum模型

https://arxiv.org/pdf/1603.01547.pdf

Gated Attention Sum模型是该模型的一个拓展形式

https://arxiv.org/abs/1606.01549

陈丹琦在CNN/Daily Mail上的工作

A Thorough Examination of the CNN/Daily Mail Reading Comprehension Task

https://www.aclweb.org/anthology/P16-1223

img

顺便介绍一下,陈丹琦在QA领域做了很多工作,对QA感兴趣的同学可以参考她的博士论文。

https://www.cs.princeton.edu/~danqic/

https://www.cs.princeton.edu/~danqic/papers/thesis.pdf

https://arxiv.org/pdf/1506.03340.pdf

https://arxiv.org/pdf/1603.01547.pdf

http://www.cs.cmu.edu/~bdhingra/papers/ga_reader.pdf

https://arxiv.org/pdf/1611.07954.pdf

RACE数据集

RACE: Large-scale ReAding Comprehension Dataset From Examinations

https://arxiv.org/pdf/1704.04683.pdf

RACE数据集来自中国的中高考英语阅读理解题。

代码:

https://github.com/huggingface/transformers/blob/master/src/transformers/modeling_bert.py#L1204

https://github.com/huggingface/transformers/blob/master/examples/utils_multiple_choice.py#L36

https://github.com/huggingface/transformers/blob/master/examples/run_multiple_choice.py

SWAG

后来出现了很多的不同方向的QA问题

参考以下链接

基于多文本的QA任务

HOTPOTQA: A Dataset for Diverse, Explainable Multi-hop Question Answering

https://www.aclweb.org/anthology/D18-1259

HotpotQA的主要特点是,这是一个基于多文本的QA任务。给定一系列的文章和一个问题,我们需要给出该问题的答案,并且回答我们是从哪些相关的句子中得到问题的答案的。

已经公开的可参考论文

Hierarchical Graph Network for Multi-hop Question Answering https://arxiv.org/pdf/1911.00484.pdf

Select, Answer and Explain: Interpretable Multi-hop Reading Comprehension over Multiple Documents https://arxiv.org/pdf/1911.03631.pdf

CoQA 基于对话的问答数据集

leaderboard https://stanfordnlp.github.io/coqa/

论文 https://arxiv.org/abs/1808.07042

搜狗有一个BERT模型的实现

https://github.com/sogou/SMRCToolkit

这位同学也实现了一些模型

https://github.com/jayelm/dialog-qa

中文数据集

法研杯 阅读理解数据集

http://cail.cipsc.org.cn/

讯飞杯 中文阅读理解评测

https://hfl-rc.github.io/cmrc2017/

https://hfl-rc.github.io/cmrc2018/

https://hfl-rc.github.io/cmrc2019/

同学们可以找到这三次比赛的数据集和相应的表现最好的模型代码进行学习。

KBQA

http://tcci.ccf.org.cn/conference/2018/papers/EV51.pdf

Transformer模型解读

contextualized word vectors

RNN, LSTM

RNN(I study at Julyedu.) –> RNN(I)->h1, RNN(study, h1)->h2, RNN(at, h2)->h3.

Encoder. 我可以同时观看全局信息。

query, keys, values

q1, q2, .., q5

k1, k2, k3, k4, k5

score(q, k1), score(q, k2), …, score(q, k5)

v1, v2, v3, v4, v5

\sum_{i=1}^5 func(score_i) v_i

dot(a, b)

mean

var(dot(a, b))

dot(a, b) = a1b1 + a2b2. ….

E(dot(a, b)) = n * E(ai*bi)

var(dot(a, b)) = E(dot(a, b)^2) - E(dot(a, b))^2

affine transformation

WX+b

Attention(Q, K, V ) = softmax(QKT √ dk )V

Q : seq_len, hid_size

K^T: hid_size, seq_len

V: seq_len, hid_size

QK^T : seq_len, seq_len

QK^T V: seq_len, hid_size

[emb_w(x), emb_p(i)]W –>

近两年来,NLP领域的模型研究已经被transformer模型以及它的各种变种给占领了。Transformer模型的火爆有很多原因,例如:

  • 模型简单易懂,encoder和decoder模块高度相似且通用

  • (encoder)容易并行,模型训练速度快

  • 效果拔群,在NMT等领域都取得了state-of-the-art的效果

论文地址

下面的文章翻译自

高屋建瓴地说,Transformer模型拿到一个序列,用来生成另一个序列。

img

打开这个黑箱,我们会看到其中包含了两个部分,encoders和decoders。

img

其中encoders和decoders都是两个堆叠架构。一层一层同质的结构堆叠到一起,组成了编码器和解码器。

img

首先我们打开每个encoder来参观一下其中包含的内容:

img

每一个encoder都包含了一个自注意力(self-attention)层和一个Feed Forward Neural Network。

encoder的输入首先会经过一个self-attention层。self-attention的作用是让每个单词可以看到自己和其他单词的关系,并且将自己转换成一个与所有单词相关的,focus在自己身上的词向量(?)

self-attention之后的输出会再经过一层feed-forward神经网络。每个位置的输出被同样的feed-forward network处理。

decoder也有同样的self-attention和feed-forward结构,但是在这两层之间还有一层encoder-decoder attention层,帮助decoder关注到某一些特别需要关注的encoder位置。

Tensor的变化

img

编码器

下面我们来详细解读一下编码器的工作。

img

Self-Attention机制

我们考虑用Transformer模型翻译下面这一句话:

“The animal didn’t cross the street because it was too tired”。

当我们翻译到 it 的时候,我们知道 it 指代的是 animal 而不是 street。所以,如果有办法可以让 it 对应位置的 embedding 适当包含 animal 的信息,就会非常有用。self-attention的出现就是为了完成这一任务。

如下图所示,self attnetion会让单词 it 和 某些单词发生比较强的联系,得到比较搞的attention分数。

img

weight(The) = softmax(v(it) * v(The) / \sqrt(d))

weight(The) = softmx(Query(It) * Key(The) / \sqrt(d))

\sum_{word} weight(word) * Value(word)

Self-attention的细节

为了实现 self-attention,每个输入的位置需要产生三个向量,分别是 Query 向量,Key 向量和 Value 向量。这些向量都是由输入 embedding 通过三个 matrices (也就是线性变化)产生的。

注意到在Transformer架构中,这些新的向量比原来的输入向量要小,原来的向量是512维,转变后的三个向量都是64维。

img

第二步是计算分数。当我们在用self-attention encode某个位置上的某个单词的时候,我们希望知道这个单词对应的句子上其他单词的分数。其他单词所得到的分数表示了当我们encode当前单词的时候,应该放多少的关注度在其余的每个单词上。又或者说,其他单词和我当前的单词有多大的相关性或者相似性。

在transformer模型中,这个分数是由query vector和key vector做点积(dot product)所得的结果。所以说,当我们在对第一个单词做self-attention处理的时候,第一个单词的分数是q_1和k_1的点积,第二个分数是q_1和k_2的分数。

img

第三步和第四步是将这些分数除以8。8这个数字是64的开方,也就是key vector的维度的开方。据说这么做可以稳定模型的gradient。然后我们将这些分数传入softmax层产生一些符合概率分布的probability scores。

img

softmax = exp(x_i) / sum exp(x_i)

这些分数就表示了在处理当前单词的时候我们应该分配多少的关注度给其他单词。

第五步是将每个value vector乘以它们各自的attention score。第六步是把这些weighted value vectors相加,成为当前单词的vector表示。

img

得到了self-attention生成的词向量之后,我们就可以将它们传入feed-forward network了。

Self-Attention中的矩阵运算

首先,我们要对每一个词向量计算Query, Key和Value矩阵。我们把句子中的每个词向量拼接到一起变成一个矩阵X,然后乘以不同的矩阵做线性变换(WQ, WK, WV)。

img

然后我们就用矩阵乘法实现上面介绍过的Self-Attention机制了。

img

Multi-headed attention

在论文当中,每个embedding vector并不止产生一个key, value, query vectors,而是产生若干组这样的vectors,称之为”multi-headed” attention。这么做有几个好处:

  • k: key, q: query, v: value

  • 模型有更强的能力产生不同的attention机制,focus在不同的单词上。

  • attention layer有多个不同的”representation space”。

img

每个attention head最终都产生了一个matrix表示这个句子中的所有词向量。在transformer模型中,我们产生了八个matrices。我们知道self attention之后就是一个feed-forward network。那么我们是否需要做8次feed-forward network运算呢?事实上是不用的。我们只需要将这8个matrices拼接到一起,然后做一次前向神经网络的运算就可以了。

img

综合起来,我们可以用下面一张图表示Self-Attention模块所做的事情。

img

Positional Encoding

thinking machine

w_1, w_2

p_1, p_2

positional_embedding = nn.Embedding(512, 300)

w_1 + p_1, w_2 + p_2, w_3 + p_3, …, w_n + p_n

到目前为止,我们的模型完全没有考虑单词的顺序。即使我们将句子中单词的顺序完全打乱,对于transformer这个模型来说,并没有什么区别。为了加入句子中单词的顺序信息,我们引入一个概念叫做positional encoding。

img

如果我们假设输入的embedding是4个维度的,那么他们的position encodings大概长下面这样。

img

下面这张图的每一行表示一个positional encoding vector。第一行表示第一个单词的positional encoding,以此类推。每一行都有512个-1到1之间的数字。我们用颜色标记了这些vectors。

img

Residuals

另外一个细节是,encoder中的每一层都包含了一个residual connection和layer-normalization。如下图所示。

img

下面这张图是更详细的vector表示。

img

decoder也是同样的架构。如果我们把encoder和decoder放到一起,他们就长这样。

img

解码器

encoder最后一层会输出attention vectors K和V。K和V会被decoder用作解码的原材料。

img

在解码的过程中,解码器每一步会输出一个token。一直循环往复,直到它输出了一个特殊的end of sequence token,表示解码结束了。

img

decoder的self attention机制与encoder稍有不同。在decoder当中,self attention层只能看到之前已经解码的文字。我们只需要把当前输出位置之后的单词全都mask掉(softmax层之前全都设置成-inf)即可。

softmax(Q matmul K^T / sqrt(d)) matmul V

weights = Q matmul K^T: [seq_len, seq_len]

Masked Self Attention

q, k (100, 24, 35 - inf, 88 - inf, -55 - inf) –> softmax –> (0.9, 0.1, 0, 0, 0)

attention_mask

0, -inf, -inf, -inf

0, 0, -inf, -inf

0, 0, 0, -inf

0, 0, 0, 0

softmax(weights - attention_mask, -1)

训练

QKV, 并行训练

预测

一个单词一个单词解码

Encoder-Decoder Attention层和普通的multiheaded self-attention一样,除了它的Queries完全来自下面的decoder层,然后Key和Value来自encoder的输出向量。

batch_size * seq_length * hidden_size

padding_mask

tgt_mask

最后的线性层和softmax层

解码器最后输出浮点向量,如何将它转成词?这是最后的线性层和softmax层的主要工作。

线性层是个简单的全连接层,将解码器的最后输出映射到一个非常大的logits向量上。假设模型已知有1万个单词(输出的词表)从训练集中学习得到。那么,logits向量就有1万维,每个值表示是某个词的可能倾向值。

softmax层将这些分数转换成概率值(都是正值,且加和为1),最高值对应的维上的词就是这一步的输出单词。

img

模型的训练

现在我们已经了解了一个训练完毕的Transformer的前向过程,顺道看下训练的概念也是非常有用的。在训练时,模型将经历上述的前向过程,当我们在标记训练集上训练时,可以对比预测输出与实际输出。为了可视化,假设输出一共只有6个单词(“a”, “am”, “i”, “thanks”, “student”, “”)

img

模型的词表是在训练之前的预处理中生成的

一旦定义了词表,我们就能够构造一个同维度的向量来表示每个单词,比如one-hot编码,下面举例编码“am”。

img

举例采用one-hot编码输出词表

下面让我们讨论下模型的loss损失,在训练过程中用来优化的指标,指导学习得到一个非常准确的模型。

损失函数

我们用一个简单的例子来示范训练,比如翻译“merci”为“thanks”。那意味着输出的概率分布指向单词“thanks”,但是由于模型未训练是随机初始化的,不太可能就是期望的输出。

img

由于模型参数是随机初始化的,未训练的模型输出随机值。我们可以对比真实输出,然后利用误差后传调整模型权重,使得输出更接近与真实输出。如何对比两个概率分布呢?简单采用 cross-entropy或者Kullback-Leibler divergence中的一种。鉴于这是个极其简单的例子,更真实的情况是,使用一个句子作为输入。比如,输入是“je suis étudiant”,期望输出是“i am a student”。在这个例子下,我们期望模型输出连续的概率分布满足如下条件:

  1. 每个概率分布都与词表同维度

  2. 第一个概率分布对“i”具有最高的预测概率值。

  3. 第二个概率分布对“am”具有最高的预测概率值。

  4. 一直到第五个输出指向””标记。

img

对一个句子而言,训练模型的目标概率分布

在足够大的训练集上训练足够时间之后,我们期望产生的概率分布如下所示:

img

训练好之后,模型的输出是我们期望的翻译。当然,这并不意味着这一过程是来自训练集。注意,每个位置都能有值,即便与输出近乎无关,这也是softmax对训练有帮助的地方。现在,因为模型每步只产生一组输出,假设模型选择最高概率,扔掉其他的部分,这是种产生预测结果的方法,叫做greedy 解码。另外一种方法是beam search,每一步仅保留最头部高概率的两个输出,根据这俩输出再预测下一步,再保留头部高概率的两个输出,重复直到预测结束

更多资料

Transformer-XL

Transformer-XL: Attentive Language Models Beyond a Fixed-Length Context

https://arxiv.org/pdf/1901.02860.pdf

相较于传统transformer decoder,引入两个新模块

  • segment-level recurrence mechanism

img

  • a novel positional encoding scheme

  • 考虑我们在attention机制中如何使用positional encoding

(E_{x_i}^T+U_i^T)W_q^TW_kE_{x_j}U_j

img

  • R他们采用的是transformer当中的positional encoding

  • u和v是需要训练的模型参数

最终Transformer XL模型

img

代码

https://github.com/kimiyoung/transformer-xl

XLNet: Generalized Autoregressive Pretraining for Language Understanding

https://arxiv.org/pdf/1906.08237.pdf

背景知识

  • 自回归语言模型(Autoregressive Language Model):采用从左往右或从右往左的语言模型,根据上文预测下文。

  • 缺点:只利用了预测单词左边或右边的信息,无法同时利用两边的信息。ELMo在一定程度上解决了这个问题。

  • img

  • 自编码模型(Denoising Auto Encoder, DAE):在输入中随机mask一些单词,利用上下文来预测被mask掉的单词。BERT采用了这一思路。

  • img

两个模型的问题

img

XLNet的目标是融合以上两种模型的优点,解决它们各自存在的问题。

XLNet模型:Permutation Language Modeling

img

Two-Stream Self-Attention

img

img

参考资料

https://zhuanlan.zhihu.com/p/70257427

代码

https://github.com/zihangdai/xlnet

英文书籍word级别的文本生成代码注释

先看丘吉尔的人物传记char级别的文本生成

举个小小的例子,来看看LSTM是怎么玩的

我们这里不再用char级别,我们用word级别来做。我们这里的文本预测就是,给了前面的单词以后,下一个单词是谁?

比如,hello from the other, 给出 side

第一步,一样,先导入各种库

导入数据并分词

In [1]:

1
2
3
4
5
6
7
8
9
10
import os
import numpy as np
import nltk
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import Dropout
from keras.layers import LSTM
from keras.callbacks import ModelCheckpoint
from keras.utils import np_utils
from gensim.models.word2vec import Word2Vec
1
Using TensorFlow backend.

In [8]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 运行资源充足的可以试试下面的代码
# raw_text = ''
# for file in os.listdir("./input/"):
# # os.listdir列出路径下的所有文件的名字
# if file.endswith(".txt"): # 取出后缀.txt的文件
# raw_text += open("./input/"+file, errors='ignore').read() + '\n\n'
raw_text = open('./input/Winston_Churchil.txt').read()
# 我们仍用丘吉尔的语料生成文本
raw_text = raw_text.lower()
sentensor = nltk.data.load('tokenizers/punkt/english.pickle')
# 加载英文的划分句子的模型
sents = sentensor.tokenize(raw_text)
# .tokenize对一段文本进行分句,分成各个句子组成的列表。详解看下这个博客,蛮有意思的
# https://blog.csdn.net/ustbbsy/article/details/80053307
print(sents[:2])
1
['\ufeffproject gutenberg’s real soldiers of fortune, by richard harding davis\n\nthis ebook is for the use of anyone anywhere at no cost and with\nalmost no restrictions whatsoever.', 'you may copy it, give it away or\nre-use it under the terms of the project gutenberg license included\nwith this ebook or online at www.gutenberg.org\n\n\ntitle: real soldiers of fortune\n\nauthor: richard harding davis\n\nposting date: february 22, 2009 [ebook #3029]\nlast updated: september 26, 2016\n\nlanguage: english\n\ncharacter set encoding: utf-8\n\n*** start of this project gutenberg ebook real soldiers of fortune ***\n\n\n\n\nproduced by david reed, and ronald j. wilson\n\n\n\n\n\nreal soldiers of fortune\n\n\nby richard harding davis\n\n\n\n\n\nmajor-general henry ronald douglas maciver\n\nany sunny afternoon, on fifth avenue, or at night in the _table d’hote_\nrestaurants of university place, you may meet the soldier of fortune who\nof all his brothers in arms now living is the most remarkable.']

In [9]:

1
2
3
4
5
6
corpus = []
for sen in sents: # 针对每个句子,再次进行分词。
corpus.append(nltk.word_tokenize(sen))

print(len(corpus))
print(corpus[:2])
1
2
1792
[['\ufeffproject', 'gutenberg', '’', 's', 'real', 'soldiers', 'of', 'fortune', ',', 'by', 'richard', 'harding', 'davis', 'this', 'ebook', 'is', 'for', 'the', 'use', 'of', 'anyone', 'anywhere', 'at', 'no', 'cost', 'and', 'with', 'almost', 'no', 'restrictions', 'whatsoever', '.'], ['you', 'may', 'copy', 'it', ',', 'give', 'it', 'away', 'or', 're-use', 'it', 'under', 'the', 'terms', 'of', 'the', 'project', 'gutenberg', 'license', 'included', 'with', 'this', 'ebook', 'or', 'online', 'at', 'www.gutenberg.org', 'title', ':', 'real', 'soldiers', 'of', 'fortune', 'author', ':', 'richard', 'harding', 'davis', 'posting', 'date', ':', 'february', '22', ',', '2009', '[', 'ebook', '#', '3029', ']', 'last', 'updated', ':', 'september', '26', ',', '2016', 'language', ':', 'english', 'character', 'set', 'encoding', ':', 'utf-8', '***', 'start', 'of', 'this', 'project', 'gutenberg', 'ebook', 'real', 'soldiers', 'of', 'fortune', '***', 'produced', 'by', 'david', 'reed', ',', 'and', 'ronald', 'j.', 'wilson', 'real', 'soldiers', 'of', 'fortune', 'by', 'richard', 'harding', 'davis', 'major-general', 'henry', 'ronald', 'douglas', 'maciver', 'any', 'sunny', 'afternoon', ',', 'on', 'fifth', 'avenue', ',', 'or', 'at', 'night', 'in', 'the', '_table', 'd', '’', 'hote_', 'restaurants', 'of', 'university', 'place', ',', 'you', 'may', 'meet', 'the', 'soldier', 'of', 'fortune', 'who', 'of', 'all', 'his', 'brothers', 'in', 'arms', 'now', 'living', 'is', 'the', 'most', 'remarkable', '.']]

word2vec生成词向量

In [45]:

1
2
3
4
5
6
7
8
w2v_model = Word2Vec(corpus, size=128, window=5, min_count=2, workers=4)
# Word2Vec()参数看这个博客:https://www.cnblogs.com/pinard/p/7278324.html
# size:词向量的维度
# window:即词向量上下文最大距离,window越大,则和某一词较远的词也会产生上下文关系。默认值为5。
# min_count:需要计算词向量的最小词频。这个值可以去掉一些很生僻的低频词,默认是5。如果是小语料,可以调低这个值。
# workers:用于控制训练的并行数。

print(w2v_model['office'][:20])
1
2
3
4
[-0.03379476 -0.22743131 -0.17660786 -0.00957653 -0.10752155 -0.14298159
0.02914934 -0.08970737 -0.15872304 -0.05246524 -0.00084796 -0.05634443
-0.1461402 0.03880814 -0.12331649 -0.06511988 -0.08555544 -0.2300725
-0.0083805 0.02204316]
1
/Users/yyg/anaconda3/lib/python3.6/site-packages/ipykernel_launcher.py:8: DeprecationWarning: Call to deprecated `__getitem__` (Method will be removed in 4.0.0, use self.wv.__getitem__() instead).

构造训练集

In [46]:

1
2
3
4
5
6
7
8
9
raw_input = [item for sublist in corpus for item in sublist]
print(len(raw_input)) # 原始语料库里的词语总数
text_stream = []
vocab = w2v_model.wv.vocab # 查看w2v_model生成的词向量
for word in raw_input:
if word in vocab:
text_stream.append(word)
print(len(text_stream))
# 查看去掉低频词后的总的词数,因为min_count把低频词去掉了
1
2
55562
51876

In [47]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 处理方式同char级别的文本生成
seq_length = 10
x = []
y = []
for i in range(0, len(text_stream) - seq_length):
given = text_stream[i:i + seq_length]
predict = text_stream[i + seq_length]
x.append([w2v_model[word] for word in given])
y.append(w2v_model[predict])

x = np.reshape(x, (-1, seq_length, 128))
y = np.reshape(y, (-1,128))
print(x.shape)
print(y.shape)
1
2
3
4
/Users/yyg/anaconda3/lib/python3.6/site-packages/ipykernel_launcher.py:8: DeprecationWarning: Call to deprecated `__getitem__` (Method will be removed in 4.0.0, use self.wv.__getitem__() instead).

/Users/yyg/anaconda3/lib/python3.6/site-packages/ipykernel_launcher.py:9: DeprecationWarning: Call to deprecated `__getitem__` (Method will be removed in 4.0.0, use self.wv.__getitem__() instead).
if __name__ == '__main__':
1
2
(51866, 10, 128)
(51866, 128)

构建和训练模型

In [53]:

1
2
3
4
5
6
7
8
model = Sequential()
model.add(LSTM(256, input_shape=(seq_length, 128),dropout=0.2, recurrent_dropout=0.2))
# 第一个dropout是x和hidden之间的dropout
# 第二个recurrent_dropout,这里我理解为是横向不同时刻隐藏层之间的dropout
model.add(Dropout(0.2)) # 第三个,这里我理解为纵向层与层之间的dropout
model.add(Dense(128, activation='sigmoid'))
model.compile(loss='mse', optimizer='adam')
# 损失用的均方差损失,优化器adam

In [54]:

1
model.fit(x, y, nb_epoch=10, batch_size=4096)
1
2
/Users/yyg/anaconda3/lib/python3.6/site-packages/ipykernel_launcher.py:1: UserWarning: The `nb_epoch` argument in `fit` has been renamed `epochs`.
"""Entry point for launching an IPython kernel.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Epoch 1/10
51866/51866 [==============================] - 28s 539us/step - loss: 0.3177
Epoch 2/10
51866/51866 [==============================] - 28s 542us/step - loss: 0.1405
Epoch 3/10
51866/51866 [==============================] - 29s 560us/step - loss: 0.1329
Epoch 4/10
51866/51866 [==============================] - 30s 584us/step - loss: 0.1318
Epoch 5/10
51866/51866 [==============================] - 28s 548us/step - loss: 0.1313
Epoch 6/10
51866/51866 [==============================] - 30s 574us/step - loss: 0.1309
Epoch 7/10
51866/51866 [==============================] - 30s 570us/step - loss: 0.1306
Epoch 8/10
51866/51866 [==============================] - 29s 551us/step - loss: 0.1303
Epoch 9/10
51866/51866 [==============================] - 27s 524us/step - loss: 0.1299
Epoch 10/10
51866/51866 [==============================] - 27s 512us/step - loss: 0.1296

Out[54]:

1
<keras.callbacks.History at 0x1a32c9a2b0>

预测模型

In [55]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 代码注释同丘吉尔的人物传记char级别的文本生成
def predict_next(input_array):
x = np.reshape(input_array, (-1,seq_length,128))
y = model.predict(x)
return y

def string_to_index(raw_input):
raw_input = raw_input.lower()
input_stream = nltk.word_tokenize(raw_input)
res = []
for word in input_stream[(len(input_stream)-seq_length):]:
res.append(w2v_model[word])
return res

def y_to_word(y):
word = w2v_model.most_similar(positive=y, topn=1)
return word

In [56]:

1
2
3
4
5
6
def generate_article(init, rounds=30):
in_string = init.lower()
for i in range(rounds):
n = y_to_word(predict_next(string_to_index(in_string)))
in_string += ' ' + n[0][0]
return in_string

In [58]:

1
2
3
init = 'His object in coming to New York was to engage officers for that service. He came at an  moment'
article = generate_article(init)
print(article) # 语料库较小,可以看到重复了
1
2
3
4
/Users/yyg/anaconda3/lib/python3.6/site-packages/ipykernel_launcher.py:12: DeprecationWarning: Call to deprecated `__getitem__` (Method will be removed in 4.0.0, use self.wv.__getitem__() instead).
if sys.path[0] == '':
/Users/yyg/anaconda3/lib/python3.6/site-packages/ipykernel_launcher.py:16: DeprecationWarning: Call to deprecated `most_similar` (Method will be removed in 4.0.0, use self.wv.most_similar() instead).
app.launch_new_instance()
1
his object in coming to new york was to engage officers for that service. he came at an  moment battery battery battery battery battery battery battery battery battery battery battery battery battery battery battery battery battery battery battery battery battery battery battery battery battery battery battery battery battery battery

In [ ]:

1
 
|