Hlavní navigace

Framework Torch: využití konvolučních sítí pro rozpoznávání a klasifikaci obrázků

Pavel Tišnovský

V již desáté části seriálu o frameworku Torch si ukážeme jeden z praktických způsobů využití takzvaných konvolučních neuronových sítí pro rozpoznávání a klasifikaci objektů v rastrových obrázcích.

Doba čtení: 31 minut

11. Aplikace roztřesení (jitter)

12. Sada trénovacích obrázků

13. Sada validačních obrázků

14. Funkce pro načtení trénovacích obrázků a vytvoření trénovacích dat pro síť

15. Funkce pro vlastní tréning sítě

16. Průběh tréningu

17. Validace sítě a výpočet chybných úsudků

18. Úplný zdrojový kód příkladu s konvoluční neuronovou sítí

19. Repositář s demonstračními příklady

20. Odkazy na Internetu

1. Framework Torch: využití konvolučních sítí pro rozpoznávání a klasifikaci obrázků

V předchozím článku jsme si vyzkoušeli použití neuronové sítě pro rozpoznávání číslic 0 až 9 v obrázcích s malým rozlišením. Mj. jsme zjistili, že běžné neuronové sítě založené na vrstvách „obyčejných“ neuronů nemusí být pro rozpoznávání a klasifikaci obrázků tím nejlepším řešením. Aby totiž bylo možné pracovat s posunutými, otočenými, zkosenými atd. vstupními obrázky, bylo by nutné síť udělat velmi hlubokou (mnoho vrstev, mnohdy dosahující 20 až 50 vrstev) a i počet neuronů by musel řádově odpovídat počtu pixelů. To je sice skutečně možné zařídit (ostatně zaplatíme za to „jen“ strojovým časem), ovšem stále zde narážíme na principiální omezení klasických vrstvených neuronových sítí – jednotlivé neurony se učí izolovaně od ostatních neuronů, zatímco na vstupu máme „plovoucí“ obrázek. Bylo by tedy výhodnější se zaměřit na vylepšení samotné architektury neuronové sítě specializované právě na to, že na vstupu bude mít bitmapy a tudíž by sousední neurony měly nějakým způsobem sdílet své váhy na vstupech. Taková architektura již ve skutečnosti byla dávno vymyšlena a jmenuje se konvoluční neuronová síť.

2. Vrstvy konvoluční neuronové sítě

Konvoluční neuronová síť používá minimálně dvě speciální vrstvy neuronů. Jedná se o konvoluční vrstvy a o takzvané subsamplingové vrstvy (někdy se setkáme s označením pooling layers). Úkolem těchto vrstev je získat z obrázku sadu koeficientů, které se posléze předají do několika klasických vrstev neuronů. Počet koeficientů vstupujících do posledních vrstev se může pohybovat od několika desítek až po stovky tisíc, což je částečně závislé na rozlišení vstupních obrázků (alespoň v síti, kterou si popíšeme dnes).

Typicky se vrstvy střídají takto:

  1. Vstupní vrstva
  2. Konvoluční vrstva #1
  3. Subsamplingová vrstva #1
  4. Konvoluční vrstva #2
  5. Subsamplingová vrstva #2
  6. Klasická skrytá vrstva
  7. Výstupní vrstva

3. Konvoluční vrstva

Úkolem konvoluční vrstvy je získání lokálních informací o rastrovém obrázku. Někdy se mohou používat i skutečně malé oblasti o velikosti 3×3 pixely, 5×5 pixelů atd. Celá tato oblast tvoří vstup do jednoho neuronu, což znamená 9 vstupů při volbě oblasti 3×3 pixely, 25 vstupů pro oblast o velikosti 5×5 pixelů apod. K těmto vstupům samozřejmě musíme připočítat i práh, jehož významem jsme se již poměrně dopodrobna zabývali. Jednotlivé oblasti se překrývají, takže pro obrázek o rozměrech x×y a pro oblasti o velikosti kx×ky budeme mít celkem (x-kx+1)×(y-ky+1) oblastí a tudíž stejný počet neuronů v konvoluční vrstvě (pro větší obrázky a menší jádra je tedy počet neuronů prakticky totožný s počtem pixelů).

Z konvolučních vrstev požadujeme získat n rovin, kde n by mělo zhruba odpovídat míře informací, které v obrázku hledáme. Když si uvědomíme, že n může být v řádu desítek až stovek a ve vrstvě bude (x-kx+1)×(y-ky+1) neuronů, zjistíme, že se jedná o obrovské číslo (násobené počtem vah). Ovšem v konvolučních vrstvách se váhy mezi neurony sdílí (včetně prahové hodnoty). To například znamená, že při volbě oblasti 5×5 pixelů bude nutné si zapamatovat pouze 5×5+1=26 vah (krát počet rovin).

4. Subsamplingová vrstva

Subsamplingové (též poolovací) vrstvy na svém vstupu akceptují výstup z předchozí konvoluční vrstvy. Tyto vrstvy taktéž používají neurony napojené na podoblasti vstupních bitmap (těch je již n), ovšem jednotlivé oblasti se v tomto případě nepřekrývají. Pokud je tedy oblast větší než 1×1 pixel, dochází ke zmenšování počtu výstupů. Příkladem může být subsamplingová vrstva s podoblastmi 2×2 pixely. Tato vrstva efektivně zmenšuje horizontální i vertikální rozlišení na polovinu. Navíc tyto vrstvy provádí jen jednoduché výpočty, například výběr pixelu s maximální hodnotou atd. Tím, že se vrstvy do sítě vkládají v pořadí konvoluční→subsamplingová→kon­voluční→subsamplingová, dochází k postupnému zmenšování množství dat, takže vstupy do poslední části sítě jsou vhodným způsobem omezeny.

5. „Obyčejné“ vrstvy pro klasifikaci dat

Na konci neuronové sítě se nachází obyčejné vrstvy neuronů, což znamená, že celou síť můžeme vlastně považovat za dvě sítě – první síť zpracovává rastrové obrázky a generuje jednorozměrný (!) tenzor koeficientů, druhá síť se pak snaží z těchto koeficientů odhadnout, jaké objekty vlastně byly na obrázku nalezeny. Ovšem nemusí být zcela jisté, co který koeficient znamená; to je již záležitost tréningu sítě (existují možnosti, jak graficky zobrazit význam koeficientů; ale to je již problematika, kterou se budeme zabývat v dalších dílech). V naší demonstrační síti použijeme jedinou „obyčejnou“ vrstvu neuronů, ovšem velmi často se setkáme i s větším množstvím vrstev (řekněme 2–3).

6. Praktická ukázka konvoluční neuronové sítě

Ukažme si nyní, jak se vytvoří prakticky použitelná konvoluční neuronová síť. Použijeme několik vrstev, konkrétně:

  1. Konvoluční vrstva #1
  2. Subsamplingová vrstva #1
  3. Konvoluční vrstva #2
  4. Subsamplingová vrstva #2
  5. Mezikrok s převodem 3D tenzorů na 1D tenzor
  6. Klasická skrytá vrstva
  7. Výstupní vrstva

Parametry sítě budou reprezentovány konstantami, s nimiž je samozřejmě možné manipulovat:

-- parametry neuronove site
INPUT_PLANES = 1
 
MIDDLE_PLANES = {64, 64}
 
HIDDEN_NEURONS = 100
OUTPUT_NEURONS = 10
 
-- parametry konvolucni vrstvy
CONVOLUTION_KERNEL_SIZE = 5
 
-- parametry pooling vrstvy
POOLING_SIZE = 2
POOLING_STEP = 2

Vlastní kód, který vytvoří novou konvoluční neuronovou síť, sice vypadá poměrně složitě, ale jednotlivé vrstvy a jejich význam je popsán v komentářích:

function construct_neural_network()
    local network = nn.Sequential()

Vytvoříme první čtyři vrstvy v pořadí: konvoluční, subsamplingová, konvoluční a konečně subsamplingová:

    -- prvni konvolucni vrstva ocekavajici na vstupu 3D tenzor
    -- o velikosti:
    -- INPUT_PLANES x vyska x sirka
    --
    -- vysledkem je 3D tenzor o velikosti:
    -- MIDDLE_PLANES_1 x (vyska - CONVOLUTION_KERNEL_SIZE + 1) x (sirka - CONVOLUTION_KERNEL_SIZE + 1) 
    network:add(nn.SpatialConvolution(INPUT_PLANES, MIDDLE_PLANES[1], CONVOLUTION_KERNEL_SIZE, CONVOLUTION_KERNEL_SIZE))
 
    -- nyni mame mezivysledky 64 x (32-5+1) x (32-5+1) = 64 x 28 x 28
 
    -- nelinearni funkce
    network:add(nn.Tanh())
 
    -- hledani maxima v regionech o velikosti 2x2 pixely
    -- s krokem nastavenym na 2 pixely v horizontalnim i 2 pixely ve vertikalnim smeru
    network:add(nn.SpatialMaxPooling(POOLING_SIZE, POOLING_SIZE, POOLING_STEP, POOLING_STEP))
 
    -- nyni mame mezivysledky 64 x 28/2 x 28/2 = 64 x 14 x 14
 
    -- druha konvolucni vrstva ocekavajici na vstupu 3D tenzor
    -- o velikosti MIDDLE_PLANES_1 x vyska x sirka
    network:add(nn.SpatialConvolution(MIDDLE_PLANES[1], MIDDLE_PLANES[2], CONVOLUTION_KERNEL_SIZE, CONVOLUTION_KERNEL_SIZE))
 
    -- nyni mame mezivysledky 64 x (14-5+1) x (14-5+1) = 64 x 10 x 10
 
    -- nelinearni funkce
    network:add(nn.Tanh())
 
    -- opetovne hledani maxima v regionech o velikosti 2x2 pixely
    -- s krokem nastavenym na 2 pixely v horizontalnim i 2 pixely ve vertikalnim smeru
    network:add(nn.SpatialMaxPooling(POOLING_SIZE, POOLING_SIZE, POOLING_STEP, POOLING_STEP))
 
    -- nyni mame mezivysledky 64 x 10/2 x 10/2 = 64 x 5 x 5
 

Nyní nastává důležitý mezikrok – konverze 3D tenzoru na 1D tenzor. To je možné provést více způsoby, ale typicky se používá reshape nebo view:

    -- nyni mame mezivysledek o velikosti MIDDLE_PLANES_2 x 5 x 5
    -- zmena tvaru: z 3D tenzoru AxBxC na 1D tenzor s A*B*C elementy
    network:add(nn.View(MIDDLE_PLANES[2]*5*5))

Výsledkem předchozího bloku je 1D tenzor, takže můžeme do sítě vložit zcela obyčejnou vrstvu neuronů a na závěr vrstvu výstupní:

    -- bezne vrstvy, jak je jiz zname
    network:add(nn.Linear(MIDDLE_PLANES[2]*5*5, HIDDEN_NEURONS))
 
    -- pridana nelinearni funkce
    network:add(nn.ReLU())
 
    -- bezne vrstvy, jak je jiz zname
    network:add(nn.Linear(HIDDEN_NEURONS, OUTPUT_NEURONS))
 
    return network
end

Nyní nám již stačí síť zkonstruovat a vypsat její strukturu:

network = construct_neural_network()
print(network)

Na standardní výstup by se měla vytisknout struktura právě zkonstruované neuronové sítě, která vypadá následovně:

nn.Sequential {
  [input -> (1) -> (2) -> (3) -> (4) -> (5) -> (6) -> (7) -> (8) -> (9) -> (10) -> output]
  (1): nn.SpatialConvolution(1 -> 64, 5x5)
  (2): nn.Tanh
  (3): nn.SpatialMaxPooling(2x2, 2,2)
  (4): nn.SpatialConvolution(64 -> 64, 5x5)
  (5): nn.Tanh
  (6): nn.SpatialMaxPooling(2x2, 2,2)
  (7): nn.View(1600)
  (8): nn.Linear(1600 -> 100)
  (9): nn.ReLU
  (10): nn.Linear(100 -> 10)
}

7. Výpočet počtu vstupů do „obyčejné“ vrstvy pro klasifikaci dat

V předchozím kódu jsme použili magickou hodnotu MIDDLE_PLANES[2]*5*5, která měla v našem případě výsledek 64×5×5 = 1600. Odkud se ale tato konstanta vzala? Hodnota 64 je jasná – jedná se o počet kvantifikovaných atributů z předchozí vrstvy. Ovšem obě hodnoty 5 a 5 již mají mnohem zajímavější způsob vzniku. Musíme si totiž uvědomit, jak se původní rastrové obrázky o rozlišení 32×32 pixelů postupně zpracovávají v jednotlivých vrstvách:

Vrstva Vstup Výstup
SpatialConvolution 32×32 64 × (32–5+1) × (32–5+1) = 64 × 28 × 28
SpatialMaxPooling 64 ×28 × 28 64 × 28/2 × 28/2 = 64 × 14 × 14
SpatialConvolution 64 × 14 × 14 64 × (14–5+1) × (14–5+1) = 64 × 10 × 10
SpatialMaxPooling 64 × 10 × 10 64 × 10/2 × 10/2 = 64 × 5 × 5 = 1600

Konvoluční vrstva totiž skutečně má na vstupu jednotlivých neuronů hodnotu pixelu a jeho okolí, tj. například pro velikost konvolučního jádra 5×5 se jedná o stejně velké okolí. A vzhledem k tomu, že pixely na okraji bitmapy nemají z jedné nebo dvou stran žádné rozumně definované okolí, je na výstupu bitmapa zmenšena o dva pixely na každé straně, tedy celkově o hodnotu –5+1:

U subsamplingové vrstvy se defacto zmenšuje rozlišení na polovinu, takže například pro vstupních 64 bitmap o rozlišení 28×28 pixelů dostaneme 64 jiných bitmap o rozlišení 14×14 pixelů.

Stejný postup se aplikuje i pro další dvě vrstvy. Výpočet velikosti tedy můžeme provést i programově, například následujícím způsobem:

-- parametry neuronove site
INPUT_PLANES = 1
 
MIDDLE_PLANES = {64, 64}
 
-- parametry konvolucni vrstvy
CONVOLUTION_KERNEL_SIZE = 5
 
-- parametry pooling vrstvy
POOLING_SIZE = 2
POOLING_STEP = 2
 
 
function calculate_size_after_convolution(input_size, middle_planes, convolution_kernel_size)
    local size = input_size
    for i=1,#middle_planes do
        -- velikost po projiti konvolucni vrstvou
        size = size - convolution_kernel_size + 1
        -- velikost po projiti pooling vrstvou
        size = size / 2
    end
    return size
end
 
print(calculate_size_after_convolution(32, MIDDLE_PLANES, CONVOLUTION_KERNEL_SIZE))
print(calculate_size_after_convolution(64, MIDDLE_PLANES, CONVOLUTION_KERNEL_SIZE))
print(calculate_size_after_convolution(128, MIDDLE_PLANES, CONVOLUTION_KERNEL_SIZE))
print(calculate_size_after_convolution(256, MIDDLE_PLANES, CONVOLUTION_KERNEL_SIZE))

Výsledek výpočtů:

Vstupní bitmapa Bitmapy na výstupu z poslední subsamplingové vrstvy
32×32 5×5
64×64 13×13
128×128 29×29
256×256 61×61

Náš program pro konstrukci neuronové sítě tedy můžeme upravit tak, aby nebyl přímo závislý na rozlišení vstupních bitmap:

require("nn")
 
WIDTH = 200
HEIGHT = 200
 
 
-- parametry neuronove site
INPUT_PLANES = 1
 
MIDDLE_PLANES = {64, 64}
 
HIDDEN_NEURONS = 100
OUTPUT_NEURONS = 10
 
-- parametry konvolucni vrstvy
CONVOLUTION_KERNEL_SIZE = 5
 
-- parametry pooling vrstvy
POOLING_SIZE = 2
POOLING_STEP = 2
 
 
function calculate_size_after_convolution(input_size, middle_planes, convolution_kernel_size)
    local size = input_size
    for i=1,#middle_planes do
        -- velikost po projiti konvolucni vrstvou
        size = size - convolution_kernel_size + 1
        -- velikost po projiti pooling vrstvou
        size = size / 2
    end
    return size
end
 
 
function construct_neural_network()
    local network = nn.Sequential()
 
    local size_x = calculate_size_after_convolution(WIDTH, MIDDLE_PLANES, CONVOLUTION_KERNEL_SIZE)
    local size_y = calculate_size_after_convolution(HEIGHT, MIDDLE_PLANES, CONVOLUTION_KERNEL_SIZE)
 
    print("Size x: " .. size_x)
    print("Size y: " .. size_y)
 
    -- prvni konvolucni vrstva ocekavajici na vstupu 3D tenzor
    -- o velikosti:
    -- INPUT_PLANES x vyska x sirka
    --
    -- vysledkem je 3D tenzor o velikosti:
    -- MIDDLE_PLANES_1 x (vyska - CONVOLUTION_KERNEL_SIZE + 1) x (sirka - CONVOLUTION_KERNEL_SIZE + 1) 
    network:add(nn.SpatialConvolution(INPUT_PLANES, MIDDLE_PLANES[1], CONVOLUTION_KERNEL_SIZE, CONVOLUTION_KERNEL_SIZE))
 
    -- nyni mame mezivysledky 64 x (vyska-5+1) x (sirka-5+1)
 
    -- nelinearni funkce
    network:add(nn.Tanh())
 
    -- hledani maxima v regionech o velikosti 2x2 pixely
    -- s krokem nastavenym na 2 pixely v horizontalnim i 2 pixely ve vertikalnim smeru
    network:add(nn.SpatialMaxPooling(POOLING_SIZE, POOLING_SIZE, POOLING_STEP, POOLING_STEP))
 
    -- druha konvolucni vrstva ocekavajici na vstupu 3D tenzor
    -- o velikosti MIDDLE_PLANES_1 x vyska x sirka
    network:add(nn.SpatialConvolution(MIDDLE_PLANES[1], MIDDLE_PLANES[2], CONVOLUTION_KERNEL_SIZE, CONVOLUTION_KERNEL_SIZE))
 
    -- nelinearni funkce
    network:add(nn.Tanh())
 
    -- opetovne hledani maxima v regionech o velikosti 2x2 pixely
    -- s krokem nastavenym na 2 pixely v horizontalnim i 2 pixely ve vertikalnim smeru
    network:add(nn.SpatialMaxPooling(POOLING_SIZE, POOLING_SIZE, POOLING_STEP, POOLING_STEP))
 
    -- zmena tvaru: z 3D tenzoru AxBxC na 1D tenzor s A*B*C elementy
    network:add(nn.View(MIDDLE_PLANES[2]*size_x*size_y))
 
    -- bezne vrstvy, jak je jiz zname
    network:add(nn.Linear(MIDDLE_PLANES[2]*size_x*size_y, HIDDEN_NEURONS))
 
    -- pridana nelinearni funkce
    network:add(nn.ReLU())
 
    -- bezne vrstvy, jak je jiz zname
    network:add(nn.Linear(HIDDEN_NEURONS, OUTPUT_NEURONS))
 
    return network
end
network = construct_neural_network()
print(network)

Výsledná síť pro vstupní bitmapy o rozlišení 200×200 pixelů bude vypadat takto. Hodnota 141376 vznikla výpočtem 64×47×47:

Size x: 47
Size y: 47
nn.Sequential {
  [input -> (1) -> (2) -> (3) -> (4) -> (5) -> (6) -> (7) -> (8) -> (9) -> (10) -> output]
  (1): nn.SpatialConvolution(1 -> 64, 5x5)
  (2): nn.Tanh
  (3): nn.SpatialMaxPooling(2x2, 2,2)
  (4): nn.SpatialConvolution(64 -> 64, 5x5)
  (5): nn.Tanh
  (6): nn.SpatialMaxPooling(2x2, 2,2)
  (7): nn.View(141376)
  (8): nn.Linear(141376 -> 100)
  (9): nn.ReLU
  (10): nn.Linear(100 -> 10)
}

8. Funkce pro vytvoření a uložení trénovacích i validačních rastrových obrázků

Pro vytvoření testovacích a validačních rastrových obrázků použijeme funkci, s jejímž principem jsme se již seznámili minule. Funkci však rozšíříme takovým způsobem, aby mohla vytvářet zvětšené obrázky s celočíselným zvětšením. Funkce bude pro každou číslici získávat barvy pixelů (tmavý/světlý) z následující tabulky:

digits = {
    {0x00, 0x3C, 0x66, 0x76, 0x6E, 0x66, 0x3C, 0x00 },
    {0x00, 0x18, 0x1C, 0x18, 0x18, 0x18, 0x7E, 0x00 },
    {0x00, 0x3C, 0x66, 0x30, 0x18, 0x0C, 0x7E, 0x00 },
    {0x00, 0x7E, 0x30, 0x18, 0x30, 0x66, 0x3C, 0x00 },
    {0x00, 0x30, 0x38, 0x3C, 0x36, 0x7E, 0x30, 0x00 },
    {0x00, 0x7E, 0x06, 0x3E, 0x60, 0x66, 0x3C, 0x00 },
    {0x00, 0x3C, 0x06, 0x3E, 0x66, 0x66, 0x3C, 0x00 },
    {0x00, 0x7E, 0x60, 0x30, 0x18, 0x0C, 0x0C, 0x00 },
    {0x00, 0x3C, 0x66, 0x3C, 0x66, 0x66, 0x3C, 0x00 },
    {0x00, 0x3C, 0x66, 0x7C, 0x60, 0x30, 0x1C, 0x00 },
}

Samotné vytvoření bitmapy je již poměrně přímočaré, musíme jen zajistit potřebné zvětšení (proto se používá dvojice počitadel):

function get_codes_for_digit(digit)
    if digit < 0 or digit > 9 then
        return nil
    end
    return digits[digit+1]
end
 
 
function setpixel(image, x, y, bit, white_value, black_value)
    if bit==1 then
        image[y][x] = white_value
    else
        image[y][x] = black_value
    end
end
 
 
function generate_image(digit, scale, black_value, white_value)
    local codes = get_codes_for_digit(digit)
    local image = {}
    local y_offset = 1
    for _, code in ipairs(codes) do
        for y = 1,scale do
            -- vytvorit dalsi obrazovy radek
            image[y_offset] = {}
            local x_offset = 1
            local byte = code
            -- vypocet barev jednotlivych pixelu v obrazku
            for _ = 1,8 do
                -- zjistit hodnotu n-teho bitu + posun bajtu s maskou znaku
                local bit = byte % 2
                byte = (byte - bit)/2
                for x = 1,scale do
                    -- obarveni konkretniho pixelu
                    setpixel(image, x_offset, y_offset, bit, white_value, black_value)
                    x_offset = x_offset + 1
                end
            end
            y_offset = y_offset + 1
        end
    end
    return image
end

Následuje funkce, která vytvořenou bitmapu uloží do externího souboru ve formátu PGM (Portable GrayMap). S touto funkcí jsme se již seznámili minule, dnes jen nepatrně upravíme logiku pro odřádkování a oddělení jednotlivých hodnot od sebe (není to však nutné, jen ušetříme 32 bajtů pro každý soubor):

function write_image(filename, image)
    local fout = io.open(filename, "w")
    if not fout then
        return
    end
 
    -- rozliseni obrazku
    local x_resolution = #image[1]
    local y_resolution = #image
 
    -- zapis hlavicky
    fout:write("P2\n")
    fout:write(x_resolution)
    fout:write(" ")
    fout:write(y_resolution)
    fout:write("\n255\n")
 
    -- zapis jednotlivych pixelu
    for j, row in ipairs(image) do
        for i, pixel in ipairs(row) do
            -- logika pro oddeleni hodnot
            if i ~= 1 then
                fout:write(" ")
            end
            fout:write(pixel)
        end
        -- odradkovani neni nutne
        fout:write("\n")
    end
 
    fout:close()
end

Obrázek 1: Číslice 2 vygenerovaná předchozími funkcemi.

Ukázka výsledného souboru s bitmapou:

P2
32 32
255
68 58 70 66 63 56 62 67 68 72 64 66 63 56 64 56 57 77 64 57 64 66 57 72 60 64 64 59 68 63 63 68
67 62 67 60 61 72 65 61 68 63 55 63 66 61 64 64 62 63 69 66 64 67 64 72 53 61 67 59 69 66 69 60
...
...
...
66 64 70 64 67 65 65 59 64 70 59 59 55 51 64 58 64 59 67 61 66 58 67 55 65 67 62 61 61 59 70 67
64 58 65 56 58 67 64 66 63 64 56 64 65 68 57 56 59 65 67 68 61 70 65 70 64 63 64 66 64 63 68 62

9. Posun rastrových dat (translate)

S obrázky samozřejmě můžeme různým způsobem manipulovat, čehož využijeme jak při tvorbě trénovacích dat, tak (a to především) při generování validačních obrázků. Základní operací, která manipuluje s celým rastrovým obrázkem, je operace jeho posunu o zadaný počet pixelů v horizontálním směru (doleva, doprava) i ve směru vertikálním (nahoru, dolů). Poněkud naivní implementace může vypadat následovně:

function translate(source_image, x_offset, y_offset)
    local target_image = {}
    local max_x = #source_image[1]
    local max_y = #source_image
 
    for y = 1,max_y do
        target_image[y] = {}
        for x = 1,max_x do
            xs = x - x_offset
            ys = y - y_offset
            xs = bound(xs, 1, max_x)
            ys = bound(ys, 1, max_y)
            target_image[y][x] = source_image[ys][xs]
        end
    end
    return target_image
end

Povšimněte si, že vytváříme nový obrázek, zatímco původní obrázek zůstane nedotčen. To je u našich malých obrázků s rozlišením pouhých 32×32 pixelů nepodstatné, protože stejně nejvíce času bude systém trávit ve funkci pro tréning neuronové sítě.

Pomocná funkce bound pouze zajišťuje, aby se souřadnice pixelů nacházely v zadaném intervalu, a to za všech okolností:

function bound(value, min_value, max_value)
    return math.max(min_value, math.min(value, max_value))
end

Tuto pomocnou funkci použijeme i při dalších operacích.

Obrázek 2: Posunutá číslice 2 vygenerovaná předchozími funkcemi.

10. Aplikace šumu (noise)

Funkce provádějící posun rastrových dat v rámci obrázku s předem daným rozlišením byla velmi jednoduchá. Podobně snadná je implementace funkce, která do obrázku vnese šum, čímž ideální (uměle vyrendrované obrázky) alespoň v malé míře přiblíží realitě. Zatímco minule jsme pro napodobení reálného šumu používali výsledek volání funkce math.random (bez dalších úprav), dnes použijeme šum s normálním rozložením. Jedna z možných implementací generátoru náhodných čísel s náhodným rozložením může vypadat následovně. Parametry této funkce jsou střední hodnota a rozptyl:

function gaussian(mean, variance)
    return math.sqrt(-2 * variance * math.log(math.random())) *
           math.cos(2 * variance * math.pi * math.random()) + mean
end

Funkci gaussian použijeme pro vnesení šumu do obrázku, přičemž opět nebudeme modifikovat původní obrázek, ale raději vytvoříme obrázek nový:

function apply_noise(source_image, variance)
    local target_image = {}
    for y, row in ipairs(source_image) do
        target_image[y] = {}
        for x, pixel in ipairs(row) do
            local delta = math.floor(gaussian(0, variance))
            target_image[y][x] = bound(pixel + delta, 0, 255)
        end
    end
    return target_image
end

Opět si povšimněte použití pomocné funkce bound, protože nechceme, aby se nám při zadání většího rozptylu a střední hodnoty hodnoty pixelů dostaly mezi meze 0..255.

Obrázek 3: Zašuměná číslice 2 vygenerovaná předchozími funkcemi.

11. Aplikace roztřesení (jitter)

Poslední funkcí, kterou použijeme pro modifikaci rastrových obrázků určených pro tréning a/nebo validaci neuronové sítě, je funkce provádějící roztřesení (jitter) pixelů, tj. posun pixelů náhodným směrem. Pro určení směru opět použijeme výše deklarovanou funkci gaussian:

function apply_jitter(source_image, variance)
    local target_image = {}
    local max_x = #source_image[1]
    local max_y = #source_image
 
    for y = 1,max_y do
        target_image[y] = {}
        for x = 1,max_x do
            xs = x + math.floor(gaussian(0, variance))
            ys = y + math.floor(gaussian(0, variance))
            xs = bound(xs, 1, max_x)
            ys = bound(ys, 1, max_y)
            target_image[y][x] = source_image[ys][xs]
        end
    end
    return target_image
end

Poznámka: ve skutečnosti není tato funkce napsána zcela korektně, protože je závislá na směru – offset pixelů pro jittering se totiž počítá pro horizontální a vertikální směr, navíc se posléze musí provést zaokrouhlení souřadnic na celá čísla. Nicméně pro první přiblížení nám může tato funkce pomoci.

Obrázek 4: Chuťovka pro neuronovou síť – zašuměná a současně i roztřesená číslice 2 vygenerovaná předchozími funkcemi.

12. Sada trénovacích obrázků

Nyní již máme vše připravené pro vygenerování trénovacích obrázků pro naši novou konvoluční neuronovou síť. Trénovací obrázky budou pouze zašuměné, přičemž úroveň šumu (rozptyl) budeme postupně zvyšovat od nuly (nezašuměný obrázek) až po hodnotu 200. Obrázky budou jak v horizontálním, tak i vertikálním směru zvětšeny 4×, což znamená, že namísto pidibitmap o rozlišení 8×8 pixelů získáme obrázky o rozměrech ikon 32×32 pixelů:

NOISE_VARIANCES = {0, 10, 20, 50, 100, 200}
REPEAT_COUNT = 3
 
SCALE = 4

Samotné vygenerování trénovacích obrázků je již snadné:

function generate_training_images()
    for _, variance in ipairs(NOISE_VARIANCES) do
        for digit = 0, 9 do
            for i = 1, REPEAT_COUNT do
                local image = generate_image(digit, SCALE, 64, 192)
                image = apply_noise(image, variance)
                local filename = string.format("training_%d_%d_%d.pgm", digit, variance, i)
                write_image(filename, image)
            end
        end
    end
end
generate_training_images()

Výsledkem byl mělo být 180 bitmap, což sice není mnoho, ale pokud máte k dispozici více času pro tréning sítě a/nebo používáte CUDA, lze jejich počet samozřejmě zvýšit změnou parametrů NOISE_VARIANCES a REPEAT_COUNT.

Obrázek 5: příklady výstupu (zvětšeno).

13. Sada validačních obrázků

Podobným způsobem vytvoříme sadu validačních obrázků, ovšem aby to námi vytvořená konvoluční neuronová síť neměla tak jednoduché, bude na validační obrázky použita funkce pro roztřesení pixelů. Síť se tedy bude muset vyrovnat s (prozatím) jí neznámým vstupem. Vytvoření validačních obrázků je provedeno následovně:

Parametry:

NOISE_VARIANCES = {20, 50, 100, 200}
JITTER_VARIANCE = {0, 1, 2}
REPEAT_COUNT = 2

Vlastní vygenerování:

function generate_images_for_validation()
    for _, noise_variance in ipairs(NOISE_VARIANCES) do
        for _, jitter_variance in ipairs(JITTER_VARIANCE) do
            for digit = 0, 9 do
                for i = 1, REPEAT_COUNT do
                    local image = generate_image(digit, SCALE, 64, 192)
                    image = apply_noise(image, noise_variance)
                    image = apply_jitter(image, jitter_variance)
                    local filename = string.format("validation_%d_%d_%d_%d.pgm", digit, noise_variance, jitter_variance, i)
                    write_image(filename, image)
                end
            end
        end
    end
end
generate_images_for_validation()

Obrázek 6: příklady výstupu (zvětšeno).

14. Funkce pro načtení trénovacích obrázků a vytvoření trénovacích dat pro síť

Při načítání trénovacích obrázků nejdříve zjistíme jejich seznam na základě předané masky:

function read_filelist(mask)
    local command = "ls -1 " .. mask
    local handle = io.popen(command)
    local filelist = {}
    for line in handle:lines() do
        table.insert(filelist, line)
    end
    handle:close()
    return filelist
end

Trénovací obrázky mají jména odpovídající masce training*.pgm, takže:

training_files = read_filelist("training*.pgm")

O načtení všech obrázků, zjištění správného výstupu (pro trénink sítě) a vytvoření pole trénovacích dat (dvojic tenzorů) se postará tato funkce:

function prepare_training_data(training_files)
    local training_data_size = #training_files
    local training_data = {}
    function training_data:size() return training_data_size end
 
    for i, training_file in ipairs(training_files) do
        local input = image.load(training_file, 1, "byte"):double()
        local digit = tonumber(string.match(training_file, "%d+"))
        local output = generate_expected_output(digit)
        training_data[i] = {input, output}
    end
 
    return training_data
end

Poznámka: správná číslice je zjištěna z prvního čísla nalezeného ve jméně souboru s obrázkem.

Pomocná funkce pro vytvoření tenzoru s očekávaným výstupem:

function generate_expected_output(digit)
    local result = torch.zeros(DIGITS)
    result[digit+1] = 1
    return result
end

15. Funkce pro vlastní tréning sítě

Tréning sítě je postaven na funkci nazvané train_neural_network, která se od minulých příkladů nezměnila. Jen se snížil počet iterací na 200, což se zdá být dostačující, pokud ovšem budete chtít provádět tréning s využitím CUDA, můžete tuto hodnotu zvýšit:

-- parametry pro uceni neuronove site
MAX_ITERATION = 200
LEARNING_RATE = 0.01

Samotná funkce provádějící trénink vypadá následovně:

function train_neural_network(network, training_data, learning_rate, max_iteration)
    local criterion = nn.MSECriterion()
    local trainer = nn.StochasticGradient(network, criterion)
    trainer.learningRate = learning_rate
    trainer.maxIteration = max_iteration
    trainer:train(training_data)
end

16. Průběh tréningu

Po spuštění tréninku se začnou vypisovat informace o jednotlivých chybách, na které síť reaguje změnou vah neuronů. Můžeme vidět, že chyba postupně klesá a to dosti výrazně (síť se dobře učí):

# StochasticGradient: training
# current error = 0.085056116904068
# current error = 0.058735151391114
# current error = 0.040517938210007
# current error = 0.028050846785708
# current error = 0.019813127132151
...
...
...
# current error = 5.1229304386063e-06
# current error = 5.0627943767942e-06
# current error = 5.0088192479628e-06
# current error = 4.9373411963832e-06
# current error = 4.886157527272e-06
# StochasticGradient: you have reached the maximum number of iterations
# training error = 4.886157527272e-06

Výsledná chyba má dosti optimistickou hodnotu 4,8×10-6, ovšem samozřejmě ještě musíme provést validaci.

17. Validace sítě a výpočet chybných úsudků

Pro validaci sítě si vytvoříme další funkci nazvanou validate_neural_network. Této funkci se předá seznam validačních obrázků a samozřejmě i vlastní síť. Obrázky se postupně posílají na vstup sítě, z jejího výstupu se zjistí tenzor s deseti prvky, v něm se najde největší prvek a jeho index se prohlásí za nalezenou číslici. Navíc si ještě spočítáme jednoduchou statistiku o míře chybných odhadů:

function validate_neural_network(network, filelist)
    local errors = 0
    for i, filename in ipairs(filelist) do
        local input = image.load(filename, 1, "byte"):double()
        local expected_digit = tonumber(string.match(filename, "%d+"))
        local output = network:forward(input)
        local result, weight = find_largest_item(output)
        if expected_digit ~= result then
            errors = errors + 1
        end
        print(expected_digit, result, expected_digit==result, weight, filename)
    end
    print("---------------------")
    print("Errors: " .. errors)
    print("Error rate: " .. 100.0*errors/#filelist .. "%")
end

Pomocná funkce pro nalezení indexu prvku tenzoru s největší hodnotou:

function find_largest_item(tensor)
    local index = -1
    local value = -math.huge
    for i = 0, 9 do
        if tensor[i+1] > value then
            index = i
            value = tensor[i+1]
        end
    end
    return index, value
end

Vše již máme připravené, takže můžeme spustit validaci:

validation_files = read_filelist("validation*.pgm")
 
validate_neural_network(network, validation_files)

Výsledky nejsou vůbec špatné, zvláště když si uvědomíme, že síť vůbec nebyla natrénována na rozpoznávání číslic v obrázcích, na které se aplikoval jittering:

0         0         true      0.95748445630588    validation_0_100_0_1.pgm
0         0         true      0.92065186393107    validation_0_100_0_2.pgm
0         0         true      0.45594509522907    validation_0_100_1_1.pgm
0         0         true      0.51737246567833    validation_0_100_1_2.pgm
0         0         true      0.31705108359018    validation_0_100_2_1.pgm
0         0         true      0.39825404762665    validation_0_100_2_2.pgm
...
...
...
8         8         true      0.4499938604602     validation_8_200_1_2.pgm
8         8         true      0.98181280205716    validation_8_20_0_1.pgm
8         7         false     0.20192176973614    validation_8_200_2_1.pgm
8         8         true      0.2397209058962     validation_8_200_2_2.pgm
8         8         true      0.98915324380974    validation_8_20_0_2.pgm
8         8         true      0.42960465297511    validation_8_20_1_1.pgm
8         8         true      0.33564594476512    validation_8_20_1_2.pgm
8         8         true      0.2004579255463     validation_8_20_2_1.pgm
8         8         true      0.1496889366345     validation_8_20_2_2.pgm
8         8         true      0.98902163781293    validation_8_50_0_1.pgm
8         8         true      0.96138545388153    validation_8_50_0_2.pgm
8         8         true      0.35222736158076    validation_8_50_1_1.pgm
8         8         true      0.31740552859615    validation_8_50_1_2.pgm
8         8         true      0.26109952804475    validation_8_50_2_1.pgm
8         3         false     0.18653662825211    validation_8_50_2_2.pgm
---------------------
Errors: 3
Error rate: 1.25%

Seznam chyb (záměn) je pochopitelný, když se podíváme na tvar číslic:

8 → 3
8 → 7
8 → 3

18. Úplný zdrojový kód příkladu s konvoluční neuronovou sítí

Pod tímto odstavcem je vypsán úplný zdrojový kód příkladu, v němž je vytvořena konvoluční neuronová síť, tato síť je následně natrénována a posléze je provedena její validace. Všechny podstatné parametry sítě naleznete na začátku zdrojového kódu a můžete s nimi manipulovat pro dosažení lepších (nebo samozřejmě i horších) výsledků:

require("nn")
require("image")
 
-- globalni nastaveni
DIGITS = 10
 
 
WIDTH = 32
HEIGHT = 32
 
 
-- parametry neuronove site
INPUT_PLANES = 1
 
MIDDLE_PLANES = {64, 64}
 
HIDDEN_NEURONS = 100
OUTPUT_NEURONS = 10
 
-- parametry konvolucni vrstvy
CONVOLUTION_KERNEL_SIZE = 5
 
-- parametry pooling vrstvy
POOLING_SIZE = 2
POOLING_STEP = 2
 
-- parametry pro uceni neuronove site
MAX_ITERATION = 200
LEARNING_RATE = 0.01
 
 
function calculate_size_after_convolution(input_size, middle_planes, convolution_kernel_size)
    local size = input_size
    for i=1,#middle_planes do
        -- velikost po projiti konvolucni vrstvou
        size = size - convolution_kernel_size + 1
        -- velikost po projiti pooling vrstvou
        size = size / 2
    end
    return size
end
 
 
function construct_neural_network()
    local network = nn.Sequential()
 
    local size_x = calculate_size_after_convolution(WIDTH, MIDDLE_PLANES, CONVOLUTION_KERNEL_SIZE)
    local size_y = calculate_size_after_convolution(HEIGHT, MIDDLE_PLANES, CONVOLUTION_KERNEL_SIZE)
 
    print("Size x: " .. size_x)
    print("Size y: " .. size_y)
 
    -- prvni konvolucni vrstva ocekavajici na vstupu 3D tenzor
    -- o velikosti:
    -- INPUT_PLANES x vyska x sirka
    --
    -- vysledkem je 3D tenzor o velikosti:
    -- MIDDLE_PLANES_1 x (vyska - CONVOLUTION_KERNEL_SIZE + 1) x (sirka - CONVOLUTION_KERNEL_SIZE + 1) 
    network:add(nn.SpatialConvolution(INPUT_PLANES, MIDDLE_PLANES[1], CONVOLUTION_KERNEL_SIZE, CONVOLUTION_KERNEL_SIZE))
 
    -- nyni mame mezivysledky 64 x (vyska-5+1) x (sirka-5+1)
 
    -- nelinearni funkce
    network:add(nn.Tanh())
 
    -- hledani maxima v regionech o velikosti 2x2 pixely
    -- s krokem nastavenym na 2 pixely v horizontalnim i 2 pixely ve vertikalnim smeru
    network:add(nn.SpatialMaxPooling(POOLING_SIZE, POOLING_SIZE, POOLING_STEP, POOLING_STEP))
 
    -- druha konvolucni vrstva ocekavajici na vstupu 3D tenzor
    -- o velikosti MIDDLE_PLANES_1 x vyska x sirka
    network:add(nn.SpatialConvolution(MIDDLE_PLANES[1], MIDDLE_PLANES[2], CONVOLUTION_KERNEL_SIZE, CONVOLUTION_KERNEL_SIZE))
 
    -- nelinearni funkce
    network:add(nn.Tanh())
 
    -- opetovne hledani maxima v regionech o velikosti 2x2 pixely
    -- s krokem nastavenym na 2 pixely v horizontalnim i 2 pixely ve vertikalnim smeru
    network:add(nn.SpatialMaxPooling(POOLING_SIZE, POOLING_SIZE, POOLING_STEP, POOLING_STEP))
 
    -- zmena tvaru: z 3D tenzoru AxBxC na 1D tenzor s A*B*C elementy
    network:add(nn.View(MIDDLE_PLANES[2]*size_x*size_y))
 
    -- bezne vrstvy, jak je jiz zname
    network:add(nn.Linear(MIDDLE_PLANES[2]*size_x*size_y, HIDDEN_NEURONS))
 
    -- pridana nelinearni funkce
    network:add(nn.ReLU())
 
    -- bezne vrstvy, jak je jiz zname
    network:add(nn.Linear(HIDDEN_NEURONS, OUTPUT_NEURONS))
 
    return network
end
 
 
function train_neural_network(network, training_data, learning_rate, max_iteration)
    local criterion = nn.MSECriterion()
    local trainer = nn.StochasticGradient(network, criterion)
    trainer.learningRate = learning_rate
    trainer.maxIteration = max_iteration
    trainer:train(training_data)
end
 
 
function generate_expected_output(digit)
    local result = torch.zeros(DIGITS)
    result[digit+1] = 1
    return result
end
 
 
function prepare_training_data(training_files)
    local training_data_size = #training_files
    local training_data = {}
    function training_data:size() return training_data_size end
 
    for i, training_file in ipairs(training_files) do
        local input = image.load(training_file, 1, "byte"):double()
        local digit = tonumber(string.match(training_file, "%d+"))
        local output = generate_expected_output(digit)
        training_data[i] = {input, output}
    end
 
    return training_data
end
 
 
function read_filelist(mask)
    local command = "ls -1 " .. mask
    local handle = io.popen(command)
    local filelist = {}
    for line in handle:lines() do
        table.insert(filelist, line)
    end
    handle:close()
    return filelist
end
 
 
function find_largest_item(tensor)
    local index = -1
    local value = -math.huge
    for i = 0, 9 do
        if tensor[i+1] > value then
            index = i
            value = tensor[i+1]
        end
    end
    return index, value
end
 
 
function validate_neural_network(network, filelist)
    local errors = 0
    for i, filename in ipairs(filelist) do
        local input = image.load(filename, 1, "byte"):double()
        local expected_digit = tonumber(string.match(filename, "%d+"))
        local output = network:forward(input)
        local result, weight = find_largest_item(output)
        if expected_digit ~= result then
            errors = errors + 1
        end
        print(expected_digit, result, expected_digit==result, weight, filename)
    end
    print("---------------------")
    print("Errors: " .. errors)
    print("Error rate: " .. 100.0*errors/#filelist .. "%")
end
 
 
training_files = read_filelist("training*.pgm")
 
network = construct_neural_network()
print(network)
 
training_data = prepare_training_data(training_files)
 
train_neural_network(network, training_data, LEARNING_RATE, MAX_ITERATION)
 
validation_files = read_filelist("validation*.pgm")
 
validate_neural_network(network, validation_files)

Mimochodem: celý zdrojový kód má velikost přibližně 5,5 kB a délku necelých 200 řádků. To vlastně není mnoho, když si uvědomíme, že je ve skriptu implementována jak celá konvoluční neuronová síť, tak i funkce pro její trénink a validaci.

MIF18 tip v článku ROSA

19. Repositář s demonstračními příklady

Všechny demonstrační příklady, které jsme si popsali v předchozích kapitolách, najdete v GIT repositáři dostupném na adrese https://github.com/tisnik/torch-examples.git. Následují odkazy na zdrojové kódy jednotlivých příkladů:

Poznámka: první skript je možné spouštět přímo z interpretru jazyka Lua, není tedy nutné používat framework Torch. Ostatní skripty se pouští přes th.

20. Odkazy na Internetu

  1. THE MNIST DATABASE of handwritten digits
    http://yann.lecun.com/exdb/mnist/
  2. MNIST database (Wikipedia)
    https://en.wikipedia.org/wi­ki/MNIST_database
  3. MNIST For ML Beginners
    https://www.tensorflow.or­g/get_started/mnist/begin­ners
  4. Stránka projektu Torch
    http://torch.ch/
  5. Torch: Serialization
    https://github.com/torch/tor­ch7/blob/master/doc/seria­lization.md
  6. Torch: modul image
    https://github.com/torch/i­mage/blob/master/README.md
  7. Data pro neuronové sítě
    http://archive.ics.uci.edu/ml/in­dex.php
  8. LED Display Domain Data Set
    http://archive.ics.uci.edu/ml/da­tasets/LED+Display+Domain
  9. Torch na GitHubu (několik repositářů)
    https://github.com/torch
  10. Torch (machine learning), Wikipedia
    https://en.wikipedia.org/wi­ki/Torch_%28machine_learnin­g%29
  11. Torch Package Reference Manual
    https://github.com/torch/tor­ch7/blob/master/README.md
  12. Torch Cheatsheet
    https://github.com/torch/tor­ch7/wiki/Cheatsheet
  13. Neural network containres (Torch)
    https://github.com/torch/nn/blob/mas­ter/doc/containers.md
  14. Simple layers
    https://github.com/torch/nn/blob/mas­ter/doc/simple.md#nn.Line­ar
  15. Transfer Function Layers
    https://github.com/torch/nn/blob/mas­ter/doc/transfer.md#nn.tran­sfer.dok
  16. Feedforward neural network
    https://en.wikipedia.org/wi­ki/Feedforward_neural_net­work
  17. Biologické algoritmy (4) – Neuronové sítě
    https://www.root.cz/clanky/biologicke-algoritmy-4-neuronove-site/
  18. Biologické algoritmy (5) – Neuronové sítě
    https://www.root.cz/clanky/biologicke-algoritmy-5-neuronove-site/
  19. Umělá neuronová síť (Wikipedia)
    https://cs.wikipedia.org/wi­ki/Um%C4%9Bl%C3%A1_neuronov%C3%A1_s%C3%AD%C5%A5
  20. Učení s učitelem (Wikipedia)
    https://cs.wikipedia.org/wi­ki/U%C4%8Den%C3%AD_s_u%C4%8Di­telem
  21. Plotting with Torch7
    http://www.lighting-torch.com/2015/08/24/plotting-with-torch7/
  22. Plotting Package Manual with Gnuplot
    https://github.com/torch/gnu­plot/blob/master/README.md
  23. An Introduction to Tensors
    https://math.stackexchange­.com/questions/10282/an-introduction-to-tensors
  24. Gaussian filter
    https://en.wikipedia.org/wi­ki/Gaussian_filter
  25. Gaussian function
    https://en.wikipedia.org/wi­ki/Gaussian_function
  26. Laplacian/Laplacian of Gaussian
    http://homepages.inf.ed.ac­.uk/rbf/HIPR2/log.htm
  27. Odstranění šumu
    https://cs.wikipedia.org/wi­ki/Odstran%C4%9Bn%C3%AD_%C5%A­1umu
  28. Binary image
    https://en.wikipedia.org/wi­ki/Binary_image
  29. Erosion (morphology)
    https://en.wikipedia.org/wi­ki/Erosion_%28morphology%29
  30. Dilation (morphology)
    https://en.wikipedia.org/wi­ki/Dilation_%28morphology%29
  31. Mathematical morphology
    https://en.wikipedia.org/wi­ki/Mathematical_morphology
  32. Cvičení 10 – Morfologické operace
    http://midas.uamt.feec.vut­br.cz/ZVS/Exercise10/conten­t_cz.php
  33. Differences between a matrix and a tensor
    https://math.stackexchange­.com/questions/412423/dif­ferences-between-a-matrix-and-a-tensor
  34. Qualitatively, what is the difference between a matrix and a tensor?
    https://math.stackexchange­.com/questions/1444412/qu­alitatively-what-is-the-difference-between-a-matrix-and-a-tensor?
  35. BLAS (Basic Linear Algebra Subprograms)
    http://www.netlib.org/blas/
  36. Basic Linear Algebra Subprograms (Wikipedia)
    https://en.wikipedia.org/wi­ki/Basic_Linear_Algebra_Sub­programs
  37. Comparison of deep learning software
    https://en.wikipedia.org/wi­ki/Comparison_of_deep_lear­ning_software
  38. TensorFlow
    https://www.tensorflow.org/
  39. Caffe2 (A New Lightweight, Modular, and Scalable Deep Learning Framework)
    https://caffe2.ai/
  40. PyTorch
    http://pytorch.org/
  41. Seriál o programovacím jazyku Lua
    http://www.root.cz/serialy/pro­gramovaci-jazyk-lua/
  42. LuaJIT – Just in Time překladač pro programovací jazyk Lua
    http://www.root.cz/clanky/luajit-just-in-time-prekladac-pro-programovaci-jazyk-lua/
  43. LuaJIT – Just in Time překladač pro programovací jazyk Lua (2)
    http://www.root.cz/clanky/luajit-just-in-time-prekladac-pro-programovaci-jazyk-lua-2/
  44. LuaJIT – Just in Time překladač pro programovací jazyk Lua (3)
    http://www.root.cz/clanky/luajit-just-in-time-prekladac-pro-programovaci-jazyk-lua-3/
  45. LuaJIT – Just in Time překladač pro programovací jazyk Lua (4)
    http://www.root.cz/clanky/luajit-just-in-time-prekladac-pro-programovaci-jazyk-lua-4/
  46. LuaJIT – Just in Time překladač pro programovací jazyk Lua (5 – tabulky a pole)
    http://www.root.cz/clanky/luajit-just-in-time-prekladac-pro-programovaci-jazyk-lua-5-tabulky-a-pole/
  47. LuaJIT – Just in Time překladač pro programovací jazyk Lua (6 – překlad programových smyček do mezijazyka LuaJITu)
    http://www.root.cz/clanky/luajit-just-in-time-prekladac-pro-programovaci-jazyk-lua-6-preklad-programovych-smycek-do-mezijazyka-luajitu/
  48. LuaJIT – Just in Time překladač pro programovací jazyk Lua (7 – dokončení popisu mezijazyka LuaJITu)
    http://www.root.cz/clanky/luajit-just-in-time-prekladac-pro-programovaci-jazyk-lua-7-dokonceni-popisu-mezijazyka-luajitu/
  49. LuaJIT – Just in Time překladač pro programovací jazyk Lua (8 – základní vlastnosti trasovacího JITu)
    http://www.root.cz/clanky/luajit-just-in-time-prekladac-pro-programovaci-jazyk-lua-8-zakladni-vlastnosti-trasovaciho-jitu/
  50. LuaJIT – Just in Time překladač pro programovací jazyk Lua (9 – další vlastnosti trasovacího JITu)
    http://www.root.cz/clanky/luajit-just-in-time-prekladac-pro-programovaci-jazyk-lua-9-dalsi-vlastnosti-trasovaciho-jitu/
  51. LuaJIT – Just in Time překladač pro programovací jazyk Lua (10 – JIT překlad do nativního kódu)
    http://www.root.cz/clanky/luajit-just-in-time-prekladac-pro-programovaci-jazyk-lua-10-jit-preklad-do-nativniho-kodu/
  52. LuaJIT – Just in Time překladač pro programovací jazyk Lua (11 – JIT překlad do nativního kódu procesorů s architekturami x86 a ARM)
    http://www.root.cz/clanky/luajit-just-in-time-prekladac-pro-programovaci-jazyk-lua-11-jit-preklad-do-nativniho-kodu-procesoru-s-architekturami-x86-a-arm/
  53. LuaJIT – Just in Time překladač pro programovací jazyk Lua (12 – překlad operací s reálnými čísly)
    http://www.root.cz/clanky/luajit-just-in-time-prekladac-pro-programovaci-jazyk-lua-12-preklad-operaci-s-realnymi-cisly/
  54. Lua Profiler (GitHub)
    https://github.com/luafor­ge/luaprofiler
  55. Lua Profiler (LuaForge)
    http://luaforge.net/projec­ts/luaprofiler/
  56. ctrace
    http://webserver2.tecgraf.puc-rio.br/~lhf/ftp/lua/
  57. The Lua VM, on the Web
    https://kripken.github.io/lu­a.vm.js/lua.vm.js.html
  58. Lua.vm.js REPL
    https://kripken.github.io/lu­a.vm.js/repl.html
  59. lua2js
    https://www.npmjs.com/package/lua2js
  60. lua2js na GitHubu
    https://github.com/basicer/lua2js-dist
  61. Lua (programming language)
    http://en.wikipedia.org/wi­ki/Lua_(programming_langu­age)
  62. LuaJIT 2.0 SSA IRhttp://wiki.luajit.org/SSA-IR-2.0
  63. The LuaJIT Project
    http://luajit.org/index.html
  64. LuaJIT FAQ
    http://luajit.org/faq.html
  65. LuaJIT Performance Comparison
    http://luajit.org/performance.html
  66. LuaJIT 2.0 intellectual property disclosure and research opportunities
    http://article.gmane.org/gma­ne.comp.lang.lua.general/58908
  67. LuaJIT Wiki
    http://wiki.luajit.org/Home
  68. LuaJIT 2.0 Bytecode Instructions
    http://wiki.luajit.org/Bytecode-2.0
  69. Programming in Lua (first edition)
    http://www.lua.org/pil/contents.html
  70. Lua 5.2 sources
    http://www.lua.org/source/5.2/
  71. REPL
    https://en.wikipedia.org/wi­ki/Read%E2%80%93eval%E2%80%93prin­t_loop
  72. The LLVM Compiler Infrastructure
    http://llvm.org/ProjectsWithLLVM/
  73. clang: a C language family frontend for LLVM
    http://clang.llvm.org/
  74. LLVM Backend („Fastcomp“)
    http://kripken.github.io/emscripten-site/docs/building_from_source/LLVM-Backend.html#llvm-backend
  75. Lambda the Ultimate: Coroutines in Lua,
    http://lambda-the-ultimate.org/node/438
  76. Coroutines Tutorial,
    http://lua-users.org/wiki/CoroutinesTutorial
  77. Lua Coroutines Versus Python Generators,
    http://lua-users.org/wiki/LuaCorouti­nesVersusPythonGenerators
Našli jste v článku chybu?