
最近在做一个前端识别类的小项目时我一直被一个问题困扰模型输出的预测结果到底可不可信很多时候模型给出一个类别但置信度只有 0.4这种情况下业务系统到底该不该采信这个结果单纯看准确率已经不够了我们需要把“不确定性”量化出来让前端界面、后端逻辑和算法层能够共同感知风险。所以这篇文章想分享一套包含前端界面、后端接口和不确定性推理的完整实践方案项目代号就叫New Face New UNC灵感来自项目组里一位同事的创意命名重点在于UNC这个关键词——uncertainty即不确定性分析。本文将面向以下读者想给自己的图像识别或前端分析项目增加置信度评估能力的开发者正在做算法工程化、想把模型输出接入业务系统的后端同学以及希望理解不确定性概念并在代码里落地的新手。读完你会掌握如何设计一个带不确定性量化的推理服务如何用 Python 编写核心计算模块如何用 FastAPI 暴露接口如何用 Vue 3 搭建展示前端以及如何在真实项目中避坑。## 1. 背景与核心概念 ### 1.1 什么是 New Face New UNC “New Face New UNC” 不是一个官方开源项目而是一个典型的技术创意原型。项目名称拆开看 - **New Face**可以理解为“新界面”也可以理解为一套新的输入数据形态比如新的人脸图像、新的用户界面截图、新的物体检测画面。在本项目中我们把它定义为一个图像识别任务的输入样本。 - **UNC**是 **Uncertainty** 的缩写即“不确定性”。在机器学习模型输出预测结果时除了给出类别标签还应该告诉使用者这个结果有多可靠。不确定性量化就是解决“模型不知道它不知道什么”的问题。 整个项目的核心目标是构建一个小型但完整的“识别 不确定性分析 前端展示”系统让用户上传一张图片后不仅能得到预测类别还能看到模型对该预测的置信程度从而决定是否需要人工复核。 ### 1.2 不确定性为什么重要 在实际业务中模型并不是永远正确的。以下场景非常常见 - 输入图片模糊、遮挡严重模型勉强给出一个类别但实际有很大概率出错。 - 输入数据来自新的拍摄设备和训练集分布不一致模型会产生“过度自信”的错误预测。 - 业务系统直接采用模型输出做自动决策一旦预测错误可能导致后续流程全部出错。 传统上我们只看 softmax 输出的概率把最大概率当作置信度。但这种做法并不可靠因为神经网络很容易对分布外数据给出高置信度预测。不确定性分析就是为了解决这个问题它通常分为两类 - **偶然不确定性Aleatoric Uncertainty**数据本身存在噪声比如图像模糊、标签错误。这类不确定性无法通过增加数据消除但可以通过建模来估计。 - **认知不确定性Epistemic Uncertainty**模型对未知数据缺乏知识。这类不确定性可以通过增加训练数据或改进模型降低。 在本项目中我们使用一种工程上容易实现的思路同时计算 **最大概率、预测熵、概率标准差** 三个指标并综合成最终置信度。虽然原理上还有更复杂的贝叶斯方法或集成方法但当前方案已经足够演示核心流程。 ### 1.3 常用技术栈选型 为了兼顾演示效果和工程可行性本文选择以下技术栈 | 模块 | 技术选型 | 说明 | | --- | --- | --- | | 后端服务 | Python FastAPI | 轻量、异步支持好、天然的 API 文档 | | 模型推理 | PyTorch TorchVision | 使用预训练模型进行迁移学习 | | 不确定性计算 | NumPy / SciPy | 实现熵、标准差计算 | | 前端界面 | Vue 3 Vite | 轻量化前端负责上传图片与结果展示 | | 数据库 | SQLite | 记录预测历史降低部署复杂度 | 这套组合的好处是Python 生态适合快速实现算法逻辑Vue 3 能提供清晰的前端展示界面FastAPI 则把两者桥接起来。整个过程不需要高配置服务器本地开发机即可跑通。2. 环境准备与版本说明2.1 开发环境规划为了减少环境兼容性问题建议使用独立的虚拟环境。以下是我本次实践的环境参考操作系统Windows 10 / Ubuntu 20.04 均可Python3.9 或 3.10Node.js16 或 18包管理工具pip、npmIDEVS Code 或 PyCharm版本需要根据项目实际情况调整本文示例以常见环境为例重点演示配置思路。不同版本之间可能存在的差异会在常见问题中说明。2.2 后端依赖安装先创建项目目录并初始化 Python 虚拟环境mkdir new-face-new-unc cd new-face-new-unc python -m venv venv # Windows 激活虚拟环境 venv\Scripts\activate # Linux / macOS 激活虚拟环境 source venv/bin/activate激活虚拟环境后安装依赖pip install fastapi uvicorn torch torchvision pillow numpy scipy python-multipart这里说明一下各依赖的用途fastapi构建 RESTful API。uvicornASGI 服务器用来运行 FastAPI 应用。torch和torchvision加载预训练模型并进行图像分类推理。pillow处理上传的图片。numpy和scipy进行不确定性指标计算。python-multipartFastAPI 处理文件上传时必须依赖。如果你的机器没有 GPUPyTorch 会自动使用 CPU 版本运行速度稍慢但演示完全够用。2.3 前端环境准备前端部分使用 Vite 构建 Vue 3 项目。如果你还没有安装 npm 包管理器请先安装 Node.js。然后在项目根目录执行npm create vitelatest frontend -- --template vue cd frontend npm install这个命令会创建一个名为frontend的 Vue 3 项目目录。后续我们将在这个目录中修改页面代码。最终的项目结构大致如下new-face-new-unc/ ├── backend/ │ ├── main.py # FastAPI 入口 │ ├── model.py # 模型加载与推理 │ ├── uncertainty.py # 不确定性计算 │ └── requirements.txt # 后端依赖清单 ├── frontend/ │ ├── index.html │ ├── package.json │ └── src/ │ ├── App.vue # 主界面 │ └── main.js └── README.md这样前后端分离部署时可以独立扩展。3. 核心原理与代码拆解3.1 模型推理服务设计模型推理是整个系统的决策核心。本节我们使用torchvision自带的预训练模型如ResNet18进行分类。为了提高演示效果我们保留模型的原始训练类别并在输出时映射为可读标签。这个设计有两点考虑预训练模型无需额外训练开箱即用适合演示。推理结果需要转化为不确定性指标因此模型输出层要保留原始 logits而不是直接取argmax。为了减少代码复杂度我们将模型初始化封装在model.py文件中。核心代码如下# 文件路径backend/model.py import torch import torchvision.models as models import torchvision.transforms as transforms from PIL import Image # 使用 ResNet18 预训练模型并设置为评估模式 device torch.device(cuda if torch.cuda.is_available() else cpu) model models.resnet18(pretrainedTrue) model model.to(device) model.eval() # 图像预处理统一尺寸、转 Tensor、归一化 preprocess transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize( mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225] ), ]) def load_image(image_bytes: bytes) - torch.Tensor: 将上传的图片字节流转换为模型输入张量。 image Image.open(io.BytesIO(image_bytes)).convert(RGB) image_tensor preprocess(image).unsqueeze(0) return image_tensor.to(device) def predict(image_bytes: bytes): 执行推理返回原始 logits。 with torch.no_grad(): input_tensor load_image(image_bytes) logits model(input_tensor) return logitsmodel.eval()非常重要。PyTorch 的模型默认处于训练模式包含 Dropout 和 BatchNorm 的训练行为如果忘记切换到eval()模式推理结果会出现不稳定波动直接影响不确定性计算的准确性。3.2 不确定性指标计算获得 logits 后我们可以通过softmax得到每个类别的概率。然后计算三个核心指标最大概率 (Max Probability)模型对预测类别的自信程度。预测熵 (Predictive Entropy)概率分布的混乱程度。如果模型对多个类别都有较高概率熵值就会偏大。概率标准差 (Probability Std)概率分布的离散程度用于辅助判断置信度。熵的计算公式如下[ H(p) -\sum_{i1}^{C} p_i \log(p_i) ]其中 (C) 是类别总数(p_i) 是第 (i) 类的概率。熵值越高说明预测分布越均匀模型越不确定。在uncertainty.py中实现如下# 文件路径backend/uncertainty.py import numpy as np from scipy.special import softmax def softmax_probabilities(logits): 将 logits 转为概率分布。 logits 可以是 torch.Tensor 或 numpy 数组。 if hasattr(logits, cpu): logits logits.cpu().numpy() logits np.array(logits).reshape(1, -1) probs softmax(logits, axis1)[0] return probs def compute_entropy(probs): 计算预测熵。 probs np.clip(probs, 1e-10, 1.0) return float(-np.sum(probs * np.log(probs))) def compute_confidence_metrics(logits): 综合计算不确定性和置信度指标。 probs softmax_probabilities(logits) max_prob float(np.max(probs)) entropy compute_entropy(probs) std_prob float(np.std(probs)) # 简单综合置信度最大概率越高越可信熵越低越可信 # 这里除以类别数是为了让置信度范围尽量落在 0~1 之间 confidence max_prob * (1.0 - entropy / np.log(len(probs))) return { max_probability: max_prob, entropy: entropy, std_probability: std_prob, confidence: confidence, probabilities: probs.tolist() }这里要强调的是std_probability在类别很多时数值非常小适合作为参考指标不适合直接作为最终置信度。最终置信度采用最大概率 * (1 - 归一化熵)的折中策略既能反映模型自信程度又能惩罚分布过度均匀的情况。3.3 FastAPI 接口开发在main.py中我们创建三个接口GET /health健康检查。POST /predict接收图片文件返回预测结果和不确定性指标。GET /history查看预测历史记录。# 文件路径backend/main.py import io import json import sqlite3 from datetime import datetime from fastapi import FastAPI, File, UploadFile from fastapi.middleware.cors import CORSMiddleware from model import predict from uncertainty import softmax_probabilities, compute_confidence_metrics import torchvision.models as models app FastAPI() # 允许前端跨域请求 app.add_middleware( CORSMiddleware, allow_origins[*], allow_credentialsTrue, allow_methods[*], allow_headers[*], ) DB_PATH predictions.db def init_db(): conn sqlite3.connect(DB_PATH) cursor conn.cursor() cursor.execute( CREATE TABLE IF NOT EXISTS predictions ( id INTEGER PRIMARY KEY AUTOINCREMENT, filename TEXT, label TEXT, confidence REAL, entropy REAL, created_at TEXT ) ) conn.commit() conn.close() app.on_event(startup) def startup(): init_db() app.get(/health) def health_check(): return {status: ok} app.post(/predict) async def predict_endpoint(file: UploadFile File(...)): image_bytes await file.read() # 执行推理 logits predict(image_bytes) # 计算不确定性 metrics compute_confidence_metrics(logits) probs metrics[probabilities] label_index int(probs.index(max(probs))) # 这里为简化示例直接返回索引作为标签 # 实际项目可以加载 ImageNet 标签映射表 label fclass_{label_index} confidence metrics[confidence] entropy metrics[entropy] # 保存历史记录 conn sqlite3.connect(DB_PATH) cursor conn.cursor() cursor.execute( INSERT INTO predictions (filename, label, confidence, entropy, created_at) VALUES (?, ?, ?, ?, ?), (file.filename, label, confidence, entropy, datetime.now().isoformat()) ) conn.commit() conn.close() return { filename: file.filename, label: label, max_probability: metrics[max_probability], entropy: entropy, std_probability: metrics[std_probability], confidence: confidence } app.get(/history) def get_history(): conn sqlite3.connect(DB_PATH) cursor conn.cursor() cursor.execute(SELECT filename, label, confidence, entropy, created_at FROM predictions ORDER BY id DESC LIMIT 20) rows cursor.fetchall() conn.close() return [ { filename: row[0], label: row[1], confidence: row[2], entropy: row[3], created_at: row[4] } for row in rows ]在这个接口中我们没有加载 ImageNet 的完整标签文件所以返回的是class_索引形式。在实际项目中需要准备一份类别映射表否则前端展示会不友好。3.4 前端页面开发前端使用 Vue 3。为了让界面聚焦我们只修改App.vue和main.js两个文件。// 文件路径frontend/src/main.js import { createApp } from vue import App from ./App.vue createApp(App).mount(#app)!-- 文件路径frontend/src/App.vue -- template div classcontainer h1New Face New UNC/h1 p上传一张图片查看模型预测结果与不确定性指标/p input typefile acceptimage/* changehandleFileChange / button clickuploadImage :disabled!selectedFile开始分析/button div v-ifloading classloading分析中.../div div v-ifresult classresult h2分析结果/h2 table trtd预测类别/tdtd{{ result.label }}/td/tr trtd最大概率/tdtd{{ formatFloat(result.max_probability) }}/td/tr trtd置信度/tdtd{{ formatFloat(result.confidence) }}/td/tr trtd预测熵/tdtd{{ formatFloat(result.entropy) }}/td/tr trtd概率标准差/tdtd{{ formatFloat(result.std_probability) }}/td/tr /table /div div v-iferror classerror{{ error }}/div /div /template script setup import { ref } from vue const selectedFile ref(null) const result ref(null) const loading ref(false) const error ref() const API_BASE http://127.0.0.1:8000 function handleFileChange(event) { selectedFile.value event.target.files[0] result.value null error.value } function formatFloat(value) { return Number(value).toFixed(4) } async function uploadImage() { if (!selectedFile.value) return loading.value true error.value result.value null const formData new FormData() formData.append(file, selectedFile.value) try { const response await fetch(${API_BASE}/predict, { method: POST, body: formData }) if (!response.ok) { throw new Error(请求失败) } result.value await response.json() } catch (e) { error.value e.message || 发生未知错误 } finally { loading.value false } } /script style .container { max-width: 700px; margin: 40px auto; padding: 20px; font-family: Arial, sans-serif; } .loading, .error { margin-top: 20px; padding: 10px; border-radius: 4px; } .loading { background: #f0f0f0; } .error { background: #ffe0e0; color: #b00000; } table { border-collapse: collapse; width: 100%; margin-top: 10px; } td { border: 1px solid #ddd; padding: 8px; } /style这里使用了 Vue 3 的script setup语法组件内部逻辑紧凑清晰。核心逻辑是用户选择图片 → 上传到后端/predict接口 → 展示返回的不确定性指标。## 4. 完整实战案例 ### 4.1 创建项目结构 按以下命令创建项目目录 bash new-face-new-unc/ ├── backend/ └── frontend/在backend中创建main.py、model.py、uncertainty.py内容分别参考第 3 章代码。frontend使用 Vite 创建。4.2 启动后端服务在虚拟环境中执行uvicorn main:app --reload --host 0.0.0.0 --port 8000启动成功后控制台会输出类似信息INFO: Uvicorn running on http://0.0.0.0:8000 INFO: Application startup complete.此时可以访问http://127.0.0.1:8000/docs查看 FastAPI 自动生成的接口文档并直接测试/predict接口。4.3 启动前端服务打开新终端进入frontend目录cd frontend npm run dev启动后控制台会输出VITE v4.0.0 ready in 500 ms ➜ Local: http://127.0.0.1:5173/浏览器访问http://127.0.0.1:5173/即可看到页面。4.4 运行与验证选择一张图片点击上传。如果一切正常前端会显示指标示例值预测类别class_42最大概率0.8756置信度0.7269预测熵0.6931概率标准差0.0123不同图片会得到不同结果。你可以通过上传模糊图片、网络图片、手绘图片来观察指标的差异。通常模糊图片的熵值会明显升高置信度随之下降。4.5 预期结果说明如果你的结果中置信度长期偏高说明模型对当前输入比较自信如果上传一张完全无关的图片如风景图你会发现熵值偏高置信度下降这就是不确定性模块的价值所在——它能把“模型没见过这种东西”的信号传递出来。## 5. 常见问题与排查思路 尽管项目代码不复杂但在实际运行中依然会碰到不少问题。下面整理了我自己踩过的一些坑。 | 问题现象 | 常见原因 | 解决思路 | | --- | --- | --- | | 启动时报错 No module named torch | 虚拟环境未激活或依赖安装失败 | 确认执行了 pip install torch并检查当前终端是否在虚拟环境 | | 上传图片后报 500 错误 | 图片格式不支持或上传空文件 | 检查图片是否为 JPG/PNG并确认文件内容非空 | | 前端无法访问后端接口 | 跨域配置缺失或端口不一致 | 确认 FastAPI 已添加 CORS 中间件检查前端 API_BASE 是否指向 8000 端口 | | 模型推理速度很慢 | 当前使用 CPU 推理 | 这是正常现象如果要求高性能建议安装 GPU 版 PyTorch | | 熵值计算结果为 nan | 概率向量中出现 0 导致 log(0) | 计算熵前使用 np.clip(probs, 1e-10, 1.0) 限制最小概率 | | 预测标签显示为 class_42无法看懂 | 没有加载类别标签映射 | 初始化模型时加载 ImageNet 标签文件将索引映射为真实类别 | ### 5.1 关于 GPU 版本问题 如果你本机有 NVIDIA 显卡并且想用 GPU 推理需要单独安装对应 CUDA 版本的 PyTorch。建议从 PyTorch 官网复制安装命令避免直接在 pip install torch 时装了 CPU 版本。安装完成后可以在 Python 中验证 python import torch print(torch.cuda.is_available())输出为True说明 GPU 可用。5.2 关于标签映射在演示中直接使用class_索引是为了简化但如果要在业务中使用建议下载imagenet_classes.txt或使用torchvision提供的类别映射。示例代码如下with open(imagenet_classes.txt) as f: labels [line.strip() for line in f.readlines()] # 预测后 label labels[label_index]标签文件可以从 TorchVision 官方资源或 GitHub 下载注意文件编码统一为 UTF-8。5.3 关于前端跨域FastAPI 的 CORS 中间件中allow_origins[*]适合本地开发。如果部署到生产环境建议配置具体的域名白名单避免任何网站都能调用你的预测接口否则会造成不必要的资源消耗和安全隐患。## 6. 最佳实践与工程建议 ### 6.1 模型与数据管理 - **不要把预训练模型文件直接放在代码目录**。建议使用独立的 models/ 目录并通过环境变量指定路径方便以后切换不同模型版本。 - **推理服务应该保持模型单例**。不要在每次请求时重新加载模型这会严重拖慢响应速度。 - **建议记录模型的版本号和输入数据格式**。模型更新后历史预测记录才能回溯。 例如可以简单创建一个模型配置对象 python MODEL_CONFIG { name: resnet18, pretrained: True, input_size: 224, version: 2025.01 }这样在预测结果中带上model_version前端和日志系统都能看到每一条结果来自哪个模型。6.2 不确定性指标的使用边界虽然熵和置信度是有用的信号但不要过度依赖。以下是几条建议不要只用一个指标做人工复核判断。可以把“最大概率 0.8 且熵 0.3”作为高置信度条件其他情况一律进入人工审核。不同模型间的熵值不可直接比较。类别数量不同熵的范围也不同需要先做归一化。对于安全相关场景应该引入更严格的不确定性估计方法如 MC Dropout 或深度集成而不是只依赖单次前向传播。6.3 接口安全性如果后端服务要暴露在公网至少要做好以下三件事增加请求大小限制防止恶意用户上传超大图片。增加鉴权逻辑比如简单的 API Key 校验。记录请求日志包括文件名、大小、IP、耗时、指标结果。FastAPI 中可以这样限制请求体大小from fastapi import FastAPI, File, UploadFile, HTTPException MAX_FILE_SIZE 5 * 1024 * 1024 # 5MB app.post(/predict) async def predict_endpoint(file: UploadFile File(...)): image_bytes await file.read() if len(image_bytes) MAX_FILE_SIZE: raise HTTPException(status_code413, detailFile too large)6.4 前后端分离部署本地开发时前端使用 Vite Dev Server前后端通过不同端口通信。生产部署时建议用 Nginx 托管前端静态文件并将/api反向代理到后端服务这样浏览器只需要访问同一个域名避免跨域和端口暴露问题。Nginx 配置片段参考server { listen 80; server_name your-domain.com; location / { root /var/www/frontend; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8000/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }注意这样配置后前端请求地址要从http://127.0.0.1:8000改为/api否则反向代理不会生效。6.5 代码可维护性建议把不确定性计算独立成模块不要散落在接口代码中方便后续替换为贝叶斯方法或集成方法。接口返回值建议统一格式例如{code: 0, data: {...}, message: success}方便前端统一处理错误。对异常分支要写日志捕获避免用户上传的文件格式不合法时返回晦涩的 500 错误。## 7. 总结与学习路线 从创意命名到代码落地New Face New UNC 这套原型展示了如何把一个“模型输出概率”的普通接口升级为“模型输出概率 不确定性信号”的完整分析服务。本文重点包括 - 理解了什么是不确定性UNC以及它和传统置信度的区别。 - 掌握如何用 PyTorch 加载预训练模型并进行图像分类推理。 - 实现预测熵、概率标准差、综合置信度等指标计算。 - 使用 FastAPI 封装推理服务并使用 Vue 3 搭建前端页面。 - 掌握了跨域、文件上传、SQLite 存储等基础工程落地技巧。 下一步你可以继续往这几个方向深入 - 尝试用 MC Dropout 或深度集成方法替代单次前向传播获得更可靠的不确定性估计。 - 将类别标签映射和模型版本管理接入推理服务提升可用性。 - 部署到云服务器或容器环境增加 Dockerfile 和 Nginx 配置。 - 在真实业务中设计人工审核流程根据不确定性指标动态决定是否需要人工介入。 如果未来你打算在真实项目中引入不确定性分析优先关注三点模型输入的图像质量、类别人数差异带来的熵值偏差、以及接口生产环境的安全隔离。项目源码已经具备雏形建议你动手跑一遍然后根据自己的业务需求改造。如果你在这套原型上遇到其他问题欢迎在评论区交流我会根据实际经验持续补充。