Universal Approximation Theorem in Action#
The code below use a single layer ANN to approximate a sinewave.
Statement of Proof for Sinewave#
Let \( C(X,\mathbb{R})\) denote the set of continuous functions from a subset \(X\) of a Euclidean \(\mathbb{R} \)space to a Euclidean space \(\mathbb{R}^.\) Let \(\sigma\) be any continuous sigmoidal function. Then the finite sums of the form:
\[G(\vec{x})=\Sigma_{j=1}^N \alpha_j\sigma(w_j\cdot x+b_j),\]
are dense in \(C([0,1]^n)\). In other words, given any \(f\in C([0,1]^n)\) and \(\epsilon >0\), there is a sum \(G(\vec{x})\) of the above form, for which:
\[|G(x)-sin(x) |<\epsilon,\]
for all \(x\).
import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
import matplotlib.pyplot as plt
# Generate synthetic data (sine wave)
x = np.linspace(0, 2 * np.pi, 100)  # Input values
y = np.sin(x)  # Target sine wave
# Convert data to PyTorch tensors
x_tensor = torch.FloatTensor(x).unsqueeze(1)  # Reshape to (100, 1)
y_tensor = torch.FloatTensor(y).unsqueeze(1)
    
# Define a simple neural network
class SimpleNN(nn.Module):
    def __init__(self,n_hidden=3):
        super(SimpleNN, self).__init__()
        self.fc1 = nn.Linear(1, n_hidden)  # Input: 1, Hidden: ?
        self.fc2 = nn.Linear(n_hidden, 1)  # Hidden: ?, Output: 1
    def forward(self, x):
        x = torch.sigmoid(self.fc1(x))
        x = self.fc2(x)
        return x
# Instantiate the model, loss function, and optimizer
model = SimpleNN(3)
criterion = nn.MSELoss()
optimizer = optim.Adam(model.parameters(), lr=0.01)
model
SimpleNN(
  (fc1): Linear(in_features=1, out_features=3, bias=True)
  (fc2): Linear(in_features=3, out_features=1, bias=True)
)
Three Node Network#
EPOCH=[]
EPOCH_LOSS=[]
# Training the model
num_epochs = 2000
for epoch in range(num_epochs):
    # Forward pass
    outputs = model(x_tensor)
    
    # Compute loss
    loss = criterion(outputs, y_tensor)
    
    # Backpropagation and optimization
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    
    if (epoch + 1) % 100 == 0:
        print(f'Epoch [{epoch + 1}/{num_epochs}], Loss: {loss.item():.4f}')
        EPOCH.append(epoch)
        EPOCH_LOSS.append(loss.item())
# Plot the estimated sine wave
model.eval()  # Switch to evaluation mode
with torch.no_grad():
    predicted = model(x_tensor).numpy()
plt.figure(figsize=(8, 6))
plt.plot(x, y, label='True Sine Wave')
plt.plot(x, predicted, label='Estimated Sine Wave', linestyle='--')
plt.legend()
plt.xlabel('x')
plt.ylabel('y')
plt.title('Sine Wave Estimation')
plt.show()
Epoch [100/2000], Loss: 0.2096
Epoch [200/2000], Loss: 0.1869
Epoch [300/2000], Loss: 0.1668
Epoch [400/2000], Loss: 0.1017
Epoch [500/2000], Loss: 0.0537
Epoch [600/2000], Loss: 0.0433
Epoch [700/2000], Loss: 0.0412
Epoch [800/2000], Loss: 0.0403
Epoch [900/2000], Loss: 0.0396
Epoch [1000/2000], Loss: 0.0391
Epoch [1100/2000], Loss: 0.0386
Epoch [1200/2000], Loss: 0.0381
Epoch [1300/2000], Loss: 0.0376
Epoch [1400/2000], Loss: 0.0373
Epoch [1500/2000], Loss: 0.0370
Epoch [1600/2000], Loss: 0.0368
Epoch [1700/2000], Loss: 0.0366
Epoch [1800/2000], Loss: 0.0364
Epoch [1900/2000], Loss: 0.0361
Epoch [2000/2000], Loss: 0.0357
plt.figure(figsize=(4, 4))
plt.plot(EPOCH,EPOCH_LOSS,':og')
plt.ylim((0,0.25))
plt.xlabel('EPOCH')
plt.ylabel('LOSS')
plt.show()
LOSS=[]
for n in range(1,9,1):
    model = SimpleNN(n)
    criterion = nn.MSELoss()
    optimizer = optim.Adam(model.parameters(), lr=0.01)
    # Training the model
    num_epochs = 2000
    for epoch in range(num_epochs):
        # Forward pass
        outputs = model(x_tensor)
        # Compute loss
        loss = criterion(outputs, y_tensor)
        # Backpropagation and optimization
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        if (epoch + 1) % 1000 == 0:
            print(f'Node [{n}], Epoch [{epoch + 1}/{num_epochs}], Loss: {loss.item():.4f}')
    LOSS.append(loss.item())
        # Plot the estimated sine wave
    model.eval()  # Switch to evaluation mode
    with torch.no_grad():
        predicted = model(x_tensor).numpy()
    plt.figure(figsize=(4, 4))
    plt.plot(x, y, label='True Sine Wave')
    plt.plot(x, predicted, label='Estimated Sine Wave', linestyle='--')
    plt.legend()
    plt.xlabel('x')
    plt.ylabel('y')
    plt.title('Sine Wave Estimation with '+str(n)+' layers')
    plt.show()
Node [1], Epoch [1000/2000], Loss: 0.0904
Node [1], Epoch [2000/2000], Loss: 0.0721
Node [2], Epoch [1000/2000], Loss: 0.0790
Node [2], Epoch [2000/2000], Loss: 0.0388
Node [3], Epoch [1000/2000], Loss: 0.0075
Node [3], Epoch [2000/2000], Loss: 0.0003
Node [4], Epoch [1000/2000], Loss: 0.0113
Node [4], Epoch [2000/2000], Loss: 0.0002
Node [5], Epoch [1000/2000], Loss: 0.0008
Node [5], Epoch [2000/2000], Loss: 0.0001
Node [6], Epoch [1000/2000], Loss: 0.0007
Node [6], Epoch [2000/2000], Loss: 0.0000
Node [7], Epoch [1000/2000], Loss: 0.0350
Node [7], Epoch [2000/2000], Loss: 0.0005
Node [8], Epoch [1000/2000], Loss: 0.0003
Node [8], Epoch [2000/2000], Loss: 0.0000
plt.figure(figsize=(4, 4))
plt.plot(np.arange(1,9),LOSS,':o')
plt.xlabel('NODES')
plt.ylabel('LOSS')
plt.show()
Ten Node Network#
EPOCH10=[]
EPOCH10_LOSS=[]
model = SimpleNN(10)
criterion = nn.MSELoss()
optimizer = optim.Adam(model.parameters(), lr=0.01)
# Training the model
num_epochs = 2000
for epoch in range(num_epochs):
    # Forward pass
    outputs = model(x_tensor)
    
    # Compute loss
    loss = criterion(outputs, y_tensor)
    
    # Backpropagation and optimization
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    
    if (epoch + 1) % 100 == 0:
        print(f'Epoch [{epoch + 1}/{num_epochs}], Loss: {loss.item():.4f}')
        EPOCH10.append(epoch)
        EPOCH10_LOSS.append(loss.item())
# Plot the estimated sine wave
model.eval()  # Switch to evaluation mode
with torch.no_grad():
    predicted = model(x_tensor).numpy()
plt.figure(figsize=(8, 6))
plt.plot(x, y, label='True Sine Wave')
plt.plot(x, predicted, label='Estimated Sine Wave', linestyle='--')
plt.legend()
plt.xlabel('x')
plt.ylabel('y')
plt.title('Sine Wave Estimation')
plt.show()
Epoch [100/2000], Loss: 0.1995
Epoch [200/2000], Loss: 0.1840
Epoch [300/2000], Loss: 0.1598
Epoch [400/2000], Loss: 0.0919
Epoch [500/2000], Loss: 0.0423
Epoch [600/2000], Loss: 0.0334
Epoch [700/2000], Loss: 0.0267
Epoch [800/2000], Loss: 0.0191
Epoch [900/2000], Loss: 0.0121
Epoch [1000/2000], Loss: 0.0070
Epoch [1100/2000], Loss: 0.0040
Epoch [1200/2000], Loss: 0.0024
Epoch [1300/2000], Loss: 0.0015
Epoch [1400/2000], Loss: 0.0009
Epoch [1500/2000], Loss: 0.0005
Epoch [1600/2000], Loss: 0.0003
Epoch [1700/2000], Loss: 0.0002
Epoch [1800/2000], Loss: 0.0001
Epoch [1900/2000], Loss: 0.0001
Epoch [2000/2000], Loss: 0.0000
plt.figure(figsize=(4, 4))
plt.plot(EPOCH10,EPOCH10_LOSS,':o',label='10 nodes')
plt.plot(EPOCH,EPOCH_LOSS,':og',label='5 nodes')
plt.ylim((0,0.25))
plt.xlabel('EPOCH')
plt.ylabel('LOSS')
plt.legend()
plt.show()