Corin1998 commited on
Commit
c67190f
·
verified ·
1 Parent(s): e9bc222

Update irpr/deps.py

Browse files
Files changed (1) hide show
  1. irpr/deps.py +122 -66
irpr/deps.py CHANGED
@@ -1,20 +1,107 @@
1
- from sentence_transformers import SentenceTransformer
2
- from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline
3
- import os, pickle, torch
4
  import numpy as np
 
 
 
 
 
 
 
5
 
6
  from irpr.config import settings
7
 
8
- # データディレクトリの存在を保証
9
- os.makedirs(settings.DATA_DIR, exist_ok=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
- # ---- Embeddings -------------------------------------------------------------
12
- emb_model = SentenceTransformer(settings.EMB_MODEL)
13
- EMB_DIM = emb_model.get_sentence_embedding_dimension()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
- def embed_texts(texts: list[str]) -> np.ndarray:
16
- # すでに正規化付きで取得
17
- v = emb_model.encode(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  texts,
19
  normalize_embeddings=True,
20
  convert_to_numpy=True,
@@ -22,91 +109,60 @@ def embed_texts(texts: list[str]) -> np.ndarray:
22
  )
23
  return v.astype(np.float32, copy=False)
24
 
25
- # ---- シンプル永続インデックス(NumPyのみ) ------------------------------------
26
- # ベクトル: data/vectors.npy(shape: [N, D], float32, 正規化済み)
27
- # メタ情報: data/store.pkl (list[dict])
28
- VEC_PATH = os.path.join(settings.DATA_DIR, "vectors.npy")
29
- STORE_PATH = os.path.join(settings.DATA_DIR, "store.pkl")
30
-
31
- # 初期化
32
- if os.path.exists(VEC_PATH):
33
- vectors = np.load(VEC_PATH, mmap_mode=None)
34
- # guard: 既存が空ファイルの可能性
35
- if vectors.ndim != 2 or vectors.shape[1] != EMB_DIM:
36
- vectors = np.empty((0, EMB_DIM), dtype=np.float32)
37
- else:
38
- vectors = np.empty((0, EMB_DIM), dtype=np.float32)
39
-
40
- if os.path.exists(STORE_PATH):
41
- with open(STORE_PATH, "rb") as f:
42
- store = pickle.load(f)
43
- if not isinstance(store, list):
44
- store = []
45
- else:
46
- store = []
47
-
48
- def _save_index():
49
- np.save(VEC_PATH, vectors)
50
- with open(STORE_PATH, "wb") as f:
51
- pickle.dump(store, f)
52
 
53
- def add_to_index(records: list[dict]):
54
  """
55
  records: [{"text":..., "source_url":..., "title":..., "doc_id":..., "chunk_id":...}]
56
  """
57
- global vectors, store
58
  if not records:
59
  return
 
60
  vecs = embed_texts([r["text"] for r in records]) # [M, D]
61
- if vectors.size == 0:
62
- vectors = vecs
 
63
  else:
64
- vectors = np.vstack([vectors, vecs])
65
- store.extend(records)
66
  _save_index()
67
 
68
  def search(query: str, top_k=8):
69
  """
70
- 内積(cos類似に相当:両方正規化済み)でトップKを返す簡易検索
71
  """
72
- if vectors.size == 0 or len(store) == 0:
 
73
  return []
74
 
75
- qv = embed_texts([query])[0] # [D]
76
- sims = vectors @ qv # [N] 内積
77
- if top_k > sims.shape[0]:
78
- top_k = sims.shape[0]
79
- idx = np.argpartition(-sims, top_k-1)[:top_k]
80
- # スコア順に並べ替え
81
  idx = idx[np.argsort(-sims[idx])]
82
 
83
  hits = []
84
- for rank, i in enumerate(idx.tolist()):
85
- rec = store[i].copy()
86
  rec["score"] = float(sims[i])
87
  hits.append(rec)
88
  return hits
89
 
90
- # ---- 生成モデル --------------------------------------------------------------
91
- tok = AutoTokenizer.from_pretrained(settings.GEN_MODEL)
92
- model = AutoModelForCausalLM.from_pretrained(
93
- settings.GEN_MODEL,
94
- torch_dtype=torch.bfloat16 if torch.cuda.is_available() else torch.float32,
95
- device_map="auto",
96
- low_cpu_mem_usage=True
97
- )
98
- gen = pipeline("text-generation", model=model, tokenizer=tok)
99
-
100
- def generate_chat(messages: list[dict], max_new_tokens=800, temperature=0.2):
101
  """
102
  messages: [{"role":"system"/"user"/"assistant", "content":"..."}]
103
  """
 
104
  prompt = tok.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
105
  out = gen(
106
  prompt,
107
  do_sample=(temperature > 0.0),
108
  temperature=temperature,
109
- max_new_tokens=max_new_tokens
110
  )[0]["generated_text"]
111
  generated = out[len(prompt):].strip()
112
  return generated or out
 
1
+ import os
2
+ import pickle
 
3
  import numpy as np
4
+ import torch
5
+
6
+ # ---- キャッシュ/データディレクトリを先に用意(import 時でも安全) -----------------
7
+ CACHE_DIR = os.environ.get("HF_HOME") or "/tmp/hf-cache"
8
+ for k in ["HF_HOME", "TRANSFORMERS_CACHE", "SENTENCE_TRANSFORMERS_HOME", "TORCH_HOME"]:
9
+ os.environ.setdefault(k, CACHE_DIR)
10
+ os.makedirs(CACHE_DIR, exist_ok=True)
11
 
12
  from irpr.config import settings
13
 
14
+ DATA_DIR = settings.DATA_DIR or "data"
15
+ os.makedirs(DATA_DIR, exist_ok=True)
16
+
17
+ # 永続化パス(NumPyベースの簡易インデックス)
18
+ VEC_PATH = os.path.join(DATA_DIR, "vectors.npy")
19
+ STORE_PATH = os.path.join(DATA_DIR, "store.pkl")
20
+
21
+ # ---- 遅延ロード用のシングルトン ---------------------------------------------------
22
+ from typing import Optional, List, Dict
23
+ from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline
24
+ from sentence_transformers import SentenceTransformer
25
+
26
+ _EMB: Optional[SentenceTransformer] = None
27
+ _EMB_DIM: Optional[int] = None
28
+
29
+ _TOK = None
30
+ _MODEL = None
31
+ _GEN = None
32
+
33
+ _VECTORS: Optional[np.ndarray] = None # shape [N, D], float32, L2正規化済み
34
+ _STORE: Optional[List[Dict]] = None # メタ情報の配列
35
 
36
+ # ---- 低レベルユーティリティ -------------------------------------------------------
37
+ def _load_index():
38
+ """ベクトルとメタ情報を遅延ロード"""
39
+ global _VECTORS, _STORE, _EMB_DIM
40
+ if _VECTORS is None:
41
+ if os.path.exists(VEC_PATH):
42
+ try:
43
+ vec = np.load(VEC_PATH)
44
+ _VECTORS = vec.astype(np.float32, copy=False)
45
+ except Exception:
46
+ _VECTORS = np.empty((0, _EMB_DIM or 0), dtype=np.float32)
47
+ else:
48
+ _VECTORS = np.empty((0, _EMB_DIM or 0), dtype=np.float32)
49
+ if _STORE is None:
50
+ if os.path.exists(STORE_PATH):
51
+ try:
52
+ with open(STORE_PATH, "rb") as f:
53
+ s = pickle.load(f)
54
+ _STORE = s if isinstance(s, list) else []
55
+ except Exception:
56
+ _STORE = []
57
+ else:
58
+ _STORE = []
59
 
60
+ def _save_index():
61
+ global _VECTORS, _STORE
62
+ if _VECTORS is None or _STORE is None:
63
+ return
64
+ # np.save はディレクトリが必要
65
+ os.makedirs(os.path.dirname(VEC_PATH), exist_ok=True)
66
+ np.save(VEC_PATH, _VECTORS)
67
+ with open(STORE_PATH, "wb") as f:
68
+ pickle.dump(_STORE, f)
69
+
70
+ # ---- モデル遅延ロード -------------------------------------------------------------
71
+ def _get_emb_model() -> SentenceTransformer:
72
+ """SentenceTransformer を初回呼び出し時にロード"""
73
+ global _EMB, _EMB_DIM
74
+ if _EMB is None:
75
+ model_name = settings.EMB_MODEL or "intfloat/multilingual-e5-base"
76
+ _EMB = SentenceTransformer(model_name, cache_folder=CACHE_DIR)
77
+ _EMB_DIM = _EMB.get_sentence_embedding_dimension()
78
+ # 既存ベクトル配列のD次元を確定させる
79
+ _load_index()
80
+ if _VECTORS is not None and _VECTORS.size == 0:
81
+ # 空なら次元を確定
82
+ _set_vectors(np.empty((0, _EMB_DIM), dtype=np.float32))
83
+ return _EMB
84
+
85
+ def _get_gen_pipeline():
86
+ """text-generation pipeline を初回呼び出し時にロード"""
87
+ global _TOK, _MODEL, _GEN
88
+ if _GEN is None:
89
+ gen_name = settings.GEN_MODEL or "Qwen/Qwen2.5-3B-Instruct"
90
+ _TOK = AutoTokenizer.from_pretrained(gen_name, cache_dir=CACHE_DIR)
91
+ _MODEL = AutoModelForCausalLM.from_pretrained(
92
+ gen_name,
93
+ cache_dir=CACHE_DIR,
94
+ torch_dtype=torch.bfloat16 if torch.cuda.is_available() else torch.float32,
95
+ device_map="auto",
96
+ low_cpu_mem_usage=True
97
+ )
98
+ _GEN = pipeline("text-generation", model=_MODEL, tokenizer=_TOK)
99
+ return _GEN, _TOK
100
+
101
+ # ---- ベクトル化と検索 -------------------------------------------------------------
102
+ def embed_texts(texts: List[str]) -> np.ndarray:
103
+ emb = _get_emb_model()
104
+ v = emb.encode(
105
  texts,
106
  normalize_embeddings=True,
107
  convert_to_numpy=True,
 
109
  )
110
  return v.astype(np.float32, copy=False)
111
 
112
+ def _set_vectors(arr: np.ndarray):
113
+ """内部ベクトル配列を安全に差し替え"""
114
+ global _VECTORS
115
+ _VECTORS = arr.astype(np.float32, copy=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
 
117
+ def add_to_index(records: List[Dict]):
118
  """
119
  records: [{"text":..., "source_url":..., "title":..., "doc_id":..., "chunk_id":...}]
120
  """
 
121
  if not records:
122
  return
123
+ _load_index()
124
  vecs = embed_texts([r["text"] for r in records]) # [M, D]
125
+ global _VECTORS, _STORE
126
+ if _VECTORS is None or _VECTORS.size == 0:
127
+ _set_vectors(vecs)
128
  else:
129
+ _set_vectors(np.vstack([_VECTORS, vecs]))
130
+ _STORE.extend(records)
131
  _save_index()
132
 
133
  def search(query: str, top_k=8):
134
  """
135
+ 内積ベース(両ベクトル正規化済み→cos 類似)で上位K件を返す
136
  """
137
+ _load_index()
138
+ if _VECTORS is None or _VECTORS.size == 0 or not _STORE:
139
  return []
140
 
141
+ qv = embed_texts([query])[0] # [D]
142
+ sims = _VECTORS @ qv # [N]
143
+ top_k = min(top_k, sims.shape[0])
144
+ idx = np.argpartition(-sims, top_k - 1)[:top_k]
 
 
145
  idx = idx[np.argsort(-sims[idx])]
146
 
147
  hits = []
148
+ for i in idx.tolist():
149
+ rec = _STORE[i].copy()
150
  rec["score"] = float(sims[i])
151
  hits.append(rec)
152
  return hits
153
 
154
+ # ---- 生成ユーティリティ ----------------------------------------------------------
155
+ def generate_chat(messages: list[dict], max_new_tokens=800, temperature=0.2) -> str:
 
 
 
 
 
 
 
 
 
156
  """
157
  messages: [{"role":"system"/"user"/"assistant", "content":"..."}]
158
  """
159
+ gen, tok = _get_gen_pipeline()
160
  prompt = tok.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
161
  out = gen(
162
  prompt,
163
  do_sample=(temperature > 0.0),
164
  temperature=temperature,
165
+ max_new_tokens=max_new_tokens,
166
  )[0]["generated_text"]
167
  generated = out[len(prompt):].strip()
168
  return generated or out