Всичко това може да се обясни и в 3-4 лекции, а може би и отделен курс.
import math
import numpy as np
from sklearn.datasets import make_moons, make_circles, make_classification
from sklearn.linear_model import LogisticRegression
from sklearn.neural_network import MLPClassifier
import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns
from IPython.display import display
%matplotlib inline
sns.set()
def plot_boundary(clf, X, y):
x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1
xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.1),
np.arange(y_min, y_max, 0.1))
f, ax = plt.subplots(figsize=(10, 8))
Z = clf.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
ax.contourf(xx, yy, Z, alpha=0.4)
ax.scatter(X[:, 0], X[:, 1], c=y, s=20, edgecolor='k')
plt.show()
np.random.seed(1)
X = np.random.rand(200, 2) * 10
y = np.logical_xor(X[:, 0] > 5, X[:, 1] > 5)
lr = LogisticRegression().fit(X, y)
plot_boundary(lr, X, y)
mlp = MLPClassifier(100, max_iter=2000, random_state=1).fit(X, y)
plot_boundary(mlp, X, y)
X, y = make_moons()
plot_boundary(mlp.fit(X, y), X, y)
X, y = make_circles()
plot_boundary(mlp.fit(X, y), X, y)
Да видим обаче какво стои зад MLP
.
mlp
x = np.linspace(-10, 10)
def sigmoid(x):
return 1 / (1 + np.exp(-x))
plt.plot(x, sigmoid(x));
plt.plot(x, np.tanh(x));
def relu(x):
z = x > 0
return x*z
plt.plot(x, relu(x));
def leaky_rely(x, leakage = 0.1):
x = np.copy(x)
x[x < 0] *= leakage
return x
plt.plot(x, leaky_rely(x));
def softplus_function(x):
return np.log(1 + np.exp(x))
plt.plot(x, softplus_function(x));
def softsign_function(x):
return x / (1 + np.abs(x))
plt.plot(x, softsign_function(x));
def elu(x, alpha=1):
x = x.copy()
neg_indices = x < 0
x[neg_indices] = alpha * (np.exp(x[neg_indices]) - 1)
return x
plt.plot(x, elu(x));
def plot_activations():
x = np.linspace(-5, 5)
plt.figure(figsize=(12,8))
plt.plot(x, sigmoid(x));
plt.plot(x, list(map(np.math.tanh, x)));
plt.plot(x, relu(x));
plt.plot(x, leaky_rely(x));
plt.plot(x, softplus_function(x));
plt.plot(x, softsign_function(x));
plt.plot(x, elu(x));
plt.legend([
"sigmoid",
"tanh",
"relu",
"leaky_rely",
"softplus_function",
"softsign_function",
"elu",
])
plot_activations()
Softmax
¶$$ \sigma (\mathbf {z} )_{j}={\frac {e^{z_{j}}}{\sum _{k=1}^{K}e^{z_{k}}}} $$
def softmax(x):
e_x = np.exp(x - np.max(x))
return e_x / e_x.sum(axis=0)
x = [0.1, 0.2, 0.6, 0.9]
print(x, "\n", softmax(x))
x = [0.1, 0.2, 0.1, 0.9]
print(x, "\n", softmax(x))
Можем да използваме softmax за multi-class prediction.
Sigmoid щеше да връща отделна вероятност за всеки възможен клас - подобно на One vs All стратегията.
http://dataaspirant.com/2017/03/07/difference-between-softmax-function-and-sigmoid-function/
"the update rule"
.$$ \hat{y} = \sigma(w^Tx+b) = \sigma(b + w_1 x_1 + w_2 x_2 + \ldots + w_n x_n) $$
Където:
$$ \sigma(z) = \frac{1}{1 + e^{-z}} $$
$w$ e вектор.
$b$ е скалар.
$x$ е вектор със стойности, но операциите могат да бъдат "векторизарани" и $X$ да е матрица, съдържаща стойностите за всички семпли от данните.
Броя на параметрите ($W$ и $b$) е: $n + 1$, където е $n$ е броя фичъри.
Примерно, ако имаме 1000 фичъра ще имаме 1001 параметри (тегла) на логистичната регресия.
В случая на NN това е само един от невроните в някой от слоевете.
$$ h_1 = g(W_1^Tx+b_1) $$ $$ \hat{y} = \sigma(W_2^T h_1 + b_2 )$$
Където:
$$ \sigma(z) \text{ - логистична функция (сигмоид)}$$ $$ g(z) \text{ - някоя активация}$$
за всеки неврон в $h_1$, имаме: $$ h_{1(i)} = g(W_{1(i)}^T x+b_i) $$
Във втория слой има само 1 неврон. Ако приемем, че резултатите след активацията в $h_1$ са нови фичъри - $x_2$, то: $$ \hat{y} = \sigma(w_2^T x_2 + b_2 ) \text{ - е логистична регресия, която работи с новите фичъри $x_2$.} $$
$W_1$ - е матрица, съдържаща теглата за всяко $x$ за всеки от невроните в слой 1.
$b_1$ - вектор съдържащ "the intercept" за всеки от невроните в слой 1.
Аналогично за слой 2.
print("n_layers_:", mlp.n_layers_)
print("n_outputs_:", mlp.n_outputs_)
print("hidden_layer_sizes:", mlp.hidden_layer_sizes)
W1, W2 = mlp.coefs_
print(W1.shape, W2.shape)
MLP
, който ползвахме в началото.¶W1
- (2, 100) W2
- (100, 1)x
съдържа 2 фичъраW1
- 2 реда, 100 колони - Има 100 неврона във слой 1. Всеки от тези 100 неврона ще има по 2 тегла за 2та фичъра.W2
- 100 реда, 1 колона - Има само 1 изходен неврон (класификация или регрисия). Този неврон има 100 тегла, за 100те неврона от предходния слой.Аналогично за intercept
. Всеки неврон се нуждае от собствен intercept
.
b1, b2 = mlp.intercepts_
print(b1.shape, b2.shape)
# b1 - 100 intercepts - one for each of the neurons in h1
# b2 - a intercetp for the final classification neuron (logistic regression)
Backpropagation е алгоритъма, който изплозваме за да намерим подходящи тегла за модела.
$$W_{current} = W_{current} - \alpha \nabla J(W_{current}) $$
Това вече го показвахме.
Регресия: $$ J(W) = \frac{1}{2m}\sum_{i=1}^m \Big(\hat{y_i} - y_i\Big) ^ 2 $$
Класификация:
$$ J(W) = - \frac {1}{m} \sum_{i=1}^ m \sum_{j=1}^l y_{ij}\ln(p_{ij} ) $$
Където:
Това също е показвано.
Заради това ще добавим още две техники:
С графа, разделяме сметките на най-простите им компоненти. Пример:
$ \hat y = \sigma\big( g(x W_1 + b) W_2 + b_2\big)$
Ще го раздробим на части:
$ op_1 = x W_1 + b $
$ op_2 = g( op_1) $
$ op_3 = op_2 * W_2 + b_2 $
$ op_4 = \sigma (op_3) $
Да добавим и грешката:
$ op_5 = J(op_4) $
"Chain rule" е фомрула за пресмятане на производните на композитни функции:
Нека $F = f ∘ g$, или $F(x) = f(g(x))$ за всяко $x$. Тогава: $$ F'(x)=f'(g(x))g'(x) $$ или ако $ z=f(y)$ и $y=g(x) $, тогава:
$$\frac{dz}{dx}={\frac{dz}{dy}}\cdot {\frac {dy}{dx}}$$
W
и b
:¶$$\frac{\partial}{\partial w} J(W)$$
$ \hat y = g(W_1 X + b) W_2 + b_2$
$ op_1 = W_1 x + b $
$ op_2 = \sigma( op_1) $
$ op_3 = op_2 * W_2 + b_2 = \hat y $
$ op_4 = J(op_3) $, където $J(W) = \frac {1}{2} \big ( \hat y - y \big ) ^2 $
$$\frac {\partial op_4} {\partial W_2 } = \frac {\partial } {\partial W_2 } \frac {1} {2} \big ( op3 - y \big )^2 = $$
$$ = (op3 - y) \frac {\partial } {\partial W_2 } \big ( op3 - y\big ) =$$
$$ = (op3 - y) \frac {\partial } {\partial W_2 } \big ( op_2 * W_2 + b_2 - y\big ) =$$
$$ = (op3 - y) op_2 $$
sigmoid
, но в следващата стъпка се налага за това ще запишем дефиницията:¶$$\frac {\partial \sigma(x)} {\partial x} = \sigma(x)(1 - \sigma(x))$$
$$\frac {\partial op_4} {\partial W_1 } = \frac {\partial } {\partial W_1 } \frac {1} {2} \big ( op3 - y \big )^2 = $$
$$ = (op3 - y) \frac {\partial } {\partial W_1 } \big ( op_2 * W_2 + b_2 - y\big ) =$$
$$ = (op3 - y) \frac {\partial } {\partial W_1 } \big ( op_2 * W_2\big ) =$$
$$ = (op3 - y) W_2 \frac {\partial } {\partial W_1 } \big ( \sigma(op1) \big ) =$$
$$ = (op3 - y) W_2 \frac {\partial } {\partial W_1 } \big ( \sigma(W_1x+b)\big ) =$$
$$ = (op3 - y) W_2 \lbrack \sigma(op1)(1-\sigma(op1) \big \rbrack \big ( \frac {\partial } {\partial W_1 }(W_1x+b)\big ) =$$
$$ = (op3 - y) W_2 \lbrack op2(1-op2) \rbrack \big ( x \big )$$
$$\frac {\partial op_4} {\partial b_1 } = (op3 - y) W_2 \lbrack op2(1-op2) \rbrack $$
След като сме пресметнали градиентите, можем да използваме "the update rule" за да обновим параметрите на модела.
Обаче също има различни разновидности и параметри:
Gradient descent variants
Gradient descent optimization algorithms
Decaying Learning Rate:
$$ W = W - \frac {\alpha} {i} \nabla J(W) $$
Exponential decay:
$$ W = W - \alpha \cdot 0.99^i \nabla J(W) $$
$i$ - номер на итерация
$\nu_t = \gamma \nu_{t-1} + \alpha \nabla J(W) $
$W = W - \nu_t$
където,
$ \gamma $ – е каква част от градиента да е взет от предишната итерация. пр. 0.9
$\nu_t = \gamma \nu_{t-1} + \alpha \nabla J(W - \gamma \nu_{t-1}) $
$W = W - \nu_t$
Изважда $\gamma \nu_{t-1}$ от теглата при пресятане на ч $J$, за да получи по-добра апроксимация на кост функцията, имайки предвид моментума.
Използват различен "laerning rate", за всяка итерация, за всяко от теглата.
Така могат да обновяват с по-голяма стъпка параметри, които са по редки (спарс фичъри) и с по-малки стъпки, тези които се срещат във всяка итерация.
RMSprop
Започват с Ada
и можете да ги погледнете в google.
$ g_t=\nabla J(W) $, за всяка стъпка $t$
$m_t = \beta_1 m_{t-1} + ( 1 - \beta_1)g_t$ – exponentially decaying average of past gradients
$\nu_t = \beta_2 \nu_{t-1} + ( 1 - \beta_2)g_t^2$ – exponentially decaying average of past squared gradients
$\hat m_t = \frac {m_t} {1 - \beta_1^t}$
$\hat \nu_t = \frac {\nu_t} {1 - \beta_2^t}$
$$ W_{t+1} = W_t - \frac {\alpha} {\sqrt{\hat \nu _t } + \epsilon} \hat m_t $$
стойности по подразбиране:
$\beta_1 = 0.9$
$\beta_2 = 0.999$
$\epsilon = 10^{-8}$
Целта на графа е да вкараме всички операции от примера и да имаме автоматично диференциране и ъпдейтване на теглата.
forward
- пресямата операцията.backward
- пресмята и запомня градиентите за текущата операция.update weights
- обновява теглата с помощтта на "the update rule".forward
за всеки от елементите и ще запази стойностотите им.backward pass
, изпълнявайки го върху елементите в обратен ред.update_wights
ще извадим пресметнатите градиенти от съответните им тегла.$ \hat y = g(X W_1 + b) W_2 + b_2$
$ op_1 = X W_1 + b $
$ op_2 = \sigma( op_1) $
$ op_3 = op_2 * W_2 + b_2 = \hat y $
$ op_4 = J(op_3) $, където $J(W) = \frac {1}{2} \big ( \hat y - y \big ) ^2 $
class MSE:
def __init__(self, y):
self.y = y
def forward(self, X):
self.X = X.ravel()
first_term = 1. / (2. *len(X))
norm = np.linalg.norm(self.y - X)
self.value = first_term * np.square(norm)
return self.value
def backward(self, _):
dX = self.X - self.y
return dX
backward pass
на графа.¶$$\frac {\partial op_4} {\partial W_2 } = \frac {\partial } {\partial W_2 } \frac {1} {2} \big ( op3 - y \big )^2 = $$
$$ = (op3 - y) \frac {\partial } {\partial W_2 } \big ( op3 - y\big ) =$$
$$ = (op3 - y) \frac {\partial } {\partial W_2 } \big ( op_2 * W_2 + b_2 - y\big ) =$$
$$ = (op3 - y) op_2 $$
Linear Unit
, който пресмята wx + b
или op1
и op3
от горния пример.¶class Linear:
def __init__(self, x_dim, h_dim, name=None):
self.W = np.random.randn(x_dim, h_dim)
self.b = np.random.randn(h_dim)
self.name = name
def forward(self, X):
self.X = X
self.values = np.dot(X, self.W) + self.b
return self.values
def backward(self, dZ):
self.db = np.dot(np.ones((1, dZ.shape[0]), dtype=np.float64), dZ)
self.dW = np.dot(np.transpose(self.X), dZ)
if dZ.ndim == 1:
dZ = np.expand_dims(dZ, axis=1)
self.dX = dZ @ np.transpose(self.W)
return self.dX
def update(self, alpha):
self.W += - alpha * self.dW.reshape(self.W.shape)
self.b += - alpha * self.db.ravel()
__init__
подаваме x_dim
и h_dim
.¶x_dim
- колко фичъра има в x
.h_dim
- колко скрити неврона искаме да има в новия слой.h_dim
може да бъде 1
за последния слой.
X
по W
ще получим нова матрица. Ред по колона :D
Sigmoid
¶Тук нещата са малко по-тривиални
class Sigmoid:
def forward(self, X):
self.values = 1.0 / (1.0 + np.exp(-X))
return self.values
def backward(self, dZ):
return (1.0 - self.values) * self.values * dZ
# print("\n"*4)
# print(self.name)
# print(f'self.db {self.db}')
# print(f'self.X: {self.X}')
# print(f'dZ: {dZ}')
# print(f'self.dW: {self.dW}')
# print(f"self.W, {self.W}")
# print("\n"*4)
from sklearn.base import BaseEstimator
class NeuralNetwork(BaseEstimator):
def __init__(self, model, alpha=0.01, iterations=100):
self.alpha = alpha
self.iterations = iterations
self.model = model
def fit(self, X, y=None):
model = self.model
self.errors = []
for i in range(self.iterations):
z = X
for e in self.model:
z = e.forward(z)
if isinstance(e, MSE):
self.errors.append(e.value)
dZ = None
for e in self.model[::-1]:
dZ = e.backward(dZ)
for e in self.model:
if hasattr(e, 'update'):
e.update(self.alpha)
return self
def predict(self, X):
z = X
for e in model[:-1]:
z = e.forward(z)
return z
X = np.array([
[1, 2, 3],
[-1, -2, -3]
])
y = np.array([0, 1])
np.random.seed(1)
model = [
Linear(3, 5, "Linear 1"),
Sigmoid(),
# Linear(5, 4, "Linear 2"),
# Sigmoid(),
Linear(5, 1, "Linear 3"),
MSE(y)
]
nn = NeuralNetwork(model)
nn.fit(X)
nn.predict(X)
plt.plot(nn.errors);
def plot_different_lr():
plt.figure(figsize=(12,12))
for i, lr in enumerate([0.001, 0.01, 0.1, 0.4, 0.5, 0.6]):
np.random.seed(1)
model = [Linear(3, 5, "Linear 1"), Sigmoid(), Linear(5, 1, "Linear 3"), MSE(y)]
nn = NeuralNetwork(model, alpha=lr)
nn.fit(X)
plt.subplot(2,3, i+1)
plt.title(lr)
plt.plot(nn.errors);
plot_different_lr()
LinearRegression
¶from sklearn.metrics import mean_squared_error, r2_score
from sklearn.datasets import load_boston
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split
boston = load_boston()
X = boston.data
X = StandardScaler().fit(X).transform(X)
y = boston.target
x_train, x_test, y_train, y_test = train_test_split(X, y)
regressor = LinearRegression().fit(x_train, y_train)
print("train score:", regressor.score(x_train, y_train))
print("test score:", regressor.score(x_test, y_test))
x_train.shape
np.random.seed(1)
model = [
Linear(13, 100, "Linear 1"),
Sigmoid(),
Linear(100, 50, "Linear 2"),
Sigmoid(),
Linear(50, 1, "Linear 3"),
MSE(y_train)
]
nn = NeuralNetwork(model, alpha=0.0001, iterations=1000)
nn.fit(x_train);
plt.plot(nn.errors);
print(r2_score(y_train, nn.predict(x_train)))
print(r2_score(y_test, nn.predict(x_test)))
relu
softmax
log loss
, среща се и с името categorical cross entropy
decaying lr
или momentum
Linear
елемент да се подава само едно измерение - това за бр. неврони в скрития слой. Другото ще се смята автоматично в първото изивкване на forward
от подаденото x
l1
и l2
. Всеки Linear
елемент може да има различни стойности за l1
и l2
.