Nlp. Вложения

Для эффективного применения глубокого обучения в NLP необходимо представлять дискретные типы данных в виде плотных векторов. Если дискретные типы представляют собой слова, то векторное представление называется вложением слов.

Здесь считаем, что вы ставите эксперименты на Google Colab, поэтому сначала вам нужно установить annoy модуль.

Annoy( Приблизительные ближайшие соседи) - это библиотека C ++ с привязками Python для поиска точек в пространстве, близких к заданной точке запроса. Он также создает большие файловые структуры данных, доступные только для чтения, которые отображаются в память, поэтому многие процессы могут использовать одни и те же данные. Подробнее читайте здесь: https://pypi.org/project/annoy/

!pip install --user annoy 

Проверяем подключение модуля

import numpy as np 
from annoy import AnnoyIndex

Если вдруг модуль не находится, то нужно перезагрузить ядро. Для работы со вложениями будет использован класс, код построен по книге с небольшими изменениями [Макмахан Б., Рао Д. - Знакомство с PyTorch 2020]. Код класса представлен ниже:

class ServiceEmbeddings(object):
   def __init__(selfword_to_intword_vectors):       
      '''        
      Аргументы:           
       word_to_int -  слова в числа(индексы) dict; 
       word_vectors - список массивов numpy
      '''
      self.word_to_int = word_to_int
      self.word_vectors = word_vectors
      self.int_to_word = {v: k for k, v in self.word_to_int.items()} # обращение наоборот 
      self.indexes = AnnoyIndex(len(word_vectors[0]), metric='euclidean'# возвращает новый индекс
      for _, i in self.word_to_int.items():
        self.indexes.add_item(i, self.word_vectors[i])
      self.indexes.build(40# создание 40 деревьев
   @classmethod
   def load_from_file(clsfile):
      '''
      Загрузка вложений из файла формата:
      Слово0 x0_0 x0_1 x0_2 ... x0_N
      Слово1 x1_0 x1_1 x1_2 ... x1_N
      ...
      CловоN xN_0 xN_1 xN_2 ... xN_N
      Аргументы: 
        file - строка местоположения файла
      Возвращает экземпляр класса
      '''
      word_to_int = {}        
      word_vectors = [
      # открытие файла
      with open(fileas fp:
        for row in fp.readlines():                
          row = row.split(" "# разбивает строку на части с пробелом в качестве разделителя
          word = row[0]
          vec = np.array([float(x) for x in row[1:]])
          word_to_int[word] = len(word_to_int)
          word_vectors.append(vec)
      return cls(word_to_int, word_vectors)
   def get_data(selfword):
      '''
      Получить вложения для заданного слова
      Аргументы:
        word - строка, задающая слово
      Возвращает вложение
      '''
      return self.word_vectors[self.word_to_int[word]] 
   def get_closes(selfword_vectorn_neighbors):
     '''
     Возвращает ближайшие соседи для заданного вектора
     Аргументы:
      word_vector - вектор, который получается из get_data;
      n_neighbors - количество ближайших соседей, которые нужно возвратить;
     Возвращается массив слов ближайших.
     '''
     neighbors = self.indexes.get_nns_by_vector(word_vector, n_neighbors)        
     return [self.int_to_word[n] for n in neighbors]

При инициализации на вход подаются word_to_int (отображение слова на индекс) и word_vectors (набор соответствий другим словам). Создается AnnoyIndex с Евклидовой метрикой для всех элементов.

Для загрузки данных используется метод класса load_from_file, который получает на вход имя файла. Собственно этот класс и формирует нужные отображения слов на индекс и вектора соответствий для данного слова.

Метод get_data возвращает для заданного слова вектор соответствий (вложений).

Метод get_closes возвращает ближайшие соседи к искомому слову.

Существуют различные наборы данных для вложений, один из них Glove. Для русского языка можно скачать здесь: https://www.kaggle.com/tunguz/russian-glove

Если вы не зарегистрированы ка Kaggle, то зарегистрируйтесь и получите токен для API. После чего можно загружать данные на Google Colab напрямую

api_token = {"username":"вашеимя","key":"ваш ключ"}
import json
import zipfile
import os
with open('/root/.kaggle/kaggle.json''w'as file:
    json.dump(api_token, file)
!chmod 600 /root/.kaggle/kaggle.json

Ну а после этого можете загружать данные

!kaggle datasets download -d tunguz/russian-glove

Команда выдаст примерно так:

Downloading russian-glove.zip to /content 99% 126M/127M [00:04<00:00, 27.3MB/s] 100% 127M/127M [00:04<00:00, 30.2MB/s]

Разархивируем:

!unzip /content/russian-glove.zip

Результат:

Archive: /content/russian-glove.zip inflating: multilingual_embeddings.ru

Инициализируем класс:

emb = ServiceEmbeddings.load_from_file("multilingual_embeddings.ru")

Проверяем работу:

word_vector = emb.get_data("собака")
print(emb.get_closes(word_vector,5))

Результат:

['собака', 'собаку', 'животное', 'местом', 'лошадь']

Ещё проверка

word_vector = emb.get_data("смотреть")
print(emb.get_closes(word_vector,4))

['смотреть', 'надо', 'говорить', 'должно']

Связи между вложениями слов

Главное во вложениях - это связи между словами. Т.е насколько в контексте разговора схожи слова. Например, часто в разговоре можно заменить кошку на собаку, т.к. речь идет о домашнем питомце. Существуют так называемые задачи на аналогии.

Например, есть три слова: лошадь, бежит, ворона

И нужно добавить четвертое: летит

Простейший способ решения следующим образом:

def find_analogy(embeddingword1word2word3):
  '''
  Возвращает аналогию - 4 слово
  Аргументы:
  embedding - класс ServiceEmbeddings;
  word1, word2, word3 - 3 слова
  '''
  x1 = embedding.get_data(word1)
  x2 = embedding.get_data(word2)
  x3 = embedding.get_data(word3)
  # слово 1 вычитается из слова 2 - это связь между словами 1 и 2
  x21 = x2 - x1
  # прибавляем разность к 3, рассчитывая получить вектор ближайший к пропущенному
  # слову
  x4 = x3 + x21
  # ближайшие слова (5)
  words = embedding.get_closes(x4,5)
  # множество слов
  in_words = set([word1, word2, word3])
  # удаляем слова, которые подаются на вход из найденных
  words = [word for word in words if word not in in_words]
  if len(words) == 0 :
    print("Не нашли решения!")
    return
  
  # вывести варианты ближайших слов
  for word4 in words:
    print("{} : {} :: {} : {}".format(word1, word2,  word3, word4))
 
Проверяем на различных примерах
find_analogy(emb,"мужчина","он","женщина")

Результат:

мужчина : он :: женщина : ей

мужчина : он :: женщина : она

мужчина : он :: женщина : её

мужчина : он :: женщина : ее

Вроде работает, но выдалось несколько результатов. С английским языком проще, будет так:

man : he :: woman : she

Собственно это обусловлено большей сложностью языка, а также обученным файлов вложений

Вот еще вызовы и результаты

find_analogy(emb,"мужчина","муж","женщина")

мужчина : муж :: женщина : мать

мужчина : муж :: женщина : мама

мужчина : муж :: женщина : подруга

мужчина : муж :: женщина : пациент

find_analogy(emb,"синий","цвет","собака")

синий : цвет :: собака : сложный

синий : цвет :: собака : инструмент

синий : цвет :: собака : хороший

синий : цвет :: собака : рисунок

синий : цвет :: собака : объект

Как видим - для русского языка и конкретного файла вложений все не очень хорошо