Files
2026-07-13 13:30:30 +08:00

208 lines
6.4 KiB
Python

# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Evaluation code for document classification use case. This code reads in a
CSV file with image paths and labels, prepares the data for evaluation, runs
inference using a specified Gemini model, and then evaluates the predictions
against the reference labels using the exact match metric in the Generative AI
Evaluation framework.
"""
import dotenv
import json
import os
import pandas as pd
from google.genai import types
import vertexai
import document_processing
# Load environment variables.
dotenv.load_dotenv()
PROJECT_ID = os.environ.get("GEMINI_PROJECT_ID")
if not PROJECT_ID:
raise ValueError("GEMINI_PROJECT_ID environment variable must be set.")
LOCATION = os.environ.get("GEMINI_LOCATION", "global")
IMAGE_PATHS = os.environ.get("IMAGE_PATHS", "")
IMAGE_PREFIX = os.environ.get("IMAGE_PREFIX", "")
EVAL_DEST = os.environ.get("EVAL_DEST")
# Other default constants.
EVAL_MODEL = "gemini-2.5-flash"
SAMPLE_SIZE = 10
def load_eval_data(csv_path: str, image_prefix: str) -> pd.DataFrame:
"""Reads eval data from CSV, formats paths, and prepares labels."""
df = pd.read_csv(csv_path)
df = df[["img_path", "label"]]
df["img_path"] = f"{image_prefix}/" + df["img_path"]
df = df.rename(columns={"label": "reference"})
return df
def prepare_eval_df(
csv_path: str,
image_prefix: str,
sample_size: int = None,
random_state: int = None,
stratify: bool = False,
classes: list[str] = None
) -> pd.DataFrame:
"""Prepares the eval_df based on the data from csv file with image paths."""
config_classes = (
document_processing.CONFIGS["classification_config"]["classes"]
)
if classes is None:
prompt_classes = config_classes
filter_classes = list(config_classes.keys())
else:
prompt_classes = {
k: v for k, v in config_classes.items() if k in classes
}
filter_classes = classes
prompt = document_processing.CLASSIFY_PROMPT_TEMPLATE.format(
classes=json.dumps(prompt_classes, indent=4)
)
print(prompt)
df = load_eval_data(csv_path, image_prefix)
# Filter the DataFrame to only include the requested classes
df = df[df["reference"].isin(filter_classes)].reset_index(drop=True)
requests = []
for uri in df["img_path"]:
image_part = types.Part.from_uri(
file_uri=uri,
mime_type="image/png"
)
requests.append([image_part, prompt])
df["request"] = requests
if sample_size and sample_size < len(df):
if stratify:
# Proportional stratified sampling per class
fraction = sample_size / len(df)
df = df.groupby("reference", group_keys=False).apply(
lambda x: x.sample(
n=max(1, int(round(len(x) * fraction))),
random_state=random_state
)
)
# Correct any slight oversampling due to rounding
if len(df) > sample_size:
df = df.sample(n=sample_size, random_state=random_state)
df = df.reset_index(drop=True)
else:
df = df.sample(
n=sample_size,
random_state=random_state
).reset_index(drop=True)
return df
def extract_class(response_str):
"""Extract the class from the JSON response."""
try:
return json.loads(response_str).get("class")
except (json.JSONDecodeError, AttributeError):
return response_str
def run_evaluation(
project_id: str = PROJECT_ID,
location: str = LOCATION,
csv_path: str = IMAGE_PATHS,
image_prefix: str = IMAGE_PREFIX,
eval_model: str = EVAL_MODEL,
sample_size: int = SAMPLE_SIZE,
random_state: int = 42,
stratify: bool = False,
classes: list[str] = None,
eval_dest: str = EVAL_DEST
):
client = vertexai.Client(project=project_id, location=location)
eval_df = prepare_eval_df(
csv_path=csv_path,
image_prefix=image_prefix,
sample_size=sample_size,
random_state=random_state,
stratify=stratify,
classes=classes
)
eval_dataset = client.evals.run_inference(
model=eval_model,
src=eval_df,
config={
"generate_content_config": {
"response_mime_type": "application/json",
"temperature": 0
},
"dest": eval_dest if eval_dest else None
}
)
if hasattr(eval_dataset, "eval_dataset_df"):
eval_dataset = eval_dataset.eval_dataset_df
eval_dataset["predicted_class"] = (
eval_dataset["response"].apply(extract_class)
)
# The evaluate function expects 'prompt', 'response', and 'reference'
# columns, even though the comparison is done between 'response' and
# 'reference' only.
eval_input_df = eval_dataset.copy()
eval_input_df["response"] = eval_input_df["predicted_class"]
eval_input_df["prompt"] = "Multimodal classification prompt"
eval_input_df = (
eval_input_df[["img_path", "prompt", "response", "reference"]]
)
eval_result = (
client.evals.evaluate(
dataset=eval_input_df,
metrics=[vertexai.types.Metric(name='exact_match')],
config={"dest": eval_dest} if eval_dest else None
)
)
exact_match_scores = [
case.response_candidate_results[0].metric_results["exact_match"].score
for case in eval_result.eval_case_results
]
# Include the original request and the exact match score back into the
# input DataFrame.
eval_input_df["exact_match"] = exact_match_scores
eval_input_df["request"] = eval_dataset["request"]
# Select and reorder columns for the final results table
results_df = eval_input_df[
["img_path", "response", "reference", "exact_match"]
]
return eval_result, results_df
# if __name__ == "__main__":
# run_evaluation()