Face Recognition (FaceNet)

Who is in this photo, and do two photos show the same person? MTCNN finds and aligns the face, a frozen InceptionResnetV1 pretrained on VGGFace2 (FaceNet) turns it into a 512-d embedding, and a small MLP head trained here names one of the 42 people of LFW with at least 25 photos. Verification compares two embeddings with the cosine distance.

Model

Detector MTCNN (P-, R-, O-Net), largest face, 160x160 crop with a 10 px margin. No face found -> center square crop
Embedder InceptionResnetV1, VGGFace2 checkpoint 20180402-114759-vggface2, frozen. Output: 512-d, L2-normalised
Head Linear(512, 256) -> ReLU -> Dropout(0.3) -> Linear(256, 42), softmax
Parameters 24,120,596 in the deployed pipeline: MTCNN 495,850 + InceptionResnetV1 23,482,624 (both frozen) + head 142,122 (the only trained part)
Input an RGB photo (PIL image, path, bytes or base64). Photos larger than 1024 px are downscaled first
Output predict: {person: probability} for all 42 people, best first · verify: {same_person, distance, threshold, faces_detected} · embed: 512 floats
Files model.safetensors + config.json (head, class names, crop settings, verify threshold), mtcnn.safetensors, inception_resnet_v1_vggface2.safetensors, model.py, facenet/ (vendored network code), handler.py

The network code in facenet/ is vendored from facenet-pytorch 2.6.0 (MIT, Copyright (c) 2019 Timothy Esler; see facenet/LICENSE.md), because the wheel pins torch<2.3. The layers and math are unchanged; the changes are listed at the top of each file. The original .pt checkpoints were converted to safetensors (training/src/model_builder.py); the 8631-identity logits layer of the VGGFace2 checkpoint was dropped because only the embedding is used. Embeddings from the vendored code match the facenet-pytorch ones (see Training).

Usage

from huggingface_hub import hf_hub_download, snapshot_download
import sys
path = snapshot_download("shalev396/face-recognition")
sys.path.insert(0, path)
import model
predictor = model.load(path, device="cpu")   # or "cuda"
image = hf_hub_download("shalev396/face-recognition", "examples/Vladimir_Putin.jpg", repo_type="space")
print(list(predictor.predict(image).items())[:3])   # [('Vladimir Putin', 0.99...), ...]
print(predictor.verify(image, image))                # {'same_person': True, 'distance': 0.0, ...}
print(len(predictor.embed(image)))                   # 512

Requirements: torch, torchvision, pillow, numpy, safetensors, huggingface_hub.

  • Space / free API: shalev396/face-recognition, POST /gradio_api/call/predict.
  • Inference Endpoint: handler.py makes the repo deployable (Deploy -> Inference Endpoints). Body {"inputs": <base64 image>} -> probabilities; {"inputs": {"image_a": ..., "image_b": ...}} -> verification; "parameters": {"task": "embed"} -> the embedding.

Training

  • Data: LFW (funneled) through sklearn.datasets.fetch_lfw_people(min_faces_per_person=25, color=True, resize=1.0): 2,588 photos (125 x 94 px center slice) of 42 people. Split 75/25, stratified per person, seed 42: 1,941 train / 647 test.
  • Embeddings: every photo goes through model.FaceEmbedder (MTCNN -> InceptionResnetV1), the same code the Predictor runs. MTCNN found no face in 26 photos (1%), which get the center-crop fallback.
  • Head: FaceIdHead, AdamW(lr=1e-3, weight_decay=1e-4), cross-entropy, batch 64, 30 epochs, last epoch kept (no early stopping; the test split is only monitored).
  • This checkpoint: retrained on 2026-09-25 on a local CPU with training/ (3.3 s for both heads on the cached embeddings; computing the embeddings takes about 2.5 min on the same CPU). The embeddings were recomputed with the vendored facenet/ code.
  • Parity with the original project (facenet-pytorch 2.6, RTX 2080 Ti): the recomputed embeddings match the original ones for all 2,588 photos (max absolute difference 3e-7, cosine similarity

    = 0.9999997, the same 26 MTCNN fallbacks), and the retrained MLP head gets exactly the original test numbers (accuracy 0.9985, macro F1 0.9975).

Full code: training/ · Colab.

Experiments

Both heads are trained on the same frozen embeddings and the same split. The original project shipped the SVM in its app; the MLP is better on the test split and is the one deployed here.

experiment (test split, 647 photos) accuracy F1 macro top-3 accuracy
MLP head 512 -> 256 -> 42 (deployed) 0.9985 0.9975 0.9985
linear SVM, C=1, Platt probabilities 0.9954 0.9924 1.0000
original run: MLP head (facenet-pytorch) 0.9985 0.9975 n/a
original run: linear SVM (facenet-pytorch) 0.9969 0.9947 n/a

All rows are in metrics.json (comparison); the "original run" rows come from the original project's results.json. The SVM is 1 photo behind its original number because this code takes the argmax of predict_proba (Platt scaling), while the original used SVC.predict; the two can disagree on borderline photos. With SVC.predict on the recomputed embeddings the SVM reproduces the original numbers exactly (0.9969 / 0.9947, checked separately, not stored in metrics.json). The differences between the heads are 1-3 photos out of 647, so they are small compared with the noise of a test set this size.

Comparison of the heads

MLP training curves

t-SNE of the embeddings

Evaluation

metric (test) value
accuracy 0.9985
f1_macro 0.9975
top3_accuracy 0.9985
verify_roc_auc 0.9995
verify_balanced_accuracy 0.9882
verify_true_accept_rate 0.9812
verify_false_accept_rate 0.0049

Identification: the deployed MLP gets 646 of 647 test photos right. The only mistake is a photo of Recep Tayyip Erdogan predicted as Luiz Inacio Lula da Silva.

Confusion matrix of the MLP head

Verification (verify: same person when the cosine distance is below the threshold), measured on all 208,981 pairs of test faces (13,670 same-person, 195,311 different-people pairs). ROC AUC 0.9995. The threshold is chosen on the training pairs (best balanced accuracy over the grid 0.3 to 1.0 in training/src/config.py), which picks 0.5; it is stored in config.json and can be overridden per call.

threshold (test pairs) same-person pairs accepted different-people pairs accepted balanced accuracy
0.5 (tuned on training pairs, deployed) 98.12% 0.49% 0.9882
0.7 (original project) 99.96% 10.37% 0.9480

The original project's 0.7 accepted 1 in 10 pairs of different people.

Distances of same-person vs different-people pairs

License

  • Code (model.py, handler.py, facenet/, the training code): MIT. facenet/ keeps its original copyright notice (Timothy Esler, 2019).
  • FaceNet weights (mtcnn.safetensors, inception_resnet_v1_vggface2.safetensors): converted from facenet-pytorch, which ported them from davidsandberg/facenet. The embedder was trained on VGGFace2, whose licence allows non-commercial research purposes only. Treat the embedder weights, and anything built on them (including the head in model.safetensors), as research-only.
  • Data: LFW photos of public figures collected from news articles, published for research.

Sensitive use

Face recognition identifies people, so it can be used for surveillance and to track people without their knowledge. This model is a learning project. It is not meant for identifying private people, access control, law enforcement or any decision about a person.

  • Only use it on photos of people who agreed to it (or on the public-figure LFW photos).
  • The Space's Enroll tab keeps embeddings in the visitor's browser session only and writes nothing to disk.
  • Do not store or share embeddings of people without their consent: an embedding is biometric data (for example under the GDPR).

Limitations

  • Closed set of 42 people. Every face gets the closest of the 42 LFW identities; there is no "unknown" class. A low top probability is a hint, not a rejection rule.
  • Easy benchmark. LFW photos are news photos, mostly frontal and well lit, and the same person's photos often come from the same events, so train and test photos can be very similar. Accuracy on other photos (profile views, low light, children, masks) will be lower.
  • Skewed data. George W Bush has 530 photos, while 23 of the 42 people have fewer than 40. LFW is mostly white, male, adult public figures; FaceNet/VGGFace2 errors are known to differ across demographic groups, and this was not measured here.
  • Verification threshold (0.5 cosine distance) was tuned on LFW training pairs. On other photos the distance distributions shift, so a threshold that works here can accept or reject too much there.
  • Fallback crop. When MTCNN finds no face, a center crop is embedded instead, which only makes sense for pre-centered photos like LFW.
Downloads last month
-
Safetensors
Model size
142k params
Tensor type
F32
·
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support

Space using shalev396/face-recognition 1

Evaluation results