| """ |
| LLaVA 评估工具模块 |
| ================== |
| |
| 本模块提供 LLaVA 评估所需的通用工具函数和类。 |
| |
| 主要功能: |
| 1. OpenAIModel 类: 封装 OpenAI API 调用(支持批量并行) |
| 2. 模型初始化: init_model() - 加载 LLaVA 模型 |
| 3. 数据读取: read_json() - 读取 JSON/JSONL 文件 |
| 4. 同义词映射: object_synonyms_txt - COCO 物体同义词表 |
| 5. 文本处理: remove_negetive_sents(), remove_woodpecker_boxes() |
| |
| 用途: |
| - 在各种评估脚本中导入使用 |
| - 提供统一的 OpenAI API 调用接口 |
| - 提供物体同义词映射 |
| """ |
|
|
| from __future__ import annotations |
|
|
| import json |
| import os |
| import re |
| import sys |
| import time |
| from concurrent.futures import Future, ThreadPoolExecutor, as_completed |
|
|
| |
| |
| |
| try: |
| import nltk |
| except ImportError: |
| nltk = None |
|
|
| try: |
| import openai |
| from openai import OpenAI |
| from openai.types.chat.chat_completion import ChatCompletion |
| except ImportError: |
| openai = None |
| OpenAI = None |
| ChatCompletion = None |
|
|
| from tqdm import tqdm |
|
|
| |
| sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) |
| sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../../.."))) |
| from llava.model.builder import load_pretrained_model |
| from llava.utils import disable_torch_init |
|
|
| |
| |
| |
| |
| DEFAULT_BASE_URL = os.getenv("OPENAI_BASE_URL") or os.getenv("OPENAI_API_BASE") or "https://api.openai.com" |
| DEFAULT_API_KEY = os.getenv("OPENAI_API_KEY") or "" |
|
|
| |
| NUM_SECONDS_TO_SLEEP = 0.5 |
| MAX_RETRIES = 3 |
|
|
|
|
| class OpenAIModel: |
| """ |
| OpenAI API 封装类 |
| |
| 提供方便的接口调用 OpenAI API(或兼容 API),支持: |
| - 批量并行请求(提高效率) |
| - 自动重试(处理 rate limit 等错误) |
| - 灵活的输入格式(支持 users/systems 或 messages) |
| |
| Attributes: |
| client: OpenAI 客户端实例 |
| model: 默认使用的模型名称(如 "gpt-4") |
| |
| Example: |
| >>> openai_model = OpenAIModel(model="gpt-4") |
| >>> # 单个请求 |
| >>> response = openai_model.gen(users="Describe this image.") |
| >>> # 批量请求(并行) |
| >>> responses = openai_model.gen( |
| ... users=["Question 1", "Question 2"], |
| ... use_parallel=True |
| ... ) |
| """ |
| |
| def __init__( |
| self, |
| base_url: str | None = None, |
| api_key: str | None = None, |
| model: str | None = None, |
| timeout_sec: int = 20, |
| ): |
| """ |
| 初始化 OpenAI 客户端 |
| |
| Args: |
| base_url: API 根地址(如果为 None,从环境变量读取) |
| api_key: API 密钥(如果为 None,从环境变量读取) |
| model: 默认模型名称 |
| timeout_sec: 请求超时时间(秒) |
| """ |
| base_url = (base_url or DEFAULT_BASE_URL).strip() |
| api_key = (api_key or DEFAULT_API_KEY).strip() |
|
|
| |
| |
| |
| |
| |
| if base_url.endswith("/"): |
| base_url = base_url[:-1] |
| if base_url.endswith("/v1"): |
| base_url = base_url[: -len("/v1")] |
| base_url = f"{base_url}/v1/" |
|
|
| |
| if not api_key: |
| raise ValueError( |
| "缺少 OpenAI API key。请设置环境变量 OPENAI_API_KEY 或通过 --openai_key 参数传递。" |
| ) |
| |
| |
| self.client: OpenAI = OpenAI( |
| base_url=base_url, |
| api_key=api_key, |
| timeout=timeout_sec, |
| max_retries=MAX_RETRIES, |
| ) |
| self.model: str | None = model |
|
|
| def _u_a_to_messages(self, users: list[str], systems: list[str]) -> list[list[dict]]: |
| assert len(users) == len(systems), "Length of users and systems must be the same." |
| messages_list: list[list[dict]] = [] |
| for u, s in zip(users, systems): |
| messages: list[dict] = [] |
| if s: |
| messages.append({"role": "system", "content": s}) |
| if u: |
| messages.append({"role": "user", "content": u}) |
| messages_list.append(messages) |
| return messages_list |
|
|
| def _prepare_messages_list( |
| self, |
| users: list[str] | str | None, |
| systems: list[str] | str | None, |
| messages: list[list[dict]] | list[dict] | None, |
| ) -> list[list[dict]]: |
| def ensure_lists(*args) -> list: |
| """ |
| 确保输入的参数都是列表形式,并且第一个参数的长度决定了后续参数的列表长度。 |
| |
| Args: |
| 多个参数,每个参数可以是单个元素或列表。 |
| Returns: |
| 处理后的参数列表,每个参数都是列表形式。 |
| 第一个参数的长度将决定后续参数的列表长度。 |
| 如果某个参数是单个元素,则会被转换为包含该元素的列表。 |
| 如果某个参数是列表,则保持不变。 |
| """ |
| if not args: |
| return [] |
| first_arg: list = args[0] if isinstance(args[0], list) else [args[0]] |
| length: int = len(first_arg) |
| result = [first_arg] + [ |
| a |
| if isinstance(a, list) and len(a) == length |
| else ([a] * length if not isinstance(a, list) else a * (length // len(a)) + a[: length % len(a)]) |
| for a in args[1:] |
| ] |
| return result if len(result) > 1 else result[0] |
|
|
| if users is not None: |
| users, systems = ensure_lists(users, systems) |
| return self._u_a_to_messages(users, systems) |
| elif systems is not None: |
| systems, users = ensure_lists(systems, users) |
| return self._u_a_to_messages(users, systems) |
| else: |
| if not isinstance(messages[0], list): |
| return [messages] |
| return messages |
|
|
| def gen( |
| self, |
| users: list[str] | str | None = None, |
| systems: list[str] | list | None = None, |
| messages: list[list[dict]] | list[dict] | None = None, |
| temperature: float = 0.2, |
| max_tokens: int = 512, |
| model: str | None = None, |
| sample=False, |
| force_list: bool = False, |
| return_completions: bool = False, |
| use_parallel: bool = True, |
| use_tqdm: bool = False, |
| max_workers: int = 64, |
| ) -> list[str] | str: |
| """ |
| 调用 OpenAI API 生成文本 |
| |
| 支持两种输入方式: |
| 1. 使用 users 和 systems 参数(推荐,简洁) |
| 2. 使用 messages 参数(灵活,完全控制) |
| |
| Args: |
| users: 用户消息(单个或列表) |
| systems: 系统消息(单个或列表) |
| messages: 完整的消息列表(与 users/systems 二选一) |
| temperature: 采样温度(0=确定性,越高越随机) |
| max_tokens: 最大生成 token 数 |
| model: 模型名称(如果为 None 使用初始化时的 model) |
| sample: 是否采样多个候选(n=5) |
| force_list: 是否强制返回列表 |
| return_completions: 是否返回完整的 completion 对象 |
| use_parallel: 是否使用并行请求(多个请求时) |
| use_tqdm: 是否显示进度条 |
| max_workers: 并行线程数 |
| |
| Returns: |
| str | list[str]: 生成的文本(单个或列表) |
| |
| Example: |
| >>> # 单个请求 |
| >>> response = model.gen(users="Hello") |
| >>> # 批量并行请求 |
| >>> responses = model.gen( |
| ... users=["Q1", "Q2", "Q3"], |
| ... use_parallel=True |
| ... ) |
| """ |
| assert (users is None and systems is None) == (messages is not None), "Invalid input arguments." |
|
|
| messages: list[list[dict]] = self._prepare_messages_list(users, systems, messages) |
|
|
| n = 1 if not sample else 5 |
| outputs: list[str | list[str]] = [None] * len(messages) |
|
|
| def gen_completion(messages: list[dict]) -> str | list[str]: |
| completions: ChatCompletion = self._gen( |
| messages=messages, |
| model=model if model else self.model, |
| temperature=temperature, |
| max_tokens=max_tokens, |
| n=n, |
| ) |
|
|
| if return_completions: |
| return completions |
| if len(completions.choices) == 1: |
| return completions.choices[0].message.content.strip() |
| else: |
| return [choice.message.content.strip() for choice in completions.choices] |
|
|
| if len(messages) == 1: |
| use_tqdm = False |
|
|
| if use_tqdm: |
| pb = tqdm(total=len(messages)) |
|
|
| if use_parallel and len(messages) > 1: |
| with ThreadPoolExecutor(max_workers=max_workers) as executor: |
| futures: dict[Future, int] = {executor.submit(gen_completion, m): i for i, m in enumerate(messages)} |
|
|
| for completed_future in as_completed(futures): |
| if use_tqdm: |
| pb.update(1) |
| index: int = futures[completed_future] |
| outputs[index] = completed_future.result() |
| else: |
| for i, m in enumerate(messages): |
| outputs[i] = gen_completion(m) |
| if use_tqdm: |
| pb.update(1) |
|
|
| return outputs if force_list or len(outputs) > 1 else outputs[0] |
|
|
| def _gen( |
| self, |
| messages: list[dict], |
| max_tokens: int = 512, |
| model: str | None = None, |
| temperature: float = 0.2, |
| n: int = 1, |
| seed: int | None = None, |
| top_p: float = 1.0, |
| ) -> ChatCompletion: |
| assert model or self.model, "Model must be provided." |
| last_err: Exception | None = None |
| for _attempt in range(MAX_RETRIES): |
| try: |
| response: ChatCompletion = self.client.chat.completions.create( |
| messages=messages, |
| model=model if model else self.model, |
| temperature=temperature, |
| max_tokens=max_tokens, |
| n=n, |
| seed=seed, |
| top_p=top_p, |
| ) |
| return response |
| except openai.RateLimitError: |
| last_err = None |
| except Exception as e: |
| last_err = e |
| print(f"Error when generating: {e}") |
| time.sleep(NUM_SECONDS_TO_SLEEP) |
| raise RuntimeError( |
| "OpenAI request failed after retries. " |
| "If you're behind a proxy, set OPENAI_BASE_URL/OPENAI_API_BASE; " |
| "otherwise check networking/DNS and that your key is valid." |
| ) from last_err |
|
|
|
|
| def init_model(args): |
| """ |
| 初始化 LLaVA 模型 |
| |
| 加载预训练的 LLaVA 模型,支持基础模型和 LoRA 微调模型。 |
| |
| Args: |
| args: 参数对象,包含: |
| - model_path: 模型路径(基础模型或 LoRA 模型) |
| - model_base: 基础模型路径(使用 LoRA 时需要) |
| |
| Returns: |
| tuple: (tokenizer, model, image_processor) |
| |
| Example: |
| >>> args.model_path = "llava-hf/llava-1.5-7b-hf" |
| >>> args.model_base = None |
| >>> tokenizer, model, image_processor = init_model(args) |
| """ |
| disable_torch_init() |
| model_path = os.path.expanduser(args.model_path) |
|
|
| |
| if not args.model_base or args.model_base == "None" or len(args.model_base) < 5: |
| |
| model_base = None |
| model_name = "llava-v1.5-7b" |
| else: |
| |
| model_base = args.model_base |
| model_name = "llava-v1.5-7b-lora" |
| |
| print(f"正在加载模型: {model_path},基础模型: {model_base}...") |
| tokenizer, model, image_processor, _ = load_pretrained_model(model_path, model_base, model_name) |
|
|
| return tokenizer, model, image_processor |
|
|
|
|
| def read_json(file_path: str) -> list[dict] | dict: |
| """ |
| 读取 JSON 文件,支持多种格式 |
| |
| 支持的格式: |
| - .json / .jsonfile: 标准 JSON 格式 |
| - .jsonl: JSON Lines 格式(每行一个 JSON 对象) |
| |
| Args: |
| file_path: 文件路径 |
| |
| Returns: |
| - .json: 返回 dict 或 list |
| - .jsonl: 返回 list[dict] |
| |
| Raises: |
| ValueError: 不支持的文件扩展名 |
| """ |
| ext = os.path.splitext(file_path)[-1] |
| if ext == ".json" or ext == ".jsonfile": |
| with open(os.path.expanduser(file_path), "r", encoding="utf-8") as f: |
| data = json.load(f) |
| elif ext == ".jsonl": |
| with open(os.path.expanduser(file_path), "r", encoding="utf-8") as f: |
| data = [json.loads(line) for line in f] |
| else: |
| raise ValueError(f"不支持的文件扩展名 {ext},文件: {file_path}") |
| return data |
|
|
|
|
| |
| |
| |
| |
| object_synonyms_txt = """ |
| person, girl, boy, man, woman, kid, child, chef, baker, people, adult, rider, children, baby, worker, passenger, sister, brother, biker, policeman, cop, officer, lady, cowboy, bride, groom, male, female, guy, traveler, mother, father, gentleman, pitcher, player, skier, snowboarder, skater, skateboarder, guy, foreigner, child, gentleman, caller, offender, coworker, trespasser, patient, politician, soldier, grandchild, serviceman, walker, drinker, doctor, bicyclist, thief, buyer, teenager, student, camper, driver, solider, hunter, shopper, villager, pedestrian |
| bicycle, bike, unicycle, minibike, trike |
| car, automobile, van, minivan, sedan, suv, hatchback, cab, jeep, coupe, taxicab, limo, taxi |
| motorcycle, scooter, motor bike, motor cycle, motorbike, scooter, moped |
| airplane, jetliner, plane, air plane, monoplane, aircraft, jet, jetliner, airbus, biplane, seaplane |
| bus, minibus, trolley |
| train, locomotive, tramway, caboose |
| truck, pickup, lorry, hauler, firetruck |
| boat, ship, liner, sailboat, motorboat, dinghy, powerboat, speedboat, canoe, skiff, yacht, kayak, catamaran, pontoon, houseboat, vessel, rowboat, trawler, ferryboat, watercraft, tugboat, schooner, barge, ferry, sailboard, paddleboat, lifeboat, freighter, steamboat, riverboat, battleship, steamship |
| traffic light, street light, traffic signal, stop light, streetlight, stoplight |
| fire hydrant, hydrant |
| stop sign |
| parking meter |
| bench, pew |
| bird, ostrich, owl, seagull, goose, duck, parakeet, falcon, robin, pelican, waterfowl, heron, hummingbird, mallard, finch, pigeon, sparrow, seabird, osprey, blackbird, fowl, shorebird, woodpecker, egret, chickadee, quail, bluebird, kingfisher, buzzard, willet, gull, swan, bluejay, flamingo, cormorant, parrot, loon, gosling, waterbird, pheasant, rooster, sandpiper, crow, raven, turkey, oriole, cowbird, warbler, magpie, peacock, cockatiel, lorikeet, puffin, vulture, condor, macaw, peafowl, cockatoo, songbird |
| cat, kitten, feline, tabby |
| dog, puppy, beagle, pup, chihuahua, schnauzer, dachshund, rottweiler, canine, pitbull, collie, pug, terrier, poodle, labrador, doggie, doberman, mutt, doggy, spaniel, bulldog, sheepdog, weimaraner, corgi, cocker, greyhound, retriever, brindle, hound, whippet, husky |
| horse, colt, pony, racehorse, stallion, equine, mare, foal, palomino, mustang, clydesdale, bronc, bronco |
| sheep, lamb, ram, lamb, goat, ewe |
| cow, cattle, oxen, ox, calf, cattle, holstein, heifer, buffalo, bull, zebu, bison |
| elephant |
| bear, panda |
| zebra |
| giraffe |
| backpack, knapsack |
| umbrella |
| handbag, wallet, purse, briefcase |
| tie, bow, bow tie |
| suitcase, suit case, luggage |
| frisbee |
| skis, ski |
| snowboard |
| sports ball, ball |
| kite |
| baseball bat |
| baseball glove |
| skateboard |
| surfboard, longboard, skimboard, shortboard, wakeboard |
| tennis racket, racket |
| bottle |
| wine glass |
| cup |
| fork |
| knife, pocketknife, knive |
| spoon |
| bowl, container |
| banana |
| apple |
| sandwich, burger, sub, cheeseburger, hamburger |
| orange |
| broccoli |
| carrot |
| hot dog |
| pizza |
| donut, doughnut, bagel |
| cake, cheesecake, cupcake, shortcake, coffeecake, pancake |
| chair, seat, stool |
| couch, sofa, recliner, futon, loveseat, settee, chesterfield |
| potted plant, houseplant |
| bed |
| dining table, table, desk, coffee table |
| toilet, urinal, commode, toilet, lavatory, potty |
| tv, monitor, televison, television |
| laptop, computer, notebook, netbook, lenovo, macbook, laptop computer |
| mouse |
| remote, remote control |
| keyboard |
| cell phone, mobile phone, phone, cellphone, telephone, phon, smartphone, iPhone |
| microwave |
| oven, stovetop, stove, stove top oven |
| toaster |
| sink |
| refrigerator, fridge, fridge, freezer |
| book |
| clock |
| vase |
| scissors |
| teddy bear, teddybear |
| hair drier, hairdryer |
| toothbrush |
| """ |
|
|
| |
| |
| visual_genome_obj: list[str] = [ |
| "tree", |
| "window", |
| "shirt", |
| "building", |
| "person", |
| "table", |
| "car", |
| "door", |
| "light", |
| "fence", |
| "chair", |
| "people", |
| "plate", |
| "glass", |
| "jacket", |
| "sidewalk", |
| "snow", |
| "flower", |
| "hat", |
| "bag", |
| "track", |
| "roof", |
| "umbrella", |
| "helmet", |
| "plant", |
| "train", |
| "bench", |
| "box", |
| "food", |
| "pillow", |
| "bus", |
| "bowl", |
| "horse", |
| "trunk", |
| "clock", |
| "mountain", |
| "elephant", |
| "giraffe", |
| "banana", |
| "house", |
| "cabinet", |
| "hill", |
| "dog", |
| "book", |
| "bike", |
| "coat", |
| "glove", |
| "zebra", |
| "bird", |
| "motorcycle", |
| "lamp", |
| "cow", |
| "skateboard", |
| "surfboard", |
| "beach", |
| "sheep", |
| "kite", |
| "cat", |
| "pizza", |
| "bed", |
| "bear", |
| "windshield", |
| "towel", |
| "desk", |
| ] |
|
|
| |
| |
| relation_sysnonyms_txt = """ |
| in, on, at |
| equals, is |
| belongs to, is part of |
| """ |
|
|
| |
| |
| |
| coco_double_words = [ |
| "motor bike", |
| "motor cycle", |
| "air plane", |
| "traffic light", |
| "street light", |
| "traffic signal", |
| "stop light", |
| "fire hydrant", |
| "stop sign", |
| "parking meter", |
| "suit case", |
| "sports ball", |
| "baseball bat", |
| "baseball glove", |
| "tennis racket", |
| "wine glass", |
| "hot dog", |
| "cell phone", |
| "mobile phone", |
| "teddy bear", |
| "hair drier", |
| "potted plant", |
| "bow tie", |
| "laptop computer", |
| "stove top oven", |
| "hot dog", |
| "teddy bear", |
| "home plate", |
| "train track", |
| "dining table", |
| "coffee table", |
| ] |
| |
| |
| animal_words = ["bird", "cat", "dog", "horse", "sheep", "cow", "elephant", "bear", "zebra", "giraffe", "animal", "cub"] |
| |
| vehicle_words = ["jet", "train"] |
|
|
|
|
| |
|
|
| def remove_negetive_sents(caption: str) -> str: |
| """ |
| 移除描述中的否定句 |
| |
| 移除包含 "There is no" 或 "There are no" 的句子, |
| 因为这些句子不包含实际物体信息。 |
| |
| Args: |
| caption: 输入描述文本 |
| |
| Returns: |
| str: 移除否定句后的文本 |
| |
| Example: |
| >>> remove_negetive_sents("A dog is here. There is no cat.") |
| "A dog is here." |
| """ |
| sents: list[str] = nltk.sent_tokenize(caption) |
| sents = [sent for sent in sents if "There is no" not in sent and "There are no" not in sent] |
| return " ".join(sents) |
|
|
|
|
| def remove_woodpecker_boxes(text: str) -> str: |
| """ |
| 移除 Woodpecker 生成的边界框标记 |
| |
| Woodpecker 是一个视觉幻觉纠正方法,会在输出中添加边界框标记。 |
| 本函数移除这些标记以便评估。 |
| |
| 标记格式: ([x1, y1, x2, y2]) 或 ([...]; |
| |
| Args: |
| text: 包含边界框标记的文本 |
| |
| Returns: |
| str: 移除标记后的文本 |
| """ |
| text = re.sub(r"\(\[.*?\]\)", "", text) |
| text = re.sub(r"\(\[.*?\]\;", "", text) |
| text = re.sub(r"\[.*?\]\;", "", text) |
| return text |
|
|