384 lines
13 KiB
Python
Executable File
384 lines
13 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Convert ConceptNet CSV to HuggingFace format.
|
|
|
|
This script handles the ConceptNet 5.7 dataset which is in CSV format with
|
|
tab-separated values and specific columns for assertions.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import csv
|
|
import gzip
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
from datasets import Dataset, DatasetDict
|
|
from rich.console import Console
|
|
from rich.progress import (
|
|
BarColumn,
|
|
Progress,
|
|
SpinnerColumn,
|
|
TextColumn,
|
|
TimeElapsedColumn,
|
|
)
|
|
|
|
|
|
def convert_conceptnet_uri_to_full(uri: str) -> str:
|
|
"""Convert ConceptNet URI format to full URI.
|
|
|
|
Args:
|
|
uri: ConceptNet URI like /c/en/word or /r/RelatedTo
|
|
|
|
Returns:
|
|
Full URI like http://conceptnet.io/c/en/word
|
|
"""
|
|
if uri.startswith("/"):
|
|
return f"http://conceptnet.io{uri}"
|
|
return uri
|
|
|
|
|
|
def parse_conceptnet_csv(
|
|
file_path: Path, max_rows: int | None = None
|
|
) -> list[dict[str, str | None]]:
|
|
"""Parse ConceptNet CSV file.
|
|
|
|
ConceptNet CSV format (tab-separated):
|
|
- assertion URI
|
|
- relation
|
|
- start node (subject)
|
|
- end node (object)
|
|
- edge info JSON (contains weight, sources, etc.)
|
|
|
|
Args:
|
|
file_path: Path to CSV file
|
|
max_rows: Maximum number of rows to parse (for testing)
|
|
|
|
Returns:
|
|
List of triple dictionaries
|
|
"""
|
|
triples = []
|
|
|
|
try:
|
|
with open(file_path, encoding="utf-8") as f:
|
|
reader = csv.reader(f, delimiter="\t")
|
|
|
|
for i, row in enumerate(reader):
|
|
if max_rows and i >= max_rows:
|
|
break
|
|
|
|
# Skip malformed rows
|
|
if len(row) < 5:
|
|
continue
|
|
|
|
# Extract fields
|
|
relation = row[1] # e.g., /r/Antonym
|
|
start_node = row[2] # e.g., /c/en/able
|
|
end_node = row[3] # e.g., /c/en/unable
|
|
|
|
# Convert to RDF-style triple
|
|
triple = {
|
|
"subject": convert_conceptnet_uri_to_full(start_node),
|
|
"predicate": convert_conceptnet_uri_to_full(relation),
|
|
"object": convert_conceptnet_uri_to_full(end_node),
|
|
"object_type": "uri", # ConceptNet edges link concepts (URIs)
|
|
"object_datatype": None,
|
|
"object_language": None,
|
|
# Store weight as metadata - could be used for filtering
|
|
# Note: We're storing this as string to maintain compatibility
|
|
# with the standard RDF format, but it could be parsed as float
|
|
# when needed for ML tasks
|
|
}
|
|
|
|
# For high-confidence assertions only (optional filter)
|
|
# Typical ConceptNet weights range from 0.5 to 20+
|
|
# if weight >= 1.0: # Uncomment to filter low-confidence edges
|
|
triples.append(triple)
|
|
except FileNotFoundError:
|
|
console = Console()
|
|
console.print(f"[red]File not found: {file_path}[/red]")
|
|
console.print(
|
|
"[yellow]Make sure the ConceptNet dataset has been downloaded "
|
|
"correctly[/yellow]"
|
|
)
|
|
raise
|
|
except OSError as e:
|
|
console = Console()
|
|
console.print(f"[red]Error reading file {file_path}: {e}[/red]")
|
|
raise
|
|
|
|
return triples
|
|
|
|
|
|
# pylint: disable=too-many-locals,too-many-statements
|
|
def convert_conceptnet(input_file: Path, output_dir: Path) -> None:
|
|
"""Convert ConceptNet CSV to HuggingFace format.
|
|
|
|
Args:
|
|
input_file: Path to ConceptNet CSV file
|
|
output_dir: Output directory for HuggingFace dataset
|
|
"""
|
|
console = Console()
|
|
|
|
console.print(
|
|
"\n[bold cyan]Converting ConceptNet to HuggingFace Dataset[/bold cyan]"
|
|
)
|
|
console.print(f"Input: {input_file}")
|
|
console.print(f"Output: {output_dir}\n")
|
|
|
|
# Emit initial progress
|
|
print("\nPROGRESS: 5", flush=True)
|
|
sys.stdout.flush()
|
|
|
|
if not input_file.exists():
|
|
console.print(f"[red]Error: Input file not found: {input_file}[/red]")
|
|
console.print("[yellow]Expected ConceptNet CSV file (tab-separated)[/yellow]")
|
|
return
|
|
|
|
# Check if file is readable and not empty
|
|
try:
|
|
file_size = input_file.stat().st_size
|
|
if file_size == 0:
|
|
console.print(f"[red]Error: Input file is empty: {input_file}[/red]")
|
|
return
|
|
except OSError as e:
|
|
console.print(f"[red]Error accessing input file: {e}[/red]")
|
|
return
|
|
|
|
all_triples = []
|
|
|
|
with Progress(
|
|
SpinnerColumn(),
|
|
TextColumn("[progress.description]{task.description}"),
|
|
BarColumn(),
|
|
TimeElapsedColumn(),
|
|
console=console,
|
|
transient=False,
|
|
) as progress:
|
|
# Count lines first for accurate progress
|
|
task = progress.add_task("[yellow]Counting assertions...[/yellow]", total=None)
|
|
try:
|
|
with open(input_file, encoding="utf-8") as f:
|
|
total_lines = sum(1 for _ in f)
|
|
progress.update(
|
|
task, description=f"[green]✓ Found {total_lines:,} assertions[/green]"
|
|
)
|
|
except OSError as e:
|
|
console.print(f"[red]Error counting lines: {e}[/red]")
|
|
return
|
|
|
|
# Emit progress after counting (10%)
|
|
print("\nPROGRESS: 10", flush=True)
|
|
sys.stdout.flush()
|
|
|
|
# Parse CSV file
|
|
task = progress.add_task(
|
|
"[yellow]Parsing ConceptNet assertions...[/yellow]", total=total_lines
|
|
)
|
|
|
|
# Track progress emission
|
|
last_progress_pct = 10.0
|
|
|
|
with open(input_file, encoding="utf-8") as f:
|
|
reader = csv.reader(f, delimiter="\t")
|
|
|
|
for i, row in enumerate(reader):
|
|
# Skip malformed rows
|
|
if len(row) < 5:
|
|
progress.advance(task)
|
|
continue
|
|
|
|
# Extract fields
|
|
try:
|
|
relation = row[1]
|
|
start_node = row[2]
|
|
end_node = row[3]
|
|
|
|
# Create triple
|
|
triple = {
|
|
"subject": convert_conceptnet_uri_to_full(start_node),
|
|
"predicate": convert_conceptnet_uri_to_full(relation),
|
|
"object": convert_conceptnet_uri_to_full(end_node),
|
|
"object_type": "uri",
|
|
"object_datatype": None,
|
|
"object_language": None,
|
|
}
|
|
|
|
all_triples.append(triple)
|
|
|
|
except (IndexError, ValueError):
|
|
# Skip problematic rows
|
|
pass
|
|
|
|
# Update progress and emit PROGRESS markers
|
|
if i % 10000 == 0:
|
|
progress.update(task, completed=i)
|
|
|
|
# Calculate overall progress (10-80% for parsing phase)
|
|
parse_pct = (i / total_lines) * 100 if total_lines > 0 else 0
|
|
overall_progress = 10 + (parse_pct / 100) * 70
|
|
|
|
# Emit progress every 5% or so
|
|
if overall_progress >= last_progress_pct + 5:
|
|
print(f"\nPROGRESS: {overall_progress:.0f}", flush=True)
|
|
sys.stdout.flush()
|
|
last_progress_pct = overall_progress
|
|
|
|
progress.update(task, completed=total_lines)
|
|
|
|
# Final parsing progress
|
|
print("\nPROGRESS: 80", flush=True)
|
|
sys.stdout.flush()
|
|
|
|
if not all_triples:
|
|
console.print(
|
|
"[yellow]Warning: No valid triples found in input file[/yellow]"
|
|
)
|
|
console.print("[yellow]Check file format and content[/yellow]")
|
|
progress.update(
|
|
task,
|
|
description=f"[green]✓ Parsed {len(all_triples):,} valid triples[/green]",
|
|
)
|
|
|
|
# Create dataset dictionary with single "data" split
|
|
console.print("[yellow]Creating HuggingFace dataset...[/yellow]")
|
|
print("\nPROGRESS: 85", flush=True)
|
|
sys.stdout.flush()
|
|
|
|
try:
|
|
dataset_dict = DatasetDict({"data": Dataset.from_list(all_triples)})
|
|
except Exception as e:
|
|
console.print(f"[red]Error creating dataset: {e}[/red]")
|
|
console.print(f"[yellow]Total triples processed: {len(all_triples)}[/yellow]")
|
|
raise
|
|
|
|
print("\nPROGRESS: 90", flush=True)
|
|
sys.stdout.flush()
|
|
|
|
# Add metadata
|
|
# pylint: disable=protected-access
|
|
# Add metadata
|
|
metadata_dict = {
|
|
"description": (
|
|
"ConceptNet 5.7 - A multilingual knowledge graph of "
|
|
"common sense knowledge"
|
|
),
|
|
"citation": """@inproceedings{speer2017conceptnet,
|
|
title={ConceptNet 5.5: An Open Multilingual Graph of General Knowledge},
|
|
author={Speer, Robyn and Chin, Joshua and Havasi, Catherine},
|
|
booktitle={Proceedings of the Thirty-First AAAI Conference on """
|
|
"""Artificial Intelligence},
|
|
pages={4444--4451},
|
|
year={2017}
|
|
}""",
|
|
"homepage": "http://conceptnet.io",
|
|
"license": "CC BY-SA 4.0",
|
|
}
|
|
|
|
for split in dataset_dict:
|
|
dataset_dict[split].info.description = metadata_dict.get("description", "")
|
|
dataset_dict[split].info.citation = metadata_dict.get("citation", "")
|
|
dataset_dict[split].info.homepage = metadata_dict.get("homepage", "")
|
|
dataset_dict[split].info.license = metadata_dict.get("license", "")
|
|
|
|
# Save dataset
|
|
console.print("[yellow]Saving dataset...[/yellow]")
|
|
print("\nPROGRESS: 95", flush=True)
|
|
sys.stdout.flush()
|
|
|
|
try:
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
except OSError as e:
|
|
console.print(f"[red]Error creating output directory: {e}[/red]")
|
|
console.print(f"[yellow]Output path: {output_dir}[/yellow]")
|
|
console.print("[yellow]Check permissions and disk space[/yellow]")
|
|
raise
|
|
|
|
try:
|
|
dataset_dict.save_to_disk(str(output_dir))
|
|
except Exception as e:
|
|
console.print(f"[red]Error saving dataset: {e}[/red]")
|
|
console.print(f"[yellow]Output directory: {output_dir}[/yellow]")
|
|
console.print("[yellow]Check disk space and permissions[/yellow]")
|
|
raise
|
|
|
|
print("\nPROGRESS: 100", flush=True)
|
|
sys.stdout.flush()
|
|
|
|
console.print(f"\n[bold green]✓ Dataset saved to {output_dir}[/bold green]")
|
|
|
|
# Print statistics
|
|
console.print("\n[bold]Dataset Statistics:[/bold]")
|
|
console.print(f" • Total triples: {len(all_triples):,}")
|
|
|
|
# Sample some relations to show variety
|
|
relations: dict[str, int] = {}
|
|
for triple in all_triples[:10000]: # Sample first 10k for stats
|
|
rel = triple["predicate"]
|
|
assert rel is not None
|
|
relations[rel] = relations.get(rel, 0) + 1
|
|
|
|
console.print(f" • Sample relation types: {len(relations)}")
|
|
console.print("\n[bold]Top 5 relations in sample:[/bold]")
|
|
for rel, count in sorted(relations.items(), key=lambda x: x[1], reverse=True)[:5]:
|
|
rel_name = rel.split("/")[-1] if "/" in rel else rel
|
|
console.print(f" • {rel_name}: {count:,}")
|
|
|
|
|
|
def main() -> None:
|
|
"""Main entry point."""
|
|
parser = argparse.ArgumentParser(
|
|
description="Convert ConceptNet CSV to HuggingFace format"
|
|
)
|
|
parser.add_argument("input", type=Path, help="Input ConceptNet CSV file")
|
|
parser.add_argument(
|
|
"output", type=Path, help="Output directory for HuggingFace dataset"
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Handle both compressed and uncompressed files
|
|
if args.input.suffix == ".gz":
|
|
console = Console()
|
|
console.print("[yellow]Decompressing .gz file...[/yellow]")
|
|
|
|
try:
|
|
# Create temp file for decompressed content
|
|
with tempfile.NamedTemporaryFile(
|
|
mode="wb", suffix=".csv", delete=False
|
|
) as tmp:
|
|
with gzip.open(args.input, "rb") as gz:
|
|
decompressed_data = gz.read()
|
|
if isinstance(decompressed_data, str):
|
|
decompressed_data = decompressed_data.encode("utf-8")
|
|
tmp.write(decompressed_data)
|
|
temp_file = Path(tmp.name)
|
|
except gzip.BadGzipFile as e:
|
|
console.print(f"[red]Invalid or corrupted gzip file: {e}[/red]")
|
|
console.print(f"[yellow]File: {args.input}[/yellow]")
|
|
console.print("[yellow]Please re-download the dataset[/yellow]")
|
|
return
|
|
except OSError as e:
|
|
console.print(f"[red]Error decompressing file: {e}[/red]")
|
|
console.print(f"[yellow]File: {args.input}[/yellow]")
|
|
return
|
|
|
|
try:
|
|
convert_conceptnet(temp_file, args.output)
|
|
finally:
|
|
# Clean up temp file
|
|
try:
|
|
temp_file.unlink()
|
|
except OSError:
|
|
console.print(
|
|
f"[yellow]Warning: Could not remove temporary file: "
|
|
f"{temp_file}[/yellow]"
|
|
)
|
|
else:
|
|
convert_conceptnet(args.input, args.output)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|