들어가며
이 글은 뷰티 도메인에서 영상 속 사람의 행동을 추론하는 AI 모델을 직접 만들며 학습한 내용과 시행착오를 정리한 글입니다.
처음에는 관리형 AI 솔루션이나 HuggingFace에 공개된 모델을 그대로 쓰려고 했습니다.
하지만 제가 풀어야 할 문제는 일반적인 객체 탐지나 범용 행동 인식으로는 해결되지 않았습니다.
범용 행동 인식 모델이 다루는 단위는 보통 걷기 · 뛰기 · 앉기처럼 큰 동작입니다.
반면 뷰티 도메인에서 필요한 행동은 훨씬 더 세밀합니다. "손이 얼굴 근처에 있다"는 사실만으로는 부족하고, 손이 어느 부위로 갔는지, 얼마나 머물렀는지 · 바르는지 · 두드리는지 · 닦아내는지를 구분해야 했습니다.
결국 범용 모델로는 충분하지 않다고 판단했고, 뷰티 도메인에 맞는 작은 행동 단위를 직접 정의하고 학습시키기로 했습니다.
최종적으로 이 모델은 영상의 매 순간마다 무슨 행동(Action)을, 어느 부위(Region)에 하고 있는지를 추론하도록 만들어졌습니다.
POV : 좌측 상단에서 Action 과 Region 을 정확하게 추론하는 모습
AI란 무엇일까?
AI는 원래 사람이 하던 '지적인 판단'을 기계가 대신하도록 만든 기술입니다. 무언가를 알아보고, 구분하고, 다음을 예측하고, 어떤 결정을 내리는 일이 모두 여기에 들어갑니다.
그런데 기계는 이런 판단을 어떻게 배울까요? 크게 두 가지 길이 있습니다.
하나는 사람이 규칙을 일일이 적어주는 방식입니다. "손이 입에서 3cm 안에 있고, 0.5초 이상 머물면 바르기"처럼요. 하지만 행동처럼 미묘하고 예외가 많은 문제는 규칙을 아무리 적어도 끝이 없습니다.
다른 하나는 데이터로 가르치는 방식(머신러닝)입니다. 정답이 달린 예시를 잔뜩 보여주면, 기계가 그 안에서 공통된 패턴을 스스로 찾아냅니다.
강아지 사진들.
예를 들어, 아이에게 "강아지란 이런 것"이라고 말로 정의해 주긴 어렵습니다. 대신 강아지 사진 수백 장을 보여주면, 아이는 어느새 '강아지다움'을 스스로 익혀 처음 보는 강아지도 알아봅니다. 머신러닝도 똑같습니다. 규칙을 받아 적는 게 아니라, 예시를 보며 스스로 감을 잡습니다.
이렇게 데이터에서 학습한 패턴 덩어리가 바로 AI 모델입니다. 한 번 패턴을 익히고 나면(학습), 처음 보는 데이터에도 답을 내놓을 수 있습니다(예측).
정리하면, 사람은 눈과 경험으로 판단하지만, AI 모델은 시각 정보라는 입력 데이터에서 패턴을 학습하고, 그 패턴으로 새로운 데이터를 예측합니다.
데이터로 배우는 머신러닝은, 다시 '어떻게 배우는가'에 따라 크게 세 갈래로 나뉩니다.
•
지도학습 (Supervised) — 정답표가 달린 데이터로 배웁니다. 정답지를 펴 놓고 문제집을 푸는 학생과 같습니다.
•
비지도학습 (Unsupervised) — 정답 없이 데이터 속 비슷한 것끼리 묶거나 숨은 구조를 스스로 찾습니다. 이름표 없는 물건들을 닮은 것끼리 정리하는 것과 같습니다.
•
강화학습 (Reinforcement) — 시행착오를 겪으며 잘하면 상, 못하면 벌을 받아 행동을 다듬습니다. 간식으로 훈련시키는 강아지를 떠올리면 됩니다.
참고로 요즘은 정답을 데이터 스스로 만들어 학습하는 자기지도학습(Self-supervised)도 따로 떼어 부르는데, 뒤에서 다룰 DINOv2가 바로 이 방식으로 학습된 모델입니다.
제가 만들 모델은 사람이 "이 장면은 볼에 바르기"처럼 정답을 일일이 달아준 데이터로 배우므로, 이 중 지도학습에 해당합니다.
같은 모델이라도 푸는 문제의 형태가 다르면 모델의 출력도 달라지기 때문에 무엇을 맞히도록 학습됐는지에 따라 한 번 더 종류가 나뉩니다.
영상·이미지를 다루는 모델만 해도 대표적으로 두 갈래가 있습니다.
•
분류(Classification) 모델 — "이 사진은 무엇인가?"에 답합니다. 사진 한 장을 보고 미리 정해둔 후보(강아지·고양이 등) 중 하나로 이름표를 붙입니다.
•
객체 탐지(Object Detection) 모델 — "무엇이, 어디에 있는가?"에 답합니다. 사진 안에서 대상의 위치(네모 상자)와 종류를 함께 찾아냅니다.
이처럼 AI 모델은 '어떤 질문에 답하도록 배웠는가'를 기준으로 갈라집니다. 그래서 새로운 문제를 만나면, 가장 먼저 "이건 어떤 질문에 가까운가"를 따져보는 데서부터 출발해야 합니다.
모델의 뼈대 만들기
앞서 본 분류 모델과 객체 탐지 모델은 모두 사진 '한 장'을 보고 답합니다. 그런데 제가 풀려는 영상 속 행동은 조금 다릅니다.
행동은 사진 한 장에 담기지 않습니다. 손이 입 근처에 있는 한 컷만 보면, 그것이 제품을 바르는 장면인지 그냥 손이 지나가는 장면인지 알 수 없습니다.
예를 들어, 만화책에서 한 컷만 보면 주인공이 넘어지는 건지 점프하는 건지 모릅니다. 앞뒤 컷을 같이 봐야 "아, 점프했다가 착지하는 중"이라고 알 수 있습니다.
행동 인식도 똑같이 연속된 여러 컷(프레임)을 묶어서 봐야 합니다. 그래서 이 문제는 이미지 분류가 아니라, 시간의 흐름까지 함께 이해해야 하는 시공간(spatio-temporal) 문제라고 판단했습니다.
참고로 영상에서 "모양"과 "움직임"을 나눠 보는 접근은 Two-Stream Networks(Simonyan & Zisserman, 2014) 논문에서 정립된 고전적인 아이디어이기도 합니다.
"Video can naturally be decomposed into spatial and temporal components. The spatial part, in the form of individual frame appearance, carries information about scenes and objects depicted in the video. The temporal part, in the form of motion across the frames, conveys the movement of the observer (the camera) and the objects."
영상은 자연스럽게 공간(spatial) 요소와 시간(temporal) 요소로 나눌 수 있다. 공간 요소는 개별 프레임의 외형으로서 장면과 객체에 대한 정보를 담고, 시간 요소는 프레임 사이의 움직임으로서 카메라와 객체의 움직임을 전달한다.
이렇게 영상을 공간과 시간으로 나눠 이해해야 한다는 큰 방향이 잡히자, 자연스럽게 모델이 무엇을 맞혀야 하는지 정의할 수 있었습니다.
제가 풀려는 건 결국 '영상 속 행동'이고, 그 행동은 무슨 동작(Action)을 어느 부위(Region)에 하는지로 나눠 표현할 수 있었습니다. 가장 단순한 방법은 '볼에-바르기', '입술에-두드리기'처럼 둘을 하나로 묶어 단일 라벨로 예측하는 것입니다.
하지만 이 방식은 행동 n종과 부위 m종을 곱한 n × m개의 많은 양의 데이터가 필요하다는 점과, 데이터가 잘게 쪼개지면서 불균형은 심해진다는 점이 있었습니다.
그래서 출력을 두 갈래(multi-head)로 분리했습니다. 하나의 backbone이 영상의 공통 표현을 학습하고, 그 위에서 Action Head는 무슨 행동인지를, Region Head는 어느 부위인지를 각각 추론합니다.
'무엇을'과 '어디에'는 서로 관련은 있지만 완전히 같은 문제는 아니므로, 각 head가 자기 질문에만 집중하도록 나눈 것입니다. 이처럼 하나의 표현을 공유하면서 여러 과제를 함께 학습하는 방식을 multi-task learning이라고 부릅니다.
이제 풀어야 할 문제(시공간)와 맞혀야 할 출력(Action · Region)이 정해졌으니, 거기에 가장 적합한 모델 구조를 찾을 차례였습니다.
저는 적은 데이터셋으로 CNN, RNN, LSTM을 각각 학습시켜 비교해봤습니다.
•
CNN은 한 시점의 공간 패턴을 잘 잡지만, 시간의 흐름은 설명하지 못했습니다.
•
RNN은 순차 데이터를 다루지만, 시퀀스가 길어지면 앞쪽 정보가 뒤까지 전달되지 못하는 한계(장기 의존성 소실)가 있었습니다.
•
LSTM은 RNN에 기억할 것 · 버릴 것을 정하는 게이트(forget · input · output gate)를 더해 이 한계를 보완한 구조입니다.
쉽게 말하면 밀가루 옮기기 게임에 비유할 수 있습니다.
CNN은 밀가루가 어떻게 전달되는지를 보는 것이 아니라, 밀가루가 얼마나 쏟아지고 흔들리는지만 봅니다. 그래서 짧은 구간의 특징은 잘 잡지만, 처음의 밀가루가 끝까지 어떻게 이어졌는지는 알기 어렵습니다.
RNN은 접시에 담긴 밀가루를 다음 사람에게 계속 넘기는 방식과 같습니다. 한 사람씩 전달될 때마다 접시가 흔들리고 밀가루가 조금씩 흘러내리듯, 시퀀스가 길어질수록 처음의 중요한 정보가 점점 희미해집니다.
LSTM은 중요한 밀가루는 덜 쏟아지는 특별한 접시에 따로 담아 옮기는 것과 같습니다. 필요 없는 밀가루는 흘려보내고, 중요한 밀가루는 최대한 보존하기 때문에 RNN보다 오래 기억할 수 있습니다.
이 중 가장 안정적으로 높은 정확도를 보인 것은 LSTM이었습니다.
손이 얼굴 근처를 잠깐 지나가는 장면은 버리고, 특정 부위에 머물며 반복적으로 움직이는 패턴은 유지해야 하는 이 문제에, "필요한 시간적 흐름만 기억하는" LSTM의 성질이 잘 맞았기 때문입니다.
LSTM은 어떻게 "필요한 것만" 기억하는 걸까요?
LSTM의 핵심은 cell state(셀 상태)라는 별도의 기억 통로입니다.
일반적인 RNN은 매 순간의 정보를 하나의 메모(hidden state)에 계속 욱여넣다 보니 금세 흐려지는 반면, LSTM은 오래 간직할 정보를 이 cell state에 따로 실어 나릅니다.
앞의 밀가루 비유에서 "흘리지 않는 특별한 접시"가 바로 이 cell state입니다.
그리고 이 접시에는 무엇을 담고 버릴지 조절하는 밸브(gate) 3개가 달려 있습니다.
•
Forget Gate(망각 게이트) — 이전까지 모아둔 기억 중 버릴 것을 정합니다. (손이 얼굴을 스쳐 지나가고 끝났다면, 그 손은 잊습니다)
•
Input Gate(입력 게이트) — 지금 들어온 정보 중 새로 담을 것을 정합니다. (손이 볼에 닿아 두드리기 시작했다면, 그 신호를 접시에 새로 실습니다)
•
Output Gate(출력 게이트) — 지금 이 순간 밖으로 내보낼 답을 정합니다. (모아둔 기억 전부가 아니라, 이번 판단에 필요한 만큼만 꺼냅니다)
비유하자면 cell state는 컨베이어 벨트 위를 흘러가는 상자이고, 세 게이트는 그 위에서 "이건 빼고(forget), 이건 넣고(input), 이건 지금 꺼내자(output)"를 정하는 세 개의 손과 같습니다.
매 프레임마다 이 세 손이 움직이며 상자의 내용물을 정리하기 때문에, 긴 영상에서도 중요한 순간의 정보가 끝까지 살아남습니다.
좀 더 쉽게 풀어보면 다음 내용과 같습니다.
LSTM은 드라마를 한 회씩 정주행하며 메모장에 줄거리를 적어가는 사람과 같습니다. 여기서 '드라마 한 회'가 바로 영상의 '한 프레임'입니다.
•
입력 — 영상을 통째로 주는 게 아니라, 드라마를 1화부터 보듯 프레임을 한 장씩 순서대로 넘겨줍니다.
•
기억 — 한 장면을 볼 때마다 메모장을 고쳐 씁니다. 스쳐 지나간 손은 지우고, 볼에 닿아 두드리기 시작한 손은 새로 적습니다. 그래서 여러 장면을 다 보고 나면 메모장엔 "이 구간 동안 손이 볼에 머물며 톡톡 두드렸다"는 줄거리가 남습니다.
•
출력 — 마지막에 이 메모(줄거리)를 보고 "볼에 두드리기"라고 답합니다. 사진 한 장이 아니라 여러 장면을 쌓아 만든 요약에서 답이 나오는 게 핵심입니다.
•
학습 — 처음에는 메모를 엉성하게 적어 자주 틀립니다. 하지만 "정답은 두드리기였어"라고 채점받기를 수천 번 반복하면, 어떤 장면을 메모에 남기고 어떤 장면을 흘려보내야 정답이 잘 나오는지 요령을 스스로 터득합니다.
즉, 한 장씩 보며(입력) → 중요한 것만 메모에 남기고(기억) → 쌓인 메모로 답하고(출력) → 틀린 만큼 메모 요령을 고쳐가며 똑똑해지는(학습) 과정이 LSTM의 한 사이클입니다.
이 구조가 이 문제와 잘 맞았던 이유는 다음과 같습니다.
동작은 "손이 잠깐 스쳐 지나가는 장면"처럼 버려야 할 순간과, "특정 부위에 머물며 반복적으로 두드리는 장면"처럼 끝까지 기억해야 할 순간이 섞여 있습니다.
forget·input 게이트가 이 둘을 알아서 가려내 주니, 모든 프레임을 똑같이 취급하는 모델보다 훨씬 안정적으로 행동을 잡아낼 수 있었습니다.
모델에 눈깔 붙이기
모델 구조를 정한 다음에는 "모델에게 무엇을 보여줄지"를 정해야 했습니다.
저는 이 부분을 모델의 눈이라고 불렀습니다. 사람이 눈으로 본 것 안에서만 판단하듯, 모델도 입력으로 받은 정보 안에서만 판단하기 때문입니다.
처음 모델에게 준 눈은 영상 프레임의 픽셀 자체가 아니라, Google의 MediaPipe를 사용해 그 프레임에서 뽑아낸 얼굴 478개, 양손 각 21개의 landmark(좌표 점), 그리고 표정 계수인 blendshapes 52개였습니다.
예를 들면 배우 몸에 점을 붙이고 그 점들의 움직임만으로 캐릭터를 살려내는 영화의 모션 캡처를 떠올리면 되는데, 배경·옷·조명을 다 걷어내고 "움직임의 뼈대"만 남기는 것이라서 적은 데이터로도 핵심을 배우기 좋습니다.
하지만 이 눈을 완성시키는데까지 몇 가지 해결해야 할 문제가 있었습니다.
첫 번째는, 좌표가 '위치'만 알려줄 뿐 그 위치가 어떻게 변하는지는 담지 못한다는 점이었습니다.
손이 입 주변에 있다는 사실 하나만으로는 그것이 바르는 행동인지, 그냥 스쳐 지나가는 장면인지 구분할 수 없었기 때문에 모델은 손이 '어디에 있는가'만 이해하고 '어떻게 움직이는가'는 이해할 수 없었습니다.
그래서 좌표를 그대로 넣는 대신, 사람이 행동을 알아볼 때 쓰는 단서로 직접 가공했습니다.
먼저, 영상마다 카메라 거리도 얼굴 크기도 제각각이라 얼굴 중심을 원점으로 얼굴 크기를 1로 맞춰 모든 좌표를 정규화했습니다. 그리고 정규화된 좌표에서 다음과 같은 단서들을 뽑아서 사용했습니다.
•
손끝과 얼굴 거리 — 양손의 엄지·검지·중지 끝과 얼굴의 여섯 기준점(이마·코·입·턱·양 볼) 사이의 거리. 손이 어느 부위에 얼마나 가까운지를 줄자로 재는 셈입니다.
•
손 모양 — 손가락 관절이 구부러진 각도와 엄지·검지 사이(핀치) 거리. 손이 펴졌는지, 오므렸는지, 무언가를 집고 있는지를 알려줍니다.
•
접촉 여부 — 손이 특정 부위에 닿을 만큼 가까워지면 1, 아니면 0으로 표시해 "지금 닿았다"를 분명히 합니다.
•
움직임 — 위 값들이 직전 프레임 대비 얼마나, 어느 방향으로 변했는지. 좌표가 아니라 좌표가 그리는 "화살표"를 보는 셈이며, 그 화살표가 다시 어떻게 변하는지(빨라지는지·느려지는지)까지 함께 담았습니다.
두 번째는, 이 모든 정보가 MediaPipe가 점을 제대로 찍어줄 때만 존재한다는 점이었습니다.
손이 화면 밖으로 나가거나 얼굴이 가려지거나 조명이 어두워 점을 놓치는 순간, 모델은 그대로 눈을 감아버렸습니다.
그래서 얼굴이나 손이 인식되지 않더라도 장면 자체에서 단서를 얻을 수 있도록, 이미지를 작은 조각(patch)으로 나눠 보는 ViT-B/16 백본 모델을 사용하여 프레임 이미지를 직접 보는 눈을 하나 더 붙였습니다.
백본이란 제가 "이건 입술, 이건 이마"라고 처음부터 가르치지 않아도 이미 수많은 이미지를 보며 시각의 기본기를 익혀 둔 모듈로써 이미지를 보고 그 안의 의미 있는 특징을 뽑아내도록 미리 학습을 마친 '눈' 전문가를 말합니다.
이러한 백본은 한 장의 이미지를 보고 피부 질감·손에 든 물체·밝기 같은 시각 정보가 응축된 수백 개의 숫자로 이루어진 한 줄의 요약본인 임베딩(embedding)을 만들어내고 이걸 이어붙여서 사용했습니다.
세 번째는, 백본 때문에 속도라는 현실적인 벽에 부딪혔다는 점이었습니다.
영상 한 편을 수많은 프레임으로 쪼개 매 프레임마다 임베딩을 뽑다 보니, 무거운 백본은 전체 파이프라인의 병목이 되었습니다.
기존에 쓰던 ViT-B/16은 정확했지만, 매 층에서 이미지 조각(patch)들이 서로의 관계를 전부 attention으로 따져보는 구조라, 프레임 한 장을 처리하는 데도 연산이 만만치 않았습니다. 그러니 프레임이 수백·수천 장 쌓이면 그 부담이 고스란히 불어났습니다.
그 대안으로 고른 것이 EfficientFormerV2-L입니다. 애초에 모바일·엣지 환경의 실시간 추론을 노리고 설계된 백본으로, 앞단에서는 가벼운 합성곱(convolution)으로 큰 정보를 빠르게 추리고, 토큰이 적어진 뒷단에서만 attention을 쓰는 하이브리드 구조입니다.
교체 뒤 효과는 분명했습니다. 정확도는 거의 그대로 유지되면서도 프레임당 임베딩 추출이 빨라져, 영상 한 편을 끝까지 처리하는 시간이 크게 줄었습니다.
실제로 같은 영상을 동일한 환경(MacBook M4 Pro 16, vCPU 14 · RAM 48GB, MPS 추론)에서 돌려 보니, 처리하는 데 ViT-B/16은 4분 25초가 걸렸던 반면 EfficientFormerV2-L은 1분 36초만에 끝났습니다. 거의 3분의 1 수준(약 2.8배 빠르게)으로 줄어든 셈입니다.
결론적으로 '모델의 눈'은 서로 다른 성격의 두 정보를 한 줄로 이어 붙인 것이 되었습니다.
•
한 프레임마다 MediaPipe가 뽑은 기하적 특징 — 좌표 · 움직임 · 거리. 접촉 여부 등을 봅니다.
•
한 프레임마다 EfficientFormerV2-L에 통과시켜 얻은 시각적 특징 — 화면 자체가 어떻게 생겼는지를 봅니다.
앞의 것이 "움직임의 뼈대"를 본다면, 뒤의 것은 "장면의 분위기"를 봅니다.
이 둘을 나란히 이어 붙여 한 프레임을 설명하는 긴 숫자 한 줄을 만들고, 이 줄들을 시간 순서대로 쌓아 앞서 본 LSTM에 넣었습니다.
성장판이 닫혀버린 모델
처음엔 정확도가 아쉬운 이유가 그저 데이터가 모자라서라고만 생각했습니다. 그런데 데이터를 아무리 늘려도, 어느 지점부터는 정확도가 더 오르지 않았습니다.
그래서 데이터를 늘리는 한편, 스무 번이 넘는 실험을 거치며 모델 구조도 함께 손봤습니다.
•
양방향 LSTM(BiLSTM)으로 확장 — 한 방향이 아니라 앞·뒤 맥락을 동시에 보면 나아질까 싶었습니다. 하지만 데이터가 많지 않다 보니 오히려 과적합되어, 단방향 LSTM보다 정확도가 떨어졌습니다.
•
층과 폭을 키워 용량 확대 — 더 많은 패턴을 담게 하려 했지만, 늘어난 용량만큼 다수 클래스로 쏠리는 현상만 심해졌습니다.
•
핵심 순간을 강조하는 attention 추가 — 중요한 프레임에 가중치를 주려 했는데, 오히려 짧은 동작의 특징이 흐트러져 특정 클래스의 정확도가 66%에서 48%로 떨어졌습니다.
•
각종 학습 기법 적용 — SWA · SAM · Cosine Scheduling · Mixup · Label Smoothing 같은 정규화·최적화 기법도 더해 봤습니다. 정확도를 조금씩 올려주긴 했지만, 천장 자체를 뚫지는 못했습니다.
그런데도 벽은 그대로였습니다. 세부 조정으로는 닿지 않는, 더 근본적인 곳에 문제가 있다는 신호였습니다.
이 문제는 Google에서 발표한 논문의 한 문단이 결정적인 힌트가 됐습니다.
"Learning long-range dependencies is a key challenge in many sequence transduction tasks. One key factor affecting the ability to learn such dependencies is the length of the paths forward and backward signals have to traverse in the network. The shorter these paths between any combination of positions in the input and output sequences, the easier it is to learn long-range dependencies."
- Attention Is All You Need, 2017.
멀리 떨어진 요소 사이의 의존 관계를 학습하는 일은 수많은 시퀀스 변환 과제에서 핵심 난제다. 그 능력을 좌우하는 핵심 요인 중 하나는, 신호가 신경망 안에서 오가야 하는 '경로의 길이'다. 입력과 출력의 어떤 위치 조합 사이든 그 경로가 짧을수록, 멀리 떨어진 의존 관계를 배우기가 쉬워진다.
실제 추론에서 모델은 제품을 바르는 동작과 얼굴을 톡톡 두드리는 동작처럼 손동작이 비슷한 행동끼리 유독 자주 헷갈렸습니다. 그런데 이런 행동을 가르는 결정적 단서는 대개 한 장면에 몰려 있지 않습니다.
뷰티 행동은 한순간에 끝나지 않고 여러 단계를 거쳐 완성되기 때문에, 무엇을 하는지 판가름하는 순간들이 시퀀스 안에서 서로 멀찍이 떨어져 있다는 게 진짜 문제였습니다.
LSTM은 정보를 앞에서 뒤로 한 방향으로만 흘려보내는 구조입니다. 첫 프레임의 정보는 마지막까지 가는 동안 작은 메모(hidden state) 하나에 계속 눌려 담기며 점점 흐려지죠. 그러니 멀리 떨어진 두 순간을 직접 맞대어 비교하기란 애초에 어려운 일이었습니다.
앞서 attention을 덧붙여도 큰 효과가 없었던 이유도 여기 있었습니다. 한 방향으로 정보를 흘려보내는 뼈대 위에 장치만 얹어서는, '모든 순간을 한자리에 펼쳐 놓고 한꺼번에 비교하는' 능력까지는 얻을 수 없었기 때문입니다.
점차 이건 데이터의 양이 아니라 모델이 담아낼 수 있는 정보의 한계 문제라는 게 분명해졌고, 데이터가 늘어날수록 바꿔야 할 것은 세부 조정이 아니라 시간을 다루는 구조 그 자체라는 결론에 이르게 되면서 모델의 대대적인 개편을 고려하게 되었습니다.
모델을 더 똑똑하게 만들기
먼저 모델 구조를 LSTM에서 Temporal Transformer로 바꾸기로 하였습니다.
LSTM이 정보를 한 시점씩 차례로 흘려보냈다면, Transformer는 각 프레임을 하나의 'token(토큰)'으로 두고 self-attention(셀프 어텐션)으로 토큰끼리 직접 관계를 따집니다.
여기서 self-attention이란, 토큰들이 서로를(self) 둘러보며 '어느 순간이 서로에게 얼마나 중요한지'에 스스로 주의를 기울인다(attention)는 뜻입니다. 그 중요도에 따라 상대 순간의 정보를 더 많이 끌어와 자기 표현을 보강합니다.
그래서 첫 프레임과 마지막 프레임처럼 멀리 떨어진 두 순간도 중간 단계를 거치지 않고 곧바로 이어집니다.
이러한 self-attention 층을 여러 겹 쌓아 올린 것이 바로 Transformer의 인코더(encoder)입니다.
입력을 받아 그 의미를 모델이 쓰기 좋게 정리해 내는 부분이죠. 그리고 이 인코더를 이미지가 아니라 시간 축(프레임의 흐름)에 적용했기에 Temporal Encoder라고 부릅니다.
이 Temporal Encoder은 차원 384 · 헤드 6개 · 층 4개로 쌓아 올렸고, 한 번에 연속 8프레임(window)을 한 묶음으로 봅니다.
•
차원 384 — 한 프레임(토큰)을 384개의 숫자로 표현한다는 뜻입니다. 숫자가 너무 적으면 한 순간에 담을 정보가 부족하고, 너무 많으면 무거워지고 과적합되기 쉽습니다.
•
헤드 6개 — 같은 구간을 서로 다른 여섯 관점으로 동시에 본다는 뜻입니다. 여러 명의 심사위원이 역할을 나눠 같은 영상을 보면서도 저마다 다른 포인트를 채점한 결과를 하나로 합쳐서 종합적으로 판단합니다.
•
층 4개 — 인코더 한 층은 '프레임끼리 정보를 한 번 주고받는' 과정입니다. 이걸 4번 쌓으면, 아래층에서는 단순한 관계(어느 순간끼리 비슷한가)를, 위로 올라갈수록 더 추상적인 관계(접근→접촉→반복이라는 흐름)를 읽게 됩니다. 회의를 네 차례 거치며 결론이 점점 정교해지는 것과 비슷하죠.
•
window 8프레임 — 한 번에 들여다보는 시간 폭입니다. 손이 다가가 잠깐 두드리는 한 동작 단위를 담기엔 충분하면서도, 너무 길게 잡으면 무거워지고 관계없는 장면까지 섞이기 때문에 8프레임으로 끊었습니다.
이렇게 모델 구조를 Transformer로 바꾸면서, 모델이 좀 더 똑똑하게 영상을 이해할 수 있도록 하였습니다.
그다음으로는 모델의 눈, 즉 무엇을 입력으로 줄지도 다시 설계했습니다.
이전까지는 좌표·거리·움직임 같은 기하적 특징과 EfficientFormer로 뽑은 시각적 특징을 하나의 긴 줄로 이어 붙여 통째로 모델에 넣었습니다. 성격이 전혀 다른 두 정보를 한 덩어리로 섞어 버린 셈이죠.
이번엔 그 둘을 한 줄에 섞는 대신, 각각 전문 인코더로 따로 처리한 뒤 Transformer가 한데 모아 보도록 바꿨습니다. 이렇게 성격이 다른 입력을 나눠 다루는 방식을 멀티모달(multimodal)이라고 부릅니다.
그래서 모델은 두 갈래의 인코더를 갖습니다 — 좌표·거리·움직임을 담는 기하 정보 인코더와, 화면을 직접 보는 시각 정보 인코더. 두 인코더가 각자 정리한 결과를 마지막에 하나로 합쳐 판단에 씁니다.
구조만 나눈 게 아니라 입력 내용도 보강했습니다. 기하 정보쪽에는 점만으로는 놓치던 움직임의 결을 채우려 OpticalFlow를 새로 더했고, 시각 정보쪽은 기존 EfficientFormer를 DINOv2로 바꿔 장면의 의미를 더 깊이 읽도록 했습니다.
첫번째로, OpticalFlow는 움직임의 결을 보기 위한 정보를 추출하는데 사용합니다.
좌표 점은 정해진 지점만 봅니다. 하지만 제품을 문지를 때 생기는 면 전체의 미세한 흐름은 점만으로는 잡히지 않습니다. 그래서 연속한 두 프레임 사이 픽셀이 어디로 움직였는지를 추정하는 OpticalFlow(Farnebäck, 2003)를 더했습니다.
특히 손동작이 비슷해 자주 헷갈리던 행동에서 '어느 방향으로 얼마나 빠르게' 움직이는지를 직접 모델에 알려주려 했고, 여기에 '손은 움직이는데 얼굴은 멈춰 있다'처럼 손과 얼굴의 흐름을 견주는 신호도 함께 넣어 '지금 무언가 하는 중'이라는 단서를 잡아내도록 설계했습니다.
두번째로, DINOv2는 장면의 의미를 좀 더 풍부하게 담기 위한 정보를 추출하는데 사용합니다.
기존 EfficientFormer는 이미지 한 장을 분류하도록 학습된 백본이라, 장면을 뭉뚱그린 요약 한 줄을 내주는 장점이 뚜렷했었습니다. 하지만 멀티모달로 재설계하며 정작 필요해진 건 '얼굴에서 따로, 손에서 따로 무엇이 보이는지'였고, 그러려면 화면을 조각 단위로 잘게 들여다보고 원하는 영역만 떠낼 수 있어야 했습니다. 바로 그 점에서 DINOv2로 교체를 하기로 했습니다.
DINOv2는 정답표 없이 수많은 이미지를 스스로 들여다보며 '시각의 기본기'를 익혀 둔 모델입니다. 앞서 학습 방식을 나눌 때 얘기한 자기지도학습(Self-supervised)으로 배워서, 사람이 라벨을 일일이 달아주지 않아도 "이 부분과 저 부분이 닮았다" 같은 의미 구조를 스스로 잡아냅니다. 그 뿌리에는 이미지를 조각으로 나눠 Transformer로 처리하는 ViT(Dosovitskiy et al., 2021)와 이 자기지도학습 방식을 정립한 DINO(Caron et al., 2021)가 있습니다.
이렇게 미리 학습된 모델을 가져다 쓰는 방법은 크게 세 가지입니다.
•
freeze — 손대지 않고 그대로 쓰는 것
•
fine-tune — 제 데이터로 전부 다시 학습시키는 것
•
partial fine-tune — 그 중간으로 일부 층만 다시 학습시키는 것
저는 partial fine-tune을 택했습니다. 전부 다시 학습시키면 비용이 크고 데이터가 적어 과적합되기 쉬운 반면, 그대로 쓰기만 하면 일반적인 시각 지식에 머물러 "토너를 바르는 손"처럼 뷰티 도메인 특유의 장면은 잘 읽어내지 못하기 때문입니다.
그래서 아래쪽 층은 얼린 채(freeze) 두고, 위쪽 몇 개 층만 풀어 제 데이터에 맞게 미세 조정(fine-tune)했습니다. 가장자리·질감 같은 보편적인 시각 기본기는 그대로 살리고, 뷰티 행동을 분간하는 감각만 새로 입히는 것입니다.
입력 데이터로는 화면을 통째로 넣는 대신 얼굴·손이 있는 영역(ROI)만 잘라 DINOv2에 보여줬습니다.
이미지 전체를 뭉뚱그린 요약 하나(CLS)만 쓰면 '장면이 대충 이렇다'에 그치기 때문에, 이 전역 요약(CLS)에 더해, 얼굴·왼손·오른손 위치의 조각을 따로 모아 함께 넣었습니다.
이로 인해 '얼굴에서 무엇이, 손에서 무엇이 보이는지'까지 나눠 학습할 수 있어 미세한 행동을 가려내기 좋기 때문에 MediaPipe가 점을 놓친 순간에도 좀 더 풍부해진 시각 정보가 빈자리를 메워 줍니다.
마지막으로 모델이 영상 속 주인공에게 좀 더 집중할 수 있도록 하였습니다.
현실 영상에는 여러 사람이 등장하거나 엉뚱한 곳을 얼굴이나 손이라고 잡습니다. 그런 것을 보고 잘못 판단하면 안 되므로, 모델의 눈이 주인공(primary) 한 명을 골라낼 필요가 있었습니다.
또한 얼굴 검출이 한 번 실패하면 그 프레임의 얼굴 정보가 통째로 비는데, 모델이 이 '얼굴 없음' 상태로 인해 엉뚱하게 분류하는 문제가 있었습니다. 그래서 가려지거나 측면이거나 어두운 얼굴까지 최대한 놓치지 않고 주인공을 끝까지 따라가는 추적이 중요했습니다.
이 '주인공 고르기'는 네 단계로 이어집니다. 한 단계로 끝내기 어려운 일이라, 각 단계가 앞 단계의 약점을 메우도록 쌓아 올린 구조입니다.
1.
얼굴을 빠르게 찾기(YuNet) — 먼저 가볍고 빠른 얼굴 검출기 YuNet으로 화면 속 얼굴 위치를 잡습니다. 작고 빠른 신경망이라 수많은 프레임을 훑어도 부담이 적습니다. 사진 한 장을 척 보고 "여기 얼굴, 저기 얼굴" 하고 네모를 쳐 주는 눈썰미 좋은 검출기라고 보면 됩니다.
2.
사이를 이어 따라가기(dlib) — 매 프레임 새로 검출하면 느리고, 박스가 프레임마다 들쭉날쭉 튑니다. 그래서 한 번 잡은 얼굴은 dlib으로 다음 프레임까지 '생김새'를 보고 부드럽게 쫓아갑니다. 한 번 점찍은 사람을 카메라로 졸졸 따라가는 셈이라, 매번 처음부터 다시 찾을 필요가 없습니다.
3.
같은 사람인지 확인(face_recognition) — 추적은 얼굴이 가려지거나 돌아서거나 장면이 바뀌면 곧잘 끊깁니다. 이때 얼굴을 숫자 벡터로 바꿔, 끊기기 전후의 벡터가 얼마나 가까운지로 "같은 사람인가"를 따져 끊긴 길을 다시 잇거나, 다른 사람이면 갈라냅니다. 얼굴을 지문처럼 고유한 숫자로 바꿔 두고 "아까 그 사람 맞네” 혹은 “이건 다른 사람이네"를 거리로 가려내는 것입니다.
4.
그래도 못 찾으면 얼굴 윤곽으로 대체(MediaPipe) — 옆모습·역광·가림 때문에 YuNet마저 얼굴을 놓치는 순간엔, MediaPipe로 얼굴 윤곽 점을 찍어 빈자리를 메웁니다. 앞서 말한, 얼굴이 통째로 비면 모델이 그 프레임을 엉뚱한 부위로 착각하던 문제를 막기 위해서입니다.
이렇게 한 프레임에서 얼굴은 최대 2명·손은 최대 4개까지 후보로 보되, 최종적으로 영상에서 가장 오래·크게 차지한 '주인공'만 추려 모델에 넘깁니다.
결론적으로 더욱 똑똑해진 모델의 입력과 출력은 다음과 같습니다.
•
입력 (멀티모달)
◦
기하 정보 인코더
▪
좌표·움직임·거리 등
▪
OpticalFlow
◦
시각 정보 인코더
▪
DINOv2
→ 두 인코더가 각자 정리한 결과를 하나로 합친 뒤 Temporal Transformer로 8프레임의 시간 맥락을 학습
•
출력 (멀티헤드)
◦
Action Head(무슨 행동인지) + Region Head(어느 부위인지)
모델 가르치기
아무리 좋은 모델이라도 결국 무엇을 보고 배웠는지가 실력을 가릅니다.
그래서 모델을 학습시키기에 앞서, 모델이 학습할 '교과서'부터 만들어야 했습니다.
교과서를 만드는 일이 바로 라벨링입니다. 영상을 전체적으로 훑으며 특정 구간마다 "이 장면은 무슨 동작(Action)을, 어느 부위(Region)에 하고 있다"를 직접 달아 줬습니다.
동작은 22가지, 부위는 20가지로 정의했으며, 앞서 출력을 두 갈래(multi-head)로 나눈 만큼 동작과 부위도 따로 라벨링했습니다. 이렇게 단 라벨은 프레임 단위로 펼쳐져 학습과 채점에 쓰입니다.
가장 까다로웠던 건 기준이었습니다. 같은 장면을 봐도 "바르는 건지 두드리는 건지", "볼인지 광대인지" 애매한 경우가 많았고, 사람마다 다르게 달면 교과서의 내용이 흔들립니다. 교과서가 잘못되면 아무리 똑똑한 학생도 엉뚱하게 배웁니다.
라벨링은 단순한 사전 준비가 아니라, 모델 성능의 절반을 좌우하는 일이었습니다.
하지만 이렇게 만든 교과서는 이미 기울어진 운동장이었습니다.
뷰티·퍼스널케어 영상은 대부분 "얼굴에 무언가 바르는" 장면입니다. 그러다 보니 “바르기”가 압도적으로 많았습니다. 실제로 가장 많은 동작인 바르기는 약 3,200세그먼트가 있던 반면, 가장 드문 “먹기”는 약 40세그먼트뿐이라 그 차이가 약 85배에 달했습니다.
이건 데이터를 덜 모아서가 아니라, 도메인 자체가 그렇게 생긴 것이라고 생각했습니다. 그래서 데이터를 바꿀 수 없다면, 가르치는 방식을 바꾸기로 했습니다.
핵심 아이디어는 단순합니다. 소수 클래스의 목소리를 일부러 키워 주는 것입니다. 저는 이를 위해 세 개의 학습 방식을 적용했습니다.
•
더 자주 호명하기(가중 샘플링) — 드문 동작이 담긴 영상을 학습 중에 더 자주 꺼내 봅니다.
•
틀렸을 때 더 크게 혼내기(Class Weight) — 소수 클래스를 틀리면 벌점을 더 무겁게 매깁니다.
•
쉬운 문제는 가볍게, 어려운 문제에 집중(Focal Loss) — 이미 잘 맞히는 다수 클래스는 점수를 깎고, 자꾸 틀리는 쪽으로 학습을 몰아줍니다.
이 셋은 곱셈처럼 맞물려 소수 클래스가 학습에 미치는 영향력을 수십 배로 끌어올립니다. 다만 강하게 적용할수록 다수 클래스의 정확도가 2~4%p 양보되는 트레이드오프가 따랐기에, 결국 '소수를 살리는 것'과 '다수를 지키는 것' 사이의 균형점을 찾는 실험의 반복이었습니다.
소수에 힘을 실어주는 세 가지 학습 방식과는 별개로, 두 가지의 '안전장치'도 함께 달았습니다.
첫 번째는, 모델이 답을 통째로 외우는 것을 막는 정규화입니다.
데이터가 적으면 모델은 정답을 외워버리기 쉬운데, 실제로 한때는 학습 정확도가 90%를 넘는데 검증 정확도는 60%대에 머물러, 둘 사이가 30%p나 벌어진 적이 있었습니다. 전형적인 과적합 신호였죠.
그래서 정답을 100% 확신하는 대신 살짝 누그러뜨려 과신을 줄이는 Label Smoothing과, 두 영상을 반반 섞은 가짜 예시까지 보여주어 클래스 사이 경계를 부드럽게 만드는 Mixup으로, 모델이 외우기보다 일반화하도록 이끌었습니다.
그 결과 한때 30%p까지 벌어졌던 학습·검증 정확도 차이는, 최근 실험에서 평균 약 14%p까지 좁혀졌습니다.
두 번째는, 학습 막판의 출렁임을 가라앉히는 안정화입니다.
데이터를 늘려 가는 동안 검증 정확도가 epoch마다 40%~80% 사이로 크게 널뛰어, 어느 시점의 모델을 골라야 할지조차 애매했습니다.
그래서 한 순간의 모델에 기대지 않도록 여러 epoch의 모델을 평균 내는 EMA·SWA와, 학습률을 코사인 곡선처럼 한 번에 부드럽게 줄여 막판 진동을 줄이는 Cosine Annealing을 함께 사용했습니다.
그 덕분에 epoch마다 40%에서 80%까지 널뛰던 검증 정확도도 이제는 평균 약 71%에 비교적 안정적으로 자리 잡았습니다.
모델 채점하기
모델이 얼마나 잘하는지 재는 가장 쉬운 잣대는 전체 정확도(accuracy), 즉 '전체 중 몇 개를 맞혔나'입니다.
하지만 이 한 숫자만 믿기엔 위험했습니다. 데이터가 많이 기울어 있다 보니, 모델이 헷갈릴 때마다 무조건 다수(바르기)로만 찍어도 정확도는 그럴듯하게 높게 나오게 됩니다.
'몇 개 맞혔나'는 알려줘도 '무엇을 무엇으로 틀렸나'는 가려 주지 못하는 문제가 있었습니다. 그래서 저는 모델을 하나의 숫자가 아니라 여러 각도에서 채점했습니다.
첫번째는, 정확도입니다.
전체(train+val) 평균 정확도도 쓰긴 했지만, 학습에 쓴 데이터(train)와 한 번도 보여주지 않은 데이터(val)를 반드시 나눠서 봤습니다.
학습 데이터는 모델이 이미 답을 본 '풀어 본 문제집'이라 잘 맞히는 게 당연하고, 진짜 실력은 처음 보는 '모의고사(val)'에서 드러나기 때문입니다.
실제로 최근 버전 모델은 학습 정확도가 평균 약 86%인데 검증은 약 71%였습니다. 이 둘의 차이(약 14%p)가 곧 '외운 정도', 즉 과적합의 크기가 됩니다.
그래서 외부에 내놓을 수 있는 정직한 점수는 검증 정확도뿐이고, 학습–검증 격차는 다음에 무엇을 손봐야 할지 알려주는 신호가 됩니다.
두번째는, Confusion Matrix입니다.
정확도가 '총점'이라면, confusion matrix(혼동 행렬)는 오답노트입니다.
세로축은 실제 정답, 가로축은 모델의 예측이고, '립을 바르는 장면을 얼굴 두드리기로 잘못 봤다' 같은 실수를 칸칸이 쌓습니다. 대각선(정답=예측)이 진할수록 잘 맞힌 것이고, 대각선 밖이 진하면 그 두 클래스를 자주 헷갈린다는 뜻입니다.
여기서 두 가지를 신경 썼습니다.
첫째, 칸을 개수가 아니라 행 기준 비율로 정규화했습니다. 개수 그대로 보면 압도적으로 많은 바르기가 행렬을 다 차지해 소수 동작이 묻혀 버리기 때문에, '실제 눈썹 중 몇 %를 눈썹이라 맞혔나'처럼 비율로 바꿔 클래스마다 공평하게 봤습니다.
둘째, 예측을 모을 때 미래 프레임을 엿보지 않고 과거~현재만으로 판단하게 했습니다. 실제 서비스가 영상을 실시간으로 흘려보며 추론하는 조건과 똑같이 채점해야 점수가 부풀려지지 않고 객관적이기 때문입니다.
세번째는, UMAP(Uniform Manifold Approximation and Projection)입니다.
confusion matrix가 ‘무엇을 무엇으로 틀렸나'를 본다면, UMAP은 그 한 단계 안쪽, 모델이 각 데이터를 내부에서 어떻게 표현하는지를 들여다봅니다.
모델이 영상에서 뽑아낸 고차원의 특징(feature)을 사람이 볼 수 있는 3차원으로 압축해, 같은 행동·부위끼리 한곳에 잘 뭉치는지(군집화)를 눈으로 확인하는 데 썼습니다.
원리는 '가까운 건 가깝게'입니다. 고차원에서 서로 이웃했던 점들의 관계를 최대한 보존하며 3차원에 펼치기 때문에, 모델이 비슷하게 본 데이터끼리 가까이 모입니다.
그래서 어떤 클래스가 또렷한 무리로 따로 떨어져 모이면 모델이 그 클래스를 확실히 구분한다는 뜻이고, 여러 색이 한데 뒤섞여 경계가 흐리면 바로 그 지점에서 표현이 덜 갈라져 헷갈린다는 신호입니다.
정확도나 혼동 행렬이 '결과'로만 알려주던 헷갈림을, 모델의 표현 공간에서 직접 짚어 볼 수 있게 됩니다.
이렇게 여러 잣대를 함께 쓴 이유는 높은 숫자 하나를 확인하기 위해서가 아니라, 모델이 무엇을, 왜 잘못 이해하는지를 찾아 다음 실험의 방향을 정하기 위해서였습니다.
예를 들어 confusion matrix가 'TAPPING
POKING
APPLYING 혼동'과 '눈썹·인중 같은 미세 부위의 낮은 정확도'를 콕 집어 주면, 거기에 맞춰 입력 해상도를 384²로 올리고 손 영역까지 함께 잘라 넣는 식으로 대응했더니 실제로 그 조정만으로 TAPPING은 +3.3%p, POKING은 +2.4%p 회복됐었습니다.
마무리
이 모델을 만들며 가장 크게 느낀 점은 모델보다 데이터가 먼저라는 것이었습니다.
처음부터 복잡한 모델을 짓기보다 작은 모델로 빠르게 검증하고 한계가 보일 때 구조를 바꾸는 방식이 현실적이라고 느꼈습니다. 데이터가 적을 땐 LSTM이 가장 적합했고, 데이터가 늘고 클래스가 다양해지자 Temporal Transformer가 필요해졌습니다.
또한 모델의 구조를 바꾸고, 문제의 복잡도가 커짐과 동시에 모델에게 무엇을 보여줄 것인가에 대한 고민도 함께 생겨났습니다. 모델이 스스로 모든 걸 추론하게 만드는 것보다, 문제를 이해하는 데 필요한 신호를 명확히 제공하는 편이 더 효과적이었습니다.
마지막으로, 도메인 특화 모델에서는 정답보다 기준이 중요하다는 것도 느꼈습니다. 같은 영상을 보고도 어떤 행동으로 라벨링할지 애매한 경우가 많았고, 기준이 흔들리면 모델 자체가 흔들렸습니다.
결론적으로 AI 모델을 만드는 일은 모델을 고르는 일이라기보다 문제를 정의하고, 모델이 그 문제를 이해하도록 데이터를 설계하는 일에 더 가까웠습니다.



























