“Не значил он анапеста от анапеста, как мы ни колотились отличить.” nevmenandr В настоящий момент количество курсов, на которых вы сможете получить знания, необходимые для трудоустройства по специальности «Аналитик данных» (Data Scientist, ML Engineer), растет и растет. И это замечательно. Но количество информации, которую требуется усвоить, просто зашкаливает и, даже уже работая в этой области, постоянно удивляешься, сколь многое ты еще не знаешь. Скорость появления новых словосочетаний в DS-специальностях тоже крайне высокая, но за многими страшными словосочетаниями может прятаться совершенно простой смысл. Кроме того, часто подход к задаче следующий: сначала навесить на нее некоторый ярлык - например, object detection и копать в этом направлении. И это правильный и эффективный подход. Оказывается, что иногда несколько подходов с разными названиями могут решать одну и ту же задачу. Недавно в работе Центра компетенции больших данных и искусственного интеллекта ЛАНИТ (ЦК ДАТА), мы столкнулись с одной задачей по классификации текстов, на которой мне захотелось такой эффект продемонстрировать.  Про основные подходы к классификации текстов в интернете можно найти много информации, в том числе и на тех самых курсах, про которые я уже упоминал. На мой взгляд, из недавних курсов наиболее интересен вот этот.    Тем не менее очень важную роль играет чистка текста при самостоятельном его анализе или анализе результатов работы ваших моделей (что на самом деле важно и в достаточной степени трудоемко, см., например, статью.  Теперь немного о задаче. Не вдаваясь в подробности, скажу, что в некотором роде это задача многоклассовой классификации. Вот только проблема в том, что каждый день часть классов может терять свою актуальность, и возникают новые классы, для которых число объектов близко к 0. Примером может служить следующий кейс: положим вы публикуете какие-то заметки, кто-то их читает и задает вопросы. И вы замечаете, что вопросы часто повторяются, т.е. по смыслу спрашивают одно и то же. И вам хотелось бы написать ответ всего один раз, а дальше, чтобы он сам отправлялся всем последующим интересующимся. От заметки к заметке тематика меняется и вопросы тоже. И оказывается, что в таких условиях обычные подходы перестают работать. Хотя есть и хорошая новость - описание класса также дано на естественном языке (в примере это ответ на вопрос). Здесь мы встречаемся с первым “страшным” словосочетанием few-shot learning. Решать такую задачу принято, переводя текст каждого объекта (здесь и далее “объект” - это текст, который мы хотим классифицировать) в такое числовое признаковое пространство, в котором объекты одного класса будут расположены близко, а разных классов - далеко. Тогда даже для нового класса по его текстовому описанию или по описанию первого же объекта, который для этого класса встретится, можно будет классифицировать новые объекты, считая расстояния в упомянутом признаковом пространстве. Что представляет собой это признаковое пространство для текстов? Что угодно. Возьмем такой пример: пусть у нас есть текст про звезды, текст про телескопы и текст про экологию. Понятно, что это все разные тематики, но где-то есть связь. Предположим, что у нас для каждого текста будет последовательность из трех чисел от 0 до 1, связанных с прилагательными “космический”, “антропогенный”, “геоцентричный”. Я бы поставил в таком случае нашим трем текстами такие числа: [1.0, 0.0, 0.0], [0.5, 1.0, 0.1], [0.1, 0.3, 0.8]. А вы? Если честно посчитать евклидово расстояние, то окажется, что звезды будут так же близко к телескопу, как телескоп к экологии, но дальше от экологии. Если вы считаете иначе, выберите другие числа и подберите признаковое описание текстов, которое лучше их разделяет.  В целом через подходы типа “мешка слов” или tf-idf мы тоже получаем некоторое признаковое описание объектов, но, во-первых, это плохо работает, а во-вторых, вектора получаются очень длинные (число слов в словаре меньше, если мы отфильтровали словарь, но все равно много). Можно было бы уменьшить размерность признакового пространства через какой-нибудь SVD, но работать будет все так же плохо, потому что в любом случае подразумевает, что слова в тексте расположены независимо друг от друга в случайном порядке. В целом нам даже не обязательно переходить в признаковое пространство.  Главное - научиться измерять расстояние между объектами. И когда хочется измерить расстояние между текстами, то на ум может прийти расстояние Левенштейна (с некоторыми дополнениями представлено в библиотеке fuzzywuzzy). Вот только с длинными текстами такой подход тоже работает плохо, да и он недалеко ушел от “мешка слов”.  И вот наконец на сцену выходит самое трендовое слово последнего времени - “эмбеддинг”. Чаще всего в таком случае идет речь о некотором обучаемом векторном представлении какого-либо объекта (в данном случае слов или кусков слов), которое наилучшим образом отражает что-то. А оно на самом деле ничего не отражает, а просто наилучшим образом решает поставленную задачу. И вот здесь мы вплотную подобрались к вопросу о том, как этот эмбеддинг получить. На этом моменте принято добавлять рисунки с некоторым набором слов в двумерном пространстве, которые, о чудо, оказываются рядом друг с другом по смыслу. Я же приведу один отрывок, где слова заменены на ближайшие по косинусному расстоянию в пространстве Word2Vec эмбеддингов (спасибо этому репозиторию) обученных на классике русской литературы, а вам предлагаю отгадать, откуда отрывок: Интересно, что часть слов в приведенном отрывке являются синонимами к оригинальным, ну или хотя бы близкими по значению с человеческой точки зрения, а часть антонимами. Связано это с принципом получения этих эмбеддингов: близость слова задается окружающими его словами. Если слово “низкий” употряблеяется также как “высокий”, то и будет в таком случае выбираться оно. И действительно все относительно - в зависимости от контекста сравнения один и тот же объект может оказаться низким, а может оказаться высоким. Более или менее абсолютное значение эти слова имеют, когда речь идет о голосе, но такого употребления в обучающей выборке могло не найтись. При увеличении объема и разнообразия обучающей выборки могли бы проявиться какие-то другие слова, но автор скорее всего обучал лишь на нескольких десятках текстов русской классики, часть из которых достаточно короткие. Самый простой вариант - посчитать эмбеддинги слов (или их частей) через word2vec-подобные модели и усреднить для каждого текста или же взвесить через тот же tf-idf. А можно взять языковую модель для текстов, а не для слов, например, BERT. По сути она делает то же самое, что мы сделали для текстов про телескопы и звезды, только о мире она не знает ничего, и всю информацию берет из текстов, которые ей дают. А числа проставляет от обратного. Выше я поставил числа, основываясь на каких-то знаниях, а языковая модель расставляет хоть как-то, лишь бы ее не ругали. Поэтому для каждого текста также будут получаться числа, но каждое из них само по себе не интерпретируемо. Более того, сформировано с целью решить конкретную задачу. Представьте, что вы работаете в библиотеке, и вам грузчик привозит каждый день книги и сваливает на пол. Затем приходит начальник и кричит на вас, требуя, чтобы вы расставили книги по тематикам. Сначала пихаете Пикуля в раздел “Романтической фантастики”, но со временем после нескольких угроз увольнения, наконец, начинаете отличать ямб от хорея в отличии от Онегина. Но когда к вам обращаются с просьбой найти конкретного автора, например, Беляева, вы теряетесь, потому что внутри тематики “Русская и советская фантастика” у вас - полный хаос, как и в других тематиках, а про расстановку по алфавиту в вашем мире со сферическими конями в вакууме не слышали. Потому что за это вас не ругали. И благо библиотека transformers от Hugging Face сделала использование таких языковых моделей крайне простым делом. Вот только изначально эти модели учатся на каких-то наборах данных общего назначения типа википедии, а тематика ваших текстов является каплей в море этого разнообразия. Вот предположим, не знаете вы ничего про мировую историю и видите два текста: один про Октавиана Августа, а другой - про Рюрика. Да, это все по древней истории, скажете вы, не имея возможности вдаваться в подробности. Вот и языковая модель, обученная на Википедии, ваши тексты воспримет как все из одной тематики, и разницу в них не уловит. Следовательно, нужно дообучить эту языковую модель на ваших текстах, чтобы она научилась отличать, что Рюрик - это про Русь, а Октавиан Август - про Римскую империю.  И вот оказывается, что подходов к решению этой задачи может быть несколько. В отличии от Рюрика и Октавиана изначально они могут казаться совершенно разными, но все они приводят в той или иной степени к одинаковому результату.  У кого-то при чтении постановки задачи уже возникали слова paraphrase classification. Ведь есть у нас объект с описанием на естественном языке, есть класс с описанием на естественном языке и, поставив их в одном тексте через ‘[SEP]’, можно научиться выдавать класс 0 или 1 по тому признаку, подходит ли описание класса к объекту. Модель будет состоять из собственной языковой модели и линейного слоя с одним нейроном на выходе. Применять обученную модель можно по-разному. Можно в нативном виде, но тогда придется для каждого объекта запускать модель M раз вместо 1, где M - число классов на настоящий момент, что очень затратно по вычислительным ресурсам. Легче взять только языковую часть, которая уже научилась лучше переводить разницу между нашими текстами в цифры и пропускать через нее отдельно текст объекта и тексты классов и уже между ними считать расстояния. При обучении тоже возникает проблема в том, что текст объекта и класса вместе требуют большую длину входящей в модель последовательности, которая, как правило, ограничивается 512 кусками слов и таким образом требует больше затрат по памяти.  Кто-то скажет, что это же metric learning - нужно научить модель переводить текст в эмбеддинги так, чтобы расстояния между объектами одного класса было меньше. В нашем случае можно прогонять последовательно языковую модель на объектах и на описаниях классов и считать расстояние между ними. Простой вариант - считаем косинусное расстояние и сравниваем с меткой 1 или -1 в зависимости от того, подходит он или нет, или применяем уже более качественный подход - используем Triplet Loss с тройкой: объект, подходящее описание класса, неподходящее описание класса. Еще одно страшное слово, а суть лишь в том, что мы хотим, чтобы расстояние (евклидово) от объекта до правильного класса было меньше, чем расстояние до неправильного. Но если хотеть так просто, то оно может быть меньше всего на 0,0001%, что неудовлетворительно, поэтому нужно, чтобы было меньше на какой-то отступ (margin).  Проблема здесь возникает при обучении: перед обратным распространением ошибки нужно запомнить промежуточные значения для двух или трех прогонов моделью по объекту и (не)подходящим классам, что дает большую нагрузку на память.  И, наконец, третий вариант - отойти от модных слов и запустить многоклассовую классификацию с простым softmax на 400 классов (примерно столько их было в очищенной тренировочной выборке за полгода). И хотя такой классификатор использовать не получится, поскольку в тестовой выборке (при использовании модели) из этих 400 классов окажется лишь 10 еще актуальных, а через неделю и они пропадут, языковая модель оказывается также вполне валидной. Такая модель получается из собственно языковой модели, линейного слоя с 400 нейронами на выходе и активацией softmax. А поскольку у нее не возникает проблем нагрузки на память, как у предыдущих двух подходов, для нее можно увеличить длину последовательности токенов и/или размер батча. Если посмотреть литературу по решению такого типа задач, то присутствуют как решения по первому пути, так и по второму. Третий естественно в статьях не освещается. Если через tSNE перейти в двухмерное пространство, то распределение объектов в нем до дообучения(слева) и после(справа) будет выглядеть следующим образом: Цветом обозначены реальные классы объектов и видно, что после дообучения классы явно лучше кластеризуются, чем до. Кроме того, выделяются явные отдельные тематики, которые интересно исследовать. Эмбеддинги нужно использовать для классификации, и самый простой способ - вычислить расстояние между объектом и каждым из классов и найти ближайший класс к объекту. От такого решения недалеко до использования алгоритма k ближайших соседей (kNN), который обучается каждый раз, когда меняется множество классов или каждый день, а если некоторый класс актуален более одного дня, то можно добавлять объекты предыдущего дня в обучающую выборку. Так в итоге задача и была решена.  Выше я описал как можно решить одну задачу несколькими способами, но в литературе по обработке естественного языка наблюдается и еще одно движение: решение нескольких задач одним способом. Вот, например, авторы статьи предлагают подход CrossFit по созданию языковой модели общего назначения, используя набор данных для целого набора задач. Для этого они делают несколько интересных вещей. Делят набор данных на “обучение” и “тест”, не только исходя из фактора данных, но и из фактора задачи. Так часть задач они используют для обучения модели, а часть откладывают для тестирования. Вводят единую нотацию, по которой модель получает на вход текст и на выходе ожидает текст, вне зависимости от того классификация ли это или question answering. Вводят обобщающую метрику, которая позволяет усреднять изменения метрик качества для разных задач, что необходимо поскольку метрики в разных задач различаются. Советую статью для ознакомления - в ней также приводятся ссылки и на другие работы в этом направлении. Изучение интересных статей - одно из направлений работы ЦК ДАТА в рамках задачи накопления экспертизы, которую мы используем для решения нестандартных задач и для помощи другим подразделениям Ланит, и мы будем очень рады, если в комментариях вы сможете что-то посоветовать по освещенной теме.  В заключении хочется сказать, что за сложными словами могут стоять простые принципы и не нужно их бояться, а, изучив, не надо навешивать на определенные задачи как ярлык, а стараться мыслить шире. DS Team Lead