post

Doccano自动标注配置

· 3 分钟阅读 · 472 字

开源自动标注工具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+端口

然后ParamsHeaders中都空着,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,即可添加标注员用户