embeddinggemma-2-lean / scripts /lean_lemma_retriever.py
flyyinghui's picture
Upload lean_lemma_retriever.py
ff241f2 verified
Raw History Blame Contribute Delete
8.16 kB
#!/usr/bin/env python3
"""
lean_lemma_retriever.py — EmbeddingGemma-2 Lean 引理检索器
接入 scientific-discovery-proof 技能 Stage 3 引理召回 (v2.22.0+)。
用途: 给定 Lean 定理名(或自由文本), 召回其依赖 DAG 中可能引用的候选引理/定义。
基座: google/embeddinggemma-2-740m, 在 openai_math Lean 依赖对(377K)上全参微调。
recall@10 = 0.5505 (增强文本口径: 名字词化 + 类型签名120 + docstring60)。
运行依赖(独立 venv, 勿用 Hermes 主 venv):
/mnt/d/AI_Training/EmbeddingGemma2_Lean检索模型_v2/venv/bin/python
torch 2.5.1+cu121 / transformers 5.19.0 / sentence-transformers 6.1.0
用法:
# 建索引(首次, 编码 18 万声明)
python lean_lemma_retriever.py --build-index
# 召回
python lean_lemma_retriever.py --query resolutionCokernelMap --top-k 10
# 自由文本召回
python lean_lemma_retriever.py --text "spectral gap of Laplacian" --top-k 10
"""
import re, json, os, sys, argparse
MODEL_DIR = "/mnt/d/AI_Training/EmbeddingGemma2_Lean检索模型_v2/embeddinggemma-2-lean"
DECLS_PATH = "/mnt/d/AI_Training/EmbeddingGemma2_Lean检索模型_v2/declarations.jsonl"
INDEX_DIR = "/mnt/d/AI_Training/EmbeddingGemma2_Lean检索模型_v2/index"
# ---------------------------------------------------------------- 文本口径
def name_to_text(name: str) -> str:
"""Lean 标识符 → 可读文本 (与训练口径一致)"""
s = re.sub(r'_+', ' ', name)
s = re.sub(r'([a-z0-9])([A-Z])', r'\1 \2', s)
return s.strip()
def enrich(name: str, decls: dict, sig_chars: int = 120, doc_chars: int = 60) -> str:
"""增强文本: 名字词化 | 类型签名 | docstring (与训练口径严格一致)"""
parts = [name_to_text(name)]
d = decls.get(name)
if d:
if d.get("signature"):
parts.append("type " + d["signature"][:sig_chars])
if d.get("docstring"):
parts.append(d["docstring"][:doc_chars])
return " | ".join(parts)
def load_decls(path: str = DECLS_PATH) -> dict:
decls = {}
with open(path, encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line:
continue
d = json.loads(line)
decls[d["name"]] = d
return decls
# ---------------------------------------------------------------- 模型与检索
class LemmaRetriever:
"""把 Lean 声明库编码为索引, 供 Stage 3 召回候选引理"""
def __init__(self, model, names, decls, normalize: bool = True):
self.model = model
self.names = list(names)
self.decls = decls
self.normalize = normalize
self.emb = None
def build_index(self, batch_size: int = 64, cache_dir: str = INDEX_DIR):
import numpy as np
txts = [enrich(n, self.decls) for n in self.names]
# 分批编码到 CPU(P620 4GB 显存下 18 万声明全量编码会 OOM,须逐批累积)
n = len(txts)
emb_batches = []
for i in range(0, n, batch_size):
chunk = txts[i:i + batch_size]
e = self.model.encode(
chunk, normalize_embeddings=self.normalize,
batch_size=batch_size, convert_to_numpy=True, show_progress_bar=False,
)
emb_batches.append(e)
if (i // batch_size) % 500 == 0 and i > 0:
print(f" [index] 已编码 {i}/{n}", flush=True)
emb = np.vstack(emb_batches)
self.emb = __import__("torch").from_numpy(emb)
# 缓存
os.makedirs(cache_dir, exist_ok=True)
np.save(os.path.join(cache_dir, "emb.npy"), emb)
with open(os.path.join(cache_dir, "names.json"), "w", encoding="utf-8") as f:
json.dump(self.names, f)
return self.emb
def load_index(self, cache_dir: str = INDEX_DIR):
import numpy as np
import torch
emb = np.load(os.path.join(cache_dir, "emb.npy"))
with open(os.path.join(cache_dir, "names.json"), encoding="utf-8") as f:
self.names = json.load(f)
self.emb = torch.from_numpy(emb)
return self.emb
def _encode_query(self, text: str):
return self.model.encode(
[text], normalize_embeddings=self.normalize, convert_to_tensor=True
)
def recall(self, theorem_name: str, top_k: int = 10):
"""给定定理名, 召回候选依赖 (用增强文本口径)"""
q = self._encode_query(enrich(theorem_name, self.decls))
return self._topk(q, top_k)
def recall_by_text(self, query_text: str, top_k: int = 10):
"""自由文本召回"""
q = self._encode_query(query_text)
return self._topk(q, top_k)
def _topk(self, q, top_k: int):
import torch
# 统一到 CPU 做矩阵乘法(18万×768 的 matmul CPU 上仅数百毫秒)
q_cpu = q.cpu() if hasattr(q, "is_cuda") and q.is_cuda else q
emb = self.emb.cpu() if hasattr(self.emb, "is_cuda") and self.emb.is_cuda else self.emb
sim = (q_cpu @ emb.T)[0]
k = min(top_k, len(self.names))
idx = torch.topk(sim, k=k).indices.tolist()
return [(self.names[i], float(sim[i])) for i in idx]
def load_model(device: str = "cpu", dtype: str = "float32"):
"""加载模型. P620(Pascal) 不支持 bf16, 默认 float32."""
from sentence_transformers import SentenceTransformer
kwargs = {}
if dtype == "float32":
import torch
kwargs["torch_dtype"] = torch.float32
model = SentenceTransformer(MODEL_DIR, device=device, model_kwargs=kwargs)
model.max_seq_length = 64
return model
# ---------------------------------------------------------------- CLI
def main():
global MODEL_DIR, DECLS_PATH
ap = argparse.ArgumentParser(description="EmbeddingGemma-2 Lean 引理检索器")
ap.add_argument("--model-dir", default=MODEL_DIR)
ap.add_argument("--decls", default=DECLS_PATH)
ap.add_argument("--device", default="cpu", choices=["cpu", "cuda"])
ap.add_argument("--dtype", default="float32", choices=["float32", "bfloat16"])
ap.add_argument("--build-index", action="store_true", help="编码声明库并缓存")
ap.add_argument("--query", type=str, help="定理名召回")
ap.add_argument("--text", type=str, help="自由文本召回")
ap.add_argument("--top-k", type=int, default=10)
args = ap.parse_args()
MODEL_DIR = args.model_dir
DECLS_PATH = args.decls
print(f"[load] 加载模型 {MODEL_DIR} (device={args.device}, dtype={args.dtype})", flush=True)
model = load_model(args.device, args.dtype)
print("[load] 模型加载完成", flush=True)
print(f"[load] 加载声明库 {DECLS_PATH}", flush=True)
decls = load_decls(args.decls)
names = list(decls.keys())
print(f"[load] 声明库 {len(names)} 条", flush=True)
retriever = LemmaRetriever(model, names, decls)
if args.build_index:
print("[index] 编码声明库...", flush=True)
retriever.build_index()
print(f"[index] 完成, 缓存到 {INDEX_DIR}", flush=True)
return
# 尝试加载缓存索引
import os as _os
emb_path = _os.path.join(INDEX_DIR, "emb.npy")
if _os.path.exists(emb_path):
print("[index] 加载缓存索引...", flush=True)
retriever.load_index()
else:
print("[index] 无缓存, 编码声明库 (约 2 分钟 GPU / 30+ 分钟 CPU)...", flush=True)
retriever.build_index()
if args.query:
print(f"[recall] 定理名: {args.query}", flush=True)
for name, score in retriever.recall(args.query, args.top_k):
d = decls.get(name, {})
kind = d.get("kind", "?")
print(f" {score:.4f} [{kind}] {name}")
elif args.text:
print(f"[recall] 文本: {args.text}", flush=True)
for name, score in retriever.recall_by_text(args.text, args.top_k):
d = decls.get(name, {})
kind = d.get("kind", "?")
print(f" {score:.4f} [{kind}] {name}")
else:
print("用法: 指定 --query 或 --text")
if __name__ == "__main__":
main()