Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

[project]
name = "pysits"
version = "2.0.0.dev4"
version = "2.0.0.dev5"
description = "Python wrapper for the sits R package"
readme = "README.md"
requires-python = ">=3.10,<4"
Expand Down
27 changes: 27 additions & 0 deletions pysits/conversions/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@
EPOCH_START = date(1970, 1, 1)


#
# Prefixes of R expressions deparsed as text
#
R_DEPARSED_PREFIXES = ("c(", "list(")


#
# Base utilities
#
Expand Down Expand Up @@ -197,6 +203,27 @@ def eval_r_language(obj):
return obj


def eval_r_deparsed(obj):
"""Evaluate a deparsed R expression.

Args:
obj: The R object to evaluate.

Returns:
The evaluated R object. Objects that are not deparsed R expressions are
returned unchanged.
"""
value = obj

if isinstance(value, ro.StrVector) and len(value) == 1:
value = value[0]

if isinstance(value, str) and value.startswith(R_DEPARSED_PREFIXES):
return ro.r(value)

return obj


def convert_to_python(obj, as_type="str"):
"""Convert an R object to a Python representation.

Expand Down
140 changes: 106 additions & 34 deletions pysits/models/data/tuning.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,65 @@
from typing import Any

from rpy2.rinterface_lib.sexp import NULLType
from rpy2.robjects.vectors import ListVector

from pysits.conversions.common import convert_to_python, eval_r_language
from rpy2.robjects.vectors import (
BoolVector,
FloatVector,
IntVector,
ListVector,
StrVector,
)

from pysits.conversions.common import (
R_DEPARSED_PREFIXES,
convert_to_python,
eval_r_deparsed,
eval_r_language,
)
from pysits.models.data.base import SITSData
from pysits.models.data.matrix import SITSConfusionMatrix
from pysits.models.data.ts import SITSTimeSeriesModel

#
# Python types associated with each R vector type
#
HPARAM_TYPES = {
BoolVector: "bool",
IntVector: "int",
FloatVector: "float",
StrVector: "str",
}


#
# Tuning results columns that are not hyper-parameters
#
TUNING_METRICS = ("accuracy", "kappa", "acc", "samples_validation")


#
# Utilities
#
def convert_hparam(value: Any) -> Any:
"""Convert a hyper-parameter value from R to Python.

Args:
value: The hyper-parameter value to convert.

Returns:
Any: Converted Python object.
"""
value = eval_r_deparsed(value)

for vector_type, as_type in HPARAM_TYPES.items():
if isinstance(value, vector_type):
return convert_to_python(value, as_type=as_type)

return convert_to_python(value)


#
# Classes
#
class SITSTuningResults(SITSData):
"""Base class for sits accuracy results."""

Expand Down Expand Up @@ -62,14 +113,13 @@ def _convert_attribute(self, attribute: str, transform_func: Callable) -> list[A
if isinstance(values, NULLType):
return [None for i in range(len(self._instance.rx2("accuracy")))]

# Check if value is a vector as string
# Check if value is a deparsed R expression (e.g., "c(128, 128, 128)")
is_vector_as_string = any(
isinstance(x, str) and (x.startswith("c(") or x.startswith("list("))
for x in values
isinstance(x, str) and x.startswith(R_DEPARSED_PREFIXES) for x in values
)

if is_vector_as_string:
return list(values)
return [transform_func(eval_r_deparsed(x)) for x in values]

if isinstance(values, ListVector):
return [
Expand Down Expand Up @@ -138,47 +188,47 @@ def samples_validation(self) -> list[SITSTimeSeriesModel | None]:
]

@property
def cnn_layers(self) -> list[float]:
def cnn_layers(self) -> list[list[float]]:
"""Get the CNN layer configurations from the tuning results.

Returns:
A list of float values representing the CNN layer configurations.
A list with the CNN layer configuration of each trial.
"""
return self._convert_from_r_list("cnn_layers", "float")

@property
def cnn_kernels(self) -> list[float]:
def cnn_kernels(self) -> list[list[float]]:
"""Get the CNN kernel configurations from the tuning results.

Returns:
A list of float values representing the CNN kernel configurations.
A list with the CNN kernel configuration of each trial.
"""
return self._convert_from_r_list("cnn_kernels", "float")

@property
def cnn_dropout_rates(self) -> list[float]:
def cnn_dropout_rates(self) -> list[list[float]]:
"""Get the CNN dropout rates from the tuning results.

Returns:
A list of float values representing the CNN dropout rates.
A list with the CNN dropout rates of each trial.
"""
return self._convert_from_r_list("cnn_dropout_rates", "float")

@property
def dense_layer_nodes(self) -> list[float]:
def dense_layer_nodes(self) -> list[list[float]]:
"""Get the dense layer node configurations from the tuning results.

Returns:
A list of float values representing the dense layer node configurations.
A list with the dense layer node configuration of each trial.
"""
return self._convert_from_r_list("dense_layer_nodes", "float")

@property
def dense_layer_dropout_rate(self) -> list[float]:
def dense_layer_dropout_rate(self) -> list[list[float]]:
"""Get the dense layer dropout rates from the tuning results.

Returns:
A list of float values representing the dense layer dropout rates.
A list with the dense layer dropout rates of each trial.
"""
return self._convert_from_r_list("dense_layer_dropout_rate", "float")

Expand Down Expand Up @@ -219,40 +269,33 @@ def optimizer(self) -> list[str]:
return [eval_r_language(x).rclass[0] for x in self._instance.rx2("optimizer")]

@property
def opt_hparams(self) -> list[dict[str, float]]:
def opt_hparams(self) -> list[dict[str, Any]]:
"""Get the optimizer hyperparameters from the tuning results.

Returns:
A list of dictionaries containing optimizer hyperparameters.
Each dictionary maps parameter names to their float values.
Each dictionary maps parameter names to their values.
"""
# Results
results = []

# Convert to Python
values = self._convert_from_r_list("opt_hparams", "float")

# Merge dict results
for value in values:
results.append({k: v for d in value for k, v in d.items()})

return results
return self._convert_attribute(
"opt_hparams",
lambda x: {str(k): convert_hparam(v) for k, v in x.items()},
)

@property
def lr_decay_epochs(self) -> list[float]:
def lr_decay_epochs(self) -> list[list[float]]:
"""Get the learning rate decay epochs from the tuning results.

Returns:
A list of float values representing the learning rate decay epochs.
A list with the learning rate decay epochs of each trial.
"""
return self._convert_from_r_list("lr_decay_epochs", "float")

@property
def lr_decay_rate(self) -> list[float]:
def lr_decay_rate(self) -> list[list[float]]:
"""Get the learning rate decay rates from the tuning results.

Returns:
A list of float values representing the learning rate decay rates.
A list with the learning rate decay rates of each trial.
"""
return self._convert_from_r_list("lr_decay_rate", "float")

Expand Down Expand Up @@ -283,9 +326,38 @@ def verbose(self) -> list[bool]:
"""
return self._convert_from_r_list("verbose", "bool")

@property
def hparams(self) -> dict[str, list[Any]]:
"""Get all the hyper-parameters tuned, one value per trial.

Returns:
A dictionary mapping each hyper-parameter name to its values.
"""
return {
name: getattr(self, name)
for name in self._instance.names
if name not in TUNING_METRICS
}

#
# Dunder methods
#
def __getattr__(self, name: str) -> list[Any]:
"""Get a hyper-parameter with no property associated with it.

Args:
name: The name of the hyper-parameter to get.

Returns:
A list with the hyper-parameter values, one per trial.
"""
if name.startswith("_") or name not in self._instance.names:
raise AttributeError(
f"'{type(self).__name__}' object has no attribute '{name}'"
)

return self._convert_attribute(name, convert_hparam)

def __str__(self):
"""String representation."""
return str(self._instance)
Expand Down
70 changes: 5 additions & 65 deletions pysits/templates/tuning.html
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@
<div class="sits-accordion">
{# Assuming number of accuracy is the same as number of runs #}
{% set n_runs = tuning_obj.accuracy|length if tuning_obj.accuracy else 0 %}
{% set hparams = tuning_obj.hparams %}
{% for i in range(n_runs) %}
<div class="sits-accordion-item">
<div class="sits-accordion-header" onclick="toggleAccordion(this)">
Expand All @@ -109,73 +110,12 @@
<th>Value</th>
</tr>

{% if tuning_obj.cnn_layers %}
{% for name, values in hparams.items() %}
<tr>
<td>CNN Layers</td>
{{ render_list_value(tuning_obj.cnn_layers[i]) }}
<td>{{ name }}</td>
{{ render_list_value(values[i]) }}
</tr>
<tr>
<td>CNN Kernels</td>
{{ render_list_value(tuning_obj.cnn_kernels[i]) }}
</tr>
<tr>
<td>CNN Dropout Rates</td>
{{ render_list_value(tuning_obj.cnn_dropout_rates[i]) }}
</tr>
{% endif %}

{% if tuning_obj.dense_layer_nodes %}
<tr>
<td>Dense Layer Nodes</td>
{{ render_list_value(tuning_obj.dense_layer_nodes[i]) }}
</tr>
<tr>
<td>Dense Layer Dropout Rate</td>
{{ render_list_value(tuning_obj.dense_layer_dropout_rate[i]) }}
</tr>
{% endif %}

<tr>
<td>Epochs</td>
<td>{{ tuning_obj.epochs[i] }}</td>
</tr>
<tr>
<td>Batch Size</td>
<td>{{ tuning_obj.batch_size[i] }}</td>
</tr>
<tr>
<td>Validation Split</td>
<td>{{ tuning_obj.validation_split[i] }}</td>
</tr>

{% if tuning_obj.optimizer %}
<tr>
<td>Optimizer</td>
<td>{{ tuning_obj.optimizer[i] }}</td>
</tr>
{% endif %}

{% if tuning_obj.lr_decay_epochs %}
<tr>
<td>Learning Rate Decay Epochs</td>
{{ render_list_value(tuning_obj.lr_decay_epochs[i]) }}
</tr>
<tr>
<td>Learning Rate Decay Rate</td>
{{ render_list_value(tuning_obj.lr_decay_rate[i]) }}
</tr>
{% endif %}

{% if tuning_obj.patience %}
<tr>
<td>Patience</td>
<td>{{ tuning_obj.patience[i] }}</td>
</tr>
<tr>
<td>Minimum Delta</td>
<td>{{ tuning_obj.min_delta[i] }}</td>
</tr>
{% endif %}
{% endfor %}
</table>
</div>
</div>
Expand Down
Loading
Loading