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

这里有几个参数,niter代表kmeans的迭代次数,一般30就足够了,nredo代表重试次数,faiss构建kmeans时会多次重复train kmeans然后对比最终loss来判断哪次聚类最好,然后最终返回那个最好的聚类中心,这个参数一般选1就好,不放心可以选个2~5之间的数,再多就不礼貌了。0~0。

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()



posted @ 2023-03-20 14:50  15375357604  阅读(208)  评论(0编辑  收藏  举报