[PYTHON] Catégoriser les images de visage de personnages d'anime avec Chainer

Dans cet article, j'expliquerai ce qui suit tout en suivant les étapes pour affiner un modèle Illustration2Vec à l'aide de l'ensemble de données animeface-character et entraîner un modèle capable de classer 146 images de visage de personnages différents avec une précision de 90% ou plus. Je vais.

Avec Chainer

--Comment créer un objet de jeu de données --Comment diviser l'ensemble de données pour la formation et la vérification

L'environnement utilisé est le suivant.

La bibliothèque utilisée est la suivante.

--Chainer> = 2.0.1 (Il a été confirmé qu'il fonctionne avec la dernière version 4.1.0) --CuPy> = 1.0.1 (Il a été confirmé qu'il fonctionne avec la dernière version 4.1.0)

Chainer est rétrocompatible

Pour résumer grossièrement le contenu

** Nous montrerons un exemple concret de la façon de se procurer un ensemble de données qui n'est pas préparé à l'avance dans Chainer de l'extérieur et de l'utiliser pour former le réseau décrit dans Chainer. Les étapes de base sont presque les mêmes que le chapitre sur l'extension de la classe de jeu de données CIFAR10 décrit dans Chainer v4: Tutorial for Beginners.

Cette fois, je vais expliquer ** comment utiliser les poids de réseau pré-entraînés comme valeurs initiales en utilisant un ensemble de données de domaines similaires aux données cibles **. Si vous souhaitez affiner un réseau distribué sous la forme du modèle .caffe de Caffe, vous pouvez appliquer presque la même procédure que cet article.

Cet article est une sortie de écrit à l'origine dans le notebook Jupyter vers Markdown.

1. Télécharger l'ensemble de données

Tout d'abord, téléchargez l'ensemble de données. Cette fois, la vignette de la zone du visage du personnage d'anime distribuée par Nagadomi, qui est le Kaggle Grand Master, à ici Utilisez un ensemble de données.

%%bash
if [ ! -d animeface-character-dataset ]; then
    curl -L -O http://www.nurs.or.jp/~nagadomi/animeface-character-dataset/data/animeface-character-dataset.zip
    unzip animeface-character-dataset.zip
    rm -rf animeface-character-dataset.zip
fi

Entrez la bibliothèque à utiliser avec pip. ** La partie cupy-cuda90 est basée sur la version CUDA de votre environnement, cupy-cuda80 (pour l'environnement CUDA8.0), cupy-cuda90 (pour l'environnement CUDA9.0), cupy- Sélectionnez celui qui convient dans cuda91 (pour l'environnement CUDA 9.1). ) **

%%bash
pip install chainer
pip install cupy-cuda80 # or cupy-cuda90 or cupy-cuda91
pip install Pillow
pip install tqdm

2. Vérifiez les paramètres du problème

Cette fois, en utilisant les images de visage de divers personnages inclus dans le jeu de données animeface-character-dataset, lorsqu'une image de visage de personnage inconnu est entrée, il indique quel visage de personnage dans la liste de classe connue semble être. Je voudrais former un réseau qui le fasse.

À ce moment-là, au lieu de former un réseau avec des paramètres initialisés au hasard, une méthode de réglage fin avec l'ensemble de données cible basé sur un modèle entraîné avec des données de domaines similaires à l'avance ** Je vais essayer.

L'ensemble de données utilisé pour l'apprentissage de cette heure est un ensemble de données contenant de nombreuses images comme indiqué ci-dessous, et chaque caractère est divisé en dossiers à l'avance. Par conséquent, cette fois aussi, ce sera un problème de classification d'images orthodoxe.

Échantillon de données correctement extrait

000_hatsune_miku 002_suzumiya_haruhi 007_nagato_yuki 012_asahina_mikuru
face_128_326_108.png face_1000_266_119.png face_83_270_92.png face_121_433_128.png

3. Création d'un objet de jeu de données

Voici comment créer un objet de jeu de données à l'aide d'une classe appelée LabeledImageDataset, qui est souvent utilisée dans les problèmes de classification d'images. Tout d'abord, préparez-vous à l'aide des fonctions Python standard.

Tout d'abord, récupérez la liste des chemins d'accès au fichier image. Les fichiers image sont divisés en répertoires pour chaque caractère sous ʻanimeface-character-dataset / thumb. Dans le code ci-dessous, s'il y a un fichier appelé ʻignore dans le dossier, l'image de ce dossier est ignorée.

import os
import glob
from itertools import chain

#Dossier d'images
IMG_DIR = 'animeface-character-dataset/thumb'

#Dossier pour chaque personnage
dnames = glob.glob('{}/*'.format(IMG_DIR))

#Liste des chemins de fichiers image
fnames = [glob.glob('{}/*.png'.format(d)) for d in dnames
          if not os.path.exists('{}/ignore'.format(d))]
fnames = list(chain.from_iterable(fnames))

Ensuite, dans le chemin du fichier image, la partie du nom de répertoire qui contient l'image représente le nom du caractère, utilisez-la pour créer un ID unique pour chaque caractère de chaque image.

#Donnez à chacun un identifiant unique à partir du nom du dossier
labels = [os.path.basename(os.path.dirname(fn)) for fn in fnames]
dnames = [os.path.basename(d) for d in dnames
          if not os.path.exists('{}/ignore'.format(d))]
labels = [dnames.index(l) for l in labels]

Créons maintenant l'objet du jeu de données de base. C'est facile à faire, il suffit de passer une liste de tuples avec le chemin du fichier et ses étiquettes à LabeledImageDataset. C'est un itérateur qui renvoie un taple comme (img, label).

from chainer.datasets import LabeledImageDataset

#Création de l'ensemble de données
d = LabeledImageDataset(list(zip(fnames, labels)))

Ensuite, utilisons une fonction pratique appelée Transform Dataset fournie par Chainer. Il s'agit d'une classe wrapper qui prend un objet d'ensemble de données et une fonction qui représente la conversion en chaque données, et elle vous permet de préparer des parties pour l'augmentation des données, le prétraitement, etc. en dehors de la classe d'ensemble de données.

from chainer.datasets import TransformDataset
from PIL import Image

width, height = 160, 160

#Fonction de redimensionnement d'image
def resize(img):
    img = Image.fromarray(img.transpose(1, 2, 0))
    img = img.resize((width, height), Image.BICUBIC)
    return np.asarray(img).transpose(2, 0, 1)

#Conversion à chaque donnée
def transform(inputs):
    img, label = inputs
    img = img[:3, ...]
    img = resize(img.astype(np.uint8))
    img = img - mean[:, None, None]
    img = img.astype(np.float32)
    #Retourner aléatoirement à gauche et à droite
    if np.random.rand() > 0.5:
        img = img[..., ::-1]
    return img, label

#Créer un ensemble de données avec conversion
td = TransformDataset(d, transform)

En faisant cela, vous pouvez créer un objet de jeu de données qui reçoit un taple tel que (img, label) retourné par l'objet LabeledImageDataset`` d, le passe à travers la fonction transform`, puis le renvoie. J'ai fait.

Maintenant, divisons cela en deux ensembles de données partiels, un pour la formation et un pour la validation. Cette fois, nous utiliserons 80% de l'ensemble de données pour la formation et les 20% restants pour la validation. Avec split_dataset_random, les données de l'ensemble de données seront mélangées une fois, puis divisées aux pauses spécifiées.

from chainer import datasets

train, valid = datasets.split_dataset_random(td, int(len(d) * 0.8), seed=0)

Le partitionnement des ensembles de données fournit également plusieurs autres fonctions, telles que get_cross_validation_datasets_random, qui renvoie plusieurs paires de jeux de données d'entraînement et de validation différentes pour les tests croisés. Jetez un œil à ceci. : SubDataset

Par ailleurs, le terme «moyenne» utilisé dans la conversion est l'image moyenne des images incluses dans l'ensemble de données d'apprentissage utilisé cette fois. Calculons cela.

import matplotlib.pyplot as plt
import numpy as np
from tqdm import tqdm_notebook

#Calculer si l'image moyenne n'est pas calculée
if not os.path.exists('image_mean.npy'):
    #Je veux calculer la moyenne avec une version de l'ensemble de données d'entraînement qui ne mord pas la conversion
    t, _ = datasets.split_dataset_random(d, int(len(d) * 0.8), seed=0)

    mean = np.zeros((3, height, width))
    for img, _ in tqdm_notebook(t, desc='Calc mean'):
        img = resize(img[:3].astype(np.uint8))
        mean += img
    mean = mean / float(len(d))
    np.save('image_mean', mean)
else:
    mean = np.load('image_mean.npy')

Affiche l'image moyenne calculée à titre d'essai.

#Affichage de l'image moyenne
%matplotlib inline
plt.imshow(mean.transpose(1, 2, 0) / 255)
plt.show()

png

C'est un peu effrayant ...

Lorsque vous soustrayez la moyenne de chaque image, la moyenne de chaque pixel est utilisée, calculez donc la valeur de pixel moyenne (RVB) de cette image moyenne.

mean = mean.mean(axis=(1, 2))

4. Définition du modèle et préparation de la mise au point

Ensuite, définissons le modèle à entraîner. Ici, basé sur le réseau utilisé dans Illustration2Vec, qui effectue la prédiction de balises et l'extraction de caractéristiques avec un modèle appris à l'aide de nombreuses images d'illustration 2D, la dernière Le nouveau modèle est celui avec deux couches supprimées et deux couches entièrement connectées initialisées aléatoirement ajoutées.

Au moment de l'apprentissage, après avoir initialisé la partie de la troisième couche et en dessous à partir de la sortie avec le poids pré-entraîné d'Illustration2Vec, le poids de cette partie est fixe. Autrement dit, ** ne formez que les deux couches entièrement connectées nouvellement ajoutées. ** **

Tout d'abord, téléchargez les paramètres entraînés du modèle Illustration2Vec distribué.

%%bash
if [ ! -f illust2vec_ver200.caffemodel ]; then
    curl -L -O https://github.com/rezoo/illustration2vec/releases/download/v2.0.0/illust2vec_ver200.caffemodel
fi

Ce paramètre entraîné est fourni sous la forme d'un modèle caffe, mais Chainer a la capacité de charger très facilement le modèle entraîné de Caffe (CaffeFunction. /reference/generated/chainer.links.caffe.CaffeFunction.html#chainer.links.caffe.CaffeFunction)), que nous utiliserons pour charger les paramètres et la structure du modèle. Cependant, le chargement prend du temps, alors enregistrez l'objet Chain que vous obtenez une fois chargé dans un fichier en utilisant le standard Python pickle. Cela accélérera le chargement de la prochaine fois.

Le code réseau réel ressemble à ceci:

import dill

import chainer
import chainer.links as L
import chainer.functions as F

from chainer import Chain
from chainer.links.caffe import CaffeFunction
from chainer import serializers

class Illust2Vec(Chain):

    CAFFEMODEL_FN = 'illust2vec_ver200.caffemodel'

    def __init__(self, n_classes, unchain=True):
        w = chainer.initializers.HeNormal()        
        model = CaffeFunction(self.CAFFEMODEL_FN)  #Chargez et enregistrez le modèle Caffe. (Cela prendra un certain temps)
        del model.encode1  #Supprimez les couches inutiles pour économiser de la mémoire.
        del model.encode2
        del model.forwards['encode1']
        del model.forwards['encode2']
        model.layers = model.layers[:-2]
        
        super(Illust2Vec, self).__init__()
        with self.init_scope():
            self.trunk = model  #Incluez le modèle Illust2Vec d'origine comme coffre dans ce modèle.
            self.fc7 = L.Linear(None, 4096, initialW=w)
            self.bn7 = L.BatchNormalization(4096)
            self.fc8 = L.Linear(4096, n_classes, initialW=w)
            
    def __call__(self, x):
        h = self.trunk({'data': x}, ['conv6_3'])[0]  #Modèle d'origine Illust2Vec conv6_Prenez la sortie de 3.
        h.unchain_backward()
        h = F.dropout(F.relu(self.bn7(self.fc7(h))))  #Les couches suivantes sont des couches nouvellement ajoutées.
        return self.fc8(h)

n_classes = len(dnames)
model = Illust2Vec(n_classes)
model = L.Classifier(model)
/home/mitmul/chainer/chainer/links/caffe/caffe_function.py:165: UserWarning: Skip the layer "encode1neuron", since CaffeFunction does notsupport Sigmoid layer
  'support %s layer' % (layer.name, layer.type))
/home/mitmul/chainer/chainer/links/caffe/caffe_function.py:165: UserWarning: Skip the layer "loss", since CaffeFunction does notsupport SigmoidCrossEntropyLoss layer
  'support %s layer' % (layer.name, layer.type))

La description h.unchain_backward () est apparue dans la partie de __call__. ʻUnchain_backward est appelé à partir d'une sortie intermédiaire Variable` etc. dans le réseau et déconnecte tous les nœuds du réseau avant ce point. Par conséquent, pendant l'apprentissage, l'erreur ne sera pas transmise aux couches avant que cela ne soit appelé et, par conséquent, les paramètres ne seront pas mis à jour.

Comme mentionné ci-dessus

Au moment de l'apprentissage, après avoir initialisé la partie de la troisième couche et en dessous à partir de la sortie avec le poids pré-entraîné d'Illustration2Vec, le poids de cette pièce est fixe.

Le code pour cela est le suivant h.unchain_backward ().

Pour plus d'informations sur son fonctionnement, consultez cet article qui explique comment l'autogradation de Chainer fonctionne avec Define-by-Run. : Create 1-file Chainer

5. Apprentissage

Maintenant, entraînons-nous en utilisant cet ensemble de données et ce modèle. Commencez par charger les modules requis.

from chainer import iterators
from chainer import training
from chainer import optimizers
from chainer.training import extensions
from chainer.training import triggers
from chainer.dataset import concat_examples

Ensuite, définissez les paramètres d'apprentissage. Cette fois

ça ira.

batchsize = 64
gpu_id = 0
initial_lr = 0.01
lr_drop_epoch = 10
lr_drop_ratio = 0.1
train_epoch = 20

Voici le code à apprendre.

train_iter = iterators.MultiprocessIterator(train, batchsize)
valid_iter = iterators.MultiprocessIterator(
    valid, batchsize, repeat=False, shuffle=False)

optimizer = optimizers.MomentumSGD(lr=initial_lr)
optimizer.setup(model)
optimizer.add_hook(chainer.optimizer.WeightDecay(0.0001))

updater = training.StandardUpdater(
    train_iter, optimizer, device=gpu_id)

trainer = training.Trainer(updater, (train_epoch, 'epoch'), out='AnimeFace-result')
trainer.extend(extensions.LogReport())
trainer.extend(extensions.observe_lr())

#Valeur que vous souhaitez écrire sur la sortie standard
trainer.extend(extensions.PrintReport(
    ['epoch',
     'main/loss',
     'main/accuracy',
     'val/main/loss',
     'val/main/accuracy',
     'elapsed_time',
     'lr']))

#Le graphique des pertes est automatiquement enregistré à chaque époque
trainer.extend(extensions.PlotReport(
        ['main/loss',
         'val/main/loss'],
        'epoch', file_name='loss.png'))

#Les tracés de précision sont également automatiquement enregistrés à chaque époque
trainer.extend(extensions.PlotReport(
        ['main/accuracy',
         'val/main/accuracy'],
        'epoch', file_name='accuracy.png'))

#Extension qui valide en définissant la propriété train du modèle sur False
trainer.extend(extensions.Evaluator(valid_iter, model, device=gpu_id), name='val')

#Taux d'apprentissage Lr pour chaque époque spécifiée_drop_Double le ratio
trainer.extend(
    extensions.ExponentialShift('lr', lr_drop_ratio),
    trigger=(lr_drop_epoch, 'epoch'))

trainer.run()
epoch       main/loss   main/accuracy  val/main/loss  val/main/accuracy  elapsed_time  lr        
1           1.58266     0.621792       0.623695       0.831607           29.4045       0.01        
2           0.579938    0.835989       0.54294        0.85179            56.3893       0.01        
3           0.421797    0.877897       0.476766       0.876872           83.9976       0.01        
4           0.3099      0.909251       0.438246       0.879637           113.476       0.01        
5           0.244549    0.928394       0.427892       0.884571           142.931       0.01        
6           0.198274    0.938638       0.41589        0.893617           172.42        0.01        
7           0.171127    0.946709       0.432277       0.89115            201.868       0.01        
8           0.146401    0.953125       0.394634       0.902549           231.333       0.01        
9           0.12377     0.964404       0.409338       0.894667           260.8         0.01        
10          0.109239    0.967198       0.400371       0.907746           290.29        0.01        
11          0.0948708   0.971337       0.378603       0.908831           319.742       0.001       
12          0.0709512   0.98065        0.380891       0.90786            349.242       0.001       
13          0.0699093   0.981892       0.384257       0.90457            379.944       0.001       
14          0.0645318   0.982099       0.370053       0.908008           410.963       0.001       
15          0.0619039   0.983547       0.379178       0.908008           441.941       0.001       
16          0.0596897   0.983646       0.375837       0.911709           472.832       0.001       
17          0.0579783   0.984789       0.379593       0.908008           503.836       0.001       
18          0.0611943   0.982202       0.378177       0.90842            534.86        0.001       
19          0.061885    0.98303        0.373961       0.90569            565.831       0.001       
20          0.0548781   0.986341       0.3698         0.910624           596.847       0.001       

L'apprentissage s'est terminé en moins de 6 minutes. La progression de la sortie standard était comme ci-dessus. En fin de compte, vous pouvez obtenir une précision de plus de 90% pour l'ensemble de données de vérification. Maintenant, affichons la courbe de perte et la courbe de précision dans le processus d'apprentissage enregistré sous forme de fichier image.

from IPython.display import Image
Image(filename='AnimeFace-result/loss.png')

output_35_0.png

Image(filename='AnimeFace-result/accuracy.png')

output_36_0.png

J'ai l'impression qu'il a convergé en toute sécurité.

Enfin, prenons quelques images de l'ensemble de données de validation et examinons les résultats de classification individuels.

%matplotlib inline
import matplotlib.pyplot as plt

from PIL import Image
from chainer import cuda

chainer.config.train = False
for _ in range(10):
    x, t = valid[np.random.randint(len(valid))]
    x = cuda.to_gpu(x)
    y = F.softmax(model.predictor(x[None, ...]))
    
    pred = os.path.basename(dnames[int(y.data.argmax())])
    label = os.path.basename(dnames[t])
    
    print('pred:', pred, 'label:', label, pred == label)

    x = cuda.to_cpu(x)
    x += mean[:, None, None]
    x = x / 256
    x = np.clip(x, 0, 1)
    plt.imshow(x.transpose(1, 2, 0))
    plt.show()
pred: 097_kamikita_komari label: 097_kamikita_komari True

output_38_1.png

pred: 127_setsuna_f_seiei label: 127_setsuna_f_seiei True

output_38_3.png

pred: 171_ikari_shinji label: 171_ikari_shinji True

output_38_5.png

pred: 042_tsukimura_mayu label: 042_tsukimura_mayu True

output_38_7.png

pred: 001_kinomoto_sakura label: 001_kinomoto_sakura True

output_38_9.png

pred: 090_minase_iori label: 090_minase_iori True

output_38_11.png

pred: 132_minamoto_chizuru label: 132_minamoto_chizuru True

output_38_13.png

pred: 106_nia label: 106_nia True

output_38_15.png

pred: 174_hayama_mizuki label: 174_hayama_mizuki True

output_38_17.png

pred: 184_suzumiya_akane label: 184_suzumiya_akane True

output_38_19.png

Lorsque j'ai sélectionné au hasard 10 images, j'ai pu répondre correctement à toutes ces images.

Enfin, enregistrez un instantané pour le moment, car il peut être utilisé pour quelque chose un jour.

from chainer import serializers

serializers.save_npz('animeface.model', model)

6. Bonus 1: Comment écrire une classe de jeu de données en plein scratch

Pour écrire une classe de jeu de données entièrement, vous pouvez préparer votre propre classe qui hérite de la classe chainer.dataset.DatasetMixin. La classe doit avoir une méthode «len» et une méthode «get_example». Par exemple:

class MyDataset(chainer.dataset.DatasetMixin):
    
    def __init__(self, image_paths, labels):
        self.image_paths = image_paths
        self.labels = labels
        
    def __len__(self):
        return len(self.image_paths)
    
    def get_example(self, i):
        img = Image.open(self.image_paths[i])
        img = np.asarray(img, dtype=np.float32)
        img = img.transpose(2, 0, 1)
        label = self.labels[i]
        return img, label

Cela se fait en passant une liste de chemins de fichiers image et une liste d'étiquettes disposées dans l'ordre correspondant au constructeur, et si vous spécifiez un index avec l'accesseur [], l'image est lue à partir du chemin correspondant et arrangée avec les étiquettes. C'est une classe d'ensemble de données qui renvoie un taple. Par exemple, vous pouvez l'utiliser comme suit.

image_files = ['images/hoge_0_1.png', 'images/hoge_5_1.png', 'images/hoge_2_1.png', 'images/hoge_3_1.png', ...]
labels = [0, 5, 2, 3, ...]

dataset = MyDataset(image_files, labels)

img, label = dataset[2]

#=> 'images/hoge_2_1.png'Les données d'image lues et son étiquette (2 dans ce cas) sont renvoyées.

Cet objet peut être transmis à Iterator tel quel et peut être utilisé pour l'entraînement à l'aide de Trainer. En d'autres termes

train_iter = iterators.MultiprocessIterator(dataset, batchsize=128)

Vous pouvez créer un itérateur comme celui-ci, le transmettre au programme de mise à jour avec l'optimiseur et transmettre le programme de mise à jour au formateur pour commencer à apprendre avec le formateur.

7. Bonus 2: Comment créer l'objet de jeu de données le plus simple

En fait, l'ensemble de données à utiliser avec Chainer's Trainer est ** juste une liste Python OK **. Cela signifie que si la longueur peut être obtenue avec len () et que l'élément peut être récupéré avec l'accesseur [], ** tout peut être traité comme un objet de jeu de données **. Par exemple

data_list = [(x1, t1), (x2, t2), ...]

Vous pouvez le transmettre à Iterator en créant une liste de taples appelée (data, label) comme

train_iter = iterators.MultiprocessIterator(data_list, batchsize=128)

Cependant, l'inconvénient de cette approche est que l'ensemble de données doit être stocké en mémoire avant l'entraînement. Pour éviter cela, [ImageDataset](http://docs.chainer.org/en/stable/reference/generated/chainer.datasets.ImageDataset.html#chainer.datasets.ImageDataset] et [TupleDataset](http: // Comment combiner docs.chainer.org/en/stable/reference/generated/chainer.datasets.TupleDataset.html#chainer.datasets.TupleDataset) et [LabaledImageDataset](http://docs.chainer.org/en/stable/ Il existe des classes telles que reference / generated / chainer.datasets.LabeledImageDataset.html # chainer.datasets.LabeledImageDataset). Veuillez vous référer au document pour plus de détails. http://docs.chainer.org/en/stable/reference/datasets.html#general-datasets

Recommended Posts

Catégoriser les images de visage de personnages d'anime avec Chainer
Reconnaissance faciale des personnages d'anime avec Keras
Première reconnaissance faciale d'anime avec Chainer
Détection de visage d'anime avec OpenCV
Classification multi-étiquette d'images multi-classes avec pytorch
Apprenez à coloriser les images monochromes avec Chainer
Classez les visages d'anime avec l'apprentissage en profondeur avec Chainer
Détection de visage en collectant des images d'Angers.
Transcription d'images avec l'API Vision de GCP
[python, openCV] base64 Reconnaissance faciale dans les images
Compter le nombre de caractères avec écho
Enregistrez automatiquement les images de vos personnages préférés à partir de la recherche d'images Google avec Python
Mélangez des centaines de milliers d'images uniformément avec tensorflow.
Chargez le modèle caffe avec Chainer et classez les images
Comparaison des performances du détecteur de visage avec Python + OpenCV
Conversion en ondelettes d'images avec PyWavelets et OpenCV
Seq2Seq (1) avec chainer
Afficher des images intégrées de mp3 et flac avec mutagène
Essayez de projeter la conversion d'image en utilisant OpenCV avec Python
Maintenant, essayons la reconnaissance faciale avec Chainer (phase de prédiction)
Créez un lot d'images et gonflez avec ImageDataGenerator
J'ai essayé la "conversion de morphologie" de l'image avec Python + OpenCV
Maintenant, essayons la reconnaissance faciale avec Chainer (phase d'apprentissage)