Spaces:
Running
Running
File size: 9,800 Bytes
bb8f662 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 | """
Knowledge Graph Service for Neuro-Symbolic VQA
Uses ConceptNet API to provide common-sense reasoning capabilities
"""
import requests
import re
from typing import Dict, List, Optional
from functools import lru_cache
import time
class KnowledgeGraphService:
"""
Lightweight ConceptNet API wrapper for common-sense reasoning.
Enhances VQA answers with external knowledge about object properties,
capabilities, uses, and relationships.
"""
CONCEPTNET_API = "https://api.conceptnet.io"
# Common-sense question patterns
COMMONSENSE_PATTERNS = [
# Capability questions
(r'can .* (melt|freeze|fly|swim|float|sink|break|burn|explode)', 'CapableOf'),
(r'is .* able to', 'CapableOf'),
(r'does .* (float|sink)', 'CapableOf'),
# Property questions
(r'is .* (edible|poisonous|dangerous|safe|hot|cold|sweet|sour)', 'HasProperty'),
(r'is this (food|drink|toy|tool|weapon)', 'HasProperty'),
# Purpose questions
(r'what .* (used for|for)', 'UsedFor'),
(r'why .* (used|made)', 'UsedFor'),
(r'how .* use', 'UsedFor'),
# Composition questions
(r'what .* made (of|from)', 'MadeOf'),
(r'what .* (material|ingredient)', 'MadeOf'),
# Location questions
(r'where .* (found|located|kept|stored)', 'AtLocation'),
(r'where (do|does) .* (live|grow)', 'AtLocation'),
]
def __init__(self, cache_size=100, timeout=5):
"""
Initialize Knowledge Graph service.
Args:
cache_size: Number of API responses to cache
timeout: API request timeout in seconds
"""
self.timeout = timeout
self.cache_size = cache_size
print("✅ Knowledge Graph service initialized (ConceptNet API)")
@lru_cache(maxsize=100)
def _query_conceptnet(self, concept: str, relation: str, limit: int = 10) -> Optional[Dict]:
"""
Query ConceptNet API with caching.
Args:
concept: Concept to query (e.g., "ice_cream")
relation: Relation type (e.g., "CapableOf", "HasProperty")
limit: Maximum number of results
Returns:
API response dict or None if failed
"""
try:
# Normalize concept (replace spaces with underscores)
concept = concept.lower().replace(' ', '_')
# Build API URL
url = f"{self.CONCEPTNET_API}/query"
params = {
'start': f'/c/en/{concept}',
'rel': f'/r/{relation}',
'limit': limit
}
# Make request
response = requests.get(url, params=params, timeout=self.timeout)
response.raise_for_status()
return response.json()
except requests.exceptions.Timeout:
print(f"⚠️ ConceptNet API timeout for {concept}")
return None
except requests.exceptions.RequestException as e:
print(f"⚠️ ConceptNet API error: {e}")
return None
except Exception as e:
print(f"⚠️ Unexpected error querying ConceptNet: {e}")
return None
def get_concept_properties(self, concept: str) -> Dict[str, List[str]]:
properties = {
'CapableOf': [],
'HasProperty': [],
'UsedFor': [],
'MadeOf': [],
'AtLocation': []
}
# Query each relation type
for relation in properties.keys():
data = self._query_conceptnet(concept, relation)
if data and 'edges' in data:
for edge in data['edges']:
# Extract the end concept
if 'end' in edge and 'label' in edge['end']:
end_label = edge['end']['label']
properties[relation].append(end_label)
return properties
def is_commonsense_question(self, question: str) -> bool:
"""
Detect if a question requires common-sense reasoning.
Args:
question: Question string
Returns:
True if question needs external knowledge
"""
q_lower = question.lower()
for pattern, _ in self.COMMONSENSE_PATTERNS:
if re.search(pattern, q_lower):
return True
return False
def _detect_question_type(self, question: str) -> Optional[str]:
"""
Detect which ConceptNet relation the question is asking about.
Args:
question: Question string
Returns:
Relation type or None
"""
q_lower = question.lower()
for pattern, relation in self.COMMONSENSE_PATTERNS:
if re.search(pattern, q_lower):
return relation
return None
def answer_commonsense_question(self, object_name: str, question: str) -> Optional[str]:
"""
Answer a common-sense question using Knowledge Graph.
Args:
object_name: Object detected by VQA (e.g., "ice cream")
question: User's question
Returns:
Enhanced answer string or None
"""
# Detect question type
relation = self._detect_question_type(question)
if not relation:
return None
# Query ConceptNet
data = self._query_conceptnet(object_name, relation, limit=5)
if not data or 'edges' not in data:
return None
# Extract relevant knowledge
knowledge = []
for edge in data['edges']:
if 'end' in edge and 'label' in edge['end']:
knowledge.append(edge['end']['label'])
if not knowledge:
return None
# Generate natural language answer based on question type
return self._synthesize_answer(object_name, question, relation, knowledge)
def _synthesize_answer(self, object_name: str, question: str,
relation: str, knowledge: List[str]) -> str:
"""
Synthesize natural language answer from knowledge.
Args:
object_name: Detected object
question: Original question
relation: Relation type
knowledge: List of related concepts from KG
Returns:
Natural language answer
"""
q_lower = question.lower()
# Capability questions (can X do Y?)
if relation == 'CapableOf':
# Check if specific capability is mentioned
for capability in knowledge:
if capability in q_lower:
return f"Yes, {object_name} can {capability}."
# General capability answer
if knowledge:
caps = ', '.join(knowledge[:3])
return f"{object_name.capitalize()} can {caps}."
# Property questions (is X Y?)
elif relation == 'HasProperty':
# Check for specific property
if 'edible' in q_lower:
if 'edible' in knowledge:
return f"Yes, {object_name} is edible."
else:
return f"No, {object_name} is not edible."
if 'dangerous' in q_lower or 'safe' in q_lower:
if any(prop in knowledge for prop in ['dangerous', 'harmful', 'poisonous']):
return f"Caution: {object_name} may be dangerous."
else:
return f"{object_name.capitalize()} is generally safe."
# General properties
if knowledge:
props = ', '.join(knowledge[:3])
return f"{object_name.capitalize()} is {props}."
# Purpose questions (what is X used for?)
elif relation == 'UsedFor':
if knowledge:
uses = ', '.join(knowledge[:3])
return f"{object_name.capitalize()} is used for {uses}."
# Composition questions (what is X made of?)
elif relation == 'MadeOf':
if knowledge:
materials = ', '.join(knowledge[:3])
return f"{object_name.capitalize()} is made of {materials}."
# Location questions (where is X found?)
elif relation == 'AtLocation':
if knowledge:
locations = ', '.join(knowledge[:2])
return f"{object_name.capitalize()} is typically found at {locations}."
return None
# Test function
if __name__ == "__main__":
print("=" * 80)
print("🧪 Testing Knowledge Graph Service")
print("=" * 80)
kg = KnowledgeGraphService()
# Test cases
test_cases = [
("ice cream", "Can this melt?"),
("apple", "Is this edible?"),
("hammer", "What is this used for?"),
("knife", "Is this dangerous?"),
("bread", "What is this made of?"),
]
for obj, question in test_cases:
print(f"\n📝 Object: {obj}")
print(f"❓ Question: {question}")
# Check if common-sense question
is_cs = kg.is_commonsense_question(question)
print(f"🔍 Common-sense: {is_cs}")
if is_cs:
# Get answer
answer = kg.answer_commonsense_question(obj, question)
print(f"💬 Answer: {answer}")
print("-" * 80)
|