In diesem Artikel erkläre ich Folgendes, indem ich die Schritte zur Feinabstimmung eines Illustration2Vec-Modells mithilfe des Animeface-Zeichendatensatzes befolge und ein Modell trainiere, das 146 verschiedene Zeichengesichtsbilder mit einer Genauigkeit von 90% oder besser klassifizieren kann. Ich werde.
Mit Chainer
Die verwendete Umgebung ist wie folgt.
Die verwendete Bibliothek ist wie folgt.
--Chainer> = 2.0.1 (Es wurde bestätigt, dass es mit der neuesten Version 4.1.0 funktioniert.) --CuPy> = 1.0.1 (Es wurde bestätigt, dass es mit der neuesten Version 4.1.0 funktioniert.)
Chainer ist abwärtskompatibel
** Die Methode zur Beschaffung eines Datensatzes, der in Chainer nicht im Voraus von außen vorbereitet wurde ** und zur Verwendung des in Chainer beschriebenen Netzwerks wird anhand eines konkreten Beispiels gezeigt. Die grundlegenden Schritte entsprechen fast dem in Chainer v4: Tutorial für Anfänger beschriebenen Kapitel zum Erweitern der CIFAR10-Dataset-Klasse.
Dieses Mal werde ich erklären, wie vorab trainierte Netzwerkgewichte als Anfangswerte unter Verwendung eines Datensatzes von Domänen verwendet werden, die den Zieldaten ähnlich sind. Wenn Sie ein Netzwerk optimieren möchten, das in Form des Caffe-Modells .caffe verteilt ist, können Sie fast das gleiche Verfahren wie in diesem Artikel anwenden.
Dieser Artikel ist eine Markdown-Ausgabe von ursprünglich in Jupyter-Notizbuch geschrieben.
Laden Sie zunächst den Datensatz herunter. Diesmal die Miniaturansicht des Anime-Charakters, der von Nagadomi, dem Großmeister von Kaggle, unter [hier] verteilt wurde (http://www.nurs.or.jp/~nagadomi/animeface-character-dataset/) Verwenden Sie einen Datensatz.
%%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
Geben Sie die Bibliothek ein, die mit pip verwendet werden soll. ** Der Teil cupy-cuda90
basiert auf der CUDA-Version Ihrer Umgebung, cupy-cuda80
(für CUDA8.0-Umgebung), cupy-cuda90
(für CUDA9.0-Umgebung), cupy- Wählen Sie die entsprechende aus cuda91
(für CUDA 9.1-Umgebung). ) **
%%bash
pip install chainer
pip install cupy-cuda80 # or cupy-cuda90 or cupy-cuda91
pip install Pillow
pip install tqdm
Dieses Mal wird unter Verwendung der Gesichtsbilder verschiedener Zeichen, die im Animeface-Zeichendatensatz enthalten sind, bei der Eingabe eines unbekannten Zeichengesichtsbildes ausgegeben, welches Gesicht des Zeichens in der bekannten Klassenliste zu sein scheint. Ich möchte ein Netzwerk trainieren, das dies tut.
Zu diesem Zeitpunkt wurde anstelle eines Trainings mit zufällig initialisierten Parametern eine Methode zur Feinabstimmung mit dem Zieldatensatz basierend auf einem Modell durchgeführt, das im Voraus mit Daten ähnlicher Domänen trainiert wurde ** Ich werde versuchen.
Der Datensatz, der dieses Mal zum Lernen verwendet wird, ist ein Datensatz, der viele Bilder enthält, wie unten gezeigt, und jedes Zeichen wird im Voraus in Ordner unterteilt. Daher wird es auch diesmal ein orthodoxes Bildklassifizierungsproblem sein.
000_hatsune_miku | 002_suzumiya_haruhi | 007_nagato_yuki | 012_asahina_mikuru |
---|---|---|---|
Hier erfahren Sie, wie Sie ein Dataset-Objekt mit einer Klasse namens "LabeledImageDataset" erstellen, die häufig bei Bildklassifizierungsproblemen verwendet wird. Bereiten Sie sich zunächst mit Standard-Python-Funktionen vor.
Rufen Sie zunächst die Liste der Pfade zur Bilddatei ab. Die Bilddateien sind in Verzeichnisse für jedes Zeichen unter "Animeface-Character-Dataset / Thumb" unterteilt. Wenn sich im folgenden Code eine Datei mit dem Namen "Ignorieren" im Ordner befindet, wird das Bild in diesem Ordner ignoriert.
import os
import glob
from itertools import chain
#Bildordner
IMG_DIR = 'animeface-character-dataset/thumb'
#Ordner für jedes Zeichen
dnames = glob.glob('{}/*'.format(IMG_DIR))
#Liste der Bilddateipfade
fnames = [glob.glob('{}/*.png'.format(d)) for d in dnames
if not os.path.exists('{}/ignore'.format(d))]
fnames = list(chain.from_iterable(fnames))
Als Nächstes stellt im Bilddateipfad der Teil des Verzeichnisnamens, der das Bild enthält, den Zeichennamen dar. Verwenden Sie ihn daher, um eine ID zu erstellen, die für jedes Zeichen für jedes Bild eindeutig ist.
#Geben Sie jedem eine eindeutige ID aus dem Ordnernamen
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]
Jetzt erstellen wir das Basisdatensatzobjekt. Es ist einfach, eine Liste von Tupeln mit dem Dateipfad und seinen Beschriftungen an LabeledImageDataset
zu übergeben. Dies ist ein Iterator, der ein Taple wie "(img, label)" zurückgibt.
from chainer.datasets import LabeledImageDataset
#Datensatzerstellung
d = LabeledImageDataset(list(zip(fnames, labels)))
Als nächstes verwenden wir eine praktische Funktion namens "Transform Dataset", die von Chainer bereitgestellt wird. Dies ist eine Wrapper-Klasse, die ein Dataset-Objekt und eine Funktion verwendet, die die Konvertierung in die einzelnen Daten darstellt. Auf diese Weise können Sie Teile für die Datenerweiterung, Vorverarbeitung usw. außerhalb der Dataset-Klasse vorbereiten.
from chainer.datasets import TransformDataset
from PIL import Image
width, height = 160, 160
#Bildgrößenänderungsfunktion
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)
#Konvertierung in die einzelnen Daten
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)
#Nach dem Zufallsprinzip nach links und rechts drehen
if np.random.rand() > 0.5:
img = img[..., ::-1]
return img, label
#Erstellen Sie einen Datensatz mit Konvertierung
td = TransformDataset(d, transform)
Auf diese Weise können Sie ein Dataset-Objekt erstellen, das ein Taple wie "(img, label)" empfängt, das vom "LabeledImageDataset" -Objekt "d" zurückgegeben wird, es durch die "transform" -Funktion leitet und dann zurückgibt. Ich tat.
Teilen wir dies nun in zwei Teildatensätze auf, einen für das Training und einen für die Validierung. Dieses Mal werden wir 80% des gesamten Datensatzes für das Training und die restlichen 20% für die Validierung verwenden. Mit split_dataset_random
werden die Daten im Dataset einmal gemischt und dann in den angegebenen Pausen aufgeteilt.
from chainer import datasets
train, valid = datasets.split_dataset_random(td, int(len(d) * 0.8), seed=0)
Die Dataset-Partitionierung bietet auch mehrere andere Funktionen, z. B. "get_cross_validation_datasets_random", die mehrere verschiedene Trainings- und Validierungs-Dataset-Paare zum Kreuztest zurückgeben. Schau dir das an. : SubDataset
Übrigens ist der bei der Konvertierung verwendete Mittelwert das Durchschnittsbild der Bilder, die in dem diesmal verwendeten Trainingsdatensatz enthalten sind. Berechnen wir das.
import matplotlib.pyplot as plt
import numpy as np
from tqdm import tqdm_notebook
#Berechnen Sie, ob das durchschnittliche Bild nicht berechnet wurde
if not os.path.exists('image_mean.npy'):
#Ich möchte den Durchschnitt mit einer Version des Trainingsdatensatzes berechnen, die die Konvertierung nicht beißt
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')
Lassen Sie uns das durchschnittliche Bild anzeigen, das als Test berechnet wurde.
#Anzeige des Durchschnittsbildes
%matplotlib inline
plt.imshow(mean.transpose(1, 2, 0) / 255)
plt.show()
Es ist irgendwie beängstigend ...
Beim Subtrahieren des Durchschnitts von jedem Bild wird der Durchschnitt für jedes Pixel verwendet. Berechnen Sie daher den durchschnittlichen Pixelwert (RGB) dieses Durchschnittsbilds.
mean = mean.mean(axis=(1, 2))
Als nächstes definieren wir das zu trainierende Modell. Hier basiert auf dem in Illustration2Vec verwendeten Netzwerk, das die Tag-Vorhersage und Merkmalsextraktion mit einem Modell durchführt, das unter Verwendung vieler 2D-Illustrationsbilder gelernt wurde, dem letzten Bei dem neuen Modell werden zwei Ebenen entfernt und zwei zufällig initialisierte, vollständig verbundene Ebenen hinzugefügt.
Zum Zeitpunkt des Lernens wird nach dem Initialisieren des Teils der dritten Schicht und darunter aus der Ausgabe mit dem vorab trainierten Gewicht von Illustration2Vec das Gewicht dieses Teils festgelegt. Das heißt, ** trainiere nur die zwei neu hinzugefügten vollständig verbundenen Ebenen. ** **.
Laden Sie zunächst die trainierten Parameter des verteilten Illustration2Vec-Modells herunter.
%%bash
if [ ! -f illust2vec_ver200.caffemodel ]; then
curl -L -O https://github.com/rezoo/illustration2vec/releases/download/v2.0.0/illust2vec_ver200.caffemodel
fi
Dieser trainierte Parameter wird in Form eines Caffe-Modells bereitgestellt, aber Chainer kann das trainierte Caffe-Modell sehr einfach laden (CaffeFunction
). /reference/generated/chainer.links.caffe.CaffeFunction.html#chainer.links.caffe.CaffeFunction)), mit dem wir die Parameter und die Modellstruktur laden. Das Laden dauert jedoch einige Zeit. Speichern Sie daher das "Chain" -Objekt, das Sie nach dem Laden in eine Datei mit dem Python-Standard "pickle" erhalten. Dies beschleunigt das Laden ab dem nächsten Mal.
Der tatsächliche Netzwerkcode sieht folgendermaßen aus:
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) #Laden und speichern Sie das Caffe-Modell. (Es braucht Zeit)
del model.encode1 #Löschen Sie nicht benötigte Ebenen, um Speicherplatz zu sparen.
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 #Nehmen Sie das ursprüngliche Illust2Vec-Modell als Kofferraum in dieses Modell auf.
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] #Original Illust2Vec Modell conv6_Nehmen Sie die Ausgabe von 3.
h.unchain_backward()
h = F.dropout(F.relu(self.bn7(self.fc7(h)))) #Die folgenden Ebenen sind neu hinzugefügte Ebenen.
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))
Die Beschreibung "h.unchain_backward ()" ist im Teil von "call" erschienen. unchain_backward
wird von einer Zwischenausgabe Variable
usw. im Netzwerk aufgerufen und trennt alle Netzwerkknoten vor diesem Punkt. Daher wird der Fehler während des Trainings nicht an die Schichten übertragen, bevor dies aufgerufen wird, und infolgedessen werden die Parameter nicht aktualisiert.
Wie oben erwähnt
Zum Zeitpunkt des Lernens wird nach dem Initialisieren des Teils der dritten Schicht und darunter aus der Ausgabe mit dem vorab trainierten Gewicht von Illustration2Vec das Gewicht dieses Teils festgelegt.
Der Code dafür ist "h.unchain_backward ()".
Weitere Informationen dazu finden Sie in diesem Artikel, in dem erläutert wird, wie Chainers Autograd mit Define-by-Run funktioniert. : 1-Datei-Chainer erstellen
Lassen Sie uns nun mit diesem Datensatz und Modell trainieren. Laden Sie zunächst die erforderlichen Module.
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
Als nächstes stellen Sie die Lernparameter ein. Diesmal
Wird besorgt.
batchsize = 64
gpu_id = 0
initial_lr = 0.01
lr_drop_epoch = 10
lr_drop_ratio = 0.1
train_epoch = 20
Unten finden Sie den Code zum Lernen.
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())
#Wert, den Sie in die Standardausgabe schreiben möchten
trainer.extend(extensions.PrintReport(
['epoch',
'main/loss',
'main/accuracy',
'val/main/loss',
'val/main/accuracy',
'elapsed_time',
'lr']))
#Die Verlustdarstellung speicherte automatisch jede Epoche
trainer.extend(extensions.PlotReport(
['main/loss',
'val/main/loss'],
'epoch', file_name='loss.png'))
#Genauigkeitsdiagramme werden auch automatisch in jeder Epoche gespeichert
trainer.extend(extensions.PlotReport(
['main/accuracy',
'val/main/accuracy'],
'epoch', file_name='accuracy.png'))
#Erweiterung, die validiert wird, indem die Train-Eigenschaft des Modells auf False gesetzt wird
trainer.extend(extensions.Evaluator(valid_iter, model, device=gpu_id), name='val')
#Lr-Lernrate für jede angegebene Epoche_drop_Verdoppeln Sie das Verhältnis
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
Das Lernen war in weniger als 6 Minuten abgeschlossen. Der Fortschritt der Standardausgabe war wie oben. Am Ende können Sie eine Genauigkeit von mehr als 90% für den Verifizierungsdatensatz erhalten. Lassen Sie uns nun die Verlustkurve und die Genauigkeitskurve im Lernprozess anzeigen, der als Bilddatei gespeichert ist.
from IPython.display import Image
Image(filename='AnimeFace-result/loss.png')
Image(filename='AnimeFace-result/accuracy.png')
Ich fühle, dass es sicher konvergiert hat.
Nehmen wir zum Schluss einige Bilder aus dem Validierungsdatensatz und sehen uns die einzelnen Klassifizierungsergebnisse an.
%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
pred: 127_setsuna_f_seiei label: 127_setsuna_f_seiei True
pred: 171_ikari_shinji label: 171_ikari_shinji True
pred: 042_tsukimura_mayu label: 042_tsukimura_mayu True
pred: 001_kinomoto_sakura label: 001_kinomoto_sakura True
pred: 090_minase_iori label: 090_minase_iori True
pred: 132_minamoto_chizuru label: 132_minamoto_chizuru True
pred: 106_nia label: 106_nia True
pred: 174_hayama_mizuki label: 174_hayama_mizuki True
pred: 184_suzumiya_akane label: 184_suzumiya_akane True
Als ich 10 Bilder zufällig ausgewählt habe, konnte ich alle diese Bilder richtig beantworten.
Speichern Sie zum Schluss vorerst einen Schnappschuss, da er eines Tages für etwas verwendet werden kann.
from chainer import serializers
serializers.save_npz('animeface.model', model)
Um eine Dataset-Klasse vollständig zu schreiben, können Sie eine eigene Klasse vorbereiten, die die Klasse "chainer.dataset.DatasetMixin" erbt. Die Klasse muss eine __len__
und eine get_example
Methode haben. Zum Beispiel:
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
Dies erfolgt durch Übergeben einer Liste von Bilddateipfaden und einer Liste von Beschriftungen, die in der entsprechenden Reihenfolge angeordnet sind, an den Konstruktor. Wenn Sie mit dem Accessor []
einen Index angeben, wird das Bild aus dem entsprechenden Pfad gelesen und mit der Beschriftung angeordnet. Es ist eine Dataset-Klasse, die einen Taple zurückgibt. Sie können es beispielsweise wie folgt verwenden.
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'Die gelesenen Bilddaten und ihre Beschriftung (in diesem Fall 2) werden zurückgegeben.
Dieses Objekt kann unverändert an Iterator übergeben und für das Training mit Trainer verwendet werden. Mit anderen Worten
train_iter = iterators.MultiprocessIterator(dataset, batchsize=128)
Sie können einen solchen Iterator erstellen, ihn mit dem Optimierer an den Updater übergeben und den Updater an den Trainer übergeben, um mit dem Trainer zu lernen.
Tatsächlich ist der Datensatz zur Verwendung mit Chainers Trainer ** nur eine Python-Liste OK **. Dies bedeutet, dass, wenn die Länge mit "len ()" ermittelt werden kann und das Element mit dem Accessor "[]" abgerufen werden kann, ** alle als Datensatzobjekt ** behandelt werden können. Zum Beispiel
data_list = [(x1, t1), (x2, t2), ...]
Sie können dies an Iterator übergeben, indem Sie eine Liste von Tupeln wie "(Daten, Beschriftung)" erstellen.
train_iter = iterators.MultiprocessIterator(data_list, batchsize=128)
Der Nachteil dieses Ansatzes besteht jedoch darin, dass der gesamte Datensatz vor dem Training im Speicher gespeichert werden muss. Um dies zu verhindern, [ImageDataset](http://docs.chainer.org/en/stable/reference/generated/chainer.datasets.ImageDataset.html#chainer.datasets.ImageDataset] und [TupleDataset](http: // So kombinieren Sie docs.chainer.org/en/stable/reference/generated/chainer.datasets.TupleDataset.html#chainer.datasets.TupleDataset) und LabaledImageDataset. Es gibt Klassen wie reference / generate / chainer.datasets.LabeledImageDataset.html # chainer.datasets.LabeledImageDataset). Weitere Informationen finden Sie im Dokument. http://docs.chainer.org/en/stable/reference/datasets.html#general-datasets
Recommended Posts