Привет, меня зовут Данил Астафуров, я стажёр в команде лаборатории машинного обучения в Альфа-Банке, работаю над кредитным скорингом. В этом году я поучаствовал в соревнованиях «Цифровой прорыв: сезон искусственного интеллекта», на котором занял второе место. Это единственный технологический проект от АНО «Россия — страна возможностей».
Соревнование длилось месяц и я был на первом месте с первого решения. Но за неделю до конца соревнования участников стал резко больше и меня обогнали. Отрыв от второго места был почти 0.1 (хотя там можно было скрыть лучший результат). Занятость стажера не давала отвлекаться, поэтому было решено, что пусть идёт всё своим чередом. Сейчас, спустя время, я могу разобрать своё решение и понять, как всё же можно было добраться до первого места.
Описание задачи
На соревновании было 3 задачи: сегментация от РЖД, регрессия по тексту от РБК, и привязка фотоснимков к местности от МФТИ.
Я участвовал в решении второй задачи. Предлагалось по характеристикам новостной статьи спрогнозировать количество просмотров, среднюю долю прочитанного объёма и процент тех, кто прочитал статью полностью. Правда, не очень понятно, корректно ли считали последние две переменные.
По словам организаторов, такое решение поможет им определять, какие тематики будут популярны, соответственно, писать тексты, попадающие под запрос пользователя. Забегая вперёд скажу, что тематика в привычном понимании (политика, культура и т.д.) почти не влияет на таргет, а одной из главный фичей оказалась длина текста. ИИ доказал, что краткость — сестра таланта.
Данные
Входные данные.
|
---|
Последние 3 признака нужно было предсказать.
Организаторы разрешили допарсить статьи и взять любую информацию, поэтому берём текст и количество картинок.
Замечательно, что в статье доступна информация о новом количестве просмотров, но ещё более необычен факт, что новое количество просмотров меньше, чем данное в датасете. Наверно, как лайки, можно и убрать. Всего было 9 уникальных категорий, но, в основном политика — какие времена, такие и нравы. С авторами интереснее, знакомьтесь: ['54244e01cbb20f03076b236d','5878a2ec9a7947e531faf52c'] — создатели статьи. Судя по данным, размер редакции — 101 автор.
Тег — самая странная вещь. Судя по их количеству и названию, каждый придумывает, что хочет, поэтому уникальных чуть больше чем 5000. В среднем, на каждую статью таких по 4.
Про кликабельность особо ничего не было известно, какая-то величина чуть больше 1, иногда возрастающая до 15. Но всё ещё не понимаю, как доля тех, кто прочитал статью полностью, превышала 100% см.
Генерация фичей
Времени на раскачку нет, мы же в первый день хотели в топе быть, поэтому особо над фичами думать некогда:
из заголовка и текста получим длины в символах;
в словах среднюю длину слова;
категорию неизменно отправим в CatBoost;
дату распарсим, вычислим день недели, время суток, количество дней с момента публикации.
С коллегами в ds-чатах были дискуссии по поводу обработки списков авторов и тегов, ведь при кодировании one-hot признаков получалось около 5 000. Но вникать особо смысла не вижу: если после кодирования, оставить только тех, кто встречается хотя бы 10 раз на 7 000 строк, то получим 200 признаков, что вполне устраивает.
|
---|
Bert, deberta, gpt это тоже you need
Часто вижу в качестве решения извлечение фичей из текста руками или CountVectorizer'ом и обучением бустинга. А продвинутый пайплайн представляет из себя новый BERT от Сбера с линейным слоем.
Хочется как-то объединить подход с фичами (особенно, когда в таблице не только текст) с сильными трансформерами, «понимающими» текст.
Давайте пропустим текст через энкодер предобученного трансформера, last_hidden выход через разные пулинги и объединим. А что нам мешает взять таких несколько? В эпоху HuggingFace эта идея ограничена лишь объëмом ОЗУ. По итогу получили много фичей, обучим на этом CatBoost, и всё это без больших затрат GPU.
В этой идее надеемся на то, что современные модели хорошо понимают текст и лучше всего переведут его в информативный вектор. Выбор моделей небольшой, вообще ещё мало, что есть для русского языка, а SOTA вроде deberta вообще не существует для него.
Попробуем взять топовые мультиязычные модели. Как правило, русского языка в них мало, но они неплохо себя показывают. С файнтюнингом не пришлось заморачиваться, русскоязычные модели предобучаются как раз на новостных корпусах.
Здесь считаем средний вектор, работает чуть лучше, чем брать просто нулевой.
def mean_pooling(model_output, attention_mask):
token_embeddings = model_output[0].detach().cpu()
input_mask_expanded = attention_mask.unsqueeze(-1).expand(token_embeddings.size()).float()
sum_embeddings = torch.sum(token_embeddings * input_mask_expanded, 1)
sum_mask = torch.clamp(input_mask_expanded.sum(1), min=1e-9)
return sum_embeddings / sum_mask
Получим фичи каждой модели по каждому столбцу.
def make_features_transformers(df, model_name, df_model, col, max_len):
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModel.from_pretrained(model_name).cuda()
text_features = []
Каждое предложение кодируем и векторизуем.
for sentence in tqdm(df[col]):
encoded_input = tokenizer([sentence], padding='max_length', truncation=True, max_length=max_len, return_tensors='pt')
with torch.no_grad():
model_output = model(input_ids=encoded_input['input_ids'].cuda())
sentence_embeddings = list(mean_pooling(model_output, encoded_input['attention_mask']).numpy())
text_features.extend(sentence_embeddings)
text_features_df = pd.DataFrame(text_features, columns = [f'{df_model}{col}feature{i}' for i in range(len(text_features[0]))])
return text_features_df
Моделей мало, берем все, ниже список.
|
---|
Признаки мы можем получить не только из текста, но и из заголовка. Осталось теперь решить, как обучиться на 20 000 фичах. Понятно, что большинство из них малоинформативны, а некоторые так вообще около константы. Поэтому для отбора лучших признаков можем разбежаться от рандомного выбора до catboost.feature_importances и SHAP — проверяйте гипотезы.
Я просто взял первые по важности признаки, исходя из мнения CatBoost. А именно обучил модель регрессии на 20 000 фич, их отсортировал по значению из catboost.feature_importances_, и, в дальнейшем, обучал CatBoostRegressor уже на первой по важности тысяче.
Ещё есть решения? Или это конец?
В финале лучше потюнить CatBoost под каждый таргет, обучить каждый на 20 фолдах и усреднить предсказания на тесте (объяснение).
def create_folds(data, target, num_splits=3):
if num_splits > 1:
data.loc[:,'kfold'] = -1
X = data.drop([target], axis=1)
y = data[[target]]
mskf = KFold(n_splits=num_splits, shuffle=True, random_state=42)
for fold, (trn_, val_) in enumerate(mskf.split(data)):
data.loc[val_,'kfold'] = fold
else:
data.loc[:,'kfold'] = 0
return data
Как и стоило ожидать, круче оказываются решения, где более кропотливо посидели над генерацией фичей. Обладатель 1-го места, помимо трансформеров, применил TF-IDF и UMAP.
Но, оказывается, если этого не сделать, можно немного потерять. Поэтому сохраняйте себе пайплайн и пользуйтесь в качестве бейзлайна, чтобы не тратить время на рутину. Советую больше участвовать в соревнованиях, чтобы научиться использовать подобные решения, а глубже погружаться в задачу и получать призы за первое место.
Рекомендуем почитать [подборка редактора блога]
Нейросетевой подход к кредитному скорингу на данных кредитных историй
Как и зачем мы начали искать бизнес-инсайты в отзывах клиентов с помощью машинного обучения
Также подписывайтесь на Телеграм-канал Alfa Digital — там мы постим новости, опросы, видео с митапов, краткие выжимки из статей, иногда шутим.