Bienvenue à la partie 2 / N de ma série sur les Fondamentaux du Machine Learning en C ! Si vous ne l’avez pas encore fait, parcourez la partie 1 —je vais supposer que vous êtes familier avec les concepts abordés dans cet article.
Au-delà d’une seule variable
Notre premier modèle de machine learning, le modèle de régression linéaire univariée, était techniquement du machine learning, mais si nous sommes honnêtes avec nous-mêmes, ce n’est pas très impressionnant. Alors, quel était l’intérêt de passer par tout ce travail ? Pour poser les bases de modèles plus complexes, comme le modèle linéaire multivarié, que nous allons examiner aujourd’hui.
Le modèle linéaire multivarié
Le modèle linéaire multivarié (MLM) est essentiellement le même que son analogue univarié, sauf qu’il nous permet d’avoir plusieurs variables d’entrée qui influencent la sortie. Dans la dernière partie, nous avons essayé de prédire les prix des maisons uniquement en fonction de la superficie. Nous savons tous qu’une maison de 5000 en Arkansas est beaucoup moins chère qu’une maison de même taille à San Jose, mais notre modèle donnerait le même prix pour les deux.
C’est là que nous réalisons que nous avons besoin que notre modèle soit capable de mélanger des variables de différentes classes, chacune pouvant avoir un effet différent sur notre prédiction.
Énoncé du problème
Définissons d’abord notre problème :
On nous donne des données pour les variables d’entrée suivantes et leurs prix de maison correspondants (en milliers de dollars) :
: Superficie (en )
: Largeur du terrain (en ft)
: Profondeur du terrain (en ft)
: Distance de la côte (en miles)
Prédire le prix d’une maison étant donné un nouvel ensemble de données.
Nous savons que chacun de ces facteurs aura un effet sur le prix. Certains (comme la superficie et la distance de la côte) auront un effet très important, tandis que d’autres auront un effet plus faible.
Alors, comment pouvons-nous exprimer ces relations sous forme de modèle ? Essayons d’en construire un en examinant quelques graphiques et en faisant des estimations.
Graphique : Superficie vs. Prix
Nous pouvons voir qu’il y a une association positive entre la superficie et le prix. La ligne de tendance est modélisée par .
Graphique : Largeur du terrain vs. Prix
Il y a également une légère association positive ici. La ligne de tendance est .
Graphique : Profondeur du terrain vs. Prix
Cela a une association positive beaucoup plus forte que la largeur du terrain, suggérant qu’une forme de terrain plus profonde est plus valorisée par les acheteurs. La ligne de tendance est .
Graphique : Distance de la côte vs. Prix
Comme nous nous y attendions, plus on s’éloigne de la côte, plus le prix diminue. Nous voyons également que l’effet diminue à mesure que nous nous éloignons de la côte. La ligne de tendance est .
Une première estimation du modèle
Les lignes de tendance nous indiquent la relation approximative entre chaque caractéristique et le prix du logement. Nous pouvons les combiner pour obtenir un modèle approximatif des données. Essayons d’additionner toutes les lignes de tendance et voyons si nous obtenons un modèle raisonnable.
Intuitivement, qu’est-ce que cela signifie ? Nous avons une valeur de base (bias) de 6,5 millions de dollars, qui représente la valeur théorique d’une maison de 0 pieds carrés, sans terrain, au bord de l’océan. Attendez… cela ne semble pas correct. Nous devrions probablement prendre la moyenne des ordonnées à l’origine car chaque graphique se chevauche avec les autres. Nouveau modèle :
Ok… Maintenant, c’est 1,6 million de dollars. Mais peut-être que c’est raisonnable puisqu’il s’agit d’une maison au bord de la mer. Le reste du modèle semble logique. Il suggère que
| Pour chaque augmentation de 1 unité en | le prix du logement change de |
|---|---|
| Superficie | 340 $ |
| Largeur du terrain | 3300 $ |
| Profondeur du terrain | 5840 $ |
| Distance de la côte | -8070 $ |
Bien que raisonnables, ce ne sont que des hypothèses basées sur des calculs approximatifs. Pour résoudre réellement notre problème, nous devons trouver le MLM optimal qui prédit les prix des maisons en fonction de ces 4 caractéristiques. En termes mathématiques, nous devons trouver les poids et le biais tels que
prédise les prix des maisons avec une erreur minimale.
MLM, en code
Comme dans la partie 1, définissons notre modèle. Puisque nous avons plus d’un poids, nous devons utiliser un tableau de w.
struct mlinear_model {
int num_weights;
double *w;
double b;
};
Maintenant, vous avez peut-être pu penser quelques étapes à l’avance et réaliser que si nous représentons w comme un vecteur et x comme un vecteur, nous pouvons représenter la sortie du modèle comme le produit scalaire de
et
, additionné avec
Ahh, beaucoup plus propre, non ? Eh bien, pas pour longtemps…
Je vais faire un autre saut ici et représenter notre vecteur de longueur comme une matrice .
Vous verrez bientôt comment cela est utile. Définissons notre structure matrix.
// matrix.h
// Juste pour que nous puissions remplacer `double` par n'importe quel type flottant plus tard
typedef double mfloat;
typedef struct {
// row major
mfloat *buf;
int rows;
int cols;
} matrix;
Maintenant, le modèle ressemble à
// main.c
struct mlinear_model {
matrix w;
mfloat b;
};
Remarquez que
num_weightsest maintenant stocké dans la matrice.
Puisque nous sommes passés aux matrices, nous devons utiliser le produit matriciel au lieu du produit scalaire vectoriel. J’ai ajouté quelques notes pour expliquer le code, au cas où vous auriez besoin de rafraîchir vos connaissances en algèbre linéaire.
// matrix.h
// Obtenir un élément dans la matrice
mfloat
matrix_get(matrix m, int row, int col)
{
return m.buf[row * m.cols + col];
}
// Définir un élément dans la matrice
void
matrix_set(matrix m, int row, int col, mfloat val)
{
m.buf[row * m.cols + col] = val;
}
// out = m1 dot m2
void
matrix_dot(matrix out, const matrix m1, const matrix m2)
{
// Sur la i-ème ligne de la première matrice
for (int row = 0; row < m1.rows; row++) {
// Sur la j-ème colonne de la deuxième matrice
for (int col = 0; col < m2.cols; col++) {
// Exécuter le produit scalaire vectoriel et mettre le résultat dans out[i][j]
mfloat sum = 0.0;
// m1.cols == m2.rows donc k itère sur tout
for (int k = 0; k < m1.cols; k++) {
mfloat x1 = matrix_get(m1, row, k);
mfloat x2 = matrix_get(m2, k, col);
sum += x1 * x2;
}
matrix_set(out, row, col, sum);
}
}
}
Tout d’abord, notons les propriétés suivantes concernant le produit matriciel entre les matrices et
- Il ne peut être calculé que si et seulement si
X.cols == W.rows - La matrice résultante a les dimensions (
X.rows,W.cols)
Dans ce cas,
Ainsi, doit être un vecteur ligne et doit être un vecteur colonne pour que nous puissions reproduire le comportement du produit scalaire vectoriel.
Vous pouvez considérer le produit matriciel comme prenant chaque colonne de la deuxième matrice, la retournant sur le côté, et la multipliant par la ligne de la première matrice.
Regardez le code et vérifiez que la multiplication ci-dessus retournera le résultat attendu.
Maintenant, nous avons les outils pour coder la prédiction.
// mlr.c
// x est un vecteur ligne, ou une matrice (1 x n)
mfloat predict(struct mlinear_model model, const matrix x) {
// (1 x n) . (n x 1) => (1 x 1) matrice, qui est un nombre
mfloat result[1][1] = {0.0};
matrix tmp = {.buf = result, .rows = 1, .cols = 1};
// Définir tmp comme le résultat
matrix_dot(tmp, x, model.w);
return tmp.buf[0] + model.b;
}
Optimisation du modèle
On nous donne 2 tableaux : les données d’entrée, qui sont formatées comme une matrice .
Chaque ligne représente un échantillon, et les valeurs dans chaque colonne sont les caractéristiques décrites ci-dessus.
Et les prix des maisons correspondants , en milliers de dollars
On nous donne 100 échantillons de données, donc
. Je vais les stocker dans data.h
// data.h
#define NUM_FEATURES 4
#define NUM_SAMPLES 100
static mfloat X_data[NUM_SAMPLES][NUM_FEATURES] = { /* données omises*/ };
static mfloat Y_data[NUM_SAMPLES] = { /* données omises */ };
static matrix X = {.buf = (mfloat *)X_data,
.rows = NUM_SAMPLES,
.cols = NUM_FEATURES};
static matrix Y = {
.buf = (mfloat *)Y_data, .rows = NUM_SAMPLES, .cols = 1};
Maintenant, c’est l’heure de l’optimisation !
Cela signifie que nous devons trouver le modèle avec les paramètres et tels que l’erreur sur tous les échantillons de données soit minimisée. Mais quelle est notre erreur ? Nous pouvons en fait utiliser la même définition que dans la partie 1, puisque nous comparons toujours deux nombres et .
C’est la moyenne de la somme des différences au carré, ou la différence moyenne au carré entre les valeurs attendues et réelles de . Plus cette valeur est proche de , meilleur est notre modèle. Réécrivons en termes de et
Ici, fait référence à la -ème ligne de . est le -ème échantillon multiplié par les poids. Après avoir ajouté le biais , nous obtenons , ou la prédiction.
En code :
// Retourne une matrice d'une seule ligne qui représente la ligne de m à i
matrix matrix_get_row(matrix m, int i) {
return (matrix){
.buf = m.buf + i * m.cols, .rows = 1, .cols = m.cols};
}
mfloat cost(struct mlinear_model *model, matrix X,
matrix Y) {
mfloat cost = 0.0;
for (int i = 0; i < X.rows; i++) {
matrix x_i = matrix_get_row(X, i);
mfloat f_wb = predict(model, x_i);
mfloat diff = matrix_get(Y, 0, i) - f_wb;
cost += diff * diff;
}
return cost / (2.0 * X.rows);
}
À quel point l’estimation était-elle bonne ?
Revenons à notre estimation et voyons comment elle se comporte.
int main() {
matrix W = matrix_new(X.cols, 1);
struct mlinear_model model = {.W = W, .b = 0.0};
printf("Coût du modèle zéro : %f\n", cost(&model, X, Y));
matrix_set(W, 0, 0, 0.34);
matrix_set(W, 1, 0, 3.3);
matrix_set(W, 2, 0, 5.84);
matrix_set(W, 3, 0, -8.07);
model.b = 1634.5;
printf("Coût du modèle d'estimation : %f\n",
cost(&model, X, Y));
}
Sortie :
Coût du modèle zéro : 3340683.781483
Coût du modèle d'estimation : 2641267.466911