해당 내용의 데이터는 ai hub 위성 영상 객체 판독을 사용했음. 링크
AI-Hub
데이터셋 다운로드 승인이 완료 된 후 API 다운로드 서비스를 이용하실 수 있습니다. API 다운로드 파일은 분할 압축되어 다운로드 됩니다. 분할 압축 해제를 위해서는 분할 압축 파일들의 병합이
aihub.or.kr
1. YOLO가 요구하는 데이터 형식
Ultralytics YOLO는 세가지 데이터로 이미지 학습이 가능하다.
1.1 디렉토리 이름으로 라벨을 찾음
Ultralytics는 이미지 경로에서 /images/를 /labels/로 문자열 치환해 라벨 파일을 찾는다. 라벨에 대한 확장자는 .txt로 하면 된다.
그래서 디렉토리 이름은 반드시 images / labels여야 하고, 이미지와 라벨의 파일명(stem)이 같아야 한다
dataset/
├── images/
│ ├── train/ OBJ00013.png OBJ00022.png ...
│ └── val/ OBJ05502.png ...
├── labels/
│ ├── train/ OBJ00013.txt OBJ00022.txt ...
│ └── val/ OBJ05502.txt ...
└── data.yaml
1.2 라벨은 정규화된 중심점과 크기
한 줄에 객체 하나에 5개의 데이터로 공백 구분으로 구분된다. (아래 데이터는 하나의 이미지에 두개의 객체가 (클래스0번)이 있는 라벨이 된다.
<class_index> <center_x> <center_y> <width> <height>
0 0.502441 0.517090 0.090332 0.088379
0 0.318848 0.771973 0.041016 0.043945
| 항목 | 범위 | |
| class_index | 0부터 시작하는 정수 | data.yaml의 names 순서와 일치해야 한다 |
| center_x, center_y | 0.0 ~ 1.0 | 좌상단 좌표가 아니라 중심점. 이미지 폭/높이로 나눈 값 |
| width, height | 0.0 ~ 1.0 |
center_x, center_y, width, height 값은 1.0을 넘으면 해당 데이터는 버려진다.
원본 좌표가 이미지 경계를 벗어나 정규화 값이 1.0을 초과하면 Ultralytics는 그 라벨을 경고 없이 무시한다. 학습은 정상적으로 돌아가고, 그 객체만 없는 것처럼 학습된다.
1.3 빈 라벨 파일 = 배경 이미지
| 객체 있음 | 정상 학습 샘플 | positive |
| 파일 존재, 내용 비어 있음 | 객체 없는 배경 이미지 | 오탐 억제 학습 |
| 파일 자체가 없음 | 경고 후 건너뜀 | 학습에서 제외 |
라벨에 대한 빈 .txt를 만들어주면 모델이 '여기엔 객체가 없다'를 배워 Precision이 올라간다. 파일을 만들지 않으면 그 해당 이미지는 학습평가에서 제외된다.
2. 데이터 변환
기존 데이터가 좌표 기반 라벨링으로 되어 있어 좌표 파싱과 AABB 변환 코드로 데이터 생성
def parse_imcoords(s) -> np.ndarray | None:
"""'x1,y1,x2,y2,...' 문자열을 (N,2) 배열로. 'EMPTY'/빈 값은 None."""
if not isinstance(s, str):
return None
s = s.strip()
if not s or s.upper() == "EMPTY":
return None
try:
v = np.fromstring(s, sep=",", dtype=np.float64)
except Exception:
return None
if v.size < 4 or v.size % 2 != 0:
return None
return v.reshape(-1, 2)
def boxes_from_json(path: str) -> list[tuple[str, float, float, float, float]]:
"""JSON 1개 -> [(type_id, x1, y1, x2, y2)] (픽셀, 축정렬, 클리핑 완료).
"""
with open(path, encoding="utf-8") as f:
doc = json.load(f)
out = []
for ft in doc.get("features", []) or []:
pr = ft.get("properties", {}) or {}
pts = parse_imcoords(pr.get("object_imcoords"))
if pts is None:
continue
x1 = float(np.clip(pts[:, 0].min(), 0, IMG_W))
y1 = float(np.clip(pts[:, 1].min(), 0, IMG_H))
x2 = float(np.clip(pts[:, 0].max(), 0, IMG_W))
y2 = float(np.clip(pts[:, 1].max(), 0, IMG_H))
if x2 - x1 < 1 or y2 - y1 < 1:
continue
out.append((str(pr.get("type_id")), x1, y1, x2, y2))
return out
def to_yolo_line(cls_idx: int, box: tuple[float, float, float, float]) -> str:
"""픽셀 AABB -> YOLO 정규화 문자열 'cls cx cy w h'."""
x1, y1, x2, y2 = box
cx = (x1 + x2) / 2 / IMG_W
cy = (y1 + y2) / 2 / IMG_H
w = (x2 - x1) / IMG_W
h = (y2 - y1) / IMG_H
return f"{cls_idx} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}"
3. 학습 코드
def train_one(exp: str, args) -> tuple[str, str]:
"""실험 하나 학습. (best 가중치 경로, data.yaml 경로) 반환."""
from ultralytics import YOLO
ds_name, default_epochs, desc = EXPERIMENTS[exp]
data_yaml = os.path.join(DS_ROOT, ds_name, "data.yaml")
if not os.path.exists(data_yaml):
raise FileNotFoundError(f"{data_yaml} 없음 -> build_yolo_dataset.py 먼저 실행")
epochs = args.epochs or default_epochs
run_name = args.name or f"exp_{exp}"
# 이어하기: 세션이 끊겨도 last.pt 부터 재개한다 (Ultralytics 가 매 에폭 저장)
last = os.path.join(RUN_ROOT, run_name, "weights", "last.pt")
if args.resume and os.path.exists(last):
print(f"[resume] {last} 에서 재개")
YOLO(last).train(resume=True)
return os.path.join(RUN_ROOT, run_name, "weights", "best.pt"), data_yaml
model = YOLO(args.model)
model.train(
data=data_yaml,
epochs=epochs,
imgsz=args.imgsz, # 이미지 사이트
batch=args.batch,
device=args.device,
workers=args.workers,
seed=0, # 시드 고정으로 데이터 확인
deterministic=True,
project=RUN_ROOT,
name=run_name,
exist_ok=True,
patience=args.patience,
# --- 최적화 (기본값은 소규모 데이터에서 발산해서 명시 고정) ---
optimizer=args.optimizer,
lr0=args.lr0,
lrf=args.lrf,
cos_lr=args.cos_lr,
warmup_epochs=args.warmup_epochs,
warmup_bias_lr=args.warmup_bias_lr, # 기본 0.1 은 140장 규모에서 헤드를 파괴
cache=args.cache,
# --- 증강 ---
flipud=0.5, # 위성영상은 상하 방향성이 없다
fliplr=0.5,
degrees=args.degrees, # 위성은 회전 불변. 다만 AABB 라벨이 팽창하니 주의
mosaic=args.mosaic,
close_mosaic=args.close_mosaic,
val=True,
plots=True,
)
best = os.path.join(RUN_ROOT, run_name, "weights", "best.pt")
return best, data_yaml
3-1. 하이퍼파라미터를 정하는 방법
Ultralytics 기본값은 COCO(12만 장) 기준으로 튜닝되어 있다. 데이터가 수백 장 규모면 몇 개는 파라미터를 바꿔야 한다.
| imgsz | 1024 | 객체 √area 중앙값 29.7px, 52.7%가 32px 미만. 640으로 줄이면 16px 객체가 10px이 되어 IoU 0.5 달성이 물리적으로 불가능해진다. 여기서 타협하면 다른 튜닝이 무의미 |
| batch | 8 또는 -1 | -1은 AutoBatch — GPU 메모리를 측정해 자동 결정(CUDA 전용). T4 14.5GB에서 1024 해상도면 5가 선택됐다 |
| epochs | 140장→150 1,747장→40 |
iteration 기준으로 환산해서 정한다. 에폭당 업데이트가 이미지수 ÷ batch회뿐이므로, 데이터가 적으면 에폭을 늘려야 총 학습량이 맞는다 |
| flipud | 0.5 (기본 0.0) | 위성·항공 영상은 상하 방향성이 없다. 기본값 0.0은 "하늘은 위"인 지상 사진 기준. 켜면 공짜로 데이터 2배 |
| degrees | 0.0 | 위성영상은 회전 불변이라 켜고 싶지만, AABB 라벨은 회전 시 면적이 팽창한다. 원형 객체엔 무해하고 세로로 긴 객체엔 해롭다. 클래스 구성 보고 판단 |
| cache | disk | Colab 무료는 vCPU 2코어. 1024 png 디코딩 + mosaic이 GPU보다 먼저 병목이 된다. .npy로 한 번만 디코딩해두면 해결 |
소규모 데이터에서 반드시 바꿔야 하는 값
| optimizer | auto | AdamW | auto는 내가 지정한 lr0를 무시하고 자기가 정한다 (로그에 "ignoring lr0"라고 찍힌다). 재현성을 위해 명시 고정 |
| lr0 | 0.01 → auto가 0.002로 덮어씀 | 0.0005 | 140장 규모에 0.002는 과대. 1/4로 |
| warmup_bias_lr | 0.1 | 0.0 | 발산의 주범. 워밍업 중 bias 학습률이 0.1이다. 에폭당 18 iteration이면 3에폭 워밍업이 54스텝밖에 안 되고, 그 짧은 구간의 lr 0.1이 분류 헤드를 파괴한다 |
| warmup_epochs | 3.0 | 1.0 | 위와 같은 이유. 짧은 학습에서 워밍업이 전체의 2%를 넘지 않게 |
| cos_lr | False | True | 코사인 감쇠가 후반 미세조정에 유리 |
| seed / deterministic | 0 / True | 유지 | 실험 비교가 시드 운빨이 되지 않게. MPS에서는 일부 연산이 결정적 구현이 없어 경고가 나오지만 무시 가능 |
'ML > 인공지능' 카테고리의 다른 글
| 위성영상 정유탱크 객체 판독 (0) | 2026.08.13 |
|---|---|
| Kaplan-Meier curve (0) | 2025.07.19 |
| bigquery ML 분류 (긍정/부정) - 영어 (1) | 2025.07.13 |
| bigquery ML Titanic (1) | 2025.06.24 |
| [논문 리뷰] RoBERTa: A Robustly Optimized BERT Pretraining Approach (0) | 2024.05.14 |