 [mlcourse.ai](https://mlcourse.ai) - Open Machine Learning Course

Auteur: [Alexey Natekin](https://www.linkedin.com/in/natekin/), fondateur d’OpenDataScience, Machine Learning Evangelist. Traduit et édité par [Olga Daykhovskaya](https://www.linkedin.com/in/odaykhovskaya/), [Anastasia Manokhina](https://www.linkedin.com/in/anastasiamanokhina/), [Egor Polusmak](https://www.linkedin.com/in/egor-polusmak/), [Yuanyuan Pao](https://www.linkedin.com/in/yuanyuanpao/) et [Ousmane Cissé](https://github.com/oussou-dev). Ce matériel est soumis aux termes et conditions de la licence [Creative Commons CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/). L'utilisation gratuite est autorisée à des fins non commerciales.

 Thème 10. Gradient Boosting

Jusqu'à présent, nous avons couvert 9 sujets allant de l'analyse exploratoire de données à l'analyse de séries chronologiques en Python:
<spoiler title="List of the series' articles">
1. [Analyse exploratoire de données avec Pandas](https://medium.com/open-machine-learning-course/open-machine-learning-course-topic-1-exploratory-data-analysis-with-pandas-de57880f1a68)
2. [Visualisation de données avec Python](https://medium.com/open-machine-learning-course/open-machine-learning-course-topic-2-visual-data-analysis-in-python-846b989675cd)
3. [Classification, arbres-de-décision et k plus proches voisins](https://medium.com/open-machine-learning-course/open-machine-learning-course-topic-3-classification-decision-trees-and-k-nearest-neighbors-8613c6b6d2cd)
4. [Classification linéaire et régression](https://medium.com/open-machine-learning-course/open-machine-learning-course-topic-4-linear-classification-and-regression-44a41b9b5220)
5. [Bagging et random forest](https://medium.com/open-machine-learning-course/open-machine-learning-course-topic-5-ensemble-of-algorithms-and-random-forest-8e05246cbba7)
6. [Feature Engineering and Feature Selection](https://medium.com/open-machine-learning-course/open-machine-learning-course-topic-6-feature-engineering-and-feature-selection-8b94f870706a)
7. [Apprentissage non supervisé : (ACP) Analyse en Composantes Principales et Clustering](https://medium.com/open-machine-learning-course/open-machine-learning-course-topic-7-unsupervised-learning-pca-and-clustering -db7879568417)
8. [Vowpal Wabbit : Learning with Gigabytes of Data](https://medium.com/open-machine-learning-course/open-machine-learning-course-topic-8-vowpal-wabbit-fast-learning-with-gigabytes-of-data-60f750086237)
9. [Analyse des séries temporelles en Python](https://medium.com/open-machine-learning-course/open-machine-learning-course-topic-9-time-series-analysis-in-python-a270cb05e0b3)

Aujourd'hui, nous allons examiner l'un des algorithmes d'apprentissage automatique les plus populaires et les plus pratiques : le Gradient Boosting.

 Sommaire de l'article

Nous vous recommandons de lire cet article dans l’ordre décrit ci-dessous, mais n'hésitez pas à parcourir les différentes sections.

1. Introduction et histoire du Boosting

Histoire de la GBM

2. L'algorithme GBM

Problème

Descente de gradient fonctionnel

L'algorithme GBM classique de Friedman

Exemple pas à pas: comment fonctionne la GBM

3. Fonctions de perte

Fonctions de perte liées à la  régression

Fonctions de perte de classification

Poids

4. Conclusion

5. Mission de démonstration

6. Ressources utiles

# 1. Introduction et histoire du Boosting
Presque tout le monde en dans le domaine de l'apprentissage automatique a entendu parler du Gradient boosting. Nombreux data scientist incluent cet algorithme dans leur boîte à outils en raison des bons résultats obtenus sur tout problème (inconnu) donné.

De plus, XGBoost est souvent la recette standard pour [gagner](https://github.com/dmlc/xgboost/blob/master/demo/README.md#usecases) les cmpétitions de [ML](http://blog.kaggle.com/tag/xgboost/). Il est si populaire que l’idée de superposer des XGBoosts est devenue un mème. De plus, le boosting est un composant important dans [de nombreux systèmes de recommandation](https://en.wikipedia.org/wiki/Learning_to_rank#Practical_usage_by_search_engines); parfois, il est même considérée comme une [marque](https://yandex.com/company/technologies/matrixnet/).
Penchons-nous sur l'histoire et le développement du boosting.

Le boosting est né de [la question :](http://www.cis.upenn.edu/~mkearns/papers/boostnote.pdf) est-il possible d'obtenir un modèle fort à partir d'un grand nombre de modèles relativement faibles et simples ? Par «modèles faibles», nous n'entendons pas de simples modèles de base tels que les arbres de décision, mais des modèles avec des performances de précision médiocres, où médiocre est un peu meilleur que le hasard.

[Une réponse mathématique positive](http://www.cs.princeton.edu/~schapire/papers/strengthofweak.pdf) a été identifiée, mais il a fallu quelques années pour développer des algorithmes pleinement fonctionnels basés sur cette solution, par exemple AdaBoost. Ces algorithmes adoptent une approche dite "gourmande" : ils construisent d’abord une combinaison linéaire de modèles simples (algorithmes de base) en repesant les données d’entrée. Ensuite, le modèle (généralement un arbre de décision) est construit sur des objets précédemment prédits de manière incorrecte, auxquels des pondérations plus importantes ont été attribuées.

<spoiler title="More about AdaBoost">
De nombreux cours d'apprentissage automatique étudient AdaBoost - l'ancêtre du GBM (Gradient Boosting Machine). Cependant, depuis la fusion d'AdaBoost avec GBM, il est devenu évident qu'AdaBoost n'est qu'une variante particulière de GBM.

L'algorithme lui-même a une interprétation visuelle très claire et une intuition permettant de définir des poids. Jetons un coup d'œil au problème de classification des jouets suivant dans lequel nous allons scinder les données entre les arbres de profondeur 1 (également appelés «stumps») à chaque itération d'AdaBoost. Pour les deux premières itérations, nous avons l'image suivante :

La taille du point correspond à son poids, attribué à une prédiction incorrecte. À chaque itération, nous pouvons constater que ces poids augmentent - les "stumps" ne peuvent pas faire face à ce problème. Cependant, si nous votons (de manière pondérée) pour les "stumps", nous obtiendrons les bonnes classifications :

Pseudocode:
- Initialiser les poids des échantillons $\Large w_i^{(0)} = \frac{1}{l}, i = 1, \dots, l$.
- pour tout $t = 1, \dots, T$
    * Entraînez l'algo $\Large b_t$, laissez $\epsilon_t$ être l’erreur d’entraînement.
    * $\Large \alpha_t = \frac{1}{2}ln\frac{1 - \epsilon_t}{\epsilon_t}$.
    * Mettez à jour les poids des échantillons : $\Large w_i^{(t)} = w_i^{(t-1)} e^{-\alpha_t y_i b_t(x_i)}, i = 1, \dots, l$.
    * Normaliser les poids des échantillons: $\Large w_0^{(t)} = \sum_{j = 1}^k w_j^{(t)}, w_i^{(t)} = \frac{w_i^{(t)}}{w_0^{(t)}}, i = 1, \dots, l$.
- Retourne $\sum_t^{T}\alpha_tb_t$

[Voici](https://www.youtube.com/watch?v=k4G2VCuOMMg) un exemple plus détaillé d'AdaBoost où, en itérant, nous pouvons voir que les poids augmentent, en particulier à la frontière entre les classes.

AdaBoost fonctionne bien, mais [le manque](https://www.cs.princeton.edu/courses/archive/spring07/cos424/papers/boosting-survey.pdf) d'explication de la raison de la réussite de l'algorithme a semé le doute. Certains considéraient cela comme un super-algorithme, une solution miracle, mais d'autres étaient sceptiques et pensaient qu'AdaBoost était juste trop bien sur-appris

Le problème de sur-apprentissage existait bel et bien, surtout lorsque les données présentaient de fortes valeurs aberrantes. Par conséquent, dans ce type de problème, AdaBoost était instable. Heureusement, quelques professeurs du département de statistique de Stanford, qui avaient créé Lasso, Elastic Net et Random Forest, ont commencé à étudier l'algorithme. En 1999, Jerome Friedman a proposé la généralisation du développement d’algorithmes de Boosting - Gradient Boosting (Machine), également connu sous le nom de GBM. Avec ce travail, Friedman a mis en place la base statistique de nombreux algorithmes fournissant l'approche générale du Boosting pour l'optimisation dans l'espace fonctionnel.

CART, bootstrap, et beaucoup d'autres algorithmes sont issus du département des statistiques de Stanford. Ce faisant, leurs noms seront introduits dans les prochains manuels. Ces algorithmes sont très pratiques et certains travaux récents ne sont pas encore largement adoptés. Par exemple, consultez [glinternet](https://arxiv.org/abs/1308.2719).

Pas beaucoup d'enregistrements vidéo de Friedman sont disponibles. Bien qu'il y ait une très intéressante [interview](https://www.youtube.com/watch?v=8hupHmBVvb0) avec lui sur la création de CART et sur la façon dont ils ont résolu les problèmes de statistiques (similaires à la data analysis et à la data science aujourd'hui) il y a plus de 40 ans.

Il existe également une excellente [conférence](https://www.youtube.com/watch?v=zBk3PK3g-Fc) de Hastie, une rétrospective sur l'analyse de données fournie par l'un des créateurs des méthodes que nous utilisons tous les jours.

En général, la recherche en ingénierie et en algorithmes est passée d'une approche à part entière à la construction et à l'étude d'algorithmes. D'un point de vue mathématique, il ne s'agit pas d'un gros changement : nous ajoutons (ou renforçons) des algorithmes faibles et élargissons notre ensemble avec des améliorations progressives pour les parties des données où le modèle était inexact. Mais, cette fois, le prochain modèle simple ne repose pas uniquement sur des objets repondérés, mais améliore son approximation du gradient de la fonction objective globale. Ce concept ouvre grandement nos algorithmes à l'imagination et aux extensions.

## Histoire de la GBM

Plus de 10 ans après l’introduction de la GBM, celle-ci est devenue un élément essentiel de la boîte à outils de la science des données.
GBM a été étendu pour s’appliquer à différents problèmes statistiques : GLMboost et GAMboost pour renforcer les modèles GAM existants, CoxBoost pour les courbes de survie et RankBoost et LambdaMART pour le classement.
De nombreuses réalisations de GBM sont également apparues sous différents noms et sur différentes plateformes : GBM stochastique, GBDT (arbres de décision boostés par gradient), GBRT (arbres de régression boostés par gradient), MART (arbres de régression additive multiples), etc. En outre, la communauté du ML (Machine Learning) était très segmentée et dissociée, ce qui rendait difficile de savoir à quel point la stimulation était devenue généralisée.

Dans le même temps, le boosting avait été activement utilisé dans le classement des recherches. Ce problème a été réécrit en termes de fonction de perte qui pénalise les erreurs dans l'ordre de sortie. Il est donc devenu pratique de l'insérer simplement dans la GBM. AltaVista a été l’une des premières entreprises à introduire le renforcement du classement. Bientôt, les idées se répandent dans Yahoo, Yandex, Bing, etc. Une fois que cela s'est produit, le boosting est devenu l'un des principaux algorithmes utilisés non seulement dans la recherche, mais également dans les technologies de base de l'industrie.

Les compétitions  ML, en particulier Kaggle, ont joué un rôle majeur dans la popularisation du Boosting. À présent, les chercheurs disposaient d’une plate-forme commune leur permettant de faire face à différents problèmes de science des données avec un grand nombre de participants du monde entier. Avec Kaggle, il est possible de tester de nouveaux algorithmes sur les données réelles, ce qui permet aux algorithmes de "briller", et de fournir des informations complètes sur le partage des résultats de performance de modèle entre jeux de données de compétition. C’est exactement ce qui est arrivé avec le Boosting quand il a été utilisé chez [Kaggle](http://blog.kaggle.com/2011/12/21/score-xavier-conort-on-coming-second-in-give-me-some-credit/) (voir les entretiens avec les gagnants de Kaggle à partir de 2011 qui ont principalement utilisé le boosting). La bibliothèque [XGBoost](https://github.com/dmlc/xgboost) a rapidement gagné en popularité après son apparition. XGBoost n'est pas un nouvel algorithme unique. il s'agit simplement d'une réalisation extrêmement efficace de la GBM classique avec des heuristiques supplémentaires.

Cet algorithme a suivi le chemin très typique des algorithmes ML aujourd'hui : problème mathématique et savoir-faire algorithmique pour des applications pratiques réussies et une adoption en masse des années après sa première apparition.

# 2. L'algorithme GBM
### Problème

Nous allons résoudre le problème de l'approximation des fonctions dans un contexte d'apprentissage supervisé général. Nous avons un ensemble de fonctionnalités $ \large x $ et les variables cibles $\large y, \large \left\{ (x_i, y_i) \right\}_{i=1, \ldots,n}$ que nous utilisons pour restaurer la dépendance $\large y = f(x) $. Nous restaurons la dépendance en approximant $ \large \hat{f}(x) $ et en comprenant quelle approximation est la meilleure lorsque nous utilisons la fonction de perte $ \large L(y,f) $, que nous voulons minimiser: $ \large y \approx \hat{f}(x), \large \hat{f}(x) = \underset{f(x)}{\arg\min} \ L(y,f(x)) $.

Pour le moment, nous ne faisons aucune hypothèse concernant le type de dépendance $ \large f(x) $, le modèle de notre approximation $ \large \hat{f}(x) $ ou la distribution de la variable cible $ \large y $. Nous nous attendons seulement à ce que la fonction $ \large L(y,f) $ soit différentiable. Notre approche est très générale : nous définissons $ \large \hat {f}(x) $ en minimisant la perte :
$$ \large  \hat{f}(x) = \underset{f(x)}{\arg\min} \ \mathbb {E} _{x,y}[L(y,f(x))]  $$

Malheureusement, le nombre de fonctions $ \large f(x) $ n'est pas simplement grand, mais son espace fonctionnel est infiniment dimensionnel. C'est pourquoi il est acceptable pour nous de limiter l'espace de recherche par une famille de fonctions $ \large f(x, \theta), \theta \in \mathbb{R}^d $. Cela simplifie beaucoup l'objectif car nous avons maintenant une optimisation solvable des valeurs de paramètres:
$ \large \hat{f}(x) = f(x, \hat{\theta}),$
$$\large \hat{\theta} = \underset{\theta}{\arg\min} \ \mathbb {E} _{x,y}[L(y,f(x,\theta))] $$

Des solutions analytiques simples pour trouver les paramètres optimaux $ \large \hat{\theta} $ n'existant souvent pas, les paramètres sont généralement approximés de manière itérative. Pour commencer, nous écrivons la fonction de perte empirique $ \large L_{\theta}(\hat{\theta}) $ qui nous permettra d’évaluer nos paramètres en utilisant nos données. De plus, écrivons notre approximation $ \large \hat{\theta} $ pour un certain nombre d'itérations $ \large M $ sous forme de somme:
$ \large \hat{\theta} = \sum_{i = 1}^M \hat{\theta_i}, \\
\large L_{\theta}(\hat{\theta}) =  \sum_{i = 1}^N L(y_i,f(x_i, \hat{\theta}))$

Ensuite, il ne reste plus qu'à trouver un algorithme itératif approprié pour minimiser $\large L_{\theta}(\hat{\theta})$. La descente de gradient est l'option la plus simple et la plus fréquemment utilisée. Nous définissons le gradient comme étant $\large \nabla L_{\theta}(\hat{\theta})$ et y ajoutons nos évaluations itératives $\large \hat{\theta_i}$ (puisque nous minimisons la perte, nous ajoutons le signe moins). Notre dernière étape consiste à initialiser notre première approximation $\large \hat{\theta_0}$ et à choisir le nombre d'itérations $\large M$. Passons en revue les étapes de cet algorithme inefficace et naïf pour approximer $\large \hat{\theta}$:

1. Définir l'approximation initiale des paramètres $\large \hat{\theta} = \hat{\theta_0}$
2. Pour chaque itération $\large t = 1, \dots, M$, répétez les étapes 3 à 7:
3. Calculer le gradient de la fonction de perte $\large \nabla L_{\theta}(\hat{\theta})$ pour l'approximation courante $\large \hat{\theta}$
$\large \nabla L_{\theta}(\hat{\theta}) = \left[\frac{\partial L(y, f(x, \theta))}{\partial \theta}\right]_{\theta = \hat{\theta}}$
4. Définir l'approximation itérative actuelle $\large \hat{\theta_t}$ en fonction du gradient calculé
$\large \hat{\theta_t} \leftarrow −\nabla L_{\theta}(\hat{\theta})$
5. Mettre à jour l'approximation des paramètres $\large \hat{\theta}$:
$\large \hat{\theta} \leftarrow \hat{\theta} + \hat{\theta_t} = \sum_{i = 0}^t \hat{\theta_i} $
6. Enregistrer le résultat de l'approximation $\large \hat{\theta}$:
$\large \hat{\theta} = \sum_{i = 0}^M \hat{\theta_i} $
7. Utiliser la fonction trouvée $\large \hat{f}(x) = f(x, \hat{\theta})$

### Descente de gradient fonctionnel

Imaginons une seconde que nous puissions améliorer l'optimisation dans l'espace des fonctions et rechercher de manière itérative les approximations $\large \hat{f}(x)$ en tant que fonctions elles-mêmes. Nous allons exprimer notre approximation comme une somme d'améliorations incrémentales, chacune étant une fonction. Pour plus de commodité, nous commencerons immédiatement par la somme de l'approximation initiale $\large \hat{f_0}(x)$:
$$\large \hat{f}(x) = \sum_{i = 0}^M \hat{f_i}(x)$$

Rien n'est encore arrivé. nous avons seulement décidé de chercher notre approximation $\large \hat{f}(x)$ non pas comme un grand modèle avec beaucoup de paramètres (par exemple, un réseau de neurones), mais comme une somme de fonctions prétendant que nous nous déplaçons dans un espace fonctionnel.

Afin d'accomplir cette tâche, nous devons limiter notre recherche à une famille de fonctions $\large \hat{f}(x) = h(x, \theta)$. Il y a quelques problèmes ici - tout d’abord, la somme des modèles peut être plus compliquée que n’importe quel modèle de cette famille; Deuxièmement, l'objectif général est toujours dans l'espace fonctionnel. Notons qu'à chaque étape, nous devrons choisir un coefficient optimal $\large \rho \in \mathbb{R}$. Pour l'étape $\large t$, le problème est le suivant:
$$\large \hat{f}(x) = \sum_{i = 0}^{t-1} \hat{f_i}(x), \\
\large (\rho_t,\theta_t) = \underset{\rho,\theta}{\arg\min} \ \mathbb {E} _{x,y}[L(y,\hat{f}(x) +  \rho \cdot h(x, \theta))], \\
\large \hat{f_t}(x) = \rho_t \cdot h(x, \theta_t)$$

C'est ici que la magie opère. Nous avons défini tous nos objectifs en termes généraux, comme si nous pouvions former tout type de modèle $\large h(x, \theta)$ pour n’importe quel type de fonction de perte $\large L(y, f(x, \theta))$. En pratique, c'est extrêmement difficile, mais heureusement, il existe un moyen simple de résoudre ce problème.

Connaissant l'expression du gradient de la fonction de perte, nous pouvons calculer sa valeur sur nos données. Donc, entraînons les modèles de telle sorte que nos prédictions soient davantage corrélées à ce gradient (avec un signe moins). En d'autres termes, nous allons utiliser les moindres carrés pour corriger les prédictions avec ces résidus. Pour les tâches de classification, de régression et de classement, nous minimiserons la différence quadratique entre les pseudo-résidus $\large r$ et nos prédictions. Pour l'étape $\large t$, le problème final se présente comme suit:
$$ \large \hat{f}(x) = \sum_{i = 0}^{t-1} \hat{f_i}(x), \\
\large r_{it} = -\left[\frac{\partial L(y_i, f(x_i))}{\partial f(x_i)}\right]_{f(x)=\hat{f}(x)}, \quad \mbox{for } i=1,\ldots,n ,\\
\large \theta_t = \underset{\theta}{\arg\min} \ \sum_{i = 1}^{n} (r_{it} - h(x_i, \theta))^2, \\
\large \rho_t = \underset{\rho}{\arg\min} \ \sum_{i = 1}^{n} L(y_i, \hat{f}(x_i) + \rho \cdot h(x_i, \theta_t))$$

### L'algorithme GBM classique de Friedman

Nous pouvons maintenant définir l’algorithme GBM classique proposé par Jerome Friedman en 1999. C’est un algorithme supervisé qui a les composants suivants:

- ensemble de données $\large \left\{ (x_i, y_i) \right\}_{i=1, \ldots,n}$;
- nombre d'itérations $\large M$;
- choix de la fonction de perte $\large L(y, f)$ avec un gradient défini;
- choix de la famille de fonctions des algorithmes de base $\large h(x, \theta)$ avec la procédure d'apprentissage;
- hyperparamètres supplémentaires $\large h(x, \theta)$ (par exemple, dans les arbres de décision, la profondeur de l’arbre);

La seule chose qui reste est l'approximation initiale $\large f_0(x)$. Pour simplifier, pour une approximation initiale, une valeur constante $\large \gamma$ est utilisée. La valeur constante, ainsi que le coefficient optimal $\large \rho $, sont identifiés via une recherche binaire ou un autre algorithme de recherche de ligne sur la fonction de perte initiale (et non un gradient). Nous avons donc notre algorithme GBM décrit comme suit:

1. Initialiser GBM avec une valeur constante $\large \hat{f}(x) = \hat{f}_0, \hat{f}_0 = \gamma,  \gamma \in \mathbb{R}$
$\large \hat{f}_0 = \underset{\gamma}{\arg\min} \ \sum_{i = 1}^{n} L(y_i, \gamma)$
2. Pour chaque itération $\large t = 1, \dots, M$, répéter :
3. Calculer les pseudo-résidus $\large r_t$
$\large r_{it} = -\left[\frac{\partial L(y_i, f(x_i))}{\partial f(x_i)}\right]_{f(x)=\hat{f}(x)}, \quad \mbox{for } i=1,\ldots,n$
4. Construire le nouvel algorithme de base $\large h_t(x)$ sous forme de régression sur les pseudo-résidus $\large \left\{ (x_i, r_{it}) \right\}_{i=1, \ldots,n}$
5. Trouver le coefficient optimal $\large \rho_t $ à $\large h_t(x)$ concernant la fonction de perte initiale
$\large \rho_t = \underset{\rho}{\arg\min} \ \sum_{i = 1}^{n} L(y_i, \hat{f}(x_i) +  \rho \cdot h(x_i, \theta))$
6. Enregistrer $\large \hat{f_t}(x) = \rho_t \cdot h_t(x)$
7. Mettre à jour l'approximation courante $\large \hat{f}(x)$
$\large \hat{f}(x) \leftarrow \hat{f}(x) + \hat{f_t}(x) = \sum_{i = 0}^{t} \hat{f_i}(x)$
8. Composez le mrdèle final GBM $\large \hat{f}(x)$
$\large \hat{f}(x) = \sum_{i = 0}^M \hat{f_i}(x) $
9. Conquérir Kaggle et le reste du monde

### Exemple pas à pas: comment fonctionne la GBM

Voyons un exemple du fonctionnement de la GBM. Dans cet exemple de basé sur un jeu, nous allons restaurer une fonction bruyante $\large y = cos(x) + \epsilon, \epsilon \sim \mathcal{N}(0, \frac{1}{5}), x \in [-5,5]$.

Il s’agit d’un problème de régression avec une cible à valeur réelle. Nous allons donc choisir d’utiliser la fonction de perte d’erreur quadratique moyenne. Nous allons générer 300 paires d'observations et les approximer avec des arbres de décision de profondeur 2. Réunissons tout ce dont nous avons besoin pour utiliser GBM:
- Jeu de données $\large \left\{ (x_i, y_i) \right\}_{i=1, \ldots,300}$ ✓
- Nombre d'itérations $\large M = 3$ ✓;
- La fonction de perte d'erreur quadratique moyenne $\large L(y, f) = (y-f)^2$ ✓
- Le gradient de perte en $\large L(y, f) = L_2$ n'est que des résidus $\large r = (y - f)$ ✓;
- Arbre de décision en tant qu'algorithme de base $\large h(x)$ ✓;
- Hyperparamètres des arbres de décision : la profondeur des arbres est égale à 2 ✓;

Pour l'erreur quadratique moyenne, l'initialisation $\large \gamma$ et les coefficients $\large \rho_t$ sont simples. Nous initialiserons GBM avec la valeur moyenne $\large \gamma = \frac{1}{n} \cdot \sum_{i = 1}^n y_i$ et définirons tous les coefficients $\large \rho_t$ sur 1.

Nous allons lancer GBM et dessiner deux types de graphes : l’approximation courante $\large \hat{f}(x)$ (graphe bleu) et chaque arbre $\large \hat{f_t}(x)$ construit sur ses pseudo-résidus (graphe vert). Le numéro du graphique correspond au numéro d'itération :

À la deuxième itération, nos arbres ont retrouvé la forme de base de la fonction. Cependant, à la première itération, nous voyons que l'algorithme n'a construit que la "branche gauche" de la fonction ($\large x \in [-5, -4]$). Cela était dû au fait que nos arbres n’avaient tout simplement pas assez de profondeur pour construire une branche symétrique à la fois et qu’ils se concentraient sur la branche gauche présentant l’erreur la plus grande. Par conséquent, la branche de droite n'est apparue qu'après la deuxième itération.

Le reste du processus se déroule comme prévu : à chaque étape, nos pseudo-résidus ont diminué et GBM a amélioré de mieux en mieux la fonction d'origine à chaque itération. Cependant, par construction, les arbres ne peuvent pas se rapprocher d'une fonction continue, ce qui signifie que GBM n'est pas idéal dans cet exemple. Pour jouer avec les approximations de fonctions GBM, vous pouvez utiliser la démo interactive impressionnante de ce blog intitulée [Brilliantly wrong](http://arogozhnikov.github.io/2016/06/24/gradient_boosting_explained.html) :

# 3. Fonctions de perte

Si nous voulons résoudre un problème de classification au lieu d'une régression, qu'est-ce qui changerait ? Il suffit de choisir une fonction de perte appropriée, $\large L(y, f)$. C’est le moment le plus important qui détermine exactement comment nous allons optimiser et à quelles caractéristiques nous pouvons nous attendre dans le modèle final.

En règle générale, nous n'avons pas besoin de l'inventer nous-mêmes : les chercheurs l'ont déjà fait pour nous. Aujourd'hui, nous allons explorer les fonctions de perte pour les deux objectifs les plus courants : la régression $\large y \in \mathbb{R}$ et la classification binaire $\large y \in \left\{-1, 1\right\}$.

### Fonctions de perte liées à la  régression

Commençons par un problème de régression pour $\large y \in \mathbb{R}$. Afin de choisir la fonction de perte appropriée, nous devons déterminer quelles propriétés de la distribution conditionnelle $\large (y|x)$ nous souhaitons restaurer. Les options les plus courantes sont:

- $\large L(y, f) = (y - f)^2$ a.k.a. $\large L_2$ perte ou perte gaussienne. C'est la moyenne conditionnelle classique, qui est le cas le plus simple et le plus courant. Si nous n'avons pas d'informations supplémentaires ou d'exigences pour qu'un modèle soit robuste, nous pouvons utiliser la perte gaussienne.
- perte $\large L_1$ ou $\large L_1$ ou perte laplacienne. Au premier abord, cette fonction ne semble pas pouvoir être différenciée, mais définit en réalité la médiane conditionnelle. Comme nous le savons, la médiane est robuste aux valeurs aberrantes, raison pour laquelle cette fonction de perte est meilleure dans certains cas. La pénalité pour les grandes variations n’est pas aussi lourde que dans $\large L_2$.
- $ \large \begin{equation}  L(y, f) =\left\{   \begin{array}{@{}ll@{}}     (1 - \alpha) \cdot |y - f|, & \text{if}\ y-f \leq 0 \\     \alpha \cdot |y - f|, & \text{if}\ y-f >0  \end{array}\right. \end{equation}, \alpha \in (0,1)
$ a.k.a. $\large L_q$ perte ou perte de Quantile. Au lieu de la médiane, il utilise des quantiles. Par exemple, $\large \alpha = 0.75$ correspond au 75%-quantile. Nous pouvons voir que cette fonction est asymétrique et pénalise les observations qui se trouvent du côté droit du quantile défini.

Utilisons la fonction de perte $\large L_q$ sur nos données. L'objectif est de restaurer le quantile conditionnel de cosinus à 75%. Mettons tout en œuvre pour GBM:
- Jeu de données $\large \left\{ (x_i, y_i) \right\}_{i=1, \ldots,300}$ ✓
- Un nombre d'itérations $\large M = 3$ ✓;
- Fonction de perte pour les quantiles $ \large \begin{equation}   L_{0.75}(y, f) =\left\{
\begin{array}{@{}ll@{}}    0.25 \cdot |y - f|, & \text{if}\ y-f \leq 0 \\     0.75 \cdot |y - f|, & \text{if}\ y-f >0   \end{array}\right. \end{equation} $ ✓;
- Gradient $\large L_{0.75}(y, f)$ - fonction pondérée par $\large \alpha = 0.75$. Nous allons former un modèle basé sur des arbres pour la classification :
$\large r_{i} = -\left[\frac{\partial L(y_i, f(x_i))}{\partial f(x_i)}\right]_{f(x)=\hat{f}(x)} = $
$\large = \alpha I(y_i > \hat{f}(x_i) ) - (1 - \alpha)I(y_i \leq \hat{f}(x_i) ), \quad \mbox{for } i=1,\ldots,300$ ✓;
- Arbre de décision en tant qu'algorithme de base $\large h(x)$ ✓;
- Hyperparamètre des arbres : profondeur = 2 ✓;

Pour notre approximation initiale, nous prendrons le quantile nécessaire de $\large y$. Cependant, nous ne savons rien des coefficients optimaux $\large \rho_t$, nous allons donc utiliser la recherche par ligne standard. Les résultats sont les suivants :

Nous pouvons observer que, à chaque itération, $\large r_{i} $ ne prend que 2 valeurs possibles, mais GBM est toujours capable de restaurer notre fonction initiale.

Les résultats globaux de GBM avec fonction de perte quantile sont les mêmes que les résultats avec fonction de perte quadratique décalée par $\large \approx 0.135$. Mais si nous utilisions le 90%-quantile, nous n'aurions pas assez de données car les classes seraient déséquilibrées. Nous devons nous en souvenir lorsque nous traitons des problèmes non standard.

*"Quelques mots sur les fonctions de perte de régression"*

Pour les tâches de régression, de nombreuses fonctions de perte ont été développées, certaines avec des propriétés supplémentaires. Par exemple, ils peuvent être robustes, comme dans la [fonction de perte de Huber](https://en.wikipedia.org/wiki/Huber_loss). Pour un petit nombre de valeurs aberrantes, la fonction de perte fonctionne comme $\large L_2$, mais après un seuil défini, la fonction devient $\large L_1$. Cela permet de réduire l'effet des valeurs aberrantes et de se concentrer sur l'image globale.

Nous pouvons illustrer cela avec l'exemple suivant. Les données sont générées à partir de la fonction $\large y = \frac{sin(x)}{x}$ avec ajout de bruit, un mélange de distributions normales et de distributions de Bernulli. Nous montrons les fonctions sur les graphes A-D et le GBM correspondant sur F-H (le graphe E représente la fonction initiale):

 [Taille originale](https://habrastorage.org/web/130/05b/222/13005b222e8a4eb68c3936216c05e276.jpg).

Dans cet exemple, nous avons utilisé des splines comme algorithme de base. Vous voyez, il ne faut pas toujours que ce soit des arbres pour le Bosting ?

Nous pouvons clairement voir la différence entre les fonctions $\large L_2$, $\large L_1$ et la perte de Huber. Si nous choisissons des paramètres optimaux pour la perte de Huber, nous pouvons obtenir la meilleure approximation possible parmi toutes nos options. La différence est également visible dans les quantiles de 10%, 50% et 90%.

Malheureusement, la fonction de perte de Huber n’est prise en charge que par très peu de bibliothèques / packages populaires; h2o le supporte, mais pas XGBoost. Il est pertinent pour d'autres choses plus exotiques comme les [expectiles conditionnelles](https://www.slideshare.net/charthur/quantile-and-expectile-regression), mais il peut quand même s'agir de connaissances intéressantes.

### Fonctions de perte de classification

Maintenant, regardons le problème de classification binaire $\large y \in \left\{-1, 1\right\}$. Nous avons vu que GBM peut même optimiser des fonctions de perte non différenciables. Techniquement, il est possible de résoudre ce problème avec une perte de régression $\large L_2$, mais ce ne serait pas correct.

La distribution de la variable cible nécessite que nous utilisions un "log-likehood", il nous faut donc différentes fonctions de perte pour les cibles multipliées par leurs prédictions : $\large y \cdot f$. Les choix les plus courants seraient les suivants:

- $\large L(y, f) = log(1 + exp(-2yf))$ ak.a. Perte logistique ou perte de Bernoulli. Cela a une propriété intéressante qui pénalise même les classes correctement prédites, ce qui aide non seulement à optimiser la perte, mais également à écarter davantage les classes, même si toutes les classes sont correctement prédites.
- perte de $\large L(y, f) = exp(-yf)$ a.k.a. AdaBoost. Le classique AdaBoost est équivalent à GBM avec cette fonction de perte. Conceptuellement, cette fonction est très similaire à la perte logistique, mais elle est plus pénalisée de façon exponentielle si la prédiction est fausse.

Générons un nouveau jeu de données pour notre problème de classification. En guise de base, nous prendrons notre cosinus "bruyant" et nous utiliserons la fonction de signe pour les classes de la variable cible. Nos données ressemblent à ceci (le "jitter-noise" est ajouté pour plus de clarté) :

Nous utiliserons la perte logistique pour rechercher ce que nous améliorons réellement. Donc, encore une fois, nous avons mis en place ce que nous allons utiliser pour GBM :
- jeu de données $\large \left\{ (x_i, y_i) \right\}_{i=1, \ldots,300}, y_i \in \left\{-1, 1\right\}$ ✓
- Nombre d'itérations $\large M = 3$ ✓;
- Perte logistique en tant que fonction de perte, son gradient est calculé de la manière suivante :
$\large r_{i} = \frac{2 \cdot y_i}{1 + exp(2 \cdot y_i \cdot \hat{f}(x_i)) }, \quad \mbox{for } i=1,\ldots,300$ ✓;
- Arbre de décision en tant qu'algorithme de base $\large h(x)$ ✓;
- Hyperparamètres des arbres de décision : la profondeur de l'arbre est égale à 2 ✓;

Cette fois, l'initialisation de l'algorithme est un peu plus difficile. Premièrement, nos classes sont déséquilibrées (63% contre 37%). Deuxièmement, il n’existe pas de formule analytique connue pour l’initialisation de notre fonction de perte, nous devons donc rechercher $\large \hat{f_0} = \gamma$ via search :

Notre approximation initiale optimale est d'environ -0,273. Vous auriez pu deviner que c'était négatif car il est plus rentable de tout prédire comme la classe la plus populaire, mais il n'y a pas de formule pour la valeur exacte. Maintenant, commençons enfin par GBM, et regardons ce qui se passe réellement sous le capot:

L'algorithme a restauré avec succès la séparation entre nos classes. Vous pouvez voir comment les zones "inférieures" se séparent car les arbres sont plus confiants dans la prédiction correcte de la classe négative et comment se forment les deux étapes des classes mixtes. Il est clair que nous avons beaucoup d'observations correctement classées et un certain nombre d'observations avec des erreurs importantes qui sont apparues en raison du bruit dans les données.

### Poids

Parfois, nous voulons une fonction de perte plus spécifique pour notre problème. Par exemple, dans les séries chronologiques financières, il se peut que nous souhaitions accorder plus de poids aux mouvements importants dans la série chronologique; pour la prévision du taux de désabonnement, il est plus utile de prédire le désabonnement des clients ayant un LTV élevé (ou une valeur à vie: combien d'argent un client rapportera-t-il à l'avenir ?).

Le guerrier statistique inventerait sa propre fonction de perte, en écrivant le gradient (pour un entraînement plus efficace, incluez le Hessian) et vérifierait avec soin si cette fonction remplissait les propriétés requises. Cependant, il y a de fortes chances que quelqu'un commette une erreur quelque part, se heurte à des difficultés de calcul et consacre une quantité excessive de temps à la recherche.

Au lieu de cela, un instrument très simple a été inventé (ce dont on se souvient rarement dans la pratique) : peser des observations et attribuer des fonctions de pondération. L'exemple le plus simple d'une telle pondération est la définition de pondérations pour la balance de classes. En général, si nous savons qu'un sous-ensemble de données, tant dans les variables d'entrée $\large x$ que dans la variable cible $\large y$, a une plus grande importance pour notre modèle, nous leur attribuons simplement une pondération plus importante, $\large w(x,y)$. L’objectif principal est de satisfaire aux exigences générales en matière de poids:
$$ \large w_i \in \mathbb{R}, \\
\large w_i \geq 0 \quad \mbox{for } i=1,\ldots,n, \\
\large \sum_{i = 1}^n w_i > 0 $$

Les poids peuvent réduire considérablement le temps passé à ajuster la fonction de perte à la tâche que nous résolvons et encourager les expériences sur les propriétés des modèles cibles. L'attribution de ces poids est entièrement fonction de la créativité. Nous ajoutons simplement des poids scalaires:
$$ \large L_{w}(y,f) = w \cdot L(y,f), \\
\large r_{it} =   - w_i \cdot \left[\frac{\partial L(y_i, f(x_i))}{\partial f(x_i)}\right]_{f(x)=\hat{f}(x)}, \quad \mbox{for } i=1,\ldots,n$$

Il est clair que, pour les poids arbitraires, nous ne connaissons pas les propriétés statistiques de notre modèle. Lier les poids aux valeurs $\large y$ peut souvent être trop compliqué. Par exemple, l'utilisation de poids proportionnels à $\large |y|$ dans la fonction de perte $\large L_1$ n'est pas équivalente à la perte de $\large L_2$ car le gradient ne prendra pas en compte les valeurs des prédictions elles-mêmes : $\large \hat{f}(x)$.

Nous mentionnons tout cela afin de mieux comprendre nos possibilités. Créons des poids très exotiques pour notre jeu de données. Nous allons définir une fonction de poids fortement asymétrique comme suit :
$$ \large \begin{equation} w(x) =\left\{   \begin{array}{@{}ll@{}}     0.1, & \text{if}\ x \leq 0 \\     0.1 + |cos(x)|, & \text{if}\ x >0 \end{array}\right. \end{equation} $$

Avec ces poids, nous nous attendons à obtenir deux propriétés : moins de détails pour les valeurs négatives de $\large x$ et la forme de la fonction, similaire au cosinus initial. Nous reprenons les réglages des autres GBM de notre exemple précédent avec une classification incluant la recherche par ligne pour les coefficients optimaux. Regardons ce que nous avons :

Nous avons atteint le résultat escompté. Premièrement, nous pouvons voir à quel point les pseudo-résidus diffèrent fortement; à l'itération initiale, ils ressemblent presque au cosinus d'origine. Deuxièmement, la partie gauche du graphique de la fonction était souvent ignorée au profit de la droite, qui avait des poids plus importants. Troisièmement, la fonction que nous avons obtenue à la troisième itération a reçu suffisamment d’attention et a commencé à ressembler au cosinus original (elle a également légèrement sur-appris)

Les poids sont un outil puissant mais risqué que nous pouvons utiliser pour contrôler les propriétés de notre modèle. Si vous souhaitez optimiser votre fonction de perte, essayez d'abord de résoudre un problème plus simple en ajoutant des pondérations aux observations, à votre discrétion.

# 4. Conclusion

Aujourd'hui, nous avons appris la théorie derrière le Gradient Boosting. GBM n'est pas seulement un algorithme spécifique, mais une méthodologie commune pour la construction d'ensembles de modèles. De plus, cette méthodologie est suffisamment flexible et extensible - il est possible de former un grand nombre de modèles en tenant compte de différentes fonctions de perte avec une variété de fonctions de pondération.

La pratique et les compétitions ML montrent que, dans les problèmes classiques (à l’exception des images, de l’audio et des données très éparses), la GBM est souvent l’algorithme le plus efficace (pour ne pas mentionner les ensembles superposés et de haut niveau, où la GBM fait presque toujours partie intégrante). En outre, il existe de nombreuses adaptations de GBM [pour l'apprentissage par renforcement](https://arxiv.org/abs/1603.04119) (Minecraft, ICML 2016). En passant, l'algorithme Viola-Jones, qui est encore utilisé en vision par ordinateur, est basé sur [AdaBoost](https://en.wikipedia.org/wiki/Viola%E2%80%93Jones_object_detection_framework#Learning_algorithm).

Dans cet article, nous avons volontairement omis de poser des questions sur la régularisation, la stochasticité et les hyper-paramètres de GBM. Ce n'est pas par hasard que nous avons utilisé un petit nombre d'itérations $\large M = 3$. Si nous utilisions 30 arbres au lieu de 3 et formions le GBM comme décrit, le résultat ne serait pas prévisible :

[Démo interactive](http://arogozhnikov.github.io/2016/07/05/gradient_boosting_playground.html)

# 5. Mission de démonstration
Votre tâche consiste à battre les bases de référence dans le cadre de la compétition "Flight delays" de [Kaggle Inclass](https://www.kaggle.com/c/flight-delays-fall-2018). Vous recevez un [démarreur CatBoost](https://www.kaggle.com/kashnitsky/mlcourse-ai-fall-2019-catboost-starter), l'astuce consiste à proposer de bonnes caractéristiques (features).

# 6. Ressources utiles
- Liste des cours [site](https://mlcourse.ai), [course repo](https://github.com/Yorko/mlcourse.ai), and YouTube [channel](https://www.youtube.com/watch?v=QKTuw4PNOsU&list=PLVlY_7IJCMJeRfZ68eVfEcu-UcN9BbwiX)
- Course materials as a [Kaggle Dataset](https://www.kaggle.com/kashnitsky/mlcourse)
- mlcourse.ai lectures on gradient boosting: [theory](https://youtu.be/g0ZOtzZqdqk) and [practice](https://youtu.be/V5158Oug4W8)
- [Original article](https://statweb.stanford.edu/~jhf/ftp/trebst.pdf) about GBM from Jerome Friedman
- “Gradient boosting machines, a tutorial”, [paper](https://www.ncbi.nlm.nih.gov/pmc/articles/PMC3885826/) by Alexey Natekin, and Alois Knoll
- [Chapter in Elements of Statistical Learning](http://statweb.stanford.edu/~tibs/ElemStatLearn/printings/ESLII_print10.pdf) from Hastie, Tibshirani, Friedman (page 337)
- [Wiki](https://en.wikipedia.org/wiki/Gradient_boosting) article about Gradient Boosting
- [Introduction to boosted trees (Xgboost docs)](https://xgboost.readthedocs.io/en/latest/tutorials/model.html)
- [Video-lecture by Hastie](https://www.youtube.com/watch?v=wPqtzj5VZus) about GBM at h2o.ai conference
- [CatBoost vs. Light GBM vs. XGBoost](https://towardsdatascience.com/catboost-vs-light-gbm-vs-xgboost-5f93620723db) on "Towards Data Science"
- [Benchmarking and Optimization of
Gradient Boosting Decision Tree Algorithms](https://arxiv.org/abs/1809.04559), [XGBoost: Scalable GPU Accelerated Learning](https://arxiv.org/abs/1806.11248) - benchmarking CatBoost, Light GBM, and XGBoost (no 100% winner)


## Data-driven Design and Analyses of Structures and Materials (3dasm)

## Lecture 1

### Miguel A. Bessa | M.A.Bessa@tudelft.nl  | Associate Professor

## Introduction

**What:** A lecture of the "3dasm" course

**Where:** This notebook comes from this [repository](https://github.com/bessagroup/3dasm_course)

**Reference for entire course:** Murphy, Kevin P. *Probabilistic machine learning: an introduction*. MIT press, 2022. Available online [here](https://probml.github.io/pml-book/book1.html)

**How:** We try to follow Murphy's book closely, but the sequence of Chapters and Sections is different. The intention is to use notebooks as an introduction to the topic and Murphy's book as a resource.
* If working offline: Go through this notebook and read the book.
* If attending class in person: listen to me (!) but also go through the notebook in your laptop at the same time. Read the book.
* If attending lectures remotely: listen to me (!) via Zoom and (ideally) use two screens where you have the notebook open in 1 screen and you see the lectures on the other. Read the book.

**Optional reference (the "bible" by the "bishop"... pun intended 😆) :** Bishop, Christopher M. *Pattern recognition and machine learning*. Springer Verlag, 2006.

**References/resources to create this notebook:**
* [Figure (Car stopping distance)](https://korkortonline.se/en/theory/reaction-braking-stopping/)
* Snippets of code from this awesome [repo](https://github.com/gerdm/prml) by Gerardo Duran-Martin that replicates many figures in Bishop's book

Apologies in advance if I missed some reference used in this notebook. Please contact me if that is the case, and I will gladly include it here.

## **OPTION 1**. Run this notebook **locally in your computer**:
1. Install miniconda3 [here](https://docs.conda.io/en/latest/miniconda.html)
2. Open a command window and create a virtual environment called "3dasm":
```
conda create -n 3dasm python=3 numpy scipy jupyter nb_conda matplotlib pandas scikit-learn rise tensorflow -c conda-forge
```
3. Install [git](https://github.com/git-guides/install-git), open command window & clone the repository to your computer:
```
git clone https://github.com/bessagroup/3dasm_course
```
4. Load jupyter notebook by typing in (anaconda) command window (it will open in your internet browser):
```
conda activate 3dasm
jupyter notebook
```
5. Open notebook (3dasm_course/Lectures/Lecture1/3dasm_Lecture1.ipynb)

**Short note:** My personal environment also has other packages that help me while teaching.

> conda install -n 3dasm -c conda-forge jupyter_contrib_nbextensions hide_code

Then in the 3dasm conda environment:

> jupyter nbextension install --py hide_code --sys-prefix
>
> jupyter nbextension enable --py hide_code
>
> jupyter serverextension enable --py hide_code
>
> jupyter nbextension enable splitcell/splitcell

## **OPTION 2**. Use **Google's Colab** (no installation required, but times out if idle):

1. go to https://colab.research.google.com
2. login
3. File > Open notebook
4. click on Github (no need to login or authorize anything)
5. paste the git link: https://github.com/bessagroup/3dasm_course
6. click search and then click on the notebook (*3dasm_course/Lectures/Lecture1/3dasm_Lecture1.ipynb*)

```python
# Basic plotting tools needed in Python.

import matplotlib.pyplot as plt # import plotting tools to create figures
import numpy as np # import numpy to handle a lot of things!

%config InlineBackend.figure_format = "retina" # render higher resolution images in the notebook
plt.style.use("seaborn") # style for plotting that comes from seaborn
plt.rcParams["figure.figsize"] = (8,4) # rescale figure size appropriately for slides
```

## Outline for today

* Introduction
    - Taking a probabilistic perspective on machine learning
* Basics of univariate statistics
    - Continuous random variables
    - Probabilities vs probability densities
    - Moments of a probability distribution
* The mindblowing Bayes' rule
    - The rule that spawns almost every ML model (even when we don't realize it)

**Reading material**: This notebook + Chapter 2 until Section 2.3

## Get hyped about Artificial Intelligence...

```python
from IPython.display import display, YouTubeVideo, HTML
YouTubeVideo('RNnZwvklwa8', width=512, height=288) # show that slides are interactive:
                                                   # rescale video to 768x432 and back to 512x288
```

**Well...** This class *might* not make you break the world (yet!). Let's focus on the fundamentals:

* Probabilistic perspective on machine learning
* Supervised learning (especially regression)

## Machine learning (ML)

* **ML definition**: A computer program that learns from experience $E$ wrt tasks $T$ such that the performance $P$ at those tasks improves with experience $E$.

* We'll treat ML from a **probabilistic perspective**:
    - Treat all unknown quantities as **random variables**

* What are random variables?
    - Variables endowed with probability distributions!

## The car stopping distance problem

Car stopping distance ${\color{red}y}$ as a function of its velocity ${\color{green}x}$ before it starts braking:

${\color{red}y} = {\color{blue}z} x + \frac{1}{2\mu g} {\color{green}x}^2 = {\color{blue}z} x + 0.1 {\color{green}x}^2$

- ${\color{blue}z}$ is the driver's reaction time (in seconds)
- $\mu$ is the road/tires coefficient of friction (assume $\mu=0.5$)
- $g$ is the acceleration of gravity (assume $g=10$ m/s$^2$).

## The car stopping distance problem

### How to obtain this formula?

$y = d_r + d_{b}$

where $d_r$ is the reaction distance, and $d_b$ is the braking distance.

### Reaction distance $d_r$

$d_r = z x$

with $z$ being the driver's reaction time, and $x$ being the velocity of the car at the start of braking.

## The car stopping distance problem

### Braking distance $d_b$

Kinetic energy of moving car:

$E = \frac{1}{2}m x^2$       where $m$ is the car mass.

Work done by braking:

$W = \mu m g d_b$       where $\mu$ is the coefficient of friction between the road and the tire, $g$ is the acceleration of gravity, and $d_b$ is the car braking distance.

The braking distance follows from $E=W$:

$d_b = \frac{1}{2\mu g}x^2$

Therefore, if we add the reacting distance $d_r$ to the braking distance $d_b$ we get the stopping distance $y$:

$$y = d_r + d_b = z x + \frac{1}{2\mu g} x^2$$

## The car stopping distance problem

$y = {\color{blue}z} x + 0.1 x^2$

The driver's reaction time ${\color{blue}z}$ is a **random variable (rv)**

* Every driver has its own reaction time $z$

* Assume the distribution associated to $z$ is Gaussian with **mean** $\mu_z=1.5$ seconds and **variance** $\sigma_z^2=0.5$ seconds$^2$

$$
z \sim \mathcal{N}(\mu_z=1.5,\sigma_z^2=0.5^2)
$$

where $\sim$ means "sampled from", and $\mathcal{N}$ indicates a Gaussian **probability density function (pdf)**

## Univariate Gaussian pdf

The gaussian pdf is defined as:

$$
    \mathcal{N}(z | \mu_z, \sigma_z^2) = \frac{1}{\sqrt{2\pi\sigma_z^2}}e^{-\frac{1}{2\sigma_z^2}(z - \mu_z)^2}
$$

Alternatively, we can write it using the **precision** term $\lambda_z := 1 / \sigma_z^2$ instead of using $\sigma_z^2$:

$$
    \mathcal{N}(z | \mu_z, \lambda_z^{-1}) = \frac{\lambda_z^{1/2}}{\sqrt{2\pi}}e^{-\frac{\lambda_z}{2}(z - \mu_z)^2}
$$

Anyway, recall how this pdf looks like...

```python
def norm_pdf(z, mu_z, sigma_z2): return 1 / np.sqrt(2 * np.pi * sigma_z2) * np.exp(-(z - mu_z)**2 / (2 * sigma_z2))
zrange = np.linspace(-8, 4, 200) # create a list of 200 z points between z=-8 and z=4
fig, ax = plt.subplots() # create a plot
ax.plot(zrange, norm_pdf(zrange, 0, 1), label=r"$\mu_z=0; \ \sigma_z^2=1$") # plot norm_pdf(z|0,1)
ax.plot(zrange, norm_pdf(zrange, 1.5, 0.5**2), label=r"$\mu_z=1.5; \ \sigma_z^2=0.5^2$") # plot norm_pdf(z|1.5,0.5^2)
ax.plot(zrange, norm_pdf(zrange, -1, 2**2), label=r"$\mu_z=-1; \ \sigma_z^2=2^2$") # plot norm_pdf(z|-1,2^2)
ax.set_xlabel("z", fontsize=20) # create x-axis label with font size 20
ax.set_ylabel("probability density", fontsize=20) # create y-axis label with font size 20
ax.legend(fontsize=15) # create legend with font size 15
ax.set_title("Three different Gaussian pdfs", fontsize=20); # create title with font size 20
```

The green curve shows the Gaussian pdf of the rv $z$ **conditioned** on the mean $\mu_z=1.5$ and variance $\sigma_z^2=0.5^2$ for the car stopping distance problem.

## Univariate Gaussian pdf

$$
    p(z) = \mathcal{N}(z | \mu_z, \sigma_z^2) = \frac{1}{\sqrt{2\pi\sigma_z^2}}e^{-\frac{1}{2\sigma_z^2}(z - \mu_z)^2}
$$

The output of this expression is the **PROBABILITY DENSITY** of $z$ **given** (or conditioned to) a particular $\mu_z$ and $\sigma_z^2$.

* **Important**: Probability Density $\neq$ Probability

So, what is a probability?

## Probability

The probability of an event $A$ is denoted by $\text{Pr}(A)$.

* $\text{Pr}(A)$ means the probability with which we believe event A is true

* An event $A$ is a binary variable saying whether or not some state of the world holds.

Probability is defined such that: $0 \leq \text{Pr}(A) \leq 1$

where $\text{Pr}(A)=1$ if the event will definitely happen and $\text{Pr}(A)=0$ if it definitely will not happen.

## Joint probability

**Joint probability** of two events: $\text{Pr}(A \wedge B)= \text{Pr}(A, B)$

If $A$ and $B$ are **independent**: $\text{Pr}(A, B)= \text{Pr}(A) \text{Pr}(B)$

For example, suppose $z_1$ and $z_2$ are chosen uniformly at random from the set $\mathcal{Z} = \{1, 2, 3, 4\}$.

Let $A$ be the event that $z_1 \in \{1, 2\}$ and $B$ be the event that **another** rv denoted as $z_2 \in \{3\}$.

Then we have: $\text{Pr}(A, B) = \text{Pr}(A) \text{Pr}(B) = \frac{1}{2} \cdot \frac{1}{4}$.

## Probability of a union of two events

Probability of event $A$ or $B$ happening is: $\text{Pr}(A \vee B)= \text{Pr}(A) + \text{Pr}(B) - \text{Pr}(A \wedge B)$

If these events are mutually exclusive (they can't happen at the same time):

$$
\text{Pr}(A \vee B)= \text{Pr}(A) + \text{Pr}(B)
$$

For example, suppose an rv denoted as $z_1$ is chosen uniformly at random from the set $\mathcal{Z} = \{1, 2, 3, 4\}$.

Let $A$ be the event that $z_1 \in \{1, 2\}$ and $B$ be the event that the **same** rv $z_1 \in \{3\}$.

Then we have $\text{Pr}(A \vee B) = \frac{2}{4} + \frac{1}{4}$.

## Conditional probability of one event given another

We define the **conditional probability** of event $B$ happening given that $A$ has occurred as follows:

$$
\text{Pr}(B | A)= \frac{\text{Pr}(A,B)}{\text{Pr}(A)}
$$

This is not defined if $\text{Pr}(A) = 0$, since we cannot condition on an impossible event.

## Conditional independence of one event given another

We say that event $A$ is conditionally independent of event $B$ if we have $\text{Pr}(A | B)= \text{Pr}(A)$

This implies $\text{Pr}(B|A) = \text{Pr}(B)$. Hence, the joint probability becomes $\text{Pr}(A, B) = \text{Pr}(A) \text{Pr}(B)$

The book uses the notation $A \perp B$ to denote this property.

## Coming back to our car stopping distance problem

$y = {\color{blue}z} x + 0.1 x^2$

where $z$ is a **continuous** rv such that $z \sim \mathcal{N}(\mu_z=1.5,\sigma_z^2=0.5^2)$.

* What is the probability of an event $Z$ defined by a reaction time $z \leq 0.52$ seconds?

$$
\text{Pr}(Z)=\text{Pr}(z \leq 0.52)= P(z=0.52)
$$

where $P(z)$ denotes the **cumulative distribution function (cdf)**. Note that cdf is denoted with a capital $P$.

Likewise, we can compute the probability of being in any interval as follows:

$\text{Pr}(a \leq z \leq b)= P(z=b)-P(z=a)$

* But how do we compute the cdf at a particular value $b$, e.g. $P(z=b)$?

## Cdf's result from pdf's

A pdf $p(z)$ is defined as the derivative of the cdf $P(z)$:

$$
p(z)=\frac{d}{d z}P(z)
$$

So, given a pdf $p(z)$, we can compute the following probabilities:

$$\text{Pr}(z \leq b)=\int_{-\infty}^b p(z) dz = P(b)$$
$$\text{Pr}(z \geq a)=\int_a^{\infty} p(z) dz = 1 - P(a)$$
$$\text{Pr}(a \leq z \leq b)=\int_a^b p(z) dz = P(b) - P(a)$$

**IMPORTANT**: $\int_{-\infty}^{\infty} p(z) dz = 1$

### Some notes about pdf's

The integration to unity is important!

$$\int_{-\infty}^{\infty} p(z) dz = 1$$

**Remember:** the integral of a pdf leads to a probability, and probabilities cannot be larger than 1.

For example, from this property we can derive the following:

$$
\int_{-\infty}^{\infty} p(z) dz = \int_{-\infty}^{a} p(z) dz + \int_{a}^{\infty} p(z) dz
$$

$$
\Rightarrow \text{Pr}(z \geq a)= 1 - \text{Pr}(z \leq a) = 1 - \text{P}(a) = 1 - \int_{-\infty}^a p(z) dz
$$

In some cases we will work with probability distributions that are **unnormalized**, so this comment is important!

* Being unnormalized means that the probability density of the distribution does not integrate to 1.
* In this case, we cannot call such function a pdf, even though its output is a probability density.

## Cdf's result from pdf's

Key point?

* Given a pdf $p(z)$, we can compute the probability of a continuous rv $z$ being in a finite interval as follows:

$$
\text{Pr}(a \leq z \leq b)=\int_a^b p(z) dz = P(b) - P(a)
$$

As the size of the interval gets smaller, we can write

$$
\text{Pr}\left(z - \frac{dz}{2} \leq z \leq z + \frac{dz}{2}\right) \approx p(z) dz
$$

Intuitively, this says the probability of $z$ being in a small interval around $z$ is the density at $z$ times
the width of the interval.

```python
from scipy.stats import norm # import from scipy.stats the normal distribution

zrange = np.linspace(-3, 3, 100) # 100 values for plot
fig_std_norm, (ax1, ax2) = plt.subplots(1, 2) # create a plot with 2 subplots side-by-side
ax1.plot(zrange, norm.cdf(zrange, 0, 1), label=r"$\mu_z=0; \ \sigma_z=1$") # plot cdf of standard normal
ax1.set_xlabel("z", fontsize=20)
ax1.set_ylabel("probability", fontsize=20)
ax1.legend(fontsize=15)
ax1.set_title("Standard Gaussian cdf", fontsize=20)

ax2.plot(zrange, norm.pdf(zrange, 0, 1), label=r"$\mu_z=0; \ \sigma_z=1$") # plot pdf of standard normal
ax2.set_xlabel("z", fontsize=20)
ax2.set_ylabel("probability density", fontsize=20)
ax2.legend(fontsize=15)
ax2.set_title("Standard Gaussian pdf", fontsize=20)
fig_std_norm.set_size_inches(25, 5) # scale figure to be wider (since there are 2 subplots)
```

## Note about scipy.stats

[scipy](https://docs.scipy.org/doc/scipy/index.html) is an open-source software for mathematics, science, and engineering. It's brilliant and widely used for many things!

**In particular**, [scipy.stats](https://docs.scipy.org/doc/scipy/reference/stats.html) is a simple module within scipy that has statistical functions and operations that are very useful. This way, we don't need to code all the functions ourselves. That's why we are using it to plot the cdf and pdf of the Gaussian distribution from now on, and we will use it for other things later.

* In case you are interested, scipy.stats has a nice [tutorial](https://docs.scipy.org/doc/scipy/tutorial/stats.html)

## Coming back to our car stopping distance problem

$y = {\color{blue}z} x + 0.1 x^2$

where $z$ is a continuous rv such that $p(z)= \mathcal{N}(z | \mu_z=1.5,\sigma_z^2=0.5^2)$.

* What is the probability of an event $Z$ defined by a reaction time $z \leq 0.52$ seconds?

$$
\text{Pr}(Z) = \text{Pr}(z \leq 0.52) = P(z=0.52) = \int_{-\infty}^{0.52} p(z) dz
$$

```python
Pr_Z = norm.cdf(0.52, 1.5, 0.5) # using scipy norm.cdf(z=0.52 | mu_z=1.5, sigma_z=0.5)

print("The probability of event Z is: Pr(Z) = ",round(Pr_Z,3))
```

    The probability of event Z is: Pr(Z) =  0.025

```python
z_value = 0.52 # z = 0.52 seconds
zrange = np.linspace(0, 3, 200) # 200 values for plot
fig_car_norm, (ax1, ax2) = plt.subplots(1, 2) # create subplot (two figures in 1)
ax1.plot(zrange, norm.cdf(zrange, 1.5, 0.5), label=r"$\mu_z=1.5; \ \sigma_z=0.5$") # Figure 1 is cdf
ax1.plot(z_value, norm.cdf(z_value, 1.5, 0.5), 'r*',markersize=15, linewidth=2,
         label=u'$P(z=0.52~|~\mu_z=1.5, \sigma_z^2=0.5^2)$')
ax1.set_xlabel("z", fontsize=20)
ax1.set_ylabel("probability", fontsize=20)
ax1.legend(fontsize=15)
ax1.set_title("Gaussian cdf of $z$ for car problem", fontsize=20)
ax2.plot(zrange, norm.pdf(zrange, 1.5, 0.5), label=r"$\mu_z=1.5; \ \sigma_z=0.5$") # figure 2 is pdf
ax2.plot(z_value, norm.pdf(z_value, 1.5, 0.5), 'r*', markersize=15, linewidth=2,
         label=u'$p(z=0.52~|~\mu_z=1.5, \sigma_z^2=0.5^2)$')
ax2.set_xlabel("z", fontsize=20)
ax2.set_ylabel("probability density", fontsize=20)
ax2.legend(fontsize=15)
ax2.set_title("Gaussian pdf of $z$ for car problem", fontsize=20)
fig_car_norm.set_size_inches(25, 5) # scale figure to be wider (since there are 2 subplots)
```

### Why is the Gaussian distribution so widely used?

Several reasons:

1. It has two parameters which are easy to interpret, and which capture some of the most basic properties of a distribution, namely its mean and variance.
2. The central limit theorem (Sec. 2.8.6 of the book) tells us that sums of independent random variables have an approximately Gaussian distribution, making it a good choice for modeling residual errors or “noise”.
3. The Gaussian distribution makes the least number of assumptions (has maximum entropy), subject to the constraint of having a specified mean and variance (Sec. 3.4.4 of the book); this makes it a good default choice in many cases.
4. It has a simple mathematical form, which results in easy to implement, but often highly effective, methods.

## Car stopping distance problem

$y = {\color{blue}z} x + 0.1 x^2$

where $z$ is a continuous rv such that $z \sim \mathcal{N}(\mu_z=1.5,\sigma_z^2=0.5^2)$.

* What is the **expected** value for the reaction time $z$?

This is not a trick question! It's the mean $\mu_z$, of course!

* But how do we compute the expected value for any distribution?

## Moments of a distribution

### First moment: Expected value or mean

The expected value (mean) of a distribution is the **first moment** of the distribution:

$$
\mathbb{E}[z]= \int_{\mathcal{Z}}z p(z) dz
$$

where $\mathcal{Z}$ indicates the support of the distribution (the $z$ domain).

* Often, $\mathcal{Z}$ is omitted as it is usually between $-\infty$ to $\infty$
* The expected value $\mathbb{E}[z]$ is often denoted by $\mu_z$

As you might expect (pun intended 😆), the expected value is a linear operator:

$$
\mathbb{E}[az+b]= a\mathbb{E}[z] + b
$$

where $a$ and $b$ are fixed variables (NOT rv's).

Additionally, for a set of $n$ rv's, one can show that the expectation of their sum is as follows:

$\mathbb{E}\left[\sum_{i=1}^n z_i\right]= \sum_{i=1}^n \mathbb{E}[z_i]$

If they are **independent**, the expectation of their product is given by

$\mathbb{E}\left[\prod_{i=1}^n z_i\right]= \prod_{i=1}^n \mathbb{E}[z_i]$

## Moments of a distribution

### Second moment (and relation to Variance)

The 2nd moment of a distribution $p(z)$ is:

$$
\mathbb{E}[z^2]= \int_{\mathcal{Z}}z^2 p(z) dz
$$

#### Variance can be obtained from the 1st and 2nd moments

The variance is a measure of the “spread” of the distribution:

$$
\mathbb{V}[z] = \mathbb{E}[(z-\mu_z)^2] = \int (z-\mu_z)^2 p(z) dz = \mathbb{E}[z^2] - \mu_z^2
$$

* It is often denoted by the square of the standard deviation, i.e. $\sigma_z^2 = \mathbb{V}[z] = \mathbb{E}[(z-\mu_z)^2]$

#### Elaboration of the variance as a result of the first two moments of a distribution

$$
\begin{align}
\mathbb{V}[z] & = \mathbb{E}[(z-\mu_z)^2] \\
& = \int (z-\mu_z)^2 p(z) dz \\
& = \int z^2 p(z) dz + \mu_z^2 \int p(z) dz - 2\mu_z \int zp(z) dz \\
& = \mathbb{E}[z^2] - \mu_z^2
\end{align}
$$

where $\mu_z = \mathbb{E}[z]$ is the first moment, and $\mathbb{E}[z^2]$ is the second moment.

Therefore, we can also write the second moment of a distribution as

$$\mathbb{E}[z^2] = \sigma_z^2 + \mu_z^2$$

#### Variance and standard deviation properties

The standard deviation is defined as

$ \sigma_z = \text{std}[z] = \sqrt{\mathbb{V}[z]}$

The variance of a shifted and scaled version of a random variable is given by

$\mathbb{V}[a z + b] = a^2\mathbb{V}[z]$

where $a$ and $b$ are fixed variables (NOT rv's).

If we have a set of $n$ independent rv's, the variance of their sum is given by the sum of their variances

$$
\mathbb{V}\left[\sum_{i=1}^n z_i\right] = \sum_{i=1}^n \mathbb{V}[z_i]
$$

The variance of their product can also be derived, as follows:

$$
\begin{align}
\mathbb{V}\left[\prod_{i=1}^n z_i\right] & = \mathbb{E}\left[ \left(\prod_i z_i\right)^2 \right] - \left( \mathbb{E}\left[\prod_i z_i \right]\right)^2\\
 & = \mathbb{E}\left[ \prod_i z_i^2 \right] - \left( \prod_i\mathbb{E}\left[ z_i \right]\right)^2\\
 & = \prod_i \mathbb{E}\left[  z_i^2 \right] - \prod_i\left( \mathbb{E}\left[ z_i \right]\right)^2\\
 & = \prod_i \left( \mathbb{V}\left[  z_i \right] +\left( \mathbb{E}\left[ z_i \right]\right)^2 \right)- \prod_i\left( \mathbb{E}\left[ z_i \right]\right)^2\\
 & = \prod_i \left( \sigma_{z,\,i}^2 + \mu_{z,\,i}^2 \right)- \prod_i\mu_{z,\,i}^2 \\
\end{align}
$$

## Note about higher-order moments

* The $k$-th moment of a distribution $p(z)$ is defined as the expected value of the $k$-th power of $z$, i.e. $z^k$:

$$
\mathbb{E}[z^k]= \int_{\mathcal{Z}}z^k p(z) dz
$$

## Mode of a distribution

The mode of an rv $z$ is the value of $z$ for which $p(z)$ is maximum.

Formally, this is written as,

$$ \mathbf{z}^* = \underset{z}{\mathrm{argmax}}~p(z)$$

If the distribution is multimodal, this may not be unique:
* That's why $\mathbf{z}^*$ is in **bold**, to denote that in general it is a vector that is retrieved!
* However, if the distribution is unimodal (one maximum), like the univariate Gaussian distribution, then it retrieves a scalar $z^*$

Note that even if there is a unique mode, this point may not be a good summary of the distribution.

## Mean vs mode for a non-symmetric distribution

```python
# 1. Create a gamma pdf with parameter a = 2.0

from scipy.stats import gamma # import from scipy.stats the Gamma distribution

a = 2.0 # this is the only input parameter needed for this distribution

# Define the support of the distribution (its domain) by using the
# inverse of the cdf (called ppf) to get the lowest z of the plot that
# corresponds to Pr = 0.01 and the highest z of the plot that corresponds
# to Pr = 0.99:
zrange = np.linspace(gamma.ppf(0.01, a), gamma.ppf(0.99, a), 200)

mu_z, var_z = gamma.stats(2.0, moments='mv') # This computes the mean and variance of the pdf

fig_gamma_pdf, ax = plt.subplots() # a trick to save the figure for later use
ax.plot(zrange, gamma.pdf(zrange, a), label=r"$\Gamma(z|a=2.0)$")
ax.set_xlabel("z", fontsize=20)
ax.set_ylabel("probability density", fontsize=20)
ax.legend(fontsize=15)
ax.set_title("Gamma pdf for $a=2.0$", fontsize=20)
plt.close(fig_gamma_pdf) # do not plot the figure now. We will show it in a later cell
```

```python
# 2. Plot the expected value (mean) for this pdf
ax.plot(mu_z, gamma.pdf(mu_z, a), 'r*', markersize=15, linewidth=2, label=u'$\mu_z = \mathbb{E}[z]$')
```

    [<matplotlib.lines.Line2D at 0x7f80bcb1c4f0>]

```python
# 3. Calculate the mode and plot it
from scipy.optimize import minimize # import minimizer

# Finding the maximum of the gamma pdf can be done by minimizing
# the negative gamma pdf. So, we create a function that outputs
# the negative of the gamma pdf given the parameter a=2.0:
def neg_gamma_given_a(z): return -gamma.pdf(z,a)

# Use the default optimizer of scipy (L-BFGS) to find the
# maximum (by minimizing the negative gamma pdf). Note
# that we need to give an initial guess for the value of z,
# so we can use, for example, z=mu_z:
mode_z = minimize(neg_gamma_given_a,mu_z).x

ax.plot(mode_z, np.max(gamma.pdf(mode_z, a)),'g^', markersize=15,
        linewidth=2,label=u'mode $\mathbf{z}^*=\mathrm{argmax}~p(z)$')
ax.legend() # show legend
```

    <matplotlib.legend.Legend at 0x7f80bc769f40>

```python
# Code to generate this Gamma distribution hidden during presentation (it's shown as notes)

print('The mean is ',mu_z) # print the mean calculated for this gamma pdf
print('The mode is approximately ',mode_z) # print the mode
fig_gamma_pdf # show figure of this gamma pdf
```

## The amazing Bayes' rule
Bayesian inference definition:
* Inference means “the act of passing from sample data to generalizations, usually with calculated degrees of certainty”.
* Bayesian is used to refer to inference methods that represent “degrees of certainty” using probability theory, and which leverage Bayes’ rule to update the degree of certainty given data.

**Bayes’ rule** is a formula for computing the probability distribution over possible values of an unknown (or hidden) quantity $z$ given some observed data $y$:

$$
p(z|y) = \frac{p(y|z) p(z)}{p(y)}
$$

Bayes' rule follows automatically from the identity: $p(z|y) p(y) = p(y|z) p(z) = p(y,z) = p(z,y)$

## The amazing Bayes' rule

* I know... You don't find it very amazing (yet!).
* Wait until you realize that almost all ML methods can be derived from this simple formula

$$
p(z|y) = \frac{p(y|z) p(z)}{p(y)}
$$

### See you next class

Have fun!


```python
## This code cell will not be shown in the HTML version of this notebook
#### some helpful imports ####
# import autograd functionality
import autograd.numpy as np

# import testing libraries
import sys
sys.path.append('../')
from mlrefined_libraries import time_series_lib as timelib
from mlrefined_libraries import pid_lib as pidlib

# import dataset path
datapath = '../datasets/'

# import various other libraries e.g., for plotting, deep copying
import copy
import matplotlib.pyplot as plt

# this is needed to compensate for %matplotl+ib notebook's tendancy to blow up images when plotted inline
from matplotlib import rcParams
rcParams['figure.autolayout'] = True
%matplotlib notebook

# autoreload function - so if anything behind the scenes is changeed those changes
# are reflected in the notebook without having to restart the kernel
%load_ext autoreload
%autoreload 2
```

# Principles of PID control

Toggle code on and off in this presentation by clicking the 'Toggle code' button below.

```python
from IPython.display import display
from IPython.display import HTML
import IPython.core.display as di # Example: di.display_html('<h3>%s:</h3>' % str, raw=True)

# This line will hide code by default when the notebook is eåxported as HTML
di.display_html('', raw=True)

# This line will add a button to toggle visibility of code blocks, for use with the HTML export version
di.display_html('''<button onclick="jQuery('.input_area').toggle(); jQuery('.prompt').toggle();">Toggle code</button>''', raw=True)
```

<button onclick="jQuery('.input_area').toggle(); jQuery('.prompt').toggle();">Toggle code</button>

# The standard PID Control Model

- with a trained Imitator or System Model in hand, we can now look at automatically controlling this Imitator to perform desired tasks

- the simplest kind of behavior we make a system obey (like e.g., temperature control and cruise control): is to make the system match a series of **training** *set points* $x_1,\,x_2,\,...,x_T$ as closely as possible

- examples of set points:
    - for cruise control / autonomous driving: speed levels to have the car drive
    - for temperature control: different temperature levels throughout the day
    - for an industrial process like water level: keep a certain level in a tank that can change throughout the day

- this involves putting our Imitator model under the authority of a Controller (or - you can say - we pass our Imitator model through a Control model that selects optimal actions for it)

- once trained the Control Model should be able to choose actions automatically so that the Imitator matches desired set points

- that is, our Controller will choose actions $a_t$ for optimally for our Imitator so that its output $s_{t+1} = f_{\text{imitator}}\left(s_t,a_t\right)$ matches the desired set points as closely as possible (as the system allows), or that

$$s_t \approx x_t \,\,\,\,\, \text{for} \,\,\,\,\, t=2,...,T$$

(often the initial state $s_1$ is determined by the problem, or set to a reasonable reference value)

- once tuned properly a Control Model is often referred to as a *Control Law* or *Optimal Policy*

- how does the Controller choose optimal actions to meet a series of set points?

- *one way* is to train a Controller to choose actions optimally **looking backwards** to decide on the best choice of action in the present

- this is based on the desire to always be correcting for previous mistakes / *historical errors*, or how well $s_t \approx x_t$ previously

- this most popular control approach is called *Proportional Integral Derivative or PID* ontrol

- this is a simple *parameterized dynamic system with unlimited memory* that captures historical *error* between our sequence of training set points and the corresponding states of our system

- as a graphical model it looks like this

- here we have used the *signed error* $e_t = x_t - s_t$ as historical feedback to the Controller Model: $f_{\text{controller}}\left(e_t\right)$

- using the signed error is a convention, you could use another (e.g., absolute value of the error)

- we uses a linear combination of this (and its history and/or its derivatives) to learn how to choose actions optimally

- the simplest parameterized controller (a *Proportional controller*) uses a linear combination of this (signed) error to determine the next action to take

\begin{equation}
f_{\text{controller}}\left(e_t;\Theta_{\text{controller}}\right) = a_t =  w_0 + w_1e_t
\end{equation}

- this is a function with weights we need to tune properly in order for the controller to properly choose actions

- it is called a *Proportional controller* because the action $a_t$ is literally being made proportional to the signed error of the system at the prior state

- a common extension of this idea: use a summary statistic of the history of the error as well as well

- this is usually chosen to be the *integral of the error*

\begin{equation}
h^e_t = h^e_t + \frac{1}{D}e_t
\end{equation}

where $\frac{1}{D}$ is the gap between steps, but in principle any dynamic system with unlimited memory will work

- adding the integral (or history) of the error gives the controller *context*, as the integral summarizes how the error has changed in the past

- this is what we mean when we say that a PID Controller 'looks backward' to decide on the best choice of action in the present

- adding the history $h^e_t$ term to our parameterized action function / control model gives the parameterized update

\begin{equation}
a_t = w_0 + w_1e_t + w_2h^e_t
\end{equation}

- this is a so-called *Proportional Integral* controller - one of the most common automatic controller used today in practice (for set-point matching automatic control problems)

- notice: this history of the error is now an explicit input to our Controller

\begin{equation}
f_{\text{control}}\left(e_t,h^e_t ; \Theta_{\text{controller}} \right) = a_t = w_0 + w_1e_t + w_2h^e_t
\end{equation}

- one final common addition: the derivative of the error: $\frac{e_t - e_{t-1}}{D}$

- proportional information derivative or *local difference* of the error can be added as well, tacking on another term as to the action update

\begin{equation}
a_t = w_0 + w_1e_t + w_2h_t + w_3\frac{e_t - e_{t-1}}{D}
\end{equation}

- using this update in a Control Model we have the so-called *Proportional Inegral Derivative* (PID) controller.

- our Control Model now takes in two prior error terms

# Tuning the weights of a PID Control Model

- how do we tune these weights properly - so that our controller learns how to produce the best actions to lead our system model to match our training set points?

- traditional (non machine-learning) approaches PID controller tuning involve "voodoo" and a lot of human trial and error

- see e.g., the recommendations for tuning on stackoverflow and youtube (note here: $w_1 = K_p$, $w_2 = K_i$, and $w_3 = K_d$)

https://robotics.stackexchange.com/questions/167/what-are-good-strategies-for-tuning-pid-loops

https://www.youtube.com/watch?v=VVOi2dbtxC0&t=1050s

- note: 'traditional' does not mean that 'old' - this is how many people tune their controllers today

- why so much 'voo-doo' and human trial and error for PID tuning?

- because the traditional way of doing PID control already involves one understand an enormous amount of specialized information

    - for Imitator / System modeling: the traditional way is to use 'first principles' differential equations modeling

    - this can lead to system models that are - by their very nature - highly unstable

    - this involves building up not only a steep mathematical knowledge stack, but expert knowledge in a particular domain (e.g., physics, chemistry, robotics, etc.,)

- things often not included in traditional automatic control pedagogy

    - programming / basic CS
    - machine learning / deep learning basics (as an alternative to differential equations modeling)
    - mathematical optimization (although there is some emphasis on this for advanced students of automatic control, the approaches taken are very limiting = only special structures like QP are studied)

# The ML perspective on PID tuning

- but 'automatic parameter tuning' is the 'bread and butter' for machine learning folks - so what do we need to do to auto-tune our PID parameters?

- Like everything else: we need to form a cost function (whose minimum recovers correctly tuned parameters)!

- to design a proper cost, lets look at our entire controller pipeline- including our system model

- to keep things simple we will perform these derivations using the simplest Proportional (P) controller, but everything that follows is the same for PI and PID controllers as well

- our basic Proportional controller takes in the current (signed) error $e_t = x_t - s_t$ and returns the action $a_t$

\begin{equation}
f_{\text{control}}\left(e_t\right) = a_t
\end{equation}

- we then feed this action into our Imitator model to get our next state $s_{t+1}$

\begin{equation}
f_{\text{imitator}}\left(s_t, a_t \right) = f_{\text{imitator}}\left(s_t,\, f_{\text{control}}\left(e_t\right) \right) = s_{t+1}
\end{equation}

- now ideally - we want these actions made *so that this next state matches the input set point*, that is

\begin{equation}
s_{t+1} \approx x_{t+1}
\end{equation}

- in other words, we want the *error* $e_{t+1} = x_{t+1} - s_{t+1}$ to be small in magnitude

- so why not tune $\Theta_{\text{controller}}$ to minimize the average e.g., squared error over the entire sequence of *training set points* $x_1,...,x_T$

\begin{equation}
\frac{1}{T-1}\sum_{t=1}^{T-1}\left(e_{t+1}\right)^2 = \frac{1}{T-1}\sum_{t=1}^T\left(s_{t+1} - x_{t+1}\right)^2
\end{equation}

- we ignore the error on our initial state $s_1$ since we cannot adjust it (its given by the problem at hand!  e.g., with cruise control the car starts with 0 velocity)

- if unwravel the definition of $s_{t+1}$ and express it in terms of our system and control model this is equivalently

\begin{equation}
\frac{1}{T-1}\sum_{t=1}^{T-1}\left(f_{\text{imitator}}\left(s_t,\, f_{\text{control}}\left(e_t;\Theta_{\text{controller}}\right) \right) - x_{t+1}\right)^2
\end{equation}

- note here: we are minimizing over the *Controller parameters* $\Theta_{\text{controller}}$, **not** the parameters of our Imitator model

- the weights of our imitator have already been tuned as necessary to real action/state data (another way to think about it: they are regularized so that the system matches a real set of input/output data)

-  our goal in optimizing our Controller parameters is to solidify our *Optimal Control Law* so that we learn an entire sequence of optimal actions $a_1,\,a_2,\,...,a_{T-1}$

- thus we are getting at our optimal set of actions indirectly (via a parameterized function)

- Lets look at a simple implementation of a PID controller

```python
# a simple implementation of a PID controller
def PID_controller(e_t,h_t,d_t,w):
    # note here in terms of inputs
    # e_t = current error
    # h_t = integral of error
    # d_t = derivative of error
    return w[0] + w[1]*e_t + w[2]*h_t + w[3]*d_t
```

```python
# loop for evaluating control model over all input/output action/state pairs
# Our inputs here:
# s_1 - the initial condition state
# x - sequence of training set points
# w - the control model parameters
def control_loop(x,w):
    # initialize key variables and containers
    s_t = copy.deepcopy(s_1)
    h_t = 0
    d_t = 0
    frac = 1/float(np.size(x))
    action_history = []
    state_history = [s_t]
    error_history = []

    # loop over training set points and run through controller, then
    # system models
    for t in range(np.size(x) - 1):
        # get current set point
        x_t = x[:,t]

        # update error
        e_t = x_t - s_t
        error_history.append(e_t)

        # update integral of error
        h_t = h_t + frac*e_t

        # update derivative of error
        if t > 0:
            d_t = frac*(error_history[-1] - error_history[-2])

        # send error, integral, and derivative to PID controller
        a_t = PID_controller(e_t,h_t,d_t,w)

        # clip a_t to match system specifications?

        # send action to system model
        s_t = tuned_system_model(s_t,a_t)

        # store state output, and actions (for plotting)
        state_history.append(s_t)
        action_history.append(a_t)

    # transition to arrays
    state_history = np.array(state_history)[np.newaxis,:]
    action_history = np.array(action_history)[np.newaxis,:]

    # return velocities and control history
    return state_history,action_history
```

```python
# an implementation of the least squares cost for PID controller tuning
# note here: s is an (1 x T) array and a an (1 x T-1) array
def least_squares(w,x):
    # system_loop - runs over all action-state pairs and produces entire
    # state prediction set
    state_history,action_history = control_loop(x,w)

    # compute least squares error between real and predicted states
    cost = np.sum((state_history[:,1:] - x[:,1:])**2)
    return cost/float(x.shape[1]-1)
```

#### Example: 1  Cruise control

- a `Python` implementation of our `control_model` for the *cruise control* problem.  Notice at each update step the action is clipped to lie in the range $[-50,100]$ - which is the angle of the pedal against the floor of the car.

- because here we are using a 'true model' of the automobile we need to use a zero order optimization method - since we cannot compute the gradient of `system_model` with respect to our PID weights.

```python
# create tuned system model for the car
ind = np.argmin(mylib1.train_cost_histories[0])
w_best = mylib1.weight_histories[0][ind]
# a_norm = mylib1.x_norm
# s_norm = mylib1.y_norm
# s_invnorm = mylib1.y_invnorm
# a_invnorm = mylib1.x_invnorm
# tuned_system_model = lambda state,action: s_invnorm(system_model(s_norm(state),a_norm(action),w_best))

tuned_system_model = lambda state,action: system_model(state,action,w_best)
s_1 = 0.0
```

```python
# loop for evaluating control model over all input/output action/state pairs
# Our inputs here:
# s_1 - the initial condition state
# x - sequence of training set points
# w - the control model parameters
def control_loop(x,w):
    # initialize key variables and containers
    s_t = copy.deepcopy(s_1)
    h_t = 0
    d_t = 0
    frac = 1/float(np.size(x))
    action_history = []
    state_history = [s_t]
    error_history = []

    # loop over training set points and run through controller, then
    # system models
    for t in range(np.size(x) - 1):
        # get current set point
        x_t = x[:,t]

        # update error
        e_t = x_t - s_t
        error_history.append(e_t)

        # update integral of error
        h_t = h_t + frac*e_t

        # update derivative of error
        if t > 0:
            d_t = frac*(error_history[-1] - error_history[-2])

        # send error, integral, and derivative to PID controller
        a_t = PID_controller(e_t,h_t,d_t,w)

        # clip action range to realistic machine standard?
        # clip inputs to -50% to 100% for car
        if a_t >= 100.0:
            a_t = 100.0
        if a_t <= -50.0:
            a_t = -50.0

        # send action to system model
        s_t = tuned_system_model(s_t,a_t)

        # store state output, and actions (for plotting)
        state_history.append(s_t)
        action_history.append(a_t)

    # transition to arrays
    state_history = np.array(state_history)[np.newaxis,:]
    action_history = np.array(action_history)[np.newaxis,:]

    # return velocities and control history
    return state_history,action_history
```

```python
# an implementation of the least squares cost for PID controller tuning
# note here: s is an (1 x T) array and a an (1 x T-1) array
def least_absolute(w,x):
    # system_loop - runs over all action-state pairs and produces entire
    # state prediction set
    state_history,action_history = control_loop(x,w)

    # compute least squares error between real and predicted states
    cost = np.sum(np.abs(state_history[:,1:] - x[:,1:]))
    return cost/float(x.shape[1]-1)
```

```python
# create an instance of the car simulator
demo = pidlib.car_simulator.MyCar()

# create a training sequence of *set points* for trying out the true simulator, and for learning a controller
x_car = demo.create_set_points()
```

```python
# This code cell will not be shown in the HTML version of this notebook
# initialize with input/output data
mylib5 = pidlib.rnn_pid_lib.super_setup.Setup(x_car)

# normalize?
mylib5.preprocessing_steps(normalizer_name = 'none')

# split into training and validation sets
mylib5.make_train_val_split(train_portion = 1)

# choose cost
mylib5.choose_cost(control_loop,cost = least_absolute)

# fit an optimization
w = 0.1*np.random.randn(4,1)
# mylib5.fit(max_its = 59,alpha_choice = 10**(0),optimizer = 'gradient_descent',w_init = w,verbose = False)

mylib5.fit(max_its = 50,alpha_choice = 10**(0),optimizer = 'zero_order',w_init = w,verbose = False)

# show cost function history
mylib5.show_histories(start = 1)
```

    <IPython.core.display.Javascript object>

```python
# This code cell will not be shown in the HTML version of this notebook
# Plot the standard normalized series and its training fit
pidlib.variable_order_plotters.plot_setpoint_train_val_sequences(mylib5)
```

    <IPython.core.display.Javascript object>

- these actions are crazy when the desired speed ramps up and down - we can fix this by *regularizing*

#### Example: 2  PID controller for two tank example

```python
# This code cell will not be shown in the HTML version of this notebook
def two_tank_control_model(x,w):
    #### simulate vehicle response to set points ####
    s_t = [0.0,0.0]
    action_history = []
    state_history = [s_t]
    h = 0.0
    for t in range(np.size(x) - 1):
        # get current set point
        x_t = x[:,t]

        # update error
        e_t = x_t - s_t[1]

        # update integral of error
        h = h + e_t*0.1

        # set action based on PI linear combination
        a_t = w[0] + w[1]*e_t + w[2]*h

        if t > 0:
            a_t += w[3]*(s_t[1] - state_history[-2][1])

        # clip inputs to -50% to 100%
        if a_t >= 100.0:
            a_t = 100.0
        if a_t <= 0.0:
            a_t = 0.0

        # cap off condition
        #if s_t[1] > x_t:
        #    a_t = 0

        # run pid controller
        s_t = demo_3.tank_model(a_t,s_t)

        # store results
        action_history.append(a_t)
        state_history.append(s_t)

    # transition to arrays
    state_history = np.array(state_history).T
    action_history = np.array(action_history)[np.newaxis,:]

    # return velocities and control history
    return state_history,action_history
```

```python
# This code cell will not be shown in the HTML version of this notebook
# an implementation of the least squares cost function for linear regression
def least_squares(w,x):
    states,actions = control_model(x,w)
    # compute cost over batch
    cost = np.sum((states[1,1:] - x[:,1:])**2)
    return cost/float(x.shape[1]-1)

# a compact least absolute deviations cost function
def least_absolute_deviations(w,x):
    states,actions = control_model(x,w)
    # compute cost over batch
    cost = np.sum(np.abs(states[1,1:] - x[:,1:]))
    return cost/float(x.shape[1]-1)
```

```python
# This code cell will not be shown in the HTML version of this notebook
# initialize with input/output data
mylib6 = pidlib.rnn_pid_lib.super_setup.Setup(x_tank2)

# split into training and validation sets
mylib6.make_train_val_split(train_portion = 1)

# choose cost
control_model = two_tank_control_model
mylib6.choose_cost(model = control_model,cost = least_absolute_deviations)

# fit an optimization
w = 0.1*np.random.randn(4,1)
mylib6.fit(max_its = 10,alpha_choice = 'diminishing',optimizer = 'zero_order',w_init = w,verbose = False)

# show cost function history
mylib6.show_histories(start = 5)
```

    <IPython.core.display.Javascript object>

```python
# This code cell will not be shown in the HTML version of this notebook
# plot results
pidlib.two_tank_plotter.plot_results(mylib6)
```

    <IPython.core.display.Javascript object>


Table of Contents

1  $\LaTeX$

1.1  Getting Started

1.1.1  Spacing

1.1.2  Justification

1.1.2.1  Centering

1.1.2.2  Multiline formatting for math

1.2  The Greeks

1.3  Mathematical Symbols

1.3.1  Superscript and Subscript

1.3.2  Grouping

1.3.3  Sets & Probability

1.3.4  Mathematical comparators

1.3.5  Mathematical operators

1.3.6  Fractions

1.4  A few examples of using $\LaTeX$

1.4.0.1  Einstein's mass-energy equivalence

1.4.0.2  Equations of motion

1.4.0.3  Quadratic Equation

1.4.0.4  Some calculus for good measure

1.5  Using in visualizations

# $\LaTeX$

>_LaTeX, which is pronounced «Lah-tech» or «Lay-tech» (to rhyme with «blech» or «Bertolt Brecht»), is a document preparation system for high-quality typesetting. It is most often used for medium-to-large technical or scientific documents but it can be used for almost any form of publishing.\
    $~~~~$ - https://www.latex-project.org/about/_

This git is designed to be a primer in using $\LaTeX$ in Jupyter notebooks with the intention of improving presentation.

$\LaTeX$ is a powerful tool in making your markdowns cells look more professional and stand out. One can introduce $\LaTeX$ into the Jupyter Notebooks markdown cells using imported libraries, however, Jupyter Notebook natively leverages JavaScripts library MathJax to allow rendering of special characters and formatting. Its intended to allow for displaying mathematical functions and text in a more legible format and is widely used in academia.

This notebook aims to act like a cheat sheet for commonly used formatting examples. As we get progress along in the notebook, explanations will be limited to expounding on usage idiosyncrasies of the function in question. In other words, sections later in the book rely on knowledge of previous sections.

## Getting Started
Lets start with the basics. To insert $\LaTeX$ formatted text in Jupyter notebook is enclose the text in '$' to get started. Eg: here is the letter 'A' before and after

A : A\
\\$A\\$ : $A$

Easy enough. However, if all we wanted to bold and italicize our text, we can do that with builtin markdown shortcuts. The real power of $\LaTeX$ comes with introducing special characters. This is done using the escape character '\'. In this example, to get the alpha symbol we prefix it with the backspace character.

\\$alpha\\$ : $alpha$\
\\$\\alpha\\$ : $\alpha$

### Spacing

$\LaTeX$ doesn't care for white space and displays text with standardized spacing regardless of how many whitespace characters are there. To allow for better separation and control one can use '~'

$A          B$ (10 spaces separating the characters)\
$A~~B$ (2 '~' between characters)

### Justification
#### Centering
By default, Markdowns are left justified and so are $\LaTeX$ entries. Enclose text in '\$$' instead of a single '\$' centers the $\LaTeX$ text. eg:

$$A = B$$

#### Multiline formatting for math
If you are typing a multi line mathematical process or derivation, it can be convenient to wrap the entire block in a \\begin and \\end. This allows us to forgo the '$' at the beginning and end of each line. A few things to keep in mind,

- Every line should end with a '\\\\'.
- By default the block right aligned, however one can provide an ***&*** at every line to specify point of alignment

$$
\begin{align}
x &= 10 + 5 +2 \\
x &= 10 + 7\\
x &= 17\\
x - 17 &= 0\\
\end{align}
$$

## The Greeks
The greek alphabets are all represented. Note that some letters have a capitalized version. Letters exempt are the ones which resemble their alphabet equivalent, such as a capital $\alpha$ is ***A***. To access the capital variations, capitalize the first letter

$$
\begin{align}
Alpha&: \alpha\\
Beta&: \beta\\
Gamma&: \gamma~\Gamma\\
Delta&: \delta~\Delta\\
Epsilon&: \epsilon\\
Zeta&: \zeta\\
Eta&: \eta\\
Theta&: \theta~\Theta\\
Iota&: \iota\\
Kappa&: \kappa\\
Lambda&: \lambda~\Lambda\\
Mu&: \mu\\
Nu&: \nu\\
Xi&: \xi~\Xi\\
Omicron&: \omicron\\
Pi&: \pi~\Pi\\
Sigma&: \sigma~\Sigma\\
Tau&: \tau\\
Upsilon&: \upsilon~\Upsilon\\
Phi&: \phi~\Phi\\
Chi&: \chi\\
Psi&: \psi~\Psi\\
Omega&: \omega~\Omega
\end{align}
$$

## Mathematical Symbols
There is a plethora of mathematical symbols available for use as well, however, due to their verbose nature, we won't be going over every single one. Here are a few divided by subsections

### Superscript and Subscript
Superscipts and Subscripts are easily accessible using the '^' and '\_' symbols. This symbol needs to be prefixed to the exponent or the subscript.

$$ a_b $$
$$ x^2 $$

### Grouping
Grouping allows us to group a set of symbols together so they are always presented together. Lets use a superscript to illustrate this example. By default the superscript symbol only captures the first character to superscript. So if I wanted to write anything a little more complicated I'd have to group characters together using the _{ }_ symbols. In the following example we get two very different outcomes depending upon whether we grouped y+z

$$
\begin{align}
No~grouping &:x ^ y + z\\
Grouping &:x ^ {y + z}
\end{align}
$$

### Sets & Probability

Operators for showing set relationship

$$
\begin{align}
Union &: \cup\\
Intersection &: \cap\\
Subset &: \subset\\
Superset &: \supset\\
\end{align}
$$

### Mathematical comparators
These symbols and their usage are fairly self explanatory. Some symbols which have a dedicated spot on the keyboard don't need any special effort. For eg:
$$ = ~ < ~ >$$

However, for others that do not have a dedicated keyboard spot they can be accessed with the escape character,

$$
\begin{align}
\approx ~ \leq~  \geq \\
\equiv ~  \ll~   \gg \\
\neq ~    \leq~  \geq \\
\end{align}
$$

### Mathematical operators
$$\pm ~\times~\cdot$$

### Fractions
Fractions can be presented using the _'\\dfrac'_ or _'\\tfrac'_ command. Depending upon need either can be utilized.

Dfrac: $\dfrac x y$

Tfrac: $\tfrac x y$

This can be combined with grouping to display complex math in a legible form

$$ \dfrac {x^{exp}} {y_{sub}^{exp}} $$

## A few examples of using $\LaTeX$
Lets use what we've learned so far and write some famous and some more obscure equations

#### Einstein's mass-energy equivalence

$$ E = mc^2$$

#### Equations of motion

$$
\begin{align}
v &= u + at \\
s &= ut + \dfrac {a t^2}{2} \\
s &= \dfrac {(u+v)t}{2} \\
v^2 &= u^2 + 2as
\end{align}
$$

#### Quadratic Equation
For,

$$
\begin{align}
a x^2 + b x + c &= 0 \\
x &= \dfrac {-b \pm \sqrt{b^2 - 4 ac}}{2a}
\end{align}
$$

#### Some calculus for good measure

##### Derivation

$$
\begin{align}
    \dfrac{d}{dx} x^n &= n x^{n-1} \\
    \dfrac{d}{dx} (f(x)\cdot g(x)) &= \dfrac{d}{dx} (f(x))\cdot g(x) + \dfrac{d}{dx} (g(x))\cdot f(x)\\
\end{align}
$$

##### Integration

$$
\begin{align}
    \int x^n \cdot dx &= \dfrac{x^{n+1}}{n+1} + C \\
    \int f(x)\cdot g'(x)\cdot dx &= f(x)\cdot g(x) - \int g(x)\cdot f'(x)\cdot dx
\end{align}
$$

## Using in visualizations

These rules can also be applied to your visualizations if needed. All labels and titles can utilize $\LaTeX$ formatting as long as they are inserted as _rstrings_. This allows the use of the backslash escape character. As an example, here we plot a sine function and its differentiation cosine. Take note of the title and legend and the x ticks

```python
import numpy as np
import matplotlib.pyplot as plt

# Generate x values
x = np.arange(0, 4*np.pi, 0.1);

# Get y values for sine wave and its differentiation cosine
y = np.sin(x)
dy = np.cos(x)

# Plot waves
plt.figure(figsize = (10,8))
plt.plot(x, y)
plt.plot(x, dy)

# Give a title for the sine wave plot
plt.title(r'Plot of $sin(x)$ & $\dfrac {d}{dx} sin(x)$')

# Give x axis label for the plot
plt.xlabel('x')

# Give y axis label for the plot
plt.ylabel('f (x)')

plt.xticks(np.arange(0, 9*(np.pi/2),(np.pi)),
          [r'0$\pi$', r'$1\pi$', r'2$\pi$',r'3$\pi$', r'4$\pi$'])

plt.grid(True, alpha =0.2)
plt.axhline(y=0, color='k')

plt.legend([r'$sin(x)$',r'$\dfrac {d}{dx} sin(x) = cos(x)$'],loc = 1)
plt.show();
```

As you can see the possibilities are limitless and learning and leveraging $\LaTeX$ is a must to improve on your presentations