Любой ML'щик время от времени делает две вещи - меняет работу и свой deeplearning фреймвок (js-фронтэндеры понимающе улыбнутся). Работу я поменял в прошлом году, теперь настало время посмотреть на модный PyTorch.
Серьезно говоря, единственная фишка питорча, ради которой захотелось его освоить - хайповые динамические графы. С их помощью делать GAN'ы должно быть намного проще, чем в keras'е, где приходится выкручиваться с необучаемыми частями графа и т.д.
Для разминки сделал на питорче сиамскую рекуррентную сетку, определяющую синонимичность двух фраз. Пример датасета лежит
тут, это часть текущего 220-тысячного корпуса.
Старая реализация модели на Keras с кучей экспериментального мусора -
тут.
Реализация на PyTorch -
тут.
Обе модели векторизуют данные лениво, батчами. В keras-версии этим заведует функция
generate_rows. Версия для pytorch использует штатный механизм с Dataset и DataLoader. Я бы сказал, что вариант для питорча проще и чище, например, рандомизация батчей в keras-варианте делается вручную, а для питорча это штатное поведение DataLoader'а. Для кераса надо писать функцию, для питорча - наследоваться от Dataset.
Описание самих моделей в обоих случаях занимает небольшую часть кода. Keras-версия после выкидывания шума выглядит так:
shared_words_rnn = Bidirectional(recurrent.LSTM(rnn_size,
input_shape=(max_wordseq_len, word_dims),
return_sequences=False))
encoder = shared_words_rnn(words_net1)
encoder = shared_words_rnn(words_net2)
...
addition = add([encoder1, encoder2])
minus_y1 = Lambda(lambda x: -x, output_shape=(sent2vec_dim,))(encoder1)
mul = add([encoder2, minus_y1])
mul = multiply([mul, mul])
words_final = keras.layers.concatenate(inputs=[mul, addition, addfeatures_input, encoder1, encoder2])
final_size = encoder_size+nb_addfeatures
words_final = Dense(units=final_size//2, activation='sigmoid')(words_final)
...
model = Model(inputs=xx, outputs=classif)
model.compile(loss='categorical_crossentropy', optimizer='nadam', metrics=['accuracy'])
Версия для питорча при той же функциональной составляющей реализована так:
class SynonymyDetector(nn.Module):
def __init__(self, computed_params):
super(SynonymyDetector, self).__init__()
self.max_len = computed_params['max_len']
vocab_size = computed_params['nb_words']
embedding_dim = 200
self.embed = nn.Embedding(vocab_size, embedding_dim=embedding_dim, padding_idx=0)
rnn_hidden_size = 200
self.rnn = nn.LSTM(input_size=embedding_dim,
hidden_size=rnn_hidden_size,
#dropout=0.0,
#num_layers=1, #self.num_layers,
bidirectional=True,
batch_first=True)
self.fc1 = nn.Linear(rnn_hidden_size*2*4, 1)
def encode(self, x):
path = self.embed(x)
out, (hidden, cell) = self.rnn(path)
res = out[:, -1, :]
return res
def forward(self, x1, x2):
path1 = self.encode(x1)
path2 = self.encode(x2)
merged = torch.cat((path1, path2, torch.abs(path1 - path2), path1 * path2), dim=-1)
merged = self.fc1(merged)
output = torch.sigmoid(merged)
return output
Можно заметить, что в питорче арифметика над тензорами выполняется немного естественнее и компактнее, без этих керасовских keras.layers.merge.add, multiply и Lambda.
Само обучение на питорче - немного закат солнца вручную в сравнении с керасом, так как в последнем есть удобный метод Model.train_generator, делающий все необходимое и т.д. Кроме того, в керасе не нужно в общем случае делать отдельный класс-наследник torch.nn.Module. Но в целом построчная разница не столь велика.
Любопытный побочный эффект архитектуры питорча - можно в отладчике видеть содержимое тензоров по мере продвижения вперед по графу в методе forward. Это позволило всего за пару часов понять, почему модель не хотела обучаться. Поэлементный визуальный контроль результатов на выходе torch.nn.LSTM обнаружил, что там есть неожиданный (для человека с keras background) параметр batch_first=True, без задания которого выходные тензоры оказываются транспонированы.