Keras LSTM'leri Anlama


311

LSTM'leri anlamamı uzlaştırmaya çalışıyorum ve bu yazıda Keras'ta uygulanan Christopher Olah tarafından işaret ettim . Keras öğreticisi için Jason Brownlee tarafından yazılan blogu takip ediyorum . Esas olarak kafam karıştı,

  1. Veri serilerinin yeniden şekillendirilmesi [samples, time steps, features]ve,
  2. Durum bilgisi olan LSTM'ler

Aşağıda yapıştırılan koda referansla yukarıdaki iki soru üzerinde yoğunlaşalım:

# reshape into X=t and Y=t+1
look_back = 3
trainX, trainY = create_dataset(train, look_back)
testX, testY = create_dataset(test, look_back)

# reshape input to be [samples, time steps, features]
trainX = numpy.reshape(trainX, (trainX.shape[0], look_back, 1))
testX = numpy.reshape(testX, (testX.shape[0], look_back, 1))
########################
# The IMPORTANT BIT
##########################
# create and fit the LSTM network
batch_size = 1
model = Sequential()
model.add(LSTM(4, batch_input_shape=(batch_size, look_back, 1), stateful=True))
model.add(Dense(1))
model.compile(loss='mean_squared_error', optimizer='adam')
for i in range(100):
    model.fit(trainX, trainY, nb_epoch=1, batch_size=batch_size, verbose=2, shuffle=False)
    model.reset_states()

Not: create_dataset, bir uzunluk N dizisini alır ve N-look_backher öğenin bir look_backuzunluk dizisi olduğu bir dizi döndürür .

Zaman Adımları ve Özellikleri Nedir?

Görüldüğü gibi TrainX, Time_steps ve Feature'ın sırasıyla son iki boyut olduğu 3 boyutlu bir dizidir (bu kodda 3 ve 1). Aşağıdaki görüntü ile ilgili olarak, bu many to onepembe kutu sayısının 3 olduğu durumu düşündüğümüz anlamına mı geliyor? Yoksa kelimenin tam anlamıyla zincir uzunluğunun 3 olduğu anlamına mı gelir (yani sadece 3 yeşil kutu dikkate alınır).resim açıklamasını buraya girin

Çok değişkenli seriler düşünüldüğünde özellikler argümanı alakalı olur mu? örneğin iki finansal hisse senedinin aynı anda modellenmesi?

Durum bilgisi olan LSTM'ler

Durum bilgisi olan LSTM'ler, hücre bellek değerlerini toplu işlemler arasında kaydettiğimiz anlamına mı geliyor? Durum buysa, batch_sizebirdir ve eğitim koşular arasında hafıza sıfırlanır, bu yüzden durum olduğunu söylemenin anlamı buydu. Bunun eğitim verilerinin karıştırılmamış olmasıyla ilgili olduğunu tahmin ediyorum, ancak nasıl yapılacağından emin değilim.

Düşüncesi olan var mı? Görüntü referansı: http://karpathy.github.io/2015/05/21/rnn-efftivity/

Düzenleme 1:

@ Van'ın kırmızı ve yeşil kutuların eşit olmasıyla ilgili yorumu biraz karıştı. Sadece onaylamak için, aşağıdaki API çağrıları kaydedilmemiş diyagramlara karşılık geliyor mu? Özellikle ikinci diyagramı not etmek ( batch_sizekeyfi olarak seçildi.): resim açıklamasını buraya girin resim açıklamasını buraya girin

Düzenleme 2:

Udacity'nin derin öğrenme kursunu yapmış ve hala time_step argümanı hakkında kafası karışmış insanlar için aşağıdaki tartışmaya bakın: https://discussions.udacity.com/t/rnn-lstm-use-implementation/163169

Güncelleme:

Bu çıkıyor model.add(TimeDistributed(Dense(vocab_len)))ben aradığımı edildi. İşte bir örnek: https://github.com/sachinruk/ShakespeareBot

Update2:

LSTM'ler hakkındaki anlayışımın çoğunu burada özetledim: https://www.youtube.com/watch?v=ywinX5wgdEU


7
İlk fotoğraf (batch_size, 5, 1) olmalıdır; ikinci fotoğraf (batch_size, 4, 3) olmalıdır (aşağıdaki sıralar yoksa). Ve neden çıktı hala "X"? "Y" olmalı mı?
Van

1
Burada X_1, X_2 ... X_6'nın tek bir sayı olduğunu varsayıyorum. Ve üç sayı (X_1, X_2, X_3) bir şekil vektörü (3,) yapar. Bir sayı (X_1), bir şekil (1,) vektörü oluşturur.
Van

2
@ Van, varsayımın doğru. Bu ilginç, bu yüzden model temelde time_steps sayısının ötesinde kalıplar öğrenmiyor. Bu yüzden 1000 uzunluğunda bir zaman serim varsa ve her 100 günde bir görsel olarak bir desen görebiliyorsam, time_steps parametresini en az 100 yapmalıyım. Bu doğru bir gözlem mi?
sachinruk

3
Evet. Günde 3 alakalı özellik toplayabilirseniz, ikinci fotoğrafta yaptığınız gibi özellik boyutunu 3 olarak ayarlayabilirsiniz. Bu durumda, giriş şekli (batch_size, 100, 3) olacaktır.
Van

1
ve ilk sorunuzu cevaplamak için tek bir zaman serisi alıyordum. Örneğin hisse senedi fiyatları, bu nedenle X ve Y aynı seriden.
sachinruk

Yanıtlar:


173

Her şeyden önce, başlamak için harika öğreticiler ( 1 , 2 ) seçersiniz .

Zaman adımı ne demektir : Time-steps==3X.Şeklinde (veri şeklini tanımlamak) üç pembe kutu olduğu anlamına gelir. Keras'ta her adım bir girdi gerektirdiğinden, yeşil kutuların sayısı genellikle kırmızı kutuların sayısına eşit olmalıdır. Yapıyı hacklemediğiniz sürece.

çoktan çoğa vs. çoktan bire : Keras'ta, return_sequencesbaşlattığınızda bir parametre vardır LSTMveya GRUveya SimpleRNN. Ne zaman return_sequencesolduğunu False(varsayılan olarak), sonra öyle birine birçok resimde gösterildiği gibi. Dönüş şekli, (batch_size, hidden_unit_length)son durumu temsil eder. Ne zaman return_sequencesolduğunu True, o zaman olduğu birçok birçok . Dönüş şekli(batch_size, time_step, hidden_unit_length)

Özellik bağımsız değişkeni alakalı mı? Özellik bağımsız değişkeni, "kırmızı kutunuz ne kadar büyük" veya her adımda girdi boyutunun ne olduğu anlamına gelir . 8 çeşit piyasa bilgisinden tahmin etmek isterseniz, verilerinizi ile oluşturabilirsiniz feature==8.

Durum bilgisi olan : Kaynak kodu arayabilirsiniz . Durumu başlatırken, stateful==Trueson eğitimden sonraki durum başlangıç ​​durumu olarak kullanılacaksa, aksi takdirde yeni bir durum oluşturur. Ben açmak değil statefulhenüz. Ancak, batch_sizesadece 1 zaman olabileceğine katılmıyorum stateful==True.

Şu anda, verilerinizi toplanan verilerle oluşturuyorsunuz. Stok bilgilerinizin, tüm sıralı bilgileri toplamak için bir gün beklemek yerine, akış olarak geliyorsa , ağ ile eğitim / tahmin yaparken çevrimiçi olarak giriş verileri oluşturmak istiyorsunuz . Aynı ağı paylaşan 400 hisse senediniz varsa ayarlayabilirsiniz batch_size==400.


Kırmızı ve yeşil kutuların neden aynı olması gerektiği konusunda biraz karıştı. Yaptığım düzenlemeye bakabilir misiniz (özellikle yeni resimler) ve yorum yapabilir misiniz?
sachinruk

1
Aslında. Belgeyi kontrol edin:stateful: Boolean (default False). If True, the last state for each sample at index i in a batch will be used as initial state for the sample of index i in the following batch.
Van

1
@Van Çok değişkenli bir zaman serim varsa, yine de kullanmalı lookback = 1mıyım?
innm

1
Çıktı alanının LSTM boyutsallığı (32) neden nöron sayısından (LSTM hücreleri) farklıdır?
Yapışkan

1
Ek stateful=True: Parti boyutu istediğiniz herhangi bir şey olabilir, ancak buna bağlı kalmanız gerekir. Eğer, sonra hepsi 5 bir toplu boyutu ile modelinizi kurarsan fit(), predict()ve benzer yöntemler bu durum ile kaydedilmez ancak o 5. Not toplu gerektirecektir model.save()istenmeyen görünebilir. Ancak, gerekiyorsa durumu hdf5 dosyasına manuel olarak ekleyebilirsiniz. Ancak etkili bir şekilde bu, sadece bir modeli kaydedip yeniden yükleyerek parti boyutunu değiştirmenize izin verir.
jlh

191

Kabul edilen cevabın bir tamamlayıcısı olarak, bu cevap keras davranışlarını ve her bir resmin nasıl elde edileceğini gösterir.

Genel Keras davranışı

Standart keras iç işleme her zaman aşağıdaki resimde olduğu gibi çok sayıdadır (kullandığım yerde features=2, basınç ve sıcaklık, örnek olarak):

ManyToMany

Bu görüntüde, diğer boyutlarla karışıklığı önlemek için adım sayısını 5'e çıkardım.

Bu örnek için:

  • N petrol tankımız var
  • Saat başı önlemler alarak 5 saat geçirdik (zaman adımları)
  • İki özelliği ölçtük:
    • Basınç P
    • Sıcaklık T

Girdi dizimiz şu şekilde şekillenmelidir (N,5,2):

        [     Step1      Step2      Step3      Step4      Step5
Tank A:    [[Pa1,Ta1], [Pa2,Ta2], [Pa3,Ta3], [Pa4,Ta4], [Pa5,Ta5]],
Tank B:    [[Pb1,Tb1], [Pb2,Tb2], [Pb3,Tb3], [Pb4,Tb4], [Pb5,Tb5]],
  ....
Tank N:    [[Pn1,Tn1], [Pn2,Tn2], [Pn3,Tn3], [Pn4,Tn4], [Pn5,Tn5]],
        ]

Sürgülü pencereler için girişler

Çoğu zaman, LSTM katmanlarının tüm sekansları işlemesi beklenir. Pencereleri bölmek en iyi fikir olmayabilir. Katman, bir sekansın ileri doğru adım atarken nasıl geliştiği hakkında dahili durumlara sahiptir. Windows, tüm dizileri pencere boyutuyla sınırlandırarak uzun dizileri öğrenme olasılığını ortadan kaldırır.

Pencerelerde, her pencere uzun bir orijinal dizinin parçasıdır, ancak Keras tarafından her biri bağımsız bir dizi olarak görülecektir:

        [     Step1    Step2    Step3    Step4    Step5
Window  A:  [[P1,T1], [P2,T2], [P3,T3], [P4,T4], [P5,T5]],
Window  B:  [[P2,T2], [P3,T3], [P4,T4], [P5,T5], [P6,T6]],
Window  C:  [[P3,T3], [P4,T4], [P5,T5], [P6,T6], [P7,T7]],
  ....
        ]

Bu durumda, başlangıçta sadece bir dizinin olduğuna dikkat edin, ancak pencereleri oluşturmak için birçok diziye böldüğünüze dikkat edin.

"Dizi nedir" kavramı soyuttur. Önemli parçalar:

  • birçok ayrı diziye sahip partileriniz olabilir
  • sekansları sekans haline getiren şey, adımlarla evrimleşmeleridir (genellikle zaman adımları)

Her vakayı "tek katmanlar" ile başarmak

Çoktan çoğa standart elde etmek:

StandardManyToMany

Basit bir LSTM katmanıyla aşağıdakileri kullanarak çoktan çoğa ulaşabilirsiniz return_sequences=True:

outputs = LSTM(units, return_sequences=True)(inputs)

#output_shape -> (batch_size, steps, units)

Bire bir başarmak:

Aynı katmanı kullanarak, keras tam olarak aynı dahili önişlemi yapar, ancak kullandığınızda return_sequences=False(veya sadece bu argümanı yoksayarsanız), keras otomatik olarak sondan önceki adımları atar:

Çoktan bire

outputs = LSTM(units)(inputs)

#output_shape -> (batch_size, units) --> steps were discarded, only the last was returned

Bire çok ulaşmak

Şimdi, bu sadece keras LSTM katmanları tarafından desteklenmemektedir. Adımları çoğaltmak için kendi stratejinizi oluşturmanız gerekir. İki iyi yaklaşım vardır:

  • Bir tensörü tekrarlayarak sabit çok adımlı giriş oluşturun
  • Bir kullan stateful=Truetekrarlı bir adım çıktısını almak ve bir sonraki adımın girdi olarak hizmet vermektedir (ihtiyaçlar output_features == input_features)

Tekrar vektör ile bire çok

Keras standart davranışına uymak için adım adım girdilere ihtiyacımız var, bu yüzden girişleri istediğimiz uzunluk için tekrarlıyoruz:

OneToManyRepeat

outputs = RepeatVector(steps)(inputs) #where inputs is (batch,features)
outputs = LSTM(units,return_sequences=True)(outputs)

#output_shape -> (batch_size, steps, units)

Durum bilgisi = Doğru

Şimdi olası kullanımlarından biri geliyor stateful=True(aynı anda bilgisayarınızın belleğine sığamayan verileri yüklemekten kaçının)

Stateful, dizilerin "kısımlarını" aşamalar halinde girmemizi sağlar. Fark şu:

  • İçinde stateful=False, ikinci parti ilk partiden bağımsız olarak tamamen yeni diziler içerir
  • Burada stateful=True, ikinci parti, aynı dizileri uzatan ilk seriyi sürdürür.

Bu iki ana farkla dizileri pencerelerde bölmek gibi:

  • bu pencereler üst üste gelmiyor !!
  • stateful=True bu pencerelerin tek bir uzun dizi olarak bağlı olduğunu görecek

İçinde stateful=True, her yeni toplu iş bir önceki toplu işi devam ettirmek olarak yorumlanacaktır (siz arayana kadar model.reset_states()).

  • Parti 2'deki sekans 1, parti 1'deki sekans 1'e devam edecektir.
  • Toplu iş 2'deki sıra 2, toplu iş 1'deki sıra 2'ye devam edecektir.
  • Parti 2'deki n dizisi, parti 1'deki n dizisine devam edecektir.

Girişlere örnek olarak, parti 1, adım 1 ve 2'yi içerir, parti 2, 3-5 arası adımları içerir:

                   BATCH 1                           BATCH 2
        [     Step1      Step2        |    [    Step3      Step4      Step5
Tank A:    [[Pa1,Ta1], [Pa2,Ta2],     |       [Pa3,Ta3], [Pa4,Ta4], [Pa5,Ta5]],
Tank B:    [[Pb1,Tb1], [Pb2,Tb2],     |       [Pb3,Tb3], [Pb4,Tb4], [Pb5,Tb5]],
  ....                                |
Tank N:    [[Pn1,Tn1], [Pn2,Tn2],     |       [Pn3,Tn3], [Pn4,Tn4], [Pn5,Tn5]],
        ]                                  ]

Parti 1 ve parti 2'deki tankların hizalamasına dikkat edin! Bu yüzden ihtiyacımız var shuffle=False(elbette sadece bir sıra kullanmıyorsak).

Süresiz olarak istediğiniz sayıda partiye sahip olabilirsiniz. (Her partide değişken uzunluklara sahip olmak için kullanın input_shape=(None,features).

Durum bilgisi olan bire çok = Doğru

Buradaki durumumuz için, parti başına sadece 1 adım kullanacağız, çünkü bir çıkış adımı almak ve bir girdi yapmak istiyoruz.

Resimdeki davranışın "neden olduğu" olmadığını lütfen unutmayın stateful=True. Bu davranışı aşağıdaki manuel döngüde zorlayacağız. Bu örnekte, stateful=Truediziyi durdurmamıza, istediğimizi değiştirmemize ve durduğumuz yerden devam etmemize "izin veren" şey budur.

OneToManyStateful

Dürüst olmak gerekirse, tekrar yaklaşımı muhtemelen bu dava için daha iyi bir seçimdir. Ama araştırdığımız için stateful=Truebu iyi bir örnek. Bunu kullanmanın en iyi yolu bir sonraki "çoktan çoğa" vakasıdır.

Katman:

outputs = LSTM(units=features, 
               stateful=True, 
               return_sequences=True, #just to keep a nice output shape even with length 1
               input_shape=(None,features))(inputs) 
    #units = features because we want to use the outputs as inputs
    #None because we want variable length

#output_shape -> (batch_size, steps, units) 

Şimdi, tahminler için manuel bir döngüye ihtiyacımız olacak:

input_data = someDataWithShape((batch, 1, features))

#important, we're starting new sequences, not continuing old ones:
model.reset_states()

output_sequence = []
last_step = input_data
for i in steps_to_predict:

    new_step = model.predict(last_step)
    output_sequence.append(new_step)
    last_step = new_step

 #end of the sequences
 model.reset_states()

Durum bilgisi olan birçok kişiye = Doğru

Şimdi, burada çok güzel bir uygulama elde ediyoruz: bir giriş sırası verildiğinde, gelecekteki bilinmeyen adımlarını tahmin etmeye çalışın.

Yukarıdaki "bire çok" ile aynı yöntemi kullanıyoruz, fark şu:

  • dizinin kendisini hedef veri olarak kullanacağız, bir adım önde
  • dizinin bir kısmını biliyoruz (sonuçların bu kısmını atarız).

ManyToManyStateful

Katman (yukarıdaki ile aynı):

outputs = LSTM(units=features, 
               stateful=True, 
               return_sequences=True, 
               input_shape=(None,features))(inputs) 
    #units = features because we want to use the outputs as inputs
    #None because we want variable length

#output_shape -> (batch_size, steps, units) 

Eğitim:

Dizilerimizin bir sonraki adımını tahmin etmek için modelimizi eğiteceğiz:

totalSequences = someSequencesShaped((batch, steps, features))
    #batch size is usually 1 in these cases (often you have only one Tank in the example)

X = totalSequences[:,:-1] #the entire known sequence, except the last step
Y = totalSequences[:,1:] #one step ahead of X

#loop for resetting states at the start/end of the sequences:
for epoch in range(epochs):
    model.reset_states()
    model.train_on_batch(X,Y)

tahmin:

Tahminimizin ilk aşaması "devletleri ayarlamayı" içerir. Bu yüzden, bu parçayı zaten biliyor olsak bile, tüm diziyi tekrar tahmin edeceğiz:

model.reset_states() #starting a new sequence
predicted = model.predict(totalSequences)
firstNewStep = predicted[:,-1:] #the last step of the predictions is the first future step

Şimdi bire çok durumda olduğu gibi döngüye gidiyoruz. Ancak durumları burada sıfırlamayın! . Modelin, dizinin hangi adımı olduğunu bilmesini istiyoruz (ve yukarıda yeni yaptığımız tahmin nedeniyle ilk yeni adımda olduğunu biliyor)

output_sequence = [firstNewStep]
last_step = firstNewStep
for i in steps_to_predict:

    new_step = model.predict(last_step)
    output_sequence.append(new_step)
    last_step = new_step

 #end of the sequences
 model.reset_states()

Bu yaklaşım şu cevaplarda ve dosyada kullanılmıştır:

Karmaşık konfigürasyonlar elde etme

Yukarıdaki tüm örneklerde "bir katmanın" davranışını gösterdim.

Tabii ki, aynı katmanı takip etmek zorunda kalmadan, birçok katmanı üst üste istifleyebilir ve kendi modellerinizi oluşturabilirsiniz.

Ortaya çıkan ilginç bir örnek, "çoktan bire enkoderi" ve ardından "birden çoka" kod çözücüsü olan "otomatik kodlayıcı" dır:

Encoder:

inputs = Input((steps,features))

#a few many to many layers:
outputs = LSTM(hidden1,return_sequences=True)(inputs)
outputs = LSTM(hidden2,return_sequences=True)(outputs)    

#many to one layer:
outputs = LSTM(hidden3)(outputs)

encoder = Model(inputs,outputs)

dekoder:

"Tekrar" yöntemini kullanarak;

inputs = Input((hidden3,))

#repeat to make one to many:
outputs = RepeatVector(steps)(inputs)

#a few many to many layers:
outputs = LSTM(hidden4,return_sequences=True)(outputs)

#last layer
outputs = LSTM(features,return_sequences=True)(outputs)

decoder = Model(inputs,outputs)

Autoencoder:

inputs = Input((steps,features))
outputs = encoder(inputs)
outputs = decoder(outputs)

autoencoder = Model(inputs,outputs)

İle eğitin fit(X,X)

Ek açıklamalar

LSTM'lerde adımların nasıl hesaplandığı veya stateful=Trueyukarıdaki durumlarla ilgili ayrıntılar istiyorsanız, bu yanıtta daha fazla bilgi bulabilirsiniz: `Keras LSTM'leri Anlama` ile ilgili şüpheler


1
Çıkışların giriş olarak kullanılmasıyla durum bilgisinin çok ilginç kullanımı. Ek bir not olarak, bunu yapmanın başka bir yolu, fonksiyonel Keras API'sini kullanmaktır (burada yaptığınız gibi, sıralı olanı kullanabileceğinize inanıyorum) ve her zaman adımı için aynı LSTM hücresini tekrar kullanmak olacaktır. hem elde edilen durumu hem de çıktıyı hücreden kendisine geçirirken. Yani my_cell = LSTM(num_output_features_per_timestep, return_state=True), ardından bir döngüa, _, c = my_cell(output_of_previous_time_step, initial_states=[a, c])
Jacob R

1
Hücreler ve uzunluk tamamen bağımsız değerlerdir. Resimlerin hiçbiri "hücre" sayısını temsil etmez. Hepsi "uzunluk" içindir.
Daniel Möller

1
@ DanielMöller Biliyorum biraz geç, ama cevabın gerçekten dikkatimi çekiyor. Bir noktan, LSTM için toplu işin ne olduğuyla ilgili anlayışım hakkındaki her şeyi paramparça etti. N tank, beş adım ve iki özellik ile örnek vereceksiniz. Eğer parti örneğin iki ise, bunun iki numunenin (5 adım 2 özellikli tanklar) ağa besleneceğine ve bundan sonra ağırlıkların uyarlanacağına inanıyorum. Ancak doğru anlarsam, parti 2'nin numunelerin zaman testlerinin 2'ye bölüneceği ve tüm numunelerin ilk yarısının LSTM-> ağırlık güncellemesine ve ikiden daha fazla besleneceği anlamına geldiğini belirtirsiniz.
viceriel

1
Evet. Durum bilgisi olan = Doğru, toplu iş 1 = örnek grubu, güncelleme. Daha sonra parti 2 = aynı örnek grubu için daha fazla adım güncelleyin.
Daniel Möller

2
Keşke bunu 100 kere oylayabilseydim. Süper yararlı cevap.
adamconkey

4

RNN'nin son katmanında return_sequences varsa, basit bir Yoğun katman kullanamazsınız, bunun yerine TimeDistributed kullanın.

Bu, başkalarına yardımcı olabilecek bir kod örneğidir.

words = keras.layers.Input (batch_shape = (Yok, self.maxSequenceLength), name = "giriş")

    # Build a matrix of size vocabularySize x EmbeddingDimension 
    # where each row corresponds to a "word embedding" vector.
    # This layer will convert replace each word-id with a word-vector of size Embedding Dimension.
    embeddings = keras.layers.embeddings.Embedding(self.vocabularySize, self.EmbeddingDimension,
        name = "embeddings")(words)
    # Pass the word-vectors to the LSTM layer.
    # We are setting the hidden-state size to 512.
    # The output will be batchSize x maxSequenceLength x hiddenStateSize
    hiddenStates = keras.layers.GRU(512, return_sequences = True, 
                                        input_shape=(self.maxSequenceLength,
                                        self.EmbeddingDimension),
                                        name = "rnn")(embeddings)
    hiddenStates2 = keras.layers.GRU(128, return_sequences = True, 
                                        input_shape=(self.maxSequenceLength, self.EmbeddingDimension),
                                        name = "rnn2")(hiddenStates)

    denseOutput = TimeDistributed(keras.layers.Dense(self.vocabularySize), 
        name = "linear")(hiddenStates2)
    predictions = TimeDistributed(keras.layers.Activation("softmax"), 
        name = "softmax")(denseOutput)  

    # Build the computational graph by specifying the input, and output of the network.
    model = keras.models.Model(input = words, output = predictions)
    # model.compile(loss='kullback_leibler_divergence', \
    model.compile(loss='sparse_categorical_crossentropy', \
        optimizer = keras.optimizers.Adam(lr=0.009, \
            beta_1=0.9,\
            beta_2=0.999, \
            epsilon=None, \
            decay=0.01, \
            amsgrad=False))
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.