지난 글에서 말로 가리킨 대상을 상자로 받아 냈다. 상자는 다루기 쉽지만 대상의 실제 모양은 아니다. 우산을 상자로 자르면 우산과 함께 하늘과 어깨가 딸려 온다. 배경을 지우거나, 넓이를 재거나, 그 부분만 색을 바꾸려면 경계가 필요하다.
픽셀 하나하나가 대상에 속하는지 아닌지를 가르는 일을 분할(segmentation)이라 부르고, 그 결과인 픽셀 단위의 참·거짓 지도를 마스크라고 한다. 오래된 주제지만 최근 몇 해 사이에 쓰는 방식이 크게 바뀌었다. 이 글은 그 바뀐 쪽, 즉 프롬프트로 지시하는 분할을 다룬다.
무엇이 달라졌나
전통적인 분할은 클래스 목록에서 출발했다. 사람·자동차·도로·하늘… 이렇게 미리 정한 목록을 두고 픽셀마다 어느 클래스인지 매기는 것이 의미 분할이고, 같은 클래스라도 개체를 따로 세는 것이 인스턴스 분할이다. 둘을 합쳐 장면 전체를 빈틈없이 덮는 것이 파놉틱 분할이다.
셋 다 같은 전제 위에 있다. 찾을 것을 미리 나열해 두었다는 전제다. 목록에 없는 것 — 「저 사람이 든 것」, 「가운데 있는 얼룩」 — 은 찾을 수가 없고, 새 대상을 넣으려면 라벨을 붙여 다시 학습해야 한다.
프롬프트로 지시하는 분할은 그 전제를 버린다. 클래스 이름 대신 「여기」라는 지시를 받는다. 점을 하나 찍든, 상자를 대충 그리든, 문구로 말하든, 모델은 그 지시가 가리키는 대상의 경계를 낸다. 그것이 무엇인지는 모델도 이름 붙이지 않는다 — 그냥 경계만 준다.
이 구분이 중요한 이유가 있다. 이름을 붙이지 않기로 하는 순간 학습 데이터가 폭발적으로 늘어난다. 라벨러가 클래스 체계를 익힐 필요 없이 「여기」만 찍으면 되기 때문이다. 그렇게 모은 대량의 마스크가 이 계열 모델들의 일반화를 떠받친다.
구조 — 무거운 쪽을 한 번만 돈다
이 계열의 설계에서 가장 눈여겨볼 것은 정확도가 아니라 비용을 나눈 방식이다.
세 조각으로 나뉜다.
- 이미지 인코더 — 큰 비전 트랜스포머다. 그림 한 장을 임베딩으로 바꾸는 데 무거운 계산이 들어간다
- 프롬프트 인코더 — 점·상자·마스크·텍스트를 작은 벡터 몇 개로 바꾼다. 거의 공짜다
- 마스크 디코더 — 이미지 임베딩과 프롬프트 벡터를 받아 마스크를 낸다. 아주 얇게 설계되어 있다
핵심은 첫째가 프롬프트와 무관하다는 것이다. 그림이 그대로면 임베딩도 그대로다. 그래서 한 번 인코딩해 두고 프롬프트만 바꿔 가며 수십 번 물을 수 있고, 그 수십 번은 손끝을 따라올 만큼 빠르다. 편집 도구에서 마우스를 옮길 때마다 경계가 실시간으로 따라붙는 화면이 이 구조 덕분에 가능하다.
뒤집으면 이렇다. 매 요청마다 인코딩을 다시 하면 이 구조의 이점이 통째로 사라진다. 서비스로 감쌀 때 가장 자주 놓치는 지점이 여기다. 세션마다 이미지 임베딩을 캐시하고, 프롬프트 요청은 그 캐시를 참조하도록 API를 갈라 두어야 한다. 임베딩은 수 메가바이트 단위라 메모리 예산도 함께 잡아야 한다.
점 하나는 모호하다
셔츠 주머니 위를 눌렀다고 하자. 사용자가 원한 것은 주머니인가, 셔츠인가, 사람인가. 그림만으로는 알 수 없다.
그래서 이 모델들은 마스크를 하나만 내지 않는다. 대개 서로 다른 크기 대역의 후보 셋을 함께 내고 각각에 예상 품질 점수를 붙인다. 고르는 것은 부르는 쪽 몫이다.
여기서 실무자가 자주 하는 실수가 점수 최고를 무조건 고르는 것이다. 점수는 「이 마스크가 얼마나 깨끗한가」에 대한 모델의 어림이지 「사용자가 원한 것인가」가 아니다. 배경 제거처럼 대상 전체가 필요한 일이라면 넓이 대역으로 먼저 거른 다음 그중 점수가 높은 것을 고르는 편이 훨씬 안정적이다.
모호함을 줄이는 더 확실한 방법은 지시를 늘리는 것이다.
| 프롬프트 | 성격 | 언제 |
|---|---|---|
| 양수 점 | 여기는 포함하라 | 사용자가 직접 클릭하는 화면 |
| 음수 점 | 여기는 빼라 | 마스크가 옆 대상까지 먹었을 때 |
| 상자 | 이 안에서 가장 두드러진 것 | 검출기가 낸 상자를 그대로 넘길 때 |
| 거친 마스크 | 이걸 다듬어라 | 앞선 결과를 되먹여 고칠 때 |
| 텍스트 | 이 이름에 해당하는 것 | 사람이 개입하지 않는 자동 처리 |
점 두세 개면 대부분의 모호함이 사라진다. 특히 음수 점의 값이 크다 — 옆 대상까지 먹은 마스크를 고치는 데 이보다 싼 수단이 없다.
텍스트로 지시하기
목록에 없는 것을 이름으로 부르는 것을 개방 어휘(open-vocabulary) 분할이라고 한다. 「강아지」라고 쓰면 강아지의 경계가 나오는 것이다.
다만 이걸 한 모델이 통째로 하는 경우는 생각보다 드물다. 실무에서 훨씬 흔한 구성은 두 모델을 잇는 것이다.
- 개방 어휘 검출기 또는 지난 글의 그라운딩 모델에게 문구를 주고 상자를 받는다
- 그 상자를 분할 모델에 프롬프트로 넣어 마스크를 받는다
나눠 두면 각각을 따로 갈아 끼울 수 있고, 중간에 상자가 남으므로 무엇이 잘못됐는지 볼 자리가 생긴다. 앞 단계가 엉뚱한 것을 잡았는지, 잡긴 잘 잡았는데 경계가 지저분한지가 구분된다. 이 구분은 디버깅 시간을 크게 줄인다.
영상으로 넓힐 때
한 프레임씩 따로 분할하면 마스크가 프레임마다 떨린다. 그래서 영상용 모델은 메모리를 둔다. 앞 프레임들의 임베딩과 마스크를 일정 개수 보관하고, 새 프레임을 처리할 때 그 기억을 참조한다. 사용자는 첫 프레임에만 지시하고 나머지는 따라가게 하는 식이다.
새로 생기는 문제가 셋 있다.
- 가려짐 — 대상이 잠깐 뒤로 숨었다 나온다. 나온 것이 아까 그것인지 판단해야 한다
- 표류 — 조금씩 어긋난 마스크가 다음 프레임의 기억이 되어 오차가 쌓인다. 중간중간 사람이 점을 하나 찍어 바로잡는 경로를 열어 두는 것이 현실적이다
- 메모리 예산 — 기억을 길게 들수록 좋지만 길이에 비례해 비용이 는다. 최근 몇 프레임과 처음 지시한 프레임을 함께 두는 절충이 흔하다
실무에서 밟는 것들
마스크는 저해상도에서 나온다. 디코더가 내는 것은 원본보다 훨씬 작은 격자이고 그걸 키워 쓴다. 그래서 머리카락·나뭇가지처럼 가는 구조의 가장자리가 뭉개진다. 가장자리 품질이 결과물의 값이라면 별도의 정제 단계가 필요하다.
조각과 구멍이 남는다. 마스크 안에 작은 구멍이 뚫리거나, 본체에서 떨어진 작은 조각이 붙어 나오는 일이 흔하다. 가장 큰 연결된 덩어리만 남기고 일정 넓이 이하의 조각을 지우는 간단한 후처리로 대부분 정리된다.
같은 점에 늘 같은 답이 나오지는 않는다. 1픽셀 차이로 다른 크기 대역이 선택되는 경우가 있다. 자동 파이프라인이라면 점을 하나만 쓰지 말고 대상 안쪽에 몇 개를 흩뿌리는 편이 안정적이다.
평가는 IoU로 한다. 예측 마스크와 정답 마스크의 겹친 넓이를 합집합 넓이로 나눈 값이다. 다만 가장자리 품질은 IoU에 거의 안 잡힌다 — 넓은 대상에서 가장자리 몇 픽셀은 전체 넓이에 비하면 무시할 수준이라서다. 가장자리가 중요하면 경계만 따로 재는 지표를 함께 봐야 한다.
정리
- 프롬프트 분할은 클래스 목록을 버리고 「여기」라는 지시를 받는다. 대상 이름을 미리 나열할 수 없을 때 값이 크다
- 구조의 핵심은 정확도가 아니라 비용 배분이다. 무거운 이미지 인코더는 한 번, 얇은 마스크 디코더는 프롬프트마다
- 서비스로 감쌀 때는 임베딩 캐시가 전부다. 매 요청마다 인코딩하면 이 구조를 쓸 이유가 없어진다
- 점 하나는 모호하므로 모델이 크기 대역이 다른 후보 여럿을 낸다. 점수 최고가 아니라 쓰임에 맞는 대역으로 고른다
- 옆 대상까지 먹은 마스크는 음수 점 하나로 대개 고쳐진다
- 텍스트 지시는 검출기와 분할기를 잇는 구성이 실무에서 다루기 쉽다 — 중간에 상자가 남아 원인을 가릴 수 있다
- 영상은 메모리를 두어 떨림을 잡되 가려짐·표류·메모리 예산 셋을 함께 설계한다
- 가장자리 품질은 IoU에 거의 안 잡힌다. 그게 중요하면 경계 지표를 따로 본다
읽어주셔서 감사합니다. 😊

