ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

使用 Scikit-Learn 为 Label Studio 构建文本分类 ML 后端:从 predict 到 fit 的完整实战

使用 Scikit-Learn 为 Label Studio 构建文本分类 ML 后端:从 predict 到 fit 的完整实战 使用 Scikit-Learn 为 Label Studio 构建文本分类 ML 后端从 predict 到 fit 的完整实战【免费下载链接】label-studioLabel Studio is a multi-type data labeling and annotation tool with standardized output format项目地址: https://gitcode.com/GitHub_Trending/la/label-studio本文以 Label Studio 开源仓库中的官方教程docs/source/guide/ml_tutorials/sklearn-text-classifier.md为主线讲解如何基于 scikit-learn 的 TF-IDF 逻辑回归模型编写一个可直接对接 Label Studio 的文本分类机器学习ML后端。读者将掌握LabelStudioMLBase子类的编写规范、predict()与fit()两个核心方法的实现细节、ML 后端的初始化与启动方式以及如何在 Label Studio 项目中接入模型、获取预标注并触发在线训练。教程背景与适用场景本教程面向一个典型的文本分类标注项目标注界面由Text对象标签和Choices控制标签组成标注员需要为每段新闻文本选择一个主题类别Politics、Technology、Sport、Weather。ML 后端在这里承担两件事预测predict在标注员打开任务时提前给出模型预测的类别与置信度分数作为预标注pre-annotation展示在界面上训练fit当人工标注数据积累到一定程度后用这些标注结果重新训练模型训练产出通过返回字典暴露给后续推理过程复用。整个后端基于 Label Studio 官方的label-studio-ml-backendSDK 开发最终以独立 Web 服务的形式运行通过 HTTP 与 Label Studio 主服务通信。准备标签配置Label Config动手写模型之前先在 Label Studio 项目中创建或确认如下标签配置。它声明了本教程模型将要消费的数据字段与输出结构View Text namenews value$text/ Choices nametopic toNamenews Choice valuePolitics/ Choice valueTechnology/ Choice valueSport/ Choice valueWeather/ /Choices /View关键点说明Text namenews value$text/从任务 JSON 的data字段中取text键作为模型输入文本输入类型为TextChoices nametopic toNamenews把控制标签topic绑定到对象标签news上Choices类型的输出即模型预测的分类结果四个Choice是模型的候选类别集合。注意在冷启动没有任何训练记录阶段模型会直接拿这组标签做伪训练初始化因此标签顺序会影响训练前预测的索引映射。编写模型脚本 model.py创建model.py核心是定义一个继承自label_studio_ml.model.LabelStudioMLBase的模型类并重写predict()与fit()两个方法。完整代码如下import pickle import os import numpy as np from sklearn.linear_model import LogisticRegression from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.pipeline import make_pipeline from label_studio_ml.model import LabelStudioMLBase class SimpleTextClassifier(LabelStudioMLBase): def __init__(self, **kwargs): # dont forget to initialize base class... super(SimpleTextClassifier, self).__init__(**kwargs) # then collect all keys from config which will be used to extract data from task and to form prediction # Parsed label config contains only one output of Choices type assert len(self.parsed_label_config) 1 self.from_name, self.info list(self.parsed_label_config.items())[0] assert self.info[type] Choices # the model has only one textual input assert len(self.info[to_name]) 1 assert len(self.info[inputs]) 1 assert self.info[inputs][0][type] Text self.to_name self.info[to_name][0] self.value self.info[inputs][0][value] if not self.train_output: # If there is no trainings, define cold-started the simple TF-IDF text classifier self.reset_model() # This is an array of Choice labels self.labels self.info[labels] # make some dummy initialization self.model.fit(Xself.labels, ylist(range(len(self.labels)))) print(Initialized with from_name{from_name}, to_name{to_name}, labels{labels}.format( from_nameself.from_name, to_nameself.to_name, labelsstr(self.labels) )) else: # otherwise load the model from the latest training results self.model_file self.train_output[model_file] with open(self.model_file, moderb) as f: self.model pickle.load(f) # and use the labels from training outputs self.labels self.train_output[labels] print(Loaded from train output with from_name{from_name}, to_name{to_name}, labels{labels}.format( from_nameself.from_name, to_nameself.to_name, labelsstr(self.labels) )) def reset_model(self): self.model make_pipeline(TfidfVectorizer(ngram_range(1, 3)), LogisticRegression(C10, verboseTrue)) def predict(self, tasks, **kwargs): # collect input texts input_texts [] for task in tasks: input_texts.append(task[data][self.value]) # get model predictions probabilities self.model.predict_proba(input_texts) predicted_label_indices np.argmax(probabilities, axis1) predicted_scores probabilities[np.arange(len(predicted_label_indices)), predicted_label_indices] predictions [] for idx, score in zip(predicted_label_indices, predicted_scores): predicted_label self.labels[idx] # prediction result for the single task result [{ from_name: self.from_name, to_name: self.to_name, type: choices, value: {choices: [predicted_label]} }] # expand predictions with their scores for all tasks predictions.append({result: result, score: score}) return predictions def fit(self, completions, workdirNone, **kwargs): input_texts [] output_labels, output_labels_idx [], [] label2idx {l: i for i, l in enumerate(self.labels)} for completion in completions: # get input text from task data print(completion) if completion[annotations][0].get(skipped) or completion[annotations][0].get(was_cancelled): continue input_text completion[data][self.value] input_texts.append(input_text) # get an annotation output_label completion[annotations][0][result][0][value][choices][0] output_labels.append(output_label) output_label_idx label2idx[output_label] output_labels_idx.append(output_label_idx) new_labels set(output_labels) if len(new_labels) ! len(self.labels): self.labels list(sorted(new_labels)) print(Label set has been changed: str(self.labels)) label2idx {l: i for i, l in enumerate(self.labels)} output_labels_idx [label2idx[label] for label in output_labels] # train the model self.reset_model() self.model.fit(input_texts, output_labels_idx) # save output resources model_file os.path.join(workdir, model.pkl) with open(model_file, modewb) as fout: pickle.dump(self.model, fout) train_output { labels: self.labels, model_file: model_file } return train_output构造函数解析标签配置与双路径初始化__init__是理解这个模型的关键入口通过self.parsed_label_configSDK 对标签配置的解析结果校验配置合法性只有一个输出、类型必须是Choices、只有一个Text输入记录from_name输出控件名如topic、to_name绑定的对象名如news、value取数据用的键如text这三个变量在predict组装结果时复用根据self.train_output是否存在走两条路径冷启动时用标签配置里的labels做一次伪拟合self.model.fit(Xself.labels, ylist(range(len(self.labels))))保证服务一启动就能返回有效预测有训练记录时则从train_output[model_file]反序列化模型并同步train_output[labels]。reset_model()定义了本教程的模型结构self.model make_pipeline(TfidfVectorizer(ngram_range(1, 3)), LogisticRegression(C10, verboseTrue))即TfidfVectorizer1~3 元词粒度配合LogisticRegression正则化强度 C10组成的 scikit-learn Pipeline。predict()把模型输出翻译成 Label Studio 预测格式predict(tasks, **kwargs)接收的是 Label Studio 任务列表每个任务含data字段其职责分为三步从task[data][self.value]收集所有输入文本调用model.predict_proba得到各类别概率用np.argmax取出最大概率类别索引与对应分数为每个任务组装一条 Label Studio 预测结果prediction JSON 格式{ result: [ { from_name: topic, to_name: news, type: choices, value: {choices: [Technology]} } ], score: 0.87 }type固定为choicesvalue.choices是长度为 1 的数组这一点必须与标签配置中的Choices类型严格对应否则前端无法渲染。fit()消费标注、动态标签集与产物持久化fit(completions, workdirNone, **kwargs)接收的是带标注的任务迭代器每个 completion 形如{data: {...}, annotations: [{...}]}跳过skipped或was_cancelled的标注对应 Label Studio JSON 标注格式 中的状态字段从completion[annotations][0][result][0][value][choices][0]取标注类别动态标签集处理训练数据中出现的类别集合与冷启动标签集不一致时自动用sorted(new_labels)重建self.labels并重映射索引使模型能够适应人工标注中新增的类别重新初始化模型并fit(input_texts, output_labels_idx)把模型以pickle形式保存到workdir/model.pkl返回train_output {labels: ..., model_file: ...}。这个返回字典是训练与推理之间的桥梁Label Studio 会把该字典持久化下次模型进程重启时通过self.train_output读回对应构造函数中的加载分支。初始化 ML 后端目录模型脚本写好后Label Studio 可以自动生成运行 ML 后端所需的全部配置与脚本。使用 SDK 提供的 CLI来自label-studio-ml-backend包初始化目录label-studio-ml init my_backend该命令会把./model.py复制到同级目录./my_backend中并生成开发/生产两种模式启动所需的配置Dockerfile、docker-compose.yml、_wsgi.py、requirements 文件等结构可参考 ML 后端创建指南。若脚本位于其他位置可显式指定label-studio-ml init my_backend --script /path/to/my/script.py启动 ML 后端服务开发模式单进程、训练即阻塞label-studio-ml start my_backend开发模式基于 Flask训练与推理运行在同一个进程内服务启动于http://localhost:9090日志直接输出到控制台。注意此模式的限制模型训练期间服务器不会响应新的预测请求适合本地调试。生产模式Redis RQ 后台训练生产模式由 Redis 与 RQ 任务队列驱动后台训练进程可以在训练进行的同时继续用当前模型版本提供预测服务训练完成后新模型版本自动生效。前提是系统已安装 Docker 与 docker-composecd my_backend/ docker-compose up运行时可排查两类日志服务运行时日志在my_backend/logs/uwsgi.log训练任务日志在my_backend/logs/rq.log。关于生产模式背后的 uWSGI、supervisord 与 RQ 技术栈可参考 ML 后端原理说明。接入 Label Studio 项目初始化并启动一个连接上述 ML 后端的 Label Studio 项目label-studio start my_project --init --ml-backends http://localhost:9090--init会创建新项目--ml-backends将http://localhost:9090注册为该项目的 ML 后端。连接建立后Label Studio 主服务会通过/api/ml相关接口对后端做健康检查healthcheck、模型版本探测setup与预测请求predict对应服务端实现位于 ml/api.py 与 ml/models.py。获取预标注预测项目连接成功后打开标注页面即可看到模型给出的预测结果。预测由 Label Studio 在任务分发时请求 ML 后端predict端点获得并按 设置机器学习 中描述的机制展示在标注界面上。触发模型训练两种方式触发训练界面操作进入项目设置的 Machine Learning 页面点击Start training按钮API 调用向 Label Studio 服务端发送 POST 请求。原教程给出的是curl -X POST http://localhost:8080/api/models/train按当前仓库的实际路由实现训练端点是带 ML 后端 ID 的POST /api/ml/{id}/train例如curl -X POST http://localhost:8080/api/ml/1/train源码级验证训练请求在 Label Studio 内部如何流转为了让读者对整条链路有源码级把握这里结合本仓库实现给出佐证路由注册ml/urls.py 中注册了path(int:pk/train, api.MLBackendTrainAPI.as_view(), nameml-train)因此实际端点为/api/ml/{pk}/train服务端视图MLBackendTrainAPI 的post方法校验权限需projects_change权限后调用ml_backend.train()后端模型层MLBackend.train() 调用api.train(project)将状态置为TRAINING并记录训练任务 ID请求构造api_connector.py 的 train 方法 只挑选含有标注的任务num_annotations 0用ExportDataSerializer序列化成{data: ..., annotations: [...]}形式连同project、label_config、hostname一起 POST 到 ML 后端的/train端点——这正是fit(completions, ...)收到的数据结构测试佐证ml.tavern.yml 中的 API 测试直接以{django_live_url}/api/ml/{ml_pk}/train为请求目标验证训练接口。由此可见本教程fit()方法中对completion[annotations][0]的读取、对skipped/was_cancelled的过滤都与 Label Studio 服务端构造训练请求的序列化格式一一对应而train_output字典则会被服务端保存用于模型版本管理与下次启动时的模型加载。小结与扩展至此一条完整的标注 → 训练 → 预测闭环已经打通scikit-learn 文本分类器通过LabelStudioMLBase子类包装成独立 Web 服务predict()负责把模型概率翻译成 Label Studio 可渲染的 choices 预标注fit()负责消费人工标注、动态维护标签集并持久化模型最终以--ml-backends参数挂接到标注项目中循环迭代。如需进一步探索可继续阅读仓库内的以下资料编写自己的 ML 后端完整指南LabelStudioMLBase的全部可重写方法、目录结构与部署细节ML 教程总览基于 HuggingFace、OpenAI、LangChain 等框架的更多后端示例任务与标注数据格式、预测数据格式、导出 JSON 格式理解predict/fit输入输出的底层约定。【免费下载链接】label-studioLabel Studio is a multi-type data labeling and annotation tool with standardized output format项目地址: https://gitcode.com/GitHub_Trending/la/label-studio创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进