Machine Learning Golf: Multiplication


68

J'aimerais proposer un autre type de défi de golf à cette communauté:

Les réseaux de neurones (artificiels) sont des modèles d’apprentissage automatique très populaires qui peuvent être conçus et formés pour se rapprocher d’une fonction donnée (généralement inconnue). Ils sont souvent utilisés pour résoudre des problèmes très complexes que nous ne savons pas résoudre par des algorithmes tels que la reconnaissance vocale, certains types de classifications d’images, diverses tâches de systèmes de conduite autonome, ... Pour une introduction aux réseaux de neurones, considérez cet excellent Article de Wikipedia .

Comme il s’agit du premier de ce que j’espère être une série de défis d’apprentissage automatique du golf, j’aimerais garder les choses aussi simples que possible:

Dans le langage et la structure de votre choix, concevez et formez un réseau de neurones qui, étant donné calcule son produit pour tous les entiers compris entre -10 et 10 .(x1,x2)x1x2x1,x21010

Objectif de performance

Pour être éligible, votre modèle ne doit pas s'écarter de plus de 0.5 du résultat correct sur l'une de ces entrées.

Règles

Votre modèle

  • doit être un réseau neuronal «traditionnel» (la valeur d'un nœud est calculée comme une combinaison linéaire pondérée de certains des nœuds d'une couche précédente suivie d'une fonction d'activation),
  • pouvez uniquement utiliser les fonctions d'activation standard suivantes:
    1. linear(x)=x ,
    2. softmax(x)i=exijexj ,
    3. seluα,β(x)={βx, if x>0αβ(ex1), otherwise ,
    4. softplus(x)=ln(ex+1) ,
    5. leaky-reluα(x)={x, if x<0αx, otherwise ,
    6. tanh(x) ,
    7. sigmoid(x)=exex+1 ,
    8. hard-sigmoid(x)={0, if x<2.51, if x>2.50.2x+0.5, otherwise ,
    9. ex
  • doit prendre soit en tant que tupel / vecteur / liste / ... d'entiers ou floats en tant que seule entrée,(x1,x2)
  • renvoie la réponse sous forme d’entier, float (ou un conteneur approprié, par exemple un vecteur ou une liste, contenant cette réponse).

Votre réponse doit inclure (ou créer un lien vers) tout le code nécessaire pour vérifier vos résultats, y compris les poids formés de votre modèle.

Notation

Le réseau de neurones avec le plus petit nombre de poids (y compris les poids de biais) l'emporte.

Prendre plaisir!


9
Bienvenue sur le site! Je pense que ce défi pourrait grandement bénéficier d'une définition plus robuste du réseau de neurones. Il y a plusieurs choses ici 1) Ce serait très bien pour vous de le dire dans un langage qui n'implique pas déjà la connaissance des NN 2) Vous devriez vraiment lister les fonctions d'activation dans votre message plutôt que de vous connecter à une source externe ( les liens extérieurs peuvent changer ou disparaître).
Wheat Wizard le

4
Peut-on réutiliser des poids / utiliser des couches convolutives? (Je recommande de supprimer le bonus, car cela n'ajoute rien au défi et détourne simplement l'objectif principal.) Les poids sont-ils supposés être réels ou peuvent-ils être complexes?
Flawr

4
Votre formulation implique que les nœuds de la couche 3 ne peuvent pas utiliser les entrées de la couche 1. Est-il coûteux de pondérer un nœud de la couche 2 ne faisant que f(x) = xtransférer son entrée?
Grimmy le

4
Il devrait y avoir un lien dans la colonne de droite vers Sandbox, qui a été créé expressément pour résoudre ce type de problèmes avant même que la question ne soit publiée sur le site principal. Et la philosophie du réseau est qu'il est préférable de fermer une question, de la corriger et de la rouvrir plutôt que d'obtenir un ensemble de réponses qui n'auront plus aucun sens une fois la question résolue ou qui limiteront étroitement les modifications pouvant être apportées à la question. .
Peter Taylor

7
Pas du tout. Ce genre de problèmes est détecté par plusieurs années d'expérience de voir d'autres personnes faire le même genre d'erreurs. Certaines ambiguïtés se glissent dans le bac à sable, mais beaucoup d’autres y sont prises. Et cela aurait certainement été pris, car comme indiqué dans mon premier commentaire, nous avions exactement les mêmes problèmes avec une question de réseau neuronal il y a deux mois.
Peter Taylor

Réponses:


37

21 13 11 9 poids

Ceci est basé sur l' identité de polarisation des formes bilinéaires qui, dans le cas réel unidimensionnel, se réduit à l'identité polynomiale:

xy=(x+y)2(xy)24

Donc, y1calcule simplement en [x+y, x-y]utilisant une transformation linéaire, et y3est juste la valeur absolue de y1comme une étape de prétraitement pour la suivante: Ensuite, la partie "difficile" consiste à calculer les carrés que je vais expliquer ci-dessous, puis à calculer une différence et une mise à l'échelle qui est encore une opération linéaire.

Pour calculer les carrés, j’utilise une série exponentielle qui devrait être précise pour tous les entiers dans l’ordre de . Cette série est de la formes{0,1,2,,20}0.5

approx_square(x)=i=02wiexp(0.0001ix)

où je viens d'optimiser pour les poids W2( ). Toute cette approximation ne comprend encore que deux transformations linéaires avec une activation exponentielle prise en sandwich entre elles. Cette approche entraîne une déviation maximale d’environ .=(wi)i0.02

function p = net(x)
% 9 weights
one = 1; 
mone =-1;
zero = 0;
fourth = 0.25;
W1 = [1e-4, 2e-4];
W2  = [-199400468.100687;99700353.6313757];
b2 = 99700114.4299316;
leaky_relu = @(a,x)max(a*x,x); 


% Linear
y0 = [one, one; one, mone] * x;

% Linear + ReLU
y1 = mone * y0;
y2 = [leaky_relu(zero, y0), leaky_relu(zero, y1)];

% Linear
y3 = y2 * [one; one];

% Linear + exp
y4 = exp(y3 * W1); 

% Linear + Bias
y5 =  y4 * W2 + b2;

% Linear
y6 = [one, mone]*y5;
p = y6 * fourth;

end

Essayez-le en ligne!


Je pense que votre code de vérification dans le pied de page du lien TIO manque une application de abs. Mais tout va bien quand même.
Christian Sievers le

@ChristianSievers Merci, j'ai mis à jour le lien TIO!
Flawr

Je ne suis pas un expert sur NN, par curiosité, comment compte-t-on le poids? y0besoins 4, y1besoins 2, y3besoins 2, y4besoins 1, y5besoins 1 et y6besoins 2. C'est 12?
Margaret Bloom le

3
@MargaretBloom Oui, c'est effectivement un peu inhabituel, mais l'OP a déclaré dans les commentaires que nous pouvons réutiliser des poids et que nous n'avons à les compter qu'une seule fois, même si nous utilisons le même poids plusieurs fois. Donc, tous les poids que j'utilise sont définis dans la première partie de la fonction.
Flawr

31

7 poids

eps = 1e-6
c = 1 / (2 * eps * eps)

def f(A, B):
	e_s = exp(eps * A + eps * B)  # 2 weights, exp activation
	e_d = exp(eps * A - eps * B)  # 2 weights, exp activation
	return c * e_s + (-c) * e_d + (-1 / eps) * B  # 3 weights, linear activation

Essayez-le en ligne!

Utilise l’égalité approximative suivante pour les petits fonction de l’agrandissement de Taylor :ϵex1+x+x22

ABeϵA+ϵBeϵAϵB2ϵ2Bϵ

Choisir assez petit nous place dans les limites d'erreur requises. Notez que et sont des poids constants dans le code.ϵepsc


1
Pas sûr que cela compte comme un «réseau de neurones traditionnel» (règle n ° 1), mais il est évident qu'il peut être reformaté en un, je ne vois donc aucun problème à cela. Belle solution!
Stefan Mesken le

1
Vous pouvez définir C = -B(1 poids) et ensuite [e_s, e_d] = conv([A,B,C], [eps, eps])(2 poids) pour enregistrer un poids :) (BTW: approche très intelligente!)
flawr

(J'ai oublié d'ajouter exp)
flawr

4
Vous pouvez même réduire considérablement le poids en réutilisant les poids - vous n'avez pas à compter le même poids plusieurs fois.
Flawr

2
@flawr C'est une bonne astuce, mais je pense que les ajustements pour la convolution et la réutilisation des pondérations dans les commentaires rendent le défi tellement différent que je vais garder cette réponse telle quelle.
xnor

22

33 31 poids

# Activation functions
sub hard { $_[0] < -2.5 ? 0 : $_[0] > 2.5 ? 1 : 0.2 * $_[0] + 0.5 }
sub linear { $_[0] }

# Layer 0
sub inputA() { $a }
sub inputB() { $b }

# Layer 1
sub a15() { hard(5*inputA) }

# Layer 2
sub a8()  { hard(-5*inputA + 75*a15 - 37.5) }

# Layer 3
sub aa()  { linear(-5*inputA + 75*a15 - 40*a8) }

# Layer 4
sub a4()  { hard(aa - 17.5) }

# Layer 5
sub a2()  { hard(aa - 20*a4 - 7.5) }

# Layer 6
sub a1()  { linear(0.2*aa - 4*a4 - 2*a2) }

# Layer 7
sub b15() { hard(0.25*inputB - 5*a15) }
sub b8()  { hard(0.25*inputB - 5*a8) }
sub b4()  { hard(0.25*inputB - 5*a4) }
sub b2()  { hard(0.25*inputB - 5*a2) }
sub b1()  { hard(0.25*inputB - 5*a1) }

# Layer 8
sub output() { linear(-300*b15 + 160*b8 + 80*b4 + 40*b2 + 20*b1 - 10*inputA) }

# Test
for $a (-10..10) {
        for $b (-10..10) {
                die if abs($a * $b - output) >= 0.5;
        }
}

print "All OK";

Essayez-le en ligne!

Cela fait une longue multiplication en (sorta) binary, et renvoie donc le résultat exact. Il devrait être possible de tirer parti de la fenêtre d’erreur 0.5 pour jouer au golf, mais je ne sais pas comment.

Les couches 1 à 6 décomposent la première entrée en 5 "bits". Pour des raisons de golf, nous n'utilisons pas de binaire réel. Le "bit" le plus significatif a un poids de -15 au lieu de 16, et lorsque l'entrée est égale à 0, tous les "bits" sont égaux à 0,5 (ce qui fonctionne toujours bien car il préserve l'identité inputA = -15*a15 + 8*a8 + 4*a4 + 2*a2 + 1*a1).


1
Je m'attendais à ce que quelqu'un propose un algorithme de multiplication codé en dur et basé sur ANN. Mais je ne pensais pas que ce serait la première réponse. Bien joué! (Je suis également impatient de voir si vous pourrez retirer quelque chose comme ceci avec le jeu de données MNIST ou un autre problème plus relastique de ML: D.)
Stefan Mesken

14

43 poids

Les deux solutions publiées jusqu'à présent ont été très intelligentes, mais leurs approches ne fonctionneront probablement pas pour des tâches plus traditionnelles d'apprentissage automatique (comme l'OCR). Par conséquent, j'aimerais soumettre une solution «générique» (sans astuce) à cette tâche qui, espérons-le, inspire d'autres personnes à l'améliorer et à se laisser entraîner dans le monde de l'apprentissage automatique:

Mon modèle est un réseau de neurones très simple avec 2 couches cachées intégré dans TensorFlow 2.0 (mais tout autre framework fonctionnerait également):

model = tf.keras.models.Sequential([
tf.keras.layers.Dense(6, activation='tanh', input_shape=(2,)),
tf.keras.layers.Dense(3, activation='tanh'),
tf.keras.layers.Dense(1, activation='linear')
])

Comme vous pouvez le constater, toutes les couches sont denses (ce qui n’est certainement pas optimal), la fonction d’activation est tanh (ce qui pourrait en fait convenir pour cette tâche), à ​​l’exception de la couche de sortie qui, en raison de la nature de cette tâche, a une fonction d'activation linéaire.

Il y a 43 poids:

  • (2+1)6=18 entre l'entrée et le premier calque masqué,
  • (6+1)3=21 entre les couches cachées et
  • (3+1)1=4 connectant le dernier calque masqué et le calque de sortie.

Les poids ont été formés (avec un optimiseur adam) par une approche d’ajustement en couches: ils ont d’abord été ajustés afin de minimiser l’erreur quadratique moyenne non seulement lors de la multiplication d’entiers entre et mais également lors des entrées dans un voisinage donné autour de ces valeurs. . Il en résulte une bien meilleure convergence due à la nature de la descente de gradient. Et cela a représenté une formation d’une valeur de 400 époques sur 57 600 échantillons d’entraînement, en utilisant une taille de lot de 32.1010

Ensuite, je les ai affinés - optimisant l’écart maximal sur l’une des tâches de multiplication d’entiers. Malheureusement, mes notes ne montrent pas beaucoup d'ajustement que j'ai fini par faire, mais c'était très mineur. Au voisinage de 100 époques sur ces 441 échantillons d’entraînement, avec une taille de lot de 441.

Ce sont les poids que j'ai fini avec:

[<tf.Variable 'dense/kernel:0' shape=(2, 6) dtype=float32, numpy=
 array([[ 0.10697944,  0.05394982,  0.05479664, -0.04538541,  0.05369904,
         -0.0728976 ],
        [ 0.10571832,  0.05576797, -0.04670485, -0.04466859, -0.05855528,
         -0.07390639]], dtype=float32)>,
 <tf.Variable 'dense/bias:0' shape=(6,) dtype=float32, numpy=
 array([-3.4242163, -0.8875816, -1.7694025, -1.9409281,  1.7825342,
         1.1364107], dtype=float32)>,
 <tf.Variable 'dense_1/kernel:0' shape=(6, 3) dtype=float32, numpy=
 array([[-3.0665843 ,  0.64912266,  3.7107112 ],
        [ 0.4914808 ,  2.1569328 ,  0.65417236],
        [ 3.461693  ,  1.2072319 , -4.181983  ],
        [-2.8746269 , -4.9959164 ,  4.505049  ],
        [-2.920127  , -0.0665407 ,  4.1409926 ],
        [ 1.3777553 , -3.3750365 , -0.10507642]], dtype=float32)>,
 <tf.Variable 'dense_1/bias:0' shape=(3,) dtype=float32, numpy=array([-1.376577  ,  2.8885336 ,  0.19852689], dtype=float32)>,
 <tf.Variable 'dense_2/kernel:0' shape=(3, 1) dtype=float32, numpy=
 array([[-78.7569  ],
        [-23.602606],
        [ 84.29587 ]], dtype=float32)>,
 <tf.Variable 'dense_2/bias:0' shape=(1,) dtype=float32, numpy=array([8.521169], dtype=float32)>]

qui atteint à peine l'objectif de performance déclaré. La déviation maximale a finalement été de comme en témoigne .0.44350433910=90.443504

Mon modèle se trouve ici et vous pouvez également l' essayer en ligne! dans un environnement Google Colab.


6

2 poids

Les autres réponses m'ont inspiré pour approcher différemment l'identité de polarisation. Pour chaque petit , il est dit queϵ>0

xyeϵx+ϵy+eϵxϵyeϵxϵyeϵx+ϵy4ϵ2.

Il suffit de prendre pour ce défi.ϵ=0.01

La mise en oeuvre évidente de cette approximation par un réseau neuronal prend des poids dans . Ces quatre poids peuvent être réduits à trois en factorisant . Comme je l'ai mentionné dans un commentaire ci-dessus, chaque réseau de neurones avec une pondération en précision de machine peut être transformé en un réseau (énorme!) De neurones avec seulement deux poids distincts. J'ai appliqué cette procédure pour écrire le code MATLAB suivant:{±ϵ,±(4ϵ2)1}{±ϵ,(4ϵ3)1}±(4ϵ2)1=±ϵ(4ϵ3)1

function z=approxmultgolfed(x,y)

w1 = 0.1;   % first weight
w2 = -w1;   % second weight

k  = 250000;
v1 = w1*ones(k,1);
v2 = w2*ones(k,1);

L1 = w1*eye(2);
L2 = [ w1 w1; w2 w2; w1 w2; w2 w1 ];
L3 = [ v1 v1 v2 v2 ];
L4 = v1';

z = L4 * L3 * exp( L2 * L1 * [ x; y ] );

Au total, ce réseau de neurones comprend 1 250 010 poids, qui résident tous dans .{±0.1}

Comment sortir avec seulement 1 poids (!)

Il s’avère que vous pouvez simuler n’importe quel réseau de neurones dont le poids est à avec un réseau de neurones plus grand qui n’a qu’un seul poids, à savoir . En effet, la multiplication par peut être mise en œuvre comme{±0.1}0.10.1

0.1x=wwx,

où est le vecteur de colonne de entrées, toutes égales à . Pour les réseaux de neurones dans lesquels la moitié des poids sont positifs, cette transformation produit un réseau de neurones fois plus grand.w100.110.5

La généralisation évidente de cette procédure transformera tout réseau de neurones avec des poids en en un réseau de neurones plus grand avec le poids unique . Combinée à la procédure décrite dans mon commentaire ci-dessus, elle considère donc que chaque réseau neuronal doté de poids de précision machine peut être transformé en un réseau neuronal à poids unique.{±10k}10k

(Peut-être devrions-nous modifier la manière dont les poids réutilisés sont notés dans les futurs défis du golf neuronal.)

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.