Немного о библиотеке 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/
ссылка на оригинал статьи https://habr.com/ru/articles/1055588/