import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import Ridge,LinearRegression
from scipy import linalg,stats # for eigenvalue and distribution for sparse connectivity
import sys
sys.path.append("./../")
from utils.timer import *
np.random.seed(42)
import musicalbeeps
from midiutil import MIDIFile
import sys
sys.path.append("../")
from utils.periodicList import periodicList
player = musicalbeeps.Player(volume = 0.3,
mute_output = True)
# Music theoretically wrong but this is what he uses
# notes = ["g#","a","a#","b","b#","c5","c5#","d5","d5#","e5","e5#","f5","f5#","g5","g5#","a5"]
notes = ["g#","a","bb","b","c5","c5#","d5","e5b","e5","f5","f5#","g5","g5#","a5"]
notes_dict = {i:j for i,j in zip(range(-1,len(notes)-1),notes)}
house = 3*[0] + [2] + 2*[3] + [7] + 2*[5] + [0] + 2*[3] + 4*[12] + [10] + [7] + [5] + 5*[7] + 4*[12] \
+ 2*[10] + [7] + 2*[5] + [0] + 2*[3] + 4*[0] + 3*[-1] + 5*[0]
degrees = [57 + i for i in house] # MIDI note number
track = 0
channel = 0
time_ = 0 # In beats
duration = 0.5 # In beats
tempo = 220 # In BPM
volume = 100 # 0-127, as per the MIDI standard
MyMIDI = MIDIFile(1) # One track, defaults to format 1 (tempo track is created automatically)
MyMIDI.addTempo(track=track,time=time_,tempo=tempo)
for i, pitch in enumerate(degrees):
MyMIDI.addNote(track, channel, pitch, time_ + i, duration, volume)
with open("house.mid", "wb") as output_file:
MyMIDI.writeFile(output_file)
# for i in house:
# player.play_note(notes_dict[i], 0.1)
data = periodicList(house)
plt.figure(figsize=(20,4),dpi=200)
plt.plot(data[:len(house)],"o")
plt.vlines(range(len(house)),-1.5,data[:len(house)],linewidth=0.7)
plt.title("House of the Rising Sun vocals");
plt.xticks(range(len(house)))
plt.grid(alpha=0.8)
plt.yticks(range(-1,len(notes)-1),[f"({notes_dict[i]}) {i}".upper() for i in range(-1,len(notes)-1)]);
plt.ylim(-1.5,12.5)
plt.xlim(-0.3,47.3);
The fact that spectral radius is close to 1 means that the network exhibits a long-lasting response to a unit impulse input. Generally, the closer spectral radius is to unity, the slower is the decay of the network’s response to an impulse input. A relatively long- lasting “echoing” of inputs in the internal network dynamics is a requisite for a sizable short-term memory performance of the network. A substantial short-term memory is required for our present learning task, because the target signal contains a subsequence of 8 consecutive 0’s (namely, the last 5 notes concatenated with the first 3 notes of the subsequent instance of the melody). This implies that the network must realize a memory span of at least 9 update steps in order to correctly produce the first non-0 output after this sequence (from Chapter 4.1.2).
resSize = 400
inSize = outSize = 1
#Creating reservoir-reservoir connections. No input-reservoir connection, since we rely only on outputs (output feedback).
W = 0.1 * stats.rv_discrete(name='sparse', values=([0,4,-4], [0.9875, 0.00625, 0.00625])).rvs(size=(resSize,resSize),random_state=42)
print('Computing spectral radius...')
rhoW = abs(linalg.eig(W)[0]).max()
print(f'Done: {rhoW}')
We do not use any inputs. Since at time n the output y(n) depends on the previous outputs, output feedback is required in this task. The output feedback weights were sampled randomly from the uniform distribution in [−2, 2].
Wback = np.random.uniform(-2,2,size=(resSize,inSize))
trainLen = 1500
initLen = 500 #We initialize the reservoir states
sigmoid = lambda k: 1 / (1 + np.exp(-k))
#Array shapes: (column vector, time)
y_train = np.array(data[:trainLen])/(data.max()*2)
X = np.zeros((resSize,trainLen-initLen)) #to store the echo states
x = np.random.rand(resSize,1) #reservoir states random init
for t in range(1,trainLen):
x = sigmoid(np.dot( W, x ) + Wback*y_train[t-1]) #output feedback
if t >= initLen:
X[:,t-initLen] = x.ravel()
y_train = np.array(data[initLen:trainLen])/(data.max()*2)
regr = LinearRegression(fit_intercept=False)
with timer("Linear Regression"):
regr.fit(X.T,np.arctanh(y_train.T))
Wout = regr.coef_
print(f"MSE train: {np.square(np.dot(Wout,X)-np.arctanh(y_train)).mean()}")
plt.figure(figsize=(30,2),dpi=200)
incr = 1
plt.plot(range(0,trainLen-initLen,incr),y_train[::incr],'blue',label='Training Data')
plt.plot(range(0,trainLen-initLen,incr),np.tanh(np.dot(Wout,X)).ravel()[::incr],'r.',alpha = 0.8,label='Fit');
plt.legend(fontsize=15,loc="upper right");
plt.xlim(0,trainLen-initLen);
In order to check whether the network has indeed learnt its task, it is tested whether it could stably continue to generate the desired ouputs after a starting period with teacher forcing. The network is started from the null state x(0) = 0, and the correct melody is written into the output units for 500 initial steps. Then the network is left running freely for further 100 steps.
testInitLen = 500
testLen = testInitLen + 100
sigmoid = lambda k: 1 / (1 + np.exp(-k))
#Array shapes: (column vector, time)
y_test = np.array(data[trainLen:trainLen+testLen])/(data.max()*2)
X = np.zeros((resSize,testLen-testInitLen)) #to store the echo states
x = np.zeros((resSize,1))
for t in range(1,testInitLen):
x = sigmoid(np.dot( W, x ) + Wback*y_test[t-1]) #output feedback
for t in range(testInitLen,testLen):
x = sigmoid(np.dot( W, x ) + Wback*np.tanh(np.dot(Wout,x))) #autonomous
X[:,t-testInitLen] = x.ravel()
plt.figure(figsize=(10,2),dpi=200)
incr = 1
plt.plot(range(0,testLen-testInitLen,incr),y_test[testInitLen:][::incr],'blue',label='Ground Truth')
plt.plot(range(0,testLen-testInitLen,incr),np.tanh(np.dot(Wout,X)).ravel()[::incr],'r.',alpha = 0.8,label='Prediction');
plt.legend(fontsize=10,loc="upper left")
plt.xlim(0,testLen-testInitLen)
plt.title("Autonomous ESN last 100 steps");
plt.figure(figsize=(20,2),dpi=200)
randints = np.random.randint(resSize,size=3)
randints.sort()
plt.plot(X[randints,:].T)
plt.legend(labels=[f"State {i+1}" for i in randints]);
$x(n + 1) = \sigma(\textbf Wx(n) + \textbf W^{\text {back}}(y(n) + \nu(n))), \nu$: uniform noise
trainLen = 1500
initLen = 500 #We initialize the reservoir states
sigmoid = lambda k: 1 / (1 + np.exp(-k))
#Array shapes: (column vector, time)
y_train = np.array(data[:trainLen])/(data.max()*2)
X = np.zeros((resSize,trainLen-initLen)) #to store the echo states
x = np.random.rand(resSize,1) #reservoir states random init
for t in range(1,trainLen):
x = sigmoid(np.dot( W, x ) + Wback*(y_train[t-1]+np.random.uniform(-1,1)/1000)) #output feedback
if t >= initLen:
X[:,t-initLen] = x.ravel()
y_train = np.array(data[initLen:trainLen])/(data.max()*2)
regr = LinearRegression(fit_intercept=False)
with timer("Linear Regression"):
regr.fit(X.T,np.arctanh(y_train.T))
Wout = regr.coef_
print(f"MSE train: {np.square(np.dot(Wout,X)-np.arctanh(y_train)).mean()}")
plt.figure(figsize=(30,2),dpi=200)
incr = 1
plt.plot(range(0,trainLen-initLen,incr),y_train[::incr],'blue',label='Training Data')
plt.plot(range(0,trainLen-initLen,incr),np.tanh(np.dot(Wout,X)).ravel()[::incr],'r.',alpha = 0.8,label='Fit');
plt.legend(fontsize=15,loc="upper right");
plt.xlim(0,trainLen-initLen);
testInitLen = 500
testLen = testInitLen + 100
sigmoid = lambda k: 1 / (1 + np.exp(-k))
#Array shapes: (column vector, time)
y_test = np.array(data[trainLen:trainLen+testLen])/(data.max()*2)
X = np.zeros((resSize,testLen-testInitLen)) #to store the echo states
x = np.zeros((resSize,1))
for t in range(1,testInitLen):
x = sigmoid(np.dot( W, x ) + Wback*(y_test[t-1]+np.random.uniform(-1,1)/1000)) #output feedback
for t in range(testInitLen,testLen):
x = sigmoid(np.dot( W, x ) + Wback*np.tanh(np.dot(Wout,x))) #autonomous
X[:,t-testInitLen] = x.ravel()
plt.figure(figsize=(10,2),dpi=200)
incr = 1
plt.plot(range(0,testLen-testInitLen,incr),y_test[testInitLen:][::incr],'blue',label='Ground Truth')
plt.plot(range(0,testLen-testInitLen,incr),np.tanh(np.dot(Wout,X)).ravel()[::incr],'r.',alpha = 0.8,label='Prediction');
plt.legend(fontsize=10,loc="upper left");
plt.xlim(0,testLen-testInitLen);
plt.title("Autonomous ESN last 100 steps");
plt.figure(figsize=(20,2),dpi=200)
randints = np.random.randint(resSize,size=3)
randints.sort()
plt.plot(X[randints,:].T)
plt.legend(labels=[f"State {i+1}" for i in randints]);
Now we compare the wobbling ESN to RNN
import tensorflow as tf
from tensorflow.keras import Model, Sequential
from tensorflow.keras.layers import Dense, Dropout, BatchNormalization, Input, Flatten, LSTM
from tensorflow.keras.optimizers import Adam
from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint, ReduceLROnPlateau, Callback, TensorBoard
model = Sequential()
model.add(LSTM(20, return_sequences=True, input_shape=(1,1)))
model.add(BatchNormalization())
model.add(LSTM(20, return_sequences=False))
model.add(BatchNormalization())
model.add(Dense(10,'relu'))
model.add(BatchNormalization())
model.add(Dense(outSize))
model.compile(optimizer=Adam(lr=0.001), loss='mse')
model.summary()
class CustomLogger(Callback):
def __init__(self, epochs_tot):
super().__init__()
self.epochs=epochs_tot
self.threshold = 1000
def on_epoch_begin(self, epoch, logs=None):
self.time_begin = time.time()
def on_epoch_end(self,epoch,logs=None):
duration = time.time() - self.time_begin
if (epoch+1) % self.threshold == 0 or epoch==0:
print(f"The average {self.model.loss} in epoch {epoch+1} is {logs['loss']:1.8f}. Duration of epoch {epoch+1}: {duration} seconds")
epochs = 10000
batch_size = 301
reduceLR = ReduceLROnPlateau(monitor='loss', factor=0.5, patience=epochs//10, min_lr=1e-7, verbose=1)
earlyStopping = EarlyStopping(monitor='loss', patience=epochs//2)
logger = CustomLogger(epochs)
with timer("RNN Training time"):
history = model.fit(y_train[:,None,None],y_train[:,None,None],batch_size=batch_size,epochs=epochs, verbose=0, callbacks=[earlyStopping,logger])
plt.figure(figsize=(30,2),dpi=200)
incr = 1
plt.plot(range(0,len(y_train),incr),y_train[::incr],'blue',label='Ground Truth')
plt.plot(range(0,len(y_train),incr),history.model.predict(y_train[:,None,None])[::incr],'r.',alpha = 0.8,label='Prediction');
plt.legend(fontsize=15,loc="lower left");
plt.xlim(0,len(y_train));
RNN_val = []; RNN_se_val = []
with timer("RNN Forecast time"):
pred = history.model.predict(y_test[testInitLen,None,None,None])
RNN_val.append(*pred.ravel())
RNN_se_val.append((RNN_val[-1]-y_test[testInitLen])**2)
for i in range(1,testLen-testInitLen):
pred = history.model.predict(np.expand_dims(pred,2))
RNN_val.append(*pred.ravel())
RNN_se_val.append((RNN_val[-1]-y_test[testInitLen:][i])**2)
RNN_mse = np.mean(RNN_se_val)
print('MSE validation: ', RNN_mse)
plt.figure(figsize=(30,4),dpi=200)
incr = 1
plt.plot(range(0,testLen-testInitLen,incr),y_test[testInitLen:][::incr],'blue',label='Ground Truth')
plt.plot(range(0,testLen-testInitLen,incr),RNN_val[::incr],'r.',alpha = 0.8,label='Prediction');
plt.legend(fontsize=15,loc ="lower left");
plt.xlim(0,testLen-testInitLen);