VisProg:根据自然语言指令解决复杂视觉任务
1. 介绍
VisProg 是一种神经符号系统,可以根据自然语言指令解决复杂的组合视觉任务。VisProg 使用 GPT3 的上下文学习能力来生成 Python 程序,然后执行这些程序以获得解决方案和全面且可解释的基本原理。生成的程序的每一行都可以调用几个现成的计算机视觉模型、图像处理例程或Python函数之一来产生可由程序的后续部分使用的中间输出。
相关论文:Paper for VisProg
2. 安装和配置 VisProg
首先,如果你的系统上还没有安装 Conda,请前往 Anaconda 安装适合你的平台的 Conda 工具。
然后,使用以下命令安装和配置 VisProg:
# 克隆 VisProg 仓库
git clone https://github.com/allenai/visprog.git
# 切换到仓库目录
cd visprog
# 创建 conda 环境并安装依赖
conda env create -f environment.yaml
# 激活 VisProg 环境
conda activate visprog
3. 使用VisProg
在VSCODE中打开文件,填入对应的OpenAI密钥
笔者在测试时Conda环境有相关依赖需要补充安装:
在visprog环境下执行
pip install appdirs
pip install ipywidgets
4. VisProg 解读
VisProg的关键组成部分是一系列名为interpreter的类,这些类在visprog/engine/step_interpreters.py
文件中定义。
例如,EvalInterpreter 类解析和执行 ‘EVAL’ 步骤。它首先使用 parse 方法来解析步骤,然后使用 execute 方法来执行该步骤。如果 inspect 参数为 True,execute 方法还会生成描述该步骤的 HTML 字符串。
class EvalInterpreter():
step_name = 'EVAL'
def __init__(self):
print(f'Registering {self.step_name} step')
# 解析步骤
def parse(self,prog_step):
parse_result = parse_step(prog_step.prog_str)
step_name = parse_result['step_name']
output_var = parse_result['output_var']
step_input = eval(parse_result['args']['expr'])
assert(step_name==self.step_name)
return step_input, output_var
# 生成 HTML 字符串
def html(self,eval_expression,step_input,step_output,output_var):
eval_expression = eval_expression.replace('{','').replace('}','')
step_name = html_step_name(self.step_name)
var_name = html_var_name(output_var)
output = html_output(step_output)
expr = html_arg_name('expression')
return f"""<div>{var_name}={step_name}({expr}="{eval_expression}")={step_name}({expr}="{step_input}")={output}</div>"""
# 执行步骤
def execute(self,prog_step,inspect=False):
step_input, output_var = self.parse(prog_step)
prog_state = dict()
for var_name,var_value in prog_step.state.items():
if isinstance(var_value,str):
if var_value in ['yes','no']:
prog_state[var_name] = var_value=='yes'
elif var_value.isdecimal():
prog_state[var_name] = var_value
else:
prog_state[var_name] = f"'{var_value}'"
else:
prog_state[var_name] = var_value
eval_expression = step_input
if 'xor' in step_input:
step_input = step_input.replace('xor','!=')
step_input = step_input.format(**prog_state)
step_output = eval(step_input)
prog_step.state[output_var] = step_output
if inspect:
html_str = self.html(eval_expression, step_input, step_output, output_var)
return step_output, html_str
return step_output
Breadcrumbsvisprog/engine /step_interpreters.py
中的parse_step函数从步骤字符串中解析出步骤名称、输出变量和参数。它使用了 Python 的 tokenize
库来解析步骤字符串。
def parse_step(step_str,partial=False):
tokens = list(tokenize.generate_tokens(io.StringIO(step_str).readline))
output_var = tokens[0].string
step_name = tokens[2].string
parsed_result = dict(
output_var=output_var,
step_name=step_name)
if partial:
return parsed_result
arg_tokens = [token for token in tokens[4:-3] if token.string not in [',','=']]
num_tokens = len(arg_tokens) // 2
args = dict()
for i in range(num_tokens):
args[arg_tokens[2*i].string] = arg_tokens[2*i+1].string
parsed_result['args'] = args
return parsed_result
5. Notebooks 介绍
VisProg 还包含一些 Jupyter 笔记本,用于展示其在不同任务上的应用:
notebooks/ok_det.ipynb
:这个 notebook 与 “外部知识对象标记(Outside Knowledge Object Tagging)” 相关。它包含了一些用于通过自然语言处理技术对外部知识对象进行标注的代码和示例。notebooks/image_editing.ipynb
:这个 notebook 与 “自然语言图像编辑(Natural Language Image Editing)” 相关。它包含了一些用于根据自然语言指令对图像进行编辑和处理的代码和示例。notebooks/nlvr.ipynb
:这个 notebook 与 “自然语言视觉推理(Natural Language Visual Reasoning)” 相关。它包含了一些用于处理自然语言与图像之间的推理任务的代码和示例。notebooks/gqa.ipynb
:这个 notebook 与 “视觉问答(Visual Question Answering)” 相关。它包含了一些用于处理视觉问答任务的代码和示例。
整体流程梳理
此处以 notebooks/gqa.ipynb
为例子,梳理VisProg
的程序整体流程:
# 导入必要的库
import os
import sys
from PIL import Image
from IPython.core.display import HTML
from functools import partial
# 添加上级目录到系统路径,使得可以导入在上级目录中的模块
module_path = os.path.abspath(os.path.join('..'))
if module_path not in sys.path:
sys.path.append(module_path)
# 设置OpenAI Key环境变量
%env OPENAI_API_KEY=<Enter you key here>
# 从自定义模块导入函数和类
from engine.utils import ProgramGenerator, ProgramInterpreter
from prompts.gqa import create_prompt
# 创建ProgramGenerator和ProgramInterpreter实例对象
# 这些对象将被用于,生成程序和解释程序
# partial方法调用create_prompt生成测试用的Prompts,这些prompts作为generator的输入
interpreter = ProgramInterpreter(dataset='gqa')
prompter = partial(create_prompt,method='all')
generator = ProgramGenerator(prompter=prompter)
# 读取一张图片,并将其大小缩放为合适的大小,并转换为 RGB 格式
# 将要进行推理的图像加载到内存中,以便在后续的操作中使用
image = Image.open('../assets/camel1.png')
image.thumbnail((640,640),Image.Resampling.LANCZOS)
init_state = dict(IMAGE=image.convert('RGB'))
image
# 指定自然语言问题/陈述/指令:定义你的自然语言问题、陈述或指令,这将作为输入提供给程序生成器以生成相应的程序
question = "How many people or animals are in the image?"
# 使用程序生成器对象,将指定的问题/陈述/指令作为输入,生成相应的程序。
prog,_ = generator.generate(dict(question=question))
print(prog)
# 利用程序解释器对象,对生成的程序进行解释和执行,返回结果
result, prog_state, html_str = interpreter.execute(prog,init_state,inspect=True)
# 输出程序的结果
result
# 输出HTML字符串
# 将返回的结果以及执行过程的可视化(比如执行追踪)展示出来,以便更好地理解和分析程序的执行情况
# 该做法也是OpenAI Code Interpreter的做法,使用可视化增强程序的可解释性
HTML(html_str)
对于其中出现的ProgramInterpreter.execute
,create_prompt
,ProgramGenerator.generate
进一步解释如下:
visprog/engine/utils.py/ProgramInterpreter
class ProgramInterpreter:
def __init__(self, dataset='nlvr'):
"""
初始化 ProgramInterpreter 类的实例。
参数:
dataset (str): 一个字符串,用来注册程序步骤的解释器。
属性:
step_interpreters (dict): 字典,存储了与每个程序步骤名称对应的解释器。
"""
# .step_interpreter中包含了各类图像处理的解释器
self.step_interpreters = register_step_interpreters(dataset)
def execute_step(self, prog_step, inspect):
"""
执行一个程序步骤,并返回结果。
参数:
prog_step (Program): 需要执行的程序步骤。
inspect (bool): 是否需要返回可供检查的结果。
返回值:
根据 inspect 的值,可能会返回步骤的输出结果,也可能会返回一个包含步骤的输出结果和 HTML 字符串的元组。
"""
# 解析程序步骤的字符串形式,获取步骤名称
step_name = parse_step(prog_step.prog_str, partial=True)['step_name']
print(step_name)
# 从步骤解释器字典中获取对应的解释器,然后用它执行程序步骤
return self.step_interpreters[step_name].execute(prog_step, inspect)
def execute(self, prog, init_state, inspect=False):
"""
执行一个完整的程序,并返回结果。
参数:
prog (str or Program): 需要执行的程序,可以是字符串形式,也可以是 Program 类的实例。
init_state (dict): 程序的初始状态。
inspect (bool): 是否需要返回可供检查的结果。
返回值:
根据 inspect 的值,可能会返回程序的输出结果和状态,也可能会返回一个包含程序的输出结果、状态和 HTML 字符串的元组。
"""
# 如果程序是字符串形式,则转化为 Program 类的实例
if isinstance(prog, str):
prog = Program(prog, init_state)
else:
assert(isinstance(prog, Program))
# 将程序的每个指令都转化为 Program 类的实例
prog_steps = [Program(instruction, init_state=prog.state) \
for instruction in prog.instructions]
html_str = '<hr>'
for prog_step in prog_steps:
if inspect:
# 如果需要返回可供检查的结果,则执行每个步骤时都返回步骤的输出结果和 HTML 字符串
step_output, step_html = self.execute_step(prog_step, inspect)
html_str += step_html + '<hr>'
else:
# 否则,只返回步骤的输出结果
step_output = self.execute_step(prog_step, inspect)
# 返回程序的结果
if inspect:
return step_output, prog.state, html_str
return step_output, prog.state
visprog/engine/utils.py/ProgramGenerator
class ProgramGenerator():
def __init__(self, prompter, temperature=0.7, top_p=0.5, prob_agg='mean'):
"""
初始化 ProgramGenerator 类的实例。
参数:
prompter (function): 函数,用于生成 prompt。
temperature (float): 控制生成的文本的随机性的参数,值越高,结果越随机。
top_p (float): 控制生成的文本的多样性的参数,值越高,结果越多样。
prob_agg (str): 用于计算输出文本概率的聚合函数,可以是 'mean' 或 'sum'。
属性:
prompter (function): 存储输入的 prompter。
temperature (float): 存储输入的 temperature。
top_p (float): 存储输入的 top_p。
prob_agg (str): 存储输入的 prob_agg。
"""
openai.api_key = os.getenv("OPENAI_API_KEY")
self.prompter = prompter
self.temperature = temperature
self.top_p = top_p
self.prob_agg = prob_agg
def compute_prob(self, response):
"""
计算生成的文本的概率。
参数:
response (openai.completion_v1.Completion): OpenAI API 返回的响应。
返回值:
float: 生成的文本的概率。
"""
eos = ''
for i, token in enumerate(response.choices[0]['logprobs']['tokens']):
if token == eos:
break
if self.prob_agg == 'mean':
agg_fn = np.mean
elif self.prob_agg == 'sum':
agg_fn = np.sum
else:
raise NotImplementedError
return np.exp(agg_fn(response.choices[0]['logprobs']['token_logprobs'][:i]))
def generate(self, inputs):
"""
根据输入生成一个程序。
参数:
inputs (dict): 字典,包含了生成 prompt 所需的输入信息。
返回值:
tuple: 包含生成的程序和程序的概率的元组。
"""
response = openai.Completion.create(
model="text-davinci-003",
prompt=self.prompter(inputs),
temperature=self.temperature,
max_tokens=512,
top_p=self.top_p,
frequency_penalty=0,
presence_penalty=0,
n=1,
logprobs=1
)
prob = self.compute_prob(response)
prog = response.choices[0]['text'].lstrip('\n').rstrip('\n')
return prog, prob
visprog/prompts/gqa.py/create_prompt
def create_prompt(inputs, num_prompts=8, method='random', seed=42, group=0):
"""
创建一个提示字符串,该字符串包含一个问题和一些之前生成的程序示例。
参数:
inputs (dict): 一个字典,包含需要插入到提示中的值。它应该有一个名为'question'的键,对应的值将被插入到提示的最后一个问题中。
num_prompts (int, 可选): 如果 method='random',这个参数决定了选择多少个随机的程序示例来构成提示。默认值为8。
method (str, 可选): 选择程序示例的方法。如果为'random',将会随机选择;如果为'all',将使用所有程序示例。默认值为'random'。
seed (int, 可选): 用于随机数生成器的种子,以便于复现。默认值为42。
group (int, 可选): 未使用的参数,保留给可能的未来扩展。默认值为0。
返回值:
str: 生成的提示字符串,它包含一些程序示例,然后是一个问题,最后是"Program:",表明下一部分应该是一个程序。
"""
if method == 'all':
# 如果方法为 'all',则选择所有的程序示例
prompt_examples = GQA_CURATED_EXAMPLES
elif method == 'random':
# 如果方法为 'random',则随机选择一些程序示例
random.seed(seed) # 设置随机数生成器的种子,以便复现
prompt_examples = random.sample(GQA_CURATED_EXAMPLES, num_prompts) # 随机选择 num_prompts 个程序示例
else:
# 如果 method 不是 'all' 或 'random',则抛出错误
raise NotImplementedError
# 将选择的程序示例合并为一个字符串,每个示例之间用换行符分隔
prompt_examples = '\n'.join(prompt_examples)
# 在前面添加一些指示性的文字
prompt_examples = f'Think step by step to answer the question.\n\n{prompt_examples}'
# 在最后添加问题和 "Program:"
return prompt_examples + "\nQuestion: {question}\nProgram:".format(**inputs)
6. 结论
本文主要介绍了VisProg,这是一种神经符号系统,可以根据自然语言指令解决复杂的组合视觉任务。VisProg利用GPT3的上下文学习能力生成Python程序,通过执行这些程序来找到解决方案,并提供全面且可解释的解答。
安装和配置VisProg主要涉及克隆VisProg仓库,创建和激活Conda环境,并安装相关依赖。
VisProg的主要组件是一系列名为interpreter的类,这些类定义在visprog/engine/step_interpreters.py
文件中。每个类都有解析和执行步骤的方法,如果inspect参数为True,execute方法还会生成描述该步骤的HTML字符串。
VisProg还提供了一些Jupyter笔记本,展示了在不同任务上的应用,包括外部知识对象标记、自然语言图像编辑、自然语言视觉推理和视觉问答等任务。