Konwolucyjne sieci neuronowe 1: konwolucja

Home > ROZPOZNAWANIE OBRAZÓW  > Konwolucyjne sieci neuronowe 1: konwolucja
Konwolucyjne sieci neuronowe

Głębokie sieci neuronowe znajdują szerokie zastosowanie w rozpoznawaniu obrazów i kształtów. Przykładowe aplikacje obejmują rozpoznawanie twarzy, analizę obrazów w medycynie, klasyfikację pisma czy detekcję obiektów otoczenia. Specjalnym rodzajem sieci neuronowej, który wyjątkowo dobrze radzi sobie z przetwarzaniem obrazu, są konwolucyjne sieci neuronowe.

Przyznam, że ConvNet to moja ulubiona architektura głębokich sieci neuronowych i bardzo lubię sięgać po nią w każdej sytuacji, gdy mam taką możliwość. Stąd już się cieszę na myśl o opisaniu jej w niniejszej serii postów. Ma ona charakter praktycznego tutoriala, więc zachęcam was do dołączenia się do aktywnego tworzenia kodu. Jeżeli nie macie jeszcze środowiska programistycznego opartego na Keras i Tensorflow 2.0, to tu znajdziecie instrukcje, jak je zbudować

Mój pomysł na niniejszego tutoriala, to sięgnięcie po w miarę wymagający, ale dostępny powszechnie zbiór danych, a następnie pokazanie, jak na takim zbiorze radzi sobie klasyczna sieć neuronowa. W kolejnym kroku sprawdzimy, jaki wynik otrzymamy, zaprzęgając do pracy konwolucyjne sieci neuronowe oraz w jaki sposób możemy zwiększyć poprawność klasyfikacji, wykorzystując różne techniki regularyzacji. Praca z sieciami neuronowymi to nie zawsze praca od podstaw. Jest wiele sprawdzonych architektur, po które można sięgnąć, ale rozbudowane sieci konwolucyjne mogą mieć całkiem spore zapotrzebowanie na moc obliczeniową. Dobre GPU na pewno się przyda, szczególnie w dalszej części tutoriala. Pracy i materiału jest sporo, więc artykuł podzieliłem na kilka części.

Z pierwszej części tutoriala dowiesz się:

  • Czym jest zbiór Cifar-10. Jak go pobrać, załadować i przygotować dane do uczenia?
  • Jak zrealizować klasyfikację zbioru z wykorzystaniem klasycznej sieci neuronowej?
  • Czym jest konwolucja?
  • Jak wykonać konwolucję na prostym przykładzie obrazka?

W kolejnych częściach poruszam zaś takie tematy, jak:

  • Czym jest i jak działa konwolucyjna sieć neuronowa?
  • Jak zbudować prostą sieć konwolucyjną z użyciem bibliotek Keras i Tensorflow?
  • Czym jest regularyzacja, jakie są techniki regularyzacji i jak wpłyną one na otrzymane rezultaty?
  • Jak podnieść wyniki klasyfikacji stosując generatory danych?

Czy oglądasz mecze NBA? Sprawdź mój darmowy serwis NBA Games Ranked i ciesz się oglądaniem wyłącznie dobrych meczów.


Zbiór danych

Dla celów naszego tutoriala użyjemy popularnego zbioru Cifar-10. Zawiera on 60.000 kolorowych obrazków w formacie 32×32 pikseli. Obrazki są w niskiej rozdzielczości i zostały sklasyfikowane na 10 klas: samoloty, samochody, statki i ciężarówki, a także koty, ptaki, jelenie, konie, psy i żaby. Klasy są ładnie zbalansowane, każda z nich ma po 6000 zdjęć.

Autorami zbioru są Alex Krizhevsky, Vinod Nair i Geoffrey Hinton. Więcej o zbiorze, w jaki sposób powstał i jak można go wykorzystać, można znaleźć w tej pracy: Learning Multiple Layers of Features from Tiny Images, Alex Krizhevsky.

Załadowanie zbioru jest bardzo proste, bo jest on obecnie dostępny w bibliotece Keras:

import numpy as np
import matplotlib.pyplot as plt
import tensorflow

print(tensorflow.__version__)
print(tensorflow.keras.__version__)

>>>2.0.0

>>>2.2.4-tf

from tensorflow.keras.datasets import cifar10
(x_train, y_train), (x_test, y_test) = cifar10.load_data()

Metoda load_data() dzieli zbiór na dane uczące i testowe w relacji 50000 do 10000. Sprawdźmy, jak wygląda pierwsze 10 labelek oraz jaki kształt mają dane uczące:

y_train.shape
>>> (50000, 1)

y_train[0:10]
>>> array([[6], [9], [9], [4], [1], [1], [2], [7], [8], [3]], dtype=uint8)

x_train.shape
>>> (50000, 32, 32, 3)

Jak widać labelki są cyframi od 0 do 9, gdzie każda cyfra oznacza daną klasę, np. 1 to samochód. Dane uczące to tablica składająca się z 50000 elementów. Każdy z nich zawiera trzy kanały (RBG) obrazka o rozdzielczości 32×32. Na początek wyświetlmy 16 losowo wybranych obrazków.

il_zdjec = 16
zdjecia = np.zeros((il_zdjec,32,32,3), dtype=int)
opisy = np.zeros((il_zdjec,1), dtype=int)
for i in range(il_zdjec):
   indeks = np.random.randint(0, 50000)
   zdjecia[i] = x_train[indeks]
   opisy[i] = y_train[indeks]

zdjecia.shape
>>> (16, 32, 32, 3)

opisy.shape
>>> (16, 1)

Do podpisania obrazków przyda nam się prosty słownik

slownik = {
   0: 'samolot',
   1: 'samochód',
   2: 'ptak',
   3: 'kot',
   4: 'jeleń',
   5: 'pies',
   6: 'żaba',
   7: 'koń',
   8: 'statek',
   9: 'ciężarówka',
}

Biblioteka matplotlib.pyplot dostarcza nam niezwykle rozbudowane możliwości prezentacji danych. My wykorzystamy niewielki ułamek tych możliwości do zaprezentowania losowo wybranych 16 zdjęć na jednym obrazku:

fig = plt.figure()
for n, (obrazek, label) in enumerate(zip(zdjecia, opisy)):
   a = fig.add_subplot(4, 4, n + 1)
   plt.imshow(obrazek)
   a.set_title(slownik[label[0]])
   a.axis('off')
fig.set_size_inches(fig.get_size_inches() * il_zdjec / 7)
plt.show()

Konwolucyjne sieci neuronowe - przykładowe obrazki ze zbioru cifar10

Klasyfikacja z wykorzystaniem klasycznej sieci neuronowej

Czas zmierzyć się z zadaniem klasyfikacji tych obrazków. Zanim przejdziemy do ConvNetów, dla celów referencyjnych zbudujemy klasyfikator na „zwykłej” gęsto połączonej sieci neuronowej – czyli takiej, w której każdy neuron danej warstwy jest połączony z każdym neuronem warstwy następnej.

Gęsto połączone sieci neuronowe oczekują na wejściu „płaskich” danych. Musimy zatem spłaszczyć nasze kolorowe obrazki do postaci jednowymiarowej. Można to zrobić jak poniżej, a można również skorzystać z predefiniowanej, kerasowej warstwy Flatten, która sama spłaszczy nam dane. Ja na pewno skorzystam z niej nieco później.

x_train = x_train.reshape((-1, 3072))
x_test = x_test.reshape((-1, 3072))

x_train.shape
>>> (50000, 3072)

Liczba 3072 wynika z przemnożenia wymiarów 32 x 32 x 3.

Ponieważ każdy piksel w każdym kanale określa intensywność składowej koloru dla piksela, to dane powinny być z przedziału od 0 do 255. Sprawdźmy to:

x_train.max()
>>> 255
x_train.min()
>>> 0

Sieci neuronowe działają najlepiej w obszarze największej aktywności swoich funkcji aktywacji. Stąd dobrą praktyką jest normalizacja danych wejściowych, Jest wiele rodzajów normalizacji – więcej pisałem o tym tutaj. My znormalizujemy dane w taki sposób, aby średnia wypadła w okolicy 0.

x_train = (x_train / 255)- 0.5
x_test = (x_test / 255)- 0.5

Gdybyśmy chcieli normalizować dane wokół 0.5, kod powinien wyglądać następująco:

x_train = (x_train / 255)

Pozostało nam dokonać koniecznych importów, zbudować model, skompilować go i uruchomić proces nauki. Bardziej szczegółowo o sposobie budowania modelu pisałem w tym poście:

from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense
from tensorflow.keras.utils import to_categorical

model = Sequential([
   Dense(1024, activation='tanh', input_shape=(3072,)),
   Dense(512, activation='tanh'),
   Dense(256, activation='tanh'),
   Dense(128, activation='tanh'),
   Dense(64, activation='tanh'),
   Dense(10, activation='softmax')
])

model.compile(
   optimizer='RMSprop',
   loss='categorical_crossentropy',
   metrics=['accuracy']
)

model.fit(
   x=x_train,
   y=to_categorical(y_train),
   epochs=15,
   shuffle=True
)

Na zbiorze treningowym powinniście uzyskać accuracy na poziomie około 53%. Pozostało nam sprawdzić jak nasza sieć neuronowa poradzi sobie z danymi testowymi, których uprzednio nie widziała. Skorzystamy z metody evaluate. Jeżeli zastanawiacie się dlaczego w metodach fit oraz evaluate znalazło się wywołanie funkcji to_categorical, to wyjaśniłem to w tym poście.

eval = model.evaluate(
   x_test,
   to_categorical(y_test)
)

eval
>>> [1.5109523288726807, 0.4749]

Zatem nasza prosta sieć neuronowa na zbiorze Cifar-10 uzyskała poprawność rzędu 47%. Biorąc pod uwagę, iż zbudowaliśmy bardzo prostą sieć neuronową, że mamy aż 10 klas oraz relatywnie niską jakość obrazków, jest to całkiem przyzwoity wynik. Szczególnie gdybyśmy porównali go do 10% jakie oferuje nam losowy wybór. Mając powyższy benchmark, możemy przejść do ConvNetów. Zaczniemy jednak od tego, że należałoby wyjaśnić czym jest konwolucja.

Konwolucja to operacja matematyczna

No tak! „Konwolucja to operacja matematyczna” – i wszystko jasne. 😀 Ameryki tym nie odkryłem, bo przecież większość tego co się dzieje w sieciach neuronowych, to operacja matematyczna. Dodam jednak, że jest ona dość prosta i ma tylko kilka parametrów, które wystarczy umieć interpretować, aby konwolucję wykonać samemu. Będę jednak unikał wzorów i obliczeń matematycznych, bo nie są one niezbędne, aby ostatecznie efektywnie wykorzystywać ConvNety.

Zacznijmy od tego, co to jest i po co nam w ogóle potrzebna konwolucja, skoro nawet prosta, klasyczna sieć neuronowa potrafi całkiem nieźle sklasyfikować zbiór i pewnie – gdyby nieco popracować nad jej architekturą oraz dodać regularyzację (o której więcej w kolejnych częściach tutoriala) – wynik mógłby być dużo lepszy niż 47%.

Czym zatem jest konwolucja? Posłużę się kilkoma przykładami. Jeżeli ktoś z Was interesuje się muzyką, to na pewno spotkał się z efektami dźwiękowymi. Oryginalny dźwięk można zmodyfikować, nakładając na niego filtr efektu dźwiękowego. W przypadku obrazu, który również jest sygnałem, nałożenie filtra może dać nam ten sam obraz, ale z pewnymi cechami uwypuklonymi lub ukrytymi, np. możemy obraz wyostrzyć lub rozmyć, możemy też zidentyfikować w obrazie krawędzie. Jeżeli ktoś jest fanem fotografii, to zapewne niejednokrotnie używał funkcji antyaliasingu. Jest to również nic innego jak nałożenie odpowiedniego filtra na oryginalny sygnał. Operację nałożenia filtra na sygnał nazywamy konwolucją.

Jak widać, zastosowań konwolucji są tysiące i często nie zdajemy sobie sprawy, jak często spotykamy się z filtrowaniem danych, szczególnie cyfrowych. Jednak w kontekście klasyfikacji obrazu i ConvNetów interesuje nas konwolucja, której celem nie jest wzbogacenie sygnału (tu obrazka) o efekty specjalne, lecz takie jego przekształcenie, aby sieć neuronowa była w stanie lepiej wychwycić cechy charakterystyczne obrazu i w efekcie skuteczniej go sklasyfikować.

Pomysłem stojącym za konwolucyjnymi sieciami neuronowymi jest przekształcenie oryginalnego obrazu przed jego przekazaniem do gęsto połączonej, klasycznej sieci neuronowej. Co uzyskujemy dzięki konwolucji?

  • Po przetworzeniu obrazu filtrem pewne cechy obrazu zostają uwypuklone, co ułatwia ich rozpoznawanie. Jest to tzw. feature extraction – sieć sama znajduje cechy istotne dla obrazu.
  • Z reguły nakładamy wiele filtrów naraz i każdy z nich może uwypuklać inne cechy. Np. przy rozpoznawaniu twarzy jeden uwypukli oczy, drugi uszy, linię włosów lub ich brak. 😉
  • W wyniku konwolucji uniezależniamy się od położenia obiektu na obrazie. To czy samolot jest zaprezentowany centralnie, czy w lewym górnym rogu dla sieci konwolucyjnej nie będzie miało znaczenia i nie wpłynie negatywnie na klasyfikację. Jeżeli czytaliście mój post nt. klasyfikacji zbioru MNIST, to zapewne pamiętacie, że wszystkie cyfry były tam zaprezentowane centralnie. Przygotowanie jednak tak „idealnego” zbioru jest trudne i pracochłonne, a czasami po prostu niemożliwe.
  • Zmniejszamy szum na analizowanych obrazkach skupiając uwagę sieci na kluczowych cechach.
  • Z reguły jednym z etapów w konwolucyjnej sieci neuronowej jest warstwa wykonująca operację tzw. poolingu, czyli pewnego rodzaju połączenia wartości kilku sąsiadujących pikseli w jeden. Zmniejsza to istotnie moc obliczeniową niezbędną do nauczenia sieci, nie tracąc jednocześnie ważnych informacji.

Dość ważny jest fakt, że to algorytm uczący dobiera odpowiednie filtry, a nie my jako zlecający uczenie. Filtry są inicjowane losowo i dalej to algorytm propagacji wstecznej decyduje, które wartości filtrów dają najlepsze rezultaty klasyfikacji. W konsekwencji wygląd obrazka po konwolucji zrealizowanej przez maszynę często niewiele mówi ludzkiemu oku, ale za to w jakiś sposób jest istotny dla wyniku działania sieci neuronowej.

Konwolucja na pojedynczym obrazku

Wiedząc czym jest konwolucja, zanim jeszcze przejdziemy do wykorzystania jej w sieciach neuronowych, spróbujmy wykonać konwolucję na pojedynczym zdjęciu i zobaczmy, jakie efekty uda nam się uzyskać.

Potrzebne będą nam importy kilku bibliotek, w tym te do przetwarzania obrazu.

import numpy as np
from PIL import Image
import matplotlib.pyplot as plt
%matplotlib inline

Zdefiniujemy również pomocniczą funkcję, która pobierze obrazek z dysku lokalnego, skonwertuje go do skali szarości, abyśmy działali w jednym, a nie w trzech kanałach koloru i zwróci całość jako tablicę numpy.

def convert_image(file):
   return np.array(Image.open(file).convert('L'))

W niniejszym przykładzie posłużę się zdjęciem. które zrobiłem latem 2019 na Krecie. Niestety skala szarości nie oddaje tego jak piękne potrafi być tam słońce we wrześniu. 😎 Oryginalny plik (choć w małym rozmiarze) do pobrania dostępny jest tutaj.

image = convert_image(r'wpisz-ścieżkę-do-zdjęcia-na-dysku\house-small.jpg')
image.shape
>>> (302, 403)
plt.imshow(image, cmap='gray')

Konwolucyjne sieci neuronowe - obrazek do konwolucji

Do zrealizowania konwolucji posłużymy się Kerasem:

import tensorflow as tf
print(tf.__version__)
>>> 2.0.0

from tensorflow.keras import Sequential
from tensorflow.keras.layers import Conv2D

Zaimportowaliśmy model typu Sequential, a budując go, wykorzystamy warstwę Conv2D, służącą do realizacji dwuwymiarowej konwolucji. Model jest bardzo prosty – zawiera tylko jedną warstwę oraz kernel (filtr) o rozmiarze 3×3. Musimy również wskazać rozmiar obrazka wejściowego – sprawdzaliśmy go wyżej: 302 x 403.

Wykonujemy konwolucję z wykorzystaniem jednego filtra, czyli parametr filters powinniśmy ustawić na wartość 1.

model = Sequential(
   Conv2D(filters=1,
   kernel_size=(3,3),
   input_shape=(302, 403, 1))
)

Model w bibliotece Keras zawiera metodę, która bywa pomocna w zrozumieniu, jak de facto model wygląda.

model.summary()

>>> Model: "sequential"
>>> _________________________________________________________________
>>> Layer (type)                 Output Shape              Param #   
>>> =================================================================
>>> conv2d (Conv2D)              (None, 300, 401, 1)       10        
>>> =================================================================
>>> Total params: 10
>>> Trainable params: 10
>>> Non-trainable params: 0

Warto zwrócić uwagę na „Output Shape” – kształt wyjściowy. Po konwolucji jest on nieznacznie różny od wejściowego, co wynika ze sposobu realizowania operacji konwolucji. Nie będę wchodził tu w szczegóły, więcej w kolejnej części tutoriala.

Inną istotną kwestią jest przeformatowanie obrazka do postaci zjadliwej dla modelu. Obrazek wejściowy jest zapisany w tablicy 302 x 403. Tymczasem warstwa Conv2d oczekuje tensora 4-wymiarowego. Musimy więc dokonać odpowiedniej transformacji, na przykład wykorzystując do tego celu dedykowaną metodę expend_dims.

image4Conv = tf.expand_dims(image, 0)
image4Conv = tf.expand_dims(image4Conv, -1)
image4Conv.shape
>>> TensorShape([1, 302, 403, 1])

Najpierw dodaliśmy pierwszy wymiar, którego zadaniem jest przechowywanie numeru (indeksowanie) elementu w paczce danych. Kiedy uczymy na dużych ilościach danych ma to znaczenie. U nas mamy jeden element / jedno zdjęcie, więc teoretycznie nie ma to znaczenia, ale Keras i tak oczekuje, że będzie ono zaindeksowane na pierwszym indeksie otrzymanego tensora. Kolejne dwa wymiary zawierają współrzędne pikseli, a ostatni wymiar wartość piksela. W naszym przykładzie mamy do czynienia z odcieniami szarości, a więc z jedną liczbą / jednym kanałem. Jeżeli przetwarzalibyśmy obraz RGB, tensor byłby kształtu [1, 302, 403, 3].

Gdybyśmy byli w procesie uczenia, tak zaprojektowany model najpierw skompilowalibyśmy, ustalając funkcję celu i metryki, a potem wywołalibyśmy proces uczenia wywołując metodę fit. My jednak chcemy po prostu przepuścić obrazek przez warstwę Conv2D, z losowo na razie dobranymi wartościami filtra i zobaczyć, jaki efekt otrzymamy. Stąd wykorzystamy metodę predict, która wykona dokładnie to, o co nam chodzi.

result = model.predict(image4Conv)
result.shape
>>> (1, 300, 401, 1)

Zgodnie z tym co pokazała nam metoda summary, na wyjściu otrzymaliśmy tensor o wymiarach 300, 401 – obrazek wynikowy będzie zatem nieznacznie mniejszy. Aby wyświetlić obrazek, musimy najpierw pozbyć się dodanych uprzednio „sztucznych” wymiarów.

result = tf.squeeze(result)
result.shape
>>> TensorShape([300, 401])

plt.imshow(result, cmap='gray')

Konwolucyjne sieci neuronowe - wynik losowej konwolucji

Jak widać, obrazek został przetworzony przez losowo ustawione parametry filtra. Jeżeli uruchamiacie ten lub podobny kod na swoim środowisku i pracujecie na moim obrazku, to mimo to wynik otrzymacie inny, bo losowo ustawione parametry filtra dadzą inne efekty końcowe.

Jeżeli chcemy otrzymać przewidywalny/konkretny efekt konwolucji, to musimy samodzielnie ustawić parametry filtra. Keras daje nam taką możliwość, choć w praktyce może być ona rzadko wykorzystywana. Jest ona opisana na samym dole tej strony, w sekcji „Using custom initializers”. Warto tu raz jeszcze podkreślić, że parametry filtra konwolucji są de facto parametrami modelu, które tenże model zmienia samodzielnie, próbując otrzymać jak najlepsze efekty końcowe. Zatem operacje, które wykonamy poniżej, mają na celu jedynie zrozumienie mechanizmu konwolucji. Trenując sieć konwolucyjną nie będziemy ingerowali w wartości filtrów, choć możemy inicjować te filtry na różne sposoby, opisane w podanym powyżej linku.

from tensorflow.keras import backend as K

def my_filter(shape, dtype=None):
   # Ustawiamy filtr na detekcję pionowych i poziomych krawędzi
   f = np.array([
         [[[-1]], [[-1]], [[-1]]],
         [[[-1]], [[ 8]], [[-1]]],
         [[[-1]], [[-1]], [[-1]]]
   ])
   return K.variable(f, dtype='float32')

Wartość filtra jest ustawiona w taki sposób, że będzie on identyfikował pionowe i poziome krawędzie, wyróżniając je na tle pozostałych elementów obrazka. Zwróćcie uwagę, że kształt tablicy filtra to (3,3,1,1) – może to nie być widoczne na pierwszy rzut oka – oraz że elementy filtra sumują się do jedności. Jeżeli nie sumowałyby się do jedności, to obrazek wyszedłby ciemniejszy lub jaśniejszy. Warto poeksperymentować z różnymi wartościami filtra. Tu jeszcze mała uwaga: jeżeli używacie swojego obrazka i ma on istotnie większy rozmiar niż okolice 300 x 400 pikseli, to należy użyć nieco większego filtra, aby efekty konwolucji na większym obrazku były widoczne gołym okiem.

Teraz pozostaje nam zbudować model w oparciu o tak zainicjowany filtr, dokonać konwolucji oraz wyświetlić obrazek oryginalny i przetworzony.

model_edge = Sequential(
   Conv2D(filters=1,
          kernel_size=(3,3),
          kernel_initializer=my_filter,
          input_shape=(302, 403, 1))
)

result_edge = model_edge.predict(image4Conv)
result_edge.shape
>>> (1, 300, 401, 1)

result_edge = tf.squeeze(result_edge)
result_edge.shape
>>> TensorShape([300, 401])

plt.rcParams['figure.figsize'] = [12, 8]
plt.rcParams['figure.dpi'] = 142
fig = plt.figure()
ax1 = fig.add_subplot(2,1,1)
ax1.imshow(image, cmap='gray')
ax1.set_title('ORIGINAL')
ax2 = fig.add_subplot(2,1,2)
ax2.imshow(result_edge, cmap='gray')
ax2.set_title('AFTER CONVOLUTION')
plt.show()

Konwolucja - wynik detekcji krawędzi

Może szału nie ma, 😉 ale widać, że operacja wychwyciła krawędzie.

Spróbujmy jeszcze jednego filtra, którego zadaniem będzie rozmycie obrazka. W tym celu każdy element filtra będzie miał wartość 1/n, gdzie n to ilość elementów filtra. Np. dla filtra 3×3, każdy element powinien mieć wartość 1/9. Ponieważ chciałbym, aby rozmycie było silniejsze (bardziej widoczne), to użyjemy filtra o wielkości 7×7, z wartością 1/49. W ten sposób wszystkie elementy filtra nadal sumują się do jedności.

def my_filter(shape, dtype=None):
   f = np.empty(shape=(7,7,1,1))
   f.fill(1/49)
   return K.variable(f, dtype='float32')

model_blur = Sequential(Conv2D(filters=1, kernel_size=(7,7), kernel_initializer=my_filter, input_shape=(302, 403, 1)))
result_blur = model_blur.predict(image4Conv)
result_blur.shape
>>> (1, 296, 397, 1)

result_blur = tf.squeeze(result_blur)

fig = plt.figure()
ax1 = fig.add_subplot(2,1,1)
ax1.imshow(image, cmap='gray')
ax1.set_title('ORIGINAL')
ax2 = fig.add_subplot(2,1,2)
ax2.imshow(result_blur, cmap='gray')
ax2.set_title('AFTER CONVOLUTION')
plt.show()

Konwolucja - wynik rozmycia

Tym razem efekt jest niepodważalny. 😎

Zapraszam do zapoznania się z dalszym ciągiem tutoriala w jego drugiej części.


Masz pytanie? Zadaj je w komentarzu.

Spodobał ci się post? Będzie mi miło, gdy go polecisz.

Do zobaczenia wkrótce, przy okazji omawiania innego ciekawego tematu!


Czy oglądasz mecze NBA? Sprawdź mój darmowy serwis NBA Games Ranked i ciesz się oglądaniem wyłącznie dobrych meczów.


7 komentarzy
  • Anonim

    20 lutego 2022at13:54

    Jak określić liczebność zbioru uczącego?

      • Anonim

        21 lutego 2022at09:31

        Bardzo dziękuję za odpowiedź.
        Ale chodziło mi o to, ile musimy przygotować obrazów w fazie przygotowania do rozwiązania problemu. W sensie czy jest jakiś wzór teoretyczny to określający. Jak nie masz czasu na odpowiedź, to może jakiś link…

  • Anonim

    10 maja 2022at15:43

    Taki błąd otrzymuję

    Dense(1024, activation=’tanh’, input_shape(3072,)),
    ^
    SyntaxError: positional argument follows keyword argument

      • Anonim

        16 maja 2022at09:47

        OK, dzięki za pomoc, wszystko mi już działa 🙂

Post a Comment