Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 49

Building a Reusable Prediction Pipeline in Python

Recommended resources

In our last lesson, you designed a flowchart that serves as the blueprint for your entire multimodal pipeline. This gave you a high-level, conceptual map of how data flows through your system. Now, we'll bring that blueprint to life by translating it into a functional, reusable Python script. This directly tackles the crucial task of pipeline integration that you were interested in.

The objective for this lesson is to create a script that encapsulates the full prediction pipeline. This means you will build a single, callable function or class that accepts a new data point—an image, a text complaint, and its associated metadata—and outputs a final prediction on meal quality or violation severity. This script will form the core of your project's inference engine, ready to be used for analysis or even deployed in a real-world application.

From Modular Scripts to an Integrated Pipeline

Throughout this course, you've developed separate components for training: scripts for image processing, text classification, and model fusion. In a production or analysis setting, however, you need a single, orchestrated workflow. Your experience with web frameworks like Django and Flask, where a single request triggers a cascade of logic, provides a good mental model for this. Our goal is to create a similar, self-contained system for prediction.

The video below, which you may recall from our exploration of ML project structure, explains the importance of converting exploratory notebook code into modular, reusable Python scripts, which are then orchestrated by a main pipeline script.

Build & Deploy ML Churn model with FastAPI, MLFlow, Docker, & AWS

This video by Anas Riad provides an excellent rationale for modularizing code and then bringing it together in a single pipeline script.

Please review these two key segments: First, watch the section on turning notebooks into Python scripts. This highlights the benefits of modularity for testing and troubleshooting. Next, watch the demonstration of the run pipeline script. Notice how it imports functions from the modular scripts (load, validate, preprocess) and executes them in a specific order to run the entire training process. We will create a similar script, but for prediction.


Structuring Your Prediction Code

Based on this principle, let's organize your project for inference. A clean structure separates models, source code, and data. This makes your project easier to understand, maintain, and eventually, containerize with Docker.

A sensible structure would be:

midday_meal_monitoring/
├── models/              # Saved model weights
│   ├── image_encoder.pth
│   ├── text_encoder.pth
│   └── fusion_classifier.pth
├── src/
│   ├── predict.py       # Our main pipeline script
│   └── utils.py         # Helper functions (e.g., preprocessing)
└── main.py              # Optional: FastAPI wrapper for deployment

Today, our focus will be on creating the logic within predict.py.


Implementing the Prediction Pipeline in PyTorch

To build our prediction script, we need to perform several key steps: load our trained models, preprocess new data in exactly the same way as we did for training, and then pass the data through the models to get a final prediction.

A great resource to understand the components of a multimodal prediction model is the Lightly AI blog.

The 101 Introduction to Multimodal Deep Learning - Lightly AI

This article explains the core components of a multimodal model and provides a PyTorch code example for an early-fusion architecture, which is very similar to what you have built.

Please read the sections on Encoders and Fusion. Pay close attention to the VisionLanguageEarlyFusion Code Example. This demonstrates how to: Initialize different encoders (ResNet and BERT). Define a fusion and classifier head. Write a forward method that processes an image and text, extracts features, concatenates them, and produces an output. This code provides the perfect template for our prediction script.

Step 1: Loading Models in Evaluation Mode

Before performing inference, it's critical to switch your models from training mode to evaluation mode. This deactivates layers like Dropout and BatchNorm that behave differently during training and inference. The official PyTorch channel has a video that explains this essential step.

Production Inference Deployment with PyTorch

This video explains the necessary steps to prepare a PyTorch model for production inference.

Watch the first segment on evaluation mode. The key takeaway is the use of the .eval() method. We will also be using the concepts from the sections on TorchScript which show how to save and load a complete model.

Your first step in the script will be to define your model classes (the same ResNet, BERT, and MLP architectures you used for training), instantiate them, and then load their saved weights (state_dict). Finally, you'll call .eval() on each of them.

Step 2: Creating a Reusable Predictor Class

To encapsulate the entire pipeline, we'll create a Python class. This is a robust software engineering practice that makes your code clean and reusable. The __init__ method will handle the one-time setup of loading models, which is much more efficient than loading them on every prediction.

Here is a skeleton for your ViolationPredictor class in predict.py. It integrates the concepts from the lightly.ai article and the PyTorch best practices.

import torch
import torch.nn as nn
from torchvision import transforms
from transformers import BertTokenizer
from PIL import Image

# Assume your model architecture classes (e.g., ImageEncoder, TextEncoder, FusionModel)
# are defined here or imported.

class ViolationPredictor:
    def __init__(self, image_encoder_path, text_encoder_path, fusion_model_path, device='cpu'):
        self.device = torch.device(device)

        # 1. Load Models
        self.image_encoder = ImageEncoder() # Instantiate your model
        self.image_encoder.load_state_dict(torch.load(image_encoder_path, map_location=self.device))
        self.image_encoder.to(self.device)
        self.image_encoder.eval() # Set to evaluation mode

        self.text_encoder = TextEncoder()
        self.text_encoder.load_state_dict(torch.load(text_encoder_path, map_location=self.device))
        self.text_encoder.to(self.device)
        self.text_encoder.eval()

        self.fusion_model = FusionModel()
        self.fusion_model.load_state_dict(torch.load(fusion_model_path, map_location=self.device))
        self.fusion_model.to(self.device)
        self.fusion_model.eval()

        # 2. Initialize Preprocessing Tools
        self.tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
        self.image_transform = transforms.Compose([
            transforms.Resize((224, 224)),
            transforms.ToTensor(),
            transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
        ])

    def predict(self, image_path, text, metadata):
        # Use torch.no_grad() for inference to disable gradient calculations
        with torch.no_grad():
            # 1. Preprocess Inputs
            # Image
            image = Image.open(image_path).convert("RGB")
            image_tensor = self.image_transform(image).unsqueeze(0).to(self.device)

            # Text
            inputs = self.tokenizer(text, return_tensors="pt", padding=True, truncation=True, max_length=128)
            input_ids = inputs['input_ids'].to(self.device)
            attention_mask = inputs['attention_mask'].to(self.device)

            # Metadata (example: one-hot encode 'school_type')
            # This should match your training preprocessing
            metadata_tensor = torch.tensor(metadata, dtype=torch.float32).unsqueeze(0).to(self.device)

            # 2. Extract Features
            image_features = self.image_encoder(image_tensor)
            text_features = self.text_encoder(input_ids, attention_mask)

            # 3. Fuse and Classify
            # Concatenate features along the feature dimension (dim=1)
            fused_features = torch.cat([image_features, text_features, metadata_tensor], dim=1)
            
            output = self.fusion_model(fused_features)
            
            # 4. Get final prediction
            probabilities = torch.softmax(output, dim=1)
            predicted_class = torch.argmax(probabilities, dim=1)
            
            return {
                "predicted_class": predicted_class.item(),
                "probabilities": probabilities.cpu().numpy().tolist()[0]
            }

Step 3: Putting it all together

Now, you can use this class in a main block to test it with a sample data point.

if __name__ == '__main__':
    # Define paths to your saved models
    IMAGE_ENCODER_PATH = '../models/image_encoder.pth'
    TEXT_ENCODER_PATH = '../models/text_encoder.pth'
    FUSION_MODEL_PATH = '../models/fusion_classifier.pth'
    
    # Check for GPU availability
    DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
    
    # Instantiate the predictor
    predictor = ViolationPredictor(
        image_encoder_path=IMAGE_ENCODER_PATH,
        text_encoder_path=TEXT_ENCODER_PATH,
        fusion_model_path=FUSION_MODEL_PATH,
        device=DEVICE
    )
    
    # Provide a sample data point
    sample_image_path = 'path/to/your/sample_image.jpg'
    sample_text = "The rice was stale and there were no vegetables provided today."
    # Example metadata: [is_govt_school, is_rural] -> [1.0, 1.0]
    sample_metadata = [1.0, 1.0] 
    
    # Make a prediction
    prediction_result = predictor.predict(sample_image_path, sample_text, sample_metadata)
    
    print(f"Prediction Result: {prediction_result}")

This script is now a self-contained, reusable artifact. Given your experience with AWS and frameworks, you can see how this ViolationPredictor class is the perfect component to be wrapped by a FastAPI endpoint, containerized with Docker, and deployed as a microservice.


Conclusion

In this lesson, you have successfully bridged the gap between your conceptual pipeline design and a concrete implementation. You've created a reusable Python script that operationalizes your entire multimodal model, transforming it from a collection of trained weights into a functional prediction engine.

Here are the key takeaways:

  • Encapsulation is Key: Structuring your prediction logic within a class (ViolationPredictor) makes it efficient (models are loaded once) and easy to reuse.
  • Inference Mode is Non-Negotiable: Always use .eval() on your models and wrap your prediction logic in a with torch.no_grad(): block to ensure correct behavior and optimal performance.
  • Consistency in Preprocessing: The preprocessing steps for new data at inference time must perfectly mirror the steps used during training.
  • From Script to Service: This reusable script is the logical core that can be easily integrated into a larger application, such as a web service API for real-time predictions.

In our next lesson, we will use this script to generate predictions for a batch of data. You'll then take these outputs and create the final analytical artifacts for your project report: the geospatial hotspot maps and the temporal trend plots that tell the story of your findings.