본문으로 건너뛰기
튜토리얼19분 분량

스프레드시트 라벨링 작업을 Potato로 옮기기

라벨이 달린 행의 CSV를 Potato에 불러오고, 두 어노테이터가 이전 라벨을 보지 않고 행을 다시 라벨링하게 한 뒤, 어노테이터별로 열 하나씩을 돌려받습니다.

Potato Team

Excel이나 Google Sheets에서 행에 라벨을 다는 팀은 이 튜토리얼을 따라 작업을 Potato 2.13.1로 옮길 수 있으며, 두 어노테이터가 서로의 라벨도, 시트에 이미 있는 라벨도 보지 않고 문장 40개를 다시 라벨링합니다. 끝에는 두 어노테이터의 일치도 수치, 시트의 라벨과의 비교, 문장당 한 행과 어노테이터당 한 열로 된 CSV가 생기며, 이 CSV는 같은 스프레드시트 프로그램에서 열립니다. 스프레드시트는 한 사람이 수백 행에 라벨을 다는 데 잘 맞으며, 어노테이션 도구를 만들 것인가 살 것인가는 두 번째 사람이 합류하면 스프레드시트가 어디에서 한계에 부딪히는지 설명합니다. 어노테이션 도구를 써 본 적이 없어도 괜찮습니다.

시트

문장은 금융 뉴스의 문장 4,840개로 이루어진 Financial PhraseBank에서 가져왔습니다(참고문헌 1). 금융 배경을 가진 16명이 각 문장을 투자자 입장에서 긍정, 부정, 중립으로 라벨링했으며, 문장당 어노테이터는 5명에서 8명이었습니다. 이 릴리스는 CC BY-NC-SA 3.0 라이선스입니다. 이 문장들로 만든 시트는 출처를 밝히고 같은 라이선스로 비상업적 용도로만 공유할 수 있습니다.

이 튜토리얼의 시트에는 무작위로 뽑은 문장 40개가 있습니다. 20개는 원래의 모든 어노테이터가 일치한 문장의 릴리스 파일에서, 20개는 50%에서 66%만 일치한 문장에서 가져왔습니다. label 열에는 릴리스의 다수 라벨이 들어 있으며, 동료가 시트에 이미 입력한 라벨을 대신합니다. agreement 열은 각 문장이 어느 그룹에서 왔는지 기록합니다. 시트의 세 행은 다음과 같습니다.

csv
id,sentence,label,agreement
s08,"Finnish financial group Aktia reports operating profit of EUR 44.4 mn in January-September 2009 , up from EUR 37.3 mn in the corresponding period in 2008 .",positive,100%
s16,"It generated an operating loss of EUR 96.3 mn , down from a profit of EUR 43.9 mn .",negative,100%
s35,It is the most awkward thing to use if you 're holding the phone with one hand .,negative,50-65%

직접 만든 시트에는 모든 행에서 값이 다른 id 열이 필요합니다. 시트에 ID 열이 없다면 행 번호나 s01 같은 코드로 이루어진 열을 추가하세요.

시트를 CSV로 저장하기

Potato는 CSV, TSV, JSON, JSON Lines, Parquet 파일을 읽으며(데이터 형식), Excel 통합 문서는 읽지 않습니다. Google Sheets에서는 파일, 다운로드, 쉼표로 구분된 값(.csv)을 선택하세요. Excel에서는 형식을 "CSV UTF-8"로 해서 다른 이름으로 저장하세요. 설정에 통합 문서를 지정하면 서버가 Unsupported input file format xlsx로 멈춥니다. Python으로 통합 문서를 변환하려면 Potato가 설치하는 pandas와 openpyxl(pip install openpyxl)을 쓰세요.

python
import pandas as pd
 
pd.read_excel("sheet.xlsx", dtype=str).to_csv("sheet.csv", index=False)

dtype=str은 모든 셀을 텍스트로 읽습니다. 이 옵션이 없으면 pandas는 빈 셀이 하나 있는 열에서 ID 007을 숫자 7.0으로 읽습니다. Potato는 서버를 시작할 때 파일의 문제를 보고하며, 테스트 실행에서 아래 문제를 각각 겪었습니다.

  • 악센트가 있는 글자는 서버를 UnicodeDecodeError로 멈추게 합니다. Excel의 일반 "CSV" 형식은 오래된 Windows 인코딩으로 씁니다. "CSV UTF-8"로 저장하거나, data_files 항목을 - {path: data/items.csv, encoding: cp1252}로 써서 설정에 인코딩을 지정하세요.
  • 맨 아래의 빈 행은 Empty ID in column 'id'를 일으킵니다. 저장하기 전에 시트의 빈 행을 삭제하세요.
  • 셀 사이의 세미콜론은 셀 수 오류를 일으킵니다. 일부 유럽 로케일의 Excel은 셀을 세미콜론으로 구분합니다. 구분자를 바꾸거나 Google Sheets에서 내보내세요.
  • 대문자 .CSV 확장자는 2.12.1에서 거부되었습니다. Potato 2.13.1은 파일을 받아들이며, 2.12.1에서는 파일 이름이 .csv로 끝나도록 바꾸세요.

이전 라벨을 어노테이터에게 보이지 않게 하기

Potato는 행의 모든 열을 어노테이터의 브라우저로 보내며, 행 전체가 어노테이션 페이지의 페이지 소스에 들어 있습니다. 테스트 실행에서는 페이지가 문장만 보여 주는데도 모든 항목의 페이지 소스에서 시트의 label 값이 발견되었습니다. 이전 라벨을 어노테이터에게 보이지 않게 하려면 Potato에 id와 sentence 열만 있는 파일을 주고, 나중의 비교를 위해 sheet.csv는 보관하세요.

python
import pandas as pd
 
pd.read_csv("sheet.csv", dtype=str)[["id", "sentence"]].to_csv("data/items.csv", index=False)

스프레드시트에서 파일을 만들려면 탭을 복사하고, 사본에서 라벨 열을 삭제하고, 사본을 CSV로 다운로드하세요.

Potato 2.13.1부터는 item_properties.private_keys: [label]이 열을 어노테이터에게 보이지 않게 하는 또 다른 방법입니다. 그러면 Potato는 브라우저로 보내는 모든 것에서 label 열을 빼고, 열은 내보내기에 남습니다.

설정

Potato를 설치하고 sheet.csv, data/items.csv, 아래 설정이 있는 프로젝트 폴더를 만드세요.

bash
pip install potato-annotation==2.13.1

설정은 완전하며 2.12.1과 2.13.1에서 python -m potato.validate_cli config.yaml --strict를 통과합니다.

yaml
annotation_task_name: Financial news sentiment
task_dir: .
output_annotation_dir: annotation_output/
data_files:
  - data/items.csv
item_properties:
  id_key: id
  text_key: sentence
 
user_config:
  allow_all_users: true
  users: []
 
num_annotators_per_item: 2
 
annotation_schemes:
  - annotation_type: radio
    name: sentiment
    description: Would this news move the company's share price up, down, or not at all?
    labels: [positive, neutral, negative]

id_key와 text_key는 data/items.csv의 두 열을 지정합니다. num_annotators_per_item: 2는 모든 문장을 두 어노테이터에게 보냅니다. radio 스킴은 각 문장에 세 라벨 중 하나의 답을 줍니다. 라벨 이름은 시트의 label 열의 철자와 일치하므로, 두 라벨 집합은 다시 코딩하지 않고 비교됩니다. 어노테이션 스킴은 다른 질문 유형을 나열하고, 설정 기본은 나머지 키를 설명합니다.

라벨링

프로젝트 폴더에서 서버를 시작합니다.

bash
potato start config.yaml -p 8000

각 어노테이터는 http://localhost:8000을 열고 사용자 이름과 비밀번호로 등록한 뒤, 한 번에 문장 하나와 그 아래의 세 라벨을 봅니다. 1, 2, 3 키가 라벨을 고르고, 다음이 답을 저장합니다. 다른 컴퓨터의 동료에게는 그 컴퓨터가 닿을 수 있는 주소의 서버가 필요하며, VPS에 Potato 배포하기가 이를 설명합니다.

이 페이지의 수치는 Potato 2.12.1에서 스크립트로 작동하는 어노테이터 ana와 ben이 헤드리스 브라우저의 어노테이션 페이지를 통해 문장 40개를 모두 라벨링한 실행에서 나왔습니다. 첫 번째 어노테이터의 라벨은 각 문장을 읽고 정했습니다. 두 번째 어노테이터의 라벨은 따로 작성되어 불확실한 문장에서 첫 번째와 다릅니다. 따라서 수치는 모의 어노테이터 두 명을 나타낼 뿐 실제 어노테이터가 쓸 때 이 스킴의 신뢰도에 대해서는 아무것도 말해 주지 않습니다.

두 어노테이터의 일치도

관리자 페이지는 라벨링이 진행되는 동안에도 일치도를 보고합니다. http://localhost:8000/admin/iaa를 열고, 누군가 처음 관리자 페이지를 요청할 때 Potato가 프로젝트 폴더의 admin_api_key.txt에 쓰는 관리자 키를 입력하세요. 스크립트 어노테이터 두 명으로 이 페이지는 두 어노테이터가 라벨링한 항목 40개, Krippendorff의 알파 0.580, Cohen의 카파 0.575, 문장의 75%에서의 일치를 보고했습니다. Cohen의 카파와 Krippendorff의 알파는 모두 두 어노테이터가 우연히 도달했을 일치도를 제외합니다. 어노테이터 간 일치도가 두 계수를 읽는 방법을 설명합니다.

보고서를 위해서는 라벨링이 끝난 뒤 신뢰도 내보내기를 한 번 실행하세요.

bash
python -m potato.export --config config.yaml --format reliability --output export/

이 명령은 export/reliability.csv와 export/reliability.md를 씁니다. 스크립트 어노테이터 두 명의 표는 다음과 같습니다.

text
| Variable | Level | Krippendorff's α | 95% CI | % agreement | Units | Coders |
|---|---|---|---|---|---|---|
| sentiment | nominal | 0.58 | [0.303, 0.8] | 75.0 | 40 | 2 |

문장 40개는 작은 표본이므로 구간이 0.303에서 0.8까지입니다. 표본이 커지면 구간이 좁아집니다.

시트의 배치로 되돌린 라벨

Potato의 CSV 내보내기는 어노테이터와 문장 한 쌍당 한 행이며 라벨마다 열이 하나씩 있습니다.

bash
python -m potato.export --config config.yaml --format csv --output export/

이 명령은 문장 40개에 대해 80행의 export/annotations.csv를 씁니다.

csv
instance_id,user_id,sentiment.positive,sentiment.negative,sentiment.neutral
s27,ana,positive,,

스프레드시트 사용자는 보통 원래 시트의 배치를 원합니다. 아래 스크립트는 세 라벨 열을 sentiment 열 하나로 합치고, 각 어노테이터를 열로 바꾸고, 그 열을 원래 시트 옆에 놓습니다.

python
"""One row per sentence, one column per annotator, next to the original sheet."""
import pandas as pd
 
sheet = pd.read_csv("sheet.csv", dtype=str)
potato = pd.read_csv("export/annotations.csv", dtype=str)
 
# Potato writes one column per option (sentiment.positive, ...); keep whichever is filled
potato["sentiment"] = potato.filter(like="sentiment.").bfill(axis=1).iloc[:, 0]
wide = potato.pivot(index="instance_id", columns="user_id", values="sentiment")
 
out = sheet.merge(wide, left_on="id", right_index=True, how="left")
out.to_csv("labels_by_annotator.csv", index=False)

프로젝트 폴더에서 python back_to_sheet.py로 스크립트를 실행하세요. labels_by_annotator.csv에는 시트의 열 뒤에 ana와 ben이 있습니다.

text
 id   label agreement      ana      ben
s01 neutral    50-65%  neutral positive
s02 neutral    50-65% negative  neutral
s03 neutral      100%  neutral  neutral
s04 neutral    50-65% positive positive

--format tsv 내보내기는 탭을 기대하는 스크립트를 위해 같은 행을 export/annotations.tsv로 씁니다.

시트와의 일치도

이 사이트의 일치도 계산기는 첫 번째 열에 항목이, 그 뒤로 열마다 어노테이터가 하나씩 있는 표를 읽습니다. labels_by_annotator.csv의 id, label, ana 열을 계산기에 붙여 넣은 다음, id, label, ben 열을 붙여 넣으세요. 테스트 실행은 다음 값을 냈습니다.

시트와의 비교Krippendorff의 αCohen의 κ같은 라벨
ana0.7450.74340개 중 34개
ben0.5630.55840개 중 30개

ana, ben, 시트 사이에 어떤 불일치라도 있는 13개 행은 모두 원래 어노테이터들의 의견이 갈렸던 20개 문장에서 나왔습니다. 원래 어노테이터가 모두 일치한 20개 문장에서는 ana, ben, 시트가 매번 같은 라벨을 주었습니다. 세 행에서 ana와 ben은 서로 일치하고 시트와는 일치하지 않았습니다. 그 세 행에서 시트의 라벨이 가장 먼저 확인할 대상입니다. 더 큰 시트에서 그런 행을 찾으려면 labels_by_annotator.csv를 스프레드시트에서 열고 ana나 ben 열이 label과 다른 행을 필터링하세요.

시트의 라벨을 Potato로 가져오기

위의 단계에서는 시트의 라벨이 Potato 밖에 머물고 두 어노테이터는 시트와 독립적으로 라벨링합니다. 시트의 라벨로 계속 작업하려는 팀에게는 다른 경로가 둘 있습니다.

시트의 라벨을 시작 답변으로. 시트와의 일치도를 재는 것이 아니라 시트를 검토하고 고치는 것이 목표일 때, Potato는 모든 문장에서 시트의 라벨을 미리 선택해 둘 수 있습니다. 아래 스크립트는 라벨을 행 ID를 키로 data/prelabels.json에 씁니다.

python
import csv
import json
 
with open("sheet.csv", encoding="utf-8") as f:
    prelabels = {row["id"]: {"sentiment": row["label"]} for row in csv.DictReader(f) if row["label"]}
 
with open("data/prelabels.json", "w", encoding="utf-8") as f:
    json.dump(prelabels, f, indent=1)

위 설정에 이 블록을 추가하세요.

yaml
pre_annotation:
  enabled: true
  predictions_file: data/prelabels.json

테스트 실행에서는 각 문장이 열릴 때 40개 문장 모두에서 시트의 라벨이 이미 선택되어 있었습니다. 답을 바꾸지 않고 다음을 클릭하는 어노테이터는 시트의 라벨을 자신의 라벨로 저장합니다. 따라서 사전 라벨링된 문장에서 시트와의 일치도는 독립적인 판단 사이의 일치도가 아닙니다. Potato 2.12.1은 pre_annotation이 Potato 3.0에서 제거될 예정이라는 안내를 출력했습니다. Potato 2.13.1은 pre_annotation이 Potato의 가져오기가 기존 답변을 들여오는 방식이므로 이 안내를 없앴습니다. 라벨링 엔진에는 계약이 보증해야 하는 라벨을 위한 static 소스가 있습니다.

시트의 열을 어노테이터로. 사람마다 열이 하나인 시트는 그 사람들의 저장된 작업으로 Potato에 들어올 수 있습니다. 그러면 관리자 페이지와 신뢰도 내보내기에 시트에서 라벨링한 사람들이 포함됩니다. 이 경로는 Potato의 Label Studio 가져오기를 거칩니다(프로젝트 가져오기). 아래 스크립트는 지정한 각 열을 Label Studio의 내보내기 형식에서 한 사람의 답변으로 씁니다.

python
"""Write each annotator column of sheet.csv as that person's Label Studio annotations."""
import csv
import json
import sys
 
columns = sys.argv[1:]  # the sheet's annotator columns
tasks = []
with open("sheet.csv", encoding="utf-8") as f:
    for row in csv.DictReader(f):
        tasks.append({
            "id": row["id"],
            "data": {"sentence": row["sentence"]},
            "annotations": [
                {"completed_by": {"email": col},
                 "result": [{"from_name": "sentiment", "to_name": "sentence",
                             "type": "choices", "value": {"choices": [row[col]]}}]}
                for col in columns if row[col]
            ],
        })
with open("sheet_tasks.json", "w", encoding="utf-8") as f:
    json.dump(tasks, f, ensure_ascii=False)

라벨링 설정이 없으면 가져오기는 사용 중인 라벨에서 질문을 만들고 아무도 고르지 않은 라벨은 뺍니다. 질문의 세 라벨을 sheet.xml로 저장하세요.

xml
<View>
  <Text name="sentence" value="$sentence"/>
  <Choices name="sentiment" toName="sentence">
    <Choice value="positive"/><Choice value="neutral"/><Choice value="negative"/>
  </Choices>
</View>

어노테이터 열의 이름으로 스크립트를 실행한 다음 가져오기를 실행하세요.

bash
python sheet_to_tasks.py label
potato import -i sheet_tasks.json -f label_studio --ls-config sheet.xml --keep-annotators -o imported

가져오기는 imported/에 설정, data/sheet_tasks.jsonl로 된 문장, label이라는 사용자의 작업으로 저장된 열의 라벨 40개가 있는 새 프로젝트를 씁니다. 새 설정은 가져온 이름만 비밀번호 없이 로그인하게 합니다. ana와 ben을 추가하려면 imported/config.yaml의 user_config 블록에 두 이름을 추가하고, 블록 뒤에 num_annotators_per_item 줄을 추가하세요.

yaml
user_config:
  allow_all_users: false
  users:
  - label
  - ana
  - ben
num_annotators_per_item: 3

num_annotators_per_item: 3이면 각 문장이 시트의 사용자와 두 어노테이터에게 가며, 관리자 페이지는 셋이 모두 답한 뒤에 문장을 점수화합니다. 테스트 실행에서 관리자 페이지와 신뢰도 내보내기는 셋에 걸쳐 Krippendorff의 알파 0.628, 95% 구간 0.414에서 0.8을 보고했습니다. 서버를 네트워크에 올리기 전에 require_password: true를 설정하고, 가져오기의 README가 안내하듯 imported/annotation_output/user_config.json의 각 사용자에게 비밀번호를 주세요.

Google Sheets에서 바로 읽기

Potato는 다운로드 단계 없이 google_sheets 데이터 소스로 Google 시트에서 행을 바로 읽습니다. 소스에 Google 자격 증명이 필요하므로 테스트 실행은 실제 시트에 닿지 못했습니다. 아래 설정은 검증기를 통과하며, 설정 단계는 Potato의 소스 코드와 Google의 문서에서 가져왔습니다.

Potato는 서비스 계정으로 Google에 로그인하며 시트에 대한 읽기 권한만 요청합니다. Google Cloud 콘솔에서 프로젝트를 만들고, Google Sheets API를 켜고, 서비스 계정을 만들고, 계정의 JSON 키를 프로젝트 폴더의 credentials/로 내려받으세요. 그런 다음 동료와 공유하듯 키의 client_email 필드에 있는 주소와 시트를 공유하세요. pip install google-api-python-client google-auth로 클라이언트 라이브러리를 설치하고, 위 설정의 data_files 블록을 다음으로 바꾸세요.

yaml
data_sources:
  - type: google_sheets
    spreadsheet_id: "YOUR_SPREADSHEET_ID"
    sheet_name: Sheet1
    range: "A:B"
    credentials_file: credentials/service_account.json

스프레드시트 ID는 시트 URL에서 /d/와 /edit 사이의 긴 문자열입니다. range: "A:B"는 처음 두 열만 읽어서, id와 sentence가 A열과 B열이라면 라벨 열을 Potato 밖에 둡니다. Potato는 credentials_file 경로를 프로젝트 폴더 기준으로 읽습니다.

다음으로 볼 자료

참고문헌

  1. Malo, P., Sinha, A., Korhonen, P., Wallenius, J., & Takala, P. (2014). Good debt or bad debt: Detecting semantic orientations in economic texts. Journal of the Association for Information Science and Technology, 65(4), 782-796. https://doi.org/10.1002/asi.23062