PyTorch - ciągły ()


90

Przeglądałem ten przykład modelu języka LSTM na github (link) . Ogólnie rzecz biorąc, jest dla mnie całkiem jasne. Ale wciąż staram się zrozumieć, co contiguous()robi wywołanie , co występuje kilka razy w kodzie.

Na przykład w linii 74/75 kodu wejściowego i sekwencji docelowej LSTM są tworzone. Dane (przechowywane w ids) są dwuwymiarowe, gdzie pierwszy wymiar to rozmiar partii.

for i in range(0, ids.size(1) - seq_length, seq_length):
    # Get batch inputs and targets
    inputs = Variable(ids[:, i:i+seq_length])
    targets = Variable(ids[:, (i+1):(i+1)+seq_length].contiguous())

A więc jako prosty przykład, gdy używasz rozmiaru partii 1 i seq_length10 inputsi targetswygląda to tak:

inputs Variable containing:
0     1     2     3     4     5     6     7     8     9
[torch.LongTensor of size 1x10]

targets Variable containing:
1     2     3     4     5     6     7     8     9    10
[torch.LongTensor of size 1x10]

Więc ogólnie moje pytanie brzmi: co contiguous()i dlaczego tego potrzebuję?

Ponadto nie rozumiem, dlaczego metoda jest wywoływana dla sekwencji docelowej, a nie dla sekwencji wejściowej, ponieważ obie zmienne zawierają te same dane.

Jak mogłoby targetsbyć nieciągłe i inputsnadal przylegać do siebie?

EDYCJA: Próbowałem pominąć wywołanie contiguous(), ale prowadzi to do komunikatu o błędzie podczas obliczania straty.

RuntimeError: invalid argument 1: input is not contiguous at .../src/torch/lib/TH/generic/THTensor.c:231

Więc oczywiście wezwanie contiguous()w tym przykładzie jest konieczne.

(Aby zachować czytelność, unikałem publikowania tutaj pełnego kodu, można go znaleźć, korzystając z powyższego linku GitHub).

Z góry dziękuję!


przydatny byłby bardziej opisowy tytuł. Proponuję poprawić tytuł lub przynajmniej napisać tldr; to the point summaryzwięzłe do punktacji podsumowanie.
Charlie Parker


Odpowiedzi:


186

Jest kilka operacji na Tensor w PyTorch, które tak naprawdę nie zmieniają zawartości tensora, ale tylko przekształcają indeksy w tensor na lokalizację bajtów. Te operacje obejmują:

narrow(), view(), expand()Itranspose()

Na przykład: kiedy wywołujesz transpose(), PyTorch nie generuje nowego tensora z nowym układem, po prostu modyfikuje metainformacje w obiekcie Tensor, więc offset i stride są dla nowego kształtu. Transponowany tensor i oryginalny tensor rzeczywiście współużytkują pamięć!

x = torch.randn(3,2)
y = torch.transpose(x, 0, 1)
x[0, 0] = 42
print(y[0,0])
# prints 42

Tutaj pojawia się pojęcie ciągłości . Powyższe xjest ciągłe, ale ynie jest, ponieważ jego układ pamięci jest inny niż tensor o tym samym kształcie, wykonany od podstaw. Zauważ, że słowo „ciągły” jest nieco mylące, ponieważ nie chodzi o to, że zawartość tensora jest rozłożona wokół odłączonych bloków pamięci. Tutaj bajty są nadal przydzielane w jednym bloku pamięci, ale kolejność elementów jest inna!

Kiedy wywołujesz contiguous(), faktycznie tworzy kopię tensora, więc kolejność elementów byłaby taka sama, jak gdyby tensor o tym samym kształcie został utworzony od podstaw.

Zwykle nie musisz się tym martwić. Jeśli PyTorch oczekuje ciągłego tensora, ale jeśli nie, otrzymasz, RuntimeError: input is not contiguousa następnie po prostu dodaj wywołanie do contiguous().


Po prostu znowu to natknąłem się. Twoje wyjaśnienie jest bardzo dobre! Zastanawiam się tylko: jeśli bloki w pamięci nie są szeroko rozrzucone, jaki jest problem z układem pamięci, który jest „inny niż tensor o tym samym kształcie wykonany od podstaw” ? Dlaczego ciągłość jest wymagana tylko w przypadku niektórych operacji?
MBT

4
Nie mogę definitywnie odpowiedzieć na to pytanie, ale przypuszczam, że część kodu PyTorch wykorzystuje wysokowydajną wektoryzowaną implementację operacji zaimplementowanych w C ++, a ten kod nie może używać dowolnych przesunięć / kroków określonych w metadanych Tensora. To tylko przypuszczenie.
Shital Shah

1
Dlaczego odbiorca nie mógł po prostu zadzwonić contiguous()sam?
information_interchange

całkiem możliwe, ponieważ nie chcesz tego w ciągły sposób i zawsze dobrze jest mieć kontrolę nad tym, co robisz.
shivam13juna

2
Inną popularną operacją tensora jest permute, która również może zwrócić tensor nie „ciągły”.
Oleg

31

Z [dokumentacji ogrodniczej] [1]:

ciągły () → Tensor

Returns a contiguous tensor containing the same data as self 

napinacz. Jeśli tensor własny jest ciągły, funkcja zwraca tensor własny.

Gdzie contiguoustutaj oznacza nie tylko ciągłe w pamięci, ale także w tej samej kolejności w pamięci co kolejność indeksów: na przykład wykonanie transpozycji nie zmienia danych w pamięci, po prostu zmienia mapę z indeksów na wskaźniki pamięci, jeśli wtedy jego zastosowanie contiguous()spowoduje zmianę danych w pamięci, tak aby mapa z indeksów do lokalizacji pamięci była kanoniczna. [1]: http://pytorch.org/docs/master/tensors.html


1
Dziękuję za Twoją odpowiedź! Czy możesz mi powiedzieć, dlaczego / kiedy potrzebuję, aby dane były ciągłe? Czy to tylko wydajność, czy z jakiegoś innego powodu? Czy PyTorch wymaga ciągłych danych dla niektórych operacji? Dlaczego cele muszą być ciągłe, a dane wejściowe nie?
MBT

Służy tylko wydajności. Nie wiem, dlaczego kody robią to dla celów, ale nie dla danych wejściowych.
patapouf_ai

2
Więc najwyraźniej pytorch wymaga, aby cele w utracie były ciągłe w pamięci, ale wejścia sieci neuronowej nie muszą spełniać tego wymagania.
patapouf_ai

2
Wielkie dzięki! Myślę, że ma to dla mnie sens, zauważyłem, że ciągłość () jest również stosowana do danych wyjściowych (które były oczywiście wcześniej danymi wejściowymi) w funkcji forward, więc zarówno wyniki, jak i cele są ciągłe podczas obliczania straty. Wielkie dzięki!
MBT

1
@patapouf_ai Nie. Twoje wyjaśnienie jest nieprawidłowe. Jak wskazano w poprawnej odpowiedzi, w ogóle nie chodzi o ciągłe bloki pamięci.
Akaisteph7

14

tensor.contiguous () utworzy kopię tensora, a element w kopii zostanie zapisany w pamięci w sposób ciągły. Funkcja contiguous () jest zwykle wymagana, gdy najpierw transponujemy () tensor, a następnie zmieniamy jego kształt (wyświetlamy). Najpierw utwórzmy ciągły tensor:

aaa = torch.Tensor( [[1,2,3],[4,5,6]] )
print(aaa.stride())
print(aaa.is_contiguous())
#(3,1)
#True

Stride () return (3,1) oznacza, że: poruszając się po pierwszym wymiarze o każdy krok (rząd po rzędzie), musimy przejść 3 kroki w pamięci. Poruszając się po drugim wymiarze (kolumna po kolumnie), musimy przejść o 1 krok w pamięci. Oznacza to, że elementy w tensorze są przechowywane w sposób ciągły.

Teraz spróbujemy zastosować funkcje come do tensora:

bbb = aaa.transpose(0,1)
print(bbb.stride())
print(bbb.is_contiguous())

#(1, 3)
#False


ccc = aaa.narrow(1,1,2)   ## equivalent to matrix slicing aaa[:,1:3]
print(ccc.stride())
print(ccc.is_contiguous())

#(3, 1)
#False


ddd = aaa.repeat(2,1)   # The first dimension repeat once, the second dimension repeat twice
print(ddd.stride())
print(ddd.is_contiguous())

#(3, 1)
#True


## expand is different from repeat.
## if a tensor has a shape [d1,d2,1], it can only be expanded using "expand(d1,d2,d3)", which
## means the singleton dimension is repeated d3 times
eee = aaa.unsqueeze(2).expand(2,3,3)
print(eee.stride())
print(eee.is_contiguous())

#(3, 1, 0)
#False


fff = aaa.unsqueeze(2).repeat(1,1,8).view(2,-1,2)
print(fff.stride())
print(fff.is_contiguous())

#(24, 2, 1)
#True

Ok, możemy stwierdzić, że transpose (), narrow () i tensor slicing oraz expand () sprawią, że wygenerowany tensor nie będzie ciągły. Co ciekawe, powtarzanie () i widok () nie powodują, że jest on niejednoznaczny. Więc teraz pytanie brzmi: co się stanie, jeśli użyję nieciągłego tensora?

Odpowiedź brzmi: funkcja view () nie może być zastosowana do nieciągłego tensora. Dzieje się tak prawdopodobnie dlatego, że metoda view () wymaga, aby tensor był przechowywany w sposób ciągły, aby mógł szybko zmienić kształt w pamięci. na przykład:

bbb.view(-1,3)

otrzymamy błąd:

---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
<ipython-input-63-eec5319b0ac5> in <module>()
----> 1 bbb.view(-1,3)

RuntimeError: invalid argument 2: view size is not compatible with input tensor's size and stride (at least one dimension spans across two contiguous subspaces). Call .contiguous() before .view(). at /pytorch/aten/src/TH/generic/THTensor.cpp:203

Aby rozwiązać ten problem, po prostu dodaj ciągłe () do nieciągłego tensora, aby utworzyć ciągłą kopię, a następnie zastosuj widok ()

bbb.contiguous().view(-1,3)
#tensor([[1., 4., 2.],
        [5., 3., 6.]])

10

Podobnie jak w poprzedniej odpowiedzi, contigous () przydziela ciągłe fragmenty pamięci , przyda się, gdy przekazujemy tensor do kodu zaplecza c lub c ++, w którym tensory są przekazywane jako wskaźniki


3

Przyjęte odpowiedzi były tak świetne, że starałem się oszukać transpose()efekt funkcji. Stworzyłem dwie funkcje, które mogą sprawdzać samestorage()i contiguous.

def samestorage(x,y):
    if x.storage().data_ptr()==y.storage().data_ptr():
        print("same storage")
    else:
        print("different storage")
def contiguous(y):
    if True==y.is_contiguous():
        print("contiguous")
    else:
        print("non contiguous")

Sprawdziłem i otrzymałem ten wynik w postaci tabeli:

Funkcje

Możesz przejrzeć kod kontrolera poniżej, ale podajmy jeden przykład, kiedy tensor nie jest ciągły . Nie możemy po prostu wywołać view()tego tensora, potrzebowalibyśmy reshape()go lub moglibyśmy również wywołać .contiguous().view().

x = torch.randn(3,2)
y = x.transpose(0, 1)
y.view(6) # RuntimeError: view size is not compatible with input tensor's size and stride (at least one dimension spans across two contiguous subspaces). Use .reshape(...) instead.
  
x = torch.randn(3,2)
y = x.transpose(0, 1)
y.reshape(6)

x = torch.randn(3,2)
y = x.transpose(0, 1)
y.contiguous().view(6)

Co więcej, istnieją metody, które na końcu tworzą ciągłe i nieciągłe tensory. Istnieją metody, które mogą działać na tym samym magazynie , a niektóre z flip()nich utworzą nowy magazyn (czytaj: sklonuj tensor) przed powrotem.

Kod kontrolny:

import torch
x = torch.randn(3,2)
y = x.transpose(0, 1) # flips two axes
print("\ntranspose")
print(x)
print(y)
contiguous(y)
samestorage(x,y)

print("\nnarrow")
x = torch.randn(3,2)
y = x.narrow(0, 1, 2) #dim, start, len  
print(x)
print(y)
contiguous(y)
samestorage(x,y)

print("\npermute")
x = torch.randn(3,2)
y = x.permute(1, 0) # sets the axis order
print(x)
print(y)
contiguous(y)
samestorage(x,y)

print("\nview")
x = torch.randn(3,2)
y=x.view(2,3)
print(x)
print(y)
contiguous(y)
samestorage(x,y)

print("\nreshape")
x = torch.randn(3,2)
y = x.reshape(6,1)
print(x)
print(y)
contiguous(y)
samestorage(x,y)

print("\nflip")
x = torch.randn(3,2)
y = x.flip(0)
print(x)
print(y)
contiguous(y)
samestorage(x,y)

print("\nexpand")
x = torch.randn(3,2)
y = x.expand(2,-1,-1)
print(x)
print(y)
contiguous(y)
samestorage(x,y) 

0

Z tego, co rozumiem, bardziej podsumowana odpowiedź:

Ciągły to termin używany do wskazania, że ​​układ pamięci tensora nie jest zgodny z jego reklamowanymi metadanymi lub informacjami o kształcie.

Moim zdaniem słowo ciągły jest terminem mylącym / wprowadzającym w błąd, ponieważ w normalnych kontekstach oznacza to, że pamięć nie jest rozłożona w rozłączonych blokach (tj. Jest „ciągła / połączona / ciągła”).

Niektóre operacje mogą potrzebować tej ciągłej właściwości z jakiegoś powodu (najprawdopodobniej wydajność w GPU itp.).

Zauważ, że .viewjest to kolejna operacja, która może powodować ten problem. Spójrz na następujący kod, który naprawiłem, po prostu wywołując ciągły (zamiast typowego problemu z transpozycją powodującego go tutaj jest przykład, który jest przyczyną, gdy RNN nie jest zadowolony ze swoich danych wejściowych):

        # normal lstm([loss, grad_prep, train_err]) = lstm(xn)
        n_learner_params = xn_lstm.size(1)
        (lstmh, lstmc) = hs[0] # previous hx from first (standard) lstm i.e. lstm_hx = (lstmh, lstmc) = hs[0]
        if lstmh.size(1) != xn_lstm.size(1): # only true when prev lstm_hx is equal to decoder/controllers hx
            # make sure that h, c from decoder/controller has the right size to go into the meta-optimizer
            expand_size = torch.Size([1,n_learner_params,self.lstm.hidden_size])
            lstmh, lstmc = lstmh.squeeze(0).expand(expand_size).contiguous(), lstmc.squeeze(0).expand(expand_size).contiguous()
        lstm_out, (lstmh, lstmc) = self.lstm(input=xn_lstm, hx=(lstmh, lstmc))

Błąd, który otrzymałem:

RuntimeError: rnn: hx is not contiguous


Źródła / zasoby:

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.