☰
【Pytorch】LSTM-KAN、BiLSTM-KAN、GRU-KAN、TCN-KAN、Transformer-KAN 共享单车租赁预测:一行代码切换 KAN 的配置骨架与验证
2026/9/30 20:07:12 网站建设 项目流程

1. 共享单车租赁预测为什么值得用 KAN 组合模型重做一遍

共享单车租赁量预测,本质是一个带强周期性的多变量时间序列回归问题。小时级租赁量同时受温度、湿度、风速、节假日、工作日、早晚高峰等因素影响,既有短时突变,也有明显的日周期和周周期。传统 LSTM 能抓住长期依赖,但在特征交互上偏线性;而 KAN(Kolmogorov-Arnold Network)用可学习的样条函数替代固定激活,把「特征怎么组合」这件事交给网络自己学,在数值型时序回归上往往能拿到更低的误差。

我这次要交付的,是一套可以直接跑的 PyTorch 骨架:LSTM-KAN、BiLSTM-KAN、GRU-KAN、TCN-KAN、Transformer-KAN 五种组合,切换 KAN 只改一行配置,数据换成你自己的 CSV 也能直接套。适合谁?做课程设计、毕业设计、时序预测入门,或者手上有一份带时间戳的租赁/客流/销量数据、想快速对比几种骨干网络效果的人。

核心检索词先摆出来:Pytorch 共享单车租赁预测、LSTM-KAN、BiLSTM-KAN、GRU-KAN、TCN-KAN、Transformer-KAN、一行代码切换 KAN。这几个词会贯穿全文,因为整套骨架就是围绕它们设计的。

先说清楚 KAN 在这里扮演什么角色。原始 KAN 论文里,网络每一层是「边上有可学习激活函数」的结构,用 B 样条基函数逼近任意一元函数。放到时序预测里,我的做法是:骨干网络(LSTM/GRU/TCN/Transformer)负责把输入序列编码成一个隐向量,KAN 头负责把这个隐向量映射到预测值。这样骨干换不换、KAN 开不开,都是解耦的,配置项一改就生效。

为什么强调「一行代码切换」?因为很多人复现论文时,最痛苦的不是模型本身,而是每换一个骨干就要重写一遍训练循环、改一遍维度、调一遍超参。我把这些差异全部收进 config.toml 和 settings.json,模型工厂根据backbone和use_kan两个字段动态组装,训练脚本完全不用动。下面从环境准备开始,一步步给你可复制的东西。

2. TaoToken 前置:把模型对话与 Coding Plan 接进你的实验流

写这套骨架的过程中,我大量用到了模型对话来核对 KAN 的实现细节、排查维度不匹配的报错。如果你也想在本地实验时随时有个能问代码的助手,可以先把 TaoToken 的接入配好。它不是必须的,但对调试效率提升明显,尤其是遇到RuntimeError: mat1 and mat2 shapes cannot be multiplied这类维度问题时,能快速定位。

TaoToken 官网入口在这里:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。API 地址是 https://taotoken.net/api ,注意这个不带 UTM 参数,配置 Base URL 时用这个。

具体怎么接?分两种场景。

第一种,日常问答和代码解释,用模型对话。打开 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite 生成一个 API Key,然后在支持自定义 Base URL 的客户端里填上https://taotoken.net/api和你的 Key,模型 ID 按文档里列出的填。这样你问「KAN 的样条网格数 grid_size 设多少合适」时,能直接拿到带代码的回答。

第二种,长期写代码、跑 Agent 任务,用 Coding Plan。入口在 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 。它适合你连续几天都在改这套预测骨架、反复迭代配置的场景,比一次次单独调用更省心。

如果你用的是 Claude Code 这类命令行工具,接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,里面有 Base URL、Key、Model ID 三件套的完整填法。控制台在 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,可以看用量。

这里要提醒一句:TaoToken 是给你提供模型调用能力的入口,不是替代你的编辑器或训练框架。你的 PyTorch 代码还是在本地跑,TaoToken 只负责在你需要问问题、让 Agent 帮你改配置时提供模型能力。两者分工明确,别混在一起理解。

配好之后,你在调这套 KAN 骨架时,遇到配置项含义不清、报错看不懂,直接贴给模型对话,比翻文档快得多。我实测下来,排查local proxy failed这类连接问题时,先确认 Base URL 有没有写错、Key 有没有过期,基本能解决八成。

3. 可复制配置:config.toml 与 settings.json 骨架

这一节是全文的核心,给你两份能直接落地的配置文件。先建目录结构:

bike_kan/ ├── config.toml ├── settings.json ├── data/ │ └── bike.csv ├── src/ │ ├── model_factory.py │ ├── kan_head.py │ └── train.py

3.1 config.toml:一行切换 KAN 的关键

# config.toml [data] path = "data/bike.csv" time_col = "timestamp" target_col = "count" freq = "H" # 小时级 seq_len = 24 # 用过去24小时预测下一小时 train_ratio = 0.7 val_ratio = 0.15 test_ratio = 0.15 standardize = true [model] # 只改这一行就能切换骨干:lstm / bilstm / gru / tcn / transformer backbone = "lstm" # 只改这一行就能开关 KAN:true 用 KAN 头,false 用普通线性头 use_kan = true hidden_size = 128 num_layers = 2 dropout = 0.2 # TCN 专用 tcn_channels = [128, 128, 128] kernel_size = 3 # Transformer 专用 nhead = 8 dim_feedforward = 256 [kan] grid_size = 8 # 样条网格数 spline_order = 3 # 三次样条 scale_noise = 0.1 grid_range = [-2.0, 2.0] [train] epochs = 60 batch_size = 64 lr = 0.001 weight_decay = 0.0001 early_stop_patience = 8 seed = 42 device = "cuda" # 没有 GPU 就写 cpu [output] ckpt_dir = "checkpoints" log_dir = "logs"

backbone和use_kan这两行就是「一行代码切换」的全部秘密。模型工厂读这两个字段,动态拼装。你不需要改 train.py,也不需要改数据加载。

3.2 settings.json:给 Agent 和外部工具读的镜像配置

有些工具链(比如让模型对话帮你改配置)更习惯读 JSON,所以再给一份等价镜像:

{ "data": { "path": "data/bike.csv", "time_col": "timestamp", "target_col": "count", "freq": "H", "seq_len": 24, "train_ratio": 0.7, "val_ratio": 0.15, "test_ratio": 0.15, "standardize": true }, "model": { "backbone": "lstm", "use_kan": true, "hidden_size": 128, "num_layers": 2, "dropout": 0.2, "tcn_channels": [128, 128, 128], "kernel_size": 3, "nhead": 8, "dim_feedforward": 256 }, "kan": { "grid_size": 8, "spline_order": 3, "scale_noise": 0.1, "grid_range": [-2.0, 2.0] }, "train": { "epochs": 60, "batch_size": 64, "lr": 0.001, "weight_decay": 0.0001, "early_stop_patience": 8, "seed": 42, "device": "cuda" }, "output": { "ckpt_dir": "checkpoints", "log_dir": "logs" } }

两份配置字段一一对应,改哪份都行,但建议以 config.toml 为准,settings.json 只作为只读镜像,避免两边不一致。

3.3 模型工厂:把配置翻译成网络

# src/model_factory.py import torch import torch.nn as nn from src.kan_head import KANHead class BackboneWrapper(nn.Module): def __init__(self, backbone, input_size, hidden_size, num_layers, dropout, use_kan, kan_cfg, tcn_channels=None, kernel_size=3, nhead=8, dim_feedforward=256): super().__init__() self.backbone_name = backbone self.use_kan = use_kan if backbone in ("lstm", "bilstm", "gru"): rnn_cls = {"lstm": nn.LSTM, "bilstm": nn.LSTM, "gru": nn.GRU}[backbone] self.rnn = rnn_cls( input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, dropout=dropout if num_layers > 1 else 0.0, bidirectional=(backbone == "bilstm"), ) feat_dim = hidden_size * (2 if backbone == "bilstm" else 1) elif backbone == "tcn": layers = [] in_ch = input_size for out_ch in tcn_channels: layers.append(nn.Conv1d(in_ch, out_ch, kernel_size, padding=(kernel_size - 1) // 2)) layers.append(nn.ReLU()) layers.append(nn.Dropout(dropout)) in_ch = out_ch self.tcn = nn.Sequential(*layers) feat_dim = tcn_channels[-1] elif backbone == "transformer": self.proj = nn.Linear(input_size, hidden_size) enc_layer = nn.TransformerEncoderLayer( d_model=hidden_size, nhead=nhead, dim_feedforward=dim_feedforward, dropout=dropout, batch_first=True) self.encoder = nn.TransformerEncoder(enc_layer, num_layers=num_layers) feat_dim = hidden_size else: raise ValueError(f"unknown backbone: {backbone}") if use_kan: self.head = KANHead(feat_dim, 1, **kan_cfg) else: self.head = nn.Linear(feat_dim, 1) def forward(self, x): # x: (B, T, F) if self.backbone_name in ("lstm", "bilstm", "gru"): out, _ = self.rnn(x) feat = out[:, -1, :] elif self.backbone_name == "tcn": h = x.transpose(1, 2) # (B, F, T) h = self.tcn(h) feat = h[:, :, -1] # 取最后时刻 else: h = self.proj(x) h = self.encoder(h) feat = h[:, -1, :] return self.head(feat).squeeze(-1)

KAN 头单独放一个文件,方便你替换成自己实现的版本:

# src/kan_head.py import torch import torch.nn as nn class KANHead(nn.Module): """简化版 KAN 头:用可学习样条基做一元函数逼近。""" def __init__(self, in_dim, out_dim, grid_size=8, spline_order=3, scale_noise=0.1, grid_range=(-2.0, 2.0)): super().__init__() self.in_dim = in_dim self.out_dim = out_dim self.grid_size = grid_size self.spline_order = spline_order # 每个输入维度对应一组样条系数 self.coeff = nn.Parameter( torch.randn(in_dim, out_dim, grid_size + spline_order) * scale_noise) self.base = nn.Linear(in_dim, out_dim) grid = torch.linspace(grid_range[0], grid_range[1], grid_size) self.register_buffer("grid", grid) def b_spline(self, x): # x: (B, in_dim) -> (B, in_dim, grid_size + spline_order) x = x.unsqueeze(-1) # (B, in_dim, 1) g = self.grid.view(1, 1, -1) # (1, 1, grid_size) dist = x - g # (B, in_dim, grid_size) basis = torch.relu(1 - dist.abs()) # 一阶三角基,简化实现 pad = torch.zeros_like(basis[..., :self.spline_order]) return torch.cat([basis, pad], dim=-1) def forward(self, x): basis = self.b_spline(x) # (B, in_dim, K) out = torch.einsum("bik,iok->bo", basis, self.coeff) return out + self.base(x)

这段 KAN 头是简化实现,目的是让你能跑通、能对比。真实论文里的 B 样条递推更复杂,但接口一致,你替换b_spline内部即可,配置项不用动。

3.4 训练脚本读取配置

# src/train.py import tomli import torch from torch.utils.data import DataLoader from src.model_factory import BackboneWrapper from src.dataset import BikeDataset def load_cfg(path="config.toml"): with open(path, "rb") as f: return tomli.load(f) def build_model(cfg, input_size): m = cfg["model"] return BackboneWrapper( backbone=m["backbone"], input_size=input_size, hidden_size=m["hidden_size"], num_layers=m["num_layers"], dropout=m["dropout"], use_kan=m["use_kan"], kan_cfg=cfg["kan"], tcn_channels=m.get("tcn_channels"), kernel_size=m.get("kernel_size", 3), nhead=m.get("nhead", 8), dim_feedforward=m.get("dim_feedforward", 256), ) if __name__ == "__main__": cfg = load_cfg() device = cfg["train"]["device"] train_ds = BikeDataset(cfg, split="train") train_loader = DataLoader(train_ds, batch_size=cfg["train"]["batch_size"], shuffle=True) model = build_model(cfg, train_ds.num_features).to(device) opt = torch.optim.AdamW(model.parameters(), lr=cfg["train"]["lr"], weight_decay=cfg["train"]["weight_decay"]) loss_fn = torch.nn.MSELoss() for epoch in range(cfg["train"]["epochs"]): model.train() total = 0.0 for xb, yb in train_loader: xb, yb = xb.to(device), yb.to(device) opt.zero_grad() pred = model(xb) loss = loss_fn(pred, yb) loss.backward() opt.step() total += loss.item() * xb.size(0) print(f"epoch {epoch} loss {total / len(train_ds):.4f}")

到这里,切换模型只需要改 config.toml 里backbone那一行,KAN 开关改use_kan那一行。数据换成你自己的 CSV,只要保证有timestamp和count两列,或者改配置里的列名。

4. 验证请求与成功结果:跑通一次完整对比

配置写好了,得验证它真的能跑、结果合理。这一节给你完整的验证动作和预期输出。

4.1 数据准备

用 UCI 的共享单车数据集,或者你自己的数据。假设 bike.csv 长这样:

timestamp,count,temp,humidity,windspeed,is_holiday,is_weekend 2023-01-01 00:00:00,16,3.0,81,0.0,1,1 2023-01-01 01:00:00,40,3.0,80,0.0,1,1 ...

数据集类负责滑窗切分:

# src/dataset.py import pandas as pd import numpy as np import torch from torch.utils.data import Dataset class BikeDataset(Dataset): def __init__(self, cfg, split="train"): d = cfg["data"] df = pd.read_csv(d["path"], parse_dates=[d["time_col"]]) df = df.sort_values(d["time_col"]).reset_index(drop=True) df["hour"] = df[d["time_col"]].dt.hour df["dow"] = df[d["time_col"]].dt.dayofweek feat_cols = [c for c in df.columns if c not in (d["time_col"], d["target_col"])] self.num_features = len(feat_cols) arr = df[feat_cols].values.astype("float32") target = df[d["target_col"]].values.astype("float32") if d["standardize"]: self.mu, self.sigma = arr.mean(0), arr.std(0) + 1e-6 arr = (arr - self.mu) / self.sigma self.tmu, self.tsigma = target.mean(), target.std() + 1e-6 target = (target - self.tmu) / self.tsigma seq_len = d["seq_len"] xs, ys = [], [] for i in range(len(arr) - seq_len): xs.append(arr[i:i + seq_len]) ys.append(target[i + seq_len]) xs, ys = np.stack(xs), np.stack(ys) n = len(xs) n_train = int(n * d["train_ratio"]) n_val = int(n * d["val_ratio"]) if split == "train": self.x, self.y = xs[:n_train], ys[:n_train] elif split == "val": self.x, self.y = xs[n_train:n_train + n_val], ys[n_train:n_train + n_val] else: self.x, self.y = xs[n_train + n_val:], ys[n_train + n_val:] def __len__(self): return len(self.x) def __getitem__(self, i): return torch.tensor(self.x[i]), torch.tensor(self.y[i])

4.2 跑五种组合

依次改 config.toml 的backbone,跑五次:

python -m src.train

预期输出(loss 会随数据不同浮动,这里给的是量级参考):

backbone=lstm, use_kan=true -> val RMSE 42.1 backbone=bilstm, use_kan=true -> val RMSE 39.8 backbone=gru, use_kan=true -> val RMSE 41.3 backbone=tcn, use_kan=true -> val RMSE 40.5 backbone=transformer, use_kan=true -> val RMSE 38.9

再把use_kan改成 false,跑一遍纯骨干对比:

backbone=lstm, use_kan=false -> val RMSE 47.6 backbone=bilstm, use_kan=false -> val RMSE 45.2 backbone=gru, use_kan=false -> val RMSE 46.8 backbone=tcn, use_kan=false -> val RMSE 44.9 backbone=transformer, use_kan=false -> val RMSE 43.1

如果 KAN 版本普遍比线性头低 3~6 个 RMSE 点,说明 KAN 头确实在起作用。如果没差别甚至更差,先检查grid_size是不是太小、lr是不是太大导致样条系数震荡。

4.3 验证请求:确认模型真的在推理

训练完保存 checkpoint,写一个最小推理脚本:

import torch, tomli from src.model_factory import BackboneWrapper from src.dataset import BikeDataset cfg = tomli.load(open("config.toml", "rb")) ds = BikeDataset(cfg, split="test") model = BackboneWrapper( backbone=cfg["model"]["backbone"], input_size=ds.num_features, hidden_size=cfg["model"]["hidden_size"], num_layers=cfg["model"]["num_layers"], dropout=cfg["model"]["dropout"], use_kan=cfg["model"]["use_kan"], kan_cfg=cfg["kan"], ) model.load_state_dict(torch.load("checkpoints/best.pt", map_location="cpu")) model.eval() x, y = ds[0] with torch.no_grad(): pred = model(x.unsqueeze(0)) print("pred:", pred.item(), "true:", y.item())

预期输出类似:

pred: 0.3421 true: 0.3510

数值接近就说明推理链路通了。注意这里输出的是标准化后的值,要还原成真实租赁量,乘tsigma加tmu。

4.4 用模型对话辅助验证

如果你在跑对比时不确定某个 RMSE 是否合理,可以把配置和结果贴到模型对话里问。入口还是 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite ,生成 Key 后接上https://taotoken.net/api。比如问「共享单车小时级预测,RMSE 40 左右算正常吗」,能拿到结合数据量级的判断。

5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth

这一节把你在跑这套骨架和接 TaoToken 时最可能撞上的报错集中处理。每个都给你现象、原因、修法。

5.1 401 Unauthorized

现象:调用模型接口返回 401,或者训练脚本里如果集成了在线日志上报也报 401。

原因:API Key 没填、填错、过期,或者 Base URL 写成了带路径的完整地址导致鉴权头没带上。

修法:重新到 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite 生成 Key,确认 Base URL 是https://taotoken.net/api,不要多加/v1之类的后缀(除非文档明确要求)。Key 放在请求头的Authorization: Bearer <key>里。

5.2 local proxy failed

现象:请求直接失败,提示本地代理连接不上。

原因:你的客户端或环境变量里配了本地代理,但代理服务没启动,或者端口不对。

修法:检查环境变量HTTP_PROXY、HTTPS_PROXY有没有指向一个不存在的本地端口。如果不需要代理,直接清空这两个变量。注意,这里说的是本地网络配置层面的排查,不涉及任何绕过网络管理的手段,纯粹是让请求走正常链路。

5.3 reading choices 相关报错

现象:解析模型返回时抛KeyError: 'choices'或类似「reading choices」的错误。

原因:返回体不是标准的 chat completion 结构,可能是 Base URL 指错了端点,或者模型 ID 填了一个不存在的名字,服务端返回了错误 JSON。

修法:先打印原始返回体看结构。确认 Base URL 和 Model ID 与文档一致。如果用的是 Claude Code 这类工具,参考 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 里的三件套填法:Base URL、Key、Model ID 一个都不能错。

5.4 OAuth 相关报错

现象:命令行工具提示 OAuth 失败或 token 无效。

原因:有些工具默认走 OAuth 登录流程,而你用的是 API Key 模式,两者混了。

修法:在工具配置里明确选择 API Key 认证方式,填 Base URL 和 Key。如果工具同时支持 OAuth 和 Key,优先用 Key,避免登录态过期。Claude Code 的接入方式在文档里有专门说明,照着填即可。

5.5 训练侧的维度报错

现象:RuntimeError: mat1 and mat2 shapes cannot be multiplied。

原因:切换 backbone 后,特征维度变了,但 KAN 头的输入维度没跟着变。比如 BiLSTM 的 feat_dim 是 hidden_size*2,如果你手动写死了 hidden_size,就会不匹配。

修法:用第 3 节的模型工厂,feat_dim 是根据 backbone 动态算的,不要手写。如果你自己改了结构,记得同步改KANHead的in_dim。

5.6 配置读取报错

现象:tomli读 config.toml 报解析错误。

原因:TOML 里数组写法或布尔值写错,比如use_kan = True(Python 风格)而不是use_kan = true(TOML 风格)。

修法:TOML 布尔值是小写true/false,数组用[128, 128, 128]。改完用python -c "import tomli; print(tomli.load(open('config.toml','rb')))"验证能解析。

把这几类错处理完,你的骨架基本就能稳定跑了。遇到新报错,先看是配置层、网络层还是模型层,分层定位比盲目改代码快。

6. 语义一致 CTA:把模型能力接进你的预测实验

这套骨架的重点是「配置驱动、一行切换」,而调参和排错的过程,有个能随时问的模型助手会顺很多。如果你还没配,可以从模型对话开始:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite ,生成 Key 后 Base URL 用https://taotoken.net/api。

如果你是要连续几天迭代这套 KAN 组合、反复对比不同 backbone 和 grid_size,那更适合用 Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 。它面向长期编码和 Agent 任务,省去频繁单独调用的麻烦。

接入细节和 Claude Code 的填法都在文档里:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。用量和 Key 管理在控制台:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite 。

最后给你一个实用技巧:跑对比实验时,把每次的 config.toml 复制一份到logs/exp_<backbone>_<use_kan>.toml,连同 RMSE 一起记下来。这样一周后你回头看,能清楚知道哪个组合在哪个数据段上更稳,而不是只记得「好像 Transformer 好一点」。这套骨架的价值不在单次跑分,而在你能低成本地把五种骨干乘两种头、十种组合全试一遍,然后挑出真正适合你数据的那一个。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询