PyTorch深度学习实战 | 人工智能项目从训练到部署

在线体验各类最新模型,更有模型 免费Token 额度领取!
立即体验
简介: 本项目基于LSTM模型对污水处理厂总曝气量(旧区+新区)进行时序预测。通过数据清洗、Min-Max归一化、滑动窗口构造(12小时输入→预测未来1小时),构建并训练轻量级LSTM模型,支持API部署与实时调用,已实现端到端预测流程及模型保存。

 任务介绍

数据处理

旧区总曝气量=OldZone_Aeration= OldZone_Aeration1+ OldZone_Aeration2

新区总曝气量=NewZone_Aeration = NewZone_Aeration1 + NewZone_Aeration2

然后发现这两列中还是有很多0值,不能直接用于建模。所以我们把这两列数据继续相加作为新的

总曝气量

image.gif


LSTM对总曝气量进行建模

数据预处理

scaler = MinMaxScaler()          # 创建归一化工具
scaler.fit(data[:train_end])     # 只在【训练集】上学习最大最小值
data_s = scaler.transform(data)  # 用训练集学到的规则,给所有数据归一化

image.gif

整理数据

# 滑窗: X=[t-12,...,t-1], y=第(t+PRED_HORIZON-1)小时的值
def make_seq(arr, seq_len, horizon):
    X, y = [], []
    for i in range(len(arr) - seq_len - horizon + 1):
        X.append(arr[i : i+seq_len])
        y.append(arr[i+seq_len+horizon-1])
    return np.array(X), np.array(y)

image.gif

arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]    len(arr)=16

seq_len = 3 # 用过去3小时

horizon = 2 # 预测未来第2小时

i=12代表总共可以构造12组数据

i = 0

X:arr[0:3] → [0, 1, 2]

y:arr[0+3+2-1] = arr[4] → 4→ 用 0,1,2 预测 第 2 小时:4

最后:

X = [
 [0,1,2],
 [1,2,3],
 ...
 [11,12,13]
]
y = [4,5,6,7,8,9,10,11,12,13,14,15]

image.gif

模型的前向传播

输入 x          (64, 12,  1)
↓ 通过 LSTM
out             (64, 12, 64)
↓ 取最后时刻
out[:, -1, :]   (64,     64)
↓ 全连接层
输出            (64,      1)

image.gif

整体代码

import numpy as np
import pandas as pd
import torch
import torch.nn as nn
from torch.utils.data import DataLoader, TensorDataset
from sklearn.preprocessing import MinMaxScaler
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
import matplotlib.pyplot as plt
# ========== 1. 配置 ==========
SEQ_LEN       = 12      # 用过去12小时
PRED_HORIZON  = 1       # 预测未来第 t 小时, 改这里即可: 1, 2, 3...
BATCH_SIZE    = 64
EPOCHS        = 30
LR            = 1e-3
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# ========== 2. 数据准备 ==========
df = pd.read_csv('aeration.csv', parse_dates=['Date'])
data = df['Aeration'].values.reshape(-1, 1)
n = len(data)
train_end, val_end = int(n*0.7), int(n*0.85)
# 归一化(只用训练集 fit, 防止数据泄漏)
scaler = MinMaxScaler()
scaler.fit(data[:train_end])
data_s = scaler.transform(data)
# 滑窗: X=[t-12,...,t-1], y=第(t+PRED_HORIZON-1)小时的值
def make_seq(arr, seq_len, horizon):
    X, y = [], []
    for i in range(len(arr) - seq_len - horizon + 1):
        X.append(arr[i : i+seq_len])
        y.append(arr[i+seq_len+horizon-1])
    return np.array(X), np.array(y)
X, y = make_seq(data_s, SEQ_LEN, PRED_HORIZON)
n_samples = len(X)
train_size = int(n_samples * 0.7)
val_size = int(n_samples * 0.15)
X_tr, y_tr = X[:train_size], y[:train_size]
X_va, y_va = X[train_size : train_size+val_size], y[train_size : train_size+val_size]
X_te, y_te = X[train_size+val_size :], y[train_size+val_size :]
def to_loader(X, y, shuffle=False):
    ds = TensorDataset(torch.FloatTensor(X), torch.FloatTensor(y))
    return DataLoader(ds, batch_size=BATCH_SIZE, shuffle=shuffle)
train_loader = to_loader(X_tr, y_tr, shuffle=True)
val_loader   = to_loader(X_va, y_va)
test_loader  = to_loader(X_te, y_te)
# ========== 3. LSTM 模型 ==========
class LSTMModel(nn.Module):
    def __init__(self, hidden=64, layers=2):
        super().__init__()
        self.lstm = nn.LSTM(1, hidden, layers, batch_first=True, dropout=0.1)
        self.fc = nn.Linear(hidden, 1)
    def forward(self, x):
        out, _ = self.lstm(x)
        return self.fc(out[:, -1, :])
model = LSTMModel().to(device)
criterion = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=LR)
# ========== 4. 训练 ==========
train_losses, val_losses = [], []
best_val, best_state = float('inf'), None
for ep in range(1, EPOCHS+1):
    model.train()
    tl = 0
    for xb, yb in train_loader:
        xb, yb = xb.to(device), yb.to(device)
        optimizer.zero_grad()
        loss = criterion(model(xb), yb)
        loss.backward()
        optimizer.step()
        tl += loss.item() * xb.size(0)
    tl /= len(train_loader.dataset)
    model.eval()
    vl = 0
    with torch.no_grad():
        for xb, yb in val_loader:
            xb, yb = xb.to(device), yb.to(device)
            vl += criterion(model(xb), yb).item() * xb.size(0)
    vl /= len(val_loader.dataset)
    train_losses.append(tl); val_losses.append(vl)
    if vl < best_val:
        best_val, best_state = vl, {k: v.clone() for k, v in model.state_dict().items()}
    print(f"Epoch {ep:02d} | train {tl:.5f} | val {vl:.5f}")
model.load_state_dict(best_state)
# ========== 5. 测试评估 ==========
model.eval()
preds = []
with torch.no_grad():
    for xb, yb in test_loader:
        preds.append(model(xb.to(device)).cpu().numpy())
preds = np.concatenate(preds)
y_pred = scaler.inverse_transform(preds)
y_true = scaler.inverse_transform(y_te)
rmse = np.sqrt(mean_squared_error(y_true, y_pred))
mae  = mean_absolute_error(y_true, y_pred)
r2   = r2_score(y_true, y_pred)
print(f"\n========== 测试集 (预测未来第 {PRED_HORIZON} 小时) ==========")
print(f"RMSE = {rmse:.3f}")
print(f"MAE  = {mae:.3f}")
print(f"R²   = {r2:.4f}")

image.gif

image.gif

模型保存

文件 作用 必须保存吗
model.pt 模型权重 ✅ 必须
scaler.pkl 归一化参数 ✅ 必须(没它模型废掉)
config.json 模型结构 + 元信息 ✅ 必须(推理时构造模型用)
model.load_state_dict(best_state)
# ========== 保存部署所需文件 ==========
import joblib
from pathlib import Path
deploy_dir = Path(f'deploy_t+{PRED_HORIZON}h')
deploy_dir.mkdir(exist_ok=True)
torch.save(model.state_dict(), deploy_dir / 'model.pt')
joblib.dump(scaler, deploy_dir / 'scaler.pkl')
print(f"已保存: {deploy_dir}/model.pt, {deploy_dir}/scaler.pkl")

image.gif

import torch
import torch.nn as nn
import joblib
import numpy as np
# ========== 1. 模型结构(必须和训练时完全一致) ==========
class LSTMModel(nn.Module):
    def __init__(self, hidden=64, layers=2):
        super().__init__()
        self.lstm = nn.LSTM(1, hidden, layers, batch_first=True, dropout=0.1)
        self.fc = nn.Linear(hidden, 1)
    def forward(self, x):
        out, _ = self.lstm(x)
        return self.fc(out[:, -1, :])
# ========== 2. 加载模型和 scaler ==========
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = LSTMModel(hidden=64, layers=2).to(device)
model.load_state_dict(torch.load('deploy_t+1h/model.pt', map_location=device))
model.eval()    # 切换到推理模式, 关闭 dropout
scaler = joblib.load('deploy_t+1h/scaler.pkl')
print("模型加载完成")
# ========== 3. 准备一个样本 (过去 12 小时的 Aeration 值) ==========
history = [
561.1596221923829,
562.870346069335,
566.306594848633,
565.813049316406,
565.824310302735,
565.37336730957,
493.188201904297,
379.360862731933,
379.547348047461,
377.33290863037,
530.632003784179,
557.160324096679
]
# 转成 numpy, reshape 成 [12, 1]
x = np.array(history, dtype=np.float32).reshape(-1, 1)
# 用训练时的 scaler 归一化
x_scaled = scaler.transform(x)
# 转成 tensor, 加 batch 维度: [1, 12, 1]
x_tensor = torch.from_numpy(x_scaled).unsqueeze(0).to(device)
# ========== 4. 推理 ==========
with torch.no_grad():
    pred_scaled = model(x_tensor).cpu().numpy()
# 反归一化得到真实数值
pred = scaler.inverse_transform(pred_scaled)
print(f"\n输入: 过去 12 小时的 Aeration = {history}")
print(f"预测: 下一小时的 Aeration = {pred[0][0]:.2f}")

image.gif

输入: 过去 12 小时的 Aeration = [561.1596221923829, 562.870346069335, 566.306594848633, 565.813049316406, 565.824310302735, 565.37336730957, 493.188201904297, 379.360862731933, 379.547348047461, 377.33290863037, 530.632003784179, 557.160324096679]

预测: 下一小时的 Aeration = 567.72

模型预测:567.72      真实值:557.32


是否需要转换模型

维度 你的场景 是否需要 ONNX
调用频率 小时级(每小时 1 次) ❌ 不需要
延迟要求 几秒内出结果即可 ❌ 不需要
并发量 可能就 1 个 SCADA / 1 个调用方 ❌ 不需要
部署语言 纯 Python ❌ 不需要
部署环境 厂里的 Linux 服务器(不是边缘设备) ❌ 不需要
模型大小 LSTM 很小(几十 KB) ❌ 不需要

如何变成api的调用?

# ━━━━━━━━━━━━━━ 第 1 块: 导入工具 ━━━━━━━━━━━━━━
from fastapi import FastAPI
from pydantic import BaseModel
from typing import List
from predictor import AerationPredictor
# ━━━━━━━━━━━━━━ 第 2 块: 初始化 ━━━━━━━━━━━━━━━
app = FastAPI()
predictor = AerationPredictor()
# ━━━━━━━━━━━━━━ 第 3 块: 定义请求格式 ━━━━━━━━━━━
class PredictRequest(BaseModel):
    history: List[float]
# ━━━━━━━━━━━━━━ 第 4 块: 定义接口 (根路径) ━━━━━━━
@app.get("/")
def root():
    return {"message": "Aeration API is running. Visit /docs for documentation."}
# ━━━━━━━━━━━━━━ 第 5 块: 定义接口 (预测) ━━━━━━━━━
@app.post("/predict")
def predict(req: PredictRequest):
    pred = predictor.predict(req.history)
    return {"prediction": pred}

image.gif

第 1 部分:导入工具

from fastapi import FastAPI
from pydantic import BaseModel
from typing import List
from predictor import AerationPredictor

image.gif

第 2 部分:创建应用 + 加载模型

app = FastAPI()
predictor = AerationPredictor()

image.gif

创建一个 FastAPI 应用实例  ,创建一个预测器实例(模型在这里被加载到内存)

appFastAPI 类创建出来的实例(对象)

predictorAerationPredictor 类创建出来的实例(对象)

它们都会继承相应的类的方法

app是什么:

  • 是整个 Web 服务的核心对象
  • 后面所有的 @app.get("/")、@app.post("/predict") 都是在给这个 app 注册接口

类比:你开了家"快递公司", app就是公司本身。后面要往这个公司里加"业务窗口"(接口)。

第 3 部分:定义请求数据的"形状"

class PredictRequest(BaseModel):
    history: List[float]

image.gif

# 客户端发的 JSON
{
    "history": [520.1, 525.3, 530.0, ...]
}
# 对应的 Python 类
class PredictRequest(BaseModel):
    history: List[float]    # ← 字段名要和 JSON 的 key 一致

image.gif

✔ 前端必须传一个 JSON

✔ JSON 里必须有一个字段叫 history

history必须是数字列表

✔ 传错了系统直接报错,不允许进入预测

✔ 自动生成接口文档

自动把 JSON 转成 Python 对象

函数体:业务逻辑

# 定义接口函数:接收请求 → 预测 → 返回结果
def predict(req: PredictRequest):
    # 调用预测模型,传入历史数据
    pred = predictor.predict(req.history)
    # 返回预测结果(JSON格式)
    return {"prediction": pred}

image.gif

# 客户端发的 JSON:
{"history": [520.1, 525.3, ...]}
# 在函数里访问:
req.history   # 等于 [520.1, 525.3, ...]
# 客户端收到的响应:
{"prediction": 562.34}

image.gif

定义一个函数predict,接收的数据是:req: PredictRequest,req = 请求数据(前端传过来的内

容),PredictRequest = 你刚才定义的数据格式(必须包含 history 列表)【FastAPI 会自动校验

前端传的数据对不对】。

image.gif 编辑

image.gif

@app.get("/")

image.gif 编辑

这是一个"装饰器"(decorator),"嘿 FastAPI!下面这个函数,请把它注册成一个 GET 接口,路

径是 /。当有人访问 / 时,就调用这个函数。"

GET vs POST 的区别

HTTP 方法 用途 特点
GET 读取数据 没有请求体,参数在 URL 里
POST 提交数据/触发动作 有请求体(JSON)
GET 请求: 查询天气
GET http://example.com/weather?city=beijing
没有 body, 参数在 URL 里
POST 请求: 提交订单
POST http://example.com/orders
Body: {"product": "iPhone", "quantity": 2}
有 body, 数据在 body 里

image.gif

@app.post("/predict")

装饰器:注册一个 POST 接口,路径 /predict。

完整 URL:http://你的服务器IP:8000/predict

为什么 /predict 用 POST 而不是 GET:

           客户端要传 12 个浮点数过来

           12 个数放 URL 里又长又难看

           而且预测是个"动作"(触发模型推理),符合 POST 的语义。

def predict(req: PredictRequest):
                类型标注: req 是 PredictRequest 类型

image.gif

req.history

image.gif

# 客户端发的 JSON:
{"history": [520.1, 525.3, 530.0, ...]}
# 在函数里:
req.history   等于   [520.1, 525.3, 530.0, ...]

image.gif

pred = 562.34    # 一个 float

image.gif

HTTP/1.1 200 OK
Content-Type: application/json
{"prediction":562.34}

image.gif

调用api的代码

用 Python 通过网络调用你的 API,拿到预测结果。

你的 Python 程序                      你的 API 服务 (FastAPI)
─────────────────                    ─────────────────────
   发请求 ──HTTP POST──→               接收请求
                                      调用模型预测
   收响应 ←──JSON 数据──                返回结果
   打印结果

image.gif

import requests                                          # ← 第 1 行: 导入工具
response = requests.post(                                # ← 第 2 行: 发送请求
    'http://127.0.0.1:8000/predict',                     # ← 第 3 行: 目标地址
    json={'history': [520.1, 525.3, ..., 560.0]}         # ← 第 4 行: 发送的数据
)
print(response.json())                                    # ← 第 5 行: 打印响应
# 输出: {'prediction': 554.8828735351562}                 # ← 第 6 行: 预期输出

image.gif

客户端的 URL                   服务端的代码
http://127.0.0.1:8000/predict
                端口 8000     @app.post("/predict")
                  ↓                   ↑
                路径 /predict          ↑
                  ↓                   ↑
                  └───── 匹配 ─────────┘

image.gif

┌─────────────────────────────────────────────────────────┐
│  客户端 (Python requests)                                │
│  requests.post(                                          │
│    'http://127.0.0.1:8000/predict',                     │
│    json={'history': [520.1, 525.3, ..., 560.0]}         │
│  )                                                       │
└────────────────────────┬────────────────────────────────┘
                         │ HTTP POST 请求
                         │ Body (JSON): {"history": [...]}
┌─────────────────────────────────────────────────────────┐
│  FastAPI 接收到请求                                       │
│                                                          │
│  Step 1: 看路径是 /predict, 方法是 POST                   │
│          ↓ 找到对应的处理函数 predict()                   │
│                                                          │
│  Step 2: 读取请求体, 解析 JSON                            │
│          ↓ {"history": [520.1, ..., 560.0]}             │
│                                                          │
│  Step 3: 用 PredictRequest 校验                          │
│          ✓ history 字段存在                              │
│          ✓ 是 list 类型                                  │
│          ✓ 内部元素都是 float                            │
│          ↓ 构造 PredictRequest 实例                      │
│                                                          │
│  Step 4: 调用 predict(req=该实例)                        │
└────────────────────────┬────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│  你的代码执行:                                            │
│                                                          │
│  pred = predictor.predict(req.history)                  │
│         │                                                │
│         ├─ 归一化输入                                     │
│         ├─ LSTM 推理                                      │
│         └─ 反归一化输出                                    │
│         ↓                                                │
│  pred = 562.34                                          │
│                                                          │
│  return {"prediction": 562.34}                          │
└────────────────────────┬────────────────────────────────┘
                         │ FastAPI 把字典转成 JSON
┌─────────────────────────────────────────────────────────┐
│  HTTP 响应                                                │
│  Status: 200 OK                                          │
│  Body: {"prediction": 562.34}                           │
└────────────────────────┬────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│  客户端收到响应                                            │
│  response.json() = {"prediction": 562.34}               │
└─────────────────────────────────────────────────────────┘

image.gif


目录
相关文章
|
28天前
|
安全 Windows
DaemonTool_10.6.0.275安装步骤详解(附虚拟光驱挂载ISO与MDF镜像教程)
DAEMON Tools Lite 10.6.0.275(安装包DaemonTool_10.6.0.275.exe)是一款轻量免费虚拟光驱工具,支持ISO、MDS/MDF等数十种镜像格式,无需物理光驱即可挂载运行,兼容WinXP至Win11,安装需以管理员身份运行。(239字)
|
28天前
|
存储 人工智能 自然语言处理
拒绝“大模型幻觉”:一文彻底搞懂 RAG(检索增强生成)技术全流程
本文深入解析RAG(检索增强生成)技术,直击大模型落地私有知识场景的核心痛点——如何让LLM精准、低成本、高时效地基于企业文档作答。从文本分片、向量化索引,到召回重排、增强生成,系统拆解五大关键步骤,揭示RAG作为“AI外挂”的底层逻辑与工程实践精髓。
拒绝“大模型幻觉”:一文彻底搞懂 RAG(检索增强生成)技术全流程
|
28天前
|
弹性计算 人工智能 网络安全
阿里云官方镜像一键部署OpenClaw保姆级教程:从实例创建到服务上线全流程
OpenClaw(原Clawdbot/Moltbot)是一款开源的本地优先AI代理与自动化平台,能通过自然语言调用浏览器、文件系统、邮件等工具,完成文档整理、邮件处理、日程安排等实际任务,堪称“能替你干活的AI数字员工”。2026年,阿里云官方推出OpenClaw专属应用镜像,依托轻量应用服务器与ECS云服务器,提供一键部署能力,无需手动配置复杂运行环境,零基础用户也能快速搭建专属AI助理,实现7×24小时稳定运行。
285 2
|
28天前
|
人工智能 Oracle 机器人
推理 → 行动 → 观察:用 LangChain + Python 实现一个智能体循环
智能体循环(Agentic Loop)突破单次问答局限,通过“推理→行动→观察”迭代闭环,让AI能自主分解任务、调用工具、持续优化直至目标完成,是构建真正自动化智能体的核心架构。
294 9
推理 → 行动 → 观察:用 LangChain + Python 实现一个智能体循环
|
28天前
|
存储 人工智能 运维
千亿级 AI 搜索的效能实战:从混合检索到 Agentic RAG 的三年实战
本文为2026 Elastic中国大会演讲实录,直击千亿级AI搜索三大挑战:搜索融合(关键词+向量+稀疏检索原生一体)、极致效能(冷热分层、硬件降级、自研FalconSeek引擎)与Agentic RAG演进(结构化知识图谱+智能体自主推理),揭示企业级AI搜索从“能用”到“好用”再到“自进化”的实战路径。
468 8
|
28天前
|
数据采集 存储 算法
视频 RAG 中分块策略:基于停顿、滑动窗口与基于 LLM 的方法
本文探讨视频RAG中的核心挑战——如何为无时间结构的视频转录文本设计有效分块策略。对比传统文本分块,提出基于停顿、重叠窗口、递归切分及LLM驱动的主题分块四层方案,实现细粒度检索与全局理解兼顾,提升视频内容检索准确性与上下文完整性。
180 13
视频 RAG 中分块策略:基于停顿、滑动窗口与基于 LLM 的方法
|
28天前
|
人工智能 前端开发 安全
周一上线|1M 上下文成标配,GPT-5.5 更会干活;Google 拟最高 400 亿美元加码 Anthropic
先看模型这边,沉寂 15 个月后,DeepSeek 终于发布了 V4 预览版,而且照例开源。最猛的是,它直接把 1M 上下文做成了标配,量大管饱。OpenAI 那边,比 DeepSeek 早一天发布的 GPT-5.5,主打“更人性化”,在带起一众模型“稳稳接住”“懂你所想”的风潮之后,这回 GPT-5.5 不弯不绕,反倒显得清新脱俗。当然,模型的重点还是能力:Agentic Coding、Computer Use 和复杂任务处理,GPT-5.5 较之前版本都有很大提升。
369 1
周一上线|1M 上下文成标配,GPT-5.5 更会干活;Google 拟最高 400 亿美元加码 Anthropic
|
28天前
|
弹性计算 前端开发 Ubuntu
阿里云服务器ECS的租用教程和简单的前端页面部署
本文详解阿里云学生福利领取(含300元卡券)及ECS轻量服务器选购与部署全流程:涵盖学生机免费申领、配置选型建议(Ubuntu/CentOS/Windows)、安全组设置、Nginx安装、网页部署及Xshell远程连接等实操步骤,新手友好。
303 8
|
28天前
|
人工智能 机器人 Shell
【开源】龙虾人工智能 —— 完全本地化的机器人大脑!不联网、不付 API 费、能看能说能理解!
龙虾本地化AI(Lobster AI)是一款完全离线、零成本、零隐私泄露的开源机器人系统,支持文本推理(Gemma4)、多模态视觉理解(桌面/摄像头)、语音识别与合成(Sherpa-ONNX),纯本地运行,不依赖任何云服务。
374 2
【开源】龙虾人工智能 —— 完全本地化的机器人大脑!不联网、不付 API 费、能看能说能理解!
|
17天前
|
弹性计算 应用服务中间件 API
阿里云 ECS 部署 AI 应用全流程实战:从选型到上线的完整指南
当百炼 API 无法满足私有化部署需求时,在阿里云 ECS 上自主部署大模型成为企业级方案的首选。本文从实际项目需求出发,完整演示在阿里云 ECS 上部署 Qwen3 模型的全流程:GPU 实例选型 → 环境配置 → vLLM 推理引擎部署 → Nginx 反向代理 → API 服务上线,并提供详细的成本分析、性能优化技巧和生产环境运维经验。

热门文章

最新文章