diff --git a/.github/workflows/basic_tests_for_aud_csv.yaml b/.github/workflows/basic_tests_for_aud_csv.yaml index ad55e24d..dd0fd84e 100644 --- a/.github/workflows/basic_tests_for_aud_csv.yaml +++ b/.github/workflows/basic_tests_for_aud_csv.yaml @@ -36,8 +36,9 @@ jobs: - name: Run csv-ravdess-praat-xgb run: | cd data/ravdess - wget https://zenodo.org/record/1188976/files/Audio_Speech_Actors_01-24.zip - unzip Audio_Speech_Actors_01-24.zip + # wget https://zenodo.org/record/1188976/files/Audio_Speech_Actors_01-24.zip + wget https://zenodo.org/records/11063852/files/Audio_Speech_Actors_01-24_16k.zip + unzip Audio_Speech_Actors_01-24_16k.zip cd ../.. python3 -m nkululeko.nkululeko --config data/ravdess/exp_praat_xgb.ini > output1.txt if grep -q "DONE" output1.txt; then diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a3c518d..5001c13f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,43 @@ Changelog ========= +Version 0.84.0 +-------------- +* added SHAP analysis +* started with finetuning + +Version 0.83.3 +-------------- +* fixed a naming error in trill features that prevented storage of experiment + +Version 0.83.2 +-------------- +* added default cuda if present and not stated + +Version 0.83.1 +-------------- +* add test module to nkuluflag + +Version 0.83.0 +-------------- +* test module now prints out reports + +Version 0.82.4 +-------------- +* fixed bug in wavlm + +Version 0.82.3 +-------------- +* fixed another audformat peculiarity to interprete time values as nanoseconds + +Version 0.82.2 +-------------- +* fixed audformat peculiarity that dataframes can have only one column + +Version 0.82.1 +-------------- +* Add more test for GC action + Version 0.82.0 -------------- * added nkuluflag module diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..8fd6e21c --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,72 @@ +# Code of Conduct + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of +experience, education, socioeconomic status, nationality, personal appearance, +race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* using welcoming and inclusive language, +* being respectful of differing viewpoints and experiences, +* gracefully accepting constructive criticism, +* focusing on what is best for the community, and +* showing empathy towards other community members. + +Examples of unacceptable behavior by participants include: + +* the use of sexualized language or imagery and unwelcome sexual + attention or advances, +* trolling, insulting/derogatory comments, and personal or political + attacks, +* public or private harassment, +* publishing others' private information, such as a physical or + electronic address, without explicit permission, and +* other conduct which could reasonably be considered inappropriate in + a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, or to ban temporarily or permanently any +contributor for other behaviors that they deem inappropriate, threatening, +offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project email +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by emailing the project team. All complaints will be reviewed +and investigated and will result in a response that is deemed necessary and +appropriate to the circumstances. The project team is obligated to maintain +confidentiality with regard to the reporter of an incident. Further details of +specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][contributor_covenant] +version 1.4. + +[contributor_covenant]: https://www.contributor-covenant.org/ diff --git a/data/crema-d/load_db.py b/data/crema-d/load_db.py index 86d97581..ba60852e 100644 --- a/data/crema-d/load_db.py +++ b/data/crema-d/load_db.py @@ -8,4 +8,4 @@ audb.config.CACHE_ROOT = cwd # load the latest version of the data -db = audb.load("crema-d", format="wav", sampling_rate=16000, mixdown=True) +db = audb.load("crema-d", version="1.3.0", verbose=True) diff --git a/data/ravdess/README.md b/data/ravdess/README.md index d4010dbe..2c78de41 100644 --- a/data/ravdess/README.md +++ b/data/ravdess/README.md @@ -5,10 +5,14 @@ I used the version downloadable from [Zenodo](https://zenodo.org/record/1188976) Download and unzip the file Audio_Speech_Actors_01-24.zip ```bash +# download original dataset in 48k $ wget https://zenodo.org/record/1188976/files/Audio_Speech_Actors_01-24.zip $ unzip Audio_Speech_Actors_01-24.zip ``` +Or, if you prefer the dataset in 16k, you can download from this link: +https://zenodo.org/records/11063852/files/Audio_Speech_Actors_01-24_16k.zip + run the file ```bash python3 process_database.py diff --git a/data/ravdess/exp_praat_xgb.ini b/data/ravdess/exp_praat_xgb.ini index ded9f0bb..bd2cc770 100644 --- a/data/ravdess/exp_praat_xgb.ini +++ b/data/ravdess/exp_praat_xgb.ini @@ -10,10 +10,12 @@ dev = ./data/ravdess/ravdess_dev.csv dev.type = csv dev.absolute_path = False dev.split_strategy = train +dev.audio_path = Audio_Speech_Actors_01-24_16k/ test = ./data/ravdess/ravdess_test.csv test.type = csv test.absolute_path = False test.split_strategy = test +test.audio_path = Audio_Speech_Actors_01-24_16k/ target = emotion labels = ['angry', 'happy', 'neutral', 'sad'] [FEATS] diff --git a/ini_file.md b/ini_file.md index 5047912b..37226779 100644 --- a/ini_file.md +++ b/ini_file.md @@ -330,7 +330,9 @@ * **dist_type**: type of plot for value counts, either histogram or density estimation (kde) * dist_type = hist * **spotlight**: open a web-browser window to inspect the data with the [spotlight software](https://github.com/Renumics/spotlight). Needs package *renumics-spotlight* to be installed! - * spotlight = False + * spotlight = False +* **shap**: comopute [SHAP](https://shap.readthedocs.io/en/latest/) values + * shap = False ### [PREDICT](#predict) * **targets**: Speaker/speech characteristics to be predicted by some models * targets = ['gender', 'age', 'snr', 'arousal', 'valence', 'dominance', 'pesq', 'mos'] diff --git a/meta/demos/multiple_exeriments/do_experiments.py b/meta/demos/multiple_experiments/do_experiments.py similarity index 85% rename from meta/demos/multiple_exeriments/do_experiments.py rename to meta/demos/multiple_experiments/do_experiments.py index 70fcd3c0..d75b7834 100644 --- a/meta/demos/multiple_exeriments/do_experiments.py +++ b/meta/demos/multiple_experiments/do_experiments.py @@ -19,13 +19,13 @@ # {'--feat': 'os', # '--set': 'ComParE_2016', # }, - {"--feat": "audmodel"}, + {"--feat": "praat"}, ] for c in classifiers: for f in features: - cmd = "python -m nkululeko.nkuluflag --config exp.ini " + cmd = "python -m nkululeko.nkuluflag --config meta/demos/multiple_exeriments/exp.ini " for item in c: cmd += f"{item} {c[item]} " for item in f: diff --git a/meta/demos/multiple_exeriments/exp.ini b/meta/demos/multiple_experiments/exp.ini similarity index 96% rename from meta/demos/multiple_exeriments/exp.ini rename to meta/demos/multiple_experiments/exp.ini index a50cf430..fcf76dc1 100644 --- a/meta/demos/multiple_exeriments/exp.ini +++ b/meta/demos/multiple_experiments/exp.ini @@ -11,6 +11,7 @@ emodb.train_tables = ['emotion.categories.train.gold_standard'] emodb.test_tables = ['emotion.categories.test.gold_standard'] target = emotion labels = ['anger', 'happiness'] +tests = ['emodb'] [FEATS] [MODEL] C_val = .001 diff --git a/meta/demos/multiple_experiments/tmp.ini b/meta/demos/multiple_experiments/tmp.ini new file mode 100644 index 00000000..de887a4d --- /dev/null +++ b/meta/demos/multiple_experiments/tmp.ini @@ -0,0 +1,28 @@ +[EXP] +root = ./ +name = results +runs = 1 +epochs = 1 + +[DATA] +databases = ['emodb'] +emodb = ../../../data/emodb/emodb +emodb.split_strategy = specified +emodb.train_tables = ['emotion.categories.train.gold_standard'] +emodb.test_tables = ['emotion.categories.test.gold_standard'] +target = emotion +labels = ['anger', 'happiness'] + +[FEATS] +type = ['praat'] + +[MODEL] +c_val = .001 +learning_rate = 0.0001 +store = True +patience = 5 +type = svm + +[PLOT] +best_model = True + diff --git a/meta/demos/result_visualization/plot_multiple_experiment_results.ipynb b/meta/demos/result_visualization/plot_multiple_experiment_results.ipynb new file mode 100644 index 00000000..4fc86f27 --- /dev/null +++ b/meta/demos/result_visualization/plot_multiple_experiment_results.ipynb @@ -0,0 +1,194 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "import pandas as pd\n", + "import numpy as np" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def barplot(df, title=\"Results\", \\\n", + " xlab='Data', metric=\"UAR\", \\\n", + " ylim_low=.4, ylim_up=1, bar_width = 0.12, \\\n", + " fontsize=10):\n", + " \"\"\"A bar plot for 2 dimensional results: e.g. models vs. features\n", + " \n", + " \"\"\"\n", + " colors = ['tab:red',\n", + " 'tab:blue',\n", + " 'tab:orange',\n", + " 'tab:green',\n", + " 'tab:purple',\n", + " 'tab:cyan',\n", + " 'tab:brown',\n", + " 'tab:olive',\n", + " 'tab:pink',\n", + " ]\n", + " bar_width = bar_width\n", + "\n", + " fig, ax = plt.subplots(figsize=(12, 8))\n", + " br = []\n", + " br.append(np.arange(df.shape[1]))\n", + " for m in range(df.shape[0]):\n", + " br.append([x + bar_width for x in br[-1]])\n", + "\n", + " for im in range(df.shape[0]):\n", + " bars = plt.bar(br[im],\n", + " df.iloc[im],\n", + " color=colors[im],\n", + " width=bar_width,\n", + " edgecolor='k',\n", + " label=df.index[im])\n", + " # Add labels to the bars\n", + " for bar, value in zip(bars, df.iloc[im]):\n", + " value = f'.{(int)(100*value)}'\n", + " plt.text(bar.get_x() + bar.get_width() / 2,\n", + " bar.get_height() + 0.01,\n", + " f'{value}', # Format the value to 2 decimal places\n", + " ha='center',\n", + " va='bottom',\n", + " fontsize=fontsize)\n", + "\n", + " plt.xlabel(xlab, fontweight='bold', fontsize=15)\n", + " plt.ylabel(metric, fontweight='bold', fontsize=15)\n", + " plt.xticks([r + (df.shape[0]-1)//2*bar_width for r in range(df.shape[1])],\n", + " df.columns,\n", + " fontsize=15)\n", + " plt.title(title.upper(), fontweight='bold', fontsize=15)\n", + " plt.grid()\n", + " plt.legend(fontsize=14)\n", + " plt.ylim([ylim_low, ylim_up])\n", + " plt.tight_layout()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# hacky code to collect the results from storage\n", + "\n", + "feats = [\n", + " \"os\",\n", + " \"praat\",\n", + " \"audmodel\",\n", + " \"hubert-large-ll60k\",\n", + " \"trill\",\n", + " \"whisper-medium\",\n", + " \"wavlm-large\",\n", + " \"wav2vec2\",\n", + "]\n", + "#result_key = \"dev\"\n", + "result_key = \"test\"\n", + "\n", + "models = [\"mlp\",\"xgb\",\"svm\"]\n", + "result_arrays = {}\n", + "for m in models:\n", + " result_arrays[m] = []\n", + "for f in feats:\n", + " for m in models:\n", + " if result_key == \"dev\":\n", + " if m == \"mlp\":\n", + " fn = f\"results/results/run_0/train_dev_{m}_{f}_16-64_C_val-10_drop-3_scale-standard_conf.txt\"\n", + " else:\n", + " fn = f\"results/results/run_0/train_dev_{m}_{f}_C_val-10_drop-3_scale-standard_conf.txt\"\n", + " elif result_key == \"test\":\n", + " if m == \"mlp\":\n", + " fn = f\"results/results/run_0/train_dev_{m}_{f}_16-64_C_val-10_drop-3_scale-standard_test-test_conf.txt\"\n", + " else:\n", + " fn = f\"results/results/run_0/train_dev_{m}_{f}_C_val-10_drop-3_scale-standard_test-test_conf.txt\"\n", + " file_in = open(fn, \"r\")\n", + " line = file_in.read()\n", + " y = line.split(\"\\n\")[0].split(\" \")[3].replace(\",\", \"\")\n", + " e = line.split(\"\\n\")[0].split(\" \")[4]\n", + " y = int(float(y) * 1000) / 1000.0\n", + " print(f, y, e)\n", + " result_arrays[m].append(y)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# make a dataframe from the results\n", + "db_df = pd.DataFrame(result_arrays, index = feats)\n", + "db_df['mean'] = db_df.mean(numeric_only=True, axis=1)\n", + "db_df" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# ... and plot it\n", + "barplot(db_df,title = f'results {result_key}',ylim_low =.25, ylim_up=0.8, bar_width=.1, fontsize=15)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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tm17y8Dobt289NJgMOTJuF27dpWLi4vpdfYSvszMTLON3z08PBQcHJwn1wQAAHmPUAoAABR4qampGjFiRK6eghcQEKCsrCyrXyVKlLDo//zzzys2NlYbN27UuHHj1KpVKxUuXNisz7Vr1zR79myztn/vayRZBjCSTEvncjonvxkMBo0YMUKnT5/WqlWrNHLkSDVt2tSijosXL5ots8srJ0+eNHvt6emZY9/vv//e6q/ZL7/8YtHX3d1d7dq1M73+66+/dOjQIUVERCg+Pt7U3qNHj3xZmggAAPIGoRQAAChQskOlM2fOaMiQIab2M2fOmJb15bVChQopKChIEydO1IYNG2Q0GvXKK6+Y9YmJiTF77e7ubvb64sWLFuNeunTJ7LW1UMwWXFxc1K5dO33wwQfatm2bzpw5oy5dupj1ufn+7lV6eroiIiLM2vJyieDNy/IWLlxosek5S/cAACjYCKUAAECBVLZsWX3xxRdq2rSpqe3QoUMWM5buVkpKikVolK148eIaN26cWdvNM26qVq1q9nrHjh1mrzMyMsz2SCpUqJAqVap0DxXfmUuXLiklJcXqMU9PT40YMcKsLa9nFP3vf//TmTNnTK9r1KihRx99NM/Gb9++vdzc3EyvFyxYoGXLlple+/r6qlmzZnl2PQAAkPcIpQAAQIE2depUs9cffPBBnsyWSkhIkJ+fn0aPHm11ltDPP/9s9rp69epmr2/eq+jNN99UZGSkMjIylJCQoJdfftkslGnWrJmKFi16z3XnVlRUlCpVqqSpU6daLKOTZLEs7ub7u1upqan64IMPLEKvyZMn58n42VxcXNS1a1fT67///lvnz583ve7du3ee7WEFAADyhyHL2gYIAAAANhIbGyt/f3/T64CAAItlX02bNtX27dtNr7///nsNGDDA6vkeHh4KCAiweq1GjRpp5MiRVs8rX768qlevriJFiujo0aNm+1cZDAbt3btXtWvXNrWlpaXp0Ucf1bFjx8yu4ejoqIyMDLM2g8GgNWvW6KmnnrJoz1akSBE9/fTTVuv29/fXhx9+KEny8/PTiRMnTMdy+qNcRESEgoKCTK8rVaqkypUry8nJSYcOHTKrO/uey5Yta3WsmwUGBmrTpk2m108//bRcXV11/vx57dmzR5cvXzbr/5///EcfffSRWduECRM0ceJE0+sGDRrI19fX6vXe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qZIlS8rFxUWPP/64PvroI2VkZJj1CwkJkcFgUEhIiFasWKFmzZqpePHi8vPz04QJExQUFCRJmjhxogwGg+nr3/d1p37++Wf17t1blStXVpEiReTu7q4WLVpo6dKlFn3//Z4dOnRIXbp0UenSpc1qSE5O1siRI1WxYkXTvc6ePVsREREyGAxWlx0eP35cL730knx8fOTs7Cxvb28NGDBAJ06cuOv7up2pU6cqKSlJX331ldXZeYUKmc8r+v7773X9+nW98847pkBKkurUqaPevXvr0KFD2rp1622vm5iYqICAADk4OGjWrFn3fiN3gJlSAAAAAADks127dmn69OkKCgrSyy+/rL179+qXX37RX3/9pf3798vFxeWexn/22We1d+9edevWTZK0dOlSvfHGG4qNjdWMGTPM+o4ePVrvv/++ypcvr65du8rd3V1btmzRiBEjtHPnTi1evNhi/MWLF2vt2rXq0KGDXn31VSUlJSkwMFCxsbGaO3euAgICFBgYaOpfokSJu76X0aNHq3DhwmrevLm8vb117tw5/frrr+revbs+++wzDR061OKcI0eOqEmTJqpZs6YGDBig8+fPq3DhwsrIyFCHDh0UHh6umjVrqk+fPrpw4YL+85//mNX7bzt37lRwcLCuXr2qDh06qEqVKoqNjdX8+fO1evVqbd++XZUqVbrr+7MmKytLixcvVunSpdWqVSvt3r1bmzZtUmZmpurUqaNWrVpZzHiKiIiQJLVp08ZivODgYIWEhGjTpk1q2bJljtc9c+aM2rZtq+joaC1YsEA9e/bM0/u6HUIpAAAAAADyWVhYmBYuXGj2l/7+/fvrxx9/1C+//KJevXrd0/iHDx/W/v37TTNmJk6cqMaNG+uTTz5R79691aBBA0k39ix6//33FRwcrKVLl6po0aKSboQir776qr766istXbrUFG5l++2337RmzRq1bt3a4tpz585VYGBgnm10HhYWZhH6XLlyRU2bNtW7776rF198UUWKFDE7vm3bNo0bN04TJ040a58zZ47Cw8P19NNPa8WKFXJ0dJQkvfXWW6pfv77FtdPT09WrVy9lZmYqMjJSdevWNR3bunWrAgMDNWzYMK1YsSJP7jXb8ePHdeHCBTVo0EAvv/yyvvnmG7PjdevW1a+//qoKFSqY2v7++28VK1ZMZcuWtRivSpUqpj45OXz4sIKDg3X+/HmtWrXK6q9tfmP5HgAAAAAA+axly5YWs1BeeOEFSTdmUd2rd99912wJl7u7u8aOHausrCzNnTvX1P6///1PkvTNN9+YAinpxtLA999/XwaDQQsWLLAYv1OnTjYLLazNQipWrJgGDBigxMREq+9X2bJl9c4771i0z5s3T9KNDcGzAylJevTRR9W/f3+L/itXrlRsbKxGjBhhFkhJUvPmzdWpUyeFhYUpKSnpju/rVs6ePStJ2rt3r0JDQ/X999/rwoULOn78uAYNGqS9e/eqe/fuZuckJiaa/Zr/m5ubm6mPNbt27VLz5s119epVhYeH2yWQkpgpBQAAAABAvrM2Kyd71sulS5fuefwWLVrk2LZ3715T244dO1S0aFF99913VsdxdXVVdHS0RXujRo3uuKaoqCj98ssvZm1+fn457pmV7ezZs3r//fe1evVqnThxQikpKWbH//nnH4tzateurcKFC1u079u3T0WLFrUImCSpWbNmFjOSduzYIUmKiYmxOvMrPj5emZmZOnz4sBo0aKCQkBCL/bM6d+6sOnXq3PIeb5aZmSlJysjI0OTJk03vUcmSJfXNN9/ozz//1M6dO7V161Y1b978jsa+2ZYtWzRjxgx5enpqzZo1pllV9kAoBQAAAABAPsueufJv2RtX37y5+N3w8vLKse3fs2UuXLig69evWyxz+7erV6/mavzbiYqKsrhOQEDALUOpCxcuqGHDhoqLi1OzZs3UunVrlShRQo6OjoqKitLy5cuVlpaW6/qSkpJUsWJFq8esnXPhwgVJ0vz583OsUfq/9yh736Z/8/Pzu+NQ6t8znjp27Ghx/JlnntHOnTv1xx9/mEIpd3f3HGdCZc/ksjaTau/evbpy5YratGmT53tj3SlCKQAAAAAACoDsjayvX79ucSyn8CFbQkKCxRPbEhISJJkHE25ubjIYDDIajXdUm8FguKP+kjRgwIDbzoq62Zw5cxQXF6fJkydr7NixZsfef/99LV++/I7qc3Nz07lz56wey35/bu4vSStWrFCHDh1uW2/2ZuP36pFHHpGjo6MyMjKsbhKf3fbvWWNVqlTR9u3bFR8fb7GvVPZeUtZmQb3++uv6559/NGfOHPXp00fz58+3eLKfrbCnFAAAAAAABUB28HD69GmLY/9egmfNli1bcmz799K1xo0b6/z587fcAPtO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+fP68MMPNWHChIJFiXzJaWeAgho1apQOHDigZs2aycfHR8ePH9fPP/8sm82mjz/+ONsaVdcq6qmZTz/9tNLT0wt9TTcSGRlpeh8AAAAoGYr6nnbq1Kl53g0bxVteH+lcv369Zs2apZ07d+rixYsqX768WrZsqU6dOlkZrkNFRERo5syZGjFihIKCgm5Y38fHR9LVTbtycvTo0SKND46Tr6RUbmsO5eTAgQN67LHHtGPHDknSd999R1KqBBoyZIgWLVqkffv26eLFi/Lx8dGAAQMUEhKi5s2b53qeGVMz27RurZTr3AgAAAAARcmMe9o1a9bYExkoufL6SOesWbM0evRo2Ww2tWvXTjVr1tTx48e1YcMGrV+/3sKIHSc9PV3BwcHy8vLS9OnT83ROUFCQlixZoqVLl+qNN97IMrNq9+7d+vXXX80KFxYzbfe9xo0ba/78+WrSpIkMw9ChQ4fM6gp/ExYWlue6oaGhCg0NzfX4448/rscffzzfMWROzQwPDy+yqZkpqala1MtTAT75fuo0X9YeStM/N1uzWB4AAACKLzPuafft21e0QcIh8rKD3NmzZ/XKK6/I1dVV69ev1z333GM/9uOPP+r+++9XSkqKUs+nyrO2p1WhW2727NnavXu35s+fn+dZgoMHD9bkyZMVExOjHj166J133lGtWrX0888/Kzg4WC4uLtmWmUHJZFpSSpL8/f3l7Oys9PT0LI/zoXQzc2pmgI+TWlS78S6QhREZa/4jggAAACjezLqnze04So68PtK5Y8cOJScnq3PnzlkSUtLVmUDt2rVTeHi4kv9KllqYHbVjHDt2TBMmTFBQUFCum2/lpGzZslq9erW6d++udevWad26dfZj9evX14svvqi33nrLhIhhNVOnnBw/fty+BhDbn94cCjo1U5KWLl2qlJSULMeYmgkAABxh4cKF9g18Jk+enGu99evXq1u3bvLx8ZGrq6sqV66sTp06adOmTRZGi6Jm5j0tH9aXbH9/pPN63N3d89SmUxlznwRxpM2bNys+Pl5nzpxRx44d1aFDB/vXggULJEnz5s1Thw4dNGrUqCznNmvWTFFRUfr000/13HPP6emnn9bcuXMVEREhFxdT59fAQqb9JOPi4vTMM89IurpjX506dczqCsUIUzMBAEBJx1oxMPOe1smp9CYgbgb5eaSzTZs2qlChgjZt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koYOHarq1atr2rRpkqRp06apVatWqlevnpKTk7V27VotXLhQH374oSMvAwAAAAAAADkotkmpAQMG6OzZs5owYYJOnTql5s2b6/vvv7cvfh4TEyOnaxadi4+P19NPP60TJ07I09NT/v7+WrRokQYMGOCoSwAAAAAAAEAuim1SSpKeffbZXB/XCwsLy/L95MmTNXnyZAuiAgAAAAAAQGGZvBcuAAAAAAAAkB1JKQAAAAAAAFiOpBQAACXIsWPH9N5776lLly7y9fWVq6urvL291aVLF3377bc3PD86OlojR45UnTp15O7uLm9vb7Vr106ff/65BdEDAAAA/1Os15QCAABZDR48WFu3bpW7u7sCAwPl6+urI0eOaN26dVq3bp1Gjx6tGTNm5Hjud999p759+yoxMVEtWrRQYGCgzp07p19//VUnTpyw+EoAAABwsyMpBQBACVKjRg299957GjZsmMqVK2cvX7NmjXr27KmZM2eqS5cu6tSpU5bzDh48qN69e6tcuXJav3697rzzTvuxjIwMLVmyRI888ohl1wEAAADw+B4AACXIF198oWeffTZLQkqSunXrpkcffVSStHTp0mznjRkzRklJSVqwYEGWhJQkOTk5qXHjxuYFDQAAAOSApBQAAKVEs2bNJEl//fVXlvLjx49r3bp1qlu3rrp27eqI0AAAAIBseHwPAIBS4siRI5IkX1/fLOVhYWHKyMjQnXfeqbS0NK1YsUJbt25Venq6brvtNg0YMMAR4QIAAOAmR1IKAIBS4OLFi/Yd9B566KEsxw4cOCBJKlu2rNq3b6/t27dnOT5u3DhNnTrVmkABAACA/8fjewAAlAIjR47U2bNnFRgYqF69emU5duHCBUnS3LlzdfDgQS1ZskTnz59XVFSUhgwZovPnz+ull15yRNgAAAC4iTFTCgCAEu6tt97SsmXLVKlSJS1evFg2my3L8YyMDElSWlqaPv74Y/Xv31+SVLFiRS1cuFBRUVHatWuX5XEDAADg5sZMKQAASrBFixbp1Vdf1S233KI1a9aobt262eqULVvW/t9+/fplOz5ixAjT4wQAAAD+jqQUAAAl1OrVqzVixAi5urpqxYoVCgwMzLFerVq1JEl+fn7ZZlFJUu3atc0MEwAAAMgRSSkAAEqg8PBw9evXT4ZhaMmSJerUqVOude+44w5J/1tb6u/Onz9v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IcFwcXExWrRokaU8c1wqX768ERsbm+28zA/O//6+BihKrCkF/L/atWurYcOGWcqcnJxUq1YtSVKnTp2ynVO3bl1J0smTJ/Pcz8CBA7OVdenSRRUrVtThw4fz1RYAa02ePFnNmzfXf/7zH3Xv3l2GYejTTz+Vt7e3vc7WrVslXf27Llu2bLY2rremjCT16dNHLi4u2coffvhhSVfXiwBQfLRs2VKS9MwzzygsLExpaWk51gsKClKNGjW0ceNGnTlzxl6elJSk5cuX65ZbblGvXr1yPPd69yBFdX8CoGgUxXuKH374QZLUs2dPOTllf8ueucbUzp07sx37888/9dFHH2nUqFF69NFHNXz4cD311FNyc3PLsvbltVq2bKnKlStnK8+8DsYSmImkFPD/qlevnmN55pvKnI5nHktOTs5THxUrVlS5cuVyPJb5D9Vff/2Vp7YAWM/Nzc2+uGhcXJxGjhypzp07Z6mTeeNWs2bNHNvw8/O7bh+ZY8Hf1a5dWxJjBFDchISEqEOHDtq6das6duyoihUrqlOnTpo9e7YuXbpkr+fk5KSHH35YaWlpWrZsmb189erViouL00MPPaRbbrklxz6udw9SFPcnAIpOUbyniI6OliSNGzdONpstx68rV64oNjY2SzszZsxQnTp19NRTT2n27Nn69NNP9dlnn+mzzz5TQkKCLl++nGNsNWrUyLE8830LYwnMlP2jWOAmldOnEPk5DuDmcO2byYiICKWnp8vZ2dmBEQFwpPLly2vTpk3aunWrVq1apbCwMG3atEnr16/XtGnTtGXLFjVo0ECSNGTIEE2fPl1LlizRc889J0lavHixJGnw4MG59nG9exDuT4DipSjeU2RkZEiS7r77btWrVy9P/W7fvl0vvviivLy8NHv2bHXo0EG+vr5yd3eXJN166625znhiHIEjkZQCLHThwgVdvnw5x9lSMTExkq7+gwGgePrpp5/01ltvydfXV7fddps2bNigKVOmaMKECfY61apVkyQdP348xzZyK8+U2zbumeWMEUDxY7PZdPfdd+vuu++WJJ05c0ajRo3S0qVLNW7cOP3nP/+RJDVt2lS33Xabtm/friNHjqhixYpau3atfHx8cnykB8DNKXPmUs+ePfXiiy/m6ZyVK1dKkqZMmaJhw4ZlOZaYmKhTp04VbZBAESElClgs88b0Wj/88IPOnz+vunXr2t/QAihe4uLi9Mgjjyg9PV2ffvqpFi1aJB8fH02aNEk7duyw17vzzjslSevWrVN8fHy2dnIaA661YsUKpaenZyv/4osvJMn+phdA8VWlShWFhoZKkn777bcsxzJnRC1ZskRfffWVUlJSNGDAgBzXkgNwc7r//vsl/S/RlBcXLlyQlPOjeF9++aUMwyia4IAiRlIKsNjEiRPtz4lLUmxsrEJCQiRdXSQVQPH07LPPKjo6Ws8++6y6dOmiqlWrau7cuUpLS9OQIUPsCagGDRro3nvv1YULF/Tyyy/bp+BL0vr16+3JpdxER0dr4sSJWcr+/e9/6+eff1bVqlXVp0+for84AAX20Ucf6ejRo9nK165dKyn7+nKDBg2SzWbTkiVL8vToHoCbT9u2bXX//fdr69ateuaZZxQXF5etzr59+/T999/bv89clHzevHlKTU21lx84cEAvv/yy+UEDBURSCrCQn5+fmjVrpiZNmqhHjx7q06ePGjRooP3796tjx456/vnnHR0igBx8+eWXWrhwoRo3bqy3337bXt6jRw8FBwfrjz/+0AsvvGAv//DDD+Xj46P3339f/v7+GjRokDp06KAuXbroySeflHR10fScBAcH680339Rtt92mQYMGqU2bNnryySfl6uqqBQsWqEyZMuZeLIB8+eijj1S3bl01adJEffv21cCBA9W8eXONHj1aHh4eWR7vla7eC7Rv316RkZEKDw9XvXr1FBgY6KDoARRXixYt0h133KEPPvhAtWrVUseOHTV48GB1795dfn5+at68eZak1IgRI+Tr66tVq1apUaNGGjBggO6//341b95c7du3z3UjFcDRSEoBFrLZbPrqq680atQo/frrr1q9erW8vLw0btw4rVmzhqn7QDH0559/6sknn5Sbm5sWL14sT0/PLMdnzpypBg0aaN68efZp9g0aNNCOHTv08MMP6/z58/r6668VFxenzz77TAMHDpSkHLdelq4+/hceHi5fX1+tXr1akZGRuvfeexUWFqYuXbqYe7EA8m3SpEl69NFHZbPZtHHjRq1atUqJiYl6/PHHFRERobvuuivbOdfOjGKWFICcVKlSRdu2bdOcOXPUuHFj/fLLL/rqq6+0f/9+1a1bV9OnT9dLL71kr1+5cmXt2rVLgwYNUkpKir799lv9+eefmjRpkpYuXerAKwGuz2bwcClgCZvNplq1amV5dA/AzefNN9/Uq6++qjfffJPp9AAAALipMVMKAIAilpSUpAMHDmQr37x5s6ZOnSoXFxf7jCkAAADgZsWzQgAAFLGLFy+qSZMmatSokRo0aCAPDw8dOnRI+/btkyS98847rO0AAACAmx5JKQAAipiXl5deeuklrV+/Xtu2bVNcXJwqVKigBx54QM8995weeOABR4cIAAAAOBxrSgEAAAAAAMByrCkFAAAAAAAAy5GUAgAAAAAAgOVISgEAAAAAAMByJKUAAAAAAABgOZJSAAAAAAAAsBxJKQAAgCISHR0tm82W5cvZ2VllypTRrbfeqtatW+uxxx7T6tWrxQbIAADgZmczuCMCAAAoEtHR0apTp06e6gYEBGjZsmW6/fbbi6Tv4cOH67PPPrN/zy0eAAAo7pgpBQAAYBJvb2/16dNH3bt3V9OmTeXk9L9br8jISAUGBmrbtm0OjBAAAMBxSEoBAACYpEmTJvrqq6+0atUq7du3T3/88Yc6d+5sP56QkKC+ffvq8uXLDowSAADAMUhKAQAAWKROnTpavXq12rRpYy87efKkPvjgA/v3n3zyiYYOHapmzZqpWrVqcnd3V5kyZVSvXj0NGjRIW7ZsydJmaGiobDZblkf3JGVZ16pDhw4Fbh8AAMAsrCkFAABQRP6+ptQ999yjsLCwbPXWrl2rbt262b9v1aqVdu3aJUmqUaOG/vzzz1z7sNlsevfddzV69GhJV5NSEydOvG5c18aR3/YBAADM4uLoAAAAAG42HTt2lIuLi9LS0iRJv/zyi9LT0+Xs7CxJ8vLyUoMGDVSpUiV5eHjo7Nmz2rt3r5KTk2UYhl5++WX17dtXNWvWVOPGjdWnTx/t3r1bx44ds/fRp08f+/83adIkS//5aR8AAMAszJQCAAAoInmdKSVJvr6+On36tP3706dPq0qVKvrtt98UEBBgT1BlOnjwoAICAuzf/+tf/9Izzzxj/z6vu+8VtH0AAICixppSAAAADvD3pJHNZpMkVatWTZMnT1a7du1UuXJlubq6ymazZUkYSdJ///vfAvVrdvsAAAB5xeN7AAAAFktISNC5c+fs3zs7O6tSpUo6ceKE2rVrpxMnTtywjbi4uHz3a3b7AAAA+UFSCgAAwGKbN29Wenq6/fsWLVrI2dlZkydPzpIwqlixotq0aaOyZctKkpYvX24/VpAVGMxuHwAAID9ISgEAAFgoNTVVoaGhWcoyFyXftm2bvezWW29VZGSkypcvL0k6depUlqTR32U+/nc9hWkfAACgqLGmFAAAgEWOHDmibt26affu3fayW2+9VU8//bSkqwmrTC4uLnJzc5MkpaWl6dVXX71u256enlm+/+uvv7LVKUz7AAAARY2ZUgAAACb5/fff1bdvX6WkpCgmJka//vqrMjIy7MdvueUWLV++XOXKlZMktW7dWgcPHpQkxcTEqFGjRrr99tv122+/KSYm5rp91a9fP8v3d911l5o3by5nZ2eNGDFC3bp1K1T7AAAARY2ZUgAAACaJjY3V8uXLtWrVKu3bty9LQqpx48bavn27AgMD7WXjxo2zP04nXU0crVmzRseOHdOkSZOu21ffvn2zzJaKjo7W119/reXLl+vQoUOFbh8AAKCokZQCAAAwkZOTkzw8POTr66sWLVpo2LBh+uabb/Trr7/qtttuy1K3UaNG2rp1q7p3765y5cqpTJkyatmypRYtWqRx48Zdtx8/Pz+tW7dO//jHP7IknoqqfQAAgKJmM9haBQAAAAAAABZjphQAAAAAAAAsR1IKAAAAAAAAliMpBQAAAAAAAMuRlAIAAAAAAIDlSEoBAAAAAADAciSlAAAAAAAAYDmSUgAAAAAAALAcSSkAAAAAAABYjqQUAAAAAAAALEdSCgAAAAAAAJYjKQUAAAAAAADLkZQCAAAAAACA5UhKAQAAAAAAwHL/B9NeM0UO8vjoAAAAAElFTkSuQmCC", + "text/plain": [ + "" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# should look like this:\n", + "from IPython.display import Image\n", + "Image(filename='../../images/results_dev.png') " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.14" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/meta/images/results_dev.png b/meta/images/results_dev.png new file mode 100644 index 00000000..002a8109 Binary files /dev/null and b/meta/images/results_dev.png differ diff --git a/nkululeko/constants.py b/nkululeko/constants.py index 94325f65..40c84b1e 100644 --- a/nkululeko/constants.py +++ b/nkululeko/constants.py @@ -1,2 +1,2 @@ -VERSION="0.82.0" +VERSION="0.84.0" SAMPLING_RATE = 16000 diff --git a/nkululeko/data/dataset_csv.py b/nkululeko/data/dataset_csv.py index 71b73b91..60c434eb 100644 --- a/nkululeko/data/dataset_csv.py +++ b/nkululeko/data/dataset_csv.py @@ -22,7 +22,18 @@ def load(self): # data_file = os.path.join(exp_root, data_file) root = os.path.dirname(data_file) audio_path = self.util.config_val_data(self.name, "audio_path", "") - df = audformat.utils.read_csv(data_file) + df = pd.read_csv(data_file) + # special treatment for segmented dataframes with only one column: + if "start" in df.columns and len(df.columns) == 4: + index = audformat.segmented_index( + df.file.values, df.start.values, df.end.values + ) + df = df.set_index(index) + df = df.drop(columns=["file", "start", "end"]) + else: + df = audformat.utils.read_csv(data_file) + if isinstance(df, pd.Series): + df = df.to_frame() rename_cols = self.util.config_val_data(self.name, "colnames", False) if rename_cols: col_dict = ast.literal_eval(rename_cols) diff --git a/nkululeko/demo.py b/nkululeko/demo.py index eda595e3..d59c73f9 100644 --- a/nkululeko/demo.py +++ b/nkululeko/demo.py @@ -2,8 +2,9 @@ # Demonstration code to use the ML-experiment framework # Test the loading of a previously trained model and demo mode # needs the project config file to run before -""" -This script is used to test the loading of a previously trained model and run it in demo mode. +"""This script is used to test the loading of a previously trained model. + +And run it in demo mode. It requires the project config file to be run before. Usage: @@ -20,17 +21,15 @@ import configparser import os -import nkululeko.glob_conf as glob_conf from nkululeko.constants import VERSION from nkululeko.experiment import Experiment +import nkululeko.glob_conf as glob_conf from nkululeko.utils.util import Util def main(src_dir): - parser = argparse.ArgumentParser( - description="Call the nkululeko DEMO framework.") - parser.add_argument("--config", default="exp.ini", - help="The base configuration") + parser = argparse.ArgumentParser(description="Call the nkululeko DEMO framework.") + parser.add_argument("--config", default="exp.ini", help="The base configuration") parser.add_argument( "--file", help="A file that should be processed (16kHz mono wav)" ) diff --git a/nkululeko/demo_predictor.py b/nkululeko/demo_predictor.py index a2239ea3..d6e38cfe 100644 --- a/nkululeko/demo_predictor.py +++ b/nkululeko/demo_predictor.py @@ -1,18 +1,19 @@ # demo_predictor.py import os -import audformat -import audiofile import numpy as np import pandas as pd +import audformat +import audiofile + import nkululeko.glob_conf as glob_conf from nkululeko.utils.util import Util class Demo_predictor: def __init__(self, model, file, is_list, feature_extractor, label_encoder, outfile): - """Constructor setting up name and configuration""" + """Constructor setting up name and configuration.""" self.model = model self.feature_extractor = feature_extractor self.label_encoder = label_encoder diff --git a/nkululeko/experiment.py b/nkululeko/experiment.py index 5ecf041d..f8d51dd2 100644 --- a/nkululeko/experiment.py +++ b/nkululeko/experiment.py @@ -5,25 +5,27 @@ import random import time -import audeer -import audformat import numpy as np import pandas as pd from sklearn.preprocessing import LabelEncoder -import nkululeko.glob_conf as glob_conf +import audeer +import audformat + from nkululeko.data.dataset import Dataset from nkululeko.data.dataset_csv import Dataset_CSV from nkululeko.demo_predictor import Demo_predictor from nkululeko.feat_extract.feats_analyser import FeatureAnalyser from nkululeko.feature_extractor import FeatureExtractor from nkululeko.file_checker import FileChecker -from nkululeko.filter_data import DataFilter, filter_min_dur +from nkululeko.filter_data import DataFilter +from nkululeko.filter_data import filter_min_dur +import nkululeko.glob_conf as glob_conf from nkululeko.plots import Plots from nkululeko.reporting.report import Report from nkululeko.runmanager import Runmanager from nkululeko.scaler import Scaler -from nkululeko.test_predictor import Test_predictor +from nkululeko.test_predictor import TestPredictor from nkululeko.utils.util import Util @@ -101,6 +103,7 @@ def load_datasets(self): self.got_speaker = True self.datasets.update({d: data}) self.target = self.util.config_val("DATA", "target", "emotion") + glob_conf.set_target(self.target) # print target via debug self.util.debug(f"target: {self.target}") # print keys/column @@ -487,11 +490,7 @@ def random_splice(self): return df_ret def analyse_features(self, needs_feats): - """ - Do a feature exploration - - """ - + """Do a feature exploration.""" plot_feats = eval( self.util.config_val("EXPL", "feature_distributions", "False") ) @@ -511,7 +510,7 @@ def analyse_features(self, needs_feats): f"unknown sample selection specifier {sample_selection}, should" " be [all | train | test]" ) - + self.util.debug(f"sampling selection: {sample_selection}") if self.util.config_val("EXPL", "value_counts", False): self.plot_distribution(df_labels) @@ -537,9 +536,13 @@ def analyse_features(self, needs_feats): f"unknown sample selection specifier {sample_selection}, should" " be [all | train | test]" ) + feat_analyser = FeatureAnalyser(sample_selection, df_labels, df_feats) + # check if SHAP features should be analysed + shap = eval(self.util.config_val("EXPL", "shap", "False")) + if shap: + feat_analyser.analyse_shap(self.runmgr.get_best_model()) if plot_feats: - feat_analyser = FeatureAnalyser(sample_selection, df_labels, df_feats) feat_analyser.analyse() # check if a scatterplot should be done @@ -672,15 +675,19 @@ def demo(self, file, is_list, outfile): def predict_test_and_save(self, result_name): model = self.runmgr.get_best_model() model.set_testdata(self.df_test, self.feats_test) - test_predictor = Test_predictor( + test_predictor = TestPredictor( model, self.df_test, self.label_encoder, result_name ) - test_predictor.predict_and_store() + result = test_predictor.predict_and_store() + return result def load(self, filename): - f = open(filename, "rb") - tmp_dict = pickle.load(f) - f.close() + try: + f = open(filename, "rb") + tmp_dict = pickle.load(f) + f.close() + except EOFError as eof: + self.util.error(f"can't open file {filename}: {eof}") self.__dict__.update(tmp_dict) glob_conf.set_labels(self.labels) @@ -688,22 +695,26 @@ def save(self, filename): if self.runmgr.modelrunner.model.is_ann(): self.runmgr.modelrunner.model = None self.util.warn( - f"Save experiment: Can't pickle the learning model so saving without it." + "Save experiment: Can't pickle the trained model so saving without it. (it should be stored anyway)" ) try: f = open(filename, "wb") pickle.dump(self.__dict__, f) f.close() - except TypeError: + except (TypeError, AttributeError) as error: self.feature_extractor.feat_extractor.model = None f = open(filename, "wb") pickle.dump(self.__dict__, f) f.close() self.util.warn( - f"Save experiment: Can't pickle the feature extraction model so saving without it." + "Save experiment: Can't pickle the feature extraction model so saving without it." + + f"{type(error).__name__} {error}" + ) + except RuntimeError as error: + self.util.warn( + "Save experiment: Can't pickle local object, NOT saving: " + + f"{type(error).__name__} {error}" ) - except (AttributeError, RuntimeError) as error: - self.util.warn(f"Save experiment: Can't pickle local object: {error}") def save_onnx(self, filename): # export the model to onnx diff --git a/nkululeko/explore.py b/nkululeko/explore.py index e03ca9d6..42178efe 100644 --- a/nkululeko/explore.py +++ b/nkululeko/explore.py @@ -12,9 +12,9 @@ def main(src_dir): parser = argparse.ArgumentParser( - description="Call the nkululeko EXPLORE framework.") - parser.add_argument("--config", default="exp.ini", - help="The base configuration") + description="Call the nkululeko EXPLORE framework." + ) + parser.add_argument("--config", default="exp.ini", help="The base configuration") args = parser.parse_args() if args.config is not None: config_file = args.config @@ -43,28 +43,34 @@ def main(src_dir): import warnings warnings.filterwarnings("ignore") - - # load the data - expr.load_datasets() - - # split into train and test - expr.fill_train_and_tests() - util.debug( - f"train shape : {expr.df_train.shape}, test shape:{expr.df_test.shape}") - - plot_feats = eval(util.config_val( - "EXPL", "feature_distributions", "False")) - tsne = eval(util.config_val("EXPL", "tsne", "False")) - scatter = eval(util.config_val("EXPL", "scatter", "False")) - spotlight = eval(util.config_val("EXPL", "spotlight", "False")) - model_type = util.config_val("EXPL", "model", False) - plot_tree = eval(util.config_val("EXPL", "plot_tree", "False")) needs_feats = False - if plot_feats or tsne or scatter or model_type or plot_tree: - # these investigations need features to explore - expr.extract_feats() + try: + # load the experiment + expr.load(f"{util.get_save_name()}") needs_feats = True - # explore + except FileNotFoundError: + # first time: load the data + expr.load_datasets() + + # split into train and test + expr.fill_train_and_tests() + util.debug( + f"train shape : {expr.df_train.shape}, test shape:{expr.df_test.shape}" + ) + + plot_feats = eval(util.config_val("EXPL", "feature_distributions", "False")) + tsne = eval(util.config_val("EXPL", "tsne", "False")) + scatter = eval(util.config_val("EXPL", "scatter", "False")) + spotlight = eval(util.config_val("EXPL", "spotlight", "False")) + shap = eval(util.config_val("EXPL", "shap", "False")) + model_type = util.config_val("EXPL", "model", False) + plot_tree = eval(util.config_val("EXPL", "plot_tree", "False")) + needs_feats = False + if plot_feats or tsne or scatter or model_type or plot_tree or shap: + # these investigations need features to explore + expr.extract_feats() + needs_feats = True + # explore expr.analyse_features(needs_feats) expr.store_report() print("DONE") diff --git a/nkululeko/feat_extract/feats_agender_agender.py b/nkululeko/feat_extract/feats_agender_agender.py index 3ab2fa50..c070ad52 100644 --- a/nkululeko/feat_extract/feats_agender_agender.py +++ b/nkululeko/feat_extract/feats_agender_agender.py @@ -28,9 +28,11 @@ def _load_model(self): if not os.path.isdir(model_root): cache_root = audeer.mkdir("cache") model_root = audeer.mkdir(model_root) - archive_path = audeer.download_url(model_url, cache_root, verbose=True) + archive_path = audeer.download_url( + model_url, cache_root, verbose=True) audeer.extract_archive(archive_path, model_root) - device = self.util.config_val("MODEL", "device", "cpu") + cuda = "cuda" if torch.cuda.is_available() else "cpu" + device = self.util.config_val("MODEL", "device", cuda) self.model = audonnx.load(model_root, device=device) # pytorch_total_params = sum(p.numel() for p in self.model.parameters()) # self.util.debug( diff --git a/nkululeko/feat_extract/feats_analyser.py b/nkululeko/feat_extract/feats_analyser.py index 4b3a291d..e7d93459 100644 --- a/nkululeko/feat_extract/feats_analyser.py +++ b/nkululeko/feat_extract/feats_analyser.py @@ -40,6 +40,39 @@ def _get_importance(self, model, permutation): importance = model.feature_importances_ return importance + def analyse_shap(self, model): + """Shap analysis. + + Use the best model from a previous run and analyse feature importance with SHAP. + https://m.mage.ai/how-to-interpret-and-explain-your-machine-learning-models-using-shap-values-471c2635b78e. + """ + import shap + + name = "my_shap_values" + if not self.util.exist_pickle(name): + + explainer = shap.Explainer( + model.predict_shap, + self.features, + output_names=glob_conf.labels, + algorithm="permutation", + npermutations=5, + ) + self.util.debug("computing SHAP values...") + shap_values = explainer(self.features) + self.util.to_pickle(shap_values, name) + else: + shap_values = self.util.from_pickle(name) + plt.tight_layout() + shap.plots.bar(shap_values) + fig_dir = self.util.get_path("fig_dir") + "../" # one up because of the runs + exp_name = self.util.get_exp_name(only_data=True) + format = self.util.config_val("PLOT", "format", "png") + filename = f"_SHAP_{model.name}" + filename = f"{fig_dir}{exp_name}{filename}.{format}" + plt.savefig(filename) + self.util.debug(f"plotted SHAP feature importance tp {filename}") + def analyse(self): models = ast.literal_eval(self.util.config_val("EXPL", "model", "['log_reg']")) model_name = "_".join(models) diff --git a/nkululeko/feat_extract/feats_hubert.py b/nkululeko/feat_extract/feats_hubert.py index 4a63dbe9..99c07b94 100644 --- a/nkululeko/feat_extract/feats_hubert.py +++ b/nkululeko/feat_extract/feats_hubert.py @@ -6,23 +6,26 @@ import os -import audeer -import nkululeko.glob_conf as glob_conf import pandas as pd import torch import torchaudio -from audformat.utils import map_file_path -from nkululeko.feat_extract.featureset import Featureset from tqdm import tqdm -from transformers import HubertModel, Wav2Vec2FeatureExtractor +from transformers import HubertModel +from transformers import Wav2Vec2FeatureExtractor + +from nkululeko.feat_extract.featureset import Featureset +import nkululeko.glob_conf as glob_conf class Hubert(Featureset): - """Class to extract HuBERT embedding)""" + """Class to extract HuBERT embedding).""" def __init__(self, name, data_df, feat_type): - """Constructor. is_train is needed to distinguish from test/dev sets, - because they use the codebook from the training""" + """Constructor. + + Is_train is needed to distinguish from test/dev sets, + because they use the codebook from the training. + """ super().__init__(name, data_df, feat_type) # check if device is not set, use cuda if available cuda = "cuda" if torch.cuda.is_available() else "cpu" @@ -61,16 +64,12 @@ def extract(self): """Extract the features or load them from disk if present.""" store = self.util.get_path("store") storage = f"{store}{self.name}.pkl" - extract = self.util.config_val( - "FEATS", "needs_feature_extraction", False - ) + extract = self.util.config_val("FEATS", "needs_feature_extraction", False) no_reuse = eval(self.util.config_val("FEATS", "no_reuse", "False")) if extract or no_reuse or not os.path.isfile(storage): if not self.model_initialized: self.init_model() - self.util.debug( - "extracting Hubert embeddings, this might take a while..." - ) + self.util.debug("extracting Hubert embeddings, this might take a while...") emb_series = pd.Series(index=self.data_df.index, dtype=object) length = len(self.data_df.index) for idx, (file, start, end) in enumerate( @@ -84,9 +83,7 @@ def extract(self): assert sampling_rate == 16000 emb = self.get_embeddings(signal, sampling_rate, file) emb_series.iloc[idx] = emb - self.df = pd.DataFrame( - emb_series.values.tolist(), index=self.data_df.index - ) + self.df = pd.DataFrame(emb_series.values.tolist(), index=self.data_df.index) self.df.to_pickle(storage) try: glob_conf.config["DATA"]["needs_feature_extraction"] = "false" diff --git a/nkululeko/feat_extract/feats_squim.py b/nkululeko/feat_extract/feats_squim.py index 93baaf30..b10cb1e4 100644 --- a/nkululeko/feat_extract/feats_squim.py +++ b/nkululeko/feat_extract/feats_squim.py @@ -28,12 +28,17 @@ class SquimSet(Featureset): - """Class to predict SQUIM features""" + """Class to predict SQUIM features.""" def __init__(self, name, data_df, feats_type): - """Constructor. is_train is needed to distinguish from test/dev sets, because they use the codebook from the training""" + """Constructor. + + Is_train is needed to distinguish from test/dev sets, + because they use the codebook from the training. + """ super().__init__(name, data_df, feats_type) - self.device = self.util.config_val("MODEL", "device", "cpu") + cuda = "cuda" if torch.cuda.is_available() else "cpu" + self.device = self.util.config_val("MODEL", "device", cuda) self.model_initialized = False def init_model(self): diff --git a/nkululeko/feat_extract/feats_trill.py b/nkululeko/feat_extract/feats_trill.py index b3c42b4d..9f3cc1e4 100644 --- a/nkululeko/feat_extract/feats_trill.py +++ b/nkululeko/feat_extract/feats_trill.py @@ -1,35 +1,39 @@ # feats_trill.py -import tensorflow_hub as hub import os + +import pandas as pd import tensorflow as tf -from numpy.core.numeric import tensordot +import tensorflow_hub as hub from tqdm import tqdm -import pandas as pd + import audiofile as af -from nkululeko.utils.util import Util -import nkululeko.glob_conf as glob_conf + from nkululeko.feat_extract.featureset import Featureset +import nkululeko.glob_conf as glob_conf +from nkululeko.utils.util import Util + # Import TF 2.X and make sure we're running eager. assert tf.executing_eagerly() class TRILLset(Featureset): - """A feature extractor for the Google TRILL embeddings""" + """A feature extractor for the Google TRILL embeddings. - """https://ai.googleblog.com/2020/06/improving-speech-representations-and.html""" + See https://ai.googleblog.com/2020/06/improving-speech-representations-and.html. + """ # Initialization of the class def __init__(self, name, data_df, feats_type): - """ - Initialize the class with name, data and Util instance - Also loads the model from hub + """Initialize the class with name, data and Util instance. - :param name: Name of the class - :type name: str - :param data_df: Data of the class - :type data_df: DataFrame - :return: None + Also loads the model from hub + Args: + :param name: Name of the class + :type name: str + :param data_df: Data of the class + :type data_df: DataFrame + :return: None """ super().__init__(name, data_df, feats_type) # Load the model from the configured path @@ -38,25 +42,21 @@ def __init__(self, name, data_df, feats_type): "trill.model", "https://tfhub.dev/google/nonsemantic-speech-benchmark/trill/3", ) - self.module = hub.load(model_path) + self.model = hub.load(model_path) self.feats_type = feats_type def extract(self): store = self.util.get_path("store") storage = f"{store}{self.name}.pkl" - extract = self.util.config_val( - "FEATS", "needs_feature_extraction", False) + extract = self.util.config_val("FEATS", "needs_feature_extraction", False) no_reuse = eval(self.util.config_val("FEATS", "no_reuse", "False")) if extract or no_reuse or not os.path.isfile(storage): - self.util.debug( - "extracting TRILL embeddings, this might take a while...") + self.util.debug("extracting TRILL embeddings, this might take a while...") emb_series = pd.Series(index=self.data_df.index, dtype=object) - length = len(self.data_df.index) for idx, file in enumerate(tqdm(self.data_df.index.get_level_values(0))): - emb = self.getEmbeddings(file) - emb_series[idx] = emb - self.df = pd.DataFrame( - emb_series.values.tolist(), index=self.data_df.index) + emb = self.get_embeddings(file) + emb_series.iloc[idx] = emb + self.df = pd.DataFrame(emb_series.values.tolist(), index=self.data_df.index) self.df.to_pickle(storage) try: glob_conf.config["DATA"]["needs_feature_extraction"] = "false" @@ -70,15 +70,15 @@ def embed_wav(self, wav): if len(wav.shape) > 1: wav = tf.reduce_mean(wav, axis=0) - emb_dict = self.module(samples=wav, sample_rate=tf.constant(16000)) + emb_dict = self.model(samples=wav, sample_rate=tf.constant(16000)) return emb_dict["embedding"] - def getEmbeddings(self, file): + def get_embeddings(self, file): wav = af.read(file)[0] - emb_short = self.getEmbeddings_signal(wav, 16000) + emb_short = self.get_embeddings_signal(wav, 16000) return emb_short - def getEmbeddings_signal(self, signal, sr): + def get_embeddings_signal(self, signal, sr): wav = tf.convert_to_tensor(signal) emb_short = self.embed_wav(wav) # you get one embedding per frame, we use the mean for all the frames @@ -86,7 +86,7 @@ def getEmbeddings_signal(self, signal, sr): return emb_short def extract_sample(self, signal, sr): - if self.module == None: + if self.model == None: self.__init__("na", None) - feats = self.getEmbeddings_signal(signal, sr) + feats = self.get_embeddings_signal(signal, sr) return feats diff --git a/nkululeko/feat_extract/feats_wav2vec2.py b/nkululeko/feat_extract/feats_wav2vec2.py index 6ace2f73..46888e20 100644 --- a/nkululeko/feat_extract/feats_wav2vec2.py +++ b/nkululeko/feat_extract/feats_wav2vec2.py @@ -21,7 +21,11 @@ class Wav2vec2(Featureset): """Class to extract wav2vec2 embeddings""" def __init__(self, name, data_df, feat_type): - """Constructor. is_train is needed to distinguish from test/dev sets, because they use the codebook from the training""" + """Constructor. + + If_train is needed to distinguish from test/dev sets, + because they use the codebook from the training + """ super().__init__(name, data_df, feat_type) cuda = "cuda" if torch.cuda.is_available() else "cpu" self.device = self.util.config_val("MODEL", "device", cuda) @@ -39,8 +43,7 @@ def init_model(self): ) config = transformers.AutoConfig.from_pretrained(model_path) layer_num = config.num_hidden_layers - hidden_layer = int(self.util.config_val( - "FEATS", "wav2vec2.layer", "0")) + hidden_layer = int(self.util.config_val("FEATS", "wav2vec2.layer", "0")) config.num_hidden_layers = layer_num - hidden_layer self.util.debug(f"using hidden layer #{config.num_hidden_layers}") self.processor = Wav2Vec2FeatureExtractor.from_pretrained(model_path) @@ -55,8 +58,7 @@ def extract(self): """Extract the features or load them from disk if present.""" store = self.util.get_path("store") storage = f"{store}{self.name}.pkl" - extract = self.util.config_val( - "FEATS", "needs_feature_extraction", False) + extract = self.util.config_val("FEATS", "needs_feature_extraction", False) no_reuse = eval(self.util.config_val("FEATS", "no_reuse", "False")) if extract or no_reuse or not os.path.isfile(storage): if not self.model_initialized: @@ -77,8 +79,7 @@ def extract(self): emb = self.get_embeddings(signal, sampling_rate, file) emb_series[idx] = emb # print(f"emb_series shape: {emb_series.shape}") - self.df = pd.DataFrame( - emb_series.values.tolist(), index=self.data_df.index) + self.df = pd.DataFrame(emb_series.values.tolist(), index=self.data_df.index) # print(f"df shape: {self.df.shape}") self.df.to_pickle(storage) try: diff --git a/nkululeko/feat_extract/feats_wavlm.py b/nkululeko/feat_extract/feats_wavlm.py index 748791e1..a21b0860 100644 --- a/nkululeko/feat_extract/feats_wavlm.py +++ b/nkululeko/feat_extract/feats_wavlm.py @@ -4,27 +4,32 @@ import os -import nkululeko.glob_conf as glob_conf import pandas as pd import torch import torchaudio -from nkululeko.feat_extract.featureset import Featureset from tqdm import tqdm -from transformers import Wav2Vec2FeatureExtractor, WavLMModel +from transformers import Wav2Vec2FeatureExtractor +from transformers import WavLMModel + +from nkululeko.feat_extract.featureset import Featureset +import nkululeko.glob_conf as glob_conf class Wavlm(Featureset): - """Class to extract WavLM embedding)""" + """Class to extract WavLM embedding).""" + + def __init__(self, name, data_df, feats_type): + """Constructor. - def __init__(self, name, data_df, feat_type): - """Constructor. is_train is needed to distinguish from test/dev sets, - because they use the codebook from the training""" - super().__init__(name, data_df) + Is_train is needed to distinguish from test/dev sets, + because they use the codebook from the training. + """ + super().__init__(name, data_df, feats_type) # check if device is not set, use cuda if available cuda = "cuda" if torch.cuda.is_available() else "cpu" self.device = self.util.config_val("MODEL", "device", cuda) self.model_initialized = False - self.feat_type = feat_type + self.feat_type = feats_type def init_model(self): # load model @@ -59,7 +64,9 @@ def extract(self): frame_offset=int(start.total_seconds() * 16000), num_frames=int((end - start).total_seconds() * 16000), ) - assert sampling_rate == 16000, f"sampling rate should be 16000 but is {sampling_rate}" + assert ( + sampling_rate == 16000 + ), f"sampling rate should be 16000 but is {sampling_rate}" emb = self.get_embeddings(signal, sampling_rate, file) emb_series.iloc[idx] = emb self.df = pd.DataFrame(emb_series.values.tolist(), index=self.data_df.index) diff --git a/nkululeko/feat_extract/feats_whisper.py b/nkululeko/feat_extract/feats_whisper.py index b0333bec..f6b6e94b 100644 --- a/nkululeko/feat_extract/feats_whisper.py +++ b/nkululeko/feat_extract/feats_whisper.py @@ -32,19 +32,22 @@ def init_model(self): model_name = f"openai/{self.feat_type}" self.model = WhisperModel.from_pretrained(model_name).to(self.device) print(f"intialized Whisper model on {self.device}") - self.feature_extractor = AutoFeatureExtractor.from_pretrained(model_name) + self.feature_extractor = AutoFeatureExtractor.from_pretrained( + model_name) self.model_initialized = True def extract(self): """Extract the features or load them from disk if present.""" store = self.util.get_path("store") storage = f"{store}{self.name}.pkl" - extract = self.util.config_val("FEATS", "needs_feature_extraction", False) + extract = self.util.config_val( + "FEATS", "needs_feature_extraction", False) no_reuse = eval(self.util.config_val("FEATS", "no_reuse", "False")) if extract or no_reuse or not os.path.isfile(storage): if not self.model_initialized: self.init_model() - self.util.debug("extracting whisper embeddings, this might take a while...") + self.util.debug( + "extracting whisper embeddings, this might take a while...") emb_series = [] for (file, start, end), _ in audeer.progress_bar( self.data_df.iterrows(), diff --git a/nkululeko/glob_conf.py b/nkululeko/glob_conf.py index c74d4ceb..c630367d 100644 --- a/nkululeko/glob_conf.py +++ b/nkululeko/glob_conf.py @@ -29,3 +29,8 @@ def set_report(report_obj): def set_labels(labels_obj): global labels labels = labels_obj + + +def set_target(target_obj): + global target + target = target_obj diff --git a/nkululeko/modelrunner.py b/nkululeko/modelrunner.py index b36d990f..af63adc3 100644 --- a/nkululeko/modelrunner.py +++ b/nkululeko/modelrunner.py @@ -2,18 +2,16 @@ import pandas as pd -from nkululeko.utils.util import Util from nkululeko import glob_conf -import nkululeko.glob_conf as glob_conf +from nkululeko.utils.util import Util class Modelrunner: - """ - Class to model one run - """ + """Class to model one run.""" def __init__(self, df_train, df_test, feats_train, feats_test, run): - """Constructor setting up the dataframes + """Constructor setting up the dataframes. + Args: df_train: train dataframe df_test: test dataframe diff --git a/nkululeko/models/model.py b/nkululeko/models/model.py index fce9c0e6..903c663c 100644 --- a/nkululeko/models/model.py +++ b/nkululeko/models/model.py @@ -20,6 +20,7 @@ class Model: def __init__(self, df_train, df_test, feats_train, feats_test): """Constructor taking the configuration and all dataframes.""" + self.name = "undefined" self.df_train, self.df_test, self.feats_train, self.feats_test = ( df_train, df_test, diff --git a/nkululeko/models/model_bayes.py b/nkululeko/models/model_bayes.py index d54dd76f..dbddbb8b 100644 --- a/nkululeko/models/model_bayes.py +++ b/nkululeko/models/model_bayes.py @@ -12,3 +12,4 @@ class Bayes_model(Model): def __init__(self, df_train, df_test, feats_train, feats_test): super().__init__(df_train, df_test, feats_train, feats_test) self.clf = GaussianNB() # set up the classifier + self.name = "bayes" diff --git a/nkululeko/models/model_cnn.py b/nkululeko/models/model_cnn.py index d5f321a6..4bc983eb 100644 --- a/nkululeko/models/model_cnn.py +++ b/nkululeko/models/model_cnn.py @@ -16,6 +16,7 @@ from sklearn.metrics import recall_score from collections import OrderedDict from PIL import Image +from traitlets import default from nkululeko.utils.util import Util import nkululeko.glob_conf as glob_conf @@ -33,7 +34,8 @@ def __init__(self, df_train, df_test, feats_train, feats_test): """Constructor taking the configuration and all dataframes""" super().__init__(df_train, df_test, feats_train, feats_test) super().set_model_type("ann") - self.target = glob_conf.config["DATA"]["target"] + self.name = "cnn" + self.target = glob_conf.target labels = glob_conf.labels self.class_num = len(labels) # set up loss criterion @@ -48,6 +50,7 @@ def __init__(self, df_train, df_test, feats_train, feats_test): self.util.error(f"unknown loss function: {criterion}") self.util.debug(f"using model with cross entropy loss function") # set up the model + # cuda = "cuda" if torch.cuda.is_available() else "cpu" self.device = self.util.config_val("MODEL", "device", "cpu") try: layers_string = glob_conf.config["MODEL"]["layers"] @@ -209,7 +212,8 @@ def load(self, run, epoch): dir = self.util.get_path("model_dir") # name = f'{self.util.get_exp_name()}_{run}_{epoch:03d}.model' name = f"{self.util.get_exp_name(only_train=True)}_{self.run}_{self.epoch:03d}.model" - self.device = self.util.config_val("MODEL", "device", "cpu") + cuda = "cuda" if torch.cuda.is_available() else "cpu" + self.device = self.util.config_val("MODEL", "device", cuda) layers = ast.literal_eval(glob_conf.config["MODEL"]["layers"]) self.store_path = dir + name drop = self.util.config_val("MODEL", "drop", False) @@ -222,7 +226,8 @@ def load(self, run, epoch): def load_path(self, path, run, epoch): self.set_id(run, epoch) with open(path, "rb") as handle: - self.device = self.util.config_val("MODEL", "device", "cpu") + cuda = "cuda" if torch.cuda.is_available() else "cpu" + self.device = self.util.config_val("MODEL", "device", cuda) layers = ast.literal_eval(glob_conf.config["MODEL"]["layers"]) self.store_path = path drop = self.util.config_val("MODEL", "drop", False) diff --git a/nkululeko/models/model_gmm.py b/nkululeko/models/model_gmm.py index ed635e83..f8d2bb7c 100644 --- a/nkululeko/models/model_gmm.py +++ b/nkululeko/models/model_gmm.py @@ -11,10 +11,9 @@ class GMM_model(Model): def __init__(self, df_train, df_test, feats_train, feats_test): super().__init__(df_train, df_test, feats_train, feats_test) + self.name = "gmm" n_components = int(self.util.config_val("MODEL", "GMM_components", "4")) - covariance_type = self.util.config_val( - "MODEL", "GMM_covariance_type", "full" - ) + covariance_type = self.util.config_val("MODEL", "GMM_covariance_type", "full") self.clf = mixture.GaussianMixture( n_components=n_components, covariance_type=covariance_type ) diff --git a/nkululeko/models/model_knn.py b/nkululeko/models/model_knn.py index 05e170b0..4c77fbbe 100644 --- a/nkululeko/models/model_knn.py +++ b/nkululeko/models/model_knn.py @@ -11,6 +11,7 @@ class KNN_model(Model): def __init__(self, df_train, df_test, feats_train, feats_test): super().__init__(df_train, df_test, feats_train, feats_test) + self.name = "knn" method = self.util.config_val("MODEL", "KNN_weights", "uniform") k = int(self.util.config_val("MODEL", "K_val", "5")) self.clf = KNeighborsClassifier( diff --git a/nkululeko/models/model_knn_reg.py b/nkululeko/models/model_knn_reg.py index 875f981a..b728679f 100644 --- a/nkululeko/models/model_knn_reg.py +++ b/nkululeko/models/model_knn_reg.py @@ -11,6 +11,7 @@ class KNN_reg_model(Model): def __init__(self, df_train, df_test, feats_train, feats_test): super().__init__(df_train, df_test, feats_train, feats_test) + self.name = "knn_reg" method = self.util.config_val("MODEL", "KNN_weights", "uniform") k = int(self.util.config_val("MODEL", "K_val", "5")) self.clf = KNeighborsRegressor( diff --git a/nkululeko/models/model_lin_reg.py b/nkululeko/models/model_lin_reg.py index 5b4eb422..dc5b7491 100644 --- a/nkululeko/models/model_lin_reg.py +++ b/nkululeko/models/model_lin_reg.py @@ -11,4 +11,5 @@ class Lin_reg_model(Model): def __init__(self, df_train, df_test, feats_train, feats_test): super().__init__(df_train, df_test, feats_train, feats_test) + self.name = "lin_reg" self.clf = LinearRegression() # set up the classifier diff --git a/nkululeko/models/model_mlp.py b/nkululeko/models/model_mlp.py index 9af7c595..2ba71285 100644 --- a/nkululeko/models/model_mlp.py +++ b/nkululeko/models/model_mlp.py @@ -1,4 +1,6 @@ # model_mlp.py +import pandas as pd + from nkululeko.utils.util import Util import nkululeko.glob_conf as glob_conf from nkululeko.models.model import Model @@ -20,6 +22,7 @@ def __init__(self, df_train, df_test, feats_train, feats_test): """Constructor taking the configuration and all dataframes""" super().__init__(df_train, df_test, feats_train, feats_test) super().set_model_type("ann") + self.name = "mlp" self.target = glob_conf.config["DATA"]["target"] labels = glob_conf.labels self.class_num = len(labels) @@ -34,8 +37,9 @@ def __init__(self, df_train, df_test, feats_train, feats_test): else: self.util.error(f"unknown loss function: {criterion}") self.util.debug(f"using model with cross entropy loss function") - # set up the model - self.device = self.util.config_val("MODEL", "device", "cpu") + # set up the model, use GPU if availabe + cuda = "cuda" if torch.cuda.is_available() else "cpu" + self.device = self.util.config_val("MODEL", "device", cuda) try: layers_string = glob_conf.config["MODEL"]["layers"] except KeyError as ke: @@ -172,13 +176,26 @@ def forward(self, x): x = x.squeeze(dim=1).float() return self.linear(x) + def predict_shap(self, features): + # predict outputs for all samples in SHAP format (pd. dataframe) + results = [] + for index, row in features.iterrows(): + feats = row.values + res_dict = self.predict_sample(feats) + class_key = max(res_dict, key=res_dict.get) + results.append(class_key) + return results + def predict_sample(self, features): - """Predict one sample""" + """Predict one sample.""" with torch.no_grad(): features = torch.from_numpy(features) features = np.reshape(features, (-1, 1)).T logits = self.model(features.to(self.device)) # logits = self.model(features) + # if tensor conver to cpu + if isinstance(logits, torch.Tensor): + logits = logits.cpu() a = logits.numpy() res = {} for i in range(len(a[0])): @@ -196,7 +213,8 @@ def load(self, run, epoch): dir = self.util.get_path("model_dir") # name = f'{self.util.get_exp_name()}_{run}_{epoch:03d}.model' name = f"{self.util.get_exp_name(only_train=True)}_{self.run}_{self.epoch:03d}.model" - self.device = self.util.config_val("MODEL", "device", "cpu") + cuda = "cuda" if torch.cuda.is_available() else "cpu" + self.device = self.util.config_val("MODEL", "device", cuda) layers = ast.literal_eval(glob_conf.config["MODEL"]["layers"]) self.store_path = dir + name drop = self.util.config_val("MODEL", "drop", False) @@ -211,7 +229,8 @@ def load(self, run, epoch): def load_path(self, path, run, epoch): self.set_id(run, epoch) with open(path, "rb") as handle: - self.device = self.util.config_val("MODEL", "device", "cpu") + cuda = "cuda" if torch.cuda.is_available() else "cpu" + self.device = self.util.config_val("MODEL", "device", cuda) layers = ast.literal_eval(glob_conf.config["MODEL"]["layers"]) self.store_path = path drop = self.util.config_val("MODEL", "drop", False) diff --git a/nkululeko/models/model_mlp_regression.py b/nkululeko/models/model_mlp_regression.py index 42949670..d2f798b9 100644 --- a/nkululeko/models/model_mlp_regression.py +++ b/nkululeko/models/model_mlp_regression.py @@ -9,6 +9,7 @@ from audmetric import concordance_cc from audmetric import mean_absolute_error from audmetric import mean_squared_error +from traitlets import default import nkululeko.glob_conf as glob_conf from nkululeko.losses.loss_ccc import ConcordanceCorCoeff @@ -24,6 +25,7 @@ class MLP_Reg_model(Model): def __init__(self, df_train, df_test, feats_train, feats_test): """Constructor taking the configuration and all dataframes""" super().__init__(df_train, df_test, feats_train, feats_test) + self.name = "mlp_reg" super().set_model_type("ann") self.target = glob_conf.config["DATA"]["target"] labels = glob_conf.labels @@ -40,7 +42,8 @@ def __init__(self, df_train, df_test, feats_train, feats_test): self.util.error(f"unknown loss function: {criterion}") self.util.debug(f"training model with {criterion} loss function") # set up the model - self.device = self.util.config_val("MODEL", "device", "cpu") + cuda = "cuda" if torch.cuda.is_available() else "cpu" + self.device = self.util.config_val("MODEL", "device", cuda) layers_string = glob_conf.config["MODEL"]["layers"] self.util.debug(f"using layers {layers_string}") try: diff --git a/nkululeko/models/model_svm.py b/nkululeko/models/model_svm.py index 6ad4ac74..1d53a95b 100644 --- a/nkululeko/models/model_svm.py +++ b/nkululeko/models/model_svm.py @@ -11,6 +11,7 @@ class SVM_model(Model): def __init__(self, df_train, df_test, feats_train, feats_test): super().__init__(df_train, df_test, feats_train, feats_test) + self.name = "svm" c = float(self.util.config_val("MODEL", "C_val", "0.001")) if eval(self.util.config_val("MODEL", "class_weight", "False")): class_weight = "balanced" diff --git a/nkululeko/models/model_svr.py b/nkululeko/models/model_svr.py index 71dd950a..ee6d4240 100644 --- a/nkululeko/models/model_svr.py +++ b/nkululeko/models/model_svr.py @@ -11,6 +11,7 @@ class SVR_model(Model): def __init__(self, df_train, df_test, feats_train, feats_test): super().__init__(df_train, df_test, feats_train, feats_test) + self.name = "svr" c = float(self.util.config_val("MODEL", "C_val", "0.001")) # kernel{‘linear’, ‘poly’, ‘rbf’, ‘sigmoid’, ‘precomputed’} or callable, default=’rbf’ kernel = self.util.config_val("MODEL", "kernel", "rbf") diff --git a/nkululeko/models/model_tree.py b/nkululeko/models/model_tree.py index a536b1d9..afa30d46 100644 --- a/nkululeko/models/model_tree.py +++ b/nkululeko/models/model_tree.py @@ -11,4 +11,5 @@ class Tree_model(Model): def __init__(self, df_train, df_test, feats_train, feats_test): super().__init__(df_train, df_test, feats_train, feats_test) + self.name = "tree" self.clf = DecisionTreeClassifier() # set up the classifier diff --git a/nkululeko/models/model_tree_reg.py b/nkululeko/models/model_tree_reg.py index 0d5648c7..f5ad2309 100644 --- a/nkululeko/models/model_tree_reg.py +++ b/nkululeko/models/model_tree_reg.py @@ -11,4 +11,5 @@ class Tree_reg_model(Model): def __init__(self, df_train, df_test, feats_train, feats_test): super().__init__(df_train, df_test, feats_train, feats_test) + self.name = "tree_reg" self.clf = DecisionTreeRegressor() # set up the classifier diff --git a/nkululeko/models/model_xgb.py b/nkululeko/models/model_xgb.py index b5a78469..681ec37a 100644 --- a/nkululeko/models/model_xgb.py +++ b/nkululeko/models/model_xgb.py @@ -7,9 +7,11 @@ class XGB_model(Model): """An XGBoost model""" - is_classifier = True - - clf = XGBClassifier() # set up the classifier + def __init__(self, df_train, df_test, feats_train, feats_test): + super().__init__(df_train, df_test, feats_train, feats_test) + self.name = "xgb" + self.is_classifier = True + self.clf = XGBClassifier() # set up the classifier def get_type(self): return "xgb" diff --git a/nkululeko/models/model_xgr.py b/nkululeko/models/model_xgr.py index 3cdcae1b..f78bfebb 100644 --- a/nkululeko/models/model_xgr.py +++ b/nkululeko/models/model_xgr.py @@ -5,8 +5,10 @@ class XGR_model(Model): - """An XGBoost model""" + """An XGBoost regression model""" - is_classifier = False - - clf = XGBRegressor() # set up the regressor + def __init__(self, df_train, df_test, feats_train, feats_test): + super().__init__(df_train, df_test, feats_train, feats_test) + self.name = "xgr" + self.is_classifier = False + self.clf = XGBRegressor() # set up the regressor diff --git a/nkululeko/nkuluflag.py b/nkululeko/nkuluflag.py index 5603bcff..a827b3e6 100644 --- a/nkululeko/nkuluflag.py +++ b/nkululeko/nkuluflag.py @@ -2,13 +2,16 @@ import configparser import os import os.path +import sys from nkululeko.nkululeko import doit as nkulu +from nkululeko.test import do_it as test_mod -def do_it(src_dir): +def doit(cla): parser = argparse.ArgumentParser(description="Call the nkululeko framework.") parser.add_argument("--config", help="The base configuration") + parser.add_argument("--mod", default="nkulu", help="Which nkululeko module to call") parser.add_argument("--data", help="The databases", nargs="*", action="append") parser.add_argument( "--label", nargs="*", help="The labels for the target", action="append" @@ -25,20 +28,23 @@ def do_it(src_dir): parser.add_argument("--model", default="xgb", help="The model type") parser.add_argument("--feat", default="['os']", help="The feature type") parser.add_argument("--set", help="The opensmile set") - parser.add_argument("--with_os", help="To add os features") parser.add_argument("--target", help="The target designation") parser.add_argument("--epochs", help="The number of epochs") parser.add_argument("--runs", help="The number of runs") parser.add_argument("--learning_rate", help="The learning rate") parser.add_argument("--drop", help="The dropout rate [0:1]") - args = parser.parse_args() + args = parser.parse_args(cla) if args.config is not None: config_file = args.config else: print("ERROR: need config file") quit(-1) + + if args.mod is not None: + nkulu_mod = args.mod + # test if config is there if not os.path.isfile(config_file): print(f"ERROR: no such file {config_file}") @@ -86,10 +92,17 @@ def do_it(src_dir): with open(tmp_config, "w") as tmp_file: config.write(tmp_file) - result, last_epoch = nkulu(tmp_config) + result, last_epoch = 0, 0 + if nkulu_mod == "nkulu": + result, last_epoch = nkulu(tmp_config) + elif nkulu_mod == "test": + result, last_epoch = test_mod(tmp_config, "test_results.csv") + else: + print(f"ERROR: unknown module: {nkulu_mod}, should be [nkulu | test]") return result, last_epoch if __name__ == "__main__": - cwd = os.path.dirname(os.path.abspath(__file__)) - do_it(cwd) # sys.argv[1]) + cla = sys.argv + cla.pop(0) + doit(cla) # sys.argv[1]) diff --git a/nkululeko/plots.py b/nkululeko/plots.py index f04a494f..0f2baa98 100644 --- a/nkululeko/plots.py +++ b/nkululeko/plots.py @@ -28,7 +28,8 @@ def plot_distributions_speaker(self, df): df_speaker["samplenum"] = df_speaker.shape[0] df_speakers = pd.concat([df_speakers, df_speaker.head(1)]) # plot the distribution of samples per speaker - fig_dir = self.util.get_path("fig_dir") + "../" # one up because of the runs + # one up because of the runs + fig_dir = self.util.get_path("fig_dir") + "../" self.util.debug(f"plotting samples per speaker") if "gender" in df_speakers: filename = f"samples_value_counts" @@ -137,7 +138,8 @@ def plot_distributions(self, df, type_s="samples"): df, att1, class_label, att1, type_s ) else: - ax, caption = self._plot2cont(df, class_label, att1, type_s) + ax, caption = self._plot2cont( + df, class_label, att1, type_s) self._save_plot( ax, caption, @@ -150,7 +152,8 @@ def plot_distributions(self, df, type_s="samples"): att1 = att[0] att2 = att[1] if att1 == self.target or att2 == self.target: - self.util.debug(f"no need to correlate {self.target} with itself") + self.util.debug( + f"no need to correlate {self.target} with itself") return if att1 not in df: self.util.error(f"unknown feature: {att1}") @@ -165,7 +168,8 @@ def plot_distributions(self, df, type_s="samples"): if self.util.is_categorical(df[att1]): if self.util.is_categorical(df[att2]): # class_label = cat, att1 = cat, att2 = cat - ax, caption = self._plot2cat(df, att1, att2, att1, type_s) + ax, caption = self._plot2cat( + df, att1, att2, att1, type_s) else: # class_label = cat, att1 = cat, att2 = cont ax, caption = self._plotcatcont( @@ -186,7 +190,8 @@ def plot_distributions(self, df, type_s="samples"): if self.util.is_categorical(df[att1]): if self.util.is_categorical(df[att2]): # class_label = cont, att1 = cat, att2 = cat - ax, caption = self._plot2cat(df, att1, att2, att1, type_s) + ax, caption = self._plot2cat( + df, att1, att2, att1, type_s) else: # class_label = cont, att1 = cat, att2 = cont ax, caption = self._plot2cont_cat( @@ -200,7 +205,8 @@ def plot_distributions(self, df, type_s="samples"): ) else: # class_label = cont, att1 = cont, att2 = cont - ax, caption = self._plot2cont(df, att1, att2, type_s) + ax, caption = self._plot2cont( + df, att1, att2, type_s) self._save_plot( ax, caption, f"Correlation of {att1} and {att2}", filename, type_s @@ -213,7 +219,8 @@ def plot_distributions(self, df, type_s="samples"): ) def _save_plot(self, ax, caption, header, filename, type_s): - fig_dir = self.util.get_path("fig_dir") + "../" # one up because of the runs + # one up because of the runs + fig_dir = self.util.get_path("fig_dir") + "../" fig = ax.figure # avoid warning # plt.tight_layout() @@ -231,7 +238,8 @@ def _save_plot(self, ax, caption, header, filename, type_s): ) def _check_binning(self, att, df): - bin_reals_att = eval(self.util.config_val("EXPL", f"{att}.bin_reals", "False")) + bin_reals_att = eval(self.util.config_val( + "EXPL", f"{att}.bin_reals", "False")) if bin_reals_att: self.util.debug(f"binning continuous variable {att} to categories") att_new = f"{att}_binned" @@ -305,7 +313,8 @@ def _plot2cat(self, df, col1, col2, xlab, ylab): return ax, caption def plot_durations(self, df, filename, sample_selection, caption=""): - fig_dir = self.util.get_path("fig_dir") + "../" # one up because of the runs + # one up because of the runs + fig_dir = self.util.get_path("fig_dir") + "../" try: ax = sns.histplot(df, x="duration", hue="class_label", kde=True) except AttributeError as ae: @@ -333,7 +342,8 @@ def plot_durations(self, df, filename, sample_selection, caption=""): def describe_df(self, name, df, target, filename): """Make a stacked barplot of samples and speakers per sex and target values. speaker, gender and target columns must be present""" - fig_dir = self.util.get_path("fig_dir") + "../" # one up because of the runs + fig_dir = self.util.get_path( + "fig_dir") + "../" # one up because of the runs sampl_num = df.shape[0] sex_col = "gender" if target == "gender": @@ -380,8 +390,10 @@ def describe_df(self, name, df, target, filename): def scatter_plot(self, feats, label_df, label, dimred_type): dim_num = int(self.util.config_val("EXPL", "scatter.dim", 2)) - fig_dir = self.util.get_path("fig_dir") + "../" # one up because of the runs - sample_selection = self.util.config_val("EXPL", "sample_selection", "all") + # one up because of the runs + fig_dir = self.util.get_path("fig_dir") + "../" + sample_selection = self.util.config_val( + "EXPL", "sample_selection", "all") filename = f"{label}_{self.util.get_feattype_name()}_{sample_selection}_{dimred_type}_{str(dim_num)}d" filename = f"{fig_dir}{filename}.{self.format}" self.util.debug(f"computing {dimred_type}, this might take a while...") @@ -423,7 +435,8 @@ def scatter_plot(self, feats, label_df, label, dimred_type): if dim_num == 2: plot_data = np.vstack((data.T, labels)).T - plot_df = pd.DataFrame(data=plot_data, columns=("Dim_1", "Dim_2", "label")) + plot_df = pd.DataFrame( + data=plot_data, columns=("Dim_1", "Dim_2", "label")) # plt.tight_layout() ax = ( sns.FacetGrid(plot_df, hue="label", height=6) @@ -515,7 +528,8 @@ def getTsne(self, feats, dim_num, perplexity=30, learning_rate=200): def plot_feature(self, title, feature, label, df_labels, df_features): # remove fullstops in the name feature_name = feature.replace(".", "-") - fig_dir = self.util.get_path("fig_dir") + "../" # one up because of the runs + # one up because of the runs + fig_dir = self.util.get_path("fig_dir") + "../" filename = f"{fig_dir}feat_dist_{title}_{feature_name}.{self.format}" if self.util.is_categorical(df_labels[label]): df_plot = pd.DataFrame( @@ -554,7 +568,8 @@ def plot_tree(self, model, features): tree.plot_tree(model, feature_names=list(features.columns), ax=ax) # plt.tight_layout() # print(ax) - fig_dir = self.util.get_path("fig_dir") + "../" # one up because of the runs + # one up because of the runs + fig_dir = self.util.get_path("fig_dir") + "../" exp_name = self.util.get_exp_name(only_data=True) format = self.util.config_val("PLOT", "format", "png") filename = f"{fig_dir}{exp_name}EXPL_tree-plot.{format}" diff --git a/nkululeko/reporter.py b/nkululeko/reporter.py deleted file mode 100644 index 41228d98..00000000 --- a/nkululeko/reporter.py +++ /dev/null @@ -1,332 +0,0 @@ -"""Reporter module. - -This module contains the Reporter class which is responsible for generating reports. -""" - - -import ast -import glob -import json -import math - -import matplotlib.pyplot as plt -import numpy as np -from scipy.stats import pearsonr -from sklearn.metrics import ( - ConfusionMatrixDisplay, - accuracy_score, - classification_report, - confusion_matrix, - mean_absolute_error, - mean_squared_error, - r2_score, - recall_score, -) -from sklearn.utils import resample - -import nkululeko.glob_conf as glob_conf -from nkululeko.reporting.defines import Header -from nkululeko.reporting.report_item import ReportItem -from nkululeko.result import Result -from nkululeko.utils.util import Util - - -class Reporter: - def __set_measure(self): - if self.util.exp_is_classification(): - self.MEASURE = "UAR" - self.result.measure = self.MEASURE - self.is_classification = True - else: - self.is_classification = False - self.measure = self.util.config_val("MODEL", "measure", "mse") - if self.measure == "mse": - self.MEASURE = "MSE" - self.result.measure = self.MEASURE - elif self.measure == "mae": - self.MEASURE = "MAE" - self.result.measure = self.MEASURE - elif self.measure == "ccc": - self.MEASURE = "CCC" - self.result.measure = self.MEASURE - - def __init__(self, truths, preds, run, epoch): - """Initialization with ground truth und predictions vector""" - self.util = Util("reporter") - self.format = self.util.config_val("PLOT", "format", "png") - self.truths = truths - self.preds = preds - self.result = Result(0, 0, 0, 0, "unknown") - self.run = run - self.epoch = epoch - self.__set_measure() - self.cont_to_cat = False - if len(self.truths) > 0 and len(self.preds) > 0: - if self.util.exp_is_classification(): - self.result.test = recall_score( - self.truths, self.preds, average="macro" - ) - self.result.loss = 1 - accuracy_score(self.truths, self.preds) - else: - # regression experiment - if self.measure == "mse": - self.result.test = mean_squared_error( - self.truths, self.preds) - elif self.measure == "mae": - self.result.test = mean_absolute_error( - self.truths, self.preds) - elif self.measure == "ccc": - self.result.test = self.ccc(self.truths, self.preds) - if math.isnan(self.result.test): - self.util.debug(f"Truth: {self.truths}") - self.util.debug(f"Predict.: {self.preds}") - self.util.debug(f"Result is NAN: setting to -1") - self.result.test = -1 - else: - self.util.error(f"unknown measure: {self.measure}") - - # train and loss are being set by the model - - def set_id(self, run, epoch): - """Make the report identifiable with run and epoch index""" - self.run = run - self.epoch = epoch - - def continuous_to_categorical(self): - if self.cont_to_cat: - return - self.cont_to_cat = True - bins = ast.literal_eval(glob_conf.config["DATA"]["bins"]) - self.truths = np.digitize(self.truths, bins) - 1 - self.preds = np.digitize(self.preds, bins) - 1 - - def plot_confmatrix(self, plot_name, epoch): - if not self.util.exp_is_classification(): - self.continuous_to_categorical() - self._plot_confmat(self.truths, self.preds, plot_name, epoch) - - -def plot_per_speaker(self, result_df, plot_name, function): - """Plot a confusion matrix with the mode category per speakers. - - This function creates a confusion matrix for each speaker in the result_df. - The result_df should contain the columns: preds, truths and speaker. - - Args: - * result_df: a pandas dataframe with columns: preds, truths and speaker - * plot_name: a string with the name of the plot - * function: a string with the function to use for each speaker, - can be 'mode' or 'mean' - - Returns: - * None - """ - # Initialize empty arrays for predictions and truths - pred = np.zeros(0) - truth = np.zeros(0) - - # Iterate over each speaker - for s in result_df.speaker.unique(): - # Filter the dataframe for the current speaker - s_df = result_df[result_df.speaker == s] - - # Get the mode or mean prediction for the current speaker - mode = s_df.pred.mode().iloc[-1] - mean = s_df.pred.mean() - if function == "mode": - s_df.pred = mode - elif function == "mean": - s_df.pred = mean - else: - self.util.error(f"unknown function {function}") - - # Append the current speaker's predictions and truths to the arrays - pred = np.append(pred, s_df.pred.values) - truth = np.append(truth, s_df["truth"].values) - - # If the experiment is not a classification or continuous to categorical conversion was performed, - # convert the truths and predictions to categorical - if not (self.is_classification or self.cont_to_cat): - bins = ast.literal_eval(glob_conf.config["DATA"]["bins"]) - truth = np.digitize(truth, bins) - 1 - pred = np.digitize(pred, bins) - 1 - - # Plot the confusion matrix for the speakers - self._plot_confmat(truth, pred.astype("int"), plot_name, 0) - - def _plot_confmat(self, truths, preds, plot_name, epoch): - # print(truths) - # print(preds) - fig_dir = self.util.get_path("fig_dir") - labels = glob_conf.labels - fig = plt.figure() # figsize=[5, 5] - uar = recall_score(truths, preds, average="macro") - acc = accuracy_score(truths, preds) - cm = confusion_matrix( - truths, preds, normalize=None - ) # normalize must be one of {'true', 'pred', 'all', None} - if cm.shape[0] != len(labels): - self.util.error( - f"mismatch between confmatrix dim ({cm.shape[0]}) and labels" - f" length ({len(labels)}: {labels})" - ) - try: - disp = ConfusionMatrixDisplay( - confusion_matrix=cm, display_labels=labels - ).plot(cmap="Blues") - except ValueError: - disp = ConfusionMatrixDisplay( - confusion_matrix=cm, - display_labels=list(labels).remove("neutral"), - ).plot(cmap="Blues") - - reg_res = "" - if not self.is_classification: - reg_res = f", {self.MEASURE}: {self.result.test:.3f}" - - if epoch != 0: - plt.title( - f"Confusion Matrix, UAR: {uar:.3f}{reg_res}, Epoch: {epoch}") - else: - plt.title(f"Confusion Matrix, UAR: {uar:.3f}{reg_res}") - img_path = f"{fig_dir}{plot_name}.{self.format}" - plt.savefig(img_path) - fig.clear() - plt.close(fig) - plt.savefig(img_path) - plt.close(fig) - glob_conf.report.add_item( - ReportItem( - Header.HEADER_RESULTS, - self.util.get_model_description(), - "Confusion matrix", - img_path, - ) - ) - - res_dir = self.util.get_path("res_dir") - uar = int(uar * 1000) / 1000.0 - acc = int(acc * 1000) / 1000.0 - rpt = f"epoch: {epoch}, UAR: {uar}, ACC: {acc}" - # print(rpt) - self.util.debug(rpt) - file_name = f"{res_dir}{self.util.get_exp_name()}_conf.txt" - with open(file_name, "w") as text_file: - text_file.write(rpt) - - def print_results(self, epoch): - """Print all evaluation values to text file""" - res_dir = self.util.get_path("res_dir") - file_name = f"{res_dir}{self.util.get_exp_name()}_{epoch}.txt" - if self.util.exp_is_classification(): - labels = glob_conf.labels - try: - rpt = classification_report( - self.truths, - self.preds, - target_names=labels, - output_dict=True, - ) - except ValueError as e: - self.util.debug( - "Reporter: caught a ValueError when trying to get" - " classification_report: " + e - ) - rpt = self.result.to_string() - with open(file_name, "w") as text_file: - c_ress = list(range(len(labels))) - for i, l in enumerate(labels): - c_res = rpt[l]["f1-score"] - c_ress[i] = float(f"{c_res:.3f}") - self.util.debug(f"labels: {labels}") - f1_per_class = f"result per class (F1 score): {c_ress}" - self.util.debug(f1_per_class) - rpt_str = f"{json.dumps(rpt)}\n{f1_per_class}" - text_file.write(rpt_str) - glob_conf.report.add_item( - ReportItem( - Header.HEADER_RESULTS, - f"Classification result {self.util.get_model_description()}", - rpt_str, - ) - ) - - else: # regression - result = self.result.test - r2 = r2_score(self.truths, self.preds) - pcc = pearsonr(self.truths, self.preds)[0] - measure = self.util.config_val("MODEL", "measure", "mse") - with open(file_name, "w") as text_file: - text_file.write( - f"{measure}: {result:.3f}, r_2: {r2:.3f}, pcc {pcc:.3f}" - ) - - def make_conf_animation(self, out_name): - import imageio - - fig_dir = self.util.get_path("fig_dir") - filenames = glob.glob( - fig_dir + f"{self.util.get_plot_name()}*_?_???_cnf.png") - images = [] - for filename in filenames: - images.append(imageio.imread(filename)) - fps = self.util.config_val("PLOT", "fps", "1") - try: - imageio.mimsave(fig_dir + out_name, images, fps=int(fps)) - except RuntimeError as e: - self.util.error("error writing anim gif: " + e) - - def get_result(self): - return self.result - - def plot_epoch_progression(self, reports, out_name): - fig_dir = self.util.get_path("fig_dir") - results, losses, train_results, losses_eval = [], [], [], [] - for r in reports: - results.append(r.get_result().test) - losses.append(r.get_result().loss) - train_results.append(r.get_result().train) - losses_eval.append(r.get_result().loss_eval) - - # do a plot per run - # scale the losses so they fit on the picture - losses, results, train_results, losses_eval = ( - np.asarray(losses), - np.asarray(results), - np.asarray(train_results), - np.asarray(losses_eval), - ) - - if np.all((results > 1)): - # scale down values - results = results / 100.0 - train_results = train_results / 100.0 - # if np.all((losses < 1)): - # scale up values - plt.figure(dpi=200) - plt.plot(train_results, "green", label="train set") - plt.plot(results, "red", label="dev set") - plt.plot(losses, "black", label="losses") - plt.plot(losses_eval, "grey", label="losses_eval") - plt.xlabel("epochs") - plt.ylabel(f"{self.MEASURE}") - plt.legend() - plt.savefig(f"{fig_dir}{out_name}.{self.format}") - plt.close() - - @staticmethod - def ccc(ground_truth, prediction): - mean_gt = np.mean(ground_truth, 0) - mean_pred = np.mean(prediction, 0) - var_gt = np.var(ground_truth, 0) - var_pred = np.var(prediction, 0) - v_pred = prediction - mean_pred - v_gt = ground_truth - mean_gt - cor = sum(v_pred * v_gt) / \ - (np.sqrt(sum(v_pred**2)) * np.sqrt(sum(v_gt**2))) - sd_gt = np.std(ground_truth) - sd_pred = np.std(prediction) - numerator = 2 * cor * sd_gt * sd_pred - denominator = var_gt + var_pred + (mean_gt - mean_pred) ** 2 - ccc = numerator / denominator - return ccc diff --git a/nkululeko/reporting/reporter.py b/nkululeko/reporting/reporter.py index f279fb3b..411b44b4 100644 --- a/nkululeko/reporting/reporter.py +++ b/nkululeko/reporting/reporter.py @@ -55,6 +55,7 @@ def __init__(self, truths, preds, run, epoch): self.run = run self.epoch = epoch self.__set_measure() + self.filenameadd = "" self.cont_to_cat = False if len(self.truths) > 0 and len(self.preds) > 0: if self.util.exp_is_classification(): @@ -206,7 +207,7 @@ def _plot_confmat(self, truths, preds, plot_name, epoch): f"Confusion Matrix, UAR: {uar_str} " + f"(+-{up_str}/{low_str}) {reg_res}" ) - img_path = f"{fig_dir}{plot_name}.{self.format}" + img_path = f"{fig_dir}{plot_name}{self.filenameadd}.{self.format}" plt.savefig(img_path) fig.clear() plt.close(fig) @@ -228,14 +229,17 @@ def _plot_confmat(self, truths, preds, plot_name, epoch): ) # print(rpt) self.util.debug(rpt) - file_name = f"{res_dir}{self.util.get_exp_name()}_conf.txt" + file_name = f"{res_dir}{self.util.get_exp_name()}{self.filenameadd}_conf.txt" with open(file_name, "w") as text_file: text_file.write(rpt) + def set_filename_add(self, my_string): + self.filenameadd = f"_{my_string}" + def print_results(self, epoch): """Print all evaluation values to text file.""" res_dir = self.util.get_path("res_dir") - file_name = f"{res_dir}{self.util.get_exp_name()}_{epoch}.txt" + file_name = f"{res_dir}{self.util.get_exp_name()}_{epoch}{self.filenameadd}.txt" if self.util.exp_is_classification(): labels = glob_conf.labels try: diff --git a/nkululeko/test.py b/nkululeko/test.py index ac1a781c..06462d77 100644 --- a/nkululeko/test.py +++ b/nkululeko/test.py @@ -10,20 +10,7 @@ from nkululeko.utils.util import Util -def main(src_dir): - parser = argparse.ArgumentParser( - description="Call the nkululeko TEST framework.") - parser.add_argument("--config", default="exp.ini", - help="The base configuration") - parser.add_argument( - "--outfile", - default="my_results.csv", - help="File name to store the predictions", - ) - - args = parser.parse_args() - - config_file = args.config +def do_it(config_file, outfile): # test if the configuration file exists if not os.path.isfile(config_file): @@ -48,10 +35,28 @@ def main(src_dir): expr.load(f"{util.get_save_name()}") expr.fill_tests() expr.extract_test_feats() - expr.predict_test_and_save(args.outfile) + result = expr.predict_test_and_save(outfile) print("DONE") + return result, 0 + + +def main(src_dir): + parser = argparse.ArgumentParser(description="Call the nkululeko TEST framework.") + parser.add_argument("--config", default="exp.ini", help="The base configuration") + parser.add_argument( + "--outfile", + default="my_results.csv", + help="File name to store the predictions", + ) + args = parser.parse_args() + if args.config is not None: + config_file = args.config + else: + config_file = f"{src_dir}/exp.ini" + do_it(config_file, args.outfile) + if __name__ == "__main__": cwd = os.path.dirname(os.path.abspath(__file__)) diff --git a/nkululeko/test_predictor.py b/nkululeko/test_predictor.py index dc9a88f2..0cfb68a5 100644 --- a/nkululeko/test_predictor.py +++ b/nkululeko/test_predictor.py @@ -1,21 +1,25 @@ -""" test_predictor.py +"""test_predictor.py. + Predict targets from a model and save as csv file. """ -import nkululeko.glob_conf as glob_conf -from nkululeko.utils.util import Util +import ast + +import numpy as np import pandas as pd +from sklearn.preprocessing import LabelEncoder + from nkululeko.data.dataset import Dataset from nkululeko.feature_extractor import FeatureExtractor +import nkululeko.glob_conf as glob_conf from nkululeko.scaler import Scaler -import numpy as np -from sklearn.preprocessing import LabelEncoder +from nkululeko.utils.util import Util -class Test_predictor: +class TestPredictor: def __init__(self, model, orig_df, labenc, name): - """Constructor setting up name and configuration""" + """Constructor setting up name and configuration.""" self.model = model self.orig_df = orig_df self.label_encoder = labenc @@ -25,6 +29,7 @@ def __init__(self, model, orig_df, labenc, name): def predict_and_store(self): label_data = self.util.config_val("DATA", "label_data", False) + result = 0 if label_data: data = Dataset(label_data) data.load() @@ -49,7 +54,15 @@ def predict_and_store(self): df[self.target] = labelenc.inverse_transform(predictions.tolist()) df.to_csv(self.name) else: + test_dbs = ast.literal_eval(glob_conf.config["DATA"]["tests"]) + test_dbs_string = "_".join(test_dbs) predictions = self.model.get_predictions() + report = self.model.predict() + result = report.result.get_result() + report.set_filename_add(f"test-{test_dbs_string}") + self.util.print_best_results([report]) + report.plot_confmatrix(self.util.get_plot_name(), 0) + report.print_results(0) # print(predictions) # df = pd.DataFrame(index=self.orig_df.index) # df["speaker"] = self.orig_df["speaker"] @@ -63,3 +76,4 @@ def predict_and_store(self): df = df.rename(columns={"class_label": target}) df.to_csv(self.name) self.util.debug(f"results stored in {self.name}") + return result diff --git a/nkululeko/test_pretrain.py b/nkululeko/test_pretrain.py new file mode 100644 index 00000000..0b94840e --- /dev/null +++ b/nkululeko/test_pretrain.py @@ -0,0 +1,117 @@ +# test_pretrain.py +import argparse +import configparser +import os.path + +import datasets +import numpy as np +import pandas as pd +import torch +import transformers + +import audeer +import audiofile + +from nkululeko.constants import VERSION +import nkululeko.experiment as exp +import nkululeko.glob_conf as glob_conf +from nkululeko.utils.util import Util + + +def doit(config_file): + # test if the configuration file exists + if not os.path.isfile(config_file): + print(f"ERROR: no such file: {config_file}") + exit() + + # load one configuration per experiment + config = configparser.ConfigParser() + config.read(config_file) + + # create a new experiment + expr = exp.Experiment(config) + module = "test_pretrain" + expr.set_module(module) + util = Util(module) + util.debug( + f"running {expr.name} from config {config_file}, nkululeko version" + f" {VERSION}" + ) + + if util.config_val("EXP", "no_warnings", False): + import warnings + + warnings.filterwarnings("ignore") + + # load the data + expr.load_datasets() + + # split into train and test + expr.fill_train_and_tests() + util.debug(f"train shape : {expr.df_train.shape}, test shape:{expr.df_test.shape}") + + sampling_rate = 16000 + max_duration_sec = 8.0 + + model_path = "facebook/wav2vec2-large-robust-ft-swbd-300h" + num_layers = None + + batch_size = 16 + accumulation_steps = 4 + + # create dataset + + dataset = {} + data_sources = { + "train": pd.DataFrame(expr.df_train[glob_conf.target]), + "dev": pd.DataFrame(expr.df_test[glob_conf.target]), + } + + for split in ["train", "dev"]: + + y = pd.Series( + data=data_sources[split].itertuples(index=False, name=None), + index=data_sources[split].index, + dtype=object, + name="labels", + ) + + y.name = "targets" + df = y.reset_index() + df.start = df.start.dt.total_seconds() + df.end = df.end.dt.total_seconds() + print(f"{split}: {len(df)}") + ds = datasets.Dataset.from_pandas(df) + dataset[split] = ds + + dataset = datasets.DatasetDict(dataset) + + config = transformers.AutoConfig.from_pretrained( + model_path, + num_labels=len(util.la), + label2id=data.gender_mapping, + id2label=data.gender_mapping_reverse, + finetuning_task="age-gender", + ) + if num_layers is not None: + config.num_hidden_layers = num_layers + setattr(config, "sampling_rate", sampling_rate) + setattr(config, "data", ",".join(sources)) + + print("DONE") + + +def main(src_dir): + parser = argparse.ArgumentParser(description="Call the nkululeko framework.") + parser.add_argument("--config", default="exp.ini", help="The base configuration") + args = parser.parse_args() + if args.config is not None: + config_file = args.config + else: + config_file = f"{src_dir}/exp.ini" + doit(config_file) + + +if __name__ == "__main__": + cwd = os.path.dirname(os.path.abspath(__file__)) + main(cwd) # use this if you want to state the config file path on command line diff --git a/nkululeko/utils/util.py b/nkululeko/utils/util.py index 5400f594..8815f55d 100644 --- a/nkululeko/utils/util.py +++ b/nkululeko/utils/util.py @@ -1,10 +1,13 @@ # util.py -import pandas as pd import ast +import configparser +import os.path +import pickle import sys + import numpy as np -import os.path -import configparser +import pandas as pd + import audeer import audformat @@ -295,6 +298,28 @@ def print_best_results(self, best_reports): f" {vals.argmax()}" ) + def exist_pickle(self, name): + store = self.get_path("store") + name = "/".join([store, name]) + ".pkl" + if os.path.isfile(name): + return True + return False + + def to_pickle(self, anyobject, name): + store = self.get_path("store") + name = "/".join([store, name]) + ".pkl" + self.debug(f"saving {name}") + with open(name, "wb") as handle: + pickle.dump(anyobject, handle) + + def from_pickle(self, name): + store = self.get_path("store") + name = "/".join([store, name]) + ".pkl" + self.debug(f"loading {name}") + with open(name, "rb") as handle: + any_opject = pickle.load(handle) + return any_opject + def write_store(self, df, storage, format): if format == "pkl": df.to_pickle(storage) diff --git a/requirements.txt b/requirements.txt index 2f92cd3d..d5a52a66 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,9 +19,12 @@ scikit_learn scipy seaborn sounddevice +splitutils # tensorflow # tensorflow_hub torch +torchaudio +torchvision # transformers xgboost umap-learn diff --git a/test_runs.sh b/test_runs.sh index cf56051c..f5ac9b16 100755 --- a/test_runs.sh +++ b/test_runs.sh @@ -39,6 +39,8 @@ function Nkulu { # test features function Feat { python -m nkululeko.nkululeko --config tests/exp_emodb_hubert_xgb.ini + python -m nkululeko.nkululeko --config tests/exp_emodb_audmodel_xgb.ini + python -m nkululeko.nkululeko --config tests/exp_emodb_wavlm_xgb.ini python -m nkululeko.nkululeko --config tests/exp_emodb_whisper_xgb.ini } # test augmentation @@ -73,6 +75,10 @@ function Demo { function Test { python -m nkululeko.nkululeko --config tests/exp_emodb_os_xgb_test.ini python -m nkululeko.test --config tests/exp_emodb_os_xgb_test.ini + python -m nkululeko.nkululeko --config tests/exp_emodb_trill_test.ini + python -m nkululeko.test --config tests/exp_emodb_trill_test.ini + python -m nkululeko.nkululeko --config tests/exp_emodb_wav2vec2_test.ini + python -m nkululeko.test --config tests/exp_emodb_wav2vec2_test.ini } # test multidb function Multi { @@ -82,8 +88,9 @@ function Multi { function Spot { python -m nkululeko.explore --config tests/exp_explore.ini } - -Help +if [ $# -eq 0 ] || [ "$1" == "--help" ]; then + Help +fi for arg in "$@"; do if [[ "$arg" = --Explore ]] || [[ "$arg" = --all ]]; then Explore diff --git a/tests/data_roots.ini b/tests/data_roots.ini index b1cf39d5..c20341c0 100644 --- a/tests/data_roots.ini +++ b/tests/data_roots.ini @@ -4,7 +4,8 @@ emodb.split_strategy = specified emodb.test_tables = ['emotion.categories.test.gold_standard'] emodb.train_tables = ['emotion.categories.train.gold_standard'] emodb.mapping = {'anger':'angry', 'happiness':'happy', 'sadness':'sad', 'neutral':'neutral'} -crema-d = ./data/crema-d/crema-d/1.3.0/fe182b91/ +; crema-d = ./data/crema-d/crema-d/1.3.0/fe182b91/ +crema-d = ./data/crema-d/crema-d/1.3.0/d3b62a9b/ crema-d.split_strategy = specified crema-d.colnames = {'sex':'gender'} crema-d.files_table = ['files'] diff --git a/tests/emodb_demo.ini b/tests/emodb_demo.ini index 451b41c0..a60c2a37 100644 --- a/tests/emodb_demo.ini +++ b/tests/emodb_demo.ini @@ -6,7 +6,7 @@ epochs = 100 save = True [DATA] databases = ['emodb'] -root_folders = data_roots.ini +root_folders = ./tests/data_roots.ini tests = ['testdb'] testdb = data/test/samples.csv testdb.type = csv diff --git a/tests/exp_emodb_audmodel_xgb.ini b/tests/exp_emodb_audmodel_xgb.ini index a4cd5009..e4ae2a9c 100644 --- a/tests/exp_emodb_audmodel_xgb.ini +++ b/tests/exp_emodb_audmodel_xgb.ini @@ -1,6 +1,6 @@ [EXP] root = ./tests/results/ -name = exp_emodb_audmodel +name = exp_emodb_audmodel_xgb runs = 1 epochs = 1 save = True @@ -8,7 +8,7 @@ save = True databases = ['emodb'] emodb = ./data/emodb/emodb emodb.split_strategy = random -emodb.limit_samples = 50 +emodb.limit_samples = 200 emodb.mapping = {'anger':'angry', 'happiness':'happy', 'sadness':'sad', 'neutral':'neutral'} labels = ['angry', 'happy', 'neutral', 'sad'] target = emotion diff --git a/tests/exp_emodb_os_knn.ini b/tests/exp_emodb_os_knn.ini index b36a7c57..c57cd0f0 100644 --- a/tests/exp_emodb_os_knn.ini +++ b/tests/exp_emodb_os_knn.ini @@ -11,9 +11,11 @@ emodb.split_strategy = specified emodb.test_tables = ['emotion.categories.test.gold_standard'] emodb.train_tables = ['emotion.categories.train.gold_standard'] target = emotion +labels = ['anger', 'happiness'] [FEATS] type = ['os'] store_format = csv scale = standard [MODEL] type = knn +save = True diff --git a/tests/exp_emodb_os_mlp.ini b/tests/exp_emodb_os_mlp.ini index ffc349d5..8eede178 100644 --- a/tests/exp_emodb_os_mlp.ini +++ b/tests/exp_emodb_os_mlp.ini @@ -11,6 +11,7 @@ emodb.split_strategy = specified emodb.test_tables = ['emotion.categories.test.gold_standard'] emodb.train_tables = ['emotion.categories.train.gold_standard'] target = emotion +labels = ['anger', 'happiness'] [FEATS] type = ['os'] scale = standard @@ -19,6 +20,7 @@ type = mlp layers = {'l1':128, 'l2':16} drop = .4 patience = 5 +save = True [PLOT] best_model = True epoch_progression = True diff --git a/tests/exp_emodb_os_svm.ini b/tests/exp_emodb_os_svm.ini index c24a1720..d9877ab3 100644 --- a/tests/exp_emodb_os_svm.ini +++ b/tests/exp_emodb_os_svm.ini @@ -1,8 +1,6 @@ [EXP] root = ./tests/results/ name = exp_emodb_classifiers -runs = 1 -epochs = 10 save = True [DATA] databases = ['emodb'] @@ -11,9 +9,12 @@ emodb.split_strategy = specified emodb.test_tables = ['emotion.categories.test.gold_standard'] emodb.train_tables = ['emotion.categories.train.gold_standard'] target = emotion +labels = ['anger', 'happiness'] [FEATS] type = ['os'] -store_format = csv scale = standard [MODEL] type = svm +tuning_params = ['C'] +scoring = recall_macro +C = [10, 1, 0.1, 0.01, 0.001, 0.0001] diff --git a/tests/exp_emodb_os_xgb.ini b/tests/exp_emodb_os_xgb.ini index c2ac7053..7031d940 100644 --- a/tests/exp_emodb_os_xgb.ini +++ b/tests/exp_emodb_os_xgb.ini @@ -1,6 +1,6 @@ [EXP] root = ./tests/results/ -name = exp_emodb +name = exp_emodb_classifiers runs = 1 epochs = 10 save = True @@ -10,8 +10,7 @@ emodb = ./data/emodb/emodb emodb.split_strategy = specified emodb.test_tables = ['emotion.categories.test.gold_standard'] emodb.train_tables = ['emotion.categories.train.gold_standard'] -emodb.mapping = {'anger':'angry', 'happiness':'happy', 'sadness':'sad', 'neutral':'neutral'} -labels = ['angry', 'happy', 'neutral', 'sad'] +labels = ['anger', 'happiness'] target = emotion [FEATS] type = ['os'] diff --git a/tests/exp_emodb_os_xgb_logo.ini b/tests/exp_emodb_os_xgb_logo.ini index 464e4602..aa84fec0 100644 --- a/tests/exp_emodb_os_xgb_logo.ini +++ b/tests/exp_emodb_os_xgb_logo.ini @@ -1,6 +1,6 @@ [EXP] root = ./tests/results/ -name = exp_emodb +name = exp_emodb_logo runs = 1 epochs = 1 save = True @@ -10,9 +10,8 @@ emodb = ./data/emodb/emodb emodb.split_strategy = specified emodb.test_tables = ['emotion.categories.test.gold_standard'] emodb.train_tables = ['emotion.categories.train.gold_standard'] -emodb.mapping = {'anger':'angry', 'happiness':'happy', 'sadness':'sad', 'neutral':'neutral'} -labels = ['angry', 'happy', 'neutral', 'sad'] target = emotion +labels = ['anger', 'happiness'] [FEATS] type = ['os'] store_format = csv diff --git a/tests/exp_emodb_os_xgb_test.ini b/tests/exp_emodb_os_xgb_test.ini index e0c86f7e..b597d160 100644 --- a/tests/exp_emodb_os_xgb_test.ini +++ b/tests/exp_emodb_os_xgb_test.ini @@ -7,7 +7,7 @@ databases = ['emodb'] root_folders = tests/data_roots.ini target = emotion tests = ['crema-d'] -labels = ['angry', 'happy', 'neutral', 'sad'] +labels = ['angry', 'happy'] no_reuse = True [FEATS] type = ['os'] diff --git a/tests/exp_emodb_shap.ini b/tests/exp_emodb_shap.ini new file mode 100644 index 00000000..50f6bb12 --- /dev/null +++ b/tests/exp_emodb_shap.ini @@ -0,0 +1,28 @@ +[EXP] +root = ./tests/results/ +name = exp_emodb_shap +runs = 1 +epochs = 500 +save = True +[DATA] +databases = ['emodb'] +emodb = ./data/emodb/emodb +emodb.split_strategy = specified +emodb.test_tables = ['emotion.categories.test.gold_standard'] +emodb.train_tables = ['emotion.categories.train.gold_standard'] +target = emotion +labels = ['anger', 'happiness'] +[FEATS] +type = ['os'] +scale = standard +[MODEL] +type = mlp +layers = {'l1':128, 'l2':16} +drop = .4 +patience = 5 +[EXPL] +shap = True +sample_selection = test +[PLOT] +best_model = True +epoch_progression = True diff --git a/tests/exp_emodb_trill_test.ini b/tests/exp_emodb_trill_test.ini new file mode 100644 index 00000000..38a2ce51 --- /dev/null +++ b/tests/exp_emodb_trill_test.ini @@ -0,0 +1,16 @@ +[EXP] +root = ./tests/results/ +name = exp_testmodule +save = True +[DATA] +databases = ['emodb'] +root_folders = tests/data_roots.ini +target = emotion +tests = ['crema-d'] +labels = ['angry', 'happy'] +no_reuse = True +[FEATS] +type = ['trill'] +[MODEL] +type = xgb +save = True diff --git a/tests/exp_emodb_wav2vec2_test.ini b/tests/exp_emodb_wav2vec2_test.ini new file mode 100644 index 00000000..f2051b51 --- /dev/null +++ b/tests/exp_emodb_wav2vec2_test.ini @@ -0,0 +1,16 @@ +[EXP] +root = ./tests/results/ +name = exp_testmodule +save = True +[DATA] +databases = ['emodb'] +root_folders = tests/data_roots.ini +target = emotion +tests = ['crema-d'] +labels = ['angry', 'happy'] +no_reuse = True +[FEATS] +type = ['wav2vec2'] +[MODEL] +type = xgb +save = True diff --git a/tests/exp_emodb_wav2vec_xgb.ini b/tests/exp_emodb_wav2vec_xgb.ini new file mode 100644 index 00000000..82906e32 --- /dev/null +++ b/tests/exp_emodb_wav2vec_xgb.ini @@ -0,0 +1,18 @@ +[EXP] +root = ./tests/results/ +name = exp_emodb_feats +runs = 1 +epochs = 1 +save = True +[DATA] +databases = ['emodb'] +emodb = ./data/emodb/emodb +emodb.test_tables = ['emotion.categories.test.gold_standard'] +emodb.train_tables = ['emotion.categories.train.gold_standard'] +emodb.mapping = {'anger':'angry', 'happiness':'happy', 'sadness':'sad', 'neutral':'neutral'} +labels = ['angry', 'happy'] +target = emotion +[FEATS] +type = ['wav2vec2'] +[MODEL] +type = xgb diff --git a/tests/exp_emodb_wavlm_xgb.ini b/tests/exp_emodb_wavlm_xgb.ini new file mode 100644 index 00000000..4db1a5c3 --- /dev/null +++ b/tests/exp_emodb_wavlm_xgb.ini @@ -0,0 +1,18 @@ +[EXP] +root = ./tests/results/ +name = exp_emodb_feats +runs = 1 +epochs = 1 +save = True +[DATA] +databases = ['emodb'] +emodb = ./data/emodb/emodb +emodb.test_tables = ['emotion.categories.test.gold_standard'] +emodb.train_tables = ['emotion.categories.train.gold_standard'] +emodb.mapping = {'anger':'angry', 'happiness':'happy', 'sadness':'sad', 'neutral':'neutral'} +labels = ['angry', 'happy'] +target = emotion +[FEATS] +type = ['wavlm-base-plus'] +[MODEL] +type = xgb