← 01 — Projects

ArtRECONon-Relational Recommendation via Contrastive Optimization

ArtRECO demo feed showing a grid of recommended artworks from the Art Institute of Chicago.
01

Part I — Embedding Model

Each artwork and caption is embedded with CLIP. Pretrained CLIP was trained on web image–caption pairs rather than curated museum art, so there’s a domain shift. Full fine-tuning of ViT-B/16 risks overfitting on roughly 58k examples; LoRA trains under 1% of parameters, regularizes implicitly, and fits on a consumer GPU.

For a frozen projection WRd×kW \in \mathbb{R}^{d \times k}, LoRA learns a low-rank update

W=W+αrBA,ARr×k,BRd×r,rmin(d,k).W' = W + \frac{\alpha}{r} B A, \qquad A \in \mathbb{R}^{r \times k}, \qquad B \in \mathbb{R}^{d \times r}, \qquad r \ll \min(d, k).

AA is Gaussian-initialized and BB is zero, so training starts exactly at the pretrained model and gradually learns a domain-specific perturbation.

Config: r=8,α=16,pdropout=0.05r = 8, \, \alpha = 16, \, p_{\mathrm{dropout}} = 0.05. Adapters target q_proj\texttt{q\_proj}, k_proj\texttt{k\_proj}, v_proj\texttt{v\_proj}, and out_proj\texttt{out\_proj}in both the vision and text towers — ~600k trainable parameters out of ~150M (under 0.4%).

02

Contrastive Training Objective

Symmetric CLIP contrastive loss. For a batch of normalized image embeddings xix_i and text embeddings tjt_j, let sij=τ1xitjs_{ij} = \tau^{-1} x_i^\top t_j. The image→text and text→image losses are

LIT=1Bilogexp(sii)jexp(sij),LTI=1Bilogexp(sii)jexp(sji).\mathcal{L}_{I \to T} = -\frac{1}{B} \sum_{i} \log \frac{\exp(s_{ii})}{\sum_{j} \exp(s_{ij})}, \qquad \mathcal{L}_{T \to I} = -\frac{1}{B} \sum_{i} \log \frac{\exp(s_{ii})}{\sum_{j} \exp(s_{ji})}.

The final objective is L=12(LIT+LTI)\mathcal{L} = \tfrac{1}{2}(\mathcal{L}_{I \to T} + \mathcal{L}_{T \to I}). Temperature is learned via logit_scale=log(1/τ)\texttt{logit\_scale} = \log(1/\tau), clamped at log(100)\log(100) to prevent τ\tau collapse. Base CLIP weights stay frozen otherwise.

03

Optimization

  • AdamW, lr 10410^{-4}, weight decay 0.01.
  • Cosine schedule, 500 warmup steps.
  • Batch size 64, 5 epochs.
  • Mixed-precision fp16 with gradient scaling.
  • Held-out 5% split, recall@5 reported each epoch.
04

Embedding Results

Recall@5 before and after LoRA tuning:

ModelImage → TextText → ImageAverage
Baseline CLIP0.16590.14180.1539
LoRA-tuned CLIP0.41520.40190.4086

Table 1 — Recall@5 before and after LoRA tuning.

A side-by-side top-5 retrieval — baseline vs. LoRA on the same query — is the clearest qualitative view of the gain.

Limitations: captions are synthesized from metadata, the dataset is modest for contrastive learning, and training uses in-batch negatives rather than hard-negative mining.

05

Part II — Recommendation Algorithm

The recommendation layer uses the CLIP–LoRA embedding space to update preference state from clicks and split future feed slots between learned taste components and random exploration.

06

State

The user state is a set of weighted components S={(si,vi,ki)}\mathcal{S} = \{(s_i, v_i, k_i)\} with isi=1\sum_i s_i = 1. Each component is either a taste component with a unit-norm vector viv_i, or the single random component, which has no vector and represents exploration mass.

07

Click Update

Let cc be the unit-norm embedding of the clicked artwork. Find the closest existing taste vector by cosine similarity,σ=maxivi,c\sigma^* = \max_i \langle v_i, c \rangle. If στ\sigma^* \ge \tau, merge the click into that component with mixing rate δ\delta:

vinorm((1δ)vi+δc),si(1δ)si+δ.v_{i^*} \leftarrow \mathrm{norm}\bigl((1-\delta) v_{i^*} + \delta c\bigr), \qquad s_{i^*} \leftarrow (1-\delta) s_{i^*} + \delta.

Otherwise spawn a new taste component (δ,c,taste)(\delta, c, \mathrm{taste}). All other weights are scaled by 1δ1 - \delta, which keeps the total mass at one.

08

Pruning

After the update, drop any component with weight below ε\varepsilon and renormalize the survivors so the weights still sum to one.

09

Feed Allocation

Let RR be the total weight on the random component. For a feed of length NN, give Np=round(N(1R))N_p = \mathrm{round}(N(1 - R)) slots to personalized items and the remaining Nr=NNpN_r = N - N_p to random exploration. Personalized slots are divided across taste components in proportion to their relative weight:

nisi1RNp.n_i \approx \frac{s_i}{1 - R} N_p.

The personalized and random items are then interleaved into the final feed.

10

Decay of Random Exploration

Clicks only merge with taste components, so the random weight decays geometrically: Rn=(1δ)nR_n = (1 - \delta)^n. It drops below ε\varepsilon once n>logε/log(1δ)n > \log\varepsilon / \log(1 - \delta). For δ=0.2\delta = 0.2 and ε=1/60\varepsilon = 1/60, that’s about 19 clicks — after which the feed is fully personalized.

11

Conclusion

The update rule removes the cold-start setup stage: the system starts with random exploration and learns taste directly from clicks, so every interaction shapes the state immediately.

Spawning new taste components when a click is far from existing ones lets the model carry multiple interests instead of collapsing into one profile. The random component keeps early recommendations exploratory, then decays as taste signal accumulates.