This script converts the pre-trained FaceNet PyTorch model from timesler/facenet-pytorch to CoreML format for use in iOS applications.
- Source: FaceNet PyTorch implementation (VGGFace2 trained)
- Repository: https://github.com/timesler/facenet-pytorch
- Model: InceptionResnetV1 trained on VGGFace2 dataset
- Input: 160×160×3 RGB image
- Output: 512-dimensional face embedding vector
- Accuracy: 99.65% on LFW benchmark
- Use Case: Face recognition and verification
# Install Python 3.11 via Homebrew
brew install python@3.11
# Verify installation
python3.11 --version
# Should output: Python 3.11.x# Create virtual environment
python3.11 -m venv facenet-env
# Activate environment
source facenet-env/bin/activate
# Install dependencies
pip install torch torchvision facenet-pytorch coremltoolsThe model can be downloaded from the facenet-pytorch repository:
- Visit: https://github.com/timesler/facenet-pytorch
- Download:
20180402-114759-vggface2.pt
# Activate virtual environment
source facenet-env/bin/activate
# Run conversion script
python convert_facenet_pytorch_to_coreml.py /path/to/20180402-114759-vggface2.pt
# Or specify output path
python convert_facenet_pytorch_to_coreml.py /path/to/model.pt FaceNet.mlpackage
✅ SUCCESS! Model saved to: FaceNet.mlpackage
Model details:
Input: 160×160×3 RGB image (normalized 0-1)
Output: 512-dimensional embedding vector
Size: ~90 MB
Method A: Drag and Drop
- Open your Xcode project
- In Project Navigator, select where to add the model (e.g.,
Resources/MLModels/) - Drag
FaceNet.mlpackagefrom Finder into Xcode - In the dialog:
- ✅ Check "Copy items if needed"
- ✅ Select your app target
- Click "Finish"
Method B: Add Files
- Right-click in Project Navigator
- Choose "Add Files to [ProjectName]..."
- Select
FaceNet.mlpackage - Ensure target is selected
- Click "Add"
- Click on
FaceNet.mlpackagein Project Navigator - Xcode will show the model details:
- Inputs:
input- Image (Color 160×160) - Outputs:
var_2167or similar - MultiArray (Float32 512)
- Inputs:
- Xcode automatically generates Swift wrapper class
import CoreML
import Vision
// Load the model
guard let model = try? VNCoreMLModel(for: FaceNet(configuration: MLModelConfiguration()).model) else {
print("Failed to load FaceNet model")
return
}
// Create Vision request
let request = VNCoreMLRequest(model: model) { request, error in
guard let results = request.results as? [VNCoreMLFeatureValueObservation],
let embedding = results.first?.featureValue.multiArrayValue else {
return
}
// embedding is a 512-dimensional vector (MLMultiArray)
print("Generated embedding with \(embedding.count) dimensions")
}
// Process image
let handler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer, options: [:])
try? handler.perform([request])- Embedding generation: ~50-100ms per face on iPhone
- Memory: ~90MB model size
- Accuracy: 99.65% on LFW benchmark
- Vector size: 512 floats = 2KB per face embedding
Once you have embeddings, compare faces using cosine similarity:
func cosineSimilarity(_ a: [Float], _ b: [Float]) -> Float {
let dotProduct = zip(a, b).map(*).reduce(0, +)
let normA = sqrt(a.map { $0 * $0 }.reduce(0, +))
let normB = sqrt(b.map { $0 * $0 }.reduce(0, +))
return dotProduct / (normA * normB)
}
// Similarity > 0.6-0.7 typically indicates same person
let similarity = cosineSimilarity(embedding1, embedding2)
if similarity > 0.75 {
print("Same person!")
}- FaceNet PyTorch: https://github.com/timesler/facenet-pytorch
- CoreML Tools Documentation: https://coremltools.readme.io/
- Apple ML Documentation: https://developer.apple.com/documentation/coreml
- This script: MIT License
- FaceNet model: Check timesler/facenet-pytorch repository for licensing
- CoreML Tools: Apache 2.0
The conversion script performs the following steps:
model = torch.load(pt_file_path, map_location='cpu')- Loads the
.ptfile which contains model weights (state dict) - Uses
map_location='cpu'to avoid GPU requirements
from facenet_pytorch import InceptionResnetV1
facenet_model = InceptionResnetV1(pretrained=None, classify=False)
facenet_model.load_state_dict(model, strict=False)- The
.ptfile only contains weights, not architecture - Uses
facenet-pytorchlibrary to create the InceptionResnetV1 architecture classify=Falsecreates embedding-only mode (no classification head)strict=Falseignoreslogits.weightandlogits.bias(classification layer weights we don't need)
Why this is necessary:
- FaceNet was trained with a classification head (predicting person IDs)
- For face recognition, we only need the embedding layer (512D vector)
- The classification layer is dataset-specific (VGGFace2 has 9000 identities)
- Embeddings are universal and can match any face
example_input = torch.randn(1, 3, 160, 160)
example_output = model(example_input)
print(f"Output shape: {example_output.shape}") # Should be [1, 512]- Tests model with random 160×160×3 RGB image
- Verifies output is 512-dimensional embedding
- Catches architecture loading errors before conversion
traced_model = torch.jit.trace(model, example_input)- PyTorch JIT tracing converts dynamic model to static computation graph
- Required by CoreML Tools for conversion
- Records all operations performed during the example forward pass
mlmodel = ct.convert(
traced_model,
inputs=[ct.ImageType(
name='input',
shape=example_input.shape,
scale=1/255.0, # Normalize to 0-1
bias=[0, 0, 0]
)],
minimum_deployment_target=ct.target.iOS15
)- Converts PyTorch computation graph to CoreML ML Program format
ct.ImageTypetells CoreML this input is an image (enables optimizations)scale=1/255.0automatically normalizes pixel values from 0-255 to 0-1minimum_deployment_target=iOS15uses modern.mlpackageformat
mlmodel.short_description = "FaceNet face recognition model"
mlmodel.author = "FaceNet PyTorch (converted to CoreML)"
mlmodel.license = "MIT"
mlmodel.version = "vggface2-pytorch"- Adds metadata visible in Xcode
- Helps document model source and version
mlmodel.save('FaceNet.mlpackage')- Saves as
.mlpackagebundle (iOS 15+ format) - Contains:
model.mil- Model code in MIL (Model Intermediate Language)Data/weights/- Model weightsManifest.json- Package metadata
The FaceNet model has two modes:
classify=True: Outputs class probabilities (for training on specific dataset)classify=False: Outputs 512D embedding (for face recognition)
For face recognition, embeddings are more useful because:
- They generalize to unseen people
- Can be compared with cosine similarity
- Work for verification (is this the same person?) and identification (who is this?)
The downloaded model includes weights for:
- Base network (convolutional layers, residual blocks) ✅ We need these
- Classification head (
logits.weight,logits.bias) ❌ We don't need these
Using strict=False allows loading base network weights while ignoring the classifier.
An embedding is a 512-dimensional vector (512 floats) that represents a face's unique characteristics:
- Captures facial geometry, features, and identity
- Two photos of the same person produce similar vectors
- Photos of different people produce different vectors
- Compared using cosine similarity:
similarity = dot(v1, v2) / (norm(v1) * norm(v2))
- Output is normalized L2 embedding vector (ready for cosine similarity)
- Model expects faces to be pre-cropped and aligned to 160×160
- Input images should be RGB (not BGR or grayscale)
- Face detection must be performed separately (use Vision Framework)