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
Commit ·
63bb3d9
0
Parent(s):
KURE-Sparse-base
Browse files- .gitattributes +37 -0
- README.md +175 -0
- assets/ndcg_flops.png +3 -0
- config.json +197 -0
- config_sentence_transformers.json +14 -0
- milco.py +218 -0
- milco_encoder.py +128 -0
- milco_encoder_config.json +5 -0
- model.safetensors +3 -0
- modules.json +8 -0
- tokenizer.json +3 -0
- tokenizer_config.json +24 -0
.gitattributes
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
*.xz 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 |
+
tokenizer.json filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
assets/ndcg_flops.png filter=lfs diff=lfs merge=lfs -text
|
README.md
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
tags:
|
| 3 |
+
- sentence-transformers
|
| 4 |
+
- sparse-encoder
|
| 5 |
+
- sparse
|
| 6 |
+
- splade
|
| 7 |
+
- learned-sparse-retrieval
|
| 8 |
+
- feature-extraction
|
| 9 |
+
language:
|
| 10 |
+
- ko
|
| 11 |
+
- en
|
| 12 |
+
base_model: omai-research/milco-650m
|
| 13 |
+
pipeline_tag: feature-extraction
|
| 14 |
+
library_name: sentence-transformers
|
| 15 |
+
license: apache-2.0
|
| 16 |
+
---
|
| 17 |
+
|
| 18 |
+
<a href="https://github.com/nlpai-lab/KURE">
|
| 19 |
+
<img src="https://huggingface.co/nlpai-lab/KURE-v2/resolve/main/kure_logo.png" width="50%"/>
|
| 20 |
+
</a>
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
# 🔎 KURE-Sparse-base
|
| 24 |
+
|
| 25 |
+
**KURE-Sparse-base** is a Korean-English bilingual learned sparse retrieval model. It shows state-of-the-art performance among sparse models supporting Korean, averging **0.7984** nDCG@10 on MTEB(kor, v2) Retrieval. It also outperforms 8B dense models, such as Qwen3-Embedding-8B and comsat-embed-ko-8b-preview.
|
| 26 |
+
|
| 27 |
+
## Key Characteristics
|
| 28 |
+
|
| 29 |
+
- **Learned Sparse Retrieval (LSR)**: every vector lives in a fixed vocabulary, so documents can be indexed in an inverted
|
| 30 |
+
index and matched terms are inspectable.
|
| 31 |
+
- **Dual view**: represented with 30,522 English pivot terms (projected from the multilingual encoder) +
|
| 32 |
+
250,002 source-language tokens.
|
| 33 |
+
- **Long documents**: up to 8,192 tokens.
|
| 34 |
+
- **Sparse at both ends**: on average 100 active dimensions per query and 1,897 per document.
|
| 35 |
+
|
| 36 |
+
## Usage
|
| 37 |
+
|
| 38 |
+
```bash
|
| 39 |
+
pip install -U sentence-transformers
|
| 40 |
+
```
|
| 41 |
+
|
| 42 |
+
```python
|
| 43 |
+
from sentence_transformers import SparseEncoder
|
| 44 |
+
|
| 45 |
+
model = SparseEncoder("nlpai-lab/KURE-Sparse-base", trust_remote_code=True)
|
| 46 |
+
|
| 47 |
+
query = "훈민정음은 언제 만들어졌나요?"
|
| 48 |
+
documents = [
|
| 49 |
+
"세종대왕은 1443년에 훈민정음을 창제하고 1446년에 이를 반포하였다.",
|
| 50 |
+
"김치는 배추나 무를 소금에 절인 뒤 고춧가루와 젓갈을 넣어 발효시킨 음식이다.",
|
| 51 |
+
"한라산은 해발 1,947m로 남한에서 가장 높은 산이며 제주도 중앙에 자리한다.",
|
| 52 |
+
]
|
| 53 |
+
|
| 54 |
+
query_embeddings = model.encode_query(query)
|
| 55 |
+
document_embeddings = model.encode_document(documents)
|
| 56 |
+
print(query_embeddings.shape, document_embeddings.shape)
|
| 57 |
+
# torch.Size([280524]) torch.Size([3, 280524])
|
| 58 |
+
|
| 59 |
+
# Dot-product scores (higher is more relevant)
|
| 60 |
+
scores = model.similarity(query_embeddings, document_embeddings)
|
| 61 |
+
print(scores)
|
| 62 |
+
# tensor([[23.9621, 2.0522, 1.0599]])
|
| 63 |
+
```
|
| 64 |
+
|
| 65 |
+
## Evaluation
|
| 66 |
+
|
| 67 |
+
KURE-Sparse-base is evaluated on the nine **MTEB(kor, v2) Retrieval** tasks. We report nDCG@10.
|
| 68 |
+
|
| 69 |
+
| Model | Params | Avg | AutoRAG | PubHealthQA | Ko-StrategyQA | LawIRKo | SQuADKorV1 | Belebele (ko-ko) | MrTidy | MLDR | MIRACL |
|
| 70 |
+
| :--- | :--- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
|
| 71 |
+
| **Our Models** | | | | | | | | | | | |
|
| 72 |
+
| **[nlpai-lab/KURE-Sparse-base](https://huggingface.co/nlpai-lab/KURE-Sparse-base)** | 0.6B | **0.7984** | 0.9445 | 0.8099 | 0.8188 | 0.7244 | 0.9815 | 0.9703 | 0.6255 | 0.5716 | 0.7395 |
|
| 73 |
+
| **[nlpai-lab/KURE-Sparse-nano](https://huggingface.co/nlpai-lab/KURE-Sparse-nano)** | 0.1B | 0.7716 | 0.9360 | 0.7711 | 0.7924 | 0.7226 | 0.9774 | 0.9608 | 0.5618 | 0.5133 | 0.7091 |
|
| 74 |
+
| **Sparse Models** | | | | | | | | | | | |
|
| 75 |
+
| omai-research/milco-650m | 0.6B | 0.7824 | 0.9483 | 0.8219 | 0.8143 | 0.6724 | 0.9700 | 0.9623 | 0.6997 | 0.4318 | 0.7209 |
|
| 76 |
+
| telepix/PIXIE-Splade-v1.5 | 0.1B | 0.7608 | 0.9412 | 0.7910 | 0.7993 | 0.7667 | 0.9687 | 0.9651 | 0.5132 | 0.4938 | 0.6085 |
|
| 77 |
+
| yjoonjang/splade-ko-v1 | 0.1B | 0.7526 | 0.9423 | 0.7977 | 0.7583 | 0.6509 | 0.9754 | 0.9576 | 0.5299 | 0.5162 | 0.6451 |
|
| 78 |
+
| telepix/PIXIE-Splade-v1.0 | 0.1B | 0.6636 | 0.9597 | 0.7860 | 0.7352 | 0.5330 | 0.9417 | 0.9413 | 0.2909 | 0.3991 | 0.3858 |
|
| 79 |
+
| opensearch-project/opensearch-neural-sparse-encoding-multilingual-v1 | 0.2B | 0.5444 | 0.6482 | 0.5506 | 0.5553 | 0.3554 | 0.7770 | 0.7321 | 0.5526 | 0.1219 | 0.6064 |
|
| 80 |
+
| **Dense (Single-Vector) Models** | | | | | | | | | | | |
|
| 81 |
+
| nvidia/Nemotron-3-Embed-8B-BF16 | 8.0B | 0.8127 | 0.9311 | 0.8898 | 0.8405 | 0.8481 | 0.9650 | 0.9714 | 0.6540 | 0.4785 | 0.7363 |
|
| 82 |
+
| sionic-ai/comsat-embed-ko-8b-preview | 7.6B | 0.7927 | 0.8518 | 0.8871 | 0.8394 | 0.8164 | 0.9168 | 0.9853 | 0.6253 | 0.5157 | 0.6964 |
|
| 83 |
+
| Qwen/Qwen3-Embedding-8B | 7.6B | 0.7826 | 0.8276 | 0.8721 | 0.8363 | 0.8171 | 0.9063 | 0.9824 | 0.6187 | 0.5046 | 0.6783 |
|
| 84 |
+
| nvidia/Nemotron-3-Embed-1B-BF16 | 1.1B | 0.7817 | 0.9074 | 0.8642 | 0.8136 | 0.7758 | 0.9505 | 0.9629 | 0.6084 | 0.4202 | 0.7325 |
|
| 85 |
+
| Qwen/Qwen3-Embedding-4B | 4.0B | 0.7737 | 0.8431 | 0.8693 | 0.8270 | 0.7769 | 0.9044 | 0.9522 | 0.6076 | 0.5022 | 0.6803 |
|
| 86 |
+
| perplexity-ai/pplx-embed-v1-4b | 4.0B | 0.7705 | 0.8321 | 0.9042 | 0.8326 | 0.7736 | 0.9402 | 0.9624 | 0.5778 | 0.4343 | 0.6775 |
|
| 87 |
+
| microsoft/harrier-oss-v1-27b | 27.0B | 0.7667 | 0.8176 | 0.8971 | 0.8361 | 0.8737 | 0.9204 | 0.9546 | 0.5306 | 0.4046 | 0.6653 |
|
| 88 |
+
| dragonkue/snowflake-arctic-embed-l-v2.0-ko | 0.6B | 0.7653 | 0.9093 | 0.8337 | 0.8050 | 0.7735 | 0.9447 | 0.9518 | 0.5712 | 0.4304 | 0.6685 |
|
| 89 |
+
| codefuse-ai/F2LLM-v2-8B | 7.6B | 0.7638 | 0.7678 | 0.9380 | 0.8371 | 0.8405 | 0.8874 | 0.9513 | 0.6162 | 0.4047 | 0.6313 |
|
| 90 |
+
| telepix/PIXIE-Rune-v1.5 | 0.6B | 0.7618 | 0.8927 | 0.8426 | 0.8064 | 0.7705 | 0.9457 | 0.9617 | 0.5492 | 0.4482 | 0.6393 |
|
| 91 |
+
| [nlpai-lab/KURE-v1](https://huggingface.co/nlpai-lab/KURE-v1) | 0.6B | 0.7616 | 0.8708 | 0.8193 | 0.7999 | 0.7426 | 0.9357 | 0.9502 | 0.5909 | 0.4637 | 0.6816 |
|
| 92 |
+
| nvidia/llama-nemotron-embed-1b-v2 | 1.2B | 0.7564 | 0.8708 | 0.8177 | 0.8055 | 0.7329 | 0.9345 | 0.9519 | 0.5988 | 0.3952 | 0.7004 |
|
| 93 |
+
| dragonkue/BGE-m3-ko | 0.6B | 0.7547 | 0.8738 | 0.8155 | 0.7959 | 0.7322 | 0.9414 | 0.9503 | 0.6099 | 0.3899 | 0.6833 |
|
| 94 |
+
| BAAI/bge-m3 | 0.6B | 0.7509 | 0.8301 | 0.8041 | 0.7941 | 0.7174 | 0.9038 | 0.9315 | 0.6471 | 0.4287 | 0.7015 |
|
| 95 |
+
| perplexity-ai/pplx-embed-v1-0.6b | 0.6B | 0.7390 | 0.8414 | 0.8255 | 0.8100 | 0.7444 | 0.9174 | 0.9393 | 0.5524 | 0.3179 | 0.7026 |
|
| 96 |
+
| [nlpai-lab/KoE5](https://huggingface.co/nlpai-lab/KoE5) | 0.6B | 0.7337 | 0.8434 | 0.8351 | 0.8001 | 0.7756 | 0.8980 | 0.9425 | 0.5841 | 0.3015 | 0.6235 |
|
| 97 |
+
| Qwen/Qwen3-Embedding-0.6B | 0.6B | 0.7140 | 0.8338 | 0.8107 | 0.7586 | 0.7526 | 0.8490 | 0.9124 | 0.4870 | 0.4199 | 0.6023 |
|
| 98 |
+
|
| 99 |
+
### Performance-Sparsity
|
| 100 |
+
|
| 101 |
+
<p align="center">
|
| 102 |
+
<img src="assets/ndcg_flops.png" width="80%" alt="Average nDCG@10 vs. average FLOPs of sparse retrievers on MTEB(kor, v2) Retrieval">
|
| 103 |
+
</p>
|
| 104 |
+
|
| 105 |
+
Average active dimensions per vector and FLOPs, averaged over the nine tasks.
|
| 106 |
+
|
| 107 |
+
| Model | Params | Query dims | Document dims | FLOPs |
|
| 108 |
+
| :--- | :--- | ---: | ---: | ---: |
|
| 109 |
+
| **KURE-Sparse-base** | 0.6B | 100 | 1,897 | 36.5 |
|
| 110 |
+
| **KURE-Sparse-nano** | 0.1B | 147 | 843 | 23.0 |
|
| 111 |
+
| omai-research/milco-650m | 0.6B | 105 | 1,167 | 32.3 |
|
| 112 |
+
| telepix/PIXIE-Splade-v1.5 | 0.1B | 117 | 1,541 | 37.3 |
|
| 113 |
+
| yjoonjang/splade-ko-v1 | 0.1B | 109 | 742 | 29.1 |
|
| 114 |
+
| telepix/PIXIE-Splade-v1.0 | 0.1B | 121 | 625 | 22.4 |
|
| 115 |
+
| opensearch-project/opensearch-neural-sparse-encoding-multilingual-v1 | 0.2B | 29 | 176 | 6.8 |
|
| 116 |
+
|
| 117 |
+
### English Retrieval (BEIR)
|
| 118 |
+
|
| 119 |
+
KURE-Sparse-base is also evaluated on the 13 English BEIR tasks (BEIR-13) with nDCG@10. SparseUp runs at its own length limits (128 query and 512 document tokens), and the other models at up to 8,192 tokens.
|
| 120 |
+
|
| 121 |
+
| Model | Params | BEIR-13 | FLOPs |
|
| 122 |
+
| :--- | :--- | ---: | ---: |
|
| 123 |
+
| **Our Models** | | | |
|
| 124 |
+
| **KURE-Sparse-base** | 0.6B | **0.5864** | 55.8 |
|
| 125 |
+
| **KURE-Sparse-nano** | 0.1B | 0.5317 | 37.0 |
|
| 126 |
+
| **Sparse Models** | | | |
|
| 127 |
+
| Linkup-Platform/linkup-sparseup-embed-v1 | 0.2B | 0.5645 | 8.5 |
|
| 128 |
+
| omai-research/milco-650m | 0.6B | 0.5545 | 20.6 |
|
| 129 |
+
| telepix/PIXIE-Splade-v1.5 | 0.1B | 0.4149 | 125.6 |
|
| 130 |
+
|
| 131 |
+
<details>
|
| 132 |
+
<summary>Per-task nDCG@10</summary>
|
| 133 |
+
|
| 134 |
+
| Model | Avg | ArguAna | ClimateFEVER | DBPedia | FEVER | FiQA | HotpotQA | NFCorpus | NQ | Quora | SCIDOCS | SciFact | TREC-COVID | Touche-2020 |
|
| 135 |
+
| :--- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
|
| 136 |
+
| **Our Models** | | | | | | | | | | | | | | |
|
| 137 |
+
| **KURE-Sparse-base** | **0.5864** | 0.5896 | 0.3959 | 0.4837 | 0.9325 | 0.4720 | 0.8084 | 0.3678 | 0.6758 | 0.8252 | 0.1741 | 0.7183 | 0.8260 | 0.3535 |
|
| 138 |
+
| **KURE-Sparse-nano** | 0.5317 | 0.5075 | 0.2865 | 0.4214 | 0.9079 | 0.4080 | 0.7476 | 0.3340 | 0.5647 | 0.7825 | 0.1581 | 0.7120 | 0.7637 | 0.3183 |
|
| 139 |
+
| **Sparse Models** | | | | | | | | | | | | | | |
|
| 140 |
+
| Linkup-Platform/linkup-sparseup-embed-v1 | 0.5645 | 0.5648 | 0.3747 | 0.4150 | 0.8969 | 0.4687 | 0.7761 | 0.3523 | 0.5717 | 0.8656 | 0.1852 | 0.7376 | 0.7823 | 0.3478 |
|
| 141 |
+
| omai-research/milco-650m | 0.5545 | 0.6157 | 0.3079 | 0.4501 | 0.8353 | 0.4262 | 0.7780 | 0.3629 | 0.6495 | 0.8818 | 0.1704 | 0.7020 | 0.7476 | 0.2813 |
|
| 142 |
+
| telepix/PIXIE-Splade-v1.5 | 0.4149 | 0.2185 | 0.1302 | 0.3519 | 0.6475 | 0.2753 | 0.5756 | 0.3325 | 0.3858 | 0.6878 | 0.1468 | 0.6694 | 0.7068 | 0.2650 |
|
| 143 |
+
|
| 144 |
+
</details>
|
| 145 |
+
|
| 146 |
+
## Citation
|
| 147 |
+
|
| 148 |
+
```bibtex
|
| 149 |
+
@misc{kure-sparse,
|
| 150 |
+
title = {KURE-Sparse: a Korean-English bilingual learned sparse retriever},
|
| 151 |
+
author = {Jang, Youngjoon and Son, Junyoung and Lee, Taemin and Hong, Seongtae and Lim, Heuiseok},
|
| 152 |
+
year = {2026},
|
| 153 |
+
url = {https://huggingface.co/nlpai-lab/KURE-Sparse-base},
|
| 154 |
+
}
|
| 155 |
+
```
|
| 156 |
+
|
| 157 |
+
```bibtex
|
| 158 |
+
@inproceedings{jang2025kure,
|
| 159 |
+
title={KURE: Embedding Model for Korean-Specific Retrieval},
|
| 160 |
+
author={Jang, Youngjoon and Son, Junyoung and Lee, Taemin and Hong, Seongtae and Park, JeongBae and Lim, Heuiseok},
|
| 161 |
+
booktitle={Annual Conference on Human and Language Technology},
|
| 162 |
+
pages={129--134},
|
| 163 |
+
year={2025},
|
| 164 |
+
organization={Human and Language Technology}
|
| 165 |
+
}
|
| 166 |
+
```
|
| 167 |
+
|
| 168 |
+
```bibtex
|
| 169 |
+
@inproceedings{nguyen2026milco,
|
| 170 |
+
title={MILCO: Learned Sparse Retrieval Across Languages via a Multilingual Connector},
|
| 171 |
+
author={Nguyen, Thong and Lei, Yibin and Ju, Jia-Huei and Yang, Eugene and Yates, Andrew},
|
| 172 |
+
booktitle={International Conference on Learning Representations},
|
| 173 |
+
year={2026}
|
| 174 |
+
}
|
| 175 |
+
```
|
assets/ndcg_flops.png
ADDED
|
Git LFS Details
|
config.json
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"architectures": [
|
| 3 |
+
"MILCOModel"
|
| 4 |
+
],
|
| 5 |
+
"auto_map": {
|
| 6 |
+
"AutoConfig": "milco.MILCOConfig",
|
| 7 |
+
"AutoModel": "milco.MILCOModel"
|
| 8 |
+
},
|
| 9 |
+
"dtype": "bfloat16",
|
| 10 |
+
"en_hidden_size": 768,
|
| 11 |
+
"en_sub_config": {
|
| 12 |
+
"_name_or_path": "naver/splade-v3",
|
| 13 |
+
"add_cross_attention": false,
|
| 14 |
+
"architectures": [
|
| 15 |
+
"BertForMaskedLM"
|
| 16 |
+
],
|
| 17 |
+
"attention_probs_dropout_prob": 0.1,
|
| 18 |
+
"bad_words_ids": null,
|
| 19 |
+
"begin_suppress_tokens": null,
|
| 20 |
+
"bos_token_id": null,
|
| 21 |
+
"chunk_size_feed_forward": 0,
|
| 22 |
+
"classifier_dropout": null,
|
| 23 |
+
"cross_attention_hidden_size": null,
|
| 24 |
+
"decoder_start_token_id": null,
|
| 25 |
+
"diversity_penalty": 0.0,
|
| 26 |
+
"do_sample": false,
|
| 27 |
+
"dtype": "float32",
|
| 28 |
+
"early_stopping": false,
|
| 29 |
+
"encoder_no_repeat_ngram_size": 0,
|
| 30 |
+
"eos_token_id": null,
|
| 31 |
+
"exponential_decay_length_penalty": null,
|
| 32 |
+
"finetuning_task": null,
|
| 33 |
+
"forced_bos_token_id": null,
|
| 34 |
+
"forced_eos_token_id": null,
|
| 35 |
+
"gradient_checkpointing": false,
|
| 36 |
+
"hidden_act": "gelu",
|
| 37 |
+
"hidden_dropout_prob": 0.1,
|
| 38 |
+
"hidden_size": 768,
|
| 39 |
+
"id2label": {
|
| 40 |
+
"0": "LABEL_0",
|
| 41 |
+
"1": "LABEL_1"
|
| 42 |
+
},
|
| 43 |
+
"initializer_range": 0.02,
|
| 44 |
+
"intermediate_size": 3072,
|
| 45 |
+
"is_decoder": false,
|
| 46 |
+
"is_encoder_decoder": false,
|
| 47 |
+
"label2id": {
|
| 48 |
+
"LABEL_0": 0,
|
| 49 |
+
"LABEL_1": 1
|
| 50 |
+
},
|
| 51 |
+
"layer_norm_eps": 1e-12,
|
| 52 |
+
"length_penalty": 1.0,
|
| 53 |
+
"max_length": 20,
|
| 54 |
+
"max_position_embeddings": 512,
|
| 55 |
+
"min_length": 0,
|
| 56 |
+
"model_type": "bert",
|
| 57 |
+
"no_repeat_ngram_size": 0,
|
| 58 |
+
"num_attention_heads": 12,
|
| 59 |
+
"num_beam_groups": 1,
|
| 60 |
+
"num_beams": 1,
|
| 61 |
+
"num_hidden_layers": 12,
|
| 62 |
+
"num_return_sequences": 1,
|
| 63 |
+
"output_attentions": false,
|
| 64 |
+
"output_hidden_states": false,
|
| 65 |
+
"output_scores": false,
|
| 66 |
+
"pad_token_id": 0,
|
| 67 |
+
"position_embedding_type": "absolute",
|
| 68 |
+
"prefix": null,
|
| 69 |
+
"problem_type": null,
|
| 70 |
+
"pruned_heads": {},
|
| 71 |
+
"remove_invalid_values": false,
|
| 72 |
+
"repetition_penalty": 1.0,
|
| 73 |
+
"return_dict": true,
|
| 74 |
+
"return_dict_in_generate": false,
|
| 75 |
+
"sep_token_id": null,
|
| 76 |
+
"suppress_tokens": null,
|
| 77 |
+
"task_specific_params": null,
|
| 78 |
+
"temperature": 1.0,
|
| 79 |
+
"tf_legacy_loss": false,
|
| 80 |
+
"tie_encoder_decoder": false,
|
| 81 |
+
"tie_word_embeddings": true,
|
| 82 |
+
"tokenizer_class": null,
|
| 83 |
+
"top_k": 50,
|
| 84 |
+
"top_p": 1.0,
|
| 85 |
+
"torchscript": false,
|
| 86 |
+
"transformers_version": "4.57.3",
|
| 87 |
+
"type_vocab_size": 2,
|
| 88 |
+
"typical_p": 1.0,
|
| 89 |
+
"use_bfloat16": false,
|
| 90 |
+
"use_cache": true,
|
| 91 |
+
"vocab_size": 30522
|
| 92 |
+
},
|
| 93 |
+
"en_vocab_size": 30522,
|
| 94 |
+
"freeze_embeddings": false,
|
| 95 |
+
"lambda_commitment": 0.0,
|
| 96 |
+
"lambda_d": 1e-05,
|
| 97 |
+
"lambda_q": 0.001,
|
| 98 |
+
"lambda_sparsity": 0.0,
|
| 99 |
+
"lsr_encoder_checkpoint": "naver/splade-v3",
|
| 100 |
+
"m_hidden_size": 1024,
|
| 101 |
+
"m_sub_config": {
|
| 102 |
+
"_name_or_path": "BAAI/bge-m3-unsupervised",
|
| 103 |
+
"add_cross_attention": false,
|
| 104 |
+
"architectures": [
|
| 105 |
+
"XLMRobertaModel"
|
| 106 |
+
],
|
| 107 |
+
"attention_probs_dropout_prob": 0.1,
|
| 108 |
+
"bad_words_ids": null,
|
| 109 |
+
"begin_suppress_tokens": null,
|
| 110 |
+
"bos_token_id": 0,
|
| 111 |
+
"chunk_size_feed_forward": 0,
|
| 112 |
+
"classifier_dropout": null,
|
| 113 |
+
"cross_attention_hidden_size": null,
|
| 114 |
+
"decoder_start_token_id": null,
|
| 115 |
+
"diversity_penalty": 0.0,
|
| 116 |
+
"do_sample": false,
|
| 117 |
+
"dtype": "float32",
|
| 118 |
+
"early_stopping": false,
|
| 119 |
+
"encoder_no_repeat_ngram_size": 0,
|
| 120 |
+
"eos_token_id": 2,
|
| 121 |
+
"exponential_decay_length_penalty": null,
|
| 122 |
+
"finetuning_task": null,
|
| 123 |
+
"forced_bos_token_id": null,
|
| 124 |
+
"forced_eos_token_id": null,
|
| 125 |
+
"hidden_act": "gelu",
|
| 126 |
+
"hidden_dropout_prob": 0.1,
|
| 127 |
+
"hidden_size": 1024,
|
| 128 |
+
"id2label": {
|
| 129 |
+
"0": "LABEL_0",
|
| 130 |
+
"1": "LABEL_1"
|
| 131 |
+
},
|
| 132 |
+
"initializer_range": 0.02,
|
| 133 |
+
"intermediate_size": 4096,
|
| 134 |
+
"is_decoder": false,
|
| 135 |
+
"is_encoder_decoder": false,
|
| 136 |
+
"label2id": {
|
| 137 |
+
"LABEL_0": 0,
|
| 138 |
+
"LABEL_1": 1
|
| 139 |
+
},
|
| 140 |
+
"layer_norm_eps": 1e-05,
|
| 141 |
+
"length_penalty": 1.0,
|
| 142 |
+
"max_length": 20,
|
| 143 |
+
"max_position_embeddings": 8194,
|
| 144 |
+
"min_length": 0,
|
| 145 |
+
"model_type": "xlm-roberta",
|
| 146 |
+
"no_repeat_ngram_size": 0,
|
| 147 |
+
"num_attention_heads": 16,
|
| 148 |
+
"num_beam_groups": 1,
|
| 149 |
+
"num_beams": 1,
|
| 150 |
+
"num_hidden_layers": 24,
|
| 151 |
+
"num_return_sequences": 1,
|
| 152 |
+
"output_attentions": false,
|
| 153 |
+
"output_hidden_states": false,
|
| 154 |
+
"output_past": true,
|
| 155 |
+
"output_scores": false,
|
| 156 |
+
"pad_token_id": 1,
|
| 157 |
+
"position_embedding_type": "absolute",
|
| 158 |
+
"prefix": null,
|
| 159 |
+
"problem_type": null,
|
| 160 |
+
"pruned_heads": {},
|
| 161 |
+
"remove_invalid_values": false,
|
| 162 |
+
"repetition_penalty": 1.0,
|
| 163 |
+
"return_dict": true,
|
| 164 |
+
"return_dict_in_generate": false,
|
| 165 |
+
"sep_token_id": null,
|
| 166 |
+
"suppress_tokens": null,
|
| 167 |
+
"task_specific_params": null,
|
| 168 |
+
"temperature": 1.0,
|
| 169 |
+
"tf_legacy_loss": false,
|
| 170 |
+
"tie_encoder_decoder": false,
|
| 171 |
+
"tie_word_embeddings": true,
|
| 172 |
+
"tokenizer_class": null,
|
| 173 |
+
"top_k": 50,
|
| 174 |
+
"top_p": 1.0,
|
| 175 |
+
"torchscript": false,
|
| 176 |
+
"transformers_version": "4.57.3",
|
| 177 |
+
"type_vocab_size": 1,
|
| 178 |
+
"typical_p": 1.0,
|
| 179 |
+
"use_bfloat16": false,
|
| 180 |
+
"use_cache": true,
|
| 181 |
+
"vocab_size": 250002
|
| 182 |
+
},
|
| 183 |
+
"m_vocab_size": 250002,
|
| 184 |
+
"model_type": "milco",
|
| 185 |
+
"model_variant": "bert",
|
| 186 |
+
"multilingual_encoder_checkpoint": "BAAI/bge-m3-unsupervised",
|
| 187 |
+
"only_distillation": true,
|
| 188 |
+
"pooling": "2d",
|
| 189 |
+
"projector_pretraining": false,
|
| 190 |
+
"prompt_length": 0,
|
| 191 |
+
"separate_mlp": true,
|
| 192 |
+
"softmax_temperature": 1.0,
|
| 193 |
+
"sparse_reg": "l1",
|
| 194 |
+
"transformers_version": "5.14.1",
|
| 195 |
+
"use_2d_mlp": true,
|
| 196 |
+
"use_cross_device_negatives": false
|
| 197 |
+
}
|
config_sentence_transformers.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"__version__": {
|
| 3 |
+
"pytorch": "2.8.0+cu128",
|
| 4 |
+
"sentence_transformers": "5.6.1",
|
| 5 |
+
"transformers": "5.14.1"
|
| 6 |
+
},
|
| 7 |
+
"default_prompt_name": null,
|
| 8 |
+
"model_type": "SparseEncoder",
|
| 9 |
+
"prompts": {
|
| 10 |
+
"document": "",
|
| 11 |
+
"query": ""
|
| 12 |
+
},
|
| 13 |
+
"similarity_fn_name": "dot"
|
| 14 |
+
}
|
milco.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import torch.nn as nn
|
| 3 |
+
from transformers import (
|
| 4 |
+
AutoConfig,
|
| 5 |
+
AutoModel,
|
| 6 |
+
AutoModelForMaskedLM,
|
| 7 |
+
AutoTokenizer,
|
| 8 |
+
PretrainedConfig,
|
| 9 |
+
PreTrainedModel,
|
| 10 |
+
)
|
| 11 |
+
from transformers.activations import GELUActivation
|
| 12 |
+
from transformers.utils import logging
|
| 13 |
+
|
| 14 |
+
logger = logging.get_logger(__name__)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class MILCOConfig(PretrainedConfig):
|
| 18 |
+
model_type = "milco"
|
| 19 |
+
|
| 20 |
+
def __init__(self, **kwargs):
|
| 21 |
+
super().__init__(**kwargs)
|
| 22 |
+
self.lsr_encoder_checkpoint = kwargs.get("lsr_encoder_checkpoint", "bert-base-uncased")
|
| 23 |
+
self.multilingual_encoder_checkpoint = kwargs.get("multilingual_encoder_checkpoint", "bert-base-uncased")
|
| 24 |
+
self.model_variant = kwargs.get("model_variant", "bert")
|
| 25 |
+
# Sub-model dimensions — cached in config.json after first save.
|
| 26 |
+
self.en_vocab_size = kwargs.get("en_vocab_size", None)
|
| 27 |
+
self.en_hidden_size = kwargs.get("en_hidden_size", None)
|
| 28 |
+
self.m_vocab_size = kwargs.get("m_vocab_size", None)
|
| 29 |
+
self.m_hidden_size = kwargs.get("m_hidden_size", None)
|
| 30 |
+
# Sub-model config dicts — stored so __init__ can rebuild architecture offline.
|
| 31 |
+
self.en_sub_config = kwargs.get("en_sub_config", None)
|
| 32 |
+
self.m_sub_config = kwargs.get("m_sub_config", None)
|
| 33 |
+
|
| 34 |
+
def _resolve_sub_configs(self):
|
| 35 |
+
"""Fetch sub-model configs from HF hub if not already cached."""
|
| 36 |
+
if self.en_sub_config is None:
|
| 37 |
+
en_config = AutoConfig.from_pretrained(self.lsr_encoder_checkpoint, trust_remote_code=True)
|
| 38 |
+
self.en_vocab_size = en_config.vocab_size
|
| 39 |
+
self.en_hidden_size = en_config.hidden_size
|
| 40 |
+
self.en_sub_config = en_config.to_dict()
|
| 41 |
+
if self.m_sub_config is None:
|
| 42 |
+
m_config = AutoConfig.from_pretrained(self.multilingual_encoder_checkpoint, trust_remote_code=True)
|
| 43 |
+
self.m_vocab_size = m_config.vocab_size
|
| 44 |
+
self.m_hidden_size = m_config.hidden_size
|
| 45 |
+
self.m_sub_config = m_config.to_dict()
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
class MILCOModel(PreTrainedModel):
|
| 49 |
+
config_class = MILCOConfig
|
| 50 |
+
base_model_prefix = "milco"
|
| 51 |
+
|
| 52 |
+
def __init__(self, config: MILCOConfig):
|
| 53 |
+
super().__init__(config)
|
| 54 |
+
config._resolve_sub_configs()
|
| 55 |
+
# Build architecture from cached configs — no pretrained weight downloads.
|
| 56 |
+
m_sub_config = AutoConfig.for_model(**config.m_sub_config)
|
| 57 |
+
self.m_model = AutoModel.from_config(m_sub_config)
|
| 58 |
+
en_sub_config = AutoConfig.for_model(**config.en_sub_config)
|
| 59 |
+
en_lsr = AutoModelForMaskedLM.from_config(en_sub_config)
|
| 60 |
+
if hasattr(en_lsr, "cls"):
|
| 61 |
+
self.mlm_head = en_lsr.cls
|
| 62 |
+
else:
|
| 63 |
+
self.mlm_head = en_lsr.lm_head
|
| 64 |
+
self.projector = nn.Linear(config.m_hidden_size, config.en_hidden_size)
|
| 65 |
+
self.activation = GELUActivation()
|
| 66 |
+
self.echo = nn.Linear(config.m_hidden_size, 1)
|
| 67 |
+
self.scale = nn.Parameter(torch.tensor([1.0]))
|
| 68 |
+
self.post_init()
|
| 69 |
+
|
| 70 |
+
@property
|
| 71 |
+
def en_tokenizer(self):
|
| 72 |
+
if not hasattr(self, "_en_tokenizer"):
|
| 73 |
+
self._en_tokenizer = AutoTokenizer.from_pretrained(
|
| 74 |
+
self.config.lsr_encoder_checkpoint, trust_remote_code=True,
|
| 75 |
+
)
|
| 76 |
+
return self._en_tokenizer
|
| 77 |
+
|
| 78 |
+
@property
|
| 79 |
+
def m_tokenizer(self):
|
| 80 |
+
if not hasattr(self, "_m_tokenizer"):
|
| 81 |
+
self._m_tokenizer = AutoTokenizer.from_pretrained(
|
| 82 |
+
self.config.multilingual_encoder_checkpoint, trust_remote_code=True,
|
| 83 |
+
)
|
| 84 |
+
return self._m_tokenizer
|
| 85 |
+
|
| 86 |
+
def get_vocab(self):
|
| 87 |
+
en_term2id = self.en_tokenizer.vocab
|
| 88 |
+
en_id2term = {idx: f"e_{term}" for term, idx in en_term2id.items()}
|
| 89 |
+
m_term2id = self.m_tokenizer.vocab
|
| 90 |
+
m_id2term = {idx + self.config.en_vocab_size: f"m_{term}" for term, idx in m_term2id.items()}
|
| 91 |
+
return en_id2term | m_id2term
|
| 92 |
+
|
| 93 |
+
def encode_query(self, *args, **kwargs):
|
| 94 |
+
return self.encode_text(*args, **kwargs)
|
| 95 |
+
|
| 96 |
+
def encode_document(self, *args, **kwargs):
|
| 97 |
+
return self.encode_text(*args, **kwargs)
|
| 98 |
+
|
| 99 |
+
def encode_text(self, texts, batch_size: int = 32, max_length: int = None, source_view=False, return_dict=False, **kwargs):
|
| 100 |
+
if isinstance(texts, str):
|
| 101 |
+
texts = [texts]
|
| 102 |
+
if max_length is None:
|
| 103 |
+
max_length = self.m_tokenizer.model_max_length
|
| 104 |
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 105 |
+
self.to(device)
|
| 106 |
+
reps = []
|
| 107 |
+
for idx in range(0, len(texts), batch_size):
|
| 108 |
+
batch_texts = texts[idx:idx + batch_size]
|
| 109 |
+
inputs = self.m_tokenizer(
|
| 110 |
+
batch_texts, padding=True, truncation=True,
|
| 111 |
+
pad_to_multiple_of=32, max_length=max_length, return_tensors="pt",
|
| 112 |
+
)
|
| 113 |
+
inputs = {k: v.to(device) for k, v in inputs.items()}
|
| 114 |
+
with torch.no_grad(), torch.autocast(device_type=device, dtype=torch.bfloat16):
|
| 115 |
+
output = self._encode(
|
| 116 |
+
input_ids=inputs["input_ids"],
|
| 117 |
+
attention_mask=inputs["attention_mask"],
|
| 118 |
+
source_view=source_view,
|
| 119 |
+
)
|
| 120 |
+
if isinstance(output, tuple):
|
| 121 |
+
sparse_rep = self._build_dual_view_sparse(output)
|
| 122 |
+
else:
|
| 123 |
+
sparse_rep = output.to_sparse()
|
| 124 |
+
reps.append(sparse_rep.cpu())
|
| 125 |
+
combined = sparse_row_cat_2d(reps)
|
| 126 |
+
assert len(texts) == combined.size(0)
|
| 127 |
+
if return_dict:
|
| 128 |
+
return self._sparse_to_dicts(combined)
|
| 129 |
+
return combined
|
| 130 |
+
|
| 131 |
+
def _sparse_to_dicts(self, sparse_tensor):
|
| 132 |
+
"""Convert a sparse [N, vocab] tensor to a list of {token: weight} dicts."""
|
| 133 |
+
id2term = self.get_vocab()
|
| 134 |
+
sparse_tensor = sparse_tensor.coalesce()
|
| 135 |
+
indices = sparse_tensor.indices()
|
| 136 |
+
values = sparse_tensor.values()
|
| 137 |
+
rows, cols = indices[0], indices[1]
|
| 138 |
+
result = [[] for _ in range(sparse_tensor.size(0))]
|
| 139 |
+
for i in range(len(values)):
|
| 140 |
+
row = rows[i].item()
|
| 141 |
+
col = cols[i].item()
|
| 142 |
+
weight = values[i].item()
|
| 143 |
+
token = id2term.get(col, f"unk_{col}")
|
| 144 |
+
result[row].append((token, weight))
|
| 145 |
+
return [
|
| 146 |
+
dict(sorted(pairs, key=lambda x: x[1], reverse=True))
|
| 147 |
+
for pairs in result
|
| 148 |
+
]
|
| 149 |
+
|
| 150 |
+
def _build_dual_view_sparse(self, output):
|
| 151 |
+
pivot_view, (source_ids, source_weights) = output
|
| 152 |
+
batch_size = source_ids.size(0)
|
| 153 |
+
vocab_size = self.config.en_vocab_size + self.config.m_vocab_size
|
| 154 |
+
|
| 155 |
+
pivot_rows, pivot_cols = pivot_view.nonzero(as_tuple=True)
|
| 156 |
+
pivot_vals = pivot_view[pivot_rows, pivot_cols].contiguous()
|
| 157 |
+
|
| 158 |
+
source_rows = torch.arange(batch_size, device=source_ids.device).repeat_interleave(source_ids.size(1))
|
| 159 |
+
source_cols = source_ids.view(-1)
|
| 160 |
+
source_vals = source_weights.view(-1)
|
| 161 |
+
nonzero_mask = source_vals > 0
|
| 162 |
+
source_rows = source_rows[nonzero_mask]
|
| 163 |
+
source_cols = source_cols[nonzero_mask] + self.config.en_vocab_size
|
| 164 |
+
source_vals = source_vals[nonzero_mask]
|
| 165 |
+
|
| 166 |
+
rows = torch.cat([pivot_rows, source_rows])
|
| 167 |
+
cols = torch.cat([pivot_cols, source_cols])
|
| 168 |
+
indices = torch.stack([rows, cols], dim=0)
|
| 169 |
+
values = torch.cat([pivot_vals, source_vals])
|
| 170 |
+
return torch.sparse_coo_tensor(indices, values, (batch_size, vocab_size))
|
| 171 |
+
|
| 172 |
+
def _encode(self, input_ids, attention_mask, source_view=False):
|
| 173 |
+
hidden_states_m = self.m_model(input_ids=input_ids, attention_mask=attention_mask).last_hidden_state
|
| 174 |
+
hidden_states_e = self.activation(self.projector(hidden_states_m))
|
| 175 |
+
logits = self.mlm_head(hidden_states_e) * attention_mask.unsqueeze(-1)
|
| 176 |
+
pivot_view = torch.log1p(torch.relu(logits.max(dim=1).values))
|
| 177 |
+
if source_view:
|
| 178 |
+
token_scores = torch.relu(self.echo(hidden_states_m).squeeze(-1))
|
| 179 |
+
source_weights = token_scores * attention_mask
|
| 180 |
+
return pivot_view, (input_ids, source_weights)
|
| 181 |
+
return pivot_view
|
| 182 |
+
|
| 183 |
+
def forward(self, *args, **kwargs):
|
| 184 |
+
return self._encode(*args, **kwargs)
|
| 185 |
+
|
| 186 |
+
def save_pretrained(self, save_directory: str, **kwargs):
|
| 187 |
+
kwargs["safe_serialization"] = False
|
| 188 |
+
super().save_pretrained(save_directory, **kwargs)
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
def sparse_row_cat_2d(tensors):
|
| 192 |
+
"""Concatenate a list of 2-D COO sparse tensors along rows (dim=0)."""
|
| 193 |
+
assert len(tensors) > 0, "empty list"
|
| 194 |
+
if len(tensors) == 1:
|
| 195 |
+
return tensors[0]
|
| 196 |
+
tensors = [t.coalesce() for t in tensors]
|
| 197 |
+
assert all(t.layout == torch.sparse_coo for t in tensors), "COO only"
|
| 198 |
+
|
| 199 |
+
ncols = tensors[0].size(1)
|
| 200 |
+
assert all(t.dim() == 2 and t.size(1) == ncols for t in tensors), "size(1) mismatch"
|
| 201 |
+
|
| 202 |
+
total_rows = sum(t.size(0) for t in tensors)
|
| 203 |
+
device = tensors[0].device
|
| 204 |
+
|
| 205 |
+
ind_parts, val_parts = [], []
|
| 206 |
+
row_offset = 0
|
| 207 |
+
for t in tensors:
|
| 208 |
+
idx = t._indices()
|
| 209 |
+
if row_offset:
|
| 210 |
+
idx = idx.clone()
|
| 211 |
+
idx[0] += row_offset
|
| 212 |
+
ind_parts.append(idx)
|
| 213 |
+
val_parts.append(t._values())
|
| 214 |
+
row_offset += t.size(0)
|
| 215 |
+
|
| 216 |
+
indices = torch.cat(ind_parts, dim=1)
|
| 217 |
+
values = torch.cat(val_parts, dim=0)
|
| 218 |
+
return torch.sparse_coo_tensor(indices, values, (total_rows, ncols), device=device).coalesce()
|
milco_encoder.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""MILCO (arXiv:2510.00671) as a sentence-transformers input module, so the SPARSE pipeline trains,
|
| 2 |
+
saves and evaluates it like any SparseEncoder.
|
| 3 |
+
|
| 4 |
+
MILCO is a bge-m3 body (XLM-R large) -> projector -> splade-v3's MLM head over the English BERT
|
| 5 |
+
vocabulary (the pivot view, 30522 dims), plus LexEcho: one relu score per input token, placed on that
|
| 6 |
+
token's XLM-R id (the source view, 250002 dims). Its native API returns the two as one sparse
|
| 7 |
+
[N, 30522 + 250002] tensor scored by a dot product; this module emits the same vector dense, as
|
| 8 |
+
``sentence_embedding``, so every loss and regularizer here applies to both views unchanged.
|
| 9 |
+
|
| 10 |
+
The source view adds up the scores of a repeated token: MILCO builds its COO tensor with one entry per
|
| 11 |
+
position and coalescing sums duplicates, so this uses scatter_add. Special tokens (<s>, </s>) are
|
| 12 |
+
attended and scored exactly as MILCO does.
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
from __future__ import annotations
|
| 16 |
+
|
| 17 |
+
import inspect
|
| 18 |
+
import shutil
|
| 19 |
+
from pathlib import Path
|
| 20 |
+
|
| 21 |
+
import torch
|
| 22 |
+
from sentence_transformers.base.modules.input_module import InputModule
|
| 23 |
+
from transformers import AutoModel, AutoTokenizer
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def is_milco(model_name_or_path: str) -> bool:
|
| 27 |
+
config = Path(model_name_or_path) / "config.json"
|
| 28 |
+
return config.is_file() and '"model_type": "milco"' in config.read_text()
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class MilcoDualView(InputModule):
|
| 32 |
+
config_file_name = "milco_encoder_config.json" # config.json is MILCO's own
|
| 33 |
+
config_keys = ["max_seq_length", "source_view", "chunk_size"]
|
| 34 |
+
|
| 35 |
+
def __init__(
|
| 36 |
+
self,
|
| 37 |
+
model_name_or_path: str,
|
| 38 |
+
max_seq_length: int = 8192,
|
| 39 |
+
source_view: bool = True,
|
| 40 |
+
chunk_size: int = 512,
|
| 41 |
+
model_kwargs: dict | None = None,
|
| 42 |
+
) -> None:
|
| 43 |
+
super().__init__()
|
| 44 |
+
model_kwargs = dict(model_kwargs or {})
|
| 45 |
+
# MILCOModel itself declares no attention backends; only its XLM-R body has them.
|
| 46 |
+
attn = model_kwargs.pop("attn_implementation", None)
|
| 47 |
+
# A dtype would reach only MILCOModel's own layers: its sub-models are built with from_config and keep
|
| 48 |
+
# their config's float32, so bf16 projector/echo would sit next to an fp32 body -- and at lr 1e-5 a bf16
|
| 49 |
+
# weight of typical size rounds every AdamW step away. Keep fp32 master weights; training runs under
|
| 50 |
+
# --bf16 autocast, and eval.py casts the whole model to bf16 itself.
|
| 51 |
+
model_kwargs.pop("torch_dtype", None)
|
| 52 |
+
model_kwargs.pop("dtype", None)
|
| 53 |
+
self.milco = AutoModel.from_pretrained(model_name_or_path, trust_remote_code=True, **model_kwargs).float()
|
| 54 |
+
if attn:
|
| 55 |
+
self.milco.m_model.set_attn_implementation(attn)
|
| 56 |
+
# BertLMPredictionHead.bias is never read in the forward (the decoder carries its own bias), but
|
| 57 |
+
# MILCO's checkpoint stores the two on one storage, which save_pretrained refuses as untied
|
| 58 |
+
# shared tensors. Give the unused one its own copy.
|
| 59 |
+
head = self.milco.mlm_head.predictions
|
| 60 |
+
head.bias = torch.nn.Parameter(head.decoder.bias.detach().clone())
|
| 61 |
+
# Never used in the forward: frozen, so they neither take optimizer state nor stall a DDP built
|
| 62 |
+
# with find_unused_parameters=False.
|
| 63 |
+
for param in [*self.milco.m_model.pooler.parameters(), self.milco.scale, head.bias]:
|
| 64 |
+
param.requires_grad_(False)
|
| 65 |
+
# The XLM-R body supports checkpointing; the wrapper class just does not say so.
|
| 66 |
+
self.milco.supports_gradient_checkpointing = True
|
| 67 |
+
has_tokenizer = (Path(model_name_or_path) / "tokenizer_config.json").is_file()
|
| 68 |
+
# trust_remote_code: a saved checkpoint's config.json maps to MILCO's code, and AutoTokenizer reads it.
|
| 69 |
+
self.tokenizer = AutoTokenizer.from_pretrained(
|
| 70 |
+
model_name_or_path if has_tokenizer else self.milco.config.multilingual_encoder_checkpoint,
|
| 71 |
+
trust_remote_code=True,
|
| 72 |
+
)
|
| 73 |
+
self.max_seq_length = max_seq_length
|
| 74 |
+
self.source_view = source_view
|
| 75 |
+
self.chunk_size = chunk_size
|
| 76 |
+
|
| 77 |
+
def preprocess(self, inputs, prompt: str | None = None, **kwargs) -> dict[str, torch.Tensor]:
|
| 78 |
+
texts = self._prepend_prompt(list(inputs), prompt) if prompt else list(inputs)
|
| 79 |
+
return self.tokenizer(
|
| 80 |
+
texts, padding=True, truncation=True, max_length=self.max_seq_length, return_tensors="pt"
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
def forward(self, features: dict[str, torch.Tensor], **kwargs) -> dict[str, torch.Tensor]:
|
| 84 |
+
# MILCOModel._encode, with the MLM head run over position chunks: it materializes
|
| 85 |
+
# [batch, seq, 30522] logits at once, ~16 GB for an evaluation batch of 32 at 8192 tokens.
|
| 86 |
+
# Max over chunks of the max is the same max, and log1p(relu(.)) is monotone, so the
|
| 87 |
+
# result is unchanged.
|
| 88 |
+
milco = self.milco
|
| 89 |
+
input_ids, attention_mask = features["input_ids"], features["attention_mask"]
|
| 90 |
+
hidden = milco.m_model(input_ids=input_ids, attention_mask=attention_mask).last_hidden_state
|
| 91 |
+
mask = attention_mask.unsqueeze(-1)
|
| 92 |
+
pivot = None
|
| 93 |
+
for i in range(0, hidden.shape[1], self.chunk_size):
|
| 94 |
+
logits = milco.mlm_head(milco.activation(milco.projector(hidden[:, i : i + self.chunk_size])))
|
| 95 |
+
chunk_max = (logits * mask[:, i : i + self.chunk_size]).max(dim=1).values
|
| 96 |
+
pivot = chunk_max if pivot is None else torch.maximum(pivot, chunk_max)
|
| 97 |
+
pivot = torch.log1p(torch.relu(pivot)).float()
|
| 98 |
+
if not self.source_view:
|
| 99 |
+
features["sentence_embedding"] = pivot
|
| 100 |
+
return features
|
| 101 |
+
weights = torch.relu(milco.echo(hidden).squeeze(-1)) * attention_mask
|
| 102 |
+
source = pivot.new_zeros((input_ids.shape[0], self.milco.config.m_vocab_size), dtype=torch.float32)
|
| 103 |
+
source = source.scatter_add(1, input_ids, weights.float())
|
| 104 |
+
features["sentence_embedding"] = torch.cat([pivot, source], dim=1)
|
| 105 |
+
return features
|
| 106 |
+
|
| 107 |
+
def get_sentence_embedding_dimension(self) -> int:
|
| 108 |
+
config = self.milco.config
|
| 109 |
+
return config.en_vocab_size + (config.m_vocab_size if self.source_view else 0)
|
| 110 |
+
|
| 111 |
+
def save(self, output_path: str, *args, safe_serialization: bool = True, **kwargs) -> None:
|
| 112 |
+
# Keep the unused bias equal to the trained one, in case a later transformers re-ties the two on load.
|
| 113 |
+
head = self.milco.mlm_head.predictions
|
| 114 |
+
head.bias.data.copy_(head.decoder.bias.data)
|
| 115 |
+
self.milco.save_pretrained(output_path)
|
| 116 |
+
# MILCO's remote code, for AutoModel(trust_remote_code=True) on the saved directory.
|
| 117 |
+
shutil.copyfile(inspect.getfile(type(self.milco)), Path(output_path) / "milco.py")
|
| 118 |
+
self.tokenizer.save_pretrained(output_path)
|
| 119 |
+
self.save_config(output_path)
|
| 120 |
+
# modules.json names this class as milco_encoder.MilcoDualView; ship the source so the
|
| 121 |
+
# checkpoint loads where SPARSE/src is not on sys.path (as pooling.py does).
|
| 122 |
+
shutil.copyfile(Path(__file__), Path(output_path) / Path(__file__).name)
|
| 123 |
+
|
| 124 |
+
@classmethod
|
| 125 |
+
def load(cls, model_name_or_path: str, subfolder: str = "", model_kwargs: dict | None = None, **kwargs):
|
| 126 |
+
config = cls.load_config(model_name_or_path, subfolder=subfolder)
|
| 127 |
+
path = str(Path(model_name_or_path) / subfolder) if subfolder else model_name_or_path
|
| 128 |
+
return cls(path, model_kwargs=model_kwargs, **config)
|
milco_encoder_config.json
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"max_seq_length": 8192,
|
| 3 |
+
"source_view": true,
|
| 4 |
+
"chunk_size": 512
|
| 5 |
+
}
|
model.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:57c97e07b025c4a0b4a61ec2e2c12bfbbabd25e2e7cad9d854dc31e64dd852cc
|
| 3 |
+
size 1185323644
|
modules.json
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"idx": 0,
|
| 4 |
+
"name": "0",
|
| 5 |
+
"path": "",
|
| 6 |
+
"type": "milco_encoder.MilcoDualView"
|
| 7 |
+
}
|
| 8 |
+
]
|
tokenizer.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:326c392f5799a1a73fb5118bac1e78bfc02ad3ca7f04c002cda9915721218c7d
|
| 3 |
+
size 17098339
|
tokenizer_config.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"add_prefix_space": true,
|
| 3 |
+
"backend": "tokenizers",
|
| 4 |
+
"bos_token": "<s>",
|
| 5 |
+
"clean_up_tokenization_spaces": true,
|
| 6 |
+
"cls_token": "<s>",
|
| 7 |
+
"eos_token": "</s>",
|
| 8 |
+
"is_local": true,
|
| 9 |
+
"local_files_only": false,
|
| 10 |
+
"mask_token": "<mask>",
|
| 11 |
+
"max_length": 8192,
|
| 12 |
+
"model_max_length": 8192,
|
| 13 |
+
"pad_to_multiple_of": null,
|
| 14 |
+
"pad_token": "<pad>",
|
| 15 |
+
"pad_token_type_id": 0,
|
| 16 |
+
"padding_side": "right",
|
| 17 |
+
"sep_token": "</s>",
|
| 18 |
+
"sp_model_kwargs": {},
|
| 19 |
+
"stride": 0,
|
| 20 |
+
"tokenizer_class": "XLMRobertaTokenizer",
|
| 21 |
+
"truncation_side": "right",
|
| 22 |
+
"truncation_strategy": "longest_first",
|
| 23 |
+
"unk_token": "<unk>"
|
| 24 |
+
}
|