Digital Signal Recognition using C and C++


In this article I explain the details behind signal recognition and the implementation of these applications in C and C++ on Linux. The DSP articles presented on this website are not intended to be comprehensive in there explanation of DSP. The information found on this site is intended to be a brief introduction to processing a digital signal and things to consider. My article on signal detection was not written to explain how to recognize a signal. The title could have easily been mistaken for recognition. A statistical change in a signal is recognized and acted upon. When the change in statistics is detected we can collect the data needed to recognize the signal. Some recognition systems are more complex than others so, the means by which you recognize a particular signal will be determined by the requirements of your application. I would also like to say that I was motivated to write this program after reading an CUJ article written by Joey Rogers some years ago. Before I go into signal recognition I go over some C++ pointer techniques used in the code.


C and C++ Pointers

Pointers are very important to understand, even after years of development you can still make a mistake if your not careful. After programming in C++ for some years I made the mistake below:


char arr[] = “ This is a string of characters “;

puts(arr); // This is equivalent to printing &arr

puts(&arr); // This is equal to printing arr

// ( arr == &arr ) is a true expression because, arr is an array not a pointer

This makes some sense, but leaves the variable arr not very well defined. Is arr a pointer also, and if it is how can arr equal &arr if the address operator takes its address. arr must be something else besides a pointer to a string. It's also an array, but this still does not explain arr == &arr because &arr[0] equals arr and &arr equals &arr[0], but *arr is clearly the first byte of the string which is arr[0], and we can't take arr's address as &arr. So if &arr equals &arr[0] then arr should equal arr[0], but it does not, it's also equal to &arr. This is not well defined behavior and I would even say unacceptable

Although the code above is accepted by the C++ complier, it's bad practice and appears to be a logical error in the specification.


More Pointers

I think it's appropiate to go over some more advanced pointer arithmatic since I used so much of it in the next program. This program was writtten before I started using the STL exstensively. We can just cover some details so things won't look too confusing to some people.

This is a dynamically allocated array of pointers to pointer-to-pointers that I use as an array of matrices after being initialized:

double ***weights;

weights = new double**[network_params::MAX_LAYERS ];


for(i=0; i<network_params::MAX_LAYERS; i++)

weights[i] = new double*[network_params::MAX_ROWS];

for(i=0; i<network_params::MAX_LAYERS; i++)

for(j=0; j<network_params::MAX_ROWS; j++)

weights[i][j]=new double[network_params::MAX_COLS];


This gives a convenient interface to the following C++ class operators:

operator double***() ;

operator double**() ;

operator double*() ;

operator double() ;


double** operator[](int index) ;

Implemeted to give the C++ class objects of type weights the ability to handle the 3-diminsional index [x][y][z] is operator [], which returns weights[index] which is a pointer to a pointer. By using weights[x][y][z] we get ***weights which is the value at that address location:

NN_Weights::operator double***() { return weights; }
NN_Weights::operator double**() { return *weights; }
NN_Weights::operator double*() { return **weights; }
NN_Weights::operator double() { return ***weights; }
double** NN_Weights::operator[] (int index) { return weights[index]; }

We can initialize the array of matrices to a range of random values like this :

double val;
/* initialize random seed: */
srand ( time(NULL) );

for(int i=0; i<network_params::MAX_LAYERS; i++)
          for(int j=0; j<network_params::MAX_ROWS; j++)
                     for(int k=0; k<network_params::MAX_COLS; k++)    {
                              val = 0.1*(rand() % 255 + 1); /* generate secret number: */
                              weights[i][j][k]=val;
                     }

That's it for the pointers techniques. The source code above assigns random values to the matrix weights. These random values assigned to the weights are used as a starting point for creating templates used to recognize one or more signals. This is a complex system. There are books that explain in more detail the techniques used in this atricle. Some related books will be listed on this site. A signal pattern is presented to the weight values through another matrix and processed. The signal to be recognized is stored in the pattern matrix. The weights matrix is searched for each value in the pattern for a weight value that resembles the pattern value the most. In other words the difference is taken and compared for each value in the pattern to determine the weight value that is the closest match to a specific pattern value, this weight is called the winning node. Once this is determined all of the weights in a predetermined area around the wining node called the winning node's neighborhood are brought closer to the pattern value by another predetermined value called the learning rate. This is repeated for each node in the pattern a predetermined number of times called the number of iterations. The mathamatics that describes this is complex, but not hard to understand at a high level. The source code below is not text book, it finds the winning node in the weights matrix. Notice how the difference taken is something llike the standard deviations. This technique is also used to get the Kohonen node value from the weighted values explained later.


void Neural_Net::Get_Winning_Node()     {
            double value       = 0;
            double min_value = 9999999.0;

            int row_dex  =  0;
           int col_dex    =  0;
           int weight_layers   = weights.get_num_layers();
           int weight_rows    = weights.get_num_rows();
           int weight_cols     = weights.get_num_cols();
           int pattern_rows   = pattern.get_num_rows();
          int pattern_cols      = pattern.get_num_cols();
          int i,j;
          // Find the Kohonen Node with the shortest distance
          for (i=0; i<weight_layers; i++)
                for (j=0; j<weight_rows; j++)   {
                       value=0;
                       // Find the distance between the Input vector
                       // and the current Kohonen node (i,j)
                       for(row_dex=0; row_dex<pattern_rows; row_dex++)
                                for(col_dex=0; col_dex<pattern_cols; col_dex++)   {
                                         value=pow(fabs(pattern[row_dex][col_dex]-weights[i][j][col_dex]),2);
                                         value=sqrt(value);
                                         if(!value)    continue;
                                         // If the Kohonen node value is less than the
                                         // smallest seen, store it in position (i,j)
                                        if((col_dex%2))    {
                                              if (value<=min_value)    {
                                                      min_value=value
                                                      weights.set_winning_layer(i);
                                                      weights.set_winning_row(j);
                                                      weights.set_winning_col(col_dex);
                                              }
                                           }
                                           else   {
                                                if (value<min_value)    {
                                                          min_value=value;
                                                          weights.set_winning_layer(i);
                                                          weights.set_winning_row(j);
                                                          weights.set_winning_col(col_dex);
                                                  }
                                            }
                                      }
                                }
                         }


The function above finds the winning node in the weights matrix. This function is called on each iteration of the training of the network. There is another function that decrement the winning nodes neighborhood on each iteration, it is also common to decrement the lerning rate also, but I did not decrement the learning rate in this program. The learning rate was kept small and I iterated differently over the network as you can tell from the modulus operator used when searching for the winning node. Also the number of itrations I used was kept pretty high. This is the actual training phase of the network. Given a signal it learns about the pattern of the signal through generalization. A number of different strategies can be employed to compensate for the limitations encountered while attempting to recognize a signal the network was trained to detect. Time shifts of the signal can cause the system to not recognize a pattern that it was actually trained to detect. Shifting the signal across the network can increase the probability of the system finding a match for the incoming signal. After the winning node is found it is necessary to update the network using the learning rate. The function that does this is listed next:


void Neural_Net::Run_Statistics()
{
  neighborhood=initial_neighborhood;
  learning_rate=initial_learning_rate;
  weights.Initialize_Weights();
  // Training Loop
  for(int iteration=1; iteration<num_iterations;)
  {
     // Decrement neighborhood size if necessary
          if(!(iteration%2000)&&((neighborhood-neighborhood_decrement)>1000))
                   neighborhood=(neighborhood-neighborhood_decrement);
         printf("%ld. Learning Rate: %lf\n Neighborhood: %d\n",iteration,learning_rate,neighborhood);
        // Store feature map
        // weights.Save_Weights();
        // Find Winning Kohonen layer
        // node for input pattern
        Get_Winning_Node();
        //Update all Kohonen node weight
        //vectors in winner's neighborhood
        Update_Weight_Vectors();
        iteration++; // Increment current iteration
  }
   // Store feature map
// weights.Save_Weights();
}

This function is written with my own ideas in mind so as I said before it's not text book but still not far from what you would see in some text book examples. As you can see the function above makes calls to Get_Winnning_Node() and Update_Weight_Vectors(). It is the function Update_Weight_Vectors() that updates the network of weights on each iteration in the function above. Here is the definition of Update_Weight_Vectors():

void Neural_Net::Update_Weight_Vectors() {

    int num_layers = weights.get_num_layers();
    int rows = weights.get_num_rows();
    int cols = weights.get_num_cols();
    int winning_row = weights.get_winning_row();
    int winning_col = weights.get_winning_col();
    int winning_layer = weights.get_winning_layer();
/ // * // * // * // * // * // * // * // * // * // * // * // * // * //
// Setup boundries and ranges and find boundries for neighborhood //
// * // * // * // * // * // * // * // * // * // * // * // * // * //
    int layer_start = winning_layer - neighborhood;
    int layer_stop = winning_layer + neighborhood;
    int row_start = winning_row - neighborhood;
    int row_stop = winning_row + neighborhood;
    int col_start = winning_col - neighborhood;
    int col_stop = winning_col + neighborhood;
// Correct range if out of bounds
    row_start = (row_start<0) ?0 :row_start;
   row_stop = (row_stop>=rows) ?(rows) :row_stop;
    col_start = (col_start<0) ?0 :col_start;
    col_stop = (col_stop>=cols) ?(cols) :col_stop;
    layer_start = (layer_start<0) ?0 :layer_start;
    layer_stop = (layer_stop>=num_layers)?(num_layers) :layer_stop;
// Update all weights in neighborhood
    int i,j,l;
    int wrow = weights.get_winning_row();
    int wcol = weights.get_winning_col();
    int wlayer = weights.get_winning_layer();
   for(l=layer_start; l<layer_stop; l++)
             for(i=row_start; i<row_stop; i++)
                         for(j=col_start; j<col_stop; j++) {
                                 if(l==wlayer&&i==wrow&&j==wcol) {
                                      continue;
                                 }
                                 if((weights[l][i][j]-pattern[i][j])<0) {
                                        weights[l][i][j]+=learning_rate;
                                 }
                                 else
                                 if((weights[l][i][j]-pattern[i][j])>0) {
                                        weights[l][i][j]-=learning_rate;
                                }
                        }
}

Notice how the checks are made for overruns and how the weights are updated depending on their current value being greater than or less than the input pattern's node value. After running this program on an input signal processed in 10,000 iterations I got the following results:

The first picture from the left is the state of the weights after their first initialization. The second picture is the input signal and the third picture is the output of the network after processing the signal using 10,000 iterations.


1) Initial State of Network Nodes

2) Input Signal

3) Network output after 10,000 Iterations




Consider the output, notice how the network attempts to transform itself into an image of the input siganl. We don't expect the exact signal as output, just an image of the signal's pattern. We use this technique to generalize on the signal by generating more templates from a continuous signal. First detecting the signal as I explained in the article on detecting a digital signal and then recognizing the signal using some technique like the one shown here in this article. This is really just the training phase, but after the templates are created they can be stored in a database as signatures of the signal. They can be loaded into the weights matrix and compared to an incoming signal the way we did here. The statistical output should give predictable values when a signal is presented to the network that it has already been trained to recognize. Now we have a piece that aquires the signal from a device and detects a statistical change then buffers the signal data of interest. Another piece that can process collected signal data and create templates that can be used for signal recognition.



Home