Faithful and Consistent Textual Scene Graph Parsing
Official repository for the ACL 2023 Findings paper, with code, datasets, pretrained models, and evaluation tools.
FACTUAL is a benchmark and toolkit for faithful, consistent, and practical textual scene graph parsing. It provides:
- pretrained parsers for converting text into scene graphs,
- benchmark datasets for training and evaluation,
- evaluation tools including SPICE, Soft-SPICE, and Set Match,
- support for both single-sentence and multi-sentence scene graph parsing.
This repository now also includes discourse-level multi-sentence parsing with two options:
| Mode | Best for | Description |
|---|---|---|
default |
Single sentences | Standard one-pass parsing |
sentence_merge |
Multi-sentence descriptions | Split → parse → merge → deduplicate |
DiscoSG-Refiner |
Highest-quality multi-sentence parsing | Sentence merging followed by iterative refinement |
- 40,369 FACTUAL scene graph instances with lemmatized predicates
- 2.9M cleaned Visual Genome scene graph instances for pretraining
- Pretrained Flan-T5 models in multiple sizes
- Advanced discourse-level parsing for long, multi-sentence descriptions
- Unified evaluation toolkit for scene graph parsing and caption evaluation
- Supports CPU, CUDA, and Apple Silicon (MPS)
pip install FactualSceneGraphThe discosg dependency is installed automatically. If installation fails or you want the latest version:
pip install --upgrade FactualSceneGraphFor development or the newest GitHub version:
pip install git+https://github.com/zhuang-li/FACTUAL.git- Python: 3.8+
- Apple Silicon: use
device='mps' - CUDA: install a CUDA-enabled PyTorch build before installing this package
- Core:
torch,transformers,nltk,spacy - Refinement:
discosg,peft,huggingface-hub - Evaluation:
sentence-transformers,pandas,numpy
from factual_scene_graph.parser.scene_graph_parser import SceneGraphParser
parser = SceneGraphParser('lizhuang144/flan-t5-base-VG-factual-sg', device='cpu')
result = parser.parse(
["2 beautiful pigs are flying on the sky with 2 bags on their backs"],
beam_size=1,
return_text=True
)
print(result[0])
# ( pigs , is , 2 ) , ( pigs , is , beautiful ) , ( bags , on back of , pigs ) ,
# ( pigs , fly on , sky ) , ( bags , is , 2 )from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
tokenizer = AutoTokenizer.from_pretrained("lizhuang144/flan-t5-base-VG-factual-sg")
model = AutoModelForSeq2SeqLM.from_pretrained("lizhuang144/flan-t5-base-VG-factual-sg")
text = tokenizer(
"Generate Scene Graph: 2 pigs are flying on the sky with 2 bags on their backs",
max_length=200,
return_tensors="pt",
truncation=True
)
generated_ids = model.generate(
text["input_ids"],
attention_mask=text["attention_mask"],
use_cache=True,
decoder_start_token_id=tokenizer.pad_token_id,
num_beams=1,
max_length=200,
early_stopping=True
)
print(tokenizer.decode(generated_ids[0], skip_special_tokens=True, clean_up_tokenization_spaces=True))
# Output: `( pigs , is , 2 ) , ( bags , on back of , pigs ), ( bags , is , 2 ) , ( pigs , fly on , sky )`In this output format, the predicate
iscorresponds to an attribute-style relation.
Modern VLMs often generate long, rich descriptions instead of single-sentence captions. FACTUAL now supports multi-sentence scene graph parsing in two ways.
Use this for efficient parsing of long descriptions, detailed captions, or short stories.
from factual_scene_graph.parser.scene_graph_parser import SceneGraphParser
parser = SceneGraphParser(
'lizhuang144/flan-t5-base-VG-factual-sg',
parser_type='sentence_merge',
device='cpu'
)
descriptions = [
"""The image captures a serene scene in a park. A gravel path, dappled with sunlight
filtering through the tall trees on either side, winds its way towards a white bridge.""",
"""A bustling urban scene unfolds in the city center. People walk along the sidewalks
carrying shopping bags. Cars and buses navigate through the busy streets.""",
]
results = parser.parse(
descriptions,
beam_size=5,
batch_size=8,
return_text=True
)How it works
- Split text into sentences with NLTK
- Parse each sentence efficiently in batches
- Merge graphs automatically
- Deduplicate repeated entities and relations
Use this when you want the highest-quality multi-sentence scene graphs.
from factual_scene_graph.parser.scene_graph_parser import SceneGraphParser
parser = SceneGraphParser(
'lizhuang144/flan-t5-base-VG-factual-sg',
parser_type='DiscoSG-Refiner',
refiner_checkpoint_path='sqlinn/DiscoSG-Refiner-Large-t5-only',
device='cuda'
)
result = parser.parse(
["""The image captures a bustling urban scene, likely in a European city.
The setting appears to be a pedestrian-friendly square or plaza.
There are numerous people of various ages and attire walking around."""],
task='delete_before_insert',
refinement_rounds=2,
max_input_len=1024,
max_output_len=512,
batch_size=2,
beam_size=5,
return_text=True
)Why use it
- iterative multi-round refinement,
- smart single-sentence vs multi-sentence handling,
- graceful fallback when the input is too long,
- strong performance for research and high-precision applications.
| Parser type | Recommended use | Strength |
|---|---|---|
default |
Simple captions or short text | Fastest single-pass parser |
sentence_merge |
Long descriptions and multi-sentence text | Fast, stable, efficient |
DiscoSG-Refiner |
Research and highest-quality parsing | Best quality through iterative refinement |
from factual_scene_graph.parser.scene_graph_parser import SceneGraphParser
parser = SceneGraphParser(
'lizhuang144/flan-t5-base-VG-factual-sg',
parser_type='DiscoSG-Refiner',
refiner_checkpoint_path='sqlinn/DiscoSG-Refiner-Large-t5-only',
device='mps'
)
mixed_descriptions = [
"A red car is parked.",
"""The dog runs in the park. The park has green grass and tall trees.
Children are playing on the swings.""",
"The cat sleeps on the sofa.",
"""The restaurant is busy tonight. Waiters serve food to customers at tables.
The kitchen staff prepares fresh meals. Soft music plays in the background."""
]
results = parser.parse(
mixed_descriptions,
max_input_len=1024,
max_output_len=512,
beam_size=5,
batch_size=4,
refinement_rounds=2,
task='delete_before_insert',
return_text=True
)max_input_len=512~1024for long descriptionsrefinement_rounds=2~3for a good quality/speed balancedevice='mps'for Apple Silicon,device='cuda'for NVIDIA GPUs- tune
batch_sizebased on memory budget
The parser checks whether max_input_len is sufficient before running refinement. If the input is too long, refinement is skipped automatically and the parser falls back to sentence merging.
parser.parse(["Very long description..."], beam_size=5, max_input_len=64)
# Warning: Skipping DiscoSG refinement because max_input_len is too small.The FACTUAL Scene Graph dataset contains 40,369 instances with lemmatized predicates/relations.
Storage
- Local:
data/factual_sg/factual_sg.csv - Hugging Face:
load_dataset('lizhuang144/FACTUAL_Scene_Graph')
Splits
| Split type | Train | Dev | Test |
|---|---|---|---|
| Random | data/factual_sg/random/train.csv |
data/factual_sg/random/dev.csv |
data/factual_sg/random/test.csv |
| Length | data/factual_sg/length/train.csv |
data/factual_sg/length/dev.csv |
data/factual_sg/length/test.csv |
Fields
image_id: Visual Genome image IDregion_id: Visual Genome region IDcaption: region captionscene_graph: scene graph for the region caption
Related resource
- Visual Genome images/regions:
load_dataset('visual_genome')
- Data:
data/factual_mr/factual_mr.csv - Metadata:
data/factual_mr/meta.json
The metadata maps quantifier abbreviations in factual_mr.csv to their full names.
- Hugging Face:
load_dataset('lizhuang144/VG_scene_graph_clean') - Size: 2.9 million instances
- Preprocessed to remove empty instances
- Hugging Face:
load_dataset('lizhuang144/FACTUAL_Scene_Graph_ID') - Includes:
- verb identifiers,
- passive voice indicators,
- node indexes.
These models are the most directly compatible with common Visual Genome style scene graphs.
| Model | Set Match | SPICE | Soft-SPICE | Weight |
|---|---|---|---|---|
| SPICE / Stanford Parser | 19.30 | 64.77 | 92.60 | modified-SPICE-score |
| (pre) Flan-T5-large | 81.63 | 93.20 | 98.75 | flan-t5-large-VG-factual-sg |
| (pre) Flan-T5-base | 81.37 | 93.27 | 98.83 | flan-t5-base-VG-factual-sg |
| (pre) Flan-T5-small | 78.18 | 92.26 | 98.67 | flan-t5-small-VG-factual-sg |
Notes
(pre)means the model was pretrained on Visual Genome scene graphs and then fine-tuned on FACTUAL.- These models remove node indexes and passive markers such as
pv:. - Verbs and prepositions are merged for compatibility with downstream scene graph tasks.
- The SPICE baseline shown here is re-evaluated against FACTUAL ground truth, which differs from the comparison protocol used in the original paper. See
tests/test_spice_parser.pyfor replication.
These models keep richer structural information, which is useful for advanced downstream tasks.
Examples
A monkey is sitting next to another monkey
→( monkey, v:sit next to, monkey:1 )A car is parked on the ground
→( car, pv:park on, ground )
This format helps:
- distinguish repeated entities,
- represent passive constructions,
- preserve more fine-grained predicate information.
| Model | Set Match | SPICE | Soft-SPICE | Weight |
|---|---|---|---|---|
| (pre) Flan-T5-large | 81.03 | 93.00 | 98.66 | flan-t5-large-VG-factual-sg-id |
| (pre) Flan-T5-base | 81.37 | 93.29 | 98.76 | flan-t5-base-VG-factual-sg-id |
| (pre) Flan-T5-small | 79.64 | 92.40 | 98.53 | flan-t5-small-VG-factual-sg-id |
from factual_scene_graph.parser.scene_graph_parser import SceneGraphParser
from factual_scene_graph.utils import tprint
parser = SceneGraphParser('lizhuang144/flan-t5-base-VG-factual-sg', device='cpu')
graph_obj = parser.parse(
["2 beautiful and strong pigs are flying on the sky with 2 bags on their backs"],
beam_size=5,
return_text=False,
max_output_len=128
)
tprint(graph_obj[0])Example output:
Entities:
+----------+------------+------------------+
| Entity | Quantity | Attributes |
|----------+------------+------------------|
| pigs | 2 | beautiful,strong |
| bags | 2 | |
| sky | | |
+----------+------------+------------------+
Relations:
+-----------+------------+----------+
| Subject | Relation | Object |
|-----------+------------+----------|
| pigs | fly on | sky |
| bags | on back of | pigs |
+-----------+------------+----------+
from factual_scene_graph.parser.scene_graph_parser import SceneGraphParser
parser = SceneGraphParser(
checkpoint_path='lizhuang144/flan-t5-base-VG-factual-sg',
parser_type='DiscoSG-Refiner',
refiner_checkpoint_path='sqlinn/DiscoSG-Refiner-Large-t5-only',
device='cuda',
lemmatize=False,
lowercase=False
)
result = parser.parse(
descriptions=["Your multi-sentence text 1 here...", "Your multi-sentence text 2 here..."],
max_input_len=1024,
max_output_len=512,
beam_size=5,
refinement_rounds=2,
task='delete_before_insert',
batch_size=32,
return_text=True
)parser_types = [
'default',
'sentence_merge',
'DiscoSG-Refiner'
]tasks = [
'delete_before_insert',
'insert_delete',
'insert',
'delete'
]This package includes a complete evaluation toolkit for scene graph parsing and caption quality analysis.
- SPICE
- Soft-SPICE
- Set Match
(list of candidate_captions, list of list reference_captions)(list of candidate_captions, list of list reference_graphs)(list of candidate_graphs, list of list reference_graphs)
- SPICE F-score: measures semantic overlap between candidate and reference scene graphs.
- Exact Set Match: checks whether candidate facts exactly match reference facts, ignoring order.
- Soft-SPICE: uses soft matching for more flexible comparison between graphs.
The Exact Set Match protocol is adapted from Yu et al. (2019), where the original metric was applied to SQL clauses. Here it is adapted for scene graph facts.
import torch
from factual_scene_graph.evaluation.evaluator import Evaluator
from factual_scene_graph.parser.scene_graph_parser import SceneGraphParser
def test_scene_graph_parsing():
device = "cuda" if torch.cuda.is_available() else "cpu"
parser = SceneGraphParser('lizhuang144/flan-t5-base-VG-factual-sg', device=device)
evaluator = Evaluator(parser=parser, device=device)
scores = evaluator.evaluate(
["2 beautiful pigs are flying on the sky with 2 bags on their backs"],
[["( pigs , is , beautiful ) , ( bags , on back of , pigs ) , ( bags , is , 2 ) , ( pigs , is , 2 ) , ( pigs , fly on , sky )"]],
method='spice',
beam_size=1,
max_output_len=128
)
print(scores)import pandas as pd
import torch
from factual_scene_graph.evaluation.evaluator import Evaluator
from factual_scene_graph.parser.scene_graph_parser import SceneGraphParser
def test_scene_graph_parsing_on_random():
device = "cuda" if torch.cuda.is_available() else "cpu"
parser = SceneGraphParser('lizhuang144/flan-t5-base-VG-factual-sg', device=device, lemmatize=False)
evaluator = Evaluator(parser=parser, text_encoder_checkpoint='all-MiniLM-L6-v2', device=device, lemmatize=True)
random_data_pd = pd.read_csv('data/factual_sg/random/test.csv')
random_data_captions = random_data_pd['caption'].tolist()
random_data_graphs = [[scene] for scene in random_data_pd['scene_graph'].tolist()]
spice_scores, cand_graphs, ref_graphs = evaluator.evaluate(
random_data_captions,
random_data_graphs,
method='spice',
beam_size=1,
batch_size=128,
max_input_len=256,
max_output_len=256,
return_graphs=True
)
print('SPICE scores for random test set:', sum(spice_scores) / len(spice_scores))
set_match_scores = evaluator.evaluate(cand_graphs, ref_graphs, method='set_match', beam_size=1)
print('Set Match scores for random test set:', sum(set_match_scores) / len(set_match_scores))
soft_spice_scores = evaluator.evaluate(cand_graphs, ref_graphs, method='soft_spice', beam_size=1)
print('Soft-SPICE scores for random test set:', sum(soft_spice_scores) / len(soft_spice_scores))We evaluated how well different metrics correlate with human judgment using Kendall's tau-c.
| Model | Tau-c |
|---|---|
| SPICE (Official-Original) | 44.77 |
| SPICE (Official-Factual) | 45.13 |
| SPICE (Ours-Factual) | 45.25 |
| Soft-SPICE | 54.20 |
| RefCLIPScore | 53.00 |
| BERTScore | 36.71 |
-
SPICE (Official-Original)
Uses the original parser from the Modified SPICE Score repository. -
SPICE (Official-Factual)
Uses the official SPICE implementation, but replaces the parser withlizhuang144/flan-t5-base-VG-factual-sg. -
SPICE (Ours-Factual)
Our implementation with an improved synonym-matching dictionary. This is the default configuration and is recommended for better alignment with human judgment. -
Soft-SPICE
A soft-matching extension using sentence embeddings. The default encoder isall-MiniLM-L6-v2.
For replication, see: tests/test_metric_human_correlation.py
If this repository helps your research, please cite:
@inproceedings{li-etal-2023-factual,
title = "{FACTUAL}: A Benchmark for Faithful and Consistent Textual Scene Graph Parsing",
author = "Li, Zhuang and
Chai, Yuyang and
Zhuo, Terry Yue and
Qu, Lizhen and
Haffari, Gholamreza and
Li, Fei and
Ji, Donghong and
Tran, Quan Hung",
booktitle = "Findings of the Association for Computational Linguistics: ACL 2023",
month = jul,
year = "2023",
address = "Toronto, Canada",
publisher = "Association for Computational Linguistics",
url = "https://aclanthology.org/2023.findings-acl.398",
pages = "6377--6390",
}
@inproceedings{lin-etal-2025-discosg,
title = "{D}isco{SG}: Towards Discourse-Level Text Scene Graph Parsing through Iterative Graph Refinement",
author = "Lin, Shaoqing and
Teng, Chong and
Li, Fei and
Ji, Donghong and
Qu, Lizhen and
Li, Zhuang",
editor = "Christodoulopoulos, Christos and
Chakraborty, Tanmoy and
Rose, Carolyn and
Peng, Violet",
booktitle = "Proceedings of the 2025 Conference on Empirical Methods in Natural Language Processing",
month = nov,
year = "2025",
address = "Suzhou, China",
publisher = "Association for Computational Linguistics",
url = "https://aclanthology.org/2025.emnlp-main.398/",
doi = "10.18653/v1/2025.emnlp-main.398",
pages = "7848--7873",
ISBN = "979-8-89176-332-6",
}This project builds on ideas and code from SceneGraphParser by Jiayuan Mao. We gratefully acknowledge their pioneering contribution to open-source scene graph parsing.