chore: import upstream snapshot with attribution
CI / ci (3.11) (push) Has been cancelled
CI / ci (3.10) (push) Has been cancelled
CI / dependabot (push) Has been cancelled
Release / release_and_publish (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:36:15 +08:00
commit e64161ec32
892 changed files with 116950 additions and 0 deletions
@@ -0,0 +1,21 @@
from rdagent.components.coder.CoSTEER import CoSTEER
from rdagent.components.coder.CoSTEER.config import CoSTEER_SETTINGS
from rdagent.components.coder.CoSTEER.evaluators import CoSTEERMultiEvaluator
from rdagent.components.coder.model_coder.evaluators import ModelCoSTEEREvaluator
from rdagent.components.coder.model_coder.evolving_strategy import (
ModelMultiProcessEvolvingStrategy,
)
from rdagent.core.scenario import Scenario
class ModelCoSTEER(CoSTEER):
def __init__(
self,
scen: Scenario,
*args,
**kwargs,
) -> None:
eva = CoSTEERMultiEvaluator(ModelCoSTEEREvaluator(scen=scen), scen=scen)
es = ModelMultiProcessEvolvingStrategy(scen=scen, settings=CoSTEER_SETTINGS)
super().__init__(*args, settings=CoSTEER_SETTINGS, eva=eva, es=es, evolving_version=2, scen=scen, **kwargs)
@@ -0,0 +1,71 @@
# TODO: inherent from the benchmark base class
import torch
from rdagent.components.coder.model_coder.model import ModelFBWorkspace
def get_data_conf(init_val):
# TODO: design this step in the workflow
in_dim = 1000
in_channels = 128
exec_config = {"model_eval_param_init": init_val}
node_feature = torch.randn(in_dim, in_channels)
edge_index = torch.randint(0, in_dim, (2, 2000))
return (node_feature, edge_index), exec_config
class ModelImpValEval:
"""
Evaluate the similarity of the model structure by changing the input and observe the output.
Assumption:
- If the model structure is similar, the output will change in similar way when we change the input.
Challenge:
- The key difference between it and implementing models is that we have parameters in the layers (Model operators often have no parameters or are given parameters).
- we try to initialize the model param in similar value. So only the model structure is different.
Comparing the correlation of following sequences
- modelA[init1](input1).hidden_out1, modelA[init1](input2).hidden_out1, ...
- modelB[init1](input1).hidden_out1, modelB[init1](input2).hidden_out1, ...
For each hidden output, we can calculate a correlation. The average correlation will be the metrics.
"""
def evaluate(self, gt: ModelFBWorkspace, gen: ModelFBWorkspace):
round_n = 10
eval_pairs: list[tuple] = []
# run different input value
for _ in range(round_n):
# run different model initial parameters.
for init_val in [-0.2, -0.1, 0.1, 0.2]:
_, gt_res = gt.execute(input_value=init_val, param_init_value=init_val)
_, res = gen.execute(input_value=init_val, param_init_value=init_val)
eval_pairs.append((res, gt_res))
# flat and concat the output
res_batch, gt_res_batch = [], []
for res, gt_res in eval_pairs:
res_batch.append(res.reshape(-1))
gt_res_batch.append(gt_res.reshape(-1))
res_batch = torch.stack(res_batch)
gt_res_batch = torch.stack(gt_res_batch)
res_batch = res_batch.detach().numpy()
gt_res_batch = gt_res_batch.detach().numpy()
# pearson correlation of each hidden output
def norm(x):
return (x - x.mean(axis=0)) / x.std(axis=0)
dim_corr = (norm(res_batch) * norm(gt_res_batch)).mean(axis=0) # the correlation of each hidden output
# aggregate all the correlation
avr_corr = dim_corr.mean()
# FIXME:
# It is too high(e.g. 0.944) .
# Check if it is not a good evaluation!!
# Maybe all the same initial params will results in extreamly high correlation without regard to the model structure.
return avr_corr
@@ -0,0 +1,134 @@
import math
from typing import Any, Callable, Dict, Optional, Union
import torch
from torch import Tensor
from torch.nn import Parameter
from torch_geometric.nn.conv import GCNConv, MessagePassing
from torch_geometric.nn.inits import zeros
from torch_geometric.nn.resolver import activation_resolver
from torch_geometric.typing import Adj
class AntiSymmetricConv(torch.nn.Module):
r"""The anti-symmetric graph convolutional operator from the
`"Anti-Symmetric DGN: a stable architecture for Deep Graph Networks"
<https://openreview.net/forum?id=J3Y7cgZOOS>`_ paper.
.. math::
\mathbf{x}^{\prime}_i = \mathbf{x}_i + \epsilon \cdot \sigma \left(
(\mathbf{W}-\mathbf{W}^T-\gamma \mathbf{I}) \mathbf{x}_i +
\Phi(\mathbf{X}, \mathcal{N}_i) + \mathbf{b}\right),
where :math:`\Phi(\mathbf{X}, \mathcal{N}_i)` denotes a
:class:`~torch.nn.conv.MessagePassing` layer.
Args:
in_channels (int): Size of each input sample.
phi (MessagePassing, optional): The message passing module
:math:`\Phi`. If set to :obj:`None`, will use a
:class:`~torch_geometric.nn.conv.GCNConv` layer as default.
(default: :obj:`None`)
num_iters (int, optional): The number of times the anti-symmetric deep
graph network operator is called. (default: :obj:`1`)
epsilon (float, optional): The discretization step size
:math:`\epsilon`. (default: :obj:`0.1`)
gamma (float, optional): The strength of the diffusion :math:`\gamma`.
It regulates the stability of the method. (default: :obj:`0.1`)
act (str, optional): The non-linear activation function :math:`\sigma`,
*e.g.*, :obj:`"tanh"` or :obj:`"relu"`. (default: :class:`"tanh"`)
act_kwargs (Dict[str, Any], optional): Arguments passed to the
respective activation function defined by :obj:`act`.
(default: :obj:`None`)
bias (bool, optional): If set to :obj:`False`, the layer will not learn
an additive bias. (default: :obj:`True`)
Shapes:
- **input:**
node features :math:`(|\mathcal{V}|, F_{in})`,
edge indices :math:`(2, |\mathcal{E}|)`,
edge weights :math:`(|\mathcal{E}|)` *(optional)*
- **output:** node features :math:`(|\mathcal{V}|, F_{in})`
"""
def __init__(
self,
in_channels: int,
phi: Optional[MessagePassing] = None,
num_iters: int = 1,
epsilon: float = 0.1,
gamma: float = 0.1,
act: Union[str, Callable, None] = "tanh",
act_kwargs: Optional[Dict[str, Any]] = None,
bias: bool = True,
):
super().__init__()
self.in_channels = in_channels
self.num_iters = num_iters
self.gamma = gamma
self.epsilon = epsilon
self.act = activation_resolver(act, **(act_kwargs or {}))
if phi is None:
phi = GCNConv(in_channels, in_channels, bias=False)
self.W = Parameter(torch.empty(in_channels, in_channels))
self.register_buffer("eye", torch.eye(in_channels))
self.phi = phi
if bias:
self.bias = Parameter(torch.empty(in_channels))
else:
self.register_parameter("bias", None)
self.reset_parameters()
def reset_parameters(self):
r"""Resets all learnable parameters of the module."""
torch.nn.init.kaiming_uniform_(self.W, a=math.sqrt(5))
self.phi.reset_parameters()
zeros(self.bias)
def forward(self, x: Tensor, edge_index: Adj, *args, **kwargs) -> Tensor:
r"""Runs the forward pass of the module."""
antisymmetric_W = self.W - self.W.t() - self.gamma * self.eye
for _ in range(self.num_iters):
h = self.phi(x, edge_index, *args, **kwargs)
h = x @ antisymmetric_W.t() + h
if self.bias is not None:
h += self.bias
if self.act is not None:
h = self.act(h)
x = x + self.epsilon * h
return x
def __repr__(self) -> str:
return (
f"{self.__class__.__name__}("
f"{self.in_channels}, "
f"phi={self.phi}, "
f"num_iters={self.num_iters}, "
f"epsilon={self.epsilon}, "
f"gamma={self.gamma})"
)
model_cls = AntiSymmetricConv
if __name__ == "__main__":
node_features = torch.load("node_features.pt")
edge_index = torch.load("edge_index.pt")
# Model instantiation and forward pass
model = AntiSymmetricConv(in_channels=node_features.size(-1))
output = model(node_features, edge_index)
# Save output to a file
torch.save(output, "gt_output.pt")
@@ -0,0 +1,89 @@
import copy
import torch
from torch import Tensor
from torch_geometric.nn.conv import MessagePassing
class DirGNNConv(torch.nn.Module):
r"""A generic wrapper for computing graph convolution on directed
graphs as described in the `"Edge Directionality Improves Learning on
Heterophilic Graphs" <https://arxiv.org/abs/2305.10498>`_ paper.
:class:`DirGNNConv` will pass messages both from source nodes to target
nodes and from target nodes to source nodes.
Args:
conv (MessagePassing): The underlying
:class:`~torch_geometric.nn.conv.MessagePassing` layer to use.
alpha (float, optional): The alpha coefficient used to weight the
aggregations of in- and out-edges as part of a convex combination.
(default: :obj:`0.5`)
root_weight (bool, optional): If set to :obj:`True`, the layer will add
transformed root node features to the output.
(default: :obj:`True`)
"""
def __init__(
self,
conv: MessagePassing,
alpha: float = 0.5,
root_weight: bool = True,
):
super().__init__()
self.alpha = alpha
self.root_weight = root_weight
self.conv_in = copy.deepcopy(conv)
self.conv_out = copy.deepcopy(conv)
if hasattr(conv, "add_self_loops"):
self.conv_in.add_self_loops = False
self.conv_out.add_self_loops = False
if hasattr(conv, "root_weight"):
self.conv_in.root_weight = False
self.conv_out.root_weight = False
if root_weight:
self.lin = torch.nn.Linear(conv.in_channels, conv.out_channels)
else:
self.lin = None
self.reset_parameters()
def reset_parameters(self):
r"""Resets all learnable parameters of the module."""
self.conv_in.reset_parameters()
self.conv_out.reset_parameters()
if self.lin is not None:
self.lin.reset_parameters()
def forward(self, x: Tensor, edge_index: Tensor) -> Tensor:
"""""" # noqa: D419
x_in = self.conv_in(x, edge_index)
x_out = self.conv_out(x, edge_index.flip([0]))
out = self.alpha * x_out + (1 - self.alpha) * x_in
if self.root_weight:
out = out + self.lin(x)
return out
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.conv_in}, alpha={self.alpha})"
model_cls = DirGNNConv
if __name__ == "__main__":
node_features = torch.load("node_features.pt")
edge_index = torch.load("edge_index.pt")
# Model instantiation and forward pass
model = DirGNNConv(MessagePassing())
output = model(node_features, edge_index)
# Save output to a file
torch.save(output, "gt_output.pt")
@@ -0,0 +1,198 @@
import inspect
from typing import Any, Dict, Optional
import torch
import torch.nn.functional as F
from torch import Tensor
from torch.nn import Dropout, Linear, Sequential
from torch_geometric.nn.attention import PerformerAttention
from torch_geometric.nn.conv import MessagePassing
from torch_geometric.nn.inits import reset
from torch_geometric.nn.resolver import activation_resolver, normalization_resolver
from torch_geometric.typing import Adj
from torch_geometric.utils import to_dense_batch
class GPSConv(torch.nn.Module):
r"""The general, powerful, scalable (GPS) graph transformer layer from the
`"Recipe for a General, Powerful, Scalable Graph Transformer"
<https://arxiv.org/abs/2205.12454>`_ paper.
The GPS layer is based on a 3-part recipe:
1. Inclusion of positional (PE) and structural encodings (SE) to the input
features (done in a pre-processing step via
:class:`torch_geometric.transforms`).
2. A local message passing layer (MPNN) that operates on the input graph.
3. A global attention layer that operates on the entire graph.
.. note::
For an example of using :class:`GPSConv`, see
`examples/graph_gps.py
<https://github.com/pyg-team/pytorch_geometric/blob/master/examples/
graph_gps.py>`_.
Args:
channels (int): Size of each input sample.
conv (MessagePassing, optional): The local message passing layer.
heads (int, optional): Number of multi-head-attentions.
(default: :obj:`1`)
dropout (float, optional): Dropout probability of intermediate
embeddings. (default: :obj:`0.`)
act (str or Callable, optional): The non-linear activation function to
use. (default: :obj:`"relu"`)
act_kwargs (Dict[str, Any], optional): Arguments passed to the
respective activation function defined by :obj:`act`.
(default: :obj:`None`)
norm (str or Callable, optional): The normalization function to
use. (default: :obj:`"batch_norm"`)
norm_kwargs (Dict[str, Any], optional): Arguments passed to the
respective normalization function defined by :obj:`norm`.
(default: :obj:`None`)
attn_type (str): Global attention type, :obj:`multihead` or
:obj:`performer`. (default: :obj:`multihead`)
attn_kwargs (Dict[str, Any], optional): Arguments passed to the
attention layer. (default: :obj:`None`)
"""
def __init__(
self,
channels: int,
conv: Optional[MessagePassing],
heads: int = 1,
dropout: float = 0.0,
act: str = "relu",
act_kwargs: Optional[Dict[str, Any]] = None,
norm: Optional[str] = "batch_norm",
norm_kwargs: Optional[Dict[str, Any]] = None,
attn_type: str = "multihead",
attn_kwargs: Optional[Dict[str, Any]] = None,
):
super().__init__()
self.channels = channels
self.conv = conv
self.heads = heads
self.dropout = dropout
self.attn_type = attn_type
attn_kwargs = attn_kwargs or {}
if attn_type == "multihead":
self.attn = torch.nn.MultiheadAttention(
channels,
heads,
batch_first=True,
**attn_kwargs,
)
elif attn_type == "performer":
self.attn = PerformerAttention(
channels=channels,
heads=heads,
**attn_kwargs,
)
else:
# TODO: Support BigBird
raise ValueError(f"{attn_type} is not supported")
self.mlp = Sequential(
Linear(channels, channels * 2),
activation_resolver(act, **(act_kwargs or {})),
Dropout(dropout),
Linear(channels * 2, channels),
Dropout(dropout),
)
norm_kwargs = norm_kwargs or {}
self.norm1 = normalization_resolver(norm, channels, **norm_kwargs)
self.norm2 = normalization_resolver(norm, channels, **norm_kwargs)
self.norm3 = normalization_resolver(norm, channels, **norm_kwargs)
self.norm_with_batch = False
if self.norm1 is not None:
signature = inspect.signature(self.norm1.forward)
self.norm_with_batch = "batch" in signature.parameters
def reset_parameters(self):
r"""Resets all learnable parameters of the module."""
if self.conv is not None:
self.conv.reset_parameters()
self.attn._reset_parameters()
reset(self.mlp)
if self.norm1 is not None:
self.norm1.reset_parameters()
if self.norm2 is not None:
self.norm2.reset_parameters()
if self.norm3 is not None:
self.norm3.reset_parameters()
def forward(
self,
x: Tensor,
edge_index: Adj,
batch: Optional[torch.Tensor] = None,
**kwargs,
) -> Tensor:
r"""Runs the forward pass of the module."""
hs = []
if self.conv is not None: # Local MPNN.
h = self.conv(x, edge_index, **kwargs)
h = F.dropout(h, p=self.dropout, training=self.training)
h = h + x
if self.norm1 is not None:
if self.norm_with_batch:
h = self.norm1(h, batch=batch)
else:
h = self.norm1(h)
hs.append(h)
# Global attention transformer-style model.
h, mask = to_dense_batch(x, batch)
if isinstance(self.attn, torch.nn.MultiheadAttention):
h, _ = self.attn(h, h, h, key_padding_mask=~mask, need_weights=False)
elif isinstance(self.attn, PerformerAttention):
h = self.attn(h, mask=mask)
h = h[mask]
h = F.dropout(h, p=self.dropout, training=self.training)
h = h + x # Residual connection.
if self.norm2 is not None:
if self.norm_with_batch:
h = self.norm2(h, batch=batch)
else:
h = self.norm2(h)
hs.append(h)
out = sum(hs) # Combine local and global outputs.
out = out + self.mlp(out)
if self.norm3 is not None:
if self.norm_with_batch:
out = self.norm3(out, batch=batch)
else:
out = self.norm3(out)
return out
def __repr__(self) -> str:
return (
f"{self.__class__.__name__}({self.channels}, "
f"conv={self.conv}, heads={self.heads}, "
f"attn_type={self.attn_type})"
)
model_cls = GPSConv
if __name__ == "__main__":
node_features = torch.load("node_features.pt")
edge_index = torch.load("edge_index.pt")
# Model instantiation and forward pass
model = GPSConv(channels=node_features.size(-1), conv=MessagePassing())
output = model(node_features, edge_index)
# Save output to a file
torch.save(output, "gt_output.pt")
@@ -0,0 +1,187 @@
import math
import torch
from torch import Tensor
from torch.nn import BatchNorm1d, Parameter
from torch_geometric.nn import inits
from torch_geometric.nn.conv import MessagePassing
from torch_geometric.nn.models import MLP
from torch_geometric.typing import Adj, OptTensor
from torch_geometric.utils import spmm
class SparseLinear(MessagePassing):
def __init__(self, in_channels: int, out_channels: int, bias: bool = True):
super().__init__(aggr="add")
self.in_channels = in_channels
self.out_channels = out_channels
self.weight = Parameter(torch.empty(in_channels, out_channels))
if bias:
self.bias = Parameter(torch.empty(out_channels))
else:
self.register_parameter("bias", None)
self.reset_parameters()
def reset_parameters(self):
inits.kaiming_uniform(self.weight, fan=self.in_channels, a=math.sqrt(5))
inits.uniform(self.in_channels, self.bias)
def forward(
self,
edge_index: Adj,
edge_weight: OptTensor = None,
) -> Tensor:
# propagate_type: (weight: Tensor, edge_weight: OptTensor)
out = self.propagate(edge_index, weight=self.weight, edge_weight=edge_weight)
if self.bias is not None:
out = out + self.bias
return out
def message(self, weight_j: Tensor, edge_weight: OptTensor) -> Tensor:
if edge_weight is None:
return weight_j
else:
return edge_weight.view(-1, 1) * weight_j
def message_and_aggregate(self, adj_t: Adj, weight: Tensor) -> Tensor:
return spmm(adj_t, weight, reduce=self.aggr)
class LINKX(torch.nn.Module):
r"""The LINKX model from the `"Large Scale Learning on Non-Homophilous
Graphs: New Benchmarks and Strong Simple Methods"
<https://arxiv.org/abs/2110.14446>`_ paper.
.. math::
\mathbf{H}_{\mathbf{A}} &= \textrm{MLP}_{\mathbf{A}}(\mathbf{A})
\mathbf{H}_{\mathbf{X}} &= \textrm{MLP}_{\mathbf{X}}(\mathbf{X})
\mathbf{Y} &= \textrm{MLP}_{f} \left( \sigma \left( \mathbf{W}
[\mathbf{H}_{\mathbf{A}}, \mathbf{H}_{\mathbf{X}}] +
\mathbf{H}_{\mathbf{A}} + \mathbf{H}_{\mathbf{X}} \right) \right)
.. note::
For an example of using LINKX, see `examples/linkx.py <https://
github.com/pyg-team/pytorch_geometric/blob/master/examples/linkx.py>`_.
Args:
num_nodes (int): The number of nodes in the graph.
in_channels (int): Size of each input sample, or :obj:`-1` to derive
the size from the first input(s) to the forward method.
hidden_channels (int): Size of each hidden sample.
out_channels (int): Size of each output sample.
num_layers (int): Number of layers of :math:`\textrm{MLP}_{f}`.
num_edge_layers (int, optional): Number of layers of
:math:`\textrm{MLP}_{\mathbf{A}}`. (default: :obj:`1`)
num_node_layers (int, optional): Number of layers of
:math:`\textrm{MLP}_{\mathbf{X}}`. (default: :obj:`1`)
dropout (float, optional): Dropout probability of each hidden
embedding. (default: :obj:`0.0`)
"""
def __init__(
self,
num_nodes: int,
in_channels: int,
hidden_channels: int,
out_channels: int,
num_layers: int,
num_edge_layers: int = 1,
num_node_layers: int = 1,
dropout: float = 0.0,
):
super().__init__()
self.num_nodes = num_nodes
self.in_channels = in_channels
self.out_channels = out_channels
self.num_edge_layers = num_edge_layers
self.edge_lin = SparseLinear(num_nodes, hidden_channels)
if self.num_edge_layers > 1:
self.edge_norm = BatchNorm1d(hidden_channels)
channels = [hidden_channels] * num_edge_layers
self.edge_mlp = MLP(channels, dropout=0.0, act_first=True)
else:
self.edge_norm = None
self.edge_mlp = None
channels = [in_channels] + [hidden_channels] * num_node_layers
self.node_mlp = MLP(channels, dropout=0.0, act_first=True)
self.cat_lin1 = torch.nn.Linear(hidden_channels, hidden_channels)
self.cat_lin2 = torch.nn.Linear(hidden_channels, hidden_channels)
channels = [hidden_channels] * num_layers + [out_channels]
self.final_mlp = MLP(channels, dropout=dropout, act_first=True)
self.reset_parameters()
def reset_parameters(self):
r"""Resets all learnable parameters of the module."""
self.edge_lin.reset_parameters()
if self.edge_norm is not None:
self.edge_norm.reset_parameters()
if self.edge_mlp is not None:
self.edge_mlp.reset_parameters()
self.node_mlp.reset_parameters()
self.cat_lin1.reset_parameters()
self.cat_lin2.reset_parameters()
self.final_mlp.reset_parameters()
def forward(
self,
x: OptTensor,
edge_index: Adj,
edge_weight: OptTensor = None,
) -> Tensor:
"""""" # noqa: D419
out = self.edge_lin(edge_index, edge_weight)
if self.edge_norm is not None and self.edge_mlp is not None:
out = out.relu_()
out = self.edge_norm(out)
out = self.edge_mlp(out)
out = out + self.cat_lin1(out)
if x is not None:
x = self.node_mlp(x)
out = out + x
out = out + self.cat_lin2(x)
return self.final_mlp(out.relu_())
def __repr__(self) -> str:
return (
f"{self.__class__.__name__}(num_nodes={self.num_nodes}, "
f"in_channels={self.in_channels}, "
f"out_channels={self.out_channels})"
)
model_cls = LINKX
if __name__ == "__main__":
node_features = torch.load("node_features.pt")
edge_index = torch.load("edge_index.pt")
# Model instantiation and forward pass
model = LINKX(
num_nodes=node_features.size(0),
in_channels=node_features.size(1),
hidden_channels=node_features.size(1),
out_channels=node_features.size(1),
num_layers=1,
)
output = model(node_features, edge_index)
# Save output to a file
torch.save(output, "gt_output.pt")
@@ -0,0 +1,118 @@
from typing import Optional
import torch
import torch.nn.functional as F
from torch import Tensor
from torch_geometric.nn import SimpleConv
from torch_geometric.nn.dense.linear import Linear
class PMLP(torch.nn.Module):
r"""The P(ropagational)MLP model from the `"Graph Neural Networks are
Inherently Good Generalizers: Insights by Bridging GNNs and MLPs"
<https://arxiv.org/abs/2212.09034>`_ paper.
:class:`PMLP` is identical to a standard MLP during training, but then
adopts a GNN architecture during testing.
Args:
in_channels (int): Size of each input sample.
hidden_channels (int): Size of each hidden sample.
out_channels (int): Size of each output sample.
num_layers (int): The number of layers.
dropout (float, optional): Dropout probability of each hidden
embedding. (default: :obj:`0.`)
norm (bool, optional): If set to :obj:`False`, will not apply batch
normalization. (default: :obj:`True`)
bias (bool, optional): If set to :obj:`False`, the module
will not learn additive biases. (default: :obj:`True`)
"""
def __init__(
self,
in_channels: int,
hidden_channels: int,
out_channels: int,
num_layers: int,
dropout: float = 0.0,
norm: bool = True,
bias: bool = True,
):
super().__init__()
self.in_channels = in_channels
self.hidden_channels = hidden_channels
self.out_channels = out_channels
self.num_layers = num_layers
self.dropout = dropout
self.bias = bias
self.lins = torch.nn.ModuleList()
self.lins.append(Linear(in_channels, hidden_channels, self.bias))
for _ in range(self.num_layers - 2):
lin = Linear(hidden_channels, hidden_channels, self.bias)
self.lins.append(lin)
self.lins.append(Linear(hidden_channels, out_channels, self.bias))
self.norm = None
if norm:
self.norm = torch.nn.BatchNorm1d(
hidden_channels,
affine=False,
track_running_stats=False,
)
self.conv = SimpleConv(aggr="mean", combine_root="self_loop")
self.reset_parameters()
def reset_parameters(self):
r"""Resets all learnable parameters of the module."""
for lin in self.lins:
torch.nn.init.xavier_uniform_(lin.weight, gain=1.414)
if self.bias:
torch.nn.init.zeros_(lin.bias)
def forward(
self,
x: torch.Tensor,
edge_index: Optional[Tensor] = None,
) -> torch.Tensor:
"""""" # noqa: D419
if not self.training and edge_index is None:
raise ValueError(f"'edge_index' needs to be present during " f"inference in '{self.__class__.__name__}'")
for i in range(self.num_layers):
x = x @ self.lins[i].weight.t()
if not self.training:
x = self.conv(x, edge_index)
if self.bias:
x = x + self.lins[i].bias
if i != self.num_layers - 1:
if self.norm is not None:
x = self.norm(x)
x = x.relu()
x = F.dropout(x, p=self.dropout, training=self.training)
return x
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.in_channels}, " f"{self.out_channels}, num_layers={self.num_layers})"
model_cls = PMLP
if __name__ == "__main__":
node_features = torch.load("node_features.pt")
edge_index = torch.load("edge_index.pt")
# Model instantiation and forward pass
model = PMLP(
in_channels=node_features.size(-1),
hidden_channels=node_features.size(-1),
out_channels=node_features.size(-1),
num_layers=1,
)
output = model(node_features, edge_index)
# Save output to a file
torch.save(output, "gt_output.pt")
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,80 @@
{
"PMLP": {
"description": "`PMLP` is identical to a standard MLP during training, but then adopts a GNN architecture (add message passing) during testing.",
"formulation": "\\hat{y}_u = \\psi(\\text{MP}(\\{h^{(l-1)}_v\\}_{v \\in N_u \\cup \\{u\\}}))",
"variables": {
"\\hat{y}_u": "The predicted output for node u",
"\\psi": "A function representing the feed-forward process, consisting of a linear feature transformation followed by a non-linear activation",
"\\text{MP}": "Message Passing operation that aggregates neighbored information",
"h^{(l-1)}_v": "The feature representation of node v at layer (l-1)",
"N_u": "The set of neighbored nodes centered at node u"
},
"key": "pmlp",
"model_type": "TimeSeries"
},
"LINKX": {
"description": "A scalable model for node classification that separately embeds adjacency and node features, combines them with MLPs, and applies simple transformations.",
"formulation": "Y = MLP_f(\\sigma(W[h_A; h_X] + h_A + h_X))",
"variables": {
"Y": "The output predictions",
"\\sigma": "Non-linear activation function",
"W": "Learned weight matrix",
"h_A": "Embedding of the adjacency matrix",
"h_X": "Embedding of the node features",
"MLP_f": "Final multilayer perceptron for prediction"
},
"key": "linkx",
"model_type": "TimeSeries"
},
"GPSConv": {
"description": "A scalable and powerful graph transformer with linear complexity, capable of handling large graphs with state-of-the-art results across diverse benchmarks.",
"formulation": "X^{(l+1)} = \\text{MPNN}^{(l)}(X^{(l)}, A) + \\text{GlobalAttn}^{(l)}(X^{(l)})",
"variables": {
"X^{(l)}": "The node features at layer l",
"A": "The adjacency matrix of the graph",
"X^{(l+1)}": "The updated node features at layer l+1",
"MPNN^{(l)}": "The message-passing neural network function at layer l",
"GlobalAttn^{(l)}": "The global attention function at layer l"
},
"key": "gpsconv",
"model_type": "TimeSeries"
},
"ViSNet": {
"description": "ViSNet is an equivariant geometry-enhanced graph neural network designed for efficient molecular modeling[^1^][1][^2^][2]. It utilizes a Vector-Scalar interactive message passing mechanism to extract and utilize geometric features with low computational costs, achieving state-of-the-art performance on multiple molecular dynamics benchmarks.",
"formulation": "\\text{ViSNet}(G) = \\sum_{u \\in G} f(\\mathbf{h}_u, \\mathbf{e}_u, \\mathbf{v}_u)",
"variables": {
"\\mathbf{h}_u": "Node embedding for atom u",
"\\mathbf{e}_u": "Edge embedding associated with atom u",
"\\mathbf{v}_u": "Direction unit vector for atom u"
},
"key": "visnet",
"model_type": "TimeSeries"
},
"Dir-GNN": {
"description": "A framework for deep learning on directed graphs that extends MPNNs to incorporate edge directionality.",
"formulation": "x^{(k)}_i = COM^{(k)}\\left(x^{(k-1)}_i, m^{(k)}_{i,\\leftarrow}, m^{(k)}_{i,\\rightarrow}\\right)",
"variables": {
"x^{(k)}_i": "The feature representation of node i at layer k",
"m^{(k)}_{i,\\leftarrow}": "The aggregated incoming messages to node i at layer k",
"m^{(k)}_{i,\\rightarrow}": "The aggregated outgoing messages from node i at layer k"
},
"key": "dirgnn",
"model_type": "TimeSeries"
},
"A-DGN": {
"description": "A framework for stable and non-dissipative DGN design, conceived through the lens of ordinary differential equations (ODEs). It ensures long-range information preservation between nodes and prevents gradient vanishing or explosion during training.",
"formulation": "\\frac{\\partial x_u(t)}{\\partial t} = \\sigma(W^T x_u(t) + \\Phi(X(t), N_u) + b)",
"variables": {
"x_u(t)": "The state of node u at time t",
"\\frac{\\partial x_u(t)}{\\partial t}": "The rate of change of the state of node u at time t",
"\\sigma": "A monotonically non-decreasing activation function",
"W": "A weight matrix",
"b": "A bias vector",
"\\Phi(X(t), N_u)": "The aggregation function for the states of the nodes in the neighborhood of u",
"X(t)": "The node feature matrix of the whole graph at time t",
"N_u": "The set of neighboring nodes of u"
},
"key": "A-DGN",
"model_type": "TimeSeries"
}
}
@@ -0,0 +1,38 @@
from typing import Optional
from pydantic_settings import SettingsConfigDict
from rdagent.components.coder.CoSTEER.config import CoSTEERSettings
from rdagent.utils.env import Env, QlibCondaConf, QlibCondaEnv, QTDockerEnv
class ModelCoSTEERSettings(CoSTEERSettings):
model_config = SettingsConfigDict(env_prefix="MODEL_CoSTEER_")
env_type: str = "conda" # or "docker"
"""Environment to run model code in coder and runner: 'conda' for local conda env, 'docker' for Docker container"""
def get_model_env(
conf_type: Optional[str] = None,
extra_volumes: dict = {},
running_timeout_period: int = 600,
enable_cache: Optional[bool] = None,
) -> Env:
conf = ModelCoSTEERSettings()
if conf.env_type == "docker":
env = QTDockerEnv()
elif conf.env_type == "conda":
env = QlibCondaEnv(conf=QlibCondaConf())
else:
raise ValueError(f"Unknown env type: {conf.env_type}")
env.conf.extra_volumes = extra_volumes.copy()
env.conf.running_timeout_period = running_timeout_period
if enable_cache is not None:
env.conf.enable_cache = enable_cache
env.prepare()
return env
MODEL_COSTEER_SETTINGS = ModelCoSTEERSettings()
@@ -0,0 +1,166 @@
import json
from typing import Dict, Tuple
import numpy as np
from rdagent.components.coder.CoSTEER.evaluators import CoSTEEREvaluator
from rdagent.components.coder.model_coder.model import ModelFBWorkspace, ModelTask
from rdagent.core.experiment import Task, Workspace
from rdagent.oai.llm_conf import LLM_SETTINGS
from rdagent.oai.llm_utils import APIBackend
from rdagent.utils.agent.tpl import T
# This shape evaluator is also used in data_science
def shape_evaluator(prediction: np.ndarray, target_shape: Tuple = None) -> Tuple[str, bool]:
if target_shape is None or prediction is None:
return (
"No output generated from the model. No shape evaluation conducted.",
False,
)
pre_shape = prediction.shape
if pre_shape == target_shape:
return "The shape of the output is correct.", True
else:
return (
f"The shape of the output is incorrect. Expected {target_shape}, but got {pre_shape}.",
False,
)
def value_evaluator(
prediction: np.ndarray,
target: np.ndarray,
) -> Tuple[np.ndarray, bool]:
if prediction is None:
return "No output generated from the model. Skip value evaluation", False
elif target is None:
return (
"No ground truth output provided. Value evaluation not impractical",
False,
)
else:
# Calculate the mean absolute difference
diff = np.mean(np.abs(target - prediction))
return (
f"The value of the output is correct. The mean absolute difference is {diff}.",
diff < 0.1,
)
class ModelCodeEvaluator(CoSTEEREvaluator):
def evaluate(
self,
target_task: Task,
implementation: Workspace,
gt_implementation: Workspace,
model_execution_feedback: str = "",
model_value_feedback: str = "",
):
assert isinstance(target_task, ModelTask)
assert isinstance(implementation, ModelFBWorkspace)
if gt_implementation is not None:
assert isinstance(gt_implementation, ModelFBWorkspace)
model_task_information = target_task.get_task_information()
code = implementation.all_codes
system_prompt = T(".prompts:evaluator_code_feedback.system").r(
scenario=(
self.scen.get_scenario_all_desc(target_task, filtered_tag=target_task.model_type)
if self.scen is not None
else "No scenario description."
)
)
execution_feedback_to_render = model_execution_feedback
for _ in range(10): # 10 times to split the content is enough
user_prompt = T(".prompts:evaluator_code_feedback.user").r(
model_information=model_task_information,
code=code,
model_execution_feedback=execution_feedback_to_render,
model_value_feedback=model_value_feedback,
gt_code=gt_implementation.all_codes if gt_implementation else None,
)
if (
APIBackend().build_messages_and_calculate_token(
user_prompt=user_prompt,
system_prompt=system_prompt,
)
> APIBackend().chat_token_limit
):
execution_feedback_to_render = execution_feedback_to_render[len(execution_feedback_to_render) // 2 :]
else:
break
critic_response = APIBackend().build_messages_and_create_chat_completion(
user_prompt=user_prompt,
system_prompt=system_prompt,
json_mode=False,
)
return critic_response, None
class ModelFinalEvaluator(CoSTEEREvaluator):
def evaluate(
self,
target_task: Task,
implementation: Workspace,
gt_implementation: Workspace,
model_execution_feedback: str,
model_shape_feedback: str,
model_value_feedback: str,
model_code_feedback: str,
):
assert isinstance(target_task, ModelTask)
assert isinstance(implementation, ModelFBWorkspace)
if gt_implementation is not None:
assert isinstance(gt_implementation, ModelFBWorkspace)
system_prompt = T(".prompts:evaluator_final_feedback.system").r(
scenario=(
self.scen.get_scenario_all_desc(target_task, filtered_tag=target_task.model_type)
if self.scen is not None
else "No scenario description."
)
)
execution_feedback_to_render = model_execution_feedback
for _ in range(10): # 10 times to split the content is enough
user_prompt = T(".prompts:evaluator_final_feedback.user").r(
model_information=target_task.get_task_information(),
model_execution_feedback=execution_feedback_to_render,
model_shape_feedback=model_shape_feedback,
model_code_feedback=model_code_feedback,
model_value_feedback=model_value_feedback,
)
if (
APIBackend().build_messages_and_calculate_token(
user_prompt=user_prompt,
system_prompt=system_prompt,
)
> APIBackend().chat_token_limit
):
execution_feedback_to_render = execution_feedback_to_render[len(execution_feedback_to_render) // 2 :]
else:
break
final_evaluation_dict = json.loads(
APIBackend().build_messages_and_create_chat_completion(
user_prompt=user_prompt,
system_prompt=system_prompt,
json_mode=True,
json_target_type=Dict[str, str | bool | int],
),
)
if isinstance(final_evaluation_dict["final_decision"], str) and final_evaluation_dict[
"final_decision"
].lower() in ("true", "false"):
final_evaluation_dict["final_decision"] = bool(final_evaluation_dict["final_decision"])
return (
final_evaluation_dict["final_feedback"],
final_evaluation_dict["final_decision"],
)
@@ -0,0 +1,104 @@
from rdagent.components.coder.CoSTEER.evaluators import (
CoSTEEREvaluator,
CoSTEERMultiFeedback,
CoSTEERSingleFeedbackDeprecated,
)
from rdagent.components.coder.model_coder.eva_utils import (
ModelCodeEvaluator,
ModelFinalEvaluator,
shape_evaluator,
value_evaluator,
)
from rdagent.components.coder.model_coder.model import ModelFBWorkspace, ModelTask
from rdagent.core.evolving_framework import QueriedKnowledge
from rdagent.core.experiment import Task, Workspace
ModelSingleFeedback = CoSTEERSingleFeedbackDeprecated
ModelMultiFeedback = CoSTEERMultiFeedback
class ModelCoSTEEREvaluator(CoSTEEREvaluator):
def evaluate(
self,
target_task: Task,
implementation: Workspace,
gt_implementation: Workspace,
queried_knowledge: QueriedKnowledge = None,
**kwargs,
) -> ModelSingleFeedback:
target_task_information = target_task.get_task_information()
if (
queried_knowledge is not None
and target_task_information in queried_knowledge.success_task_to_knowledge_dict
):
return queried_knowledge.success_task_to_knowledge_dict[target_task_information].feedback
elif queried_knowledge is not None and target_task_information in queried_knowledge.failed_task_info_set:
return ModelSingleFeedback(
execution_feedback="This task has failed too many times, skip implementation.",
shape_feedback="This task has failed too many times, skip implementation.",
value_feedback="This task has failed too many times, skip implementation.",
code_feedback="This task has failed too many times, skip implementation.",
final_feedback="This task has failed too many times, skip implementation.",
final_decision=False,
)
assert isinstance(target_task, ModelTask)
# NOTE: Use fixed input to test the model to avoid randomness
batch_size = 8
num_features = 30
num_timesteps = 40
input_value = 0.4
param_init_value = 0.6
assert isinstance(implementation, ModelFBWorkspace)
model_execution_feedback, gen_np_array = implementation.execute(
batch_size=batch_size,
num_features=num_features,
num_timesteps=num_timesteps,
input_value=input_value,
param_init_value=param_init_value,
)
if gt_implementation is not None:
assert isinstance(gt_implementation, ModelFBWorkspace)
_, gt_np_array = gt_implementation.execute(
batch_size=batch_size,
num_features=num_features,
num_timesteps=num_timesteps,
input_value=input_value,
param_init_value=param_init_value,
)
else:
gt_np_array = None
shape_feedback, shape_decision = shape_evaluator(
gen_np_array,
(batch_size, self.scen.model_output_channel if hasattr(self.scen, "model_output_channel") else 1),
)
value_feedback, value_decision = value_evaluator(gen_np_array, gt_np_array)
code_feedback, _ = ModelCodeEvaluator(scen=self.scen).evaluate(
target_task=target_task,
implementation=implementation,
gt_implementation=gt_implementation,
model_execution_feedback=model_execution_feedback,
model_value_feedback="\n".join([shape_feedback, value_feedback]),
)
final_feedback, final_decision = ModelFinalEvaluator(scen=self.scen).evaluate(
target_task=target_task,
implementation=implementation,
gt_implementation=gt_implementation,
model_execution_feedback=model_execution_feedback,
model_shape_feedback=shape_feedback,
model_value_feedback=value_feedback,
model_code_feedback=code_feedback,
)
return ModelSingleFeedback(
execution_feedback=model_execution_feedback,
shape_feedback=shape_feedback,
value_feedback=value_feedback,
code_feedback=code_feedback,
final_feedback=final_feedback,
final_decision=final_decision,
value_generated_flag=(gen_np_array is not None),
final_decision_based_on_gt=(gt_implementation is not None),
)
@@ -0,0 +1,89 @@
import json
from typing import Dict
from rdagent.components.coder.CoSTEER.config import CoSTEER_SETTINGS
from rdagent.components.coder.CoSTEER.evaluators import CoSTEERSingleFeedback
from rdagent.components.coder.CoSTEER.evolving_strategy import (
MultiProcessEvolvingStrategy,
)
from rdagent.components.coder.CoSTEER.knowledge_management import (
CoSTEERQueriedKnowledge,
CoSTEERQueriedKnowledgeV2,
)
from rdagent.components.coder.model_coder.model import ModelFBWorkspace, ModelTask
from rdagent.core.experiment import FBWorkspace
from rdagent.oai.llm_conf import LLM_SETTINGS
from rdagent.oai.llm_utils import APIBackend
from rdagent.utils.agent.tpl import T
class ModelMultiProcessEvolvingStrategy(MultiProcessEvolvingStrategy):
def implement_one_task(
self,
target_task: ModelTask,
queried_knowledge: CoSTEERQueriedKnowledge = None,
workspace: FBWorkspace | None = None,
prev_task_feedback: CoSTEERSingleFeedback | None = None,
) -> str:
model_information_str = target_task.get_task_information()
queried_similar_successful_knowledge = (
queried_knowledge.task_to_similar_task_successful_knowledge[model_information_str]
if queried_knowledge is not None
else []
)
queried_former_failed_knowledge = (
queried_knowledge.task_to_former_failed_traces[model_information_str]
if queried_knowledge is not None
else []
)
queried_former_failed_knowledge_to_render = (
queried_former_failed_knowledge[0]
if isinstance(queried_knowledge, CoSTEERQueriedKnowledgeV2)
else queried_former_failed_knowledge
)
system_prompt = T(".prompts:evolving_strategy_model_coder.system").r(
scenario=self.scen.get_scenario_all_desc(filtered_tag="model"),
queried_former_failed_knowledge=queried_former_failed_knowledge_to_render,
current_code=workspace.file_dict.get("model.py"),
)
queried_similar_successful_knowledge_to_render = queried_similar_successful_knowledge
for _ in range(10): # max attempt to reduce the length of user_prompt
user_prompt = T(".prompts:evolving_strategy_model_coder.user").r(
model_information_str=model_information_str,
queried_similar_successful_knowledge=queried_similar_successful_knowledge_to_render,
queried_former_failed_knowledge=queried_former_failed_knowledge_to_render,
)
if (
APIBackend().build_messages_and_calculate_token(
user_prompt=user_prompt,
system_prompt=system_prompt,
)
< APIBackend().chat_token_limit
):
break
elif len(queried_former_failed_knowledge_to_render) > 1:
queried_former_failed_knowledge_to_render = queried_former_failed_knowledge_to_render[1:]
elif len(queried_similar_successful_knowledge_to_render) > 1:
queried_similar_successful_knowledge_to_render = queried_similar_successful_knowledge_to_render[1:]
code = json.loads(
APIBackend(use_chat_cache=CoSTEER_SETTINGS.coder_use_cache).build_messages_and_create_chat_completion(
user_prompt=user_prompt,
system_prompt=system_prompt,
json_mode=True,
json_target_type=Dict[str, str],
),
)["code"]
return code
def assign_code_list_to_evo(self, code_list, evo):
for index in range(len(evo.sub_tasks)):
if code_list[index] is None:
continue
if evo.sub_workspace_list[index] is None:
evo.sub_workspace_list[index] = ModelFBWorkspace(target_task=evo.sub_tasks[index])
evo.sub_workspace_list[index].inject_files(**{"model.py": code_list[index]})
return evo
@@ -0,0 +1,136 @@
"""
This is just an exmaple.
It will be replaced wtih a list of ground truth tasks.
"""
import math
from typing import Any, Callable, Dict, Optional, Union
import torch
from torch import Tensor
from torch.nn import Parameter
from torch_geometric.nn.conv import GCNConv, MessagePassing
from torch_geometric.nn.inits import zeros
from torch_geometric.nn.resolver import activation_resolver
from torch_geometric.typing import Adj
class AntiSymmetricConv(torch.nn.Module):
r"""The anti-symmetric graph convolutional operator from the
`"Anti-Symmetric DGN: a stable architecture for Deep Graph Networks"
<https://openreview.net/forum?id=J3Y7cgZOOS>`_ paper.
.. math::
\mathbf{x}^{\prime}_i = \mathbf{x}_i + \epsilon \cdot \sigma \left(
(\mathbf{W}-\mathbf{W}^T-\gamma \mathbf{I}) \mathbf{x}_i +
\Phi(\mathbf{X}, \mathcal{N}_i) + \mathbf{b}\right),
where :math:`\Phi(\mathbf{X}, \mathcal{N}_i)` denotes a
:class:`~torch.nn.conv.MessagePassing` layer.
Args:
in_channels (int): Size of each input sample.
phi (MessagePassing, optional): The message passing module
:math:`\Phi`. If set to :obj:`None`, will use a
:class:`~torch_geometric.nn.conv.GCNConv` layer as default.
(default: :obj:`None`)
num_iters (int, optional): The number of times the anti-symmetric deep
graph network operator is called. (default: :obj:`1`)
epsilon (float, optional): The discretization step size
:math:`\epsilon`. (default: :obj:`0.1`)
gamma (float, optional): The strength of the diffusion :math:`\gamma`.
It regulates the stability of the method. (default: :obj:`0.1`)
act (str, optional): The non-linear activation function :math:`\sigma`,
*e.g.*, :obj:`"tanh"` or :obj:`"relu"`. (default: :class:`"tanh"`)
act_kwargs (Dict[str, Any], optional): Arguments passed to the
respective activation function defined by :obj:`act`.
(default: :obj:`None`)
bias (bool, optional): If set to :obj:`False`, the layer will not learn
an additive bias. (default: :obj:`True`)
Shapes:
- **input:**
node features :math:`(|\mathcal{V}|, F_{in})`,
edge indices :math:`(2, |\mathcal{E}|)`,
edge weights :math:`(|\mathcal{E}|)` *(optional)*
- **output:** node features :math:`(|\mathcal{V}|, F_{in})`
"""
def __init__(
self,
in_channels: int,
phi: Optional[MessagePassing] = None,
num_iters: int = 1,
epsilon: float = 0.1,
gamma: float = 0.1,
act: Union[str, Callable, None] = "tanh",
act_kwargs: Optional[Dict[str, Any]] = None,
bias: bool = True,
):
super().__init__()
self.in_channels = in_channels
self.num_iters = num_iters
self.gamma = gamma
self.epsilon = epsilon
self.act = activation_resolver(act, **(act_kwargs or {}))
if phi is None:
phi = GCNConv(in_channels, in_channels, bias=False)
self.W = Parameter(torch.empty(in_channels, in_channels))
self.register_buffer("eye", torch.eye(in_channels))
self.phi = phi
if bias:
self.bias = Parameter(torch.empty(in_channels))
else:
self.register_parameter("bias", None)
self.reset_parameters()
def reset_parameters(self):
r"""Resets all learnable parameters of the module."""
torch.nn.init.kaiming_uniform_(self.W, a=math.sqrt(5))
self.phi.reset_parameters()
zeros(self.bias)
def forward(self, x: Tensor, edge_index: Adj, *args, **kwargs) -> Tensor:
r"""Runs the forward pass of the module."""
antisymmetric_W = self.W - self.W.t() - self.gamma * self.eye
for _ in range(self.num_iters):
h = self.phi(x, edge_index, *args, **kwargs)
h = x @ antisymmetric_W.t() + h
if self.bias is not None:
h += self.bias
if self.act is not None:
h = self.act(h)
x = x + self.epsilon * h
return x
def __repr__(self) -> str:
return (
f"{self.__class__.__name__}("
f"{self.in_channels}, "
f"phi={self.phi}, "
f"num_iters={self.num_iters}, "
f"epsilon={self.epsilon}, "
f"gamma={self.gamma})"
)
if __name__ == "__main__":
node_features = torch.load("node_features.pt")
edge_index = torch.load("edge_index.pt")
# Model instantiation and forward pass
model = AntiSymmetricConv(in_channels=node_features.size(-1))
output = model(node_features, edge_index)
# Save output to a file
torch.save(output, "gt_output.pt")
@@ -0,0 +1,163 @@
import pickle
import site
import traceback
from pathlib import Path
from typing import Dict, Optional
from rdagent.components.coder.CoSTEER.task import CoSTEERTask
from rdagent.components.coder.model_coder.conf import MODEL_COSTEER_SETTINGS
from rdagent.core.experiment import Experiment, FBWorkspace
from rdagent.core.utils import cache_with_pickle
from rdagent.oai.llm_utils import md5_hash
from rdagent.utils.env import KGDockerEnv, QlibCondaConf, QlibCondaEnv, QTDockerEnv
class ModelTask(CoSTEERTask):
def __init__(
self,
name: str,
description: str,
architecture: str,
*args,
hyperparameters: Dict[str, str],
training_hyperparameters: Dict[str, str],
formulation: str = None,
variables: Dict[str, str] = None,
model_type: Optional[str] = None,
**kwargs,
) -> None:
self.formulation: str = formulation
self.architecture: str = architecture
self.variables: str = variables
self.hyperparameters: str = hyperparameters
self.training_hyperparameters: str = training_hyperparameters
self.model_type: str = (
model_type # Tabular for tabular model, TimesSeries for time series model, Graph for graph model, XGBoost for XGBoost model
)
super().__init__(name=name, description=description, *args, **kwargs)
def get_task_information(self):
task_desc = f"""name: {self.name}
description: {self.description}
"""
task_desc += f"formulation: {self.formulation}\n" if self.formulation else ""
task_desc += f"architecture: {self.architecture}\n"
task_desc += f"variables: {self.variables}\n" if self.variables else ""
task_desc += f"hyperparameters: {self.hyperparameters}\n"
task_desc += f"training_hyperparameters: {self.training_hyperparameters}\n"
task_desc += f"model_type: {self.model_type}\n"
return task_desc
def get_task_brief_information(self):
task_desc = f"""name: {self.name}
description: {self.description}
"""
task_desc += f"architecture: {self.architecture}\n"
task_desc += f"hyperparameters: {self.hyperparameters}\n"
task_desc += f"training_hyperparameters: {self.training_hyperparameters}\n"
task_desc += f"model_type: {self.model_type}\n"
return task_desc
@staticmethod
def from_dict(dict):
return ModelTask(**dict)
def __repr__(self) -> str:
return f"<{self.__class__.__name__} {self.name}>"
class ModelFBWorkspace(FBWorkspace):
"""
It is a Pytorch model implementation task;
All the things are placed in a folder.
Folder
- data source and documents prepared by `prepare`
- Please note that new data may be passed in dynamically in `execute`
- code (file `model.py` ) injected by `inject_code`
- the `model.py` that contains a variable named `model_cls` which indicates the implemented model structure
- `model_cls` is a instance of `torch.nn.Module`;
We support two ways of interface:
(version 1) for qlib we'll make a script to import the model in the implementation in file `model.py` after setting the cwd into the directory
- from model import model_cls
- initialize the model by initializing it `model_cls(input_dim=INPUT_DIM)`
- And then verify the model.
(version 2) for kaggle we'll make a script to call the fit and predict function in the implementation in file `model.py` after setting the cwd into the directory
"""
def hash_func(
self,
batch_size: int = 8,
num_features: int = 10,
num_timesteps: int = 4,
num_edges: int = 20,
input_value: float = 1.0,
param_init_value: float = 1.0,
) -> str:
target_file_name = f"{batch_size}_{num_features}_{num_timesteps}_{input_value}_{param_init_value}"
for code_file_name in sorted(list(self.file_dict.keys())):
target_file_name = f"{target_file_name}_{self.file_dict[code_file_name]}"
return md5_hash(target_file_name)
@cache_with_pickle(hash_func)
def execute(
self,
batch_size: int = 8,
num_features: int = 10,
num_timesteps: int = 4,
num_edges: int = 20,
input_value: float = 1.0,
param_init_value: float = 1.0,
):
self.before_execute()
try:
if self.target_task.version == 1:
if MODEL_COSTEER_SETTINGS.env_type == "docker":
qtde = QTDockerEnv()
elif MODEL_COSTEER_SETTINGS.env_type == "conda":
qtde = QlibCondaEnv(conf=QlibCondaConf())
else:
raise ValueError(f"Unknown env_type: {MODEL_COSTEER_SETTINGS.env_type}")
else:
qtde = KGDockerEnv()
qtde.prepare()
if self.target_task.version == 1:
dump_code = f"""
MODEL_TYPE = "{self.target_task.model_type}"
BATCH_SIZE = {batch_size}
NUM_FEATURES = {num_features}
NUM_TIMESTEPS = {num_timesteps}
NUM_EDGES = {num_edges}
INPUT_VALUE = {input_value}
PARAM_INIT_VALUE = {param_init_value}
{(Path(__file__).parent / 'model_execute_template_v1.txt').read_text()}
"""
elif self.target_task.version == 2:
dump_code = (Path(__file__).parent / "model_execute_template_v2.txt").read_text()
log, results = qtde.dump_python_code_run_and_get_results(
code=dump_code,
dump_file_names=["execution_feedback_str.pkl", "execution_model_output.pkl"],
local_path=str(self.workspace_path),
env={},
code_dump_file_py_name="model_test",
)
if len(results) == 0:
raise RuntimeError(f"Error in running the model code: {log}")
[execution_feedback_str, execution_model_output] = results
except Exception as e:
execution_feedback_str = f"Execution error: {e}\nTraceback: {traceback.format_exc()}"
execution_model_output = None
if len(execution_feedback_str) > 2000:
execution_feedback_str = (
execution_feedback_str[:1000] + "....hidden long error message...." + execution_feedback_str[-1000:]
)
return execution_feedback_str, execution_model_output
ModelExperiment = Experiment
@@ -0,0 +1,44 @@
# MODEL_TYPE = "Tabular"
# BATCH_SIZE = 32
# NUM_FEATURES = 10
# NUM_TIMESTEPS = 4
# NUM_EDGES = 20
# INPUT_VALUE = 1.0
# PARAM_INIT_VALUE = 1.0
import pickle
import torch
from model import model_cls
if MODEL_TYPE == "Tabular":
input_shape = (BATCH_SIZE, NUM_FEATURES)
m = model_cls(num_features=input_shape[1])
data = torch.full(input_shape, INPUT_VALUE)
elif MODEL_TYPE == "TimeSeries":
input_shape = (BATCH_SIZE, NUM_TIMESTEPS, NUM_FEATURES)
m = model_cls(num_features=input_shape[2], num_timesteps=input_shape[1])
data = torch.full(input_shape, INPUT_VALUE)
elif MODEL_TYPE == "Graph":
node_feature = torch.randn(BATCH_SIZE, NUM_FEATURES)
edge_index = torch.randint(0, BATCH_SIZE, (2, NUM_EDGES))
m = model_cls(num_features=NUM_FEATURES)
data = (node_feature, edge_index)
else:
raise ValueError(f"Unsupported model type: {MODEL_TYPE}")
# Initialize all parameters of `m` to `param_init_value`
for _, param in m.named_parameters():
param.data.fill_(PARAM_INIT_VALUE)
# Execute the model
if MODEL_TYPE == "Graph":
out = m(*data)
else:
out = m(data)
execution_model_output = out.cpu().detach().numpy()
execution_feedback_str = f"Execution successful, output tensor shape: {execution_model_output.shape}"
pickle.dump(execution_model_output, open("execution_model_output.pkl", "wb"))
pickle.dump(execution_feedback_str, open("execution_feedback_str.pkl", "wb"))
@@ -0,0 +1,24 @@
import os
import pickle
import numpy as np
import pandas as pd
import torch
from model import fit, predict
train_X = pd.DataFrame(np.random.randn(8, 30), columns=[f"{i}" for i in range(30)])
train_y = pd.Series(np.random.randint(0, 2, 8))
valid_X = pd.DataFrame(np.random.randn(8, 30), columns=[f"{i}" for i in range(30)])
valid_y = pd.Series(np.random.randint(0, 2, 8))
model = fit(train_X, train_y, valid_X, valid_y)
execution_model_output = predict(model, valid_X)
if isinstance(execution_model_output, torch.Tensor):
execution_model_output = execution_model_output.cpu().detach().numpy()
execution_feedback_str = f"Execution successful, output numpy ndarray shape: {execution_model_output.shape}"
pickle.dump(execution_model_output, open("execution_model_output.pkl", "wb"))
pickle.dump(execution_feedback_str, open("execution_feedback_str.pkl", "wb"))
@@ -0,0 +1,35 @@
import re
from pathlib import Path
from rdagent.components.coder.model_coder.model import ModelExperiment, ModelFBWorkspace
from rdagent.core.developer import Developer
from rdagent.oai.llm_utils import APIBackend
from rdagent.utils.agent.tpl import T
DIRNAME = Path(__file__).absolute().resolve().parent
class ModelCodeWriter(Developer[ModelExperiment]):
def develop(self, exp: ModelExperiment) -> ModelExperiment:
mti_l = []
for t in exp.sub_tasks:
mti = ModelFBWorkspace(t)
mti.prepare()
user_prompt = T(".prompts:code_implement_user").r(
name=t.name,
description=t.description,
formulation=t.formulation,
variables=t.variables,
)
system_prompt = T(".prompts:code_implement_sys").r()
resp = APIBackend().build_messages_and_create_chat_completion(user_prompt, system_prompt)
# Extract the code part from the response
match = re.search(r".*```[Pp]ython\n(.*)\n```.*", resp, re.DOTALL)
code = match.group(1)
mti.inject_files(**{"model.py": code})
mti_l.append(mti)
exp.sub_workspace_list = mti_l
return exp
@@ -0,0 +1,27 @@
code_implement_sys: |-
You are an assistant whose job is to answer user's question.
code_implement_user: |-
With the following given information, write a python code using pytorch and torch_geometric to implement the model.
This model is in the graph learning field, only have one layer.
The input will be node_feature [num_nodes, dim_feature] and edge_index [2, num_edges] (It would be the input of the forward model)
There is not edge attribute or edge weight as input. The model should detect the node_feature and edge_index shape, if there is Linear transformation layer in the model, the input and output shape should be consistent. The in_channels is the dimension of the node features.
Implement the model forward function based on the following information:model formula information.
1. model name:{{name}}
2. model description:{{description}}
3. model formulation:{{formulation}}
4. model variables:{{variables}}.
You must complete the forward function as far as you can do.
Execution Your implemented code will be executed in the follow way:
The the implemented code will be placed in a file like [uuid]/model.py
We'll import the model in the implementation in file `model.py` after setting the cwd into the directory
- from model import model_cls (So you must have a variable named `model_cls` in the file)
- So your implemented code could follow the following pattern
```Python
class XXXLayer(torch.nn.Module):
...
model_cls = XXXLayer
```
- initialize the model by initializing it `model_cls(input_dim=INPUT_DIM)`
- And then verify the model by comparing the output tensors by feeding specific input tensor.
@@ -0,0 +1,156 @@
extract_model_formulation_system: |-
offer description of the proposed model in this paper, write a latex formula with variable as well as the architecture of the model. the format should be like
{
"model_name (The name of the model)": {
"description": "A detailed description of the model",
"formulation": "A LaTeX formula representing the model's formulation",
"architecture": "A detailed description of the model's architecture, e.g., neural network layers or tree structures",
"variables": {
"\\hat{y}_u": "The predicted output for node u",
"variable_name_2": "Description of variable 2",
"variable_name_3": "Description of variable 3"
},
"hyperparameters": {
"hyperparameter_name_1": "value of hyperparameter 1",
"hyperparameter_name_2": "value of hyperparameter 2",
"hyperparameter_name_3": "value of hyperparameter 3"
},
"training_hyperparameters" { # All values are for reference; you can set them yourself
"n_epochs": "100",
"lr": "1e-3",
"early_stop": 10,
"batch_size": 256,
"weight_decay": 1e-4,
}
"model_type": "Tabular or TimeSeries or Graph or XGBoost" # Should be one of "Tabular", "TimeSeries", "Graph", or "XGBoost"
}
}
such format content should be begin with ```json and end with ``` and the content should be in json format.
evolving_strategy_model_coder:
system: |-
User is trying to implement some pytorch models in the following scenario:
{{ scenario }}
Your code is expected to align the scenario in any form which means The user needs to get the prediction of the model based on the input data.
To help you write the correct code, the user might provide multiple information that helps you write the correct code:
1. The user might provide you the correct code to similar models. Your should learn from these code to write the correct code.
2. The user might provide you the failed former code and the corresponding feedback to the code. The feedback contains to the execution, the code and the model output value. You should analyze the feedback and try to correct the latest code.
3. The user might provide you the suggestion to the latest fail code and some similar fail to correct pairs. Each pair contains the fail code with similar error and the corresponding corrected version code. You should learn from these suggestion to write the correct code.
Your must write your code based on your former latest attempt below which consists of your former code and code feedback, you should read the former attempt carefully and must not modify the right part of your former code.
{% if current_code is not none %}
User has write some code before. You should write the new code based on this code. Here is the latest code:
```python
{{ current_code }}
```
Your code should be very similar to the former code which means your code should be ninety more percent same as the former code! You should not modify the right part of the code.
{% else %}
User has not write any code before. You should write the new code from scratch.
{% endif %}
{% if queried_former_failed_knowledge|length != 0 %}
--------------Your former latest attempt:---------------
=====Code to the former implementation=====
{{ queried_former_failed_knowledge[-1].implementation.all_codes }}
=====Feedback to the former implementation=====
{{ queried_former_failed_knowledge[-1].feedback }}
{% endif %}
Please response the code in the following json format. Here is an example structure for the JSON output:
{
"code": "The Python code as a string."
}
user: |-
--------------Target model information:---------------
{{ model_information_str }}
{% if queried_similar_successful_knowledge|length != 0 %}
--------------Correct code to similar models:---------------
{% for similar_successful_knowledge in queried_similar_successful_knowledge %}
=====Model {{loop.index}}:=====
{{ similar_successful_knowledge.target_task.get_task_information() }}
=====Code:=====
{{ similar_successful_knowledge.implementation.all_codes }}
{% endfor %}
{% endif %}
{% if queried_former_failed_knowledge|length != 0 %}
--------------Former failed code:---------------
{% for former_failed_knowledge in queried_former_failed_knowledge %}
=====Code to implementation {{ loop.index }}=====
{{ former_failed_knowledge.implementation.all_codes }}
=====Feedback to implementation {{ loop.index }}=====
{{ former_failed_knowledge.feedback }}
{% endfor %}
{% endif %}
evaluator_code_feedback:
system: |-
User is trying to implement some models in the following scenario:
{{ scenario }}
User will provide you the information of the model.
Your job is to check whether user's code is align with the model information and the scenario.
The user will provide the source python code and the execution error message if execution failed.
The user might provide you the ground truth code for you to provide the critic. You should not leak the ground truth code to the user in any form but you can use it to provide the critic.
User has also compared the output generated by the user's code and the ground truth code. The user will provide you some analysis results comparing two output. You may find some error in the code which caused the difference between the two output.
If the ground truth code is provided, your critic should only consider checking whether the user's code is align with the ground truth code since the ground truth is definitely correct.
If the ground truth code is not provided, your critic should consider checking whether the user's code is reasonable and correct to the description and to the scenario.
Notice that your critics are not for user to debug the code. They are sent to the coding agent to correct the code. So don't give any following items for the user to check like "Please check the code line XXX".
You suggestion should not include any code, just some clear and short suggestions. Please point out very critical issues in your response, ignore non-important issues to avoid confusion. If no big issue found in the code, you can response "No critics found".
You should provide the suggestion to each of your critic to help the user improve the code. Please response the critic in the following format. Here is an example structure for the output:
critic 1: The critic message to critic 1
critic 2: The critic message to critic 2
user: |-
--------------Model information:---------------
{{ model_information }}
--------------Python code:---------------
{{ code }}
--------------Execution feedback:---------------
{{ model_execution_feedback }}
{% if model_value_feedback is not none %}
--------------Model value feedback:---------------
{{ model_value_feedback }}
{% endif %}
{% if gt_code is not none %}
--------------Ground truth Python code:---------------
{{ gt_code }}
{% endif %}
evaluator_final_feedback:
system: |-
User is trying to implement a model in the following scenario:
{{ scenario }}
User has finished evaluation and got some feedback from the evaluator.
The evaluator run the code and get the output and provide several feedback regarding user's code and code output. You should analyze the feedback and considering the scenario and model description to give a final decision about the evaluation result. The final decision concludes whether the model is implemented correctly and if not, detail feedback containing reason and suggestion if the final decision is False.
The implementation final decision is considered in the following logic:
1. If the value and the ground truth value are exactly the same under a small tolerance, the implementation is considered correct.
2. If no ground truth value is not provided, the implementation is considered correct if the code execution is successful and the code feedback is align with the scenario and model description.
Please response the critic in the json format. Here is an example structure for the JSON output, please strictly follow the format:
{
"final_decision": True,
"final_feedback": "The final feedback message",
}
user: |-
--------------Model information:---------------
{{ model_information }}
--------------Model Execution feedback:---------------
{{ model_execution_feedback }}
--------------Model shape feedback:---------------
{{ model_shape_feedback }}
--------------Model Code feedback:---------------
{{ model_code_feedback }}
--------------Model value feedback:---------------
{{ model_value_feedback }}
@@ -0,0 +1,128 @@
from __future__ import annotations
import json
import re
from pydantic import BaseModel, Field
from rdagent.components.coder.model_coder.model import ModelTask
from rdagent.components.document_reader.document_reader import (
load_and_process_pdfs_by_langchain,
)
from rdagent.components.loader.task_loader import ModelTaskLoader
from rdagent.log import rdagent_logger as logger
from rdagent.oai.llm_utils import APIBackend
from rdagent.scenarios.qlib.experiment.model_experiment import QlibModelExperiment
from rdagent.utils.agent.tpl import T
from rdagent.utils.workflow import wait_retry
def extract_model_from_doc(doc_content: str) -> dict:
"""
Extract model information from document content.
Parameters
----------
doc_content : str
Document content.
Returns
-------
dict
{model_name: dict{description, formulation, variables}}
"""
session = APIBackend().build_chat_session(
session_system_prompt=T(".prompts:extract_model_formulation_system").r(),
)
current_user_prompt = doc_content
# Extract model information from document content.
model_dict = {}
for _ in range(10):
# try to extract model information from the document content, retry at most 10 times.
extract_result_resp = session.build_chat_completion(
user_prompt=current_user_prompt,
json_mode=False,
)
re_search_res = re.search(r"```json(.*)```", extract_result_resp, re.S)
ret_json_str = re_search_res.group(1) if re_search_res is not None else ""
try:
ret_dict = json.loads(ret_json_str)
parse_success = bool(isinstance(ret_dict, dict))
except json.JSONDecodeError:
parse_success = False
if ret_json_str is None or not parse_success:
current_user_prompt = "Your response didn't follow the instruction might be wrong json format. Try again."
else:
for name, formulation_and_description in ret_dict.items():
if name not in model_dict:
model_dict[name] = formulation_and_description
if len(model_dict) == 0:
current_user_prompt = "No model extracted. Please try again."
else:
break
logger.info(f"已经完成{len(model_dict)}个模型的提取")
return model_dict
def merge_file_to_model_dict_to_model_dict(
file_to_model_dict: dict[str, dict],
) -> dict:
model_dict = {}
for file_name in file_to_model_dict:
for model_name in file_to_model_dict[file_name]:
model_dict.setdefault(model_name, [])
model_dict[model_name].append(file_to_model_dict[file_name][model_name])
model_dict_simple_deduplication = {}
for model_name in model_dict:
if len(model_dict[model_name]) > 1:
model_dict_simple_deduplication[model_name] = max(
model_dict[model_name],
key=lambda x: len(x["formulation"]),
)
else:
model_dict_simple_deduplication[model_name] = model_dict[model_name][0]
return model_dict_simple_deduplication
def extract_model_from_docs(docs_dict):
model_dict = {}
for doc_name, doc_content in docs_dict.items():
model_dict[doc_name] = extract_model_from_doc(doc_content)
return model_dict
class ModelExperimentLoaderFromDict(ModelTaskLoader):
def load(self, model_dict: dict) -> QlibModelExperiment:
"""Load data from a dict."""
task_l = []
for model_name, model_data in model_dict.items():
task = ModelTask(
name=model_name,
description=model_data["description"],
formulation=model_data["formulation"],
architecture=model_data["architecture"],
variables=model_data["variables"],
hyperparameters=model_data["hyperparameters"],
training_hyperparameters=model_data["training_hyperparameters"],
model_type=model_data["model_type"],
)
task_l.append(task)
return QlibModelExperiment(sub_tasks=task_l)
class ModelExperimentLoaderFromPDFfiles(ModelTaskLoader):
@wait_retry(retry_n=5)
def load(self, file_or_folder_path: str) -> QlibModelExperiment:
docs_dict = load_and_process_pdfs_by_langchain(file_or_folder_path) # dict{file_path:content}
model_dict = extract_model_from_docs(
docs_dict
) # dict{file_name: dict{model_name: dict{description, formulation, variables}}}
model_dict = merge_file_to_model_dict_to_model_dict(
model_dict
) # dict {model_name: dict{description, formulation, variables}}
return ModelExperimentLoaderFromDict().load(model_dict)