NovelEssay.com Programming Blog

Exploration of Big Data, Machine Learning, Natural Language Processing, and other fun problems.

Installing Python Chainer and Theano on Windows with Anaconda for GPU Processing

Let's say you want to do some GPU processing on Windows and you want to use Python, because of awesome things like this:


We'll show the setup steps for installing Python Chainer and Theano on Windows 10 in this blog article.


Some Terms:

CUDAan API model created by Nvidia for GPU processing.

cuDNN - a neural network plugin library for CUDA

Chainer - a Python neural network framework package

Theano - a Python deep learning package


Initial Hardware and OS Requirements:

You need an Nvidia CUDA supported video card. (I have a NVidia GeForce GTX 750 Ti.) Check for your GPU card in the support list found here: https://developer.nvidia.com/cuda-gpus 

You need Windows 10. (Everything in this procedure is x64.)


Important: 

Versions matter a lot. I tried to do this exact same setup with Python 2.7, and I was not successful. I tried to do the same thing with Anaconda 2, and that didn't work. I tried to do this same thing with cuDNN 5.5, and that didn't work. - So many combinations didn't work for me that I decided to write about what did work.


Procedure:

1) Install Visual Studio 2015. You must install Visual Studio before installing the CUDA tool kit. You need the \bin\cl.exe compiler. I have the VS2015 Enterprise Edition, but the VS2015 Community Edition is free here: https://www.microsoft.com/en-us/download/details.aspx?id=48146


2) Install the CUDA Tool kit found here: https://developer.nvidia.com/cuda-downloads

That installs v8.0 to a path like this: C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v8.0


3) Download the cuDNN v5.0 here: https://developer.nvidia.com/cudnn

There is a v5.1 there, but it did not work for me. Feel free to try it, but I suggest trying v5.0 first.

The cuDNN is just 3 files. You'll want to drop them in the CUDA path:

  • Drop the cudnn.h file in the folder:  C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v8.0\include\
  • Drop the cudnn64_5.dll file in the folder:  C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v8.0\bin\
  • Drop the cudnn.lib file in the folder:  C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v8.0\lib\x64\

4) Install Anaconda 3.6 for Windows x64 found here: https://repo.continuum.io/archive/Anaconda3-4.3.1-Windows-x86_64.exe

In case that link breaks, this is the page I found it at: https://www.continuum.io/downloads

You'll be doing most of your Anaconda/Python work in the Anaconda Console window. If Windows does not give you a nice link to the Anaconda Console, make a short cut with a link that looks like this:

"%windir%\system32\cmd.exe " "/K" C:\ProgramData\Anaconda3\Scripts\activate.bat C:\ProgramData\Anaconda3

I installed Anaconda for "All Users", so it put it at ProgramData. If you install to just one user, it puts Anaconda at a c:\users\<your name>\ path.


5) Building python packages requires a gcc/g++ compiler. Install MinGW for x64 here: https://sourceforge.net/projects/mingw-w64/

WARNING: During this install, be sure to pick the x86_64 install and not the i686 install!

The default install for MinGW is at c:\Program Files\mingw-w64\x86_64-6.3.0-posix-seh-rt_v5-rev1\mingw64\bin

The space in Program Files will break stuff later, so move it to something like this instead:

C:\mingw-w64\x86_64-6.3.0-posix-seh-rt_v5-rev1\mingw64\bin


6) Environment paths! 

If you have no idea how to set Enviornment variables in Windows, here's a link that describes how to do that: http://www.computerhope.com/issues/ch000549.htm

Add a variable called "CFlags" with this value:

  • -IC:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v8.0\include

Add a variable called "CUDA_PATH" with this value:

  • C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v8.0

Add a variable called "LD_LIBRARY_PATH" with this value:

  • C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v8.0\lib\x64

Add a variable called "LDFLAGS" with this value:

  • -LC:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v8.0\lib\x64

Add all of the following to your PATH variable (or ensure they exist):

  • C:\mingw-w64\x86_64-6.3.0-posix-seh-rt_v5-rev1\mingw64\bin
  • C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v8.0\bin
  • C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v8.0\libnvvp
  • C:\Program Files (x86)\Microsoft Visual Studio 14.0\vc\bin
  • C:\ProgramData\Anaconda3
  • C:\ProgramData\Anaconda3\Scripts
  • C:\ProgramData\Anaconda3\Library\bin

(The Anacond3 paths might get set automatically for you.)


7) Next, bring up your Anaconda console prompt and install some packages. Type the following lines:

pip install Pillow
pip install Pycuda
pip install Theano
pip install Chainer
If any of those fail to install, stop and figure out why. Append a -vvvv to the end of the install lines to get a very-very-very verbose dump of the install process. 

Note: If you can't get pycuda to install due to "missing stdlib.h" errors, you can get the pycuda Whl file and install that directly instead.

It likely is because one of your steps #1-6 isn't quite right, or because your GCC compiler is trying to use an old x32 version that you installed long ago. (That was the case for me. I had Cygwin and a x32 GCC compiler that caused failing pip package installs.)

I also had some build fails on Chainer with some errors about "_hypot" being undefined. I fixed those by going to C:\ProgramData\Anaconda3\include\pyconfig.h, and commenting out the two places in that file that do this:
//#define hypot _hypot
That appear to have fixed that issue for me, but there's probably a better solution.

8) Sanity checks and smoke tests:
First, try to import the packages from a python command window. You can run this directly from your Anaconda console like this:
  • python -c "import theano"
  • python -c "import chainer"
  • python -c "import cupy"
If one of them fails, identify the error message and ask the Google about it. They should all work:


A last smoke test is to get the "Hello GPU" test code from here:

Here's a copy:
import pycuda.autoinit
import pycuda.driver as drv
import numpy
from pycuda.compiler import SourceModule
mod = SourceModule("""
__global__ void multiply_them(float *dest, float *a, float *b)
{
  const int i = threadIdx.x;
  dest[i] = a[i] * b[i];
}
""")
multiply_them = mod.get_function("multiply_them")
a = numpy.random.randn(400).astype(numpy.float32)
b = numpy.random.randn(400).astype(numpy.float32)
dest = numpy.zeros_like(a)
multiply_them(
        drv.Out(dest), drv.In(a), drv.In(b),
        block=(400,1,1), grid=(1,1))
print (dest-a*b)

I had to change the last line of that code to have parenthesis around it like this:
print (dest-a*b)

When you run that with a command like this:
python pycuda_test.py
You should get an output of 0's that look like this:



Conclusion:
If you've gotten to here, congratulations! Your Windows 10 environment should be all setup to run Python GPU processing.

My GPU has been running for days at 90%, and my CPU is free for other work. This was a seriously miserable to figure out, but now it feels like my computer doubled the processing power!

Enjoy the awesome:

Using IEqualityComparer for finding near duplicates with custom business logic in C#

Example use case:

Let's say you're crawling the web gathering up information about people, and you want to group any matches of "John Smith" that might actually be the same person. 


In the generic case, we're walking through how to manage business logic cases for finding near duplicates using good software design principles.


We'll start by having a class like the Person example that follows:

    public class Person
    {
        [Key]
        public long Id { get; set; }
        public string Name { get; set; }
        public string LinkedInUrl { get; set; }
        public string TwitterUrl { get; set; }
        public string FacebookUrl { get; set; }


Let's say we get a hit from some blog about "John Smith", and his TwitterUrl is twitter.com/jsmith. We'll populate a Person object like that and toss it in our database, repository, or whatever storage we're using like this:

Person foundPerson = new Person() { Name = "John Smith", TwitterUrl = "twitter.com/jsmith" };
List<Person> allPeopleFound = new List<Person>();
allPeopleFound.Add(foundPerson);

Later, we find another "John Smith", and his LinkedInUrl is linkedin.com/in/jsmith. We'll add that Person to our collection:

Person foundPerson = new Person() { Name = "John Smith", LinkedInUrl = "linkedin.com/in/jsmith" };
allPeopleFound.Add(foundPerson);

Finally, we find another "J. P. Smith", and his LinkedInUrl is linkedin.com/in/jsmith and Facebook URL is facebook.com/jps. We'll add that Person to our collection:

Person foundPerson = new Person() { Name = "John Smith", LinkedInUrl = "linkedin.com/in/jsmith", FacebookUrl = "facebook.com/jps" };
allPeopleFound.Add(foundPerson);



We could group allPeopleFound by Name, but that's certainly going to have many false positives in our "John Smith" group. That approach will also not let us group "J. P. Smith" with "John Smith".

Let's show the code we want to happen before we show the solution we need.

var nearDupePeople = allPeopleFound.GroupBy(c => c, new PersonComparer());
foreach (var nearDupePerson in nearDupePeople)
{
    foreach (var person in nearDupePerson)
    {
        // Here we are iterating through all person objects that were grouped to gether by the PersonComparer above
        // TODO: Now, that "JP Smith" and "John Smith" are found equal, we need to have business rules about multi-valued fields

Now, you should be thinking - What's with that PersonComparer class?


Not messing around, we'll show off the PersonComparer class that implements an IEqualityComparer.

public class PersonComparer : IEqualityComparer<Person>
{
    public bool Equals(Person p1, Person p2)
    {
        // Social media matches, various social network identity matching here:
        if (!string.IsNullOrEmpty(p1.LinkedInUrl) && !string.IsNullOrEmpty(p2.LinkedInUrl) && p1.LinkedInUrl.Equals(p2.LinkedInUrl))
        {
            return true;
        }
        if (!string.IsNullOrEmpty(p1.TwitterUrl) && !string.IsNullOrEmpty(p2.TwitterUrl) && p1.TwitterUrl.Equals(p2.TwitterUrl))
        {
            return true;
        }
        if (!string.IsNullOrEmpty(p1.FacebookUrl) && !string.IsNullOrEmpty(p2.FacebookUrl) && p1.FacebookUrl.Equals(p2.FacebookUrl))
        {
            return true;
        }
        return false;
    }
    public int GetHashCode(Person p)
    {
        return (p.LinkedInUrl + p.TwitterUrl + p.FacebookUrl).GetHashCode();
    }
}

Notice that our IEqualityComparer implementation needs to have two functions implemented: Equals and GetHashCode.


In our code, we'll call two Person objects equal if their LinkedIn Urls are the same, or their Twitter Urls are the same, or their Facebook Urls are the same. We don't consider two Person instances equal if their Name is equal.


Our GetHashCode function needs to account for all 3 properties we are using for equating Person objects, so we concatenate our 3 Url properties to get our object's hash code.


That's all there is to executing a custom "near duplicate" grouping and easily handling the business logic inside your implementation for IEqualityComparer.


Extracting important snip-its with C# and Log Likelyhood


How does text summary software pick out the most important ideas to present?

One solution is to use Log Likelyhood to generate a summary of the sentences that contain the important terms and cover the most different topics.


What Log Likelyhood is and why it works can be read here: https://en.wikipedia.org/wiki/Likelihood_function


This article will focus on implementing it in C#. Here's the code I wrote as a translation from the Mahout version. I tried to make it as readable as possible rather than optimizing for performance.

// Log Likelyhood code roughly translated from here:
// http://grepcode.com/file/repo1.maven.org/maven2/org.apache.mahout/mahout-math/0.3/org/apache/mahout/math/stats/LogLikelihood.java#LogLikelihood.logLikelihoodRatio%28int%2Cint%2Cint%2Cint%29
static private double ShannonEntropy(List<Int64> elements)
{
    double sum = 0;
    foreach (Int64 element in elements)
    {
        sum += element;
    }
    double result = 0.0;
    foreach (Int64 element in elements)
    {
        if(element < 0)
        {
            throw new Exception("Should not have negative count for entropy computation (" + element + ")");
        }
        int zeroFlag = (element == 0 ? 1 : 0);
        result += element * Math.Log((element + zeroFlag) / sum);
    }
    return result;
}
/*
    Calculate the Raw Log-likelihood ratio for two events, call them A and B. Then we have:
 	    Event A	Everything but A
        Event B	A and B together (k_11)	B, but not A (k_12)
        Everything but B	A without B (k_21)	Neither A nor B (k_22)
    Parameters:
    k11 The number of times the two events occurred together
    k12 The number of times the second event occurred WITHOUT the first event
    k21 The number of times the first event occurred WITHOUT the second event
    k22 The number of times something else occurred (i.e. was neither of these events
*/
static public double LogLikelihoodRatio(Int64 k11, Int64 k12, Int64 k21, Int64 k22)
{
    double rowEntropy = ShannonEntropy(new List<Int64>() { k11, k12 }) + ShannonEntropy(new List<Int64>() { k21, k22 });
    double columnEntropy = ShannonEntropy(new List<Int64>() { k11, k21 }) + ShannonEntropy(new List<Int64>() { k12, k22 });
    double matrixEntropy = ShannonEntropy(new List<Int64>() { k11, k12, k21, k22 });
    return 2 * (matrixEntropy - rowEntropy - columnEntropy);
}

Now, we have a simple LogLikelihoodRatio function we can call with 4 parameters and get the score result.


Let's say we want to pick out the most important sentences from a particular Wikipedia article in order to summarize it. (See this article for loading Wikipedia in to ElasticSearch: http://blog.novelessay.com/post/loading-wikipedia-in-to-elasticsearch)

Follow these steps:

  1. Pick a Wikipedia article.
  2. Get a Term Frequency dictionary for the whole article.
  3. Parse the article in to sentences.
  4. For each token in each sentence, calculate the Log Likelyhood score with the above LogLikelihoodRatio function.
  5. If the result of LogLikelihoodRatio is less than -10, give that sentence +1 to a weight value.
  6. At the end of each sentence, you have a +X weight value. That can be normalized by the number of words in the sentence.
  7. After you've obtained the weight score from #6 for all of the sentences in an article, you can sort them and pick the most important ones.
For extra credit, you'll want to avoid redundant important sentences. In order to do that, you'll need to score the candidate sentences against the summary's output as you build it.


Here's some code with comments about populating the input values passed to the LogLikelihoodRatio function. Be sure to check the result score is less than -10 before adding a +1 weight.

// http://www.cs.columbia.edu/~gmw/candidacy/LinHovy00.pdf - Section 4.1
Int64 k11 = // frequency of current term in this article
Int64 k12 = // frequency of current term in all of Wikipedia - k11
Int64 k21 = // total count of all terms in this article - k11
Int64 k22 = // total count of all terms in Wikipedia - k12
double termWeight = LogLikelihoodRatio(k11, k12, k21, k22);

if(termWeight < -10)
{
    weightSum++;
}

Obviously, in the above you don't want to be calculating Term Frequency across Wikipedia on-the-fly. K11 and K21 will get calculated as you process an article, but K12 and K22 should be calculated in advance and cached in a lookup dictionary. 


I use LevelDb as my Term Frequency look up dictionary. You can read about using that here: http://blog.novelessay.com/post/fast-persistent-key-value-pairs-in-c-with-leveldb


In order to build your Term Frequency look up dictionary chace, you could process each documents and create your own term frequency output, or use the ElasticSearch plugin for _termList here: https://github.com/jprante/elasticsearch-index-termlist



Text Extraction using C# .Net and Apache Tika


You want to using C# to extract text from documents and web pages. You want it to have high quality and be free. Try the .Net wrapper to the Apache Tika library!


Let's build a sample app and show the use case. First step, start a C# console application with Visual Studio. Use the Nuget package manager and install the TikaOnDotNet.TextExtractor packages.



Then, try this sample code. It shows an example of text extraction examples for a file, Url, and byte array sources.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using TikaOnDotNet.TextExtraction;

namespace TikaTest
{
    class Program
    {
        static void Main(string[] args)
        {

            TextExtractor textExtractor = new TextExtractor();

            // Fun Utf8 strings found here: http://www.columbia.edu/~fdc/utf8/
            string utf8InputString = @"It's a small village in eastern Lower Saxony. The ""oe"" in this case turns out to be the Lower Saxon ""lengthening e""(Dehnungs-e), which makes the previous vowel long (used in a number of Lower Saxon place names such as Soest and Itzehoe), not the ""e"" that indicates umlaut of the preceding vowel. Many thanks to the Óechtringen-Namenschreibungsuntersuchungskomitee (Alex Bochannek, Manfred Erren, Asmus Freytag, Christoph Päper, plus Werner Lemberg who serves as Óechtringen-Namenschreibungsuntersuchungskomiteerechtschreibungsprüfer) for their relentless pursuit of the facts in this case. Conclusion: the accent almost certainly does not belong on this (or any other native German) word, but neither can it be dismissed as dirt on the page. To add to the mystery, it has been reported that other copies of the same edition of the PLZB do not show the accent! UPDATE (March 2006): David Krings was intrigued enough by this report to contact the mayor of Ebstorf, of which Oechtringen is a borough, who responded:";
            // Convert string to byte array
            byte[] byteArrayInput = Encoding.UTF8.GetBytes(utf8InputString);
            // Text Extraction Example for Byte Array
            TextExtractionResult result = textExtractor.Extract(byteArrayInput);
            Console.WriteLine(result.Text);

            // Text Extraction Example for Uri:
            result = textExtractor.Extract(new Uri("http://blog.novelessay.com"));
            Console.WriteLine(result.Text);

            // Text Extraction Example for File
            result = textExtractor.Extract(@"c:\myPdf.pdf");
            Console.WriteLine(result.Text);

            // Note that result also has metadata collection and content type attributes
            //result.Metadata
            //result.ContentType
        }
    }
}

Notice that the TextExtractionResult has a Metadata collection and also a content type attribute. Here's an example of the metadata provided along with the extracted text. It contains many things including author, dates, keywords, title, and description.


      

I've been very pleased with Tika's quality and ability to handle many different file types. I hope you try it out and enjoy it too.


Fast Persistent Key Value Pairs in C# with LevelDb



Let's say we want to crawl the internet, but we don't want to request any given URL more than once. We need to have a collection of URL keys that we can look up. It would be nice if we could have key-value pairs, so that we can give URL keys a value in case we change our minds and want to allow URL request updates every X days. We want it to handle billions of records and be really fast (and free). This article will show how to accomplish that using LevelDb and its C# wrapper.


First, start a Visual Studio C# project and download the LevelDb.Net nuget package. There are a few different one, but this is my favorite. 


You can also find this LevelDb.Net at this Github location:

https://github.com/AntShares/leveldb


First, I'm going to show how to use LevelDb via C#. Later in this article, code shows how to insert and select a large number of records for speed testing.


Let's create a LevelDb:

            Options levelDbOptions = new Options();
            levelDbOptions.CreateIfMissing = true;
            LevelDB.DB levelDb = LevelDB.DB.Open("myLevelDb.dat", levelDbOptions);

Next, we'll insert some keys:

            levelDb.Put(LevelDB.WriteOptions.Default, "Key1", "Value1");
            levelDb.Put(LevelDB.WriteOptions.Default, "Key1", "Value2");
            levelDb.Put(LevelDB.WriteOptions.Default, "Key1", "Value3");
            levelDb.Put(LevelDB.WriteOptions.Default, "Key2", "Value2");

Then, we'll select some keys:

            LevelDB.Slice outputValue;
            if (levelDb.TryGet(LevelDB.ReadOptions.Default, "Key2", out outputValue))
            {
                Console.WriteLine("Key2: Value = " + outputValue.ToString());// Expect: Value2
            }
            if (levelDb.TryGet(LevelDB.ReadOptions.Default, "Key1", out outputValue))
            {
                Console.WriteLine("Key1: Value = " + outputValue.ToString()); // Expect: Value3
            }
            if (!levelDb.TryGet(LevelDB.ReadOptions.Default, "KeyXYZ", out outputValue))
            {
                Console.WriteLine("KeyXYZ: NOT FOUND.");
            }

LevelDb supports many different types of keys and values (strings, int, float, byte[], etc...).

  1. Open instance handle.
  2. Insert = Put
  3. Select = TryGet

That's it! 

But, how fast is it?

Let's build a collection of MD5 hash keys and insert them:

            List<string> seedHashes = new List<string>();
            for (int idx = 0; idx < 500000; idx++)
            {
                byte[] encodedPassword = new UTF8Encoding().GetBytes(idx.ToString());
                byte[] hash = ((HashAlgorithm)CryptoConfig.CreateFromName("MD5")).ComputeHash(encodedPassword);
                string encoded = BitConverter.ToString(hash).Replace("-", string.Empty).ToLower();
                seedHashes.Add(encoded);
            }

            // Start Insert Speed Tests
            Stopwatch stopWatch = new Stopwatch();
            stopWatch.Start();
            foreach(var key in seedHashes)
            {
                levelDb.Put(LevelDB.WriteOptions.Default, key, "1");
            }
            stopWatch.Stop();
            Console.WriteLine("LevelDb Inserts took (ms) " + stopWatch.ElapsedMilliseconds);


Next, let's select each of the keys we just inserted several times:

            // Start Lookup Speed Tests
            stopWatch.Start();
            for (int loopIndex = 0; loopIndex < 10; loopIndex++)
            {
                for(int seedIndex = 0; seedIndex < seedHashes.Count; seedIndex++)
                {
                    if (!levelDb.TryGet(LevelDB.ReadOptions.Default, seedHashes[seedIndex], out outputValue))
                    {
                        Console.WriteLine("ERROR: Key Not Found: " + seedHashes[seedIndex]);
                    }
                }
            }
            stopWatch.Stop();
            Console.WriteLine("LevelDb Lookups took (ms) " + stopWatch.ElapsedMilliseconds);

On my junky 4 year old desktop, 500,000 inserts took just under 60 seconds and 5 Million selects took just over 2 minutes. Here's the program output:


The complete code sample is below:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using LevelDB;
using System.Security.Cryptography;
using System.Diagnostics;

namespace LevelDbExample
{
    class Program
    {
        static void Main(string[] args)
        {

            Options levelDbOptions = new Options();
            levelDbOptions.CreateIfMissing = true;
            LevelDB.DB levelDb = LevelDB.DB.Open("myLevelDb.dat", levelDbOptions);

            // Insert some records
            levelDb.Put(LevelDB.WriteOptions.Default, "Key1", "Value1");
            levelDb.Put(LevelDB.WriteOptions.Default, "Key1", "Value2");
            levelDb.Put(LevelDB.WriteOptions.Default, "Key1", "Value3");
            levelDb.Put(LevelDB.WriteOptions.Default, "Key2", "Value2");

            // Select some records
            LevelDB.Slice outputValue;
            if (levelDb.TryGet(LevelDB.ReadOptions.Default, "Key2", out outputValue))
            {
                Console.WriteLine("Key2: Value = " + outputValue.ToString());// Expect: Value2
            }
            if (levelDb.TryGet(LevelDB.ReadOptions.Default, "Key1", out outputValue))
            {
                Console.WriteLine("Key1: Value = " + outputValue.ToString()); // Expect: Value3
            }
            if (!levelDb.TryGet(LevelDB.ReadOptions.Default, "KeyXYZ", out outputValue))
            {
                Console.WriteLine("KeyXYZ: NOT FOUND.");
            }

            // Build a collection of hash keys
            List<string> seedHashes = new List<string>();
            for (int idx = 0; idx < 500000; idx++)
            {
                byte[] encodedPassword = new UTF8Encoding().GetBytes(idx.ToString());
                byte[] hash = ((HashAlgorithm)CryptoConfig.CreateFromName("MD5")).ComputeHash(encodedPassword);
                string encoded = BitConverter.ToString(hash).Replace("-", string.Empty).ToLower();
                seedHashes.Add(encoded);
            }

            // Start Insert Speed Tests
            Stopwatch stopWatch = new Stopwatch();
            stopWatch.Start();
            foreach(var key in seedHashes)
            {
                levelDb.Put(LevelDB.WriteOptions.Default, key, "1");
            }
            stopWatch.Stop();
            Console.WriteLine("LevelDb Inserts took (ms) " + stopWatch.ElapsedMilliseconds);

            // Start Lookup Speed Tests
            stopWatch.Start();
            for (int loopIndex = 0; loopIndex < 10; loopIndex++)
            {
                for(int seedIndex = 0; seedIndex < seedHashes.Count; seedIndex++)
                {
                    if (!levelDb.TryGet(LevelDB.ReadOptions.Default, seedHashes[seedIndex], out outputValue))
                    {
                        Console.WriteLine("ERROR: Key Not Found: " + seedHashes[seedIndex]);
                    }
                }
            }
            stopWatch.Stop();
            Console.WriteLine("LevelDb Lookups took (ms) " + stopWatch.ElapsedMilliseconds);

            return;
        }
    }
}