The Time-Delay Neural Network (TDNN) is a model that is able to perform sequence recognition. It maps the buffered sequence into a spatial pattern on the input layer of a Multilayer Perceptron (MLP). In essence, it is a convolutional neural network. Despite the natural association of a sequence with time, any other dynamic variable can be used, such as the DNA, a grammatical structure, etc.
There are two main drawbacks to this general approach to sequence recognition:
In the case of time-dependent signals, the restrictions defined by the sampling theorem apply to them. The signals must be adequately sampled, otherwise, a poor representation (including distortions) may have a negative impact on the learning capacity of the TDNN. However, there exists a way to compensate for this problem and make the system more robust. The constant delays (determined by the sampling period) can be replaced by spreading filters $g(\cdot)$ that broaden the signal in time as well as delaying it. This filter is defined as follows:
$$S = \sum_{i=0}^{N} \left ( \frac{i}{d+1} \right ) ^ \alpha \exp \left ( \alpha \frac{1 - i}{d + 1} \right )$$where $N$ is the size of the delay line (buffer), $\alpha$ is the spreading factor, and $d$ is the shift delay number. Note that $S$ is a normalisation factor that ensures that all shifts may deliver the sample amount of energy. Then, for each shift:
$$g(x[n-d]) = g(d) = \frac{1}{S} \sum_{i=0}^{N} x[i] \left ( \frac{i}{d+1} \right ) ^ \alpha \exp \left ( \alpha \frac{1 - i}{d + 1} \right )$$The application of the spreading filter can be seen as a fixed weight matrix on the signal buffer. The spreading factor $\alpha$ defines the sharpness of the filter (positive relation). What follows is a graphical representation of each filter shape.
In [9]:
import NeuralNetwork
%matplotlib inline
import matplotlib.pyplot as plt
# 10 is alpha filter spreading factor, 8 sequence length (input neurons), 3 hidden neurons, 2 output neurons
tdnn = NeuralNetwork.TDNN(20, [8,3,2])
plt.figure()
colors = ['sienna', 'red', 'orange', 'gold', 'chartreuse', 'turquoise', 'blue', 'purple']
g = tdnn[0]
plt.subplot(1,2,1)
for i in xrange(g.shape[0]):
plt.plot(g[i], colors[i], label='G(' + str(i) + ')', linewidth=2)
ax = plt.gca()
ax.set_ylim([0,1.0])
plt.legend(loc='upper right')
plt.xlabel('Sequence [n]')
plt.ylabel('Gain')
plt.title('G(n) with alpha=20')
# broader filter, alpha = 4
tdnn = NeuralNetwork.TDNN(4, [8,3,2])
colors = ['sienna', 'red', 'orange', 'gold', 'chartreuse', 'turquoise', 'blue', 'purple']
g = tdnn[0]
plt.subplot(1,2,2)
for i in xrange(g.shape[0]):
plt.plot(g[i], colors[i], label='G(' + str(i) + ')', linewidth=2)
ax = plt.gca()
ax.set_ylim([0,1.0])
plt.legend(loc='upper right')
plt.xlabel('Sequence [n]')
plt.title('G(n) with alpha=4')
plt.show()
In order to prove the effectiveness of this approach, the following experiment is going to sample a signal of 1Hz (class 1) and another one of 4Hz (class 2) at 10Hz, and then it will see how the TDNN works with or without the spreading filter, and with a sampling frequency jitter of 2Hz (so, the sampling rate can be any value from 8Hz to 12Hz). Note that the 4Hz signal can match Nyquist's frequency if the sampling frequency is 8Hz.
In [10]:
import numpy as np
# No sampling jitter, no spreading filter
nsamp = 50
train_dat = []
train_lab = []
test_dat = []
test_lab = []
for i in xrange(nsamp):
train_dat.append(np.sin(2*np.pi*1.0/10.0 * np.array(range(8)) + np.random.rand()*2*np.pi))
train_lab.append([0.79, 0.21])
train_dat.append(np.sin(2*np.pi*4.0/10.0 * np.array(range(8)) + np.random.rand()*2*np.pi))
train_lab.append([0.21, 0.79])
train_dat = np.array(train_dat)
train_lab = np.array(train_lab)
mlp = NeuralNetwork.MLP([8,4,2])
NeuralNetwork.MLP_Backprop(mlp, train_dat, train_lab, 0.1, 20, 0.1)
print("Test")
accuracy = 0
for i in xrange(100):
res = NeuralNetwork.MLP_Predict(mlp, np.sin(2*np.pi*1.0/10.0 * np.array(range(8)) + np.random.rand()*2*np.pi))[2]
if res[0] > res[1]:
accuracy = accuracy + 1
res = NeuralNetwork.MLP_Predict(mlp, np.sin(2*np.pi*4.0/10.0 * np.array(range(8)) + np.random.rand()*2*np.pi))[2]
if res[1] > res[0]:
accuracy = accuracy + 1
print("Accuracy = " + str(float(accuracy)/2) + "%")
In [11]:
# Sampling jitter, no spreading filter
train_dat = []
train_lab = []
test_dat = []
test_lab = []
for i in xrange(nsamp):
train_dat.append(np.sin(2*np.pi*1.0/(8.0 + np.random.rand()*4) * np.array(range(8)) + np.random.rand()*2*np.pi))
train_lab.append([0.79, 0.21])
train_dat.append(np.sin(2*np.pi*4.0/(8.0 + np.random.rand()*4) * np.array(range(8)) + np.random.rand()*2*np.pi))
train_lab.append([0.21, 0.79])
train_dat = np.array(train_dat)
train_lab = np.array(train_lab)
mlp = NeuralNetwork.MLP([8,4,2])
NeuralNetwork.MLP_Backprop(mlp, train_dat, train_lab, 0.1, 20, 0.1)
print("Test")
accuracy = 0
for i in xrange(100):
res = NeuralNetwork.MLP_Predict(mlp, np.sin(2*np.pi*1.0/(8.0 + np.random.rand()*4) * np.array(range(8)) + np.random.rand()*2*np.pi))[2]
if res[0] > res[1]:
accuracy = accuracy + 1
res = NeuralNetwork.MLP_Predict(mlp, np.sin(2*np.pi*4.0/(8.0 + np.random.rand()*4) * np.array(range(8)) + np.random.rand()*2*np.pi))[2]
if res[1] > res[0]:
accuracy = accuracy + 1
print("Accuracy = " + str(float(accuracy)/2) + "%")
In [12]:
# Sampling jitter, spreading filter
train_dat = []
train_lab = []
test_dat = []
test_lab = []
for i in xrange(nsamp):
train_dat.append(np.sin(2*np.pi*1.0/(8.0 + np.random.rand()*4) * np.array(range(8)) + np.random.rand()*2*np.pi))
train_lab.append([0.79, 0.21])
train_dat.append(np.sin(2*np.pi*4.0/(8.0 + np.random.rand()*4) * np.array(range(8)) + np.random.rand()*2*np.pi))
train_lab.append([0.21, 0.79])
train_dat = np.array(train_dat)
train_lab = np.array(train_lab)
tdnn = NeuralNetwork.TDNN(4, [8,4,2])
NeuralNetwork.TDNN_Backprop(tdnn, train_dat, train_lab, 0.1, 20, 0.1)
print("Test")
accuracy = 0
for i in xrange(100):
res = NeuralNetwork.TDNN_Predict(tdnn, np.sin(2*np.pi*1.0/(8.0 + np.random.rand()*4) * np.array(range(8)) + np.random.rand()*2*np.pi))[2]
if res[0] > res[1]:
accuracy = accuracy + 1
res = NeuralNetwork.TDNN_Predict(tdnn, np.sin(2*np.pi*4.0/(8.0 + np.random.rand()*4) * np.array(range(8)) + np.random.rand()*2*np.pi))[2]
if res[1] > res[0]:
accuracy = accuracy + 1
print("Accuracy = " + str(float(accuracy)/2) + "%")
For a given set of conditions, the TDNN with the spreading filter proves to be robust and resilient to signal issues.