Models, Code, and Tradecraft: Building AI/ML Pipelines for Intelligence Operations

A practitioner’s companion to “Algorithmic Tradecraft” — this time focused on the data classes, model architectures, and Python libraries an engineer actually picks up when building intelligence-grade ML systems.


Introduction

The strategic survey of how the U.S. intelligence community is adopting AI — Project Maven, NGA’s GEOINT modernization, NSA SIGINT processing, Palantir AIP, Anthropic’s Claude Gov, the CIA’s Osiris — answers the question of what is being bought and deployed. It does not answer the engineer’s question: how do you actually build the underlying pieces?

This article is that companion piece. It walks through the five data classes that dominate intelligence work — geospatial, signals, multilingual text, heterogeneous entities, and media authenticity — and shows the model architectures, Python libraries, and code patterns a practitioner reaches for in each. Nothing here is operational tradecraft. Everything here is constructible from public datasets and open-source libraries. The point is to show what a modern intelligence-ML stack actually looks like underneath the procurement diagrams.

A note on scope: real systems in this domain are wrapped in classification, access control, audit, and accreditation layers (ATO, IL5/IL6, cross-domain solutions) that are at least as much of the work as the model itself. We will return to that engineering reality at the end.


1. The Shape of the Problem

An intelligence analyst’s “inbox” is structurally different from a typical enterprise ML problem. It is not a clean tabular dataset with a holdout split. It is a continuous, multimodal firehose drawn from collection systems, open sources, and partner feeds. For modeling purposes, five data classes cover most of it:

  1. Geospatial imagery — electro-optical, infrared, multispectral, synthetic-aperture radar (SAR), full-motion video, increasingly with commercial constellations dominating the volume.
  2. Signals — radio-frequency I/Q samples, network traffic captures, encrypted protocol metadata, telemetry.
  3. Multilingual text and speech — documents, transcribed audio, social posts, machine-translated cables, in dozens of working languages.
  4. Heterogeneous entity graphs — people, organizations, accounts, devices, infrastructure, vehicles, with edges representing communication, financial transfer, co-location, or ownership.
  5. Media authenticity signals — image, audio, and video provenance, particularly with the explosion of fully-synthetic text-to-video output.

Each maps to a different family of models. Mixing them — geospatial detections joined to RF emitters joined to multilingual chatter — is where the real analytic value sits, and where most operational pipelines spend their engineering effort.

The rest of this article walks through each in turn, with code.


2. Geospatial: Object Detection and Change Detection with TorchGeo

The volume problem in GEOINT is well-documented. Commercial constellations now deliver sub-meter and sub-daily revisit, and the analyst-to-pixel ratio has long since crossed the point where humans can review more than a fraction of new collection. Deep learning on satellite imagery — building detection, road extraction, vehicle counting, change detection over time — is the workhorse response.

TorchGeo (Microsoft Research, ongoing) is the de facto PyTorch library for this work [1]. It handles the awkward parts of geospatial deep learning that ordinary torchvision does not: multi-band rasters beyond RGB, coordinate-reference-system reprojection between sensors, geographically-aware sampling that avoids data leakage between train and test, and pre-trained backbones for multispectral imagery (Sentinel-2's 13 bands, Landsat-8's 11).

Here is a representative object-detection setup on the NWPU VHR-10 benchmark, which contains ten classes of objects commonly tasked in overhead imagery (aircraft, ships, vehicles, storage tanks, bridges, etc.):

import torch
from torch.utils.data import DataLoader
from torchgeo.datasets import VHR10
from torchgeo.datamodules.utils import collate_fn_detection
from torchvision.models.detection import (
    fasterrcnn_resnet50_fpn_v2,
    FasterRCNN_ResNet50_FPN_V2_Weights,
)

# VHR-10 ships as a torchgeo dataset; auto-downloads, checksums, and unpacks.
train_ds = VHR10(root="data/vhr10", split="positive", download=True)
val_ds   = VHR10(root="data/vhr10", split="positive", download=False)

train_loader = DataLoader(
    train_ds, batch_size=4, shuffle=True,
    collate_fn=collate_fn_detection, num_workers=4,
)

# Faster R-CNN with COCO-pretrained weights as the starting backbone.
weights = FasterRCNN_ResNet50_FPN_V2_Weights.COCO_V1
model = fasterrcnn_resnet50_fpn_v2(
    weights=weights,
    num_classes=11,  # 10 object classes + background
)

device = "cuda" if torch.cuda.is_available() else "cpu"
model.to(device)
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, weight_decay=1e-4)

model.train()
for epoch in range(20):
    for batch in train_loader:
        images = [img.to(device) for img in batch["image"]]
        targets = [
            {"boxes": b["boxes"].to(device), "labels": b["labels"].to(device)}
            for b in batch["target"]
        ]
        loss_dict = model(images, targets)
        loss = sum(loss_dict.values())
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

For wide-area tasks the relevant primitive is not bounding-box detection but semantic segmentation — labeling every pixel as building, road, water, vegetation, etc. The combination of a U-Net or DeepLabV3+ head over a pre-trained Sentinel-2 backbone (TorchGeo ships ResNet, ViT, and Swin variants pretrained on multispectral imagery via self-supervised methods like SeCo and SatlasPretrain) is a strong default. The segment-geospatial package wraps Meta's Segment Anything for tile-level interactive segmentation, which is increasingly used to bootstrap labeled datasets from analyst clicks rather than from scratch.

Change detection — comparing scenes from different dates over the same location — is its own subproblem. Naive pixel differencing fails because of seasonal vegetation, illumination, and atmospheric variation. Modern approaches use Siamese networks that embed both scenes into a shared feature space and learn change as a distance, or transformer-based architectures (BIT, ChangeFormer) that attend across both timestamps. TorchGeo’s OSCD (Onera Satellite Change Detection) and LEVIR-CD (building change) datasets are the standard benchmarks.

The honest caveat: published benchmark accuracy is highly optimistic relative to operational accuracy. A vehicle detector trained on clean WorldView-3 panchromatic at 30cm GSD degrades sharply on cloudy 3m-resolution PlanetScope tiles. Robustness across sensors, GSDs, look angles, and atmospheres is the hard part — not architecture choice.


3. SIGINT: Deep Learning on Raw I/Q Samples

Classical signals intelligence relies on hand-engineered features — cyclostationary statistics, higher-order moments, instantaneous frequency estimates — feeding decision trees or expert-system rules. Since O’Shea and Corgan’s 2016 paper applying convolutional networks directly to raw in-phase/quadrature samples, deep learning has steadily replaced this pipeline for automatic modulation recognition (AMR) and, increasingly, for RF fingerprinting and emitter identification [2][3].

The input is a complex-valued time series. An I/Q frame from a software-defined radio is a tensor of shape (2, N) — two channels for real and imaginary parts, N samples long. The benchmark dataset is RadioML 2018.01A, with 24 modulation classes at SNRs from -20 to +30 dB [2]. State-of-the-art results sit around 95% peak accuracy with complex-valued or residual architectures [3].

A reasonable starting architecture is a 1D residual CNN treating I and Q as two input channels:

import torch
import torch.nn as nn
import torch.nn.functional as F

class ResBlock1D(nn.Module):
    def __init__(self, channels):
        super().__init__()
        self.conv1 = nn.Conv1d(channels, channels, kernel_size=3, padding=1)
        self.conv2 = nn.Conv1d(channels, channels, kernel_size=3, padding=1)
        self.bn1 = nn.BatchNorm1d(channels)
        self.bn2 = nn.BatchNorm1d(channels)

    def forward(self, x):
        identity = x
        out = F.relu(self.bn1(self.conv1(x)))
        out = self.bn2(self.conv2(out))
        return F.relu(out + identity)

class IQResNet(nn.Module):
    """1D ResNet for I/Q modulation classification.

    Input:  (batch, 2, N)     # I and Q channels, N samples
    Output: (batch, num_cls)  # modulation class logits
    """
    def __init__(self, num_classes=24, base_channels=64, num_blocks=6):
        super().__init__()
        self.stem = nn.Sequential(
            nn.Conv1d(2, base_channels, kernel_size=7, padding=3),
            nn.BatchNorm1d(base_channels),
            nn.ReLU(inplace=True),
            nn.MaxPool1d(2),
        )
        self.blocks = nn.Sequential(
            *[ResBlock1D(base_channels) for _ in range(num_blocks)]
        )
        self.head = nn.Sequential(
            nn.AdaptiveAvgPool1d(1),
            nn.Flatten(),
            nn.Dropout(0.3),
            nn.Linear(base_channels, num_classes),
        )

    def forward(self, x):
        return self.head(self.blocks(self.stem(x)))

# Training loop sketch
model = IQResNet(num_classes=24).to("cuda")
optim = torch.optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-4)
loss_fn = nn.CrossEntropyLoss()

for iq, label, snr in train_loader:        # iq shape: (B, 2, 1024)
    iq, label = iq.to("cuda"), label.to("cuda")
    logits = model(iq)
    loss = loss_fn(logits, label)
    optim.zero_grad(); loss.backward(); optim.step()

Two refinements matter operationally. First, complex-valued convolutions — where the kernel itself is complex and the multiply respects (a+bi)(c+di) = (ac−bd) + (ad+bc)i — preserve phase relationships that a naive two-channel real CNN can lose, and outperform real-valued comparables by roughly 10% on RadioML 2016.10a [3]. Implementations are available in complexPyTorch and cplxmodule. Second, time-frequency representations (short-time Fourier transforms, continuous wavelet transforms, or spectrograms) can be fed as 2D inputs into off-the-shelf ResNets or ViTs, which sometimes outperform raw-I/Q architectures, especially at low SNR.

Beyond modulation recognition, the same primitives extend to:

  • RF fingerprinting — identifying a specific transmitter from manufacturing-induced impairments in its waveform, useful for emitter tracking even when the modulation is identical to thousands of other devices.
  • Protocol identification — classifying which radio standard (LTE, 5G NR, DVB-S2, GSM-R, encrypted satellite uplinks) a captured spectrum slice belongs to.
  • Anomalous emitter detection — autoencoder reconstruction error on baseline traffic to flag novel transmitters in a monitored band.

The adversarial vulnerability is real and worth stating plainly. Sahay, Brinton, and Love showed that small, transferable perturbations applied to I/Q samples can flip a high-confidence modulation classifier reliably, with attacks crafted on one model often transferring to another [4]. A SIGINT system relying solely on a deep classifier for action — without a corroborating channel, sanity check, or analyst review — is brittle against a competent adversary.


4. OSINT NLP: Multilingual Triage at Scale

The text problem in intelligence work is dominated by language coverage and volume. An analyst monitoring a regional crisis may need to ingest material in Russian, Persian, Arabic, Mandarin, Pashto, Tigrinya, and Burmese — often within the same day. The modern stack handles this in three stages: convert speech to text, normalize across languages, and extract structured entities and topics.

Speech to text. OpenAI’s Whisper (and its descendants like faster-whisper and whisperX) handle 99 languages with reasonable accuracy on broadcast-quality audio, and degrade gracefully on telephony and field recordings [5]. NVIDIA's NeMo Canary and Meta's SeamlessM4T are competitive alternatives with on-device deployment options.

Translation. Meta’s NLLB-200 (No Language Left Behind) covers 200 languages including many low-resource pairs that older models like mBART or Marian-NMT handled poorly [6]. For analyst workflows, the standard pattern is to keep both the source-language text and the translation in storage, with translations clearly marked as machine output.

Entity extraction and topic tagging. For named-entity recognition across languages, the choices are XLM-RoBERTa or multilingual DeBERTa fine-tuned on Universal NER, or for production-grade pipelines, spaCy with the xx_ent_wiki_sm model as a fast baseline backed by a transformer model for ambiguous cases.

A minimal end-to-end ingest pipeline:

from faster_whisper import WhisperModel
from transformers import (
    AutoTokenizer,
    AutoModelForSeq2SeqLM,
    AutoModelForTokenClassification,
    pipeline,
)
from sentence_transformers import SentenceTransformer

# --- 1. Transcribe foreign-language audio ---
asr = WhisperModel("large-v3", device="cuda", compute_type="float16")

def transcribe(audio_path):
    segments, info = asr.transcribe(audio_path, beam_size=5)
    text = " ".join(seg.text for seg in segments)
    return text, info.language  # detected source language

# --- 2. Translate to English with NLLB-200 ---
nllb_tok = AutoTokenizer.from_pretrained("facebook/nllb-200-distilled-600M")
nllb = AutoModelForSeq2SeqLM.from_pretrained("facebook/nllb-200-distilled-600M").to("cuda")

LANG_CODE = {  # NLLB uses BCP-47-ish codes; partial mapping
    "ru": "rus_Cyrl", "fa": "pes_Arab", "ar": "arb_Arab",
    "zh": "zho_Hans", "ps": "pbt_Arab", "my": "mya_Mymr",
}

def translate_to_en(text, src):
    nllb_tok.src_lang = LANG_CODE[src]
    inputs = nllb_tok(text, return_tensors="pt", truncation=True).to("cuda")
    out = nllb.generate(
        **inputs,
        forced_bos_token_id=nllb_tok.convert_tokens_to_ids("eng_Latn"),
        max_new_tokens=512,
    )
    return nllb_tok.batch_decode(out, skip_special_tokens=True)[0]

# --- 3. NER + embedding for retrieval ---
ner = pipeline(
    "ner",
    model="Davlan/xlm-roberta-base-ner-hrl",
    aggregation_strategy="simple",
    device=0,
)
embedder = SentenceTransformer("intfloat/multilingual-e5-large")

def ingest(audio_path):
    transcript_src, src_lang = transcribe(audio_path)
    transcript_en = translate_to_en(transcript_src, src_lang)
    entities = ner(transcript_en)
    vector = embedder.encode(f"passage: {transcript_en}", normalize_embeddings=True)
    return {
        "src_lang": src_lang,
        "transcript_src": transcript_src,
        "transcript_en": transcript_en,
        "entities": [(e["word"], e["entity_group"]) for e in entities],
        "vector": vector,
    }

Two practical points. First, error compounds across the pipeline: a Whisper transcription error becomes an NLLB translation error becomes a downstream NER miss. Reviewing pipeline outputs with the source-language transcript alongside is essential; never let a machine-translated document stand alone as the analyst-facing artifact. Second, embeddings cross language boundaries — multilingual-e5 and LaBSE map sentences from different languages into a shared semantic space, which means a query in English can retrieve relevant material in Russian or Persian without a translation step. This is one of the highest-value capabilities in modern analyst tooling and is conceptually trivial to implement.


5. Entity Resolution and Link Analysis with GNNs

Intelligence work is, more than anything else, a graph problem. The unit of analysis is rarely a single document or image; it is a network of people, organizations, accounts, devices, addresses, vehicles, and events linked by communications, financial transfers, co-location, and shared infrastructure. Two distinct ML problems live here: entity resolution (deciding whether two records refer to the same real-world person or thing) and link prediction (inferring relationships not directly observed).

Graph neural networks handle both naturally. PyTorch Geometric (PyG) is the dominant library [7]; DGL is the main alternative. The pattern is to load a heterogeneous graph — multiple node types and edge types — into a HeteroData object, train a HeteroGNN such as HGT or a heterogeneous GraphSAGE, and either classify nodes (entity type, role, threat-relevance) or predict edges (which two accounts are likely operated by the same actor).

import torch
import torch.nn.functional as F
from torch_geometric.data import HeteroData
from torch_geometric.nn import SAGEConv, to_hetero

# Build a heterogeneous OSINT graph.
data = HeteroData()
data["person"].x      = person_feats      # name embeddings, age, etc.
data["account"].x     = account_feats     # platform, age, post-rate features
data["org"].x         = org_feats
data["device"].x      = device_feats      # IMEI, MAC OUI, IP block

# Edge index format: (2, num_edges) of source/target indices.
data["person",  "owns",     "account"].edge_index = person_owns_account
data["person",  "member_of","org"    ].edge_index = person_member_org
data["account", "mentions", "person" ].edge_index = account_mentions_person
data["device",  "used_by",  "person" ].edge_index = device_used_by_person

class GNN(torch.nn.Module):
    def __init__(self, hidden=64, out=32):
        super().__init__()
        self.conv1 = SAGEConv((-1, -1), hidden)
        self.conv2 = SAGEConv((-1, -1), out)

    def forward(self, x, edge_index):
        x = F.relu(self.conv1(x, edge_index))
        return self.conv2(x, edge_index)

# Auto-convert the homogeneous module to a heterogeneous one.
model = to_hetero(GNN(), data.metadata(), aggr="sum")

# Link prediction head: predict (person, owns, account) edges.
class LinkPredictor(torch.nn.Module):
    def forward(self, z_person, z_account, edge_label_index):
        src = z_person[edge_label_index[0]]
        dst = z_account[edge_label_index[1]]
        return (src * dst).sum(dim=-1)   # dot-product score

predictor = LinkPredictor()
optim = torch.optim.AdamW(
    list(model.parameters()) + list(predictor.parameters()), lr=1e-3
)

for epoch in range(50):
    model.train()
    z = model(data.x_dict, data.edge_index_dict)
    logits = predictor(z["person"], z["account"], train_edge_label_index)
    loss = F.binary_cross_entropy_with_logits(logits, train_edge_label)
    optim.zero_grad(); loss.backward(); optim.step()

For entity resolution specifically, the modern approach combines a bi-encoder for candidate retrieval (embedding records into a shared vector space and approximate-nearest-neighbor searching for likely matches) with a cross-encoder for pairwise verification (a transformer that takes two record texts and outputs a same-entity probability). Libraries like dedupe, splink, and record-linkage-toolkit handle the surrounding workflow — blocking, hand-labeling, threshold tuning — that an honest entity-resolution pipeline needs.

The brittleness here deserves emphasis. Identity is contested. A Cyrillic transliteration of an Arabic name routed through a Russian press release is a different surface string than the same name in a Pashto-language Telegram post, and merging two records is sometimes a substantive analytic claim, not a clerical action. Treating ER outputs as suggestions for analyst review — with merge histories preserved and reversible — is the only sane operational posture.


6. Counter-Influence: Synthetic Media Detection

Generative models have shifted the deepfake problem from manipulation detection to born-synthetic detection. Text-to-video systems like Sora 2, VEO 3.1, and Kling 2.5, plus their open-source equivalents running on consumer hardware, now produce video that face-centric forensic detectors trained on FaceForensics++ or Celeb-DF largely miss [8]. The 2026 detection literature has shifted toward larger, more general feature backbones (CLIP, DINOv2) with lightweight adapter heads, multi-modal cross-checks (does the audio match the lip movement, does the lighting match the implied geometry), and provenance-first approaches that try to authenticate genuine content rather than detect fake content [9][10].

A pragmatic detection setup looks like this:

import torch, torch.nn as nn
from transformers import CLIPModel, CLIPProcessor

class CLIPDeepfakeAdapter(nn.Module):
    """Frozen CLIP vision backbone + small trainable classifier head."""
    def __init__(self):
        super().__init__()
        clip = CLIPModel.from_pretrained("openai/clip-vit-large-patch14")
        self.backbone = clip.vision_model
        for p in self.backbone.parameters():
            p.requires_grad = False           # freeze backbone
        d = self.backbone.config.hidden_size
        self.head = nn.Sequential(
            nn.Linear(d, 256), nn.GELU(),
            nn.Dropout(0.3),
            nn.Linear(256, 2),                # real / synthetic
        )

    def forward(self, pixel_values):
        with torch.no_grad():
            feats = self.backbone(pixel_values).pooler_output
        return self.head(feats)

processor = CLIPProcessor.from_pretrained("openai/clip-vit-large-patch14")
model = CLIPDeepfakeAdapter().to("cuda")

Three operational realities that any deployment has to confront:

  1. Generation is outpacing detection. A detector trained on outputs from last-generation diffusion models often fails on next-generation outputs, sometimes within months. Continuous retraining on fresh synthetic samples — and operating with calibrated confidence rather than hard yes/no — is mandatory.
  2. Compression and reposting destroy forensic signals. Edge artifacts in re-encoded social media video are not the same as edge artifacts in raw model output, and detectors trained on raw outputs degrade sharply on the wild distribution.
  3. Provenance beats detection where available. The C2PA standard cryptographically signs the capture device, edit history, and origin of a media file. Where C2PA metadata is present and intact, authenticity is verifiable rather than statistical. Where it is absent — which is most of the threat surface — detection is what remains.

The asymmetry should not be glossed over. A counter-influence cell that treats a deepfake detector as a decisive arbiter, rather than as one signal in a broader analyst assessment, will be wrong in public, repeatedly.


7. RAG over Operational Collections

Large language models with retrieval-augmented generation are the most visible piece of recent IC adoption — Osiris at CIA, NIPRGPT and CamoGPT at DoD, Anthropic’s ClaudeGov, Microsoft’s GCC-High GPT deployments — and the underlying engineering is well within reach of any competent team. The core pattern is:

  1. Chunk each source document into ~500–1000 token passages with overlap.
  2. Embed each chunk with a sentence-encoder model.
  3. Index the vectors in an approximate-nearest-neighbor store (FAISS, Qdrant, Weaviate, Milvus, or pgvector inside Postgres).
  4. Hybrid search at query time: combine semantic similarity with lexical BM25 for terms, names, and identifiers where exact matching matters.
  5. Rerank the top-k results with a cross-encoder for relevance precision.
  6. Generate an analyst-facing answer with strict instructions to cite each claim back to its source chunk.
from sentence_transformers import SentenceTransformer, CrossEncoder
from rank_bm25 import BM25Okapi
import qdrant_client
from qdrant_client.http import models as qm

# --- 1. Indexing ---
embedder = SentenceTransformer("intfloat/multilingual-e5-large")
qclient = qdrant_client.QdrantClient(host="localhost", port=6333)

qclient.recreate_collection(
    collection_name="osint",
    vectors_config=qm.VectorParams(size=1024, distance=qm.Distance.COSINE),
)

points, bm25_corpus = [], []
for chunk_id, chunk in enumerate(corpus_chunks):
    vec = embedder.encode(f"passage: {chunk.text}", normalize_embeddings=True)
    points.append(qm.PointStruct(
        id=chunk_id,
        vector=vec.tolist(),
        payload={
            "doc_id": chunk.doc_id,
            "source": chunk.source,
            "classification": chunk.classification,
            "text": chunk.text,
        },
    ))
    bm25_corpus.append(chunk.text.split())

qclient.upsert(collection_name="osint", points=points)
bm25 = BM25Okapi(bm25_corpus)
reranker = CrossEncoder("BAAI/bge-reranker-large")

# --- 2. Hybrid retrieval + rerank at query time ---
def retrieve(query, user_clearance, k=20, final_k=5):
    qvec = embedder.encode(f"query: {query}", normalize_embeddings=True)

    # Filter by classification at the vector store layer.
    sem_hits = qclient.search(
        collection_name="osint",
        query_vector=qvec.tolist(),
        limit=k,
        query_filter=qm.Filter(
            must=[qm.FieldCondition(
                key="classification",
                match=qm.MatchAny(any=user_clearance),
            )]
        ),
    )
    bm25_scores = bm25.get_scores(query.split())
    bm25_top    = bm25_scores.argsort()[-k:][::-1]

    candidates = {h.id: h.payload for h in sem_hits}
    for idx in bm25_top:
        if idx not in candidates:
            candidates[idx] = points[idx].payload

    pairs   = [(query, c["text"]) for c in candidates.values()]
    rscores = reranker.predict(pairs)
    ranked  = sorted(zip(rscores, candidates.values()), reverse=True)
    return [c for _, c in ranked[:final_k]]

What changes in an intelligence-context RAG system, versus a commercial one:

  • Classification-aware retrieval. Every chunk carries a classification marking, and the vector store filters at query time by the user’s clearance. This is not a UI-level concern; it is a query-filter concern, and getting it wrong is a spillage.
  • Provenance and source citation are mandatory. An analyst cannot use a synthesized answer that does not point back to specific source chunks. Hallucinated citations are worse than no answer.
  • Network-isolated inference. Models run on classified networks (or on accredited cloud regions — AWS Top Secret, Azure Government Secret, OCI for Government). Commercial endpoint APIs are not options. This is what Anthropic’s ClaudeGov, AWS GovCloud’s Bedrock, and Microsoft’s GCC-High exist to support.
  • Red-team prompts and jailbreak evaluation are part of accreditation, not an afterthought.

8. The Engineering Reality: Evaluation, Drift, and Accreditation

The model is the smallest part of the system. The engineering disciplines that determine whether an intelligence-ML pipeline is actually useful in production are largely outside the model definition:

Labeling and ground truth. Most operational tasks have no clean labels. A “high-confidence threat actor” label is a downstream analytic judgment, not a measured quantity. Programs that succeed treat labeling as a first-class data product, with multiple labelers, adjudication, version history, and explicit handling of label noise. Programs that fail train on whatever labels are nearby and discover the gap when the model fails in deployment.

Evaluation under distribution shift. Held-out test sets drawn from the same distribution as the training data are nearly useless for forecasting operational performance, because adversary behavior, sensor configurations, language usage, and signal environments all shift continuously. The relevant evaluation is forward-looking: how does last quarter’s model perform on this quarter’s data? This requires preserving timestamped held-out blocks rather than random splits, and instrumenting in-production performance metrics from day one.

Drift detection. For text and tabular pipelines, monitor input embedding distributions (population stability index, Maximum Mean Discrepancy, or simpler KL-divergence on feature histograms) and trigger investigation when they shift beyond thresholds. For geospatial and signals pipelines, monitor per-class confidence distributions and prediction-volume changes by sensor, region, and time window. Drift is almost never gradual; something concrete changed upstream, and finding what changed is the operational task.

Adversarial robustness. A sophisticated adversary will probe and attack any deployed model. Threat-modeling the system explicitly — what can an adversary inject, where, with what observability — is the difference between a model that survives contact and one that does not. The literature on evasion, poisoning, and model-extraction attacks is extensive; the operational lesson is to never rely on a single classifier as the sole basis for action.

Accreditation. In the U.S. context, fielding an ML system on operational networks requires an Authority to Operate (ATO) from the relevant accrediting authority, security control implementation against NIST SP 800–53, and impact-level certification (DoD Impact Levels 4, 5, 6 for SBU through Secret). For ML systems specifically, expect additional scrutiny on training-data provenance, model supply chain, dependency vulnerabilities, and inference-time monitoring. The DoD CDAO’s responsible-AI guidelines and the IC’s AI Ethics Framework codify expectations that map directly into evaluation artifacts the team must produce. None of this is optional. None of it is the model.

The pattern across successful programs is that the modeling team is twenty percent of the staffing. The other eighty percent is data engineering, evaluation infrastructure, security and accreditation, integration into analyst workflows, and the human factors of getting analysts to trust and effectively use the system without over-trusting it.


Conclusion

The architectures and libraries in this article — TorchGeo, complex CNNs for I/Q, NLLB and Whisper, PyG, CLIP-based forensic adapters, hybrid RAG — are all open and well-documented. None of them are secret. What separates a working intelligence-ML system from a demo is everything around the model: labeled data infrastructure, classification-aware storage and retrieval, distribution-shift evaluation, drift monitoring, adversarial threat modeling, accreditation paperwork, and the analyst-facing integration that determines whether anyone actually uses the output.

The previous article in this series — Algorithmic Tradecraft — covered the strategic shape of AI adoption across the U.S. intelligence community: which agencies, which programs, which contracts, which adversary use cases. This one has tried to fill in the layer underneath: the actual models and code that sit inside those programs. Both layers matter. Strategy without an honest view of the engineering ends up over-promising on timelines; engineering without strategic context ends up building things nobody operates.

The next piece in the series will look at the inverse problem — adversary AI use, and what defensive tradecraft against AI-enabled offense looks like at the engineering level. Same approach: models, code, and the operational realities that vendor diagrams leave out.


About the Author

Brian Curry is a Kansas City-based data scientist and applied AI researcher with two decades of enterprise experience across retail, agriculture, energy, hospitality, telecommunications, and media. He is the founder of Vector1 Research, an independent lab focused on applied AI, causal inference, agentic systems, and the economics of intelligent organizations. His work centers on building systems that surface the causal and structural drivers of performance — bridging advanced analytics, machine learning, and executive decision-making. He writes regularly on Medium about applied AI, knowledge engineering, decision science, and the engineering disciplines that make machine learning actually work in production.

References

[1] Stewart, A. J., Robinson, C., Corley, I. A., Ortiz, A., Lavista Ferres, J. M., & Banerjee, A. (2022). “TorchGeo: Deep Learning With Geospatial Data.” Proceedings of the 30th International Conference on Advances in Geographic Information Systems (SIGSPATIAL ‘22). Microsoft Research. Library: https://github.com/microsoft/torchgeo

[2] O’Shea, T. J., Roy, T., & Clancy, T. C. (2018). “Over-the-Air Deep Learning Based Radio Signal Classification.” IEEE Journal of Selected Topics in Signal Processing, 12(1), 168–179. (Introduces the RadioML 2018.01A benchmark dataset.)

[3] Krzyston, J., Bhattacharjea, R., & Stark, A. (2020). “High-Capacity Complex Convolutional Neural Networks for I/Q Modulation Classification.” arXiv:2010.10717. Georgia Tech / DeepSig.

[4] Sahay, R., Brinton, C. G., & Love, D. J. (2021). “Frequency-based Automated Modulation Classification in the Presence of Adversaries.” arXiv:2011.01132. Purdue University.

[5] Radford, A., Kim, J. W., Xu, T., Brockman, G., McLeavey, C., & Sutskever, I. (2023). “Robust Speech Recognition via Large-Scale Weak Supervision.” OpenAI. Whisper open-source release: https://github.com/openai/whisper

[6] NLLB Team, Meta AI (2022). “No Language Left Behind: Scaling Human-Centered Machine Translation.” arXiv:2207.04672. Model card: https://huggingface.co/facebook/nllb-200-distilled-600M

[7] Fey, M., & Lenssen, J. E. (2019). “Fast Graph Representation Learning with PyTorch Geometric.” ICLR 2019 Workshop on Representation Learning on Graphs and Manifolds. arXiv:1903.02428. https://github.com/pyg-team/pytorch_geometric

[8] SynthForensics (2026). “A Multi-Generator Benchmark for Detecting Synthetic Video Deepfakes.” arXiv:2602.04939. (Human-centric benchmark for purely synthetic T2V outputs from open-source generators.)

[9] “Deepfake Forensics Adapter: A Dual-Stream Network for Generalizable Deepfake Detection.” arXiv 2026. (CLIP-adapter family.)

[10] Coalition for Content Provenance and Authenticity (C2PA). Technical specification: https://c2pa.org/specifications/

[11] U.S. Office of the Director of National Intelligence (2023). The IC AI Ethics Framework. https://www.dni.gov/

[12] U.S. Department of Defense, Chief Digital and Artificial Intelligence Office (CDAO). Responsible AI Strategy and Implementation Pathway. https://www.ai.mil/

[13] Wu, Q. (2026). GeoAI with Python. Leanpub. (Practical reference for torchgeo, segment-geospatial, and geoai.)

Related research