skip to content
☁️
☁️
☁️
☁️
✦✦✦✦✦✦✦✦
← all technical blogs

📌 pinned · a tour of the build

The Data Science Hiding in My Portfolio

This site isn't just a static page. Most of it is generated, organized, and answered by models, and every piece ships with an eval. Here's the tour, with the concepts explained.
0%
chatbot accuracy
0
ML/LLM features
0.00
mood confidence
0
hallucinations
The content on this site is not only displayed, a lot of it is generated, organized, and answered by models. And because I care whether each piece actually works, almost every one ships with a small evaluation, so quality is measured, not assumed. Here is how each part works, with the concept behind it.

🧭The one idea everything is built on: embeddings

💡 the concept

An embedding turns text (or an image) into a list of numbers, a vector. A good model places similar meanings close together, even with no shared words: "make LLMs faster" and "reduce inference latency" land near each other.

To measure "close" I use cosine similarity: the cosine of the angle between two vectors, from -1 (opposite) through 0 (unrelated) to 1 (identical). It cares about direction, not length, so a short phrase and a long paragraph on the same topic still match.

Once meaning becomes geometry, a lot gets simple: search is "find the nearest vectors," recommendations are "find neighbors," classification is "which label is closest." Almost everything below is a variation on that move.

💬Ask-my-portfolio chatbot (RAG)

💡 the concept

Retrieval-augmented generation fixes the fact that LLMs confidently make things up. Instead of asking the model to recall facts, you retrieve the relevant source text first (by embedding similarity), then ask it to answer using only that text. It becomes a careful summarizer of real sources, not a fuzzy memory.
Grounded in my bio, experience, every project's GitHub README, and my Substack posts (poems and photos are private and excluded). Answers stream token-by-token, cite sources, and refuse when out of scope.It also carries conversation memory: recent turns travel with each question, both into retrieval (the last exchange is embedded alongside the new question) and into the prompt, so a follow-up like "can I see a demo of it?" still knows what "it" is. And every project card has an ask about this button that opens the chat pre-loaded with that project, retrieval and grounding included.
Retrieval hit rate (right chunk in top-k)100%
Answer accuracy (contains the fact)100%
Refusal correctness (declines out-of-scope)100%
The refusals ("favorite poem?", "phone number?", "2024 Super Bowl?") are out of scope on purpose: it declines all three with zero hallucination.

🌌The embeddings galaxy (PCA)

💡 the concept

Embeddings live in hundreds of dimensions, which you can't draw. PCA finds the directions of greatest variation and projects everything onto the top two, giving an x and y to plot while keeping as much real spread as possible. I compute it by hand via the Gram matrix and power iteration, no library.
Similar projects drift near each other, so the model roughly rediscovers my technical areas from text alone. An earlier version ran k-means and let an LLM name the clusters, but at near-zero separation it mislabeled things (a car-price project once landed in a "Computer Vision" cluster). So I color each dot by its real area instead. The layout shows the structure; the colors stay honest.The dots are interactive too: hover or tap one and a little card pops up with the project's area, domains, and actions, open the code, ask the chatbot about it, or find similar, which runs nearest-neighbors over the same cached vectors and filters the project grid above.You can also drop yourself into the map. Type an interest and it's embedded and projected onto the same two PCA axes as the projects, a proper out-of-sample projection (project the query onto the axes I already found, don't re-fit them), so your "you are here" star lands where it genuinely belongs. A line points to the single nearest project by cosine, and one click asks the chatbot about it. Semantic search, turned into a place on a map instead of a ranked list.

🏷️Auto-pulled blog with embedding zero-shot tagging

💡 the concept

Zero-shot classification labels things with no training examples: describe each label in words, embed the descriptions and the post, and the closest label wins. Two refinements make it robust. Multi-prototype labels: average a few phrasings per category to cancel noise. And a confidence rule instead of blind argmax: a domain is attached only when it clears a floor and clearly beats the runner-up. When two are close, that's ambiguity, so it tags none. Precision over recall.
This replaced a brittle keyword system that once tagged an encryption post as "Food & Nutrition" because the intro mentioned my cat's empty food bowl. I weight the title 2x in the embedded text, and this very post tagged itself.The tags then earn their keep: the Technical Blogs page builds its topic filter pills from whatever tags exist across posts, so publishing a post on a new area grows a new filter on its own, no code change.The Work grid has a cheaper cousin for terse GitHub repo descriptions: a deterministic keyword classifier, now multi-label, so a project that is genuinely both IoT and Computer Vision wears both tags instead of whichever rule fired first. Matching the method to the input: embeddings for prose, rules for metadata.That same repo classifier also gives every auto-pulled project a distinct on-theme emoji: it reads the project's own words for a fitting icon (a gateway gets a traffic light, a benchmark a test tube, an encryption tool a lock), and when two projects would pick the same one it slides to a related icon, so the grid never becomes a wall of identical sparkles.

📸Photo clustering (k-means + silhouette + CLIP)

💡 the concept

k-means groups without labels: place k centers, assign each point to its nearest, move centers to the mean, repeat. The silhouette score (-1 to 1) picks the best k by comparing in-cluster tightness to the nearest other cluster. CLIP puts images and captions in a shared space, so a CLIP image embedding captures visual content directly.
silhouette with caption-text embeddings0.076
silhouette with CLIP image embeddings0.143
Switching from caption text to CLIP image embeddings roughly doubled the silhouette and produced more meaningful groups. The pixels knew something the captions didn't. The model is too heavy for the site to run on itself, so a scheduled job does the grouping: each hour it asks whether the photos have changed, and only then regroups all of them together, however long ago each one arrived. A group that is mostly the same photos keeps its name.

✨The smaller LLM touches

💡 the concept

A few features use an LLM as a function, not a chatbot: a strict instruction, temperature 0 (minimum randomness, repeatable output), and structured JSON. Constrained like that, it becomes a reliable little classifier or rewriter.
  • Poem mood classification into eight moods with a confidence score; average 0.894 across 8 poems, reported on the page.
  • AI poem art: a model distills each poem into one evocative prompt and an image model renders it. Cached so it is paid for once, but I can regenerate a fresh take, keep versions I like, or upload my own.
  • Auto-captioned photos via a vision model, low-detail to stay cheap.
  • ELI5 / expert toggle rewrites every blurb for a 10-year-old or a senior engineer, under a strict "keep every fact truthful" rule, and to the same card-filling shape as everything else.
  • Guestbook moods: when someone signs the guestbook, the same temperature-0 JSON trick sorts their note into one of six moods (sweet, excited, curious, funny, kind, proud) and pins a matching little emoji to it.

📐Writing to fit a card

💡 the concept

Cards in a row are all as tall as the wordiest one, so a short description leaves a visible hole above the buttons. The obvious fix is to ask a model for text of a given length, and the obvious fix does not work: a model cannot count characters. Told "150 to 185 characters" it landed 7 of 79 inside the band and condensed everything to a median of 116. Asking for a shape instead, exactly two sentences of 16 to 20 words each, gets the length as a side effect.
Spread fell from 149 characters to 70, and the median gap above the buttons from 74px to 32px. The expansion is grounded in the project's own README, or for the About cards in the entry's own details, and it is told to leave a description short rather than invent something to fill with. About half the repos are small experiments whose readmes genuinely cannot support two sentences, and those stay short on purpose.A prompt is a request, not a guarantee. Two rules kept reaching the page anyway: opening by restating the project's own name, which the heading directly above already says, and consultant filler ("leveraging", "seamless", "empowering"). Both are checked in code afterwards, the offenders are re-asked with the rule they broke quoted back at them, and a retry is only accepted if it actually fixed the violation. Both went from present to zero.Nobody screenshots ninety-eight repos, but a readme usually describes a pipeline and a pipeline draws well. The stages are read out of each readme once and rendered as inline SVG in the same pastels the chips wear, so the diagram is in the HTML with no client JavaScript and no chart library. A readme with no pipeline in it caches the empty answer and shows nothing, because an absent picture beats an invented one.The same material supports a deeper read: every project in recruiter mode opens into a case study with the problem, how it works, what it found, and up to four headline numbers, only numbers the readme actually states, never estimated or rounded. Anything hand-written wins, and the two live in separate stores so a draft can never overwrite one.All of it is cached per item against a hash of the source it was generated from, not per set. Keying a whole set under one hash meant publishing a single repo changed the signature and threw away all 98 rewrites to gain one card. Now a new project costs one project: cold 5.9s, warm 0.295s and byte identical, and one new repo rebuilds exactly one entry.

🔎Answering a job posting

💡 the concept

The recruiter page can take a job posting and answer it. Ranking projects against it is the easy half: a posting is just a very long search query over the same cached project vectors. The hard half is that a long posting embeds badly. It averages out to a blur of everything it mentions, so the scores bunch together (0.42 to 0.50) and stop discriminating: her flagship clinical RAG ranked below a loan-status exercise for a clinical AI job. The model reduces the posting to its technical line first, and the search runs on that, which is both better and about twice as fast.
Asked for "the skills this posting asks for that the resume evidences", it returned Swift, SwiftUI, Core Data and UIKit for an iOS posting. Those are the posting's requirements, not mine. On a portfolio that is not a rough edge, it is a claim made to a recruiter in my name, so the claim is checked rather than requested: a skill survives only if the words carrying its meaning appear in my own material. Word boundaries matter on both sides, because a leading boundary alone let "Swift" through on the strength of "Hey Swiftie: Emotion Verse". The iOS posting now claims nothing and names three honest gaps.The other half is re-angling. My human-rights LLM research is the closest thing I have to a healthcare LLM role, and reads as irrelevant because the subject differs: same pipelines, same evaluation problem, different domain. So each entry's note is rewritten to lead with whatever answers the role, out of what the entry already says. It re-angles, it never relabels: the honest version is "a two-stage retrieval-augmented LLM framework, scoring defense manufacturers on human-rights due diligence", never "healthcare research", and an entry with no bearing on the role comes back unchanged rather than stretched.The résumé on that page answers the role too, and under a stricter rule than the cards: nothing in it is rewritten. A model is handed the résumé as numbered entries with numbered bullets and answers with indices, so the only thing it can do is choose. The entries a role calls for rise to the top of their section and their strongest lines come first; the one section whose content changes is Projects, which takes that role's own lead projects, in the words already written for them. It is parsed from the same LaTeX the PDF is compiled from, so the page and the download cannot disagree.Some people file a résumé rather than read it on the spot, so there is one box that emails it. It asks for an address and nothing else: which version to send is already answered by the role that is lit and whatever is in the posting bar, and one line under the box says which is going. An open form that sends mail is an invitation, so it is capped per visitor, per address and per day, and a hidden field and a minimum time on the form turn away the scripts that fill forms faster than a person can.

🛠️The engineering around it

  • Caching + ISR. Embeddings, art, captions, and rewrites are computed once; GitHub and Substack refresh hourly, so the site stays fresh without re-paying every request.
  • Batched calls. Labels and classifications go out in one batched call, not one-per-item.
  • Graceful fallback. The live embedding classifier has a deterministic keyword backup, so a flaky API degrades quality instead of breaking the page.
  • Evals as a habit. If I can't measure it, I try not to claim it.
  • Cards that carry their work. Beyond code and demo, a project card can link straight to a live results dashboard and a Substack write-up, its tags can be anything I want (reinforcement learning, robotics), and any PDF attachment renders its first page right in the card instead of a grey box.
  • It keeps its own paperwork. The readable resume page is rendered straight from the LaTeX the PDF is compiled from, so the two can never drift. Saving a new version also commits that PDF to my GitHub profile, and a nightly job adds any repo I publish to the profile README on its own, classifying it into the right section. That one only ever appends, so the descriptions I wrote by hand are never overwritten.
  • Editable from inside itself. Almost nothing here lives only in code. The passages, project blurbs, tags and links, the About and education cards, the photos and their captions, the guestbook, even the LaTeX résumé are all edited from a little key-gated atelier on the site. Edits are stored as overrides that win at render time, so a change is live the moment I save it, with no rebuild and no deploy.
  • It checks itself on a schedule. A browser walks the whole public site every week, clicking through semantic search, the galaxy, the chatbot and the poems gate, so a broken deploy fails loudly instead of sitting there; the same pass records the demo reel, which is why the reel can never show something the site no longer does. The chatbot's accuracy is re-measured weekly too, because the numbers quoted on this page would otherwise quietly stop being true as new work is published. And everything editable is backed up encrypted every week, since it lives in one place and the poems can't be rewritten.
  • The editing rooms are locked twice. A key alone opens nothing: the right key emails a one-time code, kept only as a hash, good for ten minutes and one use, and the code opens a signed session for a day. Every route that spends a model call has a limit per visitor and across the site, because on a free plan a script leaning on the chatbot is not a bill, it is the whole site going dark. Everything editable is also backed up weekly, encrypted, since none of it lives in git.
  • Pictures are put right in the browser. An iPhone photo arrives as HEIC, which only Safari can display, and a photo off a camera is too heavy for one request. Both are fixed before anything is sent: converted to JPEG and scaled to a size nothing on the site exceeds. Framing is saved, not cropped: dragging a photo to recenter it and sliding to zoom stores three numbers beside the picture, so it can be reframed any time without uploading it again. The portrait and the tab icon are replaced the same way, from the page itself.
That is the whole machine: it embeds, retrieves, clusters, classifies, and grades its own homework. Thanks for reading the tour. 🌸