Doccano自动标注配置
JRQZ
开源自动标注工具Doccano,项目地址https://github.com/doccano/doccano
官方教程:https://doccano.github.io/doccano/
支持jsonl格式文件导入导出,支持REST API自动标注
自动标注api参考:
https://blog.csdn.net/weixin_44826203/article/details/125719480
遇到的问题:
无法正确设置自动标注api
原因是当前版本accano前端有bug,参考https://github.com/doccano/doccano/issues/2281
可通过访问http://x.x.x.x:8000/admin/进入Django管理界面手动配置
Model attrs:{"url": "http://x.x.x.x:5739", "body": {"text": "{{ text }}"}, "method": "POST", "params": {}, "headers": {}}
Template:[
{% for entity in input %}
{
"start_offset": {{ entity.start_offset }},
"end_offset": {{ entity.end_offset}},
"label": "{{ entity.label }}"
}{% if not loop.last %},{% endif %}
{% endfor %}
]
Label mapping:{"label1":"match label","label2":"match label2"}
# lable1: you config labels_span name
# match label: interface return entity class name
正确配置后,api后台可以收到数据并正常处理,但是accanno前台不能自动标注,原因不明,要么是相关参数没有正确配置(由于accano前端写得真不太行,难以在web界面上排查),要么是accano没有收到返回的数据
排查方法:
- 在accano机器上检测api端的流量包,确认是否收到数据
- 查找accano相关日志
- 看源码(到了这一步感觉不如换别的工具/直接手动标注)
解决方法:安装旧版本doccano
docker pull doccano/doccano:1.8.3
docker container create --name doccano_183 \
-e "ADMIN_USERNAME=admin" \
-e "ADMIN_EMAIL=admin@example.com" \
-e "ADMIN_PASSWORD=password" \
-v doccano-db:/data \
-p 8002:8000 doccano/doccano:1.8.3
docker container start doccano_183
# 查看tag
curl -s https://registry.hub.docker.com/v2/repositories/doccano/doccano/tags | jq '.results[].name'
自动标注
命名实体识别接口:
from flask import Flask, request, jsonify
import regex, re
app = Flask(__name__)
def load_common_words(file_path):
with open(file_path, 'r', encoding='utf-8') as file:
words = [line.strip() for line in file if line.strip()]
return words
words=load_common_words('common_words.txt')
# 定义正则表达式模式
patterns_mc = [
('Phone', r'(?<=\+86[-\s]?)1[3-9]\d{9}|(?<=\+852[-\s]?)(?:4|5|6|7|8|9)\d{7}|(?<=\+886[-\s]?)09\d{8}|(?<=\+853[-\s]?)6\d{7}'),
('TG', r'@[a-zA-Z][a-zA-Z0-9_]{4,31}'),
('Mail', r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}'),
('QQ', r'(?<=QQ[ ]?|qq[ ]?|Qq[ ]?|qQ[ ]?)[1-9][0-9]{6,10}'),
('ID', r'(\d{6}(?:\d{8}|\d{6})\d{3}(?:\d|X))'),
('Landline_Number', r'0\d{2,3}-\d{7,8}'),
('Common_words', r'\b(' + '|'.join(re.escape(word) for word in words) + r')\b')
]
patterns = [
('Phone', r'1[3-9]\d{9}|(?:4|5|6|7|8|9)\d{7}|09\d{8}|6\d{7}'), # (区号可选)中国手机号正则表达式|香港5/6/9|台湾|澳门6
('QQ', r'[1-9][0-9]{6,10}'), # QQ号正则表达式,约束在5-11位
('WX', r'[a-zA-Z][-_a-zA-Z0-9]{5,19}') # 微信号正则表达式
]
def extract_labels(text):
results = []
grapheme_clusters = list(regex.finditer(r'\X', text))
matched_positions = [False] * len(grapheme_clusters) # 标记数组,记录每个字素簇是否已被匹配
all_matches_mc = []
all_matches = []
# 优先匹配部分
for label, pattern in patterns_mc:
for match in regex.finditer(pattern, text):
match_text=match.group
start, end = match.start(), match.end()
all_matches_mc.append((label, start, end))
all_matches_mc.sort(key=lambda x: x[2] - x[1], reverse=True)
for label, start, end in all_matches_mc:
# 找到匹配的字素簇范围
start_cluster = next(i for i, m in enumerate(grapheme_clusters) if m.start() == start)
end_cluster = next(i for i, m in enumerate(grapheme_clusters) if m.end() == end)
# 检查匹配的范围内是否有字素簇已经被匹配
if not any(matched_positions[start_cluster:end_cluster]):
results.append({
"label": label,
"start_offset": start_cluster,
"end_offset": end_cluster+1
})
# 标记匹配的范围
for i in range(start_cluster, end_cluster):
matched_positions[i] = True
# 其次匹配
for label, pattern in patterns:
for match in regex.finditer(pattern, text):
start, end = match.start(), match.end()
all_matches.append((label, start, end))
# 按照匹配长度从长到短排序
all_matches.sort(key=lambda x: x[2] - x[1], reverse=True)
for label, start, end in all_matches:
# 找到匹配的字素簇范围
start_cluster = next(i for i, m in enumerate(grapheme_clusters) if m.start() == start)
end_cluster = next(i for i, m in enumerate(grapheme_clusters) if m.end() == end)
# 检查匹配的范围内是否有字素簇已经被匹配
if not any(matched_positions[start_cluster:end_cluster]):
results.append({
"label": label,
"start_offset": start_cluster,
"end_offset": end_cluster+1
})
# 标记匹配的范围
for i in range(start_cluster, end_cluster):
matched_positions[i] = True
return results
@app.route('/', methods=['POST'])
def get_result():
text = request.json['text']
print(text)
results = extract_labels(text)
return jsonify(results)
if __name__ == '__main__':
# 这里写端口的时候一定要注意不要与已有的端口冲突
# 这里的host并不是说访问的时候一定要写0.0.0.0,但是这里代码要写0.0.0.0,代表可以被本网络中所有的看到
# 如果是其他机器访问你创建的服务,访问的时候要写你的ip
app.run(host='0.0.0.0', port=5739)
测试
curl -X POST http://x.x.x.x:5739 -H "Content-Type: application/json" -d '{"text":"这是一个测试文本,包含中国大陆手机号:13912345678,香港手机号:51234567,澳门手机号:61234567,台湾手机号:0912345678"}'
现在我们有了doccano标注平台,以及一个自动标注的接口,接下来要做的就是把它们两个放在一起。
我们进入标注系统,用管理员账号登录,点击左下角的Settings
,然后选择Auto Labeling
,然后会弹出下面的窗口,我们选择Custom REST Request
点击Next
,填写自动标注服务所在的地址,就是你的ip+端口
然后Params
和Headers
中都空着,Body
填写如下
Key: text
Value: {{ text }}
注意,这里的value
中,text和括号之间有两个空格
这里写完之后可以输入一句话来测试你的接口,比如我们输入一句话“小明昨天去了北京”,点击Test
,如果得到了图中的结果,说明接口运行正常,否则需要去前面的环节找问题。
进入Next
,在图中所示位置加入这样一段代码:
[
{% for entity in input %}
{
"start_offset": {{ entity.start_offset }},
"end_offset": {{ entity.end_offset}},
"label": "{{ entity.label }}"
}{% if not loop.last %},{% endif %}
{% endfor %}
]
最后一步,需要建立从接口到标注平台的标签映射,这一步的作用是把你的接口识别出来的实体类型,映射到第2步中创建的标注平台的label,例如在api中定义了时间
,在平台创建label的时候定义的label名称是时间日期
,那么就需要建立他们之间的一个映射,把所有的映射建立起来就可以了
最后Test
->Finish
,大功告成
增加标注员用户
我们需要进入Django的管理界面,地址是你的ip+标注服务的端口+admin/,例如 111.222.33.44:1234/admin/ 进入界面之后,在users点击add,即可添加标注员用户