Формально можно описать через эпсилон-дельта, а именно это буквально спецификация устойчивости модели к шуму во входных данных. Непрерывность функции f в точке x означает, что небольшое изменение входа даёт небольшое изменение выхода. Это же лежит в основе Lipschitz-непрерывности, которую явно контролируют при обучении GAN и при защите от adversarial атак.
def is_continuous_at(f, x, delta, eps, step=1e-4):
n = int(delta / step)
for i in range(-n, n + 1):
dx = i * step
if abs(f(x + dx) - f(x)) >= eps:
return False
return True
def relu(x):
return max(0.0, x)
def step_fn(x):
return 1.0 if x >= 0 else -1.0
print(is_continuous_at(relu, 0.0, 0.01, 0.1))
print(is_continuous_at(step_fn, 0.0, 0.01, 0.1))
ReLU непрерывна в нуле, хоть и не гладкая, но маленький сдвиг входа даёт маленький сдвиг выхода. Функция знака разрывна, именно поэтому её нельзя использовать как активацию при обучении градиентным спуском, то есть производная либо ноль, либо не определена, градиент не течёт. Это основная причина, почему перцептрон со ступенчатой активацией не обучается через обратное распределение ошибок, а требует другого подхода к обучению.
import numpy as np
def model_output(x, weights):
return np.tanh(x @ weights)
weights = np.array([0.5, -0.3])
x0 = np.array([1.0, 2.0])
noise = np.random.normal(0, 0.01, size=(20, 2))
outputs = np.array([model_output(x0 + n, weights) for n in noise])
print(outputs.std())
Малый разброс outputs.std() при малом шуме на входе, является эмпирической проверкой непрерывности модели в данной точке. Если разброс выхода непропорционально велик относительно шума на входе, модель ведёт себя как разрывная функция локально.
🔵Компактность и связность
# гомеоморфизма не будет, так как я не хочу в 100500й раз рассказывать про кружки и бублики, но если кому то интересно, то можно посмотреть всякого рода обзоры доказательства гипотезы Пуанкаре(к примеру от малого мехмата)
Связность - это гарантия, что из любой точки области можно добраться в любую другую, не выходя за её пределы. В кластеризации это является критерием качества, то есть если один кластер на самом деле состоит из двух несвязных областей плотности, значит алгоритм ошибся с числом кластеров или с метрикой.
import numpy as np
from sklearn.cluster import DBSCAN
from sklearn.datasets import make_moons
X, _ = make_moons(n_samples=200, noise=0.05, random_state=0)
db = DBSCAN(eps=0.3, min_samples=5).fit(X)
print(len(set(db.labels_)) - (1 if -1 in db.labels_ else 0))
DBSCAN строит топологию на данных через открытые eps-шары и находит связные компоненты объединения этих шаров, - то есть кластеры.
Изменение эпсилон меняет топологию, то есть слишком маленький eps рвёт один кластер на много несвязных кусков, слишком большой - склеивает разные кластеры в один связный.
Компактность, это гарантия существования максимума и минимума на множестве. Инженерно: если пространство состояний компактно, любая непрерывная функция потерь на нём достигает минимума - оптимизация гарантированно не уходит в бесконечность. Некомпактное (неограниченное) пространство параметров - это одна из основных причин расходимости градиентного спуска без регуляризации.
def loss(x):
return x ** 2 - 10 * x
def grad_descent(x0, lr, steps, bound=None):
x = x0
for _ in range(steps):
grad = 2 * x - 10
x = x - lr * grad
if bound is not None:
x = np.clip(x, -bound, bound)
return x
Без clip (без компактификации допустимой области) вторая траектория расходится. С компактным ограничением, все остаётся управляемым.
P.S
Это последний сугубо теоретический пост для базы, так как дальше пойдёт уже более практически интересная теория