实时向量的匹配查询

895 阅读10分钟

构建原理与目的介绍

实际需求

在问答系统构建中,常常会涉及到实时查询,想要查询结果详细准确,就要保证问答数据足够多,但带来的问题就是:问答数据庞大。 为了解决这种问题,各个行业大佬都进行了开拓式的研究,其中非常让人吃惊的一个项目就是向量数据库,专门用于向量匹配。在实际的搭建过程中笔者遇到了一些难点,并最终决定手动复现这一能力。

实现方法

1 将问答库中的标准问题文本进行提取;
2 对标准问题进行向量化编码(此处编码方法有很多种,感兴趣的读者可以自行搜索编码相关问题);
3 在数据库中复现两张表,分别是“编码+问题索引表”和“问题索引+答案表”;
4 在查询时,对输入问题进行同组的编码处理,在数据库中进行匹配,并通过索引返回问题的答案。

实现原理

1 使用Python与MySQL进行交互,通过查询的方式进行单条数据的快速检索,避免在进行少量/极少量数据查询时,仍旧需要读取海量基础数据(矩阵/网络/向量库)造成的实时性降低问题。
2 通过在MySQL中进行数据的向量化(类相似矩阵)的方式,降低在查询过程中复杂度的同时,保留数据的匹配精度。
3 引入向量匹配权重(实际使用时进行实施),实现重点信息重点关注。

mysql安装

1 进入官网下载页面 image.png

2 页面下拉,进入社区版入口 image.png

3 进入安装程序下载 image.png

4 下载工具 image.png

image.png

5 安装 image.png image.png 往下确认安装

MySQL到此处安装结束,此处MySQL还未进行配置,暂时还无法进行接口调用,需要自身配置。

6 MySQL8.x配置

当mysql版本大于等于8.0时,根据密码进行的加密规则发生改变,需要以手动方式重新定义加密规则,进入MySQL后进行如下操作: image.png

-- 进入mysql库
use mysql;

-- 修改加密规则
alter user '/root/'@'/localhost/' identified with mysql_native_password by '/password/'; 

-- 更新用户的密码
alter user '/root/'@'/localhost/' identified by '/password/' password expire never;

-- 刷新权限
flush privileges;

-- /root/:自定义的用户名
-- /localhost/:用户开放IP(localhost / IP / %)
-- /password/:密码

Python安装

image.png

image.png

image.png

anaconda剩余安装步骤较为简单,此处不做详细记录。

完成安装后,开始进行系统构建,此处提供数据

系统构建

1 基础数据的加工

在常见的数据中,一条信息往往具有多个特征(成百上千,少了不谈),此处想要使用特征时就会出现使用困难的情况,因为在实际使用过程中,一方面受到有效特征的限制(有的特征是正确的但是对我们没有价值),另一方面受到数据质量的影响(可能在数据加工过程中,某个特征是错误的),这就使得我们需要一种方式,即保证数据匹配的准确性,又能保证存在一定的误差范围,用以保证泛化能力。

在这个基础上,向量化的方式就显得较为可行:

1 创建一个标向量

2 所有数据转化成基于标向量的距离向量/距离(以欧式距离进行演示)

a类特征b类特征
标向量111111
向量1369226
向量2258462
向量3355225

将各个向量按照特征组别进行对标向量的欧式距离计算,可以得到:

a类特征b类特征
向量19.645.20
向量28.125.92
向量36.004.24

使用标向量作为中转,并进行按照特征组进行数据合并压缩的方法有如下两点优点:

1 对于一个未知的特征向量(一个用户的全部特征),如果直接去匹配所有的原始特征向量数据,就必须要读取整个原始数据集,读取完原始数据集后,进行匹配计算(相似度/空间距离等),完成匹配计算后,再次在整个原始数据集加工表中进行搜索,完成最后的类似特征向量的检索。这种方式在原始数据集体量巨大时,会使得完成一次检索变得异常臃肿困难。

2 通过向量化的方式,对特征进行合并,能够使得数据在原有的基础上进一步实现数据量的压缩,一方面降低了数据量级,另一方面也简化了原始数据。

2 常用的向量距离计算方法

欧氏距离

欧式距离(L2范数)是最易于理解的一种距离计算方法,即空间直线距离

image.png

曼哈顿距离

从一个十字路口来车到另外一个十字路口,驾驶距离是两点间的直线距离吗?显然不是,除非你能穿越大楼。实际驾驶距离就是这个“曼哈顿距离”(L1范数),曼哈顿距离也称为城市街区距离(City Block distance)

image.png

切比雪夫距离

国际象棋中,国王走一步能够移动到相邻的8个方格中的任意一个。那么国王从格子(x1,y1)走到格子(x2,y2)最少需要多少步?这种类似的距离度量方法叫作切比雪夫距离.。

image.png

另一种等价形式是:

image.png

余弦夹角

余弦夹角是目前应用最多的一种空间向量判别方法,不同于传统的空间距离,余弦夹角通过数值的概念和正负号方向实现了对两组向量在空间中的关系定义,同时,正负号的存在使得在进行向量加权时非常方便(正向大权重,负向小权重,即可实现有效的权重分配)。

image.png

3 数据加工

我们对数据进行向量化,此处对数据按照每3个特征一组,取标向量[1,1,1],在将数据加工完成后,即实现了数据的向量化,在此之前我们通过上传数据的方式,将数据上传至数据库中的vector表中,在此基础上,进行将要处理的特征在sql中进行添加:

drop table if exists vectors;
create table vectors as
select *
,123.12345 as sim1
,123.12345 as sim2
,123.12345 as sim3
,123.12345 as sim4
,123.12345 as sim5
,123.12345 as sim6
from vector;

完成表加工后,进行特征的计算;

import pymysql
import pandas as pd
import numpy as np
import tqdm


# 构建余弦函数
def cos_sim(vec1, vec2):
    cos_sim = vec1.dot(vec2) / np.linalg.norm(vec1) * np.linalg.norm(vec2)
    return cos_sim



# 创建数据库连接
config = pymysql.connect(host="localhost", user="root", password="asdf", database="yang")

# 原始数据预览
# +----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----------+-----------+-----------+-----------+-----------+-----------+
# | id | a01 | a02 | a03 | a04 | a05 | a06 | a07 | a08 | a09 | a10 | a11 | a12 | a13 | a14 | a15 | a16 | a17 | a18 | sim1      | sim2      | sim3      | sim4      | sim5      | sim6      |
# +----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----------+-----------+-----------+-----------+-----------+-----------+
# | 1  | 7   | 8   | 9   | 8   | 1   | 6   | 6   | 8   | 11  | -4  | 4   | 3   | 10  | 1   | 9   | 0   | 11  | 10  | 123.12345 | 123.12345 | 123.12345 | 123.12345 | 123.12345 | 123.12345 |
# | 2  | 5   | 8   | -3  | 8   | 6   | 6   | 8   | 10  | 3   | 5   | -2  | 5   | -1  | 0   | 1   | 6   | 7   | 2   | 123.12345 | 123.12345 | 123.12345 | 123.12345 | 123.12345 | 123.12345 |
# | 3  | 3   | 7   | 13  | 7   | 2   | 9   | 12  | 2   | 12  | 11  | 6   | 4   | 7   | 7   | 6   | 1   | -1  | 9   | 123.12345 | 123.12345 | 123.12345 | 123.12345 | 123.12345 | 123.12345 |
# | 4  | 7   | -1  | 13  | 16  | -1  | 1   | 10  | 15  | 0   | 13  | 6   | 8   | 7   | 7   | 1   | 12  | -4  | 8   | 123.12345 | 123.12345 | 123.12345 | 123.12345 | 123.12345 | 123.12345 |
# | 5  | -1  | 6   | -4  | 1   | -4  | 12  | 10  | -2  | -2  | 3   | 2   | -3  | -3  | -1  | 12  | 6   | 1   | 5   | 123.12345 | 123.12345 | 123.12345 | 123.12345 | 123.12345 | 123.12345 |
# | 6  | 6   | 8   | 6   | 10  | 17  | 7   | -1  | 7   | 1   | 4   | 6   | -8  | 11  | 7   | 8   | 8   | -1  | 12  | 123.12345 | 123.12345 | 123.12345 | 123.12345 | 123.12345 | 123.12345 |
# | 7  | 1   | 5   | 4   | -1  | 6   | 16  | 10  | 7   | 9   | 4   | 2   | -1  | 6   | 10  | -2  | 11  | 6   | 10  | 123.12345 | 123.12345 | 123.12345 | 123.12345 | 123.12345 | 123.12345 |
# | 8  | 8   | 8   | 6   | 3   | -6  | 9   | 12  | 1   | 6   | 12  | 6   | 5   | -4  | 1   | 5   | 1   | 16  | -4  | 123.12345 | 123.12345 | 123.12345 | 123.12345 | 123.12345 | 123.12345 |
# | 9  | 4   | 10  | 5   | -1  | 6   | 10  | 11  | -1  | 9   | 7   | 10  | 10  | 4   | 8   | 5   | 9   | 8   | 8   | 123.12345 | 123.12345 | 123.12345 | 123.12345 | 123.12345 | 123.12345 |
# | 10 | 7   | 4   | 1   | 5   | 11  | 4   | -4  | 11  | 9   | 12  | 7   | 0   | 6   | 9   | 5   | 3   | 7   | 3   | 123.12345 | 123.12345 | 123.12345 | 123.12345 | 123.12345 | 123.12345 |
# +----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----------+-----------+-----------+-----------+-----------+-----------+

# 查询语句
sql1 = """
select * from vectors
"""

# 数据读取
dt = pd.read_sql(sql1, config)


# 构建各组余弦值,此过程可合并,演示时为方便调整,故而直接进行了复制
sim1 = []
sim2 = []
sim3 = []
sim4 = []
sim5 = []
sim6 = []

for i in tqdm.trange(len(dt)):
    a = np.array(dt.iloc[i,1:4].astype(float))
    b = np.array([1., 1., 1.])
    cos_sim_x1 = cos_sim(a, b)
    sim1.append(cos_sim_x1)

for i in tqdm.trange(len(dt)):
    a = np.array(dt.iloc[i,4:7].astype(float))
    b = np.array([1., 1., 1.])
    cos_sim_x2 = cos_sim(a, b)
    sim2.append(cos_sim_x2)

for i in tqdm.trange(len(dt)):
    a = np.array(dt.iloc[i,7:10].astype(float))
    b = np.array([1., 1., 1.])
    cos_sim_x3 = cos_sim(a, b)
    sim3.append(cos_sim_x3)

for i in tqdm.trange(len(dt)):
    a = np.array(dt.iloc[i,10:13].astype(float))
    b = np.array([1., 1., 1.])
    cos_sim_x4 = cos_sim(a, b)
    sim4.append(cos_sim_x4)

for i in tqdm.trange(len(dt)):
    a = np.array(dt.iloc[i,13:16].astype(float))
    b = np.array([1., 1., 1.])
    cos_sim_x5 = cos_sim(a, b)
    sim5.append(cos_sim_x5)

for i in tqdm.trange(len(dt)):
    a = np.array(dt.iloc[i,16:19].astype(float))
    b = np.array([1., 1., 1.])
    cos_sim_x6 = cos_sim(a, b)
    sim6.append(cos_sim_x6)

dt['sim1'] = sim1
dt['sim2'] = sim2
dt['sim3'] = sim3
dt['sim4'] = sim4
dt['sim5'] = sim5
dt['sim6'] = sim6



# ##########
insertdf = dt.fillna(0)

# 开始数据写回
vec_col = "id," \
          "a01,a02,a03,a04,a05,a06,a07,a08,a09," \
          "a10,a11,a12,a13,a14,a15,a16,a17,a18," \
          "sim1,sim2,sim3,sim4,sim5,sim6"

# 创建游标
cursor = config.cursor()
for i in tqdm.trange(len(insertdf)):
    sql2 = """
    insert into vectors({})
    value("{}","{}","{}","{}","{}","{}","{}","{}","{}","{}","{}","{}",
    "{}","{}","{}","{}","{}","{}","{}","{}","{}","{}","{}","{}","{}")
    """.format(
        vec_col,str(insertdf.iloc[i,0]),str(insertdf.iloc[i,1]),str(insertdf.iloc[i,2]),str(insertdf.iloc[i,3]),
        str(insertdf.iloc[i,4]),str(insertdf.iloc[i,5]),str(insertdf.iloc[i,6]),str(insertdf.iloc[i,7]),
        str(insertdf.iloc[i,8]),str(insertdf.iloc[i,9]),str(insertdf.iloc[i,10]),str(insertdf.iloc[i,11]),
        str(insertdf.iloc[i,12]),str(insertdf.iloc[i,13]),str(insertdf.iloc[i,14]),str(insertdf.iloc[i,15]),
        str(insertdf.iloc[i,16]),str(insertdf.iloc[i,17]),str(insertdf.iloc[i,18]),str(insertdf.iloc[i,19]),
        str(insertdf.iloc[i,20]),str(insertdf.iloc[i,21]),str(insertdf.iloc[i,22]),str(insertdf.iloc[i,23]),
        str(insertdf.iloc[i,24]))
    cursor.execute(sql2)
config.commit()

sql3 = """
delete
from vectors
where sim1 = '123.12345'
and sim2 = '123.12345'
and sim3 = '123.12345'
and sim4 = '123.12345'
and sim5 = '123.12345'
and sim6 = '123.12345'
"""
cursor.execute(sql3)
config.commit()

print("提交结果,代码执行完毕!")

# 加工完成后数据预览
# +----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----------+-----------+-----------+-----------+-----------+-----------+
# | id | a01 | a02 | a03 | a04 | a05 | a06 | a07 | a08 | a09 | a10 | a11 | a12 | a13 | a14 | a15 | a16 | a17 | a18 | sim1      | sim2      | sim3      | sim4      | sim5      | sim6      |
# +----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----------+-----------+-----------+-----------+-----------+-----------+
# | 1  | 7   | 8   | 9   | 8   | 1   | 6   | 6   | 8   | 11  | -4  | 4   | 3   | 10  | 1   | 9   | 0   | 11  | 10  | 2.984496  | 2.5851825 | 2.9127586 | 0.8115027 | 2.567763  | 2.4467173 |
# | 2  | 5   | 8   | -3  | 8   | 6   | 6   | 8   | 10  | 3   | 5   | -2  | 5   | -1  | 0   | 1   | 6   | 7   | 2   | 1.7496355 | 2.9704426 | 2.7653931 | 1.8856181 | 0         | 2.7539553 |
# | 3  | 3   | 7   | 13  | 7   | 2   | 9   | 12  | 2   | 12  | 11  | 6   | 4   | 7   | 7   | 6   | 1   | -1  | 9   | 2.6440857 | 2.6932752 | 2.6353758 | 2.7653931 | 2.992528  | 1.7110555 |
# | 4  | 7   | -1  | 13  | 16  | -1  | 1   | 10  | 15  | 0   | 13  | 6   | 8   | 7   | 7   | 1   | 12  | -4  | 8   | 2.2237818 | 1.7253244 | 2.4019223 | 2.851335  | 2.6111648 | 1.8516402 |
# | 5  | -1  | 6   | -4  | 1   | -4  | 12  | 10  | -2  | -2  | 3   | 2   | -3  | -3  | -1  | 12  | 6   | 1   | 5   | 0.2379155 | 1.2285425 | 1         | 0.7385489 | 1.1165811 | 2.6396481 |
# | 6  | 6   | 8   | 6   | 10  | 17  | 7   | -1  | 7   | 1   | 4   | 6   | -8  | 11  | 7   | 8   | 8   | -1  | 12  | 2.9704426 | 2.81386   | 1.6977494 | 0.3216338 | 2.9439203 | 2.2763607 |
# | 7  | 1   | 5   | 4   | -1  | 6   | 16  | 10  | 7   | 9   | 4   | 2   | -1  | 6   | 10  | -2  | 11  | 6   | 10  | 2.6726124 | 2.1249373 | 2.9694093 | 1.8898224 | 2.0493902 | 2.9171437 |
# | 8  | 8   | 8   | 6   | 3   | -6  | 9   | 12  | 1   | 6   | 12  | 6   | 5   | -4  | 1   | 5   | 1   | 16  | -4  | 2.9755098 | 0.9258201 | 2.4461041 | 2.7823485 | 0.5345225 | 1.3627703 |
# | 9  | 4   | 10  | 5   | -1  | 6   | 10  | 11  | -1  | 9   | 7   | 10  | 10  | 4   | 8   | 5   | 9   | 8   | 8   | 2.7714348 | 2.2196863 | 2.3097566 | 2.963635  | 2.8735245 | 2.9952115 |
# | 10 | 7   | 4   | 1   | 5   | 11  | 4   | -4  | 11  | 9   | 12  | 7   | 0   | 6   | 9   | 5   | 3   | 7   | 3   | 2.5584086 | 2.7216553 | 1.8769485 | 2.3688392 | 2.9070095 | 2.7508479 |
# +----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----------+-----------+-----------+-----------+-----------+-----------+

至此,已完成了基础数据的加工(即对应的特征矩阵)

4 查询构建

当完成基础数据加工后,即可构建查询系统。

import pymysql
import datetime
import pandas as pd
import numpy as np

# 加工余弦函数
def cos_sim(vec1, vec2):
    cos_sims = vec1.dot(vec2) / np.linalg.norm(vec1) * np.linalg.norm(vec2)
    return cos_sims

# 生成6v函数
def to_6v(find_x_lst):
    for i in range(len(find_x_lst)):
        a = np.array(find_x_lst[:3])
        b = np.array([1., 1., 1.])
        cos_sim_x1 = cos_sim(a, b)
        sim1 = cos_sim_x1

    for i in range(len(find_x_lst)):
        a = np.array(find_x_lst[3:6])
        b = np.array([1., 1., 1.])
        cos_sim_x2 = cos_sim(a, b)
        sim2 = cos_sim_x2

    for i in range(len(find_x_lst)):
        a = np.array(find_x_lst[6:9])
        b = np.array([1., 1., 1.])
        cos_sim_x3 = cos_sim(a, b)
        sim3 = cos_sim_x3

    for i in range(len(find_x_lst)):
        a = np.array(find_x_lst[9:12])
        b = np.array([1., 1., 1.])
        cos_sim_x4 = cos_sim(a, b)
        sim4 = cos_sim_x4

    for i in range(len(find_x_lst)):
        a = np.array(find_x_lst[12:15])
        b = np.array([1., 1., 1.])
        cos_sim_x5 = cos_sim(a, b)
        sim5 = cos_sim_x5

    for i in range(len(find_x_lst)):
        a = np.array(find_x_lst[15:])
        b = np.array([1., 1., 1.])
        cos_sim_x6 = cos_sim(a, b)
        sim6 = cos_sim_x6

    return [sim1,sim2,sim3,sim4,sim5,sim6]

# sql语句where条件生成
def find_where(sql_lst, start, end, from_table):

    # 区分正负号,按数值逐个生成单条语句,保留范围值自定义
    tmp_sql = []
    sim_lst = ["sim1", "sim2", "sim3", "sim4", "sim5", "sim6"]
    for i in range(len(sql_lst)):
        if sql_lst[i] < 0:
            strs = """{} between {}*{} and {}*{}""".format(sim_lst[i], sql_lst[i], str(end), sql_lst[i], str(start))
            tmp_sql.append(strs)
        else:
            strs = """{} between {}*{} and {}*{}""".format(sim_lst[i], sql_lst[i], str(start), sql_lst[i], str(end))
            tmp_sql.append(strs)

    # 将单句sql条件汇总成sql语句中的where条件
    where_sql = ''
    for i in range(len(tmp_sql)):
        if i == 0:
            where_sql = tmp_sql[i] + " and "
        elif i != 0 and i != len(tmp_sql) - 1:
            where_sql = where_sql + tmp_sql[i] + " and "
        else:
            where_sql = where_sql + tmp_sql[i]

    where_sql = """select * from {} where """.format(from_table) + where_sql
    return where_sql

# 执行语句
def run_sql(sql):
    time_a = datetime.datetime.now()
    config = pymysql.connect(host="localhost", user="root", password="asdf", database="yang")
    dt = pd.read_sql(sql, config)
    time_b = datetime.datetime.now()
    time_c = time_b - time_a
    ts = time_c.seconds

    if len(dt) == 0:
        print("本次查询共计耗时{}秒".format(ts),"\n","未检索到目标数据")
    else:
        print(dt)
        print("本次查询共计耗时{}秒".format(ts),"\n","共查询{}条数据".format(len(dt)))


# 查询
# 输入一条信息
x_lst = [1, 3, -1, 4, 2, 4, 1, 2, -3, 5, 4, 3, 2, 5, 5, 3, 5, 3]
# 执行查询
run_sql(find_where(to_6v(x_lst), 0.9, 1.03,"vectors"))

# vec_lst = to_6v(x_lst)
# sql = find_where(vec_lst, 0.8, 1.1,"vectors")
# run_sql(sql)

返回结果如下:

       id a01 a02 a03 a04  ...      sim2 sim3      sim4      sim5      sim6
0  217020   9   2  -3   4  ...  2.713602  0.0  2.735788  2.721794  2.898275
1  433820  -2   1   8   5  ...  2.674201  0.0  2.811603  2.894704  2.898275
[2 rows x 25 columns]
本次查询共计耗时0秒 
 共查询2条数据