Navigate
Back to Gym
← Back to Wall

Three of a Kind

Route ID: R030 • Wall: W08 • Released: Feb 19, 2026

5.11a
ready

🎉 Sent!

You made it to the top. Submit your work above!

Submission

Submit your notebook here


Deliverables

Submit your completed notebook (.ipynb) with:

  1. Baseline UMAP and silhouette score (Exercise 1)
  2. Triplet construction code and example triplets (Exercise 2)
  3. Training loss curve (Exercise 3)
  4. Before/after UMAP + silhouette scores for easy negatives (Exercise 4)
  5. Before/after UMAP + silhouette scores for hard negatives (Exercise 5)
  6. AUROC and AU-PRC comparison to your R028 baseline (Exercise 6)
  7. Reflection answers in markdown cells (Exercise 7)

Exercise 7: Reflection

Goal: Consolidate what you learned.

Answer in your notebook (2-3 sentences each):

  1. Did contrastive learning improve AUROC and AU-PRC compared to your R028 baseline? By how much?

  2. In your own words, what problem does contrastive learning solve that standard classification doesn't?

  3. Why do hard negatives make contrastive learning harder? Is that a bad thing?

  4. A colleague says "my classifier gets 90% accuracy on easy negatives, so the model is good." What would you say to them?

  5. You used silhouette score to quantify cluster quality. Can you think of another metric that would measure whether the learned representations are useful? (Hint: think about downstream tasks.)

  6. A recent paper from Leash Biosciences (Hermes, 2024) explicitly flags using continuous enrichment scores instead of binary labels as a "compelling future direction." Based on what you learned here, why might that matter for contrastive learning?


Exercise 6: The Verdict — Did It Help?

Goal: Compare contrastive learning to your baseline.

In R028, you trained a classifier and got AUROC scores for easy and hard negatives. Now let's see if contrastive learning did better.

Important: If you skipped R028, go back and do it first — you need that baseline to know if contrastive learning actually helped!

Use your fine-tuned embeddings for classification:

Take the contrastively-trained embeddings and train a simple classifier on top (logistic regression or a linear layer). Evaluate with AUROC on the same test sets.

Compare:

ApproachEasy AUROCEasy AU-PRCHard AUROCHard AU-PRC
R028 Baseline (frozen embeddings)????????????
R031 Fine-tuned (optional)????????????
R030 Contrastive (this route)????????????

Questions:

  • Did contrastive learning help? By how much (compare AUROC and AU-PRC)?
  • Did it help more on easy or hard negatives?
  • Do AUROC and AU-PRC tell the same story? If they differ, which do you trust more for this imbalanced dataset?
  • Was the extra complexity (and compute time) worth it?

Success check:

  • You have AUROC and AU-PRC numbers for both approaches
  • You can articulate whether contrastive learning was worth it for this dataset

Exercise 5: The Hard Negative Challenge

Goal: See what happens when negatives are structurally similar to positives.

Now repeat Exercises 2-4 using the hard negatives.

Rebuild your triplets using hard negatives, retrain your model from scratch (reload the pre-trained weights!), and re-visualize.

Important: You're starting fresh — don't fine-tune on top of your easy-negatives model. Reload the original pre-trained weights.

Questions:

  • How does the training loss curve compare to easy negatives?
  • Is the silhouette score improvement larger or smaller than with easy negatives?
  • Why are hard negatives... harder? What does this tell you about what the model needs to learn?
  • In a real drug discovery setting, which scenario (easy or hard) is more realistic?

Success check:

  • Before/after UMAP for hard negatives
  • Silhouette scores for hard negatives (before and after)
  • You can explain why hard negatives are a harder learning problem

Exercise 4: Did It Work? (Easy Negatives)

Goal: Visualize AND quantify the new embedding space.

Re-embed the same 500 molecules from Exercise 1 using your fine-tuned model. Run UMAP again and make a new scatter plot.

Put the two plots side by side — before and after fine-tuning.

Now compute the silhouette score again. Compare it to your baseline.

Questions:

  • Is the visual separation better after fine-tuning?
  • Did the silhouette score improve? By how much?
  • A better-looking UMAP is encouraging, but UMAP can lie. Why is silhouette score a more trustworthy metric?

Success check:

  • Side-by-side before/after UMAP plots
  • Before/after silhouette scores recorded
  • You can articulate whether fine-tuning helped and by how much

Exercise 3: Fine-Tune with Triplet Loss

Goal: Train your molecular encoder to produce better representations using triplet loss.

Compute note: Unlike R028's frozen embeddings, this exercise actually updates the transformer weights. You'll need a GPU — free Colab works, but Colab Pro is faster. Training should take 5-15 minutes depending on your setup.

Connect to a GPU in Colab: Go to Runtime → Change runtime type → Hardware accelerator → GPU (T4)

The idea behind triplet loss is simple: penalize the model when an anchor is closer to a negative than to its positive. PyTorch has a built-in loss function for this.

Ask your chatbot:

"What is PyTorch's triplet loss function called? How do I use it with embeddings?"

You'll need to:

  1. Unfreeze your model so its weights can update
  2. Embed each element of your triplets through the model
  3. Compute the loss and backpropagate
  4. Train for a few epochs (3-5 is enough to see movement)

Use a small learning rate (around 1e-5) — you're fine-tuning a pre-trained model, not training from scratch.

Plot your training loss over batches or epochs.

Questions:

  • What does it mean when the loss decreases?
  • What is the "margin" parameter in triplet loss? What happens conceptually if margin=0?
  • Why use such a small learning rate for fine-tuning?

Success check:

  • Training runs without errors
  • Loss decreases over epochs
  • You have a loss curve to include in your submission

Exercise 2: Building Triplets

Goal: Construct the training examples for contrastive learning.

In standard classification (what you did in R028), each training example is (molecule, label). In triplet-based contrastive learning, each training example is (anchor, positive, negative):

  • Anchor: a binder
  • Positive: a different binder
  • Negative: a non-binder

The model will be trained to make the anchor's embedding closer to the positive than to the negative.

Ask your chatbot:

"What are triplets in contrastive learning? How do anchor, positive, and negative relate to each other?"

Good news: The paired dataset already gives you anchors and negatives! You just need to pair each anchor with a different positive:

import random

positives = df['positive'].tolist()

def build_triplets(df, negative_col='easy', n_triplets=5000):
    """Build triplets from paired dataset.

    Args:
        df: DataFrame with 'positive', 'easy', 'hard' columns
        negative_col: which negative column to use ('easy' or 'hard')
        n_triplets: how many triplets to generate
    """
    triplets = []
    for _ in range(n_triplets):
        # Sample two different rows
        idx1, idx2 = random.sample(range(len(df)), 2)

        anchor = df.iloc[idx1]['positive']
        positive = df.iloc[idx2]['positive']  # different binder
        negative = df.iloc[idx1][negative_col]

        triplets.append((anchor, positive, negative))
    return triplets

Print a few example triplets so you can sanity-check them.

Questions:

  • We're sampling positives randomly. Can you think of a smarter strategy — for example, how might you pick the hardest possible positives? Why might that help or hurt?
  • How many unique triplets could you theoretically generate from your dataset? Is 5,000 a lot or a little?
  • Why does the dataset already pair each positive with a specific negative? What advantage does this give over random sampling?

Success check:

  • You have a list of (anchor SMILES, positive SMILES, negative SMILES) triplets
  • You understand what each element represents biologically

Exercise 1: Baseline — See the Embedding Space

Goal: Visualize how your pre-trained molecular encoder represents binders vs non-binders before contrastive training.

You already loaded a molecular transformer in R028. Use the same model here.

Embed a subset — 500 molecules (250 binders, 250 non-binders) is enough for visualization. Use the easy negatives dataset first.

Reduce to 2D with UMAP and make a scatter plot colored by label. Save this plot — you'll need it for comparison later.

Compute a baseline silhouette score on your embeddings (before UMAP). This gives you a number to compare against later.

from sklearn.metrics import silhouette_score

# embeddings: numpy array of shape (n_samples, embedding_dim)
# labels: numpy array of 0s and 1s
score = silhouette_score(embeddings, labels)
print(f"Baseline silhouette score: {score:.3f}")

Feeling lost? If UMAP or sklearn metrics are new to you, consider climbing R017 (Your First Classifier) first. R028 also covers embedding extraction in detail.

Questions:

  • Are binders and non-binders cleanly separated in this space?
  • The model was pre-trained on molecular structure, not on binding. Should we expect it to separate binders from non-binders out of the box?
  • What does it mean if the classes are mixed together in embedding space?
  • What's your baseline silhouette score? (It's probably low — that's expected.)

Success check:

  • You have a UMAP plot of frozen embeddings colored by label
  • You computed and recorded a baseline silhouette score
  • You've saved both for later comparison

Exercise 0: Setup

Goal: Load the MAPK14 dataset and prepare for contrastive learning.

Download the data

Download the dataset here

Save the file as paired_positives.parquet in your working directory.

Load the parquet file

import pandas as pd

df = pd.read_parquet("paired_positives.parquet")
print(df.shape)
print(df.columns)
df.head()

Understand the structure

The dataset contains molecules tested against MAPK14 (a kinase target). Each row is a paired triplet:

  • positive: A molecule that did bind to the target (a "hit")
  • easy: A molecule that didn't bind, and is structurally different from the positive
  • hard: A molecule that didn't bind, but is structurally similar to the positive (shares 2 of 3 building blocks)

Why this structure matters for contrastive learning: This paired format is exactly what you need for building triplets! Each positive already comes with matched negatives. The easy negative is obviously different — any model can tell them apart. The hard negative is the real test: it looks like a binder but isn't. Learning to distinguish hard negatives forces the model to learn subtle, meaningful features.

Create classification datasets (for visualization)

You'll need long-format datasets for UMAP visualization:

# Create easy negatives dataset
df_easy = pd.concat([
    df[['positive']].rename(columns={'positive': 'smiles'}).assign(label=1),
    df[['easy']].rename(columns={'easy': 'smiles'}).assign(label=0)
]).reset_index(drop=True)

# Create hard negatives dataset
df_hard = pd.concat([
    df[['positive']].rename(columns={'positive': 'smiles'}).assign(label=1),
    df[['hard']].rename(columns={'hard': 'smiles'}).assign(label=0)
]).reset_index(drop=True)

print(f"Easy dataset: {len(df_easy)} molecules")
print(f"Hard dataset: {len(df_hard)} molecules")

Load your pre-trained model

Use the same model setup from R028 — load ChemBERTa and your get_embedding() function.

Important: Make sure you're loading the pre-trained weights here, not any fine-tuned weights from R028. We want to start fresh for contrastive learning so we can compare fairly.

Feeling lost? If model loading feels unfamiliar, revisit R028 Exercise 2 for the full setup code.

Success check:

  • Data loaded (paired format + classification format)
  • Pre-trained model loaded
  • get_embedding() function works on a test SMILES

Background: DEL Data and Contrastive Learning

Quick refresher on DEL: DNA-Encoded Libraries let you screen millions of molecules at once against a protein target. The result is enrichment data — signals about which molecules bind. But the signal is noisy, and structurally similar molecules can behave very differently.

In R028, you used standard classification to predict binders. This route tries a different approach: contrastive learning. Instead of learning "is this a binder?", you learn "which molecules are similar to each other?"

Before you start

Ask your chatbot to explain:

  • "What is contrastive learning and how does it differ from classification?"
  • "What is triplet loss and how does the margin parameter work?"
  • "Why might contrastive learning help when you have hard negatives?"

If you skipped R028, go back and do it first — you'll need that baseline to know if contrastive learning actually helps.


Why this route exists

In R028, you trained a classifier the standard way: predict binder vs non-binder, minimize cross-entropy loss. That works, but here's a question: what is the model actually learning?

When a classifier fails, the standard answer is "get more data" or "tune hyperparameters." But sometimes the problem is deeper — the model is learning the wrong thing entirely because the way you framed the problem didn't force it to learn the right thing.

This route is about a different way of framing learning problems: contrastive learning. Instead of asking "is this molecule a binder?", you ask "is this molecule more similar to binders or non-binders?" It sounds subtle, but it changes everything about what the model learns — and you'll see it directly in the geometry of the embedding space.

By the end, you'll know whether contrastive learning actually helps for this dataset — or if the standard approach from R028 was good enough.

What you'll be able to do after this route

By the end, you can:

  • Explain what contrastive learning is and why it's useful
  • Construct triplets (anchor, positive, negative) from labeled molecular data
  • Fine-tune a molecular encoder using triplet loss
  • Visualize AND quantify how representation learning reshapes embedding space
  • Compare contrastive learning to standard classification using AUROC and AU-PRC
  • Articulate why hard negatives make learning harder — and more meaningful

Key definitions

Contrastive Learning A family of methods that train models by comparing examples rather than classifying them. The goal is to learn an embedding space where similar things are close and dissimilar things are far apart.

Triplet Loss A contrastive loss function that takes three examples — an anchor, a positive (same class), and a negative (different class) — and trains the model to make the anchor closer to the positive than to the negative by at least a margin.

Hard Negatives Negative examples that are structurally similar to positives but don't share the positive label. Hard to distinguish, which forces the model to learn subtle, meaningful features.

Silhouette Score A metric from -1 to 1 measuring how well-clustered data is. Higher = tighter clusters, better separation. Use this to quantify embedding quality beyond "it looks better."

AU-PRC (Area Under Precision-Recall Curve) A classification metric better suited for imbalanced datasets than AUROC. Measures how well you're finding rare positives among many negatives.

Dataset credit: Karen Pu pre-processed this dataset from the KinDEL benchmark (Chen et al., 2025).


Route 030: Three of a Kind

  • RouteID: 030
  • Wall: The DEL Wall (W08)
  • Grade: 5.11a
  • Routesetters: Karen + Adrian
  • Time: ~1.5 hours
  • Dataset: MAPK14 from KinDEL benchmark (Chen et al., 2025)
  • Prerequisites: R028 (Your First Molecular Transformer). R031 is optional but useful for comparison.

UNDER CONSTRUCTION

This route is being actively built by Adrian and Karen. The dataset link and some details may change. Check back for updates!

🧗 Base Camp

Start here and climb your way up!