DEV Community

Rikin Patel
Rikin Patel

Posted on

Privacy-Preserving Active Learning for precision oncology clinical workflows with ethical auditability baked in

Privacy-Preserving Active Learning for Precision Oncology

Privacy-Preserving Active Learning for precision oncology clinical workflows with ethical auditability baked in

Introduction: The Clinical Data Dilemma

During my research into federated learning for healthcare applications, I encountered a profound challenge that reshaped my approach to medical AI. While exploring how to apply active learning to oncology datasets across multiple hospitals, I discovered a fundamental tension: the need for high-quality labeled data versus the absolute requirement for patient privacy. In one particularly revealing experiment, I attempted to build a tumor classification model using data from three different cancer centers, only to realize that even metadata about query patterns could potentially reveal sensitive patient information.

This experience led me down a rabbit hole of differential privacy, secure multi-party computation, and ethical AI frameworks. Through studying recent papers on privacy-preserving machine learning, I learned that traditional active learning approaches—where a model selects the most informative samples for human labeling—create significant privacy risks in clinical settings. Each query to a clinician for labeling reveals something about what the model finds uncertain, which could potentially be traced back to individual patient characteristics.

My exploration of this space revealed that precision oncology presents unique challenges: rare mutations, heterogeneous tumor profiles, and the life-critical nature of treatment decisions demand both data efficiency and absolute privacy protection. This article documents my journey in developing a framework that addresses these competing requirements while baking in ethical auditability from the ground up.

Technical Background: The Convergence of Three Disciplines

Active Learning in Medical Contexts

While learning about active learning strategies, I observed that traditional approaches like uncertainty sampling, query-by-committee, and expected model change work well in data-rich environments but falter in medical contexts. In my experimentation with tumor segmentation models, I found that standard uncertainty sampling often selected edge cases that were medically irrelevant or already well-understood by clinicians.

One interesting finding from my experimentation with Bayesian active learning was that incorporating domain knowledge through clinical utility scores dramatically improved sample selection efficiency. By studying how oncologists prioritize cases, I realized that information gain alone wasn't sufficient—we needed to weight samples by their potential clinical impact.

Privacy-Preserving Techniques

Through my investigation of privacy technologies, I discovered that differential privacy (DP) provides strong mathematical guarantees but presents challenges for active learning. The noise addition required by DP can distort uncertainty estimates, making sample selection unreliable. During my research into secure multi-party computation (MPC), I found that while it offers perfect privacy in theory, the computational overhead makes real-time clinical workflows impractical.

My exploration of homomorphic encryption revealed a promising middle ground. While experimenting with partially homomorphic encryption schemes, I came across the CKKS (Cheon-Kim-Kim-Song) scheme that allows approximate arithmetic on encrypted data—perfect for the probabilistic nature of machine learning models.

Ethical Auditability Framework

As I was experimenting with various AI governance frameworks, I realized that most audit trails in medical AI focus on model performance metrics rather than decision-making processes. Through studying ethical AI literature, I learned that true auditability requires tracking not just what decisions were made, but why specific data points were selected, who was involved in labeling, and what alternatives were considered.

Implementation Architecture

System Overview

The framework I developed consists of three main components:

  1. Privacy-Preserving Query Module: Handles secure sample selection without exposing patient data
  2. Federated Active Learning Orchestrator: Coordinates learning across institutions while maintaining data locality
  3. Ethical Audit Trail Generator: Creates immutable, interpretable records of all decisions

Core Privacy Mechanism: Encrypted Uncertainty Estimation

During my investigation of encrypted machine learning, I found that computing uncertainty metrics on encrypted data requires novel approaches. Standard uncertainty measures like entropy or Bayesian dropout variance aren't directly computable under homomorphic encryption.

import tenseal as ts
import numpy as np
from typing import List, Tuple

class EncryptedUncertaintyEstimator:
    """
    Computes uncertainty metrics on encrypted model predictions
    using CKKS homomorphic encryption
    """

    def __init__(self, context: ts.Context):
        self.context = context

    def encrypted_entropy(self, encrypted_probs: List[ts.CKKSVector]) -> ts.CKKSVector:
        """
        Compute Shannon entropy on encrypted probability vectors
        H(p) = -Σ p_i * log(p_i)
        """
        # Homomorphic operations for entropy approximation
        # Using polynomial approximation of log function
        entropy = encrypted_probs[0].polyval([0, 0])  # Initialize

        for enc_prob in encrypted_probs:
            # Taylor approximation of -p*log(p) around p=0.5
            # -p*log(p) ≈ 0.5 - (p-0.5) + (p-0.5)^2 - (2/3)(p-0.5)^3
            centered = enc_prob - 0.5
            squared = centered * centered
            cubed = squared * centered

            term = 0.5 - centered + squared - (2/3) * cubed
            entropy = entropy + term

        return entropy

    def batch_encrypted_uncertainty(
        self,
        encrypted_predictions: List[List[ts.CKKSVector]],
        clinical_weights: np.ndarray = None
    ) -> List[ts.CKKSVector]:
        """
        Compute weighted uncertainty scores for batch of predictions
        """
        uncertainties = []

        for i, enc_preds in enumerate(encrypted_predictions):
            entropy = self.encrypted_entropy(enc_preds)

            # Apply clinical utility weights if provided (encrypted)
            if clinical_weights is not None:
                # Weighted uncertainty = entropy * clinical_importance
                # Clinical weights are pre-encrypted by each institution
                weight_vector = ts.ckks_vector(self.context, clinical_weights[i])
                weighted_entropy = entropy * weight_vector
                uncertainties.append(weighted_entropy)
            else:
                uncertainties.append(entropy)

        return uncertainties
Enter fullscreen mode Exit fullscreen mode

Federated Active Learning Protocol

One of the key insights from my experimentation was that federated learning and active learning have complementary privacy benefits. While federated learning keeps data local, active learning minimizes the amount of data that needs to be labeled. Combining them required developing a new protocol:

from dataclasses import dataclass
from typing import Dict, Any, List
import hashlib
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
import json

@dataclass
class AuditRecord:
    """Immutable audit record for ethical tracking"""
    query_id: str
    timestamp: str
    institution_id: str
    sample_hash: str  # Hash of sample metadata (not actual data)
    uncertainty_score: float  # Decrypted aggregate score
    selection_reason: str
    clinician_id: str  # Who labeled it
    ethical_considerations: List[str]
    previous_hash: str  # For blockchain-like chaining

    def to_immutable_record(self) -> Dict[str, Any]:
        """Create cryptographically signed record"""
        record_data = {
            'query_id': self.query_id,
            'timestamp': self.timestamp,
            'institution_id': self.institution_id,
            'sample_hash': self.sample_hash,
            'uncertainty_score': self.uncertainty_score,
            'selection_reason': self.selection_reason,
            'clinician_id': self.clinician_id,
            'ethical_considerations': self.ethical_considerations,
            'previous_hash': self.previous_hash
        }

        # Create hash chain
        record_str = json.dumps(record_data, sort_keys=True)
        current_hash = hashlib.sha256(
            f"{record_str}{self.previous_hash}".encode()
        ).hexdigest()

        record_data['current_hash'] = current_hash
        return record_data

class FederatedActiveLearningOrchestrator:
    """
    Coordinates privacy-preserving active learning across institutions
    """

    def __init__(self, institutions: List[str], context_params: Dict):
        self.institutions = institutions
        self.context = self._setup_ckks_context(context_params)
        self.audit_chain = []  # Ethical audit trail
        self.query_counter = 0

    def privacy_preserving_query_selection(
        self,
        batch_size: int = 10
    ) -> List[Dict]:
        """
        Select most informative samples across all institutions
        without revealing individual patient data
        """
        self.query_counter += 1

        # Step 1: Each institution computes encrypted uncertainties
        all_encrypted_uncertainties = []
        institution_samples = []

        for inst_id in self.institutions:
            # This happens locally at each institution
            enc_uncertainties, sample_metadata = \
                self._get_institution_uncertainties(inst_id)
            all_encrypted_uncertainties.extend(enc_uncertainties)
            institution_samples.extend([
                {'inst_id': inst_id, 'metadata': meta}
                for meta in sample_metadata
            ])

        # Step 2: Secure aggregation of uncertainties
        # Using secure multi-party computation for comparison
        aggregated = self._secure_top_k_selection(
            all_encrypted_uncertainties,
            batch_size
        )

        # Step 3: Create audit records
        selected_samples = []
        for idx in aggregated['selected_indices']:
            sample_info = institution_samples[idx]

            audit_record = AuditRecord(
                query_id=f"Q{self.query_counter:06d}",
                timestamp=self._get_timestamp(),
                institution_id=sample_info['inst_id'],
                sample_hash=self._hash_metadata(sample_info['metadata']),
                uncertainty_score=aggregated['scores'][idx],  # Decrypted
                selection_reason="High uncertainty with clinical relevance",
                clinician_id="system_initial",
                ethical_considerations=[
                    "Patient consent verified",
                    "IRB approval confirmed",
                    "Clinical utility threshold met"
                ],
                previous_hash=self._get_previous_hash()
            )

            self.audit_chain.append(audit_record.to_immutable_record())
            selected_samples.append({
                'institution': sample_info['inst_id'],
                'audit_record_id': audit_record.query_id,
                'metadata_hash': audit_record.sample_hash
            })

        return selected_samples

    def _secure_top_k_selection(
        self,
        encrypted_values: List,
        k: int
    ) -> Dict:
        """
        Select top k values without decrypting individual scores
        using secure comparison protocols
        """
        # Implementation of secure multi-party comparison
        # This is a simplified version - actual implementation
        # would use proper MPC protocols

        # For demonstration: using additive secret sharing
        shares = self._create_secret_shares(encrypted_values)

        # Institutions collaboratively compute comparisons
        # without learning individual values
        comparison_results = self._mpc_compare_shares(shares)

        # Select indices with highest values
        selected_indices = np.argsort(comparison_results)[-k:]

        return {
            'selected_indices': selected_indices,
            'scores': comparison_results  # Aggregated, not individual
        }
Enter fullscreen mode Exit fullscreen mode

Clinical Workflow Integration

Through my research into oncology workflows, I realized that any AI system must integrate seamlessly with existing clinical processes. One interesting finding from my experimentation with hospital EHR systems was that oncologists have specific patterns of interaction that the system needs to accommodate.

class ClinicalWorkflowIntegrator:
    """
    Integrates active learning into existing oncology workflows
    """

    def __init__(self, ehr_interface_config: Dict):
        self.ehr_config = ehr_interface_config
        self.labeling_queue = []
        self.clinician_feedback = {}

    def present_query_to_clinician(
        self,
        sample_info: Dict,
        context_data: Dict,
        privacy_level: str = "de-identified"
    ) -> Dict:
        """
        Present selected sample to clinician for labeling
        with appropriate privacy protections
        """
        # Extract relevant context without exposing full records
        presentation_data = self._prepare_clinical_view(
            sample_info,
            context_data,
            privacy_level
        )

        # Log presentation for audit trail
        self._log_clinician_interaction(
            clinician_id=context_data['clinician_id'],
            sample_hash=sample_info['metadata_hash'],
            presentation_time=self._get_timestamp(),
            data_elements_shown=list(presentation_data.keys())
        )

        return {
            'presentation_data': presentation_data,
            'labeling_interface': self._build_labeling_interface(
                sample_info['clinical_type']
            ),
            'ethical_checkpoints': self._get_ethical_checkpoints(
                sample_info['patient_cohort']
            )
        }

    def _prepare_clinical_view(
        self,
        sample_info: Dict,
        context: Dict,
        privacy_level: str
    ) -> Dict:
        """
        Prepare data for clinical review with privacy controls
        """
        if privacy_level == "de-identified":
            # Show only clinically relevant features
            # without direct identifiers
            return {
                'tumor_characteristics': self._extract_tumor_features(
                    sample_info,
                    include_genomics=True
                ),
                'treatment_history': self._summarize_treatments(
                    sample_info,
                    anonymize_dates=True
                ),
                'imaging_findings': self._extract_imaging_features(
                    sample_info,
                    remove_location_data=True
                ),
                'molecular_profile': self._prepare_molecular_data(
                    sample_info,
                    aggregate_rare_mutations=True
                )
            }
        elif privacy_level == "fully_identified":
            # Only with explicit consent and IRB approval
            return self._get_full_record(sample_info)
        else:
            raise ValueError(f"Unknown privacy level: {privacy_level}")

    def process_clinician_label(
        self,
        label_data: Dict,
        audit_record_id: str
    ) -> Dict:
        """
        Process clinician's label and update audit trail
        """
        # Validate label against clinical guidelines
        validation_result = self._validate_clinical_label(
            label_data['label'],
            label_data['confidence'],
            label_data['rationale']
        )

        if validation_result['is_valid']:
            # Create new audit record for labeling action
            labeling_record = AuditRecord(
                query_id=audit_record_id + "_LABELED",
                timestamp=self._get_timestamp(),
                institution_id=label_data['institution_id'],
                sample_hash=label_data['sample_hash'],
                uncertainty_score=0,  # Now resolved
                selection_reason=f"Labeled by {label_data['clinician_id']}",
                clinician_id=label_data['clinician_id'],
                ethical_considerations=[
                    "Label validated against guidelines",
                    f"Confidence: {label_data['confidence']}",
                    f"Rationale documented: {label_data['rationale'][:100]}..."
                ],
                previous_hash=self._get_previous_hash()
            )

            self.audit_chain.append(labeling_record.to_immutable_record())

            return {
                'success': True,
                'label': label_data['label'],
                'audit_record': labeling_record.to_immutable_record(),
                'model_update_required': validation_result['changes_model']
            }
Enter fullscreen mode Exit fullscreen mode

Real-World Applications in Precision Oncology

Molecular Tumor Board Support

While exploring how AI could assist molecular tumor boards, I discovered that the most valuable application was in pre-screening complex cases. Through my experimentation with real tumor board workflows, I found that our system could:

  1. Identify knowledge gaps: Flag cases where the AI model has high uncertainty about specific mutation interpretations
  2. Prioritize discussion: Surface cases with conflicting evidence from different data modalities
  3. Maintain learning: Continuously improve from board decisions while protecting patient privacy

Clinical Trial Matching

One of the most promising applications emerged during my research into clinical trial recruitment. The privacy-preserving active learning framework can:

class TrialMatchingEnhancer:
    """
    Enhances clinical trial matching using active learning
    while preserving patient privacy
    """

    def identify_trial_knowledge_gaps(
        self,
        encrypted_patient_vectors: List,
        trial_criteria: Dict
    ) -> List[Dict]:
        """
        Identify areas where trial matching is uncertain
        without exposing patient data
        """
        # Compute uncertainty about trial eligibility
        eligibility_uncertainties = []

        for enc_patient in encrypted_patient_vectors:
            # For each trial criterion, compute match uncertainty
            criterion_uncertainties = []

            for criterion in trial_criteria['molecular_criteria']:
                # Check if patient's encrypted features match criterion
                match_prob = self._encrypted_criterion_match(
                    enc_patient,
                    criterion
                )

                # Uncertainty is high when probability is near 0.5
                uncertainty = 4 * match_prob * (1 - match_prob)  # 0-1 scale
                criterion_uncertainties.append(uncertainty)

            # Weight by clinical importance of criterion
            weighted_uncertainty = self._weight_by_importance(
                criterion_uncertainties,
                trial_criteria['importance_weights']
            )

            eligibility_uncertainties.append(weighted_uncertainty)

        # Select patients where we're most uncertain about trial match
        # These are candidates for deeper review
        return self._select_high_uncertainty_cases(
            eligibility_uncertainties,
            threshold=0.3
        )
Enter fullscreen mode Exit fullscreen mode

Longitudinal Treatment Response Prediction

My exploration of treatment response prediction revealed that active learning is particularly valuable for rare treatment combinations. As I was experimenting with immunotherapy response models, I found that:

  1. Early uncertainty identification: The system could identify which patients' responses were hardest to predict early in treatment
  2. Adaptive monitoring: Suggest more frequent imaging or lab tests for high-uncertainty cases
  3. Privacy-preserving aggregation: Learn from outcomes across institutions without sharing individual response trajectories

Challenges and Solutions

Challenge 1: Balancing Privacy and Utility

During my investigation of differential privacy for active learning, I encountered the utility-privacy tradeoff problem

Top comments (0)