当前位置: 首页 > 科技观察

用CLIP构建视频搜索引擎

时间:2023-03-17 18:26:36 科技观察

CLIP(对比语言-图像预训练)是一种机器学习技术,可以准确地理解和分类图像和自然语言文本,对图像和语言处理具有深远的影响,并且已经被用作流行扩散模型DALL-E的基础机制。在这篇文章中,我们将介绍如何调整CLIP以帮助进行视频搜索。本文不会深入探讨CLIP模型的技术细节,而是介绍CLIP的另一个实际应用(除了扩散模型)。首先要做的事情:CLIP使用图像解码器和文本编码器来预测数据集中的哪些图像与哪些文本匹配。使用CLIP进行搜索通过使用预训练的拥抱面CLIP模型,我们可以构建一个简单而强大的视频搜索引擎,具有自然语言能力,无需特征工程。我们需要用到以下软件Python≥=3.8,ffmpeg,opencv有很多通过文本搜索视频的技术。我们可以认为搜索引擎将由两部分组成,索引和搜索。索引视频索引通常涉及人和机器过程的结合。人类通过在标题、标签和描述中添加相关关键字来预处理视频,而自动化过程则提取视觉和听觉特征,例如对象检测和音频转录。用户交互指标等,记录视频的哪些部分最相关,以及它们保持相关的时间。所有这些步骤都有助于创建视频内容的可搜索索引。索引过程的概述如下:将视频分成多个场景将场景采样为帧处理像素嵌入索引构建和存储将视频分成多个场景为什么场景检测很重要?视频是由场景组成的,场景是由相似的帧组成的。如果我们只对视频中的任意场景进行采样,我们可能会错过整个视频中的关键帧。所以我们需要准确识别和定位视频中的特定事件或动作。例如,如果我搜索“公园里的狗”,我正在搜索的视频包含多个场景,例如一个人骑自行车的场景和公园里一只狗的场景,场景检测可以让我识别近景。您可以使用“场景检测”python包来执行此操作。importscenedetectassdvideo_path=''#机器上视频的路径video=sd.open_video(video_path)sm=sd.SceneManager()sm.add_detector(sd.ContentDetector(threshold=27.0))sm.detect_scenes(video)scenes=sm.get_scene_list()对场景的帧进行采样,然后需要使用cv2对视频的帧进行采样。importcv2cap=cv2.VideoCapture(video_path)every_n=2#每个场景的样本数scene_frame_samples=[]forscene_idxinrange(len(scenes)):scene_length=abs(scenes[scene_idx][0].frame_num-scenes[scene_idx][1].frame_num)every_n=round(scene_length/no_of_samples)local_samples=[(every_n*n)+scenes[scene_idx][0].frame_numforninrange(3)]scenes_frame_samples.append(local_samples)转换为PixelEmbeddings收集样本后,我们需要将它们计算成CLIP模型可用的东西。每个样本首先需要转换为图像张量嵌入。从变压器导入CLIPProcessor从PIL导入图像clip_processor=CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32")defclip_embeddings(image):inputs=clip_processor(images=image,return_tensors="pt",padding=True)input_tokens={k:vfork,vininputs.items()}returninput_tokens['pixel_values']#...scene_clip_embeddings=[]#在下一步中保存场景嵌入forscene_idxinrange(len(scenes_frame_samples)):scene_samples=scenes_frame_samples[scene_idx]pixel_tensors=[]#scene_samples中frame_sample的每个样本的所有剪辑嵌入:cap.set(1,frame_sample)ret,frame=cap.read()ifnotret:print('读取失败',ret,frame_sample,scene_idx,frame)breakpil_image=Image.fromarray(frame)clip_pixel_values=clip_embeddings(pil_image)pixel_tensors.append(clip_pixel_values)接下来是对同一个场景的所有样本进行平均,这样可以降低样本的维度,也可以解决单个样本的噪声问题importtorchimportuuiddefsave_tensor(t):path=f'/tmp/{uuid.uuid4()}'torch.save(t,path)返回路径#..avg_tensor=torch.mean(torch.stack(pixel_tensors),dim=0)scene_clip_embeddings.append(save_tensor(avg_tensor))这会生成一个嵌入在CLIP中表示视频内容的张量列表。存储索引对于底层索引存储,我们使用LevelDB(LevelDB是Google维护的一个键/值库)。我们的搜索引擎的架构将由3个独立的索引组成:视频场景索引:哪些场景属于特定的视频场景嵌入式索引:保存特定的场景数据视频元数据索引:保存视频的元数据。我们首先将视频中所有计算出的元数据,以及视频的唯一标识,插入到元数据索引中。这一步是现成的,非常简单。importleveldbimportuuiddefinsert_video_metadata(videoID,data):b=json.dumps(data)level_instance=leveldb.LevelDB('./dbs/videometadata_index')level_instance.Put(videoID.encode('utf-8'),b.encode('utf-8'))#...video_id=str(uuid.uuid4())insert_video_metadata(video_id,{'VideoURI':video_path,})然后在sceneembeddingindex中新建一个entry来保存video对于每个像素嵌入,还需要一个唯一标识符来标识每个场景。importleveldbimportuuiddefinsert_scene_embeddings(sceneID,data):level_instance=leveldb.LevelDB('./dbs/scene_embedding_index')level_instance.Put(sceneID.encode('utf-8'),data)#...对于finscene_clip_embeddings:scene_id=str(uuid.uuid4())withopen(f,mode='rb')asfile:content=file.read()insert_scene_embeddings(scene_id,content)最后,我们需要保存哪些场景属于哪个视频。importleveldbimportuuiddefinsert_video_scene(videoID,sceneIds):b=",".join(sceneIds)level_instance=leveldb.LevelDB('./dbs/scene_index')level_instance.Put(videoID.encode('utf-8'),b.encode('utf-8'))#...scene_ids=[]forfinscene_clip_embeddings:#..如上一步所示scene_ids.append(scene_id)scene_embedding_index.insert(scene_id,content)scene_index.insert(video_id,scene_ids)search现在我们有了视频索引,我们可以根据模型输出搜索和排序它们。第一步需要迭代场景索引中的所有记录。然后,创建一个包含所有视频和视频中匹配场景ID的列表。records=[]level_instance=leveldb.LevelDB('./dbs/scene_index')fork,vinlevel_instance.RangeIter():record=(k.decode('utf-8'),str(v.decode('utf-8')).split(','))records.append(record)下一步需要收集每个视频中存在的所有场景嵌入张量。导入leveldbdefget_tensor_by_scene_id(id):level_instance=leveldb.LevelDB('./dbs/scene_embedding_index')b=level_instance.Get(bytes(id,'utf-8'))returnBytesIO(b)forrinrecords:tensors=[get_tensor_by_scene_id(id)foridinr[1]]在我们拥有构成视频的所有张量之后,我们可以将其传递到模型中。模型的输入是“pixel_values”,一个表示视频场景的张量。从变压器导入火炬导入CLIPProcessor,CLIPModelprocessor=CLIPProcessor.from_pretrained(“openai/clip-vit-base-patch32”)model=CLIPModel.from_pretrained(“openai/clip-vit-base-patch32”)输入=处理器(文本=文本,return_tensors="pt",padding=True)fortensorintensors:image_tensor=torch.load(tensor)inputs['pixel_values']=image_tensoroutputs=model(**inputs)然后访问模型输出中的“logits_per_image”获取模型输出。Logits本质上是网络的原始非标准化预测。由于我们只提供了一个文本字符串和一个表示视频中场景的张量,因此logit的结构将是一个单值预测。logits_per_image=outputs.logits_per_imageprobs=logits_per_image.squeeze()prob_for_tensor=probs.item()对每次迭代的概率求和并将其除以操作结束时的张量总数以获得视频的平均概率。defclip_scenes_avg(tensors,text):avg_sum=0.0fortensorintensors:#...之前的代码片段的视频,并在对概率进行排序后,返回请求的搜索结果数。importleveldbimportjsontop_n=1#我们想要的搜索结果数量json.loads(b.decode('utf-8'))results=[]forrinrecords:#..收集场景张量#r[0]:videoidreturn(clip_scenes_avg,r[0])sorted=list(结果)sorted.sort(key=lambdax:x[0],reverse=True)results=[]forsinsorted[:top_n]:data=video_metadata_by_id(s[1])results.append({'video_id':s[1],'score':s[0],'video_uri':data['VideoURI']})就是这样!现在您可以输入一些视频并测试搜索结果。总结CLIP使创建频率搜索引擎变得容易。使用预训练的CLIP模型和Google的LevelDB,我们可以索引和处理视频并使用自然语言输入进行搜索。通过这个搜索引擎,用户可以轻松找到相关的视频,最重要的是我们不需要大量的预处理或特征工程。那么我们还有什么可以改进的呢?使用场景的时间戳来确定最佳场景。修改预测以在计算集群上运行它。使用向量搜索引擎,例如Milvus代替LevelDB构建基于索引的推荐系统等。最后:你可以在这里找到本文的代码:https://github.com/GuyARoss/CLIP-video-搜索/树/文章-01。这个修改后的版本:https://github.com/GuyARoss/CLIP-video-search。