Monthly Archives: Лютий 2020

Як написати собі маленький Google Translate на Grammatical Framework за 15 хв

І з якістю трохи кращою ніж в Google Translate, але з обсягом можливих перекладів набагато меншим.

Що я роблю на роботі

Зараз я працюю в Zalando – компанії яка продає різний крам в купу країн Європи. Відповідно, треба підтримувати сайт для багатьох країн. Я працюю у відділі який займається інструментами що допомагають створювати наповнення сайту. Процес додавання нового вмісту на сайт дуже трудомісткий, бо потрібно сфотографувати продукт, на столі, на моделі, перевірити розміри, матеріали і інструкції з догляду, написати опис, заголовок і т.п.. А потім ще перекласти це на 8 мов і кілька локалей, бо деякі країни DACH (німецькомовний простір) не входять до єврозони і використовують швейцарські франки наприклад. Це велика купа часу, і великий простір для автоматизації.

Google Translate не підходить, тому що наприклад ми продаємо скатертини, і німецький менеджер пише “Tischdecken” (множина від Tischdecke), Google Translate думає що це дієслово (бо є таке дієслово), і перекладає “Set the table” (накривайте столи!).

Насправді задача стоїть простіша ніж повний машинний переклад, бо дані отримуються не якоюсь конкретною мовою, а набором параметрів – “що”, “коли”, “яке”, “для кого”, “по чому” і т.п.. Тобто якщо я отримую що=взуття, яке=класичне коли=зима для кого=хлопчики, по чому=<100 € то я (в сенсі моя програма), пише:

'de_CH': 'Klassische Winterschuhe für Jungs - unter CHF 107',
'de_DE': 'Klassische Winterschuhe für Jungs - unter 100 €',
'en_GB': 'Classic Winter Shoes for Boys - under £85',

Все просто – перекладаємо слова за словником і підставляємо в шаблон для конкретного набору компонентів. З німецькою лише один виклик – з’єднання слів. Тобто якщо я з’єдную Winter з чимось – просто конкатеную, а от коли Fruhling – додаю між Fruhling і тим словом яке приєдную ще “s”. Є ще інші правила.

Складність з’являється в слов’янських мовах. Там є відмінювання прикметників у множині.

На роботі я багато думаю як би то не робити роботу яку вже хтось зробив за мене

Програмісти ліниві створіння. Нащо писати код якщо код вже є написаний на StackOverflow і Github? Треба лише знати де той код лежить. І я почав собі шукати як інші люди це робили. До того як з’явилось машинне навчання і статистичні методи, всі пробували підхід на основі правил. І проміжної мови.

З проміжною мовою процес розбивався на дві частини – спершу проводився синтаксичний аналіз тексту на мові джерела, тоді з побудованого представлення тексту на проміжній мові (зазвичай дерево) будувався текст іншою мовою.

Моя задача вже простіша, бо ніякого синтаксичного аналізу робити не треба, треба просто згенерувати текст 8-ма мовами на основі структури даних. Це як два пальці відрендерити контекст за допомогою шаблону, ми веб-деви тільки таке й робимо.

Тільки виявляється що шаблон для генерації простого заголовку на зразок: “Mindestens 500 € sparen – Premium Herbstmode” або “Premium Herbstmode unter 999 €” займає мало не ввесь екран.

Я подумав що може нарешті настав той момент коли я на Кубику не даремно вчився, пам’ятаю Opa Chomsky Style, і треба застосувати якусь контекстно-вільну граматику, чи яка там наступна за потужністю, бо контекстно-вільна здається не потягне слов’янську морфологію.

І раптом в своїх пошуках надибав Grammatical Framework. Це штука яку вже більше ніж 20 років пишуть на Хаскелі, значить люди вміють точно більше ніж я. І автор приходить з доповіддю в Google, в якій кілька слайдів на початку – суцільний тролінг команди Google Translate:

Але мене підкупило речення з їх сайту, http://www.grammaticalframework.org/

GF is easy to learn by following the tutorial. You can write your first translator in 15 minutes.

До роботи!

Ну що ж, заводимо таймер, і спробуємо наприклад написати перекладач, що може перекласти набір шахматних словосполучень /(black|white) (queen|king)/ з англійської на українську. Шахмати, тому що одягом я й на роботі можу зайнятись, а блог я пишу вдома і хочеться трохи змінити контекст.

Для початку варто встановити компілятор/інтерпретатор gf. На сторінці завантаження є пакети для різних ОС, з інструкцією про те як їх поставити. Ще можна поставити плагін підсвітки синтаксису для вашого редактора, перелік є на цій сторінці.

Hello, world!

Тепер, в файлі Chess.gf пишемо таку граматику:

-- це, до речі, коментар
abstract Chess = {

  flags startcat = Piece ;

  cat PieceType ; Color ; Piece;

  fun
    piece : Color -> PieceType -> Piece ;
    Black, White : Color;
    Queen, King: PieceType;
}

abstract означає що ми описуємо граматику проміжної мови, тобто не якоїсь конкретної людської, а мови якою ми описуватимемо сенс тексту.

flags startcat = Piece ; означає що початковою категорією (коренем дерева) буде фігура.

cat перелічує можливі категорії (типи вузлів дерева).

Секція fun описує функції. Функції описують як будується дерево, і можуть бути будь-якої арності, в тому числі нулярні. Наприклад тут функції Black та White не приймають аргументів але повертають колір. (По суті – константи). Зате функція piece приймає дві категорії і повертає категорію для фігури.

Тепер про те як перетворити дерево на послідовність токенів якоюсь мовою. Це називається лінеаризацією, і описується конкретною граматикою:

concrete ChessEng of Chess = {

  lincat Piece, Color, PieceType = {s : Str} ;

  lin
    piece color type = {s = color.s ++ type.s} ;
    Black = {s = "black"} ;
    White = {s = "white"} ;
    Queen = {s = "queen"} ;
    King = {s = "king"} ;
}

Зберігаємо її в файлі ChessEng.gf. Для GF важливо щоб кожна граматика була в своєму файлі і щоб він називався так само як називається граматика, інакше він дає помилки.

lincat описує які типи токенів будуть відповідати категоріям абстрактної категорії. В нашому випадку це структура з одним полем типу Str, тому що пізніше нам знадобляться інші поля.

lin описує функції лінеаризації типи яких були описані в абстрактній граматиці. Для унарних функцій – як власне пишеться слово, для інших – як скомбінувати інші лінеаризації.

Щоб потестувати як все працює, запускаємо команду gf Chess*, яка завантажує обидві граматики і починає інтерактивну сесію.

Тут ми можемо наприклад перевіряти наші речення на правильність:

Chess> parse "black queen"
piece Black Queen

Chess> parse "snow queen"
The parser failed at token 1: "snow"

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

Chess> generate_random
piece White Queen

Або його лінеаризацію:

Chess> generate_random | linearize
black queen

Або всі можливі речення:

Chess> generate_trees | l
black king
black queen
white king
white queen

Морфологія

Тепер давайте зробимо переклад на українську! Для цього треба описати граматику української. Додаємо переклад з англійської в файл ChessUkr.gf:

concrete ChessUkr of Chess = {

  lincat Piece, Color, PieceType = {s : Str} ;

  lin
    piece color type = {s = color.s ++ type.s} ;
    Black = {s = "чорний"} ;
    White = {s = "білий"} ;
    Queen = {s = "королева"} ;
    King = {s = "король"} ;
}

Завантажуємо і тестуємо перекладач в консолі:

Chess> parse -lang=ChessEng "black king" | linearize -lang=ChessUkr
чорний король

Дуже добре!

Chess> parse -lang=ChessEng "white queen" | linearize -lang=ChessUkr
білий королева

От засада! Навіть Google Translate вміє краще. Треба ввести в українську граматику поняття роду. Для цього до іменників треба додати інформацію про те якого вони роду, а кольори мають описувати як вони відмінюються залежно від роду:

concrete ChessUkr of Chess = {

  param Gender = Masc | Fem ;
  lincat Piece, PieceType = {s : Str; gender : Gender} ;
  lincat Color = {s : Gender => Str} ;

  lin
    piece color type = {s = color.s ! type.gender ++ type.s; gender=type.gender} ;
    Black = {s = table {
        Masc => "чорний";
        Fem => "чорна"
    } };
    White = {s = table {
        Masc => "білий";
        Fem => "біла"
    } };
    Queen = {s = "королева"; gender=Fem} ;
    King = {s = "король"; gender=Masc} ;
}

Тут нам і стало в нагоді те що наші категорії – це структури, а не просто рядки. Чіпляємо до кожної фігури інформацію про стать.

Gender => Str задає тип таблиці (це майже як функція, тільки описується чимось типу хеша). Кожен колір тепер – це таблиця. Операція “!” – це вибір значення з таблиці.

Тестуємо нову граматику української:

Chess> generate_trees | l
black king
чорний король
black queen
чорна королева
white king
білий король
white queen
біла королева


Chess> parse -lang=ChessUkr "білий король"
piece White King

Chess> parse -lang=ChessUkr "біла король"
The parser failed at token 2: "\1082\1086\1088\1086\1083\1100"

Хаха, він думає що ми неправильно вжили слово король, бо після біла може стояти лише королева. Але помилку знайшов!

Рефакторинг

Накодили, потестували, тепер пора зробити код гарнішим.

Писати словник в форматі:

    White = {s = table {
        Masc => "білий";
        Fem => "біла"
    } };

Коли ми можливо захочемо додати ще слів які можливо матимуть ще середній рід і множину – доволі трудозатратно. Але ми можемо написати функцію. Точніше оператор, бо в GF функцією ми вже назвали штуку яка будує дерево.

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

Тоді ми можемо додати в українську граматику такий оператор:

  -- hard adjective
  oper ha: Str -> {s: Gender => Str} = \stem -> {
      s = table {
          Masc => stem+"ий";
          Fem => stem+"а"
      }
  };

Він приймає рядок і повертає токен в якого s – це таблиця що відображає рід на написання.

І тепер ми можемо швидко додавати багато прикметників:

    Black = ha "чорн";
    White = ha "біл";
    Fast = ha "швидк";
    Defenceless = ha "беззахисн";

і навіть швидко додати множину для всіх якщо додамо закінчення рід. Правда колір певне варто перейменувати в прикметник, а рід – в граматичну категорію. Але іменування – це вже складніша проблема програмування.

Тепер додайте решту слів і маєте свій перекладач!