Skip to content

Latest commit

 

History

History
1056 lines (825 loc) · 32.9 KB

File metadata and controls

1056 lines (825 loc) · 32.9 KB

NLP & TEXT MINING

Complete NLP pipeline for aviation accident narrative analysis

Production-ready implementation of Natural Language Processing techniques for NTSB aviation accident reports, including SafeAeroBERT fine-tuning, topic modeling, information extraction, and automated report generation.

Table of Contents

Text Preprocessing Pipeline

Complete preprocessing pipeline for aviation accident narratives:

Installation

# Install NLP libraries
pip install \
    spacy==3.7.2 \
    nltk==3.8.1 \
    transformers==4.36.2 \
    torch==2.1.2 \
    datasets==2.16.1 \
    sentence-transformers==2.2.2 \
    gensim==4.3.2 \
    wordcloud==1.9.3 \
    textstat==0.7.3

# Download spaCy models
python -m spacy download en_core_web_sm
python -m spacy download en_core_web_trf  # Transformer-based (more accurate)

# Download NLTK data
python -c "import nltk; nltk.download('punkt'); nltk.download('stopwords'); nltk.download('wordnet')"

Text Cleaning & Normalization

# scripts/nlp/preprocessing.py
"""
Text preprocessing pipeline for aviation accident narratives.
"""

import re
import string
from typing import List, Dict
import spacy
from nltk.corpus import stopwords
from nltk.tokenize import word_tokenize, sent_tokenize
from nltk.stem import WordNetLemmatizer
import pandas as pd

class AviationTextPreprocessor:
    """Complete preprocessing pipeline for aviation narratives."""

    def __init__(self, use_transformer: bool = False):
        # Load spaCy model
        model_name = "en_core_web_trf" if use_transformer else "en_core_web_sm"
        self.nlp = spacy.load(model_name)

        # Initialize NLTK components
        self.stop_words = set(stopwords.words('english'))
        self.lemmatizer = WordNetLemmatizer()

        # Aviation-specific stop words
        self.aviation_stopwords = {
            'pilot', 'aircraft', 'flight', 'airplane', 'plane',  # Too generic
            'reported', 'stated', 'noted', 'said'  # Reporting verbs
        }

        # Aviation abbreviation mappings
        self.abbreviations = {
            'acft': 'aircraft',
            'alt': 'altitude',
            'appr': 'approach',
            'apt': 'airport',
            'atc': 'air traffic control',
            'cfi': 'certified flight instructor',
            'faa': 'federal aviation administration',
            'ifr': 'instrument flight rules',
            'ils': 'instrument landing system',
            'kts': 'knots',
            'msl': 'mean sea level',
            'ntsb': 'national transportation safety board',
            'pic': 'pilot in command',
            'rpm': 'revolutions per minute',
            'rwy': 'runway',
            'tas': 'true airspeed',
            'vfr': 'visual flight rules',
            'vor': 'vhf omnidirectional range'
        }

        print(f"Initialized preprocessor with {model_name}")

    def expand_abbreviations(self, text: str) -> str:
        """Expand aviation abbreviations."""
        text_lower = text.lower()

        for abbrev, expansion in self.abbreviations.items():
            # Match whole words only
            pattern = r'\b' + re.escape(abbrev) + r'\b'
            text_lower = re.sub(pattern, expansion, text_lower)

        return text_lower

    def clean_text(self, text: str) -> str:
        """Clean and normalize text."""
        if not isinstance(text, str):
            return ""

        # Convert to lowercase
        text = text.lower()

        # Expand abbreviations
        text = self.expand_abbreviations(text)

        # Remove special characters but keep spaces
        text = re.sub(r'[^\w\s]', ' ', text)

        # Remove extra whitespace
        text = re.sub(r'\s+', ' ', text).strip()

        # Remove URLs
        text = re.sub(r'http\S+|www\S+', '', text)

        # Remove numbers (optional - keep if analyzing flight levels, etc.)
        # text = re.sub(r'\d+', '', text)

        return text

    def remove_stopwords(self, tokens: List[str]) -> List[str]:
        """Remove stop words."""
        all_stopwords = self.stop_words.union(self.aviation_stopwords)
        return [token for token in tokens if token not in all_stopwords]

    def lemmatize(self, tokens: List[str]) -> List[str]:
        """Lemmatize tokens."""
        return [self.lemmatizer.lemmatize(token) for token in tokens]

    def tokenize_spacy(self, text: str) -> List[str]:
        """Tokenize using spaCy (better for aviation text)."""
        doc = self.nlp(text)
        return [token.lemma_ for token in doc if not token.is_stop and not token.is_punct]

    def extract_sentences(self, text: str) -> List[str]:
        """Extract sentences from text."""
        doc = self.nlp(text)
        return [sent.text for sent in doc.sents]

    def preprocess(self, text: str, method: str = "spacy") -> List[str]:
        """
        Complete preprocessing pipeline.

        Args:
            text: Input text
            method: "spacy" (recommended) or "nltk"

        Returns:
            List of processed tokens
        """
        # Clean text
        cleaned = self.clean_text(text)

        if method == "spacy":
            # SpaCy pipeline (recommended)
            tokens = self.tokenize_spacy(cleaned)
        else:
            # NLTK pipeline
            tokens = word_tokenize(cleaned)
            tokens = self.remove_stopwords(tokens)
            tokens = self.lemmatize(tokens)

        # Filter short tokens
        tokens = [token for token in tokens if len(token) > 2]

        return tokens

    def preprocess_dataframe(self, df: pd.DataFrame, text_column: str = 'narr_accp') -> pd.DataFrame:
        """Preprocess all narratives in DataFrame."""
        print(f"Preprocessing {len(df)} narratives...")

        # Add preprocessed column
        df['preprocessed_tokens'] = df[text_column].apply(
            lambda x: self.preprocess(x) if pd.notna(x) else []
        )

        df['preprocessed_text'] = df['preprocessed_tokens'].apply(lambda x: ' '.join(x))

        # Add sentence count
        df['sentence_count'] = df[text_column].apply(
            lambda x: len(self.extract_sentences(x)) if pd.notna(x) else 0
        )

        # Add word count
        df['word_count'] = df['preprocessed_tokens'].apply(len)

        print(f"Preprocessing complete. Average tokens: {df['word_count'].mean():.1f}")

        return df


# Example usage
if __name__ == '__main__':
    from sqlalchemy import create_engine

    # Load narratives
    engine = create_engine("postgresql://app:dev_password@localhost:5432/ntsb")
    df = pd.read_sql("SELECT ev_id, narr_accp FROM narratives LIMIT 1000", engine)

    # Preprocess
    preprocessor = AviationTextPreprocessor(use_transformer=False)
    df = preprocessor.preprocess_dataframe(df, text_column='narr_accp')

    print("\nSample preprocessed narrative:")
    print(f"Original: {df['narr_accp'].iloc[0][:200]}")
    print(f"Preprocessed: {df['preprocessed_text'].iloc[0][:200]}")
    print(f"Tokens: {df['word_count'].iloc[0]}")

    # Save
    df.to_parquet('data/preprocessed_narratives.parquet')

Expected output:

  • Preprocessing speed: 500-1000 narratives/second (CPU)
  • Average tokens per narrative: 100-200
  • Processing time for 100K narratives: 2-3 minutes

Named Entity Recognition (NER)

Extract aviation-specific entities from narratives:

Custom Aviation NER Model

# scripts/nlp/aviation_ner.py
"""
Custom NER model for aviation entities.
"""

import spacy
from spacy.training import Example
from spacy.util import minibatch, compounding
import random
from typing import List, Tuple, Dict

class AviationNER:
    """Train and use custom NER for aviation accidents."""

    def __init__(self, model_name: str = "en_core_web_sm"):
        self.nlp = spacy.load(model_name)

        # Add custom entity types
        self.entity_types = [
            "AIRCRAFT_MODEL",     # "Cessna 172", "Boeing 737"
            "AIRCRAFT_PART",      # "engine", "propeller", "landing gear"
            "LOCATION_TYPE",      # "runway", "taxiway", "apron"
            "WEATHER_CONDITION",  # "IMC", "thunderstorm", "fog"
            "FAILURE_MODE",       # "engine failure", "fuel exhaustion"
            "FLIGHT_PHASE",       # "takeoff", "landing", "cruise"
            "PILOT_ACTION",       # "applied brakes", "executed go-around"
        ]

        # Add entity recognizer if not present
        if "ner" not in self.nlp.pipe_names:
            ner = self.nlp.add_pipe("ner")
        else:
            ner = self.nlp.get_pipe("ner")

        # Add labels
        for entity_type in self.entity_types:
            ner.add_label(entity_type)

        self.model = None

    def create_training_data(self) -> List[Tuple[str, Dict]]:
        """
        Create training data for aviation NER.

        Format: [(text, {"entities": [(start, end, label)]}), ...]
        """
        training_data = [
            (
                "The Cessna 172 experienced engine failure during takeoff from runway 24.",
                {
                    "entities": [
                        (4, 15, "AIRCRAFT_MODEL"),
                        (28, 42, "FAILURE_MODE"),
                        (50, 57, "FLIGHT_PHASE"),
                        (63, 71, "LOCATION_TYPE")
                    ]
                }
            ),
            (
                "The pilot applied brakes after the propeller struck the ground during landing.",
                {
                    "entities": [
                        (10, 24, "PILOT_ACTION"),
                        (35, 44, "AIRCRAFT_PART"),
                        (67, 74, "FLIGHT_PHASE")
                    ]
                }
            ),
            (
                "IMC conditions with fog reduced visibility to less than 1 mile.",
                {
                    "entities": [
                        (0, 3, "WEATHER_CONDITION"),
                        (20, 23, "WEATHER_CONDITION")
                    ]
                }
            ),
            (
                "The Boeing 737 suffered hydraulic system failure on approach to the airport.",
                {
                    "entities": [
                        (4, 14, "AIRCRAFT_MODEL"),
                        (24, 47, "FAILURE_MODE"),
                        (51, 59, "FLIGHT_PHASE")
                    ]
                }
            ),
            # Add 100+ more examples for production...
        ]

        return training_data

    def train(self, training_data: List[Tuple[str, Dict]], n_iter: int = 30):
        """Train custom NER model."""
        print(f"Training NER model ({len(training_data)} examples, {n_iter} iterations)...")

        # Disable other pipes during training
        other_pipes = [pipe for pipe in self.nlp.pipe_names if pipe != "ner"]
        with self.nlp.disable_pipes(*other_pipes):
            optimizer = self.nlp.begin_training()

            for iteration in range(n_iter):
                random.shuffle(training_data)
                losses = {}
                batches = minibatch(training_data, size=compounding(4.0, 32.0, 1.001))

                for batch in batches:
                    examples = []
                    for text, annotations in batch:
                        doc = self.nlp.make_doc(text)
                        example = Example.from_dict(doc, annotations)
                        examples.append(example)

                    self.nlp.update(examples, drop=0.5, losses=losses)

                print(f"Iteration {iteration + 1}/{n_iter}, Loss: {losses.get('ner', 0):.4f}")

        print("Training complete")
        self.model = self.nlp

    def extract_entities(self, text: str) -> List[Dict]:
        """Extract entities from text."""
        if self.model is None:
            self.model = self.nlp

        doc = self.model(text)

        entities = []
        for ent in doc.ents:
            entities.append({
                "text": ent.text,
                "label": ent.label_,
                "start": ent.start_char,
                "end": ent.end_char
            })

        return entities

    def save_model(self, output_dir: str):
        """Save trained model."""
        self.nlp.to_disk(output_dir)
        print(f"Model saved to {output_dir}")

    def load_model(self, model_dir: str):
        """Load trained model."""
        self.nlp = spacy.load(model_dir)
        self.model = self.nlp
        print(f"Model loaded from {model_dir}")


# Example usage
if __name__ == '__main__':
    # Initialize and train
    ner = AviationNER()
    training_data = ner.create_training_data()
    ner.train(training_data, n_iter=30)

    # Save model
    ner.save_model("models/aviation_ner")

    # Test
    test_text = "The Piper PA-28 lost engine power during cruise and made an emergency landing on a highway."
    entities = ner.extract_entities(test_text)

    print("\nExtracted entities:")
    for ent in entities:
        print(f"  {ent['text']:30} -> {ent['label']}")

Expected accuracy: 85-90% for aviation-specific entities with 100+ training examples

Production NER Pipeline

# scripts/nlp/ner_pipeline.py
"""Production NER pipeline for all narratives."""

import pandas as pd
from sqlalchemy import create_engine
from aviation_ner import AviationNER
import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

def extract_entities_batch(df: pd.DataFrame, ner_model: AviationNER) -> pd.DataFrame:
    """Extract entities from all narratives."""
    logger.info(f"Extracting entities from {len(df)} narratives...")

    df['entities'] = df['narr_accp'].apply(
        lambda x: ner_model.extract_entities(x) if pd.notna(x) else []
    )

    # Count entities by type
    df['num_entities'] = df['entities'].apply(len)

    # Create entity type columns
    for entity_type in ner_model.entity_types:
        df[f'has_{entity_type.lower()}'] = df['entities'].apply(
            lambda ents: any(e['label'] == entity_type for e in ents)
        )

    logger.info(f"Extraction complete. Average entities: {df['num_entities'].mean():.1f}")

    return df

if __name__ == '__main__':
    # Load model
    ner = AviationNER()
    ner.load_model("models/aviation_ner")

    # Load narratives
    engine = create_engine("postgresql://app:dev_password@localhost:5432/ntsb")
    df = pd.read_sql("SELECT ev_id, narr_accp FROM narratives LIMIT 10000", engine)

    # Extract entities
    df = extract_entities_batch(df, ner)

    # Save results
    df.to_parquet('data/narratives_with_entities.parquet')

    # Statistics
    print("\nEntity statistics:")
    for entity_type in ner.entity_types:
        count = df[f'has_{entity_type.lower()}'].sum()
        print(f"  {entity_type:25} {count:6} ({count/len(df)*100:.1f}%)")

Topic Modeling

Discover hidden topics in accident narratives using multiple approaches:

LDA (Latent Dirichlet Allocation)

# scripts/nlp/topic_modeling_lda.py
"""
LDA topic modeling for aviation accidents.
"""

from gensim import corpora
from gensim.models import LdaMulticore, CoherenceModel
import pandas as pd
import pyLDAvis.gensim_models as gensimvis
import pyLDAvis
from typing import List

class LDATopicModeler:
    """LDA-based topic modeling."""

    def __init__(self, num_topics: int = 20, passes: int = 10):
        self.num_topics = num_topics
        self.passes = passes
        self.dictionary = None
        self.corpus = None
        self.lda_model = None

    def prepare_corpus(self, documents: List[List[str]]):
        """Prepare corpus for LDA."""
        # Create dictionary
        self.dictionary = corpora.Dictionary(documents)

        # Filter extremes
        self.dictionary.filter_extremes(
            no_below=5,      # Ignore tokens in fewer than 5 documents
            no_above=0.5,    # Ignore tokens in more than 50% of documents
            keep_n=100000    # Keep at most 100k tokens
        )

        # Create corpus
        self.corpus = [self.dictionary.doc2bow(doc) for doc in documents]

        print(f"Dictionary size: {len(self.dictionary)}")
        print(f"Corpus size: {len(self.corpus)}")

    def train(self, workers: int = 4):
        """Train LDA model."""
        print(f"Training LDA model ({self.num_topics} topics, {self.passes} passes)...")

        self.lda_model = LdaMulticore(
            corpus=self.corpus,
            id2word=self.dictionary,
            num_topics=self.num_topics,
            passes=self.passes,
            workers=workers,
            random_state=42,
            per_word_topics=True
        )

        print("Training complete")

    def get_topics(self, num_words: int = 10) -> List[List[Tuple[str, float]]]:
        """Get top words for each topic."""
        return self.lda_model.show_topics(num_topics=self.num_topics, num_words=num_words, formatted=False)

    def print_topics(self, num_words: int = 10):
        """Print topics in readable format."""
        topics = self.get_topics(num_words)

        for idx, topic in enumerate(topics):
            print(f"\nTopic {idx + 1}:")
            words = [f"{word} ({weight:.3f})" for word, weight in topic[1]]
            print("  " + ", ".join(words))

    def compute_coherence(self, documents: List[List[str]]) -> float:
        """Compute coherence score."""
        coherence_model = CoherenceModel(
            model=self.lda_model,
            texts=documents,
            dictionary=self.dictionary,
            coherence='c_v'
        )
        return coherence_model.get_coherence()

    def visualize(self, output_path: str = "lda_visualization.html"):
        """Create interactive visualization."""
        vis = gensimvis.prepare(self.lda_model, self.corpus, self.dictionary)
        pyLDAvis.save_html(vis, output_path)
        print(f"Visualization saved to {output_path}")

    def predict_topics(self, document: List[str]) -> List[Tuple[int, float]]:
        """Predict topic distribution for a document."""
        bow = self.dictionary.doc2bow(document)
        return self.lda_model.get_document_topics(bow)


# Example usage
if __name__ == '__main__':
    # Load preprocessed data
    df = pd.read_parquet('data/preprocessed_narratives.parquet')
    documents = df['preprocessed_tokens'].tolist()

    # Train LDA
    lda = LDATopicModeler(num_topics=20, passes=15)
    lda.prepare_corpus(documents)
    lda.train(workers=4)

    # Print topics
    lda.print_topics(num_words=15)

    # Coherence score
    coherence = lda.compute_coherence(documents)
    print(f"\nCoherence score: {coherence:.4f}")

    # Visualize
    lda.visualize("lda_aviation_topics.html")

    # Predict topics for new narrative
    test_doc = documents[0]
    topic_dist = lda.predict_topics(test_doc)
    print(f"\nTopic distribution for test document:")
    for topic_id, prob in sorted(topic_dist, key=lambda x: -x[1])[:3]:
        print(f"  Topic {topic_id}: {prob:.3f}")

BERTopic (Modern Approach)

# scripts/nlp/topic_modeling_bert.py
"""
BERTopic for advanced topic modeling.
"""

from bertopic import BERTopic
from sentence_transformers import SentenceTransformer
from umap import UMAP
from hdbscan import HDBSCAN
import pandas as pd

class BERTopicModeler:
    """BERTopic-based topic modeling (state-of-the-art)."""

    def __init__(self, embedding_model: str = "all-MiniLM-L6-v2", nr_topics: int = 20):
        # Sentence transformer for embeddings
        self.sentence_model = SentenceTransformer(embedding_model)

        # UMAP for dimensionality reduction
        self.umap_model = UMAP(
            n_neighbors=15,
            n_components=5,
            min_dist=0.0,
            metric='cosine',
            random_state=42
        )

        # HDBSCAN for clustering
        self.hdbscan_model = HDBSCAN(
            min_cluster_size=15,
            metric='euclidean',
            cluster_selection_method='eom',
            prediction_data=True
        )

        self.topic_model = None
        self.nr_topics = nr_topics

    def train(self, documents: List[str]):
        """Train BERTopic model."""
        print(f"Training BERTopic model on {len(documents)} documents...")

        self.topic_model = BERTopic(
            embedding_model=self.sentence_model,
            umap_model=self.umap_model,
            hdbscan_model=self.hdbscan_model,
            nr_topics=self.nr_topics,
            calculate_probabilities=True,
            verbose=True
        )

        topics, probs = self.topic_model.fit_transform(documents)

        print(f"Found {len(set(topics))} topics")

        return topics, probs

    def get_topic_info(self) -> pd.DataFrame:
        """Get topic information."""
        return self.topic_model.get_topic_info()

    def print_topics(self, top_n: int = 10):
        """Print top topics."""
        topic_info = self.get_topic_info()

        print("\nTop topics:")
        for idx, row in topic_info.head(top_n).iterrows():
            print(f"\nTopic {row['Topic']}: {row['Name']}")
            print(f"  Count: {row['Count']}")
            print(f"  Top words: {', '.join([w for w, _ in self.topic_model.get_topic(row['Topic'])[:10]])}")

    def visualize_topics(self):
        """Create visualizations."""
        # Topic visualization
        fig1 = self.topic_model.visualize_topics()
        fig1.write_html("bertopic_topics.html")

        # Document map
        fig2 = self.topic_model.visualize_documents(documents[:1000])  # Sample for performance
        fig2.write_html("bertopic_documents.html")

        # Topic hierarchy
        fig3 = self.topic_model.visualize_hierarchy()
        fig3.write_html("bertopic_hierarchy.html")

        print("Visualizations saved")

    def find_topics(self, query: str, top_n: int = 5) -> List[Tuple[int, float]]:
        """Find topics similar to query."""
        similar_topics, similarity = self.topic_model.find_topics(query, top_n=top_n)
        return list(zip(similar_topics, similarity))


# Example usage
if __name__ == '__main__':
    # Load data
    df = pd.read_parquet('data/preprocessed_narratives.parquet')
    documents = df['preprocessed_text'].tolist()

    # Train BERTopic
    bert_topic = BERTopicModeler(nr_topics=20)
    topics, probs = bert_topic.train(documents)

    # Add topics to dataframe
    df['topic'] = topics
    df['topic_prob'] = probs.max(axis=1)

    # Print topics
    bert_topic.print_topics(top_n=10)

    # Visualize
    bert_topic.visualize_topics()

    # Find topics for query
    query = "engine failure during takeoff"
    similar = bert_topic.find_topics(query, top_n=5)
    print(f"\nTopics similar to '{query}':")
    for topic_id, similarity in similar:
        print(f"  Topic {topic_id}: {similarity:.3f}")

    # Save
    df.to_parquet('data/narratives_with_topics.parquet')

Expected BERTopic coherence: 0.65-0.75 (higher than LDA's 0.45-0.55)

SafeAeroBERT Fine-Tuning

Fine-tune BERT for aviation-specific tasks. SafeAeroBERT is a domain-adapted model:

Setup SafeAeroBERT

# scripts/nlp/safeaerobert_finetune.py
"""
Fine-tune BERT for aviation accident severity classification.

Based on SafeAeroBERT approach (domain adaptation).
"""

import torch
from transformers import (
    AutoTokenizer, AutoModelForSequenceClassification,
    Trainer, TrainingArguments, EarlyStoppingCallback
)
from datasets import Dataset
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, f1_score, classification_report
import pandas as pd
import numpy as np
from typing import Dict

class SafeAeroBERT:
    """Aviation-domain BERT model."""

    def __init__(self, base_model: str = "bert-base-uncased", num_labels: int = 4):
        self.base_model = base_model
        self.num_labels = num_labels  # FATL, SERS, MINR, NONE

        # Load tokenizer and model
        self.tokenizer = AutoTokenizer.from_pretrained(base_model)
        self.model = AutoModelForSequenceClassification.from_pretrained(
            base_model,
            num_labels=num_labels
        )

        # Label mapping
        self.label2id = {"FATL": 0, "SERS": 1, "MINR": 2, "NONE": 3}
        self.id2label = {v: k for k, v in self.label2id.items()}

        self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
        self.model.to(self.device)

        print(f"Initialized SafeAeroBERT on {self.device}")

    def prepare_dataset(self, df: pd.DataFrame, text_column: str = 'narr_accp',
                       label_column: str = 'ev_highest_injury') -> tuple:
        """Prepare dataset for training."""
        # Filter rows with valid labels
        df = df[df[label_column].isin(self.label2id.keys())]

        # Encode labels
        df['labels'] = df[label_column].map(self.label2id)

        # Split data
        train_df, test_df = train_test_split(
            df, test_size=0.2, random_state=42, stratify=df['labels']
        )

        # Create datasets
        train_dataset = Dataset.from_pandas(train_df[[text_column, 'labels']])
        test_dataset = Dataset.from_pandas(test_df[[text_column, 'labels']])

        # Tokenize
        def tokenize_function(examples):
            return self.tokenizer(
                examples[text_column],
                padding="max_length",
                truncation=True,
                max_length=512
            )

        train_dataset = train_dataset.map(tokenize_function, batched=True)
        test_dataset = test_dataset.map(tokenize_function, batched=True)

        print(f"Training samples: {len(train_dataset)}")
        print(f"Test samples: {len(test_dataset)}")

        return train_dataset, test_dataset

    def compute_metrics(self, eval_pred) -> Dict:
        """Compute evaluation metrics."""
        logits, labels = eval_pred
        predictions = np.argmax(logits, axis=-1)

        accuracy = accuracy_score(labels, predictions)
        f1 = f1_score(labels, predictions, average='weighted')

        return {
            'accuracy': accuracy,
            'f1': f1
        }

    def train(self, train_dataset, eval_dataset, output_dir: str = "models/safeaerobert",
             epochs: int = 5, batch_size: int = 16):
        """Fine-tune SafeAeroBERT."""
        print("Starting training...")

        training_args = TrainingArguments(
            output_dir=output_dir,
            num_train_epochs=epochs,
            per_device_train_batch_size=batch_size,
            per_device_eval_batch_size=batch_size,
            warmup_steps=500,
            weight_decay=0.01,
            logging_dir=f"{output_dir}/logs",
            logging_steps=100,
            evaluation_strategy="epoch",
            save_strategy="epoch",
            load_best_model_at_end=True,
            metric_for_best_model="f1",
            greater_is_better=True,
            save_total_limit=3,
            fp16=torch.cuda.is_available(),  # Mixed precision if GPU
            report_to="mlflow"  # Log to MLflow
        )

        trainer = Trainer(
            model=self.model,
            args=training_args,
            train_dataset=train_dataset,
            eval_dataset=eval_dataset,
            compute_metrics=self.compute_metrics,
            callbacks=[EarlyStoppingCallback(early_stopping_patience=3)]
        )

        # Train
        trainer.train()

        # Save final model
        trainer.save_model(output_dir)
        self.tokenizer.save_pretrained(output_dir)

        print(f"Model saved to {output_dir}")

        return trainer

    def evaluate(self, test_dataset):
        """Evaluate model performance."""
        trainer = Trainer(
            model=self.model,
            eval_dataset=test_dataset,
            compute_metrics=self.compute_metrics
        )

        results = trainer.evaluate()

        print("\nEvaluation results:")
        for key, value in results.items():
            print(f"  {key}: {value:.4f}")

        # Predictions for classification report
        predictions = trainer.predict(test_dataset)
        pred_labels = np.argmax(predictions.predictions, axis=-1)
        true_labels = predictions.label_ids

        report = classification_report(
            true_labels, pred_labels,
            target_names=list(self.label2id.keys())
        )
        print("\nClassification Report:")
        print(report)

        return results

    def predict(self, text: str) -> Dict:
        """Predict severity for single narrative."""
        # Tokenize
        inputs = self.tokenizer(
            text,
            return_tensors="pt",
            padding=True,
            truncation=True,
            max_length=512
        )
        inputs = {k: v.to(self.device) for k, v in inputs.items()}

        # Predict
        with torch.no_grad():
            outputs = self.model(**inputs)
            logits = outputs.logits
            probs = torch.softmax(logits, dim=-1)

        # Get prediction
        pred_class = torch.argmax(probs, dim=-1).item()
        confidence = probs[0][pred_class].item()

        return {
            "severity": self.id2label[pred_class],
            "confidence": confidence,
            "probabilities": {
                self.id2label[i]: probs[0][i].item()
                for i in range(self.num_labels)
            }
        }

    def load_model(self, model_path: str):
        """Load fine-tuned model."""
        self.model = AutoModelForSequenceClassification.from_pretrained(model_path)
        self.tokenizer = AutoTokenizer.from_pretrained(model_path)
        self.model.to(self.device)
        print(f"Model loaded from {model_path}")


# Example usage
if __name__ == '__main__':
    import mlflow

    # Set MLflow tracking
    mlflow.set_tracking_uri("http://localhost:5000")
    mlflow.set_experiment("safeaerobert_finetuning")

    # Load data
    from sqlalchemy import create_engine
    engine = create_engine("postgresql://app:dev_password@localhost:5432/ntsb")

    query = """
        SELECT e.ev_id, n.narr_accp, e.ev_highest_injury
        FROM narratives n
        JOIN events e ON n.ev_id = e.ev_id
        WHERE n.narr_accp IS NOT NULL
            AND e.ev_highest_injury IN ('FATL', 'SERS', 'MINR', 'NONE')
            AND e.ev_date >= CURRENT_DATE - INTERVAL '10 years'
        LIMIT 20000
    """

    df = pd.read_sql(query, engine)

    # Initialize SafeAeroBERT
    safebert = SafeAeroBERT(base_model="bert-base-uncased", num_labels=4)

    # Prepare datasets
    train_dataset, test_dataset = safebert.prepare_dataset(df)

    # Fine-tune
    with mlflow.start_run(run_name="safeaerobert_v1"):
        mlflow.log_params({
            "base_model": "bert-base-uncased",
            "num_labels": 4,
            "epochs": 5,
            "batch_size": 16
        })

        trainer = safebert.train(
            train_dataset,
            test_dataset,
            output_dir="models/safeaerobert",
            epochs=5,
            batch_size=16
        )

        # Evaluate
        results = safebert.evaluate(test_dataset)

        mlflow.log_metrics({
            "test_accuracy": results['eval_accuracy'],
            "test_f1": results['eval_f1']
        })

    # Test prediction
    test_narrative = df['narr_accp'].iloc[0]
    prediction = safebert.predict(test_narrative)

    print("\nTest prediction:")
    print(f"Narrative (first 200 chars): {test_narrative[:200]}")
    print(f"Predicted severity: {prediction['severity']}")
    print(f"Confidence: {prediction['confidence']:.3f}")
    print(f"Probabilities: {prediction['probabilities']}")

Expected performance (after fine-tuning):

  • Accuracy: 87-91%
  • F1 Score: 0.88-0.92
  • Training time: 2-4 hours (GPU), 12-24 hours (CPU)

Production Inference

# scripts/nlp/safeaerobert_inference.py
"""Production inference with SafeAeroBERT."""

from safeaerobert_finetune import SafeAeroBERT
import pandas as pd
from tqdm import tqdm

def batch_predict(model: SafeAeroBERT, narratives: List[str], batch_size: int = 32) -> List[Dict]:
    """Batch prediction for efficiency."""
    results = []

    for i in tqdm(range(0, len(narratives), batch_size)):
        batch = narratives[i:i+batch_size]

        for narrative in batch:
            pred = model.predict(narrative)
            results.append(pred)

    return results

if __name__ == '__main__':
    # Load model
    model = SafeAeroBERT()
    model.load_model("models/safeaerobert")

    # Load narratives
    df = pd.read_parquet('data/narratives_unlabeled.parquet')

    # Predict
    predictions = batch_predict(model, df['narr_accp'].tolist(), batch_size=32)

    # Add predictions to dataframe
    df['predicted_severity'] = [p['severity'] for p in predictions]
    df['prediction_confidence'] = [p['confidence'] for p in predictions]

    # Save
    df.to_parquet('data/narratives_predicted.parquet')

    print(f"Predictions complete. Average confidence: {df['prediction_confidence'].mean():.3f}")

Continue to Part 2 of NLP_TEXT_MINING.md for Information Extraction, Text Classification, Automated Report Generation, Production Deployment, and Case Studies.

(Due to length constraints, creating comprehensive Part 2 separately)