В этой истории я шаг за шагом создам языковую модель на основе LSTM для данного корпуса. Используемые библиотеки для препроцессинга, нейромоделей и т. д. — spaCy и Keras. Основная цель здесь — создать простую поисковую систему для предложений корпуса, используя скрытые состояния LSTM в качестве представлений.
Обратите внимание, что для этого небольшого руководства требуется, чтобы читатель имел хотя бы прочные основы в Python и машинном обучении, поскольку здесь я сосредоточился на части кодирования. Это учебник, основанный на коде, который предполагает, что читатель уже работал над подобными проектами.
ЧАСТЬ I: Набор данных
Набор данных будет представлять собой корпус Брауна. Коричневый корпус — это большой корпус, состоящий из миллиона слов. Согласно документации NLTK, это был первый электронный корпус на английском языке, включающий миллион слов, созданный в 1961 году в Университете Брауна. Natural Language Toolkit или NLTK — это платформа, предоставляющая библиотеки, корпуса и ресурсы для обработки естественного языка и написанная на Python. Я импортирую библиотеку NLTK и ее коричневый корпус.
# importing libraries import numpy as np from keras.utils import pad_sequences import tensorflow as tf import keras.backend as K # downloading nltk brown corpus import nltk from nltk.corpus import brown nltk.download('brown') nltk.download('universal_tagset')
Подготовка набора данных
Здесь я назначу уникальные идентификаторы каждому слову в наборе данных, начиная с 1, оставив 0 свободным для пустых мест. Затем определите число для максимального входного размера модели.
Кроме того, необходимо создать функцию для преобразования векторов идентификаторов предложений, которые могут использоваться моделью: string_to_model_input (sentence): 1. Здесь input — строковое предложение (« Это приговор»).
2. Создайте векторы идентификаторов слов из предложения ([3, 5, 8, 6, 1]).
3. На выходе будет кортеж из двух векторов: X и Y с длиной, если максимальный размер ввода. X будет идентификатором, кроме последнего токена ([3, 5, 8, 6, 0, 0, 0]). Y будет идентификатором, кроме первого токена ([5, 8, 6, 1, 0, 0, 0]). Используйте 0, чтобы заполнить пустые места, если они есть.
В конце концов, функция string_to_model_input будет запущена для всех предложений в корпусе, чтобы создать входные данные X и Y для обучающей модели.
# tokensFromList is a function that is created to create a Spacy Doc file for # getting tokens specifically from lists import spacy; nlp = spacy.load('en_core_web_sm') def tokensFromList(words_): doc = spacy.tokens.doc.Doc( nlp.vocab, words=words_) for name, proc in nlp.pipeline: doc = proc(doc) # return print([t for t in doc]) return doc # Vocabulary class. It has 2 useful functions for adding sentences and adding words. # It also turns words to index and vice versa. It has word count, num. of words and # sent., lengths of sent and etc. class Vocabulary: UNKNOWN = 0 def __init__(self, name): self.name = name self.word2index = {"UNKNOWN": self.UNKNOWN} self.word2count = {} self.index2word = {self.UNKNOWN: "UNKNOWN"} self.num_words = 1 self.num_sentences = 0 self.longest_sentence = 0 self.sentence_lengths = [] def add_word(self, word): if word not in self.word2index: # First entry of word into vocabulary self.word2index[word] = self.num_words self.word2count[word] = 1 self.index2word[self.num_words] = word self.num_words += 1 else: # Word exists; increase word count self.word2count[word] += 1 def add_sentence(self, sentence): sentence_len = 0 for word in sentence: sentence_len += 1 self.add_word(word.lower()) self.sentence_lengths.append(sentence_len+1) if sentence_len > self.longest_sentence: # This is the longest sentence self.longest_sentence = sentence_len # Count the number of sentences self.num_sentences += 1 def to_word(self, index): return self.index2word[index] def to_index(self, word): return self.word2index[word] # creating vocabulary brownCorpus voc = Vocabulary('brownCorpus') print(voc) # adding sentences and words and other properties of them to vocabulary for sent in brown.sents(): voc.add_sentence(sent) sentence_lengths = voc.sentence_lengths mean_sentence_length = np.mean(sentence_lengths) deviation_sentence_length = np.std(sentence_lengths)
Здесь мы можем распечатать, чтобы увидеть номера нашего словарного запаса, другими словами, его распределение и статистику. Результат печати деталей будет следующим:
print("Number of Sentences:", voc.num_sentences) print("Number of Words:", voc.num_words) print('Mean Sentence Length: {}\nSentence Length Standard Deviation: {}\n' 'Max Sentence Length: {}'.format(mean_sentence_length, deviation_sentence_length, voc.longest_sentence)) Number of Sentences: 57340 Number of Words: 49816 Mean Sentence Length: 21.250994070456922 Sentence Length Standard Deviation: 13.107004875007762 Max Sentence Length: 180 print('Token 4 corresponds to token:', voc.to_word(4)) print('Token "this" corresponds to index:', voc.to_index('this')) Token 4 corresponds to token: grand Token "this" corresponds to index: 79
Мы можем немного изучить и увидеть несколько примеров:
# EXAMPLE AND EXPLORATION i = 0 for word in range(voc.num_words): print(voc.to_word(word)) i = i+1 if i == 35: break # EXAMPLE AND EXPLORATION corpus = ['this is the first sentence .', 'this is the second .', 'there is no sentence in this corpus longer than this one .', 'the dog is named patrick .'] print(corpus) ['this is the first sentence .', 'this is the second .', 'there is no sentence in this corpus longer than this one .', 'the dog is named patrick .'] # EXAMPLE AND EXPLORATION sent_tkns = [] sent_idxs = [] for word in corpus[0].split(' '): sent_tkns.append(word) sent_idxs.append(voc.to_index(word)) print(sent_tkns) print(sent_idxs) ['this', 'is', 'the', 'first', 'sentence', '.'] [79, 137, 1, 485, 3882, 25]
Назначение ключевых понятий — это следующий шаг, слова — это длина корпуса. max_lengths будет длиной каждого предложения после заполнения. За ним следует код функции, которая преобразует предложение слов в предложение идентификаторов и дополняет их. После этого будут назначены X_train и y_train. Эта часть кода для предварительной обработки завершается преобразованием их в массивы np (numpy). См. фрагмент кода ниже:
dictOfIds = voc.index2word words = list(dictOfIds)[-1] + 1 max_length = 14 # A FUNCTION THAT CONVERTS SENTENCE OF WORDS INTO SENTENCE OF IDS AND PAD THEM def string_to_model_input(sents, maximam_longitudinem): X,y = [],[] X_temp, y_temp = [], [] for word in sents: X_temp.append(voc.to_index(word)) y_temp.append(voc.to_index(word)) # print(X_temp) if voc.to_index(word) == 25: # 25 = id of "." del X_temp[-1] X.append(X_temp) X_temp = [] del y_temp[0] y.append(y_temp) y_temp = [] if voc.to_index != 25 and len(X_temp) > 1: # just added in case the last sent of corpus is not endded with "." del X_temp[-1] X.append(X_temp) del y_temp[0] y.append(y_temp) X = pad_sequences(X, padding = 'post', maxlen=maximam_longitudinem) y = pad_sequences(y, padding='post', maxlen=maximam_longitudinem) return tuple(X), tuple(y) # ASSIGNING X_train ANDA y_train brown_words = [x.lower() for x in brown.words()] X_train,y_train = string_to_model_input(brown_words, max_length) print("Length of brown_words", len(brown_words)) # CONVERTING THEM TO NP ARRAYS X_train_arr = np.array(X_train) y_train_arr = np.array(y_train) print(len(X_train_arr)) print(len(y_train_arr)) Length of brown_words 1161192 len(X_train_arr): 49346 len(y_train_arr): 49346
ЧАСТЬ II: Языковая модель — Exemplum Linguae
В этой части мы построим языковую модель. Здесь будет выполнено создание функциональной модели Keras, как показано в приведенном ниже коде. Обратите внимание, что приведенный ниже код включает векторные функции состояния ячейки.
from keras.utils import plot_model from keras.models import Model from keras.layers import Input from keras.layers import Dense from keras.layers import LSTM from keras.layers import Dense, Activation, Flatten, Dropout, LSTM, Activation, Embedding deep_inputs = Input(shape=(max_length,)) embedding = Embedding(words, 150, input_length=max_length)(deep_inputs) # line A # flatten = Flatten()(embedding) lstm1 = LSTM(512, return_sequences=True)(embedding) lstm2, state_s, state_c = LSTM(512, return_sequences=True, return_state=True)(lstm1) hidden = Dense(words, activation='softmax')(lstm2) model = Model(inputs = deep_inputs, outputs=hidden) # summarize layers print(model.summary()) # plot graph
Обратите внимание, что я пробовал разную максимальную длину ввода, от 120 до 25 (средняя длина предложения), однако 35–40 были наиболее точными. Для оптимизатора я использовал RMSprop (среднеквадратичное распространение). Причина проста: считается, что это хороший выбор для RNN.
RMSprop работает, вычисляя градиент функции потерь по отношению к параметрам модели и обновляя параметры в направлении, противоположном градиенту, чтобы минимизировать потери. (Глоссарий глубоких проверок)
LEARNING_RATE = 0.01 EPOCHS = 10 optimizer = tf.compat.v1.train.RMSPropOptimizer(LEARNING_RATE) # it is said to be good choice for RNNs # loss_fn = tf.keras.losses.SparseCategoricalCrossentropy() model.compile(optimizer=optimizer, loss='sparse_categorical_crossentropy', metrics=['accuracy']) history = model.fit(X_train_arr, y_train_arr, epochs=EPOCHS, batch_size=128, verbose=1).history
Затем модель необходимо сохранить и правильно загрузить. Поэтому сохраняем модель для дальнейшего использования и загрузки.
# Save the entire model as a SavedModel. !mkdir -p saved_model model.save('saved_model/my_model') import pickle model.save('keras_next_word_modelFun.h5') pickle.dump(history, open("historyFun.p", "wb")) # load model from keras.models import load_model model = load_model('/content/drive/MyDrive/keras_next_word_modelFun.h5', compile=False)
Получение функции состояния ячейки
Функция для получения состояния ячейки LSTM: get_cell_state (word_id_vector)
Ввод: входной вектор, который можно использовать для вызова модели.
Вывод: вектор состояния ячейки слоя LSTM.
def get_cell_state(word_id_vector): get_output = K.function([model.layers[0].input],[model.layers[3].output[2]]) return get_output([word_id_vector]) # get_cell_state created exclusively for User Input def get_cell_state_forUser(sents): idVec, predInd = string_to_model_input_forUser(sents) # predInd is originally designed for predicting next words functions, so ignore it here idVec_arr = np.array(idVec, dtype='int32') # ids is a list of ids of a user inputed sentence. Here I am converting to np array return get_cell_state([idVec_arr])
ЧАСТЬ III: Предсказание следующего слова — Iuxta Verbum Praedictum
Здесь я попытаюсь предсказать следующее слово для коричневого корпуса и пользовательского ввода.
preprocess_corpus_forUser
и string_to_model_input_forUser
— это функции для необходимой предварительной обработки пользовательского ввода.
def preprocess_corpus_forUser(corpus): corpus_tokens = [] sentence_lengths = [] doc = nlp(str(corpus)) # Parse each line in the corpus for sent in doc.sents: # Loop over all the sentences in the line for tok in sent: # Loop over all the words in a sentence corpus_tokens.append(tok.text.lower()) return corpus_tokens def string_to_model_input_forUser(sents): X = [] X_temp = [] indexOfLast = 0 user_corpus = preprocess_corpus_forUser(sents) for word in user_corpus: if word in voc.word2index: X_temp.append(voc.to_index(word)) else: X_temp.append(voc.word2index["UNKNOWN"]) # print(X_temp) if X_temp[-1] == 25: # 25 = id of "." X.append(X_temp) X_temp = [] if voc.to_index != 25 and len(X_temp) > 1: # just added in case the last sent of corpus is not endded with "." X.append(X_temp) if len(X[-1]) < 15: indexOfLast = len(X[-1]) - 1 else: indexOfLast = 13 X = pad_sequences(X, padding = 'post', maxlen=max_length) # Xarray = np.array(X) return tuple(X), indexOfLast
Прогнозирование следующего слова (функциональная модель)
Функции прогнозирования для нахождения слова с наибольшей вероятностью
# E X A M P L E st = "The model was awesome. It will be next big " p, l = string_to_model_input_forUser(st) pred = model.predict(p[-1]) print(pred.shape) WARNING:tensorflow:Model was constructed with shape (None, 14) for input KerasTensor(type_spec=TensorSpec(shape=(None, 14), dtype=tf.float32, name='input_2'), name='input_2', description="created by layer 'input_2'"), but it was called on an input with incompatible shape (None, 1). (14, 1, 49816) # Finding the highest probabilty and its index. Also the number of probabilities assigned. def high_prob(vector, idLast): max = 0 a, b, c = -1, -1, -1 leng = 0 for ii in vector[idLast]: b+=1 c = -1 for iii in ii: c+=1 leng += 1 if max < iii: max = iii second, third = b, c else: continue return max, idLast, second, third, leng print(high_prob(pred, 4)) (0.011349797, 4, 0, 33, 49816) v, i, ii, iii, length = high_prob(pred, 5) # function to find the highest probability, its index, and length of the prediction vector # print(pred[1][i][ii][iii]) # so 1st, 2nd and 4th index of 3d pr list is 0.09 which is the highest one # len(pr[1][3]) # the form of the list is like that --> 5*30 + 4*30 = 270 print("number of words in the vocabulary: ", voc.num_words) # number of words in the vocabulary print("length of the prediction vector: ", length) # length of the prediction vector number of words in the vocabulary: 49816 length of the prediction vector: 49816
Вызов функции прогнозирования
Функция calling
для пользовательского ввода и предсказания следующего слова. Можно бесконечно предсказывать.
# a function created for easy calling of a pos-tagger and make it work unless you want it to def calling(): z = "y" while z == "y": z = input() zz, idLast = string_to_model_input_forUser(z) if len(zz) > 1: zzz = model.predict(zz[-1]) else: zzz = model.predict(zz) v, i, ii, iii, length = high_prob(zzz, idLast) predicted_word = voc.to_word(iii) # x = input(print("Leave empty if you do not want to continue.")) X = input() if x != "": calling() else: break # return print(z, predicted_word, i, ii, iii, zzz) return print(z, predicted_word) calling() # ex: Today it was announced that next year will be virus free. It sounds Today it was announced that next year will be virus free. It sounds great, and yesterday, all my dreams seemed so far away, then came Leave empty if you do not want to continue. Today it was announced that next year will be virus free. It sounds great, and yesterday, all my dreams seemed so far away, then came on
ЧАСТЬ IV: Косинусное сходство предложений — Similes Constituit
Нахождение косинусного сходства предложений
Здесь я создам функцию: cosine_similarity (a,b)
Входные данные:2 вектора
Выходные данные:косинусное сходство векторов
Затем я перейду к созданию поля ввода для чтения 2 предложений. Поместите его в цикл, выходом будет пустая строка. Затем я создаю входные векторы из предложений и запускаю языковую модель, чтобы получить состояние ячейки LSTM (вызов функции get_cell_state). Следующим шагом будет использование выходных векторов для косинусного подобия и печать предложений и результатов.
# Note that spatial.distance.cosine computes the distance, and not the similarity. So, you must subtract the value from 1 to get the similarity. from scipy import spatial def cosine_similarity(a,b): return 1 - spatial.distance.cosine(a, b) # get_cell_state_forUser def sim_of_2sents(): go = True while go: print("Please enter 2 sentences: ") sent1 = input() sent2 = input() sent2vec1 = get_cell_state_forUser(sent1) sent2vec2 = get_cell_state_forUser(sent2) cos_similarity = cosine_similarity(sent2vec1, sent2vec2) # print("Sentence I <--->", sent1) # print("Sentence II <--->", sent2) print("Cosine Similarity: ", cos_similarity) # user_choice = input(print("Leave empty if you do not want to continue.")) user_choice = input() if user_choice != "": sim_of_2sents() else: break sim_of_2sents() Please enter 2 sentences: Today it was announced that next year will be virus free. I hope peace will come upon the Earth and the cursed ones will be deleted. Sentence I --- Today it was announced that next year will be virus free. Sentence II --- I hope peace will come upon the Earth and the cursed ones will be deleted. Cosine Similarity: 0.9304618239402771 Leave empty if you do not want to continue.
ЧАСТЬ V: Мини-поисковик — Mini Quaerere Engine
Я буду использовать AnnoyIndex для индексации векторов и создания векторов состояния ячеек из всех предложений в корпусе Брауна. Затем будет создано поле ввода для чтения искомой последовательности слов, зациклим его, на выходе будет пустая строка. После этого создадим вектор состояния ячейки из последовательности искомых слов. Наконец, мы получим 5 ближайших соседей из индекса и распечатаем их.
Векторы состояния ячейки
ПОЛУЧЕНИЕ ВЕКТОРА СОСТОЯНИЯ КЛЕТКИ ВСЕХ ПРЕДЛОЖЕНИЙ КОРИЧНЕВОГО КОРПУСА И ПОЛУЧЕНИЕ 5 БЛИЖАЙШИХ СОСЕДЕЙ
# getting cell state vectors from brown corpus # conv --> converter def conv(sent): tete = np.expand_dims(sent, axis=0) return (get_cell_state(tete)) # GETTING CELL STATE VECTOR OF ALL SENTENCES OF BROWN CORPU corpora = [] for i in X_train_arr: corpora.append(conv(i)) # INDEXING SENTENCES WITH ANNOY INDEX IT WILL FIND 6 NEAREST NEIGHBOUR AS THE FIRST NEAREST ONE IS ITSELF from annoy import AnnoyIndex f = 512 t = AnnoyIndex(f, 'angular') # Length of item vector that will be indexed for i in range(0,len(X_train_arr)): v = corpora[i][0][0] t.add_item(i, v) t.build(10) # 10 trees t.save('test.ann') # ...\ u = AnnoyIndex(f, 'angular') u.load('test.ann') # super fast, will just mmap the file print(u.get_nns_by_item(6, 6)) # will find the 6 nearest neighbors [6, 22804, 29305, 13425, 29290, 7] # MINI SEARCH ENGINE FUNCTION FOR FINDING NN def searchEngine(): sentBox, simSent = [], [] go = True while go: print("Please enter a sentence: ") sent1 = input() sent2vec1 = get_cell_state_forUser(sent1) simSent = u.get_nns_by_vector(sent2vec1[0][0], 5) for sentId in simSent: sentBox.append(TreebankWordDetokenizer().detokenize([voc.to_word(x) for x in X_train_arr[sentId]])) print(sentBox) user_choice = input() if user_choice != "": searchEngine() else: break searchEngine() Please enter a sentence: I hope peace will come upon the Earth and the cursed ones will be deleted. ['mullins?? it was evident that mullins was the man to go UNKNOWN', 'of course, there were books about which nothing good could be said UNKNOWN', "if the crummy bastard could write!! that's how it should be UNKNOWN", 'he believed in being seen near the front lines and he was there UNKNOWN', 'and yet wilson knew that this place must go or he must go UNKNOWN'] print("thanks God it's finished") thanks God it's finished
Большое спасибо, что прочитали или, по крайней мере, получили от этого пользу. В будущем я опубликую 5 небольших рассказов, охватывающих всю эту историю, и постараюсь сделать их более удобными для начинающих. Пожалуйста, не забудьте указать источник, чтобы я мог получить больше подписчиков :)