Комментарии 12
Единственный вопрос — зачем? Цель всегда должна быть в любой статье. Иначе получаем «статья для статьи».
Вы пишете, что это некая «игрушка». Но я могу смотреть на это только как на placeholder text generator (генератор бессмысленного текста-заполнителя по типу lorem ipsum). Других применений не вижу.
Депутата сделали, теперь министра начинайте.
Похоже чем-то на сервис Яндекса «Балабоба»
Советую SpaCy попробовать ;)
Токенизация текста — одна из самых простых задач NLPДля английского языка — возможно, вот только мне ещё ни разу не встречался русскоязычный токенизатор, который опознавал бы части речи и падежи/склонения с менее чем 1 ошибкой на 1000 слов.
Генератор абсурда за пять минутИМХО, самый простой генератор абсурда выглядит
from collections import *
import io
from random import random
def train_char_lm(fname, order=4):
with io.open(fname, encoding="utf-8") as f:
data = f.read().lower()
lm = defaultdict(Counter)
pad = "~" * order
data = pad + data
for i in range(len(data)-order):
history, char = data[i:i+order], data[i+order]
lm[history][char]+=1
def normalize(counter):
s = float(sum(counter.values()))
return [(c,cnt/s) for c,cnt in counter.items()]
outlm = {hist:normalize(chars) for hist, chars in lm.items()}
return outlm
def generate_letter(lm, history, order):
history = history[-order:]
dist = lm[history]
x = random()
for c,v in dist:
x = x - v
if x <= 0: return c
def generate_text(lm, order, nletters=1000,history=None):
if (history is None):
history = "~" * order
else:
history=("~"*order)+history
history=history[-order:]
out = []
for i in range(nletters):
c = generate_letter(lm, history, order)
history = history[-order:] + c
out.append(c)
return "".join(out)
ORDER_NUM = 13
lm = train_char_lm("Moskva-Petushki.txt", order=ORDER_NUM)
print(generate_text(lm, ORDER_NUM))
Он очень прост, общий принцип объясняется на пальцах за несколько минут, а сам алгоритм может быть реализован на любом языке программирования без внешних зависимостей менее чем за час.
При этом на малых объёмах обучающих данных (менее 1Мб текста) ни одна нейросеть и близко не стояла по качеству текстогенерации (им всем требуется куда больше текста).
добрый день! если Вас не затруднит, не могли бы объяснить общий принцип на пальцах?) если честно, в питоне не разбираюсь от слова совсем, а суть интересна.
В данном примере используется цепь 13-го порядка, т.е. следующий символ строки определяется исходя из 13 предыдущих.
На первом этапе происходит построение модели. Для этого берётся обучающий текст, переводится в lowercase (можно и без этого, но тогда текста понадобится больше, «Москва-Петушки» довольно короткое произведение). Затем создаётся пустой словарь (структура, в которую можно добавлять пары ключ-значение, причём ключ символьный, а в качестве значения выступают списки).
Берём самое начала текста, первые 13 его символов:
"все говорят: "
Ищем эту строку в словаре. Её там нет, т.к. словарь пуст. Значит, добавляем ключ с этой строкой в качестве названия и пустым списком в качестве значения:
dict={ "все говорят: " : [] }
Сейчас список возле ключа пуст, но в дальнейшем он будет заполняться структурами вида символ-количество:
listItemType={ sym:char, count:int }
Смотрим, какой символ идёт в тексте после нашего исходного фрагмента. Это символ «к». Добавляем его в список, в качестве счётчика (count) поставив 1. Словарь будет выглядеть так:
dict={ "все говорят: " : ["к":1] }
Далее сдвигаем точку старта на 1 символ, смотрим следующую строку в 13 символов, это:
"се говорят: к"
Точно так же ищем в словаре. Её там нет, добавляем вместе со списком и следующим символом:
dict={ "все говорят: " : ["к":1] "се говорят: к" : ["р":1] }
Продолжаем двигаться вперёд в цикле:
dict={ "все говорят: " : ["к":1] "се говорят: к" : ["р":1] "е говорят: кр" : ["е":1] }
Теперь допустим, что рано или поздно какой-то кусок строки встретился повторно. В этом случае он найдётся в словаре, и мы смотрим, что за символ идёт после него. Допустим, на этот раз встретился «у». Добавляем в список у этого значения:
dict={ "все говорят: " : ["к":1] "се говорят: к" : ["р":1] "е говорят: кр" : ["е":1, "у":1] //other values //... }
Если после того же куска строки встретился тот же символ, просто увеличиваем счётчик этого символа:
dict={ "все говорят: " : ["к":1] "се говорят: к" : ["р":1] "е говорят: кр" : ["е":1, "у":2] //other values //... }
В итоге, когда мы так пробежимся по всему тексту, у нас будет словарь, где после каждого кусочка текста указаны те символы, которые после него могут встретиться, и в каком количестве они встречаются. Вот так:
dict={ "все говорят: " : ["к":1, "о":10] "се говорят: к" : ["р":1] "е говорят: кр" : ["е":45, "у":2, "а":21, "ы":1, "о":17] //other values //... }
Теперь мы можем вычислить вероятности того, что после данного кусочка встретится тот или иной символ. Для этого надо суммировать общее количество:
"е говорят: кр" : ["е":45, "у":2, "а":21, "ы":1, "о":17] sum=45+2+21+1+17=86
после чего каждое значение делим на эту сумму, получаем дробные вероятности:
"е":(45/86=0.52), "y":(2/86=0.02), ...
Модель готова! Чтобы не строить повторно при следующем запуске программы, её можно сбросить на диск. Для генерации текста берём в качестве начального фрагмента любую строчку из словаря, и в цикле начинаем дописывать к ней символ за символом. Каждый раз берём «хвост» нашего сгенерированного текста длиною 13 символов, ищем этот ключ в словаре, и в соответствующем списке там лежат те символы, которые могут встречаться в тексте после данного фрагмента, и вероятности их появления. Далее просто генерируем рандомный символ из списка с соответствующим распределением вероятностей.
Этому алгоритму можно скормить любой структурированный набор символов. Не только текст, но и например программный код. Я скормил ему полный текст библиотеки jQuery, и на выходе он мне сгенерировал такое:
/*!
* jquery javascript library v3.6.0
* https://js.foundation/
*
* date: 2021-02-16
*/
( function( global, factory ) {
"use strict";
if ( typeof selector === "string" ) {
matched = [],
targets = typeof selectors !== "string" ) {
gotoend = clearqueue;
clearqueue = type;
}
return function( elem, options, i );
} );
return elem.selectedindex = -1;
}
return tween;
} ]
},
tweener: function( name, hook ) {
object[ flag ] = true;
} );
return object;
}
невероятно подробное объяснение, спасибо большое! есть только один момент, не до конца понятный:
Для генерации текста берём в качестве начального фрагмента любую строчку из словаря
каким образом определить, что эта случайная строчка окажется осмысленной, а не каким-то набором букв, начинающимся с середины слова? брать всегда первую строку исходного текста или редактировать полученный текст после работы программы?
П.С.: Вообще, очень многие задачи, которые сейчас решаются нейросетями, раньше выполняли при помощи куда более простых алгоритмов. Просто на удивление простых. Даже алгоритм синтеза речи из текстовой строки укладывали в 4Кб-демки времён DOS. И хоть звучало очень механически, зато работало безо всяких облаков и тонн внешних зависимостей, да и сам код синтезатора занимал лишь пару сотен строчек на чистом C.
Нейросеть выгодно отличается тем, что её легко бездумно масштабировать. Добавили новые фичи - увеличили размерность входного и/или выходного вектора, обучили, работает! Добавили новые образцы - обучили, работает! Плохо работает - увеличили размерность скрытых слоёв, обучили, - работает!
Тогда как эвристические подходы - если что-то не устраивает, то возможно, что придётся выкинуть и переделать с нуля.
С другой стороны, процесс обучения эвристических подходов, возможно, более детерминирован. Взяли корпус языка, пропустили через числодробилку, вырастили марковскую сетку.
Цепочка Маркова такого порядка - не грешит ли переобученностью? Когда у каждого ключа-префикса будет, преимущественно, по единственному продолжению, и случайный выбор - из одного элемента - станет неслучайным.
Конечно, для бредогенератора можно поиграться, уменьшив порядок - в какой-то момент цепочка перестанет следить за контекстом и начнёт выдавать несогласованные по падежам/лицам словосочетания (потому что окончания выпадут из текущего окна). А потом станет куражиться в творчестве длинных слов.
---
Но более правильным должен быть подход, когда мы извлекаем из слов фичи - хотя бы и по-тупому, начало и окончание фиксированной длины, и строим произведение графов - цепочку словоформ (начало-окончание), цепочку согласований (окончания слов, какого-нибудь вменяемого порядка) и цепочку смыслов (начала слов, тоже какого-нибудь вменяемого порядка).
Такой граф, если его развернуть, вероятно, будет довольно жирным. Поэтому, возможно, придётся делать произведение графов по запросу.
Этот подход использовался в распознавании речи - произведения моделей произношения букв (триграммы), слов из букв и словосочетаний из слов (n-граммы, где n тоже равно три - этого хватало). Можно поискать по аббревиатурам HMM, LVCSR.
Программируется довольно несложно, но, если не аккуратничать, то люто жрёт память. Я этим занимался лет семь назад.
Генератор абсурда за пять минут с NLTK и TreeTagger