L'article précédent était ici Cet article décrit l'abandon, qui est l'une des méthodes classiques de suppression du surapprentissage. Bien qu'il s'agisse d'une méthode simple, son effet peut être déduit du fait qu'elle a été utilisée depuis qu'elle a été proposée. À propos, il semble qu'il n'y ait aucune explication théorique qui puisse supprimer le surapprentissage. (Peut-être manque de recherche ...) Il existe de nombreuses raisons possibles, cependant ~
Dropout: Dropout a été proposé en 2012 comme un moyen de contrôler le surapprentissage et a également été adopté par le bien connu ** AlexNet **. Le contour est simplement "exclure la sortie de chaque couche de la couche entièrement connectée avec une certaine probabilité $ ratio $ pendant l'apprentissage". Je suis surpris que le surapprentissage puisse être supprimé avec juste cela. Je ne pense pas qu'il y ait une explication théorique sur la raison pour laquelle le surapprentissage est supprimé, mais il existe différentes théories. L'un d'eux est l'apprentissage d'ensemble.
En premier lieu, l'apprentissage d'ensemble est une technologie qui réalise une grande précision en intégrant plusieurs apprenants faibles. Les abandons sont particulièrement préoccupants pour la technique d'ensachage. Pour plus d'informations [ici](https://qiita.com/kuroitu/items/57425380546f7b9ed91c#%E3%82%A2%E3%83%B3%E3%82%B5%E3%83%B3%E3%83%96 % E3% 83% AB% E5% AD% A6% E7% BF% 92), veuillez donc vous y référer. Dans tous les cas, les abandons entraînent plusieurs modèles en même temps, c'est donc une sorte d'ensachage. Le fait que les neurones désactivants soient différents à chaque fois qu'ils sont entraînés signifie qu'ils apprennent différents modèles pour chaque motif, ce qui signifie qu'ils apprennent différents modèles.
À partir de là, on considère que l'apprentissage avec plusieurs apprenants = apprentissage d'ensemble est effectué de manière simulée. L'une des caractéristiques de l'ensachage est que le résultat de l'entraînement a un biais élevé et une faible variance, de sorte qu'il ne s'adapte pas parfaitement aux données d'entraînement même s'il est organisé dans une certaine mesure. Par conséquent, on pense que le surapprentissage est supprimé.
Maintenant, jetons un bref regard sur la mise en œuvre de la théorie. Comme mentionné précédemment, la couche d'exclusion est facile à mettre en œuvre car elle "coupe uniquement la sortie de chaque couche de la couche entièrement connectée avec une certaine probabilité $ ratio $ pendant l'entraînement". Cependant, comme les passionnés l'ont peut-être remarqué, l'accent est mis sur "** lors de l'apprentissage **". Alors, que se passe-t-il lorsque vous avez fini d'apprendre et que vous commencez à raisonner?
Il n'abandonne pas pendant l'inférence, donc tous les neurones restent actifs. Comme vous pouvez l'imaginer, la sortie "** densité **" au moment de l'apprentissage et au moment de l'inférence sera différente. Pour résoudre ce problème, il existe une méthode de multiplication de la sortie par $ (1 --ratio) $ au moment de l'inférence.
Jetons un coup d'œil à la formule. En supposant que la sortie avant l'application de la suppression est $ y $ et la sortie après l'application est $ \ hat {y} $, la valeur attendue de la sortie au moment de l'apprentissage est
\mathbb{E}[\hat{y}] = \underbrace{(1 - ratio) y}_{Valeur attendue des neurones actifs} + \underbrace{ratio \times 0}_{非Valeur attendue des neurones actifs} = (1 - ratio)y
On dirait. Par contre, au moment de l'inférence, le taux de troncature $ ratio $ est 0 et la valeur attendue de la sortie est
\mathbb{E}[\hat{y}] = \underbrace{(1 - 0) y}_{Valeur attendue des neurones actifs} + \underbrace{0 \times 0}_{非Valeur attendue des neurones actifs} = y
Et la sortie $ \ frac {1} {1 --ratio} $ times est "sombre". (Notez que $ ratio $ est $ 0 \ le ratio \ lt 1 $ ici) L'idée est d'éliminer ce décalage en multipliant la sortie au moment de l'inférence par $ (1 --ratio) $ afin d'ajuster cette "obscurité".
(1 - ratio) \mathbb{E}[\hat{y}] = (1 - ratio) \left\{ \underbrace{(1 - 0) y}_{Valeur attendue des neurones actifs} + \underbrace{0 \times 0}_{非Valeur attendue des neurones actifs} \right\}= (1 - ratio)y
Mais c'est une méthode simple et dangereuse. Bien sûr, vous pouvez étudier sans aucun problème tel quel, et vous pouvez faire des inférences sans aucun problème. Cependant, cette méthode comporte le risque de "modifier la sortie de l'inférence". Je ne pense pas que ce soit un problème dans de nombreux cas, mais la sortie de la phase d'inférence est utilisée pour évaluer la précision du modèle, il est donc préférable de la toucher.
Ensuite, il existe une méthode pour "aligner la sortie au moment de l'apprentissage avec celle au moment de l'inférence". En d'autres termes, la densité est "assombrie" en divisant la sortie pendant l'entraînement par $ (1 --ratio) $.
\cfrac{1}{1 - ratio}\mathbb{E}[\hat{y}] = \cfrac{1}{1 - ratio} \left\{ \underbrace{(1 - ratio) y}_{Valeur attendue des neurones actifs} + \underbrace{ratio \times 0}_{非Valeur attendue des neurones actifs} \right\} = y
En faisant cela, la valeur attendue de la sortie sera la même au moment de l'apprentissage et au moment de l'inférence, il n'est donc pas nécessaire de toucher la sortie au moment de l'inférence. Cette méthode de jeu avec la sortie pendant l'apprentissage est appelée la ** méthode d'abandon inverse ** par opposition à la méthode d'abandon normal.
Maintenant, implémentons la couche d'abandon en utilisant la méthode d'abandon inverse.
dropout.py
class Dropout(BaseLayer):
def __init__(self, *args,
mode="cpu", ratio=0.25,
prev=1, n=None, **kwds):
if not n is None:
raise KeyError("'n' must not be specified.")
super().__init__(*args, mode=mode, **kwds)
self.ratio = ratio
self.mask = self.calculator.zeros(prev)
self.prev = prev
self.n = prev
def forward(self, x, *args, train_flag=True, **kwds):
if train_flag:
self.mask = self.calculator.random.randn(self.prev)
self.mask = self.calculator.where(self.mask >= self.ratio, 1, 0)
return x*self.mask/(1- self.ratio)
else:
return x
def backward(self, grad, *args, **kwds):
return grad*self.mask/(1 - self.ratio)
def update(self, *args, **kwds):
pass
La mise en œuvre est simple, n'est-ce pas? Le nombre de neurones dans la sortie doit correspondre à la couche précédente, il est donc repoussé lors de la phase d'initialisation.
Pour la propagation directe, pendant l'apprentissage, une variable appelée «masque» est utilisée pour sélectionner au hasard les neurones qui abandonnent. De plus, le décrochage inversé est réalisé en divisant par $ (rapport 1) $ au moment de la production. Par conséquent, c'est une implémentation qui passe telle qu'elle est au moment de l'inférence.
Comme la rétro-propagation n'est utilisée que pendant l'apprentissage, il n'est pas nécessaire de séparer le traitement comme la propagation vers l'avant. Le même «masque» que dans la propagation vers l'avant est multiplié par le produit de l'élément de sorte que seuls les neurones actifs se propagent vers l'arrière, et il est également divisé par $ (1-ratio) $.
Il n'y a pas de paramètres à apprendre dans la couche d'exclusion, donc l'implémentation est réussie.
De plus, lors de l'ajout d'une couche d'exclusion, ajoutez une couche d'exclusion à la classe _TypeManager
, et calculez l'erreur dans la fonction training
dans l'implémentation de la classe Trainer
et la fonction forward
utilisée dans la fonction prédire
. Ajoutons train_flag
à.
type_manager.py
class _TypeManager():
"""
Classe de gestionnaire pour les types de couches
"""
N_TYPE = 5 #Nombre de types de couches
BASE = -1
MIDDLE = 0 #Numérotation des couches intermédiaires
OUTPUT = 1 #Numérotation des couches de sortie
DROPOUT = 2 #Numérotation des couches de suppression
CONV = 3 #Numérotation des couches de pliage
POOL = 4 #Numérotation de la couche de pooling
REGULATED_DIC = {"Middle": MiddleLayer,
"Output": OutputLayer,
"Dropout": Dropout,
"Conv": ConvLayer,
"Pool": PoolingLayer,
"BaseLayer": None}
@property
def reg_keys(self):
return list(self.REGULATED_DIC.keys())
def name_rule(self, name):
name = name.lower()
if "middle" in name or name == "mid" or name == "m":
name = self.reg_keys[self.MIDDLE]
elif "output" in name or name == "out" or name == "o":
name = self.reg_keys[self.OUTPUT]
elif "dropout" in name or name == "drop" or name == "d":
name = self.reg_keys[self.DROPOUT]
elif "conv" in name or name == "c":
name = self.reg_keys[self.CONV]
elif "pool" in name or name == "p":
name = self.reg_keys[self.POOL]
else:
raise UndefinedLayerError(name)
return name
trainer.py
import time
import matplotlib.pyplot as plt
import matplotlib.animation as animation
softmax = type(get_act("softmax"))
sigmoid = type(get_act("sigmoid"))
class Trainer(Switch):
def __init__(self, x, y, *args, mode="cpu", **kwds):
#Si le GPU est disponible
if not mode in ["cpu", "gpu"]:
raise KeyError("'mode' must select in {}".format(["cpu", "gpu"])
+ "but you specify '{}'.".format(mode))
self.mode = mode.lower()
super().__init__(*args, mode=self.mode, **kwds)
self.x_train, self.x_test = x
self.y_train, self.y_test = y
self.x_train = self.calculator.asarray(self.x_train)
self.x_test = self.calculator.asarray(self.x_test)
self.y_train = self.calculator.asarray(self.y_train)
self.y_test = self.calculator.asarray(self.y_test)
self.make_anim = False
def forward(self, x, train_flag=True, lim_memory=10):
def propagate(x, train_flag=True):
x_in = x
n_batch = x.shape[0]
switch = True
for ll in self.layer_list:
if switch and not self.is_CNN(ll.name):
x_in = x_in.reshape(n_batch, -1)
switch = False
x_in = ll.forward(x_in, train_flag=train_flag)
#Parce que la méthode de propagation directe est également utilisée pour le calcul d'erreur et la prédiction de données inconnues
#La capacité de mémoire peut être importante
if self.calculator.prod(
self.calculator.asarray(x.shape))*8/2**20 >= lim_memory:
#Nombre à virgule flottante double précision(8byte)À 10 Mo(=30*2**20)Plus que
#Lorsque vous utilisez de la mémoire, divisez-la en 5 Mo ou moins et exécutez
n_batch = int(5*2**20/(8*self.calculator.prod(
self.calculator.asarray(x.shape[1:]))))
if self.mode == "cpu":
y = self.calculator.zeros((x.shape[0], lm[-1].n))
elif self.mode == "gpu":
y = self.calculator.zeros((x.shape[0], lm[-1].n))
n_loop = int(self.calculator.ceil(x.shape[0]/n_batch))
for i in range(n_loop):
propagate(x[i*n_batch : (i+1)*n_batch], train_flag=train_flag)
y[i*n_batch : (i+1)*n_batch] = lm[-1].y.copy()
lm[-1].y = y
else:
#Sinon, exécutez normalement
propagate(x, train_flag=train_flag)
・
・
・
def training(self, epoch, n_batch=16, threshold=1e-8,
show_error=True, show_train_error=False, **kwds):
if show_error:
self.error_list = []
if show_train_error:
self.train_error_list = []
if self.make_anim:
self.images = []
self.n_batch = n_batch
n_train = self.x_train.shape[0]//n_batch
n_test = self.x_test.shape[0]
#Commencer à apprendre
if self.mode == "gpu":
cp.cuda.Stream.null.synchronize()
start_time = time.time()
lap_time = -1
error = 0
error_prev = 0
rand_index = self.calculator.arange(self.x_train.shape[0])
for t in range(1, epoch+1):
#Création de scène
if self.make_anim:
self.make_scene(t, epoch)
#Calcul des erreurs d'entraînement
if show_train_error:
self.forward(self.x_train[rand_index[:n_test]],
train_flag=False)
error = lm[-1].get_error(self.y_train[rand_index[:n_test]])
self.train_error_list.append(error)
#Calcul d'erreur
self.forward(self.x_test, train_flag=False)
error = lm[-1].get_error(self.y_test)
if show_error:
self.error_list.append(error)
・
・
・
def predict(self, x=None, y=None, threshold=0.5):
if x is None:
x = self.x_test
if y is None:
y = self.y_test
self.forward(x, train_flag=False)
self.y_pred = self.pred_func(self[-1].y, threshold=threshold)
y = self.pred_func(y, threshold=threshold)
print("correct:", y[:min(16, int(y.shape[0]*0.1))])
print("predict:", self.y_pred[:min(16, int(y.shape[0]*0.1))])
print("accuracy rate:",
100*self.calculator.sum(self.y_pred == y,
dtype=int)/y.shape[0], "%",
"({}/{})".format(self.calculator.sum(self.y_pred == y, dtype=int),
y.shape[0]))
if self.mode == "cpu":
return self.y_pred
elif self.mode == "gpu":
return self.y_pred.get()
Expérimentons. Cependant, l'apprentissage avec l'ensemble de données MNIST ne provoque pas beaucoup de surentraînement, donc l'effet peut sembler faible. L'expérience est menée sur Google Colaboratory. J'exécute en mode GPU car j'utilise l'ensemble de données MNIST de Keras, mais cela prend encore environ 20 minutes pour 200 époques. Le code peut être exécuté tel quel en passant de github à Google Colaboratory.
test.py
%matplotlib inline
#Créer une couche de convolution et une couche de sortie
M, F_h, F_w = 10, 3, 3
lm = LayerManager((x_train, x_test), (t_train, t_test), mode="gpu")
lm.append(name="c", I_shape=(C, I_h, I_w), F_shape=(M, F_h, F_w), pad=1)
lm.append(name="p", I_shape=lm[-1].O_shape, pool=2)
lm.append(name="m", n=100, opt="eve")
lm.append(name="d", ratio=0.5)
lm.append(name="o", n=n_class, act="softmax", err_func="Cross")
#Apprendre
epoch = 200
threshold = 1e-8
n_batch = 128
lm.training(epoch, threshold=threshold, n_batch=n_batch, show_train_error=True)
#Prédire
print("training dataset")
_ = lm.predict(x=lm.x_train, y=lm.y_train)
print("test dataset")
y_pred = lm.predict()
Un peu de travail pénible est nécessaire pour illustrer les résultats expérimentaux. Tout d'abord, exécutez la cellule de code de test sans la couche de suppression, puis exécutez le code suivant préparé dans une autre cellule.
get_error.py
err_list = lm.error_list
Ensuite, exécutez la cellule de code de test avec la couche d'exclusion et exécutez le code suivant préparé dans une autre cellule.
get_drop_error.py
drop_error_list = lm.error_list
Après la configuration ci-dessus, préparez le code suivant dans une autre cellule et exécutez-le.
plot.py
fig, ax = plt.subplots(1)
fig.suptitle("error comparison")
ax.set_xlabel("epoch")
ax.set_ylabel("error")
ax.set_yscale("log")
ax.grid()
ax.plot(drop_error_list, label="dropout error")
ax.plot(err_list, label="normal error")
ax.legend(loc="best")
Vous pouvez maintenant le visualiser.
C'est un peu ennuyeux, alors pensons à une implémentation qui permet d'illustrer plus facilement ce genre de vérification comparative ...
Recommended Posts