ok now as per the Problem Statement
Feature Extraction from Images
In this hackathon, the goal is to create a machine learning model that extracts entity values from images. This capability is crucial in fields like healthcare, e-commerce, and content moderation, where precise product information is vital. As digital marketplaces expand, many products lack detailed textual descriptions, making it essential to obtain key details directly from images. These images provide important information such as weight, volume, voltage, wattage, dimensions, and many more, which are critical for digital stores.
Data Description:
The dataset consists of the following columns:
index: A unique identifier (ID) for the data sample.
image_link: Public URL where the product image is available for download. Example link - https://m.media-amazon.com/images/I/71XfHPR36-L.jpg  To download images, use the download_images function from src/utils.py. See sample code in src/test.ipynb.
group_id: Category code of the product.
entity_name: Product entity name. For example, “item_weight”.
entity_value: Product entity value. For example, “34 gram”.
Note: For test.csv, you will not see the column entity_value as it is the target variable.
Output Format:
The output file should be a CSV with 2 columns:
index: The unique identifier (ID) of the data sample. Note that the index should match the test record index.
prediction: A string which should have the following format: “x unit” where x is a float number in standard formatting and unit is one of the allowed units (allowed units are mentioned in the Appendix). The two values should be concatenated and have a space between them.
For example: “2 gram”, “12.5 centimetre”, “2.56 ounce” are valid.
Invalid cases: “2 gms”, “60 ounce/1.7 kilogram”, “2.2e2 kilogram”, etc.
Note: Make sure to output a prediction for all indices. If no value is found in the image for any test sample, return an empty string, i.e., “”. If you have less/more number of output samples in the output file as compared to test.csv, your output won’t be evaluated.
File Descriptions:
Source Files:
src/sanity.py: Sanity checker to ensure that the final output file passes all formatting checks.
Note: The script will not check if fewer/more number of predictions are present compared to the test file. See sample code in src/test.ipynb.
src/utils.py: Contains helper functions for downloading images from the image_link.
src/constants.py: Contains the allowed units for each entity type.
sample_code.py: A sample dummy code that can generate an output file in the given format. Usage of this file is optional.
Dataset Files:
dataset/train.csv: Training file with labels (entity_value).
dataset/test.csv: Test file without output labels (entity_value). Generate predictions using your model/solution on this file's data and format the output file to match sample_test_out.csv (Refer to the "Output Format" section above).
dataset/sample_test.csv: Sample test input file.
dataset/sample_test_out.csv: Sample outputs for sample_test.csv. The output for test.csv must be formatted in the exact same way.
Note: The predictions in the file might not be correct.
Constraints:
You will be provided with a sample output file and a sanity checker file. Format your output to match the sample output file exactly and pass it through the sanity checker to ensure its validity.
Note: If the file does not pass through the sanity checker, it will not be evaluated. You should receive a message like Parsing successful for file: ...csv if the output file is correctly formatted.
You are given the list of allowed units in constants.py and also in the Appendix. Your outputs must be in these units. Predictions using any other units will be considered invalid during validation.

the constants.py contains the following 
entity_unit_map = {
    'width': {'centimetre', 'foot', 'inch', 'metre', 'millimetre', 'yard'},
    'depth': {'centimetre', 'foot', 'inch', 'metre', 'millimetre', 'yard'},
    'height': {'centimetre', 'foot', 'inch', 'metre', 'millimetre', 'yard'},
    'item_weight': {'gram',
        'kilogram',
        'microgram',
        'milligram',
        'ounce',
        'pound',
        'ton'},
    'maximum_weight_recommendation': {'gram',
        'kilogram',
        'microgram',
        'milligram',
        'ounce',
        'pound',
        'ton'},
    'voltage': {'kilovolt', 'millivolt', 'volt'},
    'wattage': {'kilowatt', 'watt'},
    'item_volume': {'centilitre',
        'cubic foot',
        'cubic inch',
        'cup',
        'decilitre',
        'fluid ounce',
        'gallon',
        'imperial gallon',
        'litre',
        'microlitre',
        'millilitre',
        'pint',
        'quart'}
}

allowed_units = {unit for entity in entity_unit_map for unit in entity_unit_map[entity]}


so like i think that we have to first create a map of each attribute with their units like for weight, units are g, kg, mg, mcg, etc etc.. for all the attributes
then when traversing the input, first we check the entity, then using map store it's units allowed from the constants then search the image matching something, some images might have weight written, some have gram written , some have kg so we need to make sure that all these test cases are made sure of and none are left behind 
can you make the script for the same 
also while traversing there's a catch in the input file some index number will be omitted just to check so in the output file we need to make sure that the same index is also missing 
this is what the test csv looks like 
index,image_link,group_id,entity_name
0,https://m.media-amazon.com/images/I/110EibNyclL.jpg,156839,height
1,https://m.media-amazon.com/images/I/11TU2clswzL.jpg,792578,width
2,https://m.media-amazon.com/images/I/11TU2clswzL.jpg,792578,height
3,https://m.media-amazon.com/images/I/11TU2clswzL.jpg,792578,depth
4,https://m.media-amazon.com/images/I/11gHj8dhhrL.jpg,792578,depth
5,https://m.media-amazon.com/images/I/11gHj8dhhrL.jpg,792578,height
6,https://m.media-amazon.com/images/I/11gHj8dhhrL.jpg,792578,width
7,https://m.media-amazon.com/images/I/11lshEUmCrL.jpg,156839,height
8,https://m.media-amazon.com/images/I/21+i52HRW4L.jpg,478357,width
9,https://m.media-amazon.com/images/I/21-LmSmehZL.jpg,478357,height
10,https://m.media-amazon.com/images/I/213oP6n7jtL.jpg,953313,item_weight
11,https://m.media-amazon.com/images/I/213wY3gUsmL.jpg,276611,width
12,https://m.media-amazon.com/images/I/214CLs1oznL.jpg,648011,depth
13,https://m.media-amazon.com/images/I/214CLs1oznL.jpg,648011,height
14,https://m.media-amazon.com/images/I/214CLs1oznL.jpg,648011,width
15,https://m.media-amazon.com/images/I/216rjgJHAeL.jpg,279307,item_weight
16,https://m.media-amazon.com/images/I/2174yonQBtL.jpg,569206,maximum_weight_recommendation
17,https://m.media-amazon.com/images/I/218BCzgKxuL.jpg,348551,item_weight
18,https://m.media-amazon.com/images/I/218BCzgKxuL.jpg,348551,wattage
19,https://m.media-amazon.com/images/I/218BCzgKxuL.jpg,348551,voltage
20,https://m.media-amazon.com/images/I/21BMc5GC4iL.jpg,442321,width
21,https://m.media-amazon.com/images/I/21CxlWbim3L.jpg,751532,width
22,https://m.media-amazon.com/images/I/21DZ7BAZ6-L.jpg,498074,item_volume
23,https://m.media-amazon.com/images/I/21GLFXwC1mS.jpg,178958,width
24,https://m.media-amazon.com/images/I/21GLFXwC1mS.jpg,178958,depth
25,https://m.media-amazon.com/images/I/21H+7R85YZL.jpg,752266,width
26,https://m.media-amazon.com/images/I/21H+7R85YZL.jpg,752266,depth
27,https://m.media-amazon.com/images/I/21IGmiJi-PL.jpg,953313,item_weight
28,https://m.media-amazon.com/images/I/21Is45vdL0L.jpg,965518,item_weight

and this is what the output should look like(not the output of above test)
index,prediction
0,21.9 foot
1,10 foot
2,
3,289.52 kilovolt
4,1078.99 kilowatt
5,58.21 ton
6,10 volt
7,34.57 watt
8,
9,20 volt
10,
11,4.95 millimetre
12,
13,50 centimetre
14,6.75 pound
15,
16,40 inch
17,
18,6.2 inch
19,9 metre
20,10 foot
21,10 millimetre
22,10 inch
23,
24,11.94 kilovolt
25,10 kilowatt
26,70 centimetre
27,10 inch
28,10 yard
29,10 millimetre
30,6.37 inch
31,
32,10 millimetre
33,73 centimetre
34,107.78 kilogram
35,10 kilogram
36,
37,10.54 inch
38,
39,
40,
41,4.5 centimetre
42,163.43 kilogram
43,10 volt
44,
45,108.57 pound
46,10 metre
47,
48,10 pound
49,45 watt
50,10 kilovolt
51,
52,90 centimetre
53,
54,

so also if none matching is found we have to return empty string i.e. nothing should be printed
please make the file 

import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
from torchvision import transforms
from PIL import Image
import os
import pandas as pd
import numpy as np
from tqdm import tqdm
import re
import warnings
from jiwer import wer as jiwer_wer, cer as jiwer_cer

# Suppress warnings for clean output
warnings.filterwarnings("ignore")

# Define the character set
charset = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ .,%'
char_to_idx = {char: idx + 1 for idx, char in enumerate(charset)}  # Start indices from 1
idx_to_char = {idx: char for char, idx in char_to_idx.items()}
num_classes = len(charset) + 1  # +1 for the CTC blank label

# Check for CUDA availability
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"Using device: {device}")

# Define the CRNN model
class CRNN(nn.Module):
    def __init__(self, num_classes, input_channels=1):
        super(CRNN, self).__init__()

        self.cnn = nn.Sequential(
            nn.Conv2d(input_channels, 64, kernel_size=3, padding=1),  # Output: 64 x H x W
            nn.ReLU(inplace=True),
            nn.MaxPool2d(2, 2),  # Output: 64 x H/2 x W/2
            nn.Dropout(0.2),

            nn.Conv2d(64, 128, kernel_size=3, padding=1),  # Output: 128 x H/2 x W/2
            nn.ReLU(inplace=True),
            nn.MaxPool2d(2, 2),  # Output: 128 x H/4 x W/4

            nn.Conv2d(128, 256, kernel_size=3, padding=1),  # Output: 256 x H/4 x W/4
            nn.BatchNorm2d(256),
            nn.ReLU(inplace=True),
            nn.MaxPool2d((2, 1)),  # Output: 256 x H/8 x W/4

            nn.Conv2d(256, 512, kernel_size=3, padding=1),  # Output: 512 x H/8 x W/4
            nn.BatchNorm2d(512),
            nn.ReLU(inplace=True),
            nn.MaxPool2d((2, 1)),  # Output: 512 x H/16 x W/4

            nn.Conv2d(512, 512, kernel_size=2),  # Output: 512 x (H/16 - 1) x (W/4 - 1)
            nn.ReLU(inplace=True)
        )

        self.lstm = nn.GRU(
            input_size=512,
            hidden_size=256,
            num_layers=2,
            bidirectional=True,
            batch_first=True,
            dropout=0.5  # Added dropout for regularization
        )

        self.fc = nn.Linear(512, num_classes)

    def forward(self, x):
        conv = self.cnn(x)
        b, c, h, w = conv.size()
        conv = conv.squeeze(2)  # Remove the height dimension
        conv = conv.permute(0, 2, 1)  # [batch, width, channels]
        rnn_out, _ = self.lstm(conv)
        output = self.fc(rnn_out)  # Added missing line
        return output.log_softmax(2).permute(1, 0, 2)  # CTC loss requires [time, batch, class]

# Define the OCRDataset class
class OCRDataset(Dataset):
    def __init__(self, data_frame, image_dir, transform=None):
        self.data_frame = data_frame.reset_index(drop=True)
        self.transform = transform
        self.image_dir = image_dir  # Directory where images are stored

    def __len__(self):
        return len(self.data_frame)

    def __getitem__(self, idx):
        img_filename = self.data_frame.iloc[idx]['image_filename']
        img_path = os.path.join(self.image_dir, img_filename)
        label = self.data_frame.iloc[idx]['entity_value']

        # Check if image file exists
        if not os.path.isfile(img_path):
            print(f"Image file not found at index {idx}: {img_path}")
            return None

        # Open image
        try:
            img = Image.open(img_path).convert('L')  # Grayscale
            if self.transform:
                img = self.transform(img)
        except Exception as e:
            print(f"Failed to load image at index {idx}: {e}")
            return None  # Skip this index

        # Encode label
        label_idx = [char_to_idx.get(char, 0) for char in label]
        label_tensor = torch.tensor(label_idx, dtype=torch.long)
        return img, label_tensor, len(label_idx)

# Collate function for DataLoader
def collate_fn(batch):
    # Remove None images
    batch = [b for b in batch if b is not None]
    if len(batch) == 0:
        # Create empty tensors if batch is empty
        return torch.empty(0), torch.empty(0, dtype=torch.long), torch.empty(0, dtype=torch.long)
    
    images, labels, label_lengths = zip(*batch)
    images = torch.stack(images)
    labels = torch.cat(labels)
    label_lengths = torch.tensor(label_lengths, dtype=torch.long)
    return images, labels, label_lengths

# Data augmentation and preprocessing transformations
transform = transforms.Compose([
    transforms.Resize((32, 128)),
    transforms.RandomRotation(5),  # Data augmentation
    transforms.ToTensor(),
    transforms.Normalize((0.5,), (0.5,)),
])

# Text normalization function
def normalize_text(text):
    text = text.lower()
    text = re.sub(r'[^a-z0-9\s.,%]', '', text)
    return text

# Evaluation metrics using jiwer
def cer(predictions, ground_truths):
    return jiwer_cer(ground_truths, predictions)

def wer(predictions, ground_truths):
    return jiwer_wer(ground_truths, predictions)

# Learning rate scheduler
def get_lr_scheduler(optimizer):
    return optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.5)

# Early stopping
class EarlyStopping:
    def __init__(self, patience=5, min_delta=0):
        self.patience = patience
        self.min_delta = min_delta
        self.counter = 0
        self.best_loss = None
        self.early_stop = False

    def __call__(self, val_loss):
        if self.best_loss is None:
            self.best_loss = val_loss
            return
        elif val_loss > self.best_loss - self.min_delta:
            self.counter += 1
            print(f"EarlyStopping counter: {self.counter} out of {self.patience}")
            if self.counter >= self.patience:
                self.early_stop = True
        else:
            self.best_loss = val_loss
            self.counter = 0

# Prediction decoding function
def decode_predictions(outputs):
    outputs = outputs.permute(1, 0, 2)  # [batch, time, classes]
    outputs = outputs.cpu().detach()
    _, max_indices = torch.max(outputs, 2)
    predictions = []
    
    for indices in max_indices:
        pred_str = ''
        prev_idx = -1
        for idx in indices:
            idx = idx.item()
            if idx != prev_idx and idx != 0:  # Ignore duplicates and blanks
                pred_str += idx_to_char.get(idx, '')
            prev_idx = idx
        predictions.append(pred_str)
    return predictions

# Training function with model checkpointing and mixed precision
def train_model(model, train_loader, test_loader, criterion, optimizer, num_epochs=20):
    scheduler = get_lr_scheduler(optimizer)
    early_stopping = EarlyStopping(patience=5)
    best_val_loss = float('inf')  # For model checkpointing
    scaler = torch.cuda.amp.GradScaler()  # Initialize GradScaler for mixed precision

    for epoch in range(num_epochs):
        model.train()
        total_loss = 0
        progress_bar = tqdm(train_loader, desc=f"[Epoch {epoch + 1}/{num_epochs}]", leave=False)
        total_batches = len(train_loader)

        for batch in progress_bar:
            images, labels, label_lengths = batch
            # Skip empty batches
            if images.shape[0] == 0:
                continue
            
            images = images.to(device)
            labels = labels.to(device)
            label_lengths = label_lengths.to(device)

            optimizer.zero_grad()
            with torch.cuda.amp.autocast():  # Mixed precision
                outputs = model(images)

                batch_size = images.size(0)
                input_lengths = torch.full(size=(batch_size,), fill_value=outputs.size(0), dtype=torch.long).to(device)

                loss = criterion(outputs, labels, input_lengths, label_lengths)

            scaler.scale(loss).backward()  # Scale the loss for mixed precision
            scaler.step(optimizer)  # Update the optimizer
            scaler.update()  # Update the scale for next iteration
            total_loss += loss.item()

            progress_bar.set_postfix({'loss': loss.item()})

        avg_loss = total_loss / total_batches
        print(f"Epoch [{epoch + 1}/{num_epochs}], Training Loss: {avg_loss:.4f}")

        # Validation
        val_loss, val_cer, val_wer = validate_model(model, test_loader, criterion)
        print(f"Validation Loss: {val_loss:.4f}, CER: {val_cer:.4f}, WER: {val_wer:.4f}")

        # Early stopping
        early_stopping(val_loss)
        if early_stopping.early_stop:
            print("Early stopping triggered.")
            break

        # Model checkpointing
        if val_loss < best_val_loss:
            best_val_loss = val_loss
            torch.save(model.state_dict(), 'best_model.pth')  # Save the best model
            print("Model saved.")

        scheduler.step()  # Step the learning rate scheduler

# Validation function
def validate_model(model, test_loader, criterion):
    model.eval()
    total_loss = 0
    predictions = []
    ground_truths = []
    with torch.no_grad():
        for batch in tqdm(test_loader, desc="Validating", leave=False):
            images, labels, label_lengths = batch
            if images.shape[0] == 0:
                continue
            images = images.to(device)
            labels = labels.to(device)
            label_lengths = label_lengths.to(device)

            outputs = model(images)
            batch_size = images.size(0)
            input_lengths = torch.full(size=(batch_size,), fill_value=outputs.size(0), dtype=torch.long).to(device)

            loss = criterion(outputs, labels, input_lengths, label_lengths)
            total_loss += loss.item()

            preds = decode_predictions(outputs)
            predictions.extend(preds)

            # Reconstruct ground truth strings
            start = 0
            for i in range(batch_size):
                label_len = label_lengths[i].item()
                ground_truth = ''.join([idx_to_char.get(c.item(), '') for c in labels[start:start+label_len]])
                ground_truth = normalize_text(ground_truth)
                ground_truths.append(ground_truth)
                start += label_len

    avg_loss = total_loss / len(test_loader)
    predictions = [normalize_text(p) for p in predictions]
    cer_value = cer(predictions, ground_truths)
    wer_value = wer(predictions, ground_truths)
    return avg_loss, cer_value, wer_value

# Instantiate and train the model
if __name__ == "__main__":
    # Load the dataset
    df = pd.read_csv('/kaggle/input/sorted/sorted_file.csv')  # Replace with your CSV file path
    df = df.iloc[:25000].reset_index(drop=True)  # Use first 25,000 entries
    df['image_filename'] = df['image_link'].apply(lambda x: x.split('/')[-1])  # Extract image filenames
    df['entity_value'] = df['entity_value'].apply(normalize_text)

    image_dir = '/kaggle/input/the-dataset/images/train'  # Replace with your image directory
    dataset = OCRDataset(df, image_dir, transform=transform)
    
    # Split the dataset
    from sklearn.model_selection import train_test_split
    train_df, test_df = train_test_split(df, test_size=0.1, random_state=42)
    train_dataset = OCRDataset(train_df.reset_index(drop=True), image_dir, transform=transform)
    test_dataset = OCRDataset(test_df.reset_index(drop=True), image_dir, transform=transform)

    # Create data loaders
    train_loader = DataLoader(train_dataset, batch_size=16, shuffle=True, collate_fn=collate_fn, num_workers=2)
    test_loader = DataLoader(test_dataset, batch_size=16, shuffle=False, collate_fn=collate_fn, num_workers=2)

    # Instantiate the model, define the criterion and optimizer
    model = CRNN(num_classes).to(device)
    criterion = nn.CTCLoss(blank=0, zero_infinity=True).to(device)
    optimizer = optim.Adam(model.parameters(), lr=0.001)

    # Train the model
    train_model(model, train_loader, test_loader, criterion, optimizer, num_epochs=20)

    # Load the best model for evaluation
    model.load_state_dict(torch.load('best_model.pth'))
    model.eval()

    # Perform evaluation on the test set
    test_loss, test_cer, test_wer = validate_model(model, test_loader, criterion)
    print(f"Test Loss: {test_loss:.4f}, Test CER: {test_cer:.4f}, Test WER: {test_wer:.4f}")

this is how i made my model 
same way i have my images downloaded as in the model file we did