From c2ff686085ed4e6c658014f57706ec376c4b8601 Mon Sep 17 00:00:00 2001 From: sina Date: Sat, 23 Nov 2024 02:32:51 -0500 Subject: [PATCH] initia --- .github/workflows/deploy.yml | 50 ++++++ README.md | 39 +++++ app/auth.py | 67 +++++++ app/feedback.py | 30 ++++ app/static/app.js | 47 ++++- app/static/chart.js | 60 +++++++ app/static/styles.css | 53 +++++- app/static/swagger.json | 47 +++++ app/static/swagger_docs_extended.json | 49 ++++++ app/templates/chat.html | 29 +++ app/templates/feedback.html | 26 +++ app/ui.py | 27 ++- config/pretrained_model_config.json | 9 + dashboards/explainability_dashboard.py | 33 ++++ dashboards/performance_dashboard.py | 63 +++++++ deployment/kubernetes/ingress.yaml | 19 +- docker-compose.logging.yml | 27 +++ src/data/data_augmentation.py | 41 +++++ .../kubernetes/canary_deployment.yml | 24 +++ src/deployment/kubernetes/hpa.yml | 14 ++ .../monitoring/grafana_dashboard.json | 26 +++ src/deployment/monitoring/prometheus.yml | 9 + src/evaluation/bias_analysis.py | 25 +++ src/experiments/mlflow_tracking.py | 33 ++++ src/preprocessing/preprocess_data.py | 37 ++++ src/training/fine_tuning.py | 165 +++++++++++++----- src/training/retrain_model.py | 39 +++++ src/training/transfer_learning.py | 86 +++++++++ tests/e2e/ui_tests.spec.js | 20 +++ tests/load_testing/locustfile.py | 16 ++ 30 files changed, 1163 insertions(+), 47 deletions(-) create mode 100644 .github/workflows/deploy.yml create mode 100644 app/auth.py create mode 100644 app/feedback.py create mode 100644 app/static/chart.js create mode 100644 app/static/swagger.json create mode 100644 app/static/swagger_docs_extended.json create mode 100644 app/templates/chat.html create mode 100644 app/templates/feedback.html create mode 100644 config/pretrained_model_config.json create mode 100644 dashboards/explainability_dashboard.py create mode 100644 dashboards/performance_dashboard.py create mode 100644 docker-compose.logging.yml create mode 100644 src/data/data_augmentation.py create mode 100644 src/deployment/kubernetes/canary_deployment.yml create mode 100644 src/deployment/kubernetes/hpa.yml create mode 100644 src/deployment/monitoring/grafana_dashboard.json create mode 100644 src/deployment/monitoring/prometheus.yml create mode 100644 src/evaluation/bias_analysis.py create mode 100644 src/experiments/mlflow_tracking.py create mode 100644 src/preprocessing/preprocess_data.py create mode 100644 src/training/retrain_model.py create mode 100644 src/training/transfer_learning.py create mode 100644 tests/e2e/ui_tests.spec.js create mode 100644 tests/load_testing/locustfile.py diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..015bea1 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,50 @@ +name: CI/CD Pipeline + +on: + push: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.8' + + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Lint Code + run: pylint app/**/*.py + + - name: Run Unit Tests + run: pytest tests/ + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Build and Push Docker Image + uses: docker/build-push-action@v2 + with: + context: . + tags: user/repository:latest + push: true + + deploy: + needs: build + runs-on: ubuntu-latest + steps: + - name: Deploy to Kubernetes + uses: azure/k8s-deploy@v1 + with: + manifests: | + deployment/kubernetes/deployment.yml + deployment/kubernetes/service.yml diff --git a/README.md b/README.md index bb60efb..c8606e8 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,45 @@ For hyperparameter tuning, **Optuna** has been integrated to provide automated e - **🔍 Explainability**: SHAP and LIME explainability metrics are added to the evaluation process, providing insights into model behavior. - **📜 Logging**: Centralized logging using **ELK Stack** (Elasticsearch, Logstash, Kibana). +## 🚀 Cloud Deployment Instructions (AWS) + +To deploy the LLM Alignment Assistant on **AWS**, you can utilize **Elastic Kubernetes Service (EKS)** or **AWS Sagemaker** for model training: + +1. **AWS Elastic Kubernetes Service (EKS)**: + - Create an EKS cluster using AWS CLI or the console. + - Apply the Kubernetes deployment files: + ```bash + kubectl apply -f deployment/kubernetes/deployment.yml + kubectl apply -f deployment/kubernetes/service.yml + ``` + - Configure the **Horizontal Pod Autoscaler (HPA)** to ensure scalability: + ```bash + kubectl apply -f deployment/kubernetes/hpa.yml + ``` + +2. **AWS Sagemaker for Model Training**: + - Modify the `training/fine_tuning.py` to integrate with AWS Sagemaker. + - Use the Sagemaker Python SDK to launch a training job: + ```python + import sagemaker + from sagemaker.pytorch import PyTorch + + role = "arn:aws:iam::your-account-id:role/service-role/AmazonSageMaker-ExecutionRole-2023" + + pytorch_estimator = PyTorch( + entry_point='training/fine_tuning.py', + role=role, + instance_count=1, + instance_type='ml.p2.xlarge', + framework_version='1.8.0', + py_version='py3' + ) + + pytorch_estimator.fit({'training': 's3://your-bucket-name/training-data'}) + ``` + - Ensure IAM roles and permissions are properly set for accessing **S3** and **Sagemaker**. + + ## 🚀 Future Work - **🌍 Multi-Language Support**: Expand the LLM's training to support multiple languages. diff --git a/app/auth.py b/app/auth.py new file mode 100644 index 0000000..7bbc73c --- /dev/null +++ b/app/auth.py @@ -0,0 +1,67 @@ +# User Authentication using Flask-Login + +from flask import Flask, render_template, redirect, url_for, request, flash +from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user +from werkzeug.security import generate_password_hash, check_password_hash + +app = Flask(__name__) +app.secret_key = 'secret-key' + +# User session management setup +login_manager = LoginManager() +login_manager.init_app(app) +login_manager.login_view = "login" + +# Mock database for users +users = {} + +class User(UserMixin): + def __init__(self, id, username, password): + self.id = id + self.username = username + self.password_hash = generate_password_hash(password) + +@login_manager.user_loader +def load_user(user_id): + return users.get(user_id) + +@app.route('/register', methods=['GET', 'POST']) +def register(): + if request.method == 'POST': + username = request.form['username'] + password = request.form['password'] + user_id = len(users) + 1 + if username in [user.username for user in users.values()]: + flash("Username already exists. Please choose a different one.") + else: + users[user_id] = User(user_id, username, password) + flash("User registered successfully!") + return redirect(url_for('login')) + return render_template('register.html') + +@app.route('/login', methods=['GET', 'POST']) +def login(): + if request.method == 'POST': + username = request.form['username'] + password = request.form['password'] + user = next((u for u in users.values() if u.username == username), None) + if user and check_password_hash(user.password_hash, password): + login_user(user) + return redirect(url_for('dashboard')) + else: + flash("Invalid username or password.") + return render_template('login.html') + +@app.route('/dashboard') +@login_required +def dashboard(): + return render_template('dashboard.html') + +@app.route('/logout') +@login_required +def logout(): + logout_user() + return redirect(url_for('login')) + +if __name__ == '__main__': + app.run(debug=True) diff --git a/app/feedback.py b/app/feedback.py new file mode 100644 index 0000000..539484e --- /dev/null +++ b/app/feedback.py @@ -0,0 +1,30 @@ +# Flask route for handling feedback submission + +from flask import Flask, request, jsonify, render_template +import csv + +app = Flask(__name__) + +# Route to render feedback form +@app.route('/feedback', methods=['GET']) +def feedback(): + # You can dynamically pass the response to be rated in 'response' variable + response = "Example response from LLM to be rated" + return render_template('feedback.html', response=response) + +# Route to handle feedback submission +@app.route('/submit-feedback', methods=['POST']) +def submit_feedback(): + rating = request.form['rating'] + comments = request.form['comments'] + model_response = request.form['model-response'] + + # Save feedback to a CSV file + with open('feedback.csv', mode='a') as feedback_file: + feedback_writer = csv.writer(feedback_file, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL) + feedback_writer.writerow([model_response, rating, comments]) + + return jsonify(status='success', message='Thank you for your feedback!') + +if __name__ == "__main__": + app.run(debug=True) diff --git a/app/static/app.js b/app/static/app.js index 6faa550..0081b0b 100644 --- a/app/static/app.js +++ b/app/static/app.js @@ -1 +1,46 @@ -console.log('App JS'); \ No newline at end of file +// UI Enhancements - Adding Animations, Dark Mode Toggle, and Tooltips + +document.addEventListener('DOMContentLoaded', function() { + // Dark Mode Toggle + const darkModeToggle = document.getElementById('dark-mode-toggle'); + const body = document.body; + + darkModeToggle.addEventListener('click', () => { + body.classList.toggle('dark-mode'); + if (body.classList.contains('dark-mode')) { + localStorage.setItem('theme', 'dark'); + } else { + localStorage.setItem('theme', 'light'); + } + }); + + // Persist Dark Mode Setting + if (localStorage.getItem('theme') === 'dark') { + body.classList.add('dark-mode'); + } + + // Tooltip Initialization + const tooltips = document.querySelectorAll('.tooltip'); + tooltips.forEach(tooltip => { + tooltip.addEventListener('mouseover', function() { + const tooltipText = document.createElement('span'); + tooltipText.className = 'tooltip-text'; + tooltipText.innerText = this.getAttribute('data-tooltip'); + this.appendChild(tooltipText); + }); + tooltip.addEventListener('mouseout', function() { + const tooltipText = this.querySelector('.tooltip-text'); + if (tooltipText) this.removeChild(tooltipText); + }); + }); + + // GSAP Animations for Elements + gsap.from('.card', { + duration: 1, + y: 50, + opacity: 0, + stagger: 0.2, + ease: 'power2.out' + }); + }); + \ No newline at end of file diff --git a/app/static/chart.js b/app/static/chart.js new file mode 100644 index 0000000..d9b8d44 --- /dev/null +++ b/app/static/chart.js @@ -0,0 +1,60 @@ +// Interactive Chart.js Visualizations + +document.addEventListener('DOMContentLoaded', function() { + const ctx = document.getElementById('modelPerformanceChart').getContext('2d'); + const modelPerformanceChart = new Chart(ctx, { + type: 'line', + data: { + labels: ['Epoch 1', 'Epoch 2', 'Epoch 3', 'Epoch 4', 'Epoch 5'], + datasets: [{ + label: 'Training Loss', + data: [0.8, 0.6, 0.4, 0.3, 0.2], + borderColor: 'rgba(75, 192, 192, 1)', + backgroundColor: 'rgba(75, 192, 192, 0.2)', + fill: true, + tension: 0.4 + }, { + label: 'Validation Loss', + data: [0.9, 0.7, 0.5, 0.4, 0.3], + borderColor: 'rgba(255, 99, 132, 1)', + backgroundColor: 'rgba(255, 99, 132, 0.2)', + fill: true, + tension: 0.4 + }] + }, + options: { + responsive: true, + plugins: { + legend: { + position: 'top', + }, + tooltip: { + mode: 'index', + intersect: false, + } + }, + interaction: { + mode: 'nearest', + axis: 'x', + intersect: false + }, + scales: { + x: { + display: true, + title: { + display: true, + text: 'Epoch' + } + }, + y: { + display: true, + title: { + display: true, + text: 'Loss' + } + } + } + } + }); + }); + \ No newline at end of file diff --git a/app/static/styles.css b/app/static/styles.css index a71aaab..9b55977 100644 --- a/app/static/styles.css +++ b/app/static/styles.css @@ -1 +1,52 @@ -body { font-family: Arial; } \ No newline at end of file +/* Dark Mode Styles */ +body.dark-mode { + background-color: #121212; + color: #ffffff; + } + + button#dark-mode-toggle { + background-color: #333; + color: #fff; + border: none; + padding: 10px; + cursor: pointer; + border-radius: 5px; + } + + button#dark-mode-toggle:hover { + background-color: #555; + } + + /* Tooltip Styles */ + .tooltip { + position: relative; + cursor: pointer; + } + + .tooltip .tooltip-text { + position: absolute; + bottom: 125%; + left: 50%; + transform: translateX(-50%); + background-color: #333; + color: #fff; + padding: 5px; + border-radius: 4px; + white-space: nowrap; + opacity: 0; + transition: opacity 0.3s; + } + + .tooltip:hover .tooltip-text { + opacity: 1; + } + + /* GSAP Animation Styles */ + .card { + background: #f8f8f8; + padding: 20px; + margin: 20px; + border-radius: 10px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + } + \ No newline at end of file diff --git a/app/static/swagger.json b/app/static/swagger.json new file mode 100644 index 0000000..f01e635 --- /dev/null +++ b/app/static/swagger.json @@ -0,0 +1,47 @@ +{ + "swagger": "2.0", + "info": { + "description": "This is the API documentation for the LLM Alignment Assistant.", + "version": "1.0.0", + "title": "LLM Alignment Assistant API" + }, + "host": "localhost:5000", + "basePath": "/api", + "schemes": [ + "http" + ], + "paths": { + "/health": { + "get": { + "tags": [ + "Health" + ], + "summary": "Checks the health status of the API", + "operationId": "healthCheck", + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "API is healthy", + "schema": { + "$ref": "#/definitions/HealthStatus" + } + } + } + } + } + }, + "definitions": { + "HealthStatus": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "healthy" + } + } + } + } + } + \ No newline at end of file diff --git a/app/static/swagger_docs_extended.json b/app/static/swagger_docs_extended.json new file mode 100644 index 0000000..492094b --- /dev/null +++ b/app/static/swagger_docs_extended.json @@ -0,0 +1,49 @@ +{ + "swagger": "2.0", + "info": { + "description": "LLM Alignment Assistant API - Interactive Documentation", + "version": "1.0.0", + "title": "LLM Alignment Assistant API" + }, + "host": "localhost:5000", + "basePath": "/api", + "schemes": ["http"], + "paths": { + "/train": { + "post": { + "tags": ["Model Training"], + "summary": "Initiate model training with provided data", + "parameters": [ + { + "in": "body", + "name": "trainingData", + "description": "Training data for the model", + "required": true, + "schema": { + "$ref": "#/definitions/TrainingData" + } + } + ], + "responses": { + "200": { + "description": "Training started successfully" + } + } + } + } + }, + "definitions": { + "TrainingData": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + \ No newline at end of file diff --git a/app/templates/chat.html b/app/templates/chat.html new file mode 100644 index 0000000..49b81ef --- /dev/null +++ b/app/templates/chat.html @@ -0,0 +1,29 @@ + + + + + + + Interactive Chat + + +
+

💬 Interactive Chat

+
+ + +
+ + + + diff --git a/app/templates/feedback.html b/app/templates/feedback.html new file mode 100644 index 0000000..55ed688 --- /dev/null +++ b/app/templates/feedback.html @@ -0,0 +1,26 @@ + + + + + + + + + Feedback Form + + +

📝 User Feedback

+
+
+

+ +
+

+ +
+

+ + +
+ + diff --git a/app/ui.py b/app/ui.py index 1b3f3fb..d4e7384 100644 --- a/app/ui.py +++ b/app/ui.py @@ -1 +1,26 @@ -# Frontend logic \ No newline at end of file +from flask import Flask, jsonify +from flask_swagger_ui import get_swaggerui_blueprint + +app = Flask(__name__) + +# Swagger configuration +SWAGGER_URL = '/api/docs' # URL for accessing Swagger UI +API_URL = '/static/swagger.json' # Path to Swagger JSON + +swaggerui_blueprint = get_swaggerui_blueprint( + SWAGGER_URL, + API_URL, + config={ + 'app_name': "LLM Alignment Assistant API Documentation" + } +) + +# Register Swagger Blueprint +app.register_blueprint(swaggerui_blueprint, url_prefix=SWAGGER_URL) + +@app.route('/api/health', methods=['GET']) +def health_check(): + return jsonify(status='healthy') + +if __name__ == '__main__': + app.run(debug=True) diff --git a/config/pretrained_model_config.json b/config/pretrained_model_config.json new file mode 100644 index 0000000..1961646 --- /dev/null +++ b/config/pretrained_model_config.json @@ -0,0 +1,9 @@ +{ + "model_name": "bert-base-uncased", + "num_labels": 2, + "max_length": 128, + "learning_rate": 2e-5, + "batch_size": 16, + "epochs": 3 + } + \ No newline at end of file diff --git a/dashboards/explainability_dashboard.py b/dashboards/explainability_dashboard.py new file mode 100644 index 0000000..f3eb3a9 --- /dev/null +++ b/dashboards/explainability_dashboard.py @@ -0,0 +1,33 @@ +# SHAP-based Explainability Dashboard using Streamlit + +import streamlit as st +import shap +import matplotlib.pyplot as plt +import joblib +import pandas as pd + +# Load the trained model +model = joblib.load('model/retrained_model.pkl') + +# Sample data for explanation +X_sample = pd.DataFrame({ + 'Feature1': [1, 2, 3, 4, 5], + 'Feature2': [5, 4, 3, 2, 1], + 'Feature3': [2, 3, 4, 5, 6] +}) + +# Title of the dashboard +st.title("🧐 Model Explainability Dashboard") + +# Explain predictions using SHAP +explainer = shap.Explainer(model, X_sample) +shap_values = explainer(X_sample) + +# Plot SHAP Summary Plot +st.header("SHAP Summary Plot") +fig_summary = shap.summary_plot(shap_values, X_sample, show=False) +st.pyplot(fig_summary) + +# Feature Importance +st.header("Feature Importance") +shap.plots.bar(shap_values) diff --git a/dashboards/performance_dashboard.py b/dashboards/performance_dashboard.py new file mode 100644 index 0000000..389faf5 --- /dev/null +++ b/dashboards/performance_dashboard.py @@ -0,0 +1,63 @@ +# Expanded Performance Dashboard using Streamlit + +import streamlit as st +import pandas as pd +import numpy as np +import matplotlib.pyplot as plt +from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay + +# Title of the dashboard +st.title("📊 LLM Alignment Assistant Expanded Performance Dashboard") + +# Sidebar filters for the dashboard +st.sidebar.header("Filters") +epochs = st.sidebar.slider("Select Epoch Range", 1, 50, (1, 10)) + +# Mock Data - Training & Validation Loss +st.header("Training and Validation Loss") +train_loss = np.linspace(0.8, 0.1, 50) +val_loss = np.linspace(0.9, 0.15, 50) + +filtered_epochs = range(epochs[0], epochs[1] + 1) +filtered_train_loss = train_loss[epochs[0] - 1:epochs[1]] +filtered_val_loss = val_loss[epochs[0] - 1:epochs[1]] + +fig, ax = plt.subplots() +ax.plot(filtered_epochs, filtered_train_loss, label='Training Loss', color='blue') +ax.plot(filtered_epochs, filtered_val_loss, label='Validation Loss', color='red') +ax.set_xlabel('Epoch') +ax.set_ylabel('Loss') +ax.set_title('Training vs Validation Loss') +ax.legend() + +# Display the plot +st.pyplot(fig) + +# Performance Metrics +st.header("Model Performance Metrics") +col1, col2, col3 = st.columns(3) +col1.metric("Training Loss", f"{train_loss[-1]:.4f}") +col2.metric("Validation Loss", f"{val_loss[-1]:.4f}") +col3.metric("Accuracy", "92.5%") + +# Confusion Matrix +st.header("Confusion Matrix") +y_true = [1, 0, 1, 1, 0, 1, 0, 0, 1, 0] +y_pred = [1, 0, 1, 0, 0, 1, 0, 1, 1, 0] +cm = confusion_matrix(y_true, y_pred) +fig_cm, ax_cm = plt.subplots() +ConfusionMatrixDisplay(cm).plot(ax=ax_cm) +st.pyplot(fig_cm) + +# Bias Metrics Visualization +st.header("Bias Metrics by Group") +try: + bias_metrics_df = pd.read_csv('bias_metrics.csv') + st.dataframe(bias_metrics_df) +except FileNotFoundError: + st.warning("Bias metrics data not found. Please generate bias metrics using `bias_analysis.py`.") + +# Instructions for running the dashboard +st.markdown("---") +st.markdown("**Instructions:** To run this dashboard, use the command:") +st.code("streamlit run performance_dashboard.py", language='bash') diff --git a/deployment/kubernetes/ingress.yaml b/deployment/kubernetes/ingress.yaml index 5d0ea26..3ea0430 100644 --- a/deployment/kubernetes/ingress.yaml +++ b/deployment/kubernetes/ingress.yaml @@ -1 +1,18 @@ -apiVersion: networking.k8s.io/v1 \ No newline at end of file +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: llm-ingress + annotations: + nginx.ingress.kubernetes.io/rewrite-target: / +spec: + rules: + - host: llm.example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: llm-service + port: + number: 80 diff --git a/docker-compose.logging.yml b/docker-compose.logging.yml new file mode 100644 index 0000000..978200f --- /dev/null +++ b/docker-compose.logging.yml @@ -0,0 +1,27 @@ +version: '3.1' + +services: + elasticsearch: + image: docker.elastic.co/elasticsearch/elasticsearch:7.10.1 + container_name: elasticsearch + environment: + - discovery.type=single-node + ports: + - "9200:9200" + - "9300:9300" + + logstash: + image: docker.elastic.co/logstash/logstash:7.10.1 + container_name: logstash + ports: + - "5044:5044" + volumes: + - ./logstash/config:/usr/share/logstash/config + + kibana: + image: docker.elastic.co/kibana/kibana:7.10.1 + container_name: kibana + ports: + - "5601:5601" + environment: + - ELASTICSEARCH_HOSTS=http://elasticsearch:9200 diff --git a/src/data/data_augmentation.py b/src/data/data_augmentation.py new file mode 100644 index 0000000..7bdf3ed --- /dev/null +++ b/src/data/data_augmentation.py @@ -0,0 +1,41 @@ +# Data Augmentation Script + +from googletrans import Translator +from transformers import pipeline +import pandas as pd +import random + +# Load dataset +data = pd.read_csv('data/raw/synthetic_data.csv') + +# Translator for back-translation +translator = Translator() + +# Summarization for paraphrasing +paraphraser = pipeline("summarization") + +def back_translation(text, target_language="fr"): + # Translate to target language and back to English + translated_text = translator.translate(text, dest=target_language).text + back_translated_text = translator.translate(translated_text, dest="en").text + return back_translated_text + +def paraphrase(text): + # Use summarization as a paraphrasing tool + paraphrased = paraphraser(text, max_length=100, min_length=30, do_sample=False) + return paraphrased[0]['summary_text'] + +augmented_texts = [] +original_texts = data['text'].tolist() + +# Perform augmentation +for text in original_texts: + if random.random() < 0.5: + augmented_texts.append(back_translation(text)) + else: + augmented_texts.append(paraphrase(text)) + +# Save augmented data +augmented_data = pd.DataFrame({'text': augmented_texts, 'label': data['label']}) +augmented_data.to_csv('data/processed/augmented_training_data.csv', index=False) +print("Augmented data saved.") diff --git a/src/deployment/kubernetes/canary_deployment.yml b/src/deployment/kubernetes/canary_deployment.yml new file mode 100644 index 0000000..14032da --- /dev/null +++ b/src/deployment/kubernetes/canary_deployment.yml @@ -0,0 +1,24 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: llm-alignment-assistant-canary + labels: + app: llm-alignment-assistant + version: canary +spec: + replicas: 1 + selector: + matchLabels: + app: llm-alignment-assistant + version: canary + template: + metadata: + labels: + app: llm-alignment-assistant + version: canary + spec: + containers: + - name: llm-alignment-assistant + image: user/repository:canary + ports: + - containerPort: 5000 diff --git a/src/deployment/kubernetes/hpa.yml b/src/deployment/kubernetes/hpa.yml new file mode 100644 index 0000000..27b9973 --- /dev/null +++ b/src/deployment/kubernetes/hpa.yml @@ -0,0 +1,14 @@ +# Kubernetes Horizontal Pod Autoscaler Configuration + +apiVersion: autoscaling/v1 +kind: HorizontalPodAutoscaler +metadata: + name: llm-alignment-assistant-hpa +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: llm-alignment-assistant + minReplicas: 2 + maxReplicas: 10 + targetCPUUtilizationPercentage: 50 diff --git a/src/deployment/monitoring/grafana_dashboard.json b/src/deployment/monitoring/grafana_dashboard.json new file mode 100644 index 0000000..18dc0b9 --- /dev/null +++ b/src/deployment/monitoring/grafana_dashboard.json @@ -0,0 +1,26 @@ +{ + "title": "LLM Alignment Assistant Monitoring", + "panels": [ + { + "type": "graph", + "title": "CPU Usage", + "targets": [ + { + "expr": "node_cpu_seconds_total", + "legendFormat": "CPU Usage" + } + ] + }, + { + "type": "graph", + "title": "Model Response Time", + "targets": [ + { + "expr": "flask_http_request_duration_seconds", + "legendFormat": "Response Time" + } + ] + } + ] + } + \ No newline at end of file diff --git a/src/deployment/monitoring/prometheus.yml b/src/deployment/monitoring/prometheus.yml new file mode 100644 index 0000000..e55c843 --- /dev/null +++ b/src/deployment/monitoring/prometheus.yml @@ -0,0 +1,9 @@ +# Prometheus Configuration File + +global: + scrape_interval: 15s + +scrape_configs: + - job_name: 'flask_app_metrics' + static_configs: + - targets: ['localhost:5000'] diff --git a/src/evaluation/bias_analysis.py b/src/evaluation/bias_analysis.py new file mode 100644 index 0000000..89d463a --- /dev/null +++ b/src/evaluation/bias_analysis.py @@ -0,0 +1,25 @@ +# Bias Analysis using Fairlearn + +from fairlearn.metrics import MetricFrame +from sklearn.metrics import accuracy_score +import pandas as pd + +# Example data - Replace these with actual predictions and labels +y_true = [1, 0, 1, 1, 0, 1, 0, 0, 1, 0] +y_pred = [1, 0, 1, 0, 0, 1, 0, 1, 1, 0] +sensitive_features = ['groupA', 'groupB', 'groupA', 'groupB', 'groupA', 'groupB', 'groupA', 'groupB', 'groupA', 'groupB'] + +# Bias Evaluation with Fairlearn +metric_frame = MetricFrame( + metrics=accuracy_score, + y_true=y_true, + y_pred=y_pred, + sensitive_features=sensitive_features +) + +print("Overall Accuracy:", metric_frame.overall) +print("Group Metrics:", metric_frame.by_group) + +# Output results to a CSV for visualization +group_metrics_df = pd.DataFrame(metric_frame.by_group) +group_metrics_df.to_csv('bias_metrics.csv', index=True) diff --git a/src/experiments/mlflow_tracking.py b/src/experiments/mlflow_tracking.py new file mode 100644 index 0000000..bcfa8ac --- /dev/null +++ b/src/experiments/mlflow_tracking.py @@ -0,0 +1,33 @@ +# File: mlflow_tracking.py +# Using MLflow to track model experiments + +import mlflow +import mlflow.sklearn +from sklearn.ensemble import RandomForestClassifier +from sklearn.datasets import load_iris +from sklearn.model_selection import train_test_split + +# Load data +data = load_iris() +X_train, X_test, y_train, y_test = train_test_split(data.data, data.target, test_size=0.2, random_state=42) + +# MLflow Experiment Tracking +with mlflow.start_run(): + # Model Training + model = RandomForestClassifier(n_estimators=100, random_state=42) + model.fit(X_train, y_train) + + # Log Parameters + mlflow.log_param("n_estimators", 100) + mlflow.log_param("random_state", 42) + + # Log Metrics + train_accuracy = model.score(X_train, y_train) + test_accuracy = model.score(X_test, y_test) + mlflow.log_metric("train_accuracy", train_accuracy) + mlflow.log_metric("test_accuracy", test_accuracy) + + # Log Model + mlflow.sklearn.log_model(model, "random_forest_model") + + print(f"Model saved with train accuracy: {train_accuracy:.2f} and test accuracy: {test_accuracy:.2f}") diff --git a/src/preprocessing/preprocess_data.py b/src/preprocessing/preprocess_data.py new file mode 100644 index 0000000..b6dd4f6 --- /dev/null +++ b/src/preprocessing/preprocess_data.py @@ -0,0 +1,37 @@ +import pandas as pd +import re +from sklearn.model_selection import train_test_split + +# Load original and augmented data +try: + original_data = pd.read_csv('data/raw/synthetic_data.csv') + augmented_data = pd.read_csv('data/processed/augmented_training_data.csv') +except FileNotFoundError: + print("Error: One or more of the input files not found. Make sure the paths are correct.") + exit() + +# Combine datasets +combined_data = pd.concat([original_data, augmented_data], ignore_index=True) + +# Basic text cleaning function +def clean_text(text): + text = re.sub(r'http\S+', '', text) # Remove URLs + text = re.sub(r'[^A-Za-z0-9 ]+', '', text) # Remove non-alphanumeric characters + text = re.sub(r'\s+', ' ', text).strip() # Remove extra spaces + return text + +# Apply text cleaning +combined_data['text'] = combined_data['text'].apply(clean_text) + +# Check for missing values and handle them +if combined_data.isnull().values.any(): + print("Warning: Missing values detected. Filling with empty strings.") + combined_data.fillna('', inplace=True) + +# Splitting the combined dataset into training and validation sets +train_data, val_data = train_test_split(combined_data, test_size=0.2, random_state=42) + +# Save processed datasets +train_data.to_csv('data/processed/train_data.csv', index=False) +val_data.to_csv('data/processed/val_data.csv', index=False) +print("Combined and processed datasets saved for training and validation.") \ No newline at end of file diff --git a/src/training/fine_tuning.py b/src/training/fine_tuning.py index 5ced3d8..53ddc63 100644 --- a/src/training/fine_tuning.py +++ b/src/training/fine_tuning.py @@ -1,43 +1,122 @@ -from transformers import AutoModelForCausalLM, AutoTokenizer, Trainer, TrainingArguments -from datasets import load_dataset - -def fine_tune_model(model_name, dataset_path, output_dir): - """ - Fine-tune a pre-trained language model on a given dataset. - """ - # Load model and tokenizer - model = AutoModelForCausalLM.from_pretrained(model_name) - tokenizer = AutoTokenizer.from_pretrained(model_name) - - # Load dataset - dataset = load_dataset("csv", data_files=dataset_path) - - # Tokenize the dataset - def tokenize(batch): - return tokenizer(batch["text"], truncation=True, padding="max_length") - - tokenized_dataset = dataset.map(tokenize, batched=True) - - # Define training arguments - training_args = TrainingArguments( - output_dir=output_dir, - evaluation_strategy="steps", - per_device_train_batch_size=8, - save_steps=500, - num_train_epochs=3 - ) - - # Define Trainer - trainer = Trainer( - model=model, - args=training_args, - train_dataset=tokenized_dataset["train"], - eval_dataset=tokenized_dataset["validation"], - ) - - # Train the model - trainer.train() - - # Save the model - model.save_pretrained(output_dir) - tokenizer.save_pretrained(output_dir) +# File: src/training/fine_tuning.py +# Fine-tuning Script with Option to Use Pre-trained Models + +import torch +from torch.optim import AdamW +from transformers import BertForSequenceClassification, BertTokenizer +from torch.utils.data import DataLoader, Dataset +import pandas as pd +from sklearn.model_selection import train_test_split + +# Load configuration +use_pretrained = True +model_name = 'bert-base-uncased' + +# Load Dataset +train_data = pd.read_csv('data/processed/train_data.csv') +val_data = pd.read_csv('data/processed/val_data.csv') + +# Tokenizer Setup +tokenizer = BertTokenizer.from_pretrained(model_name) + +# Custom Dataset Class +class CustomDataset(Dataset): + def __init__(self, texts, labels, tokenizer, max_length): + self.texts = texts + self.labels = labels + self.tokenizer = tokenizer + self.max_length = max_length + + def __len__(self): + return len(self.texts) + + def __getitem__(self, idx): + text = self.texts[idx] + label = self.labels[idx] + inputs = self.tokenizer.encode_plus( + text, + None, + add_special_tokens=True, + max_length=self.max_length, + padding='max_length', + return_token_type_ids=True, + truncation=True, + return_attention_mask=True, + return_tensors='pt' + ) + return { + 'input_ids': inputs['input_ids'].flatten(), + 'attention_mask': inputs['attention_mask'].flatten(), + 'labels': torch.tensor(label, dtype=torch.long) + } + +# Prepare DataLoader for Training +train_dataset = CustomDataset( + train_data['text'].values, + train_data['label'].values, + tokenizer, + max_length=128 +) +val_dataset = CustomDataset( + val_data['text'].values, + val_data['label'].values, + tokenizer, + max_length=128 +) + +train_loader = DataLoader(train_dataset, batch_size=16, shuffle=True) +val_loader = DataLoader(val_dataset, batch_size=16) + +# Load Model +if use_pretrained: + model = BertForSequenceClassification.from_pretrained(model_name, num_labels=2) +else: + model = BertForSequenceClassification(config=config) + +# Optimizer Setup +optimizer = AdamW(model.parameters(), lr=2e-5) + +# Training Loop +device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') +model.to(device) + +for epoch in range(3): + model.train() + for batch in train_loader: + input_ids = batch['input_ids'].to(device) + attention_mask = batch['attention_mask'].to(device) + labels = batch['labels'].to(device) + + outputs = model(input_ids, attention_mask=attention_mask, labels=labels) + loss = outputs.loss + + optimizer.zero_grad() + loss.backward() + optimizer.step() + + # Validation Loop + model.eval() + val_loss_total = 0 + correct_predictions = 0 + total = 0 + with torch.no_grad(): + for batch in val_loader: + input_ids = batch['input_ids'].to(device) + attention_mask = batch['attention_mask'].to(device) + labels = batch['labels'].to(device) + + outputs = model(input_ids, attention_mask=attention_mask, labels=labels) + val_loss_total += outputs.loss.item() + logits = outputs.logits + predictions = torch.argmax(logits, dim=-1) + correct_predictions += (predictions == labels).sum().item() + total += labels.size(0) + + val_loss_avg = val_loss_total / len(val_loader) + val_accuracy = correct_predictions / total + print(f"Epoch {epoch + 1} - Validation Loss: {val_loss_avg:.4f} - Accuracy: {val_accuracy:.4f}") + +# Save Fine-tuned Model +model.save_pretrained('model/fine_tuned_bert') +tokenizer.save_pretrained('model/fine_tuned_bert') +print("Fine-tuned model saved successfully.") diff --git a/src/training/retrain_model.py b/src/training/retrain_model.py new file mode 100644 index 0000000..58cd0e5 --- /dev/null +++ b/src/training/retrain_model.py @@ -0,0 +1,39 @@ +# File: retrain_model.py +# Automated model retraining based on user feedback + +import pandas as pd +import numpy as np +import joblib +from sklearn.model_selection import train_test_split +from sklearn.ensemble import RandomForestClassifier + +# Load feedback data +try: + feedback_data = pd.read_csv('feedback.csv') +except FileNotFoundError: + print("No feedback data available for retraining.") + exit() + +# Prepare training data +X = feedback_data['model-response'] +y = feedback_data['rating'] + +# Feature extraction - Example using simple vectorization +from sklearn.feature_extraction.text import CountVectorizer +vectorizer = CountVectorizer() +X_vect = vectorizer.fit_transform(X) + +# Split data into train and validation sets +X_train, X_val, y_train, y_val = train_test_split(X_vect, y, test_size=0.2, random_state=42) + +# Retrain the model +model = RandomForestClassifier(n_estimators=100, random_state=42) +model.fit(X_train, y_train) + +# Evaluate model +val_accuracy = model.score(X_val, y_val) +print(f"Validation Accuracy after Retraining: {val_accuracy:.2f}") + +# Save the retrained model +joblib.dump(model, 'model/retrained_model.pkl') +print("Model retrained and saved successfully.") diff --git a/src/training/transfer_learning.py b/src/training/transfer_learning.py new file mode 100644 index 0000000..7149d9c --- /dev/null +++ b/src/training/transfer_learning.py @@ -0,0 +1,86 @@ +# Transfer Learning Script +import torch +from transformers import BertForSequenceClassification, BertTokenizer +from torch.optim import AdamW +from sklearn.model_selection import train_test_split +from torch.utils.data import DataLoader, Dataset +import pandas as pd + +# Load pre-trained model and tokenizer from HuggingFace +model_name = "bert-base-uncased" +model = BertForSequenceClassification.from_pretrained(model_name, num_labels=2) +tokenizer = BertTokenizer.from_pretrained(model_name) + +# Custom Dataset class +class CustomDataset(Dataset): + def __init__(self, texts, labels, tokenizer, max_length): + self.texts = texts + self.labels = labels + self.tokenizer = tokenizer + self.max_length = max_length + + def __len__(self): + return len(self.texts) + + def __getitem__(self, idx): + text = self.texts[idx] + label = self.labels[idx] + inputs = self.tokenizer.encode_plus( + text, + None, + add_special_tokens=True, + max_length=self.max_length, + padding='max_length', + return_token_type_ids=True, + truncation=True, + return_attention_mask=True, + return_tensors='pt' + ) + return { + 'input_ids': inputs['input_ids'].flatten(), + 'attention_mask': inputs['attention_mask'].flatten(), + 'labels': torch.tensor(label, dtype=torch.long) + } + +# Load dataset +data = pd.read_csv('data/processed/custom_training_data.csv') +X = data['text'].values +y = data['label'].values + +# Split data +X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, random_state=42) + +# Create DataLoader +train_dataset = CustomDataset(X_train, y_train, tokenizer, max_length=128) +val_dataset = CustomDataset(X_val, y_val, tokenizer, max_length=128) + +train_loader = DataLoader(train_dataset, batch_size=16, shuffle=True) +val_loader = DataLoader(val_dataset, batch_size=16) + +# Define optimizer +optimizer = AdamW(model.parameters(), lr=2e-5) + +# Training loop +device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') +model.to(device) + +for epoch in range(3): + model.train() + for batch in train_loader: + input_ids = batch['input_ids'].to(device) + attention_mask = batch['attention_mask'].to(device) + labels = batch['labels'].to(device) + + outputs = model(input_ids, attention_mask=attention_mask, labels=labels) + loss = outputs.loss + + optimizer.zero_grad() + loss.backward() + optimizer.step() + + print(f"Epoch {epoch + 1} completed.") + +# Save fine-tuned model +model.save_pretrained('model/fine_tuned_bert') +tokenizer.save_pretrained('model/fine_tuned_bert') +print("Fine-tuned model saved.") diff --git a/tests/e2e/ui_tests.spec.js b/tests/e2e/ui_tests.spec.js new file mode 100644 index 0000000..796179c --- /dev/null +++ b/tests/e2e/ui_tests.spec.js @@ -0,0 +1,20 @@ +// Cypress End-to-End Tests for UI + +describe('LLM Alignment Assistant UI Tests', () => { + it('Loads the Home Page and Checks Dark Mode Toggle', () => { + cy.visit('http://localhost:5000'); + cy.get('#dark-mode-toggle').click(); + cy.get('body').should('have.class', 'dark-mode'); + cy.get('#dark-mode-toggle').click(); + cy.get('body').should('not.have.class', 'dark-mode'); + }); + + it('Submits User Feedback', () => { + cy.visit('http://localhost:5000/feedback'); + cy.get('#rating').type('5'); + cy.get('#comments').type('The response was very helpful.'); + cy.get('form').submit(); + cy.contains('Thank you for your feedback!'); + }); + }); + \ No newline at end of file diff --git a/tests/load_testing/locustfile.py b/tests/load_testing/locustfile.py new file mode 100644 index 0000000..70b7102 --- /dev/null +++ b/tests/load_testing/locustfile.py @@ -0,0 +1,16 @@ +from locust import HttpUser, task, between + +class LoadTesting(HttpUser): + wait_time = between(1, 5) + + @task + def test_health_endpoint(self): + self.client.get("/api/health") + + @task + def test_feedback_submission(self): + self.client.post("/submit-feedback", { + "model-response": "Example response to rate", + "rating": "5", + "comments": "Great response!" + })