Pull to refresh

Comments 23

Интереснее было бы сделать модель которая бы распознавала оскорбительные и другие нарушающие правила никнеймы. Для этого есть реальная необходимость в онлайн мультиплеерных играх. Всякие 88, 1312 и т.п. хрень. А также всякие стилизации типа Seba55tian где в слово вставлено двойное S. И прочие WOTAN-ы

Да, я сказал пару слов об этом полезном применении в статье. Тут скорее просто фан-тест генеративной языковой модели, не совсем подходящей под задачи классификации. Фильтры никнеймов уже давно есть в нормальных играх, но они завязаны скорее на тупых алгоритмах, чем на нейросетях.

Для этого дела нужна мощная классификационная модель вроде BERT, но в русском языке по-настоящему рабочих+открытых нет (я пробовал, например, семантическую модель от deeppavlov, но это прямо скажем "такое", ну явно не подходит для больших проектов).

Можно попробовать обучить генеративную модель на подобные задачи, но чисто для проверки "нормальности" ника (в случае с ответом только да\нет) это будет прям слишком (с точки зрения использования ресурсов). Поэтому если уж делать полноценные вещи на таких моделях, как FRED-T5, то уж пусть она отдаёт пользователю не только отказ в нике, но и комментарий с критикой его несносного ника, это уж будет куда интереснее.

Здесь в статье, кстати, по формату, кроме критики ника, она ещё отдаёт эмоцию. Так вот, вместо эмоции можно написать что-то вроде "допуск=да" после анализа ника и вот, вы грубо говоря получаете первую версию импровизированного мега-нейро-фильтра ников (ну и конечно же предварительно надо поработать с промптом, чтобы там было то что вам нужно, запрещенные темы для ников, политика, все дела + примеры ников которые получили отказ по разным причинам..)

Только по первому же примеру видно, что сеточка промахивается. Sm1le про "смайл", а не про small =)

Т.е. надо дообучать, без вариантов.

Ещё один вариант рецензии на этот ник
Ещё один вариант рецензии на этот ник

Естессно надо дообучать, если ваша цель - профессиональное использование. При обучении фреда использовалось всего 300гб текста, и английского там было не так уж много. Удивительно, что она вообще его различает)

С другой стороны, смайл, это ведь что-то очевидное, а модель выдала неожиданный и не лишенный смысла, весёлый ответ)

мне кажется, что для этого лучше использовать несколько моделей попроще:


  1. для преобразования ника Seba55tian -> Sebasstian с помощью моделей text2text или image2text
  2. Классификационная модель

Подозреваю, что так можно будет проще контролировать качество и иметь меньшие требования к железу. Но как эксперимент — интересно.

Для преобразований цифро-текстовых ников в текст, я думаю, вообще не нужна модель ИИ)) С этим справится какой-нибудь качественный алгоритм, просто сделать условную таблицу соотношения цифра-символ ещё пару инструментов, чтобы отделять цифры в роли букв от просто цифр.

Классификационные модели в русском весьма слабы и требовательны (как и женщины). Приличного успеха там добился deeppavlov, но всё равно слабовато для нормального использования(

Просто классификационная модель, которая отслеживает семантику, весит 4 чертовых гига. Просто семантику текста! И то, эта модель работает "весьма относительно"...

Способов испортить ник всякими гадкими замыслами настолько много, что в идеале нужна модель, способная понять его реальный "смысл", а не просто классифицировать как допуск\нет. Как вариант, можно использовать 2 модели: первая определяет смысл, вторая по смысловому описанию определяет, есть ли нарушения в нике, или нет. Но опять же, я не знаю таких классификационных моделей в русском языке, которые безошибочно способны определять какую-либо характеристику текста. Если такие есть, буду очень рад увидеть)

Осторожно, тут есть пара токсичных моментов)

Первые три раза введено "FoMu", остальные на русском - "ФоМу".
Первые три раза введено "FoMu", остальные на русском - "ФоМу".

Чтобы каждый раз не заниматься подгрузкой модели, я бы предложил использовать клиент-серверную архитектуру, хотя бы в самом простом виде (на сокетах).
Запускаем серверную часть, которая грузит модель, а для собственно генерации — быстрый клиент, который ничего не грузит, а просто посылает данные серверу и получает ответ.


Заметим, что в результате генерации достаточно часто попадается мат. Ну что ж, видимо "модель" во всех смыслах "народная", и, каким-то образом, мат ей тоже передался, видимо, по наследству.

С 18+ сценами тоже всё в порядке. Порнорассказы отлично генерируются, причём даже в разном сеттинге (можно и в фэнтези-антураже, и всякую хрень с тентаклями, и в фантастических сеттингах).
Для генерации собственно текстов ещё очень полезна функция заполнения пропусков в тексте. Т.е. не дополнительные слова к концу пририсовать, а где-то в серёдке перефразировать или заменить "провисающий" кусок. Для авторов довольно полезно.

Клиент-сервер вроде как уже основа жанра для любых систем с ИИ. Любая модель весит до**рища, и, если куда-то встраивать, эта архитектура просто неизбежна...

Что касается 18+, то с этим всё настолько в порядке, что, даже если в запросе нет ни единого матерного или пошлого слова, или даже намека, модель с небольшой вероятностью захочет в сексуальные темы. Я намучился с чатботом на фреде, и, порой, мне выскакивал порнорассказ там, где, это, поверьте, совсем не нужно)) Так что, если использовать где-то профессионально, после дообучения в любом случае ещё понадобится мутить фильтр..

По поводу пропусков, это же вроде как особенность обучения этой модели как и всех T5 (они обучались, заполняя пропуски в тексте). Действительно очень полезно для встраивания в какой-нить проект, помогающий редактировать текст средствами ИИ.

Ну возможность исключить из генерации определённые сочетания токенов уже встроена в саму модель:


stop_list = """ слово1
 слово2
 фраза из нескольких слов"""

stop_list = stop_list.split('\n')
stop_list = [s for s in stop_list if len(s)>0]
if len(stop_list)>0:
    bad_words_ids = tokenizer(stop_list, add_special_tokens=False).input_ids
else:
    bad_words_ids = None

Далее просто передать в generate() дополнительный параметр

bad_words_ids=bad_words_ids

Правда, при составлении списка слов и сочетаний надо учитывать, как со всем этим работает токенизатор. Например, все слова начинать с пробела, потому что при генерации пробел входит в стартовый токен большинства слов.
Поэтому я сделал возможность вывода текста с разделением на токены, чтобы видеть, что же конкретно генерируется:
# Add borders "|" between tokens and show result
tokens = outputs[0][1:]
ext_tokens = []
for t in tokens:
    ext_tokens.append(t)
    ext_tokens.append(96) # token '|' as separator
print(tokenizer.decode(ext_tokens))

Этот код выдаёт примерно следующий результат:


начала| шеп|тать| заклинания|,| чтобы| приз|вать| в| свою| жизнь| силы| тьмы|.|
|Вскоре| из| алта|ря| стали| появляться| чёрные| силуэ|ты|.| Они| были| очень| похожи| на| человеческие| и| всё| же| отличались| от| них|.| Это| были| боги| и| богини| Б|езд|ны|,| которые| собрались|

Большое спасибо за код, может быть очень полезно для исключения большей части самых вредных диалогов!

Вот, кстати, развивая эту тему, на коммерческом уровне просто список бэд токенов не прокатит. Для нормального чат-бота нужен фильтр.

На днях я экспериментировал с чат-ботом, он должен отдавать ответ, эмоцию и команду из списка. В запросе есть пару неприятных вещей, но они нужны, чтобы показать модели, как отвечать на "плохие" вопросы. В любом случае, там не было необоснованной агрессии, а она очень любит это делать за просто так, как тут, например, когда я попытался заставить чат-бота изменить его выдаваемую эмоцию:

необоснованная агрессия
необоснованная агрессия

Также модель не всегда может корректно обрабатывать "запрещенные" темы, несмотря на то, что в запросе есть шаблон ответа на подобную запрещенную тему.

так модель ответила спустя пару попыток (содержит сомнительный элемент)
так модель ответила спустя пару попыток (содержит сомнительный элемент)
как-то так модель должна отвечать (согласно шаблону)
как-то так модель должна отвечать (согласно шаблону)

Кстати, у этой модели есть кое-какой баг.. У меня в чат-боте сейчас запрос длиной около 500 токенов. Раньше, когда запрос был поменьше, она спокойно выдавала ответ и всё было норм. В начале запроса надо вводить спецтокен <LM>, в конце </s>. Так вот, когда запрос стал длиной более 500 токенов, модель начала выдавать полную несуразицу, будто обрезала начало и теряла токен <LM>. Потом я [случайно] запихал токен начала <LM> в конец запроса, и всё снова заработало... Странно всё это, не логично.. Если его убрать, она опять начнет выдавать бред в виде случайных символов...

И есть ещё одна фатальная проблема из-за которой я не могу продолжить делать чат-бот. Если вставить в диалоговую часть любой текст, сгенерированный сеткой, она повторяет то, что когда-либо генерировала до этого. Звучит странно, но это реально так. Повышение температуры помогает, но не всегда и увеличивает вероятность того что модель тупо начинает уходить от нормального форматирования, генерировать несуществующие команды или эмоции, агрессировать и не следовать правилам диалога, описанным в начале запроса.

Странно, у меня даже затравки в 800 токенов нормально съедаются моделью. Вы тег </s> вручную к концу добавляете? Я не добавляю — он сам должен добавиться.
Сама модель обучалась на последовательностях 512 токенов, и где-то такую длину она обрабатывает наиболее оптимальным образом. Если последовательность длиннее 700 токенов, начинается заметная просадка качества.
Для моих целей (генерация и редактирование текстов рассказов) оптимальные параметры такие:
top_k = 40
top_p = 0.98
repetition_penalty = 1.04
temperature = 0.8
max_length = 50
min_length = 20


Если замучили повторы текста самой затравки, добавьте к generate() ещё один параметр:


no_repeat_ngram_size = 6

таким образом "выключается" повторение кусков затравки длиннее 6 токенов (с длиной куска можно поиграться).


Также можете попробовать сменить сэмплинг на лучевой поиск. Мне правда не понравилось, как оно генерирует в таком режиме, текст пресный какой-то выходит. Но, возможно, вам зайдёт больше. Для этого добавьте к generate() параметр:


num_beams = 4

Поиграйтесь с количеством лучей (Минимальное значение — два, но и слишком много не ставьте, а то упадёт из-за нехватки видеопамяти).

Не получилось с лучами запустить, выше 2 вылетает, и оно, естесственно, до невозможности пресное, так что пока стоит семплинг.

no_repeat_ngram_size вообще походу внаглую игнорируется при генерации, берет и прямо копии частей затравки вставляет в ответ... При любой длине. Может я что-то делаю не так?

параметры generate(), которые я использую

do_sample=True,

top_p=50,

temperature=0.35,

repetition_penalty=2.0,

min_length=10,

max_length=100,

no_repeat_ngram_size = 5,

early_stopping=True

Хм, странно. Попробуйте наиболее жёсткое значение:
no_repeat_ngram_size = 1
Вот это ещё исправьте:
top_p=50
на top_p=0.98 или похожее маленькое значение.
А в 40 или 50 установите top_k.
Также можно попробовать значения repetition_penailty меньше 1.0, например 0.3. Вообще, 2.0 — это многовато, хорошо работает интервал 1.00 — 1.20, а за его пределами влияние непредсказуемо.
Плюс температура явно маловата, хорошо бы хотя бы до 0.7 поднять.

Очень странно, но даже при нграм=0 он допускает частые повторения... Разделил по токенам, одни и те же последовательности прям внаглую фигачит. Температура немного помогает, но она очень портит структуру ответа (у меня четко должна в скобках отдавать эмоцию и команду из списка). Вообще есть ощущение, что параметры нграм и репит пенальти вообще не влияют на генерацию..

нграм=0

Единица — минимальное значение, ноль просто выключает этот механизм.


Температура немного помогает, но она очень портит структуру ответа

Так FRED-T5 — это не шаблонизатор, чтобы выдавать чётко структурированные данные. Шаблонизатор пишется отдельно, при этом каждое "поле" в шаблоне заполняется отдельным запросом.
Т.е. сперва вы получаете текст с описанием, потом добавляете это в шаблон, а команду и эмоцию получаете отдельными запросами (или даже отдельными нейронками-классификаторами).

Извиняюсь, опять ошибся. Нграм единицу ставил, естесственно. В общем, я заметил, что настройки повторяемости работают весьма печально, может, конечно, я что-то накриворучил.

Про шаблонизатор: на запрос, в среднем, уходит не менее 1 сек независимо от размера, а, если более 1 запроса, представить страшно, сколько вся генерация займет времени..

Фред с самого начала мне очень понравился тем, что он понимает структуру. В теории, его можно даже пихать в генерацию кода (при дообучении), он четко следит за структурой, в отличие от других русских моделей. Вот я и решил в качестве эксперимента дать ему возможность самому принимать шаблонные решения типа выбора команды или эмоции.

UPD: в какой-то степени решил проблему тем, что подал контекст сгенерированный сеткой в форме, отличающейся от образцового примера диалога в запросе. Стала почти не повторяться! Спасибо вам за помощь!

какой был формат запроса

Старый формат запроса:

*описание действующих лиц и ситуации*

{Q: вопрос (образец)

A: ответ (образец)}*n раз

Образцовый блок вопросов и ответов для того, чтобы модель примерно поняла, как нужно отвечать

{Q и A контекста в том же формате}

Q: вопрос

A:(продолжение модели)

Новый формат:

*описание действующих лиц и ситуации*

{Q: вопрос (образец)

A: ответ (образец)}*n раз

Образцовый блок вопросов и ответов для того, чтобы модель примерно поняла, как нужно отвечать

{Контекст в другом формате, вместо букв Q и A напрямую писалось Вопрос и Ответ соответственно}

Q: вопрос

A:(продолжение модели)

Возможно, выглядит бредово, но этот рефакторинг запроса очень помог) И генерация качественнее стала, будто сетка стала "мыслить глубже".

Буду, кстати, очень рад, если сможете посоветовать пару нейронок-классификаторов, подходящих для этого применения)

С русскоязычными классификаторами всё достаточно печально. Можно попробовать приспособить для этого дела сберовский же SBERT.
Поскольку документации там как таковой и нет, а из примера толком не сообразить, как это использовать, посмотрите, что такое Sentence Transformers и как они работают, потому что SBERT — это оно и есть.
Основная идея тут в том, что модель вместо генерации текста по запросу просто проецирует этот запрос на векторное пространство таким образом, чтобы схожие по смыслу или наполнению запросы давали похожие вектора. Это позволяет сравнивать куски текста с образцами и находить максимально сходные по смыслу.


Пример.


Инициализация и вспомогательная функция
# Взято из сберовского примера
from transformers import AutoTokenizer, AutoModel
import torch
from torch.nn import functional as F

#Mean Pooling - Take attention mask into account for correct averaging
def mean_pooling(model_output, attention_mask):
    token_embeddings = model_output[0] #First element of model_output contains all token embeddings
    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

Далее создадим список образцов сравнения
sentences = ['Суммируя выводы модели, можно сказать, что это — та модель, которая по-настоящему понимает язык.',
             'Что за грёбаная хрень?! Иди в задницу!',
             'Забавненько. Похоже на смайлик.',
             'Думаю, я, с горем пополам, оправдал смысл существования своего бесполезного творения']

Всего четыре образца с разной эмоциональной окраской.


Теперь тот кусок текста, который будем классифицировать:


вот он
sample = ['Я не могу написать тебе эмоцию интереса из-за того, что ты тупой и безграмотный!']

Загружаем модель
tokenizer = AutoTokenizer.from_pretrained("model")
model = AutoModel.from_pretrained("model")

Токенизируем образцы и наш текст
encoded_input = tokenizer(sentences, padding=True, truncation=True, max_length=100, return_tensors='pt')
encoded_sample = tokenizer(sample, padding=True, truncation=True, max_length=100, return_tensors='pt')

Пропускаем это через нейронку
with torch.no_grad():
    model_output = model(**encoded_input)
    sample_output = model(**encoded_sample)

embeddings = mean_pooling(model_output, encoded_input['attention_mask'])
sample_emb = mean_pooling(sample_output, encoded_sample['attention_mask'])

На выходе получаем тензоры вида [n, 1024], где n — количество наших предложений.
embeddings — это наши образцы сравнения, а sample_emb — то, что будем сравнивать.
Для сравнения используется косинусная близость. Чем более схожи вектора, тем это число выше (полностью совпадающие вектора дают 1).


Сравниваем
for i in range(embeddings.size()[0]):
    print(F.cosine_similarity(embeddings[i],sample_emb[0], dim=0).item())

Если запустить этот простейший пример, в выводе будет следующее:


0.35799089074134827
0.4960470199584961
0.24681249260902405
0.3882027864456177

Видим, что наш кусок текста ("Я не могу написать тебе эмоцию интереса из-за того, что ты тупой и безграмотный!") ближе всего (аж 0.49) ко второму варианту в списке ("Что за грёбаная хрень?! Иди в задницу!"). Т.е. у нас тут ругань, агрессия.
Конечно, для реального применения образцов для сравнения нужно много (хотя бы по 10 на каждую эмоцию) и есть ряд нюансов, как всё это собирать и настраивать, но идея именно такая.
Если примеров с разными эмоциями достаточно много (>2000), то можно вместо вычисления косинусной близости создать примитивную нейронку в 4 слоя, которая за 5 минут обучается эти эмбеддинги, предварительно сгенерированные моделью, раскладывать по полочкам самостоятельно.

Ещё обратите внимание на структуру запроса. Я не просто так выложил код разбиения на токены — это позволяет отлаживать модель, наблюдая, что идёт на вход и что выплёвывается на выходе.
Так, если запрос заканчивается пустым символом (пробел, перевод строки) — качество генерации проседает многократно. Запрос должен оканчиваться последней буквой слова либо знаком препинания, но не пробелом. Это связано с тем, что пробел при токенизации является частью слова, т.е. "просто концевых пробелов" модель при тренировке явно видела немного.


|А| ведь| я| в| детстве| считала|,| что| это| яблоко|!|

Обратите внимание, что каждый токен начинается с пробела, а не заканчивается. Запрос, завершённый пробелом — это неестественное состояние для данной модели, после которого она с высоким шансом сгенерирует мусор.

Давно планирую сделать спам бот для фильтрации малополезных сообщений в чате и комментариев на сайте. У вас не было подобного опыта? Какую модель посоветуете использовать для подобного?

Для фильтра вам нужна хорошая классификационная модель по типу BERT. Но для русского языка это больная тема, т.к. все существующие модели очень слабы, много ошибаются и часто обманываются. Можете посмотреть демку от deeppavlov как пример (там есть модель определения токсичности, и она ошибается на элементарных запросах). Какую-нибудь BERT подобную русскую модель можете попробовать взять и попробовать дообучить на датасете ваших комментариев, который, кстати, можно сгенерировать при помощи openai.

Как вариант, можно использовать SBERT. Это модель для определения наиболее сходных по смыслу предложений. В комментариях к этой статье можете отрыть годный пример использования. Для нормальной работы с этим вариантом вам понадобится придумать под сотню (а лучше тысячу) различных малополезных и полезных комментариев и сравнить их моделью с текущим комментарием.

Ну и на десерт, костыльный прикол-способ — заставить генеративную модель по типу FRED-T5 выносить вердикт о полезности сообщения в чате и выдавать что-то вроде "допуск=да" или "допуск=нет")) Для этого варианта на вход просто подаёте промпт с парой примеров этих комментов, как это делал я в этой статье с никами. Этот фан-способ очень легко реализовать, но он адски неоптимизирован (3 сек на каждый запрос, ага) и не всегда может следовать разметке текста (есть вероятность, что вместо ожидаемого результата вроде да\нет он вообще выдаст какой-нибудь бред с рандомными ссылками из инета).

Sign up to leave a comment.

Articles