2 février 202612 min de lecture

La programmation linéaire à la salle de sport

Je suis paresseux. Pas assez paresseux pour ne pas faire d’exercice, mais assez paresseux pour vouloir que mes programmes de sport se fassent tout seuls.

Par coïncidence, j’ai commencé à lire « Linear Algebra and its applications », et une section a attiré mon attention : comment appliquer l’algèbre linéaire à des problèmes du monde réel.

Il s’avère qu’il existe de nombreuses façons d’optimiser les processus qui vous entourent, si vous savez convertir votre problème de la vie réelle en un modèle mathématique.

L’objectif #

S’entraîner à la salle est une question d’équilibre. Trop peu d’intensité, et vous ne progressez pas autant que vous le souhaitez. Trop d’intensité, et vous vous fatiguez (et ne progressez pas autant que vous le souhaitez).

Quand vous créez vos programmes, vous devez avant tout prendre en compte vos objectifs. Cependant, la façon dont vous atteignez vos objectifs est un tout autre sujet :

  • Comment répartissez-vous vos entraînements : Push/Pull/Legs ; Lower/Upper ; Mixed
  • Combien de temps voulez-vous passer quotidiennement / hebdomadairement à la salle
  • Quelles priorités, pour quel groupe musculaire
  • Mon épaule gauche est-elle intacte cette semaine ?

Alors, et s’il existait un moyen de générer un programme efficacement, en prenant en compte tous ces facteurs ?

Qu’est-ce qu’un programme #

Avant de commencer, il y a quelques termes que je dois expliquer J’appelle (et la plupart des gens aussi) un programme un ensemble d’entraînements : des séries d’exercices, regroupées par jour

text
- Monday: Push 
	- Triceps:
		- Deltoid Anterior: 4 sets
		- Cable Rope Overhead Triceps extension: 4 sets
		- Triceps Pushdown: 4 sets
	- Deltoids:
		- Dumbbell Overhead Press: 4 sets
	- ...
- Tuesday: Pull
	- Back:
		- Cable row: 3 sets
	- Biceps:
		- Curl: 4 sets
	- ...
- ...

Recommencez jusqu’à satisfaction (vous ne l’êtes jamais.)

Vous voyez l’idée. Push/Pull/Leg sont des termes qui décrivent des entraînements plutôt « orientés push », « orientés pull », ou « legs ». Par exemple, une traction est un exercice pull (bien sûr), et une pompe est un exercice push (bien sûr).

Ces programmes sont généralement faits à la main, achetés ou téléchargés auprès d’un influenceur fitness. Mais ils sont rarement adaptés à vos besoins.

Avertissement #

Je ne suis ni mathématicien, ni chercheur en recherche opérationnelle. Je vais faire des erreurs qui vous feront saigner des yeux. C’est un petit projet personnel conçu pour explorer un nouveau concept que j’apprenais

Collecte de données #

Je vais faire court. J’ai créé un jeu de données d’environ 50 exercices. Chacun d’eux avait :

  • ID : entier
  • Nom : chaîne de caractères
  • Groupes musculaires :
    • Primaire : tableau d’entiers
    • Secondaire : tableau d’entiers
  • Est-ce un exercice push/pull ou legs ?

Nous avons l’entrée: données, conditions & cible.

Nous connaissons la sortie attendue: un programme d’entraînement.

Maintenant, comment créer la partie entre les deux ?

Programmation linéaire #

La programmation linéaire (LP) est un processus d’optimisation mathématique qui permet d’optimiser une fonction objectif linéaire (la maximiser/minimiser).

$$ f(x_1,x_2,x_3) = a_1x_1 + a_2x_2 + a_3x_3 $$

En « traduisant » votre problème en langage naturel « Je veux générer un programme d’entraînement qui correspond à mes contraintes » en un problème d’équations linéaires, vous pouvez utiliser un solveur pour approcher la meilleure solution possible.

En fin de compte, vous devrez vraiment trouver :

  • Une fonction objectif : la cible que vous voulez atteindre, par ex. maximiser le volume.
  • Des contraintes : par ex. je ne veux pas que le même exercice se répète deux fois sur \(x\) jours

Dans un cadre plus mathématique, voici comment elles sont exprimées.

$$ \begin{align*} & \text{Find a vector} && \mathbf{x} \\ & \text{that maximizes} && \mathbf{c}^\mathsf{T} \mathbf{x} \\ & \text{subject to} && A \mathbf{x} \le \mathbf{b} \\ & \text{and} && A \mathbf{x} \ge \mathbf{0} \\ \end{align*} $$

D’après Wikipedia

Définir les variables #

Parce que nous utilisons la programmation linéaire, nous sommes assez limités quant à l’apparence des fonctions (objectif/conditions).

$$ f(x_1,x_2,x_3) = a_1x_1 + a_2x_2 + a_3x_3 $$

En fait : nous ne pouvons définir que les poids \(a_n\), et le solveur trouvera les \(x_n\), si possible

[!tip] Cela signifie que c’est un problème impossible pour un solveur LP : \(f(x_1, x_2, x_3) = x_1x_2x_3\)

Mais nous devons définir ce que sont nos variables :

  • Quelles bornes \(x \in [-\infty; \infty]\) ou \(x \in [-5;10]\)
  • Entier/Binaire/Continu ?; est-ce un ou zéro (binaire) ; \(x\) est-il un entier, ou un flottant ?

Pour ce projet, j’ai utilisé deux variables de décision principales :

  • \(x,e,d\) : nombre de séries pour l’exercice \(e\) au jour \(d\) : entier
  • \(y,e,d\) : si l’exercice \(e\) est inclus au jour \(d\) : binaire

Contraintes #

Pour guider les algorithmes vers la solution la plus satisfaisante, j’ai dû implémenter quelques contraintes.

Les contraintes sont des (in)égalités linéaires. Voici des contraintes valides :

$$ x_1a_1 \geq 0 \\ x_1 + x_2 = 1 $$

Ceci est invalide :

$$ x_1x_2x_3 \geq 0 $$

Commençons :

Séries min & séries max #

C’est le plus simple. Je ne veux pas d’un jour avec 230 séries de développé couché.

Pour la poser comme contrainte, pour \(N\) exercices sur \(6\) jours :

$$ \sum^{N}_{e=0} \sum^{6}_{d=0} x_{d,e} \leq 5 * y_{d,e} \\\ \sum^{N}_{e=0} \sum^{6}_{d=0} x_{d,e} \geq 3 * y_{d,e} $$

« Pour chaque exercice, de chaque jour, je veux que le nombre de séries soit inférieur ou égal à 5 si l’exercice est sélectionné, ou 0 si ce n’est pas le cas »

« Pour chaque exercice, de chaque jour, je veux que le nombre de séries soit supérieur ou égal à 3 si l’exercice est sélectionné, ou 0 si ce n’est pas le cas »

Nombre min/max d’exercices #

Assez simple, aussi.

Pour \(N\) exercices sur \(6\) jours :

$$ \sum^{N}_{e=0} \sum^{6}_{d=0} y_{d,e} \leq 6 * y_{d,e} $$$$ \sum^{N}_{e=0} \sum^{6}_{d=0} y_{d,e} \geq 3 * y_{d,e} $$

Contribution musculaire #

Maintenant, étant donné mes objectifs, je ne veux pas entraîner les poignets autant que les abdos, par exemple, ou je veux garder les legdays à un confortable 1 fois par an semaine.

Rappel du jeu de données : chaque exercice a des groupes musculaires primaires et secondaires.

J’ai défini un poids (\(p_{e,m}\)) de \(1\), si l’exercice \(e\) est dans le groupe \(m\) en tant que primaire, ou \(0.25\) si secondaire. \(0\), bien sûr, si non inclus

Disons qu’il y a \(M \in \mathbb{N}\) groupes musculaires

$$ c_m = \sum^{N}_{e=0} \sum^{6}_{d=0} x_{d,e} * p_{e,m} $$

Et ce faisant, nous créons une contrainte qui définit une nouvelle variable. Et c’est la chose la plus importante à retenir de cet article. Vous pouvez construire des systèmes élaborés, en utilisant des contraintes pour définir des variables à partir de la valeur d’autres variables.

Voir la section variables auxiliaires

Objectifs de groupe musculaire #

Maintenant que nous avons nos variables de contribution musculaire (\(c_m\) avec \(m\) le groupe musculaire), nous pouvons définir des « cibles » quant à la quantité d’utilisation de chaque groupe musculaire. Je veux des abdos, pas des mollets énormes.

J’ai abordé ce problème à travers 2 autres vecteurs de variables qui représenteront à quel point une solution est au-dessus ou en-dessous de la cible.

Définissons deux variables supplémentaires : \(a_m\) représentant de combien un groupe musculaire a été programmé au-dessus de la cible, et \(b_m\) de combien un groupe musculaire est en-dessous de la cible.

$$ c_m + a_m - b_m = \text{target} $$

[!TIP] Cela ne peut fonctionner que si vous déclarez les bornes de vos variables comme des entiers positifs. Si vous autorisez \(a_m\) ou \(b_m\) à devenir négatifs, ils perdront tout le sens créé par la forme \(c_m + a_m - b_m \).

Facteur de lissage #

Enfin et surtout, je voulais lisser les entraînements : pas de gros jour avec 20+ exercices, mais 6 jours moyens.

Si nous copions l’« astuce » des objectifs de groupe musculaire, nous pouvons définir 4 variables supplémentaires :

  • \(t_d\) : pour le total de séries par jour
  • \(T\) : total général : \(\sum^{6}_{d=0}t_d\)
  • \(o_d\) : jours au-dessus de la moyenne
  • \(n_d\) : jours en-dessous de la moyenne

Et ajoutons une nouvelle contrainte

$$ \sum^{6}_{d=0} t_d - T = o_d - n_d $$

N’oubliez pas les bornes pour \(o_d\), et \(n_d\), les deux doivent être des entiers positifs.

Fonction objectif #

Nous avons maintenant deux ensembles de contraintes. Je n’ai pas trouvé de terme direct pour les nommer, j’utiliserai mes propres mots :

  • Contraintes de solution : elles coupent les régions indésirables de solutions
  • Contraintes auxiliaires : elles me permettent de créer des variables basées sur les valeurs d’autres variables

Les contraintes de solution sont nécessaires car certaines solutions sont inacceptables. Des jours vides, des jours avec 20+ entraînements

Mais nous devons aussi définir ce que le système doit trouver. Quoi optimiser.

Rappelez-vous à quoi ressemblent les fonctions linéaires. Nous devons fournir à notre solveur une seule fonction qui ressemble à ceci. Avec \(x\) un vecteur de variables, ici, ils pourraient être un mélange de \(o_d\), \(y_{e_d}\), etc.)

$$ f(x_1,x_2,x_3) = a_1x_1 + a_2x_2 + a_3x_3 $$

Bien sûr, vous pouvez vous attendre à ce qu’une véritable fonction objectif soit beaucoup plus grande. Dans un cadre professionnel, j’ai vu des systèmes avec des centaines de milliers de variables.

Je vais construire ma fonction objectif partie par partie. J’ai choisi de minimiser la fonction, mais vous pouvez faire l’inverse. Tout dépend de votre problème spécifique à résoudre.

Cibles #

C’est l’objectif principal : atteindre les cibles

Nous allons définir une pénalité pour être au-dessus/en-dessous du nombre cible de séries pour un groupe musculaire.

En-dessous :

$$ 10 \sum^{M}_{m=0} b_m $$

Au-dessus :

$$ 1 \sum^{M}_{m=0} a_m $$

À mon avis personnel, être au-dessus est bien moins indésirable qu’être en-dessous

Compacité #

Un autre objectif peut être de guider la solution vers une forme plus compacte.

$$ 0.05\sum^{6}_{d=0} \sum^{N}_{e=0} x_{d,e} \\\ 0.01\sum^{6}_{d=0} \sum^{N}_{e=0} y_{d,e} $$

[!note] J’ai des sentiments mitigés à propos de cette partie de la fonction objectif, car elle tend à prioriser les exercices qui touchent plusieurs groupes musculaires.

Lissage #

Et enfin, nous pouvons créer un objectif de lissage, en utilisant les variables définies avec les contraintes

$$ 0.01\sum^{6}_{d=0} o_d + n_d $$

Chaque jour qui est au-dessus/en-dessous des autres s’ajoutera à la fonction objectif (qui doit être minimisée), la rendant ainsi légèrement moins susceptible d’être la solution optimale

Résultats #

La résolution a pris environ 3 secondes. C’est plutôt rapide.

text
Day 1
- Front Raise (lyfta_id=133): 3 sets
- Hammer Curl (lyfta_id=134): 3 sets
- Lever Seated Reverse Fly (lyfta_id=223): 3 sets
- Straight Back Seated Row (lyfta_id=105): 3 sets
Day 2
- 45 Degrees HyperExtension (lyfta_id=2328): 3 sets
- Cable Kneeling Crunch (lyfta_id=1626): 3 sets
- Lever Preacher Curl (lyfta_id=217): 3 sets
- Straight Back Seated Row (lyfta_id=105): 3 sets
Day 3
- 45 Degrees HyperExtension (lyfta_id=2328): 3 sets
- Hammer Curl (lyfta_id=134): 3 sets
- Lever Seated Reverse Fly (lyfta_id=223): 3 sets
- Seated Shoulder Press (lyfta_id=176): 4 sets
Day 4
- 45 Degrees HyperExtension (lyfta_id=2328): 3 sets
- Front Raise (lyfta_id=133): 3 sets
- Lever Preacher Curl (lyfta_id=217): 3 sets
- Straight Back Seated Row (lyfta_id=105): 3 sets
Day 5
- 45 Degrees HyperExtension (lyfta_id=2328): 3 sets
- Hammer Curl (lyfta_id=134): 3 sets
- Lever Preacher Curl (lyfta_id=217): 3 sets
- Straight Back Seated Row (lyfta_id=105): 3 sets
Day 6
- 45 Degrees HyperExtension (lyfta_id=2328): 3 sets
- Hammer Curl (lyfta_id=134): 3 sets
- Lever Seated Reverse Fly (lyfta_id=223): 3 sets
- Straight Back Seated Row (lyfta_id=105): 3 sets
Day 7
- Hammer Curl (lyfta_id=134): 3 sets
- Lever Leg Extension (lyfta_id=212): 4 sets
- Straight Back Seated Row (lyfta_id=105): 3 sets
- Triceps Pushdown (lyfta_id=107): 3 sets
Muscle targets (effective sets):
- Biceps Brachii: 7.2 / 8.0
- Deltoid, Lateral: 5.7 / 10.0
- Hamstrings: 4.5 / 8.0
- Latissimus Dorsi: 5.4 / 10.0
- Pectoralis Major, Sternal: 5.4 / 12.0
- Quadriceps: 4.0 / 10.0
- Rectus Abdominis: 3.0 / 6.0

Variables auxiliaires #

LP n’est pas flexible. Rappelez-vous que les équations doivent être linéaires :

$$ f(x_1,x_2,x_3) = a_1x_1 + a_2x_2 + a_3x_3 $$

Comment ajouteriez-vous des contraintes telles que : deux jours Push, deux jours Pull, deux jours Leg, par semaine.

Vous ne le pourriez pas, en utilisant seulement des contraintes, ou des fonctions objectif.

Ce problème est résoluble en deux étapes :

  • Créer 3 variables, pour chaque jour : est-ce push, est-ce pull, est-ce leg. Ces variables sont des sommes de \(y_{d_e}\) pour le jour, et pour les valeurs de \(e\) liées aux exercices qui sont push/pull/legs. Nous les appellerons \(s_d\), \(e_d\) et \(l_d\)
  • Créer 3 nouvelles contraintes :
    • \(\sum^{6}_{d=0}\ s_d \geq 2\) : nous voulons au moins deux jours de push
    • \(\sum^{6}_{d=0}\ v_d \geq 2\) : nous voulons au moins deux jours de pull
    • \(\sum^{6}_{d=0}\ l_d \geq 2\) : nous voulons au moins deux jours de legs :(
  • Il nous faut aussi une nouvelle contrainte par jour : chaque jour peut être soit Push/Pull/Leg, pas 2 ou 3 à la fois : \(s_d + v_d + l_d = 1\)

C’est une utilisation directe des variables auxiliaires.

À quoi sert LP ? #

Au fil de mes lectures en ligne, j’ai vu que LP pouvait être utilisé pour modéliser BEAUCOUP de problèmes du monde réel :

  • Optimisation de la production
  • Planification du trafic et des routes
  • Planification d’itinéraires

C’est un sujet impressionnant, et il y a des ressources partout.

J’ai vu des produits « IA » que je soupçonne d’être un problème LP déguisé, ou du moins, un LLM qui pourrait être remplacé par un problème LP approprié.

Outillage #

Par commodité, j’ai utilisé un Jupyter Notebook dans un conteneur Docker. J’ai adopté les conteneurs docker jetables dans mon flux de travail depuis quelques semaines maintenant, et l’idée derrière est d’éviter d’avoir à réinstaller Linux tous les mois (j’utilise Arch btw.)

L’implémentation est faite avec Python 3.13, et PuLP

Méthodologie #

Quant à la méthodologie, c’est principalement de l’essai-erreur. Pas de grand plan, pas de connaissances complexes, juste essayer des choses. J’ai aussi demandé à ChatGPT de m’expliquer certains concepts, et de suggérer des contraintes.

[!quote] Tu peux juste faire des choses™

Quelqu’un au hasard sur , probablement

Ce qui est drôle avec LP, c’est qu’une matrice est capable de trouver des failles dans votre raisonnement. Et de vous les faire payer (en temps CPU)

  • Vous vouliez 30 exercices. Tenez, voici un lundi avec 24 exercices et un mardi avec 6.
  • Oui, vous vouliez cibler les legs. Saviez-vous que vous pouviez tout simplement faire 300 séries de leg press le lundi ?

Remerciements #

  • Ma professeure de maths Mme B, à Adimaker Lille, pour m’avoir transmis son savoir autant qu’elle a pu (et transmis sa passion pour l’enseignement !)
  • Paul, pour la relecture.
  • ChatGPT, pour quelques explications, et conseils

Plus d'articles

Comment utiliser PGO et LTO avec Meson et Clang

Un guide pas à pas testé pour activer le Profile Guided Optimization et le Link Time Optimization dans un projet Meson compilé avec Clang, y compris l'étape de fusion llvm-profdata que la plupart des guides oublient.

5 min

Partie 2 : Profiling, PGO & LTO

Mesurer PGO et LTO sur une simulation SPH en C++ avec perf et des flamegraphs, et pourquoi ils n'ont presque rien apporté.

8 min