Entraînement de modèles neuronaux pour OpenMV avec Caffe

  1. 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 :

  1. Configuration de l'environnement et installation de Caffe

  2. Collecte et préparation des données

  3. Entraînement du réseau

  4. Quantification du modèle

  5. Conversion en format binaire

  6. Déploiement sur OpenMV

  7. Exécution du réseau

  8. Dépannage

  9. 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
  1. Compilation de Caffe avec VS2013

Pour la compilation de Caffe avec Visual Studio 2013, veuillez consulter la documentation spécialisée disponible en ligne.

  1. 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.

  1. 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

  1. 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%.

  1. 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.

Étiquettes: caffe openmv deep-learning neural-network Computer-Vision

Publié le 28 juin à 01h21