# Import potrebných balíčkov
import os
import glob
import pickle
import numpy as np
import keras.backend as K
from keras.models import Model
from keras.regularizers import l2
from keras.layers import GlobalAveragePooling2D
from keras.layers import Flatten, Dense, Input, Dropout
from keras.preprocessing.image import ImageDataGenerator
from keras.applications.resnet50 import ResNet50
from keras.callbacks import EarlyStopping
from keras.optimizers import Adam
# Uistíme sa, že máme všetky potrebné dáta
!mkdir -p data/food5
!wget -nc -O data/food5.zip https://www.dropbox.com/s/bjz07tl5vimzset/food5.zip?dl=1
!unzip -oq -d data/food5 data/food5.zip
# Pomocný kód
def preproc_data(X, Y, preproc_resnet, cache_filepath):
"""
Predspracuje pôvodné obrazové dáta pomocou niekoľkých prvých vrstiev
konvolučnej siete preproc_resnet. Výsledky predspracovania sa cache-ujú
v súbore cache_filepath.
"""
if os.path.exists(cache_filepath):
preprocX, preprocY = pickle.load(open(cache_filepath, "rb"))
else:
preprocX = preproc_resnet.predict(X)
preprocY = Y
pickle.dump((preprocX, preprocY), open(cache_filepath, "wb"))
return preprocX, preprocY
def make_datagen(use_augmentation):
"""
Vytvorí generátor na základné predspracovanie obrazových dát. Ak je
use_augmentation True, použije sa aj zväčšovanie dátovej množiny.
"""
if use_augmentation:
datagen = ImageDataGenerator(
rotation_range=30.,
shear_range=0.2,
zoom_range=0.2,
horizontal_flip=True)
else:
datagen = ImageDataGenerator()
return datagen
def make_generator(dirname, datagen, batch_size=None, resize_pics=(224, 224),
shuffle=True, one_hot=False, splitVersion=1):
class_mode = 'categorical' if one_hot else 'sparse'
if batch_size is None:
batch_size = len(glob.glob(os.path.join(dirname, '*/*.jpg')))
generator = datagen.flow_from_directory(
dirname,
target_size=resize_pics,
batch_size=batch_size,
shuffle=shuffle,
class_mode=class_mode
)
return generator
Transfer učenie budeme realizovať na dátovej množine Food 5: zmenšenej verzii dátovej množiny Food 11.
Transfer učenie je veľmi užitočná technika. Za normálnych okolností si hlboké učenie vyžaduje obrovské množstvo dát. Ak ho chceme aplikovať na malú dátovú množinu, typicky sa nám nepodarí dosiahnuť, aby hlboká sieť dobre zovšeobecňovala. Problém súvisí s tým, že malá dátová množina nedokáže väčšinou dostatočne vystihnúť všetky možné variácie vzoriek, s ktorými sa je možné stretnúť. Povedzme v prípade rozpoznávania obrazu môže existovať v podstate nekonečný počet variácií fotografie psa: líšiť sa môžu prostredím, osvetlením, plemenom psa, uhlom, v ktorom je odfotografovaný a pod. Malá dátová množina potom s vysokou pravdepodobnosťou nepokryje dostatočne tento široký priestor.
Jedným z riešení, ktoré umožňujú hlboké učenie predsa len aplikovať aj na pomerne malé dátové množiny, je transfer učenie. Ide o techniku, kde sa sieť najprv predtrénuje na veľkej, všeobecnejšej dátovej množine (v prípade spracovania obrazu to býva dátová množina ImageNet) – tam sa sieť naučí napríklad o tom, ako vyzerá prirodzený obraz a ako ho treba predspracovať. Následne sa už existujúca sieť pretrénuje na konkrétnu cieľovú úlohu.
Rámcový postup transfer učenia pre spracovanie obrazu:
Predtrénovať sieť na dátovej množine ImageNet.
Z pôvodnej siete zmazať niekoľko posledných vrstiev a nahradiť ich novými. Nová výstupná vrstva bude už mať toľko výstupov, koľko je tried v novej dátovej množine.
Váhy predtrénovaných vrstiev sa zafixujú. Na novej dátovej množine sa trénujú najprv len nové vrstvy.
Keď sa nové vrstvy natrénovali, odomkneme aj váhy predtrénovaných vrstiev a doladíme váhy celej siete. Použijeme omnoho nižšiu rýchlosť učenia – jednak preto, aby sme váhy priveľkými krokmi nerozladili, ale aj preto, že pri ladení všetkých váh sa už sieť veľmi ľahko preučí.
Pripravíme dáta. Najprv ich stiahneme a roztriedime, ak sa tak ešte nestalo.
Vytvoríme generátory a načítame pomocou nich na jeden raz všetky tréningové, testovacie a validačné dáta. Naša dátová množina je malá, takže sa zmestí do pamäte aj celá naraz.
train_generator = make_generator("data/food5/training", make_datagen(use_augmentation=False))
valid_generator = make_generator("data/food5/validation", make_datagen(use_augmentation=False))
test_generator = make_generator("data/food5/testing", make_datagen(use_augmentation=False))
X_train, Y_train = train_generator.next()
X_valid, Y_valid = valid_generator.next()
X_test, Y_test = test_generator.next()
Balíček keras
poskytuje niekoľko populárnych architektúr, ktoré sú už predtrénované na dátovej množine ImageNet. Jednu takú sieť si vytvoríme, pričom pomocou argumentu include_top=False
rovno povieme, že nemáme záujem o posledné vrstvy. Špecifikovať budeme aj vstupný tenzor: v našom prípade budú mať vstupné obrázky rozmer 224 x 224:
input_tensor=Input(shape=(224, 224) + (3,))
preproc_resnet = ResNet50(include_top=False,
weights='imagenet',
input_tensor=input_tensor)
Teraz máme dve možnosti:
Spojíme našu predtrénovanú sieť s novovytvorenými vrstvami, zafixujeme váhy predtrénovanej siete a výsledok budeme trénovať ako celok.
Alebo pôvodnú sieť použijeme zatiaľ len na predspracovanie všetkých dát a nové vrstvy budeme trénovať samostatne.
Druhá možnosť bude podstatne menej výpočtovo náročná – na druhej strane nám neumožní používať priebežné zväčšovanie dátovej množiny (augmentation). Napriek tomu si teraz zvolíme túto druhú možnosť.
Predtrénovanú sieť teda použijeme na predspracovanie:
preprocX_train, preprocY_train = preproc_data(X_train, Y_train, preproc_resnet,
"preproc_train.pkl")
preprocX_valid, preprocY_valid = preproc_data(X_valid, Y_valid, preproc_resnet,
"preproc_valid.pkl")
preprocX_test, preprocY_test = preproc_data(X_test, Y_test, preproc_resnet,
"preproc_test.pkl")
Vytvoríme si nové koncové vrstvy ako osobitnú sieť. Kvôli silnejšej regularizácii môžeme použiť dropout a pod.:
y = x = Input(shape=preproc_resnet.output_shape[1:])
y = GlobalAveragePooling2D()(y)
y = Dropout(0.5)(y)
y = Dense(train_generator.num_classes, activation='softmax',
kernel_regularizer=l2(1e-2))(y)
model = Model(x, y)
model.compile(optimizer='adam', loss='sparse_categorical_crossentropy',
metrics=['accuracy'])
Ďalej pokračujte tréningom novovytvorených vrstiev. Použite skoré ukončenie učenia (pomocou validačných dát).
Poznámka: Model trénujeme na predspracovaných dátach preprocX_train
a preprocY_train
.
_, val_acc = model.evaluate(preprocX_valid, preprocY_valid, verbose=0)
print("Správnosť na validačných dátach: {}".format(val_acc))
_, test_acc = model.evaluate(preprocX_test, preprocY_test, verbose=0)
#print("Správnosť na testovacích dátach: {}".format(test_acc))
Vytvoríme si generátor tréningových dát, ktorý bude aplikovať techniky zväčšovania dátovej množiny (data augmentation). Znamená to, že tréningové obrázky vždy mierne upraví (napr. posunutí, zväčšením, otočením a pod.). Vo výsledku sa zvýši rôznorodosť dát a nikdy sa dvakrát netrénuje presne s tým istým obrázkom.
aug_train_generator = make_generator("data/food5/training",
make_datagen(use_augmentation=True), batch_size=64)
Vytvoríme kompletný model, v ktorom už budeme trénovať aj konvolučné vrstvy. Budeme voliť nižšiu rýchlosť učenia:
model_full = Model(preproc_resnet.input, model(preproc_resnet.output))
model_full.compile(
loss='sparse_categorical_crossentropy',
optimizer=Adam(lr=1e-5),
metrics=['accuracy']
)
Model trénujeme pomocou funkcie fit_generator
, ktorá tréningové dáta berie z vyššie vytvoreného generátora:
model_full.fit_generator(
aug_train_generator,
steps_per_epoch=int(np.ceil(aug_train_generator.n /
aug_train_generator.batch_size)),
epochs=50,
validation_data=(X_valid, Y_valid),
callbacks=[EarlyStopping()]
)
Schopnosť zovšeobecňovať teraz overíme na testovacích dátach.
_, test_acc = model_full.evaluate(X_test, Y_test)
print("\nSprávnosť na testovacích dátach: {}".format(test_acc))