Notebook: Dynamic Classifier / treehousePairwise.ipynb

The goal: take a new query in a chat stream and decide which of two categories it's closer to, A or B. Also report how certain that decision is, and keep a history of every call.

For a quick test I needed two categories that are clearly different, so I took the first 500 rows of SQuAD and used contexts (long passages) as A and questions (short ones) as B. That gave me 74 unique contexts and 499 questions. I used 40 of each, because each category needs the same number of rows.

python
BASE_MODEL = "all-distilroberta-v1"  # replace with any

ctx_rep = base.encode(train_context, normalize_embeddings=True)   # (40, 768)
que_rep = base.encode(train_question, normalize_embeddings=True)  # (40, 768)

Choosing a similarity score ​

I tried three scores side by side before picking a default.

Method 1: Euclidean. sentence-transformers returns the negative distance, so I checked the diagonal by hand:

python
np.linalg.norm(ctx_rep[0] - que_rep[0])  # 1.0468777  → matrix entry -1.0469 ✅
np.linalg.norm(ctx_rep[1] - que_rep[1])  # 1.1283697  → matrix entry -1.1284 ✅

Method 2: Manhattan. Same shape, much bigger numbers (around −22 to −32), so it's harder to read at a glance.

Method 3: Cosine. For normalised vectors this is the same as the dot product. That makes it the cheapest one, so it became the default.

⚠️ Self note (kept from the notebook): base.similarity always returns tensors, whatever type you pass in. In an app, handle the device properly, "for the sake of Jesus Christ".

The predict step ​

python
def predict(sample_A, sample_B, new_query):
    base.similarity_fn_name = "dot"
    inputs = base.encode([new_query], normalize_embeddings=True)
    A = base.similarity(inputs, sample_A).numpy()
    B = base.similarity(inputs, sample_B).numpy()
    pair_scores = abs(np.array([A.mean(axis=-1), B.mean(axis=-1)])).flatten()
    pair_scores /= pair_scores.sum()
    # omega: how far the query is from its closest match in the winning category
    winner = A if np.argmax(pair_scores) == 0 else B
    diff_omega = np.round(abs(1 - winner).min(), 5)
    return diff_omega, pair_scores
QueryOmegaA (context)B (question)
a training question0.00.4670.533
a training context0.00.5790.421
"what do you mean?" (unseen)0.8850.0460.954

Omega is 0 for anything the classifier has already seen, and it rises for new queries. The unseen short query lands confidently in "question", and its omega of 0.885 says it's a long way from any training example. One line isn't enough to test with, but it's a start.

Treehouse ​

The final class wraps all of that in a SentenceTransformer subclass. You give it lists A and B up front, it embeds them once, and every predict call appends {certainity, scores} to self.history so I can look back at how it behaved.

I wrote this for the dialogue side of Tom: a cheap, fast first pass that tags what kind of thing the user just said before a bigger model decides how to answer.