Ilia commited on
Commit
c485839
·
1 Parent(s): f6db1a0

create spaces

Browse files
Files changed (5) hide show
  1. app.py +68 -0
  2. gigaam_ru_en/gigaam_ru_en.ckpt +3 -0
  3. requirements.txt +4 -0
  4. src/losses.py +292 -0
  5. src/models.py +123 -0
app.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import torch
3
+ import torchaudio
4
+ from hydra.utils import instantiate
5
+ from src.models import AudioBatch
6
+
7
+ # Путь к чекпоинтам
8
+ CHECKPOINTS = {
9
+ "RU+EN": "gigaam_ru_en/gigaam_ru_en.ckpt"
10
+ }
11
+
12
+ # Кэш моделей
13
+ LOADED_MODELS = {}
14
+
15
+ def load_model(ckpt_path):
16
+ if ckpt_path in LOADED_MODELS:
17
+ return LOADED_MODELS[ckpt_path]
18
+
19
+ checkpoint = torch.load(ckpt_path, map_location='cpu')
20
+ config = checkpoint['config']
21
+ id2name = checkpoint['id2name']
22
+
23
+ model = instantiate(config, _recursive_=False)
24
+ model.load_state_dict(checkpoint['state_dict'])
25
+ model.eval()
26
+
27
+ LOADED_MODELS[ckpt_path] = (model, id2name)
28
+ return model, id2name
29
+
30
+ def classify_emotion(audio, ckpt_name):
31
+ # Load waveform
32
+ waveform, sr = torchaudio.load(audio)
33
+
34
+ # Load model
35
+ model, id2name = load_model(CHECKPOINTS[ckpt_name])
36
+
37
+ # Если нужно, ресемплим до 16к
38
+ if sr != 16000:
39
+ waveform = torchaudio.transforms.Resample(orig_freq=sr, new_freq=16000)(waveform)
40
+
41
+ # B x T
42
+ if waveform.dim() > 1:
43
+ waveform = waveform.mean(dim=0, keepdim=True)
44
+
45
+ length = torch.tensor([waveform.shape[-1]])
46
+
47
+ batch = AudioBatch(waveform, length, None)
48
+
49
+ with torch.no_grad():
50
+ logits, _ = model(batch)
51
+ probs = torch.softmax(logits, dim=-1).squeeze(0)
52
+
53
+ result = {label: float(probs[i]) for i, label in enumerate(id2name)}
54
+ return result
55
+
56
+ demo = gr.Interface(
57
+ fn=classify_emotion,
58
+ inputs=[
59
+ gr.Audio(type="filepath", label="Загрузите аудиофайл"),
60
+ gr.Dropdown(choices=list(CHECKPOINTS.keys()), label="Выберите модель", value="RU+EN")
61
+ ],
62
+ outputs=gr.Label(label="Эмоциональная окраска (вероятности)"),
63
+ title="Эмоциональная классификация речи (дообученная GigaAM на 8 классов)",
64
+ description="Выберите модель и загрузите аудио"
65
+ )
66
+
67
+
68
+ demo.launch()
gigaam_ru_en/gigaam_ru_en.ckpt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c6508c61dd2a7093161a5a8faa052a4b3f1dfb5916ba68c5701e8968068598d0
3
+ size 968547236
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ hydra-core
2
+ gigaam
3
+ gradio
4
+ soundfile
src/losses.py ADDED
@@ -0,0 +1,292 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+
3
+ import torch
4
+ import torch.nn as nn
5
+ import torch.nn.functional as F
6
+
7
+
8
+ class AMSoftmax(nn.Module):
9
+ def __init__(self, in_features, out_features, s=30.0, m=0.35):
10
+ """
11
+ in_features: размерность входных эмбеддингов
12
+ out_features: количество классов
13
+ s: масштабный множитель (scale)
14
+ m: аддитивный margin
15
+ """
16
+ super(AMSoftmax, self).__init__()
17
+ self.in_features = in_features
18
+ self.out_features = out_features
19
+ self.s = s
20
+ self.m = m
21
+ self.weight = nn.Parameter(torch.FloatTensor(out_features, in_features))
22
+ nn.init.xavier_uniform_(self.weight)
23
+ self.use_labels_when_train = True
24
+
25
+ def inference(self, x):
26
+ x_norm = F.normalize(x, p=2, dim=1)
27
+ w_norm = F.normalize(self.weight, p=2, dim=1)
28
+ logits = F.linear(x_norm, w_norm) * self.s
29
+ return logits
30
+
31
+ def forward(self, x, labels=None):
32
+ if not self.training or labels is None:
33
+ return self.inference(x)
34
+ # Нормализация входов и весов
35
+ input_norm = F.normalize(x, p=2, dim=1)
36
+ weight_norm = F.normalize(self.weight, p=2, dim=1)
37
+
38
+ # Косинус угла между входами и центрами классов
39
+ cosine = F.linear(input_norm, weight_norm) # [batch_size, num_classes]
40
+
41
+ # Скопировать для дальнейшего вычисления
42
+ phi = cosine - self.m
43
+
44
+ # Создать one-hot маску
45
+ one_hot = torch.zeros_like(cosine)
46
+ one_hot.scatter_(1, labels.view(-1, 1), 1.0)
47
+
48
+ # Применить margin только к целевым логитам
49
+ output = self.s * (one_hot * phi + (1.0 - one_hot) * cosine)
50
+ return output
51
+
52
+
53
+
54
+ class AAMSoftmax(nn.Module):
55
+ def __init__(self, in_features, out_features, s=30.0, m=0.50, easy_margin=False):
56
+ """
57
+ in_features: размерность эмбеддинга
58
+ out_features: количество классов
59
+ s: scale (обычно 30)
60
+ m: angular margin (обычно 0.5 радиан)
61
+ easy_margin: использовать "easy margin" трюк или нет
62
+ """
63
+ super(AAMSoftmax, self).__init__()
64
+ self.in_features = in_features
65
+ self.out_features = out_features
66
+ self.s = s
67
+ self.m = m
68
+ self.easy_margin = easy_margin
69
+
70
+ self.weight = nn.Parameter(torch.FloatTensor(out_features, in_features))
71
+ nn.init.xavier_uniform_(self.weight)
72
+
73
+ self.cos_m = math.cos(m)
74
+ self.sin_m = math.sin(m)
75
+ self.th = math.cos(math.pi - m)
76
+ self.mm = math.sin(math.pi - m) * m
77
+ self.use_labels_when_train = True
78
+
79
+ def inference(self, x):
80
+ x_norm = F.normalize(x, p=2, dim=1)
81
+ w_norm = F.normalize(self.weight, p=2, dim=1)
82
+ logits = F.linear(x_norm, w_norm) * self.s
83
+ return logits
84
+
85
+ def forward(self, x, labels=None):
86
+ if not self.training or labels is None:
87
+ return self.inference(x)
88
+ # Нормализуем входы и веса
89
+ cosine = F.linear(F.normalize(x), F.normalize(self.weight)) # [B, C]
90
+ sine = torch.sqrt(1.0 - cosine ** 2 + 1e-6)
91
+
92
+ # cos(θ + m) = cosθ * cos(m) - sinθ * sin(m)
93
+ phi = cosine * self.cos_m - sine * self.sin_m
94
+
95
+ if self.easy_margin:
96
+ # Используем "легкий" трюк, чтобы избежать неустойчивости
97
+ phi = torch.where(cosine > 0, phi, cosine)
98
+ else:
99
+ # Ограничиваем phi снизу
100
+ phi = torch.where(cosine > self.th, phi, cosine - self.mm)
101
+
102
+ # One-hot метки
103
+ one_hot = torch.zeros_like(cosine)
104
+ one_hot.scatter_(1, labels.view(-1, 1), 1.0)
105
+
106
+ # Вычисляем итоговый логит
107
+ output = self.s * (one_hot * phi + (1.0 - one_hot) * cosine)
108
+ return output
109
+
110
+
111
+ class RAMSoftmax(nn.Module):
112
+ """
113
+ Real Additive Margin Softmax (RAM-Softmax)
114
+
115
+ Args:
116
+ in_features: размерность входных эмбеддингов
117
+ out_features: число классов
118
+ s: scale-фактор для логитов
119
+ m: additive margin
120
+ eps: небольшой стабилизатор для sqrt
121
+ """
122
+
123
+ def __init__(self, in_features, out_features, s=30.0, m=0.35, eps=1e-6):
124
+ super(RAMSoftmax, self).__init__()
125
+ self.in_features = in_features
126
+ self.out_features = out_features
127
+ self.s = s
128
+ self.m = m
129
+ self.eps = eps
130
+ # веса центров классов
131
+ self.weight = nn.Parameter(torch.FloatTensor(out_features, in_features))
132
+ nn.init.xavier_uniform_(self.weight)
133
+
134
+ # Большое отрицательное для «отсечки» легко разделённых классов
135
+ self.register_buffer('large_neg', torch.tensor(-1e9))
136
+ self.use_labels_when_train = True
137
+
138
+ def inference(self, x):
139
+ x_norm = F.normalize(x, p=2, dim=1) # [B, D]
140
+ w_norm = F.normalize(self.weight, p=2, dim=1) # [C, D]
141
+
142
+ # 2) косинус
143
+ cosine = torch.matmul(x_norm, w_norm.t()) # [B, C]
144
+
145
+ # 3) масштаб
146
+ logits = cosine * self.s # [B, C]
147
+ return logits
148
+
149
+ def forward(self, x, labels=None):
150
+ if not self.training or labels is None:
151
+
152
+ return self.inference(x)
153
+ # 1) нормализуем эмбеддинги и веса
154
+ x_norm = F.normalize(x, p=2, dim=1) # [B, D]
155
+ w_norm = F.normalize(self.weight, p=2, dim=1) # [C, D]
156
+
157
+ # 2) вычисляем все косинусы
158
+ cosine = F.linear(x_norm, w_norm) # [B, C]
159
+
160
+ # 3) достаём целевой косинус и вычитаем margin
161
+ idx = torch.arange(x.size(0), device=x.device)
162
+ cos_y = cosine[idx, labels] # [B]
163
+ phi = cos_y - self.m # [B]
164
+
165
+ # 4) заменяем целевой логит на phi, остальные — оставляем как cosine
166
+ logits = cosine.clone()
167
+ logits[idx, labels] = phi
168
+
169
+ # 5) маскирование «легко» разделённых: для каждого j≠y
170
+ # если cos_y - cos_j > m => отсечь (логит -> large_neg)
171
+ diff = cos_y.unsqueeze(1) - cosine # [B, C]
172
+ mask = (diff > self.m) # [B, C]
173
+ mask[idx, labels] = False # не маскируем целевой
174
+ logits = torch.where(mask, self.large_neg, logits)
175
+
176
+ # 6) масштабируем
177
+ logits = logits * self.s
178
+
179
+ return logits
180
+
181
+
182
+ class RAAMSoftmax(nn.Module):
183
+ def __init__(self, in_features, out_features, s=30.0, m=0.50, eps=1e-6):
184
+ super().__init__()
185
+ self.in_features = in_features
186
+ self.out_features = out_features
187
+ self.s = s
188
+ self.m = m
189
+ self.eps = eps
190
+
191
+ self.weight = nn.Parameter(torch.FloatTensor(out_features, in_features))
192
+ nn.init.xavier_uniform_(self.weight)
193
+
194
+ self.cos_m = math.cos(m)
195
+ self.sin_m = math.sin(m)
196
+ self.large_neg = -1e9 # для masking
197
+
198
+ def inference(self, x):
199
+ x_norm = F.normalize(x, p=2, dim=1)
200
+ w_norm = F.normalize(self.weight, p=2, dim=1)
201
+ logits = F.linear(x_norm, w_norm) * self.s
202
+ return logits
203
+
204
+ def forward(self, x, labels=None):
205
+ if not self.training or labels is None:
206
+ return self.inference(x)
207
+ x = F.normalize(x, dim=1)
208
+ W = F.normalize(self.weight, dim=1)
209
+
210
+ cosine = F.linear(x, W) # [B, C]
211
+ sine = torch.sqrt(1.0 - cosine ** 2 + self.eps)
212
+
213
+ cos_theta_y = cosine[torch.arange(x.size(0)), labels]
214
+ phi = cos_theta_y * self.cos_m - sine[torch.arange(x.size(0)), labels] * self.sin_m
215
+
216
+ logits = cosine.clone()
217
+ logits[torch.arange(x.size(0)), labels] = phi
218
+
219
+ # RAM: отсечка легкоразделённых негативов
220
+ diff = cos_theta_y.unsqueeze(1) - cosine
221
+ mask = (diff > self.m)
222
+ mask[torch.arange(x.size(0)), labels] = False
223
+
224
+ logits = torch.where(mask, self.large_neg, logits)
225
+
226
+ return self.s * logits
227
+
228
+
229
+ class WeightCrossEntropy(nn.Module):
230
+ def __init__(self, id2name: list, class_distribution: dict):
231
+ super().__init__()
232
+ weight = torch.Tensor([1 / math.sqrt(class_distribution[name]) for name in id2name])
233
+ self.ce = nn.CrossEntropyLoss(weight=weight)
234
+ def forward(self, input, target):
235
+ return self.ce(input, target)
236
+
237
+
238
+ class CrossEntropyLabelSmooth(nn.Module):
239
+ """Cross entropy loss with label smoothing regularizer.
240
+
241
+ Reference:
242
+ Szegedy et al. Rethinking the Inception Architecture for Computer Vision. CVPR 2016.
243
+ Equation: y = (1 - epsilon) * y + epsilon / K.
244
+
245
+ Args:
246
+ num_classes (int): number of classes.
247
+ epsilon (float): weight.
248
+ """
249
+
250
+ def __init__(self, num_classes, epsilon=0.1, id2name=None, class_distribution=None):
251
+ super(CrossEntropyLabelSmooth, self).__init__()
252
+ self.num_classes = num_classes
253
+ self.epsilon = epsilon
254
+ self.logsoftmax = nn.LogSoftmax(dim=1)
255
+ if id2name is not None and class_distribution is not None:
256
+ weights = torch.Tensor([1 / math.sqrt(class_distribution[name]) for name in id2name])
257
+ self.weights = weights.to(torch.float32)
258
+ else:
259
+ self.weights = None
260
+
261
+ def forward(self, inputs, targets, use_label_smoothing=True):
262
+ """
263
+ Args:
264
+ inputs: prediction matrix (before softmax) with shape (batch_size, num_classes)
265
+ targets: ground truth labels with shape (b,)
266
+ """
267
+ #targets = torch.zeros(labels.size(0), self.num_classes).to(labels.device)
268
+ #targets.scatter_(1, labels.unsqueeze(1), 1)
269
+ #targets = targets.long()
270
+ log_probs = self.logsoftmax(inputs)
271
+ targets = torch.zeros(log_probs.size()).scatter_(1, targets.unsqueeze(1).data.cpu(), 1).to(targets.device)
272
+ #if self.use_gpu: targets = targets #.to(torch.device('cuda'))
273
+ if use_label_smoothing:
274
+ targets = (1 - self.epsilon) * targets + self.epsilon / self.num_classes
275
+ loss = (- targets * log_probs)
276
+ if self.weights is not None:
277
+ weights = self.weights.to(loss.device)
278
+ loss = loss * weights.unsqueeze(0) # (batch_size, num_classes)
279
+ loss = loss.sum(dim=1).mean()
280
+ return loss
281
+
282
+
283
+
284
+
285
+ class AMSoftmaxLoss(nn.Module):
286
+ def __init__(self, in_features, out_features, num_classes, s=30.0, m=0.50, easy_margin=False, epsilon=0.1, id2name=None, class_distribution=None):
287
+ super(AMSoftmaxLoss, self).__init__()
288
+ self.aam = AAMSoftmax(in_features, out_features, s, m, easy_margin)
289
+ self.criterion = CrossEntropyLabelSmooth(num_classes, epsilon, id2name, class_distribution)
290
+
291
+ def forward(self, inputs, targets):
292
+ return self.criterion(self.aam(inputs, targets), targets)
src/models.py ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ from typing import Dict, List, Tuple, Union
4
+
5
+ import hydra
6
+ import omegaconf
7
+ import torch
8
+ from torch import Tensor, nn
9
+
10
+
11
+ import omegaconf
12
+ from gigaam.model import GigaAM, GigaAMEmo
13
+ from gigaam.preprocess import SAMPLE_RATE, load_audio
14
+ from gigaam.utils import onnx_converter
15
+
16
+ import torch.nn.functional as F
17
+ from dataclasses import dataclass
18
+ @dataclass
19
+ class AudioBatch:
20
+ wavs: torch.Tensor
21
+ wav_lengths: torch.Tensor
22
+ emotions: torch.LongTensor
23
+
24
+ class FeatureExtractorGigaAM(nn.Module):
25
+ def __init__(self, cfg):
26
+ super().__init__()
27
+
28
+ #checkpoint = torch.load(path_to_weight, map_location="cpu", weights_only=False)
29
+
30
+ self.fe = GigaAM(cfg)
31
+ #self.fe.load_state_dict(checkpoint["state_dict"], strict=False)
32
+ def forward(self, features, feature_lengths): #input raw wavs, attention mask [B, WAV_LEN]
33
+ return self.fe(features, feature_lengths) # return [B, EMB_DIM, T], [B]
34
+
35
+ class MaxPooling(nn.Module):
36
+ def __init__(self):
37
+ super().__init__()
38
+
39
+ def forward(self, features, feature_lengths):
40
+ # features: [B, T, D]
41
+ features = features.transpose(1, 2) # → [B, D, T]
42
+ pooled = F.max_pool1d(features, kernel_size=features.shape[-1]) # → [B, D, 1]
43
+ return pooled.squeeze(-1) # → [B, D]
44
+
45
+
46
+ class AttentionStatsPooling(nn.Module):
47
+ def __init__(self, input_dim, attn_dim=128):
48
+ super().__init__()
49
+ self.attn = nn.Sequential(
50
+ nn.Linear(input_dim, attn_dim),
51
+ nn.Tanh(),
52
+ nn.Linear(attn_dim, 1)
53
+ )
54
+
55
+ def forward(self, x, lens): # x: [B, T, D], lens: [B]
56
+ B, T, D = x.size()
57
+ device = x.device
58
+
59
+ # [B, T, 1]
60
+ attn_scores = self.attn(x)
61
+
62
+ # создаём маску: [B, T]
63
+ mask = torch.arange(T, device=device).unsqueeze(0) < lens.unsqueeze(1) # [B, T]
64
+ mask = mask.unsqueeze(-1) # [B, T, 1]
65
+
66
+ # маскируем паддинг
67
+ attn_scores[~mask] = float('-inf')
68
+
69
+ # softmax по валидным позициям
70
+ attn_weights = F.softmax(attn_scores, dim=1) # [B, T, 1]
71
+ attn_weights = attn_weights * mask # зануляем padded веса (на всякий случай)
72
+
73
+ # считаем взвешенное среднее и std
74
+ mean = torch.sum(attn_weights * x, dim=1) # [B, D]
75
+ std = torch.sqrt(torch.sum(attn_weights * (x - mean.unsqueeze(1))**2, dim=1) + 1e-9)
76
+
77
+ return torch.cat([mean, std], dim=1) # [B, 2*D]
78
+
79
+
80
+ class SelfAttentionWithStatsPooling(nn.Module):
81
+ def __init__(self, embed_dim=768, num_heads=4, attn_dim=128, out_dim=256):
82
+ super().__init__()
83
+ self.mha = nn.MultiheadAttention(embed_dim=embed_dim, num_heads=num_heads, batch_first=True)
84
+ self.norm = nn.LayerNorm(embed_dim)
85
+ self.pool = AttentionStatsPooling(input_dim=embed_dim, attn_dim=attn_dim)
86
+ self.out = nn.Linear(2 * embed_dim, out_dim)
87
+
88
+ def forward(self, x, lens): # x: [B, T, D], lens: [B]
89
+ B, T, D = x.size()
90
+ device = x.device
91
+
92
+ # Attention mask: [B, T]
93
+ attn_mask = torch.arange(T, device=device).unsqueeze(0) >= lens.unsqueeze(1) # pad == True
94
+
95
+ # Преобразуем для MultiheadAttention: [B, T] → [B, T] → [B, T] → [B, T] (bool)
96
+
97
+ attn_out, _ = self.mha(x, x, x, key_padding_mask=attn_mask) # [B, T, D]
98
+ attn_out = self.norm(attn_out + x)
99
+
100
+ pooled = self.pool(attn_out, lens) # [B, 2*D]
101
+ return self.out(pooled) # [B, out_dim]
102
+
103
+
104
+
105
+
106
+ class EmotionModel(nn.Module):
107
+ def __init__(self, config):
108
+ super().__init__()
109
+
110
+ self.feature_extractor = FeatureExtractorGigaAM(config.feature_extractor.cfg)#hydra.utils.instantiate(config.feature_extractor)
111
+ self.pooling = hydra.utils.instantiate(config.pooling)
112
+ self.head = hydra.utils.instantiate(config.head)
113
+
114
+ def forward(self, batch: AudioBatch):
115
+ feats, lengths = self.feature_extractor(batch.wavs, batch.wav_lengths) # return [B, EMB_DIM, T], [B]
116
+ feats = feats.transpose(1, 2)# [B, T, EMB_DIM]
117
+
118
+ pooled = self.pooling(feats, lengths) # [B, NEW_EMB_DIM]
119
+ if hasattr(self.head, "use_labels_when_train") and self.head.use_labels_when_train is True:
120
+ logit = self.head(pooled, batch.emotions)
121
+ else:
122
+ logit = self.head(pooled)
123
+ return logit, None # [B, NUM_CLASSES]