# How the Weighted Attribute Loss Function Works in PFLD: A Deep Dive into the Code

> Understand PFLD's weighted attribute loss function. Learn how rare facial attributes and pose errors get higher importance in landmark detection training for better accuracy.

- Repository: [Guoqiang QI/pfld](https://github.com/guoqiangqi/pfld)
- Tags: deep-dive
- Published: 2026-03-03

---

**The weighted attribute loss function in PFLD assigns per-sample importance scores based on facial attribute rarity and pose estimation errors, multiplying the landmark MSE loss by inverse-frequency attribute weights and Euler-angle penalties to prioritize difficult and rare training samples.**

PFLD (Progressive Face Landmark Detection) uses a sophisticated training objective that goes beyond standard MSE. According to the `guoqiangqi/pfld` source code, the implementation in [`train_model.py`](https://github.com/guoqiangqi/pfld/blob/main/train_model.py) combines **facial attribute weighting** with **pose-aware penalties** to handle imbalanced datasets where rare attributes (extreme poses, heavy occlusion) would otherwise be ignored. This article breaks down the exact TensorFlow implementation.

## Overview of the Weighted Loss Architecture

The final training loss for a mini-batch combines three multiplicative factors before L2 regularization:

```

loss = mean( landmark_MSE × angle_weight × attribute_weight ) + L2_regularisation

```

- **Landmark MSE**: Per-sample mean squared error between predicted and ground-truth facial landmarks.
- **Attribute Weight**: Inverse-frequency scaling based on five binary facial attributes (pose, expression, illumination, make-up, occlusion).
- **Angle Weight**: Penalty derived from the discrepancy between predicted and ground-truth Euler angles.

This design ensures the optimizer focuses on samples that are both rare in the dataset and geometrically challenging to predict.

## Step-by-Step Implementation in train_model.py

### Step 1: Parse the Attribute Batch

The input pipeline provides a six-column integer tensor where the first column is an image ID and the remaining five columns contain binary attribute flags.

```python
attribute_batch = tf.placeholder(tf.int32, shape=(None, 6), name='attribute_batch')
attributes_w_n = tf.to_float(attribute_batch[:, 1:6])

```

The slice `[:, 1:6]` discards the ID column, keeping only the usable binary attributes. Source: [`train_model.py`](https://github.com/guoqiangqi/pfld/blob/main/train_model.py#L72-L73), L89.

### Step 2: Calculate Inverse-Frequency Weights

The code computes how frequently each attribute appears in the current batch, then inverts these frequencies so that rare attributes receive larger weights.

```python
mat_ratio = tf.reduce_mean(attributes_w_n, axis=0)  # mean of each attribute in batch

mat_ratio = tf.map_fn(
    lambda x: tf.cond(x > 0,
                      lambda: 1 / x,
                      lambda: float(args.batch_size)),
    mat_ratio)

```

- `tf.reduce_mean` calculates the batch-wise prevalence of each attribute.
- `tf.map_fn` applies an element-wise inverse (`1 / frequency`), with a fallback to `batch_size` when frequency is zero to avoid division-by-zero errors.

Source: [`train_model.py`](https://github.com/guoqiangqi/pfld/blob/main/train_model.py#L91-L93).

### Step 3: Compute Per-Sample Attribute Weights

The per-attribute scaling factors are broadcast across the batch and summed per sample to produce a scalar weight for each image.

```python
attributes_w_n = tf.convert_to_tensor(attributes_w_n * mat_ratio)
attributes_w_n = tf.reduce_sum(attributes_w_n, axis=1)

```

After broadcasting `mat_ratio` across the `[batch, 5]` attribute tensor, the element-wise multiplication scales each binary flag by its rarity. The `reduce_sum` collapses the five attributes into a single scalar weight per sample with shape `[batch]`. Source: [`train_model.py`](https://github.com/guoqiangqi/pfld/blob/main/train_model.py#L94-L95).

### Step 4: Integrate Euler-Angle Penalties

PFLD penalizes samples where the predicted head pose diverges significantly from the ground truth using a trigonometric penalty.

```python
_sum_k = tf.reduce_sum(
            tf.map_fn(lambda x: 1 - tf.cos(tf.abs(x)),
                      euler_angles_gt_batch - euler_angles_pre),
            axis=1)

```

The term `1 - tf.cos(tf.abs(diff))` grows non-linearly with angular error, acting as an additional per-sample scaling factor that increases loss for poorly-aligned pose predictions. Source: [`train_model.py`](https://github.com/guoqiangqi/pfld/blob/main/train_model.py#L98).

### Step 5: Assemble the Final Training Loss

The components are combined into the final differentiable loss objective.

```python
loss_sum = tf.reduce_sum(tf.square(landmark_batch - landmarks_pre), axis=1)
loss_sum = tf.reduce_mean(loss_sum * _sum_k * attributes_w_n)
loss_sum += L2_loss

```

- `loss_sum` starts as per-sample landmark MSE.
- Multiplication by `_sum_k` and `attributes_w_n` applies the pose and attribute weighting.
- L2 regularization is added separately.

Source: [`train_model.py`](https://github.com/guoqiangqi/pfld/blob/main/train_model.py#L99-L101). The `train_model` utility function then consumes this weighted loss to create the optimizer. Source: [`train_model.py`](https://github.com/guoqiangqi/pfld/blob/main/train_model.py#L103).

## Complete Implementation Reference

Here is the consolidated logic for computing attribute weights outside the training loop, useful for debugging or custom data pipelines:

```python
import tensorflow as tf

def compute_attribute_weight(attribute_batch, batch_size):
    """
    Compute per-sample attribute weights using inverse-frequency weighting.
    
    Args:
        attribute_batch: Tensor of shape [None, 6] (ID + 5 attributes)
        batch_size: Scalar tensor or int for zero-frequency fallback
    
    Returns:
        Tensor of shape [None] containing scalar weight per sample
    """
    # Extract 5 binary attributes, dropping the ID column

    attrs = tf.to_float(attribute_batch[:, 1:6])
    
    # Calculate inverse frequency for each attribute in the batch

    mean_per_attr = tf.reduce_mean(attrs, axis=0)
    inv_ratio = tf.map_fn(
        lambda x: tf.cond(
            x > 0,
            lambda: 1.0 / x,
            lambda: tf.cast(batch_size, tf.float32)
        ),
        mean_per_attr
    )
    
    # Apply weights and sum per sample

    weighted_attrs = attrs * inv_ratio
    sample_weights = tf.reduce_sum(weighted_attrs, axis=1)
    
    return sample_weights

```

## Key Implementation Details

- **Zero-Frequency Handling**: When an attribute does not appear in a batch (`x == 0`), the weight defaults to `batch_size` rather than infinity, preventing training instability.
- **Inverse Frequency Rationale**: By scaling with `1 / frequency`, the loss function up-weights samples containing rare attributes like extreme poses or heavy occlusion, preventing the model from overfitting to common frontal faces.
- **Pose-Attribute Synergy**: The Euler-angle penalty (`_sum_k`) and attribute weights multiply together, creating a compounding effect where samples that are both rare *and* geometrically challenging receive exponentially higher loss contributions.

## Summary

- The weighted attribute loss function resides in [`train_model.py`](https://github.com/guoqiangqi/pfld/blob/main/train_model.py#L72-L101) of the `guoqiangqi/pfld` repository.
- It extracts five binary facial attributes per sample and scales them by inverse batch frequency to prioritize rare cases.
- An Euler-angle penalty (`1 - cos(|error|)`) further weights samples based on pose prediction difficulty.
- The final loss is the mean of `(landmark_MSE × angle_weight × attribute_weight)` plus L2 regularization.
- This mechanism prevents class imbalance from biasing the model toward common frontal, well-lit faces.

## Frequently Asked Questions

### What are the five facial attributes used in PFLD's loss function?

The five binary attributes are **pose**, **expression**, **illumination**, **make-up**, and **occlusion**. These are stored in columns 1-5 of the `attribute_batch` tensor (column 0 is the image ID). Source: [`train_model.py`](https://github.com/guoqiangqi/pfld/blob/main/train_model.py#L72-L89) and [`generate_data.py`](https://github.com/guoqiangqi/pfld/blob/main/generate_data.py#L47-L55).

### Why does PFLD use inverse frequency weighting for attributes?

Inverse frequency weighting (`1 / batch_frequency`) ensures that **rare attributes receive larger loss weights**. Without this mechanism, the model would minimize loss by predicting landmarks well only for common frontal, neutral-expression faces while ignoring challenging edge cases like profile views or heavy occlusion.

### How does the Euler-angle penalty interact with attribute weights?

The Euler-angle penalty (`_sum_k`) and attribute weights (`attributes_w_n`) multiply together in the final loss calculation. This creates a **multiplicative interaction** where samples exhibiting both rare attributes *and* large pose prediction errors receive disproportionately high loss values, forcing the network to correct both geometric and attribute-specific errors simultaneously.

### Where is the weighted attribute loss implemented in the codebase?

The core logic is implemented in **[`train_model.py`](https://github.com/guoqiangqi/pfld/blob/main/train_model.py)** between lines 72 and 101, specifically within the loss construction block that defines `attribute_batch`, `mat_ratio`, `attributes_w_n`, and `loss_sum`. The helper function that creates the optimizer using this loss is located in [`utils.py`](https://github.com/guoqiangqi/pfld/blob/main/utils.py).