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.pymakes 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 vendoredfacenet/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.
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.
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.
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 inmodel.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
- -
Space using shalev396/face-recognition 1
Evaluation results
- accuracy on LFW (people with >= 25 photos)test set self-reported0.998
- f1_macro on LFW (people with >= 25 photos)test set self-reported0.997
- top3_accuracy on LFW (people with >= 25 photos)test set self-reported0.998
- verify_roc_auc on LFW (people with >= 25 photos)test set self-reported0.999
- verify_balanced_accuracy on LFW (people with >= 25 photos)test set self-reported0.988
- verify_true_accept_rate on LFW (people with >= 25 photos)test set self-reported0.981
- verify_false_accept_rate on LFW (people with >= 25 photos)test set self-reported0.005




