포스트

얼굴 인식이 자꾸 틀리는 이유: 검출, 정렬, FaceNet Triplet Loss 점검

FaceNet의 성능만 바꿔서는 얼굴 인식 오류가 해결되지 않는다. 얼굴 검출 → 특징점 탐지 → 정렬 → crop → embedding 비교 중 앞 단계가 흔들리면 같은 사람의 벡터도 멀어질 수 있기 때문이다.

따라서 최종 이름 하나만 저장하지 말고 각 단계의 box, landmark, 정렬 이미지, embedding 거리와 판정 기준을 함께 본다. 논문 수치는 구조의 근거이지 내 카메라와 등록 데이터의 보장값이 아니다.

얼굴 인식 파이프라인은 어떤 순서로 이어질까?

face_detection

1
2
3
4
5
Face Detection
→ Face Feature Point Detection
→ Face Alignment
→ Face Crop
→ Face Identification

이 글은 각 단계가 다음 단계에 무엇을 넘기는지, FaceNet의 triplet loss가 그 위에서 무엇을 학습하는지에 초점을 맞춘다.

얼굴 검출기는 무엇이 다른가

얼굴 검출 단계의 출력은 보통 얼굴 사각형 좌표다. 원문에서는 OpenCV와 Dlib의 네 방식을 비교했다.

OpenCV Haar Cascade

Haar cascade는 영역 사이의 밝기 차이로 만든 특징을 이용한다. 눈 주변이 코보다 상대적으로 어둡다는 식의 패턴을 여러 filter로 훑으며 얼굴 후보를 찾는다. 사용할 모델은 OpenCV haarcascades에서 확인할 수 있다.

haar

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import cv2

image = cv2.imread('test.jpg')
detector = cv2.CascadeClassifier(
    '../models/haarcascade_frontalface_default.xml'
)
faces = detector.detectMultiScale(image)

for x, y, width, height in faces:
    cv2.rectangle(
        image,
        (x, y),
        (x + width, y + height),
        (0, 0, 255),
        5,
    )

haar_sample

OpenCV DNN과 Dlib

OpenCV DNN 예제는 Caffe 모델과 config를 읽고, confidence가 0.9보다 큰 상자를 남겼다. 모델 파일과 deploy.prototxt가 별도로 필요하므로 아래 흐름만으로 단독 실행되지는 않는다.

1
2
3
4
5
6
7
8
9
10
11
detector = cv2.dnn.readNetFromCaffe(config_file, model_file)
blob = cv2.dnn.blobFromImage(
    image,
    scalefactor=1.0,
    size=(frame_height, frame_width),
    mean=[104, 117, 123],
    swapRB=False,
    crop=False,
)
detector.setInput(blob)
detections = detector.forward()

dnn_sample

Dlib HOG+SVM은 정면 얼굴에는 잘 맞지만 측면이나 특이한 자세에서 놓칠 수 있었다. Dlib CNN 검출기는 그런 얼굴을 더 찾을 수 있는 대신 다른 비용을 고려해야 한다. 중요한 것은 이름만 보고 선택하지 않고, 실제 카메라 각도와 조명에서 누락, 오탐, 처리 시간을 함께 비교하는 것이다.

hog_sample

cnn_sample

정렬이 identification보다 먼저인 이유

같은 사람도 고개가 기울거나 얼굴 위치가 다르면 픽셀 배열이 크게 달라진다. landmark로 눈과 코의 기준점을 찾고 얼굴을 정렬하면 embedding 모델이 자세 차이보다 얼굴 특징에 집중하기 쉬워진다.

원문은 Dlib의 5-point 또는 68-point landmark 모델과 imutils.FaceAligner를 사용했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from imutils.face_utils import FaceAligner, rect_to_bb
import dlib
import imutils
import cv2

detector = dlib.get_frontal_face_detector()
predictor = dlib.shape_predictor(
    '../models/shape_predictor_5_face_landmarks.dat'
)
aligner = FaceAligner(predictor, desiredFaceWidth=256)

image = imutils.resize(cv2.imread('test.jpg'), width=800)
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

for rect in detector(gray, 2):
    x, y, width, height = rect_to_bb(rect)
    cropped = imutils.resize(
        image[y:y + height, x:x + width], width=256
    )
    aligned = aligner.align(image, gray, rect)

이 코드는 landmark 모델 파일과 입력 이미지가 필요한 핵심 조각이다. 정렬 뒤에 다시 얼굴을 검출해 crop하는 원문 흐름에서는 두 번째 검출이 실패할 수도 있으므로, 결과가 비어 있을 때의 처리도 따로 정해야 한다.

point_sample

alignment

정렬 결과를 눈으로 확인할 때는 얼굴이 “예뻐 보이는가”보다 두 눈의 위치와 crop 범위가 여러 이미지에서 일관적인지 본다. 이 단계가 다르면 뒤의 거리 threshold를 아무리 조정해도 근본 원인이 남는다.

FaceNet은 얼굴을 어떻게 비교 가능한 벡터로 만드나

FaceNet은 얼굴 이미지를 embedding 벡터로 바꾸고 벡터 사이 거리로 같은 사람인지 판단한다. 학습의 중심은 anchor, positive, negative 세 이미지를 이용하는 triplet loss다.

triplet

  • anchor \(x_i^a\): 기준 얼굴
  • positive \(x_i^p\): anchor와 같은 사람
  • negative \(x_i^n\): 다른 사람
  • margin \(\alpha\): 두 거리 사이에 확보할 간격

원하는 관계는 같은 사람의 거리에 margin을 더해도 다른 사람의 거리보다 작아지는 것이다.

\[\lVert f(x_i^a)-f(x_i^p)\rVert_2^2+\alpha \lt\lVert f(x_i^a)-f(x_i^n)\rVert_2^2\]

loss는 이 조건을 위반하는 정도를 합한다.

\[L=\sum_i^N\left[ \lVert f(x_i^a)-f(x_i^p)\rVert_2^2 -\lVert f(x_i^a)-f(x_i^n)\rVert_2^2 +\alpha\right]\]

이미 충분히 쉬운 triplet만 계속 보면 loss에 기여하는 정보가 적다. 그래서 어려운 positive와 negative를 고르는 triplet selection이 중요하다. 전체 데이터에서 매번 찾는 것은 비용이 크므로 원문은 offline 선택과 mini-batch 안에서 찾는 online 선택을 구분한다. 무조건 가장 어려운 샘플만 고르면 잘못된 라벨이나 검출 실패까지 학습할 수 있으므로, “어렵다”는 이유와 이미지 품질을 함께 확인해야 한다.

거리 threshold는 어떻게 평가하나

embedding을 만들었다고 끝이 아니다. 두 얼굴의 거리 \(D(x_i,x_j)\)가 threshold \(d\) 이하이면 같은 사람으로 받아들인다고 하자.

  • True Accept: 같은 사람 쌍을 같은 사람으로 받음
  • False Accept: 다른 사람 쌍을 같은 사람으로 잘못 받음
\[VAL(d)=\frac{|TA(d)|}{|P_{same}|}\] \[FAR(d)=\frac{|FA(d)|}{|P_{diff}|}\]

threshold를 느슨하게 하면 같은 사람을 더 받아들이는 동시에 다른 사람도 잘못 받아들일 수 있다. 따라서 accuracy 하나보다 사용 목적에 맞는 FAR에서 VAL을 비교해야 한다.

논문에는 NN1, NN2 구조와 embedding 차원, 데이터 양, 이미지 품질에 대한 실험이 포함돼 있다.

model

model2

embedding_bench

embedding 차원은 크다고 무조건 좋아지지 않았고, 학습 데이터가 늘면서 성능은 좋아지지만 어느 지점에서는 수렴하는 모습을 보였다. 이미지 품질 실험에서는 220×220으로 학습한 모델이 품질을 20%까지 낮춘 조건에서도 견고한 결과를 보였다.

논문 수치를 내 시스템 성능으로 착각하지 않기

논문은 LFW에서 center crop 조건 98.87% ± 0.15, 별도 얼굴 검출, 정렬 조건 99.63% ± 0.09를 보고했다.

lfw

model_bench

이 수치는 논문의 데이터와 전처리 조건에서 나온 결과다. 내 시스템에는 다른 카메라, 자세, 가림, 조명과 threshold가 들어간다. 배포 전에는 전체 파이프라인을 다음처럼 나눠 기록해야 한다.

  • 검출 실패율과 잘못된 얼굴 crop 비율
  • 정렬 전후 embedding 거리 변화
  • 같은 사람, 다른 사람 거리 분포
  • 목표 FAR에서의 VAL
  • 얼굴 품질이 낮을 때의 거부 정책

FaceNet은 “얼굴을 바로 맞히는 분류기”라기보다 비교 가능한 공간을 학습하는 방법이다. 따라서 최종 오류를 줄이는 가장 빠른 길은 모델만 교체하는 것이 아니라, 어느 단계에서 정보가 잘못 들어왔는지를 먼저 찾는 것이다.

오인식 한 건을 어떻게 역추적하나

원본 frame과 검출 box를 먼저 저장한다. 얼굴 일부가 잘렸거나 다른 사람 box가 들어갔다면 embedding threshold를 조정하지 않는다. Landmark와 정렬 결과를 등록 이미지 옆에 두고 눈, 코, 입 위치와 crop scale이 일관적인지 확인한다.

정렬이 맞다면 같은 사람 후보들과 다른 사람 후보들의 거리를 함께 본다. 가장 가까운 한 값만 보지 않고 두 분포가 겹치는 구간과 입력 품질을 기록한다. 어두움, 측면, 흐림에서만 겹친다면 품질이 낮은 얼굴을 거부하거나 등록 데이터를 보완할 근거가 된다.

Threshold를 바꿀 때는 false accept와 false reject가 함께 움직인다. 보안 출입처럼 다른 사람을 받아들이는 비용과 사진 정리처럼 같은 사람을 놓치는 비용이 다르므로 하나의 보편값을 찾지 않는다. 운영 목적과 평가 dataset을 판정 기준 옆에 명시한다.

등록 사진을 추가하거나 embedding model을 교체하면 기존 threshold를 그대로 쓰지 않습니다. 같은 사람, 다른 사람 거리 분포를 새로 만들고, 예전 등록 벡터와 새 벡터가 섞이는 migration 기간의 판정도 별도로 시험해야 합니다.

Verification과 identification을 먼저 구분해야 한다

FaceNet embedding을 쓰는 방식은 질문에 따라 달라진다. Verification은 “입력 얼굴과 제시된 사용자 A가 같은 사람인가”를 묻는 1대1 비교다. Identification은 “등록된 여러 사람 가운데 누구와 가장 가까운가”를 묻는 1대다 비교다. 두 경우 모두 거리를 쓰지만 후보 수와 오인식 조건이 다르다. 등록 인원이 늘면 우연히 가까운 다른 사람을 만날 기회도 늘어나므로, 1대1 검증에서 정한 기준을 큰 검색 집합에 그대로 적용했다고 같은 오류율이 보장되지는 않는다.

식별 결과에는 가장 가까운 후보의 이름만 내보내지 않는 편이 안전하다. 최소 거리, 두 번째로 가까운 거리, 입력 품질, threshold 통과 여부를 함께 기록하면 “정답 후보는 가깝지만 다른 후보와 구분이 안 되는 상황”을 찾을 수 있다. 가장 가까운 거리도 기준보다 크다면 등록되지 않은 사람으로 거부하는 open-set 정책이 필요하다. 모든 입력을 반드시 등록 인원 중 한 명으로 배정하는 closed-set 평가는 출입 통제처럼 미등록자가 들어오는 환경을 충분히 설명하지 못한다.

얼굴 하나를 여러 장 등록할 때는 평균 embedding 하나만 남길지, 자세, 조명별 벡터를 여러 개 유지할지도 결정해야 한다. 평균은 검색 비용과 저장량을 줄이지만 품질이 낮거나 잘못 검출된 사진까지 섞이면 대표점이 흐려질 수 있다. 여러 벡터를 유지하면 측면과 정면을 각각 비교할 수 있는 대신 후보 수가 늘고 threshold를 다시 평가해야 한다. 어느 방식을 택하든 등록 단계에도 검출, 정렬 품질 검사를 적용해야 한다.

작은 검증 세트로 threshold를 정하는 순서

먼저 실제 운영 카메라에서 같은 사람 쌍과 다른 사람 쌍을 만든다. 같은 사진을 복제해 만든 쌍은 자세, 조명 변화가 없으므로 너무 쉬운 평가가 된다. 시간대, 안경이나 마스크, 카메라 거리처럼 운영에서 반복되는 조건을 사람별로 나누어 포함한다. 학습이나 threshold 선택에 쓴 사람과 최종 평가 사람을 분리하면 특정 등록자에게만 맞춘 기준을 발견하기 쉽다.

각 쌍에 대해 전처리 버전, 검출 confidence, 얼굴 box 크기, landmark 결과와 embedding 거리를 한 행에 저장한다. 그다음 threshold 후보를 낮은 값부터 올리며 false accept와 false reject를 동시에 계산한다. 여기서 좋은 기준은 단순 accuracy 최대값이 아니라 서비스가 감당할 수 있는 오류 비용을 만족하는 값이다. 사진 자동 분류는 일부 누락을 줄이는 쪽을 택할 수 있지만, 계정 인증은 다른 사람을 받아들이는 비율을 더 엄격하게 제한해야 한다.

예를 들어 같은 사람 거리 대부분이 0.5 아래이고 다른 사람 거리 대부분이 0.8 위라고 해도 0.5와 0.8 사이 숫자를 임의로 고르면 안 된다. 두 분포의 꼬리에 어떤 품질 조건이 있는지 먼저 본다. 흐린 얼굴에서만 같은 사람 거리가 커진다면 threshold를 전체적으로 느슨하게 하기보다 흐림을 거부하고 다시 촬영하게 하는 편이 다른 사람 오인식을 덜 늘린다. 반대로 특정 집단이나 촬영 각도에서만 오차가 몰린다면 전체 평균 하나로 문제를 숨기지 말고 조건별 지표를 따로 공개해야 한다.

배포 후에는 거리 분포가 기준을 정한 시점과 달라지는지도 감시한다. 카메라 교체, 얼굴 검출기 변경, 이미지 압축, embedding 모델 교체는 모두 입력 분포를 바꿀 수 있다. 모델 파일의 해시와 전처리 설정을 판정 로그에 남기면 어느 변경 뒤에 거리가 이동했는지 되짚을 수 있다. threshold 변경은 설정 한 줄의 수정이 아니라 평가 세트와 운영 정책을 함께 갱신하는 변경으로 다룬다.

증상별로 어느 단계를 먼저 의심할까

얼굴을 아예 찾지 못한다면 embedding 모델이 아니라 검출기의 최소 얼굴 크기, 각도와 조명부터 본다. 얼굴은 찾지만 이마나 턱이 반복해서 잘린다면 box 확장 비율과 crop 경계를 확인한다. 정면에서는 맞고 기울어진 얼굴에서만 거리가 커진다면 landmark와 alignment 결과를 나란히 비교한다. 동일한 정렬 이미지인데도 거리가 크게 흔들릴 때 비로소 채널 순서, 픽셀 범위, resize 방식과 embedding 모델 버전을 확인할 차례다.

다른 사람을 같은 사람으로 받는 false accept가 늘었다면 threshold만 낮추기 전에 등록 데이터 오염을 찾는다. 한 사람의 등록 묶음에 다른 얼굴이 들어갔거나 여러 사람이 찍힌 사진에서 잘못된 box를 선택하면 그 사람의 대표 벡터 자체가 잘못된다. 같은 사람을 거부하는 false reject가 특정 카메라에 집중되면 해당 카메라의 해상도, 노출, 압축과 학습 데이터 조건의 차이를 기록한다.

재현 가능한 최소 사례는 원본 두 장, 검출 box, landmark 좌표, 정렬 crop, 정규화된 입력과 두 embedding으로 구성한다. 개인정보 때문에 원본을 오래 저장할 수 없다면 승인된 짧은 보존 기간과 접근 통제를 먼저 정하고, 이후에는 품질 수치와 거리처럼 복원 위험이 낮은 진단 정보만 남길 수 있다. Embedding도 개인을 구분하는 생체정보로 취급될 수 있으므로 단순한 익명 숫자라고 가정해서는 안 된다.

출처가 설명하는 범위와 설명하지 않는 범위

이 글의 triplet loss, online triplet selection과 LFW 수치는 FaceNet 원 논문의 실험과 이 글에 인용된 구현 조각을 기준으로 설명했다. 논문은 embedding 공간을 학습하는 원리와 당시 평가 조건을 제시하지만, 특정 OpenCV, Dlib 버전이나 독자의 카메라에서 같은 성능을 보장하지 않는다. 예제 코드 역시 모델 파일 경로, 이미지 색상 순서, 입력 크기와 예외 처리를 모두 포함한 완성 애플리케이션이 아니다.

따라서 논문 accuracy는 구현 검산용 참고값이고 배포 승인 기준은 별도로 만든다. 재현 문서에는 사용한 검출기와 landmark 모델, alignment 규칙, embedding 모델 체크섬, 거리 종류, threshold 선택 데이터와 평가 날짜를 적는다. 이 정보가 있어야 숫자가 달라졌을 때 알고리즘 차이인지 전처리 차이인지 판단할 수 있다.

함께 읽으면 이해가 이어지는 글

자주 묻는 질문

FaceNet은 얼굴 이미지를 바로 사람 이름으로 분류하나요?

핵심 출력은 얼굴을 비교할 embedding입니다. 등록 얼굴과의 거리를 계산하고 threshold나 별도 분류 기준을 적용해 verification, identification 결과를 만듭니다.

같은 사람의 embedding 거리가 멀면 모델만 바꿔야 하나요?

먼저 얼굴 검출 box, landmark, 정렬, crop과 전처리가 두 이미지에서 일관적인지 확인합니다. 앞 단계가 흔들리면 같은 모델도 다른 얼굴 영역을 비교합니다.

거리 threshold는 논문 값을 그대로 쓰면 되나요?

안 됩니다. 자신의 카메라와 데이터에서 같은 사람, 다른 사람 거리 분포를 만들고 허용할 오인식, 거부 비용에 맞춰 정해야 합니다.

THE END / OPSOAI

여기까지 읽었습니다

핵심 장면을 한 번 더 떠올려 보세요. 이해가 남았다면 이 책은 제 역할을 다했습니다.

다른 책 고르기
표지 1

키와 좌우 스와이프를 지원합니다. 읽던 페이지는 이 기기에 저장됩니다.

CONTENTS

이 책의 목차

    13개 장 25 분읽는 시간