Wykorzystanie wielu wątków w sieciach neuronowych - mała efektywność

0

Witam, zacząłem przygodę z programowaniem wielowątkowym i chciałem wypróbować ich efektywność w jednym z moich projektów - sieci neuronowych. Niżej zamieszczam prostą klasę Layer, której celem jest m.in. obliczanie wyjść wszystkich neuronów w danej warstwie (proste operacje matematyczne). Mój problem jest taki, że korzystanie z 2 wątków jest w większości przypadków znacznie mniej wydajne niż z 1...

No i moja prośba jest taka, żeby po pierwsze ktoś ocenił poniższy kod pod względem poprawności a po drugie jeśli możecie mi coś doradzić jak można to lepiej zorganizować - by efekty były bardziej widoczne :).

#pragma once
#include "Neuron.h"
#include <thread>

using namespace std;

class Layer
    {
    public:
        Layer();
        ~Layer();

        void get_outputs_standard()    //standardowa funkcja
            {
            for(unsigned int i = 0; i < neurons.size(); i++)
                outputs[i] = neurons[i].get_output(inputs);        
            }

        void get_outputs_1_thread()
            {
            for(unsigned int i = 0; i < neurons.size() / 2; i++)
                outputs[i] = neurons[i].get_output(inputs);
            }
        void get_outputs_2_thread()
            {
            for(unsigned int i = (neurons.size() / 2); i < neurons.size(); i++)
                outputs[i] = neurons[i].get_output(inputs);
            }

        void set_neurons(int neurons_number, int input_number, double* inputs)
            {
            neurons.resize(neurons_number);
            outputs.resize(neurons_number); //edit: tu był mały błąd ;)
            this->inputs = inputs;

            for(int i = 0; i < neurons_number; i++)
                neurons[i].create_weights_array(input_number);
            }

        void go_threads();

        void(Layer::*wsk)();

        vector<double> outputs;
    private:
        vector<Neuron> neurons;    //to można dać jako wskaźnik...
        double* inputs;
    };

inline void Layer::go_threads()
    {
    thread t1(&Layer::get_outputs_1_thread, this);
    thread t2(&Layer::get_outputs_2_thread, this);
    t1.join();
    t2.join();
    }

Mam nadzieję, że w/w kod nie wymaga komentarza - raczej nie ma tu nic skomplikowanego ;). Funkcja

get_output(double* inputs)

w klasie Neuron liczy sumę iloczynów wag i wejść do neuronu (inputów) i zwraca wynik wybranej funkcji z argumentem tego wyniku.

0

Jaki kompilator?

0

Ja się podepnę, bo jestem ciekaw.
Czy tam na outputs i neurons nie trzeba będzie jakimiś mutex'ami pozabezpieczać?
Nawet jeśli funkcje operują na innych częściach kontenerów...

0

Jeśli w tym czasie nic innego nie będzie się działo na tej tablicy to nie trzeba. Nie piszę w C++, ale neurons.size() wywoływane jest bodajże przy każdym sprawdzeniu warunku w pętli - to może spowolnić program (nie wiem jak to jest zaimplementowane), spróbuj wyłączyć pół rozmiaru i rozmiar przed pętlę.

0

size() a złożoność const, więc czy ją wywołasz go w 2 wątkach n/2 razy, czy w jednym n razy wyjdzie na jedno...

0
_13th_Dragon napisał(a):

Jaki kompilator?

Kompilator to: VS 2013 Express

0

Ok, poniżej wnioski do jakich doszedłem i trochę o tym co dziś robiłem :)
Zacznę może od pokazania funkcji main, gdzie testowałem wydajność:

#include "stdafx.h"
#include <iostream>
#include <vector>
#include "Layer.h"
#include <random>
#include <thread>
#include <time.h>

using namespace std;

int _tmain(int argc, _TCHAR* argv[])
    {
    srand(1);

    clock_t start, stop, wynik1, wynik2;

    int inputs_number = 1000;
    int neurons_number = 10000;
    double* inputs = new double[inputs_number];

    for(int i = 0; i < inputs_number; i++)
        inputs[i] = (double)rand() / (double)RAND_MAX;

    Layer layer;
    layer.set_neurons(neurons_number, inputs_number, inputs);

    int iter = 1;
    
    start = clock();
    for(int i = 0; i < iter; i++)
        layer.get_outputs_standard();
    stop = clock();

    wynik1 = stop - start;

    //for(int i = 0; i < neurons_number; i++)
    //    cout << i << "output[i] = " << layer.outputs[i] << endl;
    //cout << endl;

    for(int i = 0; i < neurons_number; i++)
        layer.outputs[i] = 0.0;

    start = clock();
    for(int i = 0; i < iter; i++)
        layer.go_threads();
    stop = clock();

    wynik2 = stop - start;

    //for(int i = 0; i < 10; i++)
    //    cout << i << "output[i] = " << layer.outputs[i] << endl;
    //cout << endl;

    cout << "wynik1 = " << wynik1/1000.0 << endl;
    cout << "wynik2 = " << wynik2/1000.0 << endl;

    getchar();
        return 0;
    }

Jak widać, utworzyłem sobie zmienne dot. ilości inputów i neuronów w danej warstwie na bazie któych tworzyłem tablicę inputów i neuronów w jednej warstwie a następnie wrzucałem inputy do warstwy :).
W przypadku, gdy inputów i neuronów było stosunkowo niewiele np. 100 i 100 to zdecydowanie lepiej było wykorzystać do obliczeń 1 wątek. Natomiast jeśli chodzi o większe zestawy typu 1000 i 1000 to przy kompilacji jako debug było widoczne prawie 4 krotne przyśpieszenie w stosunku do 1 wątku (zrobiłem analogiczny kod dla 4 wątków i tyle posiadam na komputerze). Niestety przy kompilacji w wersji release różnica w prędkości jest albo na niekorzyść wielu wątków lub jest bardzo znikoma...

No i pytanie teraz kolejne dlaczego? Takiego zachowania bardziej bym się spodziewał w wersji debug...

Wypróbowałem również funkcję CreateThread(...) z biblioteki windows.h jednak wyniki wydają się dość podobne w porównaniu do klasy std::thread...

Reasumując: wykorzystanie wielu wątków w celu przyśpieszania obliczeń w tym przypadku chyba jest bezcelowe - w przypadku niewielkich sieci lub wielu warstw efekty mogą być znacznie gorsze niż wykonanie ich na 1 wątku...

A co Wy o tym wszystkim myślicie?

0

Problemem jest zbyt mała ilość przetwarzanych danych. Koszt tworzenia wątku jest większy aniżeli zysk z obliczeń równoległych. Jeżeli nie ma możliwości zrównoleglenia większej części algorytmu, co pozwoliłoby przetwarzać większe porcje danych przez wątki, to należy utworzyć wątki raz i przesyłać do nich kolejne porcję danych do obliczeń. W ten sposób uniknie się narzutu wynikającego z tworzenia wielu, krótko działających wątków.

0

Problem jest taki, że nawet jak dałem większą ilość iteracji na każdym wątku (w funkcji wykonywanej przez wątek a nie w main) to również zysk nie przekraczał podwójnej szybkości (mimo odpalenia 4 wątków)...
Wydaje mi się, że kosztowne jest nie tylko utworzenie samego wątku ale również jego wykorzystywanie... - chociaż pewności nie mam... W innym wypadku wraz ze zwiększającą się ilością iteracji szybkość powinna zbliżać się do 4 krotności operacji wykonywanych na 1 wątku (zakładając, że mam 4 rdzenie).

1

Pozostaje jeszcze kwestia tego, co tak naprawdę jest robione w tych wątkach. Jeżeli większość operacji sprowadza się do kopiowania danych z/do pamięci, to wąskim gardłem będzie jej przepustowość. Zamieść przykład kompletnie działającego kodu, łatwiej będzie dojść przyczyny.

0
Pebal napisał(a):

Pozostaje jeszcze kwestia tego, co tak naprawdę jest robione w tych wątkach. Jeżeli większość operacji sprowadza się do kopiowania danych z/do pamięci, to wąskim gardłem będzie jej przepustowość. Zamieść przykład kompletnie działającego kodu, łatwiej będzie dojść przyczyny.

A wiesz, że o tym nie pomyślałem ;)...
Ale to i tak trochę dziwne... Nie podoba mi się to ;-(

Jeśli chodzi o kod to skrócony do minimum wygląda następująco:

plik main (chyba nie wymaga komentarza):

#include "stdafx.h"
#include "Layer.h"
#include <random>
#include <time.h>

using namespace std;

int _tmain(int argc, _TCHAR* argv[])
    {
    srand(1);
    clock_t start, stop, wynik1, wynik2;

    const int thread_num = 4;

    int inputs_number = 100;
    int neurons_number = 100;

    double* inputs = new double[inputs_number];

    for(int i = 0; i < inputs_number; i++)
        inputs[i] = (double)rand() / (double)RAND_MAX;

    Layer layer;
    layer.set_neurons(neurons_number, inputs_number, inputs);
    
    start = clock();
    layer.get_outputs(thread_num);
    stop = clock();
    wynik1 = stop - start;

    start = clock();
    layer.get_outputs_standard();
    stop = clock();
    wynik2 = stop - start;

    cout << "wynik threads = " << wynik1 / 1000.0 << endl;
    cout << "wynik standard = " << wynik2 / 1000.0 << endl;

    getchar();
        return 0;
    }

plik Layer z komentarzami:

class Layer
    {
    public:
        Layer();
        ~Layer();
        int iter = 100;    //ilość iteracji w danym wątku -> tylko w celach testowych!!!

        void set_neurons(int neurons_number, int inputs_number, double* inputs)
            {
            neurons.resize(neurons_number);    //tablica neuronów
            if(outputs)    //nullptr w konstruktorze
                delete[] outputs;
            outputs = new double[neurons_number]{0};    //tablica outputów

            this->neurons_number = neurons_number;
            this->inputs = inputs;
            this->inputs_number = inputs_number;

            for(int i = 0; i < neurons_number; i++)
                neurons[i].create_weights_array(inputs_number);    //tworzy tablicę z wagami w każdym neuronie (i nadaje im wartości początkowe (losowe))
            }

        void get_outputs_standard()    //standardowa funkcja zapisująca wyjścia neuronów do tablicy
            {
            for(int k = 0; k < iter; k++)
            for(unsigned int i = 0; i < neurons.size(); i++)
                outputs[i] = neurons[i].get_output(inputs);
            /*
            funkcja get_output(double*&) w neuronie jest następująca:
            inline double& Neuron::get_output(double*& inputs)
                {
                double help_var = bias;
                for(unsigned int i = 0; i < weights.size(); i++)
                    help_var += weights[i] * inputs[i];

                output = act_fun(help_var);    //funkcja aktywacji
                neuron_derivative = deriv_fun(output) + improve_deriv;    //pochodna dla neuronu

                return output;
                }
            */
            }

        void get_outputs_using_threads(int thread_number, int all_threads)    /*odpowiednik w/w funkcji - korzystający z zadeklarowanej liczby wątków*/
            {
            for(int k = 0; k < iter; k++)
            for(unsigned int i = thread_number; i < neurons.size(); i = i + all_threads)
                outputs[i] = neurons[i].get_output(inputs);
            }

        void get_outputs(int thread_number)    /*trochę zmodyfikowana poprzednia funkcja -> korzysta z tylu wątków, ile się wpisze*/
            {
            thread** tab = new thread*[thread_number - 1];

            for(int i = 0; i < thread_number - 1; i++)
                tab[i] = new thread(&Layer::get_outputs_using_threads, this, i, thread_number);
            get_outputs_using_threads(thread_number - 1, thread_number);    //ostatni wątek wykonywany jest przez bieżący proces...    

            for(int i = 0; i < thread_number - 1; i++)
                {
                (*tab[i]).join();
                delete tab[i];
                }
            delete[] tab;
            }
    private:
        unsigned int neurons_number;
        unsigned int inputs_number;
        vector<Neuron> neurons;    //to można dać jako wskaźnik...
        double* outputs;
        double* inputs;    //wskaźnik
    };

Jeśli ktoś znajdzie chwilkę, by przeanalizować moje dzieło i wskazać ewentualne wąskie gardła etc. to będzie miło i będę bardzo wdzięczny :)

0

Brakuje jeszcze źródła klasy Neuron.

0
Pebal napisał(a):

Brakuje jeszcze źródła klasy Neuron.

W tym wypadku klasa Neuron jest mało istotna. Jedyna funkcja, która jest tutaj istotna została opisana w komentarzu. Okrojona i trochę zmodyfikowana wersja tej klasy może wyglądać następująco:

#include <random>
class Neuron
    {
    public:
        Neuron()
            {
            act_fun = fun_ack_1;
            deriv_fun = fun_der_1;
            }
        void create_weights_array(unsigned int inputs_number)
            {
            weights.resize(inputs_number);
            for(unsigned int i = 0; i < inputs_number; i++)
                weights[i] = rand()/RAND_MAX;
            bias = rand() / RAND_MAX;
            }
        double& get_output(double*& inputs)
            {
            output = bias;
            for(unsigned int i = 0; i < weights.size(); i++)
                output += weights[i] * inputs[i];
        
            output = act_fun(output);
            neuron_derivative = deriv_fun(output) + improve_deriv;

            return output;
            }

        static double fun_ack_1(double& x)    //wrzeczywistości to jest wskaźnik na funkcję, która tak wygląda
            {
            return (1.0 - exp(-x)) / (1.0 + exp(-x));
            }

        static double fun_der_1(double& x)
            {
            return (1.0 - (x*x));
            }
        vector<double> weights;
        double bias;
        double output;
        double neuron_derivative;
        double(*act_fun)(double&);
        double(*deriv_fun)(double&);
    };
0

Ogólnie schemat tej sieci jest do bani i nadaje się tylko do kosza. Chyba ze chcesz używać tylko sieci jednowarstwowych. Jeśli chcesz mieć więcej niz jedna warstwę to twój model do niczego się niestety nie nadaje.
Dla sieci wielowarstwowych wyliczanie pobudzeń musi iść "od tyłu" a nie od przodu. Nie bez przyczyny nazywa się je sieciami feed-forward.
Narysuj sobie na kartce sieć z kilkoma warstwami i zobacz ile razy będziesz musiał wyliczyć pobudzenie neuronu w przedostatniej warstwie jeśli będziesz chciał odczytać odpowiedź sieci "od przodu".

0
Shalom napisał(a):

Ogólnie schemat tej sieci jest do bani i nadaje się tylko do kosza. Chyba ze chcesz używać tylko sieci jednowarstwowych. Jeśli chcesz mieć więcej niz jedna warstwę to twój model do niczego się niestety nie nadaje.
Dla sieci wielowarstwowych wyliczanie pobudzeń musi iść "od tyłu" a nie od przodu. Nie bez przyczyny nazywa się je sieciami feed-forward.
Narysuj sobie na kartce sieć z kilkoma warstwami i zobacz ile razy będziesz musiał wyliczyć pobudzenie neuronu w przedostatniej warstwie jeśli będziesz chciał odczytać odpowiedź sieci "od przodu".

@Shalom dzięki za słuszne uwagi, ale podana przeze mnie warstwa to tylko jedna z wielu, które są między sobą połączone. Testy wykonuję tylko na jednej warstwie z uwagi na to, że tak jest mi po prostu łatwiej ;D -> jeszcze nie skończyłem mojego całego projektu a byłem bardzo ciekawe: czy sama warstwa działa poprawnie oraz czy uda się przyśpieszyć obliczenia zaprzęgając do tego więcej wątków ;). Jeśli działa na 1 warstwie to będzie działać na pozostałych. Niestety z testów wynika, że korzyści są tylko przy naprawdę dużych strukturach...
Podane przeze mnie klasy są w dodatku bardzo uproszczone - gdyż w całości znacznie trudniej byłoby je tutaj analizować (i pewnie nikt by im nie poświęcił czasu). Dodatkowo ja po raz pierwszy wykorzystałem więcej niż 1 wątek i miałem obawy czy sobie z tym poradzę :D.

Problem jednak jest innej natury: dlaczego przy zastosowaniu wielu wątków/rdzeni (4) sieć przynosi niewielką korzyść i to dopiero przy około 1000 wejściach i 1000 neuronach? Gdy wykorzystam mniejszą ilość danych to wykorzystanie wielowątkowości jest niekorzystne... Rozumiem, że utworzenie wątku jest czasochłonne no ale, wydawało mi się, że korzyści powinny być szybciej widoczne...
Do tego korzyść jest stosunkowo niewielka (procesor jest wykorzystywany w 100% (4 rdzeniowy) a korzyść nie przekracza 25% i to w najlepszym wypadku przy naprawdę dużej liczbie danych).

Obecnie analizuję tylko 1 warstwę, gdyż idea jest taka, że warstwa jako taka będzie podzielona na 4 części i każdy wątek będzie obliczał wyjścia ze swojej puli neuronów. Ciężko mi wymyślić inny pomysł na wydajniejsze wykorzystanie wielu rdzeni. Tzn. mam jeden, ale on jest bardziej skomplikowany i wymaga utworzenia 4 identycznych sieci (wykorzystanie wątków tylko w procesie nauki).

0

Moja rada: przepisz to na feed-forward to szybkość wzrośnie ci o kilka rzędów wielkości, a nie kombinuj z wątpliwymi optymalizacjami z wątkami ;)

0
Shalom napisał(a):

Moja rada: przepisz to na feed-forward to szybkość wzrośnie ci o kilka rzędów wielkości, a nie kombinuj z wątpliwymi optymalizacjami z wątkami ;)

Ale to jest feed-forward :), chyba, że ja nie rozumiem czegoś :P.
Klasa Layer ma N neuronów, których wyjścia są zapisywane w klasie Layer. Następna warstwa ma wskaźnik do tej tablicy outputów (outputy z warstwy l są inputami w warstwie l+1) i na jej podstawie wykonuje obliczenia na swoich neuronach. Gdy dojdzie do ostatniej warstwy inna klasa sprawdza jej wyniki, porównuje z patternami i puszcza błąd do ostatniej warstwy. Ostatnia warstwa oblicza delty zapisuje pochodne i oblicza errory dla poprzedniej warstwy. I tak do ostatniej/pierwszej warstwy ;).
W przedstawionej klasie brak m.in. wskaźników:

Layer* next_layer;
Layer* prev_layer;

Ale one są i z nich korzystam :D

1 użytkowników online, w tym zalogowanych: 0, gości: 1