[](https://huggingface.co/openai/clip-vit-base-patch32)
[](https://huggingface.co/datasets/Artificio/WikiArt)
[](https://gradio.app)
[](https://pytorch.org)
[](LICENSE)
[](https://huggingface.co/spaces/Uris001/wikiart-art-recommender)
> **Upload a painting Β· type a description Β· or both**
> Discover the 3 most visually similar artworks from **8,000 fine-art masterpieces**
> in under **10 milliseconds** β powered by CLIP and cosine similarity
---
## π¬ Presentation
> 5-minute walkthrough β dataset β EDA β embeddings β clustering β live demo
---
## π Table of Contents
| # | Section |
|:---:|:---|
| 1 | [πΊ Full Pipeline](#-full-pipeline) |
| 2 | [π¦ Dataset](#-dataset) |
| 3 | [π§Ή Data Cleaning](#-data-cleaning) |
| 4 | [π Exploratory Data Analysis](#-exploratory-data-analysis) |
| 5 | [π§ CLIP Embeddings](#-clip-embeddings) |
| 6 | [π΅ Clustering](#-clustering) |
| 7 | [π Recommendation System](#-recommendation-system) |
| 8 | [βοΈ Ethical Bias Analysis](#-ethical-bias-analysis) |
| 9 | [π How to Use](#-how-to-use-the-app) |
| 10 | [π Repository Structure](#-repository-structure) |
| 11 | [π Tech Stack](#-tech-stack) |
| 12 | [π€ Author](#-author) |
---
## πΊ Full Pipeline
```mermaid
flowchart TD
A["π Artificio/WikiArt on HuggingFace\n103,250 artworks Β· 116 styles Β· 42 genres Β· 1,104 artists"] --> B
B["π₯ Data Loading\nload_dataset Β· train split 8,000 samples\nHF token authentication"] --> C
C["π§Ή Data Cleaning\nDrop embeddings_pca512 Β· unknown source model\nDrop description Β· filename Β· image PIL objects\nDrop redundant *_name aliases\nParse year via regex from messy date strings\nFinal null check β 6 clean columns"] --> D
D["π Exploratory Data Analysis\nStyle Β· Genre Β· Artist distributions\nTemporal 700-year span Β· Date parsing\nStyle Γ Genre cross-bubble-chart\nSample image grid per style\nEthical bias audit"] --> E
E["π§ CLIP Embedding Generation\nopenai/clip-vit-base-patch32\nBatch 32 Β· GPU/CPU Β· L2 normalised\n512D vectors Β· vision_model + visual_projection"] --> F
F["π Dimensionality Reduction\nPCA 512D β 50D noise removal\nUMAP 50D β 2D global structure\nt-SNE 50D β 2D local cluster structure"] --> G
G["π΅ Clustering\nKMeans k=20 Β· Silhouette=0.0703\nDBSCAN interactive ipywidgets tuner\nCluster purity table + interpretation\nKMeans vs ground-truth genre comparison"] --> H
H["πΎ Embedding Index\nwikiart_embeddings_with_images.parquet\nembedding[512] + JPEG thumbnails + metadata"] --> I
I["π Recommendation Engine\nCosine similarity Β· <10ms latency\nImage Β· Text Β· Combined averaged query"] --> J
J["π HuggingFace Space\nGradio 6 Β· CPU Basic Β· Free tier Β· Dark UI\nFully self-contained Β· No runtime data fetch"]
style A fill:#e0e7ff,stroke:#4338ca,color:#1e1b4b
style C fill:#fef3c7,stroke:#d97706,color:#451a03
style D fill:#dcfce7,stroke:#16a34a,color:#052e16
style E fill:#e0f2fe,stroke:#0284c7,color:#0c4a6e
style G fill:#fce7f3,stroke:#db2777,color:#500724
style H fill:#f3e8ff,stroke:#9333ea,color:#2e1065
style J fill:#e0f2fe,stroke:#0369a1,color:#0c4a6e
```
---
## π¦ Dataset
**Source:** [`Artificio/WikiArt`](https://huggingface.co/datasets/Artificio/WikiArt) β a structured, cleaned version of [WikiArt.org](https://www.wikiart.org/) hosted on HuggingFace, covering 500+ years of art history.
Property
Full Dataset
Our Working Subset
Total artworks
103,250
8,000
Unique styles
116
116 (all represented)
Unique genres
42
42 (all represented)
Unique artists
~3,500
1,104
Year range
1300β2010
1300β2010
Median year
~1904
~1904
Raw size on disk
~1.7 GB
~25 MB(embeddings + thumbnails)
> **Why 8,000 images?**
> Exceeds the 5,000-image assignment minimum Β· covers all 116 styles and 42 genres Β·
> embeds in ~4 min on T4 GPU Β· fits comfortably in CPU Basic Space RAM at inference time.
**Raw schema β before any cleaning:**
| Column | Type | Status | Reason |
|:---|:---|:---:|:---|
| `title` | string | β Keep | Artwork title |
| `artist` | string | β Keep | Artist name |
| `date` | string | β Keep | Raw messy date β parsed to `year` |
| `genre` | string | β Keep | Subject matter |
| `style` | string | β Keep | Art movement |
| `image` | PIL Image | π Drop | Kept in HF dataset object β pandas waste |
| `description` | string | π Drop | Not used in any downstream task |
| `filename` | string | π Drop | Internal WikiArt reference β no value |
| `embeddings_pca512` | array | π Drop | Unknown source model β unverifiable |
| `artist_name` | string | β οΈ Alias | Duplicate of `artist` β dropped after rename |
| `genre_name` | string | β οΈ Alias | Duplicate of `genre` β dropped after rename |
| `style_name` | string | β οΈ Alias | Duplicate of `style` β dropped after rename |
---
## π§Ή Data Cleaning
### Column Quality Audit
Every transformation is documented with a specific, falsifiable reason.

*Green = kept Β· Amber = redundant alias Β· Red = dropped*
### Before vs After

| Metric | Before | After |
|:---|:---:|:---:|
| Columns | 12 | **6** |
| Memory | ~750 MB | **~5 MB** |
| Reduction | β | **99%** |
### Drop Decisions β Exact Reasons
```
embeddings_pca512 β Unknown source model. Architecture, training data,
and normalisation are undocumented. Incompatible
vector space with our CLIP embeddings.
description β WikiArt text blurbs. Not used in EDA, embeddings,
or recommendation. Unused columns violate data hygiene.
filename β Internal WikiArt file reference. Zero analytical value.
image (PIL) β PIL objects in pandas = ~700 MB RAM consumed.
Images accessed via HF dataset object (ds[i]["image"]).
*_name aliases β Exact duplicates of artist/genre/style created during
label decoding. Dropped after rename.
```
### Step-by-Step Cleaning Code
```python
# 1 β Drop all redundant / unused columns
COLS_TO_DROP = [
"embeddings_pca512", "description", "filename",
"image", "artist_name", "genre_name", "style_name"
]
dataset = dataset.drop(columns=[c for c in COLS_TO_DROP if c in dataset.columns])
# 2 β Parse year from messy raw date strings
def extract_year(date_str):
"""
Handles: '1920', '1901.0', 'c.1890', '1299', '1650-1660', 'None', 'Unknown'
Pattern: 1\d{3} captures 1000β1999 β includes medieval art missed by 1[3-9]\d{2}
"""
if pd.isna(date_str) or str(date_str).strip().lower() in ["unknown", "none", ""]:
return None
match = re.search(r'\b(1\d{3}|20[0-2]\d)\b', str(date_str))
return int(match.group()) if match else None
dataset["year"] = dataset["date"].apply(extract_year)
```
### Date Parsing Analysis

The raw `date` column contains 4 distinct string formats scraped from WikiArt.org:
```
Pure year "1920" β β Extracted directly
Float year "1901.0" β β Regex ignores decimal
Year range "1650-1660" β β Takes first year
Circa prefix "c.1179" β β Regex finds embedded digits
Pre-1000 art "893" β β Outside regex range β NaN
None/Unknown "Unknown" β β Explicitly returns None
```
> **Parse result:** ~73% of dates extracted
> The remaining ~27% are either pre-medieval (before year 1000) or
> freeform strings (`"17th century"`, `"Unknown"`) β they become `NaN`
> and are excluded from temporal analysis **only**. All other columns remain intact.
### Final Null Check

```
==========================================
NULL CHECK β FINAL
==========================================
title : β 0 nulls
artist : β 0 nulls
date : β 0 nulls (raw string kept)
genre : β 0 nulls
style : β 0 nulls
year : β 2,133 nulls (unparseable dates β expected)
==========================================
Final shape : (8000, 6)
Final columns: ['title', 'date', 'artist', 'genre', 'style', 'year']
Memory usage : ~5 MB
==========================================
```
---
## π Exploratory Data Analysis
### 2.1 β Style Distribution: 116 Styles, Severe Long-Tail

The `Artificio/WikiArt` dataset contains **116 unique art styles** β 4Γ more than the canonical 27-class `huggan/wikiart` dataset, reflecting the full tagging granularity of WikiArt.org.
```
Realism ββββββββββββββββββββ 816 artworks (10.2%)
Impressionism βββββββββββββββββββ 796 artworks (10.0%)
Romanticism ββββββββββββββββββ 722 artworks ( 9.0%)
Expressionism ββββββββββββββ 571 artworks ( 7.1%)
Post-Impressionism ββββββββββ 418 artworks ( 5.2%)
Art Nouveau ββββββββββ 403 artworks ( 5.0%)
Baroque βββββββββ 378 artworks ( 4.7%)
[109 more styles β long tail below 4% each]
```
| Metric | Value |
|:---|:---|
| Total unique styles | **116** |
| Top 7 styles as share of dataset | **50%** |
| Pareto β 80% covered by | **top 22 styles** |
| Imbalance ratio (max/min) | **~800Γ** |
| Least represented styles | 1 artwork each |
> **Key implication for embeddings:**
> KMeans cannot rely on balanced class signal.
> CLIP embeddings carry the full semantic weight of style distinction.
> Clusters reflect visual similarity β not label frequency.
---
### 2.2 β Genre Distribution: 42 Categories

Genre describes *what is depicted* β portrait, landscape, religious painting, etc.
Three lenses reveal the distribution:
**Left panel β Ranked bar chart** with artist diversity per genre:
- `portrait` leads at 1,314 images from 389 artists β broad representation
- `illustration` has disproportionately few artists per image (64 artists, 230 images)
β embedding bias risk: CLIP may learn *artist style* instead of *genre character*
- `tessellation`, `vanitas`, `shan shui` β fewer than 3 contributing artists each
**Centre panel β Donut chart:**
Portrait + Landscape + Genre Painting = **~45% of all artworks combined**
**Right panel β Image count vs artist diversity scatter:**
Genres far below the trend line (low artist diversity relative to image count)
are flagged β οΈ β they will produce recommendation echo chambers.
---
### 2.3 β Top 20 Artists by Image Count

```
Raphael Kirchner ββββββββββββ 62 images β commercial illustrator
Marc Chagall βββββββββββ 56 images
Salvador Dali ββββββββ 48 images
Ivan Shishkin ββββββββ 48 images
Isaac Levitan βββββββ 47 images
[top 10 = ~18% of dataset]
```
> β οΈ **Observation:** Raphael Kirchner (rank 1) is a commercial illustrator,
> not a canonical fine artist β WikiArt's scraping methodology did not
> distinguish between fine art and commercial illustration.
> All 15 top artists are Western European or Russian males. Zero diversity.
---
### 2.4 β Style Γ Genre Cross-Analysis

A bubble chart maps the **top 15 styles Γ top 12 genres** (bubble size = artwork count).
This reveals structural patterns invisible in marginal distributions:
| Cross-label finding | What it means |
|:---|:---|
| **Baroque** concentrates in *religious* + *mythological* | Historically accurate β Baroque was devotional art |
| **Impressionism** spans *landscape* + *genre painting* | Reflects the movement's subject breadth |
| **Surrealism** skews toward *abstract* + *symbolic* | Confirms its non-realist nature |
| **Realism** shows strong signal across **5+ genres** | The most genre-diverse style in the dataset |
| **Ukiyo-e** maps almost exclusively to *genre painting* | Woodblock prints of everyday Japanese life |
> These cross-label patterns appear as visible **sub-cluster structure**
> in the UMAP and t-SNE projections in Part 3.
---
### 2.5 β Temporal Distribution: 700 Years of Art History

**Left panel β Annotated timeline** with art movement period shading:
| Period | Share | Movement |
|:---|:---:|:---|
| Pre-1500 | ~5% | Proto-Renaissance / Medieval |
| 1500β1700 | ~11% | Renaissance / Baroque |
| 1700β1850 | ~18% | Rococo / Neoclassicism / Romanticism |
| 1850β1900 | ~21% | Impressionism era |
| **1900β1950** | **~36%** | **Modernism β dominates the dataset** |
| 1950β2010 | ~9% | Contemporary |
**Centre panel β Decade breakdown (1850β2010):**
Peak decade: **1900β1910** with ~500 artworks.
**Right panel β Century-level totals:**
20th century = 45% of all parseable dates despite covering only 1/7th of the full timeline.
> **Median year: ~1904**
> The dataset is overwhelmingly modern. A user querying a Romanesque fresco
> will receive Modernist nearest neighbours β not because CLIP failed,
> but because the index contains few medieval works to retrieve.
> This is a documented limitation, not a bug.
---
### 2.6 β Sample Artworks per Style

Visual inspection confirms:
- Labels are correctly assigned β images tagged "Baroque" visually look Baroque
- Image quality is consistent and sufficient for CLIP processing
- Style diversity is visually distinguishable even within the same historical period
- Intra-style diversity is sufficient β cluster separation is not trivial
---
### EDA Summary
| Finding | Value | Implication |
|:---|:---|:---|
| Style imbalance | 800Γ ratio | CLIP must carry full semantic weight |
| Genre concentration | Top 3 = 45% | Portrait/landscape dominate recommendations |
| Artist diversity risk | 5 genres <3 artists | Echo chamber risk for niche genres |
| Temporal bias | 36% from 1900β1950 | Modernism overrepresented in results |
| Cross-label structure | Baroqueβreligious | Visible as sub-clusters in UMAP |
| Western bias | 2% non-Western | Recommendation system reflects this directly |
---
## π§ CLIP Embeddings
### Model β `openai/clip-vit-base-patch32`
| Property | Value |
|:---|:---|
| Architecture | Vision Transformer ViT-B/32 + Text Transformer |
| Pre-training corpus | **400 million** image-text pairs |
| Output dimension | **512** |
| Downloads on HuggingFace | 581M+ |
| Key capability | Image AND text share the **same** vector space |
| Why CLIP over ViT-only | Text queries work zero-shot without retraining |
| Why ViT-B/32 vs larger | Speed/quality balance β runs on CPU Basic Space |
### Implementation β Direct Sub-Model Calls
```python
def encode_image(pil_img):
inp = processor(images=pil_img, return_tensors="pt")
pixels = inp["pixel_values"].to(DEVICE)
with torch.no_grad():
out = model.vision_model(pixel_values=pixels)
projected = model.visual_projection(out.pooler_output)
return F.normalize(projected, dim=-1).cpu().numpy().squeeze()
def encode_text(text):
inp = processor(text=[text], return_tensors="pt", padding=True, truncation=True)
with torch.no_grad():
out = model.text_model(input_ids=inp["input_ids"].to(DEVICE),
attention_mask=inp["attention_mask"].to(DEVICE))
projected = model.text_projection(out.pooler_output)
return F.normalize(projected, dim=-1).cpu().numpy().squeeze()
```
> β οΈ **Implementation note:**
> We call `model.vision_model` + `model.visual_projection` directly
> rather than `model.get_image_features()`.
> In `transformers >= 4.40`, the wrapper returns `BaseModelOutputWithPooling`
> instead of a raw tensor. The direct call is version-safe and produces identical results.
---
### PCA β Explained Variance per Component

**Stage 1 β PCA: 512D β 50D**
- Retains ~65% of total variance
- Sharp drop after component 3: three axes dominate WikiArt visually
- **Component 1** β colour temperature (warm Baroque vs bright Impressionist)
- **Component 2** β abstraction level (figurative vs non-representational)
- **Component 3** β texture density (detailed Renaissance vs loose Expressionist)
- Reduces UMAP/t-SNE runtime by **10Γ** vs. running on raw 512D
**Stage 2 β UMAP: 50D β 2D** | **Stage 3 β t-SNE: 50D β 2D**
---
### UMAP β Coloured by Style

---
### t-SNE β Coloured by Style

**Central finding across both projections:**
> Art styles occupy a **continuous visual spectrum** β not discrete, separable clusters.
> This is not a failure of CLIP; it is a structural truth about art itself.
> A painting can be Romantic in subject, Impressionist in technique,
> and Post-Impressionist in palette simultaneously.
**Structural observations:**
| Location in plot | Visual group | Explanation |
|:---|:---|:---|
| **Periphery** | Ukiyo-e, Northern Renaissance, Surrealism, Art Nouveau | Visually distinctive β CLIP separated them without supervision |
| **Dense centre** | Realism, Impressionism, Romanticism, Expressionism | Share figurative subjects and naturalistic colour |
| **Isolated top-left cluster** | Non-Western art (Yakusha-e / Bijinga) | Found purely from visual features β zero label supervision |
| **Right-side scatter** | Baroque, Symbolism | Dark palettes, cooler colour temperature |
---
## π΅ Clustering
### KMeans Hyperparameter Search

Systematic silhouette score grid search over k = 5 to 50 identifies **k = 20** as the elbow point.
We select k by silhouette score peak β not arbitrary choice.
### KMeans Results (k = 20) β Silhouette: 0.0703

| Metric | Value | Interpretation |
|:---|:---:|:---|
| Silhouette Score | **0.0703** | Expected β art is a continuous visual spectrum |
| Isolated clusters | **3** | Genuine visual sub-groups without label supervision |
| Average cluster purity | ~38% | CLIP structure meaningfully diverges from human tags |
**Three isolated clusters detected without label supervision:**
| Cluster | Location | Dominant Style | Visual Character |
|:---|:---|:---|:---|
| 3 | Far left | Romanticism / Expressionism | Warm, high-saturation, loose brushwork |
| 10 | Far right | Baroque / Northern Renaissance | Dark palette, Chiaroscuro, religious themes |
| 17 | Bottom | Ukiyo-e / Shin-hanga | Non-Western visual signature β flat, linear |
> **Critical finding:**
> *KMeans found tighter spatial groupings than ground-truth genre labels in the same 2D space.*
> CLIP's visual similarity captures structure that human genre tags blur β
> validating visual embeddings as **superior** to metadata alone for art recommendation.
---
### DBSCAN β Interactive Parameter Tuning
DBSCAN discovers cluster count from data density β no `k` required.
An interactive `ipywidgets` slider tunes `epsilon` and `min_samples` in real time:
| Parameter | Effect | Recommended range |
|:---|:---|:---|
| `eps` (epsilon) | Neighbourhood search radius | 0.3 β 1.5 in UMAP space |
| `min_samples` | Minimum points to form a core | 5 β 20 |
> High noise percentage at low epsilon confirms the continuous, non-discrete
> structure of the WikiArt embedding space β consistent with t-SNE and UMAP.
---
## π Recommendation System
### Architecture
```
User Input
β
βββ πΌ Image βββΊ model.vision_model β model.visual_projection β F.normalize β 512D vec
β
βββ βοΈ Text ββββΊ model.text_model β model.text_projection β F.normalize β 512D vec
β
βββ π Both ββββΊ average(img_vec, txt_vec) β re-normalise β 512D vec
β
dot product vs 8,000 stored unit vectors β emb_matrix @ query_vec
(cosine similarity == dot product when both L2-normalised)
β
argsort descending β top-3 indices
β
Return: image Β· title Β· artist Β· style Β· genre Β· score
```
**Query latency: < 10 ms** on CPU for 8,000 artworks β pure NumPy matmul.
---
### Image Query Results
**Baroque painting β Venice cityscapes** (scores: 0.94, 0.92)
CLIP retrieved architecturally consistent urban waterscapes matching palette, perspective, and brushwork. Rank 3 crosses the style boundary (Baroque β Rococo) but is visually indistinguishable β **correct behaviour**. Visual embeddings are more precise than style labels for fine-grained similarity.
**Surrealism painting β Symbolic figurative works** (scores: 0.82)
Rank 2 (Marc Chagall, NaΓ―ve Art) crosses the style label boundary but is visually consistent: dreamlike floating figures, non-realistic colour, symbolic imagery. System correctly prioritised visual semantics over metadata tags.
---
### Text Query Results
CLIP was trained on 400M image-text pairs β text and image embeddings share the same 512D space.
**Zero-shot text-to-image retrieval with no additional training.**
| Query | Top Result | Score | Notes |
|:---|:---|:---:|:---|
| `"dark religious painting with dramatic lighting"` | Caravaggio β Baroque | 0.334 | β CLIP recognised Chiaroscuro from text |
| `"abstract geometric shapes in primary colors"` | Neoplasticism / Orphism | 0.337 | β Art-historical precision from description |
| `"bright impressionist landscape with flowers"` | Metzinger β Impressionism | 0.331 | β Style + genre + mood perfectly matched |
| `"portrait of a woman with soft elegant brushwork"` | William Merritt Chase | 0.342 | β Crosses style boundary on visual grounds |
| `"japanese woodblock print with waves and nature"` | Utamaro / Hiroshige β Ukiyo-e | 0.348 | β Finds 2% minority from text alone |
**Score reference by query type:**
| Mode | Typical Score | Reason |
|:---|:---:|:---|
| Image β Image | 0.88 β 1.00 | Same modality, same region of embedding space |
| Text β Image | 0.31 β 0.35 | CLIP modality gap β ranking is still semantically correct |
| Combined | 0.55 β 0.75 | Averaged vector bridges the modality gap |
> **CLIP modality gap:** A well-known property of contrastive models where image and text
> embeddings occupy adjacent but not identical regions of the shared vector space.
> Lower absolute scores on text queries are expected and do not indicate poor retrieval.
---
### Combined Image + Text Query
```python
def encode_combined(pil_img, text):
img_vec = encode_image(pil_img) # 512D unit vector
txt_vec = encode_text(text) # 512D unit vector
combined = (img_vec + txt_vec) / 2.0 # average
return combined / np.linalg.norm(combined) # re-normalise
```
Use case: *"Find artworks that look like this painting, but with darker dramatic lighting."*
Produces similarity scores of **0.55β0.75** β significantly higher than text-only (0.31β0.35)
while incorporating the textual constraint.
---
## βοΈ Ethical Bias Analysis

A recommendation system is only as fair as its training data. We quantify WikiArt's bias across four dimensions:
Dimension
In Dataset
Estimated Real-World Share
Gap
Non-Western styles
2%
~40%
20Γ underrepresented
Female artists
2.5%
~50%
20Γ underrepresented
Pre-1700 art
8.5%
~30%
3.5Γ underrepresented
Post-1970 contemporary
5.8%
~25%
4Γ underrepresented
Top-20 artists as share
~18%
~2% in balanced corpus
9Γ over-concentrated
**Finding 1 β Geographic underrepresentation:**
Only 2% non-Western art in the index. A user from Japan, China, India, or the Middle East will receive results dominated by 19th-century European paintings regardless of their query.
**Finding 2 β Temporal concentration:**
35.7% of all artworks from 1900β1950 (Modernism era alone). Pre-1700 art across 400 years = under 16%.
**Finding 3 β Artist concentration risk:**
Rank 1 artist (Raphael Kirchner, 62 images) is a **commercial illustrator** β WikiArt's scraping methodology did not distinguish fine art from commercial work. Top 20 artists are entirely Western European and Russian males.
**Finding 4 β Gender gap:**
~2.5% female artists vs. ~50% of global practicing artists.
> **Production requirement before deployment:**
> Dataset augmentation (non-Western, pre-modern, female artists) +
> diversity-constrained re-ranking at query time +
> regional cultural weighting +
> clear disclosure of limitations to end users.
---
## π How to Use the App
**Live Space:** [**huggingface.co/spaces/Uris001/wikiart-art-recommender**](https://huggingface.co/spaces/Uris001/wikiart-art-recommender)
### Mode 1 β πΌ Image Upload
1. Click **Upload a Painting** β choose any artwork from your device
2. Click **π Find Similar Artworks**
3. Top-3 artworks appear with title Β· artist Β· style Β· genre Β· similarity score
### Mode 2 β βοΈ Text Description
Try these example queries:
```
"dark dramatic baroque religious painting with shadows and candlelight"
"bright impressionist landscape with flowers and warm golden light"
"abstract geometric shapes in primary colors cubist style"
"japanese woodblock print with waves mountains and nature"
"melting clocks surrealist dreamlike scene salvador dali style"
"portrait of a woman with soft elegant brushwork renaissance"
```
### Mode 3 β π Combined (Image + Text)
Upload an image **and** type a description together.
The system averages both embeddings β use this to semantically steer results:
```
Upload: [a Monet painting] + Text: "but darker and more dramatic"
Upload: [a landscape photo] + Text: "in the style of Japanese woodblock prints"
```
Click **β Clear** to reset all inputs and outputs instantly.
---
## π Repository Structure
```
wikiart-art-recommender/
β
βββ π app.py # Gradio 6 application β main entry point
βββ π requirements.txt # Python dependencies (torch>=2.5, transformers>=4.40)
βββ π README.md # This file
β
βββ π¦ wikiart_embeddings_with_images.parquet # Self-contained embedding index
β βββ embedding float32[512] # L2-normalised CLIP vector
β βββ image_bytes bytes # JPEG thumbnail 256Γ256
β βββ title string # Artwork title
β βββ artist string # Artist name
β βββ style string # Art movement
β βββ genre string # Genre category
β βββ cluster int # KMeans cluster assignment (k=20)
β βββ pca_x/y, tsne_x/y, umap_x/y # 2D projection coordinates
β
βββ π assets/ # All plots referenced in this README
βββ cleaning_audit.png # Column quality heatmap
βββ before_after_cleaning.png # Before/after comparison
βββ date_parsing.png # Date format analysis + parse rate
βββ final_verification.png # Null check + dtype confirmation
βββ style_distribution.png # 3-panel style analysis + Pareto
βββ genre_distribution.png # Genre bar + donut + scatter
βββ artist_distribution.png # Top 20 artists
βββ style_genre_bubble.png # Cross-label bubble chart
βββ year_distribution.png # 3-panel temporal analysis
βββ sample_grid.png # 4Γ8 artwork grid per style
βββ pca_variance.png # Explained variance per component
βββ umap_by_style.png # UMAP coloured by style
βββ tsne_by_style.png # t-SNE coloured by style
βββ kmeans_gridsearch.png # Elbow + silhouette grid search
βββ umap_clusters.png # KMeans k=20 in UMAP space
βββ bias_analysis.png # 4-panel ethical bias analysis
```
---
## π Tech Stack
| Component | Technology | Notes |
|:---|:---|:---|
| Embedding model | `openai/clip-vit-base-patch32` | Vision + Text in same 512D space |
| UI framework | Gradio 6.14 | Dark theme Β· CSS custom Β· Mobile-ready |
| Similarity search | NumPy matmul | Cosine sim = dot product on unit vectors |
| Dimensionality reduction | PCA + UMAP + t-SNE | scikit-learn + umap-learn |
| Clustering | KMeans + DBSCAN | With ipywidgets interactive tuner |
| Data storage | Parquet via pyarrow | Embeddings + thumbnails in single file |
| Visualisation | Matplotlib Β· Seaborn Β· Plotly | All plots reproducible from notebook |
| Deep learning | PyTorch β₯ 2.5 | CPU inference on Spaces |
| Dataset | Artificio/WikiArt | via HuggingFace Datasets |
| Platform | HuggingFace Spaces β CPU Basic | Free tier β fully self-contained |
---
## π€ Author
Uri Sivan
Data Science Course Β· Reichman University
Assignment 3 β Embeddings, Recommendation Systems & HuggingFace Spaces