机器学习三要素:
- 模型=假设
- 优化目标 / 度量
- 算法
序列标注问题
- 监督学习基本问题: 分类/序列/回归
- 最基本任务: 分类
- 扩展任务: 分类 + 图结构
- 最基本图结构: 序列
- 序列标注: sequence labeling / tagging
- 输出 = 分类^确定性时间步长
- segmentation task for 1D data
朴素的当作升维分类问题处理, 输入输出会维度爆炸, 训练样本不足, 因此需做一些(局部)结构性假设.
文本序列标注问题
- SEG / 分词, 断句 (不用tokenizer避免和后面混淆)
- POS / Part of Speech / 词性标注
- NER / Named Entity Recognition / 命名实体识别 (时间/地点/人物/…)
- Chunking / 组块 ???
目前业务NER场景: 营销词提取, 品牌, 品类, 卖点, 地点, …
主要难点, 词的二义性, 时序的复杂依赖关系
- 广州市/长隆乐园/欢迎你
- 广州/市长/隆乐园/欢迎你
- 安利/V一个/安利/B产品
- 穿/回力/B1鞋/P2玩/V回力/B2车/P1
没有二义性的词/场景, 最糙做法, 直接关键词召回简单, 不过需要考虑断句问题, 如关键词审核场景
基础知识回顾
MM / MC / 马尔可夫假设/链
- S: 状态空间/变量
- B: 状态转移概率矩阵
- T: 时间/步数
假设:
- 时间齐次: 状态变化概率时间/步数无关
- 一阶马尔可夫性: 下一步状态只和当前状态有关
import numpy as np
import pandas as pd
Sn = ("Healthy", "Fever")
nS = len(Sn)
# 状态转移概率矩阵
B = np.array([[.8,.2],[.4,.6]])
pd.DataFrame(B, Sn, Sn)
Healthy | Fever | |
---|---|---|
Healthy | 0.8 | 0.2 |
Fever | 0.4 | 0.6 |
平稳分布
状态边缘分布 / 先验概率 / …
p@B = p
是B特征值为1的对应的规范化特征向量 (概率矩阵特征值为: 1>=l1>…>0)
NOTE: 稳态分布不一定存在或者唯一, 需要转移概率举证B满足一些条件
平稳分布求解办法
- 特征值解析解 (维度高时不适用)
- Power Method / 幂法
- MCMC: Markov Chain Monte Carlo / 一种通用概率计算方法
应用: PageRank 网页评分
# 状态边缘分布 / 先验 / 稳态分布
# stationary distribution
ps = np.array([2/3,1/3])
assert (ps@B == ps).all()
# 解析解
w, v = np.linalg.eig(B.T)
print(w, v)
print(v[:,0] / v[:,0].sum())
# 幂法
print(np.linalg.matrix_power(B, 100))
[1. 0.4] [[ 0.89442719 -0.70710678]
[ 0.4472136 0.70710678]]
[0.66666667 0.33333333]
[[0.66666667 0.33333333]
[0.66666667 0.33333333]]
# MC simulation 多样本随机游走
T=100 # 总步长
T0=20 # 热身步长
N=500 # 样本数
# NOTE 样本独立性假设, 如果考虑样本交互则为传染模型
s = np.zeros((T, N), dtype=int)
s[0] = np.random.choice(nS, N)
cache = [[] for j in range(nS)]
for t in range(1, T):
for j in range(nS):
idx = (s[t-1] == j)
n = sum(idx)
if len(cache[j]) < n:
cache[j] = np.random.choice(nS, N, p=B[j])
s[t, idx] = cache[j][:n]
cache[j] = cache[j][n:]
print("单样本时间均值", np.bincount(s[T0:, 0])/len(s[T0:, 0]))
print("总样本空间均值", np.bincount(s[T-1])/N)
ss = s[T0:, :].reshape(-1)
print("总时间空间均值", np.bincount(ss)/len(ss))
单样本时间均值 [0.7125 0.2875]
总样本空间均值 [0.676 0.324]
总时间空间均值 [0.667275 0.332725]
HMM (Hidden Markov Model)
- S: 状态, 马可夫链, 隐变量(Hidden), 不可观测
- X: 表现, 可观测
P(X|S)
满足马尔可夫性 (和时间无关, 和当前状态有关)- A: 表现概率矩阵
Xn = ("normal", "cold", "dizzy")
nX = len(Xn)
A = np.array([[.5,.4,.1],[.1,.3,.6]])
pd.DataFrame(A, Sn, Xn)
normal | cold | dizzy | |
---|---|---|---|
Healthy | 0.5 | 0.4 | 0.1 |
Fever | 0.1 | 0.3 | 0.6 |
贝叶斯: P(S|X) = P(X|S) * P(S) / P(X)
# P(状态|表现) = P(表现|状态) * P(状态) / P(表现)
px = ps@A # 表现边缘分布
C = ((A.T * ps).T/px).T
pd.DataFrame(C, Xn, Sn)
# TODO simulation
Healthy | Fever | |
---|---|---|
normal | 0.909091 | 0.090909 |
cold | 0.727273 | 0.272727 |
dizzy | 0.250000 | 0.750000 |
- HMM学习: 估计参数A及B (也可能需要估计初始状态分布参数)
- Baum-Welch算法 / EM
- HMM推断: 给定表现序列X, 求条件概率最大状态序列:
argmax P(S|X)
Viterbi算法
动态规划求解最大条件概率序列的方法
递归定义: t时刻状态为S的最大概率可由t-1时刻各状态最大概率计算得到, 并记录来自何种状态
算法复杂度: 时间O(T*S**2)
, 空间O(T*S)
对比穷举直接计算: 时间O(S**T)
, 空间O(T)
步长T步长可控的时候, 其实穷举计算也不是不行, 如短文本标注
前向-后向算法思路, 类比:
- A*寻路算法: 记录最短的累计距离及来源节点
- 计算图前向后向梯度计算权重更新过程
- …
def brute_force(X, prior, B, A):
import itertools
paths = list(itertools.product(*[list(range(len(prior))) for _ in range(len(X))]))
probs = np.zeros(len(paths))
# TODO better implementation
for i, path in enumerate(paths):
p = 1
for t, (s, x) in enumerate(zip(path, X)):
p0 = A[s][x]
p *= p0
p1 = prior[s] if t == 0 else B[path[t-1], s]
p *= p1
probs[i] = p
# print(list(zip(probs, paths)))
return np.max(probs), paths[np.argmax(probs)]
def viterbi_np(x, prior, B, A):
# prior 先验状态分布
P = np.zeros((len(x), len(prior), ))
I = np.zeros((len(x), len(prior)), dtype=int)
P[0] = prior*A[:,x[0]]
for i in range(1, len(x)):
prob = P[i-1]
b = A[:,x[i]]
score = prob * B.T * b.reshape(-1,1)
P[i] = score.max(1)
I[i] = score.argmax(1)
path = np.zeros(len(x), dtype=int)
path[-1] = P[-1].argmax()
for i in range(len(x)-1, 1, -1):
path[i-1] = I[i, path[i]]
print(pd.DataFrame(P.T))
print(pd.DataFrame(I.T))
return P[-1].max(), tuple(path)
# 观测序列
X = np.array([0,1,2,2,1,2,0])
# 初始状态概率
prior = np.array([.5,.5])
import time
t0 = time.time()
r1 = brute_force(X, prior, B, A)
t1 = time.time()
r2 = viterbi_np(X, prior, B, A)
t2 = time.time()
print(t1-t0, r1)
print(t2-t1, r2)
0 1 2 3 4 5 6
0 0.25 0.080 0.0064 0.000512 0.000553 0.000044 0.000045
1 0.05 0.015 0.0096 0.003456 0.000622 0.000224 0.000013
0 1 2 3 4 5 6
0 0 0 0 0 1 0 1
1 0 0 0 1 1 1 1
0.0 (4.478976000000001e-05, (0, 0, 1, 1, 1, 1, 0))
0.0 (4.478976000000001e-05, (0, 0, 1, 1, 1, 1, 0))
对数线性模型 / log linear model
P(y|w,x) = exp(w*f(x,y)) / sum(exp(w*f(x,yy) for yy in Y)
w
: 特征向量(学习参数)f(x, y)
: 特征函数(模型假设)
对比DNN接softmax分类头的模型结构: 这里f没有可学习参数, f和w没有更深层的交互, DNN可以逼近任意函数表达.
学习: 极大似然函数及梯度计算相对比较简单, 严格凸问题, 一定有最优解
LLP(w) = sum(w*f(x,y)) - log(sum(exp(w*f(x,yy) for yy in Y)) for x y in samples)
例子:
- 二元逻辑分类 / binomial logistic:
f(x, y) = x if y else 0
- 多元逻辑分类 / multinomial logistic: k*(h-1)维空间稀疏映射 (样本维度k, 分类数h)
计算时用对数概率空间(log_prob)计算, 用log-sum-exp保障数值稳定性
和求解最大熵模型MEMM等价: 对偶问题, f
观测样本约束函数, w
松弛变量, 由于严格凸, 因此等价.
def llp(Y, w, f):
"""对数线性模型: w 学习参数, f 特征函数"""
return lambda y, x: np.exp(w.dot(f(x, y))) / sum(np.exp(w.dot(f(x, yy))) for yy in Y)
Y = {0,1,2} # 分类器
h = 5 # X维度
d = (len(Y)-1) * h # 映射空间维度
w = np.random.rand(d)
def f_ml(x, y):
r = np.zeros(d)
if y:
r[(y-1)*h:y*h] = x
return r
p = llp(Y, w, f_ml)
xs = np.random.rand(len(Y), h)
pd.DataFrame([[round(p(y, x), 2) for y in Y] for x in xs])
0 | 1 | 2 | |
---|---|---|---|
0 | 0.08 | 0.61 | 0.30 |
1 | 0.10 | 0.47 | 0.43 |
2 | 0.12 | 0.59 | 0.29 |
HMM / MEMM / CRF
对于序列标注问题, s是T维向量
- HMM:
P(s|x) = P(s0|x0)*P(s1|s0)*P(s1|x1)...
- MEMM:
P(s|x) = ...*P(si|si-1,x)*...
- CRF:
P(s|x) = ...*P(si|si-1,si+1,x)*...
HMM模型过于简单, 单个字独立蹦出来, 忽略了语句的上下文信息, 丢掉了位置信息
MEMM / 最大熵马尔科夫模型
对状态迁移做MC假设 (NOTE HMM对状态到表达做MC假设P(st|x)=P(st|xt)
则需学习的概率函数P(st|st-1,t,x)
为LLP形式. 说明:
- 一次看到整个句子信息x
- 类比: Transformer的Attention结构
- 如做了时间齐次假设, 则不需要t位置信息变量
(Linear Chain) CRF / (线性链) 条件随机场
一般是概率图, 线性链 = 序列 / 链表
同MEMM, 但是状态双向依赖
f(s,x)
整个状态空间和表征空间的交互特征函数, 维度过大
CRF假设:
MC特性f
, 即可拆解为相邻状态函数和, 全局特征等于局部特征之和: f(s,x) = sum(g(st,st-1,t,x) for t in 0...T)
齐次假设, 位置无关: f(s,x) = sum(g(st,st-1,x) for t in 0...T)
进一步g和x无关: f(s,x) = sum(g(st,st-1) + h(st|x) for t in 0...T)
, g转移特征, h状态特征
则g是个需要学习的状态转移矩阵而已
HMM vs CRF: 状态拆解的MC假设做在概率函数层面还是特征函数里面 ???
- Naive Bayes + Sequence = HMM
- Logistic Regression + Sequence = CRF
TODO need improvement
辨别模型 VS 生成模型
- 生成模型 ~
p(y,x)
- HMM
- GPT
- 自回归:
p(y1,y2,...,yn) = p(yn|y1,...,yn-1)...p(y1)
- 辨别模型 ~
p(y|x)
- MEMM/CRF
- BERT
- 生成模型自然包含辨别模型:
p(y|x)=p(y,x)/p(x) = p(y,x)/sum(p(yy,x) for yy in Y)
标注方式
- I / 内部
- B / 开始
- O / 外部
- E / 结束
- IOE -> IOB 对应, 标开头还是结束的区别
词 | IO | IOB1 | IOB2 | IOE1 | IOE2 | |
---|---|---|---|---|---|---|
狮王 | I1 | I1 | B1 | E1 | I1 | E1 |
齿力佳 | I1 | B1 | B1 | I1 | E1 | |
牙膏 | I2 | I2 | B2 | I2 | E2 | |
热卖 | O | O | O | O | O | O |
字 | IO | IOB1 | IOB2 |
---|---|---|---|
狮 | I1 | I1 | B1 |
王 | I1 | I1 | I1 |
齿 | I1 | B1 | B1 |
力 | I1 | I1 | I1 |
佳 | I1 | I1 | I1 |
牙 | I2 | I2 | B2 |
膏 | I2 | I2 | I2 |
热 | O | O | O |
卖 | O | O | O |
- 没有连续同类型实体情况下, IO = IOB1
- 业务逻辑上, 连续同类型实体是否合理?
- “狮王齿力佳”品牌? 还是 “狮王”及”齿力佳”品牌?
- 广州天河? 还是广州/天河?
(O+2*N)的分类/状态跳转问题 (N实体类目数) IOB方式下, 存在不可能出现的序列跳转
标注缺点: 不支持嵌套标注, 例:
- “广州大学”: “广州”(地名) / “广州大学”(机构名)
- 耐克Nike旗舰店: “耐克Nike”/”耐克”/”Nike”均需标为品牌词
标注方式对NER训练的影响
- IOB1 / IOB2 后者标号更为均匀, 相对更好学习 (TBC)
- IOB存在不可能跳转之序列: B-1 -> I-2, 这些状态间概率一定为0, 不需要学习! IO标号形式下, 少一半状态, 训练收敛更快
Tokenize对NER训练的影响
- 单字编码, 不存在问题
- 分词编码: 标号边界和分词边界不一致的处理
处理办法:
- 丢弃
- 根据标号调整Tokenizer
- Best effort保留标注信息
截断对NER训练的影响
文本训练数据需要补全(pad)或者截断(truncate)到定长计算
被截断的标号, 丢弃还是保留? 最后一个NER标号当作没看见, 还是要丢弃最后NER标号的字?
联合任务对于NER训练的影响
同时训练文本分类, NER标注, 等任务是否/如何提高结果
文本序列训练一般流程
text -> tokenize -> one-hot encoding -> backbone -> distributed encoding (body) -> rnn (neck) -> decoding (head)
NER训练网络结构 / BERT-BiLSTEM-CRF
- 表征层选择
- one-hot: NONONO
- word2vec: 静态词嵌入, 不考虑上下文
- BERT: 上下文感知
- 抽取层
- 双向RNN: 谁还用单向
- RNN和backbone的Attention是否有重叠?
- 输出层
- CLS (即直接Softmax)
- CRF / HMM / …
Attention in BERT 是否可以替代RNN层的作用?
- RNN (LSTM): 严格按照序列顺序传递, 相对于注意力机制有更强的结构假设
- Attention解码器: 一次性看到全部文本, 更加丰富的交互留足想象空间
- 缺点: 输入有上限, 相比RNN计算复杂度相对较高: TODO O vs
CRF层导出ONNX
习惯上无可训练参数的模块不做到网络里面, 如, loss, 后处理流程等.
CRF层本身是有学习参数的, 内化到网络结构中更加合理. 此外, 编码过程内化到网络中, 也方便直接导出ONNX, 避免单独维护两套decode方法.
def forward(self, tokens, tags=None):
if tags is not None:
return self.decode(tokens)
CTC / Connectionist Temporal Classification
辨别式模型
也是序列标号问题, 区别在于, 输入序列边界不清晰.
- 文字序列标注:
txt -> num_words * num_labels -> num_words
- OCR文字识别场景:
img -> CNN -> seq * num_chars -> num_words
- 这里seq是CNN抽特征后的序列长度 » num_words
https://distill.pub/2017/ctc/