[PYTHON] J'ai examiné l'argument class_weight de la fonction softmax_cross_entropy de Chainer.

introduction

Il existe deux façons principales de réduire le biais de classe dans un ensemble de données lors de l'apprentissage de la classification.

Cette fois, j'ai résumé ce qui m'intéressait lors de l'adoption de la deuxième méthode utilisant Chainer.

motivation

Plus précisément, la fonction softmax_cross_entropy a un argument appelé class_weight, mais en contrôlant cela, vous pouvez modifier la force d'entraînement pour chaque classe. Par exemple, dans le cas d'une classification à deux classes, cela signifie que l'apprentissage de la classe «1» peut être effectué deux fois plus fortement que l'apprentissage de la classe «0». Ensuite, quand vous lui donnez le double de poids, que signifie apprendre deux fois plus fort? Je me demandais, alors j'ai cherché.

Environnement d'exécution

Voyons l'effet sur la perte

Tout d'abord, lisez la [Documentation] de Chainer (! Https://docs.chainer.org/en/stable/reference/generated/chainer.functions.softmax_cross_entropy.html#chainer.functions.softmax_cross_entropy).

chainer.functions.softmax_cross_entropy(x, t, normalize=True, cache_score=True, class_weight=None, ignore_label=-1, reduce='mean') ... · Class_weight (ndarray ou ndarray) - Un tableau qui contient des poids constants qui seront multipliés par les valeurs de perte avec la deuxième dimension. La forme de ce tableau doit être (x.shape [1],). Si c'est pas None, chaque poids de classe class_weight [i] ʻest en fait multiplié par y [:, i] qui est la sortie log-softmax correspondante de xet a la même forme quex` avant de calculer le valeur réelle de la perte.

En d'autres termes, il semble que $ log (Softmax (x)) $ calculé avant le calcul de la perte soit multiplié selon la forme de x. Je vois.

Voyons maintenant à quel stade le class_weight est réellement multiplié. Jetons d'abord un coup d'œil à la fonction forward dans softmax_cross_entropy.py.

chainer/functions/loss/softmax_cross_entropy.py


    def forward_cpu(self, inputs):
        x, t = inputs
        if chainer.is_debug():
            self._check_input_values(x, t)

        log_y = log_softmax._log_softmax(x, self.use_cudnn)
        if self.cache_score:
            self.y = numpy.exp(log_y)
        if self.class_weight is not None:
            shape = [1 if d != 1 else -1 for d in six.moves.range(x.ndim)]
            log_y *= _broadcast_to(self.class_weight.reshape(shape), x.shape)
        log_yd = numpy.rollaxis(log_y, 1)
        log_yd = log_yd.reshape(len(log_yd), -1)
        log_p = log_yd[numpy.maximum(t.ravel(), 0), numpy.arange(t.size)]

        log_p *= (t.ravel() != self.ignore_label)
        if self.reduce == 'mean':
            # deal with the case where the SoftmaxCrossEntropy is
            # unpickled from the old version
            if self.normalize:
                count = (t != self.ignore_label).sum()
            else:
                count = len(x)
            self._coeff = 1.0 / max(count, 1)

            y = log_p.sum(keepdims=True) * (-self._coeff)
            return y.reshape(()),
        else:
            return -log_p.reshape(t.shape),

Il est à noter qu'à la ligne 11, class_weight est diffusé pour le résultat du calcul de log (Softmax (x)).

log_y *= _broadcast_to(self.class_weight.reshape(shape), x.shape)


 Autrement dit, le calcul de l'erreur d'entropie croisée $ L = - \ sum t_ {k} \ log {(Softmax (y_ {k}))} $ avant l'ajout à $ \ log {(Softmax (y_ {k}))} $ Sera multiplié par.
 A ce moment, $ k $ indique le nombre de classes.
 En d'autres termes, la formule est $ L = - \ sum t_ {k} ClassWeight_ {k} \ log {(Softmax (y_ {k}))} $.
 C'est comme documenté.

 Pour voir cela, expérimentons de manière interactive.

import numpy as np import chainer x = np.array([[1, 0]]).astype(np.float32) t = np.array([1]).astype(np.int32) #classe'1'Être entraîné avec le double du poids cw = np.array([1, 2]).astype(np.float32) sce_nonweight = chainer.functions.loss.softmax_cross_entropy.SoftmaxCrossEntropy() sce_withweight = chainer.functions.loss.softmax_cross_entropy.SoftmaxCrossEntropy(class_weight=cw) loss_nonweight = sce_nonweight(x, t) loss_withweight = sce_withweight(x, t) loss_nonweight.data array(1.31326162815094, dtype=float32)

loss_withweight.data array(2.62652325630188, dtype=float32)


 Vous pouvez voir que la valeur de la perte est doublée.

 Par conséquent, ce que nous avons appris jusqu'à présent, c'est que la pondération dans class_weight est susceptible d'être reflétée dans la valeur de perte en sortie telle quelle.

# Voyons l'effet sur la propagation arrière

 Alors, quel est l'impact de l'apprentissage ou de la rétropropagation?
 Ce que je veux vérifier ici, c'est quelle est la valeur de $ y-t $, qui est la valeur rétropropagée de softmax_cross_entropy.
 Je suppose que la valeur de $ y-t $ est multipliée par le poids tel quel, mais vérifions l'implémentation de chainer pour le moment.


#### **`chainer/functions/loss/softmax_cross_entropy.py`**
```python

    def backward_cpu(self, inputs, grad_outputs):
        x, t = inputs
        gloss = grad_outputs[0]
        if hasattr(self, 'y'):
            y = self.y.copy()
        else:
            y = log_softmax._log_softmax(x, self.use_cudnn)
            numpy.exp(y, out=y)
        if y.ndim == 2:
            gx = y
            gx[numpy.arange(len(t)), numpy.maximum(t, 0)] -= 1
            if self.class_weight is not None:
                shape = [1 if d != 1 else -1 for d in six.moves.range(x.ndim)]
                c = _broadcast_to(self.class_weight.reshape(shape), x.shape)
                c = c[numpy.arange(len(t)), numpy.maximum(t, 0)]
                gx *= _broadcast_to(numpy.expand_dims(c, 1), gx.shape)
            gx *= (t != self.ignore_label).reshape((len(t), 1))
        else:
            # in the case where y.ndim is higher than 2,
            # we think that a current implementation is inefficient
            # because it yields two provisional arrays for indexing.
            n_unit = t.size // len(t)
            gx = y.reshape(y.shape[0], y.shape[1], -1)
            fst_index = numpy.arange(t.size) // n_unit
            trd_index = numpy.arange(t.size) % n_unit
            gx[fst_index, numpy.maximum(t.ravel(), 0), trd_index] -= 1
            if self.class_weight is not None:
                shape = [1 if d != 1 else -1 for d in six.moves.range(x.ndim)]
                c = _broadcast_to(self.class_weight.reshape(shape), x.shape)
                c = c.reshape(gx.shape)
                c = c[fst_index, numpy.maximum(t.ravel(), 0), trd_index]
                c = c.reshape(y.shape[0], 1, -1)
                gx *= _broadcast_to(c, gx.shape)
            gx *= (t != self.ignore_label).reshape((len(t), 1, -1))
            gx = gx.reshape(y.shape)
        if self.reduce == 'mean':
            gx *= gloss * self._coeff
        else:
            gx *= gloss[:, None]
        return gx, None

Ici, les lignes 9 à 17 calculent $ y-t $ cette fois, et vous pouvez voir que class_weight est diffusé à la valeur de rétropropagation comme prévu.

Vous pouvez également voir que la brillance est multipliée à la fin. Et ce qu'est gloss est comme grad_output, qui est membre de la classe Variable, grad. Vous pouvez vérifier le grad de la valeur initiale, voyons-le donc.

>> loss_nonweight.backward()
>> aloss_nonweight.backward()
>> loss_nonweight.grad
array(1.0, dtype=float32)
>> loss_withweight.grad
array(1.0, dtype=float32)

Bien sûr, j'avais des problèmes autrement, mais la première valeur de rétropropagation est $ \ frac {\ partial L} {\ partial L} = 1 $. Donc, ce résultat ne semble pas être faux.

De plus, pour le moment, il existe un paramètre _coeff qui est multiplié en plus de la brillance, mais ce n'est que l'inverse de batchsize (c'est-à-dire le membre pour la moyenne) pendant l'apprentissage par lots, et dans ce cas Est 1. À propos, lors du calcul de la perte, _coeff est également multiplié.

Il semble que le poids défini par class_weight soit directement lié à l'apprentissage attendu. Ensuite, c'est un peu forcé, mais c'est une expérience.

>> sce_nonweight.backward_cpu((x,t),[loss_nonweight.grad])
(array([[ 0.7310586, -0.7310586]], dtype=float32), None)
>> sce_withweight.backward_cpu((x,t),[loss_withweight.grad])
(array([[ 1.4621172, -1.4621172]], dtype=float32), None)

La valeur de la rétropropagation était array ([[0.7310586, 0.26894143]], dtype = float32) `` `en regardant` `` chainer.functions.softmax (x) .data De, vous pouvez voir que c'est $ y --t $. Et il a été confirmé que la valeur de la rétro-propagation a également été doublée correctement. Toutes nos félicitations.

En conclusion, nous avons constaté que le poids de class_weight est reflété proportionnellement à la valeur de la rétropropagation.

Conclusion

L'argument class_weigth dans softmax_cross_entropy implémenté dans Chainer est

J'ai découvert que.

Je ne sais pas qui l'obtiendra, mais si cela aide. J'apprécierais que vous me fassiez savoir si quelque chose ne va pas.

Recommended Posts

J'ai examiné l'argument class_weight de la fonction softmax_cross_entropy de Chainer.
J'ai essayé la fonction de tableau croisé dynamique des pandas
Correction des arguments de la fonction utilisée dans map
J'ai examiné l'arborescence des appareils
J'ai essayé un peu le comportement de la fonction zip
J'ai essayé de combattre le minimum local de la fonction Goldstein-Price
Je veux obtenir le nom de la fonction / méthode en cours d'exécution
Si vous donnez une liste avec l'argument par défaut de la fonction ...
Python: je souhaite mesurer proprement le temps de traitement d'une fonction
J'ai créé une fonction pour voir le mouvement d'un tableau à deux dimensions (Python)
J'ai vérifié le contenu du volume du docker
J'ai essayé le serveur asynchrone de Django 3.0
J'ai vérifié les options de copyMakeBorder d'OpenCV
La structure des dossiers de Flask est résumée
[Python3] Réécrire l'objet code de la fonction
Je ne connaissais pas les bases de Python
A propos des arguments de la fonction setup de PyCaret
Le modèle de projet Python auquel je pense.
Rendre la valeur par défaut de l'argument immuable
[Python] J'ai essayé de remplacer le nom de la fonction par le nom de la fonction
J'ai lu l'implémentation de range (Objects / rangeobject.c)
Battre la fonction de densité de probabilité de la distribution normale
Récupérer l'appelant d'une fonction en Python
J'ai vérifié la liste des touches de raccourci de Jupyter
J'ai essayé de corriger la forme trapézoïdale de l'image
Essayez Progate Free Edition [Python I]
J'ai vérifié la période de rétention de session de django
J'ai vérifié la vitesse de traitement de la numpy unidimensionnelle
J'ai touché certaines des nouvelles fonctionnalités de Python 3.8 ①
J'ai essayé d'implémenter la fonction gamma inverse en python
J'ai lu et implémenté les variantes de UKR
Je souhaite personnaliser l'apparence de zabbix
J'ai essayé d'utiliser le filtre d'image d'OpenCV
Je souhaite utiliser la fonction d'activation Mish
J'ai essayé de vectoriser les paroles de Hinatazaka 46!
Remarque: Signification de spécifier uniquement * (astérisque) comme argument dans la définition de fonction de Python