Ceci est l'article sur le 15e jour du Calendrier de l'Avent Chainer 2016 (je suis désolé d'être en retard ...).
Dans cet article, je me suis demandé: "Je peux m'entraîner en exécutant l'exemple de Chainer, mais comment puis-je entraîner l'ensemble de données d'origine et rendre le modèle pratique comme un serveur API Web?" Ce sera un article pour ceux qui le sont (je pense que c'est relativement pour les débutants).
Ici, nous allons créer un serveur API pour la classification d'images basé sur l'exemple de code imagenet dans le référentiel officiel Chainer GitHub. Est le but final.
Dans cet article, nous avons développé dans l'environnement suivant.
~~ Il était difficile de collecter des données, donc par souci de simplicité, construisons un réseau neuronal qui classe trois types d'animaux: les chiens, les chats et les lapins.
La première étape consiste à collecter des images. Cette fois, nous avons collecté des images d'animaux à partir des sites suivants.
Stockez les images collectées dans le dossier d'origine avec la configuration indiquée dans la capture d'écran (en fait, vous devez collecter un plus grand nombre d'images).
De plus, le fichier suivant gère la correspondance entre les animaux et les étiquettes. Pour chaque classe, décrivez l'ID (nom du dossier pour stocker les images), le nom d'affichage de la classe et le libellé (index dans le vecteur de sortie du réseau neuronal) séparés par des espaces demi-largeur.
label_master.txt
000_chien chien 0
001_chat chat 1
002_lapin lapin 2
L'imagenet de Chainer semble être censé gérer des images 256x256, donc redimensionnez les images collectées. De plus, lors de l'entraînement avec imagenet, vous aurez besoin d'un fichier texte décrivant le chemin de l'image et le libellé de cette image, alors créez-le ici.
preprocess.py
# coding: utf-8
import os
import shutil
import re
import random
import cv2
import numpy as np
WIDTH = 256 #Largeur après redimensionnement
HEIGHT = 256 #Hauteur après redimensionnement
SRC_BASE_PATH = './original' #Répertoire de base contenant les images téléchargées
DST_BASE_PATH = './resized' #Répertoire de base pour stocker des images redimensionnées
LABEL_MASTER_PATH = 'label_master.txt' #Un fichier qui résume la correspondance entre les classes et les étiquettes
TRAIN_LABEL_PATH = 'train_label.txt' #Fichier d'étiquette pour l'apprentissage
VAL_LABEL_PATH = 'val_label.txt' #Fichier d'étiquette pour vérification
VAL_RATE = 0.2 #Pourcentage de données de validation
if __name__ == '__main__':
with open(LABEL_MASTER_PATH, 'r') as f:
classes = [line.strip().split(' ') for line in f.readlines()]
#Initialiser l'emplacement de stockage de l'image après le redimensionnement
if os.path.exists(DST_BASE_PATH):
shutil.rmtree(DST_BASE_PATH)
os.mkdir(DST_BASE_PATH)
train_dataset = []
val_dataset = []
for c in classes:
os.mkdir(os.path.join(DST_BASE_PATH, c[0]))
class_dir_path = os.path.join(SRC_BASE_PATH, c[0])
#Obtenez uniquement des images JPEG ou PNG
files = [
file for file in os.listdir(class_dir_path)
if re.search(r'\.(jpe?g|png)$', file, re.IGNORECASE)
]
#Redimensionner et exporter le fichier
for file in files:
src_path = os.path.join(class_dir_path, file)
image = cv2.imread(src_path)
resized_image = cv2.resize(image, (WIDTH, HEIGHT))
cv2.imwrite(os.path.join(DST_BASE_PATH, c[0], file), resized_image)
#Créer des données d'étiquette d'apprentissage / de vérification
bound = int(len(files) * (1 - VAL_RATE))
random.shuffle(files)
train_files = files[:bound]
val_files = files[bound:]
train_dataset.extend([(os.path.join(c[0], file), c[2]) for file in train_files])
val_dataset.extend([(os.path.join(c[0], file), c[2]) for file in val_files])
#Fichier d'étiquette d'apprentissage de sortie
with open(TRAIN_LABEL_PATH, 'w') as f:
for d in train_dataset:
f.write(' '.join(d) + '\n')
#Fichier d'étiquette de vérification de sortie
with open(VAL_LABEL_PATH, 'w') as f:
for d in val_dataset:
f.write(' '.join(d) + '\n')
Exécutez le code ci-dessus.
$ python preprocess.py
Espérons qu'une image redimensionnée de 256x256 sera créée sous le répertoire redimensionné, et train_label.txt
, val_label.txt
seront créés à la racine du projet.
Vous pouvez modifier le rapport entre les données d'entraînement et les données de vérification en modifiant la valeur de «VAL_RATE» dans preprocess.py. Dans le code ci-dessus, le ratio est «apprentissage: validation = 8: 2».
Après avoir redimensionné l'image, l'étape suivante consiste à créer une image moyenne pour l'ensemble de données d'entraînement (la soustraction de l'image moyenne de l'image d'entrée est une sorte de processus de normalisation, dans lequel vous créez l'image moyenne pour cela). Placez compute_mean.py dans l'ensemble imagen du référentiel GitHub de Chainer dans votre projet et exécutez la commande suivante:
$ python compute_mean.py train_label.txt -R ./resized/
Après exécution, mean.npy sera généré.
Nous allons apprendre à utiliser l'image redimensionnée.
imagenet fournit plusieurs architectures neuralnet, mais cette fois j'essaierai d'utiliser GoogleNetBN
(quelques améliorations de code seront apportées dans la section suivante). Placez train_imagenet.py et googlenetbn.py depuis imagenet dans votre projet.
L'apprentissage sera exécuté lorsque la commande suivante sera exécutée. Pour le nombre d'époques («-E»), spécifiez une valeur appropriée en fonction de la quantité de données et de la tâche. Spécifiez également l'ID GPU (-g
) en fonction de votre environnement (l'option -g
n'est pas requise lors de l'apprentissage avec le CPU).
$ python train_imagenet.py -a googlenetbn -E 100 -g 0 -R ./resized/ ./train_label.txt ./val_label.txt --test
Les modèles formés et les journaux sont stockés dans le dossier de résultats.
Utilisez le modèle entraîné pour classer (estimer) des images arbitraires. L'exemple de code imagenet contient uniquement le code des données d'entraînement et de validation, et vous devez ajouter le code pour effectuer l'estimation.
Cependant, fondamentalement, sur la base du traitement de __call__ ()
, la partie qui renvoie la valeur de la perte doit être remplacée par la valeur de probabilité. Créons une nouvelle méthode appelée «predict ()» et décrivons ce processus.
googlenetbn.py(Extrait)
class GoogLeNetBN(chainer.Chain):
# --- (réduction) ---
def predict(self, x):
test = True
h = F.max_pooling_2d(
F.relu(self.norm1(self.conv1(x), test=test)), 3, stride=2, pad=1)
h = F.max_pooling_2d(
F.relu(self.norm2(self.conv2(h), test=test)), 3, stride=2, pad=1)
h = self.inc3a(h)
h = self.inc3b(h)
h = self.inc3c(h)
h = self.inc4a(h)
# a = F.average_pooling_2d(h, 5, stride=3)
# a = F.relu(self.norma(self.conva(a), test=test))
# a = F.relu(self.norma2(self.lina(a), test=test))
# a = self.outa(a)
# a = F.softmax(a)
h = self.inc4b(h)
h = self.inc4c(h)
h = self.inc4d(h)
# b = F.average_pooling_2d(h, 5, stride=3)
# b = F.relu(self.normb(self.convb(b), test=test))
# b = F.relu(self.normb2(self.linb(b), test=test))
# b = self.outb(b)
# b = F.softmax(b)
h = self.inc4e(h)
h = self.inc5a(h)
h = F.average_pooling_2d(self.inc5b(h), 7)
h = self.out(h)
return F.softmax(h)
Voir ici pour le code complet de la version améliorée de googlenetbn.py.
Si vous regardez le code ci-dessus, vous verrez que c'est à peu près la même chose que la gestion de __call__ ()
.
Cependant, bien que GoogleNet dispose de 3 sorties (principale + 2 auxiliaires), 2 sorties auxiliaires ne sont pas nécessaires au moment de l'estimation (ce classificateur auxiliaire est introduit comme contre-mesure de la disparition du gradient pendant l'apprentissage). ) [^ 1]. La partie commentée correspond à cette partie.
Dans le code ci-dessus, la fonction softmax est appliquée à la fin, mais il est normal d'omettre softmax comme return h
. Si vous n'avez pas besoin de normaliser votre score dans la plage de 0 à 1 et que vous souhaitez maintenir la quantité de calcul aussi faible que possible, vous pouvez l'omettre.
J'ai utilisé GoogleNetBN ici, mais bien sûr, d'autres architectures de l'exemple imagenet, comme AlexNet, peuvent être modifiées de la même manière. De plus, je pense qu'il est bon de créer ResNet, etc.
Ensuite, créez un serveur API Web. Ici, nous allons construire un serveur en utilisant le framework Web Python Flask.
En tant qu'image, écrivez du code qui effectue un traitement tel que l'envoi d'une image du client au serveur par HTTP POST, la classification de l'image côté serveur et le renvoi du résultat dans JSON.
server.py
# coding: utf-8
from __future__ import print_function
from flask import Flask, request, jsonify
import argparse
import cv2
import numpy as np
import chainer
import googlenetbn #Si vous souhaitez utiliser une autre architecture, veuillez réécrire ici
WIDTH = 256 #Largeur après redimensionnement
HEIGHT = 256 #Hauteur après redimensionnement
LIMIT = 3 #Nombre de cours
model = googlenetbn.GoogLeNetBN() #Si vous souhaitez utiliser une autre architecture, veuillez réécrire ici
app = Flask(__name__)
#Évitez de convertir le japonais en JSON en code ASCII(Pour faciliter la visualisation avec la commande curl. Il n'y a pas de problème même s'il est converti en ASCII)
app.config['JSON_AS_ASCII'] = False
# train_imagenet.get py PreprocessedDataset_example()Référence
def preproduce(image, crop_size, mean):
#redimensionner
image = cv2.resize(image, (WIDTH, HEIGHT))
# (height, width, channel) -> (channel, height, width)Conversion en
image = image.transpose(2, 0, 1)
_, h, w = image.shape
top = (h - crop_size) // 2
left = (w - crop_size) // 2
bottom = top + crop_size
right = left + crop_size
image = image[:, top:bottom, left:right]
image -= mean[:, top:bottom, left:right]
image /= 255
return image
@app.route('/')
def hello():
return 'Hello!'
#API de classification d'images
# http://localhost:8090/Lancer une image pour prédire renvoie un résultat en JSON
@app.route('/predict', methods=['POST'])
def predict():
#Chargement d'image
file = request.files['image']
image = cv2.imdecode(np.fromstring(file.stream.read(), np.uint8), cv2.IMREAD_COLOR)
#Prétraitement
image = preproduce(image.astype(np.float32), model.insize, mean)
#Estimation
p = model.predict(np.array([image]))[0].data
indexes = np.argsort(p)[::-1][:LIMIT]
#Renvoie le résultat au format JSON
return jsonify({
'result': [[classes[index][1], float(p[index])] for index in indexes]
})
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--initmodel', type=str, default='',
help='Initialize the model from given file')
parser.add_argument('--mean', '-m', default='mean.npy',
help='Mean file (computed by compute_mean.py)')
parser.add_argument('--labelmaster', '-l', type=str, default='label_master.txt',
help='Label master file')
parser.add_argument('--gpu', '-g', type=int, default=-1,
help='GPU ID (negative value indicates CPU')
args = parser.parse_args()
mean = np.load(args.mean)
chainer.serializers.load_npz(args.initmodel, model)
with open(args.labelmaster, 'r') as f:
classes = [line.strip().split(' ') for line in f.readlines()]
if args.gpu >= 0:
chainer.cuda.get_device(args.gpu).use()
model.to_gpu()
app.run(host='0.0.0.0', port=8090)
Le résultat de la classification JSON suppose la structure suivante. Dans le tableau interne, le premier élément est le nom de la classe et le deuxième élément est le score. Chaque classe est triée par ordre décroissant de score.
{
"result": [
[
"chien",
0.4107133746147156
],
[
"Lapins",
0.3368038833141327
],
[
"Chat",
0.2524118423461914
]
]
}
Vous pouvez également spécifier le nombre de classes supérieures à obtenir avec la constante LIMIT
. Comme il n'y a que 3 types d'animaux cette fois, nous définissons LIMIT = 3
, mais par exemple, s'il y a 100 types de classes au total et que vous voulez le top 10 d'entre eux, vous n'avez besoin que de LIMIT = 10
, 1ère place. Dans ce cas, vous pouvez spécifier quelque chose comme «LIMIT = 1».
Maintenant que le code est complet, démarrons réellement le serveur.
$ python server.py --initmodel ./result/model_iter_120
* Running on http://0.0.0.0:8090/ (Press CTRL+C to quit)
Dans cet état, préparez un autre shell et utilisez la commande curl pour envoyer l'image au serveur (préparez une image de test de manière appropriée). Si le résultat est renvoyé, c'est un succès.
$ curl -X POST -F [email protected] http://localhost:8090/predict
{
"result": [
[
"Lapins",
0.4001327157020569
],
[
"Chat",
0.36795011162757874
],
[
"chien",
0.23191720247268677
]
]
}
Le serveur API est maintenant terminé! Après cela, si vous créez librement un frontal et implémentez un mécanisme pour accéder au serveur API, vous pouvez le publier en tant que service Web.
TODO: Je prévois d'écrire un article séparé à une date ultérieure.
Dans cet article, j'ai parcouru les étapes de création d'un serveur API Web à partir de la méthode d'apprentissage d'un réseau de neurones à l'aide de Chainer (bien qu'il ait été dit que c'était pour les débutants, il y avait des endroits où l'explication était appropriée, mais lisez jusqu'ici. Merci de votre collaboration).
Compte tenu de la gestion des erreurs et du réglage fin, je dois le rendre un peu plus ferme, mais je pense que c'est à peu près comme ça. De plus, je pense que le traitement d'image est tout à fait approprié, il y a donc place à une amélioration considérable en termes de précision.
Utilisons de plus en plus Chainer pour créer des produits d'apprentissage en profondeur!
Exemple de code pour cet article
Recommended Posts