Немного о библиотеке LANCETNIC

Логотип
Логотип

LANCETNIC это библиотека для поиска взаимосвязей между признаками объекта и целевой переменной. Классификация, регрессия и многозадачное обучение на размеченных данных.
С помощью библиотеки можно в пару строчек кода обучить небольшую модель для решения задач Классификации, Регрессии и их комбинаций.

Вот GitHub репозиторий для тех кто хочет подробнее ознакомиться:

👉 https://github.com/Lancet52/lancetnic

Но вернемся к тому о чем хочу написать.

Недавно столкнулся с проблемой: моя библиотека lancetnic при обучении на больших текстовых датасетах просто перегружала оперативную память. На ноуте в 16 ГБ RAM модель не могла обучиться даже на 25 тыс. строк. Разбирался. Я начал разбираться и нашёл пару причин критического перерасхода памяти.

Причина № 1. Плотные матрицы вместо разряженных

TfidfVectorizer (который используется для векторизации текстовых данных) из sklearn по умолчанию возвращает разреженную матрицу (sparse). Он хранит только ненулевые значения.

TfidfVectorizer обрабатывает весь датасет и собирает все уникальные слова которые встречаются хотя бы в одном сообщении. Формируется словарь.

Для наглядности возьмём датасет спам сообщений:

№ строки

Текст

1

Мастер маникюра. Обучим от 7000 в день

2

Куплю iPhone 15 недорого. Срочно

3

Бесплатный кредит без справок за 1 час

4

Мастер маникюра.Пиши в Личные сообщения

Векторайзер собирает уникальные слова из всего датасета: мастер, маникюра, обучим, от, 7000, в, день, куплю, iphone, 15, недорого, срочно, бесплатный, кредит, без, справок, за, 1 час, пиши, личные, сообщения и т.д. Из всего этого формируется словарь в виде таблицы, где каждое сообщение превращается в строку из большого количества чисел (по одному числу из словаря):

мастер

маникюра

обучим

от

7000

в

день

куплю

iphone

15

срочно

бесплатный

кредит

пиши

личные

...

0.12

0.08

0.15

0.02

0.20

0.01

0.04

0

0

0

0

0

0

0

0

...

0

0

0

0

0

0

0

0.18

0.25

0.30

0.22

0

0

0

0

...

0

0

0

0

0

0

0

0

0

0

0

0.15

0.20

0

0

...

0.12

0.08

0

0

0

0

0

0

0

0

0

0

0

0.18

0.11

...

Как видно, если слова нет в строке то столбец заполняется нулём. Чем больше датасет, тем больше слов и тем больше таблица (словарь).

ТехническиTfidfVectorizer создаёт матрицу которая хранит только ненулевые значения (это называется sparse или разреженная матрица).

В моем коде (в версии lancetnic 4.0.0) при векторизации я создавал полноценную матрицу с нулями и выглядело это выглядело так:

# Векторизация тектовых данных
def vectorize_text(text_column, df_train, max_features):
    if isinstance(text_column, str):
        text_column = [text_column]
    text_encoder_list = []
    vectorizers = []
        
    for text_col in text_column:
        vectorizer_text = TfidfVectorizer(max_features=max_features)
        # Вот тут
        text_encoder = vectorizer_text.fit_transform(df_train[text_col].fillna('')).toarray()
        text_encoder_list.append(text_encoder)
        vectorizers.append(vectorizer_text)
    # И вот тут
    combined_text = sp.hstack(text_encoder_list).tocsr()

    return combined_text, vectorizers

И если на датасете в 5000 строк это не выглядело критично для ноутбука, то на 25000 строк это съедало всю оперативную память. Поэтому решение этой проблемы было простым: я убрал .toarray() и оставил данные в sparse-формате:

# Векторизация тектовых данных
def vectorize_text(text_column, df_train, max_features):
    if isinstance(text_column, str):
        text_column = [text_column]
    text_encoder_list = []
    vectorizers = []
    for text_col in text_column:
        vectorizer_text = TfidfVectorizer(max_features=max_features)
        # Теперь так
        text_encoder = vectorizer_text.fit_transform(df_train[text_col].fillna('')) 
        text_encoder_list.append(text_encoder)
        vectorizers.append(vectorizer_text)
    # И вот так
    combined_text = sp.hstack(text_encoder_list).tocsr()

    return combined_text, vectorizers

Ну и далее по коду все поправил.

Причина № 2. Двойное хранение данных в PyTorch Dataset

Класс Dataset при создании сразу конвертировал весь массив X в PyTorch-тензор через torch.tensor(X).
На примере кода для классификации:

class ClassifierDataset(Dataset):
    def __init__(self, X, y):
        # Создание копии в памяти
        self.X = torch.tensor(X, dtype=torch.float32)
        self.y = torch.tensor(y, dtype=torch.long)

    def __len__(self):
        return len(self.X)

    def __getitem__(self, idx):
        return self.X[idx], self.y[idx]

В конструктор класса Dataset передавалась матрица после TF-IDF, да к тому же еще и в полном виде (см. Причину № 1). Строкой self.X = torch.tensor(X, dtype=torch.float32) все это превращалось в Pytorch тензор. Это создавало вторую копию данных в памяти. Получалось, что данные хранились дважды.
Новый код:

class ClassifierDataset(Dataset):
    def __init__(self, X, y):
        self.is_sparse = sparse.issparse(X)
        self.X = X
        if not self.is_sparse:
            self.X = torch.tensor(X, dtype=torch.float32)
        self.y = torch.tensor(y, dtype=torch.long)  

    def __len__(self):
        return self.X.shape[0] 

    def __getitem__(self, idx):
        if self.is_sparse:
            x = torch.tensor(self.X[idx].toarray(), dtype=torch.float32).squeeze(0)
        else:
            x = self.X[idx]
            
        return x, self.y[idx]

В конструкторе класса теперь остается только ссылка на массив из TF-IDF. А в getitem я беру только одну строку из матрицы и превращаю ее в тензор. Теперь в памяти в каждый момент времени находится только один батч, а не весь датасет.

Резюмируя

Теперь обучение на больших данных происходит и на относительно слабом железе. Это не все проблемы в моей библиотеке, но на одну проблему стало меньше. В планах расширить функционал.

Буду рад всевозможным отзывам и обратной связи. Заинтересован чтобы LANCETNIC прошёл обкатку на реальных кейсах. Парочка таких уже есть. И один из них это антиспамбот (TAB) для телеги. О нём я писал уже в своей статье на Хабре: ссылка на статью

Официальный сайт LANCETNIC: https://lancetnic.ru/
GitHub: https://github.com/Lancet52/lancetnic
Блог разработки в ТГ: https://t.me/markovstate
Дашборд антиспам бота TAB: https://tab.lancetnic.ru/