I’m bringing to an end this series of mildly interesting useless factoids on the Google Translate ability to recognize the likely language judging by just the one or two initial letters. Polish language uses almost all letters of the Roman alphabet and adds nine specific ones: ą, ę, ć, ł, ń, ó, ś, ź, ż. Some of these are also used by other languages like Lithuanian, but I suspected that all would be detected on their own as being a part of a Polish word by Google Translate. Of course this an example of Central Europe-centric thinking ;-) Was I right?

It turns out that ć, ę, ł, ś, ź, ż are indeed recognized as Polish. The letter ą was marked as English, which might mean “no detection, use default”. It’s debatable whether this behaviour is correct since no valid Polish word can start with ą. On the other hand ę never happens at the beginning of a word and it was detected as Polish. What’s interesting – the letter ó was marked as probable Irish and ń as Yoruba. I understand that in those languages ó and ń are just versions of the base letter with added acute accent, whereas in Polish they have quite distinct pronunciation from o and n.

Speaking of accents here’s some bonus information: (single apostrophe) is guessed to be Afrikaans and ` (back apostrophe): Armenian.

Finally here are the changes in the proposed language to translate from when typing “ósemka” – the name of the digit 8 and one of the few words that start with ó in Polish.

ó      Irish - detected
ós     Galician - detected
óse    Irish - detected
ósem   Icelandic - detected
ósemk  Icelandic - detected
ósemka Polish - detected


I wrote a script that ran all two letter word prefixes against Google Translate page and stored the auto detected language. It has no real use apart from a long list of mildly interesting factoids, for example “ji” prefix is most likely to be Czech, “sy” – Afrikaans and “ta” – Maltese etc.

Here’s the visualization of the prefix to language mapping done using PIL. I have used HSV color space so that the colors would be uniformly distributed among the hues, although the less saturated ones seem to be difficult to distinguish from one another. Each color represents a language, the languages are listed at the bottom. Generation of N easily distinguishable colors seems to be an interesting problem in itself.

Note that letter combinations unlikely to prefix a word in any language seem to default to English.

Two letter prefixes and detected languages

Two letter prefixes and detected languages

Disclaimer: all data came from Google Translate.

When you use Google Translate with the “Instant translation” and “Detect language” options turned on, the language is detected as soon as you type the first letter.

For example a single letter “a” is identified as a text most probably in English and both “w” and “z” are likely to be Polish. These happen to be valid words in the relevant languages.

Here is the chart of the various languages detected in response to all the letters of the Roman alphabet.

Letter to likely language mapping chart

Letter to likely language mapping chart

Note that most letters are defaulted to English. Of course this Google Translate behaviour probably depends on the user country and language settings, so your mileage can vary.

In the next installment in this series I’ll write about the language specific characters and multi-letter prefixes.

Replacing each letter by its counterpart in reversed order alphabet – English

Start with loading the list of words (plenty English word lists are available on the Internet)

In [1]:
words = [ w.strip() for w in open(r"D:\opt\data\words.txt", "r").readlines() ]
In [2]:
In [3]:
# I wonder what the longest word is
max(len(w) for w in words)
In [4]:
# out of curiosity...
[ w for w in words if len(w) == 31]

Filter the words leaving only ones consisting of letters 'a'-'z'

In [5]:
import string

az_words = [ w for w in words if set(w) <= set(string.ascii_lowercase) and len(w) > 0 ]
In [6]:

Define the mapping of each letter to its counterpart in the reversed alphabet, e.g. 'a' ↔ 'z', 'b' ↔ 'y' etc.

In [7]:
m = dict( (k, chr(ord('a')+ord('z')-ord(k))) for k in string.ascii_lowercase )
{'r': 'i', 'l': 'o', 'i': 'r', 'c': 'x', 'w': 'd', 'd': 'w', 'z': 'a', 'b': 'y', 'h': 's', 't': 'g', 'y': 'b', 'p': 'k', 'u': 'f', 'x': 'c', 'o': 'l', 'v': 'e', 'a': 'z', 's': 'h', 'q': 'j', 'f': 'u', 'n': 'm', 'g': 't', 'j': 'q', 'm': 'n', 'k': 'p', 'e': 'v'}

In [8]:
def map_word(w):
    return "".join(m[letter] for letter in w)

print(map_word('a'), map_word('zz'), map_word('abc'))
z aa zyx

The original example – WIZARD ↔ DRAWIZ

In [9]:
# note the usage of the Martian smiley
map_word('wizard') == 'wizard'[::-1]

Let's find all the 'wizard' words in English!

In [10]:
wizard_words = [ w for w in az_words if map_word(w) == w[::-1] ]
In [11]:
In [12]:

Notably wizard is one of two longest wizard words, the other one is hovels.

Another question

How many mapped words become valid words? That is, we no longer check if the reversed mapped word is the original one. We only check if the mapped word exists in the dictionary. For example if abc becomes xyz after mapping we accept abc as a member of the new set if xyz is also in the dictionary.

In [13]:
az_set = set(az_words) #speed up lookup
good_words = [ w for w in az_words if map_word(w) in az_set ]
In [14]:

Find the longest words that are still valid after being remapped, as well as the words they map to.

In [15]:
maxlen = max( len(w) for w in good_words )
[ (w, map_word(w)) for w in good_words if len(w) == maxlen ]
[('brigs', 'yirth'),
 ('drogh', 'wilts'),
 ('droob', 'willy'),
 ('erizo', 'viral'),
 ('girth', 'trigs'),
 ('grogs', 'tilth'),
 ('tilth', 'grogs'),
 ('trigs', 'girth'),
 ('viral', 'erizo'),
 ('willy', 'droob'),
 ('wilts', 'drogh'),
 ('yirth', 'brigs')]

Here’s a simple toy project – find all the word squares – a word square is a n x n matrix of letters where each row and column is a word. I used brute force approach and no recursion (to make it harder), so this algorithm may be easily improved. Let’s get the words:

words = [ w.strip() for w in open("myfavoritedictfile.txt", "r").readlines() ]

Let’s define the class for the word square. Being lazy, I will not define a class at all.

import collections
Square = collections.namedtuple('Square', ['n', 'hlist', 'vlist'])

Hlist is the list of horizontal words. Vlist is for vertical ones.

Let’s check if the new word can be added – we need to verify each cell where it crosses another word. This is symmetrical, hence we need only one function and swap the lists if needed.

def add(list1, list2, w):
    k = len(list1)
    for i, e in enumerate(list2):
        if e[k] != w[i]:
            return False
    return True

Now the actual function that fills the word square and returns the results as the iterables.

def fill_iter(n, words):
    count = 0
    z = Square(n, [], [])
    nw = len(words)
    stk = [ 0 ]
    while 0 < len(stk) <= 2 * n:
        count += 1

        if len(stk) % 2:
            r = add(z.vlist, z.hlist, words[stk[-1]])
            r = add(z.hlist, z.vlist, words[stk[-1]])
        if count % 1000000 == 0:
            print(count, z, words[stk[-1]])           

        if r:
            if len(z.hlist) == len(z.vlist) == n:
                yield z
                r = False
                if len(stk) % 2:
        if not r:
            stk[-1] += 1            
            while stk[-1] == nw:
                if len(stk) == 0:
                    return None
                if len(stk) % 2:

                stk[-1] += 1 
    return None

Since this approach is very slow, let’s reduce the search space a bit. Here’s how to get all the 5-letter words that remain valid words if read backwards e.g BUS-SUB. If we limit ourselves to these words our word square will be a “magic” word square (almost, since we do not check the diagonal). And finally – run!

s5 = set(w for w in words if len(w) == 5)
rw5 = [w for w in s5 if w[::-1] in s5 ]

for z in fill_iter(5, rw5):

Just a few selected examples in Polish:

Square(n=5, hlist=['igrom', 'glebo', 'rener', 'obelg', 'morgi'], vlist=['igrom', 'glebo', 'rener', 'obelg', 'morgi'])
Square(n=5, hlist=['momus', 'odoru', 'motor', 'urodo', 'surom'], vlist=['momus', 'odoru', 'motor', 'urodo', 'surom'])
Square(n=5, hlist=['kinaz', 'imaga', 'nagan', 'agami', 'zanik'], vlist=['kinaz', 'imaga', 'nagan', 'agami', 'zanik'])

Here’s a programming or mathematical puzzle: given integers n, m such that n>=1 and m<=2^n, compute how many “1” digits are there in ((2^n)-1)*m written in binary.

For example, if n=3 and m=2:

(2^3-1)*2=7*2 = 14 = 0b1110, so the answer is 3.

You can think about how to solve this puzzle or simply read the rest of the text.

Probably everyone who memorized the multiplication table found the multiplication by 9 relatively easy. Nine times two is eighteen, 9*2=18, and the digits of the product add up to 9, 1+8=9. The same pattern holds for 9*3=27, 2+7=9 and so on until 9*9=81, 8+1=9 and 9*10=90, 9+0=9. This little trick helps with remembering and verifying the results of multiplication of single digits by 9.

If we keep multiplying 9 by larger multipliers will the sum of the digits of the product still equal 9?

No, the pattern breaks here: 9*11=99, 9+9=18, even though it is further resumed at 12, 9*12=108. Still, we can do better if we replace 9 by a bigger number.

What probably few people realize, the number 9 is just one of the numbers with the curious property that the sum of the digits of their product by any of [1..the original number + 1] is constant. For example 99:

 99 *  1 =   99, 9+9=18
 99 *  2 =  198, 1+9+8=18
 99 *  3 =  297, 2+9+7=18
 99 * 99 = 9801, 9+8+0+1=18
 99 *100 = 9900, 9+9+0+0=18

Further such numbers are 999, 9999, etc. Multiplies of 9, 99, 999… also have a similar property which follows from the above fact. For example 4995, 4+9+9+5=27, can be multiplied by any number between 1 and 1000 and the sum of the digits will be 27, e.g. 4995*765=3821175, 3+8+2+1+1+7+5=27.

If the curious property holds for (10^n)-1 for n=1..infinity in base 10, is this true in other numerical bases?

Yes, and the pattern is similar. In base 3, the counterparts of the number above would be written as 2, 22, 222 etc. In base 16: F, FF, FFF… In base 2, i.e. binary these are 1, 11, 111, 1111 etc. For example 2^8-1=255=11111111(base 2) multiplied by any value 1..256 will still have exactly 8 ones when written in binary:

 0b11111111 * 2 = 0b111111110
 0b11111111 * 3 = 0b1011111101
 0b11111111*123 = 0b111101010000101
 0b11111111*255 = 0b1111111000000001
 0b11111111*256 = 0b1111111100000000

The obvious fact that in case of binary the count of “1” digits is equal to the sum of all digits produces the simplest answer to the puzzle at the top.

Following this very inspiring post by Andrej Karpathy a few people reported success with generating text or even music when using the published source code. I wondered whether I should have a try too since I don’t have much recent experience with neural networks. Although I used to be interested in them a long time ago.

I did take a Neural Networks course during my studies. I remember trying to teach a simple NN on an Sun workstation to recognize shapes on a very small grid, although I don’t recall if it was a success. In fact I became a bit of a sceptic regarding this area after I learned that neural networks are considered (or were at the time) little more than numerical approximation of a multi-dimensional function. So it was maths, very advanced maths… and I lost interest.

Considering that someone did the tricky math for me and went so far as to provide the source code, the answer was – go for it. As for the text selection: I wanted to try a language other than English and chose a Polish author, Nobel prize winner, Henryk Sienkiewicz. His “Trilogy” provided me with 5.5MB of XIX century prose stylized for XVII century Polish. The style is very recognizable for an educated Pole. I wondered if the generated text would be something that could be mistaken for ‘real’ Sienkiewicz.

I had to install Torch7 which requires Linux or OS X, luckily I had a Mac available. This went well apart from some minor trouble with SSL which did not affect the RNN scripts. A major problem was found when I ran the training script for the first time with the recommended parameters for the network: 2 layers of 512 nodes. Unfortunately on my machine the script reported running out of memory. Was that a LuaJIT related problem? I don’t know. The training worked with 256 nodes. But that has probably affected the accuracy of the recognition a lot. I could not use GPU which meant a long wait for the results. After the first training completed I trained another network, a 3 layer RNN with 200 nodes per layer, which was as much as I could go without getting the out-of-memory error.

Here are my observations:

  1. Small network size on my machine (due to the out-of memory errors) might make the following points invalid, so please take those with a grain of salt.
  2. Polish due to its inflection and longer words seems to be much harder to machine generate than English. At high temperatures most words are hallucinated.
  3. At low temperatures the generated text consists exclusively of single-line dialogue while the training text was a mix of dialogue and longer paragraphs.
  4. Some of the structure was preserved, for example the dialogue that in Polish is introduced by m-dash. But other quotations, introduced by a low double quotation mark and completed by a high double quotation mark were never matched in any example text I generated.
  5. To my eye the generated text still had that Sienkiewicz look, but mostly Sienkiewicz on drugs
  6. Both 2 and 3 layer networks gave results that were for me very similar

I’m concluding this post with the samples of the generated texts, although these will be of interest only for the Polish readers.

Example 1, temperature 0.3, 2-layer RNN:

— Mówić?

— Nie mówić?

— Nie chciał waćpanna — rzekł Wołodyjowski.

— Nie mówię?

— A to jest?

— Jako po wielkim było!

— A gdyby chciał się wyprawić?

— Nie mógł — rzekł Zagłoba.

— Jako mu będę się w czym zaczął. Tu też to mu chwycić!

— Nie chciał się za rękę do naszego brać.

— Jako nie mógł?

— Począł się na koniec.

— Jeśli tak przeciw to jest?

— On wystawił?

— Przeciw to mówić?

Example 2, temperature 0.3, 3-layer RNN:

— Mieszkał się na króla.

— Począł się w tym nie mógł go w dobrze, bo się na chcąc przeciw wystanie, bo z króla w koniec, ale jak nie mógł na tego nad nim było mu się za przyjechali. Też było widział się przed królewskim.

— Nie mówił — rzekł Zagłoba.

— Nie było pod król.

— Mości się na stronę do nimi rękę, ale na której podniósł potęgę, bo tak chciałem na wrócili tylko jeszcze do dobrze w nimi wybranie rękę ku niego tego będzie się na tropach.

— To mówił?

— Nie chcą! — rzekł Zagłoba.

— Nad naszego było jako waćpanna.

— To mówił?

— Jako na królewskim nie była do niego straszniejszym, ale chcąc jest? — rzekł Zagłoba.

Example 3, temperature 0.5, 2-layer RNN

— Po podobnym ujrzał coraz czasem oczy z przyjacielem, wybliżało się w drugiego powiedzieć, choćby też mi Sieczy przyjdzie go w nimi na chwilę, ale gdyby mu by nie wystrzelać. Przeciw się na brzeg, przecie miał szeroko — rzekł konia — mówiła przez chwilę jest. On się tu dobrze, bo więc jest teraz strzelał, że ona widział.

— A szlachta? — rzekł Zagłoba.

— To mówić.

Okrutnie wyradując się z nimi znów za regimentarzu.

— To jest?

— To tam tego do głowę zamknął — mówił — i w pokrywało się na koniec razy i przepominać. Jest Jan Andrzej jak bitwa, postanął w czasu pochodził. Na muszę mu?

Example 4, temperature 0.5, 3-layer RNN

— Bóg chce, niechże w królewskiego znajdzie było się na grzmienie, mieszkało się nie mógł się król pod strzymanie nie mogli. Młody największej wielką przez takim chciała.

— A na czym było wydało się w najwięcej, co jak mu czekać.

Pan Zagłoba bronił chwycił się przez razem przy chwilę, gdyby po książęcej chwilę nawet się było wszystkich przy uczynili. Nie mógł nad koniec, że wyglądał się chcących. Tu zdrowie wystać.

— Nie chcę się nad drugim nie wybaczył, jak chwila się za brany — na komendanie na zasłużby wszystkich bardziej w rzekł:

— Nie miał przeszczęście. Wielkiego widział jegomość, bo której najlepszego i bogusław.

— Rozprzyjaciele!

— Jeśli było to się dwóch, bo już w straszniejszych światłym za głowę, że się chce widzieli. Ale za obiec przybył pod żołnierzem.

— Największym jak do Chmielnickich — mówił Kmicic.

Example 5, temperature 0.8, 2-layer RNN

— To nagrodzili go czasu.

Lecz uczynił się drugie, więc tymczasem gardła na nich.

Trzeba Tuhaj-bejowicz. Tłumu ona nie zamiarał szwedzkich roczym, jak na tej odstawiały bardzo nie mogło na kompańskiego poczętych na księcia na Lepiej, ale mnie na wielki powierzył, jeśli niż uczekał się wymienać.

Zagłoba chciała konie na chorągwią staroszać. Ale co ku jeźdźców go wprawdą i zbliżał i po waszego namiestnikowskich stawały.

Niechże pierwszy bronić i nie nie majdzie nie poświecimów. Ale ze mną obsadzili. Boże witał król w takiego i jego chwilę komendanko jak w kolichu, rąków, to w śmiercię było też zwalując z takich burza. Obotem zbudziły się zawsze coraz nie wielkich zaraz chorągwie oddali z gotów i dla głosami poszli niezmiernym…

Example 6, temperature 1.0, 3-layer RNN

— Wyskadził? — brzymała następnie ów mili obiechałem, że jednak lasie było piersia, i przeznaczenie i wiesz; bo zapowiedział się rozwarszyć ciągnąć za nich szepny sama ziemi. Tak zabyt, gdzie które streszcie tego gałakiem. Kości Rasztwierz łzy nie z gach, słusznie, który duchen jej rozścigi mu i chwało chciał widziało ja tęgen. Przecie tam, młody? Przez własnym tylko do kniahina pewnym, i wśród na cztar radach wród, prawdzie pierwsze wydawać i z wszystkim kilkadzielników. Przyczewniejszy wraz bardzo skąd się nie po nasz się od pułkowniku, od żałach. Właśnie z wylertu kaniec. Może się.

Pan Krzysią i ultaniałło się do fortuni? — rzekł — wszyscy to mojej między blichem, by nie go nastał. Wołodyjowskiego ludzie — i przesłyszał, na Kosztura moim ksiądz lepszy zabył i jeno zdawała się wydumienia się i przyszła Żwędy obszache za ławych zamieść. O białym z tych za dniego mam!… Odryszkali, bo jeździ się nie nagle duszne, przyjdzie zdawił zostawią!”


Get every new post delivered to your Inbox.