```
import pandas as pd
import numpy as np
import torch
import re
from transformers import BertTokenizerFast, BertForSequenceClassification
from torch.utils.data import Dataset, DataLoader
import torch.nn as nn
import torch.optim as optim
from sklearn.model_selection import train_test_split
from tqdm import tqdm

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f'Using device: {device}')

# Load the dataset
df = pd.read_csv('limited_reviews.csv')

# Data Cleaning Function
def clean_text(text):
    # Check if text is a string
    if not isinstance(text, str):
        text = str(text)
    # Remove non-alphabetic characters
    text = re.sub(r'[^a-zA-Z\s]', '', text)
    # Lowercase
    text = text.lower()
    # Remove extra whitespace
    text = re.sub(r'\s+', ' ', text).strip()
    return text

# Fill NaN values with empty strings
df['title'] = df['title'].fillna('')
df['text'] = df['text'].fillna('')
df['sentiment'] = df['sentiment'].fillna('neutral')

# Clean the 'title' and 'text' columns and combine them
df['clean_title'] = df['title'].apply(clean_text)
df['clean_text'] = df['text'].apply(clean_text)
df['clean_full_text'] = df['clean_title'] + ' ' + df['clean_text']

# Map sentiments to labels
valid_sentiments = ['negative', 'neutral', 'positive']
df['sentiment'] = df['sentiment'].apply(lambda x: x if x in valid_sentiments else 'neutral')

label_mapping = {'negative': 0, 'neutral': 1, 'positive': 2}
df['label'] = df['sentiment'].map(label_mapping)

# Drop rows with missing labels
df = df.dropna(subset=['label'])

# Split the dataset into training and validation sets
train_df, val_df = train_test_split(df, test_size=0.1, random_state=42, stratify=df['label'])

# Custom Dataset Class
class SentimentDataset(Dataset):
    def __init__(self, texts, labels, tokenizer, max_len):
        self.texts = texts.tolist()
        self.labels = labels.tolist()
        self.tokenizer = tokenizer
        self.max_len = max_len
        
    def __len__(self):
        return len(self.labels)
    
    def __getitem__(self, idx):
        text = str(self.texts[idx])
        label = self.labels[idx]
        
        encoding = self.tokenizer.encode_plus(
            text,
            add_special_tokens=True,
            truncation=True,
            max_length=self.max_len,
            padding='max_length',  # Pad sequences to max_length
            return_attention_mask=True,
            return_tensors='pt'
        )
        
        return {
            'input_ids': encoding['input_ids'].flatten(),  # Flatten to 1D tensor
            'attention_mask': encoding['attention_mask'].flatten(),
            'labels': torch.tensor(label, dtype=torch.long)
        }

# Initialize the tokenizer
tokenizer = BertTokenizerFast.from_pretrained('bert-base-uncased')
MAX_LEN = 128

# Create DataLoaders
BATCH_SIZE = 16

train_dataset = SentimentDataset(
    texts=train_df['clean_full_text'],
    labels=train_df['label'],
    tokenizer=tokenizer,
    max_len=MAX_LEN
)

val_dataset = SentimentDataset(
    texts=val_df['clean_full_text'],
    labels=val_df['label'],
    tokenizer=tokenizer,
    max_len=MAX_LEN
)

train_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=BATCH_SIZE, shuffle=False)

# Initialize the model
model = BertForSequenceClassification.from_pretrained('bert-base-uncased', num_labels=3)
model.to(device)

# Set up the optimizer
optimizer = optim.AdamW(model.parameters(), lr=2e-5)

# Training and Evaluation Functions
def train_epoch(model, data_loader, optimizer, device):
    model.train()
    losses = []
    correct_predictions = 0
    
    for batch in tqdm(data_loader, desc="Training"):
        input_ids = batch['input_ids'].to(device)
        attention_mask = batch['attention_mask'].to(device)
        labels = batch['labels'].to(device)
        
        outputs = model(
            input_ids=input_ids,
            attention_mask=attention_mask,
            labels=labels
        )
        
        loss = outputs.loss
        logits = outputs.logits
        
        _, preds = torch.max(logits, dim=1)
        correct_predictions += torch.sum(preds == labels)
        losses.append(loss.item())
        
        loss.backward()
        optimizer.step()
        optimizer.zero_grad()
        
    return correct_predictions.double() / len(data_loader.dataset), np.mean(losses)

def eval_model(model, data_loader, device):
    model.eval()
    losses = []
    correct_predictions = 0
    
    with torch.no_grad():
        for batch in tqdm(data_loader, desc="Validation"):
            input_ids = batch['input_ids'].to(device)
            attention_mask = batch['attention_mask'].to(device)
            labels = batch['labels'].to(device)
            
            outputs = model(
                input_ids=input_ids,
                attention_mask=attention_mask,
                labels=labels
            )
            
            loss = outputs.loss
            logits = outputs.logits
            
            _, preds = torch.max(logits, dim=1)
            correct_predictions += torch.sum(preds == labels)
            losses.append(loss.item())
            
    return correct_predictions.double() / len(data_loader.dataset), np.mean(losses)

# Training Loop
EPOCHS = 3

for epoch in range(EPOCHS):
    print(f'Epoch {epoch + 1}/{EPOCHS}')
    
    train_acc, train_loss = train_epoch(
        model,
        train_loader,
        optimizer,
        device
    )
    
    print(f'Train loss {train_loss:.4f} accuracy {train_acc:.4f}')
    
    val_acc, val_loss = eval_model(
        model,
        val_loader,
        device
    )
    
    print(f'Val   loss {val_loss:.4f} accuracy {val_acc:.4f}')

# Save the trained model
model.save_pretrained('sentiment_model')
tokenizer.save_pretrained('sentiment_model')

# Function to predict sentiment
def predict_sentiment(text, tokenizer, model, device, max_len=128):
    model.eval()
    
    # Clean the text
    text = clean_text(text)
    
    encoding = tokenizer.encode_plus(
        text,
        add_special_tokens=True,
        truncation=True,
        max_length=max_len,
        return_attention_mask=True,
        return_tensors='pt'
    )
    
    input_ids = encoding['input_ids'].to(device)
    attention_mask = encoding['attention_mask'].to(device)
    
    with torch.no_grad():
        outputs = model(
            input_ids=input_ids,
            attention_mask=attention_mask
        )
        
    logits = outputs.logits
    _, prediction = torch.max(logits, dim=1)
    label_map = {0: 'negative', 1: 'neutral', 2: 'positive'}
    return label_map[prediction.item()]

# Example usage
tokenizer = BertTokenizerFast.from_pretrained('sentiment_model')
model = BertForSequenceClassification.from_pretrained('sentiment_model')
model.to(device)

# Predict sentiments for new texts
texts = [
    "I absolutely love this product! It works wonders.",
    "It's okay, nothing special.",
    "I'm really disappointed. It broke after one use."
]

for text in texts:
    sentiment = predict_sentiment(text, tokenizer, model, device)
    print(f'Text: "{text}"')
    print(f'Predicted Sentiment: {sentiment}')
    print('-' * 50)
```

this is my train.py...
Change it a bit such that important validations, graphs and tests are added along with scores... Also print important factors. (Make this a full analysis with training with graphs and save them). Return full code with all changes