假如你和我一样在准备24年的春招,在前端全栈外,再准备一些AI的内容是非常有必要的。24年是AI红利年,AIGC+各种岗位大厂机会会多些,同意的请点赞。也欢迎朋友们加我微信shunwuyu, 一起交流。
前言
在前面的文章中,我们利用Pinecone向量数据库实现了自然语义搜索、RAG、推荐系统,本文一起来看下如何实现时尚产品混合搜索。当我们提出"dark blue french connection jeans for men"问题时,它会返回我们数据集里的相关裤子图片。文生图,喔, 特6。
安装依赖
yaml
# HuggingFace 数据集,
!pip install datasets
# pinecone 向量数据库
!pip install pinecone-client
# 命令行工具
!pip install tqdm
# LLM
!pip install openai
# pinecone的文本工具库,用于创建稀疏向量
!pip install pinecone-text
数据集
我们将使用Hugging Face的ashraq/fashion-product-images-small
,这个数据集中包含时尚品的数据和图片。
下载数据集,查看数据
ini
# HuggingFace 的datasets 加载预定义的数据集
from datasets import load_dataset
fashion = load_dataset(
# 数据集名称
"ashraq/fashion-product-images-small",
# 数据集会分这不同的子集,如训练集train、验证集validation和测试集test,用于不同的阶段
# split train 希望加载的是预训练部分
split="train"
)
fashion
从上图可以看在到,有44072
行数据,最后一列image是图片,其余为相应文本字段。本文我们要挑战的就是如何用文本搜索图片。
查看图片
ini
# 取出所有的图片列
images = fashion['image']
# 移除image列后,就是文本列,我们交给了metadata
metadata = fashion.remove_columns('image')
# 显示其中一张图片,显示如下
images[900]
查看metada数据
ini
# Hugging Face 的数据集调用to_pandas方法生成pandas的DataFrame
metadata = metadata.to_pandas()
# 显示前五条
metadata.head()
在之前学习到的Sematic Search, productDisplayName
列的内容和用户的搜索相似性,我们很熟悉。productDisplayName
列与image
列也有属于同一行的关联, 等下会怎么做呢? 一起期待。
使用BM25模型创建稀疏向量
以下两个概念比较复杂,我们先来玩下:
- 稀疏向量
指大部分元素为零的向量,应用在自然语言处理、推荐系统等领域。每个文档表示为一个稀疏向量,其中非零元素对应文档中出现过的词汇及其权重。可以节省存储空间和计算资源。
- BM25
BM25是一种用于信息检索的排名算法,它基于词频(TF)、逆文档频率(IDF)以及其他统计特性来估计一个文档对于给定查询的相关性。
当我们将稀疏向量与BM25结合,比如在构建搜索引擎时,先用稀疏向量表示文档集合,然后利用MB25算法对这些稀疏向量进行打分,从而高效地找出与用户查询相关的文档。
我们会去安装一个pinecone叫做pinecone-text的库,用于创建稀疏向量。
arduino
!pip install pinecone-text
我们对数据进行BM25编码
ini
#从pinecone_text文本工具库的稀疏模块中引入BM25Encoder 编码工具
from pinecone_text.sparse import BM25Encoder
# 实例化bm25实例
bm25 = BM25Encoder()
# 训练一下,是为了让 BM25Encoder 对 `metadata` 数据集中 'productDisplayName' 列中的文本数据进行学习,计算文档中每个词的 IDF(逆文档频率)和其他相关统计量。
bm25.fit(metadata['productDisplayName'])
metadata['productDisplayName'][0]
- 对查询文本或文档进行编码
css
# 对查询字符串进行编码
bm25.encode_queries(metadata['productDisplayName'][0])
# 对文档进行编码
bm25.encode_documents(metadata['productDisplayName'][0])
bm25 就是使用encode_queries和encode_documents两者结合,实现了从海量文档中快速找到与查询语句最匹配的内容。
使用CLIP创建密集向量
- 先安装编码库
yaml
# HuggingFace 文本编码的库
!pip install sentence-transformers
进行编码, 相比于bm25, 前者是稀疏向量,后者是密集向量
ini
# 从HuggingFace的文本编码库SentenceTransformer库中引入sentence-transformers/clip-ViT-B-32 来自openai开源的clip
# 设备是否支持gpu计算
# 当下流行NLP框架
from torch import torch
device = 'cuda' if torch.cuda.is_available() else 'cpu'
model = SentenceTransformer('sentence-transformers/clip-ViT-B-32',
# 是否调用了gpu?
device=device)
model
# 也对productDisplayName 列进行clip编码
dense_vec = model.encode([metadata['productDisplayName'][0]])
# 返回向量维度
dense_vec.shape
下载了sentence-transformers/clip-ViT-B-32
模型,在实例化时,传入了device参数(cuda or not)。对productDisplayName
列的第 一个值进行编码,并编出向量的维度为512。
实例化Pinecone
看我们在Pinecone里是怎么来存稀疏和密集两种向量的。
ini
import torch
# 是否支持显卡
device = 'cuda' if torch.cuda.is_available() else 'cpu'
print(device)
INDEX_NAME = 'dl-ai'
pinecone = Pinecone(api_key='填入你自己的key')
# 如果存在删除
if INDEX_NAME in [index.name for index in pinecone.list_indexes()]:
pinecone.delete_index(INDEX_NAME)
# 创建index
pinecone.create_index(
INDEX_NAME,
# clip 密集向量是512维度的
dimension=512,
# 相似度计算不是cosine 是dotproduct 点积运算,
# cosine 是两个向量的夹角, 不考虑向量的长度
# dotproduct 在高维相似度中更适合,不仅考虑方向(夹角),也考虑长度
metric="dotproduct",
spec=ServerlessSpec(cloud='aws', region='us-west-2')
)
index = pinecone.Index(INDEX_NAME)
进行两种编码并存入pinecone
ini
from tqdm.auto import tqd
# 每批encode处理100个
batch_size = 100
# 总计1000个
fashion_data_num = 1000
# tqdm进度条显示 第一个参数使用range创建了一个可迭代对象
# min 取fashion数据集与1000 之间的较小者,
#batch_size 每次处理base_size个
for i in tqdm(range(0, min(fashion_data_num,len(fashion)), batch_size)):
# find end of batch i会自增的 怕最后一页不到batch_size
i_end = min(i+batch_size, len(fashion))
# extract metadata batch 把相应部分拿出来
meta_batch = metadata.iloc[i:i_end]
# 将dataframe转换为字典,orient="records"意思是按每一行记录转成字典
meta_dict = meta_batch.to_dict(orient="records")
# concatinate all metadata field except for id and year to form a single string
# meta_batch.columns.isin(['id', 'year'])的意思是meta_batch列表里取id year这两列
#~按位取反操作符 那么就是除id year 两列外其余列都要
# join 将这些列用空格连起来
# 这样所有的文本列都在一起了, 做embedding 包含这行的所有语义
meta_batch = [" ".join(x) for x in meta_batch.loc[:, ~meta_batch.columns.isin(['id', 'year'])].values.tolist()]
# extract image batch 将这一批的图像列拿出来
img_batch = images[i:i_end]
# create sparse BM25 vectors 对文本内容做稀疏向量编码
sparse_embeds = bm25.encode_documents([text for text in meta_batch])
# create dense vectors
# clip 密集模型用于图片的编码
dense_embeds = model.encode(img_batch).tolist()
# create unique IDs
# 列表推导式 将从i 到 i_end 的数字i 拼到一起 代表这段数据也是唯一的
ids = [str(x) for x in range(i, i_end)]
upserts = []
# loop through the data and create dictionaries for uploading documents to pinecone index
# 将ids,sparse_embeds, .... 等元素用zip打包成一个元组。
for _id, sparse, dense, meta in zip(ids, sparse_embeds, dense_embeds, meta_dict):
# 添加元素
upserts.append({
'id': _id,
# 这个之前的没有, sparse_values存的稀疏向量
'sparse_values': sparse,
# values 值存的是图片编码
'values': dense,
# 媒体数据用的是meta_dict 本身的json
'metadata': meta
})
# upload the documents to the new hybrid index
index.upsert(upserts)
# show index description after uploading the documents
index.describe_index_stats()
花了2:30秒, 一是数据量比较大,二是稀疏向量和密集向量两种编码,肯定更耗时。tqdm 帮我们打理好了进度信息展示。
- 进行查询
ini
# 我们想查找深蓝色法国连体男士牛仔裤
query = "dark blue french connection jeans for men"
# 对查询文本做稀疏编码
sparse = bm25.encode_queries(query)
# 对查询文本再做密集编码
dense = model.encode(query).tolist()
# 返回相似的14条
# pinecone index支持 稀疏、密集向量同时查询
result = index.query(
top_k=14,
vector=dense,
sparse_vector=sparse,
include_metadata=True
)
# 在输出中拿出images
imgs = [images[int(r["id"])] for r in result["matches"]]
imgs
封装图片函数并显示出来
ini
#ipython的display模块中以html的方式显示
from IPython.core.display import HTML
# 二进制流文件
from io import BytesIO
# 用于将图片转为base64
from base64 import b64encode
# function to display product images
def display_result(image_batch):
figures = []
for img in image_batch:
# 二进制实例
b = BytesIO()
# 以png的格式存放
img.save(b, format='png')
figures.append(f'''
<figure style="margin: 5px !important;">
<img src="data:image/png;base64,{b64encode(b.getvalue()).decode('utf-8')}" style="width: 90px; height: 120px" >
</figure>
''')
return HTML(data=f'''
<div style="display: flex; flex-flow: row wrap; text-align: center;">
{''.join(figures)}
</div>
''')
总结
- 当我们在index.query()方法中除入传入vector外, 还传入了
sparse_vector
, 我们完成了一次混合向量查询 - 图片由二进制表达,更适合密集向量