GulkoA commited on
Commit
499dc7f
·
verified ·
1 Parent(s): e375bc6

Publish gamespace.2: Steam game embeddings trained with co-like pairs

Browse files
1_Pooling/config.json ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ {
2
+ "embedding_dimension": 384,
3
+ "pooling_mode": "mean",
4
+ "include_prompt": true
5
+ }
2_Dense/config.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "in_features": 384,
3
+ "out_features": 128,
4
+ "bias": true,
5
+ "activation_function": "torch.nn.modules.linear.Identity",
6
+ "module_input_name": "sentence_embedding",
7
+ "module_output_name": "sentence_embedding"
8
+ }
2_Dense/model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0864454833b8281dce65b28ad0519e95e89bdecfe105d5bdbfce69b28d10a429
3
+ size 197280
3_Normalize/config.json ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ {
2
+ "module_input_name": "sentence_embedding",
3
+ "module_output_name": "sentence_embedding"
4
+ }
README.md ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ language:
4
+ - en
5
+ library_name: sentence-transformers
6
+ pipeline_tag: sentence-similarity
7
+ base_model: sentence-transformers/all-MiniLM-L6-v2
8
+ datasets:
9
+ - FronkonGames/steam-games-dataset
10
+ tags:
11
+ - transformers.js
12
+ - onnx
13
+ - sentence-transformers
14
+ - sentence-similarity
15
+ - feature-extraction
16
+ - recommendation
17
+ - video-games
18
+ - steam
19
+ ---
20
+
21
+ # gamespace.2
22
+
23
+ gamespace.2 embeds video games into a 128-dimensional space where games that the same people enjoy
24
+ sit close together. It powers the [GameMerge](https://huggingface.co/spaces/GulkoA/gamemerge)
25
+ recommender. Output vectors are L2-normalised, so a dot product is the cosine similarity.
26
+
27
+ It is [all-MiniLM-L6-v2](https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2) fine-tuned on
28
+ Steam store data, like [gamespace.1](https://huggingface.co/GulkoA/gamespace.1). It also learns from
29
+ player behaviour: pairs of games that the same Steam reviewers engaged with are positive examples.
30
+ The model still reads only a game's store text, so it works for any game, including new ones with no
31
+ players yet.
32
+
33
+ ## Steam next-game benchmark
34
+
35
+ The benchmark follows the protocol of SASRec (Kang & McAuley, ICDM 2018) on their Steam review crawl.
36
+ Each user's most recent reviewed game is held out and ranked against 100 randomly sampled games, and
37
+ also against the whole catalog. Text models score candidates by cosine similarity to the mean of the
38
+ user's earlier games. Games no longer on the Steam store have no text to embed, so they were removed
39
+ before the five-review filter. That leaves 250,955 users, 10,318 games and 3.05M interactions. The
40
+ paper had 334,730 users and 13,047 games. Popularity and SASRec were re-run on this exact split.
41
+
42
+ | Model | Learns from user histories | Hit@10, 100 negatives | NDCG@10, 100 negatives | Hit@10, all games | NDCG@10, all games |
43
+ |---|---|---|---|---|---|
44
+ | SASRec, reproduced | yes | 0.836 | 0.581 | 0.115 | 0.060 |
45
+ | Popularity | yes | 0.688 | 0.420 | 0.050 | 0.025 |
46
+ | **gamespace.2** | **pairs from training histories** | **0.511** | **0.301** | **0.034** | **0.018** |
47
+ | gamespace.2 + CLIP header art | pairs from training histories | 0.520 | 0.309 | 0.037 | 0.019 |
48
+ | gamespace.1 | no | 0.438 | 0.251 | 0.027 | 0.014 |
49
+ | e5-large-v2 | no | 0.404 | 0.234 | 0.030 | 0.016 |
50
+ | Qwen3-Embedding-0.6B | no | 0.336 | 0.185 | 0.018 | 0.010 |
51
+ | bge-large-en-v1.5 | no | 0.334 | 0.191 | 0.026 | 0.014 |
52
+ | all-MiniLM-L6-v2 | no | 0.306 | 0.162 | 0.017 | 0.010 |
53
+ | CLIP header art only | no | 0.288 | 0.158 | 0.014 | 0.007 |
54
+
55
+ For reference, the SASRec paper reports 0.873 and 0.631 on its full 13,047-game set.
56
+
57
+ gamespace.2 is the best of the text embedding models, ahead of much larger general-purpose embedders.
58
+ Sequential recommenders trained on the interaction data remain far stronger at predicting the next
59
+ game, because they also learn popularity and order. gamespace.2 trades that for working on any game
60
+ from its description alone.
61
+
62
+ ## Training
63
+
64
+ - **Data.** The [Steam Games Dataset](https://huggingface.co/datasets/FronkonGames/steam-games-dataset)
65
+ has 147,352 apps. Soundtracks, demos, playtests, servers, SDKs, and empty records were removed,
66
+ leaving 138,920 games.
67
+ - **Objective.** Contrastive training uses `CachedMultipleNegativesRankingLoss` with 128 in-batch
68
+ negatives. There are three kinds of positive pairs:
69
+ - 70,000 pairs are two randomly degraded views of the same game.
70
+ - 70,000 pairs are games whose vote-weighted user-tag profiles have a cosine of at least 0.6.
71
+ - 66,904 pairs are co-liked games. There are 36,371 distinct pairs, each shown in both orders.
72
+ They are mined from the benchmark's training histories only, so no validation or test game is
73
+ used. A pair must be shared by at least 5 reviewers, it is scored by normalised PMI, and each
74
+ game keeps its 10 strongest partners.
75
+ - **Settings.** The projection is `Dense(384→128)`, initialised from PCA. Training runs one epoch at
76
+ learning rate 3e-5, with 128-token documents.
77
+
78
+ A looser pair set, with at least 3 shared reviewers and 20 partners per game, scored lower on
79
+ validation, with 0.477 against 0.503 Hit@10. Pair quality matters more than quantity.
80
+
81
+ ## Usage
82
+
83
+ The model reads one short document per game. Build it with `docs.py` from this repo, using the same
84
+ format as gamespace.1:
85
+
86
+ ```
87
+ Hollow Knight. Genres: Action, Adventure, Indie. Tags: Metroidvania, Souls-like, ... Modes: Single-player. Developer: Team Cherry. Forge your own path...
88
+ ```
89
+
90
+ ```python
91
+ from sentence_transformers import SentenceTransformer
92
+ model = SentenceTransformer("GulkoA/gamespace.2")
93
+ vec = model.encode("Hollow Knight. Genres: Action, Adventure, Indie. Tags: Metroidvania, Souls-like.")
94
+ ```
95
+
96
+ ### In the browser
97
+
98
+ `onnx/model_quantized.onnx` is 22 MB, per-channel int8, with cosine 0.995 to the PyTorch model. It
99
+ runs with [transformers.js](https://huggingface.co/docs/transformers.js). Apply the 128x384 projection
100
+ in `dense.bin`, which holds float32 weights in row-major order followed by the 128 biases, to the
101
+ mean-pooled output:
102
+
103
+ ```js
104
+ import { pipeline } from "@huggingface/transformers";
105
+
106
+ const extractor = await pipeline("feature-extraction", "GulkoA/gamespace.2", { dtype: "q8" });
107
+ const w = new Float32Array(await (await fetch("https://huggingface.co/GulkoA/gamespace.2/resolve/main/dense.bin")).arrayBuffer());
108
+ const { data: x } = await extractor("cozy farming game with fishing", { pooling: "mean", normalize: false });
109
+ const y = Array.from({ length: 128 }, (_, i) => w[128 * 384 + i] + x.reduce((s, xj, j) => s + w[i * 384 + j] * xj, 0));
110
+ const norm = Math.hypot(...y);
111
+ const vec = y.map((v) => v / norm);
112
+ ```
113
+
114
+ ## Limitations
115
+
116
+ - The behaviour pairs come from reviews written between 2010 and January 2018. Newer games benefit
117
+ only through what the model generalises from text.
118
+ - Steam display names identify reviewers, and different people can share a name.
119
+ - Similarity ignores popularity and quality, so obscure games rank alongside well-known ones.
120
+
121
+ ## License
122
+
123
+ The model is licensed under Apache 2.0, the same as the base model. The store data is MIT-licensed.
124
+ The review crawl is from the UCSD Steam datasets (Kang & McAuley, 2018).
config.json ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "add_cross_attention": false,
3
+ "architectures": [
4
+ "BertModel"
5
+ ],
6
+ "attention_probs_dropout_prob": 0.1,
7
+ "bos_token_id": null,
8
+ "classifier_dropout": null,
9
+ "dtype": "float32",
10
+ "eos_token_id": null,
11
+ "gradient_checkpointing": false,
12
+ "hidden_act": "gelu",
13
+ "hidden_dropout_prob": 0.1,
14
+ "hidden_size": 384,
15
+ "initializer_range": 0.02,
16
+ "intermediate_size": 1536,
17
+ "is_decoder": false,
18
+ "layer_norm_eps": 1e-12,
19
+ "max_position_embeddings": 512,
20
+ "model_type": "bert",
21
+ "num_attention_heads": 12,
22
+ "num_hidden_layers": 6,
23
+ "pad_token_id": 0,
24
+ "position_embedding_type": "absolute",
25
+ "tie_word_embeddings": true,
26
+ "transformers_version": "5.22.2",
27
+ "type_vocab_size": 2,
28
+ "use_cache": false,
29
+ "vocab_size": 30522
30
+ }
config_sentence_transformers.json ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "__version__": {
3
+ "pytorch": "2.14.1+cu126",
4
+ "sentence_transformers": "6.2.0",
5
+ "transformers": "5.22.2"
6
+ },
7
+ "default_prompt_name": null,
8
+ "model_type": "SentenceTransformer",
9
+ "prompts": {
10
+ "document": "",
11
+ "query": ""
12
+ },
13
+ "similarity_fn_name": "cosine"
14
+ }
dense.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0256c1aee86727d21a96829fd9a858603adea66b6012176a9bad6a96f82bb602
3
+ size 197120
docs.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Turn a catalog row into the text document the embedding model reads."""
2
+ import numpy as np
3
+
4
+ KEEP_CATEGORIES = {
5
+ "Single-player", "Multi-player", "Co-op", "Online Co-op", "LAN Co-op", "Shared/Split Screen Co-op",
6
+ "PvP", "Online PvP", "Shared/Split Screen PvP", "MMO", "VR Support", "VR Only", "Cross-Platform Multiplayer",
7
+ }
8
+
9
+
10
+ def build_doc(row, rng: np.random.Generator | None = None, drop_tags: bool = False, max_tags: int = 15) -> str:
11
+ """Deterministic when rng is None. With rng, returns a randomly degraded view for contrastive training."""
12
+ aug = rng is not None
13
+ tags = list(row["tags"])[:max_tags]
14
+ if drop_tags or (aug and rng.random() < 0.5):
15
+ tags = []
16
+ elif aug and tags:
17
+ keep = rng.random(len(tags)) >= 0.3
18
+ tags = [t for t, k in zip(tags, keep) if k]
19
+ genres = list(row["genres"])
20
+ cats = [c for c in row["categories"] if c in KEEP_CATEGORIES][:5]
21
+ desc = (row["short"] + " " + row["about"]).strip()
22
+ if aug and desc:
23
+ if rng.random() < 0.3 and (tags or genres):
24
+ desc = ""
25
+ else:
26
+ words = desc.split()
27
+ cut = int(len(words) * rng.uniform(0.3, 1.0)) if len(words) > 20 else len(words)
28
+ desc = " ".join(words[:cut])
29
+
30
+ parts = [row["name"] + "."]
31
+ if genres:
32
+ parts.append("Genres: " + ", ".join(genres) + ".")
33
+ if tags:
34
+ parts.append("Tags: " + ", ".join(tags) + ".")
35
+ if cats:
36
+ parts.append("Modes: " + ", ".join(cats) + ".")
37
+ if len(row["developers"]):
38
+ parts.append("Developer: " + ", ".join(list(row["developers"])[:2]) + ".")
39
+ if desc:
40
+ parts.append(desc)
41
+ return " ".join(parts)
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:447eefdc22a444719100b151bb331ed6f71381427e27c030ed293d52c32f77b3
3
+ size 90864192
modules.json ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "idx": 0,
4
+ "name": "0",
5
+ "path": "",
6
+ "type": "sentence_transformers.base.modules.transformer.Transformer"
7
+ },
8
+ {
9
+ "idx": 1,
10
+ "name": "1",
11
+ "path": "1_Pooling",
12
+ "type": "sentence_transformers.sentence_transformer.modules.pooling.Pooling"
13
+ },
14
+ {
15
+ "idx": 2,
16
+ "name": "2",
17
+ "path": "2_Dense",
18
+ "type": "sentence_transformers.base.modules.dense.Dense"
19
+ },
20
+ {
21
+ "idx": 3,
22
+ "name": "3",
23
+ "path": "3_Normalize",
24
+ "type": "sentence_transformers.base.modules.normalize.Normalize"
25
+ }
26
+ ]
onnx/model.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:403bf5ef3313e841d3bd384b7ac4c68dc78752078984f6aacb5d0aec5412d4c8
3
+ size 90373210
onnx/model_quantized.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:db7047b3e693ca3ab70b7f6480dfc8198f860d77ac65c0adf611a100b211f0c2
3
+ size 22965180
sentence_bert_config.json ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "transformer_task": "feature-extraction",
3
+ "modality_config": {
4
+ "text": {
5
+ "method": "forward",
6
+ "method_output_name": "last_hidden_state"
7
+ }
8
+ },
9
+ "module_output_name": "token_embeddings"
10
+ }
tokenizer.json ADDED
The diff for this file is too large to render. See raw diff
 
tokenizer_config.json ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "backend": "tokenizers",
3
+ "cls_token": "[CLS]",
4
+ "do_basic_tokenize": true,
5
+ "do_lower_case": true,
6
+ "is_local": false,
7
+ "local_files_only": false,
8
+ "mask_token": "[MASK]",
9
+ "max_length": 128,
10
+ "model_max_length": 128,
11
+ "never_split": null,
12
+ "pad_to_multiple_of": null,
13
+ "pad_token": "[PAD]",
14
+ "pad_token_type_id": 0,
15
+ "padding_side": "right",
16
+ "sep_token": "[SEP]",
17
+ "stride": 0,
18
+ "strip_accents": null,
19
+ "tokenize_chinese_chars": true,
20
+ "tokenizer_class": "BertTokenizer",
21
+ "truncation_side": "right",
22
+ "truncation_strategy": "longest_first",
23
+ "unk_token": "[UNK]"
24
+ }