mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-21 09:28:14 +00:00
refactor(clip): CLIP 收敛为稠密 MultimodalEmbedder,不污染稀疏 Vectorizer/TF-IDF 语义
文本相似度检索是层次化系统:TF-IDF 高频削弱加权(idf<0.1 丢弃)+ 倒排剪枝(只召回共享特征者)+ cosine。CLIP 512 维稠密向量若以 map[string]float64 稀疏形式实现 vector.Vectorizer 并塞进 vector.Store, 会让 512 维全部成为倒排 key → 候选集≈全库、剪枝失效,且绕过 TF-IDF 高频削弱,与既有文本检索语义错配。 收敛: - vector.MultimodalEmbedder 改为独立稠密接口(VectorizeDense/ EmbedImageDense/Fingerprint/Dim/Loaded/Close),不再继承稀疏 Vectorizer - clip.Embedder 删除稀疏垫片 Vectorize/EmbedImage/denseToVector, 只产出稠密向量;文档/知识/上下文层继续用 TF-IDF/fastText 稀疏路径 - 分层明确:文本→文本走 TF-IDF/fastText;文本↔图像、图像↔图像走 CLIP 稠密 QueryMedia(媒体层独立稠密余弦,原样保留)
This commit is contained in:
@ -1,20 +1,22 @@
|
||||
//go:build onnxruntime
|
||||
|
||||
// Package clip 提供基于 CLIP ONNX 的多模态向量化器。
|
||||
// Package clip 提供基于 CLIP ONNX 的稠密多模态编码器。
|
||||
//
|
||||
// 构建标签 onnxruntime 控制是否编译此实现(与 internal/nlp/onnx.go 同模式)。
|
||||
// 未配置 clip_model_dir 时不会初始化 ONNX Runtime,现有 fastText/TF-IDF 行为不变。
|
||||
//
|
||||
// 支持的模型文件(统一放置于 clip_model_dir 目录):
|
||||
//
|
||||
// text.onnx — CLIP 文本编码器(input_ids + attention_mask → text_features [1,512])
|
||||
// vision.onnx — CLIP 图像编码器(pixel_values → image_features [1,512])
|
||||
// text.onnx — CLIP 文本编码器(input_ids + attention_mask → text_embed [1,512])
|
||||
// vision.onnx — CLIP 图像编码器(pixel_values → image_embed [1,512])
|
||||
// clip_config.json — 模型元数据(dimension, context_length, image_size, mean, std)
|
||||
// tokenizer.json — HuggingFace tokenizer.json(含 vocab + merges)
|
||||
// merges.txt — BPE merges 文件(CLIP 使用的字节级 BPE)
|
||||
// merges.txt — BPE merges 文件
|
||||
//
|
||||
// 设计:同时满足 vector.Vectorizer 接口(稀疏 map,供文档检索复用)和直接返回
|
||||
// []float64 的方法(供媒体嵌入与 QueryMedia 直接调用)。
|
||||
// 设计:只产出**稠密** 512 维向量(VectorizeDense / EmbedImageDense),供媒体层
|
||||
// media.Store 的稠密余弦检索(QueryMedia)消费。刻意不实现 vector.Vectorizer
|
||||
// (稀疏词向量)——文档/知识/上下文层的文本相似度检索保留 TF-IDF 高频削弱加权 +
|
||||
// 倒排剪枝 + fastText 稀疏词向量的既有设计,CLIP 稠密空间不混入那套索引。
|
||||
package clip
|
||||
|
||||
import (
|
||||
@ -30,11 +32,9 @@ import (
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/vector"
|
||||
ort "github.com/yalue/onnxruntime_go"
|
||||
)
|
||||
|
||||
@ -48,7 +48,12 @@ type clipConfig struct {
|
||||
Std []float64 `json:"std"`
|
||||
}
|
||||
|
||||
// Embedder 实现 vector.Vectorizer,提供文本向量化与图像向量化。
|
||||
// Embedder 是 CLIP 多模态稠密编码器,实现 vector.MultimodalEmbedder。
|
||||
// 产出 512 维共享稠密空间向量,供 media.Store 的稠密余弦检索。
|
||||
//
|
||||
// 刻意**不**实现 vector.Vectorizer(稀疏词向量):CLIP 稠密向量若以
|
||||
// map[string]float64 稀疏形式塞进文档/知识层的 vector.Store,会破坏其
|
||||
// TF-IDF 高频削弱加权与倒排剪枝语义。文本层的相似度检索保持 TF-IDF/fastText。
|
||||
type Embedder struct {
|
||||
mu sync.RWMutex
|
||||
config clipConfig
|
||||
@ -83,25 +88,6 @@ func (e *Embedder) Loaded() bool {
|
||||
return e.loaded
|
||||
}
|
||||
|
||||
// Vectorize 将文本转为向量,供 vector.Vectorizer 接口使用(稀疏 map)。
|
||||
func (e *Embedder) Vectorize(text string) vector.Vector {
|
||||
dense, err := e.VectorizeDense(text)
|
||||
if err != nil {
|
||||
log.Printf("[clip] Vectorize 失败: %v", err)
|
||||
return vector.Vector{}
|
||||
}
|
||||
return denseToVector(dense)
|
||||
}
|
||||
|
||||
// EmbedImage 将图像字节转为向量,供 vector.Vectorizer 接口使用(稀疏 map)。
|
||||
func (e *Embedder) EmbedImage(img []byte, mime string) (vector.Vector, error) {
|
||||
dense, err := e.EmbedImageDense(img, mime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return denseToVector(dense), nil
|
||||
}
|
||||
|
||||
// VectorizeDense 将文本转为归一化的 []float64 向量(CLIP 共享空间)。
|
||||
func (e *Embedder) VectorizeDense(text string) ([]float64, error) {
|
||||
e.mu.RLock()
|
||||
@ -378,17 +364,6 @@ func resizeImage(src image.Image, dstW, dstH int) image.Image {
|
||||
return dst
|
||||
}
|
||||
|
||||
// denseToVector 将 []float64 稀疏化为 vector.Vector(CLIP 维度通常 512,不会太大)。
|
||||
func denseToVector(d []float64) vector.Vector {
|
||||
vec := make(vector.Vector, len(d))
|
||||
for i, v := range d {
|
||||
if v != 0 {
|
||||
vec[strconv.Itoa(i)] = v
|
||||
}
|
||||
}
|
||||
return vec
|
||||
}
|
||||
|
||||
// ---- BPE Tokenizer ----
|
||||
|
||||
// loadTokenizerVocab 从 HuggingFace tokenizer.json 中提取 vocab(token→id 映射)。
|
||||
|
||||
@ -4,12 +4,10 @@ package clip
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/vector"
|
||||
)
|
||||
|
||||
// Embedder 在未启用 onnxruntime 时为 no-op 实现。
|
||||
// 构建时不链接 onnxruntime,default 构建保持原有 fastText/TF-IDF 行为不变。
|
||||
// 构建时不链接 onnxruntime,默认构建保持原有 fastText/TF-IDF 行为不变。
|
||||
type Embedder struct {
|
||||
loaded bool
|
||||
}
|
||||
@ -18,17 +16,13 @@ func New(_ string) (*Embedder, error) {
|
||||
return nil, fmt.Errorf("clip embedder requires build tag 'onnxruntime' (go build -tags onnxruntime)")
|
||||
}
|
||||
|
||||
func (e *Embedder) Fingerprint() string { return "" }
|
||||
func (e *Embedder) Dim() int { return 0 }
|
||||
func (e *Embedder) Loaded() bool { return e.loaded }
|
||||
func (e *Embedder) Vectorize(_ string) vector.Vector { return nil }
|
||||
func (e *Embedder) EmbedImage(_ []byte, _ string) (vector.Vector, error) {
|
||||
return nil, fmt.Errorf("clip embedder not available")
|
||||
}
|
||||
func (e *Embedder) Fingerprint() string { return "" }
|
||||
func (e *Embedder) Dim() int { return 0 }
|
||||
func (e *Embedder) Loaded() bool { return e.loaded }
|
||||
func (e *Embedder) Close() {}
|
||||
func (e *Embedder) VectorizeDense(_ string) ([]float64, error) {
|
||||
return nil, fmt.Errorf("clip embedder not available")
|
||||
}
|
||||
func (e *Embedder) EmbedImageDense(_ []byte, _ string) ([]float64, error) {
|
||||
return nil, fmt.Errorf("clip embedder not available")
|
||||
}
|
||||
func (e *Embedder) Close() {}
|
||||
|
||||
@ -58,12 +58,12 @@ func TestSmokeLoadAndEncode(t *testing.T) {
|
||||
t.Errorf("cat-dog sim (%.4f) should be > cat-physics sim (%.4f)", sim12, sim13)
|
||||
}
|
||||
|
||||
// 通过 Vectorizer 接口(稀疏 map)
|
||||
sparseVec := emb.Vectorize("hello world")
|
||||
if len(sparseVec) == 0 {
|
||||
t.Error("sparse Vectorize should return non-empty")
|
||||
// 稠密 VectorizeDense 再次调用验证可重复
|
||||
vAgain, _ := emb.VectorizeDense("hello world")
|
||||
if len(vAgain) != 512 {
|
||||
t.Errorf("dense VectorizeDense len = %d, want 512", len(vAgain))
|
||||
}
|
||||
fmt.Printf("sparse len = %d\n", len(sparseVec))
|
||||
fmt.Printf("dense len = %d\n", len(vAgain))
|
||||
}
|
||||
|
||||
// TestCrossModalAlignment 验证图文在同一向量空间可比:
|
||||
|
||||
@ -17,10 +17,12 @@ type Vectorizer interface {
|
||||
EmbedImage(img []byte, mime string) (Vector, error)
|
||||
}
|
||||
|
||||
// MultimodalEmbedder 扩展 Vectorizer,提供直接返回 dense 向量的方法
|
||||
// 与模型生命周期管理。CLIP 等视觉向量化器实现此接口;未启用时用空 stub。
|
||||
// MultimodalEmbedder 是稠密多模态编码器的接口(CLIP 等视觉-文本联合模型)。
|
||||
//
|
||||
// 与 Vectorizer(稀疏词向量,供 TF-IDF/倒排检索)刻意区分:多模态模型产出的
|
||||
// 是共享稠密空间(如 CLIP 512 维),直接用于 media.Store 的稠密余弦检索,
|
||||
// **不得**塞进文档/知识层的稀疏 vector.Store(会破坏倒排剪枝与 TF-IDF 语义)。
|
||||
type MultimodalEmbedder interface {
|
||||
Vectorizer
|
||||
VectorizeDense(text string) ([]float64, error)
|
||||
EmbedImageDense(img []byte, mime string) ([]float64, error)
|
||||
Fingerprint() string
|
||||
|
||||
Reference in New Issue
Block a user