Generating pseudowords with correct statistics to be words​
​by Luca Jones

Introduction

My project uses a Recurrent Neural Network (RNN) to train a program to generate “pseudowords” - phonetically correct, English-like words, based on the first syllable of an input word. Training data was generated by splitting random English words apart at random places, showing all the letters before the split and the letter after (e.g: mil -> k), and the RNN was trained with approximately 10000 pieces of training data. The changeable parameters in my function are the input word, and the length, in characters, of the output word, and the output is simply a pseudoword. I worked on this project as a part of the Wolfram High School Summer Camp 2020 (WSC2020) , where I worked closely with several mentors and Stephen Wolfram himself. My interest in pseudowords and generating them using RNNs was inspired by several resources I found online, such as the Wuggy algorithm, an application built to generate pseudowords based on input words, and a project written in TenseFlow to generate Shakespearean text. I chose to use an RNN for this project because I have long held a passion for programming AI, and this provided to be great practice and a good achievement.

Generating Training Data

My first step was to generate random English words, convert them to all lowercase, and make sure that they did not contain characters such as apostrophes and dashes using LetterQ. I did this because such characters are irregular occurrences in English, and could thus throw the network off if used to train it. Although 10000 words were used to train the RNN, only 10 random words have been generated in the example below. A set of test words were also generated for a validation set (not shown below).
Generating Random English Words:
In[]:=
trainingWords=Select[ToLowerCase/@RandomWord[10],LetterQ]
Out[]=
{comparably,keyboardist,fertilizer,hummingbird,assizes,cavort,poulterer,pharmacologist,irrupt,handkerchief}
Next, I mapped two lists together, one being the list of training words, and the other being a list of random integers between 1 and the length of the corresponding training word. I then put these two lists into an association to use to split the training words at random places. I chose to split the training words in random places because there are sometimes patterns in English that occur at certain places in words (e.g: the last three letters 'ing' in verbs), which could result in the RNN's results generating more real words than pseudowords. Once again, the same thing was done for the validation set.
Mapping Split Positions to Words:
In[]:=
randomTrainingParts=RandomInteger[{1,#-1}]&/@Flatten[StringLength[StringSplit[trainingWords]]]​​randomAssoc=AssociationThread[trainingWordsrandomTrainingParts]
Out[]=
{3,4,8,4,5,3,3,9,4,10}
Out[]=
comparably3,keyboardist4,fertilizer8,hummingbird4,assizes5,cavort3,poulterer3,pharmacologist9,irrupt4,handkerchief10
Finally, to finish formatting the training data, I printed all the characters of each training word up to the splitting point, and then the next character.
Generating Training Data:
In[]:=
trainingData=StringTake[#,StringLength[#]-randomAssoc[#]]StringPart[#,-randomAssoc[#]]&/@trainingWords
Out[]=
{comparab,keyboard,fer,hummingb,ass,cavo,poulter,pharma,irr,han}

Creating and Training the RNN

My RNN used trainingData as a training set and testData as a validation set. Although building the RNNwith NetChain seems simple, tweaking it proved to be a long and tedious task. At the beginning, during my first trial tests, my validation set's graph kept diverging from the path of that of the training set and overfitting. However, introducing a Dropout Layer lessened this and made the net far more accurate - the path of the validation set stayed more or less consistent with that of the training set. Eventually, I found the optimal settings to have the Dropout probability at 0.425 and to train the network for 25 epochs.
An example of an overfitting RNN:
In[]:=
NetTrain Results
"summary"
"batches:"100,"rounds:"25,"time:""42s","examples/s:""78."
"data"
"training examples:"99,"validation examples:"99,"processed examples:"3200,"skipped examples:"0
"method"
"ADAM""optimizer","batch size"32,"CPU"
"round"
"loss:"
"3.56"×
"-1"
10
,
"error:"
"13.3%"
"validation"
"loss:"
"6.63",
"error:"
"92.93%"
‹
"loss"
›
​
"rounds"
"loss"
"training set"
"validation set"
I generated only 100 training data and test data points, which is fewer than the 10000 that I recommend using for training the net:
trainingData=
Actual Training Data
;​​testData=
Actual Test Data
;
Building the RNN:
In[]:=
NetTrain Results
"summary"
"batches:"7775,"rounds:"25,"time:""39min","examples/s:"108
"data"
"training examples:"9946,"validation examples:"9959,"processed examples:"248800,"skipped examples:"0
"method"
"ADAM""optimizer","batch size"32,"CPU"
"round"
"loss:"
"1.97",
"error:"
"63.7%"
"validation"
"loss:"
"2.46",
"error:"
"73.1%"
‹
"loss"
›
​
"rounds"
"loss"
"training set"
"validation set"

Building the GeneratePseudowords Function

After training the RNN, all that was left was to tie everything up into a user-friendly, customizable function that took the input word and the length of the pseudoword to be generated. I did this by building a recursive function where charAdd characters were added to the first syllable of the input word, which was extracted using WordData. These characters were generated by using the RNN's results (trainedNet) to get the next character and appending it to the previously generated string. The With function around my Module verifies that the pseudoword generated is indeed a pseudoword, and not a real dictionary word. If the word turns out to be a real dictionary word, the function returns nothing.
Code for GeneratePseudowords:
GeneratePseudoword[inputWord_,wordLength_,firstTime_:True]:=​​ With[​​ word=ToString[Module[{firstSyllable,appendChar,charAdd},​​ If[wordLength0,inputWord,​​ If[firstTime,​​ firstSyllable=WordData[inputWord,"Hyphenation"][[1]];​​ appendChar=StringJoin[firstSyllable,trainedNet[firstSyllable]];charAdd=wordLength-StringLength[firstSyllable],​​ appendChar=StringJoin[inputWord,trainedNet[inputWord]];​​ charAdd=wordLength;​​ ];​​ GeneratePseudoword[appendChar,charAdd-1,False]​​ ]]],If[DictionaryWordQ[word]False,word]​​ ]
Generating pseudowords was the satisfying and fun end to this project. Below are some of the most fun pseudowords I generated:
In[]:=
GeneratePseudoword["ghost",8]ghostery​​GeneratePseudoword["sea",9]sealingly​​GeneratePseudoword["sky",9]skyberess​​GeneratePseudoword["aurora",8]auterate​​GeneratePseudoword["spa",11]spartically​​GeneratePseudoword["football",11]footmenting​​GeneratePseudoword["computer",9]competize​​GeneratePseudoword["science",10]scillingly
For more variety, I recommend retraining the RNN with different random words as training data.