J'ai soudainement commencé à étudier "Deep Learning from scratch - la théorie et l'implémentation du deep learning appris avec Python". C'est un mémo du voyage.
L'environnement d'exécution est macOS Mojave + Anaconda 2019.10, et la version Python est 3.7.4. Pour plus de détails, reportez-vous au Chapitre 1 de ce mémo.
(Vers d'autres chapitres de ce mémo: Chapitre 1 / Chapitre 2 / Chapitre 3 / Chapitre 4 / [Chapitre 5](https: // qiita. com / segavvy / items / 8707e4e65aa7fa357d8a) / Chapitre 6 / Chapitre 7 / Chapitre 8 / Résumé)
Ce chapitre décrit le réseau neuronal à convolution (CNN).
En plus de la couche Affine, de la couche Softmax et de la couche ReLU existantes, la couche Convolution et la couche Pooling apparaîtront.
L'explication de la couche de convolution est plus facile à lire si vous avez un peu de traitement d'image.
Il dit: "L'image est généralement une forme tridimensionnelle dans les directions verticale, horizontale et de canal." Cependant, puisque l'image est des données 2D verticales et horizontales, n'est-ce pas des données 3D avec de la profondeur ajoutée? Certaines personnes peuvent penser cela.
«Canal» se réfère ici aux informations pour chaque couleur comme RVB. Dans le cas des données d'échelle de gris (uniquement des nuances de noir et blanc) telles que MNIST, la densité d'un point peut être exprimée par une valeur, donc un canal est suffisant, mais dans une image couleur, un point est rouge, vert, bleu. Puisqu'elle est exprimée par la densité des trois valeurs de (RVB), trois canaux sont nécessaires. En plus de RVB, les canaux d'informations de couleur incluent CMJN, HSV et transparence alpha. Pour plus de détails, allez dans "RVB CMJN" etc. et vous y trouverez de nombreuses explications (bien qu'il y ait beaucoup d'histoires un peu plus proches de l'impression).
En outre, le mot "filtre" est également spécial et, dans le traitement d'image, il se réfère au traitement utilisé pour extraire uniquement les parties nécessaires (par exemple, les contours) d'une image ou pour supprimer les informations inutiles. Pour ceux qui ne le connaissent pas, il sera plus facile à comprendre si vous obtenez un aperçu du filtre de convolution dans le traitement d'image. @ t-tkd3a image de résultat du filtre de convolution 3x3 est recommandée car elle est facile à imaginer.
En passant, ce livre semble être une règle qui n'ajoute pas de longues notes pour katakana avec 3 notes ou plus comme "Layer". Cependant, puisque le "filtre" a une notation de note longue, il peut s'agir d'une omission unifiée. D'ailleurs, lorsque Microsoft a changé la méthode de notation de longues notes de Katakana en 2008 [^ 1], j'étais en charge du développement d'applications packagées pour Windows, et j'étais en charge de corriger le libellé des programmes et des manuels. C'était difficile. Avant cela, j'étais impliqué dans la suppression de kana demi-largeur de l'interface graphique de Windows 98 ... Dans cette industrie, vraiment japonais est gênant: transpirer:
Revenons à l'histoire et passons à autre chose.
Quant à la couche de mise en commun, je n'avais pas d'obstacles particuliers.
L'implémentation de la couche Convolution et de la couche Pooling est courte en code, mais compliquée car la forme des données cibles change rapidement avec ʻim2col,
numpy.ndarray.reshape et
numpy.ndarray.transpose`. C'était déroutant au début, mais je pouvais le comprendre en me référant à "Deep Learning from scratch" Convolution / Pooling layer implementation. ..
Le premier est l'implémentation de la couche Convolution. J'ai beaucoup de commentaires parce que je n'arrive pas à comprendre si je n'écris pas la forme.
convolution.py
# coding: utf-8
import os
import sys
import numpy as np
sys.path.append(os.pardir) #Ajouter le répertoire parent au chemin
from common.util import im2col, col2im
class Convolution:
def __init__(self, W, b, stride=1, pad=0):
"""Couche de convolution
Args:
W (numpy.ndarray):Filtre (poids), forme(FN, C, FH, FW)。
b (numpy.ndarray):Biais, forme(FN)。
stride (int, optional):Stride, la valeur par défaut est 1.
pad (int, optional):Remplissage, la valeur par défaut est 0.
"""
self.W = W
self.b = b
self.stride = stride
self.pad = pad
self.dW = None #Valeur différentielle du poids
self.db = None #Valeur différentielle du biais
self.x = None #Entrée pendant la propagation directe requise pour la propagation arrière
self.col_x = None #Résultat de l'expansion des colonnes de l'entrée au moment de la propagation vers l'avant requise pour la propagation arrière
self.col_W = None #Résultat de l'expansion de colonne du filtre au moment de la propagation vers l'avant requise pour la propagation arrière
def forward(self, x):
"""Propagation vers l'avant
Args:
x (numpy.ndarray):contribution. La forme est(N, C, H, W)。
Returns:
numpy.ndarray:production. La forme est(N, FN, OH, OW)。
"""
FN, C, FH, FW = self.W.shape # FN:Nombre de filtres, C:Nombre de canaux, FH:Hauteur du filtre, FW:largeur
N, x_C, H, W = x.shape # N:Taille du lot, x_C:Nombre de canaux, H: Hauteur des données d'entrée, W:largeur
assert C == x_C, f'Inadéquation du nombre de canaux![C]{C}, [x_C]{x_C}'
#Calcul de la taille de sortie
assert (H + 2 * self.pad - FH) % self.stride == 0, 'OH n'est pas divisible!'
assert (W + 2 * self.pad - FW) % self.stride == 0, 'OW est indivisible!'
OH = int((H + 2 * self.pad - FH) / self.stride + 1)
OW = int((W + 2 * self.pad - FW) / self.stride + 1)
#Développer les données d'entrée
# (N, C, H, W) → (N * OH * OW, C * FH * FW)
col_x = im2col(x, FH, FW, self.stride, self.pad)
#Développer le filtre
# (FN, C, FH, FW) → (C * FH * FW, FN)
col_W = self.W.reshape(FN, -1).T
#Calculer la sortie (col_x, col_W,Le calcul de b est exactement le même que celui de la couche Affine)
# (N * OH * OW, C * FH * FW)・(C * FH * FW, FN) → (N * OH * OW, FN)
out = np.dot(col_x, col_W) + self.b
#Formater le résultat
# (N * OH * OW, FN) → (N, OH, OW, FN) → (N, FN, OH, OW)
out = out.reshape(N, OH, OW, FN).transpose(0, 3, 1, 2)
#Enregistrer pour la rétropropagation
self.x = x
self.col_x = col_x
self.col_W = col_W
return out
def backward(self, dout):
"""Rétropropagation
Args:
dout (numpy.ndarray):La valeur différentielle et la forme transmises par la bonne couche(N, FN, OH, OW)。
Returns:
numpy.ndarray:Valeur de différenciation (gradient), forme(N, C, H, W)。
"""
FN, C, FH, FW = self.W.shape #La forme de la valeur différentielle est la même que W(FN, C, FH, FW)
#Développez la valeur différentielle du bon calque
# (N, FN, OH, OW) → (N, OH, OW, FN) → (N * OH * OW, FN)
dout = dout.transpose(0, 2, 3, 1).reshape(-1, FN)
#Calcul de la valeur de différenciation (col_x, col_W,Le calcul de b est exactement le même que celui de la couche Affine)
dcol_x = np.dot(dout, self.col_W.T) # → (N * OH * OW, C * FH * FW)
self.dW = np.dot(self.col_x.T, dout) # → (C * FH * FW, FN)
self.db = np.sum(dout, axis=0) # → (FN)
#Formatage de la valeur différentielle du filtre (poids)
# (C * FH * FW, FN) → (FN, C * FH * FW) → (FN, C, FH, FW)
self.dW = self.dW.transpose(1, 0).reshape(FN, C, FH, FW)
#Formatage du résultat (dégradé)
# (N * OH * OW, C * FH * FW) → (N, C, H, W)
dx = col2im(dcol_x, self.x.shape, FH, FW, self.stride, self.pad)
return dx
Vient ensuite l'implémentation de la couche Pooling. C'est aussi plein de commentaires.
pooling.py
# coding: utf-8
import os
import sys
import numpy as np
sys.path.append(os.pardir) #Ajouter le répertoire parent au chemin
from common.util import im2col, col2im
class Pooling:
def __init__(self, pool_h, pool_w, stride=1, pad=0):
"""Couche de regroupement
Args:
pool_h (int):Hauteur de la zone de regroupement
pool_w (int):Largeur de la zone de regroupement
stride (int, optional):Stride, la valeur par défaut est 1.
pad (int, optional):Remplissage, la valeur par défaut est 0.
"""
self.pool_h = pool_h
self.pool_w = pool_w
self.stride = stride
self.pad = pad
self.x = None #Entrée pendant la propagation directe requise pour la propagation arrière
self.arg_max = None #Le col utilisé pour la propagation avant, qui est requis pour la propagation arrière_x Position de chaque ligne
def forward(self, x):
"""Propagation vers l'avant
Args:
x (numpy.ndarray):Entrée, forme(N, C, H, W)。
Returns:
numpy.ndarray:Sortie, forme(N, C, OH, OW)。
"""
N, C, H, W = x.shape # N:Nombre de données, C:Nombre de canaux, H:Hauteur, W:largeur
#Calcul de la taille de sortie
assert (H - self.pool_h) % self.stride == 0, 'OH n'est pas divisible!'
assert (W - self.pool_w) % self.stride == 0, 'OW est indivisible!'
OH = int((H - self.pool_h) / self.stride + 1)
OW = int((W - self.pool_w) / self.stride + 1)
#Développer et formater les données d'entrée
# (N, C, H, W) → (N * OH * OW, C * PH * PW)
col_x = im2col(x, self.pool_h, self.pool_w, self.stride, self.pad)
# (N * OH * OW, C * PH * PW) → (N * OH * OW * C, PH * PW)
col_x = col_x.reshape(-1, self.pool_h * self.pool_w)
#Calculer la sortie
# (N * OH * OW * C, PH * PW) → (N * OH * OW * C)
out = np.max(col_x, axis=1)
#Formater le résultat
# (N * OH * OW * C) → (N, OH, OW, C) → (N, C, OH, OW)
out = out.reshape(N, OH, OW, C).transpose(0, 3, 1, 2)
#Enregistrer pour la rétropropagation
self.x = x
self.arg_max = np.argmax(col_x, axis=1) # col_x Position maximale (index) de chaque ligne
return out
def backward(self, dout):
"""Rétropropagation
Args:
dout (numpy.ndarray):La valeur différentielle et la forme transmises par la bonne couche(N, C, OH, OW)。
Returns:
numpy.ndarray:Valeur de différenciation (gradient), forme(N, C, H, W)。
"""
#Façonner la valeur différentielle à partir du bon calque
# (N, C, OH, OW) → (N, OH, OW, C)
dout = dout.transpose(0, 2, 3, 1)
#Initialiser col pour la valeur différentielle résultante avec 0
# (N * OH * OW * C, PH * PW)
pool_size = self.pool_h * self.pool_w
dcol_x = np.zeros((dout.size, pool_size))
#Définissez la valeur différentielle de dout (= dout manma) uniquement à la position adoptée comme valeur maximale pendant la propagation vers l'avant.
#La position de la valeur qui n'a pas été adoptée lors de la propagation vers l'avant reste 0 à l'initialisation.
#(Identique au traitement lorsque x est supérieur à 0 et x est inférieur à 0 dans ReLU)
assert dout.size == self.arg_max.size, 'Col pendant la propagation vers l'avant_Ne correspond pas au nombre de lignes de x'
dcol_x[np.arange(self.arg_max.size), self.arg_max.flatten()] = \
dout.flatten()
#Formatage de la valeur différentielle résultante 1
# (N * OH * OW * C, PH * PW) → (N, OH, OW, C, PH * PW)
dcol_x = dcol_x.reshape(dout.shape + (pool_size,)) #Dernier','Indique un taple à un élément
#Formatage de la valeur différentielle résultante 2
# (N, OH, OW, C, PH * PW) → (N * OH * OW, C * PH * PW)
dcol_x = dcol_x.reshape(
dcol_x.shape[0] * dcol_x.shape[1] * dcol_x.shape[2], -1
)
#Formatage de la valeur différentielle résultante 3
# (N * OH * OW, C * PH * PW) → (N, C, H, W)
dx = col2im(
dcol_x, self.x.shape, self.pool_h, self.pool_w, self.stride, self.pad
)
return dx
Implémentez CNN en combinant les implémentations précédentes.
Commençons par organiser l'entrée et la sortie dans ce réseau.
couche | Forme d'entrée / sortie | Forme au moment du montage |
---|---|---|
$ (Taille du lot N,Nombre de canaux CH,Hauteur d'image H,Largeur W) $ | $ (100, 1, 28, 28) $ | |
:one: Convolution | ↓ | |
$ (Taille du lot N,Nombre de filtres FN,Hauteur de sortie OH,Largeur OW) $ | $ (100, 30, 24, 24) $ | |
:two: ReLU | ↓ | |
$ (Taille du lot N,Nombre de filtres FN,Hauteur de sortie OH,Largeur OW) $ | $ (100, 30, 24, 24) $ | |
:three: Pooling | ↓ | |
$ (Taille du lot N,Nombre de filtres FN,Hauteur de sortie OH,Largeur OW) $ | $ (100, 30, 12, 12) $ | |
:four: Affine | ↓ | |
$ (Taille du lot N,Taille du calque masqué) $ | $ (100, 100) $ | |
:five: ReLU | ↓ | |
$ (Taille du lot N,Taille du calque masqué) $ | $ (100, 100) $ | |
:six: Affine | ↓ | |
$ (Taille du lot N,Taille de sortie finale) $ | $ (100, 10) $ | |
:seven: Softmax | ↓ | |
$ (Taille du lot N,Taille de sortie finale) $ | $ (100, 10) $ |
La mise en œuvre de la couche Convlolution et de la couche Pooling est telle que décrite ci-dessus.
La couche Affine nécessite quelques modifications par rapport à l'implémentation précédente. Auparavant [5.6.2 Batch Affine Layer](https://qiita.com/segavvy/items/8707e4e65aa7fa357d8a#562-%E3%83%90%E3%83%83%E3%83%81%E7%89% Lorsqu'il est implémenté avec 88affine% E3% 83% AC% E3% 82% A4% E3% 83% A4), l'entrée était bidimensionnelle ($ taille du lot N $, taille de l'image), mais cette fois, la quatrième L'entrée de la couche Affine est de 4 dimensions (nombre de lots $ N $, nombre de filtres $ FN $, résultat de mise en commun $ OH $, $ OW $), il est donc nécessaire de s'en occuper. À la page 152 du livre, il y a une condition que «l'implémentation d'Affine dans common / layer.py est une implémentation qui considère le cas où les données d'entrée sont des tenseur (données 4D)». Je ne savais pas s'il était laissé sans surveillance, mais il était censé être utilisé cette fois.
Ce qui suit est une implémentation de la couche Affine qui prend également en charge la saisie de 3 dimensions ou plus.
affine.py
# coding: utf-8
import numpy as np
class Affine:
def __init__(self, W, b):
"""Couche affine
Args:
W (numpy.ndarray):poids
b (numpy.ndarray):biais
"""
self.W = W #poids
self.b = b #biais
self.x = None #Entrée (après 2D)
self.dW = None #Valeur différentielle du poids
self.db = None #Valeur différentielle du biais
self.original_x_shape = None #Forme d'entrée d'origine (pour l'entrée de 3 dimensions ou plus)
def forward(self, x):
"""Propagation vers l'avant
Args:
x (numpy.ndarray):contribution
Returns:
numpy.ndarray:production
"""
#Entrée bidimensionnelle de trois dimensions ou plus (tenseur)
self.original_x_shape = x.shape #Parce qu'il est nécessaire de sauvegarder la forme et de la restaurer par propagation arrière
x = x.reshape(x.shape[0], -1)
self.x = x
#Calculer la sortie
out = np.dot(x, self.W) + self.b
return out
def backward(self, dout):
"""Rétropropagation
Args:
dout (numpy.ndarray):Valeur de différenciation transmise depuis la bonne couche
Returns:
numpy.ndarray:Valeur de différenciation
"""
#Calcul de la valeur de différenciation
dx = np.dot(dout, self.W.T)
self.dW = np.dot(self.x.T, dout)
self.db = np.sum(dout, axis=0)
#Revenir à la forme d'origine
dx = dx.reshape(*self.original_x_shape)
return dx
Les couches ReLU et Softmax sont identiques à l'implémentation précédente, mais seront réimprimées.
relu.py
# coding: utf-8
class ReLU:
def __init__(self):
"""Couche ReLU
"""
self.mask = None
def forward(self, x):
"""Propagation vers l'avant
Args:
x (numpy.ndarray):contribution
Returns:
numpy.ndarray:production
"""
self.mask = (x <= 0)
out = x.copy()
out[self.mask] = 0
return out
def backward(self, dout):
"""Rétropropagation
Args:
dout (numpy.ndarray):Valeur de différenciation transmise depuis la bonne couche
Returns:
numpy.ndarray:Valeur de différenciation
"""
dout[self.mask] = 0
dx = dout
return dx
softmax_with_loss.py
# coding: utf-8
from functions import softmax, cross_entropy_error
class SoftmaxWithLoss:
def __init__(self):
"""Softmax-with-Couche de perte
"""
self.loss = None #perte
self.y = None #sortie de softmax
self.t = None #Données de l'enseignant (une-hot vector)
def forward(self, x, t):
"""Propagation vers l'avant
Args:
x (numpy.ndarray):contribution
t (numpy.ndarray):Données des enseignants
Returns:
float:Erreur d'entropie croisée
"""
self.t = t
self.y = softmax(x)
self.loss = cross_entropy_error(self.y, self.t)
return self.loss
def backward(self, dout=1):
"""Rétropropagation
Args:
dout (float, optional):La valeur différentielle transmise depuis la couche droite. La valeur par défaut est 1.
Returns:
numpy.ndarray:Valeur de différenciation
"""
batch_size = self.t.shape[0] #Nombre de lots
dx = (self.y - self.t) * (dout / batch_size)
return dx
Les fonctions requises pour implémenter la couche softmax sont également réimprimées comme précédemment. De plus, les fonctions qui ne sont pas utilisées cette fois sont supprimées.
functions.py
# coding: utf-8
import numpy as np
def softmax(x):
"""Fonction Softmax
Args:
x (numpy.ndarray):contribution
Returns:
numpy.ndarray:production
"""
#Pour le traitement par lots, x est(Nombre de lots, 10)Il devient un tableau bidimensionnel de.
#Dans ce cas, il est nécessaire de bien calculer pour chaque image en utilisant la diffusion.
#Ici, np afin qu'il puisse être partagé en 1 et 2 dimensions..max()Et np.sum()Est axe=-Calculé par 1
#Keepdims pour qu'il puisse être diffusé tel quel=Vrai pour maintenir la dimension.
c = np.max(x, axis=-1, keepdims=True)
exp_a = np.exp(x - c) #Mesures de débordement
sum_exp_a = np.sum(exp_a, axis=-1, keepdims=True)
y = exp_a / sum_exp_a
return y
def cross_entropy_error(y, t):
"""Calcul de l'erreur d'entropie croisée
Args:
y (numpy.ndarray):Sortie de réseau neuronal
t (numpy.ndarray):Étiquette de réponse correcte
Returns:
float:Erreur d'entropie croisée
"""
#S'il y a une donnée, formez-la (faites une ligne de données)
if y.ndim == 1:
t = t.reshape(1, t.size)
y = y.reshape(1, y.size)
#Calculer l'erreur et normaliser par le nombre de lots
batch_size = y.shape[0]
return -np.sum(t * np.log(y + 1e-7)) / batch_size
Pour l'optimiseur qui optimise les paramètres, voir 6.1 Paramètres de mise à jour (https://qiita.com/segavvy/items/ca4ac4c9ee1a126bff41#61-%E3%83%91%E3%83%A9%E3%83%] A1% E3% 83% BC% E3% 82% BF% E3% 81% AE% E6% 9B% B4% E6% 96% B0) J'ai sauté l'implémentation juste en lisant, j'ai donc décidé d'utiliser AdaGrad cette fois J'ai essayé de mettre en œuvre. C'est presque le même que le code du livre.
ada_grad.py
# coding: utf-8
import numpy as np
class AdaGrad:
def __init__(self, lr=0.01):
"""Optimisation des paramètres avec AdaGrad
Args:
lr (float, optional):Coefficient d'apprentissage, la valeur par défaut est 0.01。
"""
self.lr = lr
self.h = None #Somme des carrés du gradient jusqu'à présent
def update(self, params, grads):
"""Mise à jour des paramètres
Args:
params (dict):Le dictionnaire des paramètres à mettre à jour, la clé est'W1'、'b1'Tel.
grads (dict):Dictionnaire de dégradés correspondant aux paramètres
"""
#initialisation de h
if self.h is None:
self.h = {}
for key, val in params.items():
self.h[key] = np.zeros_like(val)
#mise à jour
for key in params.keys():
#mise à jour de h
self.h[key] += grads[key] ** 2
#Mise à jour des paramètres, dernière 1e-7 évite la division par 0
params[key] -= self.lr * grads[key] / (np.sqrt(self.h[key]) + 1e-7)
CNN précédemment [5.7.2 Implémentation d'un réseau neuronal pour la rétropropagation](https://qiita.com/segavvy/items/8707e4e65aa7fa357d8a#572-%E8%AA%A4%E5%B7%AE% E9% 80% 86% E4% BC% 9D% E6% 92% AD% E6% B3% 95% E3% 81% AB% E5% AF% BE% E5% BF% 9C% E3% 81% 97% E3% 81% 9F% E3% 83% 8B% E3% 83% A5% E3% 83% BC% E3% 83% A9% E3% 83% AB% E3% 83% 8D% E3% 83% 83% E3% 83% Basé sur le TwoLayerNet
fait avec 88% E3% 83% AF% E3% 83% BC% E3% 82% AF% E3% 81% AE% E5% AE% 9F% E8% A3% 85) Je l'ai implémenté selon les instructions.
Dans le code du livre, ʻOrderedDict est utilisé, mais comme la dernière fois, normal
dictest utilisé ici. En effet, à partir de Python 3.7, l'ordre d'insertion des objets
dict` est enregistré [^ 2]. Aussi, je suis tombé sur l'implémentation de «l'exactitude», donc je l'expliquerai plus tard.
Voici la mise en œuvre de CNN.
simple_conv_net.py
# coding: utf-8
import numpy as np
from affine import Affine
from convolution import Convolution
from pooling import Pooling
from relu import ReLU
from softmax_with_loss import SoftmaxWithLoss
class SimpleConvNet:
def __init__(
self, input_dim=(1, 28, 28),
conv_param={'filter_num': 30, 'filter_size': 5, 'pad': 0, 'stride': 1},
hidden_size=100, output_size=10, weight_init_std=0.01
):
"""Réseau neuronal convolutif simple
Args:
input_dim (tuple, optional):Forme des données d'entrée, par défaut(1, 28, 28)。
conv_param (dict, optional):Hyperparamètres de la couche de convolution,
La valeur par défaut est{'filter_num':30, 'filter_size':5, 'pad':0, 'stride':1}。
hidden_size (int, optional):Le nombre de neurones dans la couche cachée, la valeur par défaut est 100.
output_size (int, optional):Nombre de neurones dans la couche de sortie, par défaut 10.
weight_init_std (float, optional):Paramètre de réglage de la valeur initiale du poids. La valeur par défaut est 0.01。
"""
#Extraire les hyperparamètres de la couche de convolution
filter_num = conv_param['filter_num'] #Nombre de filtres
filter_size = conv_param['filter_size'] #Taille du filtre (même hauteur et largeur)
filter_stride = conv_param['stride'] #foulée
filter_pad = conv_param['pad'] #Rembourrage
#Les hyper paramètres de la couche de pooling sont fixes
pool_size = 2 #Taille (même hauteur et largeur)
pool_stride = 2 #foulée
pool_pad = 0 #Rembourrage
#Calcul de la taille des données d'entrée
input_ch = input_dim[0] #Nombre de canaux de données d'entrée
assert input_dim[1] == input_dim[2], 'Les données d'entrée sont supposées avoir la même hauteur et la même largeur!'
input_size = input_dim[1] #Taille des données d'entrée
#Calcul de la taille de sortie de la couche de convolution
assert (input_size + 2 * filter_pad - filter_size) \
% filter_stride == 0, 'La taille de sortie du calque de pliage n'est pas divisible!'
conv_output_size = int(
(input_size + 2 * filter_pad - filter_size) / filter_stride + 1
)
#Calcul de la taille de sortie de la couche de pooling
assert (conv_output_size - pool_size) % pool_stride == 0, \
'La taille de sortie de la couche de pooling n'est pas divisible!'
pool_output_size_one = int(
(conv_output_size - pool_size) / pool_stride + 1 #Taille hauteur / largeur
)
pool_output_size = filter_num * \
pool_output_size_one * pool_output_size_one #Taille totale de tous les filtres
#Initialisation du poids
self.params = {}
#Couche pliante
self.params['W1'] = weight_init_std * \
np.random.randn(filter_num, input_ch, filter_size, filter_size)
self.params['b1'] = np.zeros(filter_num)
#Couche affine 1
self.params['W2'] = weight_init_std * \
np.random.randn(pool_output_size, hidden_size)
self.params['b2'] = np.zeros(hidden_size)
#Couche affine 2
self.params['W3'] = weight_init_std * \
np.random.randn(hidden_size, output_size)
self.params['b3'] = np.zeros(output_size)
#Génération de couches
self.layers = {} # Python 3.OrderedDict n'est pas nécessaire car l'ordre de stockage du dictionnaire est conservé à partir de 7
#Couche pliante
self.layers['Conv1'] = Convolution(
self.params['W1'], self.params['b1'], filter_stride, filter_pad
)
self.layers['Relu1'] = ReLU()
self.layers['Pool1'] = Pooling(
pool_size, pool_size, pool_stride, pool_pad
)
#Couche affine 1
self.layers['Affine1'] = \
Affine(self.params['W2'], self.params['b2'])
self.layers['Relu2'] = ReLU()
#Couche affine 2
self.layers['Affine2'] = \
Affine(self.params['W3'], self.params['b3'])
self.lastLayer = SoftmaxWithLoss()
def predict(self, x):
"""Inférence par réseau neuronal
Args:
x (numpy.ndarray):Entrée dans le réseau neuronal
Returns:
numpy.ndarray:Sortie de réseau neuronal
"""
#Propager les couches vers l'avant
for layer in self.layers.values():
x = layer.forward(x)
return x
def loss(self, x, t):
"""Calcul de la valeur de la fonction de perte
Args:
x (numpy.ndarray):Entrée dans le réseau neuronal
t (numpy.ndarray):Étiquette de réponse correcte
Returns:
float:Valeur de la fonction de perte
"""
#inférence
y = self.predict(x)
# Softmax-with-Calculé par propagation directe de la couche de perte
loss = self.lastLayer.forward(y, t)
return loss
def accuracy(self, x, t, batch_size=100):
"""Calcul de la précision de la reconnaissance
batch_size est la taille du lot au moment du calcul. Lorsque vous essayez de calculer une grande quantité de données à la fois
Parce que im2col consomme trop de mémoire et que des barres obliques se produisent et que cela ne fonctionne pas
Pour éviter ça.
Args:
x (numpy.ndarray):Entrée dans le réseau neuronal
t (numpy.ndarray):Étiquette de réponse correcte (une-hot)
batch_size (int), optional):Taille du lot au moment du calcul, la valeur par défaut est 100.
Returns:
float:Précision de reconnaissance
"""
#Calcul du nombre de divisions
batch_num = max(int(x.shape[0] / batch_size), 1)
#Divisé
x_list = np.array_split(x, batch_num, 0)
t_list = np.array_split(t, batch_num, 0)
#Processus en unités divisées
correct_num = 0 #Nombre total de bonnes réponses
for (sub_x, sub_t) in zip(x_list, t_list):
assert sub_x.shape[0] == sub_t.shape[0], 'La frontière de division a-t-elle changé?'
y = self.predict(sub_x)
y = np.argmax(y, axis=1)
t = np.argmax(sub_t, axis=1)
correct_num += np.sum(y == t)
#Calcul de la précision de la reconnaissance
return correct_num / x.shape[0]
def gradient(self, x, t):
"""Calculer le gradient pour le paramètre de poids par la méthode de propagation de l'erreur en retour
Args:
x (numpy.ndarray):Entrée dans le réseau neuronal
t (numpy.ndarray):Étiquette de réponse correcte
Returns:
dictionary:Un dictionnaire qui stocke les dégradés
"""
#Propagation vers l'avant
self.loss(x, t) #Propager vers l'avant pour calculer la valeur de la perte
#Rétropropagation
dout = self.lastLayer.backward()
for layer in reversed(list(self.layers.values())):
dout = layer.backward(dout)
#Extraire la valeur différentielle de chaque couche
grads = {}
grads['W1'] = self.layers['Conv1'].dW
grads['b1'] = self.layers['Conv1'].db
grads['W2'] = self.layers['Affine1'].dW
grads['b2'] = self.layers['Affine1'].db
grads['W3'] = self.layers['Affine2'].dW
grads['b3'] = self.layers['Affine2'].db
return grads
La pierre d'achoppement dans cette implémentation est la «précision», qui est omise dans le livre.
Pendant l'apprentissage, la précision de la reconnaissance est calculée en unités d'une époque, mais dans le code écrit au chapitre 4, 60 000 éléments de données d'apprentissage ont été ajoutés en même temps pour obtenir la précision de la reconnaissance. Cependant, si je fais la même chose cette fois, il semble que l'expansion de ʻim2col` consomme beaucoup de mémoire, et ma VM de 4 Go de mémoire s'arrête à slashing [^ 3]: sueur:
Cependant, la source du livre consomme toujours moins de mémoire et fonctionne normalement dans mon environnement. C'est étrange, donc quand j'ai suivi la source, elle a été divisée et traitée en interne. C'est pourquoi je l'imite et la divise également en interne. De plus, essayez d'utiliser numpy.array_split
pour implémenter le fractionnement. J'ai fait.
L'apprentissage est le précédent [5.7.4 Apprentissage utilisant la méthode de propagation des erreurs de retour](https://qiita.com/segavvy/items/8707e4e65aa7fa357d8a#574-%E8%AA%A4%E5%B7%AE%E9% 80% 86% E4% BC% 9D% E6% 92% AD% E6% B3% 95% E3% 82% 92% E4% BD% BF% E3% 81% A3% E3% 81% 9F% E5% AD% Mis en œuvre sur la base de A6% E7% BF% 92). Voici quelques points.
flatten = False
lors de la lecture des données MNIST avec load_mnist
.
--L'hyper paramètre learning_rate
a été réduit pour AdaGrad et a été essayé plusieurs fois pour le rendre 0.06
.Voici la mise en œuvre de l'apprentissage.
mnist.py
# coding: utf-8
import os
import sys
import matplotlib.pylab as plt
import numpy as np
from ada_grad import AdaGrad
from simple_conv_net import SimpleConvNet
sys.path.append(os.pardir) #Ajouter le répertoire parent au chemin
from dataset.mnist import load_mnist
#Lire les données d'entraînement MNIST et les données de test
(x_train, t_train), (x_test, t_test) = \
load_mnist(normalize=True, flatten=False, one_hot_label=True)
#Paramètres des hyper paramètres
iters_num = 6000 #Nombre de mises à jour
batch_size = 100 #Taille du lot
learning_rate = 0.06 #En supposant le taux d'apprentissage, AdaGrad
train_size = x_train.shape[0] #Taille des données d'entraînement
iter_per_epoch = max(int(train_size / batch_size), 1) #Nombre d'itérations par époque
#Génération simple de réseau neuronal convolutif
network = SimpleConvNet(
input_dim=(1, 28, 28),
conv_param={'filter_num': 30, 'filter_size': 5, 'pad': 0, 'stride': 1},
hidden_size=100, output_size=10, weight_init_std=0.01
)
#Génération d'optimiseur
optimizer = AdaGrad(learning_rate) # AdaGrad
#Confirmation de la précision de la reconnaissance avant l'apprentissage
train_acc = network.accuracy(x_train, t_train)
test_acc = network.accuracy(x_test, t_test)
train_loss_list = [] #Emplacement de stockage de la transition de la valeur de la fonction de perte
train_acc_list = [train_acc] #Emplacement de stockage des modifications de la précision de la reconnaissance des données d'entraînement
test_acc_list = [test_acc] #Destination de stockage de la transition de la précision de reconnaissance pour les données de test
print(f'Avant d'apprendre[Reconnaissance de l'exactitude des données d'entraînement]{train_acc:.4f} [Précision de reconnaissance des données de test]{test_acc:.4f}')
#Commencer à apprendre
for i in range(iters_num):
#Mini génération de lots
batch_mask = np.random.choice(train_size, batch_size, replace=False)
x_batch = x_train[batch_mask]
t_batch = t_train[batch_mask]
#Calcul du gradient
grads = network.gradient(x_batch, t_batch)
#Mise à jour des paramètres de poids
optimizer.update(network.params, grads)
#Calcul de la valeur de la fonction de perte
loss = network.loss(x_batch, t_batch)
train_loss_list.append(loss)
#Calcul de la précision de la reconnaissance pour chaque époque
if (i + 1) % iter_per_epoch == 0:
train_acc = network.accuracy(x_train, t_train)
test_acc = network.accuracy(x_test, t_test)
train_acc_list.append(train_acc)
test_acc_list.append(test_acc)
#Affichage de la progression
print(
f'[époque]{(i + 1) // iter_per_epoch:>2} '
f'[Nombre de mises à jour]{i + 1:>5} [Valeur de la fonction de perte]{loss:.4f} '
f'[Reconnaissance de l'exactitude des données d'entraînement]{train_acc:.4f} [Précision de reconnaissance des données de test]{test_acc:.4f}'
)
#Tracez la transition de la valeur de la fonction de perte
x = np.arange(len(train_loss_list))
plt.plot(x, train_loss_list, label='loss')
plt.xlabel('iteration')
plt.ylabel('loss')
plt.xlim(left=0)
plt.ylim(0, 2.5)
plt.show()
#Dessinez la transition de la précision de reconnaissance des données d'entraînement et des données de test
x2 = np.arange(len(train_acc_list))
plt.plot(x2, train_acc_list, label='train acc')
plt.plot(x2, test_acc_list, label='test acc', linestyle='--')
plt.xlabel('epochs')
plt.ylabel('accuracy')
plt.xlim(left=0)
plt.ylim(0, 1.0)
plt.legend(loc='lower right')
plt.show()
Voici les résultats de l'exécution. Cela a pris environ une heure dans mon environnement.
Avant d'apprendre[Reconnaissance de l'exactitude des données d'entraînement]0.0909 [Précision de reconnaissance des données de test]0.0909
[époque] 1 [Nombre de mises à jour] 600 [Valeur de la fonction de perte]0.0699 [Reconnaissance de l'exactitude des données d'entraînement]0.9784 [Précision de reconnaissance des données de test]0.9780
[époque] 2 [Nombre de mises à jour] 1200 [Valeur de la fonction de perte]0.0400 [Reconnaissance de l'exactitude des données d'entraînement]0.9844 [Précision de reconnaissance des données de test]0.9810
[époque] 3 [Nombre de mises à jour] 1800 [Valeur de la fonction de perte]0.0362 [Reconnaissance de l'exactitude des données d'entraînement]0.9885 [Précision de reconnaissance des données de test]0.9853
[époque] 4 [Nombre de mises à jour] 2400 [Valeur de la fonction de perte]0.0088 [Reconnaissance de l'exactitude des données d'entraînement]0.9907 [Précision de reconnaissance des données de test]0.9844
[époque] 5 [Nombre de mises à jour] 3000 [Valeur de la fonction de perte]0.0052 [Reconnaissance de l'exactitude des données d'entraînement]0.9926 [Précision de reconnaissance des données de test]0.9851
[époque] 6 [Nombre de mises à jour] 3600 [Valeur de la fonction de perte]0.0089 [Reconnaissance de l'exactitude des données d'entraînement]0.9932 [Précision de reconnaissance des données de test]0.9850
[époque] 7 [Nombre de mises à jour] 4200 [Valeur de la fonction de perte]0.0029 [Reconnaissance de l'exactitude des données d'entraînement]0.9944 [Précision de reconnaissance des données de test]0.9865
[époque] 8 [Nombre de mises à jour] 4800 [Valeur de la fonction de perte]0.0023 [Reconnaissance de l'exactitude des données d'entraînement]0.9954 [Précision de reconnaissance des données de test]0.9873
[époque] 9 [Nombre de mises à jour] 5400 [Valeur de la fonction de perte]0.0051 [Reconnaissance de l'exactitude des données d'entraînement]0.9959 [Précision de reconnaissance des données de test]0.9860
[époque]10 [Nombre de mises à jour] 6000 [Valeur de la fonction de perte]0.0037 [Reconnaissance de l'exactitude des données d'entraînement]0.9972 [Précision de reconnaissance des données de test]0.9860
En conséquence, la précision de reconnaissance des données d'apprentissage était de 99,72% et la précision de reconnaissance des données de test était de 98,60%. Avec une époque, il a déjà dépassé la précision de reconnaissance précédente. Étant donné que la précision de reconnaissance des données de test n'a pas changé depuis environ 7 époques, il se peut que cela ait simplement été un apprentissage excessif après cela. Même ainsi, la précision de 98,60% avec un simple CNN est incroyable.
J'ai également essayé d'exécuter la source du livre, mais pour une raison quelconque, le calcul de la précision de reconnaissance pour chaque époque est très rapide. Mystérieusement, j'ai trouvé qu'il était possible d'échantillonner avec le paramètre ʻevaluate_sample_num_per_epoch de la classe
Trainer`, et l'image de formation et l'image de test ont été calculées avec seulement les 1000 premières images. injuste! : non amusé:
Il est étonnant que les filtres nécessaires tels que l'extraction des bords et des objets blob soient automatiquement créés. Il est très intéressant que le degré d'abstraction augmente au fur et à mesure que les couches se superposent.
On dit que le big data et le GPU apportent une grande contribution au développement du deep learning, mais je pense que la diffusion du cloud, qui permet d'utiliser d'énormes ressources machines à faible coût, est également un gros point.
C'est aussi un aparté complet, mais j'ai été profondément ému par le fait que la proposition du LeNet remonte à 1998, il y a 20 ans, et que 1998 était une impression plus récente. Je ne veux pas vieillir: transpirer:
C'était un peu difficile à mettre en œuvre, mais grâce à cela, j'ai compris CNN. C'est tout pour ce chapitre. Si vous avez des erreurs, je vous serais reconnaissant de bien vouloir les signaler.
(Vers d'autres chapitres de ce mémo: Chapitre 1 / Chapitre 2 / Chapitre 3 / Chapitre 4 / [Chapitre 5](https: // qiita. com / segavvy / items / 8707e4e65aa7fa357d8a) / Chapitre 6 / Chapitre 7 / Chapitre 8 / Résumé)
[^ 1]: [Modifications de la longue note à la fin des mots étrangers et des termes katakana dans les produits et services Microsoft](https://web.archive.org/web/20130228002415/http://www.microsoft.com/japan/ presspass / detail.aspx? newsid = 3491) (* Puisqu'il n'y a plus de pages à ce moment-là, Wikipedia> Notes longues B3% E7% AC% A6) est également un lien vers la Wayback Machine de l'Internet Archive)
[^ 2]: Voir "Amélioration du modèle de données Python" dans Nouveautés de Python 3.7.
[^ 3]: La barre oblique est un phénomène qui se produit lorsque la mémoire est insuffisante, et elle est gênante car elle peut devenir inopérante pour chaque système d'exploitation. Si vous êtes intéressé par la gestion de la mémoire du système d'exploitation, veuillez consulter l '[Introduction à la gestion de la mémoire pour tous: 01] précédemment publiée (https://qiita.com/segavvy/items/9a9f8aa5cc4e6760307a)! : sourire: