Step 1: Text in, numbers out
An embedding model turns a piece of text into a list of numbers, a
vector, arranged so that texts with similar meaning get similar
vectors. Search, recommendations, clustering and retrieval-augmented
generation (RAG) all rest on that one property. This lab makes it
visible on Brightline's catalogue: books.csv holds 200 blurbs, each
filed under a shop section.
The model here is NVIDIA's llama-nemotron-embed-vl-1b-v2, through the
same kind of client as the chat models (client.embeddings.create). Two
details matter:
- It is asymmetric: it embeds a stored document (
input_type: "passage") slightly differently from a search question ("query"), and it refuses requests that do not say which. The OpenAI client has no such argument, so it goes inextra_body, which passes extra fields through to the server. - It returns 2,048 numbers per text by default and can return fewer
(
dimensions=256), trading a little accuracy for a lot less storage.
1. Write embed(texts, input_type="passage", dimensions=None, batch_size=64).
Send the texts in chunks of batch_size (one request per chunk rather
than one per text), with
extra_body={"input_type": input_type} and dimensions only when it is
given. Collect each item's embedding in order (sort response.data by
index) and return np.array(rows, dtype=np.float32): one row per text.
2. Run. It embeds all 200 blurbs in four requests and saves
embeddings.npy, which the later steps reuse.
vectors.py, the file you edit59 lines
"""Embeddings you can see: turn blurbs into vectors, measure closeness, search, map and find the misfits."""
import numpy as np
from openai import OpenAI
CLIENT = OpenAI()
EMBED_MODEL = "nvidia/llama-nemotron-embed-vl-1b-v2"
# ---------- Step 1: text in, vectors out ----------
def embed(texts, input_type="passage", dimensions=None, batch_size=64, client=None):
"""One row per text: a float32 numpy array of shape (len(texts), d).
input_type is "passage" for documents you store and "query" for questions you search with."""
client = client or CLIENT
# TODO (Step 1): for each chunk of batch_size texts:
# kwargs = {"model": EMBED_MODEL, "input": <the chunk as a list>, "extra_body": {"input_type": input_type}}
# add kwargs["dimensions"] = dimensions only when dimensions is given
# response = client.embeddings.create(**kwargs)
# collect item.embedding for each item in response.data (sorted by item.index)
# return np.array(rows, dtype=np.float32)
raise NotImplementedError("Step 1: write embed()")
# ---------- Step 2: how close are two meanings? ----------
def normalise(m):
"""Every row scaled to length 1 (so a dot product is the cosine similarity)."""
raise NotImplementedError("normalise() arrives in Step 2")
def cosine(a, b):
"""Cosine similarity of two vectors: 1 = same direction, 0 = unrelated."""
raise NotImplementedError("cosine() arrives in Step 2")
def nearest(i, m, k=5):
"""Indices of the k rows most similar to row i (not i itself), most similar first."""
raise NotImplementedError("nearest() arrives in Step 2")
# ---------- Step 3: search by meaning ----------
def search(query, passages, k=5, dimensions=None, client=None):
"""Indices of the k passages closest to the query (embedded with input_type="query")."""
raise NotImplementedError("search() arrives in Step 3")
# ---------- Step 4: see it ----------
def cluster(m, n_clusters=6, seed=0):
"""KMeans cluster number for every row (on normalised vectors)."""
raise NotImplementedError("cluster() arrives in Step 4")
def purity(clusters, labels):
"""Share of rows whose label is the most common label in their cluster."""
raise NotImplementedError("purity() arrives in Step 4")
# ---------- Step 5: find what does not belong ----------
def isolation_scores(m, k=5):
"""For each row, the mean similarity to its k nearest neighbours. Low = nothing like it nearby."""
raise NotImplementedError("isolation_scores() arrives in Step 5")books.csvtry_it.py