蛋白质二级结构预测实战:LSTM模型训练与Flask应用部署 简介基于Python实现的蛋白质二级结构预测项目源码适用于生物信息学课程设计、期末大作业及入门级科研实践。项目以循环神经网络为核心完整覆盖数据预处理、模型训练与预测结果可视化流程附有清晰的目录结构和运行说明适合具备基础Python知识、希望快速搭建完整项目的学习者。压缩包共34个文件包含5个Python脚本模型构建、网络定义、数据库操作、主程序等、训练好的h5模型权重、npy格式的数据集、环境配置文件以及展示效果图等包体仅6.59MB轻量易部署。目前已有291人学习下载代码经老师指导通过属于95分以上的高分项目。资源内还提供了循环神经网络预测蛋白质二级结构的Markdown笔记、界面展示HTML以及操作演示音频能够帮助使用者理解模型设计思路与调参细节减少从零起步的摸索成本是一份可直接运行、便于二次开发的实用工程源码。1. 蛋白质二级结构预测从氨基酸序列到 H/E/C 三类标签先把这个项目解决的任务讲清楚输入是一段氨基酸序列输出是等长的标签串每个位置从 Hα螺旋、Eβ折叠、C无规则卷曲三选一。二级结构预测的本质是序列标注难度在于一个残基属于哪类结构往往由它前后几十个残基共同决定跟传统表格分类问题完全不同。RNN 在这里能站住脚是因为 LSTM 的隐藏状态能沿时间步传递上下文把远端残基的影响带到当前判断点。这套源码把整条链路串了起来mytools.py 负责序列编码net.py 定义 LSTM 模型saved_model.h5 是训练好的权重main.py 和 templates/index.html 组成 Flask 演示页面。下面从数据编码开始把每一环的参数和坑过一遍。2. 氨基酸编码与 train.npy/test.npy模型输入的前置处理2.1 为什么不能把字符直接喂给 LSTMLSTM 吃的是数值张量不是字符串。20 种常见氨基酸先映射到固定顺序的索引表再用 one-hot 方式展开成 20 维向量对应位置为 1其余为 0。这种表示不会给氨基酸引入数值大小关系网络只能从数据里学模式特征泄漏的风险最低。项目里 mytools.py 维护的氨基酸表顺序按 A、C、D、E、F、G、H、I、K、L、M、N、P、Q、R、S、T、V、W、Y 排列这也是生物信息学工具链里的常规顺序。import numpy as np AMINO_ACIDS ACDEFGHIKLMNPQRSTVWY AA_TO_IDX {aa: i for i, aa in enumerate(AMINO_ACIDS)} def seq_to_onehot(seq: str) - np.ndarray: 把一条氨基酸序列转成 (L, 20) 的 one-hot 矩阵 L len(seq) mat np.zeros((L, 20), dtypenp.float32) for i, aa in enumerate(seq.upper()): if aa X: continue mat[i, AA_TO_IDX[aa]] 1.0 return mat逻辑很直白先查表拿到残基在 20 维向量里的下标再写进对应位。遇到 X 或其他不明字符时跳过保留全零向量不让异常字符让整个样本报废。特征维度固定在 20模型输入形状是 (batch, L, 20)。如果后面要接 PSSM 特征只需把返回值改成拼接矩阵维度相应变成 40。2.2 滑动窗口把上下文显式切出来氨基酸的结构倾向通常由局部上下文决定最有效的窗口范围是当前残基左右各 715 个残基。虽然 LSTM 本身能建模长依赖但训练样本有限时模型很难把长距离信息用好。比较常用的做法是在编码后按滑动窗口切块让每个时间步看到以自己为中心的连续片段。窗口设 15特征维度就是 15×20300 维窗口设 17变成 340 维。维度越大训练越慢窗口太小又丢上下文作业场景下 15 是性价比偏高的默认值。WINDOW 15 PAD WINDOW // 2 def build_window_samples(features: np.ndarray, labels: np.ndarray): 按窗口滑动返回 (N, WINDOW*20) 特征和 (N,) 标签 L len(features) padded np.vstack([ np.zeros((PAD, 20), dtypenp.float32), features, np.zeros((PAD, 20), dtypenp.float32), ]) X_list, y_list [], [] for i in range(PAD, L PAD): win padded[i - PAD : i PAD 1].reshape(-1) X_list.append(win) y_list.append(labels[i - PAD]) return np.array(X_list), np.array(y_list)切片 padded[i-PAD : iPAD1] 以当前位置为中心正好取到 WINDOW 个残基。首尾补零是为了让序列两端也能正常产生窗口样本否则开头和末尾的残基会缺一部分上下文。y_list 里取的是 labels[i-PAD]对应原始序列的下标不是窗口里的相对位置。不少初写代码的人在这里直接取 labels[i]序列和标签对不齐训练到后面精度会在 60% 上下卡死。2.3 拿到 npy 文件先做结构检查项目里的 train.npy 和 test.npy 是 numpy 二进制文件训练之前必须确认到底存的是什么形状。我一般先跑一段检查脚本而不是直接扔进 fit 里看报错。python -c import numpy as np for f in [train.npy, test.npy]: d np.load(f, allow_pickleTrue) print(f, d.shape, d.dtype) if d.dtype object: print(d.item().keys()) 输出会决定后续处理方式。常见有几种情况输出形状含义处理方式(N, L, 20)未窗口化的特征矩阵按需滑窗或直接送 LSTM(N, 300) 或 (N, 315)已滑窗展平先 reshape 成 (N, WINDOW, 20)(N, L)标签数组值在 0/1/2作为 y 使用无需转换(N, L, 3)one-hot 标签改用 categorical_crossentropy判断清楚后再写训练脚本避免二次滑窗造成维度错位。这个检查动作在作业调试里特别划算不少情况是下载源码后直接跑跑不通才回头看数据一查发现 npy 里已经带窗口又套了一次窗口函数。2.4 标签不平衡容易把指标带偏二级结构里不同状态的比例并不均匀无规则卷曲的样本通常明显多于螺旋和折叠。训练时如果直接拿 accuracy 当主指标模型很容易倾向于把犹豫不决的位置全部预测成常见类别整体精度看着还行各类别精度却参差不齐。答辩时老师追问“E 类的召回率是多少”就不好接。建议训练前先统计标签比例必要时给 loss 加类别权重weight 直接按样本数倒数设置即可。还有数据泄漏的问题。如果 train.npy 和 test.npy 是从同一条蛋白质链上切出来的连续片段重叠部分会同时出现在训练和测试里验证指标虚高。拿到数据后最好检查序列去重情况测试集至少保证与训练集不共享相同片段。这一点决定了最后换一条新序列预测时结果到底可不可信。3. RNN 层设计与参数选择为什么用 LSTM 而不是 CNN3.1 双向 LSTM 加逐帧分类的搭建方式二级结构预测依赖上下文上下文既来自序列左侧也来自右侧。单向 LSTM 只能看到过去的信息双向 LSTM 把正向和反向各跑一遍再拼接模型在每个残基位置同时拥有左右两侧的压缩表示。CNN 也能捕捉局部但卷积核的感受野固定想覆盖长距离必须堆层数而蛋白质序列里真正有效的关系常常横跨 10 到 20 个残基。从实现成本看一层双向 LSTM 的效果通常比堆三层 CNN 更直接。import tensorflow as tf def build_model(seq_len: int, feature_dim: int 20, num_classes: int 3): inputs tf.keras.Input(shape(seq_len, feature_dim)) x tf.keras.layers.Bidirectional( tf.keras.layers.LSTM( 64, return_sequencesTrue, dropout0.3, recurrent_dropout0.2, ) )(inputs) x tf.keras.layers.TimeDistributed( tf.keras.layers.Dense(64, activationrelu) )(x) x tf.keras.layers.Dropout(0.3)(x) outputs tf.keras.layers.TimeDistributed( tf.keras.layers.Dense(num_classes, activationsoftmax) )(x) return tf.keras.Model(inputs, outputs)参数需要逐个说清楚。Bidirectional 包裹后LSTM 输出维度翻倍64 变成 128。return_sequencesTrue 控制每个时间步都输出而不是只在序列末尾产出一个综合向量逐残基预测必须开这个选项。dropout0.3 作用在输入侧recurrent_dropout0.2 作用在循环状态传递上两者都用来抑制过拟合。TimeDistributed 层把同一个 Dense 应用到每个时间步等价于对整个序列逐帧分类。最后的 softmax 输出三个类别的概率分布取 argmax 得到 H/E/C。3.2 隐藏单元数与训练成本的权衡网络深度在这个任务上不需要堆太多单层双向 LSTM 加一层 TimeDistributed 全连接已经能覆盖大多数作业数据量。真正影响效果的是隐藏单元数和 dropout。隐藏单元太少模型容量不足太多序列短时容易过拟合。训练前可以用小数据跑几个候选配置对比每个配置只训 10 个 epoch看验证精度变化趋势再决定最终参数。配置隐藏单元参数量约验证精度特点训练开销单向 LSTM642.2 万后段易过拟合精度一般低双向 LSTM64×24.4 万收敛平稳适合作默认中双向 LSTM128×214.5 万需配较强 dropoutepoch 要求多高参数量按 LSTM 的四门结构粗略估算4 gates ×输入 20 隐藏 64× 隐藏 64双向再乘 2。参数量主要用来估训练时间和过拟合风险不是越大越好。作业场景我一般选双向 64 作为默认再用 10 个 epoch 快速验证一次。3.3 序列长度截断与统一填充LSTM 的输入长度必须固定而蛋白质链长短差异极大。常见处理是设置最大长度超出部分直接截断不足部分在前部补零。补零位置本身没有生物意义模型训练时把它当背景信号。如果在预测阶段传入的序列更短要补到同一个长度再推理最后只取有效长度范围内的输出。a.yaml 配置文件里通常可以定义 seq_len、feature_dim、num_classes 这些参数训练和预测共用一个配置文件避免数值不一致。注意填充方向在训练与预测两个阶段要保持一致。训练时补在开头预测时也补在开头一个阶段在头部补、另一个阶段在尾部补模型看到的序列内容完全不同预测结果会明显变差。4. 模型训练与评估loss 下降不是唯一指标4.1 早停与模型检查点训练阶段最怕两件事一直不收敛和收敛到过拟合。LSTM 训练蛋白质序列数据时样本量不大通常几十个 epoch 内验证精度就到顶峰继续训练只会让训练精度继续涨、验证精度回落。早停机制会在验证指标连续若干轮不提升时停止训练模型检查点则把验证指标最好的权重单独保存下来二者配合最终拿到的是泛化能力更好的一版参数。from tensorflow import keras from net import build_model model build_model(seq_len700, feature_dim20, num_classes3) model.compile( optimizerkeras.optimizers.Adam(learning_rate1e-3), losssparse_categorical_crossentropy, metrics[accuracy], ) callbacks [ keras.callbacks.EarlyStopping(patience8, restore_best_weightsTrue), keras.callbacks.ModelCheckpoint( saved_model.h5, monitorval_accuracy, save_best_onlyTrue, ), ] history model.fit( x_train, y_train, validation_data(x_val, y_val), batch_size64, epochs80, callbackscallbacks, ) model.save(saved_model.h5)关于 loss 的选择sparse_categorical_crossentropy 要求标签是整数正好对应 H/E/C 索引如果标签是 one-hot 编码则需要换成 categorical_crossentropy。learning_rate 从 1e-3 起步验证精度连续两轮不涨就把学习率降到 5e-4。batch_size 用 64 在序列数据场景下比较稳太小梯度波动大太大占显存且收敛慢。早停的 patience 设在 8 左右比较合适设太短容易在验证精度还没爬稳时就停掉。4.2 用 Q3 和各类别召回率验证效果Q3 是二级结构预测最主流的总体准确率指标即正确预测残基数除以总残基数。Keras 训练日志里的 accuracy 也是按残基逐帧统计的通常可以直接看作 Q3。但总体指标会掩盖类别差异需要额外按 H/E/C 分别统计召回率这样才能判断模型是否把所有样本都倾向判成占多数的类别。def evaluate_per_class(y_true: np.ndarray, y_pred: np.ndarray): y_true/y_pred: shape (N, L)元素是 0/1/2 mask y_true 0 y_true_flat y_true[mask] y_pred_flat y_pred[mask] total_acc np.mean(y_true_flat y_pred_flat) per_class {} for cls, name in enumerate([H, E, C]): idx y_true_flat cls per_class[name] np.mean(y_pred_flat[idx] cls) if idx.sum() 0 else 0.0 return total_acc, per_class评估函数把标签数组展平后先算全局准确率再按类别筛选出真实标签为该类别的样本计算其中预测正确的比例。如果 H 类的召回率明显低于 C 类说明类别不平衡问题没有处理干净需要回到数据侧加权重而不是继续调网络结构。期末作业里把这张类别精度表打在演示文稿里比单贴一条 loss 曲线更有说服力。4.3 h5 模型保存与重载的兼容性model.save(saved_model.h5) 会把结构、权重和优化器状态一起写入。加载用 tf.keras.models.load_model 即可。如果 net.py 里有自定义层或者加载环境中缺依赖库可能报参数不匹配或找不到自定义对象。为了演示时不出意外我习惯在训练结束后同时保留一版完整模型和一版只含权重的模型。只含权重的加载后重新 compile 就行不会因为自定义对象缺失而报错。requirements.txt 里应当固定 TensorFlow 的主版本不同主版本下 h5 文件的加载规则不完全兼容。5. Flask Web 服务从 h5 模型到可演示的预测页面5.1 服务入口与路由划分模型训练完成后Flask 部分要做的事只有两件接收序列、返回结构标签。main.py 是应用入口负责加载模型、定义路由templates 目录下放渲染页面。整个请求链路是浏览器提交序列 → 后端编码 → 模型推理 → 返回 JSON → 前端展示。模型对象在进程启动时加载一次避免每次请求都重新读 h5这是 Web 服务最基本的性能意识。# main.py import numpy as np import tensorflow as tf from flask import Flask, request, jsonify, render_template from mytools import seq_to_onehot, pad_or_truncate app Flask(__name__) model tf.keras.models.load_model(saved_model.h5) STRUCTURE_LABELS [H, E, C] app.route(/, methods[GET]) def index(): return render_template(index.html) app.route(/predict, methods[POST]) def predict(): seq request.form.get(sequence, ).upper() if not seq: return jsonify({error: 序列不能为空}), 400 features seq_to_onehot(seq) features, _ pad_or_truncate(features, seq_len700) features np.expand_dims(features, axis0) # (1, 700, 20) probs model.predict(features, verbose0)[0] labels [STRUCTURE_LABELS[np.argmax(p)] for p in probs[:len(seq)]] return jsonify({ sequence: seq, structure: .join(labels), }) if __name__ __main__: app.run(host0.0.0.0, port5000, debugFalse)pad_or_truncate 的作用是把 one-hot 矩阵统一到模型要求的 700 行不足补零、超出截断。预测结果里只需取 probs 的前 len(seq) 行补零部分对应的输出没有生物学含义必须切掉。这里刻意用 request.form.get前端以表单方式提交即可不需要额外引入 JSON 扩展。5.2 前端页面与异步请求templates/index.html 不需要复杂 UI一个输入框、一个按钮、一个结果区域就足够演示。提交动作用 fetch 发 POST 请求接口返回的 JSON 里包含原始序列和预测结构串直接在页面上渲染。textarea idseq rows4 cols60 placeholder输入氨基酸序列例如 MKTIIALSYIFCLVFA/textarea button onclickpredict()启动预测/button div idresult/div script async function predict() { const seq document.getElementById(seq).value.trim(); const form new FormData(); form.append(sequence, seq); const resp await fetch(/predict, { method: POST, body: form }); const data await resp.json(); if (data.structure) { document.getElementById(result).innerHTML pb序列/b data.sequence /p pb结构/b data.structure /p; } else { document.getElementById(result).innerHTML p data.error /p; } } /script页面逻辑的要点在于 FormData 构造表单负载fetch 不设置 Content-Type浏览器会自动带上 multipart 边界Flask 端才能用 request.form 正常接收。返回的 JSON 在浏览器端用 data.sequence 和 data.structure 读取。如果项目里还带了 db.py一般用来记录预测历史答辩时往前翻几条测试过的序列比现场临时输入更有说服力。5.3 端口、host 与调试开关app.run 里 host0.0.0.0 允许局域网内其他机器访问做演示时很方便。debugFalse 是常态debugTrue 虽然能自动重载和打印更详细异常但会把调试器暴露到网络不是演示环境的最佳选择。如果 5000 端口被占用终端会提示 Address already in use换成 5001 即可。模型加载放在 import 之后、路由定义之前是刻意设计的Flask 启动时慢一点但首次请求会很快不会出现现场点第一下卡住的现象。注意远程演示时记得把 debug 保持为 False等本地调试阶段再开启。把调试器开着挂到局域网里一旦有人构造恶意请求风险还是不小的。6. 从 95 分到更高完成度特征增强与常见问题对照6.1 特征增强one-hot 换成 PSSM 的收益作业要交到 95 分以上单靠 one-hot 特征加 LSTM 基本能把 Q3 做到 65%75%但离领域主流水平还有差距。常见做法是把每残基特征从 one-hot 换成 PSSM即位置特异性打分矩阵。它记录了每个位置上 20 种氨基酸的出现概率LSTM 能直接利用这一层进化信息做判断通常能在同样结构下带来几个百分点的提升。如果数据源能出 PSSM 文件我一般把它和 one-hot 拼接成 (L, 40) 张量再送进模型。答辩陈述时把这个点讲清楚比单纯贴训练曲线更有内容。6.2 演示前必查项与常见报错处理准备演示时有一些容易忽略又影响结果的小问题按顺序过一遍能省不少时间。python -c import tensorflow as tf m tf.keras.models.load_model(saved_model.h5) m.summary() 这一行命令打印模型结构主要看 InputLayer 的 shape 是否为 (None, 700, 20)。如果显示的是 (None, 315)说明模型预期输入是窗口化特征而 Flask 预测流程里直接送了滑窗前的 one-hot 序列两者必然对不上。需要在 mytools.py 里补一层窗口化或者统一改成整序列输入。常见问题基本集中在三处其一序列长度填充方式不一致训练时补零在头部、预测时补在尾部数据分布完全变样其二标签数组与序列数组长度不同通常来自滑窗时下标计算错误其三预测输出没有截断 padding 区域导致每条序列末尾多出一段随机标签。把这三处对照检查一遍绝大多数运行异常和指标异常都能定位。6.3 Python 环境与依赖安装解压项目压缩包之后先在项目根目录执行 pip install -r requirements.txt。注意 TensorFlow 版本与 Python 版本的匹配关系Python 3.8 配 TensorFlow 2.x 是比较常见的组合。机器没有 GPU 就用小 batch size 在 CPU 上跑这个体量的作业模型完全跑得动。运行 main.py 前确保 saved_model.h5 与 main.py 在同一个目录下或者调整模型路径不然 Flask 启动时会直接抛 FileNotFoundError。这类环境问题在答辩现场最容易翻车提前用 model.summary() 验证一次基本能挡掉大半意外。本文还有配套的精品资源点击获取