Intuicyjne zrozumienie konwolucji 1D, 2D i 3D w konwolucyjnych sieciach neuronowych


Odpowiedzi:


417

Chcę to wyjaśnić obrazem z C3D .

W skrócie, kierunek splotu i kształt wyjścia są ważne!

wprowadź opis obrazu tutaj

↑↑↑↑↑ 1D zwojów - Podstawowe ↑↑↑↑↑

  • tylko jeden kierunek (oś czasu), aby obliczyć konw
  • wejście = [W], filtr = [k], wyjście = [W]
  • np. wejście = [1,1,1,1,1], filtr = [0,25,0,5,0,25], wyjście = [1,1,1,1,1]
  • kształt-wyjściowy to tablica 1D
  • przykład) wygładzanie wykresów

kod tf.nn.conv1d Zabawka Przykład

import tensorflow as tf
import numpy as np

sess = tf.Session()

ones_1d = np.ones(5)
weight_1d = np.ones(3)
strides_1d = 1

in_1d = tf.constant(ones_1d, dtype=tf.float32)
filter_1d = tf.constant(weight_1d, dtype=tf.float32)

in_width = int(in_1d.shape[0])
filter_width = int(filter_1d.shape[0])

input_1d   = tf.reshape(in_1d, [1, in_width, 1])
kernel_1d = tf.reshape(filter_1d, [filter_width, 1, 1])
output_1d = tf.squeeze(tf.nn.conv1d(input_1d, kernel_1d, strides_1d, padding='SAME'))
print sess.run(output_1d)

wprowadź opis obrazu tutaj

& Uarr ; & uarr; & gt; & gt; & gt; & gt; & gt; & gt; & gt; &

  • 2 - kierunek (x, y) do obliczenia konw
  • kształt wyjściowy to Matryca 2D
  • wejście = [szer., wys.], filtr = [k, k] wyjście = [szer., wys.]
  • przykład) Sobel Egde Fllter

tf.nn.conv2d - przykład zabawki

ones_2d = np.ones((5,5))
weight_2d = np.ones((3,3))
strides_2d = [1, 1, 1, 1]

in_2d = tf.constant(ones_2d, dtype=tf.float32)
filter_2d = tf.constant(weight_2d, dtype=tf.float32)

in_width = int(in_2d.shape[0])
in_height = int(in_2d.shape[1])

filter_width = int(filter_2d.shape[0])
filter_height = int(filter_2d.shape[1])

input_2d   = tf.reshape(in_2d, [1, in_height, in_width, 1])
kernel_2d = tf.reshape(filter_2d, [filter_height, filter_width, 1, 1])

output_2d = tf.squeeze(tf.nn.conv2d(input_2d, kernel_2d, strides=strides_2d, padding='SAME'))
print sess.run(output_2d)

wprowadź opis obrazu tutaj

& Uarr ; & uarr; &; & gt; & gt; & gt; & gt; & gt; & gt; & rdquo; & rdquo; & d \ u003; d \ u003d

  • 3 -kierunkowy (x, y, z) do obliczenia konw
  • kształtem wyjściowym jest objętość 3D
  • wejście = [W, H, L ], filtr = [k, k, d ] wyjście = [W, H, M]
  • d <L jest ważne! do tworzenia objętości
  • przykład) C3D

tf.nn.conv3d - przykład zabawki

ones_3d = np.ones((5,5,5))
weight_3d = np.ones((3,3,3))
strides_3d = [1, 1, 1, 1, 1]

in_3d = tf.constant(ones_3d, dtype=tf.float32)
filter_3d = tf.constant(weight_3d, dtype=tf.float32)

in_width = int(in_3d.shape[0])
in_height = int(in_3d.shape[1])
in_depth = int(in_3d.shape[2])

filter_width = int(filter_3d.shape[0])
filter_height = int(filter_3d.shape[1])
filter_depth = int(filter_3d.shape[2])

input_3d   = tf.reshape(in_3d, [1, in_depth, in_height, in_width, 1])
kernel_3d = tf.reshape(filter_3d, [filter_depth, filter_height, filter_width, 1, 1])

output_3d = tf.squeeze(tf.nn.conv3d(input_3d, kernel_3d, strides=strides_3d, padding='SAME'))
print sess.run(output_3d)

wprowadź opis obrazu tutaj

↑↑↑↑↑ 2D zwojów z wejściem 3D - LeNet, VGG, ..., ↑↑↑↑↑

  • Mimo że dane wejściowe to 3D, np. 224x224x3, 112x112x32
  • kształt wyjściowy to nie objętość 3D , ale Matryca 2D
  • ponieważ głębokość filtra = L musi być dopasowana do kanałów wejściowych = L
  • 2 -direction (x, y) do obliczenia konw. nie 3D
  • wejście = [W, H, L ], filtr = [k, k, L ] wyjście = [W, H]
  • kształt wyjściowy to Matryca 2D
  • co jeśli chcemy wytrenować N filtrów (N to liczba filtrów)
  • wtedy kształtem wyjściowym jest (ułożona 2D) 3D = macierz 2D x N.

conv2d - LeNet, VGG, ... dla 1 filtra

in_channels = 32 # 3 for RGB, 32, 64, 128, ... 
ones_3d = np.ones((5,5,in_channels)) # input is 3d, in_channels = 32
# filter must have 3d-shpae with in_channels
weight_3d = np.ones((3,3,in_channels)) 
strides_2d = [1, 1, 1, 1]

in_3d = tf.constant(ones_3d, dtype=tf.float32)
filter_3d = tf.constant(weight_3d, dtype=tf.float32)

in_width = int(in_3d.shape[0])
in_height = int(in_3d.shape[1])

filter_width = int(filter_3d.shape[0])
filter_height = int(filter_3d.shape[1])

input_3d   = tf.reshape(in_3d, [1, in_height, in_width, in_channels])
kernel_3d = tf.reshape(filter_3d, [filter_height, filter_width, in_channels, 1])

output_2d = tf.squeeze(tf.nn.conv2d(input_3d, kernel_3d, strides=strides_2d, padding='SAME'))
print sess.run(output_2d)

conv2d - LeNet, VGG, ... dla filtrów N.

in_channels = 32 # 3 for RGB, 32, 64, 128, ... 
out_channels = 64 # 128, 256, ...
ones_3d = np.ones((5,5,in_channels)) # input is 3d, in_channels = 32
# filter must have 3d-shpae x number of filters = 4D
weight_4d = np.ones((3,3,in_channels, out_channels))
strides_2d = [1, 1, 1, 1]

in_3d = tf.constant(ones_3d, dtype=tf.float32)
filter_4d = tf.constant(weight_4d, dtype=tf.float32)

in_width = int(in_3d.shape[0])
in_height = int(in_3d.shape[1])

filter_width = int(filter_4d.shape[0])
filter_height = int(filter_4d.shape[1])

input_3d   = tf.reshape(in_3d, [1, in_height, in_width, in_channels])
kernel_4d = tf.reshape(filter_4d, [filter_height, filter_width, in_channels, out_channels])

#output stacked shape is 3D = 2D x N matrix
output_3d = tf.nn.conv2d(input_3d, kernel_4d, strides=strides_2d, padding='SAME')
print sess.run(output_3d)

wprowadź opis obrazu tutaj ↑↑↑↑↑ Bonus conv 1x1 w CNN - GoogLeNet, ..., ↑↑↑↑↑

  • Konw. 1x1 jest myląca, gdy myślisz, że jest to filtr obrazu 2D, jak sobel
  • dla konw. 1x1 w CNN wejście ma kształt 3D, jak na powyższym obrazku.
  • oblicza filtrowanie wgłębne
  • wejście = [szer., wys., dł.], filtr = [1,1, dł.] wyjście = [szer., wys.]
  • wyjściowy kształt stosu to macierz 3D = 2D x N.

tf.nn.conv2d - przypadek specjalny 1x1 konw

in_channels = 32 # 3 for RGB, 32, 64, 128, ... 
out_channels = 64 # 128, 256, ...
ones_3d = np.ones((1,1,in_channels)) # input is 3d, in_channels = 32
# filter must have 3d-shpae x number of filters = 4D
weight_4d = np.ones((3,3,in_channels, out_channels))
strides_2d = [1, 1, 1, 1]

in_3d = tf.constant(ones_3d, dtype=tf.float32)
filter_4d = tf.constant(weight_4d, dtype=tf.float32)

in_width = int(in_3d.shape[0])
in_height = int(in_3d.shape[1])

filter_width = int(filter_4d.shape[0])
filter_height = int(filter_4d.shape[1])

input_3d   = tf.reshape(in_3d, [1, in_height, in_width, in_channels])
kernel_4d = tf.reshape(filter_4d, [filter_height, filter_width, in_channels, out_channels])

#output stacked shape is 3D = 2D x N matrix
output_3d = tf.nn.conv2d(input_3d, kernel_4d, strides=strides_2d, padding='SAME')
print sess.run(output_3d)

Animacja (konwersja 2D z wejściami 3D)

wprowadź opis obrazu tutaj - Oryginalny link: LINK
- Autor: Martin Görner
- Twitter: @martin_gorner
- Google +: plus.google.com/+MartinGorne

Dodatkowe konwolucje 1D z wejściem 2D

wprowadź opis obrazu tutaj ↑↑↑↑↑ 1D zwojów z wejściem 1D ↑↑↑↑↑

wprowadź opis obrazu tutaj ↑↑↑↑↑ 1D zwojów z wejściem 2D ↑↑↑↑↑

  • Chociaż wejście to 2D ex) 20x14
  • kształt wyjściowy to nie 2D , ale matryca 1D
  • ponieważ wysokość filtra = L musi być dopasowana do wysokości wejściowej = L
  • 1 - kierunek (x) do obliczenia konw. nie 2D
  • wejście = [W, L ], filtr = [k, L ] Wyjście = [W]
  • kształt wyjściowy to Matryca 1D
  • co jeśli chcemy wytrenować N filtrów (N to liczba filtrów)
  • wtedy kształtem wyjściowym jest (nałożona 1D) macierz 2D = 1D x N.

Bonus C3D

in_channels = 32 # 3, 32, 64, 128, ... 
out_channels = 64 # 3, 32, 64, 128, ... 
ones_4d = np.ones((5,5,5,in_channels))
weight_5d = np.ones((3,3,3,in_channels,out_channels))
strides_3d = [1, 1, 1, 1, 1]

in_4d = tf.constant(ones_4d, dtype=tf.float32)
filter_5d = tf.constant(weight_5d, dtype=tf.float32)

in_width = int(in_4d.shape[0])
in_height = int(in_4d.shape[1])
in_depth = int(in_4d.shape[2])

filter_width = int(filter_5d.shape[0])
filter_height = int(filter_5d.shape[1])
filter_depth = int(filter_5d.shape[2])

input_4d   = tf.reshape(in_4d, [1, in_depth, in_height, in_width, in_channels])
kernel_5d = tf.reshape(filter_5d, [filter_depth, filter_height, filter_width, in_channels, out_channels])

output_4d = tf.nn.conv3d(input_4d, kernel_5d, strides=strides_3d, padding='SAME')
print sess.run(output_4d)

sess.close()

Wejście i wyjście w Tensorflow

wprowadź opis obrazu tutaj

wprowadź opis obrazu tutaj

Podsumowanie

wprowadź opis obrazu tutaj


18
Biorąc pod uwagę twoją pracę i przejrzystość wyjaśnień, głosy za 8 to za mało.
user3282777

3
Konw. 2d z wejściem 3d to miły akcent. Sugerowałbym edycję obejmującą konw. 1d z wejściem 2d (np. Tablica wielokanałowa) i porównanie ich różnicy z konw. 2d z wejściem 2d.
SumNeuron

2
Niesamowita odpowiedź!
Ben

Dlaczego kierunek konw. Wynosi 2d ↲. Widziałem źródła, które twierdzą, że kierunek jest dla wiersza 1, a następnie dla wiersza 1+stride. Samo splot jest niezmienny, więc dlaczego kierunek splotu ma znaczenie?
Minh Triet

Dziękuję za Twoje pytanie. Tak! sam splot jest niezmienny względem przesunięcia. więc dla obliczeń kierunek konw nie ma znaczenia (możesz obliczyć 2d konw. z dwoma dużymi mnożeniem macierzy. Framework caffe już to zrobił), ale dla zrozumienia lepiej wyjaśnić to za pomocą kierunku konw. ponieważ konw. 2d z wejściem 3d jest myląca bez kierunku. ^^
runhani

6

Podążając za odpowiedzią @runhani, dodaję kilka dodatkowych szczegółów, aby wyjaśnienie było nieco bardziej przejrzyste i spróbuję to wyjaśnić nieco dokładniej (i oczywiście z przykładami z TF1 i TF2).

Jednym z głównych dodatkowych elementów, które dołączam, są:

  • Nacisk na aplikacje
  • Użycie tf.Variable
  • Bardziej przejrzyste wyjaśnienie wejść / jąder / wyjść splot 1D / 2D / 3D
  • Efekty kroku / wyściółki

Konwolucja 1D

Oto jak możesz wykonać splot 1D używając TF 1 i TF 2.

Mówiąc konkretnie, moje dane mają następujące kształty,

  • Wektor 1D - [batch size, width, in channels](np. 1, 5, 1)
  • Jądro - [width, in channels, out channels] (np. 5, 1, 4)
  • Wynik - [batch size, width, out_channels] (np. 1, 5, 4)

Przykład TF1

import tensorflow as tf
import numpy as np

inp = tf.placeholder(shape=[None, 5, 1], dtype=tf.float32)
kernel = tf.Variable(tf.initializers.glorot_uniform()([5, 1, 4]), dtype=tf.float32)
out = tf.nn.conv1d(inp, kernel, stride=1, padding='SAME')

with tf.Session() as sess:
  tf.global_variables_initializer().run()
  print(sess.run(out, feed_dict={inp: np.array([[[0],[1],[2],[3],[4]],[[5],[4],[3],[2],[1]]])}))

Przykład TF2

import tensorflow as tf
import numpy as np

inp = np.array([[[0],[1],[2],[3],[4]],[[5],[4],[3],[2],[1]]]).astype(np.float32)
kernel = tf.Variable(tf.initializers.glorot_uniform()([5, 1, 4]), dtype=tf.float32)
out = tf.nn.conv1d(inp, kernel, stride=1, padding='SAME')
print(out)

To znacznie mniej pracy z TF2, ponieważ TF2 nie potrzebuje Sessioni variable_initializerna przykład.

Jak to mogłoby wyglądać w prawdziwym życiu?

Zrozummy więc, co to robi, na przykładzie wygładzania sygnału. Po lewej stronie masz oryginał, a po prawej wyjście Convolution 1D z 3 kanałami wyjściowymi.

wprowadź opis obrazu tutaj

Co oznacza wiele kanałów?

Wiele kanałów to w zasadzie wiele reprezentacji funkcji wejścia. W tym przykładzie masz trzy reprezentacje uzyskane przez trzy różne filtry. Pierwszy kanał to jednakowo ważony filtr wygładzający. Drugi to filtr, który waży środek filtru bardziej niż granice. Ostatni filtr działa odwrotnie niż drugi. Możesz więc zobaczyć, jak te różne filtry wywołują różne efekty.

Aplikacje uczenia głębokiego splotu 1D

Zwój 1D został pomyślnie użyty do zadania klasyfikacji zdań .

Konwolucja 2D

Wyłącz do splotu 2D. Jeśli jesteś osobą uczącą się głęboko, prawdopodobieństwo, że nie spotkałeś się z konwolucją 2D, wynosi… cóż, zero. Jest używany w CNN do klasyfikacji obrazów, wykrywania obiektów itp., A także w problemach NLP, które obejmują obrazy (np. Generowanie podpisów do obrazów).

Spróbujmy na przykładzie, mam jądro konwolucji z następującymi filtrami tutaj,

  • Jądro wykrywania krawędzi (okno 3x3)
  • Blur kernel (okno 3x3)
  • Wyostrz jądro (okno 3x3)

Mówiąc konkretnie, moje dane mają następujące kształty,

  • Obraz (czarno-biały) - [batch_size, height, width, 1](np. 1, 340, 371, 1)
  • Jądro (aka filtry) - [height, width, in channels, out channels](np. 3, 3, 1, 3)
  • Dane wyjściowe (czyli mapy obiektów) - [batch_size, height, width, out_channels](np. 1, 340, 371, 3)

Przykład TF1,

import tensorflow as tf
import numpy as np
from PIL import Image

im = np.array(Image.open(<some image>).convert('L'))#/255.0

kernel_init = np.array(
    [
     [[[-1, 1.0/9, 0]],[[-1, 1.0/9, -1]],[[-1, 1.0/9, 0]]],
     [[[-1, 1.0/9, -1]],[[8, 1.0/9,5]],[[-1, 1.0/9,-1]]],
     [[[-1, 1.0/9,0]],[[-1, 1.0/9,-1]],[[-1, 1.0/9, 0]]]
     ])

inp = tf.placeholder(shape=[None, image_height, image_width, 1], dtype=tf.float32)
kernel = tf.Variable(kernel_init, dtype=tf.float32)
out = tf.nn.conv2d(inp, kernel, strides=[1,1,1,1], padding='SAME')

with tf.Session() as sess:
  tf.global_variables_initializer().run()
  res = sess.run(out, feed_dict={inp: np.expand_dims(np.expand_dims(im,0),-1)})

Przykład TF2

import tensorflow as tf
import numpy as np
from PIL import Image

im = np.array(Image.open(<some image>).convert('L'))#/255.0
x = np.expand_dims(np.expand_dims(im,0),-1)

kernel_init = np.array(
    [
     [[[-1, 1.0/9, 0]],[[-1, 1.0/9, -1]],[[-1, 1.0/9, 0]]],
     [[[-1, 1.0/9, -1]],[[8, 1.0/9,5]],[[-1, 1.0/9,-1]]],
     [[[-1, 1.0/9,0]],[[-1, 1.0/9,-1]],[[-1, 1.0/9, 0]]]
     ])

kernel = tf.Variable(kernel_init, dtype=tf.float32)

out = tf.nn.conv2d(x, kernel, strides=[1,1,1,1], padding='SAME')

Jak to mogłoby wyglądać w prawdziwym życiu?

Tutaj możesz zobaczyć dane wyjściowe wygenerowane przez powyższy kod. Pierwszy obraz jest oryginalny i zgodnie z ruchem wskazówek zegara masz wyjścia pierwszego filtra, drugiego filtra i trzeciego filtra. wprowadź opis obrazu tutaj

Co oznacza wiele kanałów?

W kontekście splotu 2D dużo łatwiej jest zrozumieć, co oznaczają te wielokrotne kanały. Powiedz, że robisz rozpoznawanie twarzy. Możesz sobie wyobrazić (jest to bardzo nierealistyczne uproszczenie, ale ma sens) każdy filtr reprezentuje oko, usta, nos itp. Tak więc każda mapa cech byłaby binarną reprezentacją tego, czy ta cecha znajduje się na dostarczonym obrazie . Myślę, że nie muszę podkreślać, że dla modelu rozpoznawania twarzy są to bardzo cenne cechy. Więcej informacji w tym artykule .

To jest ilustracja tego, co próbuję wyrazić.

wprowadź opis obrazu tutaj

Zastosowania głębokiego uczenia splotu 2D

Konwolucja 2D jest bardzo rozpowszechniona w dziedzinie uczenia głębokiego.

CNN (Convolution Neural Networks) używają operacji splotu 2D do prawie wszystkich zadań widzenia komputerowego (np. Klasyfikacja obrazu, wykrywanie obiektów, klasyfikacja wideo).

Konwolucja 3D

Teraz coraz trudniej jest zilustrować, co się dzieje wraz ze wzrostem liczby wymiarów. Ale mając dobre zrozumienie, jak działa splot 1D i 2D, bardzo łatwo jest uogólnić to zrozumienie na splot 3D. Więc oto idzie.

Mówiąc konkretnie, moje dane mają następujące kształty,

  • Dane 3D (LIDAR) - [batch size, height, width, depth, in channels](np. 1, 200, 200, 200, 1)
  • Jądro -[height, width, depth, in channels, out channels] (np. 5, 5, 5, 1, 3)
  • Wyjście -[batch size, width, height, width, depth, out_channels] (np. 1, 200, 200, 2000, 3)

Przykład TF1

import tensorflow as tf
import numpy as np

tf.reset_default_graph()

inp = tf.placeholder(shape=[None, 200, 200, 200, 1], dtype=tf.float32)
kernel = tf.Variable(tf.initializers.glorot_uniform()([5,5,5,1,3]), dtype=tf.float32)
out = tf.nn.conv3d(inp, kernel, strides=[1,1,1,1,1], padding='SAME')

with tf.Session() as sess:
  tf.global_variables_initializer().run()
  res = sess.run(out, feed_dict={inp: np.random.normal(size=(1,200,200,200,1))})

Przykład TF2

import tensorflow as tf
import numpy as np

x = np.random.normal(size=(1,200,200,200,1))
kernel = tf.Variable(tf.initializers.glorot_uniform()([5,5,5,1,3]), dtype=tf.float32)
out = tf.nn.conv3d(x, kernel, strides=[1,1,1,1,1], padding='SAME') 

Aplikacje uczenia głębokiego splotu 3D

Konwolucja 3D została wykorzystana podczas tworzenia aplikacji uczenia maszynowego wykorzystujących dane LIDAR (Light Detection and Ranging), które mają charakter trójwymiarowy.

Co ... więcej żargonu ?: krok i wyściółka

W porządku, już prawie jesteś. Więc trzymaj się. Zobaczmy, czym jest krok i wyściółka. Są dość intuicyjne, jeśli się nad nimi zastanowić.

Jeśli przejdziesz przez korytarz, dotrzesz tam szybciej, wykonując mniejszą liczbę kroków. Ale oznacza to również, że obserwowałeś mniejsze otoczenie, niż gdybyś przeszedł przez pokój. Wzmocnijmy teraz nasze zrozumienie ładnym obrazkiem! Rozumiemy to poprzez splot 2D.

Zrozumienie kroku

Krok splotu

Kiedy używasz tf.nn.conv2dna przykład, musisz ustawić go jako wektor 4 elementów. Nie ma powodu, aby się tym onieśmielać. Po prostu zawiera kroki w następującej kolejności.

  • Konwolucja 2D - [batch stride, height stride, width stride, channel stride]. Tutaj, batch stride i channel stride, ustawiłeś tylko jeden (wdrażam modele głębokiego uczenia się od 5 lat i nigdy nie musiałem ustawiać ich na nic poza jednym). Masz więc tylko 2 kroki do ustawienia.

  • Konwolucja 3D - [batch stride, height stride, width stride, depth stride, channel stride]. Tutaj martwisz się tylko krokami wysokości / szerokości / głębokości.

Zrozumienie dopełnienia

Teraz zauważysz, że bez względu na to, jak mały jest twój krok (tj. 1), podczas splotu następuje nieuniknione zmniejszenie wymiarów (np. Szerokość wynosi 3 po skręceniu obrazu o szerokości 4 jednostek). Jest to niepożądane zwłaszcza przy budowaniu głębokich konwolucyjnych sieci neuronowych. Tutaj na ratunek przychodzi wyściółka. Istnieją dwa najczęściej używane typy wypełnienia.

  • SAME i VALID

Poniżej możesz zobaczyć różnicę.

wprowadź opis obrazu tutaj

Ostatnie słowo : jeśli jesteś bardzo ciekawy, możesz się zastanawiać. Właśnie zrzuciliśmy bombę na całą automatyczną redukcję wymiarów, a teraz rozmawiamy o różnych krokach. Ale najlepszą rzeczą w kroku jest to, że kontrolujesz, gdzie i jak zmniejszają się wymiary.


2
  1. CNN 1D, 2D lub 3D odnosi się do kierunku splotu, a nie do wymiaru wejściowego lub filtra.

  2. Dla wejścia 1-kanałowego CNN2D równa się CNN1D to długość jądra = długość wejścia. (1 kierunek konw.)


2

Podsumowując, w 1D CNN jądro porusza się w jednym kierunku. Dane wejściowe i wyjściowe 1D CNN są dwuwymiarowe. Używany głównie w przypadku danych szeregów czasowych.

W 2D CNN jądro porusza się w 2 kierunkach. Dane wejściowe i wyjściowe 2D CNN są trójwymiarowe. Używany głównie w przypadku danych obrazu.

W 3D CNN jądro porusza się w 3 kierunkach. Dane wejściowe i wyjściowe 3D CNN są 4-wymiarowe. Stosowany głównie w przypadku danych obrazu 3D (skany MRI, CT).

Więcej szczegółów znajdziesz tutaj: https://medium.com/@xzz201920/conv1d-conv2d-and-conv3d-8a59182c4d6

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.