feat: support more models for kaggle scenario (#223)

* init commit for XGBoost

* fix some bugs

* CI issues

* CI issues

* CI issue

* edit prompts for kaggle scenario & fix some bugs

* Revised Prompts To Improve Performance on Model Type & Support of Random Forest

* edit prompts & modify evaluator.py to adapt to Kaggle scenario

* edit prompts

* fix some bugs

* CI issues

---------

Co-authored-by: Taozhi Wang <taozhi.mark.wang@gmail.com>
Co-authored-by: Xisen-Wang <xisen_application@163.com>
This commit is contained in:
TPLin22
2024-08-23 15:13:50 +08:00
committed by GitHub
parent 28166fc4f5
commit 89d4f92051
8 changed files with 226 additions and 98 deletions
@@ -26,13 +26,19 @@ evaluate_prompts = Prompts(file_path=Path(__file__).parent.parent / "prompts.yam
def shape_evaluator(prediction: torch.Tensor, 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
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
return (
f"The shape of the output is incorrect. Expected {target_shape}, but got {pre_shape}.",
False,
)
def reshape_tensor(original_tensor, target_shape):
@@ -50,7 +56,10 @@ def value_evaluator(
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
return (
"No ground truth output provided. Value evaluation not impractical",
False,
)
else:
# Calculate the mean absolute difference
diff = torch.mean(torch.abs(target - prediction)).item()
@@ -270,7 +279,11 @@ class ModelCoderEvaluator(Evaluator):
else:
gt_tensor = None
shape_feedback, shape_decision = shape_evaluator(gen_tensor, (batch_size, 1))
if target_task.model_type == "XGBoost":
shape_feedback = "Not applicable for XGBoost models"
shape_decision = True
else:
shape_feedback, shape_decision = shape_evaluator(gen_tensor, (batch_size, 1))
value_feedback, value_decision = value_evaluator(gt_tensor, gen_tensor)
code_feedback, _ = ModelCodeEvaluator(scen=self.scen).evaluate(
target_task=target_task,
+33 -16
View File
@@ -6,7 +6,9 @@ import uuid
from pathlib import Path
from typing import Any, Dict, Optional
import numpy as np
import torch
import xgboost as xgb
from rdagent.components.coder.model_coder.conf import MODEL_IMPL_SETTINGS
from rdagent.core.exception import CodeFormatError
@@ -32,9 +34,7 @@ class ModelTask(Task):
self.architecture: str = architecture
self.variables: str = variables
self.hyperparameters: str = hyperparameters
self.model_type: str = (
model_type # Tabular for tabular model, TimesSeries for time series model, Graph for graph model
)
self.model_type: str = model_type # Tabular for tabular model, TimesSeries for time series model, Graph for graph model, XGBoost for XGBoost model
def get_task_information(self):
return f"""name: {self.name}
@@ -95,9 +95,17 @@ class ModelFBWorkspace(FBWorkspace):
if cache_file_path.exists():
return pickle.load(open(cache_file_path, "rb"))
mod = get_module_by_module_path(str(self.workspace_path / "model.py"))
model_cls = mod.model_cls
if self.target_task.model_type == "Tabular":
if self.target_task.model_type != "XGBoost":
model_cls = mod.model_cls
if self.target_task.model_type == "XGBoost":
X_simulated = np.random.rand(100, num_features) # 100 samples, `num_features` features each
y_simulated = np.random.randint(0, 2, 100) # Binary target for example
params = mod.get_params()
num_round = mod.get_num_round()
dtrain = xgb.DMatrix(X_simulated, label=y_simulated)
elif self.target_task.model_type == "Tabular":
input_shape = (batch_size, num_features)
m = model_cls(num_features=input_shape[1])
data = torch.full(input_shape, input_value)
@@ -113,21 +121,30 @@ class ModelFBWorkspace(FBWorkspace):
else:
raise ValueError(f"Unsupported model type: {self.target_task.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 self.target_task.model_type == "Graph":
out = m(*data)
if self.target_task.model_type == "XGBoost":
bst = xgb.train(params, dtrain, num_round)
y_pred = bst.predict(dtrain)
execution_model_output = y_pred
execution_feedback_str = "Execution successful, model trained and predictions made."
else:
out = m(data)
# Initialize all parameters of `m` to `param_init_value`
for _, param in m.named_parameters():
param.data.fill_(param_init_value)
execution_model_output = out.cpu().detach()
execution_feedback_str = f"Execution successful, output tensor shape: {execution_model_output.shape}"
# Execute the model
if self.target_task.model_type == "Graph":
out = m(*data)
else:
out = m(data)
execution_model_output = out.cpu().detach()
execution_feedback_str = f"Execution successful, output tensor shape: {execution_model_output.shape}"
if MODEL_IMPL_SETTINGS.enable_execution_cache:
pickle.dump((execution_feedback_str, execution_model_output), open(cache_file_path, "wb"))
pickle.dump(
(execution_feedback_str, execution_model_output),
open(cache_file_path, "wb"),
)
except Exception as e:
execution_feedback_str = f"Execution error: {e}\nTraceback: {traceback.format_exc()}"
@@ -15,7 +15,7 @@ extract_model_formulation_system: |-
"hyperparameter_name_2": "value of hyperparameter 2",
"hyperparameter_name_3": "value of hyperparameter 3"
},
"model_type": "Tabular or TimeSeries or Graph" # Should be one of "Tabular", "TimeSeries", or "Graph"
"model_type": "Tabular or TimeSeries or Graph or XGBoost" # Should be one of "Tabular", "TimeSeries", "Graph", or "XGBoost"
}
}
Eg.
@@ -34,7 +34,7 @@ extract_model_formulation_system: |-
"hyperparameter_name_2": "value of hyperparameter 2",
"hyperparameter_name_3": "value of hyperparameter 3"
},
"model_type": "Tabular or TimeSeries or Graph" # Should be one of "Tabular", "TimeSeries", or "Graph"
"model_type": "Tabular or TimeSeries or Graph or RandomForest or XGBoost" # If torch & Neural network models are required, the choice should be one of "Tabular", "TimeSeries", or "Graph"
}
}
such format content should be begin with ```json and end with ``` and the content should be in json format.
@@ -99,7 +99,7 @@ evaluator_code_feedback:
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 analyze result comparing two output. You may find some error in the code which caused the difference between the two output.
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.
@@ -15,7 +15,7 @@ from rdagent.log import rdagent_logger as logger
from rdagent.oai.llm_utils import APIBackend
from rdagent.utils import convert2bool
feedback_prompts = Prompts(file_path=Path(__file__).parent.parent.parent / "qlib" / "prompts.yaml")
feedback_prompts = Prompts(file_path=Path(__file__).parent.parent / "prompts.yaml")
DIRNAME = Path(__file__).absolute().resolve().parent
@@ -2,9 +2,8 @@ import random
import numpy as np
import pandas as pd
import torch
import torch.nn as nn
from model import model_cls
import xgboost as xgb
from model import get_num_round, get_params
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.metrics import accuracy_score
@@ -15,7 +14,6 @@ from sklearn.preprocessing import OneHotEncoder
# Set random seed for reproducibility
SEED = 42
random.seed(SEED)
torch.manual_seed(SEED)
np.random.seed(SEED)
@@ -27,51 +25,22 @@ def compute_metrics_for_classification(y_true, y_pred):
def train_model(X_train, y_train, X_valid, y_valid):
"""Define and train the model."""
X_train_dense = X_train.toarray() if hasattr(X_train, "toarray") else X_train
X_valid_dense = X_valid.toarray() if hasattr(X_valid, "toarray") else X_valid
dtrain = xgb.DMatrix(X_train, label=y_train)
dvalid = xgb.DMatrix(X_valid, label=y_valid)
X_train_tensor = torch.tensor(X_train_dense, dtype=torch.float32)
y_train_tensor = torch.tensor(y_train, dtype=torch.float32).unsqueeze(1)
X_valid_tensor = torch.tensor(X_valid_dense, dtype=torch.float32)
y_valid_tensor = torch.tensor(y_valid, dtype=torch.float32).unsqueeze(1)
params = get_params()
num_round = get_num_round()
# Define the model
model = model_cls(num_features=X_train.shape[1])
evallist = [(dtrain, "train"), (dvalid, "eval")]
bst = xgb.train(params, dtrain, num_round, evallist)
# Define loss function and optimizer
criterion = nn.BCELoss() # Binary cross entropy loss
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
# Train the model
num_epochs = 150 # Number of epochs
for epoch in range(num_epochs):
model.train()
optimizer.zero_grad()
y_train_pred = model(X_train_tensor)
loss = criterion(y_train_pred, y_train_tensor)
loss.backward()
optimizer.step()
# Evaluate model on validation set after each epoch
model.eval()
with torch.no_grad():
y_valid_pred = model(X_valid_tensor)
valid_loss = criterion(y_valid_pred, y_valid_tensor)
print(f"Epoch {epoch+1}/{num_epochs}, Loss: {loss.item()}, Validation Loss: {valid_loss.item()}")
return model
return bst
def predict(model, X):
"""Make predictions using the trained model."""
X_dense = X.toarray() if hasattr(X, "toarray") else X
X_tensor = torch.tensor(X_dense, dtype=torch.float32)
model.eval()
with torch.no_grad():
y_pred = model(X_tensor)
y_pred = y_pred.numpy().flatten()
return y_pred > 0.5 # Apply threshold to get boolean predictions
dtest = xgb.DMatrix(X)
y_pred_prob = model.predict(dtest)
return y_pred_prob > 0.5 # Apply threshold to get boolean predictions
if __name__ == "__main__":
@@ -88,7 +57,10 @@ if __name__ == "__main__":
# Define preprocessors for numerical and categorical features
categorical_transformer = Pipeline(
steps=[("imputer", SimpleImputer(strategy="most_frequent")), ("onehot", OneHotEncoder(handle_unknown="ignore"))]
steps=[
("imputer", SimpleImputer(strategy="most_frequent")),
("onehot", OneHotEncoder(handle_unknown="ignore")),
]
)
numerical_transformer = Pipeline(steps=[("imputer", SimpleImputer(strategy="mean"))])
@@ -118,20 +90,16 @@ if __name__ == "__main__":
print("Final Accuracy on validation set: ", accuracy)
# Save the validation accuracy
pd.Series(data=[accuracy], index=["ACC"]).to_csv("./submission.csv")
pd.Series(data=[accuracy], index=["ACC"]).to_csv("./submission_score.csv")
# Load and preprocess the test set
submission_df = pd.read_csv("/root/.data/test.csv")
passenger_ids = submission_df["PassengerId"]
submission_df = submission_df.drop(["PassengerId", "Name"], axis=1)
X_test = preprocessor.transform(submission_df)
# Make predictions on the test set and save them
y_test_pred = predict(model, X_test)
pd.Series(y_test_pred).to_csv("./submission_update.csv", index=False)
submission_result = pd.DataFrame({"PassengerId": passenger_ids, "Transported": y_test_pred})
# submit predictions for the test set
submission_df = pd.read_csv("/root/.data/test.csv")
submission_df = submission_df.drop(["PassengerId", "Name"], axis=1)
X_test = preprocessor.transform(submission_df)
y_test_pred = predict(model, X_test)
y_test_pred.to_csv("./submission_update.csv")
submission_result.to_csv("./submission.csv", index=False)
@@ -15,10 +15,11 @@ kg_model_background: |-
You are solving this data science tasks of {{ competition_type }}:
{{competition_description}}
We provide an overall pipeline in train.py. Now fill in the provided train.py script to train a {{ competition_type }} model to get a good performance on this task.
We provide an overall pipeline in train.py. Now fill in the provided train.py script to train a {{ competition_type }} model to get a good performance on this task.
The model is a machine learning or deep learning structure designed to predict {{ target_description }}.
The data is extracted from the competition dataset, focusing on passenger attributes like {{ competition_features }}.
The data is extracted from the competition dataset, focusing on relevant attributes like {{ competition_features }}.
ModelType: The type of the model, "XGBoost" for XGBoost model.
The model is defined in the following parts:
- Name: The name of the model.
@@ -31,31 +32,32 @@ kg_model_interface: |-
Your python code should follow the interface to better interact with the user's system.
You code should contain several parts:
1. The import part: import the necessary libraries.
2. A class that is a subclass of pytorch.nn.Module. This class should have an __init__ function and a forward function, which inputs a tensor and outputs a tensor.
2. A class that is a subclass of xgboost. This class should have an __init__ function and a forward function, which inputs a tensor and outputs a tensor.
3. Set a variable called "model_cls" to the class you defined.
The user will save your code into a python file called "model.py". Then the user imports model_cls in file "model.py" after setting the cwd into the directory:
```python
from model import model_cls
from model import get_params, get_num_round
```
So your python code should follow the pattern:
```python
class XXXModel(torch.nn.Module):
def get_params():
params = {
...
model_cls = XXXModel
}
return params
def get_num_round():
return xxx
```
The model has one types, "Tabular" for tabular model. The input shape to a tabular model is (batch_size, num_features).
The output shape of the model should be (batch_size, 1).
The "batch_size" is a dynamic value which is determined by the input of forward function.
The "num_features" is static which will be provided to the model through init function.
User will initialize the tabular model with the following code:
The model has one types, "XGBoost" for XGBoost model.
The XGBoost Model leverages two critical hyperparameters: "arams" and "num_round".
"params": This hyperparameter encapsulates various settings that dictate the model's behavior and learning process.
"num_round": This hyperparameter specifies the number of training iterations the model will undergo.
User will initialize the XGBoost model with the following code:
```python
model = model_cls(num_features=num_features)
```
User will initialize the tabular model with the following code:
```python
model = model_cls(num_features=num_features)
params = get_params()
num_round = get_num_round()
```
No other parameters will be passed to the model so give other parameters a default value or just make them static.
@@ -65,10 +67,8 @@ kg_model_interface: |-
kg_model_output_format: |-
Your output should be a tensor with shape (batch_size, 1).
The output tensor should be saved in a file named "output.pth" in the same directory as your python file.
The user will evaluate the shape of the output tensor so the tensor read from "output.pth" should be 8 numbers.
Your output should be an array with the appropriate number of predictions, each prediction being a single value. The output should be a 2D array with dimensions corresponding to the number of predictions and 1 column (e.g., (8, 1) if there are 8 predictions).
kg_model_simulator: |-
The models will be trained on the Spaceship Titanic dataset and evaluated on their ability to predict whether passengers were transported using metrics like accuracy and AUC-ROC.
The models will be trained on the competition dataset and evaluated on their ability to predict whether passengers were transported using metrics like accuracy and AUC-ROC.
Model performance will be iteratively improved based on feedback from evaluation results.
+130
View File
@@ -0,0 +1,130 @@
hypothesis_and_feedback: |-
{% for hypothesis, experiment, feedback in trace.hist %}
Hypothesis {{ loop.index }}: {{ hypothesis }}
Corresponding Code (that leads to the difference in performance): {{experiment.sub_workspace_list[0].code_dict.get("model.py")}}
Observation on the result with the hypothesis: {{ feedback.observations }}
Feedback on the original hypothesis: {{ feedback.hypothesis_evaluation }}
New Feedback for Context (For you to agree or improve upon): {{ feedback.new_hypothesis }}
Reasoning for new hypothesis: {{ feedback.reason }}
Did changing to this hypothesis work? (focus on the change): {{ feedback.decision }}
{% endfor %}
hypothesis_output_format: |-
The output should follow JSON format. The schema is as follows:
{
"hypothesis": "The new hypothesis generated based on the information provided.",
"reason": "The reason why you generate this hypothesis. It should be comprehensive and logical. It should cover the other keys below and extend them.",
"concise_reason": "Two-line summary. First line focuses on a concise justification for the change. Second line generalizes a knowledge statement.",
"concise_observation": "One line summary. It focuses on the observation of the given scenario, data characteristics, or previous experiences (failures & succeses).",
"concise_justification": "One line summary. Justify the hypothesis based on theoretical principles or initial assumptions.",
"concise_knowledge": "One line summary. Transferable knowledge based on theoretical principles. Use conditional grammar. eg. "If...., ..; When..., .; and etc" Make sure that you state things clearly without ambiguity. Eg. avoid saying "previous hypothesis", because one wouldn't know what that is."
}
model_hypothesis_specification: |-
Additional Specifications:
Hypotheses should grow and evolve based on the previous hypothesis. If there is no previous hypothesis, start with something simple. Gradually Build Up Upon previous hypothesis & feedbacks. In each round, hypothesis is different. Pay attention to your previous hypothesis.
Ensure that the hypothesis focuses on the architecture of a PyTorch model. Each hypothesis should address specific architectural choices such as the type of layers, activation functions, regularization techniques, and the overall structure of the model. Avoid hypotheses related to input features or optimization processes.
Remember: if there is no hypothesis, start with something simple like MLP.
Usually, a larger model works better than a smaller one.
Logic for generating a new hypothesis: If the previous hypothesis works, try to inherit from it and grow deeper. If the previous hypotheis doesn't work, try to make changes in the current level.
Sample hypothesis evolution loop: (This is the entire loop, see what stage you are at. We want hypothesis to continue growing.) Levels include **Model Type**, **Layer Configuration**, **Activation Functions**, **Regularization Techniques**
1st Round Hypothesis: The model should be a CNN.
2nd Round Hypothesis (If first round worked: CNN is the model type level, which means that we should extend to the next level, like layer configuration): The model should be a CNN. The CNN should have 5 convolutional layers. (Reasoning: As CNN worked, we now specify the layers specification to grow the hypothesis deeper.)
3rd Round Hypothesis (If second round didn't work): The model should be a CNN. The CNN should have 3 convolutional layers. (Reasoning: As 5-layer structure didn't work in the 2nd round hypothesis, try something else within the layer configuration level.)
4th Round Hypothesis (If third round worked): The model should be a CNN. The CNN should have 3 convolutional layers. Use Leaky ReLU activation for all layers. (As last round worked, now proceed to the next level: activation functions)
5th Round Hypothesis (If fourth round worked): The model should be a CNN. The CNN should have 3 convolutional layers. Use Leaky ReLU activation for all layers. Use dropout regularization with a rate of 0.5. (Similar Reasoning & Continuing to Grow to the dropout setup)
6th Round Hypothesis (If fourth round didn't work): The model should be a CNN. The CNN should have 5 convolutional layers. Use Leaky ReLU activation for all layers. Use dropout regularization with a rate of 0.3. (Reasoning: As regularisation rate of 0.5 didn't work, we only change a new regularisation and keep the other elements that worked. This means making changes in the current level.)
model_experiment_output_format: |-
So far please only design one model to test the hypothesis!
The output should follow JSON format. The schema is as follows:
{
"model_name 1 (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"
},
"model_type": "XGBoost" # Should be "XGBoost"
},
"model_name 2 (The name of the model)": {
...
}
}
Usually a larger model works better than a smaller one. Hence, the parameters should be larger.
model_feedback_generation:
system: |-
You are a professional result analysis assistant. You will receive a result and a hypothesis.
Your task is to provide feedback on how well the result supports or refutes the hypothesis by judging from the observation of performance increase or decrease.
Please provide detailed and constructive feedback. Note that as hypothesis evolve, a general trend should be that the model grows larger.
Example JSON Structure for Result Analysis:
{
"Observations": "Your overall observations here",
"Feedback for Hypothesis": "Observations related to the hypothesis",
"New Hypothesis": "Put your new hypothesis here.",
"Reasoning": "Provide reasoning for the hypothesis here.",
"Decision": <true or false>,
}
Focus on the changes in hypothesis and justify why do hypothesis evolve like this. Also, increase complexity as the hypothesis evolves (give more layers, more neurons, and etc)
Logic for generating a new hypothesis: If the previous hypothesis works, try to inherit from it and grow deeper. If the previous hypotheis doesn't work, try to make changes in the current level.
Sample hypothesis evolution loop: (This is the entire loop, see what stage you are at. We want hypothesis to continue growing.) Levels include **Model Type**, **Layer Configuration**, **Activation Functions**, **Regularization Techniques**
1st Round Hypothesis: The model should be a CNN.
2nd Round Hypothesis (If first round worked: CNN is the model type level, which means that we should extend to the next level, like layer configuration): The model should be a CNN. The CNN should have 5 convolutional layers. (Reasoning: As CNN worked, we now specify the layers specification to grow the hypothesis deeper.)
3rd Round Hypothesis (If second round didn't work): The model should be a CNN. The CNN should have 3 convolutional layers. (Reasoning: As 5-layer structure didn't work in the 2nd round hypothesis, try something else within the layer configuration level.)
4th Round Hypothesis (If third round worked): The model should be a CNN. The CNN should have 3 convolutional layers. Use Leaky ReLU activation for all layers. (As last round worked, now proceed to the next level: activation functions)
5th Round Hypothesis (If fourth round worked): The model should be a CNN. The CNN should have 3 convolutional layers. Use Leaky ReLU activation for all layers. Use dropout regularization with a rate of 0.5. (Similar Reasoning & Continuing to Grow to the dropout setup)
6th Round Hypothesis (If fourth round didn't work): The model should be a CNN. The CNN should have 5 convolutional layers. Use Leaky ReLU activation for all layers. Use dropout regularization with a rate of 0.3. (Reasoning: As regularisation rate of 0.5 didn't work, we only change a new regularisation and keep the other elements that worked. This means making changes in the current level.)
user: |-
We are in an experiment of finding hypothesis and validating or rejecting them so that in the end we have a powerful model generated.
Here are the context: {{context}}.
{% if last_hypothesis %}
Last Round Information:
Hypothesis: {{last_hypothesis.hypothesis}}
Task: {{last_task}}
Code Implemented: {{last_code}}
Result: {{last_result}}
{% else %}
This is the first round. No previous information available. As long as the performance is not too negative (eg.ICIR is greater than 0), treat it as successful. Do not set the threshold too high.
{% endif %}
Now let's come to this round. You will receive the result and you will evaluate if the performance increases or decreases.
Hypothesis: {{hypothesis.hypothesis}}
Experiment Setup: {{exp.sub_tasks[0]}}
Code Implemented: {{exp.sub_workspace_list[0].code_dict.get("model.py")}}
Relevant Reasoning: {{hypothesis.reason}}
Result: {{exp.result}}
Compare and observe. Which result has a better return and lower risk? If the performance increases, the hypothesis should be considered positive (working).
Hence, with the hypotheses, relevant reasoning, and results in mind (comparison), provide detailed and constructive feedback and suggest a new hypothesis.
@@ -14,7 +14,7 @@ from rdagent.core.prompts import Prompts
from rdagent.core.proposal import Hypothesis, Scenario, Trace
from rdagent.scenarios.kaggle.experiment.model_experiment import KGModelExperiment
prompt_dict = Prompts(file_path=Path(__file__).parent.parent.parent / "qlib" / "prompts.yaml")
prompt_dict = Prompts(file_path=Path(__file__).parent.parent / "prompts.yaml")
KGModelHypothesis = ModelHypothesis