NER训练相关点回顾

机器学习三要素:

序列标注问题

朴素的当作升维分类问题处理, 输入输出会维度爆炸, 训练样本不足, 因此需做一些(局部)结构性假设.

文本序列标注问题

目前业务NER场景: 营销词提取, 品牌, 品类, 卖点, 地点, …

主要难点, 词的二义性, 时序的复杂依赖关系

没有二义性的词/场景, 最糙做法, 直接关键词召回简单, 不过需要考虑断句问题, 如关键词审核场景

基础知识回顾

MM / MC / 马尔可夫假设/链

假设:

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满足一些条件

平稳分布求解办法

应用: 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)

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

Viterbi算法

动态规划求解最大条件概率序列的方法

递归定义: t时刻状态为S的最大概率可由t-1时刻各状态最大概率计算得到, 并记录来自何种状态

算法复杂度: 时间O(T*S**2), 空间O(T*S)

对比穷举直接计算: 时间O(S**T), 空间O(T)

步长T步长可控的时候, 其实穷举计算也不是不行, 如短文本标注

前向-后向算法思路, 类比:

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)

对比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)

例子:

计算时用对数概率空间(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模型过于简单, 单个字独立蹦出来, 忽略了语句的上下文信息, 丢掉了位置信息

MEMM / 最大熵马尔科夫模型

对状态迁移做MC假设 (NOTE HMM对状态到表达做MC假设P(st|x)=P(st|xt)

则需学习的概率函数P(st|st-1,t,x)为LLP形式. 说明:

(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假设做在概率函数层面还是特征函数里面 ???

TODO need improvement

辨别模型 VS 生成模型

标注方式

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

(O+2*N)的分类/状态跳转问题 (N实体类目数) IOB方式下, 存在不可能出现的序列跳转

标注缺点: 不支持嵌套标注, 例:

标注方式对NER训练的影响

Tokenize对NER训练的影响

处理办法:

  1. 丢弃
  2. 根据标号调整Tokenizer
  3. 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

  1. 表征层选择
  1. 抽取层
  1. 输出层

Attention in BERT 是否可以替代RNN层的作用?

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

辨别式模型

也是序列标号问题, 区别在于, 输入序列边界不清晰.

https://distill.pub/2017/ctc/

HOME