-
Notifications
You must be signed in to change notification settings - Fork 6
Add audeering wav2vec2 dimensional emotion (speech regression) support #1084
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
DingmaomaoBJTU
wants to merge
6
commits into
main
Choose a base branch
from
add-audeering-wav2vec2-emotion
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
f3b531a
Add audeering wav2vec2 emotion support
github-actions[bot] 6efc3a7
fix(wav2vec2): satisfy mypy no-any-return in RegressionHead.forward
github-actions[bot] 7110135
fix(wav2vec2): address PR review feedback
github-actions[bot] 410546f
Move audeering emotion recipe under cpu/cpu EP/device folder and add …
github-actions[bot] 540af6f
Revert examples/recipes/README.md to main
github-actions[bot] a9a1af5
recipe(wav2vec2): add cpu/cpu fp16 alongside fp32 for audeering emotion
github-actions[bot] File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
43 changes: 43 additions & 0 deletions
43
...wav2vec2-large-robust-12-ft-emotion-msp-dim/cpu/cpu/audio-classification_fp16_config.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| { | ||
| "export": { | ||
| "opset_version": 17, | ||
| "batch_size": 1, | ||
| "export_params": true, | ||
| "do_constant_folding": true, | ||
| "verbose": false, | ||
| "dynamo": false, | ||
| "enable_hierarchy_tags": false, | ||
| "clean_onnx": false, | ||
| "hierarchy_tag_format": "full", | ||
| "input_tensors": [ | ||
| { | ||
| "name": "input_values", | ||
| "dtype": "float32", | ||
| "shape": [ | ||
| 1, | ||
| 16000 | ||
| ], | ||
| "value_range": [ | ||
| -1, | ||
| 1 | ||
| ] | ||
| } | ||
| ], | ||
| "output_tensors": [ | ||
| { | ||
| "name": "hidden_states" | ||
| }, | ||
| { | ||
| "name": "logits" | ||
| } | ||
| ] | ||
| }, | ||
| "optim": {}, | ||
| "quant": null, | ||
| "compile": null, | ||
| "loader": { | ||
| "task": "audio-classification", | ||
| "model_class": "EmotionModel", | ||
| "model_type": "wav2vec2_emotion_regression" | ||
| } | ||
| } |
43 changes: 43 additions & 0 deletions
43
...wav2vec2-large-robust-12-ft-emotion-msp-dim/cpu/cpu/audio-classification_fp32_config.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| { | ||
| "export": { | ||
| "opset_version": 17, | ||
| "batch_size": 1, | ||
| "export_params": true, | ||
| "do_constant_folding": true, | ||
| "verbose": false, | ||
| "dynamo": false, | ||
| "enable_hierarchy_tags": false, | ||
| "clean_onnx": false, | ||
| "hierarchy_tag_format": "full", | ||
| "input_tensors": [ | ||
| { | ||
| "name": "input_values", | ||
| "dtype": "float32", | ||
| "shape": [ | ||
| 1, | ||
| 16000 | ||
| ], | ||
| "value_range": [ | ||
| -1, | ||
| 1 | ||
| ] | ||
| } | ||
| ], | ||
| "output_tensors": [ | ||
| { | ||
| "name": "hidden_states" | ||
| }, | ||
| { | ||
| "name": "logits" | ||
| } | ||
| ] | ||
| }, | ||
| "optim": {}, | ||
| "quant": null, | ||
| "compile": null, | ||
| "loader": { | ||
| "task": "audio-classification", | ||
| "model_class": "EmotionModel", | ||
| "model_type": "wav2vec2_emotion_regression" | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| # ------------------------------------------------------------------------- | ||
| # Copyright (c) Microsoft Corporation. All rights reserved. | ||
| # Licensed under the MIT License. | ||
| # -------------------------------------------------------------------------- | ||
| """Audeering wav2vec2 emotion-regression export variant.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from typing import Any, cast | ||
|
|
||
| import torch | ||
| import torch.nn as nn | ||
| from optimum.exporters.onnx import OnnxConfig | ||
| from optimum.utils import NormalizedConfig | ||
| from optimum.utils.input_generators import DummyAudioInputGenerator | ||
| from transformers.models.wav2vec2.modeling_wav2vec2 import ( | ||
| Wav2Vec2Model, | ||
| Wav2Vec2PreTrainedModel, | ||
| ) | ||
|
|
||
| from ...export import register_onnx_overwrite | ||
| from ..winml import register_specialization | ||
|
|
||
|
|
||
| EMOTION_REGRESSION_MODEL_TYPE = "wav2vec2_emotion_regression" | ||
|
|
||
|
|
||
| class RegressionHead(nn.Module): | ||
| """Audeering dimensional-emotion regression head.""" | ||
|
|
||
| def __init__(self, config: Any) -> None: | ||
| super().__init__() | ||
| self.dense = nn.Linear(config.hidden_size, config.hidden_size) | ||
| self.dropout = nn.Dropout(config.final_dropout) | ||
| self.out_proj = nn.Linear(config.hidden_size, config.num_labels) | ||
|
|
||
| def forward(self, features: torch.Tensor) -> torch.Tensor: # noqa: D102 | ||
| x = features | ||
| x = self.dropout(x) | ||
| x = self.dense(x) | ||
| x = torch.tanh(x) | ||
| x = self.dropout(x) | ||
| return cast("torch.Tensor", self.out_proj(x)) | ||
|
|
||
|
|
||
| class EmotionModel(Wav2Vec2PreTrainedModel): | ||
| """Audeering wav2vec2 mean-pooling regression model.""" | ||
|
|
||
| def __init__(self, config: Any) -> None: | ||
| super().__init__(config) | ||
| self.config = config | ||
| self.wav2vec2 = Wav2Vec2Model(config) | ||
| self.classifier = RegressionHead(config) | ||
| self.config.model_type = EMOTION_REGRESSION_MODEL_TYPE | ||
| self.init_weights() | ||
|
|
||
| def forward(self, input_values: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: # noqa: D102 | ||
| outputs = self.wav2vec2(input_values) | ||
| hidden_states = outputs[0] | ||
| hidden_states = torch.mean(hidden_states, dim=1) | ||
| logits = self.classifier(hidden_states) | ||
| return hidden_states, logits | ||
|
|
||
|
|
||
| _EMOTION_NORMALIZED_CONFIG = NormalizedConfig.with_args( | ||
| hidden_size="hidden_size", | ||
| num_labels="num_labels", | ||
| allow_new=True, | ||
| ) | ||
|
|
||
|
|
||
| @register_onnx_overwrite( | ||
| EMOTION_REGRESSION_MODEL_TYPE, | ||
| "audio-classification", | ||
| library_name="transformers", | ||
| ) | ||
| class Wav2Vec2EmotionRegressionIOConfig(OnnxConfig): # type: ignore[misc] # optimum base is untyped | ||
| """ONNX config for raw waveform -> hidden_states, logits.""" | ||
|
|
||
| NORMALIZED_CONFIG_CLASS = _EMOTION_NORMALIZED_CONFIG | ||
| DUMMY_INPUT_GENERATOR_CLASSES = (DummyAudioInputGenerator,) | ||
|
|
||
| @property | ||
| def inputs(self) -> dict[str, dict[int, str]]: # noqa: D102 | ||
| return {"input_values": {0: "batch_size", 1: "audio_sequence_length"}} | ||
|
|
||
| @property | ||
| def outputs(self) -> dict[str, dict[int, str]]: # noqa: D102 | ||
| return { | ||
| "hidden_states": {0: "batch_size"}, | ||
| "logits": {0: "batch_size"}, | ||
| } | ||
|
|
||
|
|
||
| MODEL_CLASS_MAPPING: dict[tuple[str, str], type] = { | ||
|
DingmaomaoBJTU marked this conversation as resolved.
|
||
| ("wav2vec2-emotion-regression", "audio-classification"): EmotionModel, | ||
| } | ||
|
|
||
| register_specialization( | ||
| EMOTION_REGRESSION_MODEL_TYPE, | ||
| "audio-classification", | ||
| "WinMLModelForGenericTask", | ||
| ) | ||
|
|
||
|
|
||
| __all__ = [ | ||
| "EMOTION_REGRESSION_MODEL_TYPE", | ||
| "MODEL_CLASS_MAPPING", | ||
| "EmotionModel", | ||
| "RegressionHead", | ||
| "Wav2Vec2EmotionRegressionIOConfig", | ||
| ] | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,142 @@ | ||
| # ------------------------------------------------------------------------- | ||
| # Copyright (c) Microsoft Corporation. All rights reserved. | ||
| # Licensed under the MIT License. | ||
| # -------------------------------------------------------------------------- | ||
| """Tests for the audeering wav2vec2 dimensional-emotion export variant. | ||
|
|
||
| The audeering ``wav2vec2-large-robust-12-ft-emotion-msp-dim`` checkpoint ships a | ||
| custom mean-pooling ``RegressionHead`` that transformers/Optimum cannot route on | ||
| their own. Support hinges on three contracts these tests lock in: | ||
|
|
||
| - the ``("wav2vec2-emotion-regression", "audio-classification")`` entry in | ||
| ``MODEL_CLASS_MAPPING`` resolving to ``EmotionModel``; | ||
| - ``resolve_task`` honouring the build variant's underscore ``model_type`` | ||
| (``wav2vec2_emotion_regression``) via the ``_`` -> ``-`` normalization the | ||
| mapping key depends on; | ||
| - ``Wav2Vec2EmotionRegressionIOConfig`` registering for the | ||
| ``audio-classification`` ONNX export with the expected input/output axes. | ||
|
|
||
| A rename of ``EMOTION_REGRESSION_MODEL_TYPE`` or a resolver normalization change | ||
| would otherwise break routing with nothing failing in CI. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import pytest | ||
| from optimum.exporters.tasks import TasksManager | ||
| from optimum.utils.input_generators import DummyAudioInputGenerator | ||
| from transformers import Wav2Vec2Config | ||
|
|
||
| from winml.modelkit.loader import resolve_task | ||
| from winml.modelkit.models.hf import MODEL_CLASS_MAPPING | ||
| from winml.modelkit.models.hf.wav2vec2 import ( | ||
| EMOTION_REGRESSION_MODEL_TYPE, | ||
| EmotionModel, | ||
| Wav2Vec2EmotionRegressionIOConfig, | ||
| ) | ||
| from winml.modelkit.models.hf.wav2vec2 import MODEL_CLASS_MAPPING as WAV2VEC2_MAPPING | ||
|
|
||
|
|
||
| # ============================================================================= | ||
| # Test Constants | ||
| # ============================================================================= | ||
|
|
||
| TASK = "audio-classification" | ||
| MAPPING_KEY = ("wav2vec2-emotion-regression", TASK) | ||
|
|
||
|
|
||
| # ============================================================================= | ||
| # Fixtures | ||
| # ============================================================================= | ||
|
|
||
|
|
||
| @pytest.fixture(scope="module") | ||
| def emotion_config(): | ||
| """Minimal Wav2Vec2Config exercising the emotion-regression head dimensions.""" | ||
| return Wav2Vec2Config(hidden_size=16, num_labels=3) | ||
|
|
||
|
|
||
| # ============================================================================= | ||
| # MODEL_CLASS_MAPPING — routing to the custom regression wrapper | ||
| # ============================================================================= | ||
|
|
||
|
|
||
| class TestWav2Vec2EmotionModelClassMapping: | ||
| """The emotion-regression variant routes to EmotionModel.""" | ||
|
|
||
| def test_mapping_entry_registered(self): | ||
| """The aggregated mapping exposes the emotion-regression entry.""" | ||
| assert MAPPING_KEY in MODEL_CLASS_MAPPING | ||
| assert MODEL_CLASS_MAPPING[MAPPING_KEY] is EmotionModel | ||
|
|
||
| def test_module_mapping_merged_into_aggregate(self): | ||
| """The module-level mapping is included in the aggregated mapping.""" | ||
| assert WAV2VEC2_MAPPING.items() <= MODEL_CLASS_MAPPING.items() | ||
|
|
||
|
|
||
| # ============================================================================= | ||
| # resolve_task — underscore model_type normalization contract | ||
| # ============================================================================= | ||
|
|
||
|
|
||
| class TestWav2Vec2EmotionTaskResolution: | ||
| """resolve_task honours the underscore build-variant model_type.""" | ||
|
|
||
| def test_model_type_override_resolves_emotion_model(self, emotion_config): | ||
| """An underscore model_type override normalizes and resolves EmotionModel.""" | ||
| resolution = resolve_task( | ||
| emotion_config, | ||
| task=TASK, | ||
| model_class="EmotionModel", | ||
| model_type_override=EMOTION_REGRESSION_MODEL_TYPE, | ||
| ) | ||
|
|
||
| assert resolution.task == TASK | ||
| assert resolution.model_class is EmotionModel | ||
|
|
||
| def test_registered_model_type_uses_underscores(self): | ||
| """The registered model_type uses underscores; the mapping key uses dashes.""" | ||
| assert EMOTION_REGRESSION_MODEL_TYPE == "wav2vec2_emotion_regression" | ||
| assert EMOTION_REGRESSION_MODEL_TYPE.replace("_", "-") == MAPPING_KEY[0] | ||
|
|
||
|
|
||
| # ============================================================================= | ||
| # Wav2Vec2EmotionRegressionIOConfig — registration and I/O axes | ||
| # ============================================================================= | ||
|
|
||
|
|
||
| class TestWav2Vec2EmotionRegressionIOConfig: | ||
| """The ONNX export config is registered with the expected axes.""" | ||
|
|
||
| def test_onnx_config_registered(self): | ||
| """Config is registered with TasksManager for audio-classification.""" | ||
| config_cls = TasksManager.get_exporter_config_constructor( | ||
| model_type=EMOTION_REGRESSION_MODEL_TYPE, | ||
| exporter="onnx", | ||
| task=TASK, | ||
| library_name="transformers", | ||
| ) | ||
| assert config_cls.func.__name__ == Wav2Vec2EmotionRegressionIOConfig.__name__ | ||
|
|
||
| def test_inputs_axes(self, emotion_config): | ||
| """Inputs expose input_values with batch and audio-length dynamic axes.""" | ||
| io_config = Wav2Vec2EmotionRegressionIOConfig(emotion_config, task=TASK) | ||
|
|
||
| assert io_config.inputs == { | ||
| "input_values": {0: "batch_size", 1: "audio_sequence_length"}, | ||
| } | ||
|
|
||
| def test_outputs_axes(self, emotion_config): | ||
| """Outputs expose hidden_states and logits with a batch dynamic axis.""" | ||
| io_config = Wav2Vec2EmotionRegressionIOConfig(emotion_config, task=TASK) | ||
|
|
||
| assert io_config.outputs == { | ||
| "hidden_states": {0: "batch_size"}, | ||
| "logits": {0: "batch_size"}, | ||
| } | ||
|
|
||
| def test_dummy_input_generator_class(self): | ||
| """Uses DummyAudioInputGenerator for raw-waveform dummy inputs.""" | ||
| assert ( | ||
| DummyAudioInputGenerator, | ||
| ) == Wav2Vec2EmotionRegressionIOConfig.DUMMY_INPUT_GENERATOR_CLASSES |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This
_fp32_config.jsonis byte-identical to its_fp16_config.jsonsibling (same git blob), and it's the only_fp32_config.jsonin the whole repo — the other 76 curated models each ship a single_fp16_config.json. Because both files set"quant": null, they produce the same output: with-c ..._fp16_config.jsonalone you still get an fp32 model, since fp16 conversion only kicks in whenquant.mode == "fp16"(build.py:1377) or when--precision fp16is passed on the CLI (which patches quant regardless of which of these two files you point at). So the fp16/fp32 filenames here imply a difference that doesn't exist, and your owntest_cpu_recipes.pyassertsquant is Nonefor both.Two consistent options:
_fp16_config.json, matching the single-recipe-per-(model,task) convention (fp16 is what the README advertises; fp32 isn't in the variant taxonomy).-calone yields fp16 without--precision), give the fp16 file a realquantblock with"mode": "fp16"— but note no recipe in the repo currently does that, so option 1 is the lower-friction path.Non-blocking, but shipping two identical files under different precision names is likely to confuse users.