Label Studio에서 Potato로 마이그레이션하기
이 단계별 가이드로 Label Studio에서 Potato로 마이그레이션하면서 프로젝트 설정, 주석 스키마, 내보낸 데이터를 변환하고 일반적인 주석 유형을 모두 다룹니다.
이 가이드는 기존 Label Studio 프로젝트를 Potato로 옮기는 과정을 차근차근 설명합니다. 먼저 일러둘 것이 있습니다. 공식 마이그레이션 도구는 없습니다. 설정은 직접 손으로 변환하고 데이터를 재구성하려면 약간의 파이썬 코드를 작성해야 하므로, 두 플랫폼 모두에 익숙해야 합니다.
기능을 나란히 비교한 내용과 Potato 자체 마이그레이션 도구는 원본 문서와 마이그레이션 CLI 문서를 참고하십시오.
왜 마이그레이션해야 할까요?
어떤 프로젝트에는 Potato가 더 잘 맞습니다. Potato는 학술 주석 연구를 위해 만들어졌고, Prolific 및 MTurk 연동을 기본으로 제공하며, 띄울 데이터베이스 없이 YAML로 설정합니다. 파이썬으로 확장하기 쉽고, 저장소가 그냥 파일이기 때문에 배포하기도 쉽습니다.
마이그레이션 개요
이 과정은 수동으로 진행되며 대략 다음과 같이 흘러갑니다.
- Label Studio XML 템플릿을 Potato YAML 설정으로 직접 변환합니다
- 데이터 형식을 변환하는 파이썬 스크립트를 작성합니다(JSON에서 JSONL로)
- 기존 주석을 마이그레이션하는 스크립트를 작성합니다(있는 경우)
- 충분히 테스트하고 변환된 데이터를 검증합니다
템플릿 변환
텍스트 분류
Label Studio XML:
<View>
<Text name="text" value="$text"/>
<Choices name="sentiment" toName="text" choice="single">
<Choice value="Positive"/>
<Choice value="Negative"/>
<Choice value="Neutral"/>
</Choices>
</View>Potato YAML:
annotation_task_name: "Sentiment Classification"
data_files:
- "data/items.jsonl"
item_properties:
id_key: id
text_key: text
annotation_schemes:
- annotation_type: radio
name: sentiment
description: "What is the sentiment?"
labels:
- name: positive
tooltip: "Positive sentiment"
- name: negative
tooltip: "Negative sentiment"
- name: neutral
tooltip: "Neutral sentiment"다중 레이블 분류
Label Studio XML:
<View>
<Text name="text" value="$text"/>
<Choices name="topics" toName="text" choice="multiple">
<Choice value="Politics"/>
<Choice value="Sports"/>
<Choice value="Technology"/>
<Choice value="Entertainment"/>
</Choices>
</View>Potato YAML:
annotation_schemes:
- annotation_type: multiselect
name: topics
description: "Select all relevant topics"
labels:
- name: politics
tooltip: "Politics content"
- name: sports
tooltip: "Sports content"
- name: technology
tooltip: "Technology content"
- name: entertainment
tooltip: "Entertainment content"개체명 인식
Label Studio XML:
<View>
<Labels name="entities" toName="text">
<Label value="PERSON" background="#FFC0CB"/>
<Label value="ORG" background="#90EE90"/>
<Label value="LOCATION" background="#ADD8E6"/>
</Labels>
<Text name="text" value="$text"/>
</View>Potato YAML:
annotation_schemes:
- annotation_type: span
name: entities
description: "Select entity spans in the text"
labels:
- name: PERSON
tooltip: "Person names"
- name: ORG
tooltip: "Organization names"
- name: LOCATION
tooltip: "Location names"참고: Potato의 스팬 주석은 Label Studio와 다른 방식으로 강조 표시할 수 있습니다. 변환한 설정을 테스트하여 표시 방식이 요구 사항에 맞는지 확인하십시오.
이미지 분류
Label Studio XML:
<View>
<Image name="image" value="$image_url"/>
<Choices name="category" toName="image">
<Choice value="Cat"/>
<Choice value="Dog"/>
<Choice value="Other"/>
</Choices>
</View>Potato YAML:
data_files:
- "data/images.jsonl"
item_properties:
id_key: id
text_key: image_url
annotation_schemes:
- annotation_type: radio
name: category
description: "What animal is in the image?"
labels:
- name: cat
tooltip: "Cat"
- name: dog
tooltip: "Dog"
- name: other
tooltip: "Other animal"바운딩 박스 주석
Label Studio XML:
<View>
<Image name="image" value="$image_url"/>
<RectangleLabels name="objects" toName="image">
<Label value="Car"/>
<Label value="Person"/>
<Label value="Bicycle"/>
</RectangleLabels>
</View>Potato YAML:
annotation_schemes:
- annotation_type: bounding_box
name: objects
description: "Draw boxes around objects"
labels:
- name: car
tooltip: "Car"
- name: person
tooltip: "Person"
- name: bicycle
tooltip: "Bicycle"참고: Potato의 바운딩 박스 지원은 Label Studio와 다를 수 있습니다. 현재 지원되는 기능은 문서를 확인하십시오.
평가 척도
Label Studio XML:
<View>
<Text name="text" value="$text"/>
<Rating name="quality" toName="text" maxRating="5"/>
</View>Potato YAML:
annotation_schemes:
- annotation_type: likert
name: quality
description: "Rate the quality"
size: 5
labels:
- name: "1"
tooltip: "Poor"
- name: "2"
tooltip: "Below average"
- name: "3"
tooltip: "Average"
- name: "4"
tooltip: "Good"
- name: "5"
tooltip: "Excellent"데이터 형식 변환
Label Studio JSON에서 Potato JSONL로
Label Studio 형식:
[
{
"id": 1,
"data": {
"text": "This is great!",
"meta_info": "source1"
}
},
{
"id": 2,
"data": {
"text": "This is terrible.",
"meta_info": "source2"
}
}
]Potato JSONL 형식:
{"id": "1", "text": "This is great!", "metadata": {"source": "source1"}}
{"id": "2", "text": "This is terrible.", "metadata": {"source": "source2"}}변환 스크립트
import json
def convert_label_studio_to_potato(ls_file, potato_file):
"""Convert Label Studio JSON to Potato JSONL"""
with open(ls_file, 'r') as f:
ls_data = json.load(f)
with open(potato_file, 'w') as f:
for item in ls_data:
potato_item = {
"id": str(item["id"]),
"text": item["data"].get("text", ""),
}
# Convert nested data fields
if "data" in item:
for key, value in item["data"].items():
if key != "text":
if "metadata" not in potato_item:
potato_item["metadata"] = {}
potato_item["metadata"][key] = value
# Handle image URLs
if "image" in item.get("data", {}):
potato_item["image_url"] = item["data"]["image"]
f.write(json.dumps(potato_item) + "\n")
print(f"Converted {len(ls_data)} items")
# Usage
convert_label_studio_to_potato("label_studio_export.json", "data/items.jsonl")주석 마이그레이션
기존 주석 변환하기
def convert_annotations(ls_export, potato_output):
"""Convert Label Studio annotations to Potato format"""
with open(ls_export, 'r') as f:
ls_data = json.load(f)
with open(potato_output, 'w') as f:
for item in ls_data:
if "annotations" not in item or not item["annotations"]:
continue
for annotation in item["annotations"]:
potato_ann = {
"id": str(item["id"]),
"text": item["data"].get("text", ""),
"annotations": {},
"annotator": annotation.get("completed_by", {}).get("email", "unknown"),
"timestamp": annotation.get("created_at", "")
}
# Convert results
for result in annotation.get("result", []):
scheme_name = result.get("from_name", "unknown")
if result["type"] == "choices":
# Classification
potato_ann["annotations"][scheme_name] = result["value"]["choices"][0]
elif result["type"] == "labels":
# NER spans
if scheme_name not in potato_ann["annotations"]:
potato_ann["annotations"][scheme_name] = []
potato_ann["annotations"][scheme_name].append({
"start": result["value"]["start"],
"end": result["value"]["end"],
"label": result["value"]["labels"][0],
"text": result["value"]["text"]
})
elif result["type"] == "rating":
potato_ann["annotations"][scheme_name] = result["value"]["rating"]
f.write(json.dumps(potato_ann) + "\n")
# Usage
convert_annotations("ls_annotated_export.json", "annotations/migrated.jsonl")스팬 주석 변환
Label Studio는 문자 오프셋을 사용하고 Potato도 문자 오프셋을 사용하므로 변환이 간단합니다:
def convert_spans(ls_spans):
"""Convert Label Studio span format to Potato format"""
potato_spans = []
for span in ls_spans:
potato_spans.append({
"start": span["value"]["start"],
"end": span["value"]["end"],
"label": span["value"]["labels"][0],
"text": span["value"]["text"]
})
return potato_spans기능 매핑
| Label Studio | Potato |
|---|---|
| Choices (single) | radio |
| Choices (multiple) | multiselect |
| Labels | span |
| Rating | likert |
| TextArea | text |
| RectangleLabels | bounding_box |
| PolygonLabels | polygon |
| Taxonomy | (use nested multiselect) |
| Pairwise | comparison |
품질 관리
마이그레이션에서 이 부분은 잃는 게 아니라 얻는 쪽입니다. Label Studio 커뮤니티 버전에는 일치도 지표가 아예 없고, 정답 데이터 표시, 검토자 배정, 품질 대시보드는 모두 유료 플랜에 있습니다. Potato에서는 전부 설정 키입니다.
주의 점검은 데이터 파일에 몰래 끼워 넣는 것이 아니라 정식 기능입니다. Potato가 알아서 삽입하고 누가 틀렸는지 기록합니다.
attention_checks:
enabled: true
items_file: "attention_checks.json"
frequency: 10
min_response_time: 3.0정답 데이터도 같은 방식으로, 답을 이미 아는 항목으로 어노테이터를 채점합니다.
gold_standards:
enabled: true
items_file: "gold_standards.json"어노테이터 간 일치도는 알아서 계산됩니다. 공통 부분집합에서 어노테이터를 겹치게 하고 켜기만 하면 됩니다. Krippendorff α가 관리자 대시보드에 나오므로 scikit-learn으로 따로 스크립트를 쓸 일이 없습니다.
num_annotators_per_item: 3
agreement_metrics:
enabled: true라벨의 불확실성은 한 걸음 더 나가고 싶을 때의 기능이고, Label Studio에는 어느 플랜에도 대응하는 것이 없습니다. Potato는 어노테이션에 문항반응이론 모델을 적합시켜 각 라벨을 사후분포와 신뢰구간과 함께 돌려주며, 표를 세는 대신 어노테이터가 실제로 보여준 신뢰도로 가중합니다.
psychometrics:
enabled: true
schema: sentiment
confidence_threshold: 0.95전체 옵션은 품질 관리, 어노테이터 간 일치도, 사이코메트릭스 엔진을 보세요.
사용자 마이그레이션
Label Studio에서 사용자 내보내기
# Label Studio API call to get users
import requests
def export_ls_users(ls_url, api_key):
response = requests.get(
f"{ls_url}/api/users",
headers={"Authorization": f"Token {api_key}"}
)
return response.json()Potato 사용자 설정 만들기
user_config:
# Simple auth for migrated users
auth_type: password
user_config:
- username: user1@example.com
password_hash: "..." # Generate new passwords
- username: user2@example.com
password_hash: "..."마이그레이션 테스트
검증 스크립트
def validate_migration(original_ls, converted_potato):
"""Validate converted data matches original"""
with open(original_ls) as f:
ls_data = json.load(f)
with open(converted_potato) as f:
potato_data = [json.loads(line) for line in f]
# Check item count
assert len(ls_data) == len(potato_data), "Item count mismatch"
# Check IDs preserved
ls_ids = {str(item["id"]) for item in ls_data}
potato_ids = {item["id"] for item in potato_data}
assert ls_ids == potato_ids, "ID mismatch"
# Check text content
for ls_item, potato_item in zip(
sorted(ls_data, key=lambda x: x["id"]),
sorted(potato_data, key=lambda x: x["id"])
):
assert ls_item["data"]["text"] == potato_item["text"], \
f"Text mismatch for item {ls_item['id']}"
print("Validation passed!")
validate_migration("label_studio_export.json", "data/items.jsonl")마이그레이션 체크리스트
- Label Studio에서 데이터 내보내기(JSON 형식)
- 템플릿 XML을 Potato YAML로 직접 변환
- 데이터 형식을 변환하는 파이썬 스크립트 작성 및 실행(JSON에서 JSONL로)
- 기존 주석을 변환하는 스크립트 작성 및 실행(있는 경우)
- Potato 프로젝트 구조 설정
- 샘플 데이터로 테스트
- 변환된 데이터가 원본과 일치하는지 검증
- 새 인터페이스에 대해 안노테이터 교육
- 파일럿 주석 배치 실행
흔히 발생하는 문제
몇 가지가 사람들을 자주 걸려 넘어지게 합니다. 두 도구 모두 UTF-8을 사용하지만, 그래도 데이터에 인코딩 오류가 없는지 확인할 가치가 있습니다. 로컬 이미지 경로는 보통 URL로 바꾸거나 적어도 Potato가 기대하는 형식에 맞춰야 합니다. Label Studio에서 만든 커스텀 컴포넌트는 Potato 커스텀 템플릿으로 다시 만들어야 합니다. 그리고 Label Studio API를 대상으로 스크립트를 작성했다면, 그 스크립트는 대신 Potato의 API를 가리키도록 바꿔야 합니다.