Форматы экспорта
Экспорт разметки Potato в JSON, JSONL, CSV, CoNLL, HuggingFace Datasets, COCO, YOLO, Pascal VOC и Apache Parquet для последующих ML-конвейеров.
В Potato есть два уровня экспорта:
- Встроенный экспорт. Работа каждого разметчика сохраняется в JSON в
output_annotation_dir, аexport_annotation_formatдобавляет периодический экспорт в csv, tsv, jsonl, parquet или любой другой зарегистрированный формат - CLI экспорта (новое в v2.2.0).
python -m potato.exportпреобразует разметку в любой из 31 зарегистрированного формата, среди которых COCO, YOLO, Pascal VOC, CoNLL-2003, CoNLL-U, Mask PNG, Parquet, EAF, TextGrid и HuggingFace. Чтобы увидеть набор, который действительно есть в вашей установке, выполнитеpython -m potato.export --list-formats
На этой странице описаны и встроенные форматы, и CLI экспорта, а также приведены примеры скриптов преобразования для распространённых целевых форматов.
Базовые форматы экспорта
JSON
Работа каждого разметчика сохраняется в <output_annotation_dir>/<user>/user_state.json, что бы ни было указано в export_annotation_format. Если оставить только данные разметки, файл выглядит так:
{
"user_id": "user_1",
"instance_id_ordering": ["doc_001", "doc_002"],
"instance_id_to_label_to_value": {
"doc_001": [[{"schema": "sentiment", "name": "positive"}, "true"]]
},
"instance_id_to_span_to_value": {}
}В реестре экспорта нет формата json. Устаревший параметр output_annotation_format: "json" читается как jsonl с предупреждением, поэтому периодический экспорт настраивайте как jsonl:
export_annotation_format: "jsonl"
output_annotation_dir: "output/"JSON Lines (JSONL)
Одна запись на каждую пару «разметчик, объект», записывается в <output_annotation_dir>/exports/jsonl/annotations.jsonl:
{"instance_id": "doc_001", "user_id": "user_1", "labels": {"sentiment": {"positive": "true"}}, "spans": {}, "links": {}}
{"instance_id": "doc_002", "user_id": "user_1", "labels": {"sentiment": {"negative": "true"}}, "spans": {}, "links": {}}export_annotation_format: "jsonl"CSV
Одна строка на каждую пару «разметчик, объект»: сначала instance_id и user_id, затем по одному столбцу на каждую пару schema.label:
instance_id,user_id,sentiment.negative,sentiment.positive
doc_001,user_1,,true
doc_002,user_1,true,export_annotation_format: "csv"TSV
Значения, разделённые табуляцией:
export_annotation_format: "tsv"CLI экспорта
Новое в v2.2.0
CLI экспорта преобразует разметку Potato в специализированные форматы одной командой:
# List available export formats
python -m potato.export --list-formats
# Export to COCO format
python -m potato.export --config config.yaml --format coco --output ./export/
# Export to YOLO format
python -m potato.export --config config.yaml --format yolo --output ./export/
# Export with options
python -m potato.export --config config.yaml --format coco --output ./export/ \
--option split_ratio=0.8 --option include_unlabeled=falseПараметры CLI
| Параметр | Описание |
|---|---|
--config, -c | Путь к YAML-файлу конфигурации Potato |
--format, -f | Формат экспорта (coco, yolo, pascal_voc, conll_2003, conll_u, mask, parquet, eaf, textgrid) |
--output, -o | Каталог для результатов (по умолчанию ./export_output) |
--option | Параметр конкретного формата в виде key=value (можно указывать несколько раз) |
--list-formats | Вывести доступные форматы и завершить работу |
--verbose, -v | Включить подробное журналирование |
Поддерживаемые форматы экспорта
| Формат | ID | Лучше всего подходит для |
|---|---|---|
| COCO | coco | Детекция объектов, сегментация экземпляров |
| YOLO | yolo | Обучение моделей YOLO |
| Pascal VOC | pascal_voc | Детекция объектов (XML) |
| CoNLL-2003 | conll_2003 | NER, разметка последовательностей |
| CoNLL-U | conll_u | Частеречная разметка, синтаксический анализ зависимостей |
| Маски сегментации | mask | Семантическая сегментация и сегментация экземпляров |
| Apache Parquet | parquet | Колоночное хранение больших наборов данных |
| ELAN EAF | eaf | Временные шкалы разметки речи и видео |
| Praat TextGrid | textgrid | Фонетическая разметка и разметка речи |
Матрица совместимости форматов
| Тип разметки | COCO | YOLO | Pascal VOC | CoNLL-2003 | CoNLL-U | Mask |
|---|---|---|---|---|---|---|
| Ограничивающие рамки | Да | Да | Да | - | - | - |
| Полигоны | Да | - | - | - | - | Да |
| Ключевые точки | Да | - | - | - | - | - |
| Текстовые спаны | - | - | - | Да | Да | - |
| Классификация | Частично | - | - | - | - | - |
Программный экспорт
Реестр экспорта можно использовать прямо из Python:
from potato.export.registry import export_registry
from potato.export.cli import build_export_context
context = build_export_context("path/to/config.yaml")
result = export_registry.export("coco", context, "./output/")
if result.success:
print(f"Exported {len(result.files_written)} files")Собственные экспортёры
Собственный экспортёр создаётся наследованием от BaseExporter:
from potato.export.base import BaseExporter, ExportContext, ExportResult
class MyExporter(BaseExporter):
format_name = "my_format"
description = "My custom export format"
file_extensions = [".myformat"]
def can_export(self, context: ExportContext) -> tuple:
has_spans = any(ann.get("spans") for ann in context.annotations)
if not has_spans:
return False, "No span annotations found"
return True, None
def export(self, context: ExportContext, output_path: str,
options: dict = None) -> ExportResult:
# Perform the export
return ExportResult(
success=True,
format_name=self.format_name,
files_written=["output.myformat"],
stats={"annotations": len(context.annotations)}
)
from potato.export.registry import export_registry
export_registry.register(MyExporter())Форматы экспорта для NLP
Формат CoNLL
Стандартный формат для задач разметки последовательностей (NER, частеречная разметка):
The O
quick B-ADJ
brown I-ADJ
fox B-NOUN
jumps B-VERB
Potato сам пишет этот формат, так что скрипт преобразования не нужен:
python -m potato.export --config config.yaml --format conll_2003 --output ./export/Экспортёр токенизирует текст каждого объекта, переводит символьные смещения каждого спана в теги BIO и пишет annotations.conll с маркерами -DOCSTART- между документами. Схему спанов выбирает --option schema_name=entities, а токенизатор выбирает --option tokenization=whitespace.
Формат IOB2
Разметка Inside-Outside-Beginning для распознавания сущностей:
John B-PER
Smith I-PER
works O
at O
Google B-ORG
Формат spaCy
Экспортёра для spaCy нет, поэтому здесь нужен скрипт. Он берёт спаны из экспорта jsonl, а текст из файла данных, потому что экспорт содержит смещения, а не текст объекта:
import json
import spacy
from spacy.tokens import DocBin
def convert_to_spacy(annotations_file, data_file, output_file, scheme_name="entities"):
"""Convert a Potato jsonl export to a spaCy DocBin for NER training."""
with open(data_file) as f:
texts = {item["id"]: item["text"] for item in json.load(f)}
nlp = spacy.blank("en")
doc_bin = DocBin()
with open(annotations_file) as f:
for line in f:
record = json.loads(line)
text = texts.get(record["instance_id"])
if text is None:
continue
doc = nlp.make_doc(text)
ents = []
for span in record["spans"].get(scheme_name, []):
# start and end are character offsets into the item text,
# and the label name is under "name".
char_span = doc.char_span(
span["start"], span["end"], label=span["name"]
)
if char_span is not None:
ents.append(char_span)
doc.ents = ents
doc_bin.add(doc)
doc_bin.to_disk(output_file)
# Usage
convert_to_spacy(
"annotation_output/exports/jsonl/annotations.jsonl",
"data/items.json", "train.spacy", "entities"
)Экспорт содержит по одной записи на разметчика и объект, поэтому, если один и тот же объект размечали несколько человек, фильтруйте по user_id.
Результат можно сразу передать в spacy train:
python -m spacy train config.cfg --paths.train ./annotations.spacyHuggingFace Datasets
Собственный экспортёр Potato для HuggingFace отправляет данные прямо в Hub:
python -m potato.export --config config.yaml --format huggingface \
--output your-org/my-annotations --option token=hf_xxxЧтобы вместо этого собрать локальный набор данных, прочитайте экспорт jsonl и соедините его с файлом данных, чтобы получить текст:
import json
from datasets import Dataset
def convert_to_huggingface(annotations_file, data_file, output_dir, scheme_names):
"""Convert a Potato jsonl export to a HuggingFace Dataset."""
with open(data_file) as f:
texts = {item["id"]: item["text"] for item in json.load(f)}
records = []
with open(annotations_file) as f:
for line in f:
r = json.loads(line)
record = {
"id": r["instance_id"],
"user_id": r["user_id"],
"text": texts.get(r["instance_id"], ""),
}
for scheme in scheme_names:
# A single-choice scheme stores {label_name: "true"}.
chosen = [name for name, value in r["labels"].get(scheme, {}).items()
if value == "true"]
record[scheme] = chosen[0] if chosen else None
records.append(record)
dataset = Dataset.from_list(records)
dataset.save_to_disk(output_dir)
print(f"Saved {len(records)} examples to {output_dir}")
# Usage
convert_to_huggingface(
"annotation_output/exports/jsonl/annotations.jsonl",
"data/items.json", "hf_dataset/", ["sentiment"]
)Загрузка в обучающем скрипте:
from datasets import load_from_disk
dataset = load_from_disk("hf_dataset/")Форматы экспорта для компьютерного зрения
Формат COCO
Стандартный формат для детекции объектов и сегментации:
{
"images": [
{"id": 1, "file_name": "image_001.jpg", "width": 640, "height": 480}
],
"annotations": [
{
"id": 1,
"image_id": 1,
"category_id": 1,
"bbox": [100, 150, 200, 300],
"area": 60000,
"segmentation": [[100, 150, 300, 150, 300, 450, 100, 450]]
}
],
"categories": [
{"id": 1, "name": "person"}
]
}Используйте экспортёр, а не собственный скрипт:
python -m potato.export --config config.yaml --format coco --output ./export/Браузер хранит координаты каждой фигуры нормализованными относительно изображения в ключе coordinates, поэтому скрипт, который читает сохранённые числа как пиксели, ставит каждую рамку в неверном масштабе. Экспортёр умножает их на размер изображения, переводит полигоны, эллипсы и маски кисти в сегментацию COCO и задаёт iscrowd. Размер в пикселях он берёт из image_width и image_height объекта, из собственного размера RLE маски или из файла изображения и выводит предупреждение, если определить размер не удаётся.
Формат YOLO
Один текстовый файл на изображение для обучения YOLO:
# class_id center_x center_y width height (normalized 0-1)
0 0.5 0.5 0.3 0.4
1 0.2 0.3 0.1 0.2
Potato пишет эти файлы за вас:
python -m potato.export --config config.yaml --format yolo --output ./export/Он пишет по одному файлу labels/<image>.txt на изображение со строками class_id cx cy w h, файл classes.txt с именами классов в порядке их id и data.yaml для Ultralytics. YOLO нужен размер каждого изображения в пикселях, поэтому, если какое-то измерение отсутствует, экспорт сразу отклоняется, а не пишет координаты относительно кадра нулевого размера.
Формат Pascal VOC
Формат XML, которым пользуются многие фреймворки детекции:
<annotation>
<filename>image_001.jpg</filename>
<size>
<width>640</width>
<height>480</height>
</size>
<object>
<name>person</name>
<bndbox>
<xmin>100</xmin>
<ymin>150</ymin>
<xmax>300</xmax>
<ymax>450</ymax>
</bndbox>
</object>
</annotation>Формат pascal_voc пишет по одному XML-файлу на изображение:
python -m potato.export --config config.yaml --format pascal_voc --output ./export/Координаты выводятся в абсолютных пикселях xmin, ymin, xmax, ymax, пересчитанных из нормализованных значений, которые хранит браузер.
Экспорт движка разметки
Новое в v2.11.0
Формат labeling_engine пишет итоговые метки движка разметки для каждого объекта, машинные и человеческие, с происхождением каждой метки, а также сертификат качества, карточку набора данных и журнал вызовов моделей. Он отказывается работать, если в конфигурации labeling_engine.enabled не равно true:
python -m potato.export --config config.yaml --format labeling_engine --output ./export/potato label export пишет те же файлы. Столбцы перечислены в разделе Файлы, которые записывает экспорт. Когда движок включён, экспорты разметки людей в форматах csv, parquet и huggingface также получают столбцы engine_purpose и engine_request, которые показывают, на какой запрос движка отвечала каждая метка. Фильтр по engine_purpose отделяет слепую случайную выборку от ответов, которые люди дали, увидев машинную метку.
Собственные скрипты экспорта
Базовый скрипт экспорта
Экспорт jsonl уже объединяет всех разметчиков в один файл, поэтому скрипт начинается с его загрузки. Каждая строка представляет собой одну запись для одного разметчика и одного объекта.
import json
def load_annotations(annotations_file):
"""Load a Potato jsonl export into a list of records."""
with open(annotations_file) as f:
return [json.loads(line) for line in f]
# Usage
records = load_annotations("annotation_output/exports/jsonl/annotations.jsonl")Агрегирование нескольких разметчиков
Голосование большинством по записям, загруженным выше. Схема с одним вариантом ответа хранит ответ как {label_name: "true"}, так что выбранная метка соответствует ключу со значением "true".
from collections import Counter
def chosen_label(record, scheme_name):
"""The label an annotator selected for a single-choice scheme, or None."""
for name, value in record["labels"].get(scheme_name, {}).items():
if value == "true":
return name
return None
def aggregate_labels(records, scheme_name):
"""Majority vote aggregation for classification tasks."""
item_labels = {}
for record in records:
label = chosen_label(record, scheme_name)
if label is not None:
item_labels.setdefault(record["instance_id"], []).append(label)
return {item_id: Counter(labels).most_common(1)[0][0]
for item_id, labels in item_labels.items()}Расчёт согласованности разметчиков
Сгруппируйте те же записи по user_id и оцените объекты, которые разметили оба разметчика.
from sklearn.metrics import cohen_kappa_score
def compute_agreement(records, scheme_name, user_a, user_b):
"""Compute Cohen's Kappa between two annotators on their shared items."""
by_user = {}
for record in records:
label = chosen_label(record, scheme_name)
if label is not None:
by_user.setdefault(record["user_id"], {})[record["instance_id"]] = label
a, b = by_user.get(user_a, {}), by_user.get(user_b, {})
common_ids = sorted(set(a) & set(b))
return cohen_kappa_score([a[i] for i in common_ids],
[b[i] for i in common_ids])Рекомендации
1. Экспортируйте регулярно
Настройте автоматический экспорт для резервного копирования и анализа:
Potato сам пишет периодический экспорт, поэтому задача резервного копирования лишь копирует то, что уже есть, а не создаёт его:
import json
import shutil
import schedule
from datetime import date
EXPORT = "annotation_output/exports/jsonl/annotations.jsonl"
def daily_backup():
shutil.copy(EXPORT, f"backups/annotations_{date.today()}.jsonl")
schedule.every().day.at("18:00").do(daily_backup)2. Добавляйте метаданные
Сохраняйте контекст в экспорте:
from datetime import datetime
records = load_annotations("annotation_output/exports/jsonl/annotations.jsonl")
export_data = {
"metadata": {
"task": "Sentiment Analysis",
"exported_at": datetime.now().isoformat(),
"total_records": len(records),
"annotators": sorted({r["user_id"] for r in records}),
},
"records": records,
}3. Проверяйте экспорт
Проверка целостности экспорта:
def validate_export(export_file, expected_records):
"""The jsonl export is one record per annotator per instance."""
records = load_annotations(export_file)
assert len(records) == expected_records, "Missing records"
assert all("instance_id" in r and "user_id" in r for r in records), "Missing keys"
print(f"Export validated: {len(records)} records")4. Версионируйте экспорт
Используйте метки времени или номера версий:
exports/
annotations_v1_2024-01-15.json
annotations_v2_2024-01-20.json
annotations_final_2024-01-25.json
Примеры интеграции
Обучение модели HuggingFace
Запись содержит instance_id, user_id, labels, spans и links. Текста объекта
в ней нет, поэтому текст берётся из файла данных, который вы дали Potato, и
присоединяется по id объекта:
import json
from datasets import Dataset
from transformers import AutoTokenizer, AutoModelForSequenceClassification
records = load_annotations("annotation_output/exports/jsonl/annotations.jsonl")
labels = aggregate_labels(records, "sentiment")
with open("data/items.json") as f:
text_by_id = {item["id"]: item["text"] for item in json.load(f)}
dataset = Dataset.from_list([
{"text": text_by_id[item_id], "label": label}
for item_id, label in labels.items()
if item_id in text_by_id
])
tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased")
model = AutoModelForSequenceClassification.from_pretrained(
"bert-base-uncased", num_labels=3)Обучение NER в spaCy
Ключи record["spans"] представляют собой имена схем. Каждый спан содержит start, end и
name (метка лежит в name, а не в label), а экспортёр jsonl добавляет
покрытый спаном text, вырезанный из объекта, на котором спан был выделен:
import json
import spacy
from spacy.tokens import DocBin
records = load_annotations("annotation_output/exports/jsonl/annotations.jsonl")
with open("data/items.json") as f:
text_by_id = {item["id"]: item["text"] for item in json.load(f)}
nlp = spacy.blank("en")
doc_bin = DocBin()
for record in records:
text = text_by_id.get(record["instance_id"])
if text is None:
continue
doc = nlp.make_doc(text)
ents = []
for span in record["spans"].get("entities", []):
ent = doc.char_span(span["start"], span["end"], label=span["name"])
if ent:
ents.append(ent)
doc.ents = ents
doc_bin.add(doc)
doc_bin.to_disk("./train.spacy")Обучение YOLO
# After exporting to YOLO format
yolo train data=dataset.yaml model=yolov8n.pt epochs=100dataset.yaml:
train: ./images/train
val: ./images/val
nc: 3
names: ['person', 'car', 'dog']