在《生成式模型算法原理深入浅出》中,我们浅浅探讨了生成式模型与判别式模型的不同之处,随后主要重点介绍了五种生成式算法模型:朴素贝叶斯(Naive Bayes)、高斯混合模型(GMM)、隐马尔可夫模型(HMM)、主题模型(LDA)、生成对抗模型(GAN)以及文生图模型(Stable Diffusion)。本文将进一步选取生成式模型中的变分自编码器(Variational Auto-Encoder,简称VAE)和判别式模型中的条件随机场(Conditional Random Field,简称CRF),对这两类算法进行详细的比较分析,以揭示生成式与判别式算法之间的区别。
1. 变分自编码器(Variational Auto-Encoder)
变分自编码器(VAE)【1】以概率的方式描述潜在空间中的观察。不仅构建一个输出单一值以描述每个潜在状态属性的编码器,而是将编码器构建为描述每个潜在属性的概率分布。核心思想涉及将复杂的生成模型学习为潜在变量模型,并通过变分推断来近似潜在变量的后验分布。
1.1 变分自编码与自编码的区别
看到VAE,可能会一下子想到AE,那么这两种有哪些不同点?我罗列了一些对比:
1. 编码和解码方式
自编码器(AE):
- 编码器:将输入数据压缩为一个固定大小的潜在向量。
- 解码器:将这个潜在向量重建回原始数据。
- 特点:编码器生成的是一个确定性的潜在表示(即一个具体的潜在向量或是单一值),解码器从这个潜在向量生成输出。
变分自编码器(VAE):
- 编码器:将输入数据编码为潜在空间中的概率分布(通常是高斯分布),输出均值和方差。
- 解码器:从分布中采样一个潜在向量,并使用它生成输出数据。
- 特点:编码器输出的是一个潜在变量的概率分布,而不是单一的潜在向量。解码器从这个分布中采样生成数据,允许模型捕捉更多的潜在空间特征。
2. 损失函数
自编码器(AE):
- 损失函数:主要是重建损失,即输入数据与重建数据之间的差异,通常使用均方误差(MSE)或交叉熵。
- 公式:
变分自编码器(VAE):
- 损失函数:包含重建损失和正则化项(KL 散度)。重建损失确保生成的数据与输入数据相似,正则化项则确保潜在空间分布符合指定的先验分布(如标准正态分布)。
- 公式:
3. 潜在空间的处理
- 自编码器(AE):
- 潜在空间:通常没有明确的分布假设,潜在空间的结构可能不规则,可能导致生成的数据质量不一致。
- 变分自编码器(VAE):
- 潜在空间:通过正则化确保潜在空间的结构更规则,通常是高斯分布。这样的正则化使得潜在空间具有更好的生成能力和一致性。
4. 生成能力
- 自编码器(AE):
- 生成能力:自编码器通常不具备生成新样本的能力,因为它没有强制潜在空间遵循某个分布。
- 变分自编码器(VAE):
- 生成能力:由于潜在空间被正则化为特定的分布,VAE 可以从潜在空间中采样生成新的数据样本。
5. 应用
- 自编码器(AE):
- 主要用于数据压缩、特征提取和降维。
- 变分自编码器(VAE):
- 除了用于数据压缩和降维外,VAE 还广泛应用于数据生成、缺失数据填补、风格迁移等任务。
【2】给出了AE和VAE的结构差异示意图:
1.2 VAE的目标(最大化数据的边际似然)
给定数据 x,VAE 的目标是最大化边际似然 p(x): 其中,
是解码器网络生成数据的条件概率,
是潜在变量 z 的先验分布。
变分自编码器(VAE)使用 KL 散度作为其损失函数,目标是最小化假设分布与数据集原始分布之间的差异。假设有一个分布 z,希望从中生成观察值 x。也就是想计算。可以通过以下方式进行计算:
,但是,直接计算 p(x)是相当困难的:
。
为什么直接计算p(x)是困难的?
在 VAE 中,数据分布 p(x) 通常通过条件分布 p(x∣z)和隐变量分布 p(z) 来表示。直接计算 p(x) 需要对隐变量 z 进行积分,这个积分在高维空间中通常非常复杂,难以解析计算。需要将
近似为
,以使其成为一个可处理的分布。
为了更好地将
近似为
,我们将最小化 KL 散度损失,这个损失计算了两个分布的相似度:
。通过简化,上述最小化问题等同于以下的最大化问题:
其中,第一项表示重建的似然性,另一项确保我们学习到的分布 q 与真实的先验分布 p 相似。
接下来,详细推导一下最小化KL散度转换为最大化ELBO的损失函数表达式:
在变分自编码器(VAE)中,我们希望最小化真实后验分布 p(z∣x)和近似后验分布 q(z∣x)之间的 KL 散度。这可以表示为:
利用贝叶斯定理,后验分布 p(z∣x) 可以表示为:
,所以,KL 散度可以写作:
代入后验分布的表达式,得到:
可以分开这个对数:
期望值操作可以分配到每个项:
其中,
是对 x 的常数,所以它对优化过程没有影响,可以忽略。
将
重新组合,得到 ELBO 的表达式:
为了最小化 KL 散度,等价地最大化 ELBO。因为:
优化的目标是最大化 ELBO,从而间接最小化 KL 散度。
那么ELBO 的意义是什么?
KL 散度
始终是非负的。这意味着:
因此,ELBO 是对边际似然 p(x) 的下界。
因为 ELBO 是对
的下界,所以可以通过最大化 ELBO 来间接提高
。通过最大化 ELBO,使得变分分布 q(z|x)更接近真实的后验分布 p(z|x),从而提高了模型的拟合效果。
重点关注ELBO 的两个部分
- 重构误差:
衡量了数据 x 给定潜在变量 z 的重构能力。它表示模型对数据的生成能力。
- 正则化项:
作为正则化项,确保了变分分布 q(z|x) 不偏离先验分布 p(z)过远。
总结来说,ELBO 是变分推断中用于优化变分分布的一个重要工具,它通过提供边际似然的下界来间接优化模型参数。通过最大化 ELBO,可以提高变分分布对真实后验分布的近似程度。
1.3 最终完整的VAE优化目标公式
因此,VAE 的训练目标是最大化 ELBO,等价于最小化以下损失函数。总损失包括两个部分,一个是重建误差,另一个是 KL 散度损失:
为了进行反向传播优化,需要引入重参数化技巧。假设潜在变量 z 的分布是高斯分布,我们可以通过以下重参数化将随机噪声从潜在变量分布中分离出来:
其中
是从标准正态分布
中采样的噪声。这种方式使得梯度可以通过网络传播。
结合上面的公式,VAE 的完整损失函数为:
在实际实现中,损失函数的第一项通常是均方误差(对于连续数据)或交叉熵(对于离散数据),而第二项是标准的 KL 散度。
至此,关于VAE的相关理论公式推导完成。
1.4 示例代码
VAE_model.py
import torch.nn as nn
import torch
class VAEModel(nn.Module):
def __init__(self, num_classes):
super().__init__()
self.num_classes = num_classes
self.encoder_fc = nn.Sequential(
nn.Linear(28*28, 196),
nn.Tanh(),
nn.Linear(196, 48),
nn.Tanh(),
)
self.combine_maps_fc = nn.Sequential(
nn.Linear(48 * (num_classes + 1), 24 * (num_classes + 1)),
nn.Tanh(),
nn.Linear(24 * (num_classes + 1), 48),
nn.Tanh(),
)
self.mean_fc = nn.Sequential(
nn.Linear(48, 16),
nn.Tanh(),
nn.Linear(16, 2)
)
self.log_var_fc = nn.Sequential(
nn.Linear(48, 16),
nn.Tanh(),
nn.Linear(16, 2)
)
self.decoder_fcs = nn.Sequential(
nn.Linear(2, 16),
nn.Tanh(),
nn.Linear(16, 48),
nn.Tanh(),
nn.Linear(48, 196),
nn.Tanh(),
nn.Linear(196, 28*28),
nn.Tanh(),
)
def forward(self, x, label):
# CREATE LABEL CHANNEL MAP
label_ch_map = torch.zeros((x.shape[0], self.num_classes, x.shape[2]))
batch_idx, label_idx = (torch.arange(0, x.size(0)), label[torch.arange(0, x.size(0))])
label_ch_map[batch_idx, label_idx, :] = 1
out = torch.cat([x, label_ch_map], dim=1) #(11, 28x28)
# ENCODE
out = self.encoder_fc(out) # (11, 48)
out = out.reshape((x.shape[0], -1)) # (528,)
combined_out = self.combine_maps_fc(out) #(48,)
mean = self.mean_fc(combined_out) #(2,)
log_var = self.log_var_fc(combined_out) #(2,)
# SAMPLE WITH RE-PARAM TRICK
std = torch.exp(0.5 * log_var)
re_param_noise = torch.rand_like(std)
z = re_param_noise * std + mean # (2,)
# DECODE
decoded = self.decoder_fcs(z)
return mean, log_var, decoded
VAE_model_training.py
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision.datasets as datasets
import torchvision.transforms as transforms
from torch.utils.data import DataLoader
from tqdm import tqdm
import numpy as np
from VAE_model import VAEModel
if __name__ == "__main__":
device = "cuda" if torch.cuda.is_available() else "cpu"
MODEL_SAVE_PATH = "models/vae.pth"
# Load data MNIST
batch_size = 64
transforms = transforms.Compose(
[transforms.ToTensor(), transforms.Normalize(0.5, 0.5)]
)
dataset = datasets.MNIST(root="dataset/", transform=transforms, download=True, train=True)
loader = DataLoader(dataset, batch_size=batch_size, shuffle=True)
# Instantiate model
model = VAEModel(num_classes=10).to(device)
# Training params
num_epochs = 25
optimizer = optim.Adam(model.parameters(), lr=1e-3)
criterion = nn.MSELoss()
recon_losses = []
kl_losses = []
losses = []
# Train
for epoch in range(num_epochs):
for im, label in tqdm(loader):
im = im.view(-1, 1, 28 * 28).float().to(device)
optimizer.zero_grad()
mean, log_var, out = model(im, label)
out = out.reshape(-1, 1, 28 * 28)
kl_loss = torch.mean(0.5 * torch.sum(torch.exp(log_var) + mean**2 - 1 - log_var, dim=-1))
recon_loss = criterion(out, im)
loss = recon_loss + 0.00001 * kl_loss
recon_losses.append(recon_loss.item())
kl_losses.append(kl_loss.item())
losses.append(loss.item())
loss.backward()
optimizer.step()
print(f'Finished Epoch: {epoch + 1} | Recon Loss : {np.mean(recon_losses):4f} | KL Loss : {np.mean(kl_losses):4f}')
print('Done Training')
torch.save(model.state_dict(), MODEL_SAVE_PATH)
VAE_model_predict.py
import torch
import matplotlib.pyplot as plt
from VAE_model import VAEModel
if __name__ == "__main__":
MODEL_LOAD_PATH = "models/vae.pth"
device = "cuda" if torch.cuda.is_available() else "cpu"
model = VAEModel(num_classes=10).to(device)
model.load_state_dict(torch.load(MODEL_LOAD_PATH, map_location=torch.device(device)))
for i in range(10):
noise = torch.randn((1, 1, 28*28)).to(device)
_, _, out = model(noise, (2,))
fake_img = out.reshape(-1, 1, 28, 28).detach()
fake_np = fake_img.squeeze().cpu().numpy()
plt.imshow(fake_np, cmap='gray')
plt.savefig(f'generated_image_{i}.png')
另外关注到ai by hand的项目,通过可视化的流程来展示模型运行,有兴趣可以关注下。
2. 条件随机场(Conditional Random Field,简称CRF)
条件随机场(CRF) 是一种判别式模型,上下文信息或邻居的状态会被用于预测的信息输入。CRF 在命名实体识别、词性标注等问题中都有应用。在介绍CRF之前,首先介绍马尔可夫随机场(MRF)相关的基本数学和术语,CRF 是建立在 MRF 基础上。然后,将详细介绍和解释一个简单的条件随机场模型,说明为什么CRF适合于序列预测问题。之后将讨论该 CRF 模型的似然最大化问题和相关推导。
2.1 马尔可夫随机场MRF及CRF
马尔可夫随机场或马尔可夫网络是一类图形模型,在随机变量之间具有无向图。下图的结构决定了随机变量之间的依赖关系或独立性【7】。
马尔可夫网络由一个图 G = (V, E) 表示,其中顶点或节点表示随机变量,边共同表示这些变量之间的依赖关系。该图可以分解为 J 个不同的团或因子,每个因子由一个因子函数 ϕⱼ 控制,其范围是随机变量 Dⱼ 的子集。对于所有可能的 dⱼ 值,ϕⱼ(dⱼ) 应严格为正。为了将随机变量的子集表示为因子或团,它们都应在图中相互连接。此外,所有团的范围的并集应等于图中存在的所有节点。变量的非归一化联合概率是所有因子函数的乘积,即对于上面显示的具有 V= (A, B, C, D) 的 MRF,联合概率可以写成下述形式,分母是对因子乘积的求和,求和范围是随机变量可能取的所有可能值。它是一个常数,也称为配分函数,通常用 Z 表示。
假设一个马尔可夫随机场,并将其分为两个随机变量集 Y 和 X。条件随机场是马尔可夫随机场的一种特殊情况,其中图满足以下性质:“当全局地对图进行条件化,即当 X 中随机变量的值固定或给定时,集合 Y 中的所有随机变量都遵循马尔可夫性质 p(Yᵤ/X,Yᵥ, u≠v) = p(Yᵤ/X,Yₓ, Yᵤ~Yₓ),其中 Yᵤ~Yₓ 表示 Yᵤ 和 Yₓ 在图中是邻居。”变量的相邻节点或变量也称为该变量的Markov Blanket。下面链式结构图满足上述性质:
由于 CRF 是一种判别式模型,即它对条件概率 P(Y/X) 进行建模,即 X 始终是给定的或观察到的。因此,图最终简化为一个简单的链。
以 X 为条件的 CRF 模型,以 X 为条件,并且尝试去为每个 Xᵢ 找到相应的 Yᵢ,因此 X 和 Y 也分别称为证据变量和标签变量。可以验证上面显示的“因子简化”CRF 模型遵循马尔可夫性质,如变量 Y₂ 所示。如所示,给定所有其他变量的 Y₂ 的条件概率最终仅取决于其相邻节点。满足马尔可夫性质的变量 Y₂,条件仅取决于相邻变量。
更一般形式的CRF【8】如下,不过本文主要讨论CRF在自然语言序列中的应用,比如实体识别等,因此主要讨论线性链结构的CRF。
2.2 线性链条件随机场(Linear-chain CRF)
线性链条件随机场(Linear-chain Conditional Random Field,简称 Linear-CRF)是一种特殊的条件随机场,其结构为线性链状,常用于序列标注任务。
线性链 CRF 的图结构类似于一个线性链,其中每个节点代表一个随机变量,对应于序列中的一个元素(例如,一个词或一个字符)。节点之间的边表示相邻元素之间的依赖关系。
线性链 CRF 的条件概率分布可以表示为:
- Y 表示标签序列,是一个随机变量序列,每个元素对应于输入序列中一个元素的标签。
- X 表示输入序列,是一个观察变量序列。
- Z(X) 是归一化因子,保证所有可能标签序列的概率之和为 1。
是转移特征函数,用于描述相邻标签和当前输入之间的关系。
是状态特征函数,用于描述当前标签和当前输入之间的关系。
和
是特征函数的权重,需要通过训练学习得到。
接下来对训练参数更新的梯度公式进行推导:
了解梯度计算的推导过程对于训练条件随机场(CRF)模型非常重要。下面是详细的梯度计算过程的推导,特别是对于线性链条件随机场(LC-CRF)模型。
2.2.1 对数似然函数
对于一组训练数据,线性链 CRF 模型的对数似然函数为:
条件概率分布为:
归一化因子Z(X)是:
对数似然函数展开为:
2.2.2 梯度计算
需要计算对数似然函数相对于参数 和
的梯度。我们先计算
的梯度。对数似然函数对
的梯度是:
对的导数为:
我们需要计算 。根据链式法则:
因此:
梯度公式变为:
类似地,对的梯度为:
最终梯度公式为:
总结
- 对数似然函数的梯度是基于实际标签序列的特征函数的求和减去模型下的期望值。
- 期望值
和
可以通过前向-后向算法来计算。
2.2.3 期望值计算
为了更好地理解如何通过前向-后向算法计算条件随机场(CRF)模型中的期望值,可以从实际计算的角度进行解释。下面给出一个例子,展示如何使用前向-后向算法计算期望值。
假设有一个简单的线性链条件随机场(LC-CRF)模型,用于标注序列数据。目标是计算特征函数和
的期望值:
前向-后向算法在这里被用于CRF中的隐变量(例如标记序列)在给定观测序列条件下的后验概率的一种动态规划算法。它分为两个步骤:前向步骤和后向步骤。
- 前向算法:计算从序列起始到当前时刻 t 的部分标注序列
的概率。
- 后向算法:计算从当前时刻 t到序列末尾的部分标注序列
的概率。
例子
假设我们有一个观测序列,对应的标记序列
的取值为
。
1. 计算前向概率
前向概率 定义为从开始到时刻 t 为止的部分标记序列
的概率,即:
初始时刻:
对于后续时刻:
2. 计算后向概率
后向概率定义为从时刻 ttt 到序列末尾的部分标记序列
的概率,即:
终止时刻t = T:
对于前面时刻:
3. 计算特征函数的期望值
一旦计算出前向概率和后向概率,就可以计算每个特征函数在所有可能的标记序列下的期望值。以状态特征函数为例:
其中,Z(X)是归一化因子,可以通过前向概率的最终时刻的和得到:
对于转移特征函数:
这里的是特征函数对应的指数项。
总结: 前向-后向算法通过动态规划高效计算每个时刻的部分标记序列的概率,这些概率可以用来计算特征函数的期望值。期望值是梯度下降更新参数的关键步骤。通过前向-后向算法,CRF模型可以利用完整的观测序列信息来推导出特征函数的期望,从而精确优化模型参数。
4. 持续训练
CRF的训练过程本质上是一个通过梯度下降来最大化对数似然的优化过程。通过持续的参数更新,CRF模型可以逐步调整其特征权重,以更好地拟合训练数据。在每次迭代中,利用前向-后向算法高效地计算梯度,从而使得CRF在大规模序列标注任务中依然能够保持良好的训练效率。
使用梯度下降算法或其变种(如随机梯度下降,SGD)来更新参数 和
:
其中,是学习率。
CRF模型的训练过程是一个迭代的过程。每次迭代中:
- 计算当前参数下的损失函数值。
- 利用前向-后向算法计算梯度。
- 使用梯度下降方法更新模型参数。
- 这个过程会持续进行,直到损失函数收敛或达到预定的最大迭代次数
case1: 关于损失值的计算,这里参考【9】中的代码示例,给出比较直观的理解,这里面的逻辑是计算真实路径与预测路径的差异。
假设:
真实标签:
tensor([1, 1, 0, 2, 0])
预测分数:
tensor([[ 0.1074, 0.5337, -0.7819],
[ 0.8806, 0.5112, -0.3205],
[ 0.5401, -2.2218, -0.8034],
[ 0.6645, 0.6061, -1.4834],
[ 1.2066, 0.1034, 0.3215]])
--------------------------------------------------
loss: tensor(6.0321, grad_fn=<SubBackward0>)
tags: [3, 2, 1, 0, 0]
class CRF(nn.Module):
def __init__(self, label_num):
super(CRF, self).__init__()
# 转移矩阵的标签数量
self.label_num = label_num
# [TAG1, TAG2, TAG3...STAR, END]
params = torch.randn(self.label_num + 2, self.label_num + 2)
self.transition_scores = nn.Parameter(params)
# 开始和结束标签
START_TAG, ENG_TAG = self.label_num, self.label_num + 1
self.transition_scores.data[:, START_TAG] = -1000
self.transition_scores.data[ENG_TAG, :] = -1000
# 定义一个较小值用于扩展发射和转移矩阵时填充
self.fill_value = -1000.0
def _log_sum_exp(self, score):
max_score, _ = torch.max(score, dim=0)
max_score_expand = max_score.expand(score.shape)
return max_score + torch.log(torch.sum(torch.exp(score - max_score_expand), dim=0))
def _get_real_path_score(self, emission_score, sequence_label):
# 计算标签的数量
seq_length = len(sequence_label)
# 计算真实路径发射分数
real_emission_score = torch.sum(emission_score[list(range(seq_length)), sequence_label])
# 在真实标签序列前后增加一个 start 和 end
b_id = torch.tensor([self.label_num], dtype=torch.int32, device=device)
e_id = torch.tensor([self.label_num + 1], dtype=torch.int32, device=device)
sequence_label_expand = torch.cat([b_id, sequence_label, e_id])
# 计算真实路径转移分数
pre_tag = sequence_label_expand[list(range(seq_length + 1))]
now_tag = sequence_label_expand[list(range(1, seq_length + 2))]
real_transition_score = torch.sum(self.transition_scores[pre_tag, now_tag])
# 计算真实路径分数
real_path_score = real_emission_score + real_transition_score
return real_path_score
def _expand_emission_matrix(self, emission_score):
# 计算标签的数量
sequence_length = emission_score.shape[0]
# 扩展时会增加 START 和 END 标签,定义该标签的值
b_s = torch.tensor([[self.fill_value] * self.label_num + [0, self.fill_value]], device=device)
e_s = torch.tensor([[self.fill_value] * self.label_num + [self.fill_value, 0]], device=device)
# 扩展发射矩阵为 (self.label_num + 2, self.label_num + 2)
expand_matrix = self.fill_value * torch.ones([sequence_length, 2], dtype=torch.float32, device=device)
emission_score_expand = torch.cat([emission_score, expand_matrix], dim=1)
emission_score_expand = torch.cat([b_s, emission_score_expand, e_s], dim=0)
return emission_score_expand
def _get_total_path_score(self, emission_score):
# 扩展发射分数矩阵
emission_score_expand = self._expand_emission_matrix(emission_score)
# 计算所有路径分数
pre = emission_score_expand[0]
for obs in emission_score_expand[1:]:
# 扩展 pre 维度
pre_expand = pre.reshape(-1, 1).expand([self.label_num + 2, self.label_num + 2])
# 扩展 obs 维度
obs_expand = obs.expand([self.label_num + 2, self.label_num + 2])
# 扩展之后 obs pre 和 self.transition_scores 维度相同
score = obs_expand + pre_expand + self.transition_scores
# 计算对数分数
pre = self._log_sum_exp(score)
return self._log_sum_exp(pre)
def forward(self, emission_scores, sequence_labels):
total_loss = 0.0
for emission_score, sequence_label in zip(emission_scores, sequence_labels):
# 计算真实路径得分
real_path_score = self._get_real_path_score(emission_score, sequence_label)
# 计算所有路径分数
total_path_score = self._get_total_path_score(emission_score)
# 最终损失
finish_loss = total_path_score - real_path_score
total_loss += finish_loss
return total_loss
def predict(self, emission_score):
"""使用维特比算法,结合发射矩阵+转移矩阵计算最优路径"""
# 扩展发射分数矩阵
emission_score_expand = self._expand_emission_matrix(emission_score)
# 计算分数
ids = torch.zeros(1, self.label_num + 2, dtype=torch.long, device=device)
val = torch.zeros(1, self.label_num + 2, device=device)
pre = emission_score_expand[0]
for obs in emission_score_expand[1:]:
# 扩展 pre 维度
pre_expand = pre.reshape(-1, 1).expand([self.label_num + 2, self.label_num + 2])
# 扩展 obs 维度
obs_expand = obs.expand([self.label_num + 2, self.label_num + 2])
# 扩展之后 obs pre 和 self.transition_scores 维度相同
score = obs_expand + pre_expand + self.transition_scores
# 获得当前多分支中最大值的分支索引
value, index = score.max(dim=0)
ids = torch.cat([ids, index.unsqueeze(0)], dim=0)
val = torch.cat([val, value.unsqueeze(0)], dim=0)
# 计算分数
pre = value
# 先取出最后一个的最大值
index = torch.argmax(val[-1])
best_path = [index]
# 再回溯前一个最大值
# 由于为了方便拼接,我们在第一个位置默认填充了0
for i in reversed(ids[1:]):
# 获得分数最大的索引
# index = torch.argmax(v)
# 获得索引对应的标签ID
index = i[index].item()
best_path.append(index)
best_path = best_path[::-1][1:-1]
return best_path
case2:【10】给出了利用skearn中的crf套件,来实现语句标注的任务训练和预测的例子。 为了将一个句子转换为可用作条件随机场(CRF)模型输入的特征序列,您可以定义一个特征函数,从句子中的每个单词中提取相关信息。以下是一个示例特征函数,为句子中的每个单词提取以下特征:
- 单词本身。
- 单词为小写。
- 单词为大写。
- 单词的长度。
- 单词是否包含连字符。
- 单词是否为句子中的第一个单词。
- 单词是否为句子中的最后一个单词。
- 句子中的前一个单词。
- 句子中的下一个单词。
这只是一个示例特征函数,提取的特征需要根据具体用例而有所不同。可以自定义此函数以提取序列标记任务相关的任何特征。
import nltk
import sklearn_crfsuite
from sklearn_crfsuite import metrics
# Load the Penn Treebank corpus
nltk.download('treebank')
corpus = nltk.corpus.treebank.tagged_sents()
print(corpus)
# Define a function to extract features for each word in a sentence
def word_features(sentence, i):
word = sentence[i][0]
features = {
'word': word,
'is_first': i == 0, # if the word is a first word
'is_last': i == len(sentence) - 1, # if the word is a last word
'is_capitalized': word[0].upper() == word[0],
'is_all_caps': word.upper() == word, # word is in uppercase
'is_all_lower': word.lower() == word, # word is in lowercase
# prefix of the word
'prefix-1': word[0],
'prefix-2': word[:2],
'prefix-3': word[:3],
# suffix of the word
'suffix-1': word[-1],
'suffix-2': word[-2:],
'suffix-3': word[-3:],
# extracting previous word
'prev_word': '' if i == 0 else sentence[i - 1][0],
# extracting next word
'next_word': '' if i == len(sentence) - 1 else sentence[i + 1][0],
'has_hyphen': '-' in word, # if word has hypen
'is_numeric': word.isdigit(), # if word is in numeric
'capitals_inside': word[1:].lower() != word[1:]
}
return features
# Extract features for each sentence in the corpus
X = []
y = []
for sentence in corpus:
X_sentence = []
y_sentence = []
for i in range(len(sentence)):
X_sentence.append(word_features(sentence, i))
y_sentence.append(sentence[i][1])
X.append(X_sentence)
y.append(y_sentence)
# Split the data into training and testing sets
split = int(0.8 * len(X))
X_train = X[:split]
y_train = y[:split]
X_test = X[split:]
y_test = y[split:]
# Train a CRF model on the training data
crf = sklearn_crfsuite.CRF(
algorithm='lbfgs',
c1=0.1,
c2=0.1,
max_iterations=100,
all_possible_transitions=True
)
crf.fit(X_train, y_train)
# Make predictions on the test data and evaluate the performance
y_pred = crf.predict(X_test)
print(metrics.flat_accuracy_score(y_test, y_pred))
case3: 也可以使用pycrfsuite.Trainer()。
import pycrfsuite
# Train a CRF model suing pysrfsuite
trainer = pycrfsuite.Trainer(verbose=False)
for x, y in zip(X_train, y_train):
trainer.append(x, y)
trainer.set_params({
'c1': 1.0,
'c2': 1e-3,
'max_iterations': 50,
'feature.possible_transitions': True
})
trainer.train('pos.crfsuite')
# Tag a new sentence
tagger = pycrfsuite.Tagger()
tagger.open('pos.crfsuite')
sentence = 'Yuanquan\'s square is a best platform for students.'.split()
features = [word_features(sentence, i) for i in range(len(sentence))]
tags = tagger.tag(features)
print(list(zip(sentence, tags)))
case4: 之前的项目中,我们是利用crf++来实现标注,可以参考【10】。
case5: 后续逐步发展出了Bi-Lstm-CRF【11, 12】。
综上所述,以VAE和CRF为例,对比了生成式模型与判别式模型的建模差异。VAE是一种生成式模型,试图对数据的生成过程进行建模,学习数据的概率分布。VAE通过学习数据的潜在表示来生成新的数据样本。CRF是一种判别式模型,它直接对条件概率进行建模,关注的是给定输入情况下输出的条件分布。生成式模型关注数据的生成过程,判别式模型关注在给定输入情况下输出的条件分布。生成式模型常用于生成新数据样本,学习数据的潜在结构;判别式模型常用于分类、标注等任务,关注输入与输出之间的映射关系。
3. 参考材料
【1】Tutorial on Variational Autoencoders
【2】Variational Autoencoder in TensorFlow
【3】Understanding Variational Autoencoders (VAEs)
【4】Tutorial - What is a variational autoencoder?
【5】The Art of Encoding: Building a Variational AutoEncoder for MNIST Digits
【6】Variational AutoEncoders (VAE) with PyTorch
【7】Conditional Random Fields Explained
【8】An Introduction to Conditional Random Fields for Relational Learning
【9】CRF 层详细实现
【10】CRF++
【11】Huang, Zhiheng, Wei Xu, and Kai Yu. "Bidirectional LSTM-CRF models for sequence tagging." arXiv preprint arXiv:1508.01991 (2015).
【12】Panchendrarajan, Rrubaa, and Aravindh Amaresan. "Bidirectional LSTM-CRF for named entity recognition." 32nd Pacific Asia Conference on Language, Information and Computation, 2018.