josortmel commited on
Commit
e6ccde2
·
verified ·
1 Parent(s): 2712b04

Upload folder using huggingface_hub

Browse files
.gitattributes CHANGED
@@ -33,3 +33,6 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ catalog.sqlite filter=lfs diff=lfs merge=lfs -text
37
+ model_fp32.onnx.data filter=lfs diff=lfs merge=lfs -text
38
+ model_full_fp32.onnx.data filter=lfs diff=lfs merge=lfs -text
MANIFEST.sha256 ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 6499646c6fdf7b6d5d88fd1c7ed0953b65632e2eedec65e9a518b2f30227a823 catalog.sqlite
2
+ af32b78680e044cd8f65513005d588e40ec50a212a0d2682e18611f8f97f0734 feature_vocabs.json
3
+ 86f9c130291b6fb799685961b5b6691f95f81547fded1a917df824746c3f728f full_aux.npz
4
+ 1940e1f1a495a209222f759c51587a4a6f0b56171de237bbc7bb84c405eb39d2 full_vocab_map.json
5
+ 8013974fe435247132c5bc448f38c76d0eb9284511a6787b7d0d8fea0b0014ed item_embeddings_full.npy
6
+ 4e00cdcbe6b633c4cbcc17d7179243bc5c3f106e281498109c7941efe7d06356 item_embeddings.npy
7
+ c1a5e720d7f5c77e6580346e77a38d5d74bb30da03d8ae3c835a583d6e7bd2a2 meta_full.json
8
+ 665e05667124c17d207a819000ac29c08fae534d8f4eae0b734bc5cb015568a6 meta.json
9
+ 5f605b98d542f94028999689aad4a70601f525b212ba102a7497aee909f24b2b model_fp32.onnx
10
+ f5449fa7a58fe2f3dbd3dc4562d51e9ce3299450f69a2903e75aee0615934cf6 model_fp32.onnx.data
11
+ a6f98f311a0d50ca20532ab3b15b482362c8f39ceebd149c57342831c62b1c07 model_full_fp32.onnx
12
+ 6e3bd12e8c6466057d8c3fb09f8b4880a7229d30e4156e993ec310624ea4587f model_full_fp32.onnx.data
13
+ 65df51b90355b7ea9af878ce6aed16a9a6fcdbe98f12bde71b4189aac3e58e53 vocab_map.json
14
+ 445db02f9855f26ef4672089bce54ab74141c587306b635ad8a5e0fa961b73d5 scripts/build_catalog.py
README.md ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: cc-by-nc-sa-4.0
3
+ tags:
4
+ - recommendation
5
+ - sequential-recommendation
6
+ - movies
7
+ - series
8
+ - gsasrec
9
+ - onnx
10
+ language:
11
+ - en
12
+ - es
13
+ library_name: onnxruntime
14
+ pipeline_tag: other
15
+ ---
16
+
17
+ # FrameLanguageLM
18
+
19
+ A language model where tokens are movies and TV series — trained on 32M viewing sequences to predict "the next film you'd love."
20
+
21
+ ## Model Description
22
+
23
+ - **Architecture:** gSASRec (2-layer transformer, d=256, gBCE loss) with compositional embeddings
24
+ - **Vocabulary:** ~100k titles (movies + TV series) selected by IMDb numVotes
25
+ - **Training data:** MovieLens 32M sequences, enriched with IMDb/TMDB metadata
26
+ - **Cold-start:** ID-dropout (p=0.2) enables recommendation of unseen titles via metadata-only embeddings
27
+ - **Dual serving:** warm checkpoint (54k items with collaborative signal) + full checkpoint (100k including cold items)
28
+
29
+ ## Performance
30
+
31
+ | Metric | Warm (feat) | Full (feat_iddrop) |
32
+ |--------|:-----------:|:------------------:|
33
+ | TEST NDCG@10 | 0.1174 | 0.1110 |
34
+ | TEST Recall@10 | 0.2164 | 0.2066 |
35
+ | TEST Recall@50 | 0.4630 | 0.4511 |
36
+ | Cold-start NDCG@10 | — | 0.0846 (63.7% of warm) |
37
+
38
+ Full ranking evaluation (no candidate sampling), leave-one-out temporal split. Total training cost: ~$1.25 (RTX 3090).
39
+
40
+ ## How to Use
41
+
42
+ ```bash
43
+ # Install the CLI
44
+ pip install frame-language-lm
45
+
46
+ # Import your FilmAffinity profile (auto-downloads model on first run)
47
+ frame-language-lm import --filmaffinity export.zip
48
+
49
+ # Get your gaps — movies you'd love but haven't seen
50
+ frame-language-lm gaps --top 50
51
+
52
+ # Check if a specific title is worth watching for you
53
+ frame-language-lm worth "Stalker"
54
+
55
+ # Find similar titles
56
+ frame-language-lm similar "Mulholland Drive"
57
+
58
+ # Search the catalog with filters
59
+ frame-language-lm search --director "Park Chan-wook" --country KR
60
+ ```
61
+
62
+ ## Files in this Repository
63
+
64
+ | File | Size | Description |
65
+ |------|------|-------------|
66
+ | `model_fp32.onnx` + `.data` | ~56 MB | Warm checkpoint (54k items with signal) |
67
+ | `model_full_fp32.onnx` + `.data` | ~101 MB | Full checkpoint (100k items, ID-dropout) |
68
+ | `item_embeddings.npy` | ~53 MB | Warm item embedding matrix |
69
+ | `item_embeddings_full.npy` | ~98 MB | Full item embedding matrix |
70
+ | `full_aux.npz` | ~0.2 MB | Cold-start flags (is_cold, is_movie, etc.) |
71
+ | `catalog.sqlite` | ~36 MB | Complete catalog with metadata |
72
+ | `vocab_map.json` | ~1 MB | Warm vocabulary mapping (tconst ↔ index) |
73
+ | `full_vocab_map.json` | ~2 MB | Full vocabulary mapping |
74
+ | `feature_vocabs.json` | ~2.4 MB | Compositional feature vocabularies |
75
+ | `meta.json` / `meta_full.json` | <1 KB each | Model configuration |
76
+
77
+ ## Training Data & Licenses
78
+
79
+ - **MovieLens 32M:** F. Maxwell Harper and Joseph A. Konstan. 2015. *The MovieLens Datasets: History and Context.* ACM Transactions on Interactive Intelligent Systems (TiiS) 5, 4, Article 19. Non-commercial use; redistribution of transformations under same conditions.
80
+ - **IMDb Non-Commercial Datasets:** Personal and non-commercial use only. [developer.imdb.com](https://developer.imdb.com/non-commercial-datasets/)
81
+ - **TMDB API:** Metadata enrichment (country, language, budget). This product uses the TMDB API but is not endorsed, certified, or otherwise approved by TMDB.
82
+
83
+ ## Limitations
84
+
85
+ - Trained on MovieLens (predominantly US/English mainstream cinema) — recommendations biased toward popular Western titles
86
+ - MovieLens ends Oct 2023 — post-2023 titles are cold-start only
87
+ - Series recommendations are metadata-based (no collaborative signal) — lower quality than films
88
+ - Budget feature: only 20-40% coverage in TMDB
89
+
90
+ ## Ethical Considerations
91
+
92
+ - No user data is collected or transmitted — all inference runs locally
93
+ - Model cannot recover individual user viewing histories from training data
94
+ - Recommendations reflect statistical patterns in viewing behavior, not quality judgments
95
+
96
+ ## Data Provenance & Takedown
97
+
98
+ This model was trained on publicly available, non-commercial datasets:
99
+
100
+ | Source | Data used | License | How to verify |
101
+ |--------|-----------|---------|---------------|
102
+ | MovieLens 32M | User viewing sequences | Custom (non-commercial, redistribution OK) | [grouplens.org](https://grouplens.org/datasets/movielens/32m/) |
103
+ | IMDb Non-Commercial | Titles, genres, directors, cast, year | Custom (personal/non-commercial) | [developer.imdb.com](https://developer.imdb.com/non-commercial-datasets/) |
104
+ | TMDB API | Country, language, budget (compositional features) | TMDB API ToS | [themoviedb.org](https://www.themoviedb.org/) |
105
+
106
+ For convenience, this repository includes a pre-built catalog (`catalog.sqlite`) containing metadata derived from IMDb and TMDB. If any rights holder objects to the inclusion of this catalog, it will be removed and users can rebuild it locally using the provided `scripts/build_catalog.py` script. Model weights are a mathematical transformation that cannot reconstruct the original training data.
107
+
108
+ **Takedown requests:** if you are a rights holder and believe any artifact in this repository infringes on your terms, please contact peportmel@gmail.com. We will respond within 72 hours and can remove the affected artifact or retrain the model excluding your data source at negligible cost (~$0.30, ~90 minutes of GPU time).
akas_es.json ADDED
The diff for this file is too large to render. See raw diff
 
catalog.sqlite ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6499646c6fdf7b6d5d88fd1c7ed0953b65632e2eedec65e9a518b2f30227a823
3
+ size 37548032
feature_vocabs.json ADDED
The diff for this file is too large to render. See raw diff
 
full_aux.npz ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:86f9c130291b6fb799685961b5b6691f95f81547fded1a917df824746c3f728f
3
+ size 208120
full_vocab_map.json ADDED
The diff for this file is too large to render. See raw diff
 
item_embeddings.npy ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4e00cdcbe6b633c4cbcc17d7179243bc5c3f106e281498109c7941efe7d06356
3
+ size 55351424
item_embeddings_full.npy ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8013974fe435247132c5bc448f38c76d0eb9284511a6787b7d0d8fea0b0014ed
3
+ size 102401152
meta.json ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "checkpoint": "sasrec_feat.pt",
3
+ "checkpoint_epoch": 50,
4
+ "valid_ndcg@10": 0.13018137636965726,
5
+ "vocab_md5": "97774e35046472f8340a1b0bb4c751da",
6
+ "vocab_md5_actual": "97774e35046472f8340a1b0bb4c751da",
7
+ "n_items": 54053,
8
+ "d": 256,
9
+ "max_len": 200
10
+ }
meta_full.json ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "checkpoint": "sasrec_feat_iddrop.pt",
3
+ "checkpoint_epoch": 39,
4
+ "valid_ndcg@10": 0.12276953213630627,
5
+ "vocab_md5": "97774e35046472f8340a1b0bb4c751da",
6
+ "vocab_md5_actual": "97774e35046472f8340a1b0bb4c751da",
7
+ "n_items": 54053,
8
+ "d": 256,
9
+ "max_len": 200
10
+ }
model_fp32.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5f605b98d542f94028999689aad4a70601f525b212ba102a7497aee909f24b2b
3
+ size 22808
model_fp32.onnx.data ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f5449fa7a58fe2f3dbd3dc4562d51e9ce3299450f69a2903e75aee0615934cf6
3
+ size 59086848
model_full_fp32.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a6f98f311a0d50ca20532ab3b15b482362c8f39ceebd149c57342831c62b1c07
3
+ size 22969
model_full_fp32.onnx.data ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6e3bd12e8c6466057d8c3fb09f8b4880a7229d30e4156e993ec310624ea4587f
3
+ size 106136576
scripts/build_catalog.py ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Fase 1.4: ensambla el catalogo final y las secuencias de entrenamiento.
2
+
3
+ Uso: uv run python scripts/build_catalog.py
4
+ Entrada: data/interim/vocab.parquet + data/interim/tmdb.jsonl + data/raw/ml-32m/
5
+ Salida: data/catalog.sqlite + data/sequences.parquet
6
+ """
7
+
8
+ import json
9
+ import sqlite3
10
+ import sys
11
+ from pathlib import Path
12
+
13
+ import duckdb
14
+
15
+ ROOT = Path(__file__).resolve().parent.parent
16
+ INTERIM = ROOT / "data" / "interim"
17
+ RAW = ROOT / "data" / "raw"
18
+ MIN_TMDB_COVERAGE = 0.80
19
+
20
+ vocab = duckdb.sql(
21
+ f"SELECT * FROM read_parquet('{(INTERIM / 'vocab.parquet').as_posix()}') ORDER BY numVotes DESC"
22
+ ).fetchall()
23
+ cols = [
24
+ "tconst", "titleType", "primaryTitle", "originalTitle", "startYear",
25
+ "runtimeMinutes", "genres", "averageRating", "numVotes",
26
+ "directors", "cast", "tmdb_id_links",
27
+ ]
28
+
29
+ tmdb: dict[str, dict] = {}
30
+ tmdb_path = INTERIM / "tmdb.jsonl"
31
+ if tmdb_path.exists():
32
+ with tmdb_path.open(encoding="utf-8") as f:
33
+ for line in f:
34
+ if line.strip():
35
+ rec = json.loads(line)
36
+ if "error" not in rec:
37
+ tmdb[rec["tconst"]] = rec
38
+
39
+ coverage = len([1 for row in vocab if row[0] in tmdb]) / len(vocab)
40
+ if coverage < MIN_TMDB_COVERAGE:
41
+ sys.exit(
42
+ f"cobertura TMDB insuficiente: {coverage:.1%} del vocabulario "
43
+ f"(minimo {MIN_TMDB_COVERAGE:.0%}). Lanza scripts/fetch_tmdb.py hasta completar."
44
+ )
45
+
46
+ db_path = ROOT / "data" / "catalog.sqlite"
47
+ db_path.unlink(missing_ok=True)
48
+ db = sqlite3.connect(db_path)
49
+ db.execute("""
50
+ CREATE TABLE items (
51
+ tconst TEXT PRIMARY KEY,
52
+ title_type TEXT NOT NULL,
53
+ primary_title TEXT NOT NULL,
54
+ original_title TEXT,
55
+ start_year INTEGER,
56
+ runtime_minutes INTEGER,
57
+ genres TEXT,
58
+ imdb_rating REAL,
59
+ num_votes INTEGER NOT NULL,
60
+ directors TEXT,
61
+ "cast" TEXT,
62
+ tmdb_id INTEGER,
63
+ media_type TEXT,
64
+ original_language TEXT,
65
+ countries TEXT,
66
+ budget INTEGER,
67
+ keywords TEXT,
68
+ poster_path TEXT,
69
+ popularity REAL
70
+ )
71
+ """)
72
+
73
+ rows = []
74
+ for row in vocab:
75
+ r = dict(zip(cols, row))
76
+ t = tmdb.get(r["tconst"], {})
77
+ rows.append((
78
+ r["tconst"], r["titleType"], r["primaryTitle"], r["originalTitle"],
79
+ r["startYear"], r["runtimeMinutes"], r["genres"], r["averageRating"],
80
+ r["numVotes"], r["directors"], r["cast"],
81
+ t.get("tmdb_id"), t.get("media_type"), t.get("original_language"),
82
+ "|".join(t.get("production_countries") or []) or None,
83
+ t.get("budget") or None,
84
+ "|".join(t.get("keywords") or []) or None,
85
+ t.get("poster_path"), t.get("popularity"),
86
+ ))
87
+ db.executemany(f"INSERT INTO items VALUES ({','.join('?' * 19)})", rows)
88
+ db.execute("CREATE INDEX idx_items_title ON items (primary_title)")
89
+ db.commit()
90
+ db.close()
91
+ print(f"catalog.sqlite: {len(rows):,} items (cobertura TMDB {coverage:.1%})")
92
+
93
+ seq_out = ROOT / "data" / "sequences.parquet"
94
+ duckdb.sql(f"""
95
+ COPY (
96
+ SELECT m.userId, v.tconst, m.rating, m.timestamp
97
+ FROM read_csv_auto('{(RAW / "ml-32m" / "ratings.csv").as_posix()}') m
98
+ JOIN read_csv_auto('{(RAW / "ml-32m" / "links.csv").as_posix()}') l USING (movieId)
99
+ JOIN read_parquet('{(INTERIM / "vocab.parquet").as_posix()}') v
100
+ ON v.tconst = printf('tt%07d', CAST(l.imdbId AS BIGINT))
101
+ ORDER BY m.userId, m.timestamp
102
+ ) TO '{seq_out.as_posix()}' (FORMAT PARQUET)
103
+ """)
104
+ n, users = duckdb.sql(
105
+ f"SELECT count(*), count(DISTINCT userId) FROM read_parquet('{seq_out.as_posix()}')"
106
+ ).fetchone()
107
+ print(f"sequences.parquet: {n:,} interacciones de {users:,} usuarios")
vocab_map.json ADDED
The diff for this file is too large to render. See raw diff