Spaces:
Sleeping
Sleeping
ktsn-ud commited on
Commit ·
41e01be
1
Parent(s): d9e5322
codex生成: topkのみに
Browse files- api/search/engine.py +45 -74
- docs/files.md +0 -2
- scripts/6_build_word_embeddings.py +3 -67
api/search/engine.py
CHANGED
|
@@ -23,7 +23,6 @@ class SearchConfig:
|
|
| 23 |
syn_limits: Dict[str, int]
|
| 24 |
banlist: List[str]
|
| 25 |
word_sim_enable: bool
|
| 26 |
-
word_sim_mode: str
|
| 27 |
word_sim_alpha: float
|
| 28 |
word_sim_topk_k: int
|
| 29 |
query_subword_enable: bool
|
|
@@ -76,7 +75,6 @@ class SearchEngine:
|
|
| 76 |
# Vectors
|
| 77 |
self.word_vocab: Dict[str, int] = {}
|
| 78 |
self.word_vectors: Optional[np.ndarray] = None
|
| 79 |
-
self.doc_vectors: Optional[np.ndarray] = None
|
| 80 |
self.ft_model = None
|
| 81 |
|
| 82 |
# ----- Init / Load -----
|
|
@@ -95,7 +93,6 @@ class SearchEngine:
|
|
| 95 |
syn_limits=search("synonyms.limits"),
|
| 96 |
banlist=search("synonyms.banlist"),
|
| 97 |
word_sim_enable=bool(search("word_sim.enable")),
|
| 98 |
-
word_sim_mode=(search("word_sim.mode") or "soft").lower(),
|
| 99 |
word_sim_alpha=float(search("word_sim.alpha")),
|
| 100 |
word_sim_topk_k=int(search("word_sim.topk_k", 3)),
|
| 101 |
query_subword_enable=bool(search("query_subword.enable")),
|
|
@@ -167,12 +164,7 @@ class SearchEngine:
|
|
| 167 |
except Exception as e:
|
| 168 |
log.warning(f"word vectors not ready: {e}")
|
| 169 |
|
| 170 |
-
|
| 171 |
-
doc_vec_path = files("embeddings.doc_vectors")
|
| 172 |
-
if os.path.exists(doc_vec_path):
|
| 173 |
-
self.doc_vectors = np.load(doc_vec_path)
|
| 174 |
-
except Exception as e:
|
| 175 |
-
log.warning(f"doc vectors not ready: {e}")
|
| 176 |
|
| 177 |
# fastText OOV
|
| 178 |
if self.cfg.query_subword_enable and self.cfg.query_subword_path and os.path.exists(self.cfg.query_subword_path):
|
|
@@ -286,7 +278,7 @@ class SearchEngine:
|
|
| 286 |
def _word_sim_scores(self, terms: List[str]) -> Optional[np.ndarray]:
|
| 287 |
if not self.cfg.word_sim_enable:
|
| 288 |
return None
|
| 289 |
-
if self.word_vectors is None
|
| 290 |
return None
|
| 291 |
|
| 292 |
# Build per-term vectors with weights (IDF; OOV down-weighted)
|
|
@@ -310,71 +302,50 @@ class SearchEngine:
|
|
| 310 |
V = np.stack(vecs).astype(np.float32) # T x D
|
| 311 |
W = np.asarray(weights, dtype=np.float32) # T
|
| 312 |
|
| 313 |
-
#
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
# collect unique doc tokens across target fields
|
| 334 |
-
fields = d.get("fields") or {}
|
| 335 |
-
doc_terms = set()
|
| 336 |
-
for fname in self.cfg.target_fields:
|
| 337 |
-
fobj = (fields.get(fname) or {})
|
| 338 |
-
tf = (fobj.get("tf") or {})
|
| 339 |
-
doc_terms.update(tf.keys())
|
| 340 |
-
if not doc_terms:
|
| 341 |
-
sims_all[i] = 0.0
|
| 342 |
-
continue
|
| 343 |
-
# build matrix of doc term vectors
|
| 344 |
-
Vd_list = []
|
| 345 |
-
for t in doc_terms:
|
| 346 |
-
idx = self.word_vocab.get(t)
|
| 347 |
-
if idx is None:
|
| 348 |
-
continue
|
| 349 |
-
Vd_list.append(self.word_vectors[idx])
|
| 350 |
-
if not Vd_list:
|
| 351 |
-
sims_all[i] = 0.0
|
| 352 |
-
continue
|
| 353 |
-
Vd = np.stack(Vd_list).astype(np.float32) # Td x D
|
| 354 |
-
# pairwise cosine (since vectors are normalized)
|
| 355 |
-
M = Vd @ Vq.T # Td x Tq
|
| 356 |
-
# weight by query term importance and clip negatives
|
| 357 |
-
if Wq.size:
|
| 358 |
-
M = M * Wq[None, :]
|
| 359 |
-
M = np.maximum(M, 0.0)
|
| 360 |
-
# take global top-k across all pairs
|
| 361 |
-
Td, Tq = M.shape
|
| 362 |
-
total = Td * Tq
|
| 363 |
-
kk = min(k, total) if total > 0 else 0
|
| 364 |
-
if kk == 0:
|
| 365 |
-
sims_all[i] = 0.0
|
| 366 |
continue
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 378 |
|
| 379 |
# ----- Public API -----
|
| 380 |
def search(
|
|
|
|
| 23 |
syn_limits: Dict[str, int]
|
| 24 |
banlist: List[str]
|
| 25 |
word_sim_enable: bool
|
|
|
|
| 26 |
word_sim_alpha: float
|
| 27 |
word_sim_topk_k: int
|
| 28 |
query_subword_enable: bool
|
|
|
|
| 75 |
# Vectors
|
| 76 |
self.word_vocab: Dict[str, int] = {}
|
| 77 |
self.word_vectors: Optional[np.ndarray] = None
|
|
|
|
| 78 |
self.ft_model = None
|
| 79 |
|
| 80 |
# ----- Init / Load -----
|
|
|
|
| 93 |
syn_limits=search("synonyms.limits"),
|
| 94 |
banlist=search("synonyms.banlist"),
|
| 95 |
word_sim_enable=bool(search("word_sim.enable")),
|
|
|
|
| 96 |
word_sim_alpha=float(search("word_sim.alpha")),
|
| 97 |
word_sim_topk_k=int(search("word_sim.topk_k", 3)),
|
| 98 |
query_subword_enable=bool(search("query_subword.enable")),
|
|
|
|
| 164 |
except Exception as e:
|
| 165 |
log.warning(f"word vectors not ready: {e}")
|
| 166 |
|
| 167 |
+
# doc_vectors.npy は topk 方式では不要
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
|
| 169 |
# fastText OOV
|
| 170 |
if self.cfg.query_subword_enable and self.cfg.query_subword_path and os.path.exists(self.cfg.query_subword_path):
|
|
|
|
| 278 |
def _word_sim_scores(self, terms: List[str]) -> Optional[np.ndarray]:
|
| 279 |
if not self.cfg.word_sim_enable:
|
| 280 |
return None
|
| 281 |
+
if self.word_vectors is None:
|
| 282 |
return None
|
| 283 |
|
| 284 |
# Build per-term vectors with weights (IDF; OOV down-weighted)
|
|
|
|
| 302 |
V = np.stack(vecs).astype(np.float32) # T x D
|
| 303 |
W = np.asarray(weights, dtype=np.float32) # T
|
| 304 |
|
| 305 |
+
# top-k pooling over term-term cosine contributions (query terms x document terms)
|
| 306 |
+
k = max(1, int(self.cfg.word_sim_topk_k))
|
| 307 |
+
n_docs = len(self.tf_token_docs)
|
| 308 |
+
sims_all = np.zeros((n_docs,), dtype=np.float32)
|
| 309 |
+
Vq = V # Tq x D (normalized)
|
| 310 |
+
Wq = W # Tq
|
| 311 |
+
for i, d in enumerate(self.tf_token_docs):
|
| 312 |
+
fields = d.get("fields") or {}
|
| 313 |
+
doc_terms = set()
|
| 314 |
+
for fname in self.cfg.target_fields:
|
| 315 |
+
fobj = (fields.get(fname) or {})
|
| 316 |
+
tf = (fobj.get("tf") or {})
|
| 317 |
+
doc_terms.update(tf.keys())
|
| 318 |
+
if not doc_terms:
|
| 319 |
+
sims_all[i] = 0.0
|
| 320 |
+
continue
|
| 321 |
+
Vd_list = []
|
| 322 |
+
for t in doc_terms:
|
| 323 |
+
idx = self.word_vocab.get(t)
|
| 324 |
+
if idx is None:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 325 |
continue
|
| 326 |
+
Vd_list.append(self.word_vectors[idx])
|
| 327 |
+
if not Vd_list:
|
| 328 |
+
sims_all[i] = 0.0
|
| 329 |
+
continue
|
| 330 |
+
Vd = np.stack(Vd_list).astype(np.float32) # Td x D
|
| 331 |
+
M = Vd @ Vq.T # Td x Tq
|
| 332 |
+
if Wq.size:
|
| 333 |
+
M = M * Wq[None, :]
|
| 334 |
+
M = np.maximum(M, 0.0)
|
| 335 |
+
Td, Tq = M.shape
|
| 336 |
+
total = Td * Tq
|
| 337 |
+
kk = min(k, total) if total > 0 else 0
|
| 338 |
+
if kk == 0:
|
| 339 |
+
sims_all[i] = 0.0
|
| 340 |
+
continue
|
| 341 |
+
flat = M.reshape(-1)
|
| 342 |
+
if kk == total:
|
| 343 |
+
top_vals = flat
|
| 344 |
+
else:
|
| 345 |
+
idxk = np.argpartition(flat, -kk)[-kk:]
|
| 346 |
+
top_vals = flat[idxk]
|
| 347 |
+
sims_all[i] = float(top_vals.mean()) if top_vals.size else 0.0
|
| 348 |
+
return sims_all
|
| 349 |
|
| 350 |
# ----- Public API -----
|
| 351 |
def search(
|
docs/files.md
CHANGED
|
@@ -13,5 +13,3 @@
|
|
| 13 |
- `scripts/6_build_word_embeddings.py`で生成
|
| 14 |
- `word_vectors.npz`
|
| 15 |
- `scripts/6_build_word_embeddings.py`で生成
|
| 16 |
-
- `doc_vectors.npy`
|
| 17 |
-
- `scripts/6_build_word_embeddings.py`で生成
|
|
|
|
| 13 |
- `scripts/6_build_word_embeddings.py`で生成
|
| 14 |
- `word_vectors.npz`
|
| 15 |
- `scripts/6_build_word_embeddings.py`で生成
|
|
|
|
|
|
scripts/6_build_word_embeddings.py
CHANGED
|
@@ -20,16 +20,11 @@ def load_configs():
|
|
| 20 |
|
| 21 |
paths = {
|
| 22 |
"tf_token": files("bm25.tf_token"),
|
| 23 |
-
"bm25_meta": files("bm25.bm25_meta"),
|
| 24 |
"fasttext_vec": files("embeddings.fasttext_vec"),
|
| 25 |
"word_vocab": files("embeddings.word_vocab"),
|
| 26 |
"word_vectors": files("embeddings.word_vectors"),
|
| 27 |
-
"doc_vectors": files("embeddings.doc_vectors"),
|
| 28 |
}
|
| 29 |
-
|
| 30 |
-
target_fields: List[str] = search("target_fields")
|
| 31 |
-
|
| 32 |
-
return paths, field_weights, target_fields
|
| 33 |
|
| 34 |
|
| 35 |
def read_vocab_from_tf_token(tf_token_path: str) -> Tuple[List[dict], set[str]]:
|
|
@@ -96,63 +91,18 @@ def stream_fasttext_vec(vec_path: str, vocab: set[str]) -> Tuple[Dict[str, int],
|
|
| 96 |
return token_to_idx, arr
|
| 97 |
|
| 98 |
|
| 99 |
-
|
| 100 |
-
docs: List[dict],
|
| 101 |
-
token_to_idx: Dict[str, int],
|
| 102 |
-
word_vecs: np.ndarray,
|
| 103 |
-
idf: Dict[str, float],
|
| 104 |
-
field_weights: Dict[str, float],
|
| 105 |
-
target_fields: List[str],
|
| 106 |
-
) -> np.ndarray:
|
| 107 |
-
dim = word_vecs.shape[1] if word_vecs.size > 0 else 300
|
| 108 |
-
doc_mat = np.zeros((len(docs), dim), dtype=np.float32)
|
| 109 |
-
|
| 110 |
-
for i, d in enumerate(docs):
|
| 111 |
-
accum = np.zeros((dim,), dtype=np.float32)
|
| 112 |
-
w_sum = 0.0
|
| 113 |
-
|
| 114 |
-
fields = (d.get("fields") or {})
|
| 115 |
-
for field in target_fields:
|
| 116 |
-
field_obj = fields.get(field) or {}
|
| 117 |
-
tf = field_obj.get("tf") or {}
|
| 118 |
-
f_weight = float(field_weights.get(field, 1.0))
|
| 119 |
-
if f_weight <= 0:
|
| 120 |
-
continue
|
| 121 |
-
for t, cnt in tf.items():
|
| 122 |
-
idx = token_to_idx.get(t)
|
| 123 |
-
if idx is None:
|
| 124 |
-
continue
|
| 125 |
-
idf_t = float(idf.get(t, 0.0))
|
| 126 |
-
w = f_weight * idf_t * float(cnt)
|
| 127 |
-
if w <= 0:
|
| 128 |
-
continue
|
| 129 |
-
accum += word_vecs[idx] * w
|
| 130 |
-
w_sum += w
|
| 131 |
-
|
| 132 |
-
if w_sum > 0:
|
| 133 |
-
vec = accum / w_sum
|
| 134 |
-
# L2正規化
|
| 135 |
-
n = np.linalg.norm(vec)
|
| 136 |
-
if n > 0:
|
| 137 |
-
vec = vec / n
|
| 138 |
-
doc_mat[i] = vec.astype(np.float32)
|
| 139 |
-
else:
|
| 140 |
-
# ベクトルなしの場合はゼロベクトル
|
| 141 |
-
doc_mat[i] = np.zeros((dim,), dtype=np.float32)
|
| 142 |
-
|
| 143 |
-
return doc_mat
|
| 144 |
|
| 145 |
|
| 146 |
def main():
|
| 147 |
log.info("語彙/文書ベクトル(word_vocab.json, word_vectors.npz, doc_vectors.npy)を生成します")
|
| 148 |
try:
|
| 149 |
-
paths
|
| 150 |
except Exception as e:
|
| 151 |
log.error(f"設定の読み込みに失敗しました: {e}")
|
| 152 |
sys.exit(1)
|
| 153 |
|
| 154 |
tf_token_path = paths["tf_token"]
|
| 155 |
-
bm25_meta_path = paths["bm25_meta"]
|
| 156 |
fasttext_vec_path = paths["fasttext_vec"]
|
| 157 |
|
| 158 |
if not os.path.exists(fasttext_vec_path):
|
|
@@ -166,14 +116,6 @@ def main():
|
|
| 166 |
log.error(f"tf_token.jsonの読み込みに失敗しました: {e}")
|
| 167 |
sys.exit(1)
|
| 168 |
|
| 169 |
-
try:
|
| 170 |
-
with open(bm25_meta_path, encoding="utf-8") as f:
|
| 171 |
-
bm25_meta = json.load(f)
|
| 172 |
-
idf = bm25_meta.get("idf", {})
|
| 173 |
-
except Exception as e:
|
| 174 |
-
log.error(f"bm25_meta.jsonの読み込みに失敗しました: {e}")
|
| 175 |
-
sys.exit(1)
|
| 176 |
-
|
| 177 |
log.info(f"コーパス語彙数: {len(vocab)}")
|
| 178 |
token_to_idx, word_vecs = stream_fasttext_vec(fasttext_vec_path, vocab)
|
| 179 |
log.info(f"抽出済み語彙ベクトル数: {word_vecs.shape[0]}")
|
|
@@ -188,21 +130,15 @@ def main():
|
|
| 188 |
token_to_idx = remap
|
| 189 |
word_vecs = remapped_vecs
|
| 190 |
|
| 191 |
-
# 文書ベクトル
|
| 192 |
-
doc_mat = build_doc_vectors(docs, token_to_idx, word_vecs, idf, field_weights, target_fields)
|
| 193 |
-
|
| 194 |
# 出力
|
| 195 |
os.makedirs(os.path.dirname(paths["word_vocab"]), exist_ok=True)
|
| 196 |
json_dumps(token_to_idx, paths["word_vocab"]) # 語→index
|
| 197 |
# 圧縮npz
|
| 198 |
np.savez_compressed(paths["word_vectors"], vectors=word_vecs)
|
| 199 |
-
np.save(paths["doc_vectors"], doc_mat)
|
| 200 |
|
| 201 |
log.info(f"word_vocab.json: {paths['word_vocab']}")
|
| 202 |
log.info(f"word_vectors.npz: {paths['word_vectors']}")
|
| 203 |
-
log.info(f"doc_vectors.npy: {paths['doc_vectors']}")
|
| 204 |
|
| 205 |
|
| 206 |
if __name__ == "__main__":
|
| 207 |
main()
|
| 208 |
-
|
|
|
|
| 20 |
|
| 21 |
paths = {
|
| 22 |
"tf_token": files("bm25.tf_token"),
|
|
|
|
| 23 |
"fasttext_vec": files("embeddings.fasttext_vec"),
|
| 24 |
"word_vocab": files("embeddings.word_vocab"),
|
| 25 |
"word_vectors": files("embeddings.word_vectors"),
|
|
|
|
| 26 |
}
|
| 27 |
+
return paths
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
|
| 30 |
def read_vocab_from_tf_token(tf_token_path: str) -> Tuple[List[dict], set[str]]:
|
|
|
|
| 91 |
return token_to_idx, arr
|
| 92 |
|
| 93 |
|
| 94 |
+
# top-k モードでは文書ベクトルは不要
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
|
| 96 |
|
| 97 |
def main():
|
| 98 |
log.info("語彙/文書ベクトル(word_vocab.json, word_vectors.npz, doc_vectors.npy)を生成します")
|
| 99 |
try:
|
| 100 |
+
paths = load_configs()
|
| 101 |
except Exception as e:
|
| 102 |
log.error(f"設定の読み込みに失敗しました: {e}")
|
| 103 |
sys.exit(1)
|
| 104 |
|
| 105 |
tf_token_path = paths["tf_token"]
|
|
|
|
| 106 |
fasttext_vec_path = paths["fasttext_vec"]
|
| 107 |
|
| 108 |
if not os.path.exists(fasttext_vec_path):
|
|
|
|
| 116 |
log.error(f"tf_token.jsonの読み込みに失敗しました: {e}")
|
| 117 |
sys.exit(1)
|
| 118 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
log.info(f"コーパス語彙数: {len(vocab)}")
|
| 120 |
token_to_idx, word_vecs = stream_fasttext_vec(fasttext_vec_path, vocab)
|
| 121 |
log.info(f"抽出済み語彙ベクトル数: {word_vecs.shape[0]}")
|
|
|
|
| 130 |
token_to_idx = remap
|
| 131 |
word_vecs = remapped_vecs
|
| 132 |
|
|
|
|
|
|
|
|
|
|
| 133 |
# 出力
|
| 134 |
os.makedirs(os.path.dirname(paths["word_vocab"]), exist_ok=True)
|
| 135 |
json_dumps(token_to_idx, paths["word_vocab"]) # 語→index
|
| 136 |
# 圧縮npz
|
| 137 |
np.savez_compressed(paths["word_vectors"], vectors=word_vecs)
|
|
|
|
| 138 |
|
| 139 |
log.info(f"word_vocab.json: {paths['word_vocab']}")
|
| 140 |
log.info(f"word_vectors.npz: {paths['word_vectors']}")
|
|
|
|
| 141 |
|
| 142 |
|
| 143 |
if __name__ == "__main__":
|
| 144 |
main()
|
|
|