一、项目简介
手写数字识别是深度学习入门项目中的经典场景,但如果只停留在模型训练脚本层面,就很难体现真实业务系统的完整链路。本文实现一个 Python 全栈项目--基于深度学习的手写识别系统:用户可以注册登录,在浏览器中绘制 28×28 手写数字,提交给后端 PyTorch 推理模型识别,并把自己的样本沉淀到数据库中,形成可持续优化的个人识别原型。
项目采用前后端分离架构:后端提供认证、样本、识别、历史接口;前端提供登录注册、画板绘制、训练样本提交、识别结果展示。为了适合离线学习和部署,推理部分使用轻量 PyTorch 原型分类网络 PrototypeDigitNet,不依赖外部大模型权重,但完整保留了 Tensor 预处理、特征池化、模型前向推理、Softmax 评分和结果记录等核心步骤。
二、技术栈
- 后端:FastAPI、SQLAlchemy、SQLite、Pydantic、PyTorch
- 前端:Vue 3、Vite、Fetch API
- 认证:PBKDF2 密码哈希、Bearer Token
- 数据存储:SQLite 本地数据库
- 项目打包:zip 压缩包
三、系统架构
text
浏览器(Vue 3 + Vite)
├── 注册/登录页面
├── 28×28 手写画板
├── 样本采集模块
└── 识别历史页面
│ HTTP + Bearer Token
▼
FastAPI 后端服务
├── Auth API:注册、登录、退出、当前用户
├── Sample API:保存与查询训练样本
├── Recognition API:PyTorch 预处理、模型推理、记录结果
└── Stats API:统计用户样本和识别次数
│ SQLAlchemy ORM
▼
SQLite 数据库
├── users
├── access_tokens
├── handwriting_samples
└── recognition_records
该架构既能满足课程设计、毕业设计、项目实训展示,也方便后续把当前 PyTorch 原型分类器替换成训练好的 CNN、CRNN 或 ONNX 推理模型。
四、功能模块
- 用户模块
- 用户注册
- 用户登录
- Token 鉴权
- 退出登录
- 手写识别模块
- 浏览器绘制 28×28 灰度图
- 后端使用 PyTorch Tensor 归一化输入数据
- 通过自适应平均池化下采样为 8×8 特征向量
PrototypeDigitNet计算输入与数字原型的距离并输出 Softmax 概率
- 样本管理模块
- 保存当前画板为训练样本
- 按用户隔离样本数据
- 动态生成用户个性化数字原型
- 历史记录模块
- 保存每次识别结果
- 查询最近识别历史
- 统计模块
- 样本数量
- 识别次数
- 各数字样本分布
五、数据库/数据模型设计
1. 用户表 users
| 字段 | 类型 | 说明 |
|---|---|---|
| id | Integer | 用户 ID |
| username | String | 用户名,唯一 |
| password_hash | String | PBKDF2 哈希密码 |
| created_at | DateTime | 创建时间 |
2. 访问令牌表 access_tokens
| 字段 | 类型 | 说明 |
|---|---|---|
| id | Integer | Token ID |
| token | String | Bearer Token |
| user_id | Integer | 所属用户 |
| created_at | DateTime | 创建时间 |
3. 手写样本表 handwriting_samples
| 字段 | 类型 | 说明 |
|---|---|---|
| id | Integer | 样本 ID |
| digit | Integer | 真实数字标签 |
| grid | Text | 28×28 灰度数组 JSON |
| source | String | 样本来源 |
| user_id | Integer | 所属用户 |
| created_at | DateTime | 创建时间 |
4. 识别记录表 recognition_records
| 字段 | 类型 | 说明 |
|---|---|---|
| id | Integer | 记录 ID |
| predicted_digit | Integer | 预测数字 |
| confidence | Float | 置信度 |
| grid | Text | 输入图像数组 |
| user_id | Integer | 所属用户 |
| created_at | DateTime | 创建时间 |
六、后端接口设计
| 方法 | 路径 | 说明 | 鉴权 |
|---|---|---|---|
| GET | /api/health |
健康检查 | 否 |
| POST | /api/auth/register |
注册 | 否 |
| POST | /api/auth/login |
登录 | 否 |
| POST | /api/auth/logout |
退出登录 | 可选 |
| GET | /api/users/me |
当前用户 | 是 |
| POST | /api/samples |
新增样本 | 是 |
| GET | /api/samples |
样本列表 | 是 |
| POST | /api/recognize |
手写识别 | 是 |
| GET | /api/records |
识别历史 | 是 |
| GET | /api/stats |
数据统计 | 是 |
其中核心业务接口均要求携带:
http
Authorization: Bearer <access_token>
七、前端页面设计
前端使用 Vue 3 + Vite,主要页面区域如下:
- 登录/注册卡片:未登录时展示,登录成功后保存 Token 到
localStorage。 - 顶部信息区:展示项目名称、技术栈和退出按钮。
- 手写画板:使用 28×28 网格模拟灰度像素画布。
- 样本训练区:选择数字标签,把当前画板提交为训练样本。
- 最近样本列表:展示用户提交过的训练样本。
- 识别历史列表:展示最近识别结果和置信度。
这种设计可以清晰体现全栈项目中的"用户操作 -> API 请求 -> 数据库存储 -> 页面反馈"闭环。
八、核心代码讲解
1. FastAPI 应用入口
backend/app/main.py 中创建数据库表、配置跨域,并声明认证依赖:
python
def current_user(authorization: str = Header(default=""), db: Session = Depends(get_db)):
if not authorization.lower().startswith("bearer "):
raise HTTPException(status_code=401, detail="Missing bearer token")
token = authorization.split(" ", 1)[1].strip()
user = crud.get_user_by_token(db, token)
if not user:
raise HTTPException(status_code=401, detail="Invalid or expired token")
return user
后续样本、识别、历史接口都通过 Depends(current_user) 保护,确保不同用户的数据不会互相访问。
2. 密码哈希与 Token 登录
backend/app/crud.py 使用标准库 hashlib.pbkdf2_hmac 进行密码哈希:
python
def hash_password(password: str) -> str:
salt = secrets.token_hex(16)
digest = hashlib.pbkdf2_hmac("sha256", password.encode(), salt.encode(), 120_000)
return f"pbkdf2_sha256$120000${salt}${digest.hex()}"
登录成功后生成随机 Token,并写入 access_tokens 表。前端后续请求统一携带 Bearer Token。
3. PyTorch 手写数字识别流程
系统接收长度为 784 的数组,代表 28×28 灰度图。后端先使用 PyTorch Tensor 完成归一化和自适应池化:
python
def downsample_to_8x8(grid):
values = torch.tensor(list(grid), dtype=torch.float32).view(1, 1, 28, 28)
pooled = F.adaptive_avg_pool2d(values, output_size=(8, 8)).view(-1)
max_value = torch.clamp(pooled.max(), min=1.0)
normalized = torch.clamp(pooled / max_value, 0.0, 1.0)
return [round(float(v), 4) for v in normalized.tolist()]
随后进入轻量级 PyTorch 模型 PrototypeDigitNet:
python
class PrototypeDigitNet(nn.Module):
def __init__(self, temperature: float = 4.0):
super().__init__()
self.temperature = temperature
def forward(self, features, prototypes):
distances = torch.cdist(features, prototypes, p=2)
logits = -distances * self.temperature
return torch.softmax(logits, dim=1)
模型把输入特征与 0-9 数字原型计算距离,将负距离转换为 logits,再通过 Softmax 输出每个数字的概率。若用户提交了自己的训练样本,系统会按用户维度重新计算对应数字的原型均值,因此同一套 PyTorch 推理代码可以逐步适配个人书写习惯。
4. Vue API 封装
frontend/src/api.js 封装所有 HTTP 请求,并自动添加 Token:
javascript
async function request(path, options = {}) {
const headers = { 'Content-Type': 'application/json', ...(options.headers || {}) }
if (token) headers.Authorization = `Bearer ${token}`
const response = await fetch(`${API_BASE}${path}`, { ...options, headers })
const data = await response.json().catch(() => ({}))
if (!response.ok) throw new Error(data.detail || '请求失败')
return data
}
这样 Vue 组件只需要调用 api.recognize()、api.addSample() 等方法即可。
5. 画板绘制逻辑
Vue 组件使用 784 个 div 表示像素点,鼠标划过时提高灰度值:
javascript
function paint(index) {
drawing.value = true
const next = [...grid.value]
const neighbors = [index, index - 1, index + 1, index - 28, index + 28]
neighbors.forEach(i => { if (i >= 0 && i < next.length) next[i] = Math.min(1, next[i] + 0.6) })
grid.value = next
}
虽然实现轻量,但已经具备真实手写识别系统的输入交互形式。
九、部署与运行步骤
1. 启动后端
bash
cd project/backend
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
后端默认地址:http://127.0.0.1:8000,接口文档:http://127.0.0.1:8000/docs。
2. 启动前端
bash
cd project/frontend
npm install
npm run dev
前端默认访问地址:http://127.0.0.1:5173。
3. 体验流程
- 注册账号,例如
demo / 123456。 - 登录后绘制一个数字。
- 点击"开始识别"。
- 选择真实标签并保存训练样本。
- 查看识别历史与统计信息。
十、可扩展方向
- 将当前 PyTorch 原型分类器替换为训练好的 CNN
.pt/.pth权重。 - 增加图片上传识别功能。
- 增加管理员样本审核模块。
- 增加模型版本管理与在线评估。
- 使用 Redis 管理 Token 或会话。
- 使用 Docker Compose 部署前后端。
十一、项目总结
本文完成了一个完整的手写识别全栈项目。它不仅包含 PyTorch 模型推理逻辑,还包含用户系统、认证鉴权、数据模型、后端 API、Vue 前端页面和运行部署说明。相比单独的算法 Demo,这类项目更接近真实业务系统,适合用于课程设计、项目实训和毕业设计原型。