Пишем генератор уникальных статей на Erlang

26 июня 2013

В заметке Генерация почти осмысленных текстов на Haskell (опубликованной полтора года назад… как быстро летит время!) был упомянут подход к генерации статей, заключающийся в написании обзоров цифровой техники на основе их характеристик. Недавно у меня дошли руки до реализации этой идеи, правда, на Erlang, а не на Haskell.

Идея следующая. Допустим, мы хотим генерировать статьи о мобильных телефонах. Выбираем модель телефона. Забираем на Яндекс.Маркете характеристики этой модели — вес, разрешение экрана, емкость батареи и так далее. Затем на основе этих характеристик пишем статью. Для того, чтобы обзоры разных устройств не были слишком похожими, воспользуемся { классическим | традиционным } { методом | способом } размножения статей.

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

  • Вес и размер устройства, а также используемая операционная система;
  • Экран — разрешение, диагональ, количество точек на дюйм;
  • Частота процессора и количество ядер в нем, оперативная память;
  • Описание аккумулятора;

Порядок, в котором будут идти параграфы, не имеет значения. Предполагая, что каждый параграф состоит из трех предложений, которые могут идти в произвольном порядке, а также что каждое предложение может быть написано по крайней мере двумя способами, получаем 288 вариантов статей для одного устройства. Этого должно быть достаточно для того, чтобы на постоянной основе обеспечивать уникальными статьями 20-30 сайтов.

Перейдем к реализации. Для описания грамматики, на основе которой будут генерироваться обзоры устройств, воспользуемся Lisp-подобной префиксной нотацией. Грамматика может состоять из следующих правил.

{any, [...]}

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

{concat, [...]}

В этом случае элементы списка должны быть объединены в одну строку.

{shuffle, [...]}

Аналогично concat, только элементы списка перемешиваются случайным образом.

{sentence, [...]}

Подобно concat, только для построения предложений. Первая буква первого слова в списке заменяется на заглавную, а в конце списка добавляется строка ". ".

В списках могут содержаться строки, атомы, кортежи и функции. Строки представляют собой просто строки. Атомы заменяются на соответствующие характеристики устройства. Например, при генерации обзора Samsung Galaxy S3 правило {concat, ["смартфон", " ", model]} эквивалентно {concat, ["смартфон", " ", "Galaxy S3"]}. Кортежи представляют собой вложенные правила. Когда в правиле встречается функция, происходит ее вызов и возвращаемое значение подставляется на место функции. Эта функция может возвращать строку, другое правило или даже другую функцию. В теории можно строить рекурсивные грамматики.

Рассмотрим несколько примеров. Самое верхнее правило, описывающее всю статью целиком, выглядит следующим образом:

article() ->
    {shuffle, [
                fun body_paragraph/0,
                fun screen_paragraph/0,
                fun cpu_paragraph/0,
                fun battery_paragraph/0
             ]}.

А так выглядит одно из правил самого низкого уровня:

device() ->
    {any, [
        "смартфон",
        "телефон",
        {concat, ["смартфон ", model]},
        {concat, ["смартфон ", producer, " ", model]},
        {concat, ["телефон ", model]},
        {concat, ["телефон ", producer, " ", model]},
        {concat, [producer, " ", model]},
        model,
        "аппарат"
    ]}.

Полная версия кода, генерирующего статьи на основе грамматик, составленных описанным выше образом:

gen_article(Str, _Desc, Ctx) when is_list(Str) ->
    {Str, Ctx};

gen_article(Fun, Desc, Ctx) when is_function(Fun, 0) ->
    Rules = Fun(),
    {Str1, Ctx1} = gen_article(Rules, Desc, Ctx),
    Key = term_to_binary(Fun),
    case dict:find(Key, Ctx1) of
        {ok, Str1} ->
            gen_article(Fun, Desc, Ctx);
        _ ->
            {Str1, dict:store(Key, Str1, Ctx1)}
    end;

gen_article(Key, Desc, Ctx) when is_atom(Key) ->
    Property = proplists:get_value(Key, Desc),
    if
        is_list(Property) ->
            {Property, Ctx};
        true ->
            throw({no_such_property, Key})
    end;

gen_article({any, Lst}, Desc, Ctx) ->
    Item = lists:nth(random:uniform(length(Lst)), Lst),
    gen_article(Item, Desc, Ctx);

gen_article({shuffle, Lst}, Desc, Ctx) ->
    NewLst = [X || {_, X} <- lists:sort([ {random:uniform(), Item}
                                          || Item <- Lst])],
    gen_article({concat, NewLst}, Desc, Ctx);

gen_article({concat, Lst}, Desc, Ctx) ->
    lists:foldl(
        fun(Item, {CurrStr, CurrCtx}) ->
            {Temp, NewCtx} = gen_article(Item, Desc, CurrCtx),
            {[CurrStr | Temp], NewCtx}
        end,
        {"", Ctx},
        Lst);

gen_article({sentence, Lst}, Desc, Ctx) ->
    Lower = "абвгдежзиклмнопрстуфхцчшщэюя",
    Upper = "АБВГДЕЖЗИКЛМНОПРСТУФХЦЧШЩЭЮЯ",
    ToUpper = lists:zip(Lower, Upper),
    {Rslt, NewCtx} = gen_article({concat, [{concat, Lst}, ". "]},
                                 Desc, Ctx),
    [H | T] = lists:flatten(Rslt),
    {[proplists:get_value(H, ToUpper, H) | T], NewCtx}.

Обратите внимание, что при вызове функции возвращаемое ею значение сохраняется в словаре Ctx. Если при следующем вызове этой функции возвращается то же значение, что и в прошлый раз, функция вызывается повторно. Это позволяет многократно использовать в грамматике функции типа device/0 с гарантией, что каждый раз функция будет возвращать значение, отличное от предыдущего.

Пример использования генератора:

#!/usr/bin/env escript

%% -*- coding: utf-8 -*-
-mode(compile).

main([]) ->
    <<A:32, B:32, C:32>> = crypto:rand_bytes(12),
    random:seed(A, B, C),
    Desc = [
            {model, "Galaxy Z3000"},
            {producer, "Samsung"}
        ],
    Ctx = dict:new(),
    main_loop(Desc, Ctx, 5).

main_loop(_Desc, _Ctx, N) when N =< 0 ->
    ok;

main_loop(Desc, Ctx, N) ->
    {Text, NewCtx} = gen_article(fun device/0, Desc, Ctx),
    io:format("~s~n----------------------~n",
              [ unicode:characters_to_binary(Text) ]),
    main_loop(Desc, NewCtx, N-1).

Вывод:

Samsung Galaxy Z3000
----------------------
телефон Samsung Galaxy Z3000
----------------------
аппарат
----------------------
смартфон Galaxy Z3000
----------------------
смартфон Samsung Galaxy Z3000
----------------------

Пример статьи, сгенерированной при помощи описанного подхода:

Смартфон Samsung Galaxy Z3000 имеет Super LCD экран. Диагональ дисплея составляет 4.2 дюймов. Число точек на дюйм — 441. Дисплей имеет разрешение 1080 на 1920.

В устройстве используется процессор частотой 1500 мегагерц. Ядер в процессоре четыре. Размер оперативной памяти составляет 2.5 гигабайт, а встроенной — 16 Гб.

Galaxy Z3000 имеет Li-Ion аккумулятор емкостью 2500 мАч.

Телефон Samsung Galaxy Z3000 весит 139 грамм при размере 71x139x7.9 миллиметров. В смартфоне используется операционная система Google Android.

Прямо скажем, до номинации на Букеровскую премию далеко. Однако тут мы получили 500 символов совершенно корректного, уникального и кому-то даже интересного текста. В действительности мы получили практически бесконечное количество символов уникального и корректного текста, поскольку для каждого нового устройства, появляющегося на рынке, генератор способен написать уникальную статью. Все это — за счет написания довольно простой грамматики на 180 строк. Здесь я ее не привожу, ибо набегут сеошники и заспамят наши с вами любимые интернеты такими вот обзорами.

По аналогичной схеме можно писать обзоры планшетов, нет-/ноут-/ультрабуков, mp3-плееров, радиотелефонов, фотоаппаратов, швейных машинок, электронных книг, авторегистраторов и прочей техники. Или не техники, а чего-то другого, скажем, сайтов. Можно попытаться придумать еще разные интересные вещи, позволяющие разнообразить генерируемый текст. Например, при помощи алгоритма ID3 на основе характеристик ноутбука можно делать выводы о том, подходит ли он больше для работы, игр или учебы. Но на мой взгляд в этом нет особого практического смысла.

Метки: , , .


Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.