Jump to content
  • Advertisement
Sign in to follow this  
chadjohnson

Neural Network outputs not correct

This topic is 4814 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

I'm working on a handwriting recognition program (I know, it's not a game - hope no one minds), and when I train my feedforward neural network using the different characteristics for each image as inputs, all the outputs (26 in all) eventually become flat zero (after a couple hundred thousand training sessions). In training, for example, when training on the letter S I set every output neuron's target value to -1 except for the one corresponding to S which I set to 1. This seems to do nothing though. I have used this network library (I wrote it) to train a network to learn a continuous function, and it worked great - and I ran it through several million training sessions (using a low learning rate). Does anyone have any helpful hints as to why it might not be working in this case? I'm having the program look at the following characteristics: number of intersections (on a grid), closest distances to image on all sides (normalized), and the percentages of colored pixels in four quadrants (using monochrome bitmaps for training). BTW, is it better to do recognition by using a block grid and just using each grid square as an input? Here's some code (it uses the wxWidgets library for the GUI). There is a lot of redundancy - I'll fix that later:
// handwriting.cpp

#include <wx/wxprec.h>

#ifndef WX_PRECOMP
    #include <wx/wx.h>
#endif

#include "handwriting.h"

IMPLEMENT_APP(Handwriting)

BEGIN_EVENT_TABLE(MainFrame, wxFrame)
    EVT_INIT_DIALOG(MainFrame::OnInitDialog)
    EVT_CLOSE(MainFrame::OnClose)
    EVT_MENU(MENU_FILE_EXIT, MainFrame::OnFileExit)
    EVT_MENU(MENU_TOOLS_TRAINING, MainFrame::OnToolsTraining)
    EVT_MENU(MENU_HELP_ABOUT, MainFrame::OnHelpAbout)
END_EVENT_TABLE()

BEGIN_EVENT_TABLE(DrawCanvas, wxScrolledWindow)
    EVT_MOTION(DrawCanvas::OnMouseMove)
    EVT_LEFT_DOWN(DrawCanvas::OnLeftMouseDown)
    EVT_LEFT_UP(DrawCanvas::OnLeftMouseUp)
END_EVENT_TABLE()

BEGIN_EVENT_TABLE(TrainingDialog, wxDialog)
    EVT_INIT_DIALOG(TrainingDialog::OnInitDialog)
    EVT_CLOSE(TrainingDialog::OnClose)
    EVT_BUTTON(BUTTON_ADD, TrainingDialog::OnAddButton)
    EVT_BUTTON(BUTTON_TEST, TrainingDialog::OnTestButton)
    EVT_BUTTON(BUTTON_REMOVE, TrainingDialog::OnRemoveButton)
    EVT_BUTTON(BUTTON_START, TrainingDialog::OnStartButton)
    EVT_BUTTON(BUTTON_CLOSE, TrainingDialog::OnCloseButton)
END_EVENT_TABLE()

/*****************************************************************************/
// Handwriting class functions
/*****************************************************************************/
bool Handwriting::OnInit()
{
    // Create a neural network instance
    m_neuralNetwork = new NeuralNetwork();

    // Create an input layer, one hidden layer, and an output layer
    m_neuralNetwork->AddLayer(INPUT_NEURON_COUNT+1);
    m_neuralNetwork->AddLayer(HIDDEN_NEURON_COUNT);
    m_neuralNetwork->AddLayer(OUTPUT_NEURON_COUNT);

    // Set a bias value
    m_neuralNetwork->SetInputValue(INPUT_NEURON_COUNT, 1);

    m_mainFrame = new MainFrame("Handwriting Recognition", 320, 320);
    m_mainFrame->Centre();
    m_mainFrame->Show(TRUE);

    return true;
}

NeuralNetwork *Handwriting::GetNeuralNetwork() const
{
    return m_neuralNetwork;
}

MainFrame *Handwriting::GetMainFrame() const
{
    return m_mainFrame;
}
/*****************************************************************************/
// MainFrame class functions
/*****************************************************************************/
MainFrame::MainFrame(const wxString &title, int width, int height)
        : wxFrame((wxFrame*) NULL, -1, title, wxDefaultPosition, wxSize(width, height), wxDEFAULT_FRAME_STYLE & ~ (wxRESIZE_BORDER | wxRESIZE_BOX | wxMAXIMIZE_BOX))
{
    // File menu
    m_menuFile = new wxMenu();
    m_menuFile->Append(MENU_FILE_EXIT, _T("E&xit"));

    // Tools menu
    m_menuTools = new wxMenu();
    m_menuTools->Append(MENU_TOOLS_TRAINING, _T("T&raining..."));

    // Help menu
    m_menuHelp = new wxMenu();
    m_menuHelp->Append(MENU_HELP_ABOUT, _T("About..."));

    m_menuBar = new wxMenuBar();
    m_menuBar->Append(m_menuFile, _T("&File"));
    m_menuBar->Append(m_menuTools, _T("&Tools"));
    m_menuBar->Append(m_menuHelp, _T("&Help"));
    SetMenuBar(m_menuBar);

    m_panel = new wxPanel(this, -1);
    m_textMain = new wxTextCtrl(m_panel, -1, _T(""), wxDefaultPosition, wxDefaultSize, wxTE_MULTILINE);
    m_canvas = new DrawCanvas(m_panel, wxDefaultPosition, wxSize(100, 100), wxSIMPLE_BORDER);

    wxFlexGridSizer *sizer = new wxFlexGridSizer(2, 1, 0, 0);
    sizer->Add(m_textMain, 0, wxALL|wxEXPAND, 4);
    sizer->Add(m_canvas, 0, wxLEFT|wxRIGHT|wxBOTTOM|wxALIGN_CENTER, 4);
    sizer->AddGrowableRow(0);
    sizer->AddGrowableCol(0);

    m_panel->SetSizer(sizer);
    m_panel->SetAutoLayout(true);
    m_panel->Layout();

    InitDialog();
}

void MainFrame::OnInitDialog(wxInitDialogEvent &event)
{
}

void MainFrame::OnClose(wxCloseEvent &event)
{
    //SaveDataToFile();
    Destroy();
}

void MainFrame::OnFileExit(wxCommandEvent &event)
{
    Close();
}

void MainFrame::OnToolsTraining(wxCommandEvent &event)
{
    TrainingDialog *dlg = new TrainingDialog(this);
    dlg->ShowModal();
}

void MainFrame::OnHelpAbout(wxCommandEvent &event)
{
    wxMessageBox(_T("Copyright 2005 Chad Johnson (chad.d.johnson@gmail.com)"), "About");
}
/*****************************************************************************/
// DrawCanvas class functions
/*****************************************************************************/
DrawCanvas::DrawCanvas(wxWindow *parent, wxPoint pos, wxSize size, long style)
        : wxScrolledWindow(parent, -1, pos, size, style)
{
    SetBackgroundColour(wxColour(*wxWHITE));
}

void DrawCanvas::OnMouseMove(wxMouseEvent& event)
{
    wxClientDC dc(this);
    DoPrepareDC(dc);

    wxPoint pt(event.GetLogicalPosition(dc));

    if (lastX > -1 && lastY > -1 && event.Dragging() && event.LeftIsDown())
    {
        wxPen pen;
        pen.SetWidth(2);
        pen.SetColour(0,0,255);

        dc.SetPen(pen);
        dc.DrawLine(lastX, lastY, pt.x, pt.y);
    }

    lastX = pt.x;
    lastY = pt.y;
}

void DrawCanvas::OnLeftMouseDown(wxMouseEvent &event)
{
}

void DrawCanvas::OnLeftMouseUp(wxMouseEvent &event)
{
    int i = 0, j = 0, k = 0;
    unsigned char *imageData;
    wxColour pixelColor;
    int imageWidth = 0;
    int imageHeight = 0;

    wxClientDC dc(this);
    DoPrepareDC(dc);

    // Get the width and height of the drawing canvas
    imageWidth = GetSize().GetWidth();
    imageHeight = GetSize().GetHeight();

    // Allocate memory for the image data
    imageData = new unsigned char[imageWidth*imageHeight];

    // Go through the pixels of the drawing canvas and put the data into the image buffer
    for (j=0; j<imageHeight; j++)
    {
        for (i=0; i<imageWidth; i++)
        {
            dc.GetPixel(i, j, &pixelColor);

            // Use 1 for colored, 0 for non-colored
            if (pixelColor.Red() == 255 && pixelColor.Green() == 255 && pixelColor.Blue() == 255)
                imageData[k] = 0;
            else
                imageData[k] = 1;

            k++;
        }
    }

    // Crop the image
    imageData = CropImage(imageData, imageWidth, imageHeight);

    // Append the recognized character to the main text box
    ((MainFrame*)wxGetTopLevelParent(this))->m_textMain->AppendText(
            wxString::Format("%c", wxGetApp().GetNeuralNetwork()->RecognizeCharacter(
                    imageData,
                    imageWidth,
                    imageHeight)));

    // Clear the drawing canvas
    Refresh();
}

int DrawCanvas::GetX()
{
    return lastX;
}

int DrawCanvas::GetY()
{
    return lastY;
}

void DrawCanvas::SetX(int x)
{
    lastX = x;
}

void DrawCanvas::SetY(int y)
{
    lastY = y;
}

// TODO need to just modify image data pointer instead of returning new pointer
unsigned char *DrawCanvas::CropImage(unsigned char *imageData, int &width, int &height)
{
    int i = 0, j = 0, k = 0;
    int startX = 0;
    int startY = 0;
    int endX = 0;
    int endY = 0;
    unsigned char *croppedImageData;

    // Find the first x column containing a colored pixel
    for (i=0; i<width; i++)
    {
        for (j=0; j<height; j++)
        {
            if (imageData[i + (j * width)] == 1)
            {
                startX = i;

                // Set i = width in order to break out of the outer loop as well
                i = width;
                break;
            }
        }
    }

    // Find the last x column containing a colored pixel
    for (i=width-1; i>=0; i--)
    {
        for (j=0; j<height; j++)
        {
            if (imageData[i + (j * width)] == 1)
            {
                endX = i;

                // Set i = width in order to break out of the outer loop as well
                i = 0;
                break;
            }
        }
    }

    // Find the first y row containing a colored pixel
    for (i=0; i<height; i++)
    {
        for (j=0; j<width; j++)
        {
            if (imageData[j + (i * width)] == 1)
            {
                startY = i;

                // Set i = height in order to break out of the outer loop as well
                i = height;
                break;
            }
        }
    }

    // Find the last y row containing a colored pixel
    for (i=height-1; i>=0; i--)
    {
        for (j=0; j<width; j++)
        {
            if (imageData[j + (i * width)] == 1)
            {
                endY = i;

                // Set i = height in order to break out of the outer loop as well
                i = 0;
                break;
            }
        }
    }

    croppedImageData = new unsigned char[((endX+1)-startX) * ((endY+1)-startY)];

    // Copy the data from the original image data array to the new one using the
    // cropped beginning and end columns and rows
    for (j=startY; j<endY+1; j++)
    {
        for (i=startX; i<endX+1; i++)
            croppedImageData[k++] = imageData[i + (j * width)];
    }

    width = (endX+1) - startX;
    height = (endY+1) - startY;

    delete [] imageData;

    return croppedImageData;
}

/*****************************************************************************/
// TrainingDialog class functions
/*****************************************************************************/
TrainingDialog::TrainingDialog(wxWindow *parent) : wxDialog(parent, -1, _T("Train From Image(s)"), wxDefaultPosition, wxSize(350, 350))
{
    m_trainingGroupBox = new wxStaticBox(this, -1, _T("Training Data"));
    m_fileList = new wxListBox(this, -1);
    m_buttonTest = new wxButton(this, BUTTON_TEST, _T("&Test"));
    m_buttonAdd = new wxButton(this, BUTTON_ADD, _T("&Add File(s)..."));
    m_buttonRemove = new wxButton(this, BUTTON_REMOVE, _T("Remove"));
    m_textTrainingCount = new wxTextCtrl(this, -1, _T("1000"), wxDefaultPosition, wxSize(50, -1));
    m_textOutput = new wxTextCtrl(this, -1, _T(""), wxDefaultPosition, wxDefaultSize, wxTE_MULTILINE);
    m_startButton = new wxButton(this, BUTTON_START, _T("Start"));
    m_closeButton = new wxButton(this, BUTTON_CLOSE, _T("Close"));

    m_startButton->SetDefault();

    // Create the sizers for the dialog - what a mess!
    wxStaticBoxSizer *mainSizerSub1 = new wxStaticBoxSizer(m_trainingGroupBox, wxVERTICAL);
    wxBoxSizer *mainSizerSub1Sub1 = new wxBoxSizer(wxVERTICAL);

    wxBoxSizer *mainSizerSub1Sub1Sub2 = new wxBoxSizer(wxHORIZONTAL);
    mainSizerSub1Sub1Sub2->Add(new wxStaticText(this, -1, _T("Training Sessions:")), 0, wxALIGN_CENTER_VERTICAL|wxRIGHT, 3);
    mainSizerSub1Sub1Sub2->Add(m_textTrainingCount);

    mainSizerSub1Sub1->Add(mainSizerSub1Sub1Sub2, 0, wxBOTTOM, 3);
    mainSizerSub1Sub1->Add(new wxStaticText(this, -1, _T("Image File(s):")), 0, wxEXPAND|wxBOTTOM, 3);
    mainSizerSub1Sub1->Add(m_fileList, 0, wxEXPAND);

    wxBoxSizer *mainSizerSub1Sub1Sub1 = new wxBoxSizer(wxHORIZONTAL);
    mainSizerSub1Sub1Sub1->Add(m_buttonTest);
    mainSizerSub1Sub1Sub1->Add(m_buttonAdd);
    mainSizerSub1Sub1Sub1->Add(m_buttonRemove);

    mainSizerSub1Sub1->Add(mainSizerSub1Sub1Sub1, 0, wxALIGN_RIGHT);
    mainSizerSub1->Add(mainSizerSub1Sub1, 0, wxEXPAND|wxALL, 3);

    wxBoxSizer *mainSizer = new wxBoxSizer(wxVERTICAL);
    mainSizer->Add(mainSizerSub1, 0, wxALL|wxEXPAND, 6);
    mainSizer->Add(new wxStaticText(this, -1, _T("Output:")), 0, wxLEFT|wxRIGHT|wxTOP, 6);
    // TODO add a spacer here
    mainSizer->Add(m_textOutput, 0, wxEXPAND|wxLEFT|wxRIGHT|wxBOTTOM, 6);

    wxBoxSizer *mainSizerSub2 = new wxBoxSizer(wxHORIZONTAL);
    mainSizerSub2->Add(m_startButton);
    mainSizerSub2->Add(m_closeButton);
    mainSizer->Add(mainSizerSub2, 0, wxALL|wxALIGN_RIGHT, 6);

    mainSizer->SetMinSize(375, -1);

    SetSizerAndFit(mainSizer);
    SetAutoLayout(true);
    Layout();

    InitDialog();
}

void TrainingDialog::OnInitDialog(wxInitDialogEvent &event)
{
}

void TrainingDialog::OnClose(wxCloseEvent &event)
{
    Destroy();
}

void TrainingDialog::OnStartButton(wxCommandEvent &event)
{
    long trainingCount = 0;
    int i = 0, j = 0;
    char curChar = 0;

    // Go through the file list if it's not empty and perform training for each image
    // the number of times the user has specified
    if (m_fileList->GetCount() > 0)
    {
        // If the training count field is blank, default to 1000 training sessions
        if (m_textTrainingCount->GetValue().Length() > 0)
            m_textTrainingCount->GetValue().ToLong(&trainingCount);
        else
        {
            m_textTrainingCount->SetValue("1000");
            trainingCount = 1000;
        }

        // Go through the list and perform the specified number of training sessions
        // for each image
        for (i=0; i<m_fileList->GetCount()*trainingCount; i++)
        {
            wxGetApp().GetNeuralNetwork()->TrainFromImage(m_fileList->GetString(j),
                                                          GetFileFirstCharacter(m_fileList->GetString(j)));

            j++;

            if (j % m_fileList->GetCount() == 0)
                j = 0;
        }
    }
}

void TrainingDialog::OnCloseButton(wxCommandEvent &event)
{
    Destroy();
}

void TrainingDialog::OnTestButton(wxCommandEvent &event)
{
    m_textOutput->AppendText(wxGetApp().GetNeuralNetwork()->ShowOutput(m_fileList->GetString(m_fileList->GetSelection()),
                                                                       GetFileFirstCharacter(m_fileList->GetString(m_fileList->GetSelection()))));
}

void TrainingDialog::OnAddButton(wxCommandEvent &event)
{
    wxFileDialog *dlg = new wxFileDialog(this, _T("Select File(s)"), "", "", "*.bmp", wxOPEN|wxMULTIPLE);
    wxArrayString filenames;
    int i = 0;

    if (dlg->ShowModal() == wxID_OK)
    {
        dlg->GetPaths(filenames);

        for (i=0; i<filenames.Count(); i++)
        {
            // Add the file to the file list if it's not already in the list
            if (m_fileList->FindString(filenames) == wxNOT_FOUND)
                m_fileList->Append(filenames);
        }
    }
}

void TrainingDialog::OnRemoveButton(wxCommandEvent &event)
{
    if (m_fileList->GetCount() > 1 && m_fileList->GetSelection() != wxNOT_FOUND)
        m_fileList->Delete(m_fileList->GetSelection());
}

char TrainingDialog::GetFileFirstCharacter(wxString filename)
{
    wxFileName temp(filename);

    return temp.GetName().GetChar(0);
}



// neuralnetwork.h

#ifndef NEURALNETWORK
#define NEURALNETWORK

#include <iostream>
#include <vector>
#include <cmath>
#include <ctime>
#include <stdlib.h>

using namespace std;

// Constants
const int GRIDLINES_COUNT_X = 6;
const int GRIDLINES_COUNT_Y = 10;
const int DISTANCES_COUNT_X = GRIDLINES_COUNT_X * 2;
const int DISTANCES_COUNT_Y = GRIDLINES_COUNT_Y * 2;
const int INPUT_NEURON_COUNT = DISTANCES_COUNT_X + DISTANCES_COUNT_Y + 7;
const int HIDDEN_NEURON_COUNT = 30;
const int OUTPUT_NEURON_COUNT = 26;

// Class prototypes
class NeuralNetwork;
class NeuronLayer;
class Neuron;

// function prototypes
double RandomDouble(const double min, const double max);

class NeuralNetwork
{
 private:
    vector<NeuronLayer*> m_layers;
    double m_outerLearningRate;
    double m_innerLearningRate;

 public:
    NeuralNetwork();
    void AddLayer(int neurons);
    void AddNeuron(const int layer);
    int GetLayerCount() const;
    void SetLearningRates(const double inner, const double outer);
    double GetInputValue(int neuron);
    void SetInputValue(int neuron, double input);
    void SetInputVector(vector<double> inputVector);
    vector<double> GetInputVector();
    double GetTargetValue(int neuron);
    vector<double> GetTargetVector();
    void SetTargetValue(int neuron, double target);
    void SetTargetVector(vector<double> targetVector);
    double GetOutputValue(int neuron);
    vector<double> GetOutputVector();
    double GetNeuronOutput(const int layer, const int neuron) const;
    void Propagate();
    void BackPropagate();
    double TransferFunction(const double value) const;
    void TrainFromImage(wxString filename, char charIndex);
    wxString ShowOutput(wxString filename, char targetChar);
    unsigned char *SimplifyImageData(unsigned char *imageData, int width, int height);
    int *GetXDistances(unsigned char *imageData, int width, int height, int gridSpaceX, int gridStartX);
    int *GetYDistances(unsigned char *imageData, int width, int height, int gridSpaceY, int gridStartY);
    int GetXIntersections(unsigned char *imageData, int width, int height, int gridSpaceX, int gridStartX);
    int GetYIntersections(unsigned char *imageData, int width, int height, int gridSpaceY, int gridStartY);
    int GetColoredPixelCountTopLeft(unsigned char *imageData, int width, int height);
    int GetColoredPixelCountTopRight(unsigned char *imageData, int width, int height);
    int GetColoredPixelCountBottomRight(unsigned char *imageData, int width, int height);
    int GetColoredPixelCountBottomLeft(unsigned char *imageData, int width, int height);
    char RecognizeCharacter(unsigned char *imageData, int width, int height);
};

class NeuronLayer
{
 private:
    vector<Neuron*> m_neurons;

 public:
    NeuronLayer();
    void AddNeuron();
    Neuron *GetNeuron(const int neuron);
    int GetNeuronCount();
};

// Holds weight values between neuron layers
class Neuron
{
 private:
    double m_value;
    double m_deltaValue;
    double m_targetValue;
    vector<double> m_connections;

 public:
    Neuron();
    double GetValue();
    void SetValue(const double value);
    double GetDeltaValue();
    void SetDeltaValue(const double value);
    double GetTargetValue();
    void SetTargetValue(double targetValue);
    void AddConnection(const double weight);
    double GetConnectionWeight(const int neuron);
    void SetConnectionWeight(const int neuron, const double weight);
};

/*****************************************************************************/
// Generic functions
/*****************************************************************************/
// Generates a random number given a minimum and maximum
double RandomDouble(const double min, const double max)
{
    static int init = 0;

    // Only seed the generator if it has not already been seeded
    if (init == 0)
    {
        srand((unsigned int)time(NULL));
        init = 1;
    }

    return (max - min) * (double)rand() / (double)RAND_MAX + min;
}

/*****************************************************************************/
// NeuralNetwork class functions
/*****************************************************************************/
// Constructor
NeuralNetwork::NeuralNetwork()
{
    // Give the network a default learning rate
    SetLearningRates(0.2, 0.15);
}

// Adds a layer to the network by adding another element to the layer vector
void NeuralNetwork::AddLayer(int neurons = 0)
{
    int i = 0;

    m_layers.push_back(new NeuronLayer());

    // Add the the number of neurons specified in the constructor to this layer
    for (i=0; i<neurons; i++)
        AddNeuron(GetLayerCount()-1);
}

// Adds a neuron to a given layer
void NeuralNetwork::AddNeuron(const int layer)
{
    int i = 0;

    // Add a neuron to this layer
    m_layers[layer]->AddNeuron();

    // Add connections from all neurons in the previous layer to the this
    // neuron if this is not the first layer
    if (layer > 0)
    {
        for (i=0; i<m_layers[layer-1]->GetNeuronCount(); i++)
            m_layers[layer-1]->GetNeuron(i)->AddConnection(RandomDouble(-0.01, 0.01));
    }
}

int NeuralNetwork::GetLayerCount() const
{
    return m_layers.size();
}

// Sets the learning rate for the neural network
void NeuralNetwork::SetLearningRates(const double inner, const double outer)
{
    m_outerLearningRate = inner;
    m_innerLearningRate = outer;
}

// Returns the input value for a given input neuron
double NeuralNetwork::GetInputValue(int neuron)
{
    return m_layers[0]->GetNeuron(neuron)->GetValue();
}

// Sets the input value for a given input neuron
void NeuralNetwork::SetInputValue(int neuron, double input)
{
    m_layers[0]->GetNeuron(neuron)->SetValue(input);
}

// Sets the values for the input neurons
void NeuralNetwork::SetInputVector(vector<double> inputVector)
{
    int i = 0;

    for (i=0; i<inputVector.size(); i++)
        m_layers[0]->GetNeuron(i)->SetValue(inputVector);
}

// Returns the input vector to the network
vector<double> NeuralNetwork::GetInputVector()
{
    vector<double> temp;
    int i = 0;

    for (i=0; i<m_layers[0]->GetNeuronCount(); i++)
        temp.push_back(m_layers[0]->GetNeuron(i)->GetValue());

    return temp;
}

// Returns the target value for a given output neuron
double NeuralNetwork::GetTargetValue(int neuron)
{
    return m_layers[GetLayerCount()-1]->GetNeuron(neuron)->GetTargetValue();
}

// Sets the target value for a given output neuron
void NeuralNetwork::SetTargetValue(int neuron, double target)
{
    m_layers[GetLayerCount()-1]->GetNeuron(neuron)->SetTargetValue(target);
}

// Sets the target vector for the neural network. Used in backpropagation
void NeuralNetwork::SetTargetVector(vector<double> targetVector)
{
    int i = 0;

    for (i=0; i<targetVector.size(); i++)
        m_layers->GetNeuron(i)->SetTargetValue(targetVector);
}

// Returns the target output value for the network
vector<double> NeuralNetwork::GetTargetVector()
{
    vector<double> temp;
    int i = 0;

    for (i=0; i<m_layers[GetLayerCount()-1]->GetNeuronCount(); i++)
        temp.push_back(m_layers[GetLayerCount()-1]->GetNeuron(i)->GetTargetValue());

    return temp;
}

// Returns the output value for a given output neuron
double NeuralNetwork::GetOutputValue(int neuron)
{
    return m_layers[GetLayerCount()-1]->GetNeuron(neuron)->GetValue();
}

// Returns a vector containing the values of the neurons in the output layer
vector<double> NeuralNetwork::GetOutputVector()
{
    vector<double> temp;
    int i = 0;

    for (i=0; i<m_layers[GetLayerCount()-1]->GetNeuronCount(); i++)
        temp.push_back(m_layers[GetLayerCount()-1]->GetNeuron(i)->GetValue());

    return temp;
}

// Returns the summation of the products of the input value and the weights for
// a given neuron
double NeuralNetwork::GetNeuronOutput(const int layer, const int neuron) const
{
    return m_layers[layer]->GetNeuron(neuron)->GetValue();
}

// Feeds the input values through the network and calculates the output value
// for the network
void NeuralNetwork::Propagate()
{
    int i = 0;
    int j = 0;
    int k = 0;
    double weight = 0;
    double input = 0;
    double newValue = 0;

    // Loop through the layers starting at the second layer (first hidden layer)
    for (i=1; i<GetLayerCount(); i++)
    {
        // Loop through the neurons in the current layer
        for (j=0; j<m_layers->GetNeuronCount(); j++)
        {
            newValue = 0;

            // Loop through the neurons from the previous layer (which connect
            // to the neurons in the current layer
            for (k=0; k<m_layers[i-1]->GetNeuronCount(); k++)
            {
                // get the connection weight from the current neuron in the
                // previous layer to the current neuron in the current layer
                weight = m_layers[i-1]->GetNeuron(k)->GetConnectionWeight(j);

                // get the value for the current neuron in the previous layer
                input = m_layers[i-1]->GetNeuron(k)->GetValue();

                // add the product of the weight and the input to the summation
                newValue += weight * input;
            }

            // Run the new value through the transfer function
            newValue = TransferFunction(newValue);

            // set the value for the current neuron to the sum of the weights
            // and inputs coming into that neuron
            m_layers->GetNeuron(j)->SetValue(newValue);
        }
    }
}

// Adjusts the weights for the connections to improve the network's accuracy
void NeuralNetwork::BackPropagate()
{
    int i = 0;
    int j = 0;
    int k = 0;
    int l = 0;
    double delta = 0;
    double deltaTemp = 0;
    double previousNeuronOutput = 0;
    double currentNeuronOutput = 0;
    double currentConnectionWeight = 0;
    double changeInConnectionWeight = 0;

    // Loop through the layers starting at the output layer and ending at the
    // first hidden layer
    for (i=GetLayerCount()-1; i>=1; i--)
    {
        // Loop through the neurons in the current layer
        for (j=0; j<m_layers->GetNeuronCount(); j++)
        {
            currentNeuronOutput = m_layers->GetNeuron(j)->GetValue();

            // Loop through the neurons from the previous layer (which connect
            // to the neurons in the current layer
            for (k=0; k<m_layers[i-1]->GetNeuronCount(); k++)
            {
                previousNeuronOutput = m_layers[i-1]->GetNeuron(k)->GetValue();

                // Test whether the loop is at the output connection layer. If it's
                // not at the output layer it's at a hidden layer
                if (i == GetLayerCount()-1)
                {
                    delta = currentNeuronOutput * (1 - currentNeuronOutput) * (m_layers->GetNeuron(j)->GetTargetValue() - currentNeuronOutput);

                    // calculate change in weight for output connection layer
                    changeInConnectionWeight = m_outerLearningRate * delta * previousNeuronOutput;
                }
                else
                {
                    deltaTemp = 0;

                    // Get the delta values for all neurons in the next layer
                    for (l=0; l<m_layers[i+1]->GetNeuronCount(); l++)
                        deltaTemp += m_layers[i+1]->GetNeuron(l)->GetDeltaValue();

                    delta = currentNeuronOutput * (1 - currentNeuronOutput) * deltaTemp;

                    // calculate change in weight for hidden connection layer
                    changeInConnectionWeight = m_innerLearningRate * delta * previousNeuronOutput;
                }

                // Get the weight of the connection from the current neuron in
                // the previous layer to the current neuron in the current layer
                currentConnectionWeight = m_layers[i-1]->GetNeuron(k)->GetConnectionWeight(j);

                // Add the change in weight to the current neuron's weight
                m_layers[i-1]->GetNeuron(k)->SetConnectionWeight(j, currentConnectionWeight + changeInConnectionWeight);
            }
        }
    }
}

// Transfer (activation) function using the Sigmoid function
double NeuralNetwork::TransferFunction(const double value) const
{
    return 1 / (1 + exp(-1 * value));
}

void NeuralNetwork::TrainFromImage(wxString filename, char targetChar)
{
    wxImage *image = new wxImage();
    unsigned char *imageData;
    int *distances_x;
    int *distances_y;
    int intersections_x = 0;
    int intersections_y = 0;
    int coloredPixelCountTopLeft = 0;
    int coloredPixelCountTopRight = 0;
    int coloredPixelCountBottomLeft = 0;
    int coloredPixelCountBottomRight = 0;
    int totalColoredPixelCount = 0;
    int imageWidth = 0;
    int imageHeight = 0;
    int gridSpaceX = 0;
    int gridSpaceY = 0;
    int gridStartX = 0;
    int gridStartY = 0;
    int neuronIndex = 0;
    int i = 0, j = 0;

    // TODO Make sure image only has two colors

    // Load the image file into memory
    image->LoadFile(filename);

    imageWidth = image->GetWidth();
    imageHeight = image->GetHeight();

    // Get the image's pixel data
    imageData = SimplifyImageData(image->GetData(), imageWidth, imageHeight);

    gridSpaceX = floor((float)imageWidth / GRIDLINES_COUNT_X);
    gridSpaceY = floor((float)imageHeight / GRIDLINES_COUNT_Y);
    gridStartX = gridSpaceX / 2;
    gridStartY = gridSpaceY / 2;

    // Get the closest distances to the image at certain intervals in the x and y directions
    distances_x = GetXDistances(imageData, imageWidth, imageHeight, gridSpaceX, gridStartX);
    distances_y = GetYDistances(imageData, imageWidth, imageHeight, gridSpaceY, gridStartY);

    // Get counts of the intersections in the x and y directions
    intersections_x = GetXIntersections(imageData, imageWidth, imageHeight, gridSpaceX, gridStartX);
    intersections_y = GetYIntersections(imageData, imageWidth, imageHeight, gridSpaceY, gridStartY);

    // Get the Counts of pixels
    coloredPixelCountTopLeft = GetColoredPixelCountTopLeft(imageData, imageWidth, imageHeight);
    coloredPixelCountTopRight = GetColoredPixelCountTopRight(imageData, imageWidth, imageHeight);
    coloredPixelCountBottomLeft = GetColoredPixelCountBottomLeft(imageData, imageWidth, imageHeight);
    coloredPixelCountBottomRight = GetColoredPixelCountBottomRight(imageData, imageWidth, imageHeight);

    totalColoredPixelCount = coloredPixelCountTopLeft + coloredPixelCountTopRight + coloredPixelCountBottomLeft + coloredPixelCountBottomRight;

    // Set the target output values to the neural network according to the character specified
    for (i=0; i<OUTPUT_NEURON_COUNT; i++)
    {
        // TODO better method of accessing correct character
        if (i == targetChar-65)
            SetTargetValue(i, 1);
        else
            SetTargetValue(i, -1);
    }

    neuronIndex = 0;

    // Set input values to the neural network using the data just collected
    for (i=0; i<DISTANCES_COUNT_X; i++)
        SetInputValue(neuronIndex++, distances_x);

    for (i=0; i<DISTANCES_COUNT_Y; i++)
        SetInputValue(neuronIndex++, distances_y);

    SetInputValue(neuronIndex++, (double)intersections_x);
    SetInputValue(neuronIndex++, (double)intersections_y);
    SetInputValue(neuronIndex++, (double)intersections_x+intersections_y);
    SetInputValue(neuronIndex++, (double)coloredPixelCountTopLeft / totalColoredPixelCount);
    SetInputValue(neuronIndex++, (double)coloredPixelCountTopRight / totalColoredPixelCount);
    SetInputValue(neuronIndex++, (double)coloredPixelCountBottomLeft / totalColoredPixelCount);
    SetInputValue(neuronIndex++, (double)coloredPixelCountBottomRight / totalColoredPixelCount);

    Propagate();
    BackPropagate();

    delete image;
    delete [] imageData;
    delete [] distances_x;
    delete [] distances_y;
}

wxString NeuralNetwork::ShowOutput(wxString filename, char targetChar)
{
    wxImage *image = new wxImage();
    unsigned char *imageData;
    int *distances_x;
    int *distances_y;
    int intersections_x = 0;
    int intersections_y = 0;
    int coloredPixelCountTopLeft = 0;
    int coloredPixelCountTopRight = 0;
    int coloredPixelCountBottomLeft = 0;
    int coloredPixelCountBottomRight = 0;
    int totalColoredPixelCount = 0;
    int imageWidth = 0;
    int imageHeight = 0;
    int gridSpaceX = 0;
    int gridSpaceY = 0;
    int gridStartX = 0;
    int gridStartY = 0;
    int neuronIndex = 0;
    int i = 0, j = 0;
    wxString outputString;

    // TODO Make sure image only has two colors

    // Load the image file into memory
    image->LoadFile(filename);

    imageWidth = image->GetWidth();
    imageHeight = image->GetHeight();

    // Get the image's pixel data
    imageData = SimplifyImageData(image->GetData(), imageWidth, imageHeight);

    gridSpaceX = floor((float)imageWidth / GRIDLINES_COUNT_X);
    gridSpaceY = floor((float)imageHeight / GRIDLINES_COUNT_Y);
    gridStartX = gridSpaceX / 2;
    gridStartY = gridSpaceY / 2;

    // Get the closest distances to the image at certain intervals in the x and y directions
    distances_x = GetXDistances(imageData, imageWidth, imageHeight, gridSpaceX, gridStartX);
    distances_y = GetYDistances(imageData, imageWidth, imageHeight, gridSpaceY, gridStartY);

    // Get counts of the intersections in the x and y directions
    intersections_x = GetXIntersections(imageData, imageWidth, imageHeight, gridSpaceX, gridStartX);
    intersections_y = GetYIntersections(imageData, imageWidth, imageHeight, gridSpaceY, gridStartY);

    // Get the Counts of pixels
    coloredPixelCountTopLeft = GetColoredPixelCountTopLeft(imageData, imageWidth, imageHeight);
    coloredPixelCountTopRight = GetColoredPixelCountTopRight(imageData, imageWidth, imageHeight);
    coloredPixelCountBottomLeft = GetColoredPixelCountBottomLeft(imageData, imageWidth, imageHeight);
    coloredPixelCountBottomRight = GetColoredPixelCountBottomRight(imageData, imageWidth, imageHeight);

    totalColoredPixelCount = coloredPixelCountTopLeft + coloredPixelCountTopRight + coloredPixelCountBottomLeft + coloredPixelCountBottomRight;

    // Set the target output values to the neural network according to the character specified
    for (i=0; i<OUTPUT_NEURON_COUNT; i++)
    {
        // TODO better method of accessing correct character
        if (i == targetChar-65)
            SetTargetValue(i, 1);
        else
            SetTargetValue(i, -1);
    }

    neuronIndex = 0;

    // Set input values to the neural network using the data just collected
    for (i=0; i<DISTANCES_COUNT_X; i++)
        SetInputValue(neuronIndex++, distances_x);

    for (i=0; i<DISTANCES_COUNT_Y; i++)
        SetInputValue(neuronIndex++, distances_y);

    SetInputValue(neuronIndex++, (double)intersections_x);
    SetInputValue(neuronIndex++, (double)intersections_y);
    SetInputValue(neuronIndex++, (double)intersections_x+intersections_y);
    SetInputValue(neuronIndex++, (double)coloredPixelCountTopLeft / totalColoredPixelCount);
    SetInputValue(neuronIndex++, (double)coloredPixelCountTopRight / totalColoredPixelCount);
    SetInputValue(neuronIndex++, (double)coloredPixelCountBottomLeft / totalColoredPixelCount);
    SetInputValue(neuronIndex++, (double)coloredPixelCountBottomRight / totalColoredPixelCount);

    Propagate();

    outputString = "CHARACTER\tTARGET\tOUTPUT\n";

    for (i=0; i<OUTPUT_NEURON_COUNT; i++)
        outputString << wxString::Format("%c\t\t%f\t%f\n", i+65, GetTargetValue(i), GetOutputValue(i));

    delete image;
    delete [] imageData;
    delete [] distances_x;
    delete [] distances_y;

    return outputString;
}

unsigned char *NeuralNetwork::SimplifyImageData(unsigned char *imageData, int width, int height)
{
    unsigned char *simplifiedData;
    unsigned char r = 0, g = 0, b = 0;
    int i = 0, j = 0;

    simplifiedData = new unsigned char[width*height];

    for (i=0; i<width*height*3; i+=3)
    {
        r = imageData;
        g = imageData[i+1];
        b = imageData[i+2];

        // Use 1 for colored, 0 for non-colored
        if (r == 255 && g == 255 && b == 255)
            simplifiedData[j] = 0;
        else
            simplifiedData[j] = 1;

        j++;
    }

    return simplifiedData;
}

int *NeuralNetwork::GetXDistances(unsigned char *imageData, int width, int height, int gridSpaceX, int gridStartX)
{
    int *distances;
    bool intersection_found = false;
    int i = 0, j = 0, k =0;

    distances = new int[DISTANCES_COUNT_X];

    // Go through the image data and measure the x distances
    for (i=gridStartX; i<width-gridSpaceX; i+=gridSpaceX)
    {
        intersection_found = false;

        // Go down the current column and find the first intersection
        for (j=0; j<height; j++)
        {
            if (imageData[i + (j * width)] == 1)
            {
                distances[k] = j;
                intersection_found = true;
                break;
            }
        }

        // If no intersection was found, set the distance to -1
        if (!intersection_found)
            distances[k] = -1;

        k++;
        intersection_found = false;

        // Go up the current column and find the last intersection
        for (j=height-1; j>=0; j--)
        {
            if (imageData[i + (j * width)] == 1)
            {
                distances[k] = j;
                intersection_found = true;
                break;
            }
        }

        // If no intersection was found, set the distance to -1
        if (!intersection_found)
            distances[k] = -1;

        k++;
    }

    return distances;
}

int *NeuralNetwork::GetYDistances(unsigned char *imageData, int width, int height, int gridSpaceY, int gridStartY)
{
    int *distances;
    bool intersection_found = false;
    int i = 0, j = 0, k = 0;

    distances = new int[DISTANCES_COUNT_Y];

    // Go through the image data and measure the y distances
    for (i=gridStartY; i<height-gridSpaceY; i+=gridSpaceY)
    {
        intersection_found = false;

        // Go down the current row and find the first intersection
        for (j=0; j<width; j++)
        {
            if (imageData[j + (i * width)] == 1)
            {
                distances[k] = j;
                intersection_found = true;
                break;
            }
        }

        // If no intersection was found, set the distance to -1
        if (!intersection_found)
            distances[k] = -1;

        k++;
        intersection_found = false;

        // Go up the current row and find the last intersection
        for (j=width-1; j>=0; j--)
        {
            if (imageData[j + (i * width)] == 1)
            {
                distances[k] = j;
                intersection_found = true;
                break;
            }
        }

        // If no intersection was found, set the distance to -1
        if (!intersection_found)
            distances[k] = -1;

        k++;
    }

    return distances;
}

int NeuralNetwork::GetXIntersections(unsigned char *imageData, int width, int height, int gridSpaceX, int gridStartX)
{
    int intersections = 0;
    int i = 0, j = 0;

    // Go through the image data and find the x intersections
    for (i=gridStartX; i<width; i+=gridSpaceX)
    {
        // Go down the current row and find the first intersection
        for (j=0; j<height; j++)
        {
            // Only count an intersection if the previous pixel was not also
            // colored or the current pixel is the first pixel
            if (imageData[i + (j * width)] == 1 && (imageData[i + ((j-1) * width)] != 1 || (j == 0 && imageData[i + (j * width)] == 1)))
                intersections++;
        }
    }

    return intersections;
}

int NeuralNetwork::GetYIntersections(unsigned char *imageData, int width, int height, int gridSpaceY, int gridStartY)
{
    int intersections = 0;
    int i = 0, j = 0;

    // Go through the image data and find the y intersections
    for (i=gridStartY; i<height; i+=gridSpaceY)
    {
        // Go down the current row and find the first intersection
        for (j=0; j<width; j++)
        {
            // Only count an intersection if the previous pixel was not also
            // colored or the current pixel is the first pixel
            if (imageData[j + (i * width)] == 1 && (imageData[(j-1) + (i * width)] != 1 || (j == 0 && imageData[j + (i * width)] == 1)))
                intersections++;
        }
    }

    return intersections;
}

int NeuralNetwork::GetColoredPixelCountTopLeft(unsigned char *imageData, int width, int height)
{
    int coloredPixels = 0;
    int startX = 0, startY = 0;
    int endX = 0, endY = 0;
    int i = 0, j = 0;

    startX = 0;
    startY = 0;
    endX = floor((float)width / 2);
    endY = floor((float)height / 2);

    for (i=startY; i<endY; i++)
    {
        for (j=startX; j<endX; j++)
        {
            if (imageData[j + (i * width)] == 1)
                coloredPixels++;
        }
    }

    return coloredPixels;
}

int NeuralNetwork::GetColoredPixelCountTopRight(unsigned char *imageData, int width, int height)
{
    int coloredPixels = 0;
    int startX = 0, startY = 0;
    int endX = 0, endY = 0;
    int i = 0, j = 0;

    startX = floor((float)width / 2);
    startY = 0;
    endX = width;
    endY = floor((float)height / 2);

    for (i=startY; i<endY; i++)
    {
        for (j=startX; j<endX; j++)
        {
            if (imageData[j + (i * width)] == 1)
                coloredPixels++;
        }
    }

    return coloredPixels;
}

int NeuralNetwork::GetColoredPixelCountBottomLeft(unsigned char *imageData, int width, int height)
{
    int coloredPixels = 0;
    int startX = 0, startY = 0;
    int endX = 0, endY = 0;
    int i = 0, j = 0;

    startX = 0;
    startY = floor((float)height / 2);
    endX = floor((float)width / 2);
    endY = height;

    for (i=startY; i<endY; i++)
    {
        for (j=startX; j<endX; j++)
        {
            if (imageData[j + (i * width)] == 1)
                coloredPixels++;
        }
    }

    return coloredPixels;
}


int NeuralNetwork::GetColoredPixelCountBottomRight(unsigned char *imageData, int width, int height)
{
    int coloredPixels = 0;
    int startX = 0, startY = 0;
    int endX = 0, endY = 0;
    int i = 0, j = 0;

    startX = floor((float)width / 2);
    startY = floor((float)height / 2);
    endX = width;
    endY = height;

    for (i=startY; i<endY; i++)
    {
        for (j=startX; j<endX; j++)
        {
            if (imageData[j + (i * width)] == 1)
                coloredPixels++;
        }
    }

    return coloredPixels;
}

char NeuralNetwork::RecognizeCharacter(unsigned char *imageData, int width, int height)
{
    int *distances_x;
    int *distances_y;
    int intersections_x;
    int intersections_y;
    int coloredPixelCountTopLeft = 0;
    int coloredPixelCountTopRight = 0;
    int coloredPixelCountBottomLeft = 0;
    int coloredPixelCountBottomRight = 0;
    int totalColoredPixelCount = 0;
    int gridSpaceX = 0;
    int gridSpaceY = 0;
    int gridStartX = 0;
    int gridStartY = 0;
    int neuronIndex = 0;
    int i = 0, j = 0;
    float maxNeuronOutput = 0.0;
    int maxNeuronOutputIndex = 0;

    gridSpaceX = floor((float)width / GRIDLINES_COUNT_X);
    gridSpaceY = floor((float)height / GRIDLINES_COUNT_Y);
    gridStartX = gridSpaceX / 2;
    gridStartY = gridSpaceY / 2;

    // Get the closest distances to the image at certain intervals in the x and y directions
    distances_x = GetXDistances(imageData, width, height, gridSpaceX, gridStartX);
    distances_y = GetYDistances(imageData, width, height, gridSpaceY, gridStartY);

    // Get counts of the intersections in the x and y directions
    intersections_x = GetXIntersections(imageData, width, height, gridSpaceX, gridStartX);
    intersections_y = GetYIntersections(imageData, width, height, gridSpaceY, gridStartY);

    // Get the Counts of pixels
    coloredPixelCountTopLeft = GetColoredPixelCountTopLeft(imageData, width, height);
    coloredPixelCountTopRight = GetColoredPixelCountTopRight(imageData, width, height);
    coloredPixelCountBottomLeft = GetColoredPixelCountBottomLeft(imageData, width, height);
    coloredPixelCountBottomRight = GetColoredPixelCountBottomRight(imageData, width, height);

    totalColoredPixelCount = coloredPixelCountTopLeft + coloredPixelCountTopRight + coloredPixelCountBottomLeft + coloredPixelCountBottomRight;

    neuronIndex = 0;

    // Set input values to the neural network using the data just collected
    for (i=0; i<DISTANCES_COUNT_X; i++)
        SetInputValue(neuronIndex++, distances_x);

    for (i=0; i<DISTANCES_COUNT_Y; i++)
        SetInputValue(neuronIndex++, distances_y);

    SetInputValue(neuronIndex++, (double)intersections_x);
    SetInputValue(neuronIndex++, (double)intersections_y);
    SetInputValue(neuronIndex++, (double)intersections_x+intersections_y);
    SetInputValue(neuronIndex++, (double)coloredPixelCountTopLeft / totalColoredPixelCount);
    SetInputValue(neuronIndex++, (double)coloredPixelCountTopRight / totalColoredPixelCount);
    SetInputValue(neuronIndex++, (double)coloredPixelCountBottomLeft / totalColoredPixelCount);
    SetInputValue(neuronIndex++, (double)coloredPixelCountBottomRight / totalColoredPixelCount);

    Propagate();

    delete [] distances_x;
    delete [] distances_y;

    // Go through the output neurons and determine which has the highest output
    for (i=0; i<OUTPUT_NEURON_COUNT; i++)
    {
        if (GetOutputValue(i) > maxNeuronOutput)
        {
            maxNeuronOutput = GetOutputValue(i);
            maxNeuronOutputIndex = i;
        }
    }

    return maxNeuronOutputIndex + 65;
}

/*****************************************************************************/
// NeuronLayer class functions
/*****************************************************************************/
// Constructor
NeuronLayer::NeuronLayer()
{
}

// Adds a neuron to the neuron vector
void NeuronLayer::AddNeuron()
{
    m_neurons.push_back(new Neuron());
}

// Returns a pointer to a given neuron for this layer
Neuron *NeuronLayer::GetNeuron(const int neuron)
{
    return m_neurons[neuron];
}

int NeuronLayer::GetNeuronCount()
{
    return m_neurons.size();
}

/*****************************************************************************/
// Neuron class functions
/*****************************************************************************/
// Constructor
Neuron::Neuron()
{
    // Give the neuron an initial value
    m_value = 0;

    // set the delta value (used in backpropagation) initially to 0
    m_deltaValue = 0;
}

// Returns the output value for the neuron
double Neuron::GetValue()
{
    return m_value;
}

// Sets the output value for the neuron
void Neuron::SetValue(const double value)
{
    m_value = value;
}

// Returns the delta value for the neuron
double Neuron::GetDeltaValue()
{
    return m_deltaValue;
}

// Sets the delta value for the neuron
void Neuron::SetDeltaValue(const double value)
{
    m_deltaValue = value;
}

// Gets the target value for the neuron. Should only be used if the neuron is
// an output neuron
double Neuron::GetTargetValue()
{
    return m_targetValue;
}

// Sets the target value for the neuron. Should only be used if the neuron is
// an output neuron
void Neuron::SetTargetValue(double targetValue)
{
    m_targetValue = targetValue;
}

// Adds a new connection to the connection vector
void Neuron::AddConnection(const double weight)
{
    m_connections.push_back(weight);
}

// Returns the connection weight to another neuron
double Neuron::GetConnectionWeight(const int neuron)
{
    return m_connections[neuron];
}

// Sets the connection weight to another neuron
void Neuron::SetConnectionWeight(const int neuron, const double weight)
{
    m_connections[neuron] = weight;
}

#endif



[Edited by - chadjohnson on September 14, 2005 4:03:12 AM]

Share this post


Link to post
Share on other sites
Advertisement
Sign in to follow this  

  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

We are the game development community.

Whether you are an indie, hobbyist, AAA developer, or just trying to learn, GameDev.net is the place for you to learn, share, and connect with the games industry. Learn more About Us or sign up!

Sign me up!