Files
pickling-mes/backend/train_models.py

331 lines
12 KiB
Python
Raw Normal View History

"""
本地训练脚本 生成合成数据训练 MLP导出 ONNX
运行方式 backend/ 目录下
python train_models.py # 纯合成数据
python train_models.py --use-real-data # 混入生产实绩10× 权重)
依赖仅本地训练用不进 Docker
pip install torch onnx onnxruntime scikit-learn numpy
"""
import sys, json, time, argparse
from pathlib import Path
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset
sys.path.insert(0, str(Path(__file__).parent))
from app.services.prediction import (
AcidSpeedModel, TensionModel, QualityPredictionModel,
_SAMPLE_FILE, get_sample_stats,
)
PT_DIR = Path(__file__).parent / "app" / "services" / "pt_models"
PT_DIR.mkdir(parents=True, exist_ok=True)
SEED = 2024
N = 12000
np.random.seed(SEED)
torch.manual_seed(SEED)
TENSION_ZONES = [
"inlet", "s1_roller", "acid_entry",
"acid1", "acid2", "acid3",
"rinse", "leveler", "s2_roller", "outlet",
]
REAL_SAMPLE_WEIGHT = 10 # 每条真实样本复制次数(等效权重)
# ─── 网络结构 ───────────────────────────────────────────────────────────────
class MLP(nn.Module):
def __init__(self, in_dim: int, out_dim: int, hidden=(128, 64, 32)):
super().__init__()
layers: list = []
prev = in_dim
for h in hidden:
layers += [nn.Linear(prev, h), nn.ReLU()]
prev = h
layers.append(nn.Linear(prev, out_dim))
self.net = nn.Sequential(*layers)
def forward(self, x: torch.Tensor) -> torch.Tensor:
return self.net(x)
# ─── 训练通用函数 ───────────────────────────────────────────────────────────
def fit(model: nn.Module, X: np.ndarray, y: np.ndarray,
epochs=300, lr=1e-3, batch_size=512) -> nn.Module:
Xt = torch.from_numpy(X)
yt = torch.from_numpy(y)
dl = DataLoader(TensorDataset(Xt, yt), batch_size=batch_size, shuffle=True)
opt = optim.AdamW(model.parameters(), lr=lr, weight_decay=1e-4)
sched = optim.lr_scheduler.CosineAnnealingLR(opt, epochs)
loss_fn = nn.MSELoss()
model.train()
for ep in range(1, epochs + 1):
tot = 0.0
for xb, yb in dl:
opt.zero_grad()
loss = loss_fn(model(xb), yb)
loss.backward()
opt.step()
tot += loss.item() * len(xb)
sched.step()
if ep % 100 == 0:
print(f" ep {ep:3d}/{epochs} RMSE={((tot/len(Xt))**0.5):.5f}")
return model
def z_scale(arr: np.ndarray, mean=None, std=None):
if mean is None:
mean = arr.mean(axis=0)
std = arr.std(axis=0) + 1e-8
return ((arr - mean) / std).astype(np.float32), mean, std
def export_onnx(model: nn.Module, in_dim: int, path: Path):
model.eval()
dummy = torch.zeros(1, in_dim)
torch.onnx.export(
model, dummy, str(path),
input_names=["input"], output_names=["output"],
dynamic_axes={"input": {0: "batch"}, "output": {0: "batch"}},
opset_version=17,
)
print(f"{path.name} ({path.stat().st_size//1024} KB)")
# ─── 读取生产实绩样本 ────────────────────────────────────────────────────────
def load_real_samples(model_name: str):
"""
production_samples.jsonl 读取指定模型的实绩样本
返回 (X_real, y_real) numpy 数组 (None, None)
"""
if not _SAMPLE_FILE.exists():
return None, None
Xs, ys = [], []
with open(_SAMPLE_FILE) as f:
for line in f:
try:
r = json.loads(line)
if r.get("model") != model_name:
continue
inp = r.get("inputs")
if not inp:
continue
if model_name == "acid_speed":
spd = r.get("actual_speed")
if spd is None: continue
Xs.append(inp[:14])
ys.append([spd])
elif model_name == "tension":
kn = r.get("actual_kn")
if kn is None: continue
zone = r.get("zone")
if zone not in TENSION_ZONES: continue
# 单区段样本:只校准该区段,其他用模型预测填充
m = TensionModel(inp[0], inp[1], inp[2], inp[3])
res = m.calculate()
tensions = [res["zones"][z]["tension_kN"] for z in TENSION_ZONES]
tensions[TENSION_ZONES.index(zone)] = kn
Xs.append(inp[:4])
ys.append(tensions)
elif model_name == "quality":
ag = r.get("actual_grade")
if ag is None: continue
grade_map = {"A1": 95.0, "A2": 85.0, "B1": 75.0, "B2": 65.0, "C": 50.0}
target_pi = grade_map.get(ag, 75.0)
Xs.append(inp[:6])
ys.append([target_pi, target_pi]) # pi ≈ surface as proxy
except Exception:
continue
if not Xs:
return None, None
print(f" 实绩样本: {model_name} = {len(Xs)} 条 (将按 {REAL_SAMPLE_WEIGHT}× 权重混入)")
return np.array(Xs, np.float32), np.array(ys, np.float32)
def mix_with_real(X_syn: np.ndarray, y_syn: np.ndarray,
X_real, y_real) -> tuple:
"""将真实样本重复 REAL_SAMPLE_WEIGHT 次后拼接到合成数据尾部。"""
if X_real is None or len(X_real) == 0:
return X_syn, y_syn
X_r = np.tile(X_real, (REAL_SAMPLE_WEIGHT, 1))
y_r = np.tile(y_real, (REAL_SAMPLE_WEIGHT, 1))
return np.concatenate([X_syn, X_r], axis=0), np.concatenate([y_syn, y_r], axis=0)
# ─── 1. 酸洗速度模型 ────────────────────────────────────────────────────────
# 输入(14): thickness, scale_weight, conc×6, temp×6
# 输出(1): max_speed
def gen_acid_speed(n: int):
rng = np.random.default_rng(SEED)
Xs, ys = [], []
skip = 0
while len(Xs) < n:
t = rng.uniform(0.5, 8.0)
sw = rng.uniform(4.0, 18.0)
conc = rng.uniform(60, 240, 6).tolist()
temp = rng.uniform(52, 87, 6).tolist()
tpi = rng.uniform(88, 97)
try:
m = AcidSpeedModel(thickness=t, width=1000.0, steel_grade="Q235",
acid_conc_list=conc, acid_temp_list=temp,
scale_weight=sw, target_pi=tpi)
spd = float(m.calculate()["max_speed"])
except Exception:
skip += 1
continue
steel_factor = rng.choice([0.92, 0.96, 1.00, 1.03, 1.06])
noise = rng.normal(1.0, 0.06)
spd_n = float(np.clip(spd * noise * steel_factor, 20, 180))
Xs.append([t, sw] + conc + temp)
ys.append([spd_n])
print(f" 合成样本: acid_speed = {len(Xs)} 条 (skipped {skip})")
return np.array(Xs, np.float32), np.array(ys, np.float32)
# ─── 2. 张力模型 ────────────────────────────────────────────────────────────
# 输入(4): thickness, width, yield_strength, tension_coef
# 输出(10): 10 区段张力 kN
def gen_tension(n: int):
rng = np.random.default_rng(SEED + 1)
Xs, ys = [], []
while len(Xs) < n:
t = rng.uniform(0.5, 8.0)
w = rng.uniform(600, 1600)
ys_= rng.uniform(150, 600)
tc = rng.uniform(0.15, 0.35)
m = TensionModel(thickness=t, width=w, yield_strength=ys_, tension_coef=tc)
res = m.calculate()
tensions = [res["zones"][z]["tension_kN"] for z in TENSION_ZONES]
noise = rng.normal(1.0, 0.04, 10)
tensions_n = [float(np.clip(v * noise[i], 0.1, 9999)) for i, v in enumerate(tensions)]
Xs.append([t, w, ys_, tc])
ys.append(tensions_n)
print(f" 合成样本: tension = {len(Xs)}")
return np.array(Xs, np.float32), np.array(ys, np.float32)
# ─── 3. 质量预测模型 ─────────────────────────────────────────────────────────
# 输入(6): thickness, avg_speed, acid_conc_avg, acid_temp_avg, scale_weight, fe_conc_avg
# 输出(2): pi_score, surface_score
def gen_quality(n: int):
rng = np.random.default_rng(SEED + 2)
Xs, ys = [], []
while len(Xs) < n:
t = rng.uniform(0.5, 8.0)
spd = rng.uniform(20, 180)
conc = rng.uniform(60, 240)
temp = rng.uniform(50, 90)
sw = rng.uniform(4, 18)
fe = rng.uniform(20, 130)
m = QualityPredictionModel(thickness=t, avg_speed=spd,
acid_conc_avg=conc, acid_temp_avg=temp,
scale_weight=sw, fe_conc_avg=fe)
res = m.calculate()
pi_n = float(np.clip(res["pi_score"] * rng.normal(1.0, 0.06), 0, 100))
suf_n = float(np.clip(res["surface_score"] * rng.normal(1.0, 0.06), 0, 100))
Xs.append([t, spd, conc, temp, sw, fe])
ys.append([pi_n, suf_n])
print(f" 合成样本: quality = {len(Xs)}")
return np.array(Xs, np.float32), np.array(ys, np.float32)
# ─── 主流程 ─────────────────────────────────────────────────────────────────
def main(use_real_data: bool = False):
scalers: dict = {}
t0 = time.time()
if use_real_data:
stats = get_sample_stats()
print(f"\n生产实绩样本统计: {stats}")
# ── 酸洗速度 ──
print("\n[1/3] 酸洗速度模型")
X, y = gen_acid_speed(N)
if use_real_data:
X, y = mix_with_real(X, y, *load_real_samples("acid_speed"))
Xn, Xm, Xs = z_scale(X)
yn, ym, ys_ = z_scale(y)
model = MLP(14, 1, hidden=(128, 64, 32))
print(" 训练中...")
fit(model, Xn, yn, epochs=300)
export_onnx(model, 14, PT_DIR / "acid_speed.onnx")
scalers["acid_speed"] = {
"X_mean": Xm.tolist(), "X_std": Xs.tolist(),
"y_mean": ym.tolist(), "y_std": ys_.tolist(),
}
# ── 张力 ──
print("\n[2/3] 张力模型")
X, y = gen_tension(N)
if use_real_data:
X, y = mix_with_real(X, y, *load_real_samples("tension"))
Xn, Xm, Xs = z_scale(X)
yn, ym, ys_ = z_scale(y)
model = MLP(4, 10, hidden=(64, 64, 32))
print(" 训练中...")
fit(model, Xn, yn, epochs=300)
export_onnx(model, 4, PT_DIR / "tension.onnx")
scalers["tension"] = {
"X_mean": Xm.tolist(), "X_std": Xs.tolist(),
"y_mean": ym.tolist(), "y_std": ys_.tolist(),
"zone_names": TENSION_ZONES,
}
# ── 质量 ──
print("\n[3/3] 质量预测模型")
X, y = gen_quality(N)
if use_real_data:
X, y = mix_with_real(X, y, *load_real_samples("quality"))
Xn, Xm, Xs = z_scale(X)
yn, ym, ys_ = z_scale(y)
model = MLP(6, 2, hidden=(64, 32))
print(" 训练中...")
fit(model, Xn, yn, epochs=300)
export_onnx(model, 6, PT_DIR / "quality.onnx")
scalers["quality"] = {
"X_mean": Xm.tolist(), "X_std": Xs.tolist(),
"y_mean": ym.tolist(), "y_std": ys_.tolist(),
}
scaler_path = PT_DIR / "scalers.json"
with open(scaler_path, "w") as f:
json.dump(scalers, f, indent=2)
print(f"\n scalers → {scaler_path.name}")
print(f"\n完成 ({time.time()-t0:.1f}s)\n")
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--use-real-data", action="store_true",
help="将 production_samples.jsonl 中的实绩混入训练集")
args = parser.parse_args()
main(use_real_data=args.use_real_data)