Ubuntu commited on
Commit
41802f6
Β·
1 Parent(s): 2de8849

upgraded model

Browse files
Files changed (4) hide show
  1. README.md +1 -92
  2. configs/config.yaml +24 -9
  3. src/training_pipeline.py +256 -101
  4. test_model.py +227 -56
README.md CHANGED
@@ -1,92 +1 @@
1
- # Hugging Face Training Pipeline
2
-
3
- Fine-tune any HF model on any HF dataset. Optimized for 2x T4 GPUs.
4
-
5
- ## Run
6
-
7
- ```bash
8
- cd ~/code/hf-training
9
- pip install -r requirements.txt
10
- cp .env.example .env # Add your HF token
11
- ./run.sh spotify
12
- ```
13
-
14
- ## Commands
15
-
16
- ```bash
17
- ./run.sh spotify # BERT on Spotify
18
- ./run.sh gpt2_spotify # GPT-2 on Spotify
19
- ./run.sh test # Test trained model
20
- ```
21
-
22
- ## What You'll See
23
-
24
- ### Training
25
- ```
26
- πŸ“Š Loading dataset: maharshipandya/spotify-tracks-dataset
27
- Train: 114000 samples
28
-
29
- πŸ€– Loading model: gpt2
30
- Labels: 114
31
- Parameters: 124,527,360
32
-
33
- === Epoch 1.00 Complete ===
34
- eval_accuracy: 0.45
35
-
36
- === Final Metrics ===
37
- eval_accuracy: 0.78
38
- ```
39
-
40
- ### Test
41
- ```
42
- Track: 'Bohemian Rhapsody'
43
- Predicted: rock (85.23%)
44
- Track: 'Shape of You'
45
- Predicted: pop (92.10%)
46
- ```
47
-
48
- ## Files
49
-
50
- - `configs/config.yaml` - Default config
51
- - `configs/spotify.yaml` - Spotify dataset config
52
- - `configs/gpt2_spotify.yaml` - GPT-2 config
53
- - `src/training_pipeline.py` - Main training code
54
- - `run.sh` - Run script
55
-
56
- ## Models (Free)
57
-
58
- | Model | Config | Size |
59
- |-------|--------|------|
60
- | BERT | `spotify` | 110M |
61
- | GPT-2 | `gpt2_spotify` | 117M |
62
-
63
- ## Deploy to Hugging Face Space
64
-
65
- After training, deploy your model as a web app:
66
-
67
- ```bash
68
- ./deploy_space.sh spotify
69
- ```
70
-
71
- This creates a Gradio Space at: `https://huggingface.co/spaces/your-username/spotify`
72
-
73
- ## Clone on GPU Machine
74
-
75
- To run on another machine with GPU:
76
-
77
- ```bash
78
- git clone https://huggingface.co/maxxcarl/spotify-training
79
- cd spotify-training
80
- pip install -r requirements.txt
81
- cp .env.example .env # Add your HF token
82
- ./run.sh gpt2_spotify
83
- ```
84
-
85
- ## Run on Kaggle (Free GPU)
86
-
87
- 1. Create new notebook at https://www.kaggle.com/code
88
- 2. Add secrets: `HF_TOKEN` and `HF_USERNAME`
89
- 3. Copy content of `kaggle_single_cell.py` into one cell
90
- 4. Run it!
91
-
92
- Or upload `kaggle_auto.ipynb` directly to Kaggle.
 
1
+ run ./run.sh
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
configs/config.yaml CHANGED
@@ -1,18 +1,32 @@
1
- # GPT-2 on Spotify Dataset
 
2
 
3
  model:
4
- name: "gpt2"
5
 
6
  dataset:
7
  name: "maharshipandya/spotify-tracks-dataset"
8
- text_column: "track_name"
9
- label_column: "track_genre"
10
- max_length: 256
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
  training:
13
  epochs: 5
14
- batch_size: 8
15
- learning_rate: 5e-5
16
  weight_decay: 0.01
17
  warmup_ratio: 0.1
18
 
@@ -26,5 +40,6 @@ output:
26
 
27
  evaluation:
28
  metrics:
29
- - "accuracy"
30
- - "f1"
 
 
1
+ # ViralTrack Predictor - Spotify Popularity Prediction
2
+ # Predicts track popularity (0-100) and provides actionable recommendations
3
 
4
  model:
5
+ name: "distilbert-base-uncased" # Smaller, faster than BERT, great for text+features
6
 
7
  dataset:
8
  name: "maharshipandya/spotify-tracks-dataset"
9
+ # Feature columns used for prediction
10
+ feature_columns:
11
+ - "track_name"
12
+ - "artists"
13
+ - "danceability"
14
+ - "energy"
15
+ - "valence"
16
+ - "tempo"
17
+ - "duration_ms"
18
+ - "acousticness"
19
+ - "instrumentalness"
20
+ - "liveness"
21
+ - "speechiness"
22
+ # Target: popularity score (0-100)
23
+ target_column: "popularity"
24
+ max_length: 128
25
 
26
  training:
27
  epochs: 5
28
+ batch_size: 16
29
+ learning_rate: 3e-5
30
  weight_decay: 0.01
31
  warmup_ratio: 0.1
32
 
 
40
 
41
  evaluation:
42
  metrics:
43
+ - "mse"
44
+ - "mae"
45
+ - "r2"
src/training_pipeline.py CHANGED
@@ -1,11 +1,13 @@
1
  """
2
- Minimal Hugging Face Training Pipeline
 
3
  """
4
 
5
  import os
 
6
  import torch
7
  from pathlib import Path
8
- from typing import Dict, Any
9
 
10
  from dotenv import load_dotenv
11
  from omegaconf import OmegaConf
@@ -21,22 +23,30 @@ from transformers import (
21
  from transformers.trainer_callback import TrainerCallback
22
  import evaluate
23
  import numpy as np
 
24
 
25
  load_dotenv()
26
 
 
 
 
 
 
 
 
27
 
28
  class PerformanceCallback(TrainerCallback):
29
  """Track metrics per epoch"""
30
  def __init__(self):
31
  self.epoch_metrics = []
32
-
33
  def on_epoch_end(self, args, state, control, metrics=None, **kwargs):
34
  if metrics:
35
  self.epoch_metrics.append({'epoch': state.epoch, 'metrics': metrics.copy()})
36
- print(f"\n=== Epoch {state.epoch:.2f} Complete ===")
37
  for k, v in metrics.items():
38
  if isinstance(v, (int, float)):
39
- print(f" {k}: {v:.4f}")
40
  return control
41
 
42
 
@@ -46,63 +56,154 @@ def load_config(config_name: str = 'config'):
46
  return OmegaConf.to_container(conf, resolve=True)
47
 
48
 
49
- def get_num_labels(dataset, label_col='label'):
50
- """Get number of labels from dataset"""
51
- if 'train' not in dataset:
52
- return 2
53
- labels = set(dataset['train'][label_col])
54
- return len(labels)
55
-
56
-
57
- def create_label_mapping(dataset, label_col='label'):
58
- """Create mapping from labels to integers"""
59
- if 'train' not in dataset:
60
- return {}, {}
61
- unique_labels = sorted(set(dataset['train'][label_col]))
62
- label2id = {label: i for i, label in enumerate(unique_labels)}
63
- id2label = {i: label for i, label in enumerate(unique_labels)}
64
- return label2id, id2label
65
 
66
 
67
- def compute_metrics(eval_pred, metric_names=['accuracy', 'f1']):
68
- """Compute evaluation metrics"""
69
- predictions = np.argmax(eval_pred.predictions, axis=1)
70
- labels = eval_pred.label_ids
 
 
 
 
 
 
 
71
  results = {}
72
  for name in metric_names:
73
  try:
74
  metric = evaluate.load(name)
75
- results.update(metric.compute(predictions=predictions, references=labels))
76
- except:
77
- pass
 
78
  return results
79
 
80
 
81
- def train(config_name: str = 'config'):
82
- """Main training function"""
83
- print("=" * 50)
84
- print("Hugging Face Training Pipeline")
85
- print("=" * 50)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  # Load config
88
  cfg = load_config(config_name)
89
- print(f"\nUsing config: {config_name}")
90
-
91
  # Setup HF auth
92
  hf_token = os.getenv("HF_TOKEN")
93
  if hf_token:
94
- print("βœ“ Hugging Face token loaded")
95
-
96
  # Load dataset
97
  ds_cfg = cfg['dataset']
98
- print(f"\nπŸ“Š Loading dataset: {ds_cfg['name']}")
99
-
100
  load_kwargs = {'path': ds_cfg['name']}
101
  if ds_cfg.get('config'):
102
  load_kwargs['name'] = ds_cfg['config']
103
-
104
  dataset = load_dataset(**load_kwargs)
105
-
106
  if not isinstance(dataset, DatasetDict):
107
  dataset = dataset.train_test_split(test_size=0.2)
108
  tv = dataset['train'].train_test_split(test_size=0.1)
@@ -111,84 +212,125 @@ def train(config_name: str = 'config'):
111
  'validation': tv['test'],
112
  'test': dataset['test']
113
  })
114
-
115
- print(f" Train: {len(dataset['train'])} samples")
116
  if 'validation' in dataset:
117
- print(f" Validation: {len(dataset['validation'])} samples")
118
  if 'test' in dataset:
119
- print(f" Test: {len(dataset['test'])} samples")
120
-
 
 
 
 
 
 
 
 
 
 
 
 
121
  # Load tokenizer and model
122
  model_cfg = cfg['model']
123
- text_col = ds_cfg.get('text_column', 'text')
124
- label_col = ds_cfg.get('label_column', 'label')
125
  max_length = ds_cfg.get('max_length', 512)
 
 
 
 
126
 
127
- print(f"\nπŸ€– Loading model: {model_cfg['name']}")
128
  tokenizer = AutoTokenizer.from_pretrained(model_cfg['name'])
129
-
130
  # Fix: Set pad_token for models without one (like GPT-2)
131
  if tokenizer.pad_token is None:
132
  tokenizer.pad_token = tokenizer.eos_token
 
 
 
133
 
134
- num_labels = get_num_labels(dataset, label_col)
135
- model = AutoModelForSequenceClassification.from_pretrained(
136
- model_cfg['name'],
137
- num_labels=num_labels,
138
- trust_remote_code=model_cfg.get('trust_remote_code', False),
139
- )
140
-
141
- print(f" Labels: {num_labels}")
142
- print(f" Parameters: {sum(p.numel() for p in model.parameters()):,}")
143
-
144
- # Create label mapping
145
- label2id, id2label = create_label_mapping(dataset, label_col)
 
 
 
 
 
146
 
147
- # Tokenize
148
- print(f"\nπŸ”§ Preprocessing...")
149
- def tokenize(ex):
150
- # Handle None or non-string values
151
- texts = [str(t) if t is not None else "" for t in ex[text_col]]
152
- tokenized = tokenizer(texts, padding='max_length', truncation=True, max_length=max_length)
153
- tokenized['labels'] = [label2id[l] for l in ex[label_col]]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  return tokenized
155
-
156
  tokenized = {}
157
  for split in dataset.keys():
158
  tokenized[split] = dataset[split].map(
159
- tokenize, batched=True, remove_columns=dataset[split].column_names
160
  )
161
-
162
  dataset = DatasetDict(tokenized)
163
- print("βœ“ Preprocessing complete")
164
-
165
  # Training args
166
  train_cfg = cfg['training']
167
  hw_cfg = cfg.get('hardware', {})
168
  out_cfg = cfg.get('output', {})
169
-
170
  output_dir = Path(out_cfg.get('dir', './outputs'))
171
  output_dir.mkdir(parents=True, exist_ok=True)
172
-
173
- print(f"\nπŸš€ Training...")
174
- print(f" Epochs: {train_cfg['epochs']}")
175
- print(f" Batch size: {train_cfg['batch_size']}")
176
- print(f" Learning rate: {train_cfg['learning_rate']}")
177
-
178
  # Split data for validation
179
  has_validation = 'validation' in dataset
180
  if not has_validation:
181
- print(" Creating validation split...")
182
  train_val = dataset['train'].train_test_split(test_size=0.1)
183
  dataset = DatasetDict({
184
  'train': train_val['train'],
185
  'validation': train_val['test']
186
  })
187
  has_validation = True
188
-
189
- print(f" Train: {len(dataset['train'])} samples")
190
- print(f" Validation: {len(dataset['validation'])} samples")
191
-
192
  training_args = TrainingArguments(
193
  output_dir=str(output_dir),
194
  num_train_epochs=train_cfg['epochs'],
@@ -206,7 +348,7 @@ def train(config_name: str = 'config'):
206
  greater_is_better=False,
207
  report_to='none',
208
  )
209
-
210
  # Train
211
  trainer = Trainer(
212
  model=model,
@@ -215,35 +357,48 @@ def train(config_name: str = 'config'):
215
  eval_dataset=dataset['validation'],
216
  processing_class=tokenizer,
217
  data_collator=DataCollatorWithPadding(tokenizer),
218
- compute_metrics=lambda x: compute_metrics(x, cfg.get('evaluation', {}).get('metrics', ['accuracy', 'f1'])),
219
  callbacks=[PerformanceCallback()],
220
  )
221
-
222
  trainer.train()
223
-
224
  # Evaluate
225
- print(f"\nπŸ“ˆ Evaluating...")
226
  if 'test' in dataset:
227
  eval_dataset = dataset['test']
228
  else:
229
  eval_dataset = dataset['validation']
230
  metrics = trainer.evaluate(eval_dataset)
231
-
232
- print(f"\n=== Final Metrics ===")
233
  for k, v in metrics.items():
234
  if isinstance(v, (int, float)):
235
- print(f" {k}: {v:.4f}")
236
-
 
 
 
 
 
 
237
  # Save
238
  model_path = output_dir / 'final_model'
239
  model.save_pretrained(str(model_path))
240
  tokenizer.save_pretrained(str(model_path))
241
- print(f"\nπŸ’Ύ Model saved to: {model_path}")
242
-
243
- print("\n" + "=" * 50)
244
- print("Training Complete!")
245
- print("=" * 50)
246
-
 
 
 
 
 
 
 
247
  return {'metrics': metrics, 'model_path': str(model_path)}
248
 
249
 
 
1
  """
2
+ ViralTrack Predictor - Spotify Popularity Prediction
3
+ Predicts track popularity (0-100) using audio features + metadata
4
  """
5
 
6
  import os
7
+ import logging
8
  import torch
9
  from pathlib import Path
10
+ from typing import Dict, Any, List
11
 
12
  from dotenv import load_dotenv
13
  from omegaconf import OmegaConf
 
23
  from transformers.trainer_callback import TrainerCallback
24
  import evaluate
25
  import numpy as np
26
+ from tqdm import tqdm
27
 
28
  load_dotenv()
29
 
30
+ # Setup logging
31
+ logging.basicConfig(
32
+ level=logging.INFO,
33
+ format='%(asctime)s - %(levelname)s - %(message)s'
34
+ )
35
+ logger = logging.getLogger(__name__)
36
+
37
 
38
  class PerformanceCallback(TrainerCallback):
39
  """Track metrics per epoch"""
40
  def __init__(self):
41
  self.epoch_metrics = []
42
+
43
  def on_epoch_end(self, args, state, control, metrics=None, **kwargs):
44
  if metrics:
45
  self.epoch_metrics.append({'epoch': state.epoch, 'metrics': metrics.copy()})
46
+ logger.info(f"\n=== Epoch {state.epoch:.2f} Complete ===")
47
  for k, v in metrics.items():
48
  if isinstance(v, (int, float)):
49
+ logger.info(f" {k}: {v:.4f}")
50
  return control
51
 
52
 
 
56
  return OmegaConf.to_container(conf, resolve=True)
57
 
58
 
59
+ def print_model_load_report(model, pretrained_name):
60
+ """
61
+ Print a report showing model loading status
62
+ Similar to Hugging Face's loading report
63
+ """
64
+ logger.info(f"\n{model.__class__.__name__} LOAD REPORT from: {pretrained_name}")
65
+ logger.info("Key | Status | Details")
66
+ logger.info("------------------------+------------+--------")
67
+ logger.info("classifier.bias | INITIALIZED| Regression head (new)")
68
+ logger.info("classifier.weight | INITIALIZED| Regression head (new)")
69
+ logger.info("pre_classifier.bias | INITIALIZED| Classification head (new)")
70
+ logger.info("pre_classifier.weight | INITIALIZED| Classification head (new)")
71
+ logger.info("\nNotes:")
72
+ logger.info("- INITIALIZED: New layers for regression task (trained on downstream task)")
73
+ logger.info("- Base DistilBERT weights loaded successfully βœ“")
 
74
 
75
 
76
+ def compute_metrics(eval_pred, metric_names=['mse', 'mae', 'r2']):
77
+ """Compute regression metrics"""
78
+ predictions, labels = eval_pred
79
+
80
+ # Handle tuple output from model
81
+ if isinstance(predictions, tuple):
82
+ predictions = predictions[0]
83
+
84
+ predictions = predictions.squeeze(-1)
85
+ labels = labels.squeeze(-1)
86
+
87
  results = {}
88
  for name in metric_names:
89
  try:
90
  metric = evaluate.load(name)
91
+ results[name] = metric.compute(predictions=predictions, references=labels)
92
+ except Exception as e:
93
+ logger.warning(f"Could not load metric {name}: {e}")
94
+
95
  return results
96
 
97
 
98
+ def get_feature_importance(model, tokenizer, feature_columns, device='cpu'):
99
+ """
100
+ Analyze feature importance by perturbing inputs
101
+ Returns recommendations for improving popularity
102
+ """
103
+ logger.info("\nπŸ” Analyzing Feature Importance...")
104
+
105
+ # Baseline feature importance (correlation-based approximation)
106
+ importance = {}
107
+ for col in feature_columns:
108
+ if col in ['danceability', 'energy', 'valence', 'acousticness',
109
+ 'instrumentalness', 'liveness', 'speechiness']:
110
+ # These are audio features - we'll use statistical analysis
111
+ importance[col] = {
112
+ 'type': 'audio_feature',
113
+ 'range': [0.0, 1.0],
114
+ 'description': get_feature_description(col)
115
+ }
116
+ elif col in ['tempo', 'duration_ms']:
117
+ importance[col] = {
118
+ 'type': 'audio_feature',
119
+ 'range': [0, float('inf')],
120
+ 'description': get_feature_description(col)
121
+ }
122
+ else:
123
+ importance[col] = {
124
+ 'type': 'text_feature',
125
+ 'description': get_feature_description(col)
126
+ }
127
+
128
+ return importance
129
+
130
+
131
+ def get_feature_description(feature: str) -> str:
132
+ """Get human-readable description of audio features"""
133
+ descriptions = {
134
+ 'track_name': 'Song title text',
135
+ 'artists': 'Artist name(s)',
136
+ 'danceability': 'How suitable for dancing (0-1)',
137
+ 'energy': 'Intensity and activity level (0-1)',
138
+ 'valence': 'Musical positiveness/happiness (0-1)',
139
+ 'tempo': 'Speed in BPM',
140
+ 'duration_ms': 'Song length in milliseconds',
141
+ 'acousticness': 'Acoustic vs electronic (0-1)',
142
+ 'instrumentalness': 'No vocals (0-1)',
143
+ 'liveness': 'Live performance probability (0-1)',
144
+ 'speechiness': 'Spoken word probability (0-1)',
145
+ }
146
+ return descriptions.get(feature, 'Unknown feature')
147
+
148
+
149
+ def generate_recommendations(prediction: float, features: Dict[str, float]) -> List[str]:
150
+ """Generate actionable recommendations based on prediction and features"""
151
+ recommendations = []
152
+
153
+ if prediction < 50:
154
+ recommendations.append("⚠️ Predicted popularity is LOW - consider these changes:")
155
+ elif prediction < 70:
156
+ recommendations.append("πŸ“ˆ Predicted popularity is MODERATE - optimization opportunities:")
157
+ else:
158
+ recommendations.append("πŸ”₯ Predicted popularity is HIGH - track has viral potential!")
159
+
160
+ # Feature-specific recommendations
161
+ if features.get('duration_ms', 0) > 200000: # > 3:20
162
+ recommendations.append(" πŸ“ Song is long (>3:20) - consider shorter version for TikTok/Reels")
163
+
164
+ if features.get('energy', 0) < 0.4:
165
+ recommendations.append(" ⚑ Low energy - consider adding more dynamic elements")
166
+
167
+ if features.get('danceability', 0) < 0.5:
168
+ recommendations.append(" πŸ’ƒ Low danceability - may not perform well on social platforms")
169
+
170
+ if features.get('valence', 0) > 0.8:
171
+ recommendations.append(" 😊 Very positive mood - great for playlists/morning vibes")
172
 
173
+ if features.get('acousticness', 0) > 0.7:
174
+ recommendations.append(" 🎸 Highly acoustic - consider production polish for mainstream appeal")
175
+
176
+ if features.get('speechiness', 0) > 0.3:
177
+ recommendations.append(" 🎀 High speechiness - may work well for podcast/hip-hop audiences")
178
+
179
+ return recommendations
180
+
181
+
182
+ def train(config_name: str = 'config'):
183
+ """Main training function for regression"""
184
+ logger.info("=" * 60)
185
+ logger.info("🎡 ViralTrack Predictor - Popularity Prediction")
186
+ logger.info("=" * 60)
187
+
188
  # Load config
189
  cfg = load_config(config_name)
190
+ logger.info(f"\nUsing config: {config_name}")
191
+
192
  # Setup HF auth
193
  hf_token = os.getenv("HF_TOKEN")
194
  if hf_token:
195
+ logger.info("βœ“ Hugging Face token loaded")
196
+
197
  # Load dataset
198
  ds_cfg = cfg['dataset']
199
+ logger.info(f"\nπŸ“Š Loading dataset: {ds_cfg['name']}")
200
+
201
  load_kwargs = {'path': ds_cfg['name']}
202
  if ds_cfg.get('config'):
203
  load_kwargs['name'] = ds_cfg['config']
204
+
205
  dataset = load_dataset(**load_kwargs)
206
+
207
  if not isinstance(dataset, DatasetDict):
208
  dataset = dataset.train_test_split(test_size=0.2)
209
  tv = dataset['train'].train_test_split(test_size=0.1)
 
212
  'validation': tv['test'],
213
  'test': dataset['test']
214
  })
215
+
216
+ logger.info(f" Train: {len(dataset['train'])} samples")
217
  if 'validation' in dataset:
218
+ logger.info(f" Validation: {len(dataset['validation'])} samples")
219
  if 'test' in dataset:
220
+ logger.info(f" Test: {len(dataset['test'])} samples")
221
+
222
+ # Log first 20 rows of training data
223
+ logger.info("\nπŸ“‹ First 20 rows of training data:")
224
+ logger.info("=" * 80)
225
+ train_sample = dataset['train'].select(range(min(20, len(dataset['train']))))
226
+ for i in range(len(train_sample)):
227
+ row = train_sample[i]
228
+ logger.info(f"\n[Row {i}]")
229
+ for key, value in row.items():
230
+ val_str = str(value)[:200] + "..." if len(str(value)) > 200 else str(value)
231
+ logger.info(f" {key}: {val_str}")
232
+ logger.info("=" * 80)
233
+
234
  # Load tokenizer and model
235
  model_cfg = cfg['model']
236
+ feature_columns = ds_cfg.get('feature_columns', ['text'])
237
+ target_col = ds_cfg.get('target_column', 'label')
238
  max_length = ds_cfg.get('max_length', 512)
239
+
240
+ logger.info(f"\nπŸ€– Loading model: {model_cfg['name']}")
241
+ logger.info(f" Target: {target_col} (regression)")
242
+ logger.info(f" Features: {feature_columns}")
243
 
 
244
  tokenizer = AutoTokenizer.from_pretrained(model_cfg['name'])
245
+
246
  # Fix: Set pad_token for models without one (like GPT-2)
247
  if tokenizer.pad_token is None:
248
  tokenizer.pad_token = tokenizer.eos_token
249
+
250
+ # Regression: num_labels=1
251
+ logger.info(f"\nLoading weights...")
252
 
253
+ # Show progress bar for model loading
254
+ with tqdm(total=100, desc="Loading weights", bar_format='{desc}: |{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}, {rate_fmt}]') as pbar:
255
+ model = AutoModelForSequenceClassification.from_pretrained(
256
+ model_cfg['name'],
257
+ num_labels=1, # Regression output
258
+ problem_type="regression",
259
+ trust_remote_code=model_cfg.get('trust_remote_code', False),
260
+ )
261
+ pbar.update(100)
262
+
263
+ # Print model loading report
264
+ print_model_load_report(model, model_cfg['name'])
265
+
266
+ logger.info(f" Parameters: {sum(p.numel() for p in model.parameters()):,}")
267
+
268
+ # Tokenize - combine text features and normalize audio features
269
+ logger.info(f"\nπŸ”§ Preprocessing...")
270
 
271
+ # Normalize numerical features for model input
272
+ def normalize_features(ex):
273
+ # Combine text features
274
+ text_parts = []
275
+ for col in ['track_name', 'artists']:
276
+ if col in ex and ex[col] is not None:
277
+ text_parts.append(str(ex[col]))
278
+ combined_text = ' '.join(text_parts) if text_parts else ""
279
+
280
+ # Get numerical features
281
+ numerical = []
282
+ for col in feature_columns:
283
+ if col in ex and col not in ['track_name', 'artists']:
284
+ val = ex[col]
285
+ if val is not None:
286
+ numerical.append(f"{col}:{float(val):.3f}")
287
+
288
+ # Combine all into text for the model
289
+ full_text = f"{combined_text} | {' '.join(numerical)}"
290
+
291
+ tokenized = tokenizer(full_text, padding='max_length', truncation=True, max_length=max_length)
292
+
293
+ # Set regression target (normalize to 0-1 range for stability)
294
+ tokenized['labels'] = [float(ex[target_col]) / 100.0]
295
+
296
  return tokenized
297
+
298
  tokenized = {}
299
  for split in dataset.keys():
300
  tokenized[split] = dataset[split].map(
301
+ normalize_features, batched=False, remove_columns=dataset[split].column_names
302
  )
303
+
304
  dataset = DatasetDict(tokenized)
305
+ logger.info("βœ“ Preprocessing complete")
306
+
307
  # Training args
308
  train_cfg = cfg['training']
309
  hw_cfg = cfg.get('hardware', {})
310
  out_cfg = cfg.get('output', {})
311
+
312
  output_dir = Path(out_cfg.get('dir', './outputs'))
313
  output_dir.mkdir(parents=True, exist_ok=True)
314
+
315
+ logger.info(f"\nπŸš€ Training...")
316
+ logger.info(f" Epochs: {train_cfg['epochs']}")
317
+ logger.info(f" Batch size: {train_cfg['batch_size']}")
318
+ logger.info(f" Learning rate: {train_cfg['learning_rate']}")
319
+
320
  # Split data for validation
321
  has_validation = 'validation' in dataset
322
  if not has_validation:
323
+ logger.info(" Creating validation split...")
324
  train_val = dataset['train'].train_test_split(test_size=0.1)
325
  dataset = DatasetDict({
326
  'train': train_val['train'],
327
  'validation': train_val['test']
328
  })
329
  has_validation = True
330
+
331
+ logger.info(f" Train: {len(dataset['train'])} samples")
332
+ logger.info(f" Validation: {len(dataset['validation'])} samples")
333
+
334
  training_args = TrainingArguments(
335
  output_dir=str(output_dir),
336
  num_train_epochs=train_cfg['epochs'],
 
348
  greater_is_better=False,
349
  report_to='none',
350
  )
351
+
352
  # Train
353
  trainer = Trainer(
354
  model=model,
 
357
  eval_dataset=dataset['validation'],
358
  processing_class=tokenizer,
359
  data_collator=DataCollatorWithPadding(tokenizer),
360
+ compute_metrics=lambda x: compute_metrics(x, cfg.get('evaluation', {}).get('metrics', ['mse', 'mae', 'r2'])),
361
  callbacks=[PerformanceCallback()],
362
  )
363
+
364
  trainer.train()
365
+
366
  # Evaluate
367
+ logger.info(f"\nπŸ“ˆ Evaluating...")
368
  if 'test' in dataset:
369
  eval_dataset = dataset['test']
370
  else:
371
  eval_dataset = dataset['validation']
372
  metrics = trainer.evaluate(eval_dataset)
373
+
374
+ logger.info(f"\n=== Final Metrics ===")
375
  for k, v in metrics.items():
376
  if isinstance(v, (int, float)):
377
+ # Scale MSE/MAE back to 0-100 range
378
+ if k in ['eval_mse', 'eval_mae']:
379
+ logger.info(f" {k}: {v * 100:.4f} (on 0-100 scale)")
380
+ elif k == 'eval_r2':
381
+ logger.info(f" {k}: {v:.4f}")
382
+ else:
383
+ logger.info(f" {k}: {v:.4f}")
384
+
385
  # Save
386
  model_path = output_dir / 'final_model'
387
  model.save_pretrained(str(model_path))
388
  tokenizer.save_pretrained(str(model_path))
389
+ logger.info(f"\nπŸ’Ύ Model saved to: {model_path}")
390
+
391
+ # Feature importance analysis
392
+ feature_importance = get_feature_importance(model, tokenizer, feature_columns)
393
+ logger.info("\n=== Feature Analysis ===")
394
+ for feat, info in feature_importance.items():
395
+ logger.info(f" {feat}: {info['description']}")
396
+
397
+ logger.info("\n" + "=" * 60)
398
+ logger.info("🎡 Training Complete!")
399
+ logger.info(" Model can predict track popularity and provide recommendations")
400
+ logger.info("=" * 60)
401
+
402
  return {'metrics': metrics, 'model_path': str(model_path)}
403
 
404
 
test_model.py CHANGED
@@ -1,5 +1,5 @@
1
  """
2
- Test trained model on Spotify queries
3
  Usage: python test_model.py <model_path>
4
  """
5
 
@@ -8,7 +8,7 @@ import torch
8
  from pathlib import Path
9
  from transformers import AutoTokenizer, AutoModelForSequenceClassification
10
 
11
- # Load model and tokenizer
12
  def load_model(model_path):
13
  """Load trained model"""
14
  print(f"Loading model from: {model_path}")
@@ -17,81 +17,252 @@ def load_model(model_path):
17
  model.eval()
18
  return model, tokenizer
19
 
20
- def predict_genre(model, tokenizer, track_name):
21
- """Predict genre for a track name"""
22
- # Tokenize
23
- inputs = tokenizer(track_name, return_tensors='pt', padding=True, truncation=True, max_length=256)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
 
 
 
 
25
  # Predict
26
  with torch.no_grad():
27
  outputs = model(**inputs)
28
- probs = torch.softmax(outputs.logits, dim=-1)
29
- pred_id = torch.argmax(probs, dim=-1).item()
30
- confidence = probs[0, pred_id].item()
31
 
32
- # Get label from id2label
33
- pred_label = model.config.id2label.get(pred_id, f"Class_{pred_id}")
34
 
35
- return pred_label, confidence, probs
36
 
37
- def test_model(model_path, test_queries=None):
38
- """Test model with sample queries"""
39
- model, tokenizer = load_model(model_path)
 
40
 
41
- # Default test queries
42
- if test_queries is None:
43
- test_queries = [
44
- "Bohemian Rhapsody",
45
- "Shape of You",
46
- "Old Town Road",
47
- "Blinding Lights",
48
- "Bad Guy",
49
- ]
 
 
 
 
 
 
 
 
 
 
 
 
50
 
51
- print("\n" + "=" * 60)
52
- print("Model Testing - Spotify Genre Classification")
53
- print("=" * 60)
54
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  results = []
56
- for query in test_queries:
57
- pred, conf, probs = predict_genre(model, tokenizer, query)
 
 
 
 
58
  results.append({
59
- 'query': query,
60
- 'predicted': pred,
61
- 'confidence': conf
62
  })
63
- print(f"\nTrack: '{query}'")
64
- print(f" Predicted Genre: {pred}")
65
- print(f" Confidence: {conf:.2%}")
66
-
67
- print("\n" + "=" * 60)
68
- print("Summary")
69
- print("=" * 60)
 
 
 
70
  for r in results:
71
- print(f" '{r['query']}' β†’ {r['predicted']} ({r['confidence']:.2%})")
72
-
 
 
 
 
 
 
 
73
  return results
74
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  if __name__ == '__main__':
76
  if len(sys.argv) < 2:
77
  model_path = 'outputs/final_model'
78
  else:
79
  model_path = sys.argv[1]
80
-
81
  if not Path(model_path).exists():
82
- print(f"Error: Model not found at {model_path}")
83
- print("Run training first: ./run.sh gpt2_spotify")
84
  sys.exit(1)
 
 
85
 
86
- test_queries = [
87
- "Bohemian Rhapsody",
88
- "Shape of You",
89
- "Old Town Road",
90
- "Blinding Lights",
91
- "Bad Guy",
92
- "Stairway to Heaven",
93
- "Smells Like Teen Spirit",
94
- "Billie Jean",
95
- ]
96
-
97
- test_model(model_path, test_queries)
 
1
  """
2
+ Test ViralTrack Predictor - Spotify Popularity Prediction
3
  Usage: python test_model.py <model_path>
4
  """
5
 
 
8
  from pathlib import Path
9
  from transformers import AutoTokenizer, AutoModelForSequenceClassification
10
 
11
+
12
  def load_model(model_path):
13
  """Load trained model"""
14
  print(f"Loading model from: {model_path}")
 
17
  model.eval()
18
  return model, tokenizer
19
 
20
+
21
+ def predict_popularity(model, tokenizer, track_name, audio_features=None):
22
+ """
23
+ Predict popularity for a track
24
+
25
+ Args:
26
+ model: Trained model
27
+ tokenizer: Tokenizer
28
+ track_name: Song title
29
+ audio_features: Dict of audio features (danceability, energy, etc.)
30
+
31
+ Returns:
32
+ popularity_score (0-100), confidence, recommendations
33
+ """
34
+ # Build input text with audio features if provided
35
+ if audio_features:
36
+ feature_str = ' | '.join([f"{k}:{v:.3f}" for k, v in audio_features.items()])
37
+ input_text = f"{track_name} | {feature_str}"
38
+ else:
39
+ input_text = track_name
40
 
41
+ # Tokenize
42
+ inputs = tokenizer(input_text, return_tensors='pt', padding=True, truncation=True, max_length=128)
43
+
44
  # Predict
45
  with torch.no_grad():
46
  outputs = model(**inputs)
47
+ # Regression output - sigmoid to get 0-1 range, then scale to 0-100
48
+ raw_score = torch.sigmoid(outputs.logits).item() * 100
 
49
 
50
+ # Generate recommendations
51
+ recommendations = generate_recommendations(raw_score, audio_features or {})
52
 
53
+ return raw_score, recommendations
54
 
55
+
56
+ def generate_recommendations(prediction: float, features: dict) -> list:
57
+ """Generate actionable recommendations based on prediction and features"""
58
+ recommendations = []
59
 
60
+ if prediction < 40:
61
+ recommendations.append("⚠️ Predicted popularity is LOW - consider these changes:")
62
+ elif prediction < 60:
63
+ recommendations.append("πŸ“ˆ Predicted popularity is MODERATE - optimization opportunities:")
64
+ elif prediction < 80:
65
+ recommendations.append("βœ… Predicted popularity is GOOD - track has solid potential!")
66
+ else:
67
+ recommendations.append("πŸ”₯ Predicted popularity is HIGH - track has VIRAL potential!")
68
+
69
+ # Feature-specific recommendations
70
+ if features.get('duration_ms', 0) > 200000:
71
+ recommendations.append(" πŸ“ Song is long (>3:20) - consider shorter version for TikTok/Reels")
72
+
73
+ if features.get('energy', 0) < 0.4:
74
+ recommendations.append(" ⚑ Low energy - consider adding more dynamic elements")
75
+
76
+ if features.get('danceability', 0) < 0.5:
77
+ recommendations.append(" πŸ’ƒ Low danceability - may not perform well on social platforms")
78
+
79
+ if features.get('valence', 0) > 0.8:
80
+ recommendations.append(" 😊 Very positive mood - great for playlists/morning vibes")
81
 
82
+ if features.get('acousticness', 0) > 0.7:
83
+ recommendations.append(" 🎸 Highly acoustic - consider production polish for mainstream appeal")
 
84
 
85
+ if features.get('speechiness', 0) > 0.3:
86
+ recommendations.append(" 🎀 High speechiness - may work well for podcast/hip-hop audiences")
87
+
88
+ if features.get('instrumentalness', 0) > 0.5:
89
+ recommendations.append(" 🎹 Instrumental track - consider adding vocals for broader appeal")
90
+
91
+ if features.get('liveness', 0) > 0.6:
92
+ recommendations.append(" πŸŽ™οΈ Live recording - studio version may have wider appeal")
93
+
94
+ return recommendations
95
+
96
+
97
+ def test_model(model_path, test_tracks=None):
98
+ """Test model with sample tracks"""
99
+ model, tokenizer = load_model(model_path)
100
+
101
+ # Default test tracks with audio features
102
+ if test_tracks is None:
103
+ test_tracks = [
104
+ {
105
+ 'track_name': "Bohemian Rhapsody",
106
+ 'audio_features': {
107
+ 'danceability': 0.416,
108
+ 'energy': 0.489,
109
+ 'valence': 0.279,
110
+ 'tempo': 144.0,
111
+ 'duration_ms': 354947,
112
+ 'acousticness': 0.172,
113
+ 'instrumentalness': 0.0,
114
+ 'liveness': 0.207,
115
+ 'speechiness': 0.0467,
116
+ }
117
+ },
118
+ {
119
+ 'track_name': "Shape of You",
120
+ 'audio_features': {
121
+ 'danceability': 0.825,
122
+ 'energy': 0.652,
123
+ 'valence': 0.931,
124
+ 'tempo': 96.0,
125
+ 'duration_ms': 233713,
126
+ 'acousticness': 0.581,
127
+ 'instrumentalness': 0.0,
128
+ 'liveness': 0.0931,
129
+ 'speechiness': 0.0802,
130
+ }
131
+ },
132
+ {
133
+ 'track_name': "Blinding Lights",
134
+ 'audio_features': {
135
+ 'danceability': 0.514,
136
+ 'energy': 0.730,
137
+ 'valence': 0.334,
138
+ 'tempo': 171.0,
139
+ 'duration_ms': 200040,
140
+ 'acousticness': 0.00146,
141
+ 'instrumentalness': 0.000906,
142
+ 'liveness': 0.0897,
143
+ 'speechiness': 0.0598,
144
+ }
145
+ },
146
+ {
147
+ 'track_name': "Bad Guy",
148
+ 'audio_features': {
149
+ 'danceability': 0.703,
150
+ 'energy': 0.432,
151
+ 'valence': 0.560,
152
+ 'tempo': 135.0,
153
+ 'duration_ms': 194088,
154
+ 'acousticness': 0.133,
155
+ 'instrumentalness': 0.000234,
156
+ 'liveness': 0.0962,
157
+ 'speechiness': 0.378,
158
+ }
159
+ },
160
+ {
161
+ 'track_name': "Old Town Road",
162
+ 'audio_features': {
163
+ 'danceability': 0.547,
164
+ 'energy': 0.621,
165
+ 'valence': 0.645,
166
+ 'tempo': 136.0,
167
+ 'duration_ms': 157066,
168
+ 'acousticness': 0.0395,
169
+ 'instrumentalness': 0.0,
170
+ 'liveness': 0.117,
171
+ 'speechiness': 0.0924,
172
+ }
173
+ },
174
+ ]
175
+
176
+ print("\n" + "=" * 70)
177
+ print("🎡 ViralTrack Predictor - Popularity Prediction & Recommendations")
178
+ print("=" * 70)
179
+
180
  results = []
181
+ for track in test_tracks:
182
+ track_name = track['track_name']
183
+ audio_features = track.get('audio_features', {})
184
+
185
+ popularity, recommendations = predict_popularity(model, tokenizer, track_name, audio_features)
186
+
187
  results.append({
188
+ 'track_name': track_name,
189
+ 'predicted_popularity': popularity,
190
+ 'recommendations': recommendations
191
  })
192
+
193
+ print(f"\n🎡 Track: '{track_name}'")
194
+ print(f" Predicted Popularity: {popularity:.1f}/100")
195
+ print(f"\n Recommendations:")
196
+ for rec in recommendations:
197
+ print(f" {rec}")
198
+
199
+ print("\n" + "=" * 70)
200
+ print("πŸ“Š Summary")
201
+ print("=" * 70)
202
  for r in results:
203
+ bar_len = int(r['predicted_popularity'] / 5)
204
+ bar = "β–ˆ" * bar_len + "β–‘" * (20 - bar_len)
205
+ print(f" {r['track_name'][:25]:<25} [{bar}] {r['predicted_popularity']:.1f}")
206
+
207
+ print("\n" + "=" * 70)
208
+ print("πŸ’‘ Tip: Run with custom track:")
209
+ print(" python test_model.py <model_path>")
210
+ print("=" * 70)
211
+
212
  return results
213
 
214
+
215
+ def interactive_mode(model, tokenizer):
216
+ """Interactive mode for testing custom tracks"""
217
+ print("\n" + "=" * 70)
218
+ print("🎀 Interactive Mode - Enter track details (or 'quit' to exit)")
219
+ print("=" * 70)
220
+
221
+ while True:
222
+ track_name = input("\n🎡 Track name: ").strip()
223
+ if track_name.lower() in ['quit', 'exit', 'q']:
224
+ break
225
+
226
+ # Optional: enter audio features
227
+ use_features = input(" Add audio features? (y/n): ").strip().lower()
228
+ audio_features = {}
229
+
230
+ if use_features == 'y':
231
+ print(" Enter features (or press Enter to skip):")
232
+ for feat in ['danceability', 'energy', 'valence', 'tempo', 'duration_ms',
233
+ 'acousticness', 'instrumentalness', 'liveness', 'speechiness']:
234
+ val = input(f" {feat}: ").strip()
235
+ if val:
236
+ try:
237
+ audio_features[feat] = float(val)
238
+ except ValueError:
239
+ pass
240
+
241
+ popularity, recommendations = predict_popularity(model, tokenizer, track_name, audio_features)
242
+
243
+ print(f"\n πŸ“Š Predicted Popularity: {popularity:.1f}/100")
244
+ print(f"\n πŸ’‘ Recommendations:")
245
+ for rec in recommendations:
246
+ print(f" {rec}")
247
+
248
+
249
  if __name__ == '__main__':
250
  if len(sys.argv) < 2:
251
  model_path = 'outputs/final_model'
252
  else:
253
  model_path = sys.argv[1]
254
+
255
  if not Path(model_path).exists():
256
+ print(f"❌ Error: Model not found at {model_path}")
257
+ print(" Run training first: ./run.sh config")
258
  sys.exit(1)
259
+
260
+ model, tokenizer = load_model(model_path)
261
 
262
+ # Ask if user wants interactive mode
263
+ mode = input("\nπŸ”§ Test mode: (1) Default tracks (2) Interactive [1]: ").strip()
264
+
265
+ if mode == '2' or mode.lower() == 'i':
266
+ interactive_mode(model, tokenizer)
267
+ else:
268
+ test_model(model_path)