- Introduction au cadre de travail
OpenMV offre actuellement uniquement la conversion des modèles Caffe vers le format network. Bien que TensorFlow pourrait être supporté à l'avenir, cette fonctionnalité n'est pas encore disponible. L'objectif final de l'entraînement avec Caffe est d'obtenir un fichier *.network qui peut être exécuté sur les caméras OpenMV.
Les étapes principales pour entraîner un réseau neuronal sont les suivantes :
-
Configuration de l'environnement et installation de Caffe
-
Collecte et préparation des données
-
Entraînement du réseau
-
Quantification du modèle
-
Conversion en format binaire
-
Déploiement sur OpenMV
-
Exécution du réseau
-
Dépannage
-
Configuration de l'environnement
La configuration utilisée dans cet article est la suivante :
- Windows 10
- Python 2.7
- Pycharm
- Visual Studio 2013
- OpenMV Cam H4
- OpenMV IDE
- Compilation de Caffe avec VS2013
Pour la compilation de Caffe avec Visual Studio 2013, veuillez consulter la documentation spécialisée disponible en ligne.
- Création de la base de données
Important : Les expériences précédentes ont montré que l'utilisation d'images de 64×64 pixels produit des modèles network trop volumineux pour OpenMV, causant des débordements de mémoire. Par conséquent, cet article utilise des images de 32×32 pixels.
La base de données est créée avec Paint et undergoes un traitement d'augmentation pour enrichir les données.
4.1 Création des dossiers pour les données originales
Le code suivant crée la structure de dossiers pour stocker les données brutes :
import os
import sys
def creer_dossiers():
base = 'E:/pydoc/blog/MY_numbers/'
i = 0
noms = ['ZERO', 'ONE', 'TWO', 'THREE', 'FOUR', 'FIVE', 'SIX', 'SEVEN', 'EIGHT', 'NINE']
for j in range(10):
nom_dossier = base + noms[i]
os.mkdir(nom_dossier)
i = i + 1
creer_dossiers()
Après exécution, la structure de dossiers suivante est créée :
4.2 Création des dossiers pour les données augmentées
import os
import sys
def creer_dossiers():
base = 'E:/pydoc/blog/MY_numbers/'
i = 0
noms = ['ZERO', 'ONE', 'TWO', 'THREE', 'FOUR', 'FIVE', 'SIX', 'SEVEN', 'EIGHT', 'NINE']
for j in range(10):
nom_dossier = base + str(i) + '_' + noms[i]
os.mkdir(nom_dossier)
i = i + 1
creer_dossiers()
4.3 Constitution de la base de données originale
En utilisant Paint, créez des images manuscrites de chiffres de 0 à 9. Chaque chiffre nécessite 5 images de taille 32×32 pixels. Enregistrez ces images dans les dossiers correspondants ./MY_numbers/ZERO, ./MY_numbers/ONE, etc.
4.4 Augmentation des données
Le script suivant augmente la base de données en appliquant diverses transformations :
import os
import sys
import argparse
import random
import cv2
import numpy as np
import imgaug as ia
from imgaug import augmenters as iaa
from tqdm import tqdm
def principale():
parser = argparse.ArgumentParser(description='Augmenter les images du dataset')
parser.add_argument("--input", action="store", help="Répertoire d'entrée")
parser.add_argument("--output", action="store", help="Répertoire de sortie")
parser.add_argument("--count", action="store", type=int, default=1, help="Nombre d'ensembles augmentés")
args = parser.parse_args()
if args.input is None or args.output is None:
parser.print_help()
sys.exit(1)
ia.seed(1)
chemins = os.listdir(args.input)
for x in range(args.count):
sequence = iaa.Sequential([
iaa.Fliplr(0.5),
iaa.Sometimes(0.5, iaa.GaussianBlur(sigma=(0, 0.2))),
iaa.Sometimes(0.5, iaa.AdditiveGaussianNoise(loc=0, scale=(0.0, 0.005*255), per_channel=0.5)),
iaa.Sometimes(0.5, iaa.Multiply((0.8, 1.2), per_channel=0.0)),
iaa.Sometimes(0.5, iaa.Affine(rotate=(-20, 20))),
iaa.Sometimes(0.5, iaa.Affine(scale={"x": (0.8, 1.2), "y": (0.8, 1.2)})),
iaa.Sometimes(0.5, iaa.Affine(translate_percent={"x": (-0.1, 0.1), "y": (-0.1, 0.1)})),
], random_order=True)
print("Augmentation de l'ensemble %d/%d" % (x+1, args.count))
for i in tqdm(range(len(chemins))):
img = cv2.imread(args.input + '/' + chemins[i], cv2.IMREAD_GRAYSCALE)
img = sequence.augment_image(img)
f = os.path.splitext(chemins[i])
cv2.imwrite(args.output + '/' + f[0] + '_aug%d' % x + f[1], img)
print('Traitement terminé\n')
if __name__ == '__main__':
principale()
4.5 Exécution du script d'augmentation
Créez un fichier batch nommé augment_pic.bat :
python augment_images.py --input MY_numbers/ZERO/ --output MY_numbers/0_ZERO/ --count 20
echo.
python augment_images.py --input MY_numbers/ONE/ --output MY_numbers/1_ONE/ --count 20
echo.
python augment_images.py --input MY_numbers/TWO/ --output MY_numbers/2_TWO/ --count 20
echo.
python augment_images.py --input MY_numbers/THREE/ --output MY_numbers/3_THREE/ --count 20
echo.
python augment_images.py --input MY_numbers/FOUR/ --output MY_numbers/4_FOUR/ --count 20
echo.
python augment_images.py --input MY_numbers/FIVE/ --output MY_numbers/5_FIVE/ --count 20
echo.
python augment_images.py --input MY_numbers/SIX/ --output MY_numbers/6_SIX/ --count 20
echo.
python augment_images.py --input MY_numbers/SEVEN/ --output MY_numbers/7_SEVEN/ --count 20
echo.
python augment_images.py --input MY_numbers/EIGHT/ --output MY_numbers/8_EIGHT/ --count 20
echo.
python augment_images.py --input MY_numbers/NINE/ --output MY_numbers/9_NINE/ --count 20
pause
4.6 Organisation finale
Créez un dossier ./blog/data et copiez-y les dossiers de données augmentées. Utilisez la commande tree pour visualiser la structure finale des fichiers.
- Création des étiquettes
5.1 Création des étiquettes LMDB
5.1.1 Script de création des étiquettes
Créez le fichier create_labels.py :
# coding=utf-8
import os
import sys
import argparse
import random
import numpy as np
from tqdm import tqdm
import time
import shutil
def melanger_tableaux(a, b):
assert len(a) == len(b)
tableau_a = np.empty(a.shape, dtype=a.dtype)
tableau_b = np.empty(b.shape, dtype=b.dtype)
permutation = np.random.permutation(len(a))
for ancien_index, nouveau_index in enumerate(permutation):
tableau_a[nouveau_index] = a[ancien_index]
tableau_b[nouveau_index] = b[ancien_index]
return tableau_a, tableau_b
def deplacer_fichiers(entree, sortie):
index = -1
for racine, dossiers, fichiers in os.walk(entree):
if index != -1:
print('Traitement du chemin', racine)
print('Index du chemin', index)
compteur = 0
for fichier in (fichiers if index == -1 else tqdm(fichiers)):
nom, extension = os.path.splitext(fichier)
if extension in ['.jpg', '.JPG', '.png', '.PNG']:
chemin_complet = os.path.join(racine, fichier)
if os.path.isfile(chemin_complet):
fichier = os.path.basename(os.path.normpath(racine)) + str(compteur) + extension
try:
test = int(fichier.split('_')[0])
except:
fichier = str(index) + '_' + fichier
shutil.copy(chemin_complet, os.path.join(sortie, fichier))
compteur += 1
index += 1
def creer_fichiers_texte(chemin, pourcentage):
images, etiquettes = [], []
os.chdir(chemin)
for element in os.listdir('.'):
if not os.path.isfile(os.path.join('.', element)):
continue
try:
etiquette = int(element.split('_')[0])
images.append(element)
etiquettes.append(etiquette)
except:
continue
images = np.array(images)
etiquettes = np.array(etiquettes)
images, etiquettes = melanger_tableaux(images, etiquettes)
X_train = images[0:int(len(images) * pourcentage)]
y_train = etiquettes[0:int(len(etiquettes) * pourcentage)]
X_test = images[int(len(images) * pourcentage):]
y_test = etiquettes[int(len(etiquettes) * pourcentage):]
os.chdir('..')
fichier_train = open("train.txt", "w")
for i, l in zip(X_train, y_train):
fichier_train.write(i + " " + str(l) + "\n")
fichier_test = open("test.txt", "w")
for i, l in zip(X_test, y_test):
fichier_test.write(i + " " + str(l) + "\n")
fichier_train.close()
fichier_test.close()
def principale():
parser = argparse.ArgumentParser(description='Créer les fichiers étiquettes')
parser.add_argument("--input", action="store", help="Répertoire d'entrée")
parser.add_argument("--output", action="store", help="Répertoire de sortie")
parser.add_argument("--percentage", action="store", type=float, default=0.85, help="Ratio test/ entrainement")
args = parser.parse_args()
if args.input is None or args.output is None:
parser.print_help()
sys.exit(1)
deplacer_fichiers(args.input, args.output)
creer_fichiers_texte(args.output, args.percentage)
print('Traitement terminé\n')
if __name__ == '__main__':
principale()
5.1.2 Génération des listes
Créez le dossier ./blog/lmdbin, puis le fichier batch create_lists.bat :
python create_labels.py --input data/ --output lmdbin/
pause
Après exécution, les fichiers train.txt et test.txt sont générés.
5.1.3 Génération du format LMDB
La génération des données au format LMDB nécessite l'utilisation des fonctions intégrées à Caffe. Ces fonctions doivent être exécutées depuis le dossier où Caffe a été compilé.
Syntaxe de la commande :
convert_imageset [FLAGS] ROOTFOLDER/ LISTFILE DB_NAME
Description des paramètres FLAGS :
- gray : Ouvrir les images en niveaux de gris (par défaut : false)
- shuffle : Méler aléatoirement l'ordre des images (par défaut : false)
- backend : Format de sortie (leveldb ou lmdb, par défaut : lmdb)
- resize_width/resize_height : Redimensionner les images (par défaut : 0)
- check_size : Vérifier que toutes les images ont la même taille (par défaut : false)
- encoded : Encoder l'image originale dans les données finales (par défaut : false)
- encode_type : Format d'encodage ('png', 'jpg')
Exécution du script de conversion (depuis ./caffe-master/Build/x64/Release/) :
convert_imageset --shuffle E:/pydoc/blog/lmdbin/ E:/pydoc/blog/train.txt E:/pydoc/blog/train_lmdb
echo.
convert_imageset --shuffle E:/pydoc/blog/lmdbin/ E:/pydoc/blog/test.txt E:/pydoc/blog/test_lmdb
Les dossiers test_lmdb et train_lmdb sont créés avec les paquets de données.
5.1.4 Génération du fichier moyen
La soustraction de la moyenne des images avant l'entraînement améliore la vitesse et la précision. Exécutez le script suivant (depuis ./caffe-master/Build/x64/Release/) :
compute_image_mean -backend=lmdb E:/pydoc/blog/train_lmdb mean.binaryproto
pause
- Entraînement du réseau neuronal
6.1 Préparation des fichiers proottxt
Téléchargez openmv-master et extrayez-le dans le dossier ./blog. Naviguez vers openmv-master\ml\cmsisnn\models\lenet. Vous trouverez :
- lenet.network (fichier binaire pour OpenMV)
- lenet_solver.prototxt (paramètres d'entraînement pour Caffe)
- lenet_train_test.prototxt (paramètres des couches)
- test.sh (script de test)
- train.sh (script d'entraînement)
Copiez les quatre derniers fichiers dans le répertoire principal.
6.2 Modification des paramètres d'entraînement
6.2.1 Configuration de lenet_solver.prototxt
Modifiez les éléments suivants :
- net : Chemin vers le fichier lenet_train_test.prototxt
- test_iter : Nombre de tests par itération = données totales / batch_size
- max_iter : Nombre maximum d'itérations
- snapshot : Fréquence des instantanés
- snapshot_prefix : Répertoire de sauvegarde
- solver_mode : CPU ou GPU
6.2.2 Configuration de lenet_train_test.prototxt
name: "LeNet"
layer {
name: "mnist"
type: "Data"
top: "data"
top: "label"
include {
phase: TRAIN
}
transform_param {
scale: 0.00390625
mean_file: "mean.binaryproto"
}
data_param {
source: "train_lmdb"
batch_size: 32
backend: LMDB
}
}
layer {
name: "mnist"
type: "Data"
top: "data"
top: "label"
include {
phase: TEST
}
transform_param {
scale: 0.00390625
mean_file: "mean.binaryproto"
}
data_param {
source: "test_lmdb"
batch_size: 10
backend: LMDB
}
}
layer {
name: "conv1"
type: "Convolution"
bottom: "data"
top: "conv1"
param { lr_mult: 1 }
param { lr_mult: 2 }
convolution_param {
num_output: 20
kernel_size: 5
stride: 1
weight_filler { type: "xavier" }
bias_filler { type: "constant" }
}
}
layer {
name: "pool1"
type: "Pooling"
bottom: "conv1"
top: "pool1"
pooling_param {
pool: MAX
kernel_size: 2
stride: 2
}
}
layer {
name: "conv2"
type: "Convolution"
bottom: "pool1"
top: "conv2"
param { lr_mult: 1 }
param { lr_mult: 2 }
convolution_param {
num_output: 50
kernel_size: 5
stride: 1
weight_filler { type: "xavier" }
bias_filler { type: "constant" }
}
}
layer {
name: "pool2"
type: "Pooling"
bottom: "conv2"
top: "pool2"
pooling_param {
pool: MAX
kernel_size: 2
stride: 2
}
}
layer {
name: "fc1"
type: "InnerProduct"
bottom: "pool2"
top: "fc1"
param { lr_mult: 1 }
param { lr_mult: 2 }
inner_product_param {
num_output: 100
weight_filler { type: "xavier" }
bias_filler { type: "constant" }
}
}
layer {
name: "relu1"
type: "ReLU"
bottom: "fc1"
top: "fc1"
}
layer {
name: "fc2"
type: "InnerProduct"
bottom: "fc1"
top: "fc2"
param { lr_mult: 1 }
param { lr_mult: 2 }
inner_product_param {
num_output: 10
weight_filler { type: "xavier" }
bias_filler { type: "constant" }
}
}
layer {
name: "accuracy"
type: "Accuracy"
bottom: "fc2"
bottom: "label"
top: "accuracy"
include { phase: TEST }
}
layer {
name: "loss"
type: "SoftmaxWithLoss"
bottom: "fc2"
bottom: "label"
top: "loss"
}
6.3 Entraînement du modèle
6.3.1 Script d'entraînement
caffe train --solver=E:/pydoc/blog/lenet_solver.prototxt
pause
6.3.2 Script de test
caffe test --model=E:/pydoc/blog/lenet_train_test.prototxt --weights=E:/pydoc/blog/lenet/_iter_500.caffemodel
pause
Le modèle atteint une précision de 87,8%.
- Génération du fichier binaire OpenMV
7.1 Préparation des scripts
Copiez les fichiers nn_quantizer.py et nn_convert.py depuis ./openmv-master/ml/cmsisnn vers le répertoire ./blog.
7.2 Quantification du modèle
python nn_quantizer.py --cpu --model E:/pydoc/blog/lenet_train_test.prototxt --weights E:/pydoc/blog/lenet/_iter_500.caffemodel --save E:/pydoc/blog/lenet/output.pkl
pause
7.3 Conversion finale
python nn_convert.py --model E:/pydoc/blog/lenet/output.pkl --mean E:/pydoc/blog/mean.binaryproto --output E:/pydoc/blog/lenet/output.network
pause
Le fichier output.network est maintenant prêt à être déployé sur OpenMV Cam.