- PVSM.RU - https://www.pvsm.ru -
В этой статье мы рассмотрим простую задачу, которая используется одной компанией в качестве тестового задания для стажеров на позицию ML-engineer. Она включает обнаружение DGA-доменов — задача, решаемая с помощью базовых инструментов машинного обучения. Мы покажем, как с ней справиться, применяя самые простые методы. Знание сложных алгоритмов важно, но куда важнее — понимать базовые концепции и уметь применять их на практике, чтобы успешно демонстрировать свои навыки.
DGA (Domain Generation Algorithm) — это алгоритм, который автоматически генерирует доменные имена, часто используемые злоумышленниками для обхода блокировок и связи с командными серверами.
В техничесокм задании присутствовали тестовые данные [1], для которых нужно было сформировать предсказания, и валидационные данные [2], на которых нужно было продемонстрировать метрики в формате:
True Positive (TP): False Positive (FP): False Negative (FN): True Negative (TN): Accuracy: Precision: Recall: F1 Score:
Иногда компании не предоставляют тренировочные данные и хотят оценить, насколько вы способны самостоятельно находить решения. Это включает:
Понимание проблемы: Четкое формулирование задачи.
Методология: Разработка плана действий и выбор методов.
Критическое : Анализ данных и выдвижение гипотез.
Практические навыки: Применение базовых концепций машинного обучения.
Важно продемонстрировать инициативу и способность работать с ограниченной информацией. В нашем случае, домены существующих компаний можно найти на kaggle [4], а несуществующие домены нам необходимо будет сгенерировать самим.
Качественные и разнообразные данные позволяют алгоритмам выявлять закономерности, делать предсказания и принимать обоснованные решения. Поэтому без хороших данных невозможно достичь успешных результатов в машинном обучении. Важно создать качественные данные для обучения модели, чтобы обеспечить её эффективность и точность. Нам необходимо сосредоточиться на создании таких данных:
Напишем функции для генерации случайных строк и доменных имён. Функция 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.
try:
logging.info('Загрузка данных')
part_df = pd.read_csv('top-1m.csv')
df_val = pd.read_csv('val_df.csv')
df_test = pd.read_csv('test_df.csv')
logging.info('Данные успешно загружены.')
except Exception as e:
logging.error(f'Ошибка при загрузке данных: {e}')
logging.info('Обработка данных')
part_df = part_df.drop('1', axis=1)
part_df.rename(columns={'google.com': 'domain'}, inplace=True)
part_df['is_dga'] = 0
list_dga = df_val[df_val.is_dga == 1].domain.tolist()
generated_domains = generate_domain_names(1000000)
part_df_dga = pd.DataFrame({
'domain': generated_domains,
'is_dga': [1] * len(generated_domains)
})
df = pd.concat([part_df, part_df_dga], ignore_index=True)
df = df.sample(frac=1).reset_index(drop=True)
Исключаем домены из валидационного и тестового наборов, затем балансируем классы, выбирая по 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)
Для texosmotr-auto.ru: "texo", "exos", "xosm", "osmo", "smot", "motr", "otr-", "r-au", "-aut", "auto"
Для 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), потому что она проста в реализации, хорошо интерпретируется и часто дает неплохие результаты, например я получил следующие метрики на валидационном наборе данных:
True Positive (TP): 4605 False Positive (FP): 479 False Negative (FN): 413 True Negative (TN): 4503 Accuracy: 0.9108 Precision: 0.9058 Recall: 0.9177 F1 Score: 0.9117
Таким образом, понимание базовых концепций, таких как N-граммы и TF-IDF, откроет перед вами возможности для решения прикладных задач и позволит уверенно заявить о себе на стажировках. Эти навыки станут крепкой основой для вашего профессионального роста в области машинного обучения и анализа данных.
PS: Код, отправленный на проверку в компанию, предоставившую это тестовое задание, находится здесь [5].
Автор: Artur0Kildiyarov
Источник [6]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/informatsionnaya-bezopasnost/397415
Ссылки в тексте:
[1] тестовые данные: https://github.com/KilArtur/DGA/blob/main/test_df.csv
[2] валидационные данные: https://github.com/KilArtur/DGA/blob/main/val_df.csv
[3] мышление: http://www.braintools.ru
[4] kaggle: https://www.kaggle.com/datasets/cheedcheed/top1m
[5] здесь: https://github.com/KilArtur/DGA/tree/main
[6] Источник: https://habr.com/ru/articles/845452/?utm_campaign=845452&utm_source=habrahabr&utm_medium=rss
Нажмите здесь для печати.