DALL-E2、稳定扩散、BLIP 等等!
生成式人工智能正处于火热状态。尤其是最近几个月,多模态机器学习——将概念跨越不同的“模态”(如文本、图像和音频)连接起来的人工智能出现了爆炸式增长。举个例子,Midjourney 是一种多模态文本到图像模型,因为它接收自然语言并输出图像。这次多模态协同复兴的杰作是 Meta AI 的 ImageBind,它可以接收 6 种输入,并以相同的“空间”表示它们。
在这样的激动人心之际,我想测试一下多模态模型的表现,看看它们实际上有多好。特别是,我想回答三个问题:
- 哪种文本到图像模型最好?
- 哪种图像到文本模型最好?
- 图像到文本和文本到图像哪个更重要?
当然,每个模型都会带来自己的偏见,从训练数据到模型架构,所以没有真正的最佳模型。但我们仍然可以在一般情况下测试模型!
为了回答这些问题,我决定玩一局 AI 电话游戏,灵感来自我们全家喜欢玩的桌游 Telestrations。
Telestrations 很像电话游戏:玩家们围成一个圈,接收来自一侧人的信息,然后依次将自己的解释传递给另一侧的人。随着游戏的进行,原始信息必然会被改变,甚至完全丢失。不过,Telestrations 通过添加双模态通信来区别,玩家们交替绘制(或描绘)描述和描述(以文本形式)的描述。
鉴于我更感兴趣的是比较模型,我改编了这个游戏以适应这个目的。
AI 电话游戏的操作步骤如下:
- 每个“游戏”将配对一个图像到文本(I2T)模型和一个文本到图像(T2I)模型
- 给定初始提示,我们使用 T2I 模型生成图像。
- 然后将此图像传递到 I2T 模型中生成描述。
- 我们重复步骤 2 和 3 固定次数的
n
次(在我们的例子中n=10
)。 - 最后,我们量化原始提示与最终描述之间的差异。
在本文中,我将为您介绍整个过程,以便您也可以玩 AI 电话游戏!最后,我将回答三个激励问题。
注意:这个 AI 电话游戏与循环一致性的概念密切相关。通过在训练期间将循环一致性项纳入损失函数中,模型可以被激励在整个电话游戏中最小化退化。据我所知,本实验中考虑的模型没有考虑循环一致性。
本文的结构如下:
- 选择多模态模型
- 生成提示
- 创建电话线路
- 进行对话
- 可视化和分析结果
运行此实验并玩 AI 电话游戏的所有代码可以在此处找到。
要运行此代码,您需要安装 FiftyOne 开源库进行数据集整理、OpenAI Python 库和 Replicate Python 客户端。
pip install fiftyone openai replicate
DALL-E2 和 BLIP 的 AI 电话游戏中图像的进展。
选择竞争对手
多模态模型的空间非常庞大:在撰写本文时,仅 Hugging Face 就拥有 4,425 个 T2I 模型和 155 个 I2T 模型。玩所有这些模型的 AI 电话游戏——甚至是其中的一个可观的部分——都是完全不可行的。我的第一个任务是将这个潜在候选人的空间缩减到一组更易管理的竞争对手。
选择API
为了开始这个项目,我知道我将与许多模型一起工作。其中一些模型相当大,并且许多模型需要它们自己的环境,具有独特的要求集。考虑到我计划将每个T2I模型与每个I2T模型配对,将这些模型本地安装以玩AI电话游戏可能会导致潜在的依赖关系深渊,特别是因为我使用的是MacBook Pro M1!
为了避免这个问题,我决定坚持使用可通过API访问的模型。特别是,我选择主要使用Replicate,其简单的界面使我能够以即插即用的方式使用T2I和I2T模型。我使用的几乎每个模型都是开源的,因此如果您比我更勇敢,可以在本地运行这些模型并避免费用。话虽如此,总的来说,这个实验成本小于15美元。
文本到图像模型
在选择T2I模型时,我从Replicate的文本到图像集合中选择了模型。我的选择标准是模型需要便宜,快速且相对流行(通过模型在Replicate上的“运行”次数来判断)。此外,该模型需要是通用的,这意味着我不会考虑超出绘画、标志生成或动漫风格的模型。如果您愿意,可以尝试使用这些类型的模型来玩AI电话!
基于这些要求,我选择了Stable Diffusion和Feed forward VQGAN CLIP。最初,我还使用了DALL-E Mini,但在早期测试中,我对该模型的表现感到失望,因此我将该模型换成了OpenAI的DALL-E2,我通过OpenAI的图像生成端点访问该模型。
顺便说一句,将我的注意力限制在API可访问的模型上意味着我没有考虑Midjourney。没有官方API,我不想使用非官方API,也不想逐个在Discord中输入提示并逐个下载生成的图像。
为了使这个过程尽可能的即插即用,我采用面向对象的方法。我定义了一个基本的类,它公开了一个方法:
import replicateclass Text2Image(object): """Text2Image模型的包装器。""" def __init__(self): self.name = None self.model_name = None def generate_image(self, text): response = replicate.run(self.model_name, input={"prompt": text}) if type(response) == list: response = response[0] return response
对于Replicate模型,只需要设置属性,即可识别Replicate上的模型。例如,对于Stable Diffusion,类定义如下:
class StableDiffusion(Text2Image): """StableDiffusion模型的包装器。""" def __init__(self): self.name = "stable-diffusion" self.model_name = "stability-ai/stable-diffusion:27b93a2413e7f36cd83da926f3656280b2931564ff050bf9575f1fdf9bcd7478"
对于其他模型,例如DALL-E2,可以重载方法:
import openaiclass DALLE2(Text2Image): """DALL-E 2模型的包装器。""" def __init__(self): self.name = "dalle-2" def generate_image(self, text): response = openai.Image.create( prompt=text, n=1, size="512x512" ) return response['data'][0]['url']
这些T2I模型中的每一个都返回生成的图像的URL,然后我们可以直接将其传递给我们的I2T模型。
图像到文本模型
我采用了类似的过程来确定I2T竞争对手,评估Replicate的图像到文本集合中的候选模型。在查看集合中所有模型的示例之后,有六个模型脱颖而出:BLIP、BLIP-2、CLIP前缀字幕、带CLIP奖励的细粒度图像字幕、mPLUG-Owl和MiniGPT-4。其他模型也很诱人,例如CLIP Interrogator,它试图反向工程一个提示,然后您可以使用该提示生成类似的图像。但是在AI电话方面,这感觉有点像作弊!
我尝试了六个I2T候选模型,很快就排除了两个模型:BLIP-2生成的响应始终太短,无法使用,而CLIP字幕奖励模型生成的响应常常不连贯。
类比于T2I模型,我定义了一个基类Image2Text
类,公开了generate_text(image_url)
方法:
class Image2Text(object): """Image2Text模型的包装器。""" def __init__(self): self.name = None self.model_name = None self.task_description = "写出这幅图像的详细描述。" def generate_text(self, image_url): response = replicate.run( self.model_name, input={ "image": image_url, "prompt": self.task_description, } ) return response
然后我为每个模型创建了子类。以下是BLIP子类的样子:
class BLIP(Image2Text): """BLIP模型的包装器。""" def __init__(self): super().__init__() self.name = "blip" self.model_name = "salesforce/blip:2e1dddc8621f72155f24cf2e0adbde548458d3cab9f00c0139eea840d0ac4746"
所有模型都使用相同的任务描述——“写出这幅图像的详细描述”。
AI电话游戏中DALL-E2和mPLUG-Owl之间的图像进展。
提示
为了尽可能“科学”,我认为最好不要自己生成初始提示。相反,(仅仅是为了好玩)我将任务外包给了ChatGPT。我问:
我正在使用文本到图像和图像到文本的AI模型玩一个电话游戏。我想根据它们在长时间对话过程中保留复杂语义信息的能力来评估这些模型。你的工作是给我10个文本提示,让我可以用它们来运行这些电话游戏。你必须给我一个3个easy,3个小猪AI,3个hard,和1个ultra-hard(“不可能”)提示
我正在使用文本到图像和图像到文本的AI模型玩一个电话游戏。我想根据它们在长时间对话过程中保留复杂语义信息的能力来评估这些模型。你的工作是给我10个文本提示,让我可以用它们来运行这些电话游戏。你必须给我一个3个easy,3个小猪AI,3个hard,和1个ultra-hard(“不可能”)提示
以下是ChatGPT生成的一些提示:
Easy:“一个红苹果放在一个木桌上,阳光从窗户中射进来。”小猪AI:“一名宇航员漂浮在国际空间站中,透过窗户看着地球,背景中停靠着一个太空舱。”Hard:“古老的中东城市熙熙攘攘的市场。商人们为香料和丝绸讨价还价,骆驼载着货物,太阳在一座清真寺后面落山,月亮上有一个新月。”Impossible:“一个遥远的外星球上先进的外星文明的全景。星际飞行器在高耸的水晶结构上方的靛蓝色天空中飞行。具有不同物理特征的外星人正在互动,交换能量球,通过光模式进行通信,并照料着异国情调的生物发光植物。行星的双月在闪闪发光的外星海洋的地平线上可见。”
更严格的科学方法将更加有意地使用提示,以及它们的分类。
然后我取得了ChatGPT生成的文本提示,并构建了Prompt
对象,其中包含提示文本和ChatGPT分配的难度“级别”:
class Prompt(object): def __init__(self, text, level): self.text = text self.level = levellevels = ["easy", "小猪AI", "hard", "impossible"]level_prompts = [easy_texts, 小猪AI_texts, hard_texts, impossible_texts]def get_prompts(): prompts = [] for level, texts in zip(levels, level_prompts): for text in texts: prompts.append(Prompt(text, level)) return prompts
在VQGAN-CLIP和MiniGPT-4之间进行的AI电报游戏中图像的进展。
电话线路
玩AI电话的最后一个组成部分是“电话线”本身。我创建了一个TelephoneLine
类来封装T2I模型和I2T模型之间的连接。给定一个单一的电话线路,通过调用play(prompt, nturns=10)
来玩电话游戏,其中对话从prompt
开始,运行nturns
次回合。
import osimport hashlibimport fiftyone as fofrom fiftyone import ViewField as Fclass TelephoneLine(object): """Class for playing telephone with AI.""" def __init__(self, t2i, i2t): self.t2i = t2i self.i2t = i2t self.name = f"{t2i.name}_{i2t.name}" self.conversations = {} def get_conversation_name(self, text): full_name = f"{self.name}{text}" hashed_name = hashlib.md5(full_name.encode()) return hashed_name.hexdigest()[:6] def play(self, prompt, nturns = 10): """Play a game of telephone.""" print(f"Connecting {self.t2i.name} <-> {self.i2t.name} with prompt: {prompt.text[:20]}...") texts = [prompt.text] image_urls = [] for _ in range(nturns): image_url = self.t2i.generate_image(texts[-1]) text = self.i2t.generate_text(image_url) texts.append(text) image_urls.append(image_url) conversation_name = self.get_conversation_name(prompt.text) self.conversations[conversation_name] = { "texts": texts, "image_urls": image_urls, "level": prompt.level }
对于每个游戏,对话都记录在一个唯一的名称下,该名称通过哈希T2I模型名称、I2T模型名称和提示文本(get_conversation_name()
方法)生成。
我还为该类配备了一个save_conversations_to_dataset()
方法,该方法将在电话线上播放的所有游戏的图像和描述保存到FiftyOne Dataset
中:
def save_conversations_to_dataset(self, dataset): """Save conversations to a dataset.""" for conversation_name in self.conversations.keys(): conversation = self.conversations[conversation_name] prompt = conversation["texts"][0] level = conversation["level"] image_urls = conversation["image_urls"] texts = conversation["texts"] for i in range(len(image_urls)): filename = f"{conversation_name}_{i}.jpg" filepath = os.path.join(IMAGES_DIR, filename) download_image(image_urls[i], filepath) sample = fo.Sample( filepath = filepath, conversation_name = conversation_name, prompt = prompt, level = level, t2i_model = self.t2i.name, i2t_model = self.i2t.name, step_number = i, text_before = texts[i], text_after = texts[i+1] ) dataset.add_sample(sample)
在稳定扩散和CLIP前缀字幕之间进行的AI电话游戏中图像的进展。
执行对话
有了所有构建块,玩AI电话就是小菜一碟!
我们可以实例化T2I和I2T模型:
## Image2Text modelsmplug_owl = MPLUGOwl()blip = BLIP()clip_prefix = CLIPPrefix()mini_gpt4 = MiniGPT4()image2text_models = [mplug_owl, blip, clip_prefix, mini_gpt4]## Text2Image modelsvqgan_clip = VQGANCLIP()sd = StableDiffusion()dalle2 = DALLE2()text2image_models = [sd, dalle2, vqgan_clip]
然后为每个组合创建一个电话线路:
combos = [(t2i, i2t) for t2i in text2image_models for i2t in image2text_models]lines = [TelephoneLine(*combo) for combo in combos]
然后我们加载我们的提示:
prompts = get_prompts()
接着创建一个FiftyOne的Dataset
,用于存储生成的图像和与对话相关的所有信息:
import fiftyone as fodataset = fo.Dataset(name = 'telephone', persistent=True)dataset.add_sample_field("conversation_name", fo.StringField)dataset.add_sample_field("prompt", fo.StringField)dataset.add_sample_field("level", fo.StringField)dataset.add_sample_field("t2i_model", fo.StringField)dataset.add_sample_field("i2t_model", fo.StringField)dataset.add_sample_field("step_number", fo.IntField)dataset.add_sample_field("text_before", fo.StringField)dataset.add_sample_field("text_after", fo.StringField)
然后我们可以运行所有120个telephone游戏:
from tqdm import tqdmfor line in tqdm(lines): for prompt in prompts: line.play(prompt, nturns = 10) line.save_conversations_to_dataset(dataset)session = fo.launch_app(dataset)
在FiftyOne App中,点击菜单栏中的分割图标,按对话分组图像,选择conversation_name
,然后切换选择器为ordered
并选择step_number
。
结果和结论
为了评估一段会话的质量——纯粹是以最终描述的含义与初始提示的含义有多接近为标准,我决定为提示和描述生成嵌入,并计算它们之间的余弦距离(在[0, 2]
之间)。
from scipy.spatial.distance import cosine as cosine_distance
对于一个嵌入模型,我希望使用一个可以嵌入文本和图像的模型,考虑到这个任务的多模态性质。最终我选择使用ImageBind,原因有三:
- 其他流行的联合图像-文本嵌入模型如CLIP和BLIP与我在实验中使用的一些模型相关(BLIP和CLIP前缀字幕),我想避免使用相同类型的模型可能带来的任何偏差。
- 许多文本嵌入模型具有较小的
max_token_count
——文本中允许的最大标记数。例如,CLIP的max_token_count=77
。我们的一些描述明显比这个要长。幸运的是,ImageBind的最大标记数量要长得多。 - 我一直想尝试ImageBind,这是一个很好的机会!
我在一个名为embed_text(text)
的函数中封装了Replicate的ImageBind API:
MODEL_NAME = "daanelson/imagebind:0383f62e173dc821ec52663ed22a076d9c970549c209666ac3db181618b7a304"def embed_text(text): response = replicate.run( MODEL_NAME, input={ "text_input": text, "modality": "text" } ) return np.array(response)
为了避免冗余计算,我对提示进行了哈希处理,并将提示嵌入存储在一个字典中。这样,我们只需要为每个提示嵌入一次,而不是为12个telephone线路的每个提示都嵌入一次:
import hashlibdef hash_prompt(prompt): return hashlib.md5(prompt.encode()).hexdigest()[:6]### Embed initial promptsprompt_embeddings = {}dataset.add_sample_field("prompt_hash", fo.StringField)## Group samples by initial prompt## Add hash to all samples in groupprompt_groups = dataset.group_by("prompt")for pg in prompt_groups.iter_dynamic_groups(): prompt = pg.first().prompt hash = hash_prompt(prompt) prompt_embeddings[hash] = embed_text(prompt) view = pg.set_field("prompt_hash", hash) view.save("prompt_hash")
然后我们可以按对话名称对样本进行分组,遍历这些组,为每个步骤计算文本嵌入,并记录文本嵌入与初始提示嵌入之间的余弦距离(越小越好):
dataset.add_sample_field("text_after_dist", fo.FloatField)prompt_groups = dataset.group_by("conversation_name")for cg in conversation_groups.iter_dynamic_groups(progress=True): hash = cg.first().prompt_hash prompt_embedding = prompt_embeddings[hash] ordered_samples = cg.sort_by("step_number") for sample in ordered_samples.iter_samples(autosave=True): text_embedding = embed_text(sample.text_after) sample["text_embedding"] = text_embedding sample.text_after_dist = cosine_distance( prompt_embedding, text_embedding )
我计算了每个难度级别下所有提示的每个T2I-I2T对的平均分数,并绘制了结果。在每个视频中,生成的图像上打印了I2T和T2I模型,以及用于生成该图像的文本(红色)和从该图像生成的描述(绿色)。
简单
对于简单的提示,表现往往最大程度地取决于文本到图像模型。 DALL-E2和Stable Diffusion明显优于VQGAN-CLIP。 MiniGPT-4是最佳配对的成员。
以下是上述简单提示的一些示例:
AI电话针对简单提示,带有文本到图像和图像到文本模型的配对。
在使用MiniGPT-4(稍微较少的是BLIP)的游戏中,苹果仍然保持在中心位置,而对于涉及CLIP Prefix的游戏,苹果随着时间的推移逐渐消失。
小猪AI
随着提示变得更加困难,情况开始发生变化。
AI电话针对小猪AI难度提示,带有文本到图像和图像到文本模型的配对。
对于几乎所有游戏,主题在第四或第五步左右发生变化。早期,MiniGPT-4占优势。但到游戏结束时,该优势似乎已经完全丧失。
困难
当提示变得具有挑战性时,我们开始看到一些有趣的事情:在早期阶段,图像到文本模型最重要(MiniGPT-4最好,而CLIP Prefix在大多数情况下最差)。然而,到了后期阶段,文本到图像模型变得最重要。而且,让情况更加复杂的是,VQGAN-CLIP在这里表现最好!
有人可能会担心“更好”只能意味着保持一致性,而不能准确地表示原始概念。但是,当我们查看示例时,我们可以看到这不是这种情况。
AI电话针对困难提示,带有文本到图像和图像到文本模型的配对。
以视频中突出显示的示例为例,初始提示是关于“熙熙攘攘的市场”的“困难”提示。虽然VQGAN-CLIP生成的图像无疑是颗粒状的,但主题仍然可以被辨认出来,并且与原始提示相当吻合。
不可能
毫不奇怪,我们的竞争对手都做得不太好。有人可能会认为VQGAN-CLIP是赢家。但在大多数情况下,这只是噪音。在视频中,即使涉及VQGAN-CLIP的游戏,主题也很难被识别。
AI电话针对“不可能”提示,带有文本到图像和图像到文本模型的配对。
总结
这次探索远非科学:我只观察了十个提示,没有真正验证它们的难度级别。我只将对话运行到了十个来回的步骤;我只评估了一项绩效指标。
很明显,哪些T2I和I2T模型表现最佳在很大程度上取决于提示的复杂性以及您想让模型继续交谈的时间长短。尽管如此,值得注意的是几个关键观察结果:
- 对于更具挑战性的提示,VQGAN-CLIP可能表现更好,但这并不意味着它是更好的T2I模型。 VQGAN-CLIP生成的图像往往比Stable Diffusion或DALL-E2生成的图像不连贯且不全局一致。
- 上述分析都是关于语义相似性的-它不考虑样式。这些图像的样式在AI电话游戏过程中可以发生很大变化。据我所知,对于像mPLUG-Owl这样给出长描述的I2T模型,其风格更加一致,而对于BLIP这样的模型,其描述更加以主题为中心,其风格变化更大。
- 在大约五到六个迭代之后,游戏基本上已经收敛到稳定的均衡状态。
- 尽管嵌入模型ImageBind是多模式的,但是连续图像嵌入和文本嵌入之间的距离远远大于连续图像或连续描述之间的距离。总的来说,它们遵循相同的趋势,但不太明显,这就是我没有在绘图中包括它们的原因。
我希望这能激发你使用生成式人工智能进行自己的实验,无论是玩AI电话还是进行其他实验!
如果你尝试了这个的变种并获得了有趣的结果,请在此留言!