Memorising Pi

From Nottinghack Wiki
Jump to: navigation, search


Laser Etching Stainless Steel
Primary Contact Michael Erskine
Created 08/02/2016


QR code


On January 5th 2016 I set myself a goal to memorise Pi to 100 decimal places. My motivation for this was to learn the [Major Mnemonic System] and it's also a personal challenge. Prior to mid December 2015 I knew the five digits that I had learned at school: 3.14159. This had been sufficient for rough calculations with a calculator (for mental arithmetic I use the approximation of 22/7) and for any accuracy and programming I would use constants provided by the language or software. I had previously looked at the Major system but was unable to apply it due to lack of practice. So in December whilst discussing pi with one of my daughter's friends, she was able to recite 10 decimal places (3.1415926535) which I found to be quite impressive. So in the following weeks I learned the next 5 digits I needed to get up to 10 but without using a particular established method; just repetition. A short while later, at the start of January, I had a discussion with a friend at the Hackspace about memory sports and I decided to learn Major system properly with a well-defined goal: to be able to recite pi up to the 100th decimal place. Once I applied myself to the task I quickly exceeded to 100 digits and by the end of January I was up to 300. I set a further goal to memorise 1000 digits in time for World Pi Day 2016.

My memorisation process is really just an exercise in creative writing! I like to work in groups of 10 digits at a time. Each group is remembered as a sentence of 3 to 5 words and each word encodes perhaps 2 to 5 digits. The sentences are not proper English though; it would be way too difficult to try and find the right words in the Major system. The sentences are just keywords and carry a general meaning as part of my overall story. The theme of my story is a large battle set on a Shell World from a science fiction novel by Iain M Banks. In order to be memorable, the story contains peril, includes all the senses, and has remarkable and bizarre happenings featuring familiar people and objects from my everyday life.

--Michael Erskine (talk) 11:56, 8 February 2016 (UTC)

Major System Software

I found that although there were many online and offline software tools for the Major system, none of them fit nicely with my requirements. I started to work on my own software to find handy mnemonics from the digit sequences. Because the major system is based on pronunciation and not spelling, I started looking into ways of encoding sounds with systems like the International Phonetic Alphabet (IPA). Again, there are many online resources for phonetics but I wanted to write my own software (as usual!). I had previously worked with speech recognition for the Cheesioid project and the CMU Sphinx software; what I was looking for was a phonetic database of British English words that could be programmatically mapped to number sequences. The CMU Sphinx libraries make use of such a dictionary, known as the CMU Pronouncing Dictionary, which uses the ARPABET ASCII notation for the encoding of phonemes.

Here are the valid ARPABET phonemes with examples: -

    //  Phoneme Example Translation
    //  ------- ------- -----------
    //  AA      odd     AA D
    //  AE      at      AE T
    //  AH      hut     HH AH T
    //  AO      ought   AO T
    //  AW      cow     K AW
    //  AY      hide    HH AY D
    //  B       be      B IY
    //  CH      cheese  CH IY Z
    //  D       dee     D IY
    //  DH      thee    DH IY
    //  EH      Ed      EH D
    //  ER      hurt    HH ER T
    //  EY      ate     EY T
    //  F       fee     F IY
    //  G       green   G R IY N
    //  HH      he      HH IY
    //  IH      it      IH T
    //  IY      eat     IY T
    //  JH      gee     JH IY
    //  K       key     K IY
    //  L       lee     L IY
    //  M       me      M IY
    //  N       knee    N IY
    //  NG      ping    P IH NG
    //  OW      oat     OW T
    //  OY      toy     T OY
    //  P       pee     P IY
    //  R       read    R IY D
    //  S       sea     S IY
    //  SH      she     SH IY
    //  T       tea     T IY
    //  TH      theta   TH EY T AH
    //  UH      hood    HH UH D
    //  UW      two     T UW
    //  V       vee     V IY
    //  W       we      W IY
    //  Y       yield   Y IY L D
    //  Z       zee     Z IY
    //  ZH      seizure S IY ZH ER

APRABET phonemes are modified into "symbols" by adding optional digits to indicate emphasis within the word. The full set of possible ARPABET symbols used by CMU Sphinx dictionaries is provided in the CMU Sphinx downloads as file "cmudict-0.7b.symbols". This is a reasonably small set (84 symbols) and I was quickly able to create the following simple mapping of all possible APRABET symbols to the equivalent major digits by ignoring emphasis, vowels and other elements silent to Major system.

symbol major notes
AA vowels not encoded
AA0
AA1
AA2
AE
AE0
AE1
AE2
AH
AH0
AH1
AH2
AO
AO0
AO1
AO2
AW
AW0
AW1
AW2
AY
AY0
AY1
AY2
B 9
CH 6
D 1
DH 1
EH
EH0
EH1
EH2
ER 4
ER0 4
ER1 4
ER2 4
EY
EY0
EY1
EY2
F 8
G 7
HH
IH
IH0
IH1
IH2
IY
IY0
IY1
IY2
JH 6
K 7
L 5
M 3
N 2
NG 2
OW
OW0
OW1
OW2
OY
OY0
OY1
OY2
P 9
R 4
S 0
SH 6
T 1
TH 1
UH
UH0
UH1
UH2
UW
UW0
UW1
UW2
V 8
W
Y
Z 0
ZH 6

Armed with this table I was able to encode the entire CMU English dictionary (cmudict-0.7b) of some 133000 words to Major system equivalents. I don't know if this has ever been done before but I am happy to share my work! --Michael Erskine (talk) 12:16, 8 February 2016 (UTC)

Software to process the CMU Dictionary

I have written some software that reads the CMU Sphinx dictionary in ARPABET format and produces a database table that attaches Major encodings to each word.

Cmudict-0.7b-major-system.png
CREATE TABLE `cmudict_07b_words` (
	`counter` INT(11) NULL DEFAULT NULL,
	`line` INT(11) NULL DEFAULT NULL,
	`word` VARCHAR(50) NULL DEFAULT NULL,
	`maj` VARCHAR(50) NULL DEFAULT NULL,
	`len` INT(11) NULL DEFAULT NULL,
	`phones` VARCHAR(100) NULL DEFAULT NULL
)
COLLATE='utf8_general_ci'
ENGINE=InnoDB;

A database is nice but it is much, much faster to slurp the whole CMU dictionary from the original text file and process it in memory. Here's an example of C# code to read the CMU dictionary into a list of objects (one-per-word) that are the same as the database above, with major translations. This is part of a class named "CmuDict". By the way: the software is in C# but not because the language is particularly suited to the task: it's just what I happen to be using right now. Any expressive language with closures/lambdas/functors will do the job (Scheme, Java, Perl, ML, Haskell, etc.)

 1         public event EventHandler<Dentry> OnEntry;
 2 
 3         /// <summary>
 4         /// Read and iterate all CMU dictionary entries and raise event for each entry
 5         /// </summary>
 6         public void MajorEntriesIterateAll()
 7         {
 8             try {
 9                 using (StringReader r = new StringReader(gpsd_tests.Properties.Resources.cmudict_0_7b)) {
10                     int line = 0;
11                     int count = 0;
12                     int problem_entry = -1;
13                     string s;
14                     StringBuilder sb = new StringBuilder();
15                     while ((s = r.ReadLine()) != null) {
16                         line++;
17                         if (s.StartsWith(";;;"))
18                             continue;
19                         var ix = s.IndexOf("  ");
20                         if (ix < 0) {
21                             problem_entry = line;
22                             continue;
23                         }
24                         if (!Char.IsLetter(s[0]))
25                             continue;
26 
27                         var word = s.Substring(0, ix);
28                         var phones = s.Substring(ix + 2);
29                         var sa = phones.Split(' ');
30                         sb.Clear();
31                         foreach (var p in sa) {
32                             string m = majmap[p];
33                             sb.Append(m);
34                         }
35                         var maj = sb.ToString();
36                         int ml = maj.Length;
37                         count++;
38                         if(OnEntry != null) {
39                             OnEntry(this, new Dentry(count, line, word, maj, ml, phones));
40                         }
41                     }
42                     log.Info("entries in CMU dictionary: " + line);
43                     if (problem_entry != -1) {
44                         log.Info("problem entry at " + problem_entry);
45                     }
46                 }
47             } catch (Exception x) {
48                 log.Error("failed: " + x.Message);
49             }
50         }

The simple "Dentry" class and "majmap" hashtables are as follows...

 1         public class Dentry
 2         {
 3             public int count;
 4             public int line;
 5             public string maj;
 6             public int ml;
 7             public string phones;
 8             public string word;
 9 
10             public Dentry(int count, int line, string word, string maj, int ml, string phones)
11             {
12                 this.count = count;
13                 this.line = line;
14                 this.word = word;
15                 this.maj = maj;
16                 this.ml = ml;
17                 this.phones = phones;
18             }
19         }
20 
21 
22         public static Dictionary<string, string> majmap = new Dictionary<string, string>() {
23             { "AA", ""}, {"AA0", ""}, {"AA1", ""}, {"AA2", ""}, {"AE", ""}, {"AE0", ""},
24             { "AE1",""}, {"AE2", ""}, {"AH", ""}, {"AH0", ""}, {"AH1", ""}, {"AH2", ""},
25             { "AO", ""}, {"AO0", ""}, {"AO1", ""}, {"AO2", ""}, {"AW", ""}, {"AW0", ""},
26             { "AW1", ""}, {"AW2",""}, {"AY", ""}, {"AY0", ""}, {"AY1", ""}, {"AY2", ""},
27             { "B", "9"}, {"CH", "6"}, {"D", "1"}, {"DH", "1"}, {"EH", ""}, {"EH0", ""},
28             { "EH1", ""}, {"EH2", ""}, {"ER", "4"}, {"ER0", "4"}, {"ER1", "4"},
29             { "ER2", "4"}, {"EY", ""}, {"EY0", ""}, {"EY1", ""}, {"EY2", ""},
30             { "F", "8"}, {"G", "7"}, {"HH", ""}, {"IH", ""}, {"IH0", ""}, {"IH1", ""},
31             {"IH2",""}, {"IY", ""}, {"IY0", ""}, {"IY1", ""}, {"IY2", ""}, {"JH", "6"}, {"K", "7"},
32             {"L","5"}, {"M", "3"}, {"N", "2"}, {"NG", "2"}, {"OW", ""}, {"OW0", ""}, {"OW1", ""},
33             {"OW2", ""}, {"OY", ""}, {"OY0", ""}, {"OY1", ""}, {"OY2", ""}, {"P", "9"}, {"R","4"},
34             {"S", "0"}, {"SH", "6"}, {"T", "1"}, {"TH", "1"}, {"UH", ""}, {"UH0", ""}, {"UH1",""},
35             {"UH2", ""}, {"UW", ""}, {"UW0", ""}, {"UW1", ""}, {"UW2", ""}, {"V", "8"}, {"W",""},
36             {"Y", ""}, {"Z", "0"}, {"ZH", "6"},
37         };

An example usage of the iterator that just stores each word entry in a list is as follows...

1         public static List<CmuDict.Dentry> ReadAllCmuMajor()
2         {
3             CmuDict c = new CmuDict();
4             var d = new List<CmuDict.Dentry>();
5             c.OnEntry += (sender, x) => d.Add(x);
6             c.MajorEntriesIterateAll();
7             return d;
8         }

And here's some code to search for all words in the first 100 digits of pi...

 1             log.Info("Scanning for all words in first 100 digits of pi...");
 2             s1 = Stopwatch.StartNew();
 3             // Live scan of some pi digits while we have it in memory...
 4             var digits = PiDigits.GetDigitsString(0, 100);
 5             // split up into groups of 10 digits as I don't want to have words that span these boundaries - stick in some spacing...
 6             digits = Regex.Replace(digits, ".{10}", "$0 ");
 7             int minimumDigits = 2;
 8             foreach(var e in d) {
 9                 if (e.ml < minimumDigits) continue;
10                 var pos = digits.IndexOf(e.maj);
11                 if (pos < 0) continue;
12                 var t = digits.Substring(0, pos) + "[" + digits.Substring(pos, e.ml) + "]" + digits.Substring(pos + e.ml);
13                 // do something with data...
14                 log.Debug("MAJOR! " + e.word + " length " + e.ml + " = " + e.maj + " found in target at " + pos + "    " + t);
15             }
16             s1.Stop();
17             log.Info("Scanned for all words in first 100 digits of pi in " + s1.Elapsed.TotalMilliseconds + " ms");

This takes 79ms on my machine when processing the list in memory. When reading from the database it takes more than 12 hours! The example just writes the found words to a log file but they could be held in memory and sorted by position found, then consecutive words could be selected and spoken by TTS (eSpeak, FreeTTS, Festival, or what have you). A genericised scanner that takes a lambda expression as a callback is shown below, but this takes approximately 100 times longer to run! It does, however, allow the lambda expression to end the iteration if desired, e.g. if we have a "good enough" result...

 1         private static void DigitScanner(List<CmuDict.Dentry> d, string digits, int minimumDigits, Func<CmuDict.Dentry, int, string, bool> callback)
 2         {
 3             foreach (var e in d) {
 4                 if (e.ml < minimumDigits) continue;
 5                 var pos = digits.IndexOf(e.maj);
 6                 if (pos < 0) continue;
 7                 var map = digits.Substring(0, pos) + "[" + digits.Substring(pos, e.ml) + "]" + digits.Substring(pos + e.ml);
 8                 // do something with data...
 9                 if(callback != null) {
10                     if (!callback.Invoke(e, pos, map))
11                         return;
12                 }
13             }
14         }
15 
16 
17 // ...and here is how it is called...
18 
19             Func< CmuDict.Dentry, int, string, bool > callback = (CmuDict.Dentry e, int pos, string map) => {
20                 log.Debug("MAJOR! " + e.word + " length " + e.ml + " = " + e.maj + " found in target at " + pos + "    " + map);
21                 return true;
22             };
23             DigitScanner(d, digits, minimumDigits, callback);

The output in the log file is fascinating...

 1 2016-02-09 11:56:43.2722 DEBUG MAJOR! DROID length 3 = 141 found in target at 0    [141]5926535 8979323846 2643383279 5028841971 6939937510 5820974944 5923078164 0628620899 8628034825 3421170679 
 2 2016-02-09 11:56:43.2722 DEBUG MAJOR! DROUGHT length 3 = 141 found in target at 0    [141]5926535 8979323846 2643383279 5028841971 6939937510 5820974944 5923078164 0628620899 8628034825 3421170679 
 3 2016-02-09 11:56:43.2722 DEBUG MAJOR! DRUID length 3 = 141 found in target at 0    [141]5926535 8979323846 2643383279 5028841971 6939937510 5820974944 5923078164 0628620899 8628034825 3421170679 
 4 2016-02-09 11:56:43.2722 DEBUG MAJOR! DUARTE length 3 = 141 found in target at 0    [141]5926535 8979323846 2643383279 5028841971 6939937510 5820974944 5923078164 0628620899 8628034825 3421170679 
 5 2016-02-09 11:56:43.2722 DEBUG MAJOR! DUBACH length 3 = 197 found in target at 39    1415926535 8979323846 2643383279 502884[197]1 6939937510 5820974944 5923078164 0628620899 8628034825 3421170679 
 6 2016-02-09 11:56:43.2722 DEBUG MAJOR! DUBUC length 3 = 197 found in target at 39    1415926535 8979323846 2643383279 502884[197]1 6939937510 5820974944 5923078164 0628620899 8628034825 3421170679 
 7 2016-02-09 11:56:43.2722 DEBUG MAJOR! DUBUQUE length 3 = 197 found in target at 39    1415926535 8979323846 2643383279 502884[197]1 6939937510 5820974944 5923078164 0628620899 8628034825 3421170679 
 8 2016-02-09 11:56:43.2722 DEBUG MAJOR! DUCK'S length 3 = 170 found in target at 103    1415926535 8979323846 2643383279 5028841971 6939937510 5820974944 5923078164 0628620899 8628034825 3421[170]679 
 9 2016-02-09 11:56:43.2722 DEBUG MAJOR! DUCKIES length 3 = 170 found in target at 103    1415926535 8979323846 2643383279 5028841971 6939937510 5820974944 5923078164 0628620899 8628034825 3421[170]679 
10 2016-02-09 11:56:43.2722 DEBUG MAJOR! DUCKS length 3 = 170 found in target at 103    1415926535 8979323846 2643383279 5028841971 6939937510 5820974944 5923078164 0628620899 8628034825 3421[170]679 
11 2016-02-09 11:56:43.2722 DEBUG MAJOR! DUCKS' length 3 = 170 found in target at 103    1415926535 8979323846 2643383279 5028841971 6939937510 5820974944 5923078164 0628620899 8628034825 3421[170]679 
12 2016-02-09 11:56:43.2722 DEBUG MAJOR! DUDACK length 3 = 117 found in target at 102    1415926535 8979323846 2643383279 5028841971 6939937510 5820974944 5923078164 0628620899 8628034825 342[117]0679 
13 2016-02-09 11:56:43.2722 DEBUG MAJOR! DUDECK length 3 = 117 found in target at 102    1415926535 8979323846 2643383279 5028841971 6939937510 5820974944 5923078164 0628620899 8628034825 342[117]0679 
14 2016-02-09 11:56:43.2722 DEBUG MAJOR! DUDECK'S length 4 = 1170 found in target at 102    1415926535 8979323846 2643383279 5028841971 6939937510 5820974944 5923078164 0628620899 8628034825 342[1170]679 
15 2016-02-09 11:56:43.2722 DEBUG MAJOR! DUDEK length 3 = 117 found in target at 102    1415926535 8979323846 2643383279 5028841971 6939937510 5820974944 5923078164 0628620899 8628034825 342[117]0679

Here's a handy bit of code that collects the hits in a List of Tuples, sorts it, and spits out a CSV file for perusal...

 1             log.Info("Scanning again with a collecting callback...");
 2             s1 = Stopwatch.StartNew();
 3             minimumDigits = 2;
 4             List<Tuple<int, CmuDict.Dentry, string>> funStuff = new List<Tuple<int, CmuDict.Dentry, string>>();
 5             callback = (CmuDict.Dentry e, int pos, string map) => {
 6                 funStuff.Add(new Tuple<int, CmuDict.Dentry, string>(pos, e, map));
 7                 return true;
 8             };
 9             DigitScanner(d, digits, minimumDigits, callback);
10             s1.Stop();
11             log.Info("collecting callback found "+funStuff.Count+ "words in " + s1.Elapsed.TotalMilliseconds + " ms");
12 
13             // sort the collection by word position...
14             log.Info("Sorting collected hits by hit position...");
15             s1 = Stopwatch.StartNew();
16 
17             funStuff.Sort((x, y) => x.Item1.CompareTo(y.Item1));
18             s1.Stop();
19             log.Info("sort took " + s1.Elapsed.TotalMilliseconds + " ms");
20             // Write to file...
21             var csvFile = "sorted_major_pi100.csv";
22             log.Info("Writing results to file: " + csvFile);
23             try {
24                 var sw = new StreamWriter(csvFile);
25                 sw.WriteLine("{0},{1},{2},{3},{4},{5}", "position", "word", "maj", "len", "phones", "map");
26                 foreach (var a in funStuff) {
27                     var e = a.Item2;
28                     sw.WriteLine("{0},{1},{2},{3},{4},{5}", a.Item1, e.word, e.maj, e.ml, e.phones, a.Item3);
29                 }
30                 sw.Close();
31             } catch (Exception x) {
32                 log.Error("failed: " + x.Message);
33             }
34             log.Info("Finished writing results to file: " + csvFile);

Entries can then be selected to make a fun and memorable sentence...

 1 0	DROID	141	3	D R OY1 D	[141]5926535 8979323846 2643383279 5028841971 6939937510 5820974944 5923078164 0628620899 8628034825 3421170679 
 2 3	LEAPING	592	3	L IY1 P IH0 NG	141[592]6535 8979323846 2643383279 5028841971 6939937510 5820974944 5923078164 0628620899 8628034825 3421170679 
 3 6	SHYLY	65	2	SH AY1 L IY0	141592[65]35 8979323846 2643383279 5028841971 6939937510 5820974944 5923078164 0628620899 8628034825 3421170679 
 4 8	HAMMILL	35	2	HH AE1 M AH0 L	14159265[35] 8979323846 2643383279 5028841971 6939937510 5820974944 5923078164 0628620899 8628034825 3421170679 
 5 
 6 11	PHOBIC	897	3	F OW1 B IH0 K	1415926535 [897]9323846 2643383279 5028841971 6939937510 5820974944 5923078164 0628620899 8628034825 3421170679 
 7 14	BOMBING	932	3	B AA1 M IH0 NG	1415926535 897[932]3846 2643383279 5028841971 6939937510 5820974944 5923078164 0628620899 8628034825 3421170679 
 8 17	MAFIA	38	2	M AA1 F IY0 AH0	1415926535 897932[38]46 2643383279 5028841971 6939937510 5820974944 5923078164 0628620899 8628034825 3421170679 
 9 19	ARCHWAY	46	2	AA1 R CH W EY2	1415926535 89793238[46] 2643383279 5028841971 6939937510 5820974944 5923078164 0628620899 8628034825 3421170679 
10 
11 22	ONSHORE	264	3	AA1 N SH AO2 R	1415926535 8979323846 [264]3383279 5028841971 6939937510 5820974944 5923078164 0628620899 8628034825 3421170679 
12 25	MAIM	33	2	M EY1 M	1415926535 8979323846 264[33]83279 5028841971 6939937510 5820974944 5923078164 0628620899 8628034825 3421170679 
13 27	FOAMY	83	2	F OW1 M IY0	1415926535 8979323846 26433[83]279 5028841971 6939937510 5820974944 5923078164 0628620899 8628034825 3421170679 
14 29	KNEECAP	279	3	N IY1 K AE2 P	1415926535 8979323846 2643383[279] 5028841971 6939937510 5820974944 5923078164 0628620899 8628034825 3421170679