Message Boards Message Boards

[WSS19] Computer Analysis of Poetry — Part 2: Rhyme

Posted 5 years ago

This is the second part of an ongoing series. The first part can be found here . I have since improved the code from the first part with a neural network and use of the new SequencePredict function. At the end of this post is all the new code from Part 1 and Part 2. words and meter of Edgar Allan Poe's Poems often rhyme. A naive explanation of rhyme says that one word rhymes with another if the ending sounds are the same. Rhyme in context is more complicated than that, and the concept of rhyme can be extended to a larger class of what we might call sound echoes. Inside a poem, we rhyme one part of the stress pattern with another, a stressed syllable followed by zero to two unstressed syllables. With this definition, we can understand rhymes like "...pick it..." with "thicket" and "Lenore" with "nevermore". The term rhyme can include a larger family of sound features in a poem. Here, we will consider traditional rhyme (end rhyme), alliteration (front rhyme), assonance (vowel rhyme), and consonance (consonant rhyme). Poets are certainly aware of these echoic features, often plying them for effect. With the help of the Wolfram Language, we can represent these rhymes in an informative way.

Alliteration

To map the rhymes across an image of the stanza, I need four pieces of information: the phonetic representation of each word, each word broken into separate syllables, the phonetic representation of each syllable, and the stress of each syllable. With thoughtful application of Wolfram magic, alliteration looks like this: Poe's poem with alliteration mapped on it. Some alliterations ring louder than others, and we can analyze that too. If the alliteration is between two stressed syllables, then we hear the echo clearly. If not, then the echo is faint. This could be represented by line thickness or transparency, though I have not yet implemented this idea. You might also notice that the computer is not always interpreting the sound pairs correctly. For instance, it says that the second syllable of “weary” alliterates with the word “while,” which is certainly not true. The algorithm I am using to get the phonetics of the syllables has room for improvement.

Consonance

For consonance, which is the repetition of consonant sounds anywhere in two syllables, we change the code slightly. Consonance mapped on Poe's poem.

Assonance

And again for assonance, the echo of vowel sounds, the code is slightly different. Assonance mapped on Poe's poem.

End Rhyme

The echoes of traditional end rhyme, what we commonly think of as rhyme, are more difficult to map because they can span multiple lines, they involve groups of syllables, and they are restricted to certain parts of the metrical pattern. This takes some prep work. I gather a list of rhyme candidates and then test them for phonetically identical endings. The results are somewhat better than with the other types of rhyme, and with some additional work I think I can eliminate most of the miscues. Traditional rhyme mapped to Poe's poem.

Conclusion

The point isn’t so much that the computer catches every rhyme; perhaps in time it will. The point is that the computer can detect features in poetry and visualize them. Even with the mistakes this would be a good teaching tool. Questions like “Can ‘rapping’ rhyme with ‘rapping’?” and “Why does the computer think that ‘while I’ is a candidate for rhyming?” Are good discussion starters that lead to a deeper understanding of rhyme and the craft of poetry.

Adding machine learning to the analysis of meter in poetry and extending that to rhyme was my project at the Wolfram Summer School of 2019. Time was limited, so I didn’t polish it as much as I would have liked. I do plan a Part 3 for this series on the cadence or pauses built into written poetry. I hope to also update the code for this section at that time.

Code

Below is the code for both Part 1 and Part 2, broken into sections with some comments interspersed.

Gathers training data for the new neural network that takes a word and returns the word's phonetic form and syllabification.

SeedRandom[1234];
allWords = 
  RandomSample[
   Select[ToLowerCase[WordData[]], 
    StringMatchQ[LetterCharacter ..]]];
allWordsAssocs = 
  Quiet[DeleteMissing[<|"Input" -> #, 
       "FullTargetSequence" -> WordData[#, "PhoneticForm"], 
       "SyllabicTarget" -> WordData[#, "Hyphenation"]|> & /@ allWords,
     1, 2]];
netInputData = 
  RandomSample[Fold[#2[#1] &, DeleteMissing[allWordsAssocs, 1, 2], {
     MapAt[">" <> # <> "<" &, {All, "FullTargetSequence"}],
     MapAt[
      ToExpression[
        Characters[
         StringJoin[
          StringReplace[#, {_ ~~ EndOfString -> "2", _ -> 
             "1"}]]]] &, {All, "SyllabicTarget"}]}]];
{train3, test3} = 
  TakeDrop[netInputData, Floor[Length[netInputData]*0.85]] ;
Column[RandomSample[test3, 4]]

Creates the encoders for the net.

ipaChars = 
  Sort@DeleteDuplicates@Flatten@Characters[netInputData[[All, 2]]];
inputEncoder = NetEncoder[{"Characters"}];
targetEncoder = NetEncoder[{"Characters", ipaChars}]

Creates the training configuration of the neural net.

net = NetGraph[
    {
    "charEnc" -> NetChain[{
         EmbeddingLayer[32],
         NetBidirectionalOperator[LongShortTermMemoryLayer[64]]
         }],
    "phoneticDec" -> NetGraph[
          {
         SequenceLastLayer[],
         EmbeddingLayer[32],
         GatedRecurrentLayer[128],
         NetMapOperator[LinearLayer[]],
         SoftmaxLayer[]
         }
          ,
          { 
             NetPort["State"] -> 1 -> NetPort[3, "State"],
             NetPort["Input"] -> 
       2 -> 3 -> 4 -> 5 -> NetPort["phoneticOutput"]
             }
          ],
    "sylabicDec" -> NetGraph[
          {
         SequenceLastLayer[],
         GatedRecurrentLayer[128],
         NetMapOperator[LinearLayer[2]],
         SoftmaxLayer[]
         }
          ,
           { 
             NetPort["Input"] -> 1 -> NetPort[2, "State"],
             NetPort["Input"] -> 2 -> 3 -> 4 -> NetPort["sylabicOutput"]
             }
          ]
    }
    ,
    {
    "charEnc" -> NetPort["phoneticDec", "State"],
    "charEnc" -> "sylabicDec",
    NetPort["Target"] -> NetPort["phoneticDec", "Input"]
    }
    ,
    "Input" -> inputEncoder
    ]

Creates the loss mechanism for the net.

lossNet = NetGraph[
    <|
    "studentNet" -> net
    , "sylabicLoss" -> CrossEntropyLossLayer["Index"]
    , "phoneticLoss" -> CrossEntropyLossLayer["Index"]
    , "toPredict" -> SequenceRestLayer[]
    , "previousAnswer" -> SequenceMostLayer[]
    , "finalLoss" -> ThreadingLayer[Plus]
    |>
    ,
    {
    NetPort["FullTargetSequence"] -> 
    "toPredict" -> NetPort["phoneticLoss", "Target"],
    NetPort["FullTargetSequence"] -> 
    "previousAnswer" -> NetPort["studentNet", "Target"],
    NetPort["studentNet", "phoneticOutput"] -> 
    NetPort["phoneticLoss", "Input"],
    NetPort["studentNet", "sylabicOutput"] -> 
    NetPort["sylabicLoss", "Input"],
    NetPort["SylabicTarget"] -> NetPort["sylabicLoss", "Target"],
    {"phoneticLoss", "sylabicLoss"} -> "finalLoss" -> NetPort["Loss"]
    },
    "FullTargetSequence" -> targetEncoder
  ]

Trains the net. I stopped at round 19 because the validation loss didn't improve after that.

netTrainRes = 
 NetTrain[lossNet, <|"Input" -> train3[[All, 1]], 
   "FullTargetSequence" -> train3[[All, 2]], 
   "SylabicTarget" -> train3[[All, 3]]|>, All, 
  ValidationSet -> <|"Input" -> test3[[All, 1]], 
    "FullTargetSequence" -> test3[[All, 2]], 
    "SylabicTarget" -> test3[[All, 3]]|>, MaxTrainingRounds -> 50]

Training results for neural net Trims neural network to its production configuration.

trainedNet2 = netTrainRes["TrainedNet"];
phoTeachNet = NetDelete[trainedNet2, {"sylabicLoss", "finalLoss"}];
generationNet = 
  NetDelete[
   trainedNet2, {"phoneticLoss", "sylabicLoss", "finalLoss", 
    "toPredict"}];
wordEncoding = 
  NetChain[{NetExtract[generationNet, {"studentNet", 1}]}, 
   "Input" -> NetExtract[generationNet, "Input"]];
phoneticDec = 
  NetReplacePart[
   NetDelete[NetExtract[generationNet, {"studentNet", "phoneticDec"}],
     1], "Input" -> NetExtract[phoTeachNet, "FullTargetSequence"]];
phoneticDec = 
 NetReplacePart[
  NetRename[
   NetAppend[NetFlatten[phoneticDec], 
    SequenceLastLayer[]], {NetPort["Input"] -> 
     NetPort["PrevPhoneticChar"]}], 
  "Output" -> 
   NetDecoder[{"Class", targetEncoder[["Encoding"]]}]]; phoneticDec = 
 NetReplacePart[
  NetDelete[NetExtract[generationNet, {"studentNet", "phoneticDec"}], 
   1], "Input" -> NetExtract[phoTeachNet, "FullTargetSequence"]];
phoneticDec = 
 NetReplacePart[
  NetRename[
   NetAppend[NetFlatten[phoneticDec], 
    SequenceLastLayer[]], {NetPort["Input"] -> 
     NetPort["PrevPhoneticChar"]}], 
  "Output" -> 
   NetDecoder[{"Class", targetEncoder[["Encoding"]]}]]; phoneticDec = 
 NetReplacePart[
  NetDelete[NetExtract[generationNet, {"studentNet", "phoneticDec"}], 
   1], "Input" -> NetExtract[phoTeachNet, "FullTargetSequence"]];
phoneticDec = 
  NetReplacePart[
   NetRename[
    NetAppend[NetFlatten[phoneticDec], 
     SequenceLastLayer[]], {NetPort["Input"] -> 
      NetPort["PrevPhoneticChar"]}], 
   "Output" -> NetDecoder[{"Class", targetEncoder[["Encoding"]]}]];
syllabicDec = 
  NetExtract[generationNet, {"studentNet", "sylabicDec"}];
toStrChunks[x_Integer, y_] := {x, y};
toStrChunks[x_List, y_] := {Last[x] + 1, y};

Calls the neural net and displays some results.

getPhonsAndSylls[word_String] := Block[
   {previousAnswer = ">", phonetic = "", syllabic},
   Module[{wordenc = wordEncoding[word], stateNet},
    (* decode the syllables *)

    syllabic = 
     Flatten@Position[
       syllabicDec[wordenc], {pNonBreack_, pBreack_} /; 
        pBreack > pNonBreack];
    syllabic = 
     StringTake[word, Rest@FoldList[toStrChunks, 1, syllabic]];
    (* decode the phonetic *)

    stateNet = 
     NetStateObject[phoneticDec, <|{2, "State"} :> Last@wordenc|>];
    While[previousAnswer =!= "<",
     previousAnswer = stateNet[previousAnswer];
     phonetic = phonetic <> previousAnswer];
    <|"Phonetic" -> StringDrop[phonetic, -1], 
     "Syllables" -> syllabic|>]];
testSample = RandomSample[test3, 10][[All, 1]];
outputSample = 
  Flatten[{#, Values[getPhonsAndSylls[#]]}, 1] & /@ testSample;
sampleDataset = 
 Dataset[{<|"input" -> #[[1]], "phonetic" -> #[[2]], 
      "syllabic" -> #[[3]]|>} & /@ outputSample]

enter image description here

Gathers data for metric analysis.

ipaVowels = {"a?", "a?", "e?", "??", "o?", "?", "?", "?", "?", "?", 
  "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", 
  "?", "?", "a", "æ", "e", "i", "o", "œ", "ø", "u", 
  "y"}; oneSylStops = 
 Select[DeleteMissing[
   WordData[#, "PhoneticForm"] & /@ WordData[All, "Stopwords"]], 
  StringCount[#, ipaVowels] < 2 &];
getPhoneticForm[word_] := 
  Module[{phoneticForm}, 
   WordData[ToLowerCase[word], "PhoneticForm"] /. _Missing -> 
     getPhonsAndSylls[word]["Phonetic"]];
getMeter[phon_] := Module[{ipaSylCt},
   If[MemberQ[oneSylStops, phon], Return[{.5}]];
   ipaSylCt = StringCount[phon, ipaVowels];
   If[ipaSylCt == 1, {1},
    vowelSounds = StringCases[phon, "?" | "?" ... ~~ ipaVowels];
    # /. {a_ /; StringContainsQ[a, "?"] -> 1, 
        a_ /; StringContainsQ[a, "?"] -> .5, _ -> 0} & /@ 
     vowelSounds]];
getSyllables[word_] := Module[{syllables, phonCheck, part1, part2},
   phonCheck = StringCount[getPhoneticForm[word], ipaVowels];
   If[phonCheck <= 1, Return[{word}]];
   syllables = 
    WordData[ToLowerCase[word], "Hyphenation"] /. _Missing -> 
      getPhonsAndSylls[ToLowerCase[word]]["Syllables"];
   If[Length[syllables] == 1 && phonCheck == 2, 
    vowels = {"a", "e", "i", "o", "u"};
    part1 = 
     StringCases["over", 
       StartOfString ~~ Except[vowels] ... ~~ vowels ..][[1]];
    part2 = 
     StringDelete["over", 
      StartOfString ~~ Except[vowels] ... ~~ vowels ..];
    Return[{part1, part2}], Return[syllables]]];
getWordInfo[word_] := Module[
   {phon = getPhoneticForm[ToLowerCase[word]]},
   {word, getMeter[phon], getSyllables[word], phon}];
first10words = Take[TextWords[ToLowerCase[stanza]], 10];
first10info = getWordInfo[#] & /@ first10words;
first10assoc = <|"word" -> #[[1]], "meter" -> #[[2]], 
     "syllables" -> #[[3]], "phonetics" -> #[[4]]|> & /@ first10info //
   Dataset

Displays metric graphic.

iambic = Table[Flatten[Table[{0, 1}, n]], {n, 1, 12}];
trochaic = RotateLeft[#] & /@ iambic;
anapestic = Table[Flatten[Table[{0, 0, 1}, n]], {n, 1, 12}];
dactylic = RotateRight[#] & /@ anapestic;
allPatterns = Join[iambic, trochaic, anapestic, dactylic];
analyzeMeter[stanza_] := (
   lines = StringSplit[stanza, EndOfLine];
   words = TextWords[#] & /@ lines;
   wordInfo = Map[getWordInfo[#] &, words, {2}];
   meter1 = Flatten[#] & /@ wordInfo[[All, All, 2]];
   meter2 = meter1 //. {
      {a___, 1, 1, 1, b___} -> {a, 1, 0, 1, b},
      {a___, 1, 1, .5, b___} -> {a, 1, 1, 0, b},
      {a___, .5, 1, 1, b___} -> {a, 0, 1, 1, b},
      {a___, 0, 0, 0, b___} -> {a, 0, 1, 0, b},
      {a___, 0, 0, .5, b___} -> {a, 0, 0, 1, b},
      {a___, .5, 0, 0, b___} -> {a, 1, 0, 0, b}} ;
   midCts = Count[#, .5, 2] & /@ meter2;
   zero1s = Tuples[{0, 1}, #] & /@ midCts;
   midPos = Flatten[Position[#, .5]] & /@ meter2;
   prelim = MapIndexed[{midPos[[#2[[1]]]], #1} &, zero1s, {2}];
   repRules = Map[(Rule @@@ Partition[Riffle @@ #, 2] &), prelim, {2}];
   replaced = 
    MapIndexed[ReplacePart[meter2[[#2[[1]]]], #1] &, repRules, {2}];
   seqPredict = SequencePredict[allPatterns];
   result = seqPredict[#, "SequenceProbability"] & /@ replaced;
   resultPos = Position[#, Max[#]][[1, 1]] & /@ result;
   meter3 = MapIndexed[replaced[[#2, #1]][[1]] &, resultPos];
   alignPt = Max[Position[#, 1]] & /@ meter3;
   splitMeters = Partition[#, UpTo[Max[Position[#, 1]]]] & /@ meter3;
   bkOff = Min[Position[Reverse[#], 1]] & /@ meter3;
   coords = Flatten[MapIndexed[If[
        #2[[2]] == 1,
        {1 - #2[[3]], #1 + 1.2 (#2[[1]] - 1)},
        {#2[[3]] - 1, #1 + 1.2 (#2[[1]] - 1)}
        ] &, 
      Reverse[MapAt[Prepend[1], 
        MapAt[Reverse, 
         splitMeters /. ({lhs_List} :> {lhs, {}}), {All, 1}], {All, 
         2}]], {3}], 1];
   xRuns = Range[Length[#]] & /@ meter3;
   lens = Length /@ meter3;
   endPts = bkOff;
   begPts = endPts - lens;
   coordsX = MapThread[Range[#1, #2] &, {begPts + 1, endPts}];
   coordsY = MapIndexed[{#1 + 1.2*#2[[1]]}[[1]] &, Reverse[meter3]];
   coords2 = 
    MapThread[
     Partition[Riffle[#1, #2], 2] &, {Reverse[coordsX], coordsY}];
   txtCoo = MapIndexed[{#1[[1]], 1.2*#2[[1]] + .5} &, coords2, {2}];
   syllab = #[[;; , 3]] & /@ Reverse[wordInfo];
   textGr = 
    MapThread[
     Style[Text[#1, #2], 15, FontFamily -> "Times"] &, {Flatten[
       syllab], Partition[Flatten[txtCoo, 2], 2]}];
   meterGraphic = 
    Graphics[{GrayLevel[.7], Line[coords2], Black, textGr}, 
     ImageMargins -> {{10, 10}, {0, 0}}, ImageSize -> 900]
   );
analyzeMeter[stanza]

Prepares information for rhyme graphics.

syllab2 = Flatten[#] & /@ Reverse[syllab];
phonSylls = 
  Flatten[MapIndexed[{StringDelete[
       WordData[#1, "PhoneticForm"] /. _Missing -> 
         getPhonsAndSylls[#1]["Phonetic"], "?" | "?"], #2} &, 
    syllab2, {2}], 1];
allPairs = Subsets[phonSylls, {2}];
{Short[phonSylls], Short[allPairs]} // Column

Creates the alliteration graphic.

alliterationAll = 
  Cases[allPairs, {{ps1_, _}, {ps2_, _}} /; 
    Characters[StringDelete[ps1, "?" | "?"]][[1]] == 
     Characters[StringDelete[ps2, "?" | "?"]][[1]]];
alliterationClose = 
  Cases[alliterationAll, {{_, {l1_, s1_}}, {_, {l2_, s2_}}} /; 
    l1 === l2 && Abs[s2 - s1] < 6];
alliterationCoords = {Reverse[txtCoo][[#[[1, 2, 1]], #[[1, 2, 2]]]], 
     Reverse[txtCoo][[#[[2, 2, 1]], #[[2, 2, 2]]]]} & /@ 
   alliterationClose;
alliterationArcs = 
  BezierCurve[{#[[1]] + {0, .1}, 
      Mean[{#[[1]], #[[2]]}] + {0, 1.5}, #[[2]] + {0, .1}}] & /@ 
   alliterationCoords;
Show[meterGraphic, Graphics[{Darker[Green], alliterationArcs}]]

Creates the consonance graphic.

allPhonemes = ipaChars /. ("<" | ">" | "?" | "?") -> Nothing;
ipaConsonants = Complement[allPhonemes, ipaVowels];
consonanceAll = 
  Cases[allPairs, {{ps1_, _}, {ps2_, _}} /; 
    Length[Intersection[Characters[ps1], Characters[ps2], 
       ipaConsonants]] > 0];
consonanceClose = 
  Cases[consonanceAll, {{_, {l1_, s1_}}, {_, {l2_, s2_}}} /; 
    l1 === l2 && Abs[s2 - s1] < 4];
consonanceCoords = {Reverse[txtCoo][[#[[1, 2, 1]], #[[1, 2, 2]]]], 
     Reverse[txtCoo][[#[[2, 2, 1]], #[[2, 2, 2]]]]} & /@ 
   consonanceClose;
consonanceArcs = 
  BezierCurve[{#[[1]] + {0, .1}, 
      Mean[{#[[1]], #[[2]]}] + {0, 1.5}, #[[2]] + {0, .1}}] & /@ 
   consonanceCoords;
Show[meterGraphic, Graphics[{Purple, consonanceArcs}]]

Creates the assonance graphic.

assonanceAll = 
  Cases[allPairs, {{ps1_, _}, {ps2_, _}} /; 
    Length[Intersection[Characters[ps1], Characters[ps2], 
       ipaVowels]] > 0];
assonanceClose = 
  Cases[assonanceAll, {{_, {l1_, s1_}}, {_, {l2_, s2_}}} /; 
    l1 === l2 && Abs[s2 - s1] < 4];
assonanceCoords = {Reverse[txtCoo][[#[[1, 2, 1]], #[[1, 2, 2]]]], 
     Reverse[txtCoo][[#[[2, 2, 1]], #[[2, 2, 2]]]]} & /@ 
   assonanceClose;
assonanceArcs = 
  BezierCurve[{#[[1]] + {0, .1}, 
      Mean[{#[[1]], #[[2]]}] + {0, 1.5}, #[[2]] + {0, .1}}] & /@ 
   assonanceCoords;
Show[meterGraphic, Graphics[{Darker[Orange], assonanceArcs}]]

Creates the end rhyme graphic.

phonSyllsMeter = 
  MapThread[Append[#1, #2] &, {phonSylls, Flatten[meter3]}];
endRhymeMultiCans = 
  SequenceCases[
   phonSyllsMeter, {{_, _, 1}, {_, _, 0}, 
    Repeated[{_, _, 0}, {0, 1}]}];
endRhymeMultiSwords = {#[[1, 1]] <> #[[2, 1]], {#[[1, 2]], #[[-1, 
       2]]}} & /@ endRhymeMultiCans;
endRhymeUniCans = 
  Append[#[[1]] & /@ 
    SequenceCases[
     phonSyllsMeter, {{_, {a_, _}, 1}, {_, {b_, _}, _}} /; a != b], 
   phonSyllsMeter[[-1]]];
endRhymeUniSwords = {#[[1]], {#[[2]], #[[2]]}} & /@ endRhymeUniCans;
endRhymeSwords = Join[endRhymeMultiSwords, endRhymeUniSwords];
endRhymeSubsets = Subsets[endRhymeSwords, {2}];
endRhymes = 
  Cases[endRhymeSubsets, {{s1_, _}, {s2_, _}} /; 
    StringContainsQ[s1, 
     StringDelete[s2, StartOfString ~~ ipaConsonants ...]]];
endRhymeBgLt = 
  Rectangle[
     Reverse[txtCoo][[#[[1, 2, 1, 1]], #[[1, 2, 1, 2]]]] + {-.4, -.1},
      Reverse[txtCoo][[#[[1, 2, 2, 1]], #[[1, 2, 2, 
        2]]]] + {.4, .1}] & /@ endRhymes;
endRhymeBgRt = 
  Rectangle[
     Reverse[txtCoo][[#[[2, 2, 1, 1]], #[[2, 2, 1, 2]]]] + {-.4, -.1},
      Reverse[txtCoo][[#[[2, 2, 2, 1]], #[[2, 2, 2, 
        2]]]] + {.4, .1}] & /@ endRhymes;
endRhymeBg = Join[endRhymeBgLt, endRhymeBgRt];
endRhymeCoords = {Reverse[txtCoo][[#[[1, 2, 1, 1]], #[[1, 2, 1, 2]]]],
      Reverse[txtCoo][[#[[2, 2, 1, 1]], #[[2, 2, 1, 2]]]]} & /@ 
   endRhymes;
endRhymeArcs = 
  BezierCurve[{#[[1]] + {0, .1}, 
      Mean[{#[[1]], #[[2]]}] + {1.5, .5}, #[[2]] + {0, .1}}] & /@ 
   endRhymeCoords;
Show[Graphics[{RGBColor[.8, .8, 1], endRhymeBg}], meterGraphic, 
 Graphics[{Blue, endRhymeArcs}], ImageSize -> 800]
POSTED BY: Mark Greenberg
7 Replies

"We use imperfect rhyme (slant rhyme) so much in modern song and verse"

Reminds me of a stanza from Tom Lehrer's "The Folksong Army". Funny..I just now looked it up to get an appropriate link.

https://www.google.com/search?channel=fs&client=ubuntu&q=The+Folksong+Army

The Youtube hit actually shows the punchline, so to speak, for that particular verse.

POSTED BY: Daniel Lichtblau
Posted 4 years ago

Yes, clever. But if we're talking about sounds, how is rap/wrap rhyme while rap/rap isn't? Another question... We use imperfect rhyme (slant rhyme) so much in modern song and verse, isn't telling a computer to search for words that rhyme, like dog and log, missing half the rhymes? I'm leaning toward identifying phoneme echoes. True rhymes will show heavy phoneme echoes, while slant rhyme will show some. Weighting the phoneme echoes by whether they're in a stressed or unstressed syllable brings this out even more. Off-beat rhymes are subtle.

POSTED BY: Mark Greenberg
Posted 4 years ago

The Wolfram Language has the means to analyze a poem for such things as meter, rhyme, and cadence. The project above is proof that this can be done. I have recently started rewriting the project to improve it and remove its reliance on custom neural networks (because I don't have the skill to manage them).

If we could drop a stanza into a computer and have it show us the rhythms, echoes, and pauses, then we could see a large part of the craft that went into making the verse (some more than others). Often the little bits of instruction we give to students about rhyme schemes and meter leave them with the impression that writing poetry is like making waffles because the poet has to somehow pour words into a preexisting mould. Having a visualization would allow students to see that the beat and the echoes come from the words themselves, and that the poet is creating the patterns, not following them.

Thanks, Gizem, for making me aware of the Journal of Humanistic Mathematics. In a few months I should be finishing up the current iteration of the project. I'll consider submitting an article at that time.

POSTED BY: Mark Greenberg

A rap that rhymes rapping with wrapping is fair game no rapper would rip. But to rhyme rapping with...rapping? That's a trap no rapper would trip. While I might be wily, I'd not be unruly, if I rhymed while I wrote why I'm rapt.

POSTED BY: Daniel Lichtblau

This is very interesting! If you might be interested in somewhat traditional publication, Journal of Humanistic Mathematics could be a good possible outlet for this kind of work. (I am one of the editors. And no, I don't spam everyone's content page to get submissions. This really looked interesting! In particular I'd love to know what motivated you to get into this project and what kinds of teaching uses you can envision.)

I was intrigued by your sentence: "The point is that the computer can detect features in poetry and visualize them." For me, this brings up interesting questions about poetry and its humanity. Can a computer that is aware of these poetic features eventually create "good poetry" (whatever that might mean)? Maybe a future project?

POSTED BY: Gizem Karaali
Posted 5 years ago

For those of you taking a close look at the diagrams, you may notice that the code links some rhymes most people would not consider rhymes. There are three main reasons for this. First, it counts repetition as rhyme, so "dog" rhymes with "dog." Second, the algorithm for splitting syllables is not consistent, which leads to some unexpected connections. Finally, we hear rhymes anchored to stressed syllables, while the code is linking echoes from any syllables, stressed or not. I am working toward a better version that will account for these factors. The current code is my project from the Wolfram Summer School. As such, it is richer than it would have been, including ideas from many people, but it also had to be published before I smoothed all the rough edges.

POSTED BY: Mark Greenberg

enter image description here - Congratulations! This post is now featured in our Staff Pick column as distinguished by a badge on your profile of a Featured Contributor! Thank you, keep it coming, and consider contributing your work to the The Notebook Archive!

POSTED BY: EDITORIAL BOARD
Reply to this discussion
Community posts can be styled and formatted using the Markdown syntax.
Reply Preview
Attachments
Remove
or Discard

Group Abstract Group Abstract