Posts Tagged ‘мова’
Як написати собі маленький 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 "беззахисн";
і навіть швидко додати множину для всіх якщо додамо закінчення рід. Правда колір певне варто перейменувати в прикметник, а рід – в граматичну категорію. Але іменування – це вже складніша проблема програмування.
Тепер додайте решту слів і маєте свій перекладач!
Навчання на місці
Навчання мови на місці ефективніше за навчання вдома, бо то постійний екзамен.
Алекс питає чи я не фолбекаю до інглішу. Ну я стараюсь, але бачте, навіть в українській фолбекаю. Часто я говорю як в цьому діалозі:
Me: Um… hast du… der… ah, fuck
German: It’s okay, friend. Learning a second language is difficult, but with enough practice and time you’ll acquire the vernacular and colloquialisms to communicate in a concordant matter vis-à-vis other Germans. I myself still struggle with the endeavor of mastering the English language, ergo, I hope I have articulated myself in a proper manner
Me:
Me:
Me: Fahrrad.
Ну а німці на щастя – ні, вони мають рівень англійської десь як мій.
Дивився німецьке TV. 50% зрозумів.
Вже маю автобус до Мюнхена, і не маю хоста. Страшненько. Букінг ком дозволяє лише на день наперед букати. Доведеться дивитись там адреси і проситись. 🙂
Halt and catch fire
Публікація про те як у мене і мови “все складно” і як я намагаюсь налагодити стосунки.
Halt and catch fire (“зупинитись і загорітись”) – цікава модифікація інструкції HALT, працює як написано лише на комп’ютерах з пам’яттю на магнітних сердечниках в яких збільшено струми доступу. Також телешоу все ще в розробці. Терміну вже більше 50 років (Пам’ять на магнітних сердечниках, ви знаєте що це? Отож бо, я теж не знаю.), а я про нього дізнався лише коли перекладав Dive into Python.
І от біда, я не знаю як перенести цей термін в український культурний контекст. Зрозуміло що коли Марк Пілігрим використовує термін HCF, він має на увазі що програма повинна з шумом і тріском завалитись. Але як це зрозуміло і коротко виразити?
Тим часом на вікіпедії триває війна “Зневадження” проти “Налагодження”, чи то пак навпаки. Всі словники, крім якогось там Мейнаровича-Кратко – за другий термін. Кількість результатів в Google – теж за другий, хоча в документах які він знаходить частіше йдеться про “налагодження стосунків”. Я стараюсь триматись і не лізти, тому що війна – зло. Так, там танки не горять, але суму витрачених зусиль вже можна прирівняти до десяти-двацяти черг з автомата (7.62×33 мм патрон коштує на ринку в середньому 50 центів за штуку, черга – 15$. Люди які заробляють 5-20$ за годину (ну, чи проблеми чим годувати дітей точно не мають, бо інакше б на вікіпедію не мали часу), могли б свій час використати якось приємніше, чи продуктивніше. Й не обов’язково працюючи на заводі з виготовлення патронів…)
Статтю майже ніхто (я так точно) далі назви читати не буде (бо аби справді прочитати щось корисне на цю тему – треба гуглити hg bisect
та ipdb.set_trace()
чи що там ви збираєтесь використовувати), то чому за неї так воюють? Тому що стояти на своєму – це приємно, а також тому що вікіпедія – таки словник, що б там не казали правила і переклад слова debug я, і напевне багато інших будуть дивитись саме там. І це зараз філологи на вікіпедію кажуть “фу, там так багато помилок, що ми туди ні ногою”, а через років 5-10 їм доведеться вивчати мовний корпус вікіпедії, і напевне адаптувати мовні норми до нього, нікуди вони дінуться.
Що ще? Занадто просте автовиправлення правопису може бути поганою ідеєю, хоча б тому що “аргументу” – це, можливо, не помилково написаний родовий відмінок “аргумента”, а давальний відмінок цього ж слова. “Графа” – це, можливо, не помилково написаний родовий відмінок слова чоловічого роду, яке означає титул, або математичну структуру, а називний відмінок іменника жіночого роду що означає поле форми наприклад. Тому я зараз активно дивлюсь в сторону hunspell, вже скачав, поставив плагін для Firefox (який здається не працює), скачав останню версію (1.6.6) словника uk_UA.dic, вивчаю його консольний інтерфейс і прив’язки до Vim та Python.
Щодо граматики, я все думаю взяти гарний україномовний корпус (де його взяти?), записати в базу 5-грами, а далі підсвічувати слова залежно від частоти знайдених в ньому n-грам. Чим рідкісніша n-грама – тим червоніший шрифт чи підкреслювання.
bwikibot 0.4.2, відтепер з автовиправленням правопису!
Автовиправлення правопису, як пізніше виявилось – передчасна ідея, але суть публікації в тому що bwikibot активно розвивається і на сьогодні вже має версію 0.4.13. Очікуйте 0.5.0!
Правда не всього правопису, а лише деяких іменників чоловічого роду в родовому відмінку, але й це вже краще ніж нічого.
bwikibot встановлюється просто як:
sudo pip install bwikibot
Або, якщо цією програмою ще хтось крім мене користується (дякую вам) і вже встановив, то оновлюємо:
sudo pip install --upgrade bwikibot
Нагадаю що pip повинен бути для третього Python, підтримка другого можлива лише теоретично, якщо хтось допоможе мені з тестами.
Після чого виправлення правопису запускається командою:
python3 main.py spell "Пориньте у Python 3/Модульне тестування"
І якщо вказана сторінка існує на вікі, на яку він залогінений, то текст цієї сторінки буде прочитано, будуть здійснені всі можливі автозаміни, вам буде показаний диф між новою і старою версіями (треба буде подумати над тим, як зробити так аби цей диф виглядав коротшим) і після отримання від вас згоди на запис, виправлена версія записана назад у вікі.
Що він може? Небагато, поки-що. В нього є словник genitive_a_u.txt
, в якому містяться слова чоловічого роду в родовому відмінку з закінченнями “а”, “у”. Словник поки що невеликий, містить сім слів: аргументу, графа, менеджменту, методу, об’єкта, параметра, файла.
Проте це слова які я не вмію правильно писати, хоча пишу часто. 🙂 Словник буде доповнюватись. Потім може ще додам словник автовиправлень частих помилок загального виду, і якісь n-грами.
Ну і щойно виявив помилку. Мій коректор хоче замінювати слово “аргументами” на “аргументуми”. 🙂 Треба згадати що там в кінці шаблону треба поставити… Здається "аргумент\b"
?
А ще, коли проект виросте до справді корисного, напевне варто буде його відокремити і додати можливість перевірки і автовиправлення блоґу через xml-rpc.
Ах, і якщо хтось знає як не винаходити велосипед, а інтегрувати aspell чи щось подібне – поділіться досвідом.
Доповнення на 16:15 : Помилку з “аргументуми”, занадто довгі дифи, виправлено в версії 0.4.3, також для цієї версії тести проходяться як в Python3 так і в Python2, але вручну я тестував тільки 3.