Download self_train.py from LIJINGHAI111/symbolic_regression_DeepSets_LSTM_Generator: direct link, hf CLI and curl.
- Browser
- Download file 16.3 kB
-
https://huggingface.co/LIJINGHAI111/symbolic_regression_DeepSets_LSTM_Generator/resolve/main/self_train.py
- Command line
-
hf download hf://LIJINGHAI111/symbolic_regression_DeepSets_LSTM_Generator/self_train.py
-
curl -L -o self_train.py https://huggingface.co/LIJINGHAI111/symbolic_regression_DeepSets_LSTM_Generator/resolve/main/self_train.py
16.3 kB
| """主动自我训练(用户数据 → 拆 train/test → 精英保留 + 模型回滚) | |
| 策略: | |
| 1. 每轮同时采样模型生成 + 随机生成候选 | |
| 2. 在 train 集上评估 R²,跨轮保留精英候选(elite pool) | |
| 3. 只对 R² > 0.1 的正向候选做伪标签 CE 微调(避免强化错误模式) | |
| 4. REINFORCE 策略梯度用于模型采样的候选 | |
| 5. 保存每轮开始前的模型 state,如果本轮最佳 R² 下降则回滚 | |
| 6. 终止条件:R² > 0.995 或 连续 4 轮 test R² 无改善 | |
| """ | |
| import warnings | |
| import sys | |
| import numpy as np | |
| import torch | |
| import torch.nn.functional as F | |
| import config | |
| from vocab import Vocab | |
| from model import SymbolicRegressionModel | |
| from data_generator import RandomExpressionGenerator | |
| # 关键:stdout 行缓冲,保证 print() 立即刷新。 | |
| # 否则在后台/管道运行时(python 默认块缓冲),进度会攒在一旁, | |
| # 直到进程结束才一次性输出 —— 这正是"训练过程不输出"的根因。 | |
| if hasattr(sys.stdout, "reconfigure"): | |
| sys.stdout.reconfigure(line_buffering=True) | |
| # 全局静音候选表达式在数值评估时的 numpy 警告(exp/sqrt/log/power 溢出、 | |
| # 定义域外、除零等均属常规现象,结果已用 isfinite/complex 过滤)。 | |
| # 全局设置可覆盖 lambdify 生成的已编译模块,避免每一轮都刷屏。 | |
| np.seterr(over="ignore", invalid="ignore", divide="ignore") | |
| warnings.filterwarnings("ignore", module="lambdifygenerated") | |
| COPIED_MODEL_STATE_KEY = "_saved_lstm_state" | |
| def train_test_split_arrays(X, y, test_size=0.3, seed=None): | |
| """手写随机 train/test 拆分。""" | |
| rng = np.random.RandomState(seed) | |
| n = X.shape[0] | |
| idx = rng.permutation(n) | |
| n_te = max(1, int(test_size * n)) | |
| te_idx, tr_idx = idx[:n_te], idx[n_te:] | |
| return X[tr_idx], X[te_idx], y[tr_idx], y[te_idx] | |
| def eval_expr_on_data(expr_str: str, X: np.ndarray, y_true: np.ndarray) -> tuple: | |
| """计算给定表达式在数据上的预测值和 MSE。无效返回 NaN。""" | |
| import sympy as sp | |
| import warnings | |
| D = X.shape[1] | |
| try: | |
| expr = sp.sympify(expr_str) | |
| symbols = [sp.Symbol(f"x{i}") for i in range(D)] | |
| f = sp.lambdify(symbols, expr, modules="numpy") | |
| # 候选表达式常落在定义域外或数值溢出(exp/sqrt/log/power), | |
| # 静音这些 numpy RuntimeWarning,避免刷屏。结果 NaN/inf 会被下方过滤。 | |
| with warnings.catch_warnings(): | |
| warnings.simplefilter("ignore") | |
| with np.errstate(over="ignore", invalid="ignore", divide="ignore"): | |
| y_pred = f(*[X[:, i] for i in range(D)]) | |
| if np.iscomplexobj(y_pred) or not np.isfinite(y_pred).all(): | |
| return np.full_like(y_true, np.nan), np.nan | |
| mse = float(np.nanmean((y_pred - y_true) ** 2)) | |
| return y_pred, mse | |
| except Exception: | |
| return np.full_like(y_true, np.nan), np.nan | |
| def _generate_random_candidates(num_candidates: int, D: int, seed: int = None) -> list[str]: | |
| """生成随机候选,混合深度。""" | |
| gen = RandomExpressionGenerator(seed=seed) | |
| rng = np.random.RandomState(seed) | |
| candidates = [] | |
| depths = [2, 3, 4] | |
| while len(candidates) < num_candidates * 3: | |
| try: | |
| depth = rng.choice(depths) | |
| data = gen.generate(var_count=D, max_depth=int(depth)) | |
| candidates.append(data["expr_str"]) | |
| except Exception: | |
| continue | |
| return candidates[:num_candidates] | |
| def _evaluate_candidates(expr_strings: list[str], X: np.ndarray, y: np.ndarray, | |
| y_var: float) -> tuple: | |
| """批量评估候选,返回 rewards (R²) 和 mses。无效 reward=-1。""" | |
| n = len(expr_strings) | |
| rewards = np.full(n, -1.0) | |
| mses = np.full(n, np.nan) | |
| for i, expr_str in enumerate(expr_strings): | |
| _, mse = eval_expr_on_data(expr_str, X, y) | |
| mses[i] = mse | |
| if np.isfinite(mse): | |
| rewards[i] = 1.0 - mse / max(y_var, 1e-9) | |
| return rewards, mses | |
| def _encode_input(X: np.ndarray, y: np.ndarray, device: str): | |
| """编码用户数据为模型输入 [1, N, D_MAX], [1, N].""" | |
| D = X.shape[1] | |
| N = X.shape[0] | |
| X_t = torch.zeros(1, N, config.D_MAX, dtype=torch.float32, device=device) | |
| X_t[0, :, :D] = torch.from_numpy(X).to(device) | |
| y_t = torch.from_numpy(y).float().unsqueeze(0).to(device) | |
| n_samples = torch.tensor([N], device=device) | |
| var_counts = torch.tensor([D], device=device) | |
| return X_t, y_t, n_samples, var_counts | |
| def _sample_model(model, X_t, y_t, n_samples, var_counts, device, | |
| max_len=None, num_candidates=20, greedy=False, temperature=1.5): | |
| """从模型采样候选。greedy → argmax;否则多项式采样 + 噪声扰动。""" | |
| if max_len is None: | |
| max_len = config.T_MAX | |
| sample_size = num_candidates | |
| vocab = model.vocab | |
| z = model.encoder(X_t, y_t, n_samples, var_counts) | |
| if not greedy: | |
| noise_scale = 0.15 | |
| z = z + torch.randn_like(z) * noise_scale | |
| temp = temperature | |
| else: | |
| temp = 0.1 | |
| hidden = z.unsqueeze(0).expand(-1, sample_size, -1).contiguous() | |
| cell = z.unsqueeze(0).expand(-1, sample_size, -1).contiguous() | |
| if not greedy: | |
| hidden = hidden + torch.randn_like(hidden) * noise_scale | |
| cell = cell + torch.randn_like(cell) * noise_scale | |
| token = torch.full((sample_size, 1), fill_value=vocab.sos_id, device=device) | |
| sequences = [token] | |
| log_probs = [] | |
| done = torch.zeros(sample_size, dtype=torch.bool, device=device) | |
| for _ in range(max_len): | |
| embedded = model.lstm.embedding(token) | |
| output, (hidden, cell) = model.lstm.lstm(embedded, (hidden, cell)) | |
| logits = model.lstm.proj(output).squeeze(1) | |
| logits = logits / temp | |
| if greedy: | |
| token = logits.argmax(dim=-1, keepdim=True) | |
| else: | |
| log_probs_t = F.log_softmax(logits, dim=-1) | |
| probs_t = F.softmax(logits, dim=-1) | |
| dist = torch.distributions.Categorical(probs=probs_t) | |
| token = dist.sample().unsqueeze(-1) | |
| lp = torch.gather(log_probs_t, 1, token) | |
| lp = lp.masked_fill(done.unsqueeze(-1), 0.0) | |
| log_probs.append(lp) | |
| sequences.append(token) | |
| done = done | (token.squeeze(-1) == vocab.eos_id) | |
| if done.all(): | |
| break | |
| tokens = torch.cat(sequences, dim=1) | |
| if not greedy: | |
| log_probs = torch.cat(log_probs, dim=1) | |
| tokens = tokens[:, 1:] | |
| expr_strings = [] | |
| for i in range(sample_size): | |
| toks = tokens[i] | |
| if (toks == vocab.eos_id).any(): | |
| eos = (toks == vocab.eos_id).nonzero(as_tuple=True)[0][0].item() | |
| toks = toks[:eos] | |
| expr_strings.append(vocab.decode(toks.tolist())) | |
| return tokens, log_probs if not greedy else None, expr_strings | |
| def _save_model_snapshot(model): | |
| """保存 LSTM 参数快照到 model 的临时属性。""" | |
| sd = {k: v.data.clone() for k, v in model.lstm.state_dict().items()} | |
| setattr(model, COPIED_MODEL_STATE_KEY, sd) | |
| def _restore_model_snapshot(model): | |
| """从快照恢复 LSTM 参数。""" | |
| sd = getattr(model, COPIED_MODEL_STATE_KEY, None) | |
| if sd is not None: | |
| for k, v in sd.items(): | |
| model.lstm.state_dict()[k].data.copy_(v) | |
| def _encode_expr(expr_str: str, vocab: Vocab, device: str): | |
| """把表达式字符串编码为 token 序列。""" | |
| token_ids = vocab.encode(expr_str) | |
| token_ids = [vocab.sos_id] + token_ids + [vocab.eos_id] | |
| token_ids = token_ids[:config.T_MAX] | |
| return torch.tensor(token_ids, dtype=torch.long, device=device).unsqueeze(0) | |
| def self_train_on_user_data(model, X, y, device, split=0.7, | |
| max_rounds=15, candidates=96, seed=None, | |
| verbose=True): | |
| """针对用户数据 (X, y) 执行主动自我训练。 | |
| 返回 dict {best_expr, best_r2, converged, rounds, results, round_results} | |
| """ | |
| vocab = model.vocab | |
| if X.ndim == 1: | |
| X = X.reshape(-1, 1) | |
| D = X.shape[1] | |
| Xtr, Xte, ytr, yte = train_test_split_arrays( | |
| X, y, test_size=1 - split, seed=seed | |
| ) | |
| ytr_var = float(np.var(ytr)) | |
| yte_var = float(np.var(yte)) | |
| if verbose: | |
| print(f" [数据] 训练集: {Xtr.shape[0]} 样本, 测试集: {Xte.shape[0]} 样本, 维度 D={D}") | |
| print(f" [数据] y 方差: train={ytr_var:.4f}, test={yte_var:.4f}") | |
| Xtr_t, ytr_t, ntr, vctr = _encode_input(Xtr, ytr, device) | |
| optimizer = torch.optim.Adam(model.lstm.parameters(), lr=3e-4) | |
| # ---- 精英池:跨轮保留历史最佳候选 ---- | |
| elite_pool = [] | |
| PATIENCE = 4 | |
| results = [] | |
| best_r2 = -float("inf") | |
| best_expr = None | |
| no_improve = 0 | |
| converged = False | |
| for rnd in range(1, max_rounds + 1): | |
| model.train() | |
| # --- 保存本轮开始前的模型 --- | |
| _save_model_snapshot(model) | |
| # --- 温度退火 --- | |
| temp = max(1.2, 3.0 - 0.18 * (rnd - 1)) | |
| # --- 生成候选 --- | |
| n_model = int(candidates * 0.45) | |
| n_rand = candidates - n_model | |
| # 模型采样 | |
| tokens, log_probs, model_exprs = _sample_model( | |
| model, Xtr_t, ytr_t, ntr, vctr, device, | |
| num_candidates=n_model, greedy=False, temperature=temp | |
| ) | |
| # 随机生成(混合深度,提供多样性) | |
| rand_candidates = _generate_random_candidates( | |
| n_rand, D, seed=(seed or 0) + rnd * 131 + 7 | |
| ) | |
| # 加入精英池中的候选(去重后按 R² 排序取前10) | |
| elite_exprs = [e for e, _ in elite_pool[:10]] | |
| expr_strings = list(model_exprs) + list(rand_candidates) + elite_exprs | |
| # --- 评估候选 --- | |
| rewards, mses = _evaluate_candidates(expr_strings, Xtr, ytr, ytr_var) | |
| valid_mask = np.isfinite(mses) | |
| valid_idxs = np.where(valid_mask)[0] | |
| if valid_idxs.size == 0: | |
| if verbose: | |
| print(f" [自我训练] round {rnd}/{max_rounds} | T={temp:.2f} | 所有候选无效,跳过此轮") | |
| results.append({"round": rnd, "train_r2": -1.0, "test_r2": -1.0, | |
| "test_expr": "", "n_valid": 0}) | |
| continue | |
| best_local_idx = valid_idxs[int(np.argmax(rewards[valid_idxs]))] | |
| best_local_expr = expr_strings[best_local_idx] | |
| best_local_r2 = float(rewards[best_local_idx]) | |
| # --- 更新精英池 --- | |
| for i in valid_idxs: | |
| r2 = float(rewards[i]) | |
| if r2 > 0.05: | |
| elite_pool.append((expr_strings[i], r2)) | |
| elite_pool = sorted(set(elite_pool), key=lambda x: -x[1])[:20] | |
| # --- 训练步:伪标签 + REINFORCE --- | |
| # 只对 R² > 0.1 的正向候选做 CE 伪标签 | |
| good_mask = rewards > 0.1 | |
| good_idxs = np.where(good_mask)[0] | |
| ce_losses = [] | |
| for idx in good_idxs: | |
| expr_str = expr_strings[idx] | |
| if expr_str == "": | |
| continue | |
| try: | |
| t_tensor = _encode_expr(expr_str, vocab, device) | |
| z = model.encoder(Xtr_t, ytr_t, ntr, vctr) | |
| hidden = z.unsqueeze(0) | |
| cell = z.unsqueeze(0) | |
| logits, _, _ = model.lstm(t_tensor[:, :-1], hidden, cell) | |
| targets = t_tensor[:, 1:] | |
| ce = F.cross_entropy(logits.reshape(-1, vocab.vocab_size), targets.reshape(-1)) | |
| ce_losses.append(ce * 0.3) | |
| except Exception: | |
| continue | |
| if ce_losses and verbose: | |
| pos_count = len(ce_losses) | |
| best_r2_among_pos = max(rewards[good_idxs]) | |
| print(f" └─ 伪标签: {pos_count} 个正向候选 (R²>{0.1:.1f}, 最高={best_r2_among_pos:.3f})") | |
| # REINFORCE:仅对模型采样候选(前 n_model 个)中有效且导通的 | |
| reinforce_loss = None | |
| model_valid_idx = valid_idxs[valid_idxs < n_model] | |
| non_collapsed = [] | |
| for idx in model_valid_idx: | |
| if idx < n_model and log_probs is not None: | |
| lp_row = log_probs[idx] | |
| if lp_row.numel() > 0 and lp_row.sum() < -0.01: | |
| non_collapsed.append(idx) | |
| if len(non_collapsed) >= 3: | |
| m_rewards = rewards[non_collapsed] | |
| m_rewards = np.where(np.isfinite(m_rewards), m_rewards, -1.0) | |
| m_advantage_t = torch.tensor(m_rewards - m_rewards.mean(), | |
| dtype=torch.float32, device=device) | |
| m_logp = log_probs[non_collapsed] | |
| m_logp_sum = m_logp.sum(dim=-1) | |
| reinforce_loss = -(m_advantage_t * m_logp_sum).mean() | |
| if ce_losses or reinforce_loss is not None: | |
| all_losses = ce_losses[:] | |
| if reinforce_loss is not None: | |
| all_losses.append(reinforce_loss * 0.5) | |
| loss = torch.stack(all_losses).mean() | |
| optimizer.zero_grad() | |
| loss.backward() | |
| torch.nn.utils.clip_grad_norm_(model.lstm.parameters(), 1.0) | |
| optimizer.step() | |
| # --- test 评估 --- | |
| te_expr = best_local_expr | |
| _, te_mse = eval_expr_on_data(te_expr, Xte, yte) | |
| if np.isfinite(te_mse): | |
| te_r2 = 1.0 - te_mse / max(yte_var, 1e-9) | |
| else: | |
| te_r2 = -float("inf") | |
| # --- 模型回滚:如果本轮最佳 R² 比上一轮差,恢复模型 --- | |
| prev_r2 = results[-1]["best_local_r2"] if results else -float("inf") | |
| if best_local_r2 < prev_r2 - 0.1 and len(results) >= 1: | |
| _restore_model_snapshot(model) | |
| if verbose: | |
| print(f" └─ 模型回滚(best R² {best_local_r2:.3f} < 上一轮 {prev_r2:.3f})") | |
| # 重新用恢复后的模型采样一次作为补充 | |
| tokens2, lp2, model_exprs2 = _sample_model( | |
| model, Xtr_t, ytr_t, ntr, vctr, device, | |
| num_candidates=n_model, greedy=False, temperature=temp | |
| ) | |
| expr_strings2 = list(model_exprs2) + elite_exprs[:5] | |
| rewards2, mses2 = _evaluate_candidates(expr_strings2, Xtr, ytr, ytr_var) | |
| vm2 = np.isfinite(mses2) | |
| vi2 = np.where(vm2)[0] | |
| if vi2.size > 0: | |
| best_i2 = vi2[int(np.argmax(rewards2[vi2]))] | |
| best_local_expr = expr_strings2[best_i2] | |
| best_local_r2 = float(rewards2[best_i2]) | |
| _, te_mse2 = eval_expr_on_data(best_local_expr, Xte, yte) | |
| if np.isfinite(te_mse2): | |
| te_r2 = 1.0 - te_mse2 / max(yte_var, 1e-9) | |
| else: | |
| te_r2 = -float("inf") | |
| if te_r2 > best_r2: | |
| best_r2 = te_r2 | |
| best_expr = te_expr | |
| no_improve = 0 | |
| else: | |
| no_improve += 1 | |
| nv = int(valid_mask.sum()) | |
| results.append({ | |
| "round": rnd, "train_r2": float(np.nanmean(rewards)), | |
| "test_r2": float(te_r2), "test_expr": te_expr, | |
| "best_local_expr": best_local_expr, | |
| "best_local_r2": best_local_r2, "n_valid": nv, | |
| "n_good_ce": len(ce_losses), | |
| }) | |
| if verbose: | |
| avg_r2 = float(np.nanmean(rewards)) | |
| print(f" [自我训练] round {rnd}/{max_rounds} | T={temp:.2f} | " | |
| f"valid={nv}/{candidates} | " | |
| f"avg R²={avg_r2:.4f} | " | |
| f"best R²={best_local_r2:.4f} | " | |
| f"test R²={te_r2:.4f} | {te_expr}") | |
| # --- 终止判定 --- | |
| if te_r2 > 0.995: | |
| if verbose: | |
| print(f" → R² 达标 ({te_r2:.4f}),停止自我训练 ✓") | |
| converged = True | |
| break | |
| if no_improve >= PATIENCE: | |
| if verbose: | |
| print(f" → test R² 连续 {PATIENCE} 轮无改善,停止自我训练(最佳: {best_r2:.4f})") | |
| converged = True | |
| break | |
| return { | |
| "best_expr": best_expr, | |
| "best_r2": best_r2, | |
| "converged": converged, | |
| "rounds": len(results), | |
| "round_results": results, | |
| } | |