Codex Lab 환경(iran-airstrike-replay-codex)에서 검증 완료된 어구 모선 자동 추론 + 검토 워크플로우 전체를 이식. ## Python (prediction/) - gear_parent_inference(1,428줄): 다층 점수 모델 (correlation + name + track + prior bonus) - gear_parent_episode(631줄): Episode 연속성 (Jaccard + 공간거리) - gear_name_rules: 모선 이름 정규화 + 4자 미만 필터 - scheduler: 추론 호출 단계 추가 (4.8) - fleet_tracker/kcgdb: SQL qualified_table() 동적화 - gear_correlation: timestamp 필드 추가 ## DB (database/migration/ 012~015) - 후보 스냅샷, resolution, episode, 라벨 세션, 제외 관리 테이블 9개 + VIEW 2개 ## Backend (Java) - 12개 DTO/Controller (ParentInferenceWorkflowController 등) - GroupPolygonService: parent_resolution LEFT JOIN + 15개 API 메서드 ## Frontend - ParentReviewPanel: 모선 검토 대시보드 - vesselAnalysis: 10개 신규 API 함수 + 6개 타입 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
17 KiB
17 KiB
Gear Parent Inference Algorithm Spec
문서 목적
이 문서는 현재 구현된 어구 모선 추적 알고리즘을 모듈, 메서드, 파라미터, 판단 기준, 저장소, 엔드포인트, 영향 관계 기준으로 정리한 구현 명세다. GEAR-PARENT-INFERENCE-DATAFLOW-PAPER.md가 서술형 통합 문서라면, 이 문서는 구현과 후속 변경 작업에 바로 연결할 수 있는 참조 스펙이다.
1. 시스템 요약
1.1 현재 목적
- 최근 24시간 한국 수역 AIS를 캐시에 유지한다.
- 어구 이름 패턴과 위치를 기준으로 어구 그룹을 만든다.
- 주변 선박/오분류 어구를 correlation 후보로 평가한다.
- 후보 중 대표 모선 가능성이 높은 선박을 추론한다.
- 사람의 라벨/제외를 별도 저장소에 남겨 향후 모델 평가와 자동화 전환에 활용한다.
1.2 현재 점수 구조의 역할 구분
gear_correlation_scores.current_score- 후보 스크리닝용 correlation score
- EMA 기반 단기 메모리
gear_group_parent_candidate_snapshots.final_score- 모선 추론용 최종 후보 점수
- coverage-aware 보정과 이름/안정성/episode/lineage/label prior 반영
gear_group_parent_resolution- 그룹별 현재 추론 상태
gear_group_episodes,gear_group_episode_snapshotssub_cluster_id와 분리된 continuity memory
gear_parent_label_tracking_cycles- 라벨 세션 동안의 자동 추론 성능 추적
2. 현재 DB 저장소와 유지 기간
| 저장소 | 역할 | 현재 유지 규칙 |
|---|---|---|
group_polygon_snapshots |
1h/1h-fb/6h 그룹 스냅샷 |
7일 cleanup |
gear_correlation_raw_metrics |
correlation raw metric 시계열 | 7일 retention partition |
gear_correlation_scores |
correlation EMA score 현재 상태 | 30일 미관측 시 cleanup |
gear_group_parent_candidate_snapshots |
cycle별 parent candidate snapshot | 현재 자동 cleanup 없음 |
gear_group_parent_resolution |
그룹별 현재 추론 상태 1행 | 현재 자동 cleanup 없음 |
gear_group_episodes |
active/merged/expired episode 현재 상태 | 현재 자동 cleanup 없음 |
gear_group_episode_snapshots |
cycle별 episode continuity 스냅샷 | 현재 자동 cleanup 없음 |
gear_parent_candidate_exclusions |
그룹/전역 후보 제외 | 기간 종료 또는 수동 해제까지 |
gear_parent_label_sessions |
정답 라벨 세션 | 만료 시 EXPIRED, row는 유지 |
gear_parent_label_tracking_cycles |
라벨 세션 cycle별 추적 | 현재 자동 cleanup 없음 |
3. 모듈 인덱스
3.1 시간/원천 적재
| 모듈 | 메서드 | 역할 |
|---|---|---|
prediction/time_bucket.py |
compute_safe_bucket() |
DB 적재 완료 전 bucket 차단 |
prediction/time_bucket.py |
compute_initial_window_start() |
초기 24h window 시작점 |
prediction/time_bucket.py |
compute_incremental_window_start() |
overlap backfill 시작점 |
prediction/db/snpdb.py |
fetch_all_tracks() |
safe bucket까지 초기 bulk 적재 |
prediction/db/snpdb.py |
fetch_incremental() |
backfill 포함 증분 적재 |
prediction/cache/vessel_store.py |
load_initial() |
초기 메모리 캐시 구성 |
prediction/cache/vessel_store.py |
merge_incremental() |
증분 merge + dedupe |
prediction/cache/vessel_store.py |
evict_stale() |
24h sliding window 유지 |
3.2 어구 identity / 그룹
| 모듈 | 메서드 | 역할 |
|---|---|---|
prediction/fleet_tracker.py |
track_gear_identity() |
어구 이름 파싱, identity log 관리 |
prediction/algorithms/gear_name_rules.py |
normalize_parent_name() |
모선명 정규화 |
prediction/algorithms/gear_name_rules.py |
is_trackable_parent_name() |
짧은 이름 제외 |
prediction/algorithms/polygon_builder.py |
detect_gear_groups() |
어구 그룹 및 서브클러스터 생성 |
prediction/algorithms/polygon_builder.py |
build_all_group_snapshots() |
1h/1h-fb/6h 스냅샷 저장용 생성 |
3.3 correlation / parent inference
| 모듈 | 메서드 | 역할 |
|---|---|---|
prediction/algorithms/gear_correlation.py |
run_gear_correlation() |
raw metric + EMA score 계산 |
prediction/algorithms/gear_correlation.py |
_compute_gear_vessel_metrics() |
proximity/visit/activity 계산 |
prediction/algorithms/gear_correlation.py |
update_score() |
EMA + freeze/decay 상태 전이 |
prediction/algorithms/gear_parent_episode.py |
build_episode_plan() |
continuity source와 episode assignment 계산 |
prediction/algorithms/gear_parent_episode.py |
compute_prior_bonus_components() |
episode/lineage/label prior bonus 계산 |
prediction/algorithms/gear_parent_episode.py |
sync_episode_states() |
gear_group_episodes upsert |
prediction/algorithms/gear_parent_episode.py |
insert_episode_snapshots() |
episode snapshot append |
prediction/algorithms/gear_parent_inference.py |
run_gear_parent_inference() |
최종 모선 추론 실행 |
prediction/algorithms/gear_parent_inference.py |
_build_candidate_scores() |
후보별 상세 점수 계산 |
prediction/algorithms/gear_parent_inference.py |
_name_match_score() |
이름 점수 규칙 |
prediction/algorithms/gear_parent_inference.py |
_build_track_coverage_metrics() |
coverage-aware evidence 계산 |
prediction/algorithms/gear_parent_inference.py |
_select_status() |
상태 전이 규칙 |
3.4 backend read model / workflow
| 모듈 | 메서드 | 역할 |
|---|---|---|
GroupPolygonService.java |
group list/review/detail SQL | 최신 1h live + stale suppression read model |
ParentInferenceWorkflowController.java |
exclusion/label API | 사람 판단 저장소 API |
4. 메서드 상세
4.1 prediction/time_bucket.py
compute_safe_bucket(now: datetime | None = None) -> datetime
- 입력:
- 현재 시각
- 출력:
safe_delay를 뺀 뒤 5분 단위로 내림한 KST naive bucket
- 기준:
SNPDB_SAFE_DELAY_MIN
- 영향:
- 초기 적재, 증분 적재, eviction 기준점
compute_incremental_window_start(last_bucket: datetime) -> datetime
- 입력:
- 현재 캐시의 마지막 처리 bucket
- 출력:
last_bucket - SNPDB_BACKFILL_BUCKETS * 5m
- 의미:
- 늦게 들어온 같은 bucket row 재흡수
4.2 prediction/db/snpdb.py
fetch_all_tracks(hours: int = 24) -> pd.DataFrame
- 역할:
- safe bucket까지 최근
N시간 full load
- safe bucket까지 최근
- 핵심 쿼리 조건:
- bbox:
122,31,132,39 time_bucket > window_starttime_bucket <= safe_bucket
- bbox:
- 출력 컬럼:
mmsi,timestamp,time_bucket,lat,lon,raw_sog
fetch_incremental(last_bucket: datetime) -> pd.DataFrame
- 역할:
- overlap backfill 포함 증분 load
- 핵심 쿼리 조건:
time_bucket > from_buckettime_bucket <= safe_bucket
- 주의:
- 이미 본 bucket도 일부 다시 읽는 구조다
4.3 prediction/cache/vessel_store.py
load_initial(hours: int = 24) -> None
- 역할:
- 초기 bulk DataFrame을 MMSI별 track cache로 구성
- 파생 효과:
_last_bucket갱신- static info refresh
- permit registry refresh
merge_incremental(df_new: pd.DataFrame) -> None
- 역할:
- 증분 batch merge
- 기준:
timestamp,time_bucket정렬timestamp기준 dedupe
- 영향:
- 같은 bucket overlap backfill에서도 최종 row만 유지
evict_stale(hours: int = 24) -> None
- 역할:
- sliding 24h 유지
- 기준:
time_bucket있으면 bucket 기준- 없으면 timestamp fallback
4.4 prediction/fleet_tracker.py
track_gear_identity(gear_signals, conn) -> None
- 역할:
- 어구 이름 패턴에서
parent_name,gear_index_1,gear_index_2추출 gear_identity_loginsert/update
- 어구 이름 패턴에서
- 입력:
- gear signal list
- 주요 기준:
- 정규화 길이
< 4면 건너뜀 - 같은 이름, 다른 MMSI는 identity migration 처리
- 정규화 길이
- 영향:
gear_correlation_scores.target_mmsi를 새 MMSI로 이전 가능
4.5 prediction/algorithms/polygon_builder.py
detect_gear_groups(vessel_store) -> list[dict]
- 역할:
- 어구 이름 기반 raw group 생성
- 거리 기반 서브클러스터 분리
- 근접 병합
- 입력:
all_positions
- 주요 기준:
- 어구 패턴 매칭
440/441제외is_trackable_parent_name()MAX_DIST_DEG = 0.15
- 출력:
parent_name,parent_mmsi,sub_cluster_id,members
build_all_group_snapshots(vessel_store, company_vessels, companies) -> list[dict]
- 역할:
FLEET,GEAR_IN_ZONE,GEAR_OUT_ZONE의1h/1h-fb/6hsnapshot 생성
- 주요 기준:
- 같은
parent_name전체 기준 1h active member 수 GEAR_OUT_ZONE최소 멤버 수- parent nearby 시
isParent=true
- 같은
4.6 prediction/algorithms/gear_correlation.py
run_gear_correlation(vessel_store, gear_groups, conn) -> dict
- 역할:
- 그룹당 후보 탐색
- raw metric 저장
- EMA score 갱신
- 입력:
gear_groups
- 출력:
updated,models,raw_inserted
_compute_gear_vessel_metrics(gear_center_lat, gear_center_lon, gear_radius_nm, vessel_track, params) -> dict
- 출력 metric:
proximity_ratiovisit_scoreactivity_synccomposite
- 한계:
- raw metric은 짧은 항적에 과대 우호적일 수 있음
- 이 문제는 parent inference 단계의 coverage-aware 보정에서 완화
update_score(prev_score, raw_score, streak, last_observed, now, gear_group_active_ratio, shadow_bonus, params) -> tuple
- 상태:
ACTIVEPATTERN_DIVERGEGROUP_QUIETNORMAL_GAPSIGNAL_LOSS
- 의미:
- correlation score는 장기 기억보다 short-memory EMA에 가깝다
4.7 prediction/algorithms/gear_parent_inference.py
run_gear_parent_inference(vessel_store, gear_groups, conn) -> dict[str, int]
- 역할:
- direct parent 보강
- active exclusion/label 적용
- 후보 점수 계산
- 상태 전이
- snapshot/resolution/tracking 저장
_load_existing_resolution(conn, group_keys) -> dict
- 역할:
- 현재 그룹의 이전 resolution 상태 로드
- 현재 쓰임:
PREVIOUS_SELECTION후보 seedstable_cyclesMANUAL_CONFIRMED유지- reject cooldown
_build_candidate_scores(...) -> list[CandidateScore]
- 후보 집합 원천:
- 상위 correlation 후보
- registry name exact bucket
- previous selection
- 제거 규칙:
- global exclusion
- group exclusion
- reject cooldown
- 점수 항목:
base_corr_scorename_match_scoretrack_similarity_scorevisit_score_6hproximity_score_6hactivity_sync_score_6hstability_scoreregistry_bonuschina_mmsi_bonus후가산
_name_match_score(parent_name, candidate_name, registry) -> float
- 규칙:
- 원문 동일
1.0 - 정규화 동일
0.8 - prefix/contains
0.5 - 숫자 제거 후 문자 부분 동일
0.3 - else
0.0
- 원문 동일
_build_track_coverage_metrics(center_track, vessel_track, gear_center_lat, gear_center_lon) -> dict
- 역할:
- short-track 과대평가 방지용 증거 강도 계산
- 핵심 출력:
trackCoverageFactorvisitCoverageFactoractivityCoverageFactorcoverageFactor
- downstream:
track,visit,proximity,activityraw score에 곱해 effective score 생성
4.8 prediction/algorithms/gear_parent_episode.py
build_episode_plan(groups, previous_by_lineage) -> EpisodePlan
- 역할:
- 현재 cycle group을 이전 active episode와 매칭
NEW,CONTINUED,SPLIT_CONTINUE,SPLIT_NEW,MERGE_NEW결정
- 입력:
GroupEpisodeInput[]- 최근
6hactiveEpisodeState[]
- continuity score:
0.75 * member_jaccard + 0.25 * center_support
- 기준:
member_jaccard- 중심점 거리
12nm - continuity score threshold
0.45 - merge score threshold
0.35
- 출력:
- assignment map
- expired episode set
- merged target map
compute_prior_bonus_components(...) -> dict[str, float]
- 역할:
- 동일 candidate에 대한 episode/lineage/label prior bonus 계산
- 입력 집계 범위:
- episode prior:
24h - lineage prior:
7d - label prior:
30d
- episode prior:
- cap:
episode <= 0.10lineage <= 0.05label <= 0.10total <= 0.20
- 출력:
episodePriorBonuslineagePriorBonuslabelPriorBonuspriorBonusTotal
sync_episode_states(conn, observed_at, plan) -> None
- 역할:
- active/merged/expired episode 상태를
gear_group_episodes에 반영
- active/merged/expired episode 상태를
- 기준:
- merge 대상은
MERGED - continuity 없는 old episode는
EXPIRED
- merge 대상은
insert_episode_snapshots(conn, observed_at, plan, payloads) -> int
- 역할:
- cycle별 continuity 결과와 top candidate/result를
gear_group_episode_snapshots에 저장
- cycle별 continuity 결과와 top candidate/result를
- 기록:
episode_idparent_episode_idstop_candidate_mmsitop_candidate_scoreresolution_status
_select_status(top_candidate, margin, stable_cycles) -> tuple[str, str]
- 상태:
NO_CANDIDATEAUTO_PROMOTEDREVIEW_REQUIREDUNRESOLVED
- auto promotion 조건:
target_type == VESSELCORRELATIONsource 포함final_score >= 0.72margin >= 0.15stable_cycles >= 3
- review 조건:
final_score >= 0.60
5. 현재 엔드포인트 스펙
5.1 조회 계열
/api/kcg/vessel-analysis/groups/parent-inference/review
- 역할:
- 최신 전역
1h기준 검토 대기 목록
- 최신 전역
- 조건:
- stale resolution 숨김
- candidate count는 latest candidate snapshot 기준
/api/kcg/vessel-analysis/groups/{groupKey}/parent-inference
- 역할:
- 특정 그룹의 현재 live sub-cluster 상세
- 주의:
- “현재 최신 전역
1h에 실제 존재하는 sub-cluster만” 반환
- “현재 최신 전역
/api/kcg/vessel-analysis/parent-inference/candidate-exclusions
- 역할:
- 그룹/전역 제외 목록 조회
/api/kcg/vessel-analysis/parent-inference/label-sessions
- 역할:
- active 또는 전체 라벨 세션 조회
5.2 액션 계열
POST /candidate-exclusions/global
- 역할:
- 전역 후보 제외 생성
- 영향:
- 다음 cycle부터 모든 그룹에서 해당 MMSI 제거
POST /groups/{groupKey}/parent-inference/{subClusterId}/exclude
- 역할:
- 그룹 단위 후보 제외 생성
- 영향:
- 다음 cycle부터 해당 그룹에서만 제거
POST /groups/{groupKey}/parent-inference/{subClusterId}/label
- 역할:
- 기간형 정답 라벨 세션 생성
- 영향:
- 다음 cycle부터 tracking row 누적
6. 현재 기억 구조와 prior bonus
6.1 short-memory와 long-memory의 분리
gear_correlation_scores- EMA short-memory
- 미관측 시 decay
- 현재 후보 seed 역할
gear_group_parent_resolution- 현재 상태 1행
- same-episode가 아니면
PREVIOUS_SELECTIONcarry를 직접 사용하지 않음
gear_group_episodes- continuity memory
candidate_snapshots- bonus 집계 원천
6.2 현재 final score의 장기 기억 반영
현재는 과거 점수를 직접 carry하지 않고, 약한 prior bonus만 후가산한다.
final_score =
current_signal_score
+ china_mmsi_bonus
+ prior_bonus_total
여기서 prior_bonus_total은:
episode_prior_bonuslineage_prior_bonuslabel_prior_bonus
의 합이며 총합 cap은 0.20이다.
6.3 왜 weak prior인가
과거 점수를 그대로 넘기면:
- 다른 episode로 잘못 관성이 전이될 수 있다
- split/merge 이후 잘못된 top1이 고착될 수 있다
- 오래된 오답이 장기 drift로 남을 수 있다
그래서 현재 구현은 과거 점수를 “현재 score 자체”가 아니라 “작은 bonus”로만 쓴다.
7. 현재 continuity / prior 동작
7.1 episode continuity
- 같은 lineage 안에서 최근
6hactive episode를 불러온다. - continuity score가 높은 이전 episode가 있으면
CONTINUED - 1개 parent episode가 여러 current cluster로 이어지면
SPLIT_CONTINUE+SPLIT_NEW - 여러 previous episode가 하나 current cluster로 모이면
MERGE_NEW - 어떤 current와도 연결되지 못한 old episode는
EXPIRED
7.2 prior 집계
| prior | 참조 범위 | 현재 집계 값 |
|---|---|---|
| episode prior | 최근 동일 episode 24h |
seen_count, top1_count, avg_score, last_seen_at |
| lineage prior | 동일 이름 lineage 7d |
seen_count, top1_count, top3_count, avg_score, last_seen_at |
| label prior | 라벨 세션 30d |
session_count, last_labeled_at |
7.3 구현 시 주의
- 과거 점수를 직접 누적하지 말 것
- prior는 bonus로만 사용하고 cap을 둘 것
- split/merge 이후 parent 후보 관성은 약하게만 상속할 것
- stale live sub-cluster와 vanished old sub-cluster를 혼동하지 않도록, aggregation도 최신 episode anchor를 기준으로 할 것