Experiments into markov chains, n-grams, and text generation.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

112 lines
4.0 KiB

using System;
using System.Collections.Generic;
using System.Linq;
using MarkovGrams.Utilities;
using SBRL.Algorithms;
namespace MarkovGrams
/// <summary>
/// An unweighted character-based markov chain.
/// </summary>
public class WeightedMarkovChain
private WeightedRandom<string> wrandom = new WeightedRandom<string>();
/// <summary>
/// The ngrams that this markov chain currently contains.
/// </summary>
private Dictionary<string, double> ngrams;
/// <summary>
/// Whether to always start generating a new word from an n-gram that starts with
/// an uppercase letter.
/// </summary>
public bool StartOnUppercase = false;
/// <summary>
/// The generation mode to use when running the Markov Chain.
/// </summary>
/// <remarks>
/// The input n-grams must have been generated using the same mode specified here.
/// </remarks>
public GenerationMode Mode { get; private set; } = GenerationMode.CharacterLevel;
/// <summary>
/// Creates a new character-based markov chain.
/// </summary>
/// <param name="inNgrams">The ngrams to populate the new markov chain with.</param>
public WeightedMarkovChain(Dictionary<string, double> inNgrams, GenerationMode inMode) {
ngrams = inNgrams;
Mode = inMode;
public WeightedMarkovChain(Dictionary<string, int> inNgrams, GenerationMode inMode) {
ngrams = new Dictionary<string, double>();
foreach (KeyValuePair<string, int> ngram in inNgrams)
ngrams[ngram.Key] = ngram.Value;
Mode = inMode;
/// <summary>
/// Returns a random ngram that's currently loaded into this WeightedMarkovChain.
/// </summary>
/// <returns>A random ngram from this UnweightedMarkovChain's cache of ngrams.</returns>
public string RandomNgram()
if (wrandom.Count == 0) {
if (!StartOnUppercase)
else {
Dictionary<string, double> filteredNGrams = new Dictionary<string, double>();
foreach (KeyValuePair<string, double> pair in ngrams.Where((pair) => char.IsUpper(pair.Key[0])))
filteredNGrams.Add(pair.Key, pair.Value);
if (filteredNGrams.Count() == 0)
throw new Exception($"Error: No valid starting ngrams were found (StartOnUppercase: {StartOnUppercase}).");
return wrandom.Next();
/// <summary>
/// Generates a new random string from the currently stored ngrams.
/// </summary>
/// <param name="length">
/// The length of ngram to generate.
/// Note that this is a target, not a fixed value - e.g. passing 2 when the n-gram order is 3 will
/// result in a string of length 3. Also, depending on the current ngrams this markov chain contains,
/// it may end up being cut short.
/// </param>
/// <returns>A new random string.</returns>
public string Generate(int length)
string result = RandomNgram();
string lastNgram = result;
while(result.Length < length)
// The substring that the next ngram in the chain needs to start with
string nextStartsWith = Mode == GenerationMode.CharacterLevel ? lastNgram.Substring(1) : lastNgram.Split(' ')[0];
// Get a list of possible n-grams we could choose from next
Dictionary<string, double> convNextNgrams = new Dictionary<string, double>();
ngrams.Where(gram_data => gram_data.Key.StartsWith(nextStartsWith))
.ForEach((KeyValuePair<string, double> ngramData) => convNextNgrams.Add(ngramData.Key, ngramData.Value));
// If there aren't any choices left, we can't exactly keep adding to the new string any more :-(
if(convNextNgrams.Count() == 0)
// Pick a random n-gram from the list
string nextNgram = wrandom.Next();
// Add the last character from the n-gram to the string we're building
if (Mode == GenerationMode.CharacterLevel)
result += nextNgram[nextNgram.Length - 1];
result += string.Join(" ", nextNgram.Split(' ').Skip(1));
lastNgram = nextNgram;
return result;