jobfit-embed-ko
μ΄λ ₯μμ μ±μ©κ³΅κ³ λ₯Ό λ§€μΉνκΈ° μν΄ νμΈνλν λ¬Έμ₯ μλ² λ© λͺ¨λΈμ λλ€. JobFit AI νλ‘μ νΈμμ μ¬μ©ν©λλ€.
μ νμΈνλνλ
λ²μ© λ¬Έμ₯ μ μ¬λ λͺ¨λΈμ μ§κ΅° κ²½κ³λ₯Ό μ ꡬλΆνμ§ λͺ»νμ΅λλ€. ML μμ§λμ΄ μ΄λ ₯μλ₯Ό λ£μΌλ©΄ λ°±μλ κ³΅κ³ κ° 1μλ‘ μ¬λΌμ€λ λ¬Έμ κ° μμμ΅λλ€.
μ΄λ ₯μβκ³΅κ³ μμΌλ‘ λμ‘°νμ΅(MultipleNegativesRankingLoss)μ μ μ©νκ³ ,
λ¨μν 무κ΄ν κ³΅κ³ λμ μΈμ μ§κ΅° κ³΅κ³ λ₯Ό hard negativeλ‘ μ£Όμ΄
λ―Έλ¬ν κ²½κ³λ₯Ό νμ΅μμΌ°μ΅λλ€.
μ±λ₯
νμ΅μ μ¬μ©νμ§ μμ 쿼리 32건, κ²μ λμ κ³΅κ³ 421κ° κΈ°μ€μ λλ€.
| μ§ν | νμΈνλ μ | νμΈνλ ν | λ³ν |
|---|---|---|---|
| Recall@1 | 37.5% | 84.4% | +46.9%p |
| Recall@3 | 62.5% | 90.6% | +28.1%p |
| Recall@5 | 90.6% | 96.9% | +6.3%p |
| MRR | 0.552 | 0.889 | +0.337 |
| μ§κ΅° μΌμΉμ¨ (1μ κ³΅κ³ ) | 78.1% | 100% | +21.9%p |
λ§μ§λ§ μ§νλ μ΄ νλ‘μ νΈμ λͺ©ν(μ§κ΅° κ²½κ³ κ΅¬λΆ)λ₯Ό μ§μ μΈ‘μ νλ €κ³ μ μν κ²μ λλ€.
INT8 μμνλ₯Ό μ±ννμ§ μμ μ΄μ
λ°°ν¬ νΈμλ₯Ό μν΄ λμ INT8 μμνλ₯Ό μλνμ΅λλ€. μ©λμ 471MB β 118MBλ‘ μ€μμ§λ§ μ§κ΅° μΌμΉμ¨μ΄ 100% β 93.8%λ‘ λ¨μ΄μ Έ μ±ννμ§ μμμ΅λλ€. λ°°ν¬ νκ²½μ λ©λͺ¨λ¦¬ μ¬μ κ° μμ΄ μ νλλ₯Ό ν¬κΈ°ν μ΄μ κ° μμμ΅λλ€.
μ¬μ©λ²
ONNX RuntimeμΌλ‘ μΆλ‘ ν©λλ€. PyTorchκ° νμ μμ΅λλ€.
import numpy as np
import onnxruntime as ort
from huggingface_hub import snapshot_download
from tokenizers import Tokenizer
path = snapshot_download("Kimhakyung/jobfit-embed-ko")
tokenizer = Tokenizer.from_file(f"{path}/tokenizer.json")
tokenizer.enable_truncation(max_length=128) # νμ΅ μ€μ κ³Ό λ°λμ μΌμΉμν¬ κ²
tokenizer.enable_padding()
session = ort.InferenceSession(f"{path}/model.onnx", providers=["CPUExecutionProvider"])
def embed(texts: list[str]) -> np.ndarray:
enc = tokenizer.encode_batch(texts)
ids = np.array([e.ids for e in enc], dtype=np.int64)
mask = np.array([e.attention_mask for e in enc], dtype=np.int64)
hidden = session.run(None, {
"input_ids": ids,
"attention_mask": mask,
"token_type_ids": np.zeros_like(ids),
})[0]
# λ§μ€ν¬λ₯Ό λ°μν νκ· νλ§ ν L2 μ κ·ν
m = mask[..., None].astype(np.float32)
pooled = (hidden * m).sum(1) / np.clip(m.sum(1), 1e-9, None)
return pooled / np.linalg.norm(pooled, axis=1, keepdims=True)
vectors = embed(["Pythonκ³Ό Airflowλ‘ λ°μ΄ν° νμ΄νλΌμΈμ ꡬμΆν 3λ
μ°¨ μμ§λμ΄μ
λλ€."])
μ΅λ κΈΈμ΄ 128μ μ§μΌμΌ ν©λλ€. νμ΅ μ 128ν ν°μμ μλκΈ° λλ¬Έμ, μΆλ‘ μμ λ€λ₯Έ κ°μ μ°λ©΄ κΈ΄ λ¬Έμμ 벑ν°κ° λ¬λΌμ Έ μ νλκ° μ‘°μ©ν λ¨μ΄μ§λλ€. μ€λ₯κ° λμ§ μμΌλ―λ‘ μμμ±κΈ° μ΄λ ΅μ΅λλ€.
sentence_bert_config.jsonμ μ΄ κ°μ ν¨κ» λ΄μ λμμ΅λλ€.
νμ΅ μ 보
| νλͺ© | κ° |
|---|---|
| λ² μ΄μ€ λͺ¨λΈ | sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2 |
| μμ€ ν¨μ | MultipleNegativesRankingLoss |
| νμ΅ μμ | 384μ (anchor, positive, hard negative) |
| μν / λ°°μΉ | 3 / 16 (72 μ€ν ) |
| νμ΅λ₯ | 2e-5 |
| μ΅λ ν ν° | 128 |
| νλμ¨μ΄ | CPU (μ½ 6λΆ) |
μ¬ν μ€ν¬λ¦½νΈ: scripts/finetune_embeddings.py
νμ΅ λ°μ΄ν°
μ€μ μ΄λ ₯μλ κ°μΈμ 보μ΄λ―λ‘ μμ§νμ§ μμμ΅λλ€. νμ΅μ μ¬μ©ν μ΄λ ₯μ μμ½κ³Ό μ±μ©κ³΅κ³ λ λͺ¨λ LLMμΌλ‘ μμ±ν ν©μ± λ°μ΄ν°μ΄λ©°, λ±μ₯νλ νμ¬λ μ€μ‘΄νμ§ μμ΅λλ€.
νκ³
- ν©μ± λ°μ΄ν°λ‘ νμ΅Β·νκ°νμΌλ―λ‘ μ€μ μ±μ©κ³΅κ³ λΆν¬μ λ€λ₯Ό μ μμ΅λλ€.
- νκ° μΏΌλ¦¬κ° 32건μΌλ‘ μ μ΄, μ§ν 1건μ μ°¨μ΄κ° 3%p μ΄μμΌλ‘ λνλ©λλ€.
- νκ΅ IT μ§κ΅° 10μ’ μ λμμΌλ‘ νμ΅νμ΅λλ€. λ€λ₯Έ λλ©μΈμλ μ ν©νμ§ μμ΅λλ€.