В этой статье мы рассмотрим простую задачу, которая используется одной компанией в качестве тестового задания для стажеров на позицию ML-engineer. Она включает обнаружение DGA-доменов — задача, решаемая с помощью базовых инструментов машинного обучения. Мы покажем, как с ней справиться, применяя самые простые методы. Знание сложных алгоритмов важно, но куда важнее — понимать базовые концепции и уметь применять их на практике, чтобы успешно демонстрировать свои навыки.
DGA (Domain Generation Algorithm) — это алгоритм, который автоматически генерирует доменные имена, часто используемые злоумышленниками для обхода блокировок и связи с командными серверами.
В техничесокм задании присутствовали тестовые данные, для которых нужно было сформировать предсказания, и валидационные данные, на которых нужно было продемонстрировать метрики в формате:
Иногда компании не предоставляют тренировочные данные и хотят оценить, насколько вы способны самостоятельно находить решения. Это включает:
Понимание проблемы: Четкое формулирование задачи.
Методология: Разработка плана действий и выбор методов.
Критическое мышление: Анализ данных и выдвижение гипотез.
Практические навыки: Применение базовых концепций машинного обучения.
Важно продемонстрировать инициативу и способность работать с ограниченной информацией. В нашем случае, домены существующих компаний можно найти на kaggle, а несуществующие домены нам необходимо будет сгенерировать самим.
Качественные и разнообразные данные позволяют алгоритмам выявлять закономерности, делать предсказания и принимать обоснованные решения. Поэтому без хороших данных невозможно достичь успешных результатов в машинном обучении. Важно создать качественные данные для обучения модели, чтобы обеспечить её эффективность и точность. Нам необходимо сосредоточиться на создании таких данных:
Напишем функции для генерации случайных строк и доменных имён. Функция generate_random_string генерирует строку заданной длины с буквами и, опционально, цифрами. Функция generate_domain_names создает список доменных имён с различными паттернами.
def generate_random_string(length, use_digits=True):
"""
Генерирует случайную строку заданной длины, включающую буквы и опционально цифры.
:param length: Длина строки
:param use_digits: Включать ли цифры в строку
:return: Случайная строка
"""
characters = string.ascii_lowercase
if use_digits:
characters += string.digits
return ''.join(random.choice(characters) for _ in range(length))
def generate_domain_names(count):
"""
Генерирует список доменных имён с различными паттернами и TLD.
:param count: Количество доменных имён для генерации
:return: Список сгенерированных доменных имён
"""
tlds = ['.com', '.ru', '.net', '.org', '.de', '.edu', '.gov', '.io', '.shop', '.co', '.nl', '.fr', '.space', '.online', '.top', '.info']
def generate_domain_name():
tld = random.choice(tlds)
patterns = [
lambda: generate_random_string(random.randint(5, 10), use_digits=False) + '-' + generate_random_string(random.randint(5, 10), use_digits=False),
lambda: generate_random_string(random.randint(8, 12), use_digits=False),
lambda: generate_random_string(random.randint(5, 7), use_digits=False) + '-' + generate_random_string(random.randint(2, 4), use_digits=True),
lambda: generate_random_string(random.randint(4, 6), use_digits=False) + generate_random_string(random.randint(3, 5), use_digits=False),
lambda: generate_random_string(random.randint(3, 5), use_digits=False) + '-' + generate_random_string(random.randint(3, 5), use_digits=False),
]
domain_pattern = random.choice(patterns)
return domain_pattern() + tld
domain_list = [generate_domain_name() for _ in range(count)]
return domain_list
Код загружает три CSV-файла, обрабатывает данные, удаляя столбец '1' и добавляя 'is_dga' со значением 0. Генерирует 1 миллион DGA-доменных имён, объединяет их с part_df и перемешивает итоговый DataFrame.
Исключаем домены из валидационного и тестового наборов, затем балансируем классы, выбирая по 500,000 примеров для каждого из них. Итоговый сбалансированный набор перемешивается и сбрасывает индексы
# Исключение доменов из валидационного и тестового наборов
train_set = set(df.domain.tolist())
val_set = set(df_val.domain.tolist())
test_set = set(df_test.domain.tolist())
intersection_val = train_set.intersection(val_set)
intersection_test = train_set.intersection(test_set)
if intersection_val or intersection_test:
df = df[~df['domain'].isin(intersection_val | intersection_test)]
# Балансировка классов до одинакового числа примеров
logging.info('Балансировка классов')
df_train_0 = df[df['is_dga'] == 0]
df_train_1 = df[df['is_dga'] == 1]
num_samples_per_class = 500000
df_train_0_sampled = df_train_0.sample(n=num_samples_per_class, random_state=42)
df_train_1_sampled = df_train_1.sample(n=num_samples_per_class, random_state=42)
df_balanced = pd.concat([df_train_0_sampled, df_train_1_sampled])
df_train = df_balanced.sample(frac=1, random_state=42).reset_index(drop=True)
Создаем и обучаем модель, используя конвейер, который включает векторизацию с помощью TfidfVectorizer и логистическую регрессию. После обучения модель сохраняется в файл model_pipeline.pkl
logging.info('Создание и обучение модели')
model_pipeline = Pipeline([
("vectorizer", TfidfVectorizer(tokenizer=n_grams, token_pattern=None)),
("model", LogisticRegression(solver='saga', n_jobs=-1, random_state=12345))
])
model_pipeline.fit(df_train['domain'], df_train['is_dga'])
logging.info('Сохранение модели')
joblib_file = "model_pipeline.pkl"
joblib.dump(model_pipeline, joblib_file)
logging.info(f'Модель сохранена в {joblib_file}')
Вся наша задача сводится к тому, что нам необходимо домены разбить на N-граммы и векторизовать их с помощью TF-IDF. N-грамма — это последовательность из N элементов (слов или символов) в тексте, но в нашей задаче мы применяем их к одному слову, чтобы выделять и анализировать слоги доменов. TF-IDF (Term Frequency-Inverse Document Frequency) — это метод, который помогает оценить важность слова в документе по сравнению с другими документами в коллекции.
Таким образом, комбинируя N-граммы и TF-IDF, мы можем эффективно анализировать домены и выявлять их ключевые характеристики. Рассмотрим на примере существующих доменов: texosmotr-auto.ru и pokerdomru.ru, разобьем их на 4-граммы, не беря во внимание родовой домен (.ru)
Для pokerdomru.ru: "poke", "oker", "kerd", "erdo", "domr", "omru"
Мы рассмотрели 4-граммы, но разве для всех доменов необходимо использовать фиксированные N-граммы? Конечно, нет. Для каждого домена создаются 3-мерные, 4-мерные и 5-мерные граммы, чтобы выявить различные языковые паттерны и особенности структуры. Такой подход позволяет лучше захватывать контекст и увеличивает возможность обнаружения уникальных характеристик, которые могут быть полезны для классификации.
код для созднания 3-мерных, 4-мерных и 5-мерных грамм для домена
def n_grams(domain):
"""
Генерирует n-граммы для доменного имени.
:param domain: Доменное имя
:return: Список n-грамм
"""
grams_list = []
# Размеры n-грамм
n = [3, 4, 5]
domain = domain.split('.')[0]
for count_n in n:
for i in range(len(domain)):
if len(domain[i: count_n + i]) == count_n:
grams_list.append(domain[i: count_n + i])
return grams_list
Все полученные N-граммы необходимо векторизовать, и в этом нам поможет вышеупомянутый метод TF-IDF. Этот подход позволяет оценить важность каждой N-граммы в контексте доменов, преобразуя текстовые данные в числовую форму. Векторизация с помощью TF-IDF учитывает частоту встречаемости N-грамм в каждом домене и их редкость в общем наборе.
Финальным этапом необходимо обучить нашу модель. Вы можете использовать различные алгоритмы, которые улучшают вашу метрику, однако я выбрал классическую логистическую регрессию (LR), потому что она проста в реализации, хорошо интерпретируется и часто дает неплохие результаты, например я получил следующие метрики на валидационном наборе данных:
Таким образом, понимание базовых концепций, таких как N-граммы и TF-IDF, откроет перед вами возможности для решения прикладных задач и позволит уверенно заявить о себе на стажировках. Эти навыки станут крепкой основой для вашего профессионального роста в области машинного обучения и анализа данных.
PS: Код, отправленный на проверку в компанию, предоставившую это тестовое задание, находится здесь.