Dawn's Blogs

分享技术 记录成长

0%

Adjacency List Oriented Relational Fact Extraction via Adaptive Multi-task Learning

来源:ACL(finding) 2021

作者:Fubang Zhao, Zhuoren Jiang, Yangyang Kang, Changlong Sun, Xiaozhong Liu

机构:Alibaba Group, Zhejiang University

motivation:论文提出所有的关系抽取模型都可以根据图的角度来组织,并且提出了一种邻接表视角的关系抽取模型 DIRECT(aDjacency LIst ORiented rElational faCT)。为了解决子任务错误的传播和子任务失衡问题,采用了一种新的自适应多任务学习策略和动态子任务损失平衡方法。、

GitHub:https://github.com/fyubang/direct-ie

关系抽取可以被描述为一个有向图的构造任务,实体为节点,关系为边。有三种方法表示图:

  • Edge List
  • Adjacency Matrix
  • Adjacency List

image-20230301092155201

DIRECT 框架

DIRECT 框架其中包括一个共享的BERT编码器和三个输出层:subject 提取、object 提取和关系分类

  • 首先将输入的句子输入到 subject 提取模块中提取所有的 subject;
  • 然后将每个提取的 subject 与句子连接,输入 object 提取模块,提取所有object ,可以形成一组 subject-object pair;
  • 最后,将 subject object pair 与句子连接,输入关系分类模块,得到它们之间的关系。

为了平衡子任务损失的权值,提高全局任务性能,三个模块共享 BERT 编码器层,并采用自适应多任务学习策略进行训练。

image-20230301094709905

阅读全文 »

SEPC: Improving Joint Extraction of Entities and Relations by Strengthening Entity Pairs Connection

来源:KDD 2021

作者:Jiapeng Zhao, Panpan Zhang, Tingwen Liu

机构:Institute of Information Engineering Chinese Academy of Sciences, Beijing

motivation:而现有的方法通常只通过共享 encoder 来建模实体对,这不足以利用实体对的内在联系,而且无法纠正 subject 识别错误的情况。为了解决这一问题,论文提出利用实体对的对偶性来加强实体对连接(strengthen entity pairs connection,SEPC),将关系抽取任务转化为不仅从 subject 到 object 的映射、而且从 object 到 subject 的映射

GitHub: https://github.com/zjp9574/SEPC

方法

Entity Tagger

利用四个二分类器来标注 subject 和 object 的 start 和 end 位置

Entity Pair Recognizer:Strengthening Entity Pairs Connection

Entity Pair Recognizer 旨在加强 subject 和 object 之间的内在联系,也就是说它最大化如下 3 式的可能性(最大化 3 式可能性就是同时最大化 4(subject 到 object)和 5(object 到 subject)的可能性):

image-20230228171624761

在此部分中,SEPC 包括四个映射:subject to object start position,subject to the object end position,object to subject start position,object to subject end position。

这四个映射为相同的网络结构,以 subject to object start position 为例进行解释。整个网络结构包含三个部分:

  • generation network
  • similarity network
  • backward network

image-20230228171937061

阅读全文 »

Continual Few-shot Relation Learning via Embedding Space Regularization

来源:ACL 2022

作者:Chenwei Qin, Shafiq Joty

机构:Nanyang Technological University

motivation:现有的持续关系学习(Continual Relation Learning,CRL)方法依赖于大量的标记训练数据来学习新的任务,这在实际场景中很难获得,因为获取大的、具有代表性的标记数据往往昂贵且耗时。因此,模型有必要在很少的标记数据下学习新的关系模式,同时避免对先前任务知识的灾难性遗忘(在论文中,这个场景被称为 CFRL(Continual Few-shot Relation Learning)。因此,论文提出了 ERDA,一个基于 embedding space regularization 和 data augmentation 的 CFRL 方法。

GitHub:https://github.com/qcwthu/Continual_Fewshot_Relation_Learning

在持续学习中,避免灾难性遗忘的方法:

  • regularization-based methods:对神经权值的更新施加约束,使得以前的任务很重要,以减轻灾难性遗忘。
  • architecture-based methods:动态地改变模型架构,以获取新的信息,同时记住以前的知识。
  • memory-based methods:将以前的任务中的几个关键例子保存到记忆中,并在学习新任务时重放。

其中 memory-based 的效果最好,但是在 memory-based continual learning 中,这些方法的一个主要限制是,他们都假设大量的训练数据学习新关系,这在实际场景中是非常昂贵的。如果新的训练数据很少,那么现有的方法就会出现过拟合

模型方法

问题描述

CFRL 假设除了第一个任务有足够的数据进行训练外,后续的新任务都是 few-shot 的。CFRL 的问题设置与真实场景一致,在真实场景中,我们通常有足够的数据来处理现有的任务,但当新任务出现时,只有少数标记数据。

假设在后续的任务中,每一个 few-shot 任务的关系数量为 N,每个关系的样本数量为 K,那么称为 N-way K-shot continual learning。

为了避免灾难性以往,一个存储一些之前任务的关键样本在 memory M = {M1, M2, ...} 在学习期间被维护。由于对任务的数量没有限制,所以 Mk 被限制为很小。在 CFRL 设置中,每个关系只允许将一个样本保存在 memory 中

总体框架

在每一个时间步,根据任务是否是 few-shot,过程分别有四个或三个工作模块。对于一般的学习任务(general learning process)有三个步骤,适用于所有的任务。如果这个任务是一个 few-shot 的任务(k > 1),我们应用一个额外的步骤来创建一个增强的训练集。

image-20230224162137253

image-20230224114237872

阅读全文 »

反向代理

反向代理代理的是服务器,所以用户是无感知的。用户发起请求后,由 Nginx 服务器分发请求到具体的服务器中

1
2
3
4
5
6
7
8
9
10
11
12
server {
listen 9090;
server_name _;

location /video/ {
proxy_pass http://XXX:8080;
}

location /book/ {
proxy_pass http://XXX:8081;
}
}

负载均衡

Nginx 负载均衡需要配置 upstream 组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
upstream server_pool {
server xxx:80 weight=4;
server yyy:80 weight=2;
}

server {
listen 80;
server_name _;

location / {
root html;
proxy_pass http://server_poll; # 负载均衡
index index.html index.htm;
}
}

负载均衡策略

第一种 轮询

每个请求按时间顺序逐一分配到不同的后端服务器。

第二种 weight

weight 代表权重默认为 1,权重越高被分配的客户端越多。

1
2
3
4
upstream server_pool {
server xxx:80 weight=4;
server yyy:80 weight=2;
}

第三种 ip_hash

每个请求按访问 ip 的 hash 结果分配,这样每个访客固定访问一个后端服务器。

1
2
3
4
5
upstream server_pool {
iphash;
server xxx:80;
server yyy:80;
}

第四种 第三方配置

如 fair 就是第三方配置:按后端服务器的响应时间来分配请求,响应时间短的优先分配。

1
2
3
4
5
upstream server_pool {
server xxx:80;
server yyy:80;
fair;
}

动静分离

动静分离就是把把动态跟静态请求分开。

动静分离从目前实现角度来讲大致分为两种:

  • 一种是纯粹把静态文件独立成单独的域名,放在独立的服务器上,也是目前主流推崇的方案。
  • 另外一种方法就是动态跟静态文件混合在一起发布,通过 nginx 来分开

Nginx 配置文件的内容如下,包括三个部分:全局块、events 块和 http 块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
########### 全局块 ###########
user nginx; # 用户、用户组
worker_processes auto; # worker 数量

error_log /var/log/nginx/error.log notice; # 日志、级别
pid /var/run/nginx.pid;


########### events块 ###########
events {
worker_connections 1024; # worker 的最大连接数
}


########### http块 ###########
http {
include /etc/nginx/mime.types; # 文件扩展名与文件类型映射表
default_type application/octet-stream;

log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';

access_log /var/log/nginx/access.log main;

sendfile on;
#tcp_nopush on;

keepalive_timeout 65;

#gzip on;

upstream mysvr {
server 127.0.0.1:7878;
server 192.168.10.121:3333 backup; #热备
}

server {
keepalive_requests 120;
listen 80;
server_name _;

# charset utf-8;

location / { # 请求的 url 过滤,正则匹配
# root html;
# index index.html index.htm;
proxy_pass http://mysvr; #请求转向mysvr 定义的服务器列表
deny 127.0.0.1; #拒绝的ip
allow 172.18.5.54; #允许的ip
}
}

include /etc/nginx/conf.d/*.conf;
}
阅读全文 »

Nginx 是一个高性能的 HTTP 和反向代理服务器,同时也提供了 IMAP/POP3/SMTP 服务,具有高并发的优点。

Nginx 平台初探

Nginx 架构

Nginx 在启动后,在系统中会议 daemon(守护进程)的方式在后台运行,后台进程包含一个 master 和多个 worker 进程。

master 进程主要用来管理 worker 进程,包含:

  • 接收来自外界的信号,向各 worker 进程发送信号。
  • 监控 worker 进程的运行状态,当 worker 进程退出后(异常情况下),会自动重新启动新的 worker 进程。

而基本的网络事件,则是放在 worker 进程中来处理

  • 多个 worker 进程之间是对等的,他们同等竞争来自客户端的请求,各进程互相之间是独立的。一个请求,只可能在一个 worker 进程中处理,一个 worker 进程,不可能处理其它进程的请求。
  • worker 进程的个数是可以设置的,一般我们会设置与机器 cpu 核数一致,这里面的原因与 Nginx 的进程模型以及事件处理模型是分不开的。

img

操作 Nginx

因为 Nginx 是 master-worker 模型,所以如果要操作 Nginx,只需要与 master 通信即可

比如可以用 ./nginx -s reload 来重启 Nginx:此时会启动一个新的 Nginx 进程,在解析到 reload 参数后它会向 master 进程发送信号:

  • 首先 master 进程在接到信号后,会先重新加载配置文件

  • 然后再启动新的 worker 进程,并向所有老的 worker 进程发送信号,告诉他们可以光荣退休了。

  • 新的 worker 在启动后,就开始接收新的请求,而老的 worker 在收到来自 master 的信号后,就不再接收新的请求,并且在当前进程中的所有未处理完的请求处理完成后,再退出。

阅读全文 »

Span-based Joint Entity and Relation Extraction with Transformer Pre-training

来源:ECAI 2021

作者:Markus Eberts, Adrian Ulges

贡献:提出了模型 Span-based Entity and Relation Transformer(SpERT)

  • 提出了一种基于 span 的联合实体和关系提取的新方法。
  • 论文探究了模型成功的因素,结果表明:
    • 来自同一句子的负样本产生既有效率又有效果的训练,足够数量的强负样本是至关重要的。
    • 一个局部的上下文表示是有益的,特别是对于较长的句子。
    • 对预训练的模型进行微调会比从头开始的训练产生很强的性能提高。

数据集:

  • CoNLL04:从新闻中提取的带有命名实体识别和关系的句子。包含四类实体(地点、组织、人员、其他),以及物种关系类型(Work-For、Kill、Organization-Based-In,Live-In,Located-In)。包含训练集 1153 句话、测试集 288 句话。
  • SciERC:来源于五百篇人工智能论文的摘要,这个数据集包含六种实体(Task,Method,Metric,Material,Other-Scientific-Term,Generic),以及七种关系(Compare,Conjunction,Evaluate-For,Used-For,Feature-Of,Part-Of,Hyponym-Of),共 2687 个句子。
  • ADE:由 4272 个句子和 6821 个关系组成,这些关系描述了药物使用产生的不良反应。它包含一个单一的关系类型不良反应和两种实体类型不良反应和药物。

GitHub:https://github.com/markus-eberts/spert

模型方法

SpERT 模型分为三部分:

  • span classification
  • span filtering
  • relation classification

image-20230221160310427

Span Classification

对于任意一个 span 作为输入,目标是进行实体分类(实体类型 + none)。其输入包含三个部分:

  • 对这个 span 中所有 BERT 输出,利用融合函数进行融合。实验发现,最大池化的表现是最好的。
  • span 对应的宽度 embedding,加入宽度嵌入的原因是跨度过长不太可能表示为实体。
  • CLS 对应的嵌入,它代表整个句子的上下文表示。

将这三部分进行拼接,接着送入线性层、softmax 进行实体分类。

Span Filtering

论文采用了一种最简单的方法,直接将实体类型为 none 的 span 过滤掉。

Relation Classification

对于两个 span 进行的关系分类,需要得到 span pair 的表示。span pair 表示由以下组成:

  • 两个 span BERT 输出后经过融合函数后得到的表示、宽度 embedding。
  • 本地上下文表示(可以用 CLS,但是 CLS 不适合表达出多种关系的长句子,所以用更加本地化的方式),从第一个实体的结束到第二个实体的开始,接着送入融合函数(最大池化),就是本地上下文表示。如果这段为空(如遇到了 overlap),则将上下文表示设置为全零。

因为关系一般不是对称的,所以需要交换两个 span 的位置,分别进行分类。

Training

在训练时,采样方式如下:

  • 对于 span classification:将所有的已经标记的实体作为正样本,随机选取一些 span 作为负样本。
  • 对于 relation classification:将所有的 ground truth 作为正样本,在一句话中对于没有标记为任何关系的实体对作为负样本。

在训练过程中,只会过一遍 BERT,这样大大提高了训练速度。

实验

image-20230221170628613

实验结果探究

negative sample

论文探究了 negative sample 对于实验结果的影响:发现当 negative sample 只有 1 时,SciERC 的 f1 仅仅只有 10,当 negative sample 数量为 20 时,f1 值达到了 50。这说明,negative sample 对于实验的结果影响非常大

image-20230221171502963

localized contex

论文比较了三种不同的上下文表示方式,发现局部上下文表示最好(尤其在长句子的场景下)。

image-20230221172058239

fuse function

image-20230221172442326

REBEL: Relation Extraction By End-to-end Language generation

年份:2021

会议:EMNLP

作者:Pere-Lluís Huguet Cabot, Roberto Navigli

机构:Sapienza University of Rome

motivation:在端到端的联合关系抽取中,通常模型很复杂、需要适应关系或者实体类型的数量(不够灵活)、无法处理不同性质的文本(句子和文档级别)、需要长时间的训练

dataset:REBEL,一个利用自然语言推理、获得的大规模远程监督数据集。使用 REBEL 进行预训练。

image-20230219210930225

GitHub:https://github.com/babelscape/rebel

REBEL

论文将关系抽取作为一个生成任务来处理,采用 seq2seq 模型,因此采用 BART 作为基础模型。

Triplets linearization

因为这是一个生成任务,所以需要将复杂的三元组结构(overlap 问题)转为线性的表示,从而将三元组转为一句话。同时,解码也需要简单。

为此,论文引入了三个特殊的标签

  • triplet:若干共享头实体的三元组的开始。
  • subj:头实体的结束以及尾实体的开始。
  • obj:尾实体的结束,以及表明头实体和尾实体的关系。

算法如下:

image-20230219210304143

image-20230219210313395

阅读全文 »

对于带有 ttl 的 key,到期清理有两种解决思路:

  • 将所有带有 ttl 的 key 记录下来,比如用一个 list 保存,启动一个协程定期的去轮询。但是这样有一个缺点,就是效率低下:因为每一个的轮询都会遍历所有的 list 项,才能知道是否到期了。
  • 时间轮,时间轮避免了每一次轮询所有的 list 项,每一次只会查询可能到期的 key

时间轮

简单时间轮

时间轮实际上是一个环形队列,底层用数组实现。数组中的每个元素可以存放一个定时任务列表。定时任务列表是一个双向链表,链表中的每一项表示的都是定时任务项,其中封装了真正的定时任务。

环形队列的每一个元素,可以看作是一个时间格,每个时间格代表当前时间轮的基本时间跨度。时间格的个数是固定的,时间轮的总体事件跨度 = 时间格个数 × 时间格的事件跨度

时间轮还有一个表盘指针,用来表示时间轮当前所处的时间(就是当前指向了哪一个时间格)。表盘指针指向的是到期的时间格,表示需要处理的时间格所对应的链表中的所有任务。

如下图所示,时间格个数为 10,基本时间跨度为 1s 的时间轮,每一格里面放的是一个定时任务链表,链表里面存有真正的任务项。

taskList

初始情况下,表盘指针指向 0。若此时有一个 2s 的任务插入进来,就会放到时间格为 2 的任务链表中。当表盘指针指向 2 时,就会执行其中的任务。

timewheel

在这样的简单时间轮中,若有一个 15s 的定时任务,那么至少需要设置一个总体时间跨度为 15s 的时间轮才够用。如果需要一个一万秒的时间轮,那么可能需要一个很大的数组去存放(如果时间基本跨度为 1s,那么数组长度为 1 万)。不仅占用很大的内存空间,而且也会因为需要遍历这么大的数组从而拉低效率。

因此引入了层级时间轮的概念。

层级时间轮

层级时间轮就是引入多层的时间轮。

如下图所示,是一个两层的时间轮。第二层时间轮也是由 10 个时间格组成,每一个时间格的跨度是第一层时间轮的总体时间跨度,所以第二次时间轮的总体时间跨度为 100s。

如果像向该时间轮中添加一个 15s 的任务,那么当第一层时间轮容纳不下时,进入第二层时间轮,并插入到过期时间为 [10,19] 的时间格中。

timewheellevel2

随着时间的流逝,当原本 15s 的任务还剩下 5s 的时候,这里就有一个时间轮降级的操作,此时第一层时间轮的总体时间跨度已足够,此任务被添加到第一层时间轮到期时间为5的时间格中,之后再经历 5s 后,此任务真正到期,最终执行相应的到期操作。

在实际的实现中,一种简单的实现方式是:可以为每个任务记录下走过的圈数(circle),来表示逻辑上的层级关系。

godis 中时间轮的实现

在 godis 中,时间轮的实现采用层级时间轮(为每个任务记录下此时需要走过的圈数,表示逻辑上的层级关系)

数据结构

TimeWheel 时间轮

godis 中,时间轮的主体结构 TimeWheel 如下:

  • interval:每个时间格的基本时间跨度。
  • ticker:定时器,每过 interval 的时间,就会移动到下一个时间格、并且执行任务。
  • slots:时间格。
  • timer:因为每一个 key 都可以移除 ttl 或者 改变 ttl,所以用 timer 来定位每一个任务(key)。
  • currentPos:表盘指针,指向当前的时间格。
  • addTaskChannel:添加任务采用异步的操作,先将任务加入到 channel 中。
  • removeTaskChannel:移除任务也采用异步的操作,将需要取消 ttl 的 key 加入到通道中。
1
2
3
4
5
6
7
8
9
10
11
12
13
// TimeWheel can execute job after waiting given duration
type TimeWheel struct {
interval time.Duration
ticker *time.Ticker
slots []*list.List

timer map[string]*location
currentPos int
slotNum int
addTaskChannel chan task
removeTaskChannel chan string
stopChannel chan bool
}

location 的结构如下,用于定位任务在时间格中,所在的位置:

  • slot:表示时间格的下标。
  • etask:表示时间格维护的双向链表中,任务元素的地址。
1
2
3
4
type location struct {
slot int
etask *list.Element
}

task 任务

task 的结构如下:

  • circle:表示当前表针走过的圈数,表示逻辑上的层级。每一次指向当前 task 所在的时间格时都会令 circle 减一,circle 为 0 时说明已经到达了第一层时间轮。
  • delay:延迟 delay 时间之后,执行任务。
1
2
3
4
5
6
type task struct {
delay time.Duration
circle int
key string
job func()
}

时间轮开始后,会开启一个 start 协程来维护时间轮。采用 select 的方式:

  • 定期轮询时间到之后,调用 tickHandler 处理某一个时间格上的任务。
  • 异步处理需要添加的任务、需要删除的任务。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func (tw *TimeWheel) start() {
for {
select {
case <-tw.ticker.C:
tw.tickHandler()
case task := <-tw.addTaskChannel:
tw.addTask(&task)
case key := <-tw.removeTaskChannel:
tw.removeTask(key)
case <-tw.stopChannel:
tw.ticker.Stop()
return
}
}
}

添加任务

首先在 addTaskChannel 通道中加入一个 task,就表示添加了一个任务。之后时间轮会调用 addTask 方法,异步的将 task 从通道中转移到时间格中。

1
func (tw *TimeWheel) addTask(task *task)

步骤如下:

  • 首先计算这个任务所对应的时间格下标 pos,以及等待的圈数
1
2
pos, circle := tw.getPositionAndCircle(task.delay)
task.circle = circle
  • 将任务加到相应的时间格中。
1
2
3
4
5
e := tw.slots[pos].PushBack(task)
loc := &location{
slot: pos,
etask: e,
}
  • 如果之前存在相同的 key 则移除位置信息,记录这个任务的位置
1
2
3
4
5
6
7
if task.key != "" {
_, ok := tw.timer[task.key]
if ok {
tw.removeTask(task.key)
}
}
tw.timer[task.key] = loc

删除任务

删除任务(为一个 key 加上 ttl)和添加任务是类似的,也是异步的方式。步骤如下:

  • 首先找到这个 key 所对应的 location。
  • 接着在相应的时间格内删除任务,删除 location 的信息。
1
2
3
4
5
6
7
8
9
func (tw *TimeWheel) removeTask(key string) {
pos, ok := tw.timer[key]
if !ok {
return
}
l := tw.slots[pos.slot]
l.Remove(pos.etask)
delete(tw.timer, key)
}

处理任务

每过 interval 时间,都会调用 tickHandler 方法,指向下一个时间格,处理时间格上的任务。

1
2
3
4
5
6
7
8
9
func (tw *TimeWheel) tickHandler() {
l := tw.slots[tw.currentPos]
if tw.currentPos == tw.slotNum-1 {
tw.currentPos = 0
} else {
tw.currentPos++
}
go tw.scanAndRunTask(l)
}

scanAndRunTask 处理任务

scanAndRunTask 是真正的处理任务逻辑,它扫描时间格列表上的每一个任务,并且执行需要执行的任务。

处理逻辑如下,首先依次扫描 list 上的每一个元素:

  • 若 circle > 0,令 circle 减一,并移动到下一个元素上。
  • 否则,开启一个协程执行任务,并且在 list 上删除当前元素、删除位置信息 location。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func (tw *TimeWheel) scanAndRunTask(l *list.List) {
for e := l.Front(); e != nil; {
task := e.Value.(*task)
if task.circle > 0 {
task.circle--
e = e.Next()
continue
}

go func() {
defer func() {
if err := recover(); err != nil {
logger.Error(err)
}
}()
job := task.job
job()
}()
next := e.Next()
l.Remove(e)
if task.key != "" {
delete(tw.timer, task.key)
}
e = next
}
}

lambda 表达式

Lambda 是一个匿名函数,使用它可以写出更简洁、更灵活的代码。Lambda 表达式的本质:函数式接口的实现

在 Java 8 中引入了新的操作符 ->,称为 lambda 操作符或者箭头操作符。将 lambda 分为两个部分:

  • 左侧:指定了 Lambda 表达式需要的参数列表
  • 右侧:指定了 Lambda ,是抽象方法的实现逻辑。

lambda 表达式中参数数据类型可以省略,因为可由编译器推断得出,称为类型推断

应用举例:

lambda 表达式可以从匿名类转换为 lambda 表达式。

匿名类:

1
2
3
4
5
6
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("hello world");
}
};

lambda 表达式:

1
Runnable r = () -> System.out.println("hello world");
阅读全文 »