Feature Extraction
sentence-transformers
Safetensors
Korean
English
milco
sparse-encoder
sparse
splade
learned-sparse-retrieval
custom_code
Instructions to use nlpai-lab/KURE-Sparse-base with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- sentence-transformers
How to use nlpai-lab/KURE-Sparse-base with sentence-transformers:
from sentence_transformers import SparseEncoder model = SparseEncoder("nlpai-lab/KURE-Sparse-base", trust_remote_code=True) queries = ["Which planet is known as the Red Planet?"] documents = [ "Venus is often called Earth's twin because of its similar size and proximity.", "Mars, known for its reddish appearance, is often referred to as the Red Planet.", "Jupiter, the largest planet in our solar system, has a prominent red spot.", ] query_embeddings = model.encode_query(queries) document_embeddings = model.encode_document(documents) similarities = model.similarity(query_embeddings, document_embeddings) print(similarities) - Notebooks
- Google Colab
- Kaggle
Download milco_encoder.py from nlpai-lab/KURE-Sparse-base: direct link, hf CLI and curl.
- Browser
- Download file 7.25 kB
-
https://huggingface.co/nlpai-lab/KURE-Sparse-base/resolve/main/milco_encoder.py
- Command line
-
hf download hf://nlpai-lab/KURE-Sparse-base/milco_encoder.py
-
curl -L -o milco_encoder.py https://huggingface.co/nlpai-lab/KURE-Sparse-base/resolve/main/milco_encoder.py
7.25 kB
| """MILCO (arXiv:2510.00671) as a sentence-transformers input module, so the SPARSE pipeline trains, | |
| saves and evaluates it like any SparseEncoder. | |
| MILCO is a bge-m3 body (XLM-R large) -> projector -> splade-v3's MLM head over the English BERT | |
| vocabulary (the pivot view, 30522 dims), plus LexEcho: one relu score per input token, placed on that | |
| token's XLM-R id (the source view, 250002 dims). Its native API returns the two as one sparse | |
| [N, 30522 + 250002] tensor scored by a dot product; this module emits the same vector dense, as | |
| ``sentence_embedding``, so every loss and regularizer here applies to both views unchanged. | |
| The source view adds up the scores of a repeated token: MILCO builds its COO tensor with one entry per | |
| position and coalescing sums duplicates, so this uses scatter_add. Special tokens (<s>, </s>) are | |
| attended and scored exactly as MILCO does. | |
| """ | |
| from __future__ import annotations | |
| import inspect | |
| import shutil | |
| from pathlib import Path | |
| import torch | |
| from sentence_transformers.base.modules.input_module import InputModule | |
| from transformers import AutoModel, AutoTokenizer | |
| def is_milco(model_name_or_path: str) -> bool: | |
| config = Path(model_name_or_path) / "config.json" | |
| return config.is_file() and '"model_type": "milco"' in config.read_text() | |
| class MilcoDualView(InputModule): | |
| config_file_name = "milco_encoder_config.json" # config.json is MILCO's own | |
| config_keys = ["max_seq_length", "source_view", "chunk_size"] | |
| def __init__( | |
| self, | |
| model_name_or_path: str, | |
| max_seq_length: int = 8192, | |
| source_view: bool = True, | |
| chunk_size: int = 512, | |
| model_kwargs: dict | None = None, | |
| ) -> None: | |
| super().__init__() | |
| model_kwargs = dict(model_kwargs or {}) | |
| # MILCOModel itself declares no attention backends; only its XLM-R body has them. | |
| attn = model_kwargs.pop("attn_implementation", None) | |
| # A dtype would reach only MILCOModel's own layers: its sub-models are built with from_config and keep | |
| # their config's float32, so bf16 projector/echo would sit next to an fp32 body -- and at lr 1e-5 a bf16 | |
| # weight of typical size rounds every AdamW step away. Keep fp32 master weights; training runs under | |
| # --bf16 autocast, and eval.py casts the whole model to bf16 itself. | |
| model_kwargs.pop("torch_dtype", None) | |
| model_kwargs.pop("dtype", None) | |
| self.milco = AutoModel.from_pretrained(model_name_or_path, trust_remote_code=True, **model_kwargs).float() | |
| if attn: | |
| self.milco.m_model.set_attn_implementation(attn) | |
| # BertLMPredictionHead.bias is never read in the forward (the decoder carries its own bias), but | |
| # MILCO's checkpoint stores the two on one storage, which save_pretrained refuses as untied | |
| # shared tensors. Give the unused one its own copy. | |
| head = self.milco.mlm_head.predictions | |
| head.bias = torch.nn.Parameter(head.decoder.bias.detach().clone()) | |
| # Never used in the forward: frozen, so they neither take optimizer state nor stall a DDP built | |
| # with find_unused_parameters=False. | |
| for param in [*self.milco.m_model.pooler.parameters(), self.milco.scale, head.bias]: | |
| param.requires_grad_(False) | |
| # The XLM-R body supports checkpointing; the wrapper class just does not say so. | |
| self.milco.supports_gradient_checkpointing = True | |
| has_tokenizer = (Path(model_name_or_path) / "tokenizer_config.json").is_file() | |
| # trust_remote_code: a saved checkpoint's config.json maps to MILCO's code, and AutoTokenizer reads it. | |
| self.tokenizer = AutoTokenizer.from_pretrained( | |
| model_name_or_path if has_tokenizer else self.milco.config.multilingual_encoder_checkpoint, | |
| trust_remote_code=True, | |
| ) | |
| self.max_seq_length = max_seq_length | |
| self.source_view = source_view | |
| self.chunk_size = chunk_size | |
| def preprocess(self, inputs, prompt: str | None = None, **kwargs) -> dict[str, torch.Tensor]: | |
| texts = self._prepend_prompt(list(inputs), prompt) if prompt else list(inputs) | |
| return self.tokenizer( | |
| texts, padding=True, truncation=True, max_length=self.max_seq_length, return_tensors="pt" | |
| ) | |
| def forward(self, features: dict[str, torch.Tensor], **kwargs) -> dict[str, torch.Tensor]: | |
| # MILCOModel._encode, with the MLM head run over position chunks: it materializes | |
| # [batch, seq, 30522] logits at once, ~16 GB for an evaluation batch of 32 at 8192 tokens. | |
| # Max over chunks of the max is the same max, and log1p(relu(.)) is monotone, so the | |
| # result is unchanged. | |
| milco = self.milco | |
| input_ids, attention_mask = features["input_ids"], features["attention_mask"] | |
| hidden = milco.m_model(input_ids=input_ids, attention_mask=attention_mask).last_hidden_state | |
| mask = attention_mask.unsqueeze(-1) | |
| pivot = None | |
| for i in range(0, hidden.shape[1], self.chunk_size): | |
| logits = milco.mlm_head(milco.activation(milco.projector(hidden[:, i : i + self.chunk_size]))) | |
| chunk_max = (logits * mask[:, i : i + self.chunk_size]).max(dim=1).values | |
| pivot = chunk_max if pivot is None else torch.maximum(pivot, chunk_max) | |
| pivot = torch.log1p(torch.relu(pivot)).float() | |
| if not self.source_view: | |
| features["sentence_embedding"] = pivot | |
| return features | |
| weights = torch.relu(milco.echo(hidden).squeeze(-1)) * attention_mask | |
| source = pivot.new_zeros((input_ids.shape[0], self.milco.config.m_vocab_size), dtype=torch.float32) | |
| source = source.scatter_add(1, input_ids, weights.float()) | |
| features["sentence_embedding"] = torch.cat([pivot, source], dim=1) | |
| return features | |
| def get_sentence_embedding_dimension(self) -> int: | |
| config = self.milco.config | |
| return config.en_vocab_size + (config.m_vocab_size if self.source_view else 0) | |
| def save(self, output_path: str, *args, safe_serialization: bool = True, **kwargs) -> None: | |
| # Keep the unused bias equal to the trained one, in case a later transformers re-ties the two on load. | |
| head = self.milco.mlm_head.predictions | |
| head.bias.data.copy_(head.decoder.bias.data) | |
| self.milco.save_pretrained(output_path) | |
| # MILCO's remote code, for AutoModel(trust_remote_code=True) on the saved directory. | |
| shutil.copyfile(inspect.getfile(type(self.milco)), Path(output_path) / "milco.py") | |
| self.tokenizer.save_pretrained(output_path) | |
| self.save_config(output_path) | |
| # modules.json names this class as milco_encoder.MilcoDualView; ship the source so the | |
| # checkpoint loads where SPARSE/src is not on sys.path (as pooling.py does). | |
| shutil.copyfile(Path(__file__), Path(output_path) / Path(__file__).name) | |
| def load(cls, model_name_or_path: str, subfolder: str = "", model_kwargs: dict | None = None, **kwargs): | |
| config = cls.load_config(model_name_or_path, subfolder=subfolder) | |
| path = str(Path(model_name_or_path) / subfolder) if subfolder else model_name_or_path | |
| return cls(path, model_kwargs=model_kwargs, **config) | |