Восстанавливаем предложения из эмбеддингов LaBSE

от автора

На прошлой неделе меня дважды спрашивали, как восстановить текст предложения из его LaBSE эмбеддинга. Я дважды отвечал, что никак. Но на самом деле, конечно, можно обучить декодер генерировать текст по его эмбеддингу. Зачем? Например, чтобы:

  • переводить со 100 разных языков на русский;

  • суммаризовать много похожих предложений одним;

  • реалистично заменять фразы в составе предложений;

  • менять смысл или стиль предложений.

Модель для восстановления предложений из эмбеддингов опубликована как cointegrated/rut5-base-labse-decoder, а подробности – под катом.

Обзор предлагаемой системы. Рисунок автора.
Обзор предлагаемой системы. Рисунок автора.

LaBSE и другие энкодеры предложений

Энкодер предложений (sentence encoder) – это модель (обычно нейросеть), которая получает на вход текст предложения, а на выходе отдаёт многомерный вектор (например, 768-мерный), примерно описывающий смысл этого предложения. То есть такой, что у предложений, похожих друг на друга по смыслу, векторы похожи друг на друга геометрически. Энкодеры предложений можно использовать для классификации текстов и массы других полезных задач; подробнее читайте в моих постах про маленький BERT и про рейтинг энкодеров предложений.

LaBSE (language-agnostic BERT sentence embeddings) – это модель, предложенная в статье 2020 года от исследователей из Google. По архитектуре это BERT, а обучался он на выборке текстов на 100+ языков в многозадачном режиме. Основная задача – сближать друг с другом эмбеддинги предложений с одинаковым смыслом на разных языках, и с этой задачей модель справляется очень хорошо. Благодаря этой способности можно, например, обучать модель классифицировать английские тексты, а потом применять на русских, или находить в большом корпусе пары предложений на разных языках, являющиеся переводами друг друга.

А вот чего LaBSE не умеет делать совсем, так это генерировать тексты. Единожды превратив текст в вектор, мы уже не сможем получить из него обратно текст. Для этого нужна отдельная модель. И то не факт, что она с этим справится: как говаривал профессор Raymond J. Mooney, You can’t cram the meaning of a single $&!#* sentence into a single $!#&* vector! Но мы всё-таки попробуем.

Обучение декодера

Декодер в NLP – это как раз модель, которая из векторов генерирует тексты, т.е. решает задачу, обратную задаче энкодера. Для русского языка есть несколько декодеров, из которых я выбрал некогда обученную мною модель T5, т.к. это требовало минимальных изменений в коде. Как альтернатива, я мог бы попробовать дообучить русскую GPT; если попробуете – расскажите, пожалуйста!

Кодирование текстов в векторы происходит абсолютно стандартно: извлекаем эмбеддинг CLS-токена из LaBSE и нормализуем его.

bert_name = 'sentence-transformers/LaBSE' enc_tokenizer = AutoTokenizer.from_pretrained(bert_name) encoder = AutoModel.from_pretrained(bert_name)  def encode(texts, do_norm=True):     encoded_input = enc_tokenizer(texts, padding=True, truncation=True, max_length=512, return_tensors='pt')     with torch.no_grad():         model_output = encoder(**encoded_input.to(encoder.device))         embeddings = model_output.pooler_output         if do_norm:             embeddings = torch.nn.functional.normalize(embeddings)     return embeddings

Декодирование выглядит так же просто. Это стандартная генерация текстов с помощью T5 (или любого другого seq2seq трансформера), только на вход мы подаём эмбеддинги из LaBSE, которые «прикидываются» эмбеддингами от энкодера T5 (благо размерность и у тех, и у других оказалась 768, так что мне даже не пришлось модифицировать слои cross-attention в T5).

t5_name = 'cointegrated/rut5-base-labse-decoder' dec_tokenizer = AutoTokenizer.from_pretrained(t5_name) decoder = AutoModelForSeq2SeqLM.from_pretrained(t5_name)  def decode(embeddings, max_length=256, repetition_penalty=3.0, num_beams=3, **kwargs):     out = decoder.generate(         encoder_outputs=BaseModelOutput(last_hidden_state=embeddings.unsqueeze(1)),          max_length=max_length,          num_beams=num_beams,         repetition_penalty=repetition_penalty,     )     return [dec_tokenizer.decode(tokens, skip_special_tokens=True) for tokens in out]

Естественно, без файнтюнинга T5 предложения, сгенерированные таким образом, будут бессмысленными, ведь T5 обучался смотреть на эмбеддинги из другого пространства, причём не на один, а на целую последовательность эмбеддингов (для каждого токена).

Для дообучения я взял 2 миллиона коротких текстов: opus100, Leipzig collection, и комментарии из Одноклассников. В качестве аугментации добавил ещё 400К отдельно взятых слов. И на всём этом стандартным образом (teacher-forced cross-entropy) обучил T5 генерировать из эмбеддинга исходный текст. Обучал с батчом 8 в течение примерно миллиона шагов; это заняло 2.5 дня на Google Colab. Блокнот – туть.

После дообучения T5 справляется с новой задачей вполне сносно. Можно, например, закодировать такие тексты:

embeddings = encode([     "4 декабря 2000 года",     "Давно такого не читала, очень хорошо пишешь!",     "Я тогда не понимала, что происходит, не понимаю и сейчас.",     "London is the capital of Great Britain.", ]) print(embeddings.shape) # torch.Size([4, 768])

После декодирования тексты меняются, но смысл их модель примерно воспроизводит:

for text in decode(embeddings):     print(text) # После 4 декабря 2000 года # Не так давно, это многое читала! # Я не понимала того, что происходит сейчас, тогда же. # Британская столица Англии.

Примеры применения

Окей, нейросеть обучена, и что теперь? В общем-то ничего, ведь этот эксперимент я проделал в первую очередь просто для развлечения. Но если хочется развлекаться дальше, в этом блокноте собрано несколько примеров применения такого декодера. Самый очевидный – это перефразирование, но возможны и более креативные применения.

Перевод

LaBSE умеет «переводить» тексты с разных языков в общее векторное пространство, а наш декодер умеет переводить из этого пространства на русский. Значит, вместе эта парочка моделей можем переводить на русский с любого из 109 языков, известных LaBSE!

Ниже пример десятка языков. Модель не совсем понимает разницу между словами «господа» и «господи», а в остальном вполне справляется с задачей.

Исходный текст

Перевод

Господа, я не ел 6 дней!

Господи, я не ел 6 дней!

Панове, я не їв 6 днів!

Господи, я не ел 6 дней!

Gentlemen, I haven’t eaten for 6 days!

Господи, я 6 дней не кормила!

Messieurs, je n’ai pas mangé depuis 6 jours!

Господи, я не съела 6 дней!

Meine Herren, ich habe seit sechs Tagen nichts gegessen!

Господи, у меня шесть дней не ели ничего!

Худовандо, ман 6 рӯз боз чизе нахӯрдаам!

Боже, у меня еще 6 дней не было!

Tanrım, 6 gündür yemek yemedim!

Господи, я не ел 6 дней!

אלוהים, לא אכלתי 6 ימים

Боже, у меня не было 6 дней!

主啊,我已经6天没吃东西了

Господи, я уже 6 дней не ела.

हे प्रभु, मैंने 6 दिनों से कुछ नहीं खाया है!

Господи, я не съела ничего из этого дня!

Суммаризация

Иногда бывает нужно по множеству предложений понять, в чём их основная общая идея. Например, не читать 50 отзывов на товар, а прочитать один «усреднённый отзыв». Наша модель вообще-то совсем не предназначалась для суммаризации – но вдруг у неё получится?

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

# соковыжималка  Braun J500 # примеры отдельных предложений Дизайн строгий, но это понятно - фирма-то немецая. Из 5 кг яблок выходит меньше литра сока, а пены больше чем сам сок!!! Жмых сухой, сеточка мелкая и не пропускает куски. ... # декодированное усреднённое предложение Устройство очень хорошее, потому что выбрасывать яблоки неплохо.  # Планшет Lenovo Tab 3 Plus # примеры отдельных предложений Минусов никаких не заметно пока что. Экран, Lte, gps, ГЛОНАСС, 2 сим, быстрый, шустрый, размер, тонкий, сборка. Замечательный планшет! ... # декодированное усреднённое предложение Всё очень красиво, у нас на экране есть сенсорная картинка.    # Планшет Prestigio MultiPad  # примеры отдельных предложений Не очень понравилось что динамик только 1, стерео нет( На расстоянии пары сантиметров видны пиксели, батарейка заряжается конечно долго. Зарядное устройство стандартвое без изъян. ... # декодированное усреднённое предложение Встроенный экран очень хороший, даже несмотря на то, что у меня есть сенсорная камера.

Как видим, усреднённые предложения не очень информативные, но в целом неплохо отражают настроение отзывов и некоторые аспекты описываемых товаров.

Сложение и вычитание предложений

Мы помним и любим word2vec за поддержку прикольных алгебраических операций над векторами слов, в духе «king + woman — man = queen». Оказывается, LaBSE так тоже умеет!

embeddings = encode(['король', 'женщина', 'мужчина']) print(decode(embeddings[[0]] + embeddings[[1]] - embeddings[[2]])) # ['королева']  embeddings = encode(['Лондон', 'Франция', 'Англия']) print(decode(embeddings[[0]] + embeddings[[1]] - embeddings[[2]])) # ['Париж']

Более того, LaBSE может складывать и вычитать не только слова, но и небольшие фразы. С длинными текстами у него получается хуже, но зато декодер иногда прикольно додумывает детали:

embeddings = encode([     'Это произошло во время правления Петра Первого.',      'Иван Грозный',     'Пётр Первый', ])   print(decode(embeddings[[0]] + embeddings[[1]] - embeddings[[2]])) # ['Это произошло в режиме правления Ивана Грозного.']  embeddings = encode([     'Кошка обучает своих котят охотиться за мышами.',      'белый медведь',     'кот', ])   print(decode(embeddings[[0]] + embeddings[[1]] - embeddings[[2]])) # ['Белый Медведь обучает медведям следить за охотой на оленей.']  embeddings = encode([     'Я не хочу делать прививку, потому что не доверяю врачам.',      'Я верю в народную медицину.',     'Я не доверяю никаким врачам.', ])   print(decode(embeddings[[0]] + embeddings[[1]] - embeddings[[2]])) # ['Я верю в вакцинацию, потому что я хочу лечиться.']

Перенос стиля текстов

Как мы видели в примере с отзывами, векторы LaBSE сохраняют информацию о стиле и настрое текстов. Получается, если мы возьмём несколько пар текстов с похожим смыслом, но в разном стиле, то средняя разница между их векторами может отражать разницу между стилями. Может быть, её можно использовать для изменения смысла других текстов, как в статьях про TextSETTR или DIFFUR?

Для примера возьмём отсюда примеры сдержанных и эмоциональных текстов. Примеры на английском, но LaBSE на это плевать.

texts_reserved = [     "That is a very pretty painting.",     "I’m excited to see the show.",     "I’m surprised they rescheduled the meeting.",     "This specimen is an example of the baroque style.",     "After the performance, we ate a meal.", ] texts_emotive = [     "OMG, that’s such a beautiful painting!",     "I’m sooo excited to see the show, it’s going to be stellar!!",     "I absolutely can not believe that they rescheduled the meeting!",     "This wonderful specimen is a truly spectacular example of the baroque style.",     "After the superb performance, we ate a delicious meal.", ] delta = encode(texts_emotive).mean(0) - encode(texts_reserved).mean(0)  print(decode(encode('Этот фильм произвёл на меня хорошее впечатление.') + delta * 1)) # ['Этот фильм мне очень понравился хорошим впечатлением!']  print(decode(encode('Внешний долг США достиг рекордной величины.') + delta * 1.5)) # ['Увеличенная США долговая задолженность в целом достигла рекордных размеров!']

Видим, что перефразированные тексты действительно стали более эмоциональными и экспрессивными.

Другой пример – превращение формальных текстов в неформальные:

texts_formal = [     "This was a remarkably thought-provoking read.",     "It is certainly amongst my favorites.",     "We humbly request your presence at our gala on the 12th.", ] texts_informal = [     "reading this rly makes u think",     "Its def one of my favs",     "come swing by our bbq next week if ya can make it", ] delta = encode(texts_formal).mean(0) - encode(texts_informal).mean(0)  print(decode(encode('Убедительно просим вас покинуть помещение учреждения.') - delta * 0.5)) # ['пожалуйста, уходите из помещения']  print(decode(encode('Был рад нашей с Вами встрече!') - delta * 0.5)) # ['Хорошо встретился с тобой!']

Как видим, оно тоже приблизительно работает.

Ещё я пробовал применить этот подход для детоксификации текстов, но оказалось, что LaBSE понимает смысл грубых текстов на русском языке не очень хорошо – видимо, в его обучающей выборке таких было немного.

Заключение

Энкодерами предложений в последнее время занимаются довольно много, и генераторами текста (такими, как GPT) – тоже. Но к таким декодерам, которые бы инвертировали работу энкодера, интерес в последнее время угас (хотя когда-то автоэнкодеры были модной штучкой). Возможно, зря: как видим, для инвертированного энкодера в 2022 вполне можно найти любопытные применения.

Мой декодер (cointegrated/rut5-base-labse-decoder) выложен на HF; вы можете использовать его в паре с облегчённым русско-английским энкодером cointegrated/LaBSE-en-ru или с полноценной моделью на 100+ языков sentence-transformers/LaBSE. В любом случае, лайкайте понравившиеся вам модели, и пишите в комментарии об интересных кейсах их применения. Подписывайтесь на мой канал, пользуйтесь солнцезащитным кремом и боритесь за мир!


ссылка на оригинал статьи https://habr.com/ru/post/677618/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *