DeepTaxa Tutorial: Classifying V3-V4 16S ASVs and Validating Against a Mock Community

Open In Colab

A 16S survey produces a set of amplicon sequence variants (ASVs), and the analytical goal is to determine which organisms they represent and how far down the taxonomy the names can be trusted. This notebook pursues that goal with DeepTaxa on V3-V4 reads from a community of known composition, the ZymoBIOMICS Microbial Community Standard, where each assignment can be checked directly.

The key choice when using DeepTaxa is to match the model checkpoint to the sequenced region. DeepTaxa provides region-specific checkpoints; these reads are V3-V4, so the V3-V4 checkpoint is used. The sample is the ZymoBIOMICS standard, eight known bacteria, sequenced 2 x 300 on an Illumina MiSeq over V3-V4 (accession SRR35133066). The analysis begins from ASVs produced with DADA2; the Appendix details that step. The notebook runs on a free Colab GPU.

1 Install

!pip install -q deeptaxa-rrna
import pandas as pd, torch
if torch.cuda.is_available():
    g = torch.cuda.get_device_properties(0)
    print(f"GPU: {g.name}, {g.total_memory // 1024**3} GB, compute {g.major}.{g.minor}, CUDA {torch.version.cuda}")
else:
    print("No GPU detected. In Colab: Runtime > Change runtime type > GPU, then run again.")
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0.0/82.7 kB ? eta -:--:--
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 82.7/82.7 kB 5.2 MB/s eta 0:00:00
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0.0/3.2 MB ? eta -:--:--
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸ 3.2/3.2 MB 104.4 MB/s eta 0:00:01
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 3.2/3.2 MB 68.5 MB/s eta 0:00:00
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0.0/425.6 kB ? eta -:--:--
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 425.6/425.6 kB 39.5 MB/s eta 0:00:00
GPU: Tesla T4, 14 GB, compute 7.5, CUDA 12.8

2 The sequences

Fifteen ASVs were inferred from a ZymoBIOMICS Microbial Community Standard (Zymo Research) sequenced over the V3-V4 region, from the public SRA run SRR35133066. The Appendix shows how they were produced.

import urllib.request
base = "https://raw.githubusercontent.com/systems-genomics-lab/deeptaxa/main/tutorials/validation/data"
urllib.request.urlretrieve(f"{base}/v3v4_seqs.fasta", "asvs.fasta")
counts = pd.read_csv(f"{base}/v3v4_counts.tsv", sep="\t", index_col=0)["count"]
print(len(counts), "ASVs")
15 ASVs

3 Download the V3-V4 checkpoint

The checkpoints are on Hugging Face. This is V3-V4 data, so the V3-V4 checkpoint is downloaded.

!wget -q -nc https://huggingface.co/systems-genomics-lab/deeptaxa/resolve/main/deeptaxa-v3v4-v2.pt

4 Classify

A single command performs the classification. --tabular writes one row per sequence with a column per rank.

!deeptaxa predict --fasta-file asvs.fasta --checkpoint deeptaxa-v3v4-v2.pt --output-dir out --tabular
pred = pd.read_csv("out/asvs_deeptaxa_predictions.tsv", sep="\t").set_index("sequence_id")
counts = counts.reindex(pred.index)  # align read counts to the classified ASVs
2026-06-27 09:05:36,373 - INFO - 
======================================================================
          DeepTaxa Prediction Session (v1.1.0)
--------------------------------------------------
          Started: 2026-06-27T09:05:36.373131
======================================================================
    
config.json: 100% 904/904 [00:00<00:00, 3.09MB/s]
configuration_bert.py: 100% 1.01k/1.01k [00:00<00:00, 2.82MB/s]
[transformers] A new version of the following files was downloaded from https://huggingface.co/zhihan1996/DNABERT-2-117M:
- configuration_bert.py
. Make sure to double-check they do not contain any added malicious code. To avoid downloading new versions of the code file, you can pin a revision.
tokenizer_config.json: 100% 158/158 [00:00<00:00, 701kB/s]
Warning: You are sending unauthenticated requests to the HF Hub. Please set a HF_TOKEN to enable higher rate limits and faster downloads.
2026-06-27 09:05:37,959 - WARNING - Warning: You are sending unauthenticated requests to the HF Hub. Please set a HF_TOKEN to enable higher rate limits and faster downloads.
tokenizer.json: 100% 168k/168k [00:00<00:00, 85.5MB/s]
2026-06-27 09:05:39,130 - INFO - Initialized HybridCNNBERTClassifier with CNN: {'embed_dim': 896, 'num_filters': 256, 'kernel_sizes': [3, 5, 7], 'num_conv_layers': 1, 'dropout_prob': 0.2}, BERT hidden_size=896, output_attentions=False
2026-06-27 09:05:39,291 - INFO - Model loaded from checkpoint: deeptaxa-v3v4-v2.pt with type: hybridcnnbert
2026-06-27 09:05:39,987 - INFO - Loaded 15 sequences from asvs.fasta
Predicting: 100% 1/1 [00:01<00:00,  1.68s/it]
2026-06-27 09:05:41,671 - INFO - Saved run UUID to out/deeptaxa_uuid.txt
2026-06-27 09:05:41,672 - INFO - 
======================================================================
          DeepTaxa Prediction Summary (v1.1.0)
--------------------------------------------------
          Here's a summary of your prediction results:
          - Total Sequences Processed: 15
          - Prediction Time: 5.30 seconds
          - Completed At: 2026-06-27T09:05:41.671186
======================================================================
    
2026-06-27 09:05:41,672 - INFO - Performance Metrics by Taxonomic Rank:
2026-06-27 09:05:41,672 - INFO - Rank       | Mean Score   | Std Score    | Accuracy     | Top-{top_k} Acc | F1       | Precision    | Recall       | AUC         
2026-06-27 09:05:41,672 - INFO - ---------------------------------------------------------------------------------------------------------------------------------
2026-06-27 09:05:41,672 - INFO - domain     | 1.0000       | 0.0000       | N/A          | N/A          | N/A      | N/A          | N/A          | N/A         
2026-06-27 09:05:41,673 - INFO - phylum     | 1.0000       | 0.0001       | N/A          | N/A          | N/A      | N/A          | N/A          | N/A         
2026-06-27 09:05:41,673 - INFO - class      | 1.0000       | 0.0000       | N/A          | N/A          | N/A      | N/A          | N/A          | N/A         
2026-06-27 09:05:41,673 - INFO - order      | 0.9993       | 0.0016       | N/A          | N/A          | N/A      | N/A          | N/A          | N/A         
2026-06-27 09:05:41,673 - INFO - family     | 0.9665       | 0.1220       | N/A          | N/A          | N/A      | N/A          | N/A          | N/A         
2026-06-27 09:05:41,674 - INFO - genus      | 0.9731       | 0.0962       | N/A          | N/A          | N/A      | N/A          | N/A          | N/A         
2026-06-27 09:05:41,674 - INFO - species    | 0.8010       | 0.2472       | N/A          | N/A          | N/A      | N/A          | N/A          | N/A         
2026-06-27 09:05:41,894 - INFO - Saved prediction results to: out/asvs_deeptaxa_predictions.json
2026-06-27 09:05:41,909 - INFO - Saved tabular prediction results to: out/asvs_deeptaxa_predictions.tsv
2026-06-27 09:05:41,909 - INFO - 
======================================================================
Thank you for using DeepTaxa 🤗
======================================================================

5 Read the predictions

Each rank has a predicted label and a raw_score, the model’s confidence in that label, from 0 to 1. Confidence is high on a region-matched amplicon and falls at the deeper ranks, because the 16S gene carries less information about species than about phylum. A low score means the model is uncertain and should be treated as provisional. Where DeepTaxa cannot resolve a species, it keeps the genus and returns it with an “Unclassified_” prefix, for example “Unclassified_Escherichia”, rather than forcing a species call.

pred[["genus_predicted", "genus_raw_score", "species_predicted", "species_raw_score"]].head()
genus_predicted genus_raw_score species_predicted species_raw_score
sequence_id
ASV1 Limosilactobacillus 0.999996 Limosilactobacillus fermentum 0.999981
ASV2 Bacillus_P_294101 0.999701 Bacillus_P_294101 halotolerans 0.748827
ASV3 Escherichia 0.999959 Unclassified_Escherichia 0.537758
ASV4 Salmonella 0.999756 Salmonella bongori 0.999722
ASV5 Staphylococcus 0.999766 Unclassified_Staphylococcus 0.953749

6 Validate against the known genera

The sample contains eight known genera, so the result can be checked. DeepTaxa reports GTDB names, which differ from the NCBI names of the standard: Lactobacillus fermentum is Limosilactobacillus in GTDB, Pseudomonas may appear as Pseudomonas_E, and Escherichia and Shigella are reported together. The short function below reduces a label to a plain genus name so the two can be compared.

For comparison, the result of a conventional pipeline is loaded as a reference: DADA2 with the SILVA v138 database. That pipeline classifies the genus with a naive Bayes model and assigns a species only by exact matching against the reference. It is labeled DADA2 + SILVA below, since SILVA itself is the reference database, not the classifier.

import re
silva = pd.read_csv(f"{base}/v3v4_silva.tsv", sep="\t").set_index("ASV").reindex(pred.index)

def base_genus(name):
    if not isinstance(name, str):                      # missing genus (NaN)
        return None
    name = re.sub(r"[ _-].*", "", name).lower()        # drop GTDB suffixes and "sensu stricto"
    return {"limosilactobacillus": "lactobacillus", "shigella": "escherichia"}.get(name, name)

EXPECTED = {"pseudomonas", "escherichia", "salmonella", "lactobacillus",
            "enterococcus", "staphylococcus", "listeria", "bacillus"}

dt_genus = pred["genus_predicted"].map(base_genus)
sv_genus = silva["Genus"].map(base_genus)

print("DeepTaxa recovered", len(set(dt_genus) & EXPECTED), "of 8 genera")
print("DADA2 + SILVA recovered", len(set(sv_genus.dropna()) & EXPECTED), "of 8 genera")
print("They agree on the genus of",
      round(counts[dt_genus == sv_genus].sum() / counts.sum() * 100), "% of reads")
DeepTaxa recovered 8 of 8 genera
DADA2 + SILVA recovered 8 of 8 genera
They agree on the genus of 100 % of reads

7 Species: where the two methods differ

At the species rank the methods diverge. The table lists the calls for the most abundant ASVs; a single-word entry is a genus only, where the method stopped, and a two-word entry is a species. DADA2 + SILVA assigns a species only on an exact, unambiguous reference match, so here it stops at the genus on all but a few low-abundance ASVs. Those genus-only entries are reads it declined to name rather than reads it named incorrectly, and every species it does assign is correct. DeepTaxa names a species for a much larger share of the reads, but from this short region it too stops at the genus on several ASVs, such as Staphylococcus, Escherichia, and Listeria, where the V3-V4 window does not support a confident species call. Where it does name a species, the call is occasionally a close relative of the expected organism, such as Shigella for Escherichia coli. The full-length example resolves species more reliably.

pd.DataFrame({
    "reads": counts,
    "DeepTaxa": (pred["species_predicted"]
                 .str.replace("Unclassified_", "", regex=False)
                 .str.replace(r"_[A-Za-z0-9]+", "", regex=True)),
    "DADA2 + SILVA": (silva["Genus"].fillna("") + " " + silva["Species"].fillna("")).str.strip(),
}).sort_values("reads", ascending=False).head(10)
reads DeepTaxa DADA2 + SILVA
sequence_id
ASV1 7473 Limosilactobacillus fermentum Lactobacillus
ASV2 3550 Bacillus halotolerans Bacillus
ASV3 3494 Escherichia Escherichia-Shigella
ASV4 3356 Salmonella bongori Salmonella
ASV5 2821 Staphylococcus Staphylococcus
ASV6 2558 Listeria Listeria
ASV7 2084 Pseudomonas paraeruginosa Pseudomonas
ASV8 1633 Enterococcus faecalis Enterococcus
ASV9 574 Escherichia Escherichia-Shigella
ASV10 573 Staphylococcus Staphylococcus aureus

The calls reduce to a single set of numbers: the share of reads reaching the correct species. DADA2 + SILVA is credited for exact matches only; DeepTaxa is credited for exact matches, and separately for exact matches plus close relatives that 16S cannot separate from the expected organism.

EXPECTED_SP = {"escherichia": "coli", "salmonella": "enterica", "lactobacillus": "fermentum",
               "enterococcus": "faecalis", "staphylococcus": "aureus", "listeria": "monocytogenes",
               "pseudomonas": "aeruginosa", "bacillus": "subtilis"}
SIBLINGS = {"escherichia": {"flexneri", "sonnei", "boydii", "dysenteriae", "albertii"},
            "pseudomonas": {"paraeruginosa"}, "bacillus": {"spizizenii", "inaquosorum", "halotolerans"}}

def epithet(label):
    return re.sub(r"_[A-Za-z0-9]+", "", str(label).split()[-1]).lower() if " " in str(label) else ""

dt_g = pred["genus_predicted"].map(base_genus)
dt_e = pred["species_predicted"].map(epithet)
confident = pred["species_raw_score"] >= 0.5
dt_exact = confident & (dt_e == dt_g.map(EXPECTED_SP))
dt_sibling = confident & pd.Series([e in SIBLINGS.get(g, set()) for g, e in zip(dt_g, dt_e)], index=dt_g.index)
sv_e = silva["Species"].map(lambda s: str(s).lower() if isinstance(s, str) else "")
sv_exact = sv_e == silva["Genus"].map(base_genus).map(EXPECTED_SP)

def reads_pct(mask):
    return round(counts[mask].sum() / counts.sum() * 100)

print("Reads correctly identified to species (%):")
print(f"  DADA2 + SILVA, exact:               {reads_pct(sv_exact)}")
print(f"  DeepTaxa, exact:                    {reads_pct(dt_exact)}")
print(f"  DeepTaxa, exact or close relative:  {reads_pct(dt_exact | dt_sibling)}")
Reads correctly identified to species (%):
  DADA2 + SILVA, exact:               4
  DeepTaxa, exact:                    32
  DeepTaxa, exact or close relative:  52

8 Summary

DeepTaxa recovered all eight genera, agreed with DADA2 + SILVA on the genus of essentially every read, and named a species for many more reads than the reference method, though from this short region it stopped at the genus on some ASVs. The two lessons that carry to other datasets are to use the checkpoint that matches the sequenced region, and to read the confidence score as a measure of information rather than a guarantee, filtering low-confidence calls. The full-length example shows the same workflow where the longer gene supports species-level assignment.

9 Appendix: how the ASVs were made

The ASVs were produced offline from raw reads with primer removal and DADA2.

cutadapt -g CCTACGGGNGGCWGCAG -G GGACTACNVGGGTWTCTAAT \
         --discard-untrimmed -e 0.15 --minimum-length 100 \
         -o fwd.trim.fastq.gz -p rev.trim.fastq.gz fwd.fastq.gz rev.fastq.gz
library(dada2)
filterAndTrim("fwd.trim.fastq.gz", "F.fq.gz", "rev.trim.fastq.gz", "R.fq.gz",
              truncLen = c(280, 220), maxEE = c(2, 4), truncQ = 2, rm.phix = TRUE, multithread = TRUE)
eF <- learnErrors("F.fq.gz", multithread = TRUE); eR <- learnErrors("R.fq.gz", multithread = TRUE)
dF <- dada("F.fq.gz", err = eF, pool = "pseudo"); dR <- dada("R.fq.gz", err = eR, pool = "pseudo")
st <- removeBimeraDenovo(makeSequenceTable(mergePairs(dF, "F.fq.gz", dR, "R.fq.gz", minOverlap = 12)),
                         method = "consensus", multithread = TRUE)

DeepTaxa does not require DADA2; any FASTA of 16S sequences works.