Sztuczna inteligencja: wyzwania i ryzyka oczami inżyniera

Dzień dobry koleżanki i koledzy. Jako inżynier chciałbym uważniej spojrzeć na tak popularne dzisiaj zjawiska jak sztuczna inteligencja oraz uczenie maszynowe, uporządkować fakty i zbudować strategię sukcesu. Dlaczego podobna analiza jest tak ważna? Dlatego, że kiedy od modeli laboratoryjnych w python, matplotlib, numpy czy lua projekt jest przenoszony do obciążonej produkcji na serwerze klienta, a błąd w danych wyjściowych rujnuje wyniki całego wysiłku, to przestaje być wesoło.

 

Współczesny development

Zacznijmy od przypomnienia sobie, w jaki sposób odbywa się budowa oprogramowania. Jako podstawy z reguły używa się klasycznych, dobrze przestudiowanych w ostatnim stuleciu algorytmów: wyszukiwanie, sortowanie itd. Ulubieniec programistów SZBD (system zarządzania bazą danych aka DBMS), to dobry przykład dokładnie sprawdzonego algorytmu "z brodą".

Do tego są dodawane uznawane przez całe społeczeństwo inżynierów standardy sieci (DNS, TCP/IP), formaty plików, serwisy systemu operacyjnego (POSIX) itd. 

Kolejnym etapem jest język. Niestety, w ostatnich latach nie nastąpiła żadna rewolucja w tym obszarze: automatyczne gettery i settery są dodawane i usuwane, typy są automatycznie wprowadzane i wyprowadzane (jakby one rzeczywiście miały znaczenie), krążą wokół idei funkcyjnego programowania z “Alicji w krainie czarów” i Haskel w Scala. Próbuje się cząstkowo załatać dziury  С++ w Rust, tworzy się C dla humanistów w postaci Go, po raz kolejny wymyślany “od zera” jest javascript (ECMAScript 6). Wszystkie te działania, niebezpodstawnie, są podgrzewane przez idee wielowątkowego programowania na wielordzeniowych procesorach i GPU. Możliwe w klasterach, za pomocą akt, danych immutable, currying i rekursywnej budowy drzewa binarnego. Rewolucji w zasięgu wzroku jednak póki co nie widać.  Logika zetknęła się z fizyką i wszystkie pomysły rozbijają się o ograniczenia ludzkiego mózgu, błędy kompilatorów i naturalną cechę ludzi - przepraszam za te słowa - aby popełniać błędy i nieumyślnie produkować tony bugów. Coraz bardziej aktualnie w tym kontekście brzmią słowa Edsgera Dijksta’y  o tym, że poważne programowanie jest przywilejem  mądrych ludzi. Choć jest nadzieja, że w przyszłości pomogą nam futurystyczne komputery kwantowe - ludzie lubią wierzyć w cokolwiek. 

Na to, aby nie zgubić się w mnogości  języków, a kod nie wykrwawił  się od bugów, istnieje jedna sprawdzona metoda - pisać... “poprawnie”. Aby zrozumieć znaczenie słowa “poprawnie”, trzeba przeczytać dużo książek, przestudiować setki linijek kodu w trybie “jutro punkt dziesiąta aplikacja powinna działać i cieszyć klientów”, wypić hektolitry kawy i zepsuć niemało klawiatur kolegów. Jednak ten sposób działa. Zawsze. 

 

Świat oczami inżyniera

Jeśli mówimy o bibliotekach, oczywiście żadna nie jest tworzona “od zera” - nie miałoby to żadnego sensu, za to generowałoby ogromne koszty. Jednak kiedy korzystamy z “cudzych” tworów, zawsze akceptujemy ryzyko, że zawierają one błędy i na ten obszar nie mamy wpływu. Ponadto, co chwilę słyszymy od wszystkich: “bierz gotową, oni użyli takiej i wszystko działa!”. I wszyscy, oczywiście, postępują dokładnie tak. Korzystają z Linuxa (system operacyjny, choć tak naprawdę taka sama  “biblioteka” dostępu do hardware), nginx, apache, mysql, php, standardowych bibliotek kolekcji (java, c++ STL). Biblioteki, niestety, dość mocno różnią się między sobą prędkością, stopniem udokumentowania oraz liczbą “niepoprawionych błędów” —  dlatego sukces osiągają tylko mające “wyczucie” zespoły.

Tak więc, w teorii, po dokonaniu pewnego wysiłku, można stworzyć adekwatne oprogramowanie w bardzo krótkim czasie przy bardzo ograniczonych zasobach, używając jedynie sprawdzonych matematycznie algorytmów i biblioteki o dostatecznym, niekrytycznym stopniu “wadliwości”, w sprawdzonym języku programowania. Nie znajdziemy zbyt wiele dobrych studiów przypadku na tej ścieżce, ale takowe są ;-)

Uczenie maszynowe

Mówi się głównie o algorytmach uczących się. Na przykład “biznes” zebrał pewną ilość ”bigdaty” i chce ten zbiór monetyzować. Potrzebny jest algorytm, który pozwoli pomóc w tym klientom “biznesu” i/albo podwyższyć wydajność pracy tego przedsięwzięcia. Rozwiązanie zadania w sposób analityczny może być bardzo trudne, a nawet niemożliwe. Może zająć lata pracy ekspertów i jest zależne od ogromnej liczby czynników. W teorii można też postąpić bezczelnie: wskakujemy na naszego rumaka (głęboką sieć neuronową), jeździmy po danych i spinamy jej ostrogi do czasu, aż poziom błędu nie zejdzie poniżej odpowiedniej wartości (pamiętajmy, że istnieją dwa typy błędów: błąd uczenia się i błąd generalizacji na testowym zestawie danych). Krążą legendy o tym, że przy na milionie i więcej przykładów, głęboka sieć neuronowa potrafi “myśleć” na poziomie człowieka, a nawet jest w stanie wyprzedzić homo sapiens, jeśli dostanie większy zestaw danych. Kiedy przykładów jednak mamy mniej, sieć może nadal pozostać pożyteczna nawet nie zastępując człowieka. 

Brzmi to ładnie i praktycznie: mamy “duże” dane, dajemy sieci komendę “aport”, “ucz się i pomagaj człowieczeństwu”. Diabeł jednak tkwi w szczegółach. 

Próg wejścia w development  i uczenie się maszynowe

Technologie developmentu można podzielić na dwie kategorie w zależności od wysokości barier wejścia do pracy z nimi. Większość zajmuje się prostymi i dostępnymi technologiami. Często te osoby nie mają wykształcenia w tej dziedzinie. W takim otoczeniu często tworzy się wiele niepoprawnych, krótkotrwałych i walczących ze sobą bibliotek. Do tej niszy dobrze pasują JavaScript i Node.js. Jeśli jednak podejść do sprawy poważnie, po zagłębieniu się w sprawę można zobaczyć sporo nieoczywistych na pierwszy rzut oka szczegółów. Właśnie wtedy  zaczynają się wyróżniać prawdziwi Guru przez duże “G”. Jednak by zostać zwykłym developerem w tej grupie wystarczy siatka na motyle i wolny weekend.

 

Młodzi front-end deweloperzy JavaScript. Jedne wakacje nie wystarczą by zostać mega-guru.

Do “średniej” według bariery wejścia kategorii można zaliczyć dynamiczne języki programowania typu: PHP, Python, Ruby, Lua. Tutaj sprawa jest znacznie trudniejsza –  bardziej rozwinięte koncepcje programowania obiektowego, jedynie częściowo dostępne możliwości funkcyjnego programowania, a tylko czasami dostępna jest podstawowa wielowątkowość, modularność. Dostępne są także funkcje systemowe, cząstkowo realizowane są standardowe typy danych i algorytmy. W ciągu tygodnia, bez spiny, można się w tym odnaleźć i nauczyć się tworzyć pożyteczny kod, nawet nie mając specjalistycznego wykształcenia.

 

Dynamiczne słabo typowane języki skryptowe mają niewysoki próg wejścia. Przez wakacje można się nauczyć, jak rozpalać ogień i łowić ryby.

Do “najwyższej” kategorii zaliczane są sprawdzone już języki przemysłowe typu C++, Java, C# i zaczynające deptać im po piętach Scala, bash i VisualBasic (ostatnie dwa to tylko mały żarcik:). Tutaj zobaczymy bardzo rozwinięte narzędzia przemysłowe sterowania złożonością, jakościowe biblioteki struktur danych i algorytmów, ogromną ilość wysokiej jakości dokumentacji, profilowania i świetne otoczenia wizualne developmentu. Wstąpić do tej kategorii specjalistów najprościej jest osobom, które mają specjalistyczne wykształcenie, ogromną pasję do programowania, kilka lat intensywnego doświadczenia oraz dobrą znajomość algorytmów i struktur danych, ponieważ prace często się toczą na bardzo głębokim poziomie. Niezbędna jest wiedza na temat szczegółów funkcjonowania systemu operacyjnego czy protokołów sieciowych.

 

Przemysłowe języki programowania wymagają wielogodzinnej praktyki  i nie wybaczają błędów.

Wychodzi więc na to, że początkującego dewelopera od pożytecznego inżyniera dzieli zaledwie kilka miesięcy ciężkiej pracy czy lata doświadczenia projektowego i mycia tablicy po brainstormach.

Pewny aspekt maszynowego uczenia się wygląda jednak inaczej. Analitycy w tej dziedzinie często mają wrodzone predyspozycje. Proces nauki przypomina naukę gry na instrumencie: 2-3 lata solfeggio, 2-3 lata nieustannych praktyk gam, 3 lata w orkiestrze, 5 lat własnej jeszcze bardziej męczącej pracy i wylane litry potu. Aby nauczyć człowieka podstaw matematyki i statystyki, analizy matematycznej, algebry liniowej i rachunku różniczkowego, teorii prawdopodobieństwa, nie wystarczy kilku miesięcy, potrzebne są lata, a nawet wtedy nie każdy da radę czy będzie chciał kontynuować naukę. Wielu studentów może zrezygnować i przejść na inny wydział. Nie każdy może zostać profesorem.

 

Praktyka  analityków

A może jednak się uda? Przez weekend się nauczę, opanuję materiał.  Niestety, aby zrozumieć, jak działa najbardziej elementarna w uczeniu się maszynowym regresja logistyczna, która jest swojego rodzaju «hello world» w programowaniu, trzeba najpierw przestudiować co najmniej kilka rozdziałów zaawansowanej matematyki: teorię prawdopodobieństwa i algebrę liniową. Dla zrozumienia logiki “stochastycznego osłabienia gradientowego” przydadzą się co najmniej podstawy rachunku różniczkowego.

Teraz staje się oczywiste, dlaczego parada “uniwersalnych” frameworków zaczęłą się dopiero w 2015 roku. Uczenie maszynowe zyskało na popularności po raz trzeci dopiero w 2006 roku, po wielu dziesięcioleciach niepewności i stagnacji. GPU dopiero niedawno się znalazły we właściwym miejscu i czasie.

Niestety, TensorFlow jest na razie bardzo wolny i dziwaczny na produkcji, Torch7 cierpi na brak dobrej dokumentacji i obecność języka Lua, deeplearning4j próbuje okiełznać GPU, a takich kandydatów, jak Theano w pythonie, nie da się używać bez pomocy ciężkich narkotyków.  Istnieją legendy, że uczenie sieci neuronowej to nie to samo, co jej eksploatacja i że powinni się nią zajmować inni ludzie z wykorzystaniem innych technologii - jednak w rzeczywistości liczą się pieniądze, a więc takie rozwiązanie zostanie zdyskwalifikowane jako zbyt drogie i niewygodne. Najbardziej uniwersalną i przystosowaną do rozwiązywania konkretnych zadań biznesowych w najkrótszym terminie w “normalnym” języku biblioteką na razie jest tylko deeplearning4j – choć nawet ona znajduje się na etapie szybkiego wzrostu i rozwoju ze wszystkimi tego konsekwencjami.

Jak wybrać dobrą architekturę do rozwiązania zadania biznesowego?

Nie każdy może studiować publikacje naukowe, pisane trudnym językiem z użyciem metod analizy matematycznej, dlatego więc dla większości inżynierów najbardziej stosownym sposobem na analizę możliwości architektury będzie zagłębienie się w plikach wyjściowych frameworków. W większości przypadków pisanych w “przeklętym” studenckim python i studiowanie wielu przykładów, których liczba w różnych frameworkach staje się coraz większa. 

Z tego wychodzi taka recepta: wybranie najbardziej pasującej do zadania  architektury spośród dostępnych przykładów i zakodowanie jej jeden do jednego w najbardziej wygodnym frameworku. Wszystko kończymy krótką modlitwą o szczęście. Szczęście oznacza, że wszystko zadziała dla naszych danych. W praktyce mamy do czynienia z takimi zagrożeniami:

1) Architektura sieci dobrze działa na danych badającego, jednak absolutnie nie chce współpracować z Waszymi albo na odwrót.

2) W wybranym frameworku może brakować całego spektrum podstawowych “klocków”: automatycznej dyferencjacji, algorytmu odświeżania (updater), rozszerzonych środków regularyzacji (dropout i innych), potrzebnej funkcji błędu, pewnej operacji z danymi (wektorowe odtwarzanie). Możecie zastąpić je przez analogiczne, ale narazi Was to na nowe ryzyka.

3) Najprawdopodobniej, sieć neuronową trzeba będzie dopasować według dodatkowych wymogów biznesowych, które pojawiły się później i absolutnie nie wpasowują się do świata matematyki. Przykład – znaczące obniżenie poziomu defektu fałszywie-pozytywnego, zwiększenie Recall, obniżenie czasu uczenia się, dostosowanie modelu do znacznie większego zestawu danych, dodanie i wzięcie pod uwagę nowe informacje. Tutaj z reguły trzeba dość mocno zagłębić się w architekturę sieci. Przykręcać i odkręcać ukryte śrubki – a dokręcanie śrubek na opak to droga do przekroczenia terminu releasu i nocnych koszmarów. Nawet nie będąc profesorem, można spędzić miesiąc, dwa czy pół roku ze śrubokrętem i zagubioną miną.

 

Co zrobić, którą śrubkę skręcić? Developer C++  będzie próbował zrozumieć różnicę pomiędzy softmax i softsign

Dzień dobry,  tensory!

Dla inżyniera tensor – to “zwykły” wielowymiarowy masyw, nad którym można dokonywać różnorodnych działań i składać mu ofiary. Do pracy tensorami trzeba jednak się przyzwyczaić. W ciągu pierwszych kilku tygodni głowa może boleć od nawet najprostszych trójwymiarowych przypadków, nie mówiąc nawet o bardziej “głębokich” tensorach dla sieci rekurencyjnych. Można spędzić sporo czasu manipulując na niskich poziomach, poprawianiu liczb w tensorach i wyszukiwaniu błędów w jednym znaczeniu z 40 000. Ważne, aby mieć takie ryzyko na uwadze podczas planowania – tensory tylko z pozoru są proste.


 

Bądźcie uważni! Próba wyobrażenia sobie struktury czterech i większej liczby wymiarów tensora  prowadzi do agresywnego zeza.

Szczegóły pracy z GPU

Z początku może nie być to oczywiste, ale często szybciej można “uczyć” sieć i otrzymywać od niej odpowiedzi wtedy, kiedy wszystkie niezbędne dane są zapisane w pamięci GPU. Pamięć tych drogocennych i tak lubianych przez gejmerów urządzeń jest ograniczona i najczęściej znacznie mniejsza od pamięci serwera. Trzeba więc dość często zgadzać się na kompromis: wprowadzać pośrednie cache’owanie tensorów w pamięci operacyjnej. Dlatego bierzemy pod uwagę również ryzyko dotyczące pracochłonności i zapotrzebowania na godziny pracy. Wszystkie terminy można śmiało pomnożyć razy trzy. 

 

Sieć neuronowa w produkcji

Załóżmy, że mamy szczęście. Rzetelnie popracowaliśmy i udało się nam doprowadzić działający prototyp do jakości produkcyjnej, załadowaliśmy się do pamięci serwera czy od razu do pamięci GPU, ale… dane są zmienne. Musimy dostosowywać czy douczać model według zmian tych danych. Musimy nieustannie pilnować jakości pracy sieci neuronowej, mierzyć jej dokładność i inne parametry, w zależności od zadania biznesowego. Dogłębnie przemyśleć porządek jej odnawiania i uczenia. Pracy w tym przypadku jest znacznie więcej, niż w klasycznym systemie zarządzania bazą danych. Zapomnijcie o wsparcu które polega na optymalizacji raz na 5 lat i usuwaniu pajęczyny z płyty głównej raz na 10 lat:-)

Krążą legendy, że sieć neuronową można po prostu “douczyć” i nie będzie przeprowadzać ją ponownie z  przez cały zakres danych. W rzeczywistości, zrobić tak się nie da, choć każdy by bardzo chciał. Niektórzy nawet mają szczęście. Jeśli objętość danych jest wystarczająco mała to powinno się zapamiętywać ich jak najwięcej (beż obniżenia poziomu błędu na zestawie testowym oczywiście) - tylko tak, by się ich douczać, ale nie przez uczenie od początku całego zestawu danych i bez zapominania starych. Nie ma żadnych gwarancji, że stochastyczne osłabienie gradientowe (SGD) po zapamiętaniu nowych danych będzie pamiętało stare ;-) Jeśli operujemy na dużej liczbie danych (na przykład milionach zdjęć) i nie jest wymagane, aby zapamiętać konkretny przykład, to takie podejście zadziała. 

 

Developer, tester i samobójstwo

Nie każdy ma świadomość tego, że w klasycznym programowaniu błędy mogą się tworzyć tylko w kodzie albo bibliotece. W przypadku uczenia i eksploatacji sieci neuronowej sprawa staje się znacznie bardziej skomplikowana, bo:

1) Sieć zaczyna działać niepoprawnie dlatego, że dane - ukryte w wyjściowym zestawie danych - zmieniły się. Tak o, po prostu.

2) Błąd architektury sieci neuronowej pojawia się po zmianach w danych wyjściowych. Zakładamy kask i studiujmy gradienty i wagi każdej z warstw – kask pełni jedynie funkcję estetyczną, bo i tak głowa boli mocno.

3) Mamy szczęście i widzimy błąd we frameworku czy sieci naturalnej… na tydzień przed releasem. 

Opisane wyżej ryzyka uczą nas temu, że programowanie serwisów klienckich z użyciem głębokiego uczenia maszynowego powinno być dokładne i doskonale, pokrywając cały istniejący i nawet jeszcze nie stworzony kod siatką asercji, testów, komentarzy i dokładnie zalane nalewką z perfekcjonizmu graniczącego z paranoją.

Wnioski

W sposób szczery i otwarty postarałem się opisać z punktu widzenia inżyniera kluczowe fakty i ryzyka, związane z wprowadzeniem i używaniem głębokich sieci neuronowych. Nie wyciągam na razie żadnych wniosków. Pokazałem, że to jest bardzo trudna praca, jednak jednocześnie szalenie ciekawa. Że sukces potrafią osiągnąć tylko profesjonaliści, którzy umieją połączyć wiedzę ludzką z różnych obszarów i mające intuicję do synergii. Oczywiście nie wystarczy tylko umiejętność programowania i jako takie wyczucie systemu. Trzeba albo mieć własne głębokie zrozumienie tematu albo zapraszać do współpracy ekspertów z zakresu matematyki i tworzyć dla im warunki, które pobudzają ich kreatywność. W przeciwnym wypadku, możemy spędzić długie miesiące, odkręcając wspomniane wcześniej przypadkowe śrubki i z zazdrością patrzeć na blask potęgi i piękna sztucznej inteligencji z rozwiązania konkurencji. Życzę Wam inżynieryjnego powodzenia, dobrych sieci, pewności siebie, energii i jak najmniejszej ilości przeoczonych błędów! ;-)