screenshots | ||
index.html | ||
LICENSE | ||
README.md | ||
wordgen.js |
nickname-generator
Данный скрипт является примером использования на практике алгоритма случайного выбора элементов массива с учётом их веса.
В данном случае практикой будет являться генерирование случайных слов (никнеймов) на основе букв английского (латинского) алфавита.
Начнём для начала с самого простого подхода. Если мы просто будем брать случайные буквы и составлять их них слова, то они будут выглядеть неестественно и неприглядно. Примеры:
- srjxdq
- moyssj
- ywtckmw
- wjvzw
- xtwey
и т.д.
Как видим, такой подход не позволяет нам генерировать слова, которые хотя бы отдалённо напоминали обычные - получается просто набор бессмысленных букв, который больше походит на пароли. Чтобы придать словам натуральность и "человечность", нам нужно сделать как минимум две вещи (на мой взгляд):
- Исключить появления более двух гласных/согласных при генерировании слова. Данная задача является тривиальной и ее не имеет смысла рассматривать.
- Подбирать случайные буквы для слова с учётом их веса. Весами в данном случае будут являться частотность букв в английском языке. Таким образом мы должны уменьшить/увеличить шанс того, что определенная буква попадёт в наше генерируемое слово, и таких редко используемых букв, как, например, Q, Z и X будут встречаться в наших словах гораздо реже, чем E, T, A, O, I, которые по статистике являются самыми частыми в английских словах.
Используя всего два этих подхода, мы генерируем гораздо более натуральные "слова". Примеры:
Алгоритм выбора случайных элементов массива на основе весов в JS
Относительно простой имплементацией подобного алгоритма является преобразование ряда рациональных чисел s1 (массива), являющимися весами для элементов, в ряд чисел s2, который получается посредством кумулятивного сложения чисел:
где Sn
- это массив со значениями весов, ai
- элементы этого массива.
Разберём алгоритм по шагам в JS:
- Создаём в качестве примера два массива
items
иweights
, гдеitems
- это элементы, которые будут выбираться случайно, аweights
- это весы этих элементов:
const items = [ '🍌', '🍎', '🥕' ];
const weights = [ 3, 7, 1 ];
- Подготавливаем массив весов посредством кумулятивного сложения (то есть список
cumulativeWeights
, который будет иметь то же количество элементов, что и исходный список весовweights
). В нашем случае такой массив будет выглядеть следующим образом:
cumulativeWeights = [3, 3 + 7, 3 + 7 + 1] = [3, 10, 11]
-
Генерируем случайное число
randomNumber
от0
до самого высокого кумулятивного значения веса. В нашем случае случайное число будет находиться в диапазоне[0..11]
. Допустим, чтоrandomNumber = 8
. -
Проходим с помощью цикла по массиву
cumulativeWeights
слева направо и выбираем первый элемент, который больше или равенrandomNumber
. Индекс такого элемента мы будем использовать для выбора элемента из массива элементов.
Идея этого подхода заключается в том, что более высокие веса будут "занимать" больше числового пространства. Следовательно, существует более высокая вероятность того, что случайное число попадет в "числовое ведро" с более высоким весом.
Попробую наглядно показать это на нашем примере:
const weights = [3, 7, 1 ];
const cumulativeWeights = [3, 10, 11];
// В псевдопредставлении мы можем представить cumulativeWeights следующим образом:
const pseudoCumulativeWeights = [
1, 2, 3, // <-- [3] числа
4, 5, 6, 7, 8, 9, 10, // <-- [7] чисел
11, // <-- [1] число
];
Как видим, более тяжёлые весы занимают более высокое числовое пространство, а следовательно, имеют более высокий шанс быть случайно выбранными. Процентное соотношение шанса выбора для элементов weights
будет таким:
Элемент 3
: ≈ 27%,
Элемент 7
: ≈ 64%,
Элемент 1
: ≈ 9%
Как можно еще лучше алгоритм генерации слов?
Данный скрипт является больше примером использования алгоритма выбора случайного элемента массива на основе их веса, поэтому я не стал сильно углубляться в лингвистику и алгоритмы искусственного интеллекта. Но навскидку сразу бросаются в глаза неприглядные комбинации некоторых гласных и согласных пар, которые выглядят неестественно и не встречаются в настоящих словах:
- satlenl
- tohhi
- tiowh
- aahepw
и т.д.
Самым простым решением этого вопроса является ограничение на чередование более двух гласных/согласных слов:
if (vowelCounter >= maxVowelsInRow) {
i -= 1;
continue;
}
и
if (consonantCounter >= maxConsonantsInRow) {
i -= 1;
continue;
}
Пусть значения maxConsonantsInRow = 1 и maxVowelsInRow = 1, тогда сгенерированые слова будут выглядеть примерно так:
Отмечу здесь, что th и ae являются диграмами, и считаются как одна буква.
Очевидной минус данного подхода заключается в том, что сгенерированные слова получаются более однотипными и с гораздо меньшим вариативным потенциалом.