В этой истории я шаг за шагом создам языковую модель на основе 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 небольших рассказов, охватывающих всю эту историю, и постараюсь сделать их более удобными для начинающих. Пожалуйста, не забудьте указать источник, чтобы я мог получить больше подписчиков :)