This example shows how to train a deep learning LSTM network to generate text word-by-word.
To train a deep learning network for word-by-word text generation, train a sequence-to-sequence LSTM network to predict the next word in a sequence of words. To train the network to predict the next word, specify the responses to be the input sequences shifted by one time step.
This example reads text from a website. It reads and parses the HTML code to extract the relevant text, then uses a custom mini-batch datastore
documentGenerationDatastore to input the documents to the network as mini-batches of sequence data. The datastore converts documents to sequences of numeric word indices. The deep learning network is an LSTM network that contains a word embedding layer.
A mini-batch datastore is an implementation of a datastore with support for reading data in batches. You can use a mini-batch datastore as a source of training, validation, test, and prediction data sets for deep learning applications. Use mini-batch datastores to read out-of-memory data or to perform specific preprocessing operations when reading batches of data.
You can adapt the custom mini-batch datastore
documentGenerationDatastore.m to your data by customizing the functions. For an example showing how to create your own custom mini-batch datastore, see Develop Custom Mini-Batch Datastore.
Load the training data. Read the HTML code from Alice's Adventures in Wonderland by Lewis Carroll from Project Gutenberg.
url = "https://www.gutenberg.org/files/11/11-h/11-h.htm"; code = webread(url);
The HTML code contains the relevant text inside
<p> (paragraph) elements. Extract the relevant text by parsing the HTML code using
htmlTree and then finding all the elements with element name
tree = htmlTree(code); selector = "p"; subtrees = findElement(tree,selector);
Extract the text data from the HTML subtrees using
extractHTMLText and view the first 10 paragraphs.
textData = extractHTMLText(subtrees); textData(1:10)
ans = 10×1 string array "" "" "" "" "" "" "Alice was beginning to get very tired of sitting by her sister on the bank, and of having nothing to do: once or twice she had peeped into the book her sister was reading, but it had no pictures or conversations in it, ‘and what is the use of a book,’ thought Alice ‘without pictures or conversations?’ " "So she was considering in her own mind (as well as she could, for the hot day made her feel very sleepy and stupid), whether the pleasure of making a daisy-chain would be worth the trouble of getting up and picking the daisies, when suddenly a White Rabbit with pink eyes ran close by her. " "There was nothing so very remarkable in that; nor did Alice think it so very much out of the way to hear the Rabbit say to itself, ‘Oh dear! Oh dear! I shall be late!’ (when she thought it over afterwards, it occurred to her that she ought to have wondered at this, but at the time it all seemed quite natural); but when the Rabbit actually took a watch out of its waistcoat-pocket, and looked at it, and then hurried on, Alice started to her feet, for it flashed across her mind that she had never before seen a rabbit with either a waistcoat-pocket, or a watch to take out of it, and burning with curiosity, she ran across the field after it, and fortunately was just in time to see it pop down a large rabbit-hole under the hedge. " "In another moment down went Alice after it, never once considering how in the world she was to get out again. "
Remove the empty paragraphs and view the first 10 remaining paragraphs.
textData(textData == "") = ; textData(1:10)
ans = 10×1 string array "Alice was beginning to get very tired of sitting by her sister on the bank, and of having nothing to do: once or twice she had peeped into the book her sister was reading, but it had no pictures or conversations in it, ‘and what is the use of a book,’ thought Alice ‘without pictures or conversations?’ " "So she was considering in her own mind (as well as she could, for the hot day made her feel very sleepy and stupid), whether the pleasure of making a daisy-chain would be worth the trouble of getting up and picking the daisies, when suddenly a White Rabbit with pink eyes ran close by her. " "There was nothing so very remarkable in that; nor did Alice think it so very much out of the way to hear the Rabbit say to itself, ‘Oh dear! Oh dear! I shall be late!’ (when she thought it over afterwards, it occurred to her that she ought to have wondered at this, but at the time it all seemed quite natural); but when the Rabbit actually took a watch out of its waistcoat-pocket, and looked at it, and then hurried on, Alice started to her feet, for it flashed across her mind that she had never before seen a rabbit with either a waistcoat-pocket, or a watch to take out of it, and burning with curiosity, she ran across the field after it, and fortunately was just in time to see it pop down a large rabbit-hole under the hedge. " "In another moment down went Alice after it, never once considering how in the world she was to get out again. " "The rabbit-hole went straight on like a tunnel for some way, and then dipped suddenly down, so suddenly that Alice had not a moment to think about stopping herself before she found herself falling down a very deep well. " "Either the well was very deep, or she fell very slowly, for she had plenty of time as she went down to look about her and to wonder what was going to happen next. First, she tried to look down and make out what she was coming to, but it was too dark to see anything; then she looked at the sides of the well, and noticed that they were filled with cupboards and book-shelves; here and there she saw maps and pictures hung upon pegs. She took down a jar from one of the shelves as she passed; it was labelled ‘ORANGE MARMALADE’, but to her great disappointment it was empty: she did not like to drop the jar for fear of killing somebody, so managed to put it into one of the cupboards as she fell past it. " "‘Well!’ thought Alice to herself, ‘after such a fall as this, I shall think nothing of tumbling down stairs! How brave they’ll all think me at home! Why, I wouldn’t say anything about it, even if I fell off the top of the house!’ (Which was very likely true.) " "Down, down, down. Would the fall never come to an end! ‘I wonder how many miles I’ve fallen by this time?’ she said aloud. ‘I must be getting somewhere near the centre of the earth. Let me see: that would be four thousand miles down, I think-’ (for, you see, Alice had learnt several things of this sort in her lessons in the schoolroom, and though this was not a very good opportunity for showing off her knowledge, as there was no one to listen to her, still it was good practice to say it over) ‘-yes, that’s about the right distance-but then I wonder what Latitude or Longitude I’ve got to?’ (Alice had no idea what Latitude was, or Longitude either, but thought they were nice grand words to say.) " "Presently she began again. ‘I wonder if I shall fall right through the earth! How funny it’ll seem to come out among the people that walk with their heads downward! The Antipathies, I think-’ (she was rather glad there was no one listening, this time, as it didn’t sound at all the right word) ‘-but I shall have to ask them what the name of the country is, you know. Please, Ma’am, is this New Zealand or Australia?’ (and she tried to curtsey as she spoke-fancy curtseying as you’re falling through the air! Do you think you could manage it?) ‘And what an ignorant little girl she’ll think me for asking! No, it’ll never do to ask: perhaps I shall see it written up somewhere.’ " "Down, down, down. There was nothing else to do, so Alice soon began talking again. ‘Dinah’ll miss me very much to-night, I should think!’ (Dinah was the cat.) ‘I hope they’ll remember her saucer of milk at tea-time. Dinah my dear! I wish you were down here with me! There are no mice in the air, I’m afraid, but you might catch a bat, and that’s very like a mouse, you know. But do cats eat bats, I wonder?’ And here Alice began to get rather sleepy, and went on saying to herself, in a dreamy sort of way, ‘Do cats eat bats? Do cats eat bats?’ and sometimes, ‘Do bats eat cats?’ for, you see, as she couldn’t answer either question, it didn’t much matter which way she put it. She felt that she was dozing off, and had just begun to dream that she was walking hand in hand with Dinah, and saying to her very earnestly, ‘Now, Dinah, tell me the truth: did you ever eat a bat?’ when suddenly, thump! thump! down she came upon a heap of sticks and dry leaves, and the fall was over. "
Visualize the text data in a word cloud.
figure wordcloud(textData); title("Alice's Adventures in Wonderland")
Create a datastore that contains the data for training using
documentGenerationDatastore. To create the datastore, first save the custom mini-batch datastore
documentGenerationDatastore.m to the path. For the predictors, this datastore converts the documents into sequences of word indices using a word encoding. The first word index for each document corresponds to a "start of text" token. The "start of text" token is given by the string
"startOfText". For the responses, the datastore returns categorical sequences of the words shifted by one.
Tokenize the text data using
documents = tokenizedDocument(textData);
Create a document generation datastore using the tokenized documents.
ds = documentGenerationDatastore(documents);
To reduce the amount of padding added to the sequences, sort the documents in the datastore by sequence length.
ds = sort(ds);
Define the LSTM network architecture. To input sequence data into the network, include a sequence input layer and set the input size to 1. Next, include a word embedding layer of dimension 100 and the same number of words as the word encoding. Next, include an LSTM layer and specify the hidden size to be 100. Finally, add a fully connected layer with the same size as the number of classes, a softmax layer, and a classification layer. The number of classes is the number of words in the vocabulary plus an extra class for the "end of text" class.
inputSize = 1; embeddingDimension = 100; numWords = numel(ds.Encoding.Vocabulary); numClasses = numWords + 1; layers = [ sequenceInputLayer(inputSize) wordEmbeddingLayer(embeddingDimension,numWords) lstmLayer(100) dropoutLayer(0.2) fullyConnectedLayer(numClasses) softmaxLayer classificationLayer];
Specify the training options. Specify the solver to be
'adam'. Train for 300 epochs with learn rate 0.01. Set the mini-batch size to 32. To keep the data sorted by sequence length, set the
'Shuffle' option to
'never'. To monitor the training progress, set the
'Plots' option to
'training-progress'. To suppress verbose output, set
options = trainingOptions('adam', ... 'MaxEpochs',300, ... 'InitialLearnRate',0.01, ... 'MiniBatchSize',32, ... 'Shuffle','never', ... 'Plots','training-progress', ... 'Verbose',false);
Train the network using
net = trainNetwork(ds,layers,options);
Generate the first word of the text by sampling a word from a probability distribution according to the first words of the text in the training data. Generate the remaining words by using the trained LSTM network to predict the next time step using the current sequence of generated text. Keep generating words one-by-one until the network predicts the "end of text" word.
To make the first prediction using the network, input the index that represents the "start of text" token. Find the index by using the
word2ind function with the word encoding used by the document datastore.
enc = ds.Encoding; wordIndex = word2ind(enc,"startOfText")
wordIndex = 1
For the remaining predictions, sample the next word according to the prediction scores of the network. The prediction scores represent the probability distribution of the next word. Sample the words from the vocabulary given by the class names of the output layer of the network.
vocabulary = string(net.Layers(end).Classes);
Make predictions word by word using
predictAndUpdateState. For each prediction, input the index of the previous word. Stop predicting when the network predicts the end of text word or when the generated text is 500 characters long. For large collections of data, long sequences, or large networks, predictions on the GPU are usually faster to compute than predictions on the CPU. Otherwise, predictions on the CPU are usually faster to compute. For single time step predictions, use the CPU. To use the CPU for prediction, set the
'ExecutionEnvironment' option of
generatedText = ""; maxLength = 500; while strlength(generatedText) < maxLength % Predict the next word scores. [net,wordScores] = predictAndUpdateState(net,wordIndex,'ExecutionEnvironment','cpu'); % Sample the next word. newWord = datasample(vocabulary,1,'Weights',wordScores); % Stop predicting at the end of text. if newWord == "EndOfText" break end % Add the word to the generated text. generatedText = generatedText + " " + newWord; % Find the word index for the next input. wordIndex = word2ind(enc,newWord); end
The generation process introduces whitespace characters between each prediction, which means that some punctuation characters appear with unnecessary spaces before and after. Reconstruct the generated text by removing the spaces before and after the appropriate punctuation characters.
Remove the spaces that appear before the specified punctuation characters.
punctuationCharacters = ["." "," "’" ")" ":" "?" "!"]; generatedText = replace(generatedText," " + punctuationCharacters,punctuationCharacters);
Remove the spaces that appear after the specified punctuation characters.
punctuationCharacters = ["(" "‘"]; generatedText = replace(generatedText,punctuationCharacters + " ",punctuationCharacters)
generatedText = " ‘Sure, it’s a good Turtle!’ said the Queen in a low, weak voice."
To generate multiple pieces of text, reset the network state between generations using
net = resetState(net);
doc2sequence (Text Analytics Toolbox) |
extractHTMLText (Text Analytics Toolbox) |
findElement (Text Analytics Toolbox) |
htmlTree (Text Analytics Toolbox) |
tokenizedDocument (Text Analytics Toolbox) |
wordcloud (Text Analytics Toolbox) |
wordEmbeddingLayer (Text Analytics Toolbox)