Czym są warstwy dekonwolucyjne?


188

Niedawno przeczytałem w pełni sieci konwergentne dla segmentacji semantycznej autorstwa Jonathana Longa, Evana Shelhamera, Trevora Darrella. Nie rozumiem, co robią „warstwy dekonwolucyjne” / jak działają.

Odpowiednia część to

3.3 Upsampling jest konwertowanym krokiem wstecz

Innym sposobem łączenia gruboziarnistych wyjść z gęstymi pikselami jest interpolacja. Na przykład prosta interpolacja dwuliniowa oblicza każde wyjście z najbliższych czterech danych wejściowych za pomocą mapy liniowej, która zależy tylko od względnych pozycji komórek wejściowych i wyjściowych. W pewnym sensie upsampling ze współczynnikiem jest splotem z ułamkiem wejściowym kroku 1 / f. Tak długo, jak jest całką, naturalnym sposobem na upsamplowanie jest zatem splot wsteczny (czasami nazywany dekonwolucją) z krokiem wyjściowym . Taka operacja jest trywialna do wdrożenia, ponieważ po prostu odwraca postępy splotu do przodu i do tyłu. f f fyij
fff
W ten sposób upsampling wykonywany jest w sieci w celu kompleksowego uczenia się przez propagację wsteczną po utracie pikseli.
Zauważ, że filtr dekonwolucji w takiej warstwie nie musi być ustalony (np. Do dwuliniowego upsamplingu), ale można się go nauczyć. Stos warstw dekonwolucji i funkcji aktywacyjnych może nawet nauczyć się nieliniowego próbkowania w górę.
W naszych eksperymentach okazało się, że upsampling wewnątrz sieci jest szybki i skuteczny w nauce gęstego przewidywania. Nasza najlepsza architektura segmentacji wykorzystuje te warstwy do nauki próbkowania w celu uzyskania dokładniejszych prognoz w Rozdziale 4.2.

Nie sądzę, że naprawdę zrozumiałem, w jaki sposób trenowane są warstwy splotowe.

Wydaje mi się, że zrozumiałem, że warstwy splotowe z wielkością jądra uczą się filtrów wielkości . Dane wyjściowe warstwy splotowej o rozmiarze jądra , kroku i filtrach mają wymiary . Nie wiem jednak, jak działa uczenie się warstw splotowych. (Rozumiem, jak proste MLP uczą się z opadaniem gradientu, jeśli to pomaga).k × k k s N n Wejście dimkk×kksNnInput dims2n

Więc jeśli moje rozumienie warstw splotowych jest prawidłowe, nie mam pojęcia, jak można to odwrócić.

Czy ktoś mógłby mi pomóc zrozumieć warstwy dekonwolucyjne?


3
Ten wykład wideo wyjaśnia dekonwolucji / upsampling: youtu.be/ByjaPdWXKJ4?t=16m59s
user199309

6
Mając nadzieję, że może się przydać każdemu, stworzyłem notatnik, aby zbadać, w jaki sposób splot i transponowany splot można zastosować w TensorFlow (0.11). Być może posiadanie praktycznych przykładów i liczb może nieco pomóc zrozumieć, jak działają.
AkiRoss,

1
Dla mnie ta strona dostarczyła mi lepszego wyjaśnienia, wyjaśnia także różnicę między dekonwolucją a transpozycją splotu: towardsdatascience.com/…
T.Antoni

Czy próbkowanie w górę nie przypomina bardziej buforowania wstecznego niż splotu krokowego wstecznego, ponieważ nie ma parametrów?
Ken Fehling,

Uwaga: Nazwa „warstwa dekonwolucyjna” wprowadza w błąd, ponieważ ta warstwa nie dokonuje dekonwolucji .
user76284

Odpowiedzi:


210

Warstwa dekonwolucji jest bardzo niefortunną nazwą i należy ją raczej nazwać transponowaną warstwą splotową .

Wizualnie, dla transponowanego splotu z krokiem pierwszym i bez dopełniania, po prostu wypełniamy oryginalne wejście (niebieskie wpisy) zerami (białe wpisy) (Rysunek 1).

Rycina 1

W przypadku kroku drugiego i wypełnienia transponowany splot wyglądałby tak (Ryc. 2):

Rysunek 2

Można znaleźć więcej (świetnych) wizualizacji arytmetyki splotowej tutaj .


16
Żeby się upewnić, że to rozumiem: „Dekonwolucja” jest prawie taka sama jak konwekcja, ale dodajesz trochę wypełnienia? (Wokół obrazu / kiedy s> 1 również wokół każdego piksela)?
Martin Thoma,

17
Tak, warstwa dekonwolucji wykonuje również splot! Właśnie dlatego transponowany splot pasuje o wiele lepiej niż nazwa, a termin „dekonwolucja” jest w rzeczywistości mylący.
David Dao

11
Dlaczego na rycinie 1 mówisz „bez wypełnienia”, jeśli w rzeczywistości wprowadzanie jest zerowane?
Stas S

8
Nawiasem mówiąc: W TensorFlow nazywa się to teraz transponowaniem splotu: tensorflow.org/versions/r0.10/api_docs/python/…
Martin Thoma

9
Dzięki za tę bardzo intuicyjną odpowiedź, ale nie jestem pewien, dlaczego drugi to przypadek „kroku drugiego”, zachowuje się dokładnie tak samo jak pierwszy, gdy jądro się porusza.
Demonedge,

49

Myślę, że jednym ze sposobów uzyskania naprawdę podstawowej intuicji na poziomie splotu jest przesuwanie filtrów K, które można traktować jak szablony K, nad obrazem wejściowym i wytwarzanie aktywacji K - każdy reprezentuje stopień dopasowania do określonego szablonu . Odwrotną operacją tego byłoby wzięcie aktywacji K i rozwinięcie ich w preimage operacji splotu. Intuicyjnym wyjaśnieniem operacji odwrotnej jest zatem z grubsza rekonstrukcja obrazu przy uwzględnieniu szablonów (filtrów) i aktywacji (stopień dopasowania dla każdego szablonu), a zatem na podstawowym poziomie intuicyjnym chcemy wysadzić każdą aktywację za pomocą maski szablonu i dodaj je.

Innym sposobem podejścia do zrozumienia deconv byłoby zbadanie implementacji warstwy dekonwolucji w Caffe, zobacz następujące odpowiednie fragmenty kodu:

DeconvolutionLayer<Dtype>::Forward_gpu
ConvolutionLayer<Dtype>::Backward_gpu
CuDNNConvolutionLayer<Dtype>::Backward_gpu
BaseConvolutionLayer<Dtype>::backward_cpu_gemm

Widać, że jest on zaimplementowany w Caffe dokładnie tak, jak backprop dla zwykłej naprzód warstwy splotowej (dla mnie było to bardziej oczywiste po tym, jak porównałem implementację backprop w warstwie cuDNN vs ConvolutionLayer :: Backward_gpu zaimplementowane przy użyciu GEMM). Jeśli więc przeanalizujesz, w jaki sposób wykonuje się propagację wsteczną dla regularnego splotu, zrozumiesz, co dzieje się na poziomie obliczeń mechanicznych. Sposób, w jaki działa to obliczenie, odpowiada intuicji opisanej w pierwszym akapicie tej notki.

Nie wiem jednak, jak działa uczenie się warstw splotowych. (Rozumiem, jak proste MLP uczą się z opadaniem gradientu, jeśli to pomaga).

Aby odpowiedzieć na inne pytanie w pierwszym pytaniu, istnieją dwie główne różnice między propagacją wsteczną MLP (warstwa w pełni połączona) a sieciami splotowymi:

1) wpływ wag jest zlokalizowany, więc najpierw wymyśl, jak zrobić backprop, powiedzmy, filtr 3x3 splątany z małym obszarem 3x3 obrazu wejściowego, odwzorowując go do pojedynczego punktu na obrazie wynikowym.

2) wagi filtrów splotowych są wspólne dla niezmienności przestrzennej. W praktyce oznacza to, że w przebiegu do przodu ten sam filtr 3x3 o tych samych wagach jest przeciągany przez cały obraz z tymi samymi wagami do obliczeń w przód w celu uzyskania obrazu wyjściowego (dla tego konkretnego filtra). Oznacza to, że gradienty wsteczne dla każdego punktu na obrazie źródłowym są sumowane w całym zakresie, który przeciągnęliśmy ten filtr podczas przejścia do przodu. Zauważ, że istnieją również różne gradienty strat wrt x, w i stronniczości, ponieważ dLoss / dx wymaga uprzedniej propagacji, a dLoss / dw to sposób, w jaki aktualizujemy wagi. w i stronniczość są niezależnymi danymi wejściowymi w obliczeniach DAG (nie ma wcześniejszych danych wejściowych), więc nie ma potrzeby przeprowadzania na nich propagacji wstecznej.

(my notation here assumes that convolution is y = x*w+b where '*' is the convolution operation)

7
Myślę, że to najlepsza odpowiedź na to pytanie.
kli_nlpr,

8
Zgadzam się, że to najlepsza odpowiedź. Najlepsza odpowiedź ma ładne animacje, ale dopóki jej nie przeczytałem, wyglądały jak zwykłe zwoje z jakimś arbitralnym wypełnieniem. Och, jak ludzie są oczarowani słodyczami.
Reii Nakano,

1
Zgadzam się, przyjęta odpowiedź niczego nie wyjaśniła. To jest dużo lepsze.
BjornW,

Dziękuję za wspaniałe wyjaśnienie. Obecnie nie mogę wymyślić, jak prawidłowo wykonać backprop. Czy mógłbyś mi podpowiedzieć?
Bastian,

33

Matematyka krok po kroku wyjaśniająca, w jaki sposób transpozycja splotu robi 2x upsampling z filtrem 3x3 i krok 2:

wprowadź opis zdjęcia tutaj

Najprostszy fragment kodu TensorFlow do sprawdzania poprawności matematyki:

import tensorflow as tf
import numpy as np

def test_conv2d_transpose():
    # input batch shape = (1, 2, 2, 1) -> (batch_size, height, width, channels) - 2x2x1 image in batch of 1
    x = tf.constant(np.array([[
        [[1], [2]], 
        [[3], [4]]
    ]]), tf.float32)

    # shape = (3, 3, 1, 1) -> (height, width, input_channels, output_channels) - 3x3x1 filter
    f = tf.constant(np.array([
        [[[1]], [[1]], [[1]]], 
        [[[1]], [[1]], [[1]]], 
        [[[1]], [[1]], [[1]]]
    ]), tf.float32)

    conv = tf.nn.conv2d_transpose(x, f, output_shape=(1, 4, 4, 1), strides=[1, 2, 2, 1], padding='SAME')

    with tf.Session() as session:
        result = session.run(conv)

    assert (np.array([[
        [[1.0], [1.0],  [3.0], [2.0]],
        [[1.0], [1.0],  [3.0], [2.0]],
        [[4.0], [4.0], [10.0], [6.0]],
        [[3.0], [3.0],  [7.0], [4.0]]]]) == result).all()

Myślę, że twoje obliczenia są tutaj błędne. Wyjście pośrednie powinno wynosić 3+ 2 * 2 = 7, a następnie dla jądra 3x3 ostateczne wyjście powinno wynosić 7-3 + 1 = 5x5
Alex

Przepraszam, @Alex, ale nie rozumiem, dlaczego wynik pośredni wynosi 7. Czy możesz to rozwinąć?
andriys,

2
@andriys Na obrazie, który pokazałeś, dlaczego przycięto ostateczny wynik?
James Bond

28

Te notatki, które towarzyszą Stanford CS klasy CS231n : splotowego sieci neuronowych do wizualnego rozpoznawania, Andrej Karpathy , zrobić doskonałą pracę wyjaśniając splotowych sieci neuronowych.

Czytanie tego artykułu powinno dać ci ogólny pogląd na:

  • Deconvolutional Networks Matthew D. Zeiler, Dilip Krishnan, Graham W. Taylor i Rob Fergus Dept. of Computer Science, Courant Institute, New York University

Te slajdy są idealne dla sieci Deconvolutional.


29
Czy w skrócie można streścić treść któregokolwiek z tych linków? Linki mogą być przydatne do dalszych badań, ale idealnie odpowiedź wymiany stosu powinna zawierać wystarczającą ilość tekstu, aby odpowiedzieć na podstawowe pytanie bez konieczności opuszczania witryny.
Neil Slater,

Przykro mi, ale treść tych stron jest zbyt duża, aby streścić ją w krótkim akapicie.
Azrael,

12
Pełne streszczenie nie jest wymagane, a jedynie nagłówek - np. „Dekonwolucyjna sieć neuronowa jest podobna do CNN, ale jest tak wyszkolona, ​​że ​​funkcje w dowolnej ukrytej warstwie można wykorzystać do odtworzenia poprzedniej warstwy (i poprzez powtarzanie między warstwami, ostatecznie dane wejściowe można zrekonstruować na podstawie danych wyjściowych). Dzięki temu można je trenować bez nadzoru w celu nauczenia się ogólnych funkcji wysokiego poziomu w dziedzinie problemowej - zwykle przetwarzania obrazu ”(uwaga: nie jestem nawet pewien, czy jest to poprawne, dlatego nie piszę własna odpowiedź).
Neil Slater,

6
Chociaż linki są dobre, krótkie streszczenie modelu własnymi słowami byłoby lepsze.
SmallChess,

11

Właśnie znalazłem świetny artykuł na stronie theaon na ten temat [1]:

Potrzeba transponowania zwojów zasadniczo wynika z chęci zastosowania transformacji zmierzającej w przeciwnym kierunku niż normalny splot, [...] do projekcji map obiektów do przestrzeni o wyższych wymiarach. [...] tj. odwzoruj z przestrzeni 4-wymiarowej na przestrzeń 16-wymiarową, zachowując wzór połączenia splotu.

Transponowane zwoje - zwane także zwojami ułamkowymi - działają poprzez zamianę przednich i tylnych przebiegów splotu. Jednym ze sposobów jest zwrócenie uwagi, że jądro definiuje splot, ale to, czy jest to splot bezpośredni, czy transponowany, zależy od tego, jak obliczane są przejścia do przodu i do tyłu.

Transponowaną operację splotu można traktować jako gradient jakiegoś splotu w odniesieniu do jej wkładu, co zwykle jest sposobem implementacji transponowanych splotów w praktyce.

Na koniec zauważ, że zawsze możliwe jest wdrożenie transponowanego splotu z bezpośrednim splotem. Wadą jest to, że zwykle wymaga dodania wielu kolumn i wierszy zer do danych wejściowych, co powoduje znacznie mniej wydajną implementację.

Mówiąc w skrócie, „transponowany splot” jest operacją matematyczną z wykorzystaniem macierzy (podobnie jak splot), ale jest bardziej wydajny niż normalna operacja splotu w przypadku, gdy chcesz powrócić od splotu wartości do pierwotnego (przeciwnego kierunku). Dlatego w implementacjach preferuje się splot podczas obliczania kierunku przeciwnego (tj. Aby uniknąć wielu niepotrzebnych zwielokrotnień 0 spowodowanych przez rzadką macierz wynikającą z wypełniania danych wejściowych).

Image ---> convolution ---> Result

Result ---> transposed convolution ---> "originalish Image"

Czasami zapisujesz niektóre wartości wzdłuż ścieżki splotu i ponownie używasz tych informacji podczas „powrotu”:

Result ---> transposed convolution ---> Image

To prawdopodobnie powód, dla którego błędnie nazywa się to „dekonwolucją”. Ma to jednak coś wspólnego z transponowaniem macierzy splotu (C ^ T), stąd bardziej odpowiednia nazwa „transpozycja splotu”.

Rozważanie kosztów obliczeniowych ma więc sens. Za amazon gpus zapłaciłbyś dużo więcej, gdybyś nie używał transponowanego splotu.

Przeczytaj uważnie animacje tutaj: http://deeplearning.net/software/theano_versions/dev/tutorial/conv_arithmetic.html#no-zero-padding-unit-strides-transposed

Kilka innych istotnych lektur:

Transpozycja (lub bardziej ogólnie transpozycja hermitowska lub sprzężona) filtra jest po prostu dopasowanym filtrem [3]. Stwierdzono to poprzez czasowe odwrócenie jądra i pobranie koniugatu wszystkich wartości [2].

Jestem również nowy w tym względzie i byłbym wdzięczny za wszelkie opinie lub poprawki.

[1] http://deeplearning.net/software/theano_versions/dev/tutorial/conv_arithmetic.html

[2] http://deeplearning.net/software/theano_versions/dev/tutorial/conv_arithmetic.html#transposed-convolution-arithmetic

[3] https://en.wikipedia.org/wiki/Mched_filter


1
Zbieranie gnidów
Herbert

1
Myślę, że to najlepsza odpowiedź !!!
kli_nlpr

10

Przydałoby się PCA do analogii.

W przypadku korzystania z konw. Przejście do przodu służy do wyodrębnienia współczynników składników zasadowych z obrazu wejściowego, a przejście do tyłu (które aktualizuje dane wejściowe) polega na wykorzystaniu (gradientu) współczynników do odtworzenia nowego obrazu wejściowego, tak aby nowy obraz wejściowy ma współczynniki PC, które lepiej pasują do pożądanych współczynników.

Podczas używania deconv przejście do przodu i przejście do tyłu są odwrócone. Przebieg do przodu próbuje zrekonstruować obraz ze współczynników PC, a przejście do tyłu aktualizuje współczynniki PC na podstawie (gradientu) obrazu.

Przełożenie do przodu deconv wykonuje dokładnie obliczenia gradientu konwekcyjnego podane w tym poście: http://andrew.gibiansky.com/blog/machine-learning/convolutional-neural-networks/

Właśnie dlatego w implementacji deconv w kofeinie (patrz odpowiedź Andrieja Pokrovsky'ego) przekazanie w przód deconv wywołuje backward_cpu_gemm (), a przekazywanie w tył wywołuje forward_cpu_gemm ().


6

Oprócz odpowiedzi Davida Dao: można również pomyśleć na odwrót. Zamiast skupiać się na tym, które piksele wejściowe (niskiej rozdzielczości) są używane do wytworzenia pojedynczego piksela wyjściowego, możesz również skupić się na tym, które poszczególne piksele wejściowe przyczyniają się do tego, który region pikseli wyjściowych.

Odbywa się to w tej destylowanej publikacji , w tym w serii bardzo intuicyjnych i interaktywnych wizualizacji. Jedną z zalet myślenia w tym kierunku jest to, że wyjaśnianie artefaktów szachownicy staje się łatwe.


5

Zwoje z perspektywy DSP

Trochę się spóźniłem, ale nadal chciałbym podzielić się moją perspektywą i spostrzeżeniami. Moje wykształcenie to fizyka teoretyczna i cyfrowe przetwarzanie sygnałów. W szczególności badałem falki i zwoje są prawie w moim kręgosłupie;)

Sposób, w jaki ludzie ze społeczności zajmującej się głębokim uczeniem się mówią o zwojach, również mnie dezorientował. Z mojej perspektywy wydaje się, że brakuje właściwego podziału problemów. Wyjaśnię splot głębokiego uczenia się przy użyciu niektórych narzędzi DSP.

Zrzeczenie się

Moje wyjaśnienia będą nieco faliste, a nie matematyczne rygorystyczne, aby uzyskać główne punkty.


Definicje

xn={xn}n=={,x1,x0,x1,}

ynxn

(yx)n=k=ynkxk

q=(q0,q1,q2)x=(x0,x1,x2,x3)T

qx=(q1q000q2q1q000q2q1q000q2q1)(x0x1x2x3)

kN

kxn=xnk

kk1

kxn={xn/kn/kZ0otherwise

k=3

3{,x0,x1,x2,x3,x4,x5,x6,}={,x0,x3,x6,}
3{,x0,x1,x2,}={x0,0,0,x1,0,0,x2,0,0,}

k=2

2x=(x0x2)=(10000010)(x0x1x2x3)

i

2x=(x00x10)=(10000100)(x0x1)

k=kT


Rozwinięcia dogłębnego uczenia się według części

qx

  • kk(qx)
  • k(kq)x
  • kq(kx)

q(kx)=q(kTx)=(k(q)T)Tx

(q)q

q(kx)=(q1q000q2q1q000q2q1q000q2q1)(10000100)(x0x1)=(q1q200q0q1q200q0q1q200q0q1)T(10000010)T(x0x1)=((10000010)(q1q200q0q1q200q0q1q200q0q1))T(x0x1)=(k(q)T)Tx

Jak widać, transponowana jest operacja, stąd nazwa.

Połączenie z próbkowaniem do najbliższego sąsiada

2(11)xq2(11)qxq=(q0q1q2)

(11)q=(q0q0+q1q1+q2q2),

tzn. możemy zastąpić powtarzający się upsampler współczynnikiem 2 i splot jądrem o rozmiarze 3 transponowanym splotem o rozmiarze jądra 4. Ten transponowany splot ma tę samą „zdolność interpolacji”, ale byłby w stanie nauczyć się lepiej dopasowanych interpolacji.


Wnioski i uwagi końcowe

Mam nadzieję, że uda mi się wyjaśnić niektóre powszechne sploty, które można znaleźć w głębokim uczeniu się, dzieląc je na części w podstawowych operacjach.

Nie omawiałem tu basenów. Jest to jednak nieliniowy próbnik próbkujący w dół i można go również traktować w ramach tej notacji.


Doskonała odpowiedź. Przyjęcie perspektywy matematycznej / symbolicznej często wyjaśnia sprawy. Czy mam rację sądząc, że termin „dekonwolucja” w tym kontekście koliduje z istniejącą terminologią ?
user76284

To tak naprawdę nie koliduje, po prostu nie ma sensu. Dekonwolucja to po prostu splot z operatorem próbkowania. Termin „dekonwolucja” wydaje się być jakąś formą operacji odwrotnej. Mówienie tutaj o odwrotności ma sens tylko w kontekście operacji macierzowych. Mnoży się przez macierz odwrotną, a nie odwrotną operację splotu (jak dzielenie vs mnożenie).
André Bergner

zθx=zzθz=x

θz=xz=(θ)+x

Krótko mówiąc, tak zwana „warstwa dekonwolucyjna” OP faktycznie nie dokonuje dekonwolucji. Robi coś innego (co opisałeś w swojej odpowiedzi).
user76284

4

Miałem wiele problemów ze zrozumieniem, co dokładnie wydarzyło się w gazecie, dopóki nie natknąłem się na ten post na blogu: http://warmspringwinds.github.io/tensorflow/tf-slim/2016/11/22/upsampling-and-image-segmentation -z -tensorflow-and-tf-slim /

Oto podsumowanie tego, jak rozumiem, co dzieje się w 2x próbkowaniu w górę:

Informacje z papieru

  • Co to jest upsampling?
  • Jakie są parametry tego splotu?
  • Czy odważniki są stałe, czy można je trenować?
    • Artykuł stwierdza: „inicjalizujemy 2x upsampling do interpolacji dwuliniowej, ale pozwalamy na naukę parametrów [...]”.
    • Jednak odpowiednia strona github stwierdza: „W naszych oryginalnych eksperymentach warstwy interpolacji zostały zainicjowane na dwuliniowe jądra, a następnie wyuczone. W eksperymentach następczych i tej implementacji referencyjnej dwuliniowe jądra są naprawione”
    • → ustalone ciężary

Prosty przykład

  1. wyobraź sobie następujący obraz wejściowy:

Wprowadź obraz

  1. Frakcje ułożone krokowo działają poprzez wstawienie współczynnika-1 = 2-1 = 1 zera między tymi wartościami, a następnie przyjęcie kroku = 1 później. W ten sposób otrzymujesz następujący wyściełany obraz 6x6

wyściełany obraz

  1. Dwuliniowy filtr 4x4 wygląda następująco. Jego wartości dobiera się w taki sposób, aby użyte masy (= wszystkie masy nie pomnożone przez wstawione zero) sumowały się do 1. Jego trzy unikalne wartości to 0,56, 0,19 i 0,06. Co więcej, środek filtra to zgodnie z konwencją piksel w trzecim rzędzie i trzeciej kolumnie.

filtr

  1. Zastosowanie filtru 4x4 na wyściełanym obrazie (używając padding = „same” i stride = 1) daje następujący obraz próbkowany w górę 6x6:

Skalowany obraz

  1. Ten rodzaj próbkowania w górę jest wykonywany dla każdego kanału osobno (patrz wiersz 59 w https://github.com/shelhamer/fcn.berkeleyvision.org/blob/master/surgery.py ). Na koniec, 2x upsampling to naprawdę bardzo prosta zmiana rozmiaru przy użyciu interpolacji dwuliniowej i konwencji dotyczących obsługi granic. Uważam, że upsampling 16x lub 32x działa w podobny sposób.

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.