Как да направите FastAI още по-бързо

Ускорете предварителната обработка с помощта на вградената в паралелна функция fastai

Силата на паралелната обработка. Снимка на Марк-Оливие Джодойн на Unsplash

Влизам в първото си състезание Kaggle, вече приключилото
Предизвикателство за разпознаване на реч на Tensor Flow, което включва обработка
една секунда сурови аудиоклипове, за да разберете / предскажете коя дума се казва.

Моят метод включва преобразуване на суровото аудио в спектрограми, преди да направя класификация на изображенията. Бях изградил работещ и точен модел и бях развълнуван да го изпробвам на тестовия набор и да направя първото си представяне, когато видях, че тестовият набор е 150 000+ WAV файлове. В момента произвеждам около 4 спектрограми в секунда с размер 224x224. Това е около 10 часа, трябва да има по-добър начин.

Веднага ми хрумват многообработването и многоточковото четене, но не знам как да ги направя, изглеждат страшни и изискват повече обучение, когато чинията ми вече е наистина пълна. За щастие fastai има "паралелна" функция, която Джереми случайно споменава в Урок 7. Първата стъпка е винаги да проверявате документите.

док (паралелен)
Допълнителна документация в бележника за fastai.core.parallel

Показване в документи паралелно [източник] [тест]

Страхотно, изглежда просто, колкото може да бъде. Предавам функция и колекция от аргументи на тази функция, която да се изпълнява паралелно, а fastai обработва останалите.

Пример за играчка на паралел

Функцията по-долу ще вземе число и ще отпечата квадрат.

def print_square (число):
    печат (номер ** 2)

След това можем да генерираме списък, съдържащ списъци с числа, които искаме да получим сумите за използване на разбирането на списъка.

num_list = [x за х в обхват (10)]
паралелен (print_square, num_list)

Пуснете този код и ще видите следното:

Изглежда, че работи страхотно, но къде са нашите квадрати? Ние трябваше да ги разпечатаме. Ако прочетете по-отблизо тук, ще видите какво се случва:

Пълна документация за fastai.core.parallel

„Func трябва да приема както стойността, така и индекса на всеки елемент arr“

Функцията, която предавате, трябва да бъде от специален тип, приемайки само два аргумента:

  1. Стойност (това съдържа arr и е нормалният аргумент на вашата функция)
  2. Индексът * на стойността в arr (забележете: функцията ви всъщност не трябва да прави нищо с индекса, просто трябва да я има в определението на функцията)
* Sylvain Gugger ме информира във форумите на fastai, че индексът е необходим, за да може проверяването на снимки да работи. Също така паралелът е предназначен за функция за удобство за вътрешна употреба, така че ако сте по-напреднали, можете да внедрите вашата собствена версия с помощта на ProcessPoolExecutor

Нека да пренапишем нашата функция, за да приемем индекс. Отново нашата функция не трябва да прави нищо с индекса и дори бихме могли да я заменим с _ в дефиницията на функцията.

def print_square (число, индекс):
    печат (бр ** 2)

И сега се опитваме отново да призовем ...

Работи! Ако имате проста функция, която взема един аргумент, сте готови. Вече знаете как да използвате паралелната функция на fastai, за да го направите 2-10 пъти по-бързо!
* Това е пример за играчка и е около 1000 пъти по-бавно да се използва паралелно, вижте по-долу за реален пример с реални показатели

Как изглежда това на практика с по-реалистичен пример ...

Ето моят действителен код за генериране и запазване на спектрограми.
Оригинален код, предоставен от Джон Хартквист и Кайтан Олшевски
TLDR: Прочетете WAV файл на src_path / fname, създайте спектрограма, запишете на dst_path.

def gen_spec (име, src_path, dst_path):
    y, sr = librosa.load (src_path / fname)
    S = librosa.feature.melspectrogram (y, sr = sr, n_fft = 1024,
                                       hop_length = 512, n_mels = 128,
                                      мощност = 1,0, fmin = 20, fmax = 8000)
    plt.figure (figsize = (2.24, 2.24))
    pylab.axis ( "изключено")
    pylab.axes ([0., 0., 1., 1.], frameon = False, xticks = [], yticks = [])
    librosa.display.specshow (librosa.power_to_db (S, ref = np.max),
                             y_axis = 'mel', x_axis = 'време')
    save_path = f '{dst_path / fname} .png'
    pylab.savefig (save_path, bbox_inches = Няма, pad_inches = 0, dpi = 100)
    pylab.close ()

Преди да продължим по-нататък, нека надникнем в изходния код за паралел.

Изходният код за fastai.core.parallel, не е толкова страшен, колкото изглежда

Изглежда страховито, но всичко, което наистина се случва тук, е, че получаваме всяка стойност от нашата колекция от аргументи и я съхраняваме в o, а също така съхраняваме индекса на споменатата стойност в i и след това callfunc (o, i)
За нас това означава, че паралелно ще извика gen_spec (име, индекс) за всяко име в колекцията (в нашия случай списък), която изпращаме, и ще обработва паралелната обработка за нас, което води до много по-бърза обработка.

Но какво да правим, ако нашата функция приема повече от един аргумент?

Както можете да видите, нашата функция gen_spec взема 3 аргумента и паралелно очаква функция, която отнема два. Решението зависи от това дали нашите допълнителни аргументи са винаги еднакви като файлова пътека или константа, или дали те ще варират.

A. Ако допълнителните аргументи са фиксирани / статични, направете нова функция със стойности по подразбиране или използвайте частичния python, за да създадете функция, която отговаря на модела на паралели. Предпочитам да използвам частично, така че това ще демонстрирам по-долу.

B. Ако имате множество аргументи, които ще се променят с всяко обаждане на функция, предайте ги като набор от аргументи и след това ги разопаковайте.

Решение А: Всичките 150 000 мои wav файлове са разположени в един и същ src_path и аз ще извеждам всички спектрограми на един и същ dst_path, така че единственият променлив аргумент е fname. Това е идеалното място за използване на частично

Тъй като паралелът не индексира името изрично във функцията, винаги трябва да бъде вторият аргумент в нашата дефиниция. Нека да поправим това. Сега имаме:

def gen_spec (име, индекс, src_path, dst_path):

След това правим нова функция gen_spec_partial, преминавайки в нашите статични пътища

gen_spec_partial = частичен (gen_spec, src_path = path_audio,
                           dst_path = path_spectrogram)

Това е, свършихме. Нека създадем 1000 спектрограми, използвайки собствения си gen_spec_partial, и паралелно и сравнете колко време отнема.

296 секунди без успоредка, 104 секунди с паралел, близо 3 пъти по-бързо.

Решение Б: Сега за нашия последен случай, какво да правим, ако нашите допълнителни аргументи не са статични? Ние пренаписваме нашата функция да приемаме набор от аргументи и индекс и след това предаваме колекция от кортежи, съдържаща аргументите. За нашия пример на спектрограма, това изглежда така:

def gen_spec_parallel_tuple (arg_tuple, index):
    fname, src_path, dst_path = arg_tuple
    # останалият код е същият и е пропуснат

След това пакетираме всички аргументи, които искаме да предадем в кортеж с подходящ размер, след което предаваме gen_spec_parallel_tuple и нашия arg_tuple_list на паралел

Работи! Сега знаете как да приемате функции с произволен брой аргументи и да ги стартирате паралелно, за да ускорите предварителната си обработка и да отделите повече време за обучение.