Archive for the ‘Кодерство’ Category
Як написати собі маленький 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 "беззахисн";
і навіть швидко додати множину для всіх якщо додамо закінчення рід. Правда колір певне варто перейменувати в прикметник, а рід – в граматичну категорію. Але іменування – це вже складніша проблема програмування.
Тепер додайте решту слів і маєте свій перекладач!
Hunspell
Hunspell розшифровується як “угорський правопис”, і це найбільш просунута програма перевірки правопису якщо не враховувати Grammarly. А все тому що угорська мова – найбільш скажена в плані морфології. Тому якщо щось підходить для угорської – для інших європейських мов точно підійде.
sudo apt install hunspell hunspell-uk hunspell-de-de libhunspell-dev
sudo pip install hunspell
hunspell має доволі простий інтерфейс для використання в мовах програмування (хоча й складні словники (читати man hunspell.5
)):
>>> import hunspell
>>> spellchecker = hunspell.HunSpell('/usr/share/hunspell/uk_UA.dic', '/usr/share/hunspell/uk_UA.aff')
>>> spellchecker.spell('ласка')
True
>>> spellchecker.spell('ласкає')
False
>>> spellchecker.suggest('ласкає')
['ласка', 'ласкам', 'лускає', 'ласках', 'ляскає', 'ласка є', 'скалатає']
Як збудувати геодезичний купол?
Я простий програміст, але іноді свербить побути архітектором. Всякий ентерпрайз проектувати ще не пускають, але тут випала нагода зпроектувати проект іграшкової хатинки для донечки. Тому що після покупки меблів в порожню квартиру в нас все ще залишається вільна кімната і в ній куууупа картону.
Я погуглив які можна робити хатинки і знайшов таке на instructables. Але там виміри подані все ще в імперській системі і я вирішив перерахувати. І взагалі зробити проект правильною водоспадною методологією – поетапно.
Прочитати решту цього запису »
Корисні налаштування Git
Перше. Як не задовбувати всіх сміттям яке створює ваше IDE, і за замовчуванням мати gitignore для всіх репозиторіїв.
git config --global core.excludesfile ~/.gitignore
Ця команда відредагує файл ~/.gitconfig
і в ~/.gitignore
можна буде перелічити всякі там *.swp
, чи що там ваш редактор створює.
Друге. Якщо Go не хоче встановлювати модулі з помилкою “fatal: could not read Username for ‘https://github.com’: terminal prompts disabled”, бо залежності прописані через https, а ви використовєуте SSH, це можна виправити таким налаштуванням:
git config --global url."git@github.com:".insteadOf "https://github.com/"
На цьому поки що все, дякую за увагу. 🙂
Скільки слів треба щоб написати вікіпедію? І які зустрічаються частіше?
Виявляється відтоді як я рахував символи вікіпедії пройшло вже більше року. Рахував я їх за допомогою Go, хоча можна було сильно спростити собі життя і рахувати їх за допомогою Python і pywikipediabot. Сьогодні розкажу як, і як можна побачити з назви – рахуватимемо слова.
Я чомусь боявся що щоб порахувати слова пам’яті не вистачить, тому треба якусь базу даних. Або пробувати все в пам’яті, але аби комп’ютеру не стало погано якось обмежити доступну пам’ять. Але мої 4Gb використовувались лише щось трохи більше ніж на 40% для підрахунку всіх слів включно зі сторінками обговорень, категорій, шаблонів, сторінок опису файлів, і т.п. німецької вікіпедії.
В модулі pywikibot.pagegenerators
є об’єкти XMLDumpOldPageGenerator
і XMLDumpPageGenerator
. Вони приймають назву архіву з XML дампом в конструкторі, а після створення по них можна ітеруватися отримуючи об’єкти сторінки. Не ведіться на слово “Old” в назві першого об’єкта, це означає не депрекацію, а те що текст сторінки буде братись з дампа, а в другому випадку з дампа буде братись лише заголовки, а за свіжим текстом зроблять запит, що сповільнить обробку разів в 500. Тобто замість кількох годин ви будете чекати рік. 🙂
Я спробував проаналізувати німецьку вікіпедію (код буде пізніше), на це пішло 694 хв (трохи менше ніж 12 годин) і вийшло що в ній на 6,425,028 сторінках використовується 2,381,457,397 слів (приблизно 371 на сторінку), з них різних слів 18,349,393. В кінцевому результаті CSV з частотним словничком виходить на 300MB.
Серед тих що зустрічаються лише раз є слова типу PikettdienstPikettdienst (помилка парсера який видаляв розмітку), слово – це юридичний термін швейцарської німецької і перекладається як “служба за викликом”. І є слова на зразок Werkshöfe – подвір’я фабрик.
Топ 50 слів виглядає так, і складає 28% всіх слів загалом:
der | 58761447 | sich | 9169933 |
und | 49084873 | wurde | 9114619 |
die | 44536463 | CET | 8614461 |
in | 35684744 | an | 8385637 |
von | 24448221 | er | 7835324 |
ist | 20614114 | dass | 7550955 |
den | 19454023 | du | 7435099 |
nicht | 17519638 | bei | 7420172 |
das | 17302844 | Diskussion | 7237855 |
zu | 16167971 | aus | 7065523 |
mit | 15906145 | Artikel | 6967243 |
im | 15167140 | oder | 6824420 |
des | 14661593 | werden | 6508092 |
für | 14016308 | war | 6449858 |
auf | 13957013 | nach | 6426826 |
auch | 12849476 | wird | 6117566 |
eine | 11903977 | aber | 6052645 |
ein | 11780352 | am | 6017703 |
Kategorie | 11369651 | sind | 5953632 |
als | 11167157 | Der | 5623930 |
dem | 11124726 | Das | 5545595 |
CEST | 11104741 | einen | 5465687 |
ich | 10886406 | noch | 5409154 |
Die | 10761776 | wie | 5293658 |
es | 10204681 | einer | 5228368 |
До списку потрапили такі вікіпедійно специфічні слова як Kategorie (сторінки без категорій вважаються не комільфо), CEST і CET (центральноєвропейський літній час і центральноєвропейський час, в підписах в обговореннях).
Ну а що без сторінок обговорень? Проблемав тому, що при створенні об’єкту сторінки XMLDumpOldPageGenerator
бере з дампа лише текст і заголовок, простір імен залишається не заповненим і за замовчуванням 0 (основний). Є ще поле isredirect
так при спробі доступу до нього знову здійснюється запит. Тому, краще перейти на рівень нижче і використати XmlDump
з pywikibot.xmlreader
, він використовується так само, просто дає об’єкти не Page, а попростіші, які не вміють робити запити до вікіпедії і не мають методу save
. Але нам його й не треба, правда?
Ось код який ігнорує перенаправлення і всі сторінки крім статтей:
"""Count word frequencies in wikipedia dump""" import csv from collections import Counter from itertools import islice import re import sys import mwparserfromhell from pywikibot.xmlreader import XmlDump def main(): """Iterate over pages and count words""" if len(sys.argv) < 2: print('Please give file name of dump') return filename = sys.argv[1] pages = 0 words = 0 words_counts = Counter() print('Processing dump') for page in XmlDump(filename).parse(): if (page.ns != '0') or page.isredirect: continue try: text = mwparserfromhell.parse(page.text).strip_code() except Exception as e: print(page.title, e) continue text = text.replace('\u0301', '') # remove accents # Ukrainian: # page_words = re.findall( # r'[абвгґдеєжзиіїйклмнопрстуфхцчшщьюя' # r'АБВГҐДЕЄЖЗИІЇЙКЛМНОПРСТУФХЦЧШЩЬЮЯ’\'-]+', # text # ) # Any language: page_words = re.findall(r'\b[^\W\d]+\b', text) pages += 1 words += len(page_words) words_counts.update(page_words) if pages % 123 == 0: print('\rPages: %d. Words: %d. Unique: %d. Processing: %s' % ( pages, words, len(words_counts), (page.title + ' ' * 70)[:70], ), end='') print('Done. Writing csv') with open('common_words.csv', 'w', newline='') as csvfile: csvwriter = csv.writer(csvfile) for item in words_counts.most_common(): csvwriter.writerow(item) if __name__ == '__main__': main()
Він працює майже вдвічі швидше, 381 хвилину, бо обробляє лише 2,295,426 сторінок (обсяг німецької вікіпедії цього року). На цих сторінках є 1,074,446,116 слів (в середньому 468 на сторінку), з них різних – 12,002,417. (Виявляється є аж 6 мільйонів всяких слів які вживаються на всіляких службових сторінках німецької вікіпедії, і яких нема в статтях).
Якщо ж взяти українські статті, то на них треба ще менше часу – 131 хвилину (забув уточнити що в мене SSD), їх є 923238 (скоро мільйон!), слів 238263126 (в середньому 258 на сторінку, треба доповнювати 😉 ). З них різних – 4,571,418. Отак, в мене тепер є частотний словник української на 4.5 мільйони слів. І німецької на 12 мільйонів.
Хоча не спішіть з висновками що українська мова бідніша, бо мої методи потребують вдосконалення. По перше, так як Morgen (ранок) і morgen (завтра) – різні слова, то я не приводив букви в німецькій до одного регістру. (Правда й в українській забув це зробити).
По друге, в німецькому словнику 350590 разів зустрічається слово “www”, бо я вважав словом будь-яку послідовність літер латинки, а в українській відфільтрував кирилицю. Слово youtube зустрічається 8375 разів, а значить є ризик знайти якесь рідкісне слово на зразок “fCn8zs912OE”. 🙂
На WordPress глючить додавання картинок, тому нате вам відео:
А, і ось топ-10 української вікіпедії:
в,4551982
на,3730686
і,3475086
у,3353796
з,3053407
-,2695783
Категорія,2417267
та,2350573
до,1815429
року,1553492
Частота “року” наводить на думку що в українській вікіпедії якийсь перекос на історичні методи викладу. 🙂