| """ |
| Feature Extraction Module for ContextFlow RL Model |
| |
| This module extracts the 64-dimensional state vector used by the RL model |
| for doubt prediction. |
| |
| State Vector Structure (64 features): |
| βββ Topic Embedding (32 dims) - TF-IDF of learning topic |
| βββ Progress (1 dim) - Session progress 0.0-1.0 |
| βββ Confusion Signals (16 dims) - Behavioral indicators |
| βββ Gesture Signals (14 dims) - Hand gesture frequencies |
| βββ Time Spent (1 dim) - Normalized session time |
| """ |
|
|
| import numpy as np |
| from sklearn.feature_extraction.text import TfidfVectorizer |
| from typing import Dict, List, Optional |
|
|
|
|
| class FeatureExtractor: |
| """Extract 64-dimensional state vector for RL model""" |
| |
| def __init__(self): |
| self.state_dim = 64 |
| |
| |
| self.topic_vectorizer = TfidfVectorizer(max_features=32) |
| self._fit_topic_vectorizer() |
| |
| |
| self.confusion_signal_names = [ |
| 'mouse_hesitation', 'scroll_reversals', 'time_on_page', |
| 'eye_tracking_x', 'eye_tracking_y', 'page_scrolling', |
| 'click_frequency', 'back_button', 'tab_switches', |
| 'copy_attempts', 'zoom_level', 'scroll_speed', |
| 'reading_pauses', 'search_usage', 'bookmark_usage', 'print_usage' |
| ] |
| |
| self.gesture_signal_names = [ |
| 'pinch', 'swipe_up', 'swipe_down', 'swipe_left', 'swipe_right', |
| 'two_finger_swipe', 'point', 'wave', 'thumbs_up', 'thumbs_down', |
| 'fist', 'open_palm', 'rotation', 'zoom_gesture' |
| ] |
| |
| def _fit_topic_vectorizer(self): |
| """Fit TF-IDF on common learning topics""" |
| topics = [ |
| 'machine learning', 'deep learning', 'neural networks', |
| 'python programming', 'data science', 'statistics', |
| 'linear algebra', 'calculus', 'probability', |
| 'natural language processing', 'computer vision', |
| 'reinforcement learning', 'supervised learning', 'unsupervised learning', |
| 'classification', 'regression', 'clustering', |
| 'backpropagation', 'gradient descent', 'optimization', |
| 'transformers', 'attention mechanism', 'bert', 'gpt', |
| 'cnn', 'rnn', 'lstm', 'gru', |
| 'overfitting', 'underfitting', 'regularization', |
| 'cross validation', 'hyperparameters', 'training' |
| ] |
| self.topic_vectorizer.fit(topics) |
| |
| def extract_topic_embedding(self, topic: str) -> np.ndarray: |
| """Extract 32-dimensional topic embedding""" |
| topic_vec = self.topic_vectorizer.transform([topic.lower()]).toarray()[0] |
| |
| |
| if len(topic_vec) < 32: |
| topic_vec = np.pad(topic_vec, (0, 32 - len(topic_vec))) |
| |
| return topic_vec[:32] |
| |
| def extract_confusion_signals(self, signals: Dict) -> np.ndarray: |
| """ |
| Extract 16-dimensional confusion signal vector |
| |
| Args: |
| signals: Dict with keys like 'mouse_hesitation', 'scroll_reversals', etc. |
| |
| Returns: |
| Normalized confusion signals (0.0-1.0) |
| """ |
| result = np.zeros(16) |
| |
| for i, name in enumerate(self.confusion_signal_names): |
| if name in signals: |
| value = float(signals[name]) |
| |
| if name == 'mouse_hesitation': |
| result[i] = min(value / 5.0, 1.0) |
| elif name == 'scroll_reversals': |
| result[i] = min(value / 10.0, 1.0) |
| elif name == 'time_on_page': |
| result[i] = min(value / 300.0, 1.0) |
| elif 'eye_tracking' in name: |
| result[i] = min(abs(value), 1.0) |
| else: |
| result[i] = min(value, 1.0) |
| |
| return result |
| |
| def extract_gesture_signals(self, gestures: Dict) -> np.ndarray: |
| """ |
| Extract 14-dimensional gesture signal vector |
| |
| Args: |
| gestures: Dict with gesture counts or frequencies |
| |
| Returns: |
| Normalized gesture signals (0.0-1.0) |
| """ |
| result = np.zeros(14) |
| |
| for i, name in enumerate(self.gesture_signal_names): |
| if name in gestures: |
| value = float(gestures[name]) |
| result[i] = min(value / 20.0, 1.0) |
| |
| return result |
| |
| def extract_state( |
| self, |
| topic: str, |
| progress: float, |
| confusion_signals: Dict, |
| gesture_signals: Dict, |
| time_spent: float |
| ) -> np.ndarray: |
| """ |
| Extract complete 64-dimensional state vector |
| |
| Args: |
| topic: Learning topic string |
| progress: Session progress (0.0-1.0) |
| confusion_signals: Dict of behavioral signals |
| gesture_signals: Dict of gesture counts |
| time_spent: Time spent in seconds |
| |
| Returns: |
| 64-dimensional state vector |
| """ |
| |
| topic_emb = self.extract_topic_embedding(topic) |
| |
| |
| progress_arr = np.array([np.clip(progress, 0.0, 1.0)]) |
| |
| |
| confusion_arr = self.extract_confusion_signals(confusion_signals) |
| |
| |
| gesture_arr = self.extract_gesture_signals(gesture_signals) |
| |
| |
| time_arr = np.array([min(time_spent / 1800.0, 1.0)]) |
| |
| |
| state = np.concatenate([ |
| topic_emb, |
| progress_arr, |
| confusion_arr, |
| gesture_arr, |
| time_arr |
| ]) |
| |
| assert len(state) == 64, f"State vector should be 64 dims, got {len(state)}" |
| |
| return state |
| |
| def get_feature_names(self) -> List[str]: |
| """Get interpretable feature names""" |
| names = [] |
| |
| |
| for i in range(32): |
| names.append(f"topic_{i}") |
| |
| names.append("progress") |
| |
| |
| names.extend(self.confusion_signal_names) |
| |
| |
| names.extend(self.gesture_signal_names) |
| |
| names.append("time_spent") |
| |
| return names |
|
|
|
|
| def create_sample_state() -> np.ndarray: |
| """Create a sample state vector for testing""" |
| extractor = FeatureExtractor() |
| |
| return extractor.extract_state( |
| topic="machine learning", |
| progress=0.5, |
| confusion_signals={ |
| 'mouse_hesitation': 2.5, |
| 'scroll_reversals': 4, |
| 'time_on_page': 120, |
| 'click_frequency': 8, |
| 'back_button': 2 |
| }, |
| gesture_signals={ |
| 'pinch': 5, |
| 'swipe_right': 3, |
| 'point': 2 |
| }, |
| time_spent=300 |
| ) |
|
|
|
|
| if __name__ == "__main__": |
| |
| extractor = FeatureExtractor() |
| state = create_sample_state() |
| |
| print(f"State vector shape: {state.shape}") |
| print(f"Sum of features: {state.sum():.4f}") |
| print(f"Features > 0: {(state > 0).sum()}") |
|
|