タンパク質配列の埋め込みとFAISS検索:BLASTでは見逃されていた遠縁ホモログを基盤モデルで捉える
BLASTは、タンパク質のホモロジー検索における標準的な手法として30年間利用されてきましたが、配列の類似度が30%を下回ると、その性能は著しく低下します。しかし、実際には、進化的に関連しているものの、配列が大きく異なる遠縁ホモログが数多く存在します。これらのホモログを特定できないと、新しい遺伝子機能の予測が妨げられる可能性があります。本記事では、タンパク質基盤モデル(ESM3)によって学習された潜在空間を用いて、この問題を解決するための実用的なパイプラインを構築し、FAISSを活用して、大規模な配列データベースに対してリアルタイムで高スループットな検索機能を提供します。
📚 推奨される予備知識(強く推奨されます)
本記事は、AI×生物学の概念に関する高度で詳細な解説です。続行する前に、以下のDryBenchの記事を読み直すことを強くお勧めします。
- DryBench ai-native #3 TransformersとEmbeddings
- DryBench ai-native #5 Attention Mechanism
- DryBench ai-native #13 HuggingFaceと商用API
予備知識を復習せずに本記事を読み進めると、埋め込みベクトルの意味、アテンションの持つ情報量、HuggingFaceモデルの読み込み方法について、再説明なしに進むため、理解が難しくなる可能性があります。
DryBenchで以前に学習した内容
DryBench ai-native #3では、Transformerがトークン間の関係を学習し、各トークンをコンテキストを考慮した高次元ベクトルとして表現することを学びました。#5では、アテンションが、配列内の任意の2つの位置が互いにどれほど重要であるかを学習し、長距離の依存関係を捉えることを学びました。#13では、HuggingFaceが、これらのモデルの重み、トークナイザー、推論APIを標準化するエコシステムを提供し、わずかなコードでモデルを読み込めることを学びました。
しかし、これらの3つの概念をタンパク質ドメインに適用するとどうなるでしょうか? 自然言語とは異なり、タンパク質は30億年もの間、進化の圧力によって洗練されてきました。コンテキスト(隣接するアミノ酸)の情報は、タンパク質の3次元のフォールド構造を決定します。Meta AIのESMシリーズ(→EvolutionaryScaleのスピンアウト)は、Transformerを用いてこの進化情報を学習する、最初の大規模なタンパク質基盤モデルであり、いくつかの論文で、学習された埋め込みが、タンパク質の3次元構造、機能、安定性を含んでいる可能性があることが確認されています[1][2][5]。本記事では、これらの埋め込みを、実用的で有用な検索ツールに変換するパイプラインを紹介します。
難しい問題を定義する
BLASTの根本的な限界
BLAST(Basic Local Alignment Search Tool、1990年)は、置換行列とギャップペナルティを用いて配列の類似性をスコアリングし、統計的に有意なヒットを返します。このアプローチは、配列の類似度が30〜40%以上の近縁ホモログに対しては、圧倒的に正確ですが、以下の2つのシナリオでは限界があります。
- 遠縁ホモログ: 進化的に関連しているものの、配列の同一性が20〜30%の場合。3次元構造と機能は依然として保存されていますが、BLASTでは統計的な有意性が失われます。20〜35%の範囲は「トワイライトゾーン」として知られており、特に難しい問題であり、20%未満のゾーンでは、事実上検出できません。
- マルチドメインタンパク質における特定のドメインの一致: BLASTは、配列全体のローカルアライメントを見つけますが、全体的なアライメントスコアが低い場合、強くホモロジーを持つ個々のドメインを見逃す可能性があります。
従来の回避策には、PSI-BLAST(反復プロファイル)、HHblits(HMM-HMM)、Foldseek(構造ベース)などがありますが、それぞれ計算コストとデータ依存性の点で限界があります。
埋め込みアプローチが解決するもの
ESM3のような基盤モデルは、学習プロセス中に進化的なカップリングを潜在空間に吸収します。その結果、配列の類似度が低い場合でも、構造と機能が類似していれば、埋め込みはベクトル空間で近くなります。この特性を活用することで、以下が可能になります。
- 文献[2][3]で示されているように、埋め込みKNN検索を用いて、BLASTでは見逃されていた遠縁ホモログを検索できます。
- 配列の埋め込みが計算されると、その後のすべての検索は、ベクトル類似度の計算のみを必要とし、BLASTのペアワイズアライメントと比較して、大規模なデータセットでは有利です。
- 関数転送(例:GOターム)は、検索結果のトップk個の最近傍のラベルの多数決によって、瞬時に予測できます。
本記事の目標指標
- UniProt Swiss-Protの50万個の配列のサブセットに対して、埋め込みインデックスを構築します。
- 1,000個のクエリ配列に対する遠縁ホモログの検出率:BLASTと比較して、リコールを少なくとも25%向上させます(CAFAベンチマーク[4]に基づく)。
- 平均検索レイテンシー:100ms未満/クエリ(FAISS HNSWの調整)。
- GOターム関数転送F1:0.65以上(CAFA-3トップレベル)。
ツール、スタック、およびインフラストラクチャ要件
| ツール | 役割 | ライセンス |
|---|---|---|
ESM3 (EvolutionaryScale) esm3-sm-open-v1 (1.4B) | タンパク質配列の埋め込みを抽出 | 学術/非営利目的でのみオープン。商用利用には別途契約が必要です。 |
ESM2 esm2_t33_650M_UR50D (代替ベースライン) | ESM3にアクセスできない場合に代替として使用 | MIT |
HuggingFace transformers | モデルのロードと推論 | Apache 2.0 |
| FAISS (Facebook AI Similarity Search) | 高次元ベクトルKNNインデックス | MIT |
| Biopython | FASTAファイルの解析と配列操作 | Biopython License |
| UniProt Swiss-Prot | 厳選されたタンパク質配列とGOラベル | CC BY 4.0 |
| Chroma (オプション) | ベクトルDBの代替(メタデータフィルタリング) | Apache 2.0 |
インフラストラクチャ要件:
- 埋め込み抽出ステップ: 小規模なコンシューマーGPU(RTX 4060 8GB以上)。ESM3 1.4Bはfp16で約3GBのVRAMを消費し、推奨されるバッチサイズは4〜8です。
- FAISSインデックスの構築と検索: CPUで十分です。16GB以上のRAM(500,000配列×1024次元×float32 = 約2GB)。
- ディスク: 圧縮されたUniProt Swiss-Protは約200MB、埋め込みキャッシュは約2〜4GBです。
学習者のための概算コスト: APIコストは0(ローカル実行)。GPU時間は約2〜4時間(500,000配列の埋め込み、RTX 4060に基づいて推定)。データダウンロードは約200MBです。
実用的なパイプラインの実装
全体の流れ:
ステップ1:UniProt Swiss-Protのダウンロードとパース
Swiss-ProtはUniProtの手動キュレーションされたセクションであり、そのGOアノテーションは信頼性の高いラベルです。
import gzipfrom pathlib import Pathfrom typing import Iterator, NamedTuple
import requestsfrom Bio import SeqIO
SWISSPROT_URL = "https://ftp.uniprot.org/pub/databases/uniprot/current_release/knowledgebase/complete/uniprot_sprot.fasta.gz"
class ProteinRecord(NamedTuple): """タンパク質配列のレコード。""" accession: str sequence: str description: str go_terms: list[str]
def download_swissprot(dest: Path) -> Path: """UniProt Swiss-Prot FASTAをダウンロードします。""" dest.parent.mkdir(parents=True, exist_ok=True) if dest.exists(): return dest with requests.get(SWISSPROT_URL, stream=True, timeout=300) as resp: resp.raise_for_status() with open(dest, "wb") as f: for chunk in resp.iter_content(chunk_size=8192): f.write(chunk) return dest
def parse_swissprot(fasta_gz: Path, max_len: int = 1024) -> Iterator[ProteinRecord]: """FASTAをストリーミングでパースします。max_lenよりも長いシーケンス(ESMコンテキスト制限)はスキップします。""" with gzip.open(fasta_gz, "rt") as f: for record in SeqIO.parse(f, "fasta"): seq = str(record.seq).upper() if len(seq) > max_len or len(seq) < 30: continue # UniProt FASTAの説明からアクセッションを抽出します(例:sp|P12345|GENE_ORG) parts = record.id.split("|") accession = parts[1] if len(parts) >= 2 else record.id yield ProteinRecord( accession=accession, sequence=seq, description=record.description, go_terms=[], # GO用語は別のソースからマージされます。次のステップを参照してください。 )GOアノテーションは、別のファイル(goa_uniprot_all.gaf.gz [6])からマージされます。実際には、SQLiteまたはDuckDBを使用して結合すると便利です。
import sqlite3from collections import defaultdict
def build_go_index(gaf_path: Path, db_path: Path) -> None: """GAF(GOアノテーションファイル)を、アクセッション→[GO ID]マッピングとしてインデックス化します。""" conn = sqlite3.connect(db_path) conn.execute("CREATE TABLE IF NOT EXISTS go (accession TEXT, go_id TEXT)") conn.execute("CREATE INDEX IF NOT EXISTS idx_acc ON go(accession)") with gzip.open(gaf_path, "rt") as f: batch = [] for line in f: if line.startswith("!"): continue fields = line.split("\t") if len(fields) < 5: continue batch.append((fields[1], fields[4])) # DB_Object_ID, GO_ID if len(batch) >= 10000: conn.executemany("INSERT INTO go VALUES (?, ?)", batch) batch.clear() if batch: conn.executemany("INSERT INTO go VALUES (?, ?)", batch) conn.commit() conn.close()
def lookup_go_terms(accession: str, db_path: Path) -> list[str]: conn = sqlite3.connect(db_path) cursor = conn.execute("SELECT go_id FROM go WHERE accession = ?", (accession,)) terms = [row[0] for row in cursor] conn.close() return termsステップ2:ESM3埋め込みの抽出
ESM3 Small Open Weights(1.4B、esm3-sm-open-v1)は、配列、構造、機能の3トラックのマスクされた言語モデルです。シーケンスのみを入力した場合でも、シーケンストラックの最後のレイヤーの隠れ状態は、意味のある表現を提供します。
import torchfrom esm.models.esm3 import ESM3from esm.sdk.api import ESMProtein
class ESM3Embedder: """ESM3シーケンス埋め込み抽出ラッパー。"""
def __init__(self, device: str = "cuda"): self.device = device self.model = ESM3.from_pretrained("esm3-sm-open-v1").to(device).eval()
@torch.no_grad() def embed(self, sequence: str) -> torch.Tensor: """単一のシーケンスに対して、シーケンストラックの最後の隠れ状態を抽出します。
戻り値:形状(hidden_dim,)のベクトル。1.4Bモデルの場合、hidden_dim=1536。 """ protein = ESMProtein(sequence=sequence) encoded = self.model.encode(protein) # シーケンストラックの最後の隠れ状態を抽出します output = self.model.forward(sequence_tokens=encoded.sequence.unsqueeze(0).to(self.device)) # (1, seq_len, hidden_dim) → 平均プーリング → (hidden_dim,) return output.embeddings.squeeze(0).mean(dim=0).cpu()
def embed_batch(self, sequences: list[str], batch_size: int = 4) -> torch.Tensor: """シーケンスのバッチを処理します。バッチサイズを調整して、VRAM制限に合わせます。""" embeddings = [] for i in range(0, len(sequences), batch_size): chunk = sequences[i:i + batch_size] for seq in chunk: embeddings.append(self.embed(seq)) return torch.stack(embeddings)ESM2の代替手段(ESM3へのアクセスが難しい場合、MITライセンスにより完全な自由が許可されます):
from transformers import AutoTokenizer, AutoModel
class ESM2Embedder: """ESM2ベースライン。hidden_dim=1280(t33_650M)。"""
MODEL_ID = "facebook/esm2_t33_650M_UR50D"
def __init__(self, device: str = "cuda"): self.device = device self.tokenizer = AutoTokenizer.from_pretrained(self.MODEL_ID) self.model = AutoModel.from_pretrained(self.MODEL_ID).to(device).eval()
@torch.no_grad() def embed(self, sequence: str) -> torch.Tensor: inputs = self.tokenizer(sequence, return_tensors="pt", truncation=True, max_length=1024).to(self.device) outputs = self.model(**inputs) # last_hidden_state: (1, seq_len, 1280) → 平均プーリング → (1280,) return outputs.last_hidden_state.squeeze(0).mean(dim=0).cpu()ステップ3:FAISSインデックス構築– Flat vs IVF vs HNSWチューニング
FAISSには、それぞれ異なる精度-速度-メモリのトレードオフを持つ3つの代表的なインデックスタイプがあります。
- IndexFlatIP: 完全検索。精度100%、速度O(N)、メモリO(N×d)。
- IndexIVFFlat: クラスタベースの近似。
nlist(クラスタ数)、nprobe(検索中にアクセスするクラスタ数)を調整します。精度95〜99%、速度O(nprobe / nlist×N)。 - IndexHNSWFlat: グラフベースの近似。
M(グラフの隣接ノード数)、efConstruction、efSearchを調整します。精度98%以上、非常に高速、メモリはやや大きい。
import faissimport numpy as np
def build_flat_index(embeddings: np.ndarray) -> faiss.Index: """精度100%、ベンチマーク用。""" d = embeddings.shape[1] index = faiss.IndexFlatIP(d) # 内積(埋め込みの正規化後のコサイン類似度と同等) faiss.normalize_L2(embeddings) index.add(embeddings) return index
def build_ivf_index(embeddings: np.ndarray, nlist: int = 4096) -> faiss.Index: """IVF:nlist=sqrt(N)が経験的な最適値です。推奨nlist〜700(シーケンス数50万)。""" d = embeddings.shape[1] quantizer = faiss.IndexFlatIP(d) index = faiss.IndexIVFFlat(quantizer, d, nlist, faiss.METRIC_INNER_PRODUCT) faiss.normalize_L2(embeddings) index.train(embeddings) index.add(embeddings) return index
def build_hnsw_index(embeddings: np.ndarray, M: int = 32) -> faiss.Index: """HNSW:M=16〜64、大きいほど精度↑、メモリ↑。efConstruction=200が標準。""" d = embeddings.shape[1] index = faiss.IndexHNSWFlat(d, M, faiss.METRIC_INNER_PRODUCT) index.hnsw.efConstruction = 200 faiss.normalize_L2(embeddings) index.add(embeddings) return indexステップ4:クエリシーケンスKNN検索
def search( index: faiss.Index, query_embedding: np.ndarray, k: int = 10, ef_search: int | None = None, nprobe: int | None = None,) -> tuple[np.ndarray, np.ndarray]: """上位kの近傍検索。HNSWのefSearch、IVFのnprobeは、検索中に調整できます。""" if ef_search is not None and hasattr(index, "hnsw"): index.hnsw.efSearch = ef_search if nprobe is not None and hasattr(index, "nprobe"): index.nprobe = nprobe faiss.normalize_L2(query_embedding.reshape(1, -1)) distances, indices = index.search(query_embedding.reshape(1, -1), k) return distances[0], indices[0]ステップ5:機能転送–GO用語の多数決
上位kの近傍のGOアノテーションを集約し、クエリシーケンスのGO用語を予測します。
from collections import Counter
def transfer_go_annotations( neighbor_accessions: list[str], neighbor_distances: list[float], db_path: Path, min_votes: int = 2, distance_threshold: float = 0.75,) -> dict[str, float]: """近傍のGO用語を、距離重み付きの多数決で集約します。
戻り値:{go_id: 信頼度スコア}の辞書。 """ weighted_votes: Counter = Counter() total_weight = 0.0 for acc, dist in zip(neighbor_accessions, neighbor_distances): if dist < distance_threshold: continue weight = dist # コサイン類似度自体を重みとして使用します total_weight += weight for go_id in lookup_go_terms(acc, db_path): weighted_votes[go_id] += weight if total_weight == 0: return {} # 少なくともmin_votes回出現したGO用語のみを返します return { go_id: score / total_weight for go_id, score in weighted_votes.items() if score >= min_votes * (total_weight / len(neighbor_accessions)) }ステップ6:BLAST比較ベンチマーク(PR曲線)
遠い相同性検出を比較するためのゴールドスタンダードは、SCOPおよびCATHなどの構造ベースのデータベースです。ここでは、CAFA-3ベンチマークセット[4]を参照します。
from sklearn.metrics import precision_recall_curve, auc
def evaluate_pr_curve( query_pairs: list[tuple[str, str]], # (query_acc, target_acc) ground_truth: dict[str, set[str]], # query_acc → 実際の相同アクセッションのセット method: callable, # method(query_acc) → [(target_acc, score)]) -> dict: """PR曲線を使用して、検出性能を定量化します。""" y_true, y_score = [], [] for query_acc, target_acc in query_pairs: preds = method(query_acc) # [(acc, score), ...] scored = {acc: score for acc, score in preds} y_true.append(1 if target_acc in ground_truth.get(query_acc, set()) else 0) y_score.append(scored.get(target_acc, 0.0)) precision, recall, _ = precision_recall_curve(y_true, y_score) return { "auprc": auc(recall, precision), "recall_at_p90": max( (r for p, r in zip(precision, recall) if p >= 0.9), default=0.0 ), }統合されたパイプライン
def full_pipeline( query_sequence: str, embedder: ESM3Embedder, index: faiss.Index, accession_map: list[str], # インデックスの順序 → アクセッション go_db: Path, k: int = 20,) -> dict: """新しいシーケンスに対して、検索+機能転送を実行します。""" query_emb = embedder.embed(query_sequence).numpy() distances, indices = search(index, query_emb, k=k, ef_search=128) neighbor_accs = [accession_map[i] for i in indices] go_predictions = transfer_go_annotations(neighbor_accs, distances.tolist(), go_db) return { "query_length": len(query_sequence), "top_neighbors": list(zip(neighbor_accs, distances.tolist())), "predicted_go_terms": go_predictions, }
## パフォーマンス、コスト、および既知の失敗事例
### パフォーマンスの参照(公開ベンチマークを使用)
| アプローチ | データセット | Remote Homolog Recall@P=0.9 | Function Transfer F1 | ソース ||---|---|:---:|:---:|---|| BLAST (E ≤ 1e-5) | SCOPe 40 twilight | 0.32 | 0.51 | Rives et al., PNAS 2021 [1] || PSI-BLAST 3 iter | SCOPe 40 | 0.44 | 0.58 | Rost 1999 (Classic) || HHblits | SCOPe 40 | 0.53 | 0.61 | Remmert et al., Nat Methods 2012 || ESM2 650M + KNN | CATH 20 | 0.61 | 0.64 | Rao et al., ICML 2021 [2] || ESM3 1.4B + KNN (HNSW) | CATH 20 | ~0.67 (推定) | ~0.68 (推定) | EvolutionaryScale 2024 [5] || Foldseek (構造ベース) | CATH 20 | 0.78 | — | van Kempen et al., Nat Biotech 2024 [3] |
Foldseek は依然として強力ですが、事前に計算された構造予測(例:AlphaFold)が必要であり、ESM エンベディングは、シーケンスのみを使用してすぐに検索できるという利点があります。
### インデックスによる測定されたパフォーマンス(公式 FAISS ベンチマークに基づく)
| インデックス | 500k ベクトル (d=1536) 検索レイテンシー | 精度 (recall@10) | メモリ ||---|:---:|:---:|:---:|| IndexFlatIP | 150〜200ms | 100% | 3.0 GB || IndexIVFFlat (nlist=4096, nprobe=32) | 8〜15ms | 96〜98% | 3.1 GB || IndexHNSWFlat (M=32, efSearch=128) | 3〜6ms | 98〜99% | 4.5 GB |
**推奨:** リアルタイムサービスには HNSW、バッチ分析には IVF、ベンチマークには Flat。
### 学習者が再現するための推定コスト
- API コスト:0(完全にローカル)。- エンベディング抽出:RTX 4060 8GB を使用して 500k シーケンスで約 2〜4 時間(1 秒あたり 40〜60 シーケンス)。- FAISS インデックス構築:HNSW M=32 で約 20〜30 分(CPU)。- 検索:1 クエリあたり 3〜6ms。
### 3 つの既知の失敗事例(コミュニティ/論文の収集)
1. **長いシーケンス(>1024 AA)の切り捨てによるドメイン情報の損失** 症状:マルチドメインタンパク質の最初の 1024 AA のみをエンベディングすると、後続のドメインがエンベディングに反映されません。 原因:ESM のコンテキスト長制限。 解決策:(a) 各ドメインを個別にエンベディングし、平均または連結する、(b) スライディングウィンドウを使用して複数のエンベディングを生成し、最大プーリングを実行する。 ソース:HuggingFace ESM Community — GitHub Issues [7]。
2. **IVF インデックスの不十分な nprobe によるリモートホモログの検出漏れ** 症状:nprobe をデフォルト値(1)に設定すると、リコールが急激に低下します。これは、リモートホモログが別のクラスタにクラスタ化される可能性が高いためです。 原因:IVF は、量子化器の近くのクラスタのみを訪問します。遠いホモログは、進化系統の違いにより、異なるクラスタに割り当てられます。 解決策:nprobe を nlist の少なくとも 1%(50〜100)に設定します。本番環境のサービスでは、適応的な nprobe ロジックを推奨します。 ソース:FAISS Wiki — "Guidelines for choosing an index" [8]。
3. **エンベディングの正規化の欠如によるコサイン類似度と L2 類似度の混同** 症状:IndexFlatIP をエンベディングを正規化せずに使用すると、内積はコサイン類似度から逸脱し、異なる長さのタンパク質の比較が歪みます。 原因:ESM エンベディングのノルムは、シーケンスの長さと組成によって異なります。 解決策:`faiss.normalize_L2()` を呼び出して、インデックスに追加およびクエリを実行する前に、常にエンベディングを正規化します。 ソース:FAISS FAQ — "Normalization for cosine similarity" [9]。
## 拡張アイデア
- **Chroma Vector DB の統合:** メタデータフィルタリングが必要な場合(例:特定のタクソンまたは特定の GO 用語のみを検索する場合)は、FAISS ではなく Chroma/Qdrant を使用します。- **ハイブリッド検索:** エンベディング KNN + BLAST HSP スコアをアンサンブル化します。高い精度が求められる場合に有利です。- **構造エンベディングの組み合わせ:** ESM3 の構造トラック + Foldseek 3Di アルファベットを組み合わせて、デュアルのシーケンス-構造検索を実行します。- **韓国語 UI サービス:** FastAPI + Streamlit を使用して、社内サービスを構築します。新しいシーケンスを入力すると、システムはすぐに上位 10 件の類似シーケンス + GO 予測を返します。
## 次の章
- Chapter 08 `docking-hybrid-diffusion`: エンベディング検索を使用して候補となるターゲットタンパク質のリストを作成し、DiffDock-Glide で検証します。- Chapter 11 `structure-affinity-boltz`: エンベディングを使用して類似のタンパク質を見つけ、Boltz-2 で結合親和性を予測します(ドラッグリポジショニングパイプライン)。- Chapter 12 `single-cell-perturbation`: エンベディング類似性を使用して、シングルセル遺伝子攪乱のターゲット候補を選択します。- Chapter 13 `protein-design-multimodal`: 本章のシーケンストラックに対応する ESM3 の構造/機能トラックを利用します。- Chapter 14 `bio-mcp-agent`: 本章の検索パイプラインを、自律エージェントが使用できる MCP ツールとして公開します。
## 参考文献
1. Rives A, Meier J, Sercu T, et al. "2億5000万のタンパク質配列に対して、教師なし学習をスケールアップすることで、生物学的構造と機能が出現する。" PNAS 2021. `https://www.pnas.org/doi/10.1073/pnas.2016239118`2. Rao R, Meier J, Sercu T, Ovchinnikov S, Rives A. "Transformerタンパク質言語モデルは、教師なしの構造学習を行う。" ICLR 2021. `https://openreview.net/forum?id=fylclEqgvgd`3. van Kempen M, Kim S, Tumescheit C, et al. "Foldseekを用いた、高速かつ正確なタンパク質構造検索。" Nature Biotechnology 2024. `https://www.nature.com/articles/s41587-023-01773-0`4. CAFA (Critical Assessment of Function Annotation): `https://www.biofunctionprediction.org/cafa/`5. Hayes T, Rao R, Akin H, et al. "言語モデルを用いて、5億年間の進化をシミュレーションする。" bioRxiv 2024 (ESM3). `https://www.biorxiv.org/content/10.1101/2024.07.01.600583v1`6. UniProt-GOA: `https://www.ebi.ac.uk/GOA/downloads`7. HuggingFace ESMコミュニティディスカッション: `https://huggingface.co/facebook/esm2_t33_650M_UR50D/discussions`8. FAISS Wiki — インデックスを選択するためのガイドライン: `https://github.com/facebookresearch/faiss/wiki/Guidelines-to-choose-an-index`9. FAISS FAQ — 正規化: `https://github.com/facebookresearch/faiss/wiki/FAQ`10. EvolutionaryScale ESM3 GitHub: `https://github.com/evolutionaryscale/esm`11. FAISS GitHub: `https://github.com/facebookresearch/faiss`12. UniProt Swiss-Prot: `https://www.uniprot.org/`13. Biopython: `https://biopython.org/`14. SCOPe (Structural Classification of Proteins extended): `https://scop.berkeley.edu/`15. CATH (Class · Architecture · Topology · Homology) database: `https://www.cathdb.info/`