Hard Negative Mining in a Contrastive Batch
Problem Statement
Not all negatives are equal: the ones that hurt are the off-diagonal texts scoring close to or above the true match. For each image, find its hardest negative and flag the images whose hardest negative beats the positive by more than a margin — the ones the model is actively getting wrong.
Background
Given the N x N similarity matrix sim (row = image, col = text), for image i:
- the positive score is sim[i][i];
- the hardest negative is max_{j != i} sim[i][j], with ties broken by the smallest column index;
- image i is a violation when hardest_negative - positive > margin.
Your Task
Implement:
def hard_negatives(sim, margin):
Return a dict:
- "hardest_idx": list where entry i is the column index of image i's hardest negative.
- "violations": sorted list of image indices that violate the margin.
- "mean_gap": mean of (positive - hardest_negative) over all images, rounded to 4 decimals.
For N == 1 there are no negatives: hardest_idx = [-1], violations = [], mean_gap = 0.0.
Input Format
- sim: N x N nested list.
- margin (float).
Output Format
- A dict with the three keys above.
Sample
print(hard_negatives([[0.9, 0.8], [0.1, 0.7]], 0.0))
Output:
{'hardest_idx': [1, 0], 'violations': [], 'mean_gap': 0.35}
Example
print(hard_negatives([[0.9, 0.8], [0.1, 0.7]], 0.0))
{'hardest_idx': [1, 0], 'violations': [], 'mean_gap': 0.35}Image 0: pos 0.9, hardest neg 0.8 (col1), gap 0.1. Image 1: pos 0.7, hardest neg 0.1 (col0), gap 0.6. Neither violates margin 0; mean gap (0.1+0.6)/2 = 0.35.
Constraints
1 <= N <= 1000.- Hardest negative excludes the diagonal; ties go to the smallest column index.
- A violation is
hardest_negative - positive > margin(strict). N == 1is the documented special case.