File size: 5,996 Bytes
6aabd96 5d1c546 6aabd96 5d1c546 6aabd96 cc4ff55 6aabd96 cc4ff55 6aabd96 cc4ff55 6aabd96 cc4ff55 6aabd96 cc4ff55 6aabd96 cc4ff55 6aabd96 cc4ff55 6aabd96 cc4ff55 4892b72 6aabd96 cc4ff55 6aabd96 cc4ff55 4892b72 aec710a cc4ff55 6aabd96 aec710a 6aabd96 | 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 | """
fact_extractor.py
Module: Explicit Fact Extraction for SIGMA Intelligence Analyzer
Purpose:
Extract structured, syntactic facts from narrative text.
Uses spaCy transformer pipeline for:
- Named Entity Recognition (NER)
- Dependency parsing
- Sentence segmentation
Output:
- Entities (with labels)
- Structured factual triples:
(subject → action → object)
Design Philosophy:
Conservative extraction.
No speculative inference.
Only syntactically grounded relations.
"""
import spacy
from spacy.cli import download
class FactExtractor:
def __init__(self):
"""
Load spaCy transformer-based English model.
This model includes:
- NER
- POS tagging
- Dependency parsing
"""
#self.nlp = spacy.load("en_core_web_trf") # transformer-based & very heavy
#self.nlp = spacy.load("en_core_web_sm") # lightweight version
try:
self.nlp = spacy.load("en_core_web_sm")
except:
download("en_core_web_sm")
self.nlp = spacy.load("en_core_web_sm")
def extract_entities(self, text):
"""
Extract named entities from text.
Returns:
List[Dict] with:
{
"text": entity string,
"label": entity type (ORG, PERSON, GPE, DATE, etc.)
}
"""
doc = self.nlp(text)
return [
{"text": ent.text, "label": ent.label_}
for ent in doc.ents
]
def extract_structured_facts(self, text):
"""
Extract structured factual triples from text.
Strategy:
- Iterate through sentences
- Identify ROOT verb of each sentence
- Extract:
- Subject (nsubj / nsubjpass)
- Object/complement structures
- Expand subject and object to full phrase spans
Returns: List[Dict]
"""
doc = self.nlp(text)
facts = []
for sent in doc.sents:
for token in sent:
# Identify main predicate (ROOT verb)
# note: some sentences whose ROOT is AUX or NOUN may still not extract perfectly
if token.pos_ == "VERB" and token.dep_ == "ROOT":
subject = None
obj = None
# -----------------------------
# SUBJECT EXTRACTION
# -----------------------------
for child in token.children:
if child.dep_ in ("nsubj", "nsubjpass"):
subject = self._expand_phrase(child)
# -----------------------------
# OBJECT / COMPLEMENT EXTRACTION
# -----------------------------
for child in token.children:
# Direct object / attribute / object predicate
if child.dep_ in ("dobj", "attr", "oprd"):
obj = self._expand_phrase(child)
# Clausal complements
elif child.dep_ in ("ccomp", "xcomp"):
obj = self._expand_phrase(child)
# Remove leading complementizer
if obj.startswith("that "):
obj = obj[5:]
# -----------------------------------
# Attach ROOT-level prep phrases
# -----------------------------------
if obj:
prep_phrases = []
for child in token.children:
if child.dep_ == "prep":
prep_text = self._expand_phrase(child)
# Avoid duplicate attachment
if prep_text not in obj:
prep_phrases.append(prep_text)
if prep_phrases:
obj += " " + " ".join(prep_phrases)
# Detect negation
negated = any(
child.dep_ == "neg"
for child in token.children
)
action = token.lemma_
if negated:
action = f"not {action}"
# -----------------------------
# STORE FACT
# -----------------------------
facts.append({
"sentence": sent.text.strip(),
"subject": subject,
"action": action,
"object": obj
})
return facts
def _expand_phrase(self, token):
"""
Expand token to its full subtree span and normalize whitespace.
Why:
Dependency parsing identifies only head tokens.
We want full noun phrases including modifiers.
Example:
Head token: "division"
Expanded: "The cybersecurity division"
Implementation:
- Collect all tokens in subtree
- Get span from first to last token
"""
subtree = list(token.subtree)
start = subtree[0].i
end = subtree[-1].i + 1
span_text = token.doc[start:end].text
# Normalize whitespace (remove leading/trailing spaces and newlines)
return " ".join(span_text.strip().split())
def extract_sentences(self, text):
"""
Return list of individual sentences.
Useful for:
- Candidate hypothesis generation
- Summarization preprocessing
"""
doc = self.nlp(text)
return [sent.text.strip() for sent in doc.sents] |