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
File size: 7,249 Bytes
63bb3d9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 | """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)
@classmethod
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)
|