Views
No views yet
SparseEncoder(
(0): MLMTransformer({'max_seq_length': 8192, 'do_lower_case': False, 'architecture': 'ModernBertForMaskedLM'})
(1): SpladePooling({'pooling_strategy': 'max', 'activation_function': 'relu', 'word_embedding_dimension': 50000})
)pip install -U sentence-transformers1from sentence_transformers import SparseEncoder
2
3# Download from the 🤗 Hub
4model = SparseEncoder("yjoonjang/splade-ko-v1")
5# Run inference
6sentences = [
7 '양이온 최적화 방법은 산소공공을 감소시키기 때문에 전자 농도가 증가하는 문제점을 갖고있을까?',
8 '산화물 TFT 소자 신뢰성 열화기구\n그러나 이와 같은 양이온 최적화 방법은 산소공공을 감소시키기 때문에 전자농도 역시 감소하게 되어 전계 이동도가 감소하는 문제점을 않고 있다.\n이는 산화물 반도체의 전도기구가 Percolation Conduction에 따르기 때문이다. ',
9 '세포대사 기능 분석을 위한 광학센서 기반 용존산소와 pH 측정 시스템의 제작 및 특성 분석\n수소이온 농도가 증가하는 경우인 \\( \\mathrm{pH} \\) 가 낮아지면 다수의 수소이온들과 충돌한 방출 광이 에너지를 잃고 짧은 검출시간을 갖는다. \n반대로 \\( \\mathrm{pH} \\)가 높아질수록 형광물질로부터 방출된 광의 수명이 길어져 긴 검출시간을 가진다.',
10]
11embeddings = model.encode(sentences)
12print(embeddings.shape)
13# [3, 50000]
14
15# Get the similarity scores for the embeddings
16similarities = model.similarity(embeddings, embeddings)
17print(similarities)
18# tensor([[ 46.0239, 57.8961, 22.8014],
19# [ 57.8961, 270.6235, 56.5666],
20# [ 22.8014, 56.5666, 275.8828]], device='cuda:0')| Dataset | Description | Average Length (characters) |
|---|---|---|
| Ko-StrategyQA | Korean ODQA multi-hop retrieval dataset (translated from StrategyQA) | 305.15 |
| AutoRAGRetrieval | Korean document retrieval dataset constructed by parsing PDFs across 5 domains: finance, public sector, healthcare, legal, and commerce | 823.60 |
| MIRACLRetrieval | Wikipedia-based Korean document retrieval dataset | 166.63 |
| PublicHealthQA | Korean document retrieval dataset for medical and public health domains | 339.00 |
| BelebeleRetrieval | FLORES-200-based Korean document retrieval dataset | 243.11 |
| MrTidyRetrieval | Wikipedia-based Korean document retrieval dataset | 166.90 |
| MultiLongDocRetrieval | Korean long document retrieval dataset across various domains | 13,813.44 |
1{
2 "query": "Is it unopened?",
3 "document": "No. It is a renewed product."
4},
5{
6 "query": "Is it compatible with iPad Air 3?",
7 "document": "Yes, it is possible."
8}1from sentence_transformers import SparseEncoder
2from datasets import load_dataset
3from sentence_transformers.sparse_encoder.evaluation import SparseInformationRetrievalEvaluator
4import os
5import pandas as pd
6from tqdm import tqdm
7import json
8from multiprocessing import Process, current_process
9import torch
10from setproctitle import setproctitle
11import traceback
12
13# GPU별로 평가할 데이터셋 매핑
14DATASET_GPU_MAPPING = {
15 0: [
16 "yjoonjang/markers_bm",
17 "taeminlee/Ko-StrategyQA",
18 "facebook/belebele",
19 "xhluca/publichealth-qa",
20 "Shitao/MLDR"
21 ],
22 1: [
23 "miracl/mmteb-miracl",
24 ],
25 2: [
26 "mteb/mrtidy",
27 ]
28}
29
30model_name = "yjoonjang/splade-ko-v1"
31
32def evaluate_dataset(model_name, gpu_id, eval_datasets, output_dir):
33 output_dir = f"{output_dir}/{model_name}"
34 os.makedirs(output_dir, exist_ok=True)
35
36 """단일 GPU에서 할당된 데이터셋들을 평가하는 함수"""
37 import torch
38 try:
39 device = torch.device(f"cuda:{str(gpu_id)}")
40 torch.cuda.set_device(device)
41
42 setproctitle(f"yjoonjang splade-eval-gpu{gpu_id}")
43 print(f"Running datasets: {eval_datasets} on GPU {gpu_id} in process {current_process().name}")
44
45 # 모델 로드
46 model = SparseEncoder(model_name, trust_remote_code=True, device=device)
47
48 for eval_dataset in eval_datasets:
49 short_dataset_name = eval_dataset.split("/")[-1]
50
51 prediction_filepath = f"{output_dir}/{short_dataset_name}.json"
52 if os.path.exists(prediction_filepath):
53 print(f"Skipping evaluation for {eval_dataset} as output already exists at {prediction_filepath}")
54 continue
55
56 corpus = {}
57 queries = {}
58 relevant_docs = {}
59 split = "dev"
60 if eval_dataset == "yjoonjang/markers_bm" or eval_dataset == "yjoonjang/squad_kor_v1":
61 split = "test"
62
63 if eval_dataset in ["yjoonjang/markers_bm", "taeminlee/Ko-StrategyQA"]:
64 dev_corpus = load_dataset(eval_dataset, "corpus", split="corpus")
65 dev_queries = load_dataset(eval_dataset, "queries", split="queries")
66 relevant_docs_data = load_dataset(eval_dataset, "default", split=split)
67
68 queries = dict(zip(dev_queries["_id"], dev_queries["text"]))
69 # Combine title and text if title exists (MTEB format)
70 if "title" in dev_corpus.column_names and "text" in dev_corpus.column_names:
71 corpus = {
72 row["_id"]: (row["title"] + " " + row["text"]).strip()
73 for row in dev_corpus
74 }
75 elif "text" in dev_corpus.column_names:
76 corpus = dict(zip(dev_corpus["_id"], dev_corpus["text"]))
77 else:
78 raise ValueError(f"Corpus dataset must have 'text' field")
79 for row in relevant_docs_data:
80 qid_str = str(row["query-id"])
81 corpus_ids_str = str(row["corpus-id"])
82 score = row.get("score", 1) # Default to 1 if no score field
83 if score >= 1: # Only include relevant documents (score >= 1)
84 if qid_str not in relevant_docs:
85 relevant_docs[qid_str] = set()
86 relevant_docs[qid_str].add(corpus_ids_str)
87
88 elif eval_dataset == "facebook/belebele":
89 split = "test"
90 ds = load_dataset(eval_dataset, "kor_Hang", split=split)
91
92 corpus_df = pd.DataFrame(ds)
93 corpus_df = corpus_df.drop_duplicates(subset=["link"])
94 corpus_df["cid"] = [f"C{i}" for i in range(len(corpus_df))]
95 corpus = dict(zip(corpus_df["cid"], corpus_df["flores_passage"]))
96
97 link_to_cid = dict(zip(corpus_df["link"], corpus_df["cid"]))
98
99 queries_df = pd.DataFrame(ds)
100 queries_df = queries_df.drop_duplicates(subset=["question"])
101 queries_df["qid"] = [f"Q{i}" for i in range(len(queries_df))]
102 queries = dict(zip(queries_df["qid"], queries_df["question"]))
103
104 question_to_qid = dict(zip(queries_df["question"], queries_df["qid"]))
105
106 for row in tqdm(ds, desc="Processing belebele"):
107 qid = question_to_qid[row["question"]]
108 cid = link_to_cid[row["link"]]
109 if qid not in relevant_docs:
110 relevant_docs[qid] = set()
111 relevant_docs[qid].add(cid)
112
113 elif eval_dataset == "miracl/mmteb-miracl":
114 split = "dev"
115 corpus_ds = load_dataset(eval_dataset, "corpus-ko", split="corpus", trust_remote_code=True)
116 queries_ds = load_dataset(eval_dataset, "queries-ko", split="queries", trust_remote_code=True)
117 qrels_ds = load_dataset(eval_dataset, "ko", split=split, trust_remote_code=True)
118
119 # Combine title and text if title exists (MTEB format)
120 if "title" in corpus_ds.column_names:
121 corpus = {
122 row['docid']: (row['title'] + " " + row['text']).strip()
123 for row in corpus_ds
124 }
125 else:
126 corpus = {row['docid']: row['text'] for row in corpus_ds}
127 queries = {row['query_id']: row['query'] for row in queries_ds}
128
129 for row in qrels_ds:
130 qid = row["query_id"]
131 cid = row["docid"]
132 score = row.get("score", 1) # Default to 1 if no score field
133 if score >= 1: # Only include relevant documents (score >= 1)
134 if qid not in relevant_docs:
135 relevant_docs[qid] = set()
136 relevant_docs[qid].add(cid)
137
138 elif eval_dataset == "mteb/mrtidy":
139 split = "test"
140 corpus_ds = load_dataset(eval_dataset, "korean-corpus", split="train", trust_remote_code=True)
141 queries_ds = load_dataset(eval_dataset, "korean-queries", split=split, trust_remote_code=True)
142 qrels_ds = load_dataset(eval_dataset, "korean-qrels", split=split, trust_remote_code=True)
143
144 # Combine title and text if title exists (MTEB format)
145 if "title" in corpus_ds.column_names and "text" in corpus_ds.column_names:
146 corpus = {
147 row["_id"]: (row["title"] + " " + row["text"]).strip()
148 for row in corpus_ds
149 }
150 elif "text" in corpus_ds.column_names:
151 corpus = {row['_id']: row['text'] for row in corpus_ds}
152 else:
153 raise ValueError(f"Corpus dataset must have 'text' field")
154 queries = {row['_id']: row['text'] for row in queries_ds}
155
156 for row in qrels_ds:
157 qid = str(row["query-id"])
158 cid = str(row["corpus-id"])
159 score = row.get("score", 1) # Default to 1 if no score field
160 if score >= 1: # Only include relevant documents (score >= 1)
161 if qid not in relevant_docs:
162 relevant_docs[qid] = set()
163 relevant_docs[qid].add(cid)
164
165 elif eval_dataset == "Shitao/MLDR":
166 split = "dev"
167 corpus_ds = load_dataset(eval_dataset, "corpus-ko", split="corpus", trust_remote_code=True)
168 lang_data = load_dataset(eval_dataset, "ko", split=split, trust_remote_code=True)
169
170 # Combine title and text if title exists (MTEB format)
171 if "title" in corpus_ds.column_names:
172 corpus = {
173 row['docid']: (row['title'] + " " + row['text']).strip()
174 for row in corpus_ds
175 }
176 else:
177 corpus = {row['docid']: row['text'] for row in corpus_ds}
178 queries = {row['query_id']: row['query'] for row in lang_data}
179
180 for row in lang_data:
181 qid = row["query_id"]
182 cid = row["positive_passages"][0]["docid"]
183 if qid not in relevant_docs:
184 relevant_docs[qid] = set()
185 relevant_docs[qid].add(cid)
186
187 elif eval_dataset == "xhluca/publichealth-qa":
188 split = "test"
189 ds = load_dataset(eval_dataset, "korean", split=split, trust_remote_code=True)
190
191 ds = ds.filter(lambda x: x["question"] is not None and x["answer"] is not None)
192
193 corpus_df = pd.DataFrame(list(ds))
194 corpus_df = corpus_df.drop_duplicates(subset=["answer"])
195 corpus_df["cid"] = [f"D{i}" for i in range(len(corpus_df))]
196 corpus = dict(zip(corpus_df["cid"], corpus_df["answer"]))
197 answer_to_cid = dict(zip(corpus_df["answer"], corpus_df["cid"]))
198
199 queries_df = pd.DataFrame(list(ds))
200 queries_df = queries_df.drop_duplicates(subset=["question"])
201 queries_df["qid"] = [f"Q{i}" for i in range(len(queries_df))]
202 queries = dict(zip(queries_df["qid"], queries_df["question"]))
203 question_to_qid = dict(zip(queries_df["question"], queries_df["qid"]))
204
205 for row in tqdm(ds, desc="Processing publichealth-qa"):
206 qid = question_to_qid[row["question"]]
207 cid = answer_to_cid[row["answer"]]
208 if qid not in relevant_docs:
209 relevant_docs[qid] = set()
210 relevant_docs[qid].add(cid)
211
212 else:
213 continue
214
215 if torch.cuda.get_device_name().startswith('NVIDIA A100'):
216 batch_size = 16
217 else:
218 batch_size = 4
219
220 evaluator = SparseInformationRetrievalEvaluator(
221 queries=queries,
222 corpus=corpus,
223 relevant_docs=relevant_docs,
224 write_csv=False,
225 name=f"{eval_dataset}",
226 show_progress_bar=True,
227 batch_size=batch_size,
228 write_predictions=False
229 )
230 short_dataset_name = eval_dataset.split("/")[-1]
231 output_filepath = f"{output_dir}/{short_dataset_name}.json"
232 metrics = evaluator(model)
233 print(f"GPU {gpu_id} - {eval_dataset} metrics: {metrics}")
234 with open(output_filepath, "w", encoding="utf-8") as f:
235 json.dump(metrics, f, ensure_ascii=False, indent=2)
236
237 except Exception as ex:
238 print(f"Error on GPU {gpu_id}: {ex}")
239 traceback.print_exc()
240
241if __name__ == "__main__":
242 torch.multiprocessing.set_start_method('spawn')
243
244 print(f"Starting evaluation for model: {model_name}")
245 output_dir = "./results_inference_free_new_idf"
246 # output_dir = "./results_inference_free_new"
247 processes = []
248
249 for gpu_id, datasets in DATASET_GPU_MAPPING.items():
250 p = Process(target=evaluate_dataset, args=(model_name, gpu_id, datasets, output_dir))
251 p.start()
252 processes.append(p)
253
254 for p in processes:
255 p.join()
256
257 print(f"Completed evaluation for model: {model_name}")| Model | Parameters | Recall@10 | NDCG@10 | MRR@10 | AVG_Query_Active_Dims | AVG_Corpus_Active_Dims |
|---|---|---|---|---|---|---|
| yjoonjang/splade-ko-v1 | 0.1B | 0.8391 | 0.7376 | 0.7260 | 110.7664 | 783.7026 |
| telepix/PIXIE-Splade-Preview | 0.1B | 0.8107 | 0.7175 | 0.7072 | 30.481 | 566.8242 |
| opensearch-project/opensearch-neural-sparse-encoding-multilingual-v1 | 0.1B | 0.6570 | 0.5383 | 0.5233 | 27.8722 | 177.5564 |
| Model | Parameters | Average NDCG@10 |
|---|---|---|
| Sparse Embedding | ||
| yjoonjang/splade-ko-v1 | 0.1B | 0.7376 |
| telepix/PIXIE-Splade-Preview | 0.1B | 0.7175 |
| opensearch-project/opensearch-neural-sparse-encoding-multilingual-v1 | 0.1B | 0.5383 |
| Dense Embedding | ||
| Qwen/Qwen3-Embedding-8B | 8B | 0.7635 |
| Qwen/Qwen3-Embedding-4B | 4B | 0.7484 |
| telepix/PIXIE-Rune-Preview | 0.6B | 0.7420 |
| nlpai-lab/KURE-v1 | 0.6B | 0.7395 |
| dragonkue/snowflake-arctic-embed-l-v2.0-ko | 0.6B | 0.7386 |
| telepix/PIXIE-Spell-Preview-1.7B | 1.7B | 0.7342 |
| BAAI/bge-m3 | 0.6B | 0.7339 |
| dragonkue/BGE-m3-ko | 0.6B | 0.7312 |
| Snowflake/snowflake-arctic-embed-l-v2.0 | 0.6B | 0.7179 |
| telepix/PIXIE-Spell-Preview-0.6B | 0.6B | 0.7106 |
| intfloat/multilingual-e5-large | 0.6B | 0.7075 |
| FronyAI/frony-embed-medium-arctic-ko-v2.5 | 0.6B | 0.7067 |
| nlpai-lab/KoE5 | 0.6B | 0.7043 |
| google/embeddinggemma-300m | 0.3B | 0.6944 |
| BAAI/bge-multilingual-gemma2 | 9.4B | 0.6931 |
| Qwen/Qwen3-Embedding-0.6B | 0.6B | 0.6895 |
| Alibaba-NLP/gte-multilingual-base | 0.3B | 0.6879 |
| jinaai/jina-embeddings-v3 | 0.6B | 0.6872 |
| SamilPwC-AXNode-GenAI/PwC-Embedding_expr | 0.6B | 0.6846 |
| nomic-ai/nomic-embed-text-v2-moe | 0.5B | 0.6799 |
| intfloat/multilingual-e5-large-instruct | 0.6B | 0.6799 |
| intfloat/multilingual-e5-base | 0.3B | 0.6709 |
| Alibaba-NLP/gte-Qwen2-7B-instruct | 7.6B | 0.6689 |
| intfloat/e5-mistral-7b-instruct | 7.1B | 0.6649 |
| openai/text-embedding-3-large | Unkown | 0.6513 |
| upskyy/bge-m3-korean | 0.6B | 0.6434 |
| Salesforce/SFR-Embedding-2_R | 2.6B | 0.6391 |
| jhgan/ko-sroberta-multitask | 0.1B | 0.5165 |
eval_strategy: stepsper_device_train_batch_size: 4per_device_eval_batch_size: 2learning_rate: 2e-05num_train_epochs: 2warmup_ratio: 0.1bf16: Truenegs_per_query: 6 (from our dataset)gather_device: True (Makes samples available to be shared across devices)overwrite_output_dir: Falsedo_predict: Falseeval_strategy: stepsprediction_loss_only: Trueper_device_train_batch_size: 4per_device_eval_batch_size: 2per_gpu_train_batch_size: Noneper_gpu_eval_batch_size: Nonegradient_accumulation_steps: 1eval_accumulation_steps: Nonetorch_empty_cache_steps: Nonelearning_rate: 2e-05weight_decay: 0.0adam_beta1: 0.9adam_beta2: 0.999adam_epsilon: 1e-08max_grad_norm: 1.0num_train_epochs: 2max_steps: -1lr_scheduler_type: linearlr_scheduler_kwargs: {}warmup_ratio: 0.1warmup_steps: 0log_level: passivelog_level_replica: warninglog_on_each_node: Truelogging_nan_inf_filter: Truesave_safetensors: Truesave_on_each_node: Falsesave_only_model: Falserestore_callback_states_from_checkpoint: Falseno_cuda: Falseuse_cpu: Falseuse_mps_device: Falseseed: 42data_seed: Nonejit_mode_eval: Falseuse_ipex: Falsebf16: Truefp16: Falsefp16_opt_level: O1half_precision_backend: autobf16_full_eval: Falsefp16_full_eval: Falsetf32: Nonelocal_rank: 7ddp_backend: Nonetpu_num_cores: Nonetpu_metrics_debug: Falsedebug: []dataloader_drop_last: Truedataloader_num_workers: 0dataloader_prefetch_factor: Nonepast_index: -1disable_tqdm: Falseremove_unused_columns: Truelabel_names: Noneload_best_model_at_end: Falseignore_data_skip: Falsefsdp: []fsdp_min_num_params: 0fsdp_config: {'min_num_params': 0, 'xla': False, 'xla_fsdp_v2': False, 'xla_fsdp_grad_ckpt': False}fsdp_transformer_layer_cls_to_wrap: Noneaccelerator_config: {'split_batches': False, 'dispatch_batches': None, 'even_batches': True, 'use_seedable_sampler': True, 'non_blocking': False, 'gradient_accumulation_kwargs': None}parallelism_config: Nonedeepspeed: Nonelabel_smoothing_factor: 0.0optim: adamw_torch_fusedoptim_args: Noneadafactor: Falsegroup_by_length: Falselength_column_name: lengthddp_find_unused_parameters: Noneddp_bucket_cap_mb: Noneddp_broadcast_buffers: Falsedataloader_pin_memory: Truedataloader_persistent_workers: Falseskip_memory_metrics: Trueuse_legacy_prediction_loop: Falsepush_to_hub: Falseresume_from_checkpoint: Nonehub_model_id: Nonehub_strategy: every_savehub_private_repo: Nonehub_always_push: Falsehub_revision: Nonegradient_checkpointing: Falsegradient_checkpointing_kwargs: Noneinclude_inputs_for_metrics: Falseinclude_for_metrics: []eval_do_concat_batches: Truefp16_backend: autopush_to_hub_model_id: Nonepush_to_hub_organization: Nonemp_parameters:auto_find_batch_size: Falsefull_determinism: Falsetorchdynamo: Noneray_scope: lastddp_timeout: 1800torch_compile: Falsetorch_compile_backend: Nonetorch_compile_mode: Noneinclude_tokens_per_second: Falseinclude_num_input_tokens_seen: Falseneftune_noise_alpha: Noneoptim_target_modules: Nonebatch_eval_metrics: Falseeval_on_start: Falseuse_liger_kernel: Falseliger_kernel_config: Noneeval_use_gather_object: Falseaverage_tokens_across_devices: Trueprompts: Nonebatch_sampler: batch_samplermulti_dataset_batch_sampler: proportionalrouter_mapping: {}learning_rate_mapping: {}1@software{spladeko,
2 author = {Youngjoon, Jang},
3 title = {splade-ko-v1},
4 subtitle = {Independent Korean SPLADE Model for the Industry},
5 publisher = {Hugging Face},
6 year = {2025},
7 version = {1.0.0},
8 url = {https://huggingface.co/yjoonjang/splade-ko-v1}
9}1@inproceedings{reimers-2019-sentence-bert,
2 title = "Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks",
3 author = "Reimers, Nils and Gurevych, Iryna",
4 booktitle = "Proceedings of the 2019 Conference on Empirical Methods in Natural Language Processing",
5 month = "11",
6 year = "2019",
7 publisher = "Association for Computational Linguistics",
8 url = "https://arxiv.org/abs/1908.10084",
9}1@misc{formal2022distillationhardnegativesampling,
2 title={From Distillation to Hard Negative Sampling: Making Sparse Neural IR Models More Effective},
3 author={Thibault Formal and Carlos Lassance and Benjamin Piwowarski and Stéphane Clinchant},
4 year={2022},
5 eprint={2205.04733},
6 archivePrefix={arXiv},
7 primaryClass={cs.IR},
8 url={https://arxiv.org/abs/2205.04733},
9}1@misc{henderson2017efficient,
2 title={Efficient Natural Language Response Suggestion for Smart Reply},
3 author={Matthew Henderson and Rami Al-Rfou and Brian Strope and Yun-hsuan Sung and Laszlo Lukacs and Ruiqi Guo and Sanjiv Kumar and Balint Miklos and Ray Kurzweil},
4 year={2017},
5 eprint={1705.00652},
6 archivePrefix={arXiv},
7 primaryClass={cs.CL}
8}1@article{paria2020minimizing,
2 title={Minimizing flops to learn efficient sparse representations},
3 author={Paria, Biswajit and Yeh, Chih-Kuan and Yen, Ian EH and Xu, Ning and Ravikumar, Pradeep and P{'o}czos, Barnab{'a}s},
4 journal={arXiv preprint arXiv:2004.05665},
5 year={2020}
6}