Существует множество статей, в которых рассказывается о том, как построить простую сиамскую сеть для обнаружения дубликатов.
- https://towardsdatascience.com/one-shot-learning-with-siamese-networks-using-keras-17f34e75bb3d
- https://github.com/MahmoudWahdan/Siamese-Sentence-Similarity
- https://medium.com/mlreview/implementing-malstm-on-kaggles-quora-question-pairs-competition-8b31b0b16a07
Но многие из этих статей имеют вспомогательные коды либо в Keras, либо в Tensorflow. Рабочий процесс на основе MxNet для сиамского языка не очень хорошо документирован, особенно для текста. В этой статье я предоставляю код для создания простой сиамской сети с использованием MxNet — обучение и тестирование, выполненные на наборе данных вопросов и ответов Quora. Я также показал, как MxNet может выполняться на машинах с графическим процессором, что также не очень хорошо задокументировано.
Сиамская сеть
Вдохновленная командой-победителем в конкурсе Kaggle по выявлению похожих пар предложений банка Quora-Questions, в этой работе представлена простая сиамская сетевая модель, сгенерированная в MxNet, которая находит похожие пары предложений с целью дедупликации.
< br /> Что считается сиамской сетью?
Любая сеть, имеющая две или более идентичных подсети, называется сиамской сетью. Такие сети, кажется, хорошо справляются с задачами сходства, такими как сходство предложений, распознавание поддельных подписей и т. д.
Сиамские сети также используются для выполнения так называемого обучения одним выстрелом. Преимущество использования сиамской сети для однократного обучения состоит в том, что она помогает изучить функцию подобия, используя ограниченные размеченные данные. Есть много приложений, которые могут извлечь выгоду из таких моделей.
Набор данных
Чтобы объяснить сиамскую сеть, я использую набор данных Quora, выпущенный в рамках конкурса данных Kaggle. Этот набор данных состоит из двух текстовых файлов — один для обучения, а другой для тестирования. Каждая строка в наборе данных состоит из трех столбцов — столбца для вопроса 1, другого для вопроса 2 и столбца индикатора, который, если он установлен на 1, указывает, что вопрос 1 похож на вопрос 2, 0 в противном случае.
Сиамская архитектура
Простая сиамская архитектура учится вычислять меру расстояния между двумя похожими предложениями в пространстве вложений. Архитектура требует, чтобы веса прерывистых скрытых слоев распределялись между двумя предложениями, которые поступают в сеть для обучения. Любая мера расстояния (евклидово расстояние, косинусное расстояние, манхэттенское расстояние) может быть использована для определения порога подобия.
Архитектура простой сиамской сети выглядит следующим образом
Слой внедрения, слой LSTM и плотный слой имеют общие веса. Оба предложения проходят через одни и те же уровни.
Хотя сама архитектура проста в реализации, существует несколько этапов предварительной обработки, которые необходимо пройти данным, прежде чем они будут переданы в качестве входных данных в сеть.
Этапы предварительной обработки
Ниже описаны некоторые основные шаги, предпринятые для очистки данных. Также показан метод создания вложений слов из векторов перчаток. Эти шаги ни в коем случае не совершенны. Есть несколько улучшений, которые можно и нужно сделать выше этого, чтобы улучшить производительность модели. Одним из них, например, является процесс размещения сущностей. Второй — лучший способ вычислить встраивание неизвестных слов.
Примечание. Приведенный ниже код относится к https://medium.com/mlreview/implementing-malstm-on-kaggles-quora- пары вопросов-конкурс-8b31b0b16a07
#this function cleans the text of all punctuations, expands the short forms def text_to_word_list(text): ''' Pre process and convert texts to a list of words ''' text = str(text) text = text.lower() # Clean the text text = re.sub(r"[^A-Za-z0-9^,!.\/'+-=]", " ", text) text = re.sub(r"what's", "what is ", text) text = re.sub(r"\'s", " ", text) text = re.sub(r"\'ve", " have ", text) text = re.sub(r"can't", "cannot ", text) text = re.sub(r"n't", " not ", text) text = re.sub(r"i'm", "i am ", text) text = re.sub(r"\'re", " are ", text) text = re.sub(r"\'d", " would ", text) text = re.sub(r"\'ll", " will ", text) text = re.sub(r",", " ", text) text = re.sub(r"\.", " ", text) text = re.sub(r"!", " ! ", text) text = re.sub(r"\/", " ", text) text = re.sub(r"\^", " ^ ", text) text = re.sub(r"\+", " + ", text) text = re.sub(r"\-", " - ", text) text = re.sub(r"\=", " = ", text) text = re.sub(r"'", " ", text) text = re.sub(r"(\d+)(k)", r"\g<1>000", text) text = re.sub(r":", " : ", text) text = re.sub(r" e g ", " eg ", text) text = re.sub(r" b g ", " bg ", text) text = re.sub(r" u s ", " american ", text) text = re.sub(r"\0s", "0", text) text = re.sub(r" 9 11 ", "911", text) text = re.sub(r"e - mail", "email", text) text = re.sub(r"j k", "jk", text) text = re.sub(r"\s{2,}", " ", text) text = text.split() return text EMBEDDING_FILE = 'GoogleNews-vectors-negative300.bin.gz' #load stopwords from nltk stops = set(stopwords.words('english')) #load word vectors using gensim word2vec = KeyedVectors.load_word2vec_format(EMBEDDING_FILE, binary=True) # Prepare embedding vocabulary = dict() inverse_vocabulary = ['<unk>'] # '<unk>' will never be used, it is only a placeholder for the [0, 0, ....0] embedding questions_cols = ['question1', 'question2'] # Iterate over the questions only of both training and test datasets for dataset in [train_df, test_df]: for index, row in dataset.iterrows(): # Iterate through the text of both questions of the row for question in questions_cols: q2n = [] # q2n -> question numbers representation for word in text_to_word_list(row[question]): # Check for unwanted words if word in stops and word not in word2vec.vocab: continue if word not in vocabulary: vocabulary[word] = len(inverse_vocabulary) q2n.append(len(inverse_vocabulary)) inverse_vocabulary.append(word) else: q2n.append(vocabulary[word]) # Replace questions as word to question as number representation dataset.set_value(index, question, q2n) #each token in a sentence now needs to reflect its embeddings .. Also the sequence # in the sentence needs to be padded #Pad the sequences to maxlen. #if sentences is greater than maxlen, truncates the sentences #if sentences is less the 500, pads with value 0 (most commonly occurrning word) def pad_sequences(sentences,maxlen=500,value=0): """ Pads all sentences to the same length. The length is defined by maxlen. Returns padded sentences. """ padded_sentences = [] for sen in sentences: new_sentence = [] if(len(sen) > maxlen): new_sentence = sen[:maxlen] padded_sentences.append(new_sentence) else: num_padding = maxlen - len(sen) new_sentence = np.append(sen,[value] * num_padding) padded_sentences.append(new_sentence) return padded_sentences #generate the embeddings of all the words in teh vocabulary embedding_dim = 300 embeddings = 1 * np.random.randn(len(vocabulary) + 1, embedding_dim) # This will be the embedding matrix embeddings[0] = 0 # So that the padding will be ignored # Build the embedding matrix for word, index in vocabulary.items(): if word in word2vec.vocab: embeddings[index] = word2vec.word_vec(word) #make sure you release mem of word2vec del word2vec #generate the maximum length of the sequence max_seq_length = max(train_df.question1.map(lambda x: len(x)).max(), train_df.question2.map(lambda x: len(x)).max(), test_df.question1.map(lambda x: len(x)).max(), test_df.question2.map(lambda x: len(x)).max()) print(max_seq_length) # Split to train validation validation_size = 80000 training_size = len(train_df) - validation_size X = train_df[questions_cols] Y = train_df['is_duplicate'] #split into training and test X_train, X_validation, Y_train, Y_validation = train_test_split(X, Y, test_size=validation_size) # Split to dicts X_train = {'left': X_train.question1, 'right': X_train.question2} X_validation = {'left': X_validation.question1, 'right': X_validation.question2} X_test = {'left': test_df.question1, 'right': test_df.question2} # Convert labels to their numpy representations Y_train = Y_train.values Y_validation = Y_validation.values # Zero padding for dataset, side in itertools.product([X_train, X_validation], ['left', 'right']): dataset[side] = pad_sequences(dataset[side], maxlen=max_seq_length) Y_net_train = {'label' : Y_train} Y_net_validation = {'label' : Y_validation}
Благодаря приведенным выше запускам у вас теперь есть доступ к наборам данных для обучения, тестирования и проверки, которые готовы к использованию в нашей архитектуре. — X_train, Y_net_train, X_test, X_validation, Y_net_validation
Обучение и проверка
Сиамская архитектура, с которой мы работаем, построена с использованием MxNet следующим образом
Примечание: два встраивания имеют одинаковые веса. Расстояние вычисляется при прямом проходе…
class Siamese(gluon.Block): def __init__(self, input_dim, embedding_dim, **kwargs): super(Siamese, self).__init__(**kwargs) #self.nn = gluon.nn.HybridSequential() self.embedding = nn.Embedding(input_dim, embedding_dim) self.encoder = gluon.rnn.LSTM(50, bidirectional=True, input_size=embedding_dim) #self.nn.add(gluon.rnn.LSTM(10, bidirectional=True)) self.dropout = gluon.nn.Dropout(0.3) self.dense = gluon.nn.Dense(32, activation="relu") def forward(self,input0, input1): out0emb = self.embedding(input0) out0 = self.encoder(out0emb) out1emb = self.embedding(input1) out1 = self.encoder(out1emb) out0 = self.dense(self.dropout(out0)) out1 = self.dense(self.dropout(out1)) batchsize = out1.shape[0] xx = out0.reshape(batchsize, -1) yy = out1.reshape(batchsize, -1) manhattan_dis = F.exp(-F.sum(F.abs(xx - yy), axis=1, keepdims = True) + 0.0001 return manhattan_dis
На данный момент в коде архитектуры нет ничего, что указывало бы на предпочтения графического процессора для обучения/оценки. Но когда мы определим модель и инициализируем сеть, нам нужно будет указать контекст, который указывает предпочтение ЦП или ГП.
Примечание. Сама архитектура очень проста, однако задача состоит в том, чтобы распараллелить обучение на нескольких графических процессорах. Сам этот процесс не очень хорошо задокументирован для MxNet и требует корректурного прочтения людьми из команды Алекса Смола (Шенг и Лю). В приведенном ниже коде подробно описаны усилия по распараллеливанию.
#initialize the network net = Siamese(input_dim, embedding_dim) ctx = d2l.try_all_gpus() #check if you see all your 8 gpus if you have a p2x.8large instance print(ctx) #initialize the network using the context of GPU net.initialize(init=init.Normal(sigma=0.01), ctx=ctx)
Вложения словаря слов должны быть доступны для всех графических процессоров. В приведенном ниже коде показано, как установить контекст словаря встраивания и сделать его доступным для модели.
gpuembeddings = (nd.array(embeddings)).as_in_context(mx.gpu()) #adding pretrained embeddings net.embedding.weight.set_data(gpuembeddings) net.embedding.collect_params().setattr('grad_req', 'null')
Функция потерь, которая хорошо справляется с этой задачей, называется L2Loss. Кроме того, я выбрал здесь градиент «adadelta». Можно экспериментировать с другими типами потерь и градиентов.
trainer = gluon.Trainer(net.collect_params(), 'adadelta', {'clip_gradient': 1.25}) loss = gluon.loss.L2Loss()
Процесс обучения:
Данные обучения, отправляемые в сеть, должны быть распределены по всем доступным графическим процессорам. Это достигается с помощью функции split_and_load глюона.
Убыток также может накапливаться с помощью функции d2l Accumulator. Однако на данный момент я просто суммирую потери по мере получения доступа к ресурсу.
def train_model(dataiter, epoch): train_loss = 0 total_size = 0 for i, batch in enumerate(dataiter): data_list1 = gluon.utils.split_and_load(batch.data[0], ctx, even_split=True) data_list2 = gluon.utils.split_and_load(batch.data[1], ctx, even_split=True) label_list = gluon.utils.split_and_load(batch.label[0], ctx, even_split=True) with autograd.record(): # Start recording the derivatives losses = [loss(net(X1, X2), Y) for X1, X2, Y in zip(data_list1, data_list2, label_list)] for l in losses: l.backward() trainer.step(batch.data[0].shape[0]) total_size += batch.data[0].shape[0] train_loss += sum([l.sum().asscalar() for l in losses]) nd.waitall() return train_loss/total_size
Для каждой эпохи мы проверяем обученную сеть по проверочному набору.
def validate_model(valdataiter): test_loss = 0. total_size = 0 for batch in valdataiter: # Do forward pass on a batch of validation data data_list1 = gluon.utils.split_and_load(batch.data[0], ctx, even_split=False) data_list2 = gluon.utils.split_and_load(batch.data[1], ctx, even_split=False) labels = gluon.utils.split_and_load(batch.label[0], ctx, even_split=False) pys = [loss(net(X1, X2), Y) for X1, X2, Y in zip(data_list1, data_list2, labels)] test_loss += sum([l.sum().asscalar() for l in pys]) total_size += batch.data[0].shape[0] return test_loss/total_size
Собрав все это вместе, процесс обучения и проверки выглядит так:
training_loss = [] validation_loss = [] BATCH_SIZE = 1000 LEARNING_R = 0.001 EPOCHS = 10 THRESHOLD = 0.5 dataiter = mx.io.NDArrayIter(X_train, Y_net_train, BATCH_SIZE, True, last_batch_handle='discard') valdataiter = mx.io.NDArrayIter(X_validation, Y_net_validation, BATCH_SIZE, True, last_batch_handle='discard') animator = d2l.Animator('epoch', legend=['train loss','validation loss'], xlim=[1, EPOCHS]) accuracy_lst = [] timer = d2l.Timer() for epoch in range(EPOCHS): timer.start() dataiter.reset() valdataiter.reset() train_loss = train_model(dataiter, epoch) timer.stop() animator.add(epoch+1, (train_loss, validate_model(valdataiter)) ) print('train loss: %.2f, %.1f sec/epoch on %s' % ( animator.Y[0][-1], timer.avg(), ctx))
График потери обучения и потери проверки
Кривая ROC по набору проверки
Статистика:
данные о тренировках:
204544 : are not duplicates 119746 : are duplicates
данные проверки:
50483 : are not duplicates 29517 : are duplicates
Приложение
Модель была запущена на большом инстансе EC2 p2.8X с 8 инстансами GPU.