Mes très chers confrères adorés,
Comme vous le savez si vous lisez mes excellents articles d'opinion, tous géniaux (hum !), cela fait quelques années maintenant que je travaille pour des toxicologues.
Alors, c'est bien, j'ai appris plein de choses, au point de me définir aujourd'hui comme « computational toxicologist ». Mais j'ai surtout mis mon nez dans plein de trucs qui peuvent paraître insignifiants comme ça, mais qui peuvent vite vous rendre complètement chèvre meilleur dans votre travail si vous vous y intéressez un peu.
Aujourd'hui, j'aimerais parler de science reproductible, et surtout de reproductibilité des résultats en fonction de différents langages de programmation. Au hasard, R, Python et un peu Matlab hein, faisons simple.
Pourquoi est-ce intéressant de se poser ce genre de questions, si ce n'est pour la beauté du code ? Soyons sincères, je me fiche de la beauté du code. Donc si j'ai fini par faire ce genre de choses, c'est que derrière il y avait une bonne raison. Voyons cela ensemble.
Un peu de contexte, histoire de râler sur mes anciens collègues avant de râler sur les nouveaux
Contexte de l’anecdote : Pendant ma thèse, et même avant, mon directeur de thèse adoré m'a fait un cadeau dont rêve tout bioinformaticien qui se respecte : une licence MATLAB. Combien d'entre vous utilisent MATLAB déjà ? Hum, pas beaucoup. Et pourtant, je vais être sincère, pour faire de jolies visualisations de cartes chromosomiques, bah, c'était fort pratique.
Sauf que voilà, quand on aime faire des chromosomes en 3D, on est content, on a nos codes MATLAB, ça tourne. Mais vu que le bionfo de base ne code pas en MATLAB, un jour, pendant l'été, tous les stagiaires du couloir s'y sont mis et on a recodé toutes nos fonctions préférées en Python, histoire de pouvoir rester dans notre langage préféré.
L’exemple concret : Maintenant, rigolons un peu et racontons mes erreurs de débutant. Pour comparer deux cartes de contacts chromosomiques entre elles, il existe une méthode connue qui consiste à faire le ratio logarithmique des deux matrices après normalisation. Pendant que je fabriquais des figures pour un papier, j'avais mes deux implémentations python et matlab de toutes les méthodes de normalisation et il m'arrivait régulièrement de faire tout mes traitements en python pour ensuite visualiser en matlab. Pourquoi ? Tout simplement car j'arrivais à produire des figures plus belles. Voici les deux implémentations de la normalisation de la SCN que nous avions.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
%Version matlab function [DataFN] = SCN_sumV2(DataF) DataFN = spdiags(spfun(@(x) 1./x,sum(DataF,2)),0,size(DataF,1),size(DataF,1))*DataF ; DataFN = spdiags(spfun(@(x) 1./x,sum(DataFN',2)),0,size(DataFN',1),size(DataFN',1))*DataFN'; DataFN = spdiags(spfun(@(x) 1./x,sum(DataFN',2)),0,size(DataFN',1),size(DataFN',1))*DataFN'; DataFN = spdiags(spfun(@(x) 1./x,sum(DataFN',2)),0,size(DataFN',1),size(DataFN',1))*DataFN'; DataFN = spdiags(spfun(@(x) 1./x,sum(DataFN',2)),0,size(DataFN',1),size(DataFN',1))*DataFN'; DataFN = spdiags(spfun(@(x) 1./x,sum(DataFN',2)),0,size(DataFN',1),size(DataFN',1))*DataFN'; %Remove nan and infs DataFN(isnan(DataFN))=0 ; DataFN(isinf(DataFN))=0 ; %RE-SYMETRIZED DataFN=(DataFN+DataFN')/2 ; end |
1 2 3 4 5 6 7 8 |
#Version python def SCN(D, max_iter = 100, mean = True): # Iteration over max_iter for i in range(max_iter): D /= np.maximum(1, D.sum(axis = 0)) D /= np.maximum(1, D.sum(axis = 1)[:, None]) # To make matrix symetric again return (D + D.T)/2 |

Alors oui, si on demande à n'importe quel interpréteur de code (GPT ou autre), ces codes font la même chose dans l'esprit. Ils font une normalisation itérative des données mises en entrée. Mais admettons qu'un bidouilleur prenne une matrice, la normalise en Python d'un côté, en MATLAB de l'autre, et compare les deux, le résultat sera-t-il identique ?
Pas du tout, il y aura des différences car le nombre total d'interactions dans une matrice ne sera pas strictement égal à son équivalent dans un autre langage, surtout vu la différence massive d'implémentation. En déroulant le même traitement à mes données, pas de soucis. Avec une fonction qui, ici, a des légères différences et en prenant des résultats d'un langage pour les comparer à un autre, j'aurais fourni un résultat faux à mes collègues. Il faut alors imaginer cette figure pleine de bleu et de rouge car, en log10, une infime différence devient catastrophique.
Pour pouvoir être comparées entre elles, les matrices doivent être traitées par les mêmes outils. Ici, la différence est flagrante car le code n'est pas écrit de la même manière. Il a cependant été ma première prise de conscience de l'impact du langage et de l'implémentation sur un résultat possible. Les exemples suivants ont vraiment commencé à me griller les neurone à me rendre rigoureux.
Bonne chance si vous voulez le même random forest en R et en Python
Warning, cette partie est grandement, très grandement basée sur l'article en open access que je soumettrai un jour quelque part s'il est accepté si j'ai le temps de mettre en œuvre les efforts nécessaires pour le rendre acceptable à tous.
Contexte de l’anecdote : Les années passent, les cheveux se perdent, et pourtant on espère être meilleur chaque jour. Allez, me voilà au début de mon travail actuel, et je découvre que le poste que j'occupe était occupé par quelqu'un avant moi. Une personne brillante et bien organisée qui m'a laissé une myriade d'archives de tout ce qu'elle a fait… en R.
Dans cette myriade de sujet il y en avait un inachevé d’une dizaines de ligne… Allez, je recode en Python, histoire de me faire la main et de vérifier que j'ai bien compris tout ce que mon collègue avait fait. En voulant reproduire à l'identique ses résultats, je ne cherche pas à faire le code le plus optimal, mais simplement à m'assurer que j'ai compris chaque infime détail des travaux précédents.
L’exemple concret : Là, on arrive sur un problème que je caricaturerais grossièrement par : « Tu prends ces produits cosmetiques (shampoings et autre), tu envoies toutes les données sur ces produits cosmetiques dans un random forest que tu entraînes sur le type d'irritation définis en laboratoire avec des tubes a essais » (cf. le papier pour ceux qui veulent le détail). Là, je me dis, OK, facile, gloire à scikit-learn ! Je lance le code, paramètres par défaut, sur les données archivées. Et là, les résultats en R et en Python sont différents. Pas de beaucoup, juste quelques pourcentages, mais tout de même pas STRICTEMENT identiques sur quelques milliers de valeurs.

Alors oui, on est sur du machine learning, qui repose sur une myriade d'éléments aléatoires. Donc vous allez me dire : « C'est normal, arrête de te prendre la tête, c'est peut-être la graine aléatoire. » Mais en fait, je peux relancer son script, les résultats sont stables ; je relance le mien, les résultats sont stables aussi, avec toujours cette petite différence de rien du tout. Je tente de faire converger les graines aléatoires, toujours des différences donc les implémentations ont des différences qui se tiennent.
Sauf que je bosse pour des toxicologues. Donc si vous leur dites droit dans les yeux : « Bon alors, j'ai un modèle qui te dit que ce produit n'est pas irritant, donc tu peux valider, il y aura aucun souci. Quand j'ai reproduit le code, j’ai une conclusion d’irritation, mais t'inquiète, c'est juste une graine aléatoire dans mon modèle, on s'en fiche… », quelle va être la réaction de ces personnes ? Ces personnes qui ont pour responsabilité de ne pas mettre de produit irritant sur le marché, sans quoi les conséquences seraient terribles ? Vous pensez qu'ils vont aimer ma preuve de concept et valider mon jouet avec un discours pareil ? Bah vous creusez, vous creusez encore, en y passant une semaine, puis deux, et là vous vous dites : « Bon, il y a un vrai os, j'ai testé plein de trucs de manière plus ou moins intelligente, je ne trouve pas. »
On creuse tout, les effets aléatoires, les implémentations sur lesquelles les random forests se basent, et là vous comprenez qu'il y a plein de petites subtilités. Primo, si vous ne voulez pas vous prendre la tête, vous remarquez que R et Python se basent tous les deux sur la même bibliothèque C++ pour faire un random forest si on passe par ranger.

En fait, même si les implémentations en C++ sont les mêmes, il y a quelques subtilités dans les librairies dans leur manière de l'appeler et de préparer les données. Il suffit de quelques différences subtiles de paramètres par défaut pour donner cette variation. La conclusion majeure est que pour que les différentes implémentations fournissent des résultats similaires, il faut bien optimiser le paramètre min_node_size (min_sample_split en scikit-learn, nombre minimum d’échantillons pour découper des nœuds). Si on met ce paramètre à 1 on commencera à avoir des arbres identiques mais il peut rester quelques arbres récalcitrants.
Pour aller plus loin dans la reproductibilité, il est nécessaire de fixer l’aléatoire de la même manière entre les deux implémentations. Il se trouve que les versions scikit-learn et R de l'algorithme ranger ne construise pas des arbres de la même manière. SKranger crée des arbres de probabilité basés sur l’argument max de la probabilité d’appartenance à une classe. Alors que l'implémentation en R repose sur un système de vote majoritaire. Dans la majorité des cas les prédictions seront les mêmes mais cela peut différer de quelques pourcentage (897 prédictions identiques sur 920 dans nos tests). La seule astuce qu’on a trouvée pour rendre les deux identiques était de demander au code R de ne prendre que la classe qui a la probabilité la plus forte, comme ceci :
1 2 3 4 5 6 |
pred_proba = predict(RF, X_test,predict_all=TRUE)$predictions pred_classe = numeric(length(pred_proba)) for (i in 1 :length(pred_proba)) { pred[i] = which.max(pred_proba[[i]]) } |
Avec ce code R et en ayant fixé les autres hyperparamètres, les deux codes ont le mêmes comportement de bout en bout ! A-t-on essayé de rendre fixe, stable et identique un algorithme qui contient le mot hasard dans son titre ? Franchement, oui, mais c'était fortement instructif et de toute manière j'aurais trouvé un toxicologue pour me poser ces questions si je ne l'avais pas fait donc… Exemple suivant !
Coucou ANOVA, dis-moi, tu n'es pas exactement pensée pareille entre R et Python, non ?
Bon, si vous faites partie des courageux qui ont encore envie de me lire en dépit de ma prose chaotique, vous vous doutez qu'après les random forests, je ne pouvais pas en rester là. Mais que s'est-il passé dans ma fabuleuse vie cette fois-ci, hein ?
Contexte de l’anecdote : Un jour, une personne qu'on appellera Élise, pour lui donner un nom fictif, est venue me voir avec un truc qui avait l'air tout simple et si facile : « Bon alors, on avait mis en place ce protocole là avec un de tes collègues il y a dix ans ; on aimerait voir si avec l'historique accumulé on aurais pas intérêt à changer de stratégie. On prend nos données, on fait une ANOVA dessus, on applique une correction de Tukey. Ensuite, on récupère la matrice des comparaisons par paires pour voir si nos scores sont meilleurs ou non que notre cible en fonction de son groupe. Bien que cette stratégie soit toujours parfaitement valide, penses-tu qu'il existe d'autres alternatives intéressantes pour nos usages quotidiens ? Ça devrait te prendre une seule journée, non ? » Ça sent le piège, hein ? Bingo.
Collègue différent, problème identique, un script en R, je suis un papy grincheux avec ses habitudes. Je commence à me dire que faire le double contrôle dans un langage différent est parfois pratique sur ces problèmes simples. Donc c'est parti, je passe deux heures à écrire le truc en Python. Mais… mais… mais… attends, je n'arrive pas à faire exactement les choses dans le même ordre logique. Pourquoi ?
L’exemple concret : Python, au moment ou j’ai fait mes tests (novembre 2024), n'était pas rigoureusement identique à R dans la manière dont l'ANOVA est implémentée. Les deux ANOVA marchent et sont reproductibles, mais avoir les mêmes résultats entre les deux langages n'est pas toujours évident. Ici mon souci était l'implémentation de la fonction de Tukey. Le code de la correction de Tukey en R me retournait l'ensemble des groupes similaires les uns aux autres là où la correction de Tukey en Python ne me retourne que des paires de combinaisons, sans les groupes.
On peut trouver quelques posts en ligne sur le sujet si on creuse bien mais en toute franchise quand j'ai découvert tout ça on voyait bien que ça n'est pas si grave… Il me restait juste à recoder en R avec les mêmes packages que l'original si je voulais une copie 100 % identique, ou à créer les fonctions manquantes en Python, ou à me servir de ce contexte comparatif comme base pour toutes les nouvelles questions posées. Connaissant votre humble serviteur, je vous laisse deviner quelle option j'ai choisie.
La reproductibilité doit-elle aller aussi loin ?
En écrivant cet article, j'ai eu un léger sentiment de faire une petite publicité pour certaines de mes bafouilles littéraires. En toute franchise, j'espère que le lecteur moyen saura comprendre que ce n'était pas le but, mais bien de voir que certains métiers peuvent amener à se poser des questions de précision qui, en fait, finissent par tous nous aider.
L'étude de la reproductibilité entre langages de programmation est un monde truffé de questions ignobles et chronophages passionnantes. Nous sommes tous de plus en plus sensibilisés à l'importance d'être capables de reproduire nos résultats dans un même langage et avec un même script. Atteindre ce niveau de maîtrise permet de comprendre finement, dans les moindres rouages, tout ce que nous faisons au quotidien et l'impact de chaque virgule de code.
En toute franchise, je ne suis pas certain que la majeure partie d'entre vous ait besoin d'aller à ce niveau de reproductibilité quand vous parlez de science reproductible. Dans votre quotidien, faire des tests unitaires est la chose la plus importante et, déjà, une garantie plus que suffisante pour avancer sereinement. Ensuite nous pourrions également parler de toutes les technologies pour avoir un environnement reproductible mais ce n’étais pas le but du billet du jour.
Testez vos codes, faites valider les résultats sur chaque fonction du code autant que possible, et déjà là vous aurez un niveau de rigueur suffisant. En toute franchise, j'utilise maintenant le double contrôle dans un langage différent uniquement quand j'ai le sentiment que ça m'apportera une utilité précise : de la compréhension. Le changement de langage me permet de m'assurer qu'il n'y a aucun écart, même infime, entre ce qu'exprime un besoin d'un biologiste et la façon dont je l'écrirais. C'est un peu le test ultime, à mon sens : dire deux fois la même chose dans deux langages différents, et si vous voyez que vous avez obtenu exactement le même résultat, vous êtes sûr que votre première copie est la bonne, à condition de l'avoir testée. Dans le cadre d'un double contrôle qui se veut efficace, rester dans le même langage et le même environnement pour voir si la conclusion est la même à la fin est un préliminaire capital à une base de test solide et robuste. Ça évite de se poser ces questions d'implémentation et de devoir vérifier un niveau de détails pas toujours utile au quotidien.
Mais si vous êtes un étudiant de master qui aime réfléchir à des questions qui ennuient la plupart des gens, postulez à mes offres de stage ! La cantine est fabuleuse !
Allez, j'ai vraiment écrit beaucoup trop comme je pensais dans cet article, comme si j'étais en pause-café. Ciao les gens !
Merci aux relecteurs d'avoir, échangé, subi ce billet : Évoluscope, Bonob, Jacques Dainat, Azerin, Lee Mariault.
Laisser un commentaire