Spaces:
Sleeping
Sleeping
ktsn-ud commited on
Commit ·
f446d43
1
Parent(s): 20bc01d
codex生成: 4以降のscript全て
Browse files- config/files.json +12 -1
- config/search_model.json +54 -28
- docs/files.md +7 -0
- scripts/4_prepare_bm25f_meta.py +117 -0
- scripts/5_prepare_tf_token.py +124 -0
- scripts/6_build_word_embeddings.py +208 -0
- scripts/build_all.py +53 -0
config/files.json
CHANGED
|
@@ -10,5 +10,16 @@
|
|
| 10 |
"projects": {
|
| 11 |
"original_csv": "resources/test_data.csv",
|
| 12 |
"projects_json": "data/generated/projects.json"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
}
|
| 14 |
-
}
|
|
|
|
| 10 |
"projects": {
|
| 11 |
"original_csv": "resources/test_data.csv",
|
| 12 |
"projects_json": "data/generated/projects.json"
|
| 13 |
+
},
|
| 14 |
+
"bm25": {
|
| 15 |
+
"bm25_meta": "data/generated/bm25_meta.json",
|
| 16 |
+
"tf_token": "data/generated/tf_token.json"
|
| 17 |
+
},
|
| 18 |
+
"embeddings": {
|
| 19 |
+
"fasttext_vec": "resources/embeddings/cc.ja.300.vec",
|
| 20 |
+
"fasttext_bin": "resources/embeddings/cc.ja.300.bin",
|
| 21 |
+
"word_vocab": "data/generated/word_vocab.json",
|
| 22 |
+
"word_vectors": "data/generated/word_vectors.npz",
|
| 23 |
+
"doc_vectors": "data/generated/doc_vectors.npy"
|
| 24 |
}
|
| 25 |
+
}
|
config/search_model.json
CHANGED
|
@@ -1,30 +1,56 @@
|
|
| 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 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
{
|
| 2 |
+
"target_pos_l1": [
|
| 3 |
+
"名詞",
|
| 4 |
+
"動詞",
|
| 5 |
+
"形容詞",
|
| 6 |
+
"形容動詞語幹"
|
| 7 |
+
],
|
| 8 |
+
"target_fields": [
|
| 9 |
+
"title",
|
| 10 |
+
"organization",
|
| 11 |
+
"description",
|
| 12 |
+
"prComment",
|
| 13 |
+
"prCommentLong",
|
| 14 |
+
"reading"
|
| 15 |
+
],
|
| 16 |
+
"synonyms": {
|
| 17 |
+
"enable": true,
|
| 18 |
+
"sources": {
|
| 19 |
+
"sudachi": true,
|
| 20 |
+
"custom_json": "resources/synonyms_custom.json"
|
| 21 |
+
},
|
| 22 |
+
"limits": {
|
| 23 |
+
"max_expansions_per_term": 4,
|
| 24 |
+
"max_query_variants": 5,
|
| 25 |
+
"min_char_len": 2
|
| 26 |
+
},
|
| 27 |
+
"banlist": [
|
| 28 |
+
"部",
|
| 29 |
+
"会",
|
| 30 |
+
"サークル"
|
| 31 |
+
]
|
| 32 |
+
},
|
| 33 |
+
"bm25f": {
|
| 34 |
+
"k1": 1.2,
|
| 35 |
+
"b": 0.75,
|
| 36 |
+
"field_weights": {
|
| 37 |
+
"title": 2.0,
|
| 38 |
+
"organization": 1.5,
|
| 39 |
+
"description": 1.0,
|
| 40 |
+
"prComment": 1.0,
|
| 41 |
+
"prCommentLong": 0.8,
|
| 42 |
+
"reading": 0.6
|
| 43 |
}
|
| 44 |
+
},
|
| 45 |
+
"word_sim": {
|
| 46 |
+
"enable": false,
|
| 47 |
+
"mode": "soft",
|
| 48 |
+
"alpha": 0.7
|
| 49 |
+
},
|
| 50 |
+
"query_subword": {
|
| 51 |
+
"enable": true,
|
| 52 |
+
"path": "resources/embeddings/cc.ja.300.bin",
|
| 53 |
+
"oov_weight": 0.8,
|
| 54 |
+
"cache_size": 50000
|
| 55 |
+
}
|
| 56 |
+
}
|
docs/files.md
CHANGED
|
@@ -2,9 +2,16 @@
|
|
| 2 |
- `cc.ja.300.bin`: OOV(Out of Vocabrary)時に使用 更新不要
|
| 3 |
- `generated/`: 毎年更新
|
| 4 |
- `projects.json`: API返却用データ
|
|
|
|
| 5 |
- `bm25_meta.json`: BM25Fメタデータ
|
|
|
|
| 6 |
- `tf_token.json`: フィールド別TF/トークン
|
|
|
|
| 7 |
- `synonyms_cache.json`: Sudachi同義語キャッシュ
|
|
|
|
| 8 |
- `word_vocab.json`
|
|
|
|
| 9 |
- `word_vectors.npz`
|
|
|
|
| 10 |
- `doc_vectors.npy`
|
|
|
|
|
|
| 2 |
- `cc.ja.300.bin`: OOV(Out of Vocabrary)時に使用 更新不要
|
| 3 |
- `generated/`: 毎年更新
|
| 4 |
- `projects.json`: API返却用データ
|
| 5 |
+
- `scripts/1_build_dict.py`で生成
|
| 6 |
- `bm25_meta.json`: BM25Fメタデータ
|
| 7 |
+
- `scripts/4_prepare_bm25f_meta.py`で生成
|
| 8 |
- `tf_token.json`: フィールド別TF/トークン
|
| 9 |
+
- `scripts/5_prepare_tf_token.py`で生成
|
| 10 |
- `synonyms_cache.json`: Sudachi同義語キャッシュ
|
| 11 |
+
- `scripts/3_build_synonyms_from_sudachi.py`から生成
|
| 12 |
- `word_vocab.json`
|
| 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`で生成
|
scripts/4_prepare_bm25f_meta.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
import json
|
| 4 |
+
from collections import defaultdict
|
| 5 |
+
from typing import Dict, List
|
| 6 |
+
|
| 7 |
+
from sudachipy import dictionary, tokenizer
|
| 8 |
+
|
| 9 |
+
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
| 10 |
+
|
| 11 |
+
from utils.logger import setup_logger
|
| 12 |
+
from utils.json import get_file_path_from_config, field_getter, json_dumps
|
| 13 |
+
|
| 14 |
+
log = setup_logger(__name__)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def load_configs():
|
| 18 |
+
search = field_getter("config/search_model.json")
|
| 19 |
+
files = field_getter("config/files.json")
|
| 20 |
+
|
| 21 |
+
target_pos_l1: List[str] = search("target_pos_l1")
|
| 22 |
+
target_fields: List[str] = search("target_fields")
|
| 23 |
+
ban_list: List[str] = search("synonyms.banlist")
|
| 24 |
+
|
| 25 |
+
stopwords_path = files("sudachi.stopwords")
|
| 26 |
+
with open(stopwords_path, encoding="utf-8") as f:
|
| 27 |
+
stopwords = set(json.load(f))
|
| 28 |
+
|
| 29 |
+
sudachi_config_path = files("sudachi.sudachi_config")
|
| 30 |
+
|
| 31 |
+
return target_pos_l1, target_fields, set(ban_list), stopwords, sudachi_config_path
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def build_tokenizer(sudachi_config_path: str):
|
| 35 |
+
tok = dictionary.Dictionary(config_path=sudachi_config_path).create()
|
| 36 |
+
mode = tokenizer.Tokenizer.SplitMode.A
|
| 37 |
+
return tok, mode
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def tokenize(text: str, tok, mode, target_pos_l1: List[str], ban_list: set, stopwords: set) -> List[str]:
|
| 41 |
+
if not text:
|
| 42 |
+
return []
|
| 43 |
+
out: List[str] = []
|
| 44 |
+
for m in tok.tokenize(text, mode):
|
| 45 |
+
base = m.normalized_form().lower().strip()
|
| 46 |
+
if not base:
|
| 47 |
+
continue
|
| 48 |
+
pos = m.part_of_speech()
|
| 49 |
+
if pos[0] not in target_pos_l1:
|
| 50 |
+
continue
|
| 51 |
+
if base in stopwords or base in ban_list:
|
| 52 |
+
continue
|
| 53 |
+
out.append(base)
|
| 54 |
+
return out
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def main():
|
| 58 |
+
log.info("BM25Fメタデータ(bm25_meta.json)を生成します")
|
| 59 |
+
try:
|
| 60 |
+
target_pos_l1, target_fields, ban_list, stopwords, sudachi_config_path = load_configs()
|
| 61 |
+
except Exception as e:
|
| 62 |
+
log.error(f"設定の読み込みに失敗しました: {e}")
|
| 63 |
+
sys.exit(1)
|
| 64 |
+
|
| 65 |
+
projects_path = get_file_path_from_config("projects.projects_json", "data/generated/projects.json")
|
| 66 |
+
output_path = get_file_path_from_config("bm25.bm25_meta", "data/generated/bm25_meta.json")
|
| 67 |
+
|
| 68 |
+
try:
|
| 69 |
+
with open(projects_path, encoding="utf-8") as f:
|
| 70 |
+
projects = json.load(f)
|
| 71 |
+
except Exception as e:
|
| 72 |
+
log.error(f"projects.jsonの読み込みに失敗しました: {e}")
|
| 73 |
+
sys.exit(1)
|
| 74 |
+
|
| 75 |
+
tok, mode = build_tokenizer(sudachi_config_path)
|
| 76 |
+
|
| 77 |
+
N = len(projects)
|
| 78 |
+
df: Dict[str, int] = defaultdict(int)
|
| 79 |
+
field_token_lens_sum: Dict[str, int] = {f: 0 for f in target_fields}
|
| 80 |
+
|
| 81 |
+
log.info(f"ドキュメント数: {N}")
|
| 82 |
+
|
| 83 |
+
for p in projects:
|
| 84 |
+
seen_in_doc = set()
|
| 85 |
+
for field in target_fields:
|
| 86 |
+
text = p.get(field) or ""
|
| 87 |
+
toks = tokenize(str(text), tok, mode, target_pos_l1, ban_list, stopwords)
|
| 88 |
+
field_token_lens_sum[field] += len(toks)
|
| 89 |
+
for t in set(toks):
|
| 90 |
+
if t not in seen_in_doc:
|
| 91 |
+
df[t] += 1
|
| 92 |
+
seen_in_doc.add(t)
|
| 93 |
+
|
| 94 |
+
# IDF 計算(BM25で一般的な +0.5 smoothing と +1 オフセット)
|
| 95 |
+
idf: Dict[str, float] = {}
|
| 96 |
+
for term, dfi in df.items():
|
| 97 |
+
idf_val = max(0.0, ( ( (N - dfi + 0.5) / (dfi + 0.5) ) ))
|
| 98 |
+
# 数値安定化のためlog1p
|
| 99 |
+
import math
|
| 100 |
+
|
| 101 |
+
idf[term] = math.log1p(idf_val)
|
| 102 |
+
|
| 103 |
+
avg_len = {field: (field_token_lens_sum[field] / N if N > 0 else 0.0) for field in target_fields}
|
| 104 |
+
|
| 105 |
+
meta = {
|
| 106 |
+
"N": N,
|
| 107 |
+
"avg_len": avg_len,
|
| 108 |
+
"idf": idf,
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
| 112 |
+
json_dumps(meta, output_path)
|
| 113 |
+
log.info(f"bm25_meta.json を出力しました: {output_path}")
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
if __name__ == "__main__":
|
| 117 |
+
main()
|
scripts/5_prepare_tf_token.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
import json
|
| 4 |
+
from collections import Counter
|
| 5 |
+
from typing import Dict, List
|
| 6 |
+
|
| 7 |
+
from sudachipy import dictionary, tokenizer
|
| 8 |
+
|
| 9 |
+
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
| 10 |
+
|
| 11 |
+
from utils.logger import setup_logger
|
| 12 |
+
from utils.json import get_file_path_from_config, field_getter, json_dumps
|
| 13 |
+
import schemas.tf_token as tf_schema
|
| 14 |
+
|
| 15 |
+
log = setup_logger(__name__)
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def load_configs():
|
| 19 |
+
search = field_getter("config/search_model.json")
|
| 20 |
+
files = field_getter("config/files.json")
|
| 21 |
+
|
| 22 |
+
target_pos_l1: List[str] = search("target_pos_l1")
|
| 23 |
+
target_fields: List[str] = search("target_fields")
|
| 24 |
+
ban_list: List[str] = search("synonyms.banlist")
|
| 25 |
+
|
| 26 |
+
stopwords_path = files("sudachi.stopwords")
|
| 27 |
+
with open(stopwords_path, encoding="utf-8") as f:
|
| 28 |
+
stopwords = set(json.load(f))
|
| 29 |
+
|
| 30 |
+
sudachi_config_path = files("sudachi.sudachi_config")
|
| 31 |
+
|
| 32 |
+
return target_pos_l1, target_fields, set(ban_list), stopwords, sudachi_config_path
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def build_tokenizer(sudachi_config_path: str):
|
| 36 |
+
tok = dictionary.Dictionary(config_path=sudachi_config_path).create()
|
| 37 |
+
mode = tokenizer.Tokenizer.SplitMode.A
|
| 38 |
+
return tok, mode
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def tokenize(text: str, tok, mode, target_pos_l1: List[str], ban_list: set, stopwords: set) -> List[str]:
|
| 42 |
+
if not text:
|
| 43 |
+
return []
|
| 44 |
+
out: List[str] = []
|
| 45 |
+
for m in tok.tokenize(text, mode):
|
| 46 |
+
base = m.normalized_form().lower().strip()
|
| 47 |
+
if not base:
|
| 48 |
+
continue
|
| 49 |
+
pos = m.part_of_speech()
|
| 50 |
+
if pos[0] not in target_pos_l1:
|
| 51 |
+
continue
|
| 52 |
+
if base in stopwords or base in ban_list:
|
| 53 |
+
continue
|
| 54 |
+
out.append(base)
|
| 55 |
+
return out
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def main():
|
| 59 |
+
log.info("フィールド別TF/トークン(tf_token.json)を生成します")
|
| 60 |
+
try:
|
| 61 |
+
target_pos_l1, target_fields, ban_list, stopwords, sudachi_config_path = load_configs()
|
| 62 |
+
except Exception as e:
|
| 63 |
+
log.error(f"設定の読み込みに失敗しました: {e}")
|
| 64 |
+
sys.exit(1)
|
| 65 |
+
|
| 66 |
+
projects_path = get_file_path_from_config("projects.projects_json", "data/generated/projects.json")
|
| 67 |
+
output_path = get_file_path_from_config("bm25.tf_token", "data/generated/tf_token.json")
|
| 68 |
+
|
| 69 |
+
try:
|
| 70 |
+
with open(projects_path, encoding="utf-8") as f:
|
| 71 |
+
projects = json.load(f)
|
| 72 |
+
except Exception as e:
|
| 73 |
+
log.error(f"projects.jsonの読み込みに失敗しました: {e}")
|
| 74 |
+
sys.exit(1)
|
| 75 |
+
|
| 76 |
+
tok, mode = build_tokenizer(sudachi_config_path)
|
| 77 |
+
|
| 78 |
+
results: List[tf_schema.Project] = []
|
| 79 |
+
|
| 80 |
+
for p in projects:
|
| 81 |
+
project_id = p.get("projectId")
|
| 82 |
+
|
| 83 |
+
# 各フィールドのトークン化とTF
|
| 84 |
+
field_objs: Dict[str, tf_schema.TfOfField] = {}
|
| 85 |
+
doc_tf_counter: Counter = Counter()
|
| 86 |
+
doc_token_set: set = set()
|
| 87 |
+
|
| 88 |
+
for field in target_fields:
|
| 89 |
+
text = p.get(field) or ""
|
| 90 |
+
toks = tokenize(str(text), tok, mode, target_pos_l1, ban_list, stopwords)
|
| 91 |
+
tf = Counter(toks)
|
| 92 |
+
field_objs[field] = tf_schema.TfOfField(len=len(toks), tf=dict(tf))
|
| 93 |
+
doc_tf_counter.update(tf)
|
| 94 |
+
doc_token_set.update(tf.keys())
|
| 95 |
+
|
| 96 |
+
# スキーマ Fields へ詰める(未定義フィールドは長さ0/空dictで埋める)
|
| 97 |
+
def get_field(name: str) -> tf_schema.TfOfField:
|
| 98 |
+
return field_objs.get(name, tf_schema.TfOfField(len=0, tf={}))
|
| 99 |
+
|
| 100 |
+
fields_obj = tf_schema.Fields(
|
| 101 |
+
title=get_field("title"),
|
| 102 |
+
organization=get_field("organization"),
|
| 103 |
+
reading=get_field("reading"),
|
| 104 |
+
description=get_field("description"),
|
| 105 |
+
prComment=get_field("prComment"),
|
| 106 |
+
prCommentLong=get_field("prCommentLong"),
|
| 107 |
+
)
|
| 108 |
+
|
| 109 |
+
project_entry = tf_schema.Project(
|
| 110 |
+
projectId=project_id,
|
| 111 |
+
fields=fields_obj,
|
| 112 |
+
tf=dict(doc_tf_counter),
|
| 113 |
+
# tokens はユニーク語彙の存在フラグ(1)とする
|
| 114 |
+
tokens={t: 1 for t in sorted(doc_token_set)},
|
| 115 |
+
)
|
| 116 |
+
results.append(project_entry)
|
| 117 |
+
|
| 118 |
+
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
| 119 |
+
json_dumps([r.model_dump() for r in results], output_path)
|
| 120 |
+
log.info(f"tf_token.json を出力しました: {output_path}")
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
if __name__ == "__main__":
|
| 124 |
+
main()
|
scripts/6_build_word_embeddings.py
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
import json
|
| 4 |
+
import math
|
| 5 |
+
from typing import Dict, List, Tuple
|
| 6 |
+
|
| 7 |
+
import numpy as np
|
| 8 |
+
|
| 9 |
+
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
| 10 |
+
|
| 11 |
+
from utils.logger import setup_logger
|
| 12 |
+
from utils.json import get_file_path_from_config, field_getter, json_dumps
|
| 13 |
+
|
| 14 |
+
log = setup_logger(__name__)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def load_configs():
|
| 18 |
+
files = field_getter("config/files.json")
|
| 19 |
+
search = field_getter("config/search_model.json")
|
| 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 |
+
field_weights: Dict[str, float] = search("bm25f.field_weights")
|
| 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]]:
|
| 36 |
+
with open(tf_token_path, encoding="utf-8") as f:
|
| 37 |
+
docs = json.load(f)
|
| 38 |
+
vocab: set[str] = set()
|
| 39 |
+
for d in docs:
|
| 40 |
+
# doc全体tfから語彙を得る
|
| 41 |
+
for t in (d.get("tf") or {}).keys():
|
| 42 |
+
vocab.add(t)
|
| 43 |
+
return docs, vocab
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def stream_fasttext_vec(vec_path: str, vocab: set[str]) -> Tuple[Dict[str, int], np.ndarray]:
|
| 47 |
+
"""fastText .vec から、必要語彙のみ抽出してベクトル行列を返す。"""
|
| 48 |
+
token_to_idx: Dict[str, int] = {}
|
| 49 |
+
vectors: List[np.ndarray] = []
|
| 50 |
+
|
| 51 |
+
dim = None
|
| 52 |
+
kept = 0
|
| 53 |
+
with open(vec_path, encoding="utf-8", errors="ignore") as f:
|
| 54 |
+
header = f.readline()
|
| 55 |
+
# ヘッダ行は "<count> <dim>" のことが多い
|
| 56 |
+
try:
|
| 57 |
+
parts = header.strip().split()
|
| 58 |
+
if len(parts) >= 2 and parts[0].isdigit():
|
| 59 |
+
dim = int(parts[1])
|
| 60 |
+
except Exception:
|
| 61 |
+
pass
|
| 62 |
+
|
| 63 |
+
for line in f:
|
| 64 |
+
sp = line.rstrip().split(" ")
|
| 65 |
+
if len(sp) < 2:
|
| 66 |
+
continue
|
| 67 |
+
token = sp[0]
|
| 68 |
+
if token not in vocab:
|
| 69 |
+
continue
|
| 70 |
+
vec_vals = sp[1:]
|
| 71 |
+
if dim is None:
|
| 72 |
+
dim = len(vec_vals)
|
| 73 |
+
if len(vec_vals) != dim:
|
| 74 |
+
continue
|
| 75 |
+
try:
|
| 76 |
+
v = np.asarray([float(x) for x in vec_vals], dtype=np.float32)
|
| 77 |
+
except ValueError:
|
| 78 |
+
continue
|
| 79 |
+
# L2正規化
|
| 80 |
+
norm = np.linalg.norm(v)
|
| 81 |
+
if norm > 0:
|
| 82 |
+
v = v / norm
|
| 83 |
+
token_to_idx[token] = kept
|
| 84 |
+
vectors.append(v)
|
| 85 |
+
kept += 1
|
| 86 |
+
|
| 87 |
+
if dim is None:
|
| 88 |
+
raise RuntimeError(".vec の次元を特定できませんでした")
|
| 89 |
+
|
| 90 |
+
if not vectors:
|
| 91 |
+
log.warning("語彙に一致するベクトルが見つかりませんでした")
|
| 92 |
+
arr = np.zeros((0, dim), dtype=np.float32)
|
| 93 |
+
return token_to_idx, arr
|
| 94 |
+
|
| 95 |
+
arr = np.vstack(vectors).astype(np.float32)
|
| 96 |
+
return token_to_idx, arr
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def build_doc_vectors(
|
| 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, field_weights, target_fields = load_configs()
|
| 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):
|
| 159 |
+
log.warning(f".vec が見つかりません: {fasttext_vec_path}")
|
| 160 |
+
log.warning("Step 6 をスキップします")
|
| 161 |
+
sys.exit(0)
|
| 162 |
+
|
| 163 |
+
try:
|
| 164 |
+
docs, vocab = read_vocab_from_tf_token(tf_token_path)
|
| 165 |
+
except Exception as e:
|
| 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]}")
|
| 180 |
+
|
| 181 |
+
# 語彙インデックスの安定化(token_to_idxは追加順次第なのでソート)
|
| 182 |
+
sorted_tokens = sorted(token_to_idx.keys())
|
| 183 |
+
remap = {t: i for i, t in enumerate(sorted_tokens)}
|
| 184 |
+
remapped_vecs = np.zeros_like(word_vecs)
|
| 185 |
+
for t, old_i in token_to_idx.items():
|
| 186 |
+
new_i = remap[t]
|
| 187 |
+
remapped_vecs[new_i] = word_vecs[old_i]
|
| 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 |
+
|
scripts/build_all.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
import subprocess
|
| 4 |
+
|
| 5 |
+
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
| 6 |
+
|
| 7 |
+
from utils.logger import setup_logger
|
| 8 |
+
from utils.json import field_getter
|
| 9 |
+
|
| 10 |
+
log = setup_logger(__name__)
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def run_step(cmd: list[str], allow_fail: bool = False):
|
| 14 |
+
log.info("==> %s", " ".join(cmd))
|
| 15 |
+
try:
|
| 16 |
+
subprocess.run(cmd, check=True)
|
| 17 |
+
except subprocess.CalledProcessError as e:
|
| 18 |
+
if allow_fail:
|
| 19 |
+
log.warning(f"step失敗を無視します: {cmd} (code={e.returncode})")
|
| 20 |
+
else:
|
| 21 |
+
raise
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def main():
|
| 25 |
+
files = field_getter("config/files.json")
|
| 26 |
+
|
| 27 |
+
# Step 1: Sudachi user dict (optional but recommended before tokenization)
|
| 28 |
+
run_step([sys.executable, "scripts/1_build_dict.py"], allow_fail=True)
|
| 29 |
+
|
| 30 |
+
# Step 2: projects.json
|
| 31 |
+
run_step([sys.executable, "scripts/2_create_projects_data.py"])
|
| 32 |
+
|
| 33 |
+
# Step 3: synonyms cache
|
| 34 |
+
run_step([sys.executable, "scripts/3_build_synonyms_from_sudachi.py"]) # idempotent
|
| 35 |
+
|
| 36 |
+
# Step 4: bm25 meta
|
| 37 |
+
run_step([sys.executable, "scripts/4_prepare_bm25f_meta.py"]) # needs projects.json
|
| 38 |
+
|
| 39 |
+
# Step 5: tf_token
|
| 40 |
+
run_step([sys.executable, "scripts/5_prepare_tf_token.py"]) # needs projects.json
|
| 41 |
+
|
| 42 |
+
# Step 6: embeddings (.vec がある場合のみ)
|
| 43 |
+
vec_path = files("embeddings.fasttext_vec")
|
| 44 |
+
if os.path.exists(vec_path):
|
| 45 |
+
run_step([sys.executable, "scripts/6_build_word_embeddings.py"]) # needs tf_token, bm25_meta
|
| 46 |
+
else:
|
| 47 |
+
log.info(".vec が見つからないため Step 6 をスキップします: %s", vec_path)
|
| 48 |
+
|
| 49 |
+
log.info("全ステップ完了")
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
if __name__ == "__main__":
|
| 53 |
+
main()
|