Lesson 6.1.2: Neural networks for spectral classification

Lesson 6.1.2: Neural networks for spectral classification
Dashboard › My courses › Course › Lesson 6.1.2: Neural networks for spectral classification

Lesson 6.1.2: Neural networks for spectral classification

Lesson 6.1.2: Neural networks for spectral classification

⏲ 17 min read 📄 3318 words

Learning Objectives

  • Explain the basic architecture of a feed‑forward neural network and its relevance to spectral classification.
  • Describe how to preprocess astronomical spectra for input into a neural network.
  • Implement a simple neural network in Python using Keras or PyTorch for classifying stellar spectra.
  • Interpret the performance metrics and identify common pitfalls in training neural networks on spectral data.

Key Terms

Feed‑forward neural networkA type of artificial neural network where connections between the nodes do not form cycles, typically used for classification tasks.
Spectral preprocessingThe series of steps applied to raw spectral data, such as normalisation, continuum subtraction, and wavelength alignment, to make it suitable for machine learning.
Activation functionA non‑linear function applied to the output of a neuron, e.g., ReLU, sigmoid, or tanh, enabling the network to learn complex patterns.
OverfittingA modelling error where a neural network learns noise in the training data, resulting in poor generalisation to unseen spectra.
Cross‑entropy lossA loss function commonly used for classification that measures the difference between predicted probabilities and true labels.
flowchart TD
    A[Observed Spectrum] --> B[Pre‑processing (normalisation, noise filtering)]
    B --> C[Feature Extraction (e.g. PCA, line indices)]
    C --> D[Input Layer → Hidden Layer(s)]
    D --> E[Output Layer (class probabilities)]
    E --> F[Spectral Class Assignment]
    F --> G[Evaluation (confusion matrix, accuracy)]

Concept: Foundations of Neural Networks for Spectral Classification

Stellar spectra encode a wealth of physical information: effective temperature, surface gravity, chemical composition, radial velocity and more. Extracting this information efficiently requires a framework that can learn complex, non‑linear mappings from high‑dimensional data to discrete or continuous labels. Artificial neural networks (ANNs) provide exactly this capability. In this section we unpack the theoretical underpinnings of neural networks, explain how they are adapted to spectral data, and illustrate the workflow with a minimal Python example.

Discussion: Neural Networks vs. Classical Methods

How do neural networks compare with traditional template‑matching techniques for spectral classification? List at least two advantages and two potential disadvantages.

1. The perceptron: the building block

A perceptron is a simple computational unit that receives a vector of inputs \( \mathbf{x} = (x_1,\dots,x_n) \), multiplies each by a weight \( w_i \), adds a bias \( b \), and applies an activation function \( \sigma \) to produce an output:

\[ y = \sigma!\left(\sum_{i=1}^{n} w_i x_i + b\right). \]

In spectral classification, each input might be the flux at a particular wavelength pixel. The weight vector encodes how strongly that pixel influences the network’s decision. The bias shifts the activation threshold, allowing the perceptron to be more or less sensitive to the combined input.

Practical: Build a Perceptron in NumPy

Write a short NumPy script that creates a perceptron with 5 input features, random weights and bias, and computes the output for a random input vector. Use a sigmoid activation function.

2. From a single neuron to a multi‑layer network

A single perceptron can only model linear separable problems. To capture the richly structured patterns in spectra—absorption line blends, continuum curvature, instrumental artefacts—ANNs stack many perceptrons into layers. A typical feed‑forward architecture for spectral classification consists of:

  • Input layer: one node per pixel (or per feature extracted from the spectrum).
  • Hidden layers: one or more layers where neurons compute weighted sums and non‑linear activations. The number of hidden units and layers is a hyper‑parameter tuned to balance expressiveness and over‑fitting.
  • Output layer: for classification, a softmax layer producing a probability distribution over spectral classes; for regression, a linear or ReLU layer predicting continuous parameters.

Training adjusts the weights and biases to minimise a loss function. For classification, categorical cross‑entropy is common; for regression, mean squared error (MSE) is typical. Gradient descent, often with a variant such as Adam, propagates error gradients back through the network (back‑propagation), updating parameters iteratively.

Quiz: Back‑Propagation Basics
  1. What is the primary purpose of the loss function in training a neural network?
    • To initialise weights randomly
    • To measure the error between predictions and targets
    • To increase the number of layers
  2. Which optimisation algorithm is mentioned for updating weights in the lesson?
    • RMSProp
    • Adam
    • SGD

3. Handling spectral data: preprocessing and feature engineering

Raw spectra can be irregularly sampled, noisy, and affected by telluric absorption or instrumental response. Before feeding them into a neural network, several preprocessing steps are standard:

  1. Flux normalisation: divide by the continuum to remove large‑scale variations.
  2. Resampling: interpolate onto a common wavelength grid to ensure each input vector has the same dimensionality.
  3. Noise filtering: apply a Savitzky–Golay filter or wavelet denoising to suppress high‑frequency noise that could mislead the network.
  4. Dimensionality reduction: optional use of Principal Component Analysis (PCA) or autoencoders to compress the data while preserving variance critical for classification.

These steps are crucial because neural networks are highly sensitive to the scale and distribution of inputs. Standardising each feature to zero mean and unit variance (z‑score normalisation) is a common practice that stabilises training.

Reflection: Preprocessing Choices

Consider a dataset of low‑resolution spectra with significant missing data. Which preprocessing steps would you prioritise and why? Write a brief reflection (3–5 sentences).

4. Why neural networks excel at spectral classification

Stellar spectra are characterised by subtle, high‑dimensional patterns that are difficult to capture with simple template matching. Neural networks offer several advantages:

  • Learning non‑linear feature interactions: The hierarchical layers allow the network to learn composite features, such as the relative depth of two absorption lines, that are not linearly separable.
  • Robustness to noise: With sufficient training data, a network can learn to disregard random noise while focusing on the underlying spectral signatures.
  • Scalability: Once trained, inference is computationally cheap, enabling real‑time classification of large survey datasets.
  • Transferability: Models trained on one instrument’s spectra can be fine‑tuned for another, leveraging shared physical features across datasets.
Scenario: Transfer Learning for a New Instrument

You have a model trained on SDSS spectra and need to adapt it to data from a new telescope with a different spectral resolution. Outline the steps you would take to fine‑tune the model and evaluate its performance.

5. A minimal implementation example

Below is a short Python snippet using the popular torch library to define a simple feed‑forward neural network for binary spectral classification (e.g. hot vs. cool stars). The code demonstrates the essential components: model definition, loss calculation, and a single training step.

import torch
import torch.nn as nn
import torch.optim as optim

1. Define a simple network

class SpectralNet(nn.Module): def __init__(self, input_dim, hidden_dim): super(SpectralNet, self).__init__() self.fc1 = nn.Linear(input_dim, hidden_dim) # Input -> Hidden self.relu = nn.ReLU() # Non‑linear activation self.fc2 = nn.Linear(hidden_dim, 1) # Hidden -> Output self.sigmoid = nn.Sigmoid() # Binary output def forward(self, x): out = self.fc1(x) out = self.relu(out) out = self.fc2(out) out = self.sigmoid(out) return out

2. Hyper‑parameters

input_dim = 1024 # e.g. 1024 wavelength pixels hidden_dim = 128 learning_rate = 0.001

3. Instantiate model, loss, optimiser

model = SpectralNet(input_dim, hidden_dim) criterion = nn.BCELoss() # Binary cross‑entropy optimizer = optim.Adam(model.parameters(), lr=learning_rate)

4. Dummy training data (batch of 32 spectra)

Assume X is a tensor of shape (32, 1024) and y is (32, 1)

X = torch.randn(32, input_dim) y = torch.randint(0, 2, (32, 1)).float()

5. Forward pass

outputs = model(X) # Shape: (32, 1)

6. Compute loss

loss = criterion(outputs, y)

7. Backward pass and optimisation

optimizer.zero_grad() # Clear gradients from previous step loss.backward() # Compute gradients optimizer.step() # Update parameters print(f'Loss: {loss.item():.4f}')

Line‑by‑line explanation:

  • import torch – imports the PyTorch library.
  • Class SpectralNet inherits from nn.Module and defines the network architecture: a linear layer to the hidden layer, a ReLU activation, another linear layer to the output, and a sigmoid to squash the output to [0,1].
  • In __init__, nn.Linear creates a weight matrix and bias vector for each layer.
  • The forward method specifies how data flows through the network.
  • Hyper‑parameters set the dimensionality of input and hidden layers, plus the learning rate.
  • We instantiate the model, the binary cross‑entropy loss (appropriate for two‑class problems), and the Adam optimiser.
  • For illustration, random tensors X and binary targets y simulate a mini‑batch of spectra.
  • The forward pass returns predictions outputs.
  • The loss is computed by comparing predictions to true labels.
  • Gradients are cleared, back‑propagated, and the optimiser updates the model parameters.
  • The loss value is printed, giving a quick diagnostic of training progress.
Discussion: Choosing Loss Functions

When would you prefer mean squared error over cross‑entropy for a classification task? Provide a scenario and justify your choice.

6. Common pitfalls and best practices

While the example above is deliberately simple, real‑world spectral classification pipelines confront several challenges:

  • Class imbalance: Some spectral types may be under‑represented. Techniques such as class weighting, focal loss, or oversampling can mitigate bias.
  • Over‑fitting: Deep networks with many parameters can memorise noise. Regularisation methods—dropout, L2 weight decay, early stopping—help generalise.
  • Spectral resolution mismatch: Training data from one instrument may differ in resolution from test data. Convolutional layers can learn to be resolution‑invariant, or data can be resampled to a common resolution.
  • Interpretability: Unlike template fitting, neural networks are often seen as black boxes. Techniques such as layer‑wise relevance propagation (LRP) or saliency maps can highlight which spectral regions drive predictions.
  • Hardware constraints: Large input dimensions (high‑resolution spectra) can strain memory. Batch processing and mixed‑precision training (FP16) can alleviate this.
Practical: Implement Dropout in the Example

Add a dropout layer with a rate of 0.5 after the first linear layer in the SpectralNet class. Show the modified code snippet.

7. Outlook: from classification to parameter inference

Once a network reliably assigns a spectral class, the same architecture can be repurposed for regression tasks, such as estimating effective temperature or metallicity. This typically involves replacing the sigmoid output with a linear activation and using MSE loss. Transfer learning—initialising a regression network with the weights of a pre‑trained classifier—often speeds up convergence and improves performance, especially when training data are scarce.

Scenario: Re‑using a Classifier for Regression

Describe how you would adapt a trained classification model to predict stellar effective temperature. Include any changes to the architecture and loss function.

8. Quick example: Convolutional 1‑D classifier

For spectra, local wavelength patterns can be captured with a 1‑D convolutional layer before the dense blocks. The following minimal example demonstrates how to replace the first dense layer with a convolutional feature extractor.


model = models.Sequential(name='spectral_cnn_classifier')

Convolutional feature extractor

model.add(layers.Conv1D(32, kernel_size=5, activation='relu', input_shape=(X_train.shape[1], 1), kernel_initializer='he_normal')) model.add(layers.MaxPooling1D(pool_size=2)) model.add(layers.Dropout(0.3))

Flatten and dense layers

model.add(layers.Flatten()) model.add(layers.Dense(128, activation='relu', kernel_initializer='he_normal')) model.add(layers.Dropout(0.3)) model.add(layers.Dense(num_classes, activation='softmax', kernel_initializer='glorot_uniform'))

Compile and train as before

optimizer = optimizers.Adam(learning_rate=0.001) model.compile(optimizer=optimizer, loss='categorical_crossentropy', metrics=['accuracy']) history = model.fit(X_train[..., np.newaxis], y_train, epochs=200, batch_size=64, validation_data=(X_val[..., np.newaxis], y_val), callbacks=[early_stop, checkpoint], verbose=2)

Note the reshaping of inputs with [..., np.newaxis] to provide a channel dimension required by Conv1D.

Quiz: Convolution vs. Dense
  1. Which layer is best suited for capturing local spectral features?
    • Dense
    • Conv1D
    • Dropout
  2. What does the kernel_initializer='he_normal' do?
    • Sets the learning rate
    • Initializes weights with He normal distribution
    • Adds bias terms

Worked Example: Classifying Real‑World Spectra

In this worked example we walk through every stage of a typical neural‑network workflow for classifying astronomical spectra. The example uses the publicly available Sloan Digital Sky Survey (SDSS) dataset, specifically the spectroscopic catalogue of galaxies and quasars, to illustrate how raw data are transformed into a machine‑learning ready format, how a model is trained and evaluated, and finally how the trained model can be deployed for production inference. The steps are organised into three logical units: data preparation, model development, and deployment. Each unit is described in detail, with code snippets that can be run in a standard Python 3 environment with the usual scientific stack (NumPy, Pandas, Scikit‑Learn, TensorFlow/Keras).

Discussion: Dataset Bias

Identify a potential source of bias in the SDSS dataset used here and suggest a method to mitigate it.

1. Data Acquisition and Pre‑processing

The SDSS spectra are distributed as FITS files, each containing a flux vector, an error vector, and a wavelength array. For this example we have pre‑selected 10 000 spectra: 7 000 galaxies, 2 000 quasars, and 1 000 stars. These files are stored in a directory structure that mirrors the SDSS plate‑IFU naming convention. The first task is to read the FITS files, normalise the flux, and label each spectrum according to its class.

Because the raw flux values span several orders of magnitude, we apply a log‑transform after adding a small offset to avoid taking the logarithm of zero:

import os
import numpy as np
import pandas as pd
from astropy.io import fits

def read_spectrum(file_path):
    with fits.open(file_path) as hdul:
        flux = hdul[1].data['flux']      # 1‑D array of flux values
        error = hdul[1].data['error']    # corresponding errors
        wave = hdul[1].data['wavelength']  # wavelength grid
    return wave, flux, error

def log_normalise(flux, eps=1e-10):
    return np.log10(flux + eps)

def load_dataset(root_dir):
    spectra = []
    labels = []
    for cls in ['galaxy', 'quasar', 'star']:
        cls_dir = os.path.join(root_dir, cls)
        for fname in os.listdir(cls_dir):
            if fname.endswith('.fits'):
                wave, flux, error = read_spectrum(os.path.join(cls_dir, fname))
                spectra.append(log_normalise(flux))
                labels.append(cls)
    return np.array(spectra), np.array(labels)

Line‑by‑line explanation:

  • import os – standard library for file system access.
  • import numpy as np, import pandas as pd – numerical and tabular tools.
  • from astropy.io import fits – FITS file reader.
  • def read_spectrum(file_path) – opens a FITS file, pulls the flux, error, and wavelength arrays from the second HDU (HDU index 1).
  • def log_normalise(flux, eps=1e-10) – applies a base‑10 logarithm after adding a tiny epsilon to avoid log(0).
  • def load_dataset(root_dir) – iterates over the three class subdirectories, reads each spectrum, normalises it, and stores the label.
  • np.array(spectra) – converts the list of arrays into a 2‑D NumPy array where each row is a spectrum.
  • Labels are stored as a NumPy array of strings.

After loading, we perform a train–validation–test split using Scikit‑Learn’s train_test_split, ensuring stratified sampling so that each class is represented proportionally in all subsets.

from sklearn.model_selection import train_test_split

X, y = load_dataset('data/sdss_spectra')
X_train, X_temp, y_train, y_temp = train_test_split(
    X, y, test_size=0.3, stratify=y, random_state=42)
X_val, X_test, y_val, y_test = train_test_split(
    X_temp, y_temp, test_size=0.5, stratify=y_temp, random_state=42)

We then apply a standardisation pipeline that centres each spectrum to zero mean and unit variance. This is essential because the neural network is sensitive to the scale of its inputs.

from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_val_scaled = scaler.transform(X_val)
X_test_scaled = scaler.transform(X_test)

2. Model Development and Evaluation

For spectral classification a one‑dimensional convolutional neural network (1‑D CNN) is well suited, as it can learn local spectral features such as emission or absorption lines. The following architecture consists of two convolutional blocks followed by a dense classifier.

import tensorflow as tf
from tensorflow.keras import layers, models, optimizers, callbacks

def build_cnn(input_shape, num_classes):
    model = models.Sequential()
    # First convolutional block
    model.add(layers.Conv1D(32, kernel_size=5, activation='relu',
                            input_shape=input_shape))
    model.add(layers.BatchNormalization())
    model.add(layers.MaxPooling1D(pool_size=2))
    # Second convolutional block
    model.add(layers.Conv1D(64, kernel_size=5, activation='relu'))
    model.add(layers.BatchNormalization())
    model.add(layers.MaxPooling1D(pool_size=2))
    # Flatten and dense head
    model.add(layers.Flatten())
    model.add(layers.Dense(128, activation='relu'))
    model.add(layers.Dropout(0.5))
    model.add(layers.Dense(num_classes, activation='softmax'))
    return model

Line‑by‑line explanation:

  • model = models.Sequential() – creates a linear stack of layers.
  • Conv1D(32, kernel_size=5, activation='relu', input_shape=input_shape) – 32 filters of width 5, ReLU non‑linearity, expects input shape (n_steps, 1).
  • BatchNormalization() – stabilises training by normalising activations.
  • MaxPooling1D(pool_size=2) – reduces the temporal dimension by a factor of two.
  • Second block doubles the filter count to 64, enabling more complex feature extraction.
  • Flatten() – converts the feature map into a 1‑D vector.
  • Dense(128, activation='relu') – fully connected layer with 128 units.
  • Dropout(0.5) – randomly zeroes 50 % of the units during training to prevent over‑fitting.
  • Dense(num_classes, activation='softmax') – final classifier; softmax ensures the outputs sum to one.

We compile the model with categorical cross‑entropy loss and the Adam optimiser, and we include a learning‑rate scheduler and early stopping callbacks to avoid unnecessary epochs.

num_classes = len(np.unique(y_train))
model = build_cnn((X_train_scaled.shape[1], 1), num_classes)

model.compile(optimizer=optimizers.Adam(learning_rate=1e-3),
              loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])

lr_scheduler = callbacks.ReduceLROnPlateau(
    monitor='val_loss', factor=0.5, patience=3, verbose=1)

early_stop = callbacks.EarlyStopping(
    monitor='val_loss', patience=6, restore_best_weights=True, verbose=1)

history = model.fit(
    X_train_scaled[..., np.newaxis],  # add channel dimension
    y_train,
    validation_data=(X_val_scaled[..., np.newaxis], y_val),
    epochs=50,
    batch_size=64,
    callbacks=[lr_scheduler, early_stop],
    verbose=2)

After training, we evaluate the model on the held‑out test set. The confusion matrix gives a clear picture of class‑wise performance.

from sklearn.metrics import classification_report, confusion_matrix
import matplotlib.pyplot as plt
import seaborn as sns

y_pred = model.predict(X_test_scaled[..., np.newaxis])
y_pred_classes = np.argmax(y_pred, axis=1)

print(classification_report(y_test, y_pred_classes,
                            target_names=['galaxy', 'quasar', 'star']))

cm = confusion_matrix(y_test, y_pred_classes)
plt.figure(figsize=(6,5))
sns.heatmap(cm, annot=True, fmt='d',
            xticklabels=['galaxy', 'quasar', 'star'],
            yticklabels=['galaxy', 'quasar', 'star'],
            cmap='Blues')
plt.xlabel('Predicted class')
plt.ylabel('True class')
plt.title('Confusion matrix')
plt.show()

The report typically shows an accuracy above 95 % for galaxies and quasars, with a slightly lower rate for stars due to their spectral similarity to some late‑type galaxies. The confusion matrix visualises the misclassifications, revealing that most stellar spectra are confused with dwarf galaxies.

Practical: Visualise Confusion Matrix

Using the confusion matrix code above, add a colour bar and ensure the plot is saved as a PNG file named confusion.png.

2.1 Alternative Architecture: Fully‑Connected MLP

As an alternative to convolutional layers, a simple multi‑layer perceptron (MLP) can be used when the dataset is small or when computational resources are limited. The MLP treats each wavelength bin as an independent feature, learning global relationships via dense layers.

def build_mlp(input_shape, num_classes):
    model = models.Sequential()
    model.add(layers.Flatten(input_shape=input_shape))
    model.add(layers.Dense(256, activation='relu'))
    model.add(layers.BatchNormalization())
    model.add(layers.Dropout(0.3))
    model.add(layers.Dense(128, activation='relu'))
    model.add(layers.BatchNormalization())
    model.add(layers.Dropout(0.3))
    model.add(layers.Dense(num_classes, activation='softmax'))
    return model

mlp_model = build_mlp((X_train_scaled.shape[1], 1), num_classes)
mlp_model.compile(optimizer=optimizers.Adam(1e-3),
                  loss='sparse_categorical_crossentropy',
                  metrics=['accuracy'])
mlp_history = mlp_model.fit(
    X_train_scaled[..., np.newaxis], y_train,
    validation_data=(X_val_scaled[..., np.newaxis], y_val),
    epochs=50, batch_size=64,
    callbacks=[lr_scheduler, early_stop], verbose=2)

Although the MLP may be slightly slower to train, it can achieve comparable performance on this dataset, especially when the spectral resolution is modest.

Reflection: MLP vs. CNN

Write a short reflection (2–3 sentences) on when you would choose an MLP over a CNN for spectral classification.

Aspect 1‑D CNN MLP (Fully‑Connected)
Feature extraction Local patterns via convolutional filters Global patterns via dense layers
Parameter count Lower for similar depth (filters vs units) Higher for same input dimensionality
Training time Faster convergence on large spectral sets Longer epochs for similar accuracy
Robustness to missing pixels Handles gaps via pooling and local context Sensitive to missing values unless imputed
Interpretability Filter visualisation highlights line features Weights less directly interpretable per wavelength
Deployment footprint Smaller model size (convolutional reuse) Potentially larger due to dense layers

3. Model Deployment

Once a satisfactory model has been achieved, the next step is to expose it as an inference service. In a research environment we might use a simple Flask API that accepts a raw spectrum, applies the same pre‑processing, and returns the predicted class. For production settings, a TensorFlow SavedModel format can be exported and served via TensorFlow Serving or a cloud inference endpoint.

import joblib
import tensorflow as tf

Persist the scaler

joblib.dump(scaler, 'models/scaler.pkl')

Save the model in TensorFlow SavedModel format

model.save('models/spectral_classifier')

The Flask wrapper demonstrates how the pipeline would be applied to new data:

from flask import Flask, request, jsonify
import numpy as np
import joblib
import tensorflow as tf

app = Flask(__name__)
scaler = joblib.load('models/scaler.pkl')
model = tf.keras.models.load_model('models/spectral_classifier')

@app.route('/predict', methods=['POST'])
def predict():
    # Expect JSON with a key 'flux' containing a list of values
    data = request.get_json()
    flux = np.array(data['flux'])
    flux_log = np.log10(flux + 1e-10)
    flux_scaled = scaler.transform([flux_log])
    pred = model.predict(flux_scaled[..., np.newaxis])
    class_idx = np.argmax(pred, axis=1)[0]
    classes = ['galaxy', 'quasar', 'star']
    return jsonify({'prediction': classes[class_idx]})

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)
Discussion: Deployment Choices

What are the trade‑offs between deploying a model locally with Flask versus using a managed cloud inference service?

4. Performance Monitoring and Re‑training

Deploying a model is not a one‑off event. Spectral quality can change over time due to instrument degradation, and new classes (e.g. rare exotic objects) may appear in the data stream. Hence, continuous evaluation is essential. A simple strategy is to log incoming spectra and their predictions, then periodically re‑run the training pipeline on a curated subset that includes any misclassified examples. This incremental learning approach keeps the model robust without retraining from scratch.

In addition, metrics such as the Receiver Operating Characteristic (ROC) curve for each class can be plotted over successive deployments to detect drift. If the area under the curve (AUC) falls below a threshold, a full retraining cycle is triggered.

By following this end‑to‑end workflow—starting from raw FITS files, through rigorous pre‑processing, a carefully designed 1‑D CNN, thorough evaluation

Comments are closed.

{"email":"Email address invalid","url":"Website address invalid","required":"Required field missing"}
FREEDOM TO LEARN • 10% OFF All Courses & Workshops • Use Code: NANOINDIA10 • ⏳ Offer Ends In: Loading... • Learn Today. Lead Tomorrow. • Explore Programs →
FREEDOM TO LEARN • 10% OFF All Courses & Workshops • Use Code: NANOINDIA10 • ⏳ Offer Ends In: Loading... • Learn Today. Lead Tomorrow. • Explore Programs →
Support