# Feature Extractor Тех. проект. # Открытые вопросы ### - Что делать, если текст смешанный, из двух языков? Как он будет обрабатываться? На данный момент к каждому слову будут применяться фичи описанные в конфиге. Как обрабатывать подобный текст, разграничивая фичи вопрос пока открытый. ### - А будет ли разделение фич по категориям? Фичи-словари, фичи для частей речи и пр. Предлагаю разграничивать на уровне префиксов. Т.к. технически фичи работают по одному и тому же принципу. ### - Где хранить параметры для CRF? # Закрытые вопросы ### - Почему в CSV разделитель - пробел? Может нормально сделаем? Переделал на запятую. ### - Как будет выполняться дообучение? Допустим, у нас есть исходная модель и мы хотим дообучить ее на данных клиента. Можно взять CSV с фичами для текущей модели и смержить ее с новой CSV данных клиента. Затем запустить обучение/дообучение. ### - Как внутри будет работать FeatureExtractor? Описано в главе "Feature Extractor (внутреннее устройство)". ### - Как будет происходить валидация? Допустим, если я добавил фичу в конфиг, но не реализовал ее. Или я вызвал Predict с конфигом модели, а реализации нужной фичи нет? Валидация будет хардкорной. Если фича указана в конфиге, а реализации для нее нет - бросается Exception, пишется в лог и прекращается работа. Т.к. для train обучать с пропущенными фичами нет смысла, а для Predict это чревато долгим поиском причины ухудшения метрик. ### - Я, как аналитик данных, хочу добавить новую фичу. Как мне это сделать? По аналогии с форматтерами в DFES. 1. Имеется базовый класс для фичи, от которого нужно унаследоваться и реализовать его интерфейс. 2. Необходимо добавить название своего класса-фичи в коллекцию со всеми фичами. Таким образом происходит регистрация. ### - А если моя фича генерируется на основе других фич? Как это можно реализовать? Трудоемко получится и замедлит генерацию фич. Но сделать можно примерно так: 1. Создать такую иерархию: BaseFeature, Feature:BaseFeature, FeatureForFeatures:BaseFeature. 2. Унаследовать все стандартные фичи от Feature (а не BaseFeature). 3. Изменить реализацию в классе FeatureExtract, в методе Extract(Data): + в первом проходе по коллекции фич запускать только наследников от Feature; + собрать с них результат; + во втором проходе запустить наследников от FeatureOfFeatures; + собрать все результаты и смержить; - Альтернатива: завести отдельную коллекцию для фич над фичами и запускать их вообще отдельно. Но нужно аккуратно мержить результаты, что бы извлеченнные фичи были в том же порядке, в каком указаны в конфиге. ### - Как будет разграничиваться работа с rus/en текстом и фичами для языков? Технически никак не будет разграничиваться, т.к. принцип работы фич для разных или смешанных языков одинаков. А вот семантически имеет смысл разграничивать. После реализации нужно будет описать как добавлять новые фичи и как их надо называть Для Фич под конкретный язык нужно добавлять префикс в название. Для универсальных фич - просто название, без префиксов. В рамках контекста уже подбирать набор фич для train и predict. ### - Если изменился способ расчета фичи, как только(!) ее обновить в существующей CSV с фичами? Для этого нужно будет пилить отдельный алгоритм, в текущей реализации такой возможности нет. # Требования, ограничения Так как встраивается в сервисы, необходимо сделать максимально быстрым и экономным: + минимальное использование памяти; + минимальное время работы; Фичи будут постоянно меняться, поэтому: + удобное и простое управление составом фич (добавление, удаление, конфигурирование списка в целом); + конфигурация фич должна быть синхронизирована с моделью в рамках Predict-а. # Input (train / predict) Json с данными на вход. Должны присутствовать: + слова; + координаты расположения слов; + номер страницы для каждого слова; + для train - поле с тэгом разметки; + информация о странице: + ширина; + высота; + количество символов. Формат json, ожидаемого на вход (для predict поля "tag" не будет): ``` { "results": [ { "textSegments": [ { "text": "20", "tag": "I_NUM", "position": { "page": 3, "top": 627.277161, "left": 2167.815, "width": 22.906786, "height": 32.9631271 } }, { "text": "года", "tag": "O", "position": { "page": 3, "top": 517.2772, "left": 2167.815, "width": 22.906786, "height": 60.9332733 } } ], "pages": [ { "text": "...", "metadata": { "number": 1, "width": 2479.0, "height": 3508.0, "length": 1098 } }, { "text": "...", "metadata": { "number": 2, "width": 3508.0, "height": 2479.0, "length": 1575 } }, { "text": "...", "metadata": { "number": 3, "width": 3508.0, "height": 2479.0, "length": 2753 } } ] } ] } ``` # Формат описания набора фич На основе этого json конфигурируется FeatureExtractor. В случае train он меняется вручную. В случае predict берется из метаданных модели. ``` { "features": [ "TextPosition", "ParagraphPosition", "Length", "XCoord", "YCoord" ] } ``` # Feature Extractor (внутреннее устройство) Структура для хранения всех извлеченных фич должна быть такой же, как и структура, которую возвращает FeatureExtractor.Extract(data). На вход как train, так и predict должен приходить один и тот же формат json описанный в главе Input. Единственное различие в том, что при train в textSegments есть поле "tag", а при predict оно отсутствует. ## Концептуальная схема ![](https://i.imgur.com/5DONdup.png) ### Train ![](https://i.imgur.com/LlvsEWr.png) На вход путь до папки с json файлами после разметки (см. главу Input) и json конфиг с описанием фич. Далее: 1. Проверяем валидность пути и наличие файлов. - Если проверка не прошла, кидаем Exception, пишем в лог и выходим. 2. Создаем объект FeatureExtractor. Конструктору передаем конфиг с описанием фич. - если конфиг неверный или не хватает фич, генерируется Exception с записью в лог. Про этот блок будет подробнее дальше. 4. Создаем структуру для хранения результатов (которую потом наполним и выгрузим в csv). 5. В цикле идем по json файлам с разметкой: - отдельным методом (или классом) загружаем файл и парсим; - вызываем у FeatureExtract.Extract(data), получаем извлеченные фичи; - добавляем фичи в общую структуру с результатами. 6. Заполненную структуру с результатами либо выгружаем в CSV, либо отдаем напрямик CRF для обучения. ### Predict ![](https://i.imgur.com/hPWMRJ2.png) На вход ответ от DTES с извлеченным текстовым слоем (в ответе интересует секция textSegments, pages и, возможно, tables) и метаданные модели (где нас интересует секция features): 1. Создаем объект FeatureExtractor. Конструктору передаем конфиг метаданными модели. - если конфиг неверный или не хватает фич, генерируется Exception с записью в лог. Про этот блок будет подробнее дальше. 2. Отдельным методом (или классом) загружаем файл и парсим (в данном случае вместо файла ответ от DTES, но это тот же json); 3. Вызываем у FeatureExtract.Extract(data), получаем извлеченные фичи; 4. Извлеченные фичи либо выгружаем в CSV, либо отдаем напрямик CRF для обучения. ## Детализированное описание блоков ### Create Feature Extractor ![](https://i.imgur.com/lgTnhsx.png) Отдельно хочется отметить: + проверку существования реализации фичи указаной в конфиге делать по аналогии с форматерами в DFES; + важно создавать фичи в том же порядке, в каком они указаны в конфиге. ### FeatureExtractor.Extract(Data) Блок: "Извлечь фичи fe.Extract(Data)". ![](https://i.imgur.com/TuSOKPh.png) > Видится, что асинхронный вызов фич сейчас не нужен, т.к. у нас они все простые. В итоге больше времени на переключение контекста потратим, нежели на сам расчет. Можно пока сделать последовательно, после распараллелить. Идеи по последнему в пункте: "Возможности оптимизации/расширения". В конструкторе мы уже создали коллекцию с объектами фич. Теперь асинхронно вызываем метод execute для каждой фичи. На вход каждой фиче подаем загруженные из json (текст, метаданные) данные. Естественно, ни одна фича не должна их менять - readonly. Каждая фича после работы хранит результат внутри себя. Это сделано, что бы не усложнять асинхронный вызов фич. После завершения работы всех фич, последовательно идем по коллекции и собираем результаты в одну структуру, которую возвращаем. > Важно добавлять извлеченные фичи в результат в том же порядке, в каком они лежат в коллекции. ## Возможности оптимизации/расширения Архитектура ориентирована на быструю работу Predict-a, а не Train. Для этого все фичи вызываются асинхронно для одного "документа". Точка улучшения: + фич будет явно больше чем четыре, поэтому особого профита по скорости при асинхронном вызове можем и не получить. + Для ускорения можно для "регистрации" фич использовать не одну коллекцию, а две. В первой регистрировать быстрые фичи, во второй - медленные. И параллелить только вторую категорию. + Либо, в базовом классе фичи завести признак, bool, надо параллелить или нет. И при вызове всех фич проверять признак запуская некоторые последовательно, а некоторые параллельно. Так как все они будут лежать по порядку в коллекции, а собираем мы результат после отработки всех фич - результаты не перепутаются. Точка улучшения для train: + Сейчас мы последовательно обрабатываем все файлы с BIO разметкой, при этом извлечение фич для каждого файла происходит асинхронно. + Можно сделать два метода в FeatureExtract: async_extract(data) && extract(data). Тогда можно извлекать фичи последовательно, а вот обработку файлов с BIO разметкой параллелить; Возможности масштабирования: + FeatureExtractor сам по себе не зависим после конструирования. Можно собрать пул этих сущностей для модели и использовать их при обработки входящих запросов; # Output CSV файл. Первая строка - заголовки полей, далее - данные. В конце всегда будет поле "tag". Для train будут выставлены соответствующие метки, для predict - "O" (Other). Пример: ``` Text,TextPosition,ParagraphPosition,Length,XPosition,YPosition,Tag WDD,0,0,3,30,6,B-ORG Software,4,4,8,35,6,I-ORG ```