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.6375 0.3625]
总样本空间均值 [0.698 0.302]
总时间空间均值 [0.66995 0.33005]

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(S)

# 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) VS 穷举直接计算: 时间O(S**T), 空间O(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.0010280609130859375 (4.478976000000001e-05, (0, 0, 1, 1, 1, 1, 0))
0.0029993057250976562 (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没有可学习参数

学习: 极大似然函数及梯度计算相对比较简单, 严格凸问题, 一定有最优解

LLH(w) = sum(w*f(x,y)) - log(sum(exp(w*f(x,yy) for yy in Y)) for x y in samples)

例子:

最大熵模型MEMM的对偶问题: f观测样本约束函数, w松弛变量, 由于严格凸, 因此是等价问题.

计算时用对数概率空间(log_prob)计算, 用log-sum-exp保障数值稳定性

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)
# f_bl = lambda x, y: x if y else np.zeros(x.shape)
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.31
1 0.15 0.34 0.51
2 0.12 0.57 0.32

HMM / MEMM / CRF

对于序列标注问题, s是T维向量

HMM模型过于简单, 单个字独立蹦出来, 忽略了语句的上下文信息, 丢掉了位置信息

MEMM / 最大熵马尔科夫模型

只对状态做MC假设的模型, 需学习的概率函数P(st|st-1,t,x)为LLP形式, 说明:

(linear chain) CRF / (线性链) 条件随机场

同MEMM, 但是状态双向依赖

f(s,x)整个状态空间和表征空间的交互特征函数, 维度过大

假设可拆解: f(s,x) = sum(g(st,st-1,t,x) for t in 0...T)

MEMM vs CRF: 状态拆解的MC假设做在概率函数层面还是特征函数里面

辨别模型 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

确定性的压缩算法

文本数据中提取独热向量的预训练过程, 输出编码/解码器. 对比训练过程: 迭代的找寻符合任务目标的的文本分布式表示的过程

subword编码

https://huggingface.co/docs/transformers/tokenizer_summary

https://huggingface.co/course/chapter6/1

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