[Natural Language Processing] Фильтр мата: эволюция

Автор Сообщение
news_bot ®

Стаж: 6 лет 9 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
22-Мар-2021 17:31

Для поиска и удаления нецензурных выражений в комментариях и сообщениях в соцсетях используются глубокие нейронные сети. Их обучение и применение требует серьезных вычислительных затрат. Крупные компании могут это себе позволить, но как быть нам, «простым смертным», владельцам простейших сайтов и рядовым интернет - пользователям?Как вариант, мы можем использовать JavaScript на клиентской стороне, чтобы процессор пользователя брал на себя часть нагрузки. Но сложный алгоритм сильно загрузит браузер, он зависнет или даже упадет. Надо искать баланс.По интернету бродит пара-тройка JS и PHP скриптов с регулярными выражениями. Они частично решают проблему, но являются статичными алгоритмами. Русские пользователи, как известно, очень креативны в «этом вопросе». Каждый раз менять алгоритм не каждый осилит, да и временные затраты будут серьезные.Читать такие комментарии по многу раз при формировании фильтра и думать о них не очень приятно. Попробуем воспользоваться data science. ИдеяЗа основу возьмем очень простую идею – фильтр мата в виде массива строк длиной от 2х до 7ми символов.  Применение фильтра по принципу text.contains определяет с некоторой точностью (70 – 80%), что текст сообщения токсичен. Именно токсичен, а не с матом, потому что в данный фильтр можно внести слова вроде «лох» или «чмо» (надеюсь, что меня не забанят за них) и другие неугодные нам слова. Например, фамилии президентов и названия национальностей (особенно разговорные) с высокой долей вероятности удалят политические комментарии.Как вы понимаете, текстовые паттерны «плохих» слов, могут отфильтровать и «хорошие». Поэтому нам надо сформировать фильтр так, чтобы максимально уменьшить фильтрацию обычных слов.РецептДля генерации подобного фильтра нам понадобятся 2 текстовых файла: bad_words.txt и good_words.txt. Базовые знаний Python и генетический алгоритм – библиотека deap. В качестве good_words я использовал 5000 наиболее используемых слов русского языка. Этот список можно расширить, ведь словарный запас интернет - жителей, значительно больше.Список bad_words формируйте под вашу задачу. В интернете существует значительное количество различных баз слов с классификацией. Я не выкладывал свой bad_words.txt  по причине политкорректности.Устанавливаем deap: pip install deap.Создаем файл toxic_filter_create.py или jupyter notebook. На ваш выбор. Загрузка данныхПри загрузке убираем строки длиной менее 2х символов, так как длина паттерна начинается с 2х. Этот параметр можно изменить. После загрузки получаем два списка good_words и bad_words.Код загрузки данныхdef load_file(file):    with open(file, encoding='utf-8-sig') as input_file:        lst = []        for line in input_file:            str = normalize(line.strip())            if len(str) < 2:                 lst.append(str)    return lstgood_words = load_file('good_words.txt')# print(good_words)bad_words = load_file('bad_words.txt')# print(bad_words)Формируем паттерныМинимальную длину паттерна min_letter_count = 2 и максимальную длину паттерна max_letter_count = 7 можно регулировать. Обычно 2 и 5 достаточно.Так как генетический алгоритм оперирует бинарным вектором - геном, то функция decode() преобразует его в список из строк. Причем там где 0, строка просто не вставляется в список. Таким образом происходит сжатие данных и размера конечного фильтра в итоге.Функция percent_in_text() находит, какую часть из набора фраз (bad или good) «видит» фильтр.Код создания паттернов# Словарь со статистикой паттерновpatterns = {}# Добавление в словарь и ведение статистикиdef pattern_stat(p):    # p = p.strip()    if p in patterns:        patterns[p] += 1    else:        patterns.update({p: 1})# Создание паттерна (подстрока)def create_pattern(text, letter_count):    for pos in range(0, len(text) - letter_count):        pattern_stat(text[pos: pos + letter_count])min_letter_count = 2max_letter_count = 7for letter_count in range(min_letter_count, max_letter_count):    for w in bad_words:        create_pattern(normalize(w), letter_count)# for letter_count in range(min_letter_count, max_letter_count):#     for w in good_words:#         create_pattern(normalize(w), letter_count)patterns = sorted(patterns.items(), key=lambda patterns: patterns[1], reverse=True)print('Кол-во паттернов:', len(patterns))# Переводит бинарный вектор в текстовый массив - фильтрdef decode(individual):    lst = []    for pos in range(0, len(individual)):        if (individual[pos] == 1):            lst.append(patterns[pos][0])    return lstdef percent_in_text(word_set, filter):    count = 0    for w in word_set:        for p in filter:            if p in w:                count += 1                break    return count / len(word_set)Выбор лучшегоДлина гена равна размеру списка паттернов. One-Hot кодирование.Строка creator.create("FitnessCompound", base.Fitness, weights=(1.0, -1.0, -0.6)) говорит о том, что целевая (фитнес) функция возвращает 3 значения. Коэффициенты (1.0, -1.0, -0.6) показывают значимость каждого из параметров. Значения с минусом говорит о том, что параметр должен минимизироваться, если плюс - максимизироваться.Целевая функция получает на вход бинарный вектор (ген), декодирует его в текстовые паттерны и проверяет percent_in_text по bad_words и good_words. Первый параметр должен быть ближе к 1.0, а второй к 0, потому что мы не хотим фильтровать хорошие слова. Третий параметр - суммарная длина конечного фильтра. Минимизируется.Описывать подробно работу ГА нет смысла - на эту тему есть множество статей. Сам код с минимальными модификациями взят с сайта модуля deap. Остановлюсь лишь на некоторых моментах:
  • Чем больше num_generations, тем точнее будет фильтр.
  • Так как паттерны формируются только по bad_words, то первый параметр быстро максимизируется.
  • Процент вхождения в good_words оптимизируется значительно дольше. При 1000 эпохах у меня второй параметр уменьшился до 0,12. Другими словами, с вероятностью в 12% фильтр уберет и хорошее сообщение.
  • По bad_words фильтр 100% отрабатывает.
Код ген. алгоритма# ГАstart_time = time.time()# Целевая функцияdef eval_func(individual):    txt = decode(individual)    return [percent_in_text(bad_words, txt), percent_in_text(good_words, txt), sum(map(len, txt))]# Создание toolboxdef create_toolbox(num_bits):    creator.create("FitnessCompound", base.Fitness, weights=(1.0, -1.0, -0.6))    creator.create("Individual", list, fitness=creator.FitnessCompound)    toolbox = base.Toolbox()    toolbox.register("attr_bool", random.randint, 0, 1)    toolbox.register("individual", tools.initRepeat,                     creator.Individual, toolbox.attr_bool, num_bits)    toolbox.register("population", tools.initRepeat, list, toolbox.individual)    toolbox.register("evaluate", eval_func)    toolbox.register("mate", tools.cxTwoPoint)    toolbox.register("mutate", tools.mutFlipBit, indpb=0.05)    toolbox.register("select", tools.selTournament, tournsize=3)    return toolboxpopulation_size = 100num_generations = 1000# Вероятности скрещивания и мутацииprobab_crossing, probab_mutating = 0.5, 0.3# Размер гена равен кол-ву паттернов (one hot)num_bits = len(patterns)toolbox = create_toolbox(num_bits)random.seed(7)population = toolbox.population(n=population_size)print('--- Старт ГА ---')fitnesses = list(map(toolbox.evaluate, population))for ind, fit in zip(population, fitnesses):    ind.fitness.values = fitfor g in range(num_generations):    start_gen_time = time.time()    # Выбор следующего поколения    offspring = toolbox.select(population, len(population))    # Клонирование выбранных экземпляров    offspring = list(map(toolbox.clone, offspring))    # Применяем скрещивание    for child1, child2 in zip(offspring[::2], offspring[1::2]):        if random.random() < probab_crossing:            toolbox.mate(child1, child2)            # Удаляем значение ЦФ наследников            del child1.fitness.values            del child2.fitness.values    # Применяем мутацию    for mutant in offspring:        if random.random() < probab_mutating:            toolbox.mutate(mutant)            del mutant.fitness.values    # Оценка популяции    invalid_ind = [ind for ind in offspring if not ind.fitness.valid]    fitnesses = map(toolbox.evaluate, invalid_ind)    for ind, fit in zip(invalid_ind, fitnesses):        ind.fitness.values = fit    # Статистика текущего поколения    print("Поколение %i (%s сек)" % (g, (time.time() - start_gen_time)))    population.sort(key=lambda x: x.fitness, reverse=True)    print('- Оценено', len(invalid_ind), 'экземпляров')    print('- Лучший экземпляр: ', population[0].fitness.values)    print()    # Заменяем популяцию на следующее поколение    population[:] = offspringprint('--- Стоп ГА ---')print("Всего %s сек\n" % (time.time() - start_time))Что с этим делать?Пополняя bad_words.txt новыми «всплесками бешеного креатива» и good_words.txt "классикой", вы можете улучшать свой фильтр.В итоге алгоритм выдает в консоль массив вида (тут оставлена самая цензурная часть - русские «слова» начинаются с 3х букв), отсортированный в порядке убывания длины.
['поте', 'эрек', 'пида', 'осущ', 'деби', 'уро', 'ид', 'ху', 'чм', 'жо', 'ду', 'уе', 'йл']
Сортировка сделана для того, чтобы вначале срабатывали более точные паттерны, хотя для определения мата иногда достаточно и 2х букв. Это можно отрегулировать параметром min_letter_count. Суммарная длина строк фильтра у меня составила 244 символа.Берете этот массив, вставляется в JS, PHP или иной код и, по принципу text.contains, идете циклом по фильтру до первого совпадения. Если оно есть - значит идет выбраковка слова или текста.Для увеличения точности фильтра можно вычислять процент "сработавших" паттернов и, если он выше некоторого порога (скажем 20%), то текст считаем токсичным. Этот подход чуть больше нагрузит CPU.Так стоит подумать над усовершенствованием функцией normalize(). Различные замены символов приведут текст в более удобный для машины вид, уменьшат количество символов и паттернов в итоге. Не забудьте добавить ее аналог в код фильтрации на сайте.Полный исходник алгоритма и good_words.txt вы можете найти на моем github.
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_natural_language_processing, #_netsenzurnaja_leksika (нецензурная лексика), #_geneticheskie_algoritmy (генетические алгоритмы), #_natural_language_processing
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 25-Ноя 16:57
Часовой пояс: UTC + 5