背景痛点:别让毕业设计止步于 Notebook
很多同学习惯在 Jupyter Notebook 里把算法跑通就交差,结果答辩现场一演示就翻车:
- 数据路径写死,换台电脑找不到文件
- 模型推理一次要加载 3 分钟,评委刷个手机就错过关键帧
- 输入一张非规范图直接崩溃,只能尴尬重启内核
归根结底,是把“跑通算法”误当成“交付系统”。毕业设计真正的分水岭,是把算法封装成服务、让外人也能毫无意外地复现结果。下面这份全链路笔记,来自我去年指导的《基于轻量级图像去雾算法的 Web 服务》项目,成绩 A+,代码开源后还被两家小公司拿去当 Demo。整套流程可复制到任何“基于算法的毕业设计”中,只要按部就班,你也能把 Jupyter 脚本升级成工业级雏形。
技术选型对比:别在起跑线上挖坑
框架:Flask vs FastAPI
- Flask 生态老、插件多,但异步支持靠 gevent“打补丁”,并发高时容易踩 GIL 坑
- FastAPI 原生 async,自动 Swagger 文档,Pydantic 做输入校验一行代码搞定。毕业设计周期短,直接上 FastAPI 能省掉 30% 冗余代码
部署:裸机 vs Docker
- 裸机最怕“在我电脑能跑”。答辩现场给你一台 Ubuntu 18.04,Python 3.8 没装,CUDA 驱动版本不对,当场社死
- Docker 镜像把 OS+依赖+模型一并打包,拉到哪台机都是同一环境。还能用多阶段构建把 8 GB 基础镜像压到 800 MB,评委笔记本 8 G 内存也能跑
推理加速:ONNX Runtime vs 原生 PyTorch
- 毕设不求毫秒级延迟,但冷启动从 90 秒降到 15 秒,评委体验天差地别
- ONNX 导出后占显存减半,CPU 推理也能把线程数调到物理核数,一举两得
核心实现:把算法变成黑盒服务
下面以最简“图像去雾”算法为例,展示如何把训练好的dehaze.pth封装成幂等服务。项目结构遵循 Clean Architecture:
app/ ├─ main.py # FastAPI 入口 ├─ model/ │ ├─ __init__.py │ └─ dehaze.py # 算法核心 ├─ schema.py # Pydantic 校验 ├─ service.py # 业务胶水 └─ Dockerfile1. 算法模块解耦
model/dehaze.py
import torch import onnxruntime as ort from pathlib import Path class DehazeModel: """ 纯推理接口,训练代码完全不耦合。 支持 PyTorch 与 ONNX 双后端,方便后续横向对比。 """ def __init__(self, weights: Path, backend: str = "onnx"): self.backend = backend if backend == "onnx": self.session = ort.InferenceSession(str(weights)) self.input_name = self.session.get_inputs()[0].name else: self.model = torch.jit.load(weights, map_location="cpu") self.model.eval() def predict(self, rgb_tensor: "torch.Tensor") -> "torch.Tensor": if self.backend == "onnx": # ONNX 期望 numpy out = self.session.run(None, {self.input_name: rgb_tensor.numpy()})[0] return torch.from_numpy(out) with torch.no_grad(): return self.model(rgb_tensor)要点
- 构造函数只干一件事:把模型塞进内存
predict方法无状态,天然幂等,方便做并发压测
2. 输入校验与序列化
schema.py
from pydantic import BaseModel, Field from typing import List class DehazeRequest(BaseModel): # 前端传 base64,避免二进制 JSON 逃逸 image_b64: str = Field(..., description="RGB 图像 base64") resize_max: int = Field(800, le=2000, description="长边缩放阈值") class DehazeResponse(BaseModel): image_b64: str cost_ms: int用 Pydantic 做字段校验,一行代码解决“上传 20 MB 大图把内存打爆”的隐患。
3. 服务层组装
service.py
import base64, io, time from PIL import Image import numpy as np import torch from schema import DehazeRequest, DehazeResponse from model.dehaze import DehazeModel model = DehazeModel("weights/dehaze.onnx") # 启动时一次性加载 def dehaze_logic(req: DehazeRequest) -> DehazeResponse: t0 = time.perf_counter_ns() # 1. 解码 img = Image.open(io.BytesIO(base64.b64decode(req.image_b64))).convert("RGB") # 2. 缩放 img.thumbnail((req.resize_max, req.resize_max)) arr = np.asarray(img) / 255.0 tensor = torch.from_numpy(arr).unsqueeze(0).permute(0,3,1,2).float() # 3. 推理 with torch.no_grad(): out = model.predict(tensor) # 4. 编码回前端 out = out.clamp(0,1).permute(0,2,3,1).squeeze().numpy() * 255 out_img = Image.fromarray(out.astype("uint8")) buf = io.BytesIO() out_img.save(buf, format="JPEG", quality=90) b64 = base64.b64encode(buf.getvalue()).decode() cost = (time.perf_counter_ns() - t0) // 1_000_000 return DehazeResponse(image_b64=b64, cost_ms=cost)注意
- 所有耗时操作都包在
dehaze_logic,方便后面加 Redis 缓存 - 图像解码用 Pillow,避免 OpenCV 多线程与 UWSGI 的
fork竞争死锁
4. FastAPI 入口
main.py
from fastapi import FastAPI, HTTPException from service import dehaze_logic from schema import DehazeRequest, DehazeResponse app = FastAPI(title="Dehaze API", version="1.0.0") @app.post("/dehaze", response_model=DehazeResponse) def dehaze(req: DehazeRequest): try: return dehaze_logic(req) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.get("/health") def health(): return "ok"/health给 Nginx 做反向代理时探活,防止刚启动就打进流量导致冷启动雪崩。
性能与安全性:让评委挑不出刺
冷启动
- Docker 启动即
python main.py,模型权重提前COPY到镜像,避免运行时下载 - 使用 ONNX 后,首次推理从 90 s 降到 15 s;再开
ENV OMP_NUM_THREADS=4限制 OpenMP,防止 CPU 打满
- Docker 启动即
并发与竞争
- FastAPI 默认单进程,压测 50 并发 QPS≈15;加
uvicorn workers=4后 QPS≈45,线性提升 - 算法里无共享状态,天然线程安全;若用 PyTorch CUDA,记得加
torch.cuda.set_device避免多进程竞争上下文
- FastAPI 默认单进程,压测 50 并发 QPS≈15;加
输入校验
- Pydantic 已做字段级校验;再加文件头嗅探,拒绝非 JPEG/PNG,防止“图片马”
- 限制
resize_max≤2000,否则 5000×5000 输入会把显存撑爆
防 DoS
- 反向代理层
client_max_body_size 5M - 应用层用
slowapi做令牌桶,单 IP 10 次/秒,超出返回 429,轻量且毕业设计够用
- 反向代理层
幂等性
- 相同 base64 输入给出相同输出,方便前端做防抖;若后续加 Redis 缓存,可直接用 base64 做 key,TTL 300 s
避坑指南:血泪踩出来的 checklist
依赖版本锁定
写requirements.txt时别用>=,一律==;再配pip-compile生成锁定文件,防止答辩前夜numpy发新版导致 ABI 不兼容日志
至少输出 access + error 两路,JSON 格式方便 ELK 展示。不要print,用structlog,字段统一,评委问“哪里报错”你能秒定位健康检查
很多同学习惯把/当健康地址,结果前端一刷就 404。单独留/health,返回 200 且不带业务逻辑,才是正经探活多线程与 OpenCV
OpenCV 编译时默认开 TBB,和 UWSGI 的fork模型一起用会死锁。要么cv2.setNumThreads(0),要么干脆 Pillow 解码显存泄漏
PyTorch 每次推理后torch.cuda.empty_cache()可缓解,但别频繁调用,否则反而拖慢。毕设场景每 100 次清一次足够可复现性
把随机种子全部钉死:torch.manual_seed(42)、numpy.random.seed(42)、PYTHONHASHSEED=0;Dockerfile 里写死ubuntu:20.04@sha256:xxx,别用latest标签
效果展示:从 0 到可访问的链接
本地docker build -t dehaze . && docker run -p 8000:8000 dehaze后,浏览器打开http://localhost:8000/docs就能看到自动生成的 Swagger。上传一张雾图,800 ms 内返回去雾结果,内存占用稳定在 600 MB。用locust -f bench.py -u 50 -r 10压测 1 分钟,无 5xx 错误,QPS≈45,完全符合本科毕设“工业级雏形”要求。
下一步:把毕设当成产品来迭代
走到这一步,你已经拥有:
- 可复现的容器镜像
- 带校验的 REST API
- 压测报告与日志
接下来不妨再往前一步:
- 把镜像推到阿里云 ACR,免费额度足够毕设演示
- 写 GitHub Actions:每次 tag 自动跑单测→构建镜像→部署到云服务器,答辩时直接甩 GitHub 链接
- 补充 README,说明如何替换自己的算法权重、如何调参,让师弟师妹一键复用
毕业设计不是课程作业,而是你在校园里最后一次“合法胡闹”的机会。把算法真正搬到线上,让外网用户也能调用,你就提前体验了“需求-开发-测试-部署-监控”的完整闭环。等面试官让你讲项目时,你聊的不只是准确率,还有 QPS、冷启动、幂等性、并发竞争——这就是差距所在。
打开终端,新建一个refactor分支,把你的 Notebook 逐步挪进app/model,再写一条 Dockerfile,今晚就能在云端看到自己的算法跑起来。下一次迭代,不妨思考:如果用户量翻十倍,哪个环节最先撑不住?答案会指引你继续深挖——而这份思考,正是工业级雏形的起点。