Classe générique pour des réseaux de neurones en C++

Templates, héritages, std::function, constructeur curryfié en C++ ?

L'auteur de ce sujet a trouvé une solution à son problème.
Staff
Auteur du sujet

Salut tout le monde !

Problème

Le titre n'est pas très clair alors je vais essayer de détailler.
Je suis actuellement en train de coder des réseaux de neurones artificiels, en tant qu'exercice pour apprendre à m'en servir. Je vais donc tout coder avec la bibliothèque standard du C++, pas question pour moi d'utiliser une bibliothèque spécialisée.

Ce n'est pas juste un perceptron multicouche, j'essaie de faire du deep learning. J'ai donc besoin de pouvoir créer facilement des réseaux avec plusieurs couches de profondeur.
Il existe une bibliothèque en Python appelée Theano permettant de faire ça, et on créait un réseau de neurones de la façon suivante :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
>>> net = Network([
        ConvPoolLayer(image_shape=(mini_batch_size, 1, 28, 28), 
                      filter_shape=(20, 1, 5, 5), 
                      poolsize=(2, 2)),
        ConvPoolLayer(image_shape=(mini_batch_size, 20, 12, 12), 
                      filter_shape=(40, 20, 5, 5), 
                      poolsize=(2, 2)),
        FullyConnectedLayer(n_in=40*4*4, n_out=100),
        SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)
>>> net.SGD(training_data, 60, mini_batch_size, 0.1, 
            validation_data, test_data)        

Source : http://neuralnetworksanddeeplearning.com/chap6.html

J'essaie donc de faire quelque chose de similaire en C++ (et je ne compte pas changer de langage, c'est vraiment un exercice et je garde mes outils). Je me demandais donc comment implémenter ça.

C'est donc plus un problème de conception qu'un vrai problème de réseaux de neurones.

Code de départ

J'ai cette classe pour l'instant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class NeuralNetwork {

public:

    NeuralNetwork(const std::vector<Layer>&);

    void feedForward();
    void backPropagation();

private:

    std::vector<std::unique_ptr<Layer>> layers;
};

Et pour l'instant un Layer c'est ça :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
typedef std::function<double (double)> Neuron;

class Layer {

public:

    Layer(unsigned int, unsigned int, const Neuron&);
    std::vector<double> feedForward(const std::vector<double>&);
    std::vector<double> backPropagation(const std::vector<double>&);

private:

    std::vector<double> a_in;
    std::vector<double> z;
};

Les méthodes feedForward() et backPropagation() de NeuralNetwork appellent simplement les méthodes de même nom sur chaque layer, dans l'ordre. Elles sont spécifiques pour chaque type de layer.
Là plusieurs solutions possibles.

Solution 1 : héritage

Là je rend les méthodes feedForward() et backPropagation() virtuelles pures, je fais dériver les classes ConvPoolLayer, FullyConnectedLayer et SoftmaxLayer de Layer et je ré-implémente les méthodes feedForward() et backPropagation().

J'aime pas trop cette solution. Déjà elle m'oblige à utiliser un vector de pointeurs plutôt qu'un tableau d'objets dans NeuralNetwork (pour profiter du polymorphisme) ce qui n'est pas génial. Ensuite créer une nouvelle classe à chaque nouveau type de Layer me paraît lourd.

Qu'en pensez-vous ?

Solution 2 : std::function

Je l'a trouve plus séduisante car elle correspond plus naturellement à ma façon de penser mais plus difficile à mettre en œuvre. Là le constructeur de Layer prend en paramètre deux std::function<> qui encapsulent le comportement de feedForward() et backPropagation(). Et donc les méthodes feedForward() et backPropagation() se content d'appeler l'objet std::function<> stocké en interne.

L'objectif serait de pouvoir créer mes layers spécifiques comme ça :

1
2
3
// Notez que le constructeur utilise ici n'est pas celui du code donne plus haut  
Layer ConvPoolLayer = Layer(ConvPoolLayerFeedForward, ConvPoolLayerBackPropagation);
Layer FullyConnectedLayer = Layer(FullyConnectedLayerFeedForward, FullyConnectedLayerBackPropagation);

Est-ce une bonne idée ?

Néanmoins on voit ici un gros problème. Chaque type de Layer possède un constructeur différent (vous pouvez le voir sur le code Python) parce qu'il fonctionne de manière différente des autres, et donc les natures des données qui servent à l'initialiser ne sont pas les mêmes.

Donc je me demandais si en C++ il était possible de faire quelque chose qui ressemblerait à la curryfication dans les langages fonctionnels (application partiel d'arguments).

Je m'explique : j'initialiserais partiellement mon objet avec les fonction correspondant à backPropagation et feedForward (ce qui correspondrait à des politiques différentes).
À ce stade j'aurais des objets génériques partiellement initialisés du type ConvPoolLayer ou FullyConnectedLayer, et je pourrais compléter leur instanciation en les appelant avec des paramètres spécifiques (nombre de neurones d'entré, de sortie… ).

Naïvement comme ça, je me doute que ce n'est pas possible. Néanmoins serait-il possible de simuler ce principe ?

Il s'agit juste de trouver une alternative à l'héritage pour redéfinir les méthodes pour les prendre en paramètre. Pensez-vous que ça puisse être une bonne idée ? Je trouve ça conceptuellement plus logique (mais bon c'est peut-être débile) mais dur à mettre en œuvre dans un langage non fonctionnel.

Notez que je bosse sous VS2012 qui supporte seulement partiellement C++11.

+0 -0

Je tenterais des choses avec une fonction variadique aux template variadique genre :

1
2
template<typename RETOUR, typename TYPE, typename TYPE_2...>  
RETOUR fonction(TYPE arg, TYPE_2 next...)  

Mais vu que je viens d'apprendre ce qu'est la curryfication je peux pas aller beaucoup plus loin dans ma réponse. Je repasse quand j'aurais eu du temps a essayé des trucs, ça m'intéresse.

Édité par leroivi

Ce n'est pas parce qu'ils sont nombreux à avoir tort qu'ils ont raison - Coluche

+0 -0

Cette réponse a aidé l'auteur du sujet

Lu'!

@leroivi : le problème n'est pas ici en fait. Là tu vas te heurter au fait que si tes fonctions ont un nombre d'arguments différents, au moment du typage tu seras marron. Pour les fonctions, c'est pas la solution, ça le serait plutôt pour le constructeur, mais on y reviendra.

@Algue-Rythme : La deuxième solution est très largement la plus flexible. Et elle te permettra également de te débarrasser de l'indirection sur les unique_ptr qui n'est, je pense, pas utile.

Notez que je bosse sous VS2012 qui supporte seulement partiellement C++11.

Algue-Rythme

Pas moyen de passer sur VS Community ? Ce serait mieux pour ta vie. Les oiseaux chanteraient mieux dans ton jardin et ton frigo serait plus souvent plein.

Tu l'as dit un genre de curryfication serait plus pratique. Et tu l'as dit, tu as envie d'application partielle. La bonne nouvelle, c'est que les lambdas permettent ça sans une trop grosse prise de tête mis à part que tu vas devoir nommer les éléments, contrairement à un langage fonctionnel.

Il y a un aspect en revanche que tu n'évoques pas : deux types de layers sont initialisés avec des données différentes : OK. Mais l'état interne de ces deux types est-il, lui, différent ?

Si c'est le cas, tu es marron pour la version avec fonction. Tu vas devoir bosser avec du polymorphisme. Ou alors, il faut que ton état interne soit derrière une indirection qui efface le type et que les fonctions d'usage restaurent ce type par cast mais c'est hyper casse gueule.

Sinon, si c'est juste le constructeur et la manière de construire l'état interne qui diffèrent, c'est simple :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Layer{
public:
  template<class F, class P>
  Layer(F f, P p, InitState sb) : a_in{sb.a_in}, z{sb.z} {}

  std::vector<double> feedForward(const std::vector<double>&);
  std::vector<double> backPropagation(const std::vector<double>&);

private:
  std::vector<double> a_in;
  std::vector<double> z;
};

Layer some_builder(/*params*/){
  return Layer{foo, bar, init_for_foo_bar(/*params*/)};
}

Layer some_other_builder(/*params*/){
  return Layer{foofoo, foobar, init_for_foofoo_foobar(/*params*/)};
}

auto partial_some_other_builder = [](/* remaning params */){
  return some_other_builder(/* some params */, /* remaining params */);
}

Édité par Ksass`Peuk

First : Always RTFM - "Tout devrait être rendu aussi simple que possible, mais pas plus." A.Einstein

+2 -0

en fait j'ai proposé ça pour avoir un appel du genre f(Objet(3), f("deux", f(1))), en équivalence avec f(1)("deux")(Object(3)) de la curryfication. Mais je me rend compte que sortir la fonction variadique pour simplement avoir un appel possible à 1 argument, c'est un peu se compliquer la vie. Y' a surement moyen de le faire avec une fonction à 2 arguments et une valeur par défaut sur le deuxième argument. Quant au template variadique, j'avais mal compris ce qu'est la curryfication, je pensais que des appels du genre f(1)("deux", Object(3)) devait être possible (ce qui donnait f(Object(3), "deux", f(1)) dans mon idée).

Ce n'est pas parce qu'ils sont nombreux à avoir tort qu'ils ont raison - Coluche

+0 -0

Cette réponse a aidé l'auteur du sujet

Salut,

Je n'utiliserai pas de std::vector, mais des types dédiés aux maths Hautes Performances. Tu vas avoir besoin de calculs matrice * Vecteur, donc des choses comme Eigen ou blaze seront infiniment préférables – en plus ils permettent de plugger la MKL quand elle est dans le coin. Je ne parle pas d'utiliser des libs dédiées comme spark ou OpenCV, mais de ne pas perdre de temps à implémenter le niveau 2 des BLAS sur des std::vector. Ca serait idiot. Surtout des std::vector.

De plus, j'éviterai de retourner des nouveaux objets. Si la propagation doit être faite des millions de fois, cela veut dire des millions d'allocations avec le schéma actuel. Pareil pour la back-prop. D'ailleurs il existe quantité d'algo de backprop du naïf qui ne marche pas très bien jusqu'à la rprop qui donnait de bons résultats il y a bientôt 20ans quand je m'en servais sur des MLP. Donc pour moi, cela n'a rien à faire dans la classe couche. Les algos sont externes, surtout ceux d'apprentissage.

Je dirai aussi que l'algo de propagation ne doit pas être lié à la couche, mais au réseau. Maintenant, j'ai l'impression qu'avec ses modèles, il y a quelques spécificités qui sont hardcodées comme des topologies imposées, etc. (heureusement pas de boucle). Du coup, la couche peut être plus d'un simple vecteur de valeurs, et il peut être intéressant de lui associer la fonction de mise à jour. Quitte à ce qu'elle ne contienne d'une matrice de poids plus la multiplication.

Mais je partirai plutôt sur une liste de couches, plus une liste de transformations pour chaque paire de couches (par défaut: [&](layer const& in, layer & out) { out = activation_quivabien(Wquivabien * in); }). Et des algos de backprog en dehors de tout ça histoire de pouvoir en tester des différents.

+1 -0
Staff
Auteur du sujet

Salut,

Merci pour vos réponses, c'est très intéressant et très instructif.

@Ksass`Peuk : c'est une solution intéressante !

Je ne pense pas reprendre ta solution telle quel mais je la garde dans un coin de ma tête si jamais j'ai un jour besoin de quelque chose de similaire. Tu m'as posé la bonne question :

Il y a un aspect en revanche que tu n'évoques pas : deux types de layers sont initialisés avec des données différentes : OK. Mais l'état interne de ces deux types est-il, lui, différent ?

Cette simple question m'a fait réfléchir et il est vrai que la réponse conditionne beaucoup l'implémentation. Eh bien l'état interne des données doit à priori rester le même : une matrice de poids, de biais, et quelques autres trucs.

Mais les donnés propres à chaque type de layer servent à rien d'autre que paramétrer l'algorithme spécifique au layer. Donc je me suis dit autant faire un foncteur de mes algorithmes. L'opérateur operator() serait l'algo en lui même, et le constructeur du foncteur servirait à paramétrer l'algo. Le soucis était que ma classe layer devenait une classe template (à cause du foncteur pris en paramètre).

Autant j'ai rien contre utiliser les type template mais créer les miens souvent c'est casse-gueule car je m'en sers mal.

Heureusement je peux régler le problème de la façon suivante :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Layer {
public:
   Layer(function<vector<double> (Layer&, const vector<double>&)> _feedForward):s_feedForward(_feedForward)

   vector<double> feedForward(const vector<double>& lol) {
      return s_feedForward(*this, lol);
   }

private:
function<vector<double> (Layer&, const vector<double>&)> s_feedForward;
};  

Layer makeConvPoolLayer(/*convPoolLayer params*/ params) {
   auto f = [=params](Layer& layer, const vector<double>& in) {
        /*trololo convPoolLayer*/
   }
   return Layer(f);
}

Layer makeFullyConnectedLayer(/*FullyConnectedLayer params*/ otherParams) {
   auto f = [=otherParams](Layer& layer, const vector<double>& in) {
        /*trololo FullyConnectedLayer*/
   }
   return Layer(f);
}

Cette méthode me paraît pas mal : les différentes fonctions sont bien paramétrées comme voulu, et la classe layer est paramétré par les fonctions elles mêmes.
J'ai même l'impression que l'introduction des lambdas a enterré les foncteurs du C++ mais je me trompe peut-être.

L'utilisation de lambda nuit-elle beaucoup aux performances pour ce type d'applications ?
On voit également que Layer doit exposer d'une manière ou d'un autre ses attributs pour que s_feedForward puisse y avoir accès facilement. C'est pas top en terme d'encapsulation, mais je sais pas si c'est vraiment pertinent de parler d'encapsulation pour ce type d'objet.

Pas moyen de passer sur VS Community ? Ce serait mieux pour ta vie. Les oiseaux chanteraient mieux dans ton jardin et ton frigo serait plus souvent plein.

Je ne sais plus, je crois que j'avais essayé mais que j'avais été emmerdé pour installer CUDA, mais peut-être que c'était dû à autre chose. Là CUDA marche donc je garde ce que j'ai.

@lmghs:

Merci pour tes conseils. J'étais réticent à utiliser d'autres bibliothèques car c'est souvent long et lourd (j'ai déjà Cuda et SFML là) mais je suis surpris de découvrir qu'on peut faire de l'algèbre linéaire avec des bibliothèques simples et légères (juste besoin d'inclure les headers, c'est génial !). J'ai donc téléchargé Eigen et je vais continuer avec. Les valarray de C++ n'auraient pas suffi ceci dit ? Je vois personne les utiliser.

J'aime bien la solution que tu proposes. Tu sépares clairement les données (poids, biais, vecteur d'entrée et de sortie de chaque couche) et les algorithmes de l'autre côté. Du coup la classe layer c'est juste un gros paquet de donnés agrégées (poids, etc… ) et mon NeuralNetwork serait simplement initialisé avec une liste de transformations comme tu le proposes.

Ma solution réunissait les données ET l'algo dans la class Layer, et mon NeuralNetwork transportait juste une liste de Layers.

Ce qui est gênant c'est que ta ligne doit se réécrire [&](layer const& in, layer & out) { out.a = out.activation_quivabien(out.Wquivabien * in.a); } car les poids dépendent du layer courant. De plus ils ne se gèrent pas tout à fait pareil selon le type de chaque layer : pour un layer qui réalise une convolution les poids sont partagés alors pour un FullyConnectedLayer ils sont indépendants, etc… l'interprétation de Wquivabien dépend du contexte (type de traitement appliqué). Donc je pensais l'intégrer à la couche en cours d'entraînement, qu'en penses-tu ?

Édité par Algue-Rythme

+0 -0

Cette réponse a aidé l'auteur du sujet

Pour les valarray, tu n'as qu'un BLAS de niveau 1 (i.e. op(vecteur, vecteur)), et pas particulièrement optimisé dans mes souvenirs. Or tu as besoin du niveau 2 pour les opérations Matrice X Vecteur. Disons que si ton objectif est d'implémenter un RdNA, tu as autre chose à faire que de passer 4 mois à écrire une lib de calcul matriciel qui supporte le niveau 2 des BLAS (juste quelques jours pour la v1, et des mois pour avoir des bonnes optims (cache, élimination des temporaires (expression-templates), alignement, TU, tests de non-régression, tests de couverture…)). C'est pour ça que je t'ai poussé dans cette direction.

Les poids & cie dépendent de la couche courante d'une certain façon oui. En fait, on pourrait dire qu'un RdNA est défini par ses matrices de poids et fonctions d'activation. Les couches, c'est un détail d'implémentation qui arrive au moment de la propagation et de l'apprentissage.

Quand tu vas vouloir sauvegarder un RdNA, tu ne sauveras pas ses couches, mais uniquement ce qui participe à la propagation. J'ai presque envie de dire du coup, que les couches, seront créés au moment d'instancier l'objet de propagation. Le RdNA aura lui les détails qui disent comment on propage d'une couche à la suivante. C'est plus ça que je stockerai. Et si je dois factoriser certaines matrices de poids, cela se ferait là.

Sinon, dans mes souvenirs théoriques, les lambdas ont un surcoût nul dans les algos qui les reçoivent sous forme template – ex: for_each, copy_if, sort, etc. Quand stockées dans des std::function, cela coute un peu plus cher qu'un appel de fonction direct, mais c'est du au caractère dynamique des std::function. J'ai vu plusieurs "projets" de gens qui tentaient de définir des frameworks callbacks plus légers.

+1 -0
Staff
Auteur du sujet

J'ai bien réfléchi.

Chaque couche encapsule :

  • des fonctions d'activation, qui sont bien spécifique à la couche et non au neurone lui même. En effet tous les neurones de la couche ont la même fonction d'activation. De plus certaines fonctions d'activation dépendent de tous les neurones de la couche courante (je pense notamment à softmax).
  • une topologie particulière (ensemble de connections, poids partagées ou non, etc… )
  • cette topologie conditionne la nature (comprendre : le type) de l'entrée et de la sortie (un réseau de convolution retourne un ensemble de maps bidimensionnelles, alors qu'une couche complètement connectée retourne un vecteur colonne)
  • elle conditionne également l'algorithme de propagation feedForward, et celui de backPropagation

Ainsi je vais reprendre la solution du début : une classe Layer dont héritera chaque type de Layer particulier, qui viendra avec ses propres algorithmes et ses propres données (poids, biais, etc… ) mai aussi la format de son entrée et de sa sortie.

La difficulté va être de connecter des layers alors que les entrées/sorties ne sont pas identiques (ce sont des tenseurs 3D ce que ne gère pas Eigen, je vais donc devoir utiliser des std::vector<Eigen::MatrixXd> ou des Eigen::Matrix<Eigen::MatrixXd, Eigen::Dynamic, 1> en priant pour que les performances ne soient pas trop négativement impactées).

Je vais donc chaîner entre eux les layers pour qu'ils partagent les entrées et les sorties.

Je trouvais cette solution trop lourde mais je ne vois rien de mieux. Je me suis finalement décidé quand j'ai vu que quelqu'un avait mis en application ce système et ça rend plutôt bien : tout est là. C'est la première fois que j'examine sérieusement le travail de quelqu'un d'autre et il faut bien reconnaître que c'est un peu déprimant quand je vois la masse de travail qui m'attend.

En terme de performance le surcout induit par le fait que les fonctions soient virtuelles n'est pas gênant car :
- négligeable devant le prix du calcul du FeedForward
- à priori identique à celui induit par l'utilisation de std::function comme on en discutait plus tôt

En tout cas merci pour l'aide. Si vous avez quelque chose à en redire je suis preneur, mais là je pense que je vais commencer à implémenter ça.

Édité par Algue-Rythme

+0 -0

Petite remarque, si tu récupère la branche à jour de Eigen, il y a une bibli Tensor (principalement écrite par google il me semble) qui te sera surement utile.

Par contre, pour les fonctions d'activation, tu va probablement devoir les encapsuler dans des structures un peu plus complexe, la back prop ayant généralement besoin de la dérivée …

+1 -0
Staff
Auteur du sujet

Salut !

Merci de tes conseils ! J'ai justement téléchargé la bibliothèque Tensor de Eigen, sur la version en cours de développement (celle de la dernière release stable ne la contient pas). J'ai même dû bidouiller car ça ne compile pas sous Visual Studio, voici la solution ici pour ceux qui auraient le même problème : http://stackoverflow.com/questions/35410677/compile-errors-of-eigens-unsupported-cxx11-tensor-module

Il est vrai que les features maps de la couche réalisant une convolution constitue un tableau de cartes bidimensionnelles (un tenseur 3D donc), et que lorsque cette couche est mappée avec une couche unidimensionnelle de neurones complètement connectés, le formalisme matriciel $A_{l+1}=\sigma(W_l A_l+B_l)$ ne convient plus (enfin disons qu'il se traduit mal informatiquement parlant, transformer le tenseur 3D en vecteur colonne est probablement fort couteux, mieux vaut travailler en place). Je pense donc que à aucun moment j'ai intérêt à utiliser les Matrix et Array de Eigen (si cependant vous avez des objections je suis preneur).

Je vais également, comme tu l'as suggéré, encapsuler les fonctions d'activation dans une classe abstraite ActivationFunction dont dérivera chaque fonction particulière (comme Sigmoid, Perceptron, etc… ).

Je suis ouvert à toute proposition/remarque/correction, et je vous ferais régulièrement des retours sur l'avancement du projet. Merci !

Édité par Algue-Rythme

+0 -0

Pour des trucs comme ça, je fuis au maximum "virtual" et des hiérarchies de tout et n'importe quoi.

Je préfère reposer autant se faire que peu sur une résolution statique des points de variation (i.e. avec des template). De temps en temps, il y a besoin de remettre une couche de virtual, mais je préfère donc, quand possible, la mettre au niveau le plus extérieur aux boucles.

+1 -0
Staff
Auteur du sujet

Pour des trucs comme ça, je fuis au maximum "virtual" et des hiérarchies de tout et n'importe quoi.

Pour les fonctions d'activation ? Pour les layers ça va être difficile…

Le défaut des templates c'est quand même devoir foutre tout mon code dans le .hpp, je suis pas fan.

Et puis ça n'empêchera pas de voir créer des classes qui implémentent les méthodes operator(). Et faudra également une fonction template<typename T> std::function<double (double)> derivative(); pour la dérivée (ou quelque chose de ressemblant) avec une spécialisation pour chaque classe encapsulant une fonction d'activation qu'on utiliserait comme auto sigmoid_prime = derivative<Sigmoid>(); // then use sigmoid_prime (encore que ça reste dynamique, or le seul argument est template - donc statique - donc on doit pouvoir rajouter un argument template pour le type de retour qui serait une classe encapsulant la dérivée - mais ça commence à faire beaucoup de classes).

Dans ces conditions en quoi l'utilisation de virtual est pire ? Note bien que je me fie à ton expérience, mais j'essaie juste de comprendre les raisons profondes^^ ici le polymorphisme apporte une couche d'abstraction tout en restant facile à comprendre et utiliser - d'autant plus que chaque classe correspondant à une fonction d'activation serait instanciée une unique fois dans le programme (puisque ce sont des classes sans attributs elles sont toutes identiques) et les layers ne manipuleront rien d'autre qu'une référence vers cette classe (pour préserver le polymorphisme) - faudra juste se méfier avec les layers initialisés dans l'espace global s'ils sont initialisés avant l'initialisation de l'objet *FunctionActivation qu'ils utilisent.

Or les templates c'est :
- volumineux et lourd en terme de code (très verbeux,souvent pas clair, masque le sens de la fonction)
- violente mon temps de compilation (déjà malmené par Eigen)
- Tout dans les .hpp :'(

Édité par Algue-Rythme

+0 -0

Mon soucis avec virtual, c'est quand il est au fin fond d'une boucle et qu'il n'y a pas vraiment de caractère dynamique pour résoudre les liens (i.e. si des millions d'appels finissent au même endroit, virtual me gêne). Après, je ne suis pas sûr qu'il y ait des moyens simples et efficaces pour éliminer l'appel virtuel au niveau de chaque résolution de fonction d'activation. Sauf si la virtualité on la met au niveau des layers qui auraient des fonctions génériques d'application des calculs.

Pour ce qui est de tout mettre dans les .h, c'est un faux problème. Même vim sait replier le code. Le soucis n'est guère qu'au niveau des temps de (re)compilation quand on ne joue pas avec les extern template (ou technique C++98 équivalente). Pour la lourdeur (des binaires), c'est pas plus qu'un copier-coller.

+1 -0
Staff
Auteur du sujet

Je comprends ! Merci de tes explications.

J'ai résolu le problème avec cette classe :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
typedef Eigen::Tensor<float, 3> LayerGrid;
typedef std::function<void(LayerGrid&)> Neuron;
class ActivationFunction {
public:
    ActivationFunction(const Neuron&, const Neuron&);
    void operator()(LayerGrid&) const;
    void derivative(LayerGrid&) const;
private:
    Neuron m_function;
    Neuron m_derivative;
};

ActivationFunction::ActivationFunction(
    const Neuron& _m_function,
    const Neuron& _m_derivative) :
    m_function(_m_function),
    m_derivative(_m_derivative)
{}

void ActivationFunction::operator()(LayerGrid& input) const {m_function(input);}
void ActivationFunction::derivative(LayerGrid& input) const {m_derivative(input);}  

namespace Activations {
    void Perceptron(LayerGrid&);

    float sigmoid(float);
    void Sigmoid(LayerGrid&);
    float sigmoid_derivative(float z);
    void Sigmoid_derivative(LayerGrid&);

    float tanh(float);
    void Tanh(LayerGrid&);
    void Tanh_derivative(LayerGrid&);

    void ReLU(LayerGrid&);

    Neuron constant(float);
};

const ActivationFunction Perceptron(Activations::Perceptron, Activations::constant(0.));
const ActivationFunction Sigmoid(Activations::Sigmoid, Activations::Sigmoid_derivative);
const ActivationFunction Tanh(Activations::Tanh, Activations::Tanh_derivative);
const ActivationFunction ReLU(Activations::ReLU, Activations::Perceptron); // J'ai l'impression que la derivee d'un ReLU est un perceptron, c'est rigolo

float Activations::sigmoid(float z) {
    return 1 / (1 + exp(-z));
}

void Activations::Sigmoid(LayerGrid& grid) {
    grid.unaryExpr(sigmoid); // Application vectorialisee
}  

// etc... 

J'instancie autant d'objets ActivationFunction que je veux dans l'espace global, et ils encapsulent chacun une fonction d'activation (vectorialisée) et sa dérivée. Ces fonctions vectorialisées utilisent elles mêmes des fonctions unaire float -> float.

J'utilise float pour trois raisons :

  • Le deep-learning ne requiert pas une précision extraordinaire, les double sont inutiles
  • Les calculs sont bien plus rapides sur GPU, donc j'anticipe
  • Sur CPU également. Je crois qu'il existe des circuits spécialisés pour les double aussi rapides que les float, mais l'optimisation sur laquelle je compte ce n'est pas le calcul en lui même mais l'optimisation sur les caches : puisqu'un float prend moins de place je pourrais mettre plus de données dans le cache, et donc réduire le nombre d'échanges avec la RAM.

Comme tu l'as fait remarquer, là je n'ai normalement pas des milliers d'appels d'un truc virtuel à l'intérieur d'une boucle. La résolution de l'indétermination se fait au sein du layer à travers l'objet ActivationFunction, qui encapsule des fonctions vectorialisées (et donc là normalement pas de problèmes de performances, non ?). J'ai certes des milliers d'éléments par layer mais seulement quelques layers en tout.

Édité par Algue-Rythme

+0 -0
  • Les calculs sont bien plus rapides sur GPU, donc j'anticipe

Algue-Rythme

Non c'est pareil. En revanche, tu as plus d'unité de calcul en float qu'en double sur les cartes non professionnelles, et tu peux les écrire 4 par 4 en mémoire locale au multi-processeur. Après, comme sur CPU, tu auras une meilleure utilisation des caches.

First : Always RTFM - "Tout devrait être rendu aussi simple que possible, mais pas plus." A.Einstein

+1 -0
Vous devez être connecté pour pouvoir poster un message.
Connexion

Pas encore inscrit ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte