iBetter Books
수정

이제 앞 장들의 직관을 코드로 만질 차례입니다. facenet-pytorch는 FaceNet 계열 모델을 파이토치로 손쉽게 쓰게 해 주는 라이브러리로, 얼굴 한 장에서 512차원 임베딩을 뽑아 줍니다. 이 장에서는 두 얼굴의 임베딩을 비교해 같은 사람인지 가리고, 단 한 장의 사진만으로 사람을 등록하는 원샷 인식의 직관을 익힙니다.

두 개의 부품 — MTCNN과 InceptionResnetV1

facenet-pytorch는 두 부품을 함께 제공합니다. 하나는 얼굴을 찾아 정렬해 주는 MTCNN(PART 02·03에서 본 그 검출·정렬), 다른 하나는 정렬된 얼굴에서 임베딩을 뽑는 InceptionResnetV1입니다. 즉 한 라이브러리 안에서 "검출·정렬 → 임베딩"이 한 번에 이어집니다.

# 파일: embed.py"""facenet-pytorch로 얼굴 임베딩을 뽑는다."""import torchfrom PIL import Imagefrom facenet_pytorch import MTCNN, InceptionResnetV1# 얼굴을 찾아 160x160으로 정렬해 주는 검출기mtcnn = MTCNN(image_size=160, margin=0)# VGGFace2로 학습된 임베딩 모델 (512차원). eval()로 추론 모드resnet = InceptionResnetV1(pretrained="vggface2").eval()def get_embedding(path):    img = Image.open(path).convert("RGB")    face = mtcnn(img)                 # 정렬된 얼굴 텐서, 없으면 None    if face is None:        return None    with torch.no_grad():            # 추론에는 기울기 계산 불필요        return resnet(face.unsqueeze(0))   # 결과 shape: (1, 512)

mtcnn(img)가 PIL 이미지에서 얼굴을 찾아 160×160으로 정렬한 텐서를 돌려주고, resnet(...)이 그것을 512개 숫자로 바꿉니다. pretrained="vggface2"는 VGGFace2라는 대규모 얼굴 데이터셋으로 학습된 가중치를 쓰겠다는 뜻입니다(다른 선택지로 casia-webface도 있습니다).

두 얼굴 비교하기

임베딩을 뽑았으면, 2장에서 배운 거리로 비교하면 끝입니다.

# 파일: compare.py (embed.py의 get_embedding 사용)import torch.nn.functional as Fe1 = get_embedding("person_a_1.jpg")e2 = get_embedding("person_a_2.jpg")   # 같은 사람의 다른 사진e3 = get_embedding("person_b.jpg")     # 다른 사람def report(name, a, b):    dist = (a - b).norm().item()    sim = F.cosine_similarity(a, b).item()    print(f"{name}: 거리 {dist:.3f}, 코사인 {sim:.3f}")report("같은 사람", e1, e2)   # 거리 작게, 코사인 1에 가깝게report("다른 사람", e1, e3)   # 거리 크게, 코사인 낮게

같은 사람의 두 사진은 거리가 작고 코사인이 1에 가깝게, 다른 사람은 거리가 크고 코사인이 낮게 나옵니다. 임계값(예: 거리 1.0)을 정하면 자동 판정이 됩니다. 1장에서 상상한 "같은 사람끼리 모이는" 임베딩 공간을 실제 숫자로 확인하는 순간입니다.

원샷 인식 — 한 장으로 등록하기

여기서 임베딩의 진짜 위력이 드러납니다. 새로운 사람을 등록하는 데 재학습이 전혀 필요 없습니다. 그 사람의 얼굴 한 장에서 임베딩을 뽑아 저장해 두기만 하면, 이후 들어오는 얼굴과 거리를 재서 같은 사람인지 판별할 수 있습니다. 이렇게 한 장(또는 소수)의 예시만으로 인식하는 것을 원샷(one-shot) 인식이라 합니다.

# 파일: oneshot.py (개념)gallery = {                          # 등록부: 이름 → 임베딩 (한 장씩)    "홍길동": get_embedding("hong.jpg"),    "김철수": get_embedding("kim.jpg"),}def identify(path, threshold=1.0):    query = get_embedding(path)    best_name, best_dist = None, 999    for name, emb in gallery.items():        d = (query - emb).norm().item()        if d < best_dist:            best_name, best_dist = name, d    return best_name if best_dist < threshold else "미등록자"

새 직원이 들어오면 사진 한 장만 gallery에 추가하면 됩니다. 수천 장을 모아 모델을 다시 훈련시킬 필요가 없습니다. 이것이 임베딩 기반 인식이 실무에서 사랑받는 이유입니다.

Siamese라는 이름의 직관

이런 방식의 바탕에는 Siamese(샴) 구조라는 아이디어가 있습니다. 두 입력을 똑같은 하나의 신경망에 각각 통과시켜 임베딩을 뽑고, 그 둘을 비교하는 구조입니다. "쌍둥이 네트워크"라는 이름처럼, 같은 잣대(같은 모델)로 두 얼굴을 재기 때문에 공정한 비교가 됩니다. 우리가 위에서 get_embedding을 두 번 호출해 비교한 것이 바로 Siamese 구조의 실천입니다.

실무 팁. mtcnn(img)None을 돌려줄 때가 있습니다. 사진에서 얼굴을 못 찾은 경우입니다. 실제 코드에서는 항상 None 검사를 넣어, 등록·조회 단계에서 얼굴이 없는 사진이 들어와도 프로그램이 멈추지 않게 해야 합니다. 위 get_embeddingNone을 반환하도록 짠 것도 그 때문입니다.

이 장에서 기억할 것

facenet-pytorch는 MTCNN으로 얼굴을 정렬하고 InceptionResnetV1로 512차원 임베딩을 뽑아, 두 얼굴을 거리로 비교하게 해 줍니다. 새 사람은 임베딩 한 장만 저장하면 등록되는 원샷 인식이 가능하며, 이는 같은 모델로 두 입력을 재는 Siamese 구조 덕분입니다. 얼굴을 못 찾으면 None이 나오므로 검사를 잊지 마세요. 다음 장에서는 여러 사람의 임베딩을 t-SNE로 2차원에 펼쳐, 군집을 직접 눈으로 봅니다.