2020年11月30日

数据标注系统核心逻辑设计

点击数:21

理解数据标注

人工智能快速发展,离不开海量数据的支撑,而提供给机器学习的大数据采集工作,仍基于密集劳动力的人工智能数据标注。

\"数据标注现场\"

图1 数据标注现场

有一段时间,某视频分享社交APP上,有一个妈妈教baby认识图片的视频火了,妈妈给出了鸭子、鹅、狗、兔子、猪的5张图片,小孩子依次识别,当问到猪的图片是什么,baby毫不犹豫地说“爸爸”。这个过程中,一开始baby的认知能力为0,是妈妈告诉baby每张图片是什么,baby一遍一遍的学习和认识后,每当妈妈问baby时,baby则把5张图片识别的结果告诉了妈妈。妈妈在这个角色中就是数据标注员,把图片打上了标注,将标注后的数据教给baby,baby的角色就是模型,在经过多次的训练之后,baby说出了识别的结果。这就是典型的数据标注和机器学习(或模型训练)。数据标注,就是把输入的海量数据根据其特征打上标签,给学习模型提供认知功能。

所以说,人工智能是大数据喂养出来的,而数据标注是形成有价值的海量数据中非常重要的一环。

数据标注流程

我们暂且不关注数据的来源,就已有数据源创建多个任务,每个任务包含了很多待标注的文件。

标注师是一线流水线员工,是加工数据的开端;审核员是二线全量审批的员工,对于不符合条件的数据需要作出修改或者打回给标注师;抽检员则是质量把控的最后一道关卡,抽样检查。

\"\"

图2 标注流程

这里需要明确的是,最终输出的数据是一定经手审核员的数据。而抽检员仅仅是基于审核员已审核的数据抽样检查,不对数据修改,抽样通过的数据才可输出。

如何保证抽样数据和审核数据标注一致,凭借人工逐一对比效率不高,这里可以我们给出一种方案,计算抽样标注的数据和审核标注的数据的编辑距离(MED),如果是语音标注,还可以计算WER(字错误率)和SER(句错误率)。

\"\"

图3 详细流程

在此,特别对抽检这一阶段,作出详细的说明。抽检过程是将审核员提交抽检的数据任务,根据设定的阈值(百分比或者条数)选择N条,每个审核员的多个任务中抽取的数据划分为一个批次,抽检是按照批次操作(打回或归档)。对于每次新建抽检批次任务生成的批次中的所有数据,根据抽检员的数量划分为M个抽检任务,抽检任务是按照抽检员的维度操作的。详细的流程可参见图3抽检管理员中的业务逻辑流程。

举例说明:
标注任务有: A、B、C、D、E、F,每个任务有100个文件,审核员审核了这6个标注任务,并提交抽检
抽检批次:假设抽检了3个审核员X、Y、Z,而上面A、B、C、D、E 5个任务属于这三个审核员,其中,X对应A、C,Y对应B、E,Z对应D,抽检阈值是10个文件,于是根据三个审核员生成三个抽检批次,从5个标注任务中分别抽取10个文件归入对应审核员的批次中。批次对应的标注任务为:P1_X(a,c),P2_Y(b,e),P3_Z(d),括号中小写字母比作是对应标注任务10个文件
抽检任务:在创建抽检批次的时候,同时会根据抽检员生成抽检任务。抽检任务的数据来源抽检批次,如上,三个抽检批次,会划分给某几个抽检员。假设抽检员是2个分别为M,N,那么抽检任务则为2,每个抽检员对应一个。抽检任务对应的批次任务可能为:CJ_M(a5,d10,b7,e3),CJ_N(a5,c10,b3,e7),括号中的字母为标注任务(也就是批次中的标注任务),字母后的数字表示抽检批次抽取的样本数据量

任务状态扭转

数据标注,无非就是对已有数据进行识别,打上具有某些特征的标签或者标出某些数据结果,比如图片识别OCR,可以识别身份证的信息,比如语音识别ASR,识别语音,输出文本。所以,数据标注的思想基本是一致的,不同的是,对于特定的输入数据其对应输出数据有所区别。图片OCR需要标注文件显示的位置和完整的文字,涉黄涉爆等危害图片,则需要给出不同的类型;语音数据标注,需要标记语音片段的始末、话者性别、语言类别、转写的文本等。

根据设计,我们梳理了三种类型的任务状态:标注任务、抽检批次、抽检任务。下图则是标注任务的状态扭转图(或状态机)。

从创建任务到最终的归档,该任务流程才算彻底结束。每一个状态的转变,不管是人工操作还是系统服务内部发起,都有一个事件的驱动,促使标注任务的状态转变。

\"标注任务状态扭转图\"

下图则为抽检批次状态扭转图,从图中可以看出,抽检批次状态相交简单,仅仅只有五种状态。这五种状态与标注任务的状态也有关联关系,抽检批次状态的变更会影响标注任务状态的变更。虽然有相同之处,但亦有差别,主要体现在打回的逻辑上,对于批次打回或归档,则表示该批次完成了,但对于标注任务而言,打回需要重申,甚至需要重标。

\"抽检批次状态扭转图\"

下面是抽检任务状态扭转图,该图体现的是抽检员抽检任务时,抽检任务随着抽检事件的驱动发生转变的过程。

  • 待抽检:抽检批次创建时,依据抽检员生成给自的抽检任务,此时抽检任务为待抽检。
  • 抽检中:抽检员开始抽检第一条标注文件时,任务变为抽检中。
  • 抽检完成:当所有文件抽检完成时,系统计算,并将抽检任务状态设置为抽检完成,抽检完成的任务抽检员还可以进行修改。
  • 已提交:抽检完成后,该任务已提交给抽检批次,抽检员不能再操作该抽检任务,只能在抽检批次中进行状态的变更(退回或归档)。

\"抽检任务状态扭转图\"

综上,从图上可以清晰明了的看出三种类型的任务状态的扭转,这对于业务方来说更易于梳理逻辑,对于开发者可以提前设计适应于该场景的系统架构(包括代码模式的设计、数据库表字段的设计、业务逻辑的优化、功能点checklist清单的整理等)。

代码实现

分析完业务逻辑,从代码编写或者模式的使用上来谈一谈如何设计。首先,要明白,为什么我们要用设计模式?设计模式是一套理论,是业界的大牛(ERRJ)总结的一套可以反复使用的经验,这些经验可以提高代码的可重用性,增强系统的可维护性,以及解决一系列的复杂问题。对于我们开发人员,设计模式可以让我们写出更加高效、优雅的代码,也可以让我们设计出健壮、稳定、高效的系统,并且自动地预防未来业务变化可能对系统带来的影响。

从上面的状态图的扭转来看,三种类型的任务都适合使用状态模式,通过设置状态的改变,达到行为的改变。对于状态模式,由于对于每种状态都需要一个单独的类来表示,因此一般建议其状态不超过五种,故而,标注任务不适合用状态模式。

标注任务逻辑

标注任务,可以分为三个阶段:标注阶段、审核阶段、抽检阶段,三个阶段唯一的共性就是打标。而其他的行为则完全不一致,所以,鉴于这种情况,装饰器模式可能是一个不错的选择。

\"\"

各阶段实现根据需要添加自己的接口,并且可以重写标注接口。各自实现自己的业务逻辑,分工明确。

代码结构

  • 标注接口
public interface ILabel {
    void label();
}
  • 标注实现类
public class Label implements ILabel {
    @Override
    public void label() {
        // TODO 标注任务
    }
}
  • 装饰器类
public class Decorator implements ILabel  {

    protected ILabel iLabel;

    public Decorator(ILabel iLabel) {
        this.iLabel = iLabel;
    }

    @Override
    public void label() {
        // 实现自己的标注逻辑
        iLabel.label();
    }
}
  • 标注阶段装饰类
public class LabelDecorator extends Decorator {
    public LabelDecorator(ILabel iLabel) {
        super(iLabel);
    }

    @Override
    public void label() {
        // 标注师标注的方式
    }

    // 标注师的其他操作
    public void doSomething() {

    }
}

  • 审核阶段装饰类
public class AuditDecorator extends Decorator {

    public AuditDecorator(ILabel iLabel) {
        super(iLabel);
    }
    @Override
    public void label() {
        // 审核员标注的方式
    }

    // 审核员的其他操作
    public void doSomething() {

    }
}
  • 抽检阶段装饰类
public class SampleDecorator extends Decorator {
    public SampleDecorator(ILabel iLabel) {
        super(iLabel);
    }

    @Override
    public void label() {
        // 抽检员标注的方式

    }

    // 标抽检员的其他操作
    public void doSomething() {
        // TODO: 2019-10-08 其他操作
    }
}
  • servie调用demo
Label label = new Label();
// 创建标注阶段的标注装饰器
ILabel iLabel = new LabelDecorator(label);
// 创建审核阶段的装饰器
iLabel = new AuditDecorator(label);
// 创建抽检阶段的装饰器
iLabel = new SampleDecorator(label);

iLabel.label();

抽检任务逻辑

状态模式主要解决的是当控制一个对象状态装换的条件表达式过于复杂时的情况。把状态的判断逻辑转移到表示不同状态的一系列类中,可以把复杂的判断逻辑简单化。

状态模式是通过对象状态的改变来驱动器行为,当一个对象行为取决于它的状态,并且它必须在运行时刻根据状态改变它的行为时,就可以考虑使用状态模式了。

也许大部分人认为,这些操作可以放在一个类中不就可以了嘛,为什么还要新建这么繁琐的子类呢?大家需要注意的,每个动作都有先后顺序,在待抽检状态下,唯一的操作就是开始抽检,而不能去提交任务。

\"\"

抽检状态

import lombok.Getter;
import lombok.Setter;

/**
 * 抽检状态抽象类
 *
 * @author zhoujunwen
 * @date 2019-10-08
 * @time 10:52
 * @desc
 */
public abstract class SampleState {

    // 上下文,接收状态的变化
    @Setter
    @Getter
    protected SampleTaskContent sampleTaskContent;

    /**
     * 创建抽检任务
     */
    public abstract void createSampleTask();

    /**
     * 抽检任务
     */
    public abstract void startSampleTask();

    /**
     * 检验抽检是否完成,如果完成,返回true,否则返回false
     * @return
     */
    public abstract boolean isCompleteSampled();

    /**
     * 抽检任务已提交
     */
    public abstract void submitSampleTask();
}

具体抽检状态

待抽检

/**
 * 创建抽检任务时,状态属于待抽检,要实现具体的<code>createSampleTask</code>方法
 * 在待抽检状态下,接下来的动作,应该只能是开始抽检。
 *
 * @author zhoujunwen
 * @date 2019-10-08
 * @time 10:38
 * @desc
 */
public class PendingInspectionState extends SampleState {

    /**
     * 实现创建抽检任务的逻辑
     */
    @Override
    public void createSampleTask() {
        // 任务已创建

    }

    // 在待抽检中,唯一能做的事情就是开始抽检
    @Override
    public void startSampleTask() {
        // 状态修改
        sampleTaskContent.setSampleState(SampleTaskContent.SPOT_CHECK);
        // 抽检动作委派
        sampleTaskContent.getSampleState().startSampleTask();
    }

    @Override
    public boolean isCompleteSampled() {
        // do nothing
        return false;
    }

    @Override
    public void submitSampleTask() {
        // do nothing
    }
}

抽检中

/**
 * 抽检任务时,状态属于抽检中,要实现具体的<code>startSampleTask</code>方法
 * 在抽检中的状态下,接下来的动作,应该需要校验是否完成。
 *
 * @author zhoujunwen
 * @date 2019-10-08
 * @time 10:42
 * @desc
 */
public class SpotCheckingState extends SampleState {

    /**
     * 在抽检状态下,不能创建任务
     */
    @Override
    public void createSampleTask() {
        // do nothing
    }

    /**
     * 开始真正抽检任务
     */
    @Override
    public void startSampleTask() {
        // 抽检任务 TODO
    }

    /**
     * 抽检任务中,唯一能做的就是校验是否抽检完成
     */
    @Override
    public boolean isCompleteSampled() {
        // 设置抽检状态
        sampleTaskContent.setSampleState(SampleTaskContent.COMPLETE_SAMPLED);
        // 委托动作
        sampleTaskContent.getSampleState().isCompleteSampled();

        return false;
    }

    @Override
    public void submitSampleTask() {
        // do nothing
    }
}

抽检完成

/**
 * 校验抽检任务是否完成时时,状态属于已完成,要实现具体的<code>isCompleteSampled</code>方法
 * 在待抽检状态下,接下来的动作,应该只能是开始抽检或者提交任务。
 * @author zhoujunwen
 * @date 2019-10-08
 * @time 11:28
 * @desc
 */
public class CompleteSampledState extends SampleState {
    @Override
    public void createSampleTask() {
        // do nothing
    }

    @Override
    public void startSampleTask() {
        sampleTaskContent.setSampleState(SampleTaskContent.PENDING_INSPECTION);
        sampleTaskContent.getSampleState().submitSampleTask();
    }

    @Override
    public boolean isCompleteSampled() {
        // TODO: 2019-10-08 具体业务实现
        return true;
    }

    /**
     * 抽检完成状态下,唯一能做的就是提交任务
     */
    @Override
    public void submitSampleTask() {
        sampleTaskContent.setSampleState(SampleTaskContent.Submitted_SAMPLED);
        sampleTaskContent.getSampleState().submitSampleTask();
    }
}

已提交

/**
 * 提交抽检任务时,状态属于已提交,要实现具体的<code>submitSampleTask</code>方法
 * 在待抽检状态下,接下来的动作,应该只能是开始抽检或者提交任务。
 *
 * @author zhoujunwen
 * @date 2019-10-08
 * @time 11:28
 * @desc
 */
public class SubmittedSampleState extends SampleState {
    @Override
    public void createSampleTask() {
        // do nothing
    }

    @Override
    public void startSampleTask() {
        // do nothing
    }

    @Override
    public boolean isCompleteSampled() {
        // do nothing
        return true;
    }

    @Override
    public void submitSampleTask() {
        // TODO 2019年10月08日11:47:48
    }
}

抽检任务上下文

import lombok.Getter;

/**
 * 抽检任务上下文环境
 *
 * @author zhoujunwen
 * @date 2019-10-08
 * @time 10:35
 * @desc
 */
public class SampleTaskContent {
    public static SampleState PENDING_INSPECTION = new PendingInspectionState();
    public static SampleState SPOT_CHECK = new SpotCheckingState();
    public static SampleState COMPLETE_SAMPLED = new CompleteSampledState();
    public static SampleState Submitted_SAMPLED = new SubmittedSampleState();
    /**
     * 抽样任务状态
     */
    @Getter
    private SampleState sampleState;

    public void setSampleState(SampleState sampleState) {
        this.sampleState = sampleState;
        this.sampleState.setSampleTaskContent(this);
    }

    public void createSampleTask() {
        this.sampleState.createSampleTask();
    }

    public void startSpotCheckTask() {
        this.sampleState.startSampleTask();
    }

    public void isCompleteSampled() {
        this.sampleState.isCompleteSampled();
    }

    public void submitSampleTask() {
        this.sampleState.submitSampleTask();
    }
}

抽检批次逻辑

参考抽检任务逻辑

发表评论

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据