acp-classifier 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,20 @@
1
+ """acp-classifier — clasificación de péptidos anticancerígenos.
2
+
3
+ Universidad Industrial de Santander
4
+ Escuela de Ingeniería de Sistemas e Informática
5
+ Trabajo de grado, 2026
6
+ """
7
+
8
+ __version__ = "1.0.0"
9
+
10
+ from .fasta import ErrorFasta, Peptido, leer_fasta
11
+ from .predictor import AcpClassifier, Prediccion
12
+
13
+ __all__ = [
14
+ "AcpClassifier",
15
+ "Prediccion",
16
+ "Peptido",
17
+ "leer_fasta",
18
+ "ErrorFasta",
19
+ "__version__",
20
+ ]
acp_classifier/cli.py ADDED
@@ -0,0 +1,92 @@
1
+ """Interfaz de línea de comandos de acp-classifier."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import csv
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ from . import __version__
11
+ from .fasta import ErrorFasta
12
+
13
+ CAMPOS = ["identificador", "secuencia", "longitud", "probabilidad_acp", "prediccion"]
14
+
15
+
16
+ def construir_parser() -> argparse.ArgumentParser:
17
+ p = argparse.ArgumentParser(
18
+ prog="acp-classifier",
19
+ description="Clasificación de péptidos anticancerígenos (ACPs) a partir de "
20
+ "secuencias en formato FASTA, mediante embeddings de ESM-2 y "
21
+ "Regresión Logística.",
22
+ epilog="Ejemplo: acp-classifier -i peptidos.fasta -o resultados.csv",
23
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
24
+ )
25
+ p.add_argument("-i", "--input", required=True, metavar="FASTA",
26
+ help="archivo FASTA de entrada con una o varias secuencias")
27
+ p.add_argument("-o", "--output", metavar="CSV",
28
+ help="archivo CSV de salida (por defecto se imprime en pantalla)")
29
+ p.add_argument("-u", "--umbral", type=float, default=0.5, metavar="P",
30
+ help="probabilidad mínima para clasificar como ACP")
31
+ p.add_argument("-b", "--batch-size", type=int, default=32, metavar="N",
32
+ help="secuencias procesadas por lote")
33
+ p.add_argument("-m", "--modelo", metavar="RUTA",
34
+ help="artefacto .joblib alternativo (por defecto, el incluido)")
35
+ p.add_argument("-q", "--quiet", action="store_true",
36
+ help="suprime los mensajes de progreso")
37
+ p.add_argument("--version", action="version", version=f"acp-classifier {__version__}")
38
+ return p
39
+
40
+
41
+ def main(argv: list[str] | None = None) -> int:
42
+ args = construir_parser().parse_args(argv)
43
+
44
+ if not 0.0 < args.umbral < 1.0:
45
+ print("Error: el umbral debe estar entre 0 y 1 (exclusivos).", file=sys.stderr)
46
+ return 2
47
+
48
+ def log(mensaje: str) -> None:
49
+ if not args.quiet:
50
+ print(mensaje, file=sys.stderr)
51
+
52
+ try:
53
+ from .predictor import AcpClassifier
54
+
55
+ log("Cargando modelo...")
56
+ clf = AcpClassifier(args.modelo)
57
+
58
+ log(f"Leyendo {args.input}...")
59
+ predicciones = clf.predecir_fasta(args.input, args.umbral, args.batch_size)
60
+
61
+ except ErrorFasta as e:
62
+ print(f"Error de lectura: {e}", file=sys.stderr)
63
+ return 1
64
+ except FileNotFoundError as e:
65
+ print(f"Archivo no encontrado: {e}", file=sys.stderr)
66
+ return 1
67
+ except Exception as e: # noqa: BLE001 - la CLI no debe volcar trazas al usuario
68
+ print(f"Error: {type(e).__name__}: {e}", file=sys.stderr)
69
+ return 1
70
+
71
+ filas = [p.como_dict() for p in predicciones]
72
+
73
+ if args.output:
74
+ destino = Path(args.output)
75
+ destino.parent.mkdir(parents=True, exist_ok=True)
76
+ with destino.open("w", newline="", encoding="utf-8") as fh:
77
+ escritor = csv.DictWriter(fh, fieldnames=CAMPOS)
78
+ escritor.writeheader()
79
+ escritor.writerows(filas)
80
+ log(f"Resultados guardados en {destino}")
81
+ else:
82
+ escritor = csv.DictWriter(sys.stdout, fieldnames=CAMPOS)
83
+ escritor.writeheader()
84
+ escritor.writerows(filas)
85
+
86
+ n_acp = sum(f["prediccion"] == "ACP" for f in filas)
87
+ log(f"\n{len(filas)} secuencias analizadas: {n_acp} ACP, {len(filas) - n_acp} no-ACP")
88
+ return 0
89
+
90
+
91
+ if __name__ == "__main__":
92
+ raise SystemExit(main())
@@ -0,0 +1,90 @@
1
+ """Generación de embeddings con ESM-2 8M (esm2_t6_8M_UR50D).
2
+
3
+ El modelo se descarga de HuggingFace la primera vez (~31 MB) y queda en caché
4
+ local. No requiere GPU: sobre CPU procesa varios cientos de secuencias por minuto.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Iterable, Literal
10
+
11
+ import numpy as np
12
+
13
+ MODELO = "facebook/esm2_t6_8M_UR50D"
14
+ DIMENSIONES = 320
15
+
16
+ Pooling = Literal["tesis", "estricto"]
17
+
18
+
19
+ def _mascara_tesis(attention_mask):
20
+ """Enmascarado empleado para obtener los resultados reportados en el trabajo.
21
+
22
+ Excluye el token <cls> y la ÚLTIMA POSICIÓN CON ATENCIÓN CONTADA TRAS ANULAR <cls>,
23
+ que corresponde al último aminoácido y no al token <eos>. Se conserva tal cual
24
+ porque es el procedimiento con el que se entrenó y evaluó el modelo distribuido
25
+ (AUC-ROC de 0.8745 sobre el conjunto de prueba independiente de mACPpred 2.0).
26
+ """
27
+ mask = attention_mask.clone()
28
+ mask[:, 0] = 0
29
+ for j, longitud in enumerate(mask.sum(dim=1)):
30
+ if longitud > 1:
31
+ mask[j, longitud - 1] = 0
32
+ return mask
33
+
34
+
35
+ def _mascara_estricta(attention_mask):
36
+ """Excluye exactamente <cls> y <eos>, dejando solo los aminoácidos reales."""
37
+ mask = attention_mask.clone()
38
+ mask[:, 0] = 0
39
+ longitudes = attention_mask.sum(dim=1)
40
+ for j, longitud in enumerate(longitudes):
41
+ if longitud > 1:
42
+ mask[j, longitud - 1] = 0
43
+ return mask
44
+
45
+
46
+ class Esm2Embedder:
47
+ """Convierte secuencias peptídicas en vectores de 320 dimensiones."""
48
+
49
+ def __init__(self, pooling: Pooling = "tesis", dispositivo: str | None = None):
50
+ import torch
51
+ from transformers import AutoModel, AutoTokenizer
52
+
53
+ if pooling not in ("tesis", "estricto"):
54
+ raise ValueError(f"pooling debe ser 'tesis' o 'estricto', no {pooling!r}")
55
+
56
+ self._torch = torch
57
+ self.pooling = pooling
58
+ self.dispositivo = dispositivo or ("cuda" if torch.cuda.is_available() else "cpu")
59
+
60
+ self.tokenizer = AutoTokenizer.from_pretrained(MODELO)
61
+ self.model = AutoModel.from_pretrained(MODELO).to(self.dispositivo).eval()
62
+
63
+ def __call__(self, secuencias: Iterable[str], tamano_lote: int = 32) -> np.ndarray:
64
+ secuencias = list(secuencias)
65
+ if not secuencias:
66
+ return np.empty((0, DIMENSIONES), dtype=np.float32)
67
+
68
+ torch = self._torch
69
+ lotes: list[np.ndarray] = []
70
+
71
+ for i in range(0, len(secuencias), tamano_lote):
72
+ lote = secuencias[i : i + tamano_lote]
73
+ entradas = self.tokenizer(
74
+ lote, return_tensors="pt", padding=True, truncation=True, max_length=512
75
+ )
76
+ entradas = {k: v.to(self.dispositivo) for k, v in entradas.items()}
77
+
78
+ with torch.no_grad():
79
+ salidas = self.model(**entradas)
80
+
81
+ oculto = salidas.last_hidden_state
82
+ construir = _mascara_tesis if self.pooling == "tesis" else _mascara_estricta
83
+ mask = construir(entradas["attention_mask"])
84
+
85
+ expandida = mask.unsqueeze(-1).float()
86
+ suma = (oculto * expandida).sum(dim=1)
87
+ cuenta = expandida.sum(dim=1).clamp(min=1e-9)
88
+ lotes.append((suma / cuenta).cpu().numpy())
89
+
90
+ return np.vstack(lotes).astype(np.float32)
@@ -0,0 +1,96 @@
1
+ """Lectura y validación de archivos FASTA."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+
8
+ AMINOACIDOS = frozenset("ACDEFGHIKLMNPQRSTVWY")
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class Peptido:
13
+ """Una secuencia peptídica leída de un archivo FASTA."""
14
+
15
+ identificador: str
16
+ secuencia: str
17
+
18
+ @property
19
+ def longitud(self) -> int:
20
+ return len(self.secuencia)
21
+
22
+
23
+ class ErrorFasta(ValueError):
24
+ """El archivo FASTA no pudo interpretarse."""
25
+
26
+
27
+ def limpiar(secuencia: str) -> str:
28
+ """Deja únicamente los 20 aminoácidos estándar, en mayúsculas.
29
+
30
+ Los residuos no estándar (X, B, Z, U, O y cualquier otro carácter) se descartan.
31
+ Es el mismo preprocesamiento aplicado durante el entrenamiento del modelo.
32
+ """
33
+ return "".join(aa for aa in secuencia.upper() if aa in AMINOACIDOS)
34
+
35
+
36
+ def leer_fasta(ruta: str | Path, longitud_minima: int = 2) -> list[Peptido]:
37
+ """Lee un archivo FASTA y devuelve sus secuencias.
38
+
39
+ Admite registros repartidos en varias líneas. Las secuencias que quedan por
40
+ debajo de `longitud_minima` tras la limpieza se descartan con un aviso.
41
+
42
+ Raises
43
+ ------
44
+ ErrorFasta
45
+ Si el archivo no existe, está vacío o no contiene ninguna cabecera '>'.
46
+ """
47
+ ruta = Path(ruta)
48
+ if not ruta.is_file():
49
+ raise ErrorFasta(f"No existe el archivo: {ruta}")
50
+
51
+ peptidos: list[Peptido] = []
52
+ descartados: list[str] = []
53
+ identificador: str | None = None
54
+ partes: list[str] = []
55
+
56
+ def cerrar() -> None:
57
+ if identificador is None:
58
+ return
59
+ limpia = limpiar("".join(partes))
60
+ if len(limpia) >= longitud_minima:
61
+ peptidos.append(Peptido(identificador, limpia))
62
+ else:
63
+ descartados.append(identificador)
64
+
65
+ with ruta.open(encoding="utf-8", errors="replace") as fh:
66
+ for linea in fh:
67
+ linea = linea.strip()
68
+ if not linea:
69
+ continue
70
+ if linea.startswith(">"):
71
+ cerrar()
72
+ identificador = linea[1:].strip() or f"secuencia_{len(peptidos) + 1}"
73
+ partes = []
74
+ else:
75
+ partes.append(linea)
76
+ cerrar()
77
+
78
+ if identificador is None:
79
+ raise ErrorFasta(
80
+ f"{ruta} no contiene ninguna cabecera FASTA ('>'). "
81
+ "Verifique que el archivo esté en formato FASTA."
82
+ )
83
+ if not peptidos:
84
+ raise ErrorFasta(
85
+ f"{ruta} no contiene ninguna secuencia válida de al menos "
86
+ f"{longitud_minima} aminoácidos estándar."
87
+ )
88
+ if descartados:
89
+ print(
90
+ f" Aviso: {len(descartados)} secuencia(s) descartada(s) por quedar "
91
+ f"con menos de {longitud_minima} aminoácidos estándar: "
92
+ + ", ".join(descartados[:5])
93
+ + (" ..." if len(descartados) > 5 else "")
94
+ )
95
+
96
+ return peptidos
@@ -0,0 +1 @@
1
+ """Artefactos serializados del modelo entrenado."""
@@ -0,0 +1,121 @@
1
+ """Clasificador de péptidos anticancerígenos."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, asdict
6
+ from importlib import resources
7
+ from pathlib import Path
8
+
9
+ import joblib
10
+ import numpy as np
11
+
12
+ from .embedder import Esm2Embedder
13
+ from .fasta import Peptido, leer_fasta
14
+
15
+ UMBRAL_POR_DEFECTO = 0.5
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class Prediccion:
20
+ identificador: str
21
+ secuencia: str
22
+ longitud: int
23
+ probabilidad_acp: float
24
+ prediccion: str
25
+
26
+ def como_dict(self) -> dict:
27
+ return asdict(self)
28
+
29
+
30
+ def _probabilidades_desde_pesos(X: np.ndarray, pesos: dict) -> np.ndarray:
31
+ """Estandariza y aplica la regresión logística a partir de los pesos.
32
+
33
+ Equivale a `LogisticRegression.predict_proba` sobre `StandardScaler.transform`,
34
+ pero sin instanciar objetos de scikit-learn, de modo que la predicción no
35
+ depende de la versión de la biblioteca que haya instalada.
36
+ """
37
+ Z = (np.asarray(X, dtype=np.float64) - pesos["media"]) / pesos["escala"]
38
+ z = Z @ pesos["coef"] + pesos["intercepto"]
39
+ return 1.0 / (1.0 + np.exp(-z))
40
+
41
+
42
+ class AcpClassifier:
43
+ """Predice actividad anticancerígena a partir de secuencias peptídicas.
44
+
45
+ Combina embeddings de ESM-2 8M con un clasificador de Regresión Logística,
46
+ seleccionado por obtener el mayor AUC-ROC sobre el conjunto de prueba
47
+ independiente de mACPpred 2.0 (Sangaraju et al., 2024).
48
+
49
+ Ejemplo
50
+ -------
51
+ >>> from acp_classifier import AcpClassifier
52
+ >>> clf = AcpClassifier()
53
+ >>> clf.predecir_secuencias(["KWKLFKKIEKVGQNIRDGIIKAGPAVAVVGQATQIAK"])
54
+ """
55
+
56
+ def __init__(self, ruta_modelo: str | Path | None = None):
57
+ artefacto = self._cargar_artefacto(ruta_modelo)
58
+ self.metadatos = artefacto["metadatos"]
59
+ self._pesos = artefacto.get("pesos")
60
+ # Compatibilidad con artefactos del formato antiguo, que guardaban los
61
+ # objetos de scikit-learn serializados.
62
+ self._modelo = artefacto.get("modelo")
63
+ self._scaler = artefacto.get("scaler")
64
+ if self._pesos is None and self._modelo is None:
65
+ raise ValueError(
66
+ "El artefacto no contiene ni 'pesos' ni 'modelo'; no es un modelo "
67
+ "de acp-classifier."
68
+ )
69
+ self._embedder: Esm2Embedder | None = None
70
+
71
+ @staticmethod
72
+ def _cargar_artefacto(ruta_modelo):
73
+ if ruta_modelo is not None:
74
+ return joblib.load(Path(ruta_modelo))
75
+ with resources.as_file(
76
+ resources.files("acp_classifier.modelos") / "acp_esm2_8m_lr.joblib"
77
+ ) as p:
78
+ return joblib.load(p)
79
+
80
+ @property
81
+ def embedder(self) -> Esm2Embedder:
82
+ """El modelo de lenguaje se carga de forma diferida, en el primer uso."""
83
+ if self._embedder is None:
84
+ self._embedder = Esm2Embedder(pooling=self.metadatos["pooling"])
85
+ return self._embedder
86
+
87
+ def probabilidades(self, secuencias, tamano_lote: int = 32) -> np.ndarray:
88
+ X = self.embedder(secuencias, tamano_lote=tamano_lote)
89
+ if self._pesos is not None:
90
+ return _probabilidades_desde_pesos(X, self._pesos)
91
+ return self._modelo.predict_proba(self._scaler.transform(X))[:, 1]
92
+
93
+ def predecir_secuencias(
94
+ self,
95
+ secuencias,
96
+ umbral: float = UMBRAL_POR_DEFECTO,
97
+ tamano_lote: int = 32,
98
+ ) -> list[Prediccion]:
99
+ peptidos = [
100
+ p if isinstance(p, Peptido) else Peptido(f"secuencia_{i + 1}", p)
101
+ for i, p in enumerate(secuencias)
102
+ ]
103
+ probas = self.probabilidades([p.secuencia for p in peptidos], tamano_lote)
104
+ return [
105
+ Prediccion(
106
+ identificador=p.identificador,
107
+ secuencia=p.secuencia,
108
+ longitud=p.longitud,
109
+ probabilidad_acp=round(float(pr), 4),
110
+ prediccion="ACP" if pr >= umbral else "no-ACP",
111
+ )
112
+ for p, pr in zip(peptidos, probas)
113
+ ]
114
+
115
+ def predecir_fasta(
116
+ self,
117
+ ruta_fasta: str | Path,
118
+ umbral: float = UMBRAL_POR_DEFECTO,
119
+ tamano_lote: int = 32,
120
+ ) -> list[Prediccion]:
121
+ return self.predecir_secuencias(leer_fasta(ruta_fasta), umbral, tamano_lote)
@@ -0,0 +1,245 @@
1
+ Metadata-Version: 2.4
2
+ Name: acp-classifier
3
+ Version: 1.0.0
4
+ Summary: Clasificación de péptidos anticancerígenos (ACPs) mediante modelos de lenguaje proteico
5
+ Author: Brayan Arturo Navarro Sinuco, Ivan Andres Mendoza Oñate
6
+ Maintainer: Escuela de Ingeniería de Sistemas e Informática, Universidad Industrial de Santander
7
+ License-Expression: MIT
8
+ Project-URL: Homepage, https://github.com/ivan2171977/acp-classifier
9
+ Project-URL: Repository, https://github.com/ivan2171977/acp-classifier
10
+ Project-URL: Issues, https://github.com/ivan2171977/acp-classifier/issues
11
+ Keywords: bioinformatics,anticancer-peptides,acp,protein-language-model,esm2,peptide-classification,machine-learning
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
21
+ Requires-Python: >=3.9
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: numpy>=1.21
25
+ Requires-Dist: joblib>=1.1
26
+ Requires-Dist: torch>=1.11
27
+ Requires-Dist: transformers>=4.20
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest>=7.0; extra == "dev"
30
+ Requires-Dist: scikit-learn>=1.0; extra == "dev"
31
+ Dynamic: license-file
32
+
33
+ # acp-classifier
34
+
35
+ Clasificación de péptidos anticancerígenos (ACPs) a partir de secuencias en formato FASTA,
36
+ mediante representaciones contextuales generadas por el modelo de lenguaje proteico
37
+ **ESM-2** y un clasificador de **Regresión Logística**.
38
+
39
+ Desarrollado como trabajo de grado en la Escuela de Ingeniería de Sistemas e Informática de
40
+ la **Universidad Industrial de Santander**.
41
+
42
+ ---
43
+
44
+ ## Por qué existe
45
+
46
+ Las herramientas de referencia para predicción de ACPs —AntiCP 2.0, MLACP 2.0, mACPpred 2.0,
47
+ ACP-EPC— están disponibles únicamente como servidores web, o como repositorios sin empaquetar.
48
+ Ninguna se instala mediante gestores estándar, lo que dificulta su integración en flujos de
49
+ trabajo bioinformáticos automatizados.
50
+
51
+ `acp-classifier` se instala con `pip`, se ejecuta desde consola y no requiere GPU.
52
+
53
+ ## Desempeño
54
+
55
+ Evaluado sobre el **conjunto de prueba independiente oficial** de mACPpred 2.0
56
+ (610 ACPs y 2.760 no-ACPs), sin haber visto ninguna de esas secuencias durante el entrenamiento:
57
+
58
+ | Métrica | Valor |
59
+ |---|---|
60
+ | AUC-ROC | **0.8745** |
61
+ | F1-score | 0.6230 |
62
+ | Exactitud | 0.8362 |
63
+ | MCC | 0.5341 |
64
+
65
+ Como referencia, sobre ese mismo conjunto mACPpred 2.0 reporta un AUC-ROC de 0.887 y
66
+ MLACP 2.0 uno de 0.893. Este paquete no supera a ninguna de las dos, pero sí a las otras
67
+ nueve herramientas con AUC-ROC publicado sobre el conjunto —entre ellas mACPpred (0.842),
68
+ iDACP (0.852) y AntiCP 2.0 en su variante alterna (0.835)—, y lo hace con un modelo de ocho
69
+ millones de parámetros y un clasificador lineal, sin GPU en inferencia.
70
+
71
+ > El conjunto de evaluación está desbalanceado (18,1 % de positivos). Por ese motivo se
72
+ > reporta AUC-ROC como métrica principal, junto con F1 y MCC, y no la exactitud: un
73
+ > clasificador que asignara siempre la clase mayoritaria obtendría un 81,9 % de exactitud
74
+ > sin haber aprendido nada.
75
+
76
+ ## Instalación
77
+
78
+ ```bash
79
+ pip install acp-classifier
80
+ ```
81
+
82
+ Desde el código fuente:
83
+
84
+ ```bash
85
+ git clone https://github.com/ivan2171977/acp-classifier.git
86
+ cd acp-classifier
87
+ pip install -e .
88
+ ```
89
+
90
+ Con Conda:
91
+
92
+ ```bash
93
+ conda env create -f environment.yml
94
+ conda activate acp-classifier
95
+ ```
96
+
97
+ Requiere Python 3.9 o superior. La primera ejecución descarga el modelo ESM-2 8M
98
+ (~31 MB) desde HuggingFace y lo deja en caché local.
99
+
100
+ ## Uso desde consola
101
+
102
+ ```bash
103
+ # Resultados por pantalla
104
+ acp-classifier -i peptidos.fasta
105
+
106
+ # Resultados a un archivo CSV
107
+ acp-classifier -i peptidos.fasta -o resultados.csv
108
+
109
+ # Umbral de decisión más exigente
110
+ acp-classifier -i peptidos.fasta -o resultados.csv --umbral 0.8
111
+ ```
112
+
113
+ Opciones:
114
+
115
+ | Opción | Descripción | Por defecto |
116
+ |---|---|---|
117
+ | `-i`, `--input` | Archivo FASTA de entrada (obligatorio) | — |
118
+ | `-o`, `--output` | Archivo CSV de salida | pantalla |
119
+ | `-u`, `--umbral` | Probabilidad mínima para clasificar como ACP | `0.5` |
120
+ | `-b`, `--batch-size` | Secuencias procesadas por lote | `32` |
121
+ | `-m`, `--modelo` | Artefacto `.joblib` alternativo | el incluido |
122
+ | `-q`, `--quiet` | Suprime los mensajes de progreso | — |
123
+
124
+ ### Entrada
125
+
126
+ Un archivo FASTA estándar, con una o varias secuencias. Se admiten registros repartidos en
127
+ varias líneas. Los residuos no estándar (`X`, `B`, `Z`, `U`, `O`) se descartan, igual que
128
+ durante el entrenamiento.
129
+
130
+ ```fasta
131
+ >peptido_1
132
+ GLFSVVTGVLKAVGKNVAKNVGGSLLEQLKCKKISGGC
133
+ >peptido_2
134
+ NNEQIRRFSQVLLDAGIVTTIRKTRGDDIDA
135
+ ```
136
+
137
+ ### Salida
138
+
139
+ ```csv
140
+ identificador,secuencia,longitud,probabilidad_acp,prediccion
141
+ peptido_1,GLFSVVTGVLKAVGKNVAKNVGGSLLEQLKCKKISGGC,38,0.7350,ACP
142
+ peptido_2,NNEQIRRFSQVLLDAGIVTTIRKTRGDDIDA,31,0.0643,no-ACP
143
+ ```
144
+
145
+ ## Uso desde Python
146
+
147
+ ```python
148
+ from acp_classifier import AcpClassifier
149
+
150
+ clf = AcpClassifier()
151
+
152
+ # Desde secuencias
153
+ for p in clf.predecir_secuencias(["GIGKFLHSAKKFGKAFVGEIMNS"]):
154
+ print(p.identificador, p.probabilidad_acp, p.prediccion)
155
+
156
+ # Desde un archivo FASTA
157
+ predicciones = clf.predecir_fasta("peptidos.fasta", umbral=0.7)
158
+
159
+ # Solo las probabilidades, como arreglo de NumPy
160
+ probas = clf.probabilidades(["GIGKFLHSAKKFGKAFVGEIMNS", "AAAAAAAAAA"])
161
+ ```
162
+
163
+ ## Cómo funciona
164
+
165
+ ```
166
+ FASTA → limpieza → ESM-2 8M → mean pooling → StandardScaler → Regresión Logística → CSV
167
+ (20 aa) (320 dim) (C=0.01, saga)
168
+ ```
169
+
170
+ 1. **Lectura y limpieza.** Se conservan únicamente los 20 aminoácidos estándar.
171
+ 2. **Embeddings.** `esm2_t6_8M_UR50D` genera un vector de 320 dimensiones por secuencia,
172
+ promediando los estados ocultos de la última capa.
173
+ 3. **Normalización.** `StandardScaler` ajustado sobre el conjunto de entrenamiento.
174
+ 4. **Clasificación.** Regresión Logística (`C=0.01`, solver `saga`), entrenada sobre las
175
+ 2.352 secuencias de `Training.fasta` de mACPpred 2.0.
176
+
177
+ ### Por qué el modelo más pequeño
178
+
179
+ Durante el trabajo se evaluaron ESM-2 8M, ESM-2 150M y ProtBERT. **ESM-2 8M obtuvo el mejor
180
+ desempeño**, pese a ser el de menor capacidad. Los péptidos del conjunto de evaluación tienen
181
+ una longitud mediana de 9 residuos: sobre secuencias tan cortas, las representaciones de mayor
182
+ dimensionalidad no aportan poder discriminante y favorecen el sobreajuste.
183
+
184
+ Esto tiene una consecuencia práctica: el modelo con mejor desempeño es también el de menores
185
+ requisitos (31 MB frente a los 1,7 GB de ProtBERT), lo que permite ejecutarlo sin GPU.
186
+
187
+ ## Reproducir el entrenamiento
188
+
189
+ ```bash
190
+ python entrenar_modelo.py
191
+ ```
192
+
193
+ Regenera el artefacto desde los FASTA de mACPpred 2.0, compara las dos estrategias de
194
+ agregación implementadas e imprime las métricas sobre el conjunto de prueba independiente.
195
+ Toma alrededor de un minuto sobre CPU.
196
+
197
+ ## Pruebas
198
+
199
+ ```bash
200
+ pip install -e ".[dev]"
201
+ pytest -q
202
+ ```
203
+
204
+ ## Limitaciones
205
+
206
+ - La validación es **exclusivamente computacional** (TRL 4). Una predicción positiva es una
207
+ hipótesis a verificar experimentalmente, no una afirmación de actividad biológica.
208
+ - El modelo fue entrenado sobre péptidos de entre 6 y 50 residuos. Su comportamiento fuera de
209
+ ese rango no ha sido caracterizado.
210
+ - Existe un desplazamiento de distribución conocido entre los conjuntos de entrenamiento y
211
+ prueba del benchmark: los ACPs de entrenamiento tienen una longitud mediana de 20 residuos
212
+ frente a 9 en el conjunto de prueba.
213
+ - El desempeño está caracterizado sobre un único benchmark (mACPpred 2.0).
214
+
215
+ ## Datos
216
+
217
+ Conjunto de referencia de **mACPpred 2.0** (Sangaraju et al., 2024): 1.176 ACPs y 1.176
218
+ no-ACPs para entrenamiento; 610 ACPs y 2.760 no-ACPs para prueba independiente.
219
+
220
+ ## Citación
221
+
222
+ ```bibtex
223
+ @software{acp_classifier_2026,
224
+ title = {acp-classifier: clasificación de péptidos anticancerígenos
225
+ mediante modelos de lenguaje proteico},
226
+ author = {Navarro Sinuco, Brayan Arturo and Mendoza Oñate, Ivan Andres},
227
+ year = {2026},
228
+ school = {Universidad Industrial de Santander},
229
+ url = {https://github.com/ivan2171977/acp-classifier}
230
+ }
231
+ ```
232
+
233
+ ### Referencias
234
+
235
+ - Lin, Z. et al. (2023). Evolutionary-scale prediction of atomic level protein structure with
236
+ a language model. *Science*, 379(6637), 1123–1130.
237
+ - Sangaraju, V. K. et al. (2024). mACPpred 2.0: Stacked deep learning for anticancer peptide
238
+ prediction. *Journal of Molecular Biology*, 436(17), 168687.
239
+ - Pedregosa, F. et al. (2011). Scikit-learn: Machine learning in Python. *JMLR*, 12, 2825–2830.
240
+ - Wolf, T. et al. (2020). Transformers: State-of-the-art natural language processing. *EMNLP:
241
+ System Demonstrations*, 38–45.
242
+
243
+ ## Licencia
244
+
245
+ MIT. Véase [LICENSE](LICENSE).
@@ -0,0 +1,13 @@
1
+ acp_classifier/__init__.py,sha256=Oi-_37SVHkJ6VuQpMRr-A5QCAnxJ9W33Myk-ZYTjb4o,431
2
+ acp_classifier/cli.py,sha256=oU0MPWHmwu2cK9uYoU3-afDWCKZjgGLtKfic12NcZxw,3450
3
+ acp_classifier/embedder.py,sha256=LEQW9sG4ApIckzE4vfrWzsZUzghWb7CYR1wsxTOUrtk,3279
4
+ acp_classifier/fasta.py,sha256=HeXl_4Q6fJXP0VLVEAD0ObN_-vwyhX9541-QsilEqgs,2915
5
+ acp_classifier/predictor.py,sha256=ud24i2N8eMOfXRL-T8Xr5GHgUscs0sqWPZTmOybYE9I,4215
6
+ acp_classifier/modelos/__init__.py,sha256=VPux3gRT8nFEnrYPwgaGxgICm1-m2RGXlKhi_WTIE3o,53
7
+ acp_classifier/modelos/acp_esm2_8m_lr.joblib,sha256=R8JO0rcBcHrnuwip2jXzoU0rQgswFXsID8GlVIj5yXI,7490
8
+ acp_classifier-1.0.0.dist-info/licenses/LICENSE,sha256=oGK-94ZeLM9pMXaJDOfOz_RuPtvzLtt6VEutMKKCGME,1203
9
+ acp_classifier-1.0.0.dist-info/METADATA,sha256=68ya-vv9t7Gcs94lCxpBSTdB0fL9yj-yOFig3Xgali8,9139
10
+ acp_classifier-1.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
11
+ acp_classifier-1.0.0.dist-info/entry_points.txt,sha256=mX3u0390RnTaGWYNUoltRVzsVggYKPynIWeXcRgQP-Y,59
12
+ acp_classifier-1.0.0.dist-info/top_level.txt,sha256=iFfnZHoL3SW6mGOLqPhZG72J54MwwP4zW74H3Esqs5A,15
13
+ acp_classifier-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ acp-classifier = acp_classifier.cli:main
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Brayan Arturo Navarro Sinuco, Ivan Andres Mendoza Oñate
4
+ Universidad Industrial de Santander — Escuela de Ingeniería de Sistemas e Informática
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ acp_classifier