query改写
Posted 15375357604
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了query改写相关的知识,希望对你有一定的参考价值。
标准query库的构建,如何才能打造一个高质量的标准query库
- 前面说了,query改写模块主要是为了让高频query的错体、变体归一,所以query库中就必须包含头部pv部分的query。
- 其次有些运营类的query,比如白名单的query,或者商业策略规定的买词等query也需要加入。
- 还有一些规则类的词、app名称、当下火爆的一些梗或者新事物新词也需要包含进去。
- 标准库绝大部分query的来源,就是在海量的用户输入query中用K-means的方式聚类,将离主类中心最近的query作为我们的标准库中的query,这个具体需要多少中心query需要自己判断,一般来说其实不用太多,万级别足够了。
什么?K-means速度太慢?这里推荐使用faiss gpu版本自带的K-means,目前500w query、1w的聚类中心大约半小时即可聚类完成,使用方法如下:
def train_kmeans(input_vecs, k_centers, niter=30, redo=10):
model = faiss.Kmeans(
input_vecs.shape[-1],
k_centers, niter=niter, gpu=True, max_points_per_centroid=int(1e7), verbose=True, nredo=redo, seed=42)
model.train(input_vecs.astype(np.float32))
return model
query embedding的方式,如何才能在短query场景下充分的表示信息
这个我司目前使用的是苏剑林开源的无监督的预训练模型simbert(4层312维),最后再加一层whitening解决空间坍缩问题,向量最终被whitening压缩至256维,想要详细了解simbert和whitening的同学可以移步苏神的文章:
有条件(主要是有时间+有钱)的大佬们可以尝试标注数据训练自己的有监督相似检索模型,并且在评论区留下微信,请务必让我成为你的朋友,你可以免费得到一个大腿挂件。0w0。
其实笔者之前也使用过无监督simcse,不知道是打开方式不对还是场景不合适,simcse的效果不如simbert,当然也不会差到哪去,感兴趣的同学可以多尝试尝试各种SOTA模型。
总之simbert + whitening是一个相当不错的baseline,而且比较百搭,不论是短query场景还是中长query场景都表现相当稳定。
bert whitening: def compute_kernel_bias(vecs, n_components=256): """计算kernel和bias vecs.shape = [num_samples, embedding_size], 最后的变换:y = (x + bias).dot(kernel) """ mu = vecs.mean(axis=0, keepdims=True) cov = np.cov(vecs.T) u, s, vh = np.linalg.svd(cov) W = np.dot(u, np.diag(1 / np.sqrt(s))) return W[:, :n_components], -mu def transform_and_normalize(vecs, kernel=None, bias=None): """ 最终向量标准化 """ if not (kernel is None or bias is None): vecs = (vecs + bias).dot(kernel) return vecs / (vecs**2).sum(axis=1, keepdims=True)**0.5 v_data = np.array(v_data) kernel,bias=compute_kernel_bias(v_data,256) v_data=transform_and_normalize(v_data, kernel=kernel, bias=bias)
线上部署Bert和Faiss遇到的问题
诡异的多进程性能反而下降
准备好上面的所有物料后,笔者开始将query rewrite部署上线,由于我司的query rewrite模块是query parser中的一部分,query parser是我司使用纯python编写的一个后端,是综合了多种query理解功能的一个服务,在上线rewrite模块前为了应对线上高并发场景,开启了十个进程,当笔者用同样的进程数部署了query rewrite时发现了诡异的一幕:
- 本来离线测试P99 6ms的onnx bert线上P99耗时飙升到了100ms;
- 本来离线测试P99 0.5ms的HNSW index也耗时飙到了100ms左右;
- 按道理进程越多性能越高才对,但是现在线上压测完全不达标,发生了肾么情况?0.o?
原来python自编程序一般没办法使用其他的核,所以我们的原始代码使用多进程来提高并发性能。但是onnx和faiss其实都是有多进程优化的,天生就可以使用其他的核,导致进程数开的越多,进程之间的抢资源现象会越严重,从而导致线上推理和检索没办法速度很快。
对于faiss如何解决这个问题,只需要设置如下环境变量即可:
- 如果想在python里面配置:
os.environ["MKL_NUM_THREADS"] = \'1\'
os.environ["NUMEXPR_NUM_THREADS"] = \'1\'
os.environ["OMP_NUM_THREADS"] = \'1\'
- 如果想直接在启动shell脚本里配置:
export MKL_NUM_THREADS=1
export NUMEXPR_NUM_THREADS=1
export OMP_NUM_THREADS=1
但是对于onnx bert来说,以上办法还是不行,最后笔者发现进程数由10减少至2才能使性能和离线一致,最近笔者仍然在解决这个问题,目前的切入点是多进程间共享内存的方法。
Python自带lru_cache优雅的进一步减轻线上压力
为了让高频出现的query不再被重新推理,笔者使用了python自带的cache方法,大大的帮我们缓解了线上压力,python自带的cache使用起来非常方便和优雅,而且线程安全,只需要使用装饰器lru_cache即可做到:
def lru_search_init(max_cache_length):
@lru_cache(max_cache_length, typed=False)
def lru_search(text: str, topk: int) -> (ndarray, ndarray, ndarray):
# your search code
return
return lru_search
searcher = lru_search_init(1024)
像上述代码一样构建的searcher就具备了lru_cache的能力,max_cache_length为最大缓存的条数,如果输入为None则为全部缓存,不建议,会爆内存,如果输入为0或者负数则代表不缓存。如果想清空缓存,可以使用:
searcher.cache_clear()
以上是关于query改写的主要内容,如果未能解决你的问题,请参考以下文章
推荐系统[九]项目技术细节讲解z2:搜索Query理解[Term WeightQuery 改写同义词扩写]和语义召回技术