andreasvc commited on
Commit
2cf4246
·
verified ·
1 Parent(s): c70116a

Upload folder using huggingface_hub

Browse files
README.md ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ language: en
3
+ license: mit
4
+ base_model: declare-lab/segue-w2v2-base
5
+ datasets:
6
+ - declare-lab/MELD
7
+ tags:
8
+ - audio
9
+ - speech
10
+ - sentiment-analysis
11
+ - emotion-recognition
12
+ - multitask
13
+ ---
14
+
15
+ # SEGUE fine-tuned on MELD (multitask sentiment + emotion)
16
+
17
+ This model is a fine-tuned version of [declare-lab/segue-w2v2-base](https://huggingface.co/declare-lab/segue-w2v2-base)
18
+ trained jointly on sentiment (3-class) and emotion (7-class) recognition
19
+ on the [MELD dataset](https://github.com/declare-lab/MELD) (Friends TV show dialogues).
20
+
21
+ ## Labels
22
+
23
+ **Sentiment:** neutral, positive, negative
24
+ **Emotion:** neutral, surprise, fear, sadness, joy, disgust, anger
25
+
26
+ ## Performance (test set)
27
+
28
+ | Task | Weighted F1 | Macro F1 |
29
+ |-----------|-------------|----------|
30
+ | Sentiment | 0.558 | 0.519 |
31
+ | Emotion | 0.475 | 0.273 |
32
+
33
+ ## Requirements
34
+
35
+ This model depends on the [declare-lab/segue](https://github.com/declare-lab/segue)
36
+ repository, which is not pip-installable. You need to clone it and add it to your path:
37
+
38
+ git clone https://github.com/declare-lab/segue
39
+ # run your scripts from inside the segue/ directory, or:
40
+ import sys; sys.path.append('/path/to/segue')
41
+
42
+ ## Usage
43
+
44
+ Download `model.pt` and `inference.py` from this repository, then:
45
+
46
+ from inference import load_segue_multitask, segue_predict
47
+ model, processor = load_segue_multitask("model.pt")
48
+ sent_probs, emo_probs = segue_predict(model, processor, audio_array, sampling_rate=16000)
49
+
50
+ ## Training details
51
+
52
+ - Base model: `declare-lab/segue-w2v2-base`
53
+ - Dataset: MELD (9989 train / 1109 dev / 2610 test utterances)
54
+ - Learning rate: 3e-5, warmup ratio: 0.3, 3 epochs
55
+ - Checkpoint averaging: last 10 checkpoints (every 100 steps)
56
+ - Multitask loss: 0.5 × sentiment + 0.5 × emotion cross-entropy
config.json ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_type": "segue-multitask",
3
+ "base_model": "declare-lab/segue-w2v2-base",
4
+ "tasks": {
5
+ "sentiment": {"num_labels": 3, "labels": ["neutral", "positive", "negative"]},
6
+ "emotion": {"num_labels": 7, "labels": ["neutral", "surprise", "fear", "sadness", "joy", "disgust", "anger"]}
7
+ },
8
+ "dataset": "MELD",
9
+ "training": {
10
+ "learning_rate": 3e-5,
11
+ "num_epochs": 3,
12
+ "warmup_ratio": 0.3,
13
+ "batch_size": 1,
14
+ "gradient_accumulation_steps": 8,
15
+ "avg_checkpoints": 10,
16
+ "save_steps": 100,
17
+ "seed": 39
18
+ },
19
+ "results": {
20
+ "test_sentiment_weighted_f1": 0.5582,
21
+ "test_sentiment_macro_f1": 0.5185,
22
+ "test_emotion_weighted_f1": 0.4750,
23
+ "test_emotion_macro_f1": 0.2733
24
+ }
25
+ }
inference.py ADDED
@@ -0,0 +1,296 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ inference.py — load and run the SEGUE multitask sentiment + emotion model.
3
+
4
+ Requirements
5
+ ------------
6
+ 1. Clone the declare-lab/segue repository and make it importable:
7
+ git clone https://github.com/declare-lab/segue
8
+ Then either run your script from inside the segue/ directory, or add it to
9
+ your path explicitly:
10
+ import sys; sys.path.append("/path/to/segue")
11
+
12
+ 2. Install dependencies (matching the versions used for training):
13
+ pip install torch torchaudio
14
+ pip install transformers==4.35.0 huggingface_hub==0.17.0
15
+ pip install numpy>=1.24,<2.0 accelerate>=0.20.1,<0.24.0
16
+
17
+ Quick start
18
+ -----------
19
+ import torchaudio
20
+ from inference import load_segue_multitask, segue_predict
21
+
22
+ model, processor = load_segue_multitask("model.pt")
23
+
24
+ waveform, sr = torchaudio.load("speech.wav")
25
+ audio = waveform.mean(0).numpy() # mono, float32
26
+
27
+ sent_probs, emo_probs = segue_predict(model, processor, [audio], sampling_rate=sr)
28
+ # sent_probs: np.ndarray (N, 3) — neutral / positive / negative
29
+ # emo_probs: np.ndarray (N, 7) — neutral / surprise / fear / sadness / joy / disgust / anger
30
+ """
31
+
32
+ import os
33
+ from typing import List, Optional, Tuple
34
+ import numpy as np
35
+ import torch
36
+ import torchaudio
37
+
38
+ # SegueForClassification lives in the segue repo — must be on sys.path.
39
+ try:
40
+ from segue.modeling_segue import SegueForClassification
41
+ except ImportError:
42
+ raise ImportError(
43
+ "Could not import `segue`. "
44
+ "Clone https://github.com/declare-lab/segue and either run your script "
45
+ "from inside that directory or add it to sys.path:\n"
46
+ " import sys; sys.path.append('/path/to/segue')")
47
+
48
+ # ---------------------------------------------------------------------------
49
+ # Label definitions
50
+ # ---------------------------------------------------------------------------
51
+
52
+ SENTIMENT_LABELS = {0: "neutral", 1: "positive", 2: "negative"}
53
+ EMOTION_LABELS = {
54
+ 0: "neutral",
55
+ 1: "surprise",
56
+ 2: "fear",
57
+ 3: "sadness",
58
+ 4: "joy",
59
+ 5: "disgust",
60
+ 6: "anger"}
61
+
62
+ TARGET_SAMPLE_RATE = 16_000
63
+
64
+
65
+ # ---------------------------------------------------------------------------
66
+ # Model wrapper
67
+ # ---------------------------------------------------------------------------
68
+
69
+ class SegueMultiTask(torch.nn.Module):
70
+ """
71
+ Two SegueForClassification heads (sentiment + emotion) that share a single
72
+ wav2vec2 speech encoder backbone.
73
+
74
+ This is the same architecture used during fine-tuning on MELD.
75
+ The speech encoder is owned by `sentiment_model`; `emotion_model` holds
76
+ only its own text encoder and classification head.
77
+ """
78
+
79
+ def __init__(
80
+ self,
81
+ sentiment_model: SegueForClassification,
82
+ emotion_model: SegueForClassification):
83
+ super().__init__()
84
+ self.sentiment_model = sentiment_model
85
+ self.emotion_model = emotion_model
86
+ # Tie the encoders so the backbone is shared
87
+ self.emotion_model.speech_encoder = self.sentiment_model.speech_encoder
88
+ self.processor = self.sentiment_model.processor
89
+
90
+ def forward(
91
+ self,
92
+ speech: dict,
93
+ n_speech_tokens: list,
94
+ **kwargs) -> dict:
95
+ """
96
+ Args:
97
+ speech: dict with key "input_values": FloatTensor (B, T)
98
+ n_speech_tokens: list of ints, length B
99
+
100
+ Returns:
101
+ dict with keys:
102
+ "sentiment_predictions": FloatTensor (B, 3) — raw logits
103
+ "emotion_predictions": FloatTensor (B, 7) — raw logits
104
+ """
105
+ # SegueForClassification.forward() unconditionally calls
106
+ # labels.unsqueeze(-1), so we must always supply labels.
107
+ # We pass dummy zeros and ignore the returned loss.
108
+ batch_size = speech["input_values"].shape[0]
109
+ dummy_labels = torch.zeros(
110
+ batch_size, dtype=torch.long, device=speech["input_values"].device)
111
+
112
+ sent_out = self.sentiment_model(
113
+ speech=speech, n_speech_tokens=n_speech_tokens, labels=dummy_labels)
114
+ emo_out = self.emotion_model(
115
+ speech=speech, n_speech_tokens=n_speech_tokens, labels=dummy_labels)
116
+
117
+ return {
118
+ "sentiment_predictions": sent_out["predictions"],
119
+ "emotion_predictions": emo_out["predictions"]}
120
+
121
+
122
+ # ---------------------------------------------------------------------------
123
+ # Loading
124
+ # ---------------------------------------------------------------------------
125
+
126
+ def load_segue_multitask(
127
+ weights_path: str,
128
+ base_model: str = "declare-lab/segue-w2v2-base",
129
+ device: Optional[str] = None) -> Tuple[SegueMultiTask, object]:
130
+ """
131
+ Load the fine-tuned SegueMultiTask model from a weights file.
132
+
133
+ Args:
134
+ weights_path: path to `model.pt` (the fine-tuned state dict)
135
+ base_model: HuggingFace model ID used as the architecture template
136
+ device: "cuda", "cpu", or None (auto-detect)
137
+
138
+ Returns:
139
+ (model, processor)
140
+ model: SegueMultiTask in eval mode, moved to `device`
141
+ processor: SegueProcessor for pre-processing audio
142
+ """
143
+ if device is None:
144
+ device = "cuda" if torch.cuda.is_available() else "cpu"
145
+
146
+ # Load architecture from the pre-trained base (weights will be overwritten)
147
+ sentiment_model = SegueForClassification.from_pretrained(
148
+ base_model, n_classes=3, ignore_mismatched_sizes=True)
149
+ emotion_model = SegueForClassification.from_pretrained(
150
+ base_model, n_classes=7, ignore_mismatched_sizes=True)
151
+
152
+ model = SegueMultiTask(sentiment_model, emotion_model)
153
+
154
+ # Disable wav2vec2 feature masking — it's a pre-training trick that causes
155
+ # errors on short sequences and is not needed for inference.
156
+ model.sentiment_model.speech_encoder.config.mask_time_prob = 0.0
157
+ model.sentiment_model.speech_encoder.config.mask_feature_prob = 0.0
158
+
159
+ # Load fine-tuned weights
160
+ state_dict = torch.load(weights_path, map_location="cpu")
161
+ missing, unexpected = model.load_state_dict(state_dict, strict=False)
162
+ if missing:
163
+ print(f"Warning — missing keys when loading weights: {missing}")
164
+ if unexpected:
165
+ print(f"Warning — unexpected keys when loading weights: {unexpected}")
166
+
167
+ # Re-tie the shared speech encoder (load_state_dict breaks the reference)
168
+ model.emotion_model.speech_encoder = model.sentiment_model.speech_encoder
169
+
170
+ model = model.to(device)
171
+ model.eval()
172
+
173
+ return model, model.processor
174
+
175
+
176
+ # ---------------------------------------------------------------------------
177
+ # Inference
178
+ # ---------------------------------------------------------------------------
179
+
180
+ def segue_predict(
181
+ model: SegueMultiTask,
182
+ processor,
183
+ chunks: List[np.ndarray],
184
+ sampling_rate: int = TARGET_SAMPLE_RATE) -> Tuple[np.ndarray, np.ndarray]:
185
+ """
186
+ Run the model on a list of audio chunks and return softmax probabilities.
187
+
188
+ Args:
189
+ model: SegueMultiTask returned by load_segue_multitask()
190
+ processor: processor returned by load_segue_multitask()
191
+ chunks: list of 1-D float32 numpy arrays (mono audio)
192
+ sampling_rate: sample rate of the audio (model expects 16 000 Hz;
193
+ pass the actual rate and it will be resampled if needed)
194
+
195
+ Returns:
196
+ sent_probs: np.ndarray (N, 3) softmax probabilities for sentiment
197
+ columns: neutral / positive / negative
198
+ emo_probs: np.ndarray (N, 7) softmax probabilities for emotion
199
+ columns: neutral / surprise / fear / sadness / joy / disgust / anger
200
+ """
201
+ device = next(model.parameters()).device
202
+
203
+ # Resample if the audio doesn't match the model's expected rate
204
+ if sampling_rate != TARGET_SAMPLE_RATE:
205
+ resampler = torchaudio.transforms.Resample(sampling_rate, TARGET_SAMPLE_RATE)
206
+ chunks = [
207
+ resampler(torch.from_numpy(c).unsqueeze(0)).squeeze(0).numpy()
208
+ for c in chunks]
209
+
210
+ all_sent_logits = []
211
+ all_emo_logits = []
212
+
213
+ for chunk in chunks:
214
+ proc = processor(audio=chunk, sampling_rate=TARGET_SAMPLE_RATE)
215
+ input_values = torch.tensor(
216
+ proc["speech"]["input_values"], dtype=torch.float32
217
+ ).unsqueeze(0).to(device)
218
+ n_speech_tokens = [int(proc["n_speech_tokens"][0])]
219
+
220
+ with torch.no_grad():
221
+ out = model(
222
+ speech={"input_values": input_values},
223
+ n_speech_tokens=n_speech_tokens)
224
+
225
+ all_sent_logits.append(out["sentiment_predictions"].cpu())
226
+ all_emo_logits.append(out["emotion_predictions"].cpu())
227
+
228
+ sent_probs = torch.softmax(torch.cat(all_sent_logits, dim=0), dim=1).numpy()
229
+ emo_probs = torch.softmax(torch.cat(all_emo_logits, dim=0), dim=1).numpy()
230
+
231
+ return sent_probs, emo_probs
232
+
233
+
234
+ # ---------------------------------------------------------------------------
235
+ # Convenience: predict a single audio file
236
+ # ---------------------------------------------------------------------------
237
+
238
+ def predict_file(
239
+ audio_path: str,
240
+ weights_path: str = "model.pt",
241
+ device: Optional[str] = None) -> dict:
242
+ """
243
+ Convenience function: load the model and run it on a single audio file.
244
+
245
+ Returns a dict with:
246
+ sentiment: dict mapping label -> probability
247
+ emotion: dict mapping label -> probability
248
+ sentiment_score: float in [-1, 1] (prob_positive - prob_negative)
249
+ """
250
+ model, processor = load_segue_multitask(weights_path, device=device)
251
+
252
+ waveform, sr = torchaudio.load(audio_path)
253
+ if waveform.shape[0] > 1:
254
+ waveform = waveform.mean(dim=0, keepdim=True)
255
+ audio = waveform.squeeze(0).numpy().astype(np.float32)
256
+
257
+ sent_probs, emo_probs = segue_predict(model, processor, [audio], sampling_rate=sr)
258
+
259
+ return {
260
+ "sentiment": {
261
+ SENTIMENT_LABELS[i]: float(sent_probs[0, i])
262
+ for i in range(len(SENTIMENT_LABELS))},
263
+ "emotion": {
264
+ EMOTION_LABELS[i]: float(emo_probs[0, i])
265
+ for i in range(len(EMOTION_LABELS))},
266
+ "sentiment_score": float(sent_probs[0, 1] - sent_probs[0, 2])}
267
+
268
+
269
+ # ---------------------------------------------------------------------------
270
+ # Quick test when run directly
271
+ # ---------------------------------------------------------------------------
272
+
273
+ if __name__ == "__main__":
274
+ import sys
275
+
276
+ if len(sys.argv) < 2:
277
+ print("Usage: python inference.py <audio_file> [model.pt]")
278
+ sys.exit(1)
279
+
280
+ audio_path = sys.argv[1]
281
+ weights_path = sys.argv[2] if len(sys.argv) > 2 else "model.pt"
282
+
283
+ print(f"Audio: {audio_path}")
284
+ print(f"Weights: {weights_path}")
285
+ print()
286
+
287
+ result = predict_file(audio_path, weights_path)
288
+
289
+ print("Sentiment probabilities:")
290
+ for label, prob in result["sentiment"].items():
291
+ print(f" {label:10s}: {prob:.4f}")
292
+ print(f" → score (pos - neg): {result['sentiment_score']:+.4f}")
293
+
294
+ print("\nEmotion probabilities:")
295
+ for label, prob in result["emotion"].items():
296
+ print(f" {label:10s}: {prob:.4f}")
model.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:77c82ff3a34c33131d718e9eb89fc545136df9fa9960085a990cb5c446afc16f
3
+ size 1253698911
preprocessor_config.json ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "do_normalize": true,
3
+ "feature_extractor_type": "Wav2Vec2FeatureExtractor",
4
+ "feature_size": 1,
5
+ "padding_side": "right",
6
+ "padding_value": 0.0,
7
+ "processor_class": "SegueProcessor",
8
+ "return_attention_mask": false,
9
+ "sampling_rate": 16000
10
+ }
special_tokens_map.json ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "bos_token": {
3
+ "content": "<s>",
4
+ "lstrip": false,
5
+ "normalized": false,
6
+ "rstrip": false,
7
+ "single_word": false
8
+ },
9
+ "cls_token": {
10
+ "content": "<s>",
11
+ "lstrip": false,
12
+ "normalized": true,
13
+ "rstrip": false,
14
+ "single_word": false
15
+ },
16
+ "eos_token": {
17
+ "content": "</s>",
18
+ "lstrip": false,
19
+ "normalized": false,
20
+ "rstrip": false,
21
+ "single_word": false
22
+ },
23
+ "mask_token": {
24
+ "content": "<mask>",
25
+ "lstrip": true,
26
+ "normalized": false,
27
+ "rstrip": false,
28
+ "single_word": false
29
+ },
30
+ "pad_token": {
31
+ "content": "<pad>",
32
+ "lstrip": false,
33
+ "normalized": false,
34
+ "rstrip": false,
35
+ "single_word": false
36
+ },
37
+ "sep_token": {
38
+ "content": "</s>",
39
+ "lstrip": false,
40
+ "normalized": true,
41
+ "rstrip": false,
42
+ "single_word": false
43
+ },
44
+ "unk_token": {
45
+ "content": "[UNK]",
46
+ "lstrip": false,
47
+ "normalized": false,
48
+ "rstrip": false,
49
+ "single_word": false
50
+ }
51
+ }
tokenizer.json ADDED
The diff for this file is too large to render. See raw diff
 
tokenizer_config.json ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "added_tokens_decoder": {
3
+ "0": {
4
+ "content": "<s>",
5
+ "lstrip": false,
6
+ "normalized": false,
7
+ "rstrip": false,
8
+ "single_word": false,
9
+ "special": true
10
+ },
11
+ "1": {
12
+ "content": "<pad>",
13
+ "lstrip": false,
14
+ "normalized": false,
15
+ "rstrip": false,
16
+ "single_word": false,
17
+ "special": true
18
+ },
19
+ "2": {
20
+ "content": "</s>",
21
+ "lstrip": false,
22
+ "normalized": false,
23
+ "rstrip": false,
24
+ "single_word": false,
25
+ "special": true
26
+ },
27
+ "3": {
28
+ "content": "<unk>",
29
+ "lstrip": false,
30
+ "normalized": true,
31
+ "rstrip": false,
32
+ "single_word": false,
33
+ "special": true
34
+ },
35
+ "104": {
36
+ "content": "[UNK]",
37
+ "lstrip": false,
38
+ "normalized": false,
39
+ "rstrip": false,
40
+ "single_word": false,
41
+ "special": true
42
+ },
43
+ "30526": {
44
+ "content": "<mask>",
45
+ "lstrip": true,
46
+ "normalized": false,
47
+ "rstrip": false,
48
+ "single_word": false,
49
+ "special": true
50
+ }
51
+ },
52
+ "bos_token": "<s>",
53
+ "clean_up_tokenization_spaces": true,
54
+ "cls_token": "<s>",
55
+ "do_lower_case": true,
56
+ "eos_token": "</s>",
57
+ "mask_token": "<mask>",
58
+ "max_length": 128,
59
+ "model_max_length": 512,
60
+ "pad_to_multiple_of": null,
61
+ "pad_token": "<pad>",
62
+ "pad_token_type_id": 0,
63
+ "padding_side": "right",
64
+ "processor_class": "SegueProcessor",
65
+ "sep_token": "</s>",
66
+ "stride": 0,
67
+ "strip_accents": null,
68
+ "tokenize_chinese_chars": true,
69
+ "tokenizer_class": "MPNetTokenizer",
70
+ "truncation_side": "right",
71
+ "truncation_strategy": "longest_first",
72
+ "unk_token": "[UNK]"
73
+ }
vocab.txt ADDED
The diff for this file is too large to render. See raw diff