# 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 оно отсутствует.
## Концептуальная схема

### Train

На вход путь до папки с json файлами после разметки (см. главу Input) и json конфиг с описанием фич. Далее:
1. Проверяем валидность пути и наличие файлов.
- Если проверка не прошла, кидаем Exception, пишем в лог и выходим.
2. Создаем объект FeatureExtractor. Конструктору передаем конфиг с описанием фич.
- если конфиг неверный или не хватает фич, генерируется Exception с записью в лог. Про этот блок будет подробнее дальше.
4. Создаем структуру для хранения результатов (которую потом наполним и выгрузим в csv).
5. В цикле идем по json файлам с разметкой:
- отдельным методом (или классом) загружаем файл и парсим;
- вызываем у FeatureExtract.Extract(data), получаем извлеченные фичи;
- добавляем фичи в общую структуру с результатами.
6. Заполненную структуру с результатами либо выгружаем в CSV, либо отдаем напрямик CRF для обучения.
### Predict

На вход ответ от DTES с извлеченным текстовым слоем (в ответе интересует секция textSegments, pages и, возможно, tables) и метаданные модели (где нас интересует секция features):
1. Создаем объект FeatureExtractor. Конструктору передаем конфиг метаданными модели.
- если конфиг неверный или не хватает фич, генерируется Exception с записью в лог. Про этот блок будет подробнее дальше.
2. Отдельным методом (или классом) загружаем файл и парсим (в данном случае вместо файла ответ от DTES, но это тот же json);
3. Вызываем у FeatureExtract.Extract(data), получаем извлеченные фичи;
4. Извлеченные фичи либо выгружаем в CSV, либо отдаем напрямик CRF для обучения.
## Детализированное описание блоков
### Create Feature Extractor

Отдельно хочется отметить:
+ проверку существования реализации фичи указаной в конфиге делать по аналогии с форматерами в DFES;
+ важно создавать фичи в том же порядке, в каком они указаны в конфиге.
### FeatureExtractor.Extract(Data)
Блок: "Извлечь фичи fe.Extract(Data)".

> Видится, что асинхронный вызов фич сейчас не нужен, т.к. у нас они все простые. В итоге больше времени на переключение контекста потратим, нежели на сам расчет. Можно пока сделать последовательно, после распараллелить. Идеи по последнему в пункте: "Возможности оптимизации/расширения".
В конструкторе мы уже создали коллекцию с объектами фич.
Теперь асинхронно вызываем метод 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
```