您应当知道的 MQL5 向导技术(第 60 部分):推理学习(Wasserstein-VAE),配合移动平均线和随机振荡器形态·进阶篇
◍ 瓦瑟斯坦 VAE 的 Python 骨架与四件套
做这类无监督编码模型,先用 Python 而不是原生 MQL5 训练网络,速度差不是一个量级;MQL5 虽有 OpenCL 变通方案能缩小差距,但本系列暂未涉及。我们落地的瓦瑟斯坦 VAE 类,核心就四块:特征编码器、引用缓冲区、预测方法、双模前向通验。 特征编码器是三层 MLP,把输入压成潜在空间参数(z、z均值、z对数变量)。引用缓冲区存的是预训练输入——特征、状态、动作、奖励及其编码。预测方法在潜在空间内补残缺数据:KNN 映射状态、高斯加权映射动作、线性回归映射奖励。双模前向通验同时服务训练与推理。 编码过程把 FSAR 输入过网络,切成 z / z均值 / z对数变量,重参数化技巧保证可微采样。引用系统靠 update_references() 显式冻结输出与配对输入,不是自动更新的。 原结构仍有缝可补:架构上加谱归一化强制 Lipschitz、给高斯加权做可学习温度、引用内存搞 FIFO 修剪、蒙特卡洛抽 Uncertain;训练上加瓦瑟斯坦梯度惩罚、MMD 潜在正则、自适应选预测器;健壮侧做分布外检测、参考品质评分、动态邻域、输入依赖噪声缩放。外汇与贵金属行情高波动,这类模型外推失真概率不低,上 MT5 前务必用历史片段回测。 下面这段是类定义主干,逐行拆完你能直接抄去改 dim 试。
class WassersteinVAEUnsupervised(nn.Module): def __init__(self, feature_dim, encoding_dim, k_neighbors=class="num">5): super().__init__() self.encoding_dim = encoding_dim self.k_neighbors = k_neighbors # Feature encoder self.feature_encoder = nn.Sequential( nn.Linear(feature_dim, class="num">256), nn.ReLU(), nn.Linear(class="num">256, class="num">128), nn.ReLU(), nn.Linear(class="num">128, encoding_dim * class="num">2) # mean and logvar ) # Buffer for storing training references self.register_buffer(&class="macro">#x27;ref_encoding&class="macro">#x27;, torch.zeros(class="num">1, encoding_dim)) self.register_buffer(&class="macro">#x27;ref_states&class="macro">#x27;, torch.zeros(class="num">1, class="num">1)) self.register_buffer(&class="macro">#x27;ref_actions&class="macro">#x27;, torch.zeros(class="num">1, class="num">1)) self.register_buffer(&class="macro">#x27;ref_rewards&class="macro">#x27;, torch.zeros(class="num">1, class="num">1)) self._references_loaded = False def encode(self, features): h = self.feature_encoder(features) z_mean, z_logvar = torch.chunk(h, class="num">2, dim=class="num">1) class="kw">return z_mean, z_logvar def reparameterize(self, mean, logvar): std = torch.exp(class="num">0.5 * logvar) eps = torch.randn_like(std) class="kw">return mean + eps * std def update_references(self, encoding_vectors, states, actions, rewards): """Store reference data for unsupervised prediction""" self.ref_encoding = encoding_vectors.detach().clone() self.ref_states = states.detach().clone().unsqueeze(-class="num">1) self.ref_actions = actions.detach().clone().unsqueeze(-class="num">1) self.ref_rewards = rewards.detach().clone().unsqueeze(-class="num">1) self._references_loaded = True def knn_predict(self, z, ref_values): # z shape: [batch_size, encoding_dim] # ref_values shape: [ref_size, class="num">1] or [ref_size] # Ensure ref_values is properly shaped ref_values = ref_values.view(-class="num">1) # Flatten to [ref_size] # Calculate distances between z and reference encodings
近邻与高斯加权两种推断实现的细节差异
在编码空间 z 上做推断时,k 近邻分支先算 batch 内每个样本到参考编码的欧氏距离矩阵,形状为 [batch_size, ref_size],再取距离最小的 k 个邻居下标。 拿到下标后用 torch.gather 从参考值里抠出对应邻居的真实标签,沿 k 维求平均得到预测,这种硬截断方式对离群参考点不敏感,但 k 选太小容易受噪声编码干扰。 高斯加权分支走的是软权重路线:把负距离送进 softmax 得到 [batch, ref_size] 的归一化权重,再与强制 reshape 成 [792, 1] 的参考值做矩阵乘。 参考集固定 792 条样本时,权重分布倾向于把预测拉向距离近且密集的区域;外汇与贵金属行情里编码漂移快,这类软加权在样本外可能比 k 近邻更平滑但也更滞后,实盘前建议在 MT5 用历史 tick 复算一遍距离尺度。 两个分支都要求 z 是二维 [batch, encoding],否则 assert 会直接拦下;linear_predict 则额外拼了一列偏置项走正规方程,适合参考值和目标近似线性的场景。
distances = torch.cdist(z, self.ref_encoding) # [batch_size, ref_size] # Get top-k nearest neighbors _, indices = torch.topk(distances, k=self.k_neighbors, largest=False) # [batch_size, k] # Gather corresponding reference values neighbor_values = torch.gather( ref_values.unsqueeze(class="num">0).expand(indices.size(class="num">0), -class="num">1), # [batch_size, ref_size] class="num">1, indices ) # [batch_size, k] # Average the nearest values predictions = neighbor_values.mean(dim=class="num">1, keepdim=True) # [batch_size, class="num">1] class="kw">return predictions def gaussian_predict(self, z, ref_values): # Input validation assert z.dim() == class="num">2, "z must be 2D [batch, encoding]" assert ref_values.dim() == class="num">2, "ref_values must be 2D" # Calculate distances(Euclidean) distances = torch.cdist(z, self.ref_encoding) # [batch, ref_size] # Convert to similarities(Gaussian weights) weights = torch.softmax(-distances, dim=class="num">1) # [batch, ref_size] # Prepare reference values ref_values = ref_values.squeeze(-class="num">1) if ref_values.size(class="num">1) == class="num">1 else ref_values ref_values = ref_values.unsqueeze(class="num">0) if ref_values.dim() == class="num">1 else ref_values # Ensure proper shapes ref_values = ref_values.view(-class="num">1, class="num">1) # Force [class="num">792, class="num">1] shape # Calculate distances distances = torch.cdist(z, self.ref_encoding) # [batch_size, class="num">792] # Convert to weights weights = torch.softmax(-distances, dim=class="num">1) # [batch_size, class="num">792] # Matrix multiplication Weighted combination predictions = torch.matmul(weights, ref_values) # [batch, class="num">1] class="kw">return predictions.unsqueeze(-class="num">1) if predictions.dim() == class="num">1 else predictions def linear_predict(self, z, ref_values): """Linear regression prediction using normal equations""" # Add bias term X = torch.cat([self.ref_encoding, torch.ones_like(self.ref_encoding[:, :class="num">1])], dim=class="num">1) y = ref_values
「编码空间里的三类预测分支」
这段实现把隐变量 z 当作统一中介,分别用三种算法头去逼近状态、动作与奖励。线性头走闭式解,高斯头做近邻插值,KNN 头直接查参考集,三者共享同一个 encoder 输出的 z。 闭式解部分先算 XtX 与 Xty,再用 torch.linalg.solve 一步出 theta,比梯度下降省事且数值稳定。预测时把新 z 拼一列常数偏置,矩阵乘即得连续值输出。 predict_from_encoding 会先校验参考集是否已加载,否则直接抛 RuntimeError;之后把 ref_states、ref_actions、ref_rewards 都 view 成 (-1,1) 单列,避免维度不匹配。 forward 的逻辑分两路:训练时给足 states/actions/rewards 就只回传 z 与分布参数;推理时缺标签就走预测分支,回 pred_states、pred_actions、pred_rewards 三元组。 在 MT5 里接这套,重点看 z 维度是否和参考集对齐——维度错一位,predict_from_encoding 里的 view 就会静默变形,预测曲线看着顺实际上已偏移。外汇与贵金属行情高波动,这类模型输出仅作概率参考,实盘须自担风险。
# Compute closed-form solution
XtX = torch.matmul(X.T, X)
Xty = torch.matmul(X.T, y)
theta = torch.linalg.solve(XtX, Xty)
# Predict with new z values
X_new = torch.cat([z, torch.ones_like(z[:, :class="num">1])], dim=class="num">1)
class="kw">return torch.matmul(X_new, theta)
def predict_from_encoding(self, z):
if not self._references_loaded:
raise RuntimeError("Reference data not loaded")
# Validate reference shapes
self.ref_states = self.ref_states.view(-class="num">1, class="num">1)
self.ref_actions = self.ref_actions.view(-class="num">1, class="num">1)
self.ref_rewards = self.ref_rewards.view(-class="num">1, class="num">1)
states = self.knn_predict(z, self.ref_states)
actions = self.gaussian_predict(z, self.ref_actions)
rewards = self.linear_predict(z, self.ref_rewards)
class="kw">return states, actions, rewards
def forward(self, features, states=None, actions=None, rewards=None):
z_mean, z_logvar = self.encode(features)
z = self.reparameterize(z_mean, z_logvar)
if states is not None and actions is not None and rewards is not None:
class="kw">return {
&class="macro">#x27;z&class="macro">#x27;: z,
&class="macro">#x27;z_mean&class="macro">#x27;: z_mean,
&class="macro">#x27;z_logvar&class="macro">#x27;: z_logvar
}
else:
pred_states, pred_actions, pred_rewards = self.predict_from_encoding(z)
class="kw">return {
&class="macro">#x27;states&class="macro">#x27;: pred_states,
&class="macro">#x27;actions&class="macro">#x27;: pred_actions,
&class="macro">#x27;rewards&class="macro">#x27;: pred_rewards
}◍ 用 MMD 把真实与生成分布拉到一起算
瓦瑟斯坦 VAE 在落地时常用 MMD-损失来替掉传统的 KL 项,核心就是拿真实样本 y_true 和生成样本 y_pred 做分布比对。维度必须对齐,否则配对距离根本算不出来;kernel_mul 与 kernel_num 这两个参数管的是 RBF 内核的带宽组,直接决定模型对分布差异多尺度的敏感程度。 代码里先把 xx、yy 拼成 xy 做一次 cdist(p=2 欧氏距离),所有配对在一个矩阵里算完,省内存也避免分开算带来不一致。内核用几何间隔带宽 kernel_mul^k 覆盖多个尺度,并跳过 σ=0 防零除;每个内核按计数平均,保证损失尺度在不同配置下可比。 MMD 主公式是 k_xx + k_yy - 2k_xy,从有限样本取均值估期望,最后除以 2*kernel_num 归一。默认 kernel_num=5 意味着实际用了 11 个带宽(k 从 -5 到 5),这是回测时最先该动的旋钮。 想让数值更稳,可在分母加小 ε、对极小内核值走对数域、或剪裁极端距离防溢出;带宽自适应、换非 RBF 内核也值得试。下面这段 Python(PyTorch)可直接抄去验证,注意 FASR 输入特征来自前几篇 MA 与随机振荡器代码,动作奖励来自强化学习部分。
<span class="keyword">def</span> mmd_loss(y_true, y_pred, kernel_mul=<span class="number">class="num">2.0</span>, kernel_num=<span class="number">class="num">5</span>): <span class="class="type">class="kw">string">""" MMD loss using Gaussian RBF kernel. Args: y_true: Ground truth samples(shape: [batch_size, dim]) y_pred: Predicted samples(shape: [batch_size, dim]) kernel_mul: Multiplier for kernel bandwidths kernel_num: Number of kernels to use Returns: MMD loss(scalar) """</span> batch_size = y_true.size(<span class="number">class="num">0</span>) <span class="comment"># Combine real and predicted samples</span> xx = y_true yy = y_pred xy = torch.cat([xx, yy], dim=<span class="number">class="num">0</span>) <span class="comment"># Compute pairwise distances</span> distances = torch.cdist(xy, xy, p=<span class="number">class="num">2</span>) <span class="comment"># Compute MMD using multiple RBF kernels</span> loss = <span class="number">class="num">0.0</span> <span class="keyword">for</span> sigma <span class="keyword">in</span> [kernel_mul ** k <span class="keyword">for</span> k <span class="keyword">in</span> <span class="built_in">range</span>(-kernel_num, kernel_num + <span class="number">class="num">1</span>)]: <span class="keyword">if</span> sigma == <span class="number">class="num">0</span>: <span class="keyword">class="kw">continue</span> kernel_val = torch.exp(-distances ** <span class="number">class="num">2</span> / (<span class="number">class="num">2</span> * sigma ** <span class="number">class="num">2</span>)) k_xx = kernel_val[:batch_size, :batch_size] k_yy = kernel_val[batch_size:, batch_size:] k_xy = kernel_val[:batch_size, batch_size:] <span class="comment"># MMD formula: E[k(x,x)] + E[k(y,y)] - class="num">2*E[k(x,y)]</span> loss += (k_xx.mean() + k_yy.mean() - <span class="number">class="num">2</span> * k_xy.mean()) <span class="keyword">class="kw">return</span> loss / (<span class="number">class="num">2</span> * kernel_num) <span class="keyword">def</span> compute_loss(predictions, batch): <span class="comment"># Ensure shapes match(squeeze if needed)</span> pred_states = predictions[<span class="class="type">class="kw">string">&class="macro">#x27;states&class="macro">#x27;</span>].squeeze(-<span class="number">class="num">1</span>) <span class="comment"># [B, class="num">1] → [B]</span> pred_actions = predictions[<span class="class="type">class="kw">string">&class="macro">#x27;actions&class="macro">#x27;</span>].squeeze(-<span class="number">class="num">1</span>) pred_rewards = predictions[<span class="class="type">class="kw">string">&class="macro">#x27;rewards&class="macro">#x27;</span>].squeeze(-<span class="number">class="num">1</span>) <span class="comment"># MMD Loss(distributional matching)</span> mmd_state = mmd_loss(batch[<span class="class="type">class="kw">string">&class="macro">#x27;states&class="macro">#x27;</span>], pred_states) mmd_action = mmd_loss(batch[<span class="class="type">class="kw">string">&class="macro">#x27;actions&class="macro">#x27;</span>], pred_actions) mmd_reward = mmd_loss(batch[<span class="class="type">class="kw">string">&class="macro">#x27;rewards&class="macro">#x27;</span>], pred_rewards) <span class="comment"># Combine losses(adjust weights as needed)</span> total_loss = mmd_state + mmd_action + mmd_reward <span class="keyword">class="kw">return</span> { <span class="class="type">class="kw">string">&class="macro">#x27;loss&class="macro">#x27;</span>: total_loss, <span class="class="type">class="kw">string">&class="macro">#x27;mmd_state&class="macro">#x27;</span>: mmd_state, <span class="class="type">class="kw">string">&class="macro">#x27;mmd_action&class="macro">#x27;</span>: mmd_action, <span class="class="type">class="kw">string">&class="macro">#x27;mmd_reward&class="macro">#x27;</span>: mmd_reward }