# Module feyn

```
Feyn is the main Python module to build and execute models that utilizes a QLattice.
A QLattice (short for Quantum Lattice) is a device which can be used to generate and explore a vast number of models linking a set of input observations to an output prediction. The actual QLattice runs on a dedicated computing cluster which is operated by Abzu. The `feyn.QLattice` class provides a client interface to communcate with, extract models from, and update the QLattice.
The QLattice stores and updates probabilistic information about the mathematical relationships (models) between observable quantities.
The workflow is typically:
# Connect to the QLattice
>>> ql = feyn.QLattice()
# Extract a Regression QGraph
>>> qgraph = gl.get_regressor(data.columns, output="out")
# Fit the QGraph to a local dataset
>>> qgraph.fit(data)
# Pick the best Graph from the QGraph
>>> graph = qgraph[0]
# Possibly update the QLattice with the graph to make the QLattice better at this kind of model in the future
>>> ql.update(graph)
# Or use the graph to make predictions
>>> predicted_y = graph.predict(new_data)
```

## Sub-modules

*class* QLattice

```
def __init__(
url: str = None,
api_token: str = None,
config: str = None
) -> QLattice
```

```
A QLattice (short for Quantum Lattice) is a device which can be used to generate and explore a vast number of models linking a set of input observations to an output prediction.
The QLattice stores and updates probabilistic information about the mathematical relationships (models) between observable quantities.
The actual QLattice runs on a dedicated computing cluster which is operated by Abzu. The `feyn.QLattice` class provides a client interface to communicate with, extract models from, and update the QLattice.
Most interactions with the QLattice takes place through the QGraph object which represents an infinite list of graphs (or models) for a specific problem.
So, while a QLattice is able to generate any graph linking any input to any output, a QGraph merely contains the graphs that relate a specified set of inputs to a specified output.
The workflow is typically:
1) extract a QGraph from the QLattice using `QLattice.get_regressor` or `QLattice.get_classifier`.
2) fit the QGraph to a local dataset using `QGraph.fit`.
3) choose one or more models from the QGraph using `QGraph[index]`.
4) potentially update the QLattice with new knowledge using `QLattice.update`.
Arguments:
url -- URL of your QLattice. (Should not to be used in combination with the config parameter).
api_token -- Authentication token for the communicating with this QLattice. (Should not to be used in combination with the config parameter).
config -- The configuration setting in your feyn.ini or .feynrc file to load the url and api_token from. These files should be located in your home folder.
```

*property* QLattice.registers

```
registers
```

```
The RegisterCollection of the QLattice
The RegisterCollection is used to find, create and remove registers from the QLattice.
```

*property* QLattice.snapshots

```
snapshots
```

```
Collection of snapshots for this QLattice
Use this collection to capture, list and restore the complete state of a QLattice.
```

*method* QLattice.get_classifier

```
def get_classifier(
self,
registers: List[str],
output: str,
stypes: Dict[str, str] = {},
max_depth: int = 5,
specs=None
) -> 'QGraph'
```

```
Extract QGraph classifier from inputs registers to an output register.
Use this function to extract a QGraph for binary classification. Once the QGraph has been extracted from the QLattice, you'll typically use the `QGraph.fit()` function to fit the QGraph to an actual dataset.
Arguments:
registers -- List of register names to use in the QGraph.
output -- The output register name.
stypes -- An optional map from register names to semantic types.
max_depth -- The maximum depth of the graphs.
Returns:
QGraph -- The QGraph instance from the inputs to the output.
```

*method* QLattice.get_qgraph

```
def get_qgraph(
self,
registers: List[str],
output: str,
stypes: Dict[str, str] = {},
max_depth: int = 5
) -> 'QGraph'
```

```
Deprecated function to extract a QGraph. Use one of the following instead:
* qlattice.get_classifier
* qlattice.get_regressor
```

*method* QLattice.get_regressor

```
def get_regressor(
self,
registers: List[str],
output: str,
stypes: Dict[str, str] = {},
max_depth: int = 5
) -> 'QGraph'
```

```
Extract QGraph regressor from inputs registers to an output register.
Use this function to extract a QGraph for regression (continuous output values). Once the QGraph has been extracted from the QLattice, you'll typically use the `QGraph.fit()` function to fit the QGraph to an actual dataset.
Arguments:
registers -- List of register names to use in the QGraph.
output -- The output register name.
stypes -- An optional map from register names to semantic types.
max_depth -- The maximum depth of the graphs.
Returns:
QGraph -- The QGraph instance from the inputs to the output.
```

*method* QLattice.reset

```
def reset(
self
) -> None
```

```
Clear all learnings in this QLattice.
This is a potentially dangerous action. Think twice before calling this method.
```

*method* QLattice.update

```
def update(
self,
graphs: Union[feyn._graph.Graph, Iterable[feyn._graph.Graph]]
) -> None
```

```
Update QLattice with learnings from a `Graph`.
When updated, the QLattice learns to produce better QGraphs. This is how a QLattice evolves and narrows in on producing QGraphs with better and better models.
Without updating, the QLattice will not learn about good models and the QGraphs produced from the QLattice will not contain better models.
# Pick the best Graph from the QGraph and update the QLattice with it's learnings
>>> graph = qgraph[0]
>>> ql.update(graph)
Arguments:
graphs -- Graph or list of Graphs with learnings worth storing.
```

*class* QGraph

```
def __init__(
qlattice,
registers: Dict[str, str]
) -> QGraph
```

```
A QGraph is extracted from the QLattice with the `QLattice.get_regressor()` or the `QLattice.get_classifier` method. It represents infinite list of graphs linking a set of input registers to an output register.
One way to think about a QGraph is as a partially ordered list of all possible graphs complying with some constraints. The head of the list contains the best graphs found for solving a problem at any given time.
A QGraph can be gradually sorted by fitting it to one or more datasets. Fitting is done using one of two methods, the `fit` method which searches further and further into the infinite list for better graphs, and the `sort` method which merely re-sorts the head of the list with new conditions.
Since the list is infinite, the sorting can never actually finish, but using the QLattice simulator, the QGraph can be extremely smart about how and where in the infinite list to search for better graphs.
It is possible to limit or constrain which graphs belong in the infinite list in several ways, most importantly using the `filter` method.
The constructor is for internal use. QGraphs should always be generated with the `QLattice.get_graph` method.
Arguments:
graph_dict -- A dictionary containing the QGraph descriptor.
```

*method* QGraph.best

```
def best(
self,
n=5
) -> List[feyn._graph.Graph]
```

```
```

*method* QGraph.copy

```
def copy(
self
) -> 'QGraph'
```

```
Make a shallow copy of the QGraph.
Returns:
QGraph -- The copied QGraph.
```

*method* QGraph.filter

```
def filter(
self,
qfilter: feyn.filters.QGraphFilter
) -> 'QGraph'
```

```
Create a new filtered QGraph which represents all graphs matching the specified filter.
A QGraph is an infinite list of graphs complying with some constraints. These constraints are specified as filters on the QGraph itself. Notice that when filtering an infinite list, the result is still infinite!
The search for graphs can be guided or limited by filtering a QGraph on for example depth, the number of edges, or required input features.
Arguments:
qfilter {feyn.filters.QGraphFilter} -- The filter to apply.
Raises:
ValueError: Raised when the qfilter could not be understood.
Returns:
QGraph -- The filtered QGraph
```

*method* QGraph.fit

```
def fit(
self,
data,
n_samples: Union[int, NoneType] = None,
loss_function='squared_error',
show: Union[str, Callable[[feyn._graph.Graph, float], NoneType], NoneType] = 'graph',
threads: int = 1,
sample_weights=None
) -> 'QGraph'
```

```
Fit and sort the `QGraph` with the given data set. After the fitting, the head of the QGraph will be sorted by increasing loss.
Each call will further explore the infinite list of graphs, and may find better graphs that are then brought forward to their correct sorting order. Sometimes this leads to a new best graph making it to the first position in the list.
A call to fit() is comparable to an epoch in other frameworks. Every call will potentially lead to a new best graph being found. Since the list is infinite, there is always the possibility that a new superior graph is found, but in practice between 5 and 100 calls are usually sufficient.
The n_samples parameter controls how much each graph in the QGraph is trained during the process. The default behavior is to train each graph once with each sample in the dataset, unless the set is smaller than 10000, in which case the dataset will be upsampled to 10000 samples before fitting.
The samples are shuffled randomly before fitting to avoid issues with the Stochastic Gradient Descent algorithm.
The recommended workflow for fitting a QGraph involves several steps in a loop:
> for epoch in range(10):
> qgraph.fit(train) # fit the data to the training set
> best = qgraph.best() # get a list of the best performing graphs
> qlattice.update(best) # update the qlattice with the best graphs
Arguments:
data -- Training data including both input and expected values. Can be either a dict mapping register names to value arrays, or a pandas.DataFrame.
n_samples -- Number of samples to fit on. The samples will be taken from `data`, possibly over- or undersampling the data. Passing `None` means use every sample in the dataset or 10000 samples, whichever is larger.
loss_function -- Name of the loss function or the function itself. This is the loss function to use for fitting. Can either be a string or one of the functions provided in `feyn.losses`.
show -- Controls status display. If specified, it should be either "graph", "text", or a user-defined callback function which receives the best graph found so far, along with it's loss.
threads -- Number of concurrent threads to use for fitting. Choose this number to match the number of CPU cores on your machine.
sample_weights -- An optional numpy array of weights for each sample. If present, the array must have the same size as the data set, i.e. one weight for each sample
Returns:
QGraph -- The QGraph itself.
```

*method* QGraph.head

```
def head(
self,
n=5
)
```

```
Show the first `n` Graphs in the QGraph.
This only works in an interactive environment like Jupyter Notebook
Keyword Arguments:
n {int} -- Number of graphs to show. (default: {5})
```

*method* QGraph.sort

```
def sort(
self,
data,
loss_function='squared_error',
threads: int = 1,
sample_weights=None
)
```

```
Sort the head of the QGraph by loss of each graph against the provided data set. The difference between QGraph.fit and QGraph.sort is that while the fit function will bring completely new graphs to the head of the QGraph, the sort function will only sort the graphs that are already in the head.
# Fit and sort a graph from a qgraph
>>> qgraph.fit(training_data, loss_function=feyn.losses.squared_error)
>>> qgraph.sort(some_other_data, loss_function=feyn.losses.absolute_error)
# After this, the first graph in the QGraph will be the best performing graph on "some_other_data"
>>> best = qgraph[0]
Arguments:
data -- The data set to sort by. Can be either a dict mapping register names to value arrays, or a pandas.DataFrame.
loss_function -- The loss function to use when sorting graphs. Can either be a string or one of the functions provided in `feyn.losses`.
threads -- Number of concurrent threads to use for sorting. Choose this number to match the number of CPU cores on your machine.
sample_weights -- An optional numpy array of weights for each sample. If present, the array must have the same size as the data set, i.e. one weight for each sample
Returns:
QGraph -- The QGraph itself
```

*class* Graph

```
def __init__(
size: int
) -> Graph
```

```
A Graph represents a single mathematical model which can be used used for predicting.
The constructor is for internal use. You will typically use `QGraph[ix]` to pick graphs from QGraphs, or load them from a file with Graph.load().
Arguments:
size -- The number of nodes this graph contains. The actual nodes must be added to the graph after construction.
```

*property* Graph.edge_count

```
edge_count
```

```
Get the total number of edges in this graph.
```

*property* Graph.depth

```
depth
```

```
Get the depth of the graph
```

*property* Graph.edges

```
edges
```

```
Get the total number of edges in this graph.
```

*property* Graph.features

```
features
```

```
None
```

*property* Graph.target

```
target
```

```
Get the name of the output node
```

*static method* Graph.load

```
def load(
file: Union[~AnyStr, pathlib.Path, TextIO]
) -> 'Graph'
```

```
Load a `Graph` from a file.
Usually used together with `Graph.save`.
Arguments:
file -- A file-like object or a path to load the `Graph` from.
Returns:
Graph -- The loaded `Graph`-object.
```

*method* Graph.absolute_error

```
def absolute_error(
self,
data: Iterable
)
```

```
Compute the graph's absolute error on the provided data.
This function is a shorthand that is equivalent to the following code:
> y_true = data[
```

*method* Graph.accuracy_score

```
def accuracy_score(
self,
data: Iterable
)
```

```
Compute the graph's accuracy score on a data set
The accuracy score is useful to evaluate classification graphs. It is the fraction of the preditions that are correct. Formally it is defned as
(number of correct predictions) / (total number of preditions)
Arguments:
data -- Data set including both input and expected values. Can be either a dict mapping register names to value arrays, or a pandas.DataFrame.
Returns:
accuracy score for the predictions
```

*method* Graph.binary_cross_entropy

```
def binary_cross_entropy(
self,
data: Iterable
)
```

```
Compute the graph's binary cross entropy on the provided data.
This function is a shorthand that is equivalent to the following code:
> y_true = data[
```

*method* Graph.fit

```
def fit(
self,
data,
loss_function='squared_error',
sample_weights=None
)
```

```
Fit the `Graph` with the given data set. Unlike fitting a QGraph, this does not involve searching an infinite list or using the QLattice in any other way.
The purpose of this function is to allow re-fitting the model to a different dataset, for example with different baserates, or for cross-validation of a chosen Graph.
Arguments:
data -- Training data including both input and expected values. Can be either a dict mapping register names to value arrays, or a pandas.DataFrame.
loss_function -- Name of the loss function or the function itself. This is the loss function to use for fitting. Can either be a string or one of the functions provided in `feyn.losses`.
sample_weights -- An optional numpy array of weights for each sample. If present, the array must have the same size as the data set, i.e. one weight for each sample
```

*method* Graph.mae

```
def mae(
self,
data
)
```

```
Compute the graph's mean absolute error on a data set
Arguments:
data -- Data set including both input and expected values. Can be either a dict mapping register names to value arrays, or a pandas.DataFrame.
Returns:
MAE for the predictions
```

*method* Graph.mse

```
def mse(
self,
data
)
```

```
Compute the graph's mean squared error on a data set
Arguments:
data -- Data set including both input and expected values. Can be either a dict mapping register names to value arrays, or a pandas.DataFrame.
Returns:
MSE for the predictions
```

*method* Graph.plot_confusion_matrix

```
def plot_confusion_matrix(
self,
data: Iterable,
labels: Iterable = None,
title: str = 'Confusion matrix',
color_map='abzu-partial',
ax=None
) -> None
```

```
Compute and plot a Confusion Matrix.
Arguments:
data -- Data set including both input and expected values. Can be either a dict mapping register names to value arrays, or a pandas.DataFrame.
labels -- List of labels to index the matrix
title -- Title of the plot.
color_map -- Color map from matplotlib to use for the matrix
ax -- matplotlib axes object to draw to, default None
Returns:
[plot] -- matplotlib confusion matrix
```

*method* Graph.plot_regression_metrics

```
def plot_regression_metrics(
self,
data: Iterable,
title: str = 'Regression metrics',
ax=None
) -> None
```

```
Plot the graph's metrics for a regression.
This is a shorthand for calling feyn.plots.plot_regression_metrics.
Arguments:
data -- Data set including both input and expected values. Can be either a dict mapping register names to value arrays, or a pandas.DataFrame.
title -- Title of the plot.
ax -- matplotlib axes object to draw to, default None
```

*method* Graph.plot_roc_curve

```
def plot_roc_curve(
self,
data: Iterable,
title: str = 'ROC curve',
ax=None,
**kwargs
) -> None
```

```
Plot the graph's ROC curve.
This is a shorthand for calling feyn.plots.plot_roc_curve.
Arguments:
data -- Data set including both input and expected values. Can be either a dict mapping register names to value arrays, or a pandas.DataFrame.
title -- Title of the plot.
ax -- matplotlib axes object to draw to, default None
**kwargs -- additional options to pass on to matplotlib
```

*method* Graph.plot_segmented_loss

```
def plot_segmented_loss(
self,
data: Iterable,
by: Union[str, NoneType] = None,
loss_function='squared_error',
title='Segmented Loss',
ax=None
) -> None
```

```
Plot the loss by segment of a dataset.
This plot is useful to evaluate how a model performs on different subsets of the data.
Example:
> qg = qlattice.get_regressor(["age","smoker","heartrate"], output="heartrate")
> qg.fit(data)
> best = qg[0]
> feyn.plots.plot_segmented_loss(best, data, by="smoker")
This will plot a histogram of the model loss for smokers and non-smokers separately, which can help evaluate wheter the model has better performance for euther of the smoker sub-populations.
You can use any column in the dataset as the `by` parameter. If you use a numerical column, the data will be binned automatically.
Arguments:
data -- The dataset to measure the loss on.
by -- The column in the dataset to segment by.
loss_function -- The loss function to compute for each segmnent,
title -- Title of the plot.
ax -- matplotlib axes object to draw to
```

*method* Graph.plot_summary

```
def plot_summary(
self,
data: Iterable,
test: Union[Iterable, NoneType] = None,
corr_func: Union[str, NoneType] = None
) -> 'SVG'
```

```
Plot the graph's summary metrics and show the signal path.
This is a shorthand for calling feyn.plots.plot_graph_summary.
Arguments:
data {Iterable} -- Data set including both input and expected values. Must be a pandas.DataFrame.
Keyword Arguments:
test {Optional[Iterable]} -- Additional data set including both input and expected values. Must be a pandas.DataFrame. (default: {None})
corr_func {Optional[str]} -- Correlation function to use in showing the importance of individual nodes. Must be either "mi" or "pearson". (default: {None})
Returns:
SVG -- SVG of the graph summary.
```

*method* Graph.predict

```
def predict(
self,
X
) -> numpy.ndarray
```

```
Calculate predictions based on input values.
>>> graph.predict({ "age": [34, 78], "sex": ["male", "female"] })
[True, False]
Arguments:
X -- The input values. Can be either a dict mapping register names to value arrays, or a pandas.DataFrame.
Returns:
np.ndarray -- The calculated predictions.
```

*method* Graph.r2_score

```
def r2_score(
self,
data: Iterable
)
```

```
Compute the graph's r2 score on a data set
The r2 score for a regression model is defined as
1 - rss/tss
Where rss is the residual sum of squares for the predictions, and tss is the total sum of squares.
Intutively, the tss is the resuduals of a so-called "worst" model that always predicts the mean. Therefore, the r2 score expresses how much better the predictions are than such a model.
A result of 0 means that the model is no better than a model that always predicts the mean value
A result of 1 means that the model perfectly predicts the true value
It is possible to get r2 scores below 0 if the predictions are even worse than the mean model.
Arguments:
data -- Data set including both input and expected values. Can be either a dict mapping register names to value arrays, or a pandas.DataFrame.
Returns:
r2 score for the predictions
```

*method* Graph.rmse

```
def rmse(
self,
data
)
```

```
Compute the graph's root mean squared error on a data set
Arguments:
data -- Data set including both input and expected values. Can be either a dict mapping register names to value arrays, or a pandas.DataFrame.
Returns:
RMSE for the predictions
```

*method* Graph.save

```
def save(
self,
file: Union[~AnyStr, pathlib.Path, TextIO]
) -> None
```

```
Save the `Graph` to a file-like object.
The file can later be used to recreate the `Graph` with `Graph.load`.
Arguments:
file -- A file-like object or path to save the graph to.
```

*method* Graph.squared_error

```
def squared_error(
self,
data: Iterable
)
```

```
Compute the graph's squared error loss on the provided data.
This function is a shorthand that is equivalent to the following code:
> y_true = data[
```

*class* Interaction

```
def __init__(
/,
*args,
**kwargs
) -> Interaction
```

```
A graph is a collection of Interactions linked together. Interactions can be either registers or pure mathematical functions such as 'tanh' or 'multiply'.
You normally get an interaction by accessing it from a Graph with the index operation. Interactions can be inspected in various ways to examine the behavior and properties of the graph.
Example usage. Get a Graph from a QGraph and print information about it's first interaction
>>> best_graph = qgraph.select(data)[0]
>>> interaction = best[0]
>>> print(interaction.name, interaction.type)
x, fixed
```

*property* Interaction.activation

```
activation
```

```
Get the latest activation(s) from this interaction.
This is the most recent value emitted by this interaction after each use - either fit or predict.
This property is useful to examine the graph's processing of individual samples.
>>> graph.predict({'x': [3,4]})
>>> graph[0].activation
.54
```

*property* Interaction.depth

```
depth
```

```
Get the depth of this interaction within it's graph.
```

*property* Interaction.name

```
name
```

```
Get the name of the interaction.
For register interactions this is the name of the register. For other interactions, it will normally be the same as the interaction type.
```

*property* Interaction.sources

```
sources
```

```
Get the indices of the predecessors of this interaction within the graph.
```

*property* Interaction.spec

```
spec
```

```
Get the spec string of the interaction, such as 'cell:tanh(i)->i', etc.
```

*property* Interaction.state

```
state
```

```
Get internal state of interaction, such as the weigths or other paramcount.
The actual properties of the state depends on the interaction. You may examine them using dir() or though the InteractionState.__dict__ property.
Example:
>>> g = qgraph.select(data)[0]
>>> print(g[0].state.__dict__)
{'feature_min': -4.996334075927734, 'feature_max': 4.994193077087402, 'auto_adapt': True}
Or you can modify them dynamically:
>>> g[0].state.auto_adapt=False
```

*class* Snapshot

```
def __init__(
id: str,
when: datetime.datetime,
note: str
) -> Snapshot
```

```
A reference to a snapshot of a QLattice.
The constructor is for internal use. You will normally access snapshots from the `SnapshotCollection` on a QLattice:
>>> snapshot = qlattice.snapshots.capture("my snapshot")
>>> for snapshot in qlattice.snapshots:
>>> print(snapshot.id)
```

*property* Snapshot.id

```
id
```

```
A unique identifier for this snapshot
```

*property* Snapshot.note

```
note
```

```
An optional note provided by the user when the snapshot was taken
```

*property* Snapshot.when

```
when
```

```
A timestamp for when this snapshot was taken
```

*class* SnapshotCollection

```
def __init__(
qlattice
) -> SnapshotCollection
```

```
The SnapshotCollection is used to capture, list and restore the state of a QLattice.
Note: The actual snapshot is stored on the server side, so this collection manipulates references to these snapshots, including metadata about the snapshot.
Use this collection to capture snapshots of the state of the QLattice:
>>> snap = ql.snapshots.capture("Important lattice")
And restore it again later:
>>> ql.snapshots.restore(snap)
It is also possible to treat the snapshot collection as a list:
>>> for snap in ql.snapshots:
>>> print(snapshot.id, snapshot.when, snapshot.note)
1587645521.535264 2020-04-21 11:28:31.535264+02:00 Some lattice
1587645521.623532 2020-04-23 14:38:41.623532+02:00 Important lattice
```

*method* SnapshotCollection.capture

```
def capture(
self,
note: str
) -> feyn._snapshots.Snapshot
```

```
Capture a snapshot of this QLattice.
Arguments:
note -- A note which is stored along with the snapshot.
Returns:
Snapshot -- A reference to the snapshot
```

*method* SnapshotCollection.restore

```
def restore(
self,
snapshot: Union[str, feyn._snapshots.Snapshot]
)
```

```
Restore the QLattice to the state of a specific snapshot.
Arguments:
snapshot -- The snapshot the restore. Either a `Snapshot` instance returned from snapshots.capture() or a string id.
```