It operates by incrementally learning patterns and identifying unexpected events in dynamic environments. Is a unsupervised and adaptative method with a algorithmic complexity .
Based on two strategies:
- GWR: Grow When Required
- SOM: Self-Organizing Maps
Main parameters:
- : Changes the cluster centroid
- : Controls the Gaussian spread
- : Sigma adaptation for each specific cluster
In the context of Markov Chains each cluster defined by a Gaussian represents one possible state on the chain.
import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
from ucimlrepo import fetch_ucirepo
sns.set_theme()
data = np.array(
[
[5, 5],
[5.2, 4.8],
[4.7, 5.3],
[10, 10],
[10.2, 9.8],
[9.7, 10.3],
[5, 5],
]
)
data
array([[ 5. , 5. ],
[ 5.2, 4.8],
[ 4.7, 5.3],
[10. , 10. ],
[10.2, 9.8],
[ 9.7, 10.3],
[ 5. , 5. ]])
def get_closest_gaussian(example, centroids, sigma, threshold=1e-3):
"""Find the closest Gaussian cluster from the example point. If no clusters
have been created the function return -1."""
# check if there is no centroids
if centroids.size == 0:
return -1
euclidian_distance = np.apply_along_axis(
lambda center: (np.sum((center - example) ** 2)) ** (1 / 2),
1,
centroids,
)
activation = np.exp(-(euclidian_distance**2) / (2 * sigma**2))
idx = activation.argmax()
# manages the sensitivity for new clusters creation
if activation[idx] < threshold:
return -1
return idx
def estimate_markov_chain_entropy_rate(adj_mat: np.array) -> float:
"""Calculates the Shannon entropy for a Markov Chain given your
adjacency matrix representation."""
return -1 * np.sum(adj_mat * np.log2(adj_mat))
def sonde(X, alpha=0.01, sigma=1, eps=1e-7, delta=0.01, verbose=True):
d = X.shape[1]
centroids = np.empty((0, d))
old_pos = -1
curr_pos = -1
markov = None
old_H = -1
curr_H = -1
H = []
delta_H = []
for point in X:
example = point.reshape(1, d)
curr_pos = get_closest_gaussian(example, centroids, sigma)
if curr_pos == -1:
# create a new Gaussian
if centroids.size == 0:
centroids = np.concatenate((centroids, example), axis=0)
curr_pos = 0
markov = np.array([[eps]])
else:
centroids = np.concatenate((centroids, example), axis=0)
curr_pos = centroids.shape[0] - 1
if centroids.shape[0] > 1:
markov = np.pad(
markov,
((0, 1), (0, 1)),
mode="constant",
constant_values=eps,
)
else:
# adapt an existing Gaussian
centroids[curr_pos, :] = (1 - alpha) * centroids[
curr_pos, :
] + alpha * example
if old_pos != -1:
markov[old_pos, curr_pos] = markov[old_pos, curr_pos] + delta
if verbose:
print(f"Moved from state ( {old_pos} ) to ( {curr_pos} )")
# normalize the adjacency matrix
for i in range(markov.shape[0]):
markov[i] = markov[i, :] / sum(markov[i, :])
# calculate the entropy
curr_H = estimate_markov_chain_entropy_rate(markov)
H.append(curr_H)
# calculate the entropy difference
if old_H != -1:
delta_H.append(curr_H - old_H)
if verbose:
print(f"Markov Adjacency Matrix:\n{markov}")
print("-" * 50)
old_pos = curr_pos
old_H = curr_H
return delta_H, H, centroids, markov
dH, H, centroids, markov = sonde(X=data, verbose=True)
Markov Adjacency Matrix:
[[1.]]
--------------------------------------------------
Moved from state ( 0 ) to ( 0 )
Markov Adjacency Matrix:
[[1.]]
--------------------------------------------------
Moved from state ( 0 ) to ( 0 )
Markov Adjacency Matrix:
[[1.]]
--------------------------------------------------
Moved from state ( 0 ) to ( 1 )
Markov Adjacency Matrix:
[[0.99009891 0.00990109]
[0.5 0.5 ]]
--------------------------------------------------
Moved from state ( 1 ) to ( 1 )
Markov Adjacency Matrix:
[[0.99009891 0.00990109]
[0.4950495 0.5049505 ]]
--------------------------------------------------
Moved from state ( 1 ) to ( 1 )
Markov Adjacency Matrix:
[[0.99009891 0.00990109]
[0.49014802 0.50985198]]
--------------------------------------------------
Moved from state ( 1 ) to ( 0 )
Markov Adjacency Matrix:
[[0.99009891 0.00990109]
[0.49519606 0.50480394]]
--------------------------------------------------
centroids
array([[ 4.9989902, 5.0010098],
[ 9.99898 , 10.00102 ]])
Time Series Data Example
# fetch dataset
toy_data = fetch_ucirepo(id=849)
toy_data = pd.DataFrame(
toy_data.data.targets["Zone 1 Power Consumption"]
).values
dH, H, centroids, markov = sonde(
X=toy_data, verbose=False, sigma=1000, delta=0.08
)
print(
f"Total Number of Gaussians: {len(centroids)}\n\nCentroids:\n\n{centroids}"
)
Total Number of Gaussians: 11
Centroids:
[[33312.57309826]
[30891.4939403 ]
[28318.79031478]
[25022.61994598]
[36808.20341654]
[39446.49818873]
[22225.51194093]
[13963.59379581]
[44080.06259361]
[46417.1414591 ]
[20081.03920279]]
fig, (ax1, ax2, ax3) = plt.subplots(3, figsize=(20, 10))
fig.suptitle("SONDE Applied to Power Consumption Time Series Data")
ax1.plot(toy_data[:20000, :], "tab:gray")
ax2.plot(H[:20000], "tab:orange")
ax3.plot(dH[:20000], "tab:red")
ax1.set_ylabel("Power Consumption")
ax2.set_ylabel("Entropy")
ax3.set_ylabel(r"Entropy Variation ($\Delta H$)")
ax3.set_xlabel("Time Series")
fig.tight_layout();