年初的时候做了几个市场的切分模型, 年末终于有时间来回顾一下了. 当时考虑实现一个双指针的写法, 但是进度比较着急, 没来得及改好, 只做了一个分层的实现. 年末比较有时间, 复盘一下项目.
首先, global pointer这个做法是苏剑林大佬提出的. GlobalPointer:用统一的方式处理嵌套和非嵌套NER
基本思想是这样的: 我们假设一个序列长度为n, 那么我们最多可以获取到n*(n+1) // 2个substring, 每一个subs都会做一个打分, 然后看哪些类型的打分可以超出阈值透出结果.
数学形式
我们假设序列输入进来之后有n个token,
我们encoding之后得到[h1,h2,...hn]
简单做一下变换
$ q_{i,\alpha} = W_{q,\alpha}h_i + b_{q,\alpha} $ 以及
$ k_{i,\alpha} = W_{k,\alpha}h_i + b_{k,\alpha} $
然后我们可以得到
$ [\boldsymbol{q}_{1,\alpha},\boldsymbol{q}_{2,\alpha},\cdots,\boldsymbol{q}_{n,\alpha}] $
和
$ [\boldsymbol{k}_{1,\alpha},\boldsymbol{k}_{2,\alpha},\cdots,\boldsymbol{k}_{n,\alpha}] $
然后我们再定义
$ \begin{equation}s_{\alpha}(i,j) = \boldsymbol{q}_{i,\alpha}^{\top}\boldsymbol{k}_{j,\alpha}\label{eq:s}\end{equation} $
本质就是一个内积作为打分. 不过苏佬实验显示, 这样的形式还是不够, 这个公式没有显示包含相对位置信息, 于是一个实验是给global pointer加上RoPE
$ \begin{equation}s_{\alpha}(i,j) = (\boldsymbol{\mathcal{R}}_i\boldsymbol{q}_{i,\alpha})^{\top}(\boldsymbol{\mathcal{R}}_j\boldsymbol{k}_{j,\alpha}) = \boldsymbol{q}_{i,\alpha}^{\top} \boldsymbol{\mathcal{R}}_i^{\top}\boldsymbol{\mathcal{R}}_j\boldsymbol{k}_{j,\alpha} = \boldsymbol{q}_{i,\alpha}^{\top} \boldsymbol{\mathcal{R}}_{j-i}\boldsymbol{k}_{j,\alpha}\end{equation} $
损失函数
我们设计好了打分系统, 正常来说, 我们后面应该就是一个n*(n+1)//2个的多标签分类问题, 区别于一般的多分类, 这里我们每个subs可能会对应多个标签.
那么一个朴素的想法就是把问题转化成k个2分类的问题, 但是这又有问题了, 我们每次可以获得n * (n+1) / 2个subs, 然而里面实际存在的标签数量非常有限, 这又是一个典型的类别不均衡的问题.
这里又有另一篇文章: 将“Softmax+交叉熵”推广到多标签分类问题
我们从单标签分类的交叉熵出发:
\[ \begin{equation}-\log \frac{e^{s_t}}{\sum\limits_{i=1}^n e^{s_i}}=-\log \frac{1}{\sum\limits_{i=1}^n e^{s_i-s_t}}=\log \sum\limits_{i=1}^n e^{s_i-s_t}=\log \left(1 + \sum\limits_{i=1,i\neq t}^n e^{s_i-s_t}\right)\end{equation} \]
这里logsumexp其实是max的光滑近似, 其类似于我们把所有打分都和目标类别的打分做差, 然后我们期望所有其他的s_x - s_target都应该小于0.
\[\begin{equation}\log \left(1 + \sum\limits_{i=1,i\neq t}^n e^{s_i-s_t}\right)\approx \max\begin{pmatrix}0 \\ s_1 - s_t \\ \vdots \\ s_{t-1} - s_t \\ s_{t+1} - s_t \\ \vdots \\ s_n - s_t\end{pmatrix}\end{equation}\]
那么如果是多标签分类, 我们合理的期望应该是每一个目标类别的打分都应该大于非目标类别的打分
所以类似的我们就有公式
\[ \begin{equation}\log \left(1 + \sum\limits_{i\in\Omega_{neg},j\in\Omega_{pos}} e^{s_i-s_j}\right)=\log \left(1 + \sum\limits_{i\in\Omega_{neg}} e^{s_i}\sum\limits_{j\in\Omega_{pos}} e^{-s_j}\right)\label{eq:unified}\end{equation} \]
如果我们引入一个0类, 我们希望target类打分都大于0类, 其他类别小于0类, 那么我们就有推导:
\[ \begin{equation}\begin{aligned} &\log \left(1 + \sum\limits_{i\in\Omega_{neg},j\in\Omega_{pos}} e^{s_i-s_j}+\sum\limits_{i\in\Omega_{neg}} e^{s_i-s_0}+\sum\limits_{j\in\Omega_{pos}} e^{s_0-s_j}\right)\\ =&\log \left(e^{s_0} + \sum\limits_{i\in\Omega_{neg}} e^{s_i}\right) + \log \left(e^{-s_0} + \sum\limits_{j\in\Omega_{pos}} e^{-s_j}\right)\\ \end{aligned}\end{equation} \] 如果我们指定0类的打分为0, 那么我们就有
\[ \begin{equation}\log \left(1 + \sum\limits_{i\in\Omega_{neg}} e^{s_i}\right) + \log \left(1 + \sum\limits_{j\in\Omega_{pos}} e^{-s_j}\right)\label{eq:final}\end{equation} \] 这就是最终的打分形式, 其优化点就是我们并不是把多标签分类转化成了多个二分类的问题, 而是转化成了类似排序的过程(目标类应该大于非目标类)
最后
理论大概就是上面这些.
基本实现形式是这样, 代码实现苏神也已经整理到了bert4keras里面了
我这边实现的时候因为底座想用mdeberta-v3, 用ai重写了一下代码, 初步看下来, 似乎rope的作用没有太明显, 损失函数倒是比较有用, 跑了几组实验, 后续出结果了再梳理一下.