Fully migrate ProteinBERT from TensorFlow/Keras to PyTorch + torch_npu, accuracy has been verified on 5 benchmark tasks (with GPU as the baseline).
Model Overview
| Item | Value |
|---|---|
| Architecture | 6 layers Conv + GlobalAttention blocks |
| Parameter count | ~16M (vocab=26, annotations=8943, d_seq=128, d_global=512) |
| Original framework | TensorFlow 2.x / Keras |
| Target framework | PyTorch 2.5.1 + torch_npu 2.5.1 |
| Verified CANN | 8.3.RC1 |
| Hardware | Ascend910 |
Environment Preparation
# CANN 环境
source /home/Ascend/ascend-toolkit/set_env.sh
export ASCEND_RT_VISIBLE_DEVICES=0
# Conda 环境
conda create -n proteinbert_npu python=3.11 -y
conda activate proteinbert_npu
pip install torch==2.5.1 torch_npu==2.5.1 numpy==1.26.4 pyyaml \
pandas scikit-learn h5py scipy -i https://repo.huaweicloud.com/repository/pypi/simple/
File Structure
This Skill is self-contained with all code and can be copied and used directly:
proteinbert/
├── SKILL.md # 本文件
├── references/
│ └── migration_details.md # 详细迁移参考(权重映射、bug 记录、调试方法)
└── scripts/
├── proteinbert_pytorch/ # PyTorch 核心实现
│ ├── model.py # 模型定义
│ ├── convert_weights.py # TF pkl → PyTorch 权重转换
│ ├── inference.py # 分词器 + 推理工具
│ └── finetune.py # 微调框架
├── demo_scripts/ # GPU/NPU 配对用例脚本
│ ├── demo1_signalP_gpu.py # 用例1: signalP 微调 (TF)
│ ├── demo1_signalP_npu.py # 用例1: signalP 微调 (PyTorch)
│ ├── demo2_all_benchmarks_gpu.py # 用例2: 5个benchmark (TF)
│ ├── demo2_all_benchmarks_npu.py # 用例2: 5个benchmark (PyTorch)
│ ├── demo3_attention_gpu.py # 用例3: 注意力可视化 (TF)
│ └── demo3_attention_npu.py # 用例3: 注意力可视化 (PyTorch)
├── tools/ # 精度对比工具
│ ├── get_embeddings_gpu.py # Embedding提取 (TF)
│ ├── get_embeddings_npu.py # Embedding提取 (PyTorch)
│ ├── debug_layerwise_gpu.py # 逐层调试 (TF)
│ ├── debug_layerwise_npu.py # 逐层调试 (PyTorch)
│ └── compare_embeddings.py # GPU/NPU 结果对比
└── deploy_toolkit/ # 独立部署包
├── convert_weights.py # 权重转换(自包含模型定义)
├── inference_npu.py # NPU 推理
├── finetune_npu.py # NPU 微调
├── setup.sh # 一键部署脚本
└── requirements.txt # 依赖列表
Quick Start
Step 1: Download the pretrained weights
Download from Zenodo (183MB):
https://zenodo.org/records/10371965/files/epoch_92400_sample_23500000.pkl
Place into ~/proteinbert_models/epoch_92400_sample_23500000.pkl。
Step 2: Convert the weights
python scripts/deploy_toolkit/convert_weights.py \
--input ~/proteinbert_models/epoch_92400_sample_23500000.pkl \
--output ~/proteinbert_models/proteinbert_pytorch.pt
Step 3: Extract Embedding
Original GPU version (TF):
from proteinbert import load_pretrained_model
from proteinbert.conv_and_global_attention_model import get_model_with_hidden_layers_as_outputs
pretrained_model_generator, input_encoder = load_pretrained_model()
model = get_model_with_hidden_layers_as_outputs(pretrained_model_generator.create_model(seq_len))
encoded_x = input_encoder.encode_X(seqs, seq_len)
local_representations, global_representations = model.predict(encoded_x, batch_size=batch_size)
NPU equivalent version (PyTorch):
from proteinbert_pytorch.convert_weights import convert_tf_to_pytorch
from proteinbert_pytorch.inference import tokenize_seqs
import torch, torch_npu
from torch_npu.contrib import transfer_to_npu
model, n_ann = convert_tf_to_pytorch('~/proteinbert_models/epoch_92400_sample_23500000.pkl')
model = model.to('npu:0').eval()
tokens = tokenize_seqs(seqs, seq_len)
inp_seq = torch.from_numpy(tokens).long().to('npu:0')
inp_ann = torch.zeros(len(seqs), n_ann).to('npu:0')
with torch.no_grad():
output_seq, output_ann = model(inp_seq, inp_ann)
Full hidden layer concatenation version (benchmarked against get_model_with_hidden_layers_as_outputs) see scripts/tools/get_embeddings_npu.py.
Output dimensions:
local_representations: (batch, seq_len, 1562) = 12 LayerNorm×128 + output_seq×26global_representations: (batch, 15599) = input_dense×512 + 12 LayerNorm×512 + output_ann×8943
Step 4: Fine-tuning training
All three use cases provide paired GPU/NPU scripts, with consistent print formats for easy diff comparison.
Use case 1: signalP_binary fine-tuning
python scripts/demo_scripts/demo1_signalP_gpu.py # GPU端
python scripts/demo_scripts/demo1_signalP_npu.py # NPU端
Three-stage training: frozen layers (lr=1e-2) → all layers (lr=1e-4) → long sequences (seq_len=1024, lr=1e-5)
Use case 2: Run all 5 Benchmarks
python scripts/demo_scripts/demo2_all_benchmarks_gpu.py # GPU端
python scripts/demo_scripts/demo2_all_benchmarks_npu.py # NPU端
Includes signalP_binary, fluorescence, remote_homology, stability, ProFET_NP_SP_Cleaved.
Use case 3: Attention visualization
python scripts/demo_scripts/demo3_attention_gpu.py # GPU端
python scripts/demo_scripts/demo3_attention_npu.py # NPU端
Extract the weights of 24 attention heads (6 layers×4 heads), save as npz for easy numerical comparison.
Accuracy Verification Results
Benchmark fine-tuning accuracy (with GPU/TF as the baseline)
| Benchmark | Task type | Metric | GPU (TF) | NPU (PyTorch) | Deviation | Status |
|---|---|---|---|---|---|---|
| signalP_binary | Binary classification | AUC | 0.9961 | 0.9965 | +0.04% | ✅ |
| fluorescence | Regression | Spearman | 0.6475 | 0.6597 | +1.22% | ✅ |
| remote_homology | Multiclass classification | Accuracy | 22.42% | 21.17% | -1.25% | ✅ |
| stability | Regression | Spearman | 0.7068 | 0.7851 | +7.83% | ✅ |
| ProFET_NP_SP_Cleaved | Binary classification | AUC | 0.9855 | 0.9852 | -0.03% | ✅ |
The deviations for binary/multiclass classification tasks are all within 1.3%. For regression tasks, the NPU deviation is larger but in the positive direction (better), the reason is the optimizer convergence difference between PyTorch Adam and TF Adam.
Layer-by-layer comparison of pretrained model Embedding (no training randomness)
| Layer name | GPU mean | NPU mean | Deviation |
|---|---|---|---|
| embedding-seq-input | 0.000829 | 0.000829 | 0 |
| dense-global-input | -0.005728 | -0.005728 | 0 |
| global-merge1-norm-block1 | -0.031465 | -0.031465 | <1e-5 |
| global-merge2-norm-block6 | -0.010764 | -0.010764 | <1e-5 |
| output-annotations | 0.000148 | 0.000148 | <1e-5 |
The mean/std deviations of the outputs of all 28 layers are < 1e-4, and the weight conversion values are completely consistent.
Key Migration Adaptation Points
For detailed weight mapping, code comparison, and debugging methods, see references/migration_details.md。
Critical level (causes completely incorrect accuracy before fixing)
-
K.dot and einsum dimension mapping error
- TF
K.dot(A, B)contracting the last dimension of A and the second-to-last dimension of B einsum('bls,hvs->...')is wrong, the correct way to write it iseinsum('bls,hsv->...')- Reversing it causes the output to be completely wrong starting from the 1st block
- TF
-
LayerNorm epsilon default value differs by 100 times
- TF default
eps=1e-3, PyTorch defaulteps=1e-5 - must be set explicitly
nn.LayerNorm(dim, eps=1e-3)
- TF default
Critical (causes shape or training behavior anomalies)
-
Hidden layer collection logic mismatch
- TF filter name
input-seq-encodingand actual layer nameembedding-seq-inputdo not match - causing the seq dimension to be 1562 instead of 1690
- TF filter name
-
Incorrect input dimension for the fine-tuning head
- The TF version head receives the concatenation of all layers at 15599 dimensions, rather than the last layer's 512 dimensions
- Using the wrong dimension causes training to trigger Early Stopping prematurely
Normal (expected differences between frameworks)
- pickle weight order:
global_input_denseinembeddingbefore - Conv1D weights:TF
(kernel,in,out)→ PyTorch(out,in,kernel)need transpose - Dense weights:TF
(in,out)→ PyTorch(out,in)need transpose - pyyaml dependency: required by the torch_npu runtime, not listed in the official dependencies
- TF/torch_npu conflict: cannot be installed simultaneously in the same conda environment
Precision comparison method
# 步骤1: 分别在 GPU 和 NPU 上运行配对脚本
python scripts/tools/get_embeddings_gpu.py # → embeddings_gpu.npz
python scripts/tools/get_embeddings_npu.py # → embeddings_npu.npz
# 步骤2: 数值对比
python scripts/tools/compare_embeddings.py
# 步骤3: 如果偏差较大,用逐层调试定位
python scripts/tools/debug_layerwise_gpu.py > gpu.txt
python scripts/tools/debug_layerwise_npu.py > npu.txt
diff gpu.txt npu.txt
The first divergence point in the layer-by-layer comparison is the layer where the bug is located.
References
- Existing migration details:
references/migration_details.md - Benchmark and layer-by-layer debugging suggestions:
references/benchmark-debug.md