Compare commits

..
48 Commits
Author SHA1 Message Date
weigmi87303 0702864bc3 add note to files on remote directory 2026-03-27 09:39:58 +01:00
weigmi87303 ac2c88c339 fixed grammar error in read me 2026-03-22 14:09:59 +00:00
korzerce84997 96b3e35248 changes to CNN report 2026-03-19 18:44:00 +01:00
korzerce84997 0483c3fea3 upload CNN report 2026-03-19 17:42:32 +01:00
TimoKurz 3701d11c77 - added accuracy results for xgboost models 2026-03-19 17:37:32 +01:00
TimoKurz 145a5ecf78 - added eyeFeature_kalibrierung
- added documentation for eyeTracking data
2026-03-19 16:20:42 +01:00
weigmi87303 eba9b07487 fixed typos and added clickable links to doc files 2026-03-18 12:29:44 +01:00
TimoKurz 4df1187f84 - added notes for XGBoost without accuracy details
- deleted unused files for data_creation and modified the project_report file overview
- translated the documentation for the pyfeat implementation
2026-03-14 14:33:35 +01:00
TimoKurz c439e35e39 -- added correct versioning for feature extraction 2026-03-14 13:41:58 +01:00
weigmi87303 9406be3c18 models vorlaeufig fertig 2026-03-10 21:29:04 +01:00
weigmi87303 f344808802 wrote chapter Isolation forest 2026-03-10 19:38:00 +01:00
weigmi87303 10fdafa244 added prediction env 2026-03-10 19:17:08 +01:00
weigmi87303 314c4433d3 general information in model training 2026-03-10 16:16:46 +01:00
weigmi87303 b252082991 chapter EDA written 2026-03-10 15:09:51 +01:00
weigmi87303 2ec0af5f62 2.1 and 2.2 written 2026-03-10 14:23:02 +01:00
weigmi87303 9c2619daa9 crash 2026-03-10 13:11:03 +01:00
weigmi87303 910e642398 mini readme change 2026-03-10 12:03:59 +01:00
weigmi87303 0e6f39556b changed chapter numbers 2026-03-10 11:48:50 +01:00
weigmi87303 fb0d39c668 cleaned readme, added project report 2026-03-09 20:45:24 +01:00
weigmi87303 0b2c629d16 changed owncloud download notebook 2026-03-09 20:25:54 +01:00
weigmi87303 ef785283f0 updatet subject performance notebook 2026-03-09 20:10:55 +01:00
weigmi87303 182fc102de clean up in eda distribution plots 2026-03-05 13:41:50 +01:00
weigmi87303 a064f6cc90 outsourcing of functions 2026-03-05 13:38:04 +01:00
weigmi87303 a4b7190756 first draft for read me + requirements.txt 2026-03-05 13:18:17 +01:00
weigmi87303 f95d59e44d removed dataset file from repo 2026-03-05 12:16:52 +01:00
weigmi87303 de12c1407c current files from jetson board 2026-03-05 12:13:27 +01:00
weigmi87303 537b452449 changed leaky relu syntax to remove keras bug 2026-03-04 17:59:05 +01:00
weigmi87303 3169c29319 mini changes in predict pipeline 2026-03-04 17:01:43 +01:00
weigmi87303 13bd76631f outsourcing of scaler in iforest and deep svdd, removal of paths 2026-03-04 16:51:22 +01:00
weigmi87303 6cc38291df clean up predict pipeline 1 2026-03-04 16:21:06 +01:00
weigmi87303 8b6c547387 deletion of vae files 2026-03-04 15:19:15 +01:00
weigmi87303 de0084dc09 getting rid of redundant files in dataset creation 2026-03-04 15:09:23 +01:00
weigmi87303 af3f9d16b2 minor fixes to new paths / dataset with all columns 2026-03-04 12:25:07 +01:00
weigmi87303 3d8c7c6639 added predict service, timer and config file from jetson board 2026-03-04 11:14:40 +01:00
weigmi87303 7b7f89cf9d changed gitignore to allow service, timer and yaml 2026-03-04 11:11:29 +01:00
weigmi87303 8f24adbdbd merge of deployment into main 2026-03-04 11:04:00 +01:00
weigmi87303 4eab3c9876 adjusted paths (this is the deployment setting) 2026-02-16 20:11:07 +00:00
weigmi87303 2b01085a9e scaler v2 2026-02-16 19:32:32 +01:00
weigmi87303 0088cef32a small changes and lazy import of tensorflow 2026-02-16 18:58:18 +01:00
weigmi87303 cf88f88814 uploaded config file 2026-02-16 17:07:49 +00:00
weigmi87303 2a014e1e4e first version 2026-02-16 18:06:07 +01:00
weigmi87303 3d86bfe6d0 empty feature creation 2026-01-31 17:51:27 +01:00
weigmi87303 9b7bb945bc new files for comissioning 2026-01-29 20:06:42 +00:00
weigmi87303 a9ff3880e2 rearrarngement of files 2026-01-29 18:12:13 +01:00
weigmi87303 5a216b22fd tool functions for sqlite database 2026-01-29 17:04:36 +00:00
weigmi87303 0294d4e584 files for testing 2026-01-29 13:48:47 +00:00
weigmi87303 4f6c3b7370 created low code script for model deployment 2026-01-27 19:28:12 +01:00
weigmi87303 5f2db4d0c9 init commit on deployment, removed EDA files 2026-01-27 18:42:40 +01:00
41 changed files with 2631 additions and 5322 deletions
+5
View File
@@ -3,4 +3,9 @@
!*.py !*.py
!*.ipynb !*.ipynb
!*.md !*.md
!*.parquet
!.gitignore !.gitignore
!*.service
!*.timer
!*.yaml
!*.txt
+3 -4
View File
@@ -23,7 +23,7 @@
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"file_path = \"adabase-public-0020-v_0_0_2.h5py\"" "file_path = \"YOUR_FILE_PATH.h5py\""
] ]
}, },
{ {
@@ -87,7 +87,7 @@
"id": "a4731c56", "id": "a4731c56",
"metadata": {}, "metadata": {},
"source": [ "source": [
"Actions units" "Insights on actions units"
] ]
}, },
{ {
@@ -167,7 +167,7 @@
"id": "332740a8", "id": "332740a8",
"metadata": {}, "metadata": {},
"source": [ "source": [
"Plots" "Example plot of ECG curve"
] ]
}, },
{ {
@@ -177,7 +177,6 @@
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"# df_signals_ecg = pd.read_hdf(file_path, \"SIGNALS\", mode=\"r\", columns=[\"STUDY\",\"LEVEL\", \"PHASE\", 'RAW_ECG_I'])\n",
"df_signals_ecg = df_signals[[\"STUDY\",\"LEVEL\", \"PHASE\", 'RAW_ECG_I']]\n", "df_signals_ecg = df_signals[[\"STUDY\",\"LEVEL\", \"PHASE\", 'RAW_ECG_I']]\n",
"df_signals_ecg.shape" "df_signals_ecg.shape"
] ]
+1 -3
View File
@@ -37,9 +37,7 @@
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"dataset_path = Path(r\"/home/jovyan/data-paulusjafahrsimulator-gpu/new_datasets/combined_dataset_25hz.parquet\")\n", "dataset_path = Path(r\"\") # TODO: enter path to dataset"
"# dataset_path = Path(r\"/home/jovyan/data-paulusjafahrsimulator-gpu/new_datasets/60s_combined_dataset_25hz.parquet\")\n",
"# dataset_path = Path(r\"/home/jovyan/data-paulusjafahrsimulator-gpu/new_datasets/120s_combined_dataset_25hz.parquet\")"
] ]
}, },
{ {
+55 -22
View File
@@ -1,5 +1,13 @@
{ {
"cells": [ "cells": [
{
"cell_type": "markdown",
"id": "cc08936c",
"metadata": {},
"source": [
"## Insights into the dataset with histogramms and scatter plots"
]
},
{ {
"cell_type": "markdown", "cell_type": "markdown",
"id": "1014c5e0", "id": "1014c5e0",
@@ -17,7 +25,8 @@
"source": [ "source": [
"import pandas as pd\n", "import pandas as pd\n",
"import numpy as np\n", "import numpy as np\n",
"import matplotlib.pyplot as plt" "import matplotlib.pyplot as plt\n",
"from pathlib import Path"
] ]
}, },
{ {
@@ -27,7 +36,7 @@
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"path =r\"C:\\Users\\micha\\FAUbox\\WS2526_Fahrsimulator_MSY (Celina Korzer)\\AU_dataset\\output_windowed.parquet\"\n", "path = Path(r\".parquet\") # TODO: enter path to dataset\n",
"df = pd.read_parquet(path=path)" "df = pd.read_parquet(path=path)"
] ]
}, },
@@ -104,21 +113,27 @@
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"# Get all columns that start with 'AU'\n", "face_au_cols = [c for c in low_all.columns if c.startswith(\"FACE_AU\")]\n",
"au_columns = [col for col in low_all.columns if col.startswith('AU')]\n", "eye_cols = ['Fix_count_short_66_150', 'Fix_count_medium_300_500',\n",
" 'Fix_count_long_gt_1000', 'Fix_count_100', 'Fix_mean_duration',\n",
" 'Fix_median_duration', 'Sac_count', 'Sac_mean_amp', 'Sac_mean_dur',\n",
" 'Sac_median_dur', 'Blink_count', 'Blink_mean_dur', 'Blink_median_dur',\n",
" 'Pupil_mean', 'Pupil_IPA']\n",
"\n",
"cols = face_au_cols+eye_cols\n",
"\n", "\n",
"# Calculate number of rows and columns for subplots\n", "# Calculate number of rows and columns for subplots\n",
"n_cols = len(au_columns)\n", "n_cols = len(cols)\n",
"n_rows = 4\n", "n_rows = 7\n",
"n_cols_subplot = 5\n", "n_cols_subplot = 5\n",
"\n", "\n",
"# Create figure with subplots\n", "# Create figure with subplots\n",
"fig, axes = plt.subplots(n_rows, n_cols_subplot, figsize=(20, 16))\n", "fig, axes = plt.subplots(n_rows, n_cols_subplot, figsize=(20, 16))\n",
"axes = axes.flatten()\n", "axes = axes.flatten()\n",
"fig.suptitle('Action Unit (AU) Distributions: Low vs High', fontsize=20, fontweight='bold', y=0.995)\n", "fig.suptitle('Feature Distributions: Low vs High', fontsize=20, fontweight='bold', y=0.995)\n",
"\n", "\n",
"# Create histogram for each AU column\n", "# Create histogram for each AU column\n",
"for idx, col in enumerate(au_columns):\n", "for idx, col in enumerate(cols):\n",
" ax = axes[idx]\n", " ax = axes[idx]\n",
" \n", " \n",
" # Plot overlapping histograms\n", " # Plot overlapping histograms\n",
@@ -133,32 +148,50 @@
" ax.grid(True, alpha=0.3)\n", " ax.grid(True, alpha=0.3)\n",
"\n", "\n",
"# Hide any unused subplots\n", "# Hide any unused subplots\n",
"for idx in range(len(au_columns), len(axes)):\n", "for idx in range(len(cols), len(axes)):\n",
" axes[idx].set_visible(False)\n", " axes[idx].set_visible(False)\n",
"\n", "\n",
"# Adjust layout\n", "# Adjust layout\n",
"plt.tight_layout()\n", "plt.tight_layout()\n",
"plt.show()" "plt.show()"
] ]
},
{
"cell_type": "code",
"execution_count": null,
"id": "6cd53cdb",
"metadata": {},
"outputs": [],
"source": [
"# Create figure with subplots\n",
"fig, axes = plt.subplots(n_rows, n_cols_subplot, figsize=(20, 16))\n",
"axes = axes.flatten()\n",
"fig.suptitle('Feature Scatter: Low vs High', fontsize=20, fontweight='bold', y=0.995)\n",
"\n",
"for idx, col in enumerate(cols):\n",
" ax = axes[idx]\n",
"\n",
" # Scatterplots\n",
" ax.scatter(range(len(low_all[col])), low_all[col], alpha=0.6, color='blue', label='low_all', s=10)\n",
" ax.scatter(range(len(high_all[col])), high_all[col], alpha=0.6, color='red', label='high_all', s=10)\n",
"\n",
" ax.set_title(col, fontsize=10, fontweight='bold')\n",
" ax.set_xlabel('Sample index', fontsize=8)\n",
" ax.set_ylabel('Value', fontsize=8)\n",
" ax.legend(fontsize=8)\n",
" ax.grid(True, alpha=0.3)\n",
"\n",
"\n",
"plt.tight_layout()\n",
"plt.show()"
]
} }
], ],
"metadata": { "metadata": {
"kernelspec": { "kernelspec": {
"display_name": "base", "display_name": "Python 3 (ipykernel)",
"language": "python", "language": "python",
"name": "python3" "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.11.5"
} }
}, },
"nbformat": 4, "nbformat": 4,
+2
View File
@@ -0,0 +1,2 @@
- url: # enter url
- password: # enter passwort
-157
View File
@@ -1,157 +0,0 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "aab6b326-a583-47ad-8bb7-723c2fddcc63",
"metadata": {
"scrolled": true
},
"outputs": [],
"source": [
"# %pip install pyocclient\n",
"import yaml\n",
"import owncloud\n",
"import pandas as pd\n",
"import time"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "4f42846c-27c3-4394-a40a-e22d73c2902e",
"metadata": {},
"outputs": [],
"source": [
"start = time.time()\n",
"\n",
"with open(\"../login.yaml\") as f:\n",
" cfg = yaml.safe_load(f)\n",
"url, password = cfg[0][\"url\"], cfg[1][\"password\"]\n",
"file = \"adabase-public-0022-v_0_0_2.h5py\"\n",
"oc = owncloud.Client.from_public_link(url, folder_password=password)\n",
"\n",
"\n",
"oc.get_file(file, \"tmp22.h5\")\n",
"\n",
"end = time.time()\n",
"print(end - start)\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "3714dec2-85d0-4f76-af46-ea45ebec2fa3",
"metadata": {},
"outputs": [],
"source": [
"start = time.time()\n",
"df_performance = pd.read_hdf(\"tmp22.h5\", \"PERFORMANCE\")\n",
"end = time.time()\n",
"print(end - start)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "f50e97d0",
"metadata": {},
"outputs": [],
"source": [
"print(22)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "c131c816",
"metadata": {},
"outputs": [],
"source": [
"df_performance"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "6ae47e52-ad86-4f8d-b929-0080dc99f646",
"metadata": {},
"outputs": [],
"source": [
"start = time.time()\n",
"df_4_col = pd.read_hdf(\"tmp.h5\", \"SIGNALS\", mode=\"r\", columns=[\"STUDY\"], start=0, stop=1)\n",
"end = time.time()\n",
"print(end - start)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "7c139f3a-ede8-4530-957d-d1bb939f6cb5",
"metadata": {},
"outputs": [],
"source": [
"df_4_col.head()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "a68d58ea-65f2-46c4-a2b2-8c3447c715d7",
"metadata": {},
"outputs": [],
"source": [
"df_4_col.shape"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "95aa4523-3784-4ab6-bf92-0227ce60e863",
"metadata": {},
"outputs": [],
"source": [
"df_4_col.info()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "defbcaf4-ad1b-453f-9b48-ab0ecfc4b5d5",
"metadata": {},
"outputs": [],
"source": [
"df_4_col.isna().sum()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "72313895-c478-44a5-9108-00b0bec01bb8",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "base",
"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.11.5"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
+112
View File
@@ -0,0 +1,112 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "457e7807",
"metadata": {},
"source": [
"# Get data from owncloud"
]
},
{
"cell_type": "markdown",
"id": "dc9ed3f8",
"metadata": {},
"source": [
"Imports"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# %pip install pyocclient\n",
"import os\n",
"import time\n",
"import yaml\n",
"import owncloud\n",
"import pandas as pd\n"
]
},
{
"cell_type": "markdown",
"id": "68e34abc",
"metadata": {},
"source": [
"Download and save"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"start = time.time()\n",
"\n",
"# TODO: User input: directory where downloaded files should be saved\n",
"save_dir = r\"./downloads\"\n",
"\n",
"\n",
"os.makedirs(save_dir, exist_ok=True)\n",
"\n",
"# Load credentials\n",
"with open(\"login.yaml\", \"r\") as f:\n",
" cfg = yaml.safe_load(f)\n",
"\n",
"url = cfg[0][\"url\"]\n",
"password = cfg[1][\"password\"]\n",
"\n",
"# Connect to OwnCloud public link\n",
"oc = owncloud.Client.from_public_link(url, folder_password=password)\n",
"\n",
"# List all available files in the shared folder\n",
"remote_files = oc.list(\".\")\n",
"\n",
"# Keep only HDF5 files and sort them by name\n",
"hdf5_files = sorted([f.get_name() for f in remote_files if f.get_name().endswith(\".hdf5\")])\n",
"\n",
"print(f\"Found {len(hdf5_files)} .hdf5 files in OwnCloud\")\n",
"\n",
"if not hdf5_files:\n",
" print(\"No .hdf5 files found.\")\n",
"else:\n",
" for i, remote_name in enumerate(hdf5_files):\n",
" local_name = f\"tmp_{i:04d}.h5\"\n",
" local_path = os.path.join(save_dir, local_name)\n",
"\n",
" try:\n",
" oc.get_file(remote_name, local_path)\n",
" print(f\"Downloaded: {remote_name} -> {local_path}\")\n",
" except Exception as e:\n",
" print(f\"Failed to download {remote_name}: {e}\")\n",
"\n",
"end = time.time()\n",
"print(f\"Finished in {end - start:.2f} seconds\")\n"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "base",
"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.11.5"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
+66 -102
View File
@@ -15,6 +15,7 @@
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"%pip install pyocclient\n",
"import yaml\n", "import yaml\n",
"import owncloud\n", "import owncloud\n",
"import pandas as pd\n", "import pandas as pd\n",
@@ -36,101 +37,109 @@
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"# Load credentials\n", "# Load credentials from YAML\n",
"with open(\"../login.yaml\") as f:\n", "with open(\"login.yaml\", \"r\") as f:\n",
" cfg = yaml.safe_load(f)\n", " cfg = yaml.safe_load(f)\n",
" \n",
"url, password = cfg[0][\"url\"], cfg[1][\"password\"]\n",
"\n", "\n",
"# Connect once\n", "url = cfg[0][\"url\"]\n",
"password = cfg[1][\"password\"]\n",
"\n",
"# Connect once to the public OwnCloud link\n",
"oc = owncloud.Client.from_public_link(url, folder_password=password)\n", "oc = owncloud.Client.from_public_link(url, folder_password=password)\n",
"# File pattern\n", "\n",
"# base = \"adabase-public-{num:04d}-v_0_0_2.h5py\"\n", "num_files = 1 # number of subject IDs to process (min: 1, max: 30)\n",
"base = \"{num:04d}-*.h5py\""
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "07c03d07",
"metadata": {},
"outputs": [],
"source": [
"num_files = 2 # number of files to process (min: 1, max: 30)\n",
"performance_data = []\n", "performance_data = []\n",
"\n", "\n",
"# Read remote file list once\n",
"remote_files = oc.list(\".\")\n",
"remote_names = [f.get_name() for f in remote_files]\n",
"\n",
"for i in range(num_files):\n", "for i in range(num_files):\n",
" file_pattern = f\"{i:04d}-*\"\n", " prefix = f\"{i:04d}-\"\n",
" \n", " matching_files = [name for name in remote_names if name.startswith(prefix) and name.endswith(\".hdf5\")]\n",
" # Get list of files matching the pattern\n",
" files = oc.list('.')\n",
" matching_files = [f.get_name() for f in files if f.get_name().startswith(f\"{i:04d}-\")]\n",
" \n",
" if matching_files:\n",
" file_name = matching_files[0] # Take the first matching file\n",
" local_tmp = f\"tmp_{i:04d}.h5\"\n",
" \n",
" oc.get_file(file_name, local_tmp)\n",
" print(f\"{file_name} geöffnet\")\n",
" else:\n",
" print(f\"Keine Datei gefunden für Muster: {file_pattern}\")\n",
" # file_name = base.format(num=i)\n",
" # local_tmp = f\"tmp_{i:04d}.h5\"\n",
"\n", "\n",
" # oc.get_file(file_name, local_tmp)\n", " if not matching_files:\n",
" # print(f\"{file_name} geöffnet\")\n", " print(f\"No file found for pattern: {prefix}*.hdf5\")\n",
"\n",
" # check SIGNALS table for AUs\n",
" with pd.HDFStore(local_tmp, mode=\"r\") as store:\n",
" cols = store.select(\"SIGNALS\", start=0, stop=1).columns\n",
" au_cols = [c for c in cols if c.startswith(\"AU\")]\n",
" if not au_cols:\n",
" print(f\"Subject {i} enthält keine AUs\")\n",
" continue\n", " continue\n",
"\n", "\n",
" # load performance table\n", " # Take the first matching file, e.g. 0000-AACA.hdf5\n",
" file_name = matching_files[0]\n",
" local_tmp = f\"tmp_{i:04d}.hdf5\"\n",
"\n",
" try:\n",
" # Download the file locally\n",
" oc.get_file(file_name, local_tmp)\n",
" print(f\"Downloaded and opened file: {file_name} -> {local_tmp}\")\n",
" except Exception as e:\n",
" print(f\"Failed to download file {file_name}: {e}\")\n",
" continue\n",
"\n",
" # Check SIGNALS table for AU columns\n",
" try:\n",
" with pd.HDFStore(local_tmp, mode=\"r\") as store:\n",
" cols = store.select(\"SIGNALS\", start=0, stop=1).columns\n",
" except Exception as e:\n",
" print(f\"Failed to read SIGNALS from {local_tmp}: {e}\")\n",
" continue\n",
"\n",
" au_cols = [c for c in cols if c.startswith(\"AU\")]\n",
" if not au_cols:\n",
" print(f\"Subject {i:04d} contains no AU columns\")\n",
" continue\n",
"\n",
" # Load PERFORMANCE table\n",
" try:\n",
" with pd.HDFStore(local_tmp, mode=\"r\") as store:\n", " with pd.HDFStore(local_tmp, mode=\"r\") as store:\n",
" perf_df = store.select(\"PERFORMANCE\")\n", " perf_df = store.select(\"PERFORMANCE\")\n",
" except Exception as e:\n",
" print(f\"Failed to read PERFORMANCE from {local_tmp}: {e}\")\n",
" continue\n",
"\n", "\n",
" f1_cols = [c for c in [\"AUDITIVE F1\", \"VISUAL F1\", \"F1\"] if c in perf_df.columns]\n", " f1_cols = [c for c in [\"AUDITIVE F1\", \"VISUAL F1\", \"F1\"] if c in perf_df.columns]\n",
" if not f1_cols:\n", " if not f1_cols:\n",
" print(f\"Subject {i}: keine F1-Spalten gefunden\")\n", " print(f\"Subject {i:04d}: no F1 columns found\")\n",
" continue\n", " continue\n",
"\n", "\n",
" subject_entry = {\"subjectID\": i}\n", " subject_entry = {\"subjectID\": i}\n",
" valid_scores = []\n", " valid_scores = []\n",
"\n", "\n",
" # iterate rows: each (study, level, phase)\n", " # Iterate through PERFORMANCE rows: each row is one (study, level, phase) combination\n",
" for _, row in perf_df.iterrows():\n", " for _, row in perf_df.iterrows():\n",
" study, level, phase = row[\"STUDY\"], row[\"LEVEL\"], row[\"PHASE\"]\n", " study = row[\"STUDY\"]\n",
" level = row[\"LEVEL\"]\n",
" phase = row[\"PHASE\"]\n",
" col_name = f\"STUDY_{study}_LEVEL_{level}_PHASE_{phase}\"\n", " col_name = f\"STUDY_{study}_LEVEL_{level}_PHASE_{phase}\"\n",
"\n", "\n",
" # collect valid F1 values among the three columns\n", " # Collect non-NaN F1 values from the available F1 columns\n",
" scores = [row[c] for c in f1_cols if pd.notna(row[c])]\n", " scores = [row[c] for c in f1_cols if pd.notna(row[c])]\n",
" if scores:\n", " if scores:\n",
" mean_score = float(np.mean(scores))\n", " mean_score = float(np.mean(scores))\n",
" subject_entry[col_name] = mean_score\n", " subject_entry[col_name] = mean_score\n",
" valid_scores.extend(scores)\n", " valid_scores.extend(scores)\n",
"\n", "\n",
" # compute overall average across all valid combinations\n", " # Compute overall average across all valid F1 values\n",
" if valid_scores:\n", " if valid_scores:\n",
" subject_entry[\"overall_score\"] = float(np.mean(valid_scores))\n", " subject_entry[\"overall_score\"] = float(np.mean(valid_scores))\n",
" performance_data.append(subject_entry)\n", " performance_data.append(subject_entry)\n",
" print(f\"Subject {i}: {len(valid_scores)} gültige Scores, Overall = {subject_entry['overall_score']:.3f}\")\n", " print(\n",
" f\"Subject {i:04d}: {len(valid_scores)} valid scores, \"\n",
" f\"overall = {subject_entry['overall_score']:.3f}\"\n",
" )\n",
" else:\n", " else:\n",
" print(f\"Subject {i}: keine gültigen F1-Scores\")\n", " print(f\"Subject {i:04d}: no valid F1 scores found\")\n",
"\n", "\n",
"# build dataframe\n", "# Build final DataFrame and save CSV\n",
"if performance_data:\n", "if performance_data:\n",
" performance_df = pd.DataFrame(performance_data)\n", " performance_df = pd.DataFrame(performance_data)\n",
" combination_cols = sorted([c for c in performance_df.columns if c.startswith(\"STUDY_\")])\n", " combination_cols = sorted([c for c in performance_df.columns if c.startswith(\"STUDY_\")])\n",
" final_cols = [\"subjectID\", \"overall_score\"] + combination_cols\n", " final_cols = [\"subjectID\", \"overall_score\"] + combination_cols\n",
" performance_df = performance_df[final_cols]\n", " performance_df = performance_df[final_cols]\n",
" performance_df.to_csv(\"n_au_performance.csv\", index=False)\n", " performance_df.to_csv(\"performance.csv\", index=False)\n",
"\n", "\n",
" print(f\"\\nGesamt Subjects mit Action Units: {len(performance_df)}\")\n", " print(f\"\\nTotal subjects with Action Units: {len(performance_df)}\")\n",
" print(\"Saved results to performance.csv\")\n",
"else:\n", "else:\n",
" print(\"Keine gültigen Daten gefunden.\")" " print(\"No valid data found.\")"
] ]
}, },
{ {
@@ -142,56 +151,11 @@
"source": [ "source": [
"performance_df.head()" "performance_df.head()"
] ]
},
{
"cell_type": "code",
"execution_count": null,
"id": "db95eea7",
"metadata": {},
"outputs": [],
"source": [
"with pd.HDFStore(\"tmp_0000.h5\", mode=\"r\") as store:\n",
" md = store.select(\"META\")\n",
"print(\"File 0:\")\n",
"print(md)\n",
"with pd.HDFStore(\"tmp_0001.h5\", mode=\"r\") as store:\n",
" md = store.select(\"META\")\n",
"print(\"File 1\")\n",
"print(md)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "8067036b",
"metadata": {},
"outputs": [],
"source": [
"pd.set_option('display.max_columns', None)\n",
"pd.set_option('display.max_rows', None)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "f18e7385",
"metadata": {},
"outputs": [],
"source": [
"with pd.HDFStore(\"tmp_0000.h5\", mode=\"r\") as store:\n",
" md = store.select(\"SIGNALS\", start=0, stop=1)\n",
"print(\"File 0:\")\n",
"md.head()\n",
"# with pd.HDFStore(\"tmp_0001.h5\", mode=\"r\",start=0, stop=1) as store:\n",
"# md = store.select(\"SIGNALS\")\n",
"# print(\"File 1\")\n",
"# print(md.columns)"
]
} }
], ],
"metadata": { "metadata": {
"kernelspec": { "kernelspec": {
"display_name": "base", "display_name": "310",
"language": "python", "language": "python",
"name": "python3" "name": "python3"
}, },
@@ -205,7 +169,7 @@
"name": "python", "name": "python",
"nbconvert_exporter": "python", "nbconvert_exporter": "python",
"pygments_lexer": "ipython3", "pygments_lexer": "ipython3",
"version": "3.11.5" "version": "3.10.19"
} }
}, },
"nbformat": 4, "nbformat": 4,
@@ -1,58 +0,0 @@
from feat import Detector
from feat.utils.io import get_test_data_path
from moviepy.video.io.VideoFileClip import VideoFileClip
import os
def extract_aus(path, model, skip_frames):
detector = Detector(au_model=model)
video_prediction = detector.detect(
path, data_type="video", skip_frames=skip_frames, face_detection_threshold=0.95 # alle 5 Sekunden einbeziehen - 24 Frames pro Sekunde
)
return video_prediction.aus.sum()
def split_video(path, chunk_length=120):
video = VideoFileClip(path)
duration = int(video.duration)
subclips_dir = os.path.join(os.dirname(path), "subclips")
os.makedirs(subclips_dir, exist_ok=True)
paths = []
for start in range(0, duration, chunk_length):
end = min(start + chunk_length, duration)
subclip = (
video
.subclip(start, end)
.without_audio()
.set_fps(video.fps)
)
output_path = f"{subclips_dir}_part_{start//chunk_length + 1}.mp4"
subclip.write_videofile(
output_path,
)
paths.append(output_path)
return output_path
# def start(path):
# results = []
# clips = split_video(path)
# for clip in clips:
# results.append(extract_aus(clip, 'svm', 25*5))
# return results
if __name__ == "__main__":
results = []
clips = []
test_video_path = "AU_creation/YTDown.com_YouTube_Was-ist-los-bei-7-vs-Wild_Media_Gtj9zu_WikU_001_1080p.mp4"
clips = split_video(test_video_path)
for clippath in clips:
results.append(extract_aus(clippath, 'svm', 25*5))
print(results)
+39 -19
View File
@@ -5,27 +5,47 @@
"id": "3b0c6c82", "id": "3b0c6c82",
"metadata": {}, "metadata": {},
"source": [ "source": [
"Hier entsteht die Dokumentation, wie die Action Units erzeugt wurden.\n", "## Action Unit Documentation and Setup\n",
"Daraus wird dann letztendlich ein Skript erstellt, welches automatisch AUs aus Videodateien erstellen soll.\n",
"\n", "\n",
"Py-Feat besitzt Dependencies, die ab Python 3.12 nicht mehr verfügbar sind.\n", "This documentation outlines the process for generating **Action Units (AUs)** and the eventual creation of a script to automate AU extraction from video files.\n",
"Dazu muss ein Kernel mit Python 3.11 erstellt werden.\n",
"Folgendes Vorgehen:\n",
"1. Seite des Jupyter Labs öffnen\n",
"2. Terminal öffnen und folgende Befehle eingeben:\n",
" conda create -n py311 python=3.11\n",
" source ~/.bashrc\n",
" conda activate py311\n",
" conda install jupyter\n",
" python -m ipykernel install --user --name=py311 --display-name \"Python 3.11\"\n",
" pip install py-feat\n",
" pip install \"moviepy<2.0\" (falls benötigt)\n",
"3. den Kernel neustarten\n",
"4. in VSC den Kernel neu hinzufügen und dann den Kernel mit dem Namen \"Python 3.11\" auswählen.\n",
"\n", "\n",
"Der Code unten zeigt eine beispielhafte Integration der py-feat Bibliothek.\n", "### Python Environment Configuration\n",
"Die Klassifizierung zu 0,1 kommt durch die Wahl des AU-Modells zustande. Dabei wird SVM gewählt. (ADABase Paper)\n", "\n",
"Gibt die Klassifizierung einen Gleitkommawert zwischen 0 & 1 aus, dann kommt XGB zum Einsatz. (REVELIO Paper)" "**Py-Feat** relies on dependencies that are incompatible with Python 3.12 and later. To ensure functionality, you must set up a dedicated **Python 3.11** kernel.\n",
"\n",
"#### Setup Instructions:\n",
"\n",
"1. Open your **Jupyter Lab** interface.\n",
"2. Open a **Terminal** and execute the following commands:\n",
"```bash\n",
"conda create -n py311 python=3.11\n",
"source ~/.bashrc\n",
"conda activate py311\n",
"conda install jupyter\n",
"python -m ipykernel install --user --name=py311 --display-name \"Python 3.11\"\n",
"pip install py-feat\n",
"pip install \"moviepy<2.0\" # Only if required\n",
"\n",
"```\n",
"\n",
"\n",
"3. **Restart** the kernel.\n",
"4. In **VS Code**, refresh your kernel list and select the one labeled **\"Python 3.11\"**.\n",
"\n",
"---\n",
"\n",
"### Implementation Details\n",
"\n",
"The following code demonstrates a sample integration of the `py-feat` library. The classification output format is determined by the specific AU model selected:\n",
"\n",
"| Model | Output Type | Reference Paper |\n",
"| --- | --- | --- |\n",
"| **SVM** | Binary (0 or 1) | *ADABase* |\n",
"| **XGB** | Floating Point (0.0 - 1.0) | *REVELIO* |\n",
"\n",
"---\n",
"\n",
"Would you like me to provide the Python code block to implement the **SVM** or **XGB** detector using these libraries?"
] ]
}, },
{ {
@@ -1,296 +0,0 @@
import cv2
import time
import os
import threading
from datetime import datetime
from feat import Detector
import torch
import mediapipe as mp
import csv
# Konfiguration
CAMERA_INDEX = 0
OUTPUT_DIR = "recordings"
VIDEO_DURATION = 10 # Sekunden
START_INTERVAL = 5 # Sekunden bis zum nächsten Start
FPS = 25.0 # Feste FPS
if not os.path.exists(OUTPUT_DIR):
os.makedirs(OUTPUT_DIR)
# Globaler Detector, um ihn nicht bei jedem Video neu laden zu müssen (spart massiv Zeit/Speicher)
print("Initialisiere AU-Detector (bitte warten)...")
detector = Detector(au_model="xgb")
# ===== MediaPipe FaceMesh Setup =====
mp_face_mesh = mp.solutions.face_mesh
face_mesh = mp_face_mesh.FaceMesh(
static_image_mode=False,
max_num_faces=1,
refine_landmarks=True, # wichtig für Iris
min_detection_confidence=0.5,
min_tracking_confidence=0.5
)
LEFT_IRIS = [474, 475, 476, 477]
RIGHT_IRIS = [469, 470, 471, 472]
LEFT_EYE_LIDS = (159, 145)
RIGHT_EYE_LIDS = (386, 374)
LEFT_EYE_GAZE_IDXS = (33, 133, 159, 145)
RIGHT_EYE_GAZE_IDXS = (263, 362, 386, 374)
EYE_OPEN_THRESHOLD = 6
# CSV vorbereiten
gaze_csv = open("gaze_data.csv", mode="w", newline="")
gaze_writer = csv.writer(gaze_csv)
gaze_writer.writerow([
"timestamp",
"left_gaze_x",
"left_gaze_y",
"right_gaze_x",
"right_gaze_y",
"left_valid",
"right_valid",
"left_diameter",
"right_diameter"
])
def eye_openness(landmarks, top_idx, bottom_idx, img_height):
top = landmarks[top_idx]
bottom = landmarks[bottom_idx]
return abs(top.y - bottom.y) * img_height
def compute_gaze(landmarks, iris_center, indices, w, h):
idx1, idx2, top_idx, bottom_idx = indices
p1 = landmarks[idx1]
p2 = landmarks[idx2]
top = landmarks[top_idx]
bottom = landmarks[bottom_idx]
x1 = p1.x * w
x2 = p2.x * w
y_top = top.y * h
y_bottom = bottom.y * h
iris_x, iris_y = iris_center
eye_left = min(x1, x2)
eye_right = max(x1, x2)
eye_width = eye_right - eye_left
eye_height = abs(y_bottom - y_top)
if eye_width == 0 or eye_height == 0:
return 0.5, 0.5
gaze_x = (iris_x - eye_left) / eye_width
gaze_y = (iris_y - min(y_top, y_bottom)) / eye_height
gaze_x = max(0, min(1, gaze_x))
gaze_y = max(0, min(1, gaze_y))
return gaze_x, gaze_y
def extract_aus(path, skip_frames):
# torch.no_grad() deaktiviert die Gradientenberechnung.
# Das löst den "Can't call numpy() on Tensor that requires grad" Fehler.
with torch.no_grad():
video_prediction = detector.detect_video(
path,
skip_frames=skip_frames,
face_detection_threshold=0.95
)
# Falls video_prediction oder .aus noch Tensoren sind,
# stellen wir sicher, dass sie korrekt summiert werden.
try:
# Wir nehmen die Summe der Action Units über alle detektierten Frames
res = video_prediction.aus.sum()
return res
except Exception as e:
print(f"Fehler bei der Summenbildung: {e}")
return 0
def startAU_creation(video_path):
"""Diese Funktion läuft nun in einem eigenen Thread."""
try:
print(f"\n[THREAD START] Analyse läuft für: {video_path}")
# skip_frames berechnen (z.B. alle 5 Sekunden bei 25 FPS = 125)
output = extract_aus(video_path, skip_frames=int(FPS*5))
print(f"\n--- Ergebnis für {os.path.basename(video_path)} ---")
print(output)
print("--------------------------------------------------\n")
except Exception as e:
print(f"Fehler bei der Analyse von {video_path}: {e}")
class VideoRecorder:
def __init__(self, filename, width, height):
self.filename = filename
fourcc = cv2.VideoWriter_fourcc(*'XVID')
self.out = cv2.VideoWriter(filename, fourcc, FPS, (width, height))
self.frames_to_record = int(VIDEO_DURATION * FPS)
self.frames_count = 0
self.is_finished = False
def write_frame(self, frame):
if self.frames_count < self.frames_to_record:
self.out.write(frame)
self.frames_count += 1
else:
self.finish()
def finish(self):
if not self.is_finished:
self.out.release()
self.is_finished = True
abs_path = os.path.abspath(self.filename)
print(f"Video fertig gespeichert: {self.filename}")
# --- MULTITHREADING HIER ---
# Wir starten die Analyse in einem neuen Thread, damit main() sofort weiter frames lesen kann
analysis_thread = threading.Thread(target=startAU_creation, args=(abs_path,))
analysis_thread.daemon = True # Beendet sich, wenn das Hauptprogramm schließt
analysis_thread.start()
def main():
cap = cv2.VideoCapture(CAMERA_INDEX)
if not cap.isOpened():
print("Fehler: Kamera konnte nicht geöffnet werden.")
return
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
active_recorders = []
last_start_time = 0
print("Aufnahme läuft. Drücke 'q' zum Beenden.")
try:
while True:
ret, frame = cap.read()
if not ret:
break
rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
h, w, _ = frame.shape
results = face_mesh.process(rgb)
left_valid = 0
right_valid = 0
left_diameter = None
right_diameter = None
left_gaze_x = None
left_gaze_y = None
right_gaze_x = None
right_gaze_y = None
if results.multi_face_landmarks:
face_landmarks = results.multi_face_landmarks[0]
left_open = eye_openness(
face_landmarks.landmark,
LEFT_EYE_LIDS[0],
LEFT_EYE_LIDS[1],
h
)
right_open = eye_openness(
face_landmarks.landmark,
RIGHT_EYE_LIDS[0],
RIGHT_EYE_LIDS[1],
h
)
left_valid = 1 if left_open > EYE_OPEN_THRESHOLD else 0
right_valid = 1 if right_open > EYE_OPEN_THRESHOLD else 0
for eye_name, eye_indices in [("left", LEFT_IRIS), ("right", RIGHT_IRIS)]:
iris_points = []
for idx in eye_indices:
lm = face_landmarks.landmark[idx]
x_i, y_i = int(lm.x * w), int(lm.y * h)
iris_points.append((x_i, y_i))
if len(iris_points) == 4:
cx = int(sum(p[0] for p in iris_points) / 4)
cy = int(sum(p[1] for p in iris_points) / 4)
radius = max(
((x - cx) ** 2 + (y - cy) ** 2) ** 0.5
for (x, y) in iris_points
)
diameter = 2 * radius
cv2.circle(frame, (cx, cy), int(radius), (0, 255, 0), 2)
if eye_name == "left" and left_valid:
left_diameter = diameter
left_gaze_x, left_gaze_y = compute_gaze(
face_landmarks.landmark,
(cx, cy),
LEFT_EYE_GAZE_IDXS,
w, h
)
elif eye_name == "right" and right_valid:
right_diameter = diameter
right_gaze_x, right_gaze_y = compute_gaze(
face_landmarks.landmark,
(cx, cy),
RIGHT_EYE_GAZE_IDXS,
w, h
)
# CSV schreiben
gaze_writer.writerow([
time.time(),
left_gaze_x,
left_gaze_y,
right_gaze_x,
right_gaze_y,
left_valid,
right_valid,
left_diameter,
right_diameter
])
current_time = time.time()
if current_time - last_start_time >= START_INTERVAL:
timestamp = datetime.now().strftime("%H%M%S")
filename = os.path.join(OUTPUT_DIR, f"rec_{timestamp}.avi")
new_recorder = VideoRecorder(filename, width, height)
active_recorders.append(new_recorder)
last_start_time = current_time
for rec in active_recorders[:]:
rec.write_frame(frame)
if rec.is_finished:
active_recorders.remove(rec)
cv2.imshow('Kamera Livestream', frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
time.sleep(1/FPS)
finally:
gaze_csv.close()
face_mesh.close()
cap.release()
cv2.destroyAllWindows()
print("Programm beendet. Warte ggf. auf laufende Analysen...")
if __name__ == "__main__":
main()
@@ -0,0 +1,174 @@
import cv2
import mediapipe as mp
import numpy as np
import pyautogui
import pandas as pd
import time
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
# Bildschirmgröße
screen_w, screen_h = pyautogui.size()
# MediaPipe Setup
mp_face_mesh = mp.solutions.face_mesh
face_mesh = mp_face_mesh.FaceMesh(refine_landmarks=True)
cap = cv2.VideoCapture(0)
# Iris Landmark Indizes
LEFT_IRIS = [468, 469, 470, 471, 472]
RIGHT_IRIS = [473, 474, 475, 476, 477]
def get_iris_center(landmarks, indices):
points = np.array([[landmarks[i].x, landmarks[i].y] for i in indices])
return np.mean(points, axis=0)
# Kalibrierpunkte
calibration_points = [
(0.1,0.1),(0.5,0.1),(0.9,0.1),
(0.1,0.5),(0.5,0.5),(0.9,0.5),
(0.1,0.9),(0.5,0.9),(0.9,0.9)
]
left_data = []
right_data = []
print("Kalibrierung startet...")
for idx, (px, py) in enumerate(calibration_points):
screen = np.zeros((screen_h, screen_w, 3), dtype=np.uint8)
for j, (cpx, cpy) in enumerate(calibration_points):
cx = int(cpx * screen_w)
cy = int(cpy * screen_h)
if j == idx:
color = (0, 0, 255)
radius = 25
else:
color = (255, 255, 255)
radius = 15
cv2.circle(screen, (cx, cy), radius, color, -1)
# Fenster vorbereiten
cv2.namedWindow("Calibration", cv2.WINDOW_NORMAL)
cv2.imshow("Calibration", screen)
cv2.waitKey(1000)
samples_left = []
samples_right = []
start = time.time()
while time.time() - start < 2:
ret, frame = cap.read()
frame = cv2.flip(frame, 1)
rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
results = face_mesh.process(rgb)
if results.multi_face_landmarks:
mesh = results.multi_face_landmarks[0].landmark
left_center = get_iris_center(mesh, LEFT_IRIS)
right_center = get_iris_center(mesh, RIGHT_IRIS)
samples_left.append(left_center)
samples_right.append(right_center)
avg_left = np.mean(samples_left, axis=0)
avg_right = np.mean(samples_right, axis=0)
target_x = int(px * screen_w)
target_y = int(py * screen_h)
left_data.append([avg_left[0], avg_left[1], target_x, target_y])
right_data.append([avg_right[0], avg_right[1], target_x, target_y])
cv2.destroyWindow("Calibration")
# Training
def train_model(data):
data = np.array(data)
X = data[:, :2]
yx = data[:, 2]
yy = data[:, 3]
model_x = make_pipeline(PolynomialFeatures(2), LinearRegression())
model_y = make_pipeline(PolynomialFeatures(2), LinearRegression())
model_x.fit(X, yx)
model_y.fit(X, yy)
return model_x, model_y
model_lx, model_ly = train_model(left_data)
model_rx, model_ry = train_model(right_data)
print("Kalibrierung abgeschlossen. Tracking startet...")
# Datenaufzeichnung
records = []
while True:
ret, frame = cap.read()
frame = cv2.flip(frame, 1)
rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
results = face_mesh.process(rgb)
if results.multi_face_landmarks:
mesh = results.multi_face_landmarks[0].landmark
left_center = get_iris_center(mesh, LEFT_IRIS)
right_center = get_iris_center(mesh, RIGHT_IRIS)
left_input = np.array([left_center])
right_input = np.array([right_center])
lx = model_lx.predict(left_input)[0]
ly = model_ly.predict(left_input)[0]
rx = model_rx.predict(right_input)[0]
ry = model_ry.predict(right_input)[0]
# Pixel-Koordinaten begrenzen
lx = np.clip(lx, 0, screen_w)
ly = np.clip(ly, 0, screen_h)
rx = np.clip(rx, 0, screen_w)
ry = np.clip(ry, 0, screen_h)
# Normierung 0–1
lx_norm = lx / screen_w
ly_norm = ly / screen_h
rx_norm = rx / screen_w
ry_norm = ry / screen_h
records.append([
lx_norm, ly_norm,
rx_norm, ry_norm
])
print("L:", int(lx), int(ly), " | R:", int(rx), int(ry))
cv2.imshow("Tracking", frame)
key = cv2.waitKey(1) & 0xFF
if key == ord('q'):
print("q gedrückt – beende Tracking")
break
cap.release()
cv2.destroyAllWindows()
# CSV speichern
df = pd.DataFrame(records, columns=[
"EYE_LEFT_GAZE_POINT_ON_DISPLAY_AREA_X",
"EYE_LEFT_GAZE_POINT_ON_DISPLAY_AREA_Y",
"EYE_RIGHT_GAZE_POINT_ON_DISPLAY_AREA_X",
"EYE_RIGHT_GAZE_POINT_ON_DISPLAY_AREA_Y"
])
df.to_csv("gaze_data1.csv", index=False)
print("Daten gespeichert als gaze_data1.csv")
@@ -1,91 +0,0 @@
import os
import pandas as pd
from pathlib import Path
print(os.getcwd())
num_files = 2 # number of files to process (min: 1, max: 30)
print("connection aufgebaut")
data_dir = Path("/home/jovyan/Fahrsimulator_MSY2526_AI/EDA")
# os.chdir(data_dir)
# Get all .h5 files and sort them
matching_files = sorted(data_dir.glob("*.h5"))
# Chunk size for reading (adjust based on your RAM - 100k rows is ~50-100MB depending on columns)
CHUNK_SIZE = 100_000
for i, file_path in enumerate(matching_files):
print(f"Subject {i} gestartet")
print(f"{file_path} geoeffnet")
# Step 1: Get total number of rows and column names
with pd.HDFStore(file_path, mode="r") as store:
cols = store.select("SIGNALS", start=0, stop=1).columns
nrows = store.get_storer("SIGNALS").nrows
print(f"Total columns: {len(cols)}, Total rows: {nrows}")
# Step 2: Filter columns that start with "FACE_AU"
eye_cols = [c for c in cols if c.startswith("EYE_")]
print(f"eye-tracking columns found: {eye_cols}")
if len(eye_cols) == 0:
print(f"keine eye-tracking-Signale in Subject {i}")
continue
# Columns to read
columns_to_read = ["STUDY", "LEVEL", "PHASE"] + eye_cols
# Step 3: Process file in chunks
chunks_to_save = []
for start_row in range(0, nrows, CHUNK_SIZE):
stop_row = min(start_row + CHUNK_SIZE, nrows)
print(f"Processing rows {start_row} to {stop_row} ({stop_row/nrows*100:.1f}%)")
# Read chunk
df_chunk = pd.read_hdf(
file_path,
key="SIGNALS",
columns=columns_to_read,
start=start_row,
stop=stop_row
)
# Add metadata columns
df_chunk["subjectID"] = i
df_chunk["rowID"] = range(start_row, stop_row)
# Clean data
df_chunk = df_chunk[df_chunk["LEVEL"] != 0]
df_chunk = df_chunk.dropna()
# Only keep non-empty chunks
if len(df_chunk) > 0:
chunks_to_save.append(df_chunk)
# Free memory
del df_chunk
print("load and cleaning done")
# Step 4: Combine all chunks and save
if chunks_to_save:
df_final = pd.concat(chunks_to_save, ignore_index=True)
print(f"Final dataframe shape: {df_final.shape}")
# Save to parquet
base_dir = Path(r"/home/jovyan/data-paulusjafahrsimulator-gpu/new_ET_Parquet_files")
os.makedirs(base_dir, exist_ok=True)
out_name = base_dir / f"ET_signals_extracted_{i:04d}.parquet"
df_final.to_parquet(out_name, index=False)
print(f"Saved to {out_name}")
# Free memory
del df_final
del chunks_to_save
else:
print(f"No valid data found for Subject {i}")
print("All files processed!")
@@ -1,91 +0,0 @@
import os
import pandas as pd
from pathlib import Path
print(os.getcwd())
num_files = 2 # number of files to process (min: 1, max: 30)
print("connection aufgebaut")
data_dir = Path(r"C:\Users\x\repo\UXKI\Fahrsimulator_MSY2526_AI\newTmp")
# Get all .h5 files and sort them
matching_files = sorted(data_dir.glob("*.h5"))
# Chunk size for reading (adjust based on your RAM - 100k rows is ~50-100MB depending on columns)
CHUNK_SIZE = 100_000
for i, file_path in enumerate(matching_files):
print(f"Subject {i} gestartet")
print(f"{file_path} geoeffnet")
# Step 1: Get total number of rows and column names
with pd.HDFStore(file_path, mode="r") as store:
cols = store.select("SIGNALS", start=0, stop=1).columns
nrows = store.get_storer("SIGNALS").nrows
print(f"Total columns: {len(cols)}, Total rows: {nrows}")
# Step 2: Filter columns that start with "FACE_AU"
eye_cols = [c for c in cols if c.startswith("FACE_AU")]
print(f"FACE_AU columns found: {eye_cols}")
if len(eye_cols) == 0:
print(f"keine FACE_AU-Signale in Subject {i}")
continue
# Columns to read
columns_to_read = ["STUDY", "LEVEL", "PHASE"] + eye_cols
# Step 3: Process file in chunks
chunks_to_save = []
for start_row in range(0, nrows, CHUNK_SIZE):
stop_row = min(start_row + CHUNK_SIZE, nrows)
print(f"Processing rows {start_row} to {stop_row} ({stop_row/nrows*100:.1f}%)")
# Read chunk
df_chunk = pd.read_hdf(
file_path,
key="SIGNALS",
columns=columns_to_read,
start=start_row,
stop=stop_row
)
# Add metadata columns
df_chunk["subjectID"] = i
df_chunk["rowID"] = range(start_row, stop_row)
# Clean data
df_chunk = df_chunk[df_chunk["LEVEL"] != 0]
df_chunk = df_chunk.dropna()
# Only keep non-empty chunks
if len(df_chunk) > 0:
chunks_to_save.append(df_chunk)
# Free memory
del df_chunk
print("load and cleaning done")
# Step 4: Combine all chunks and save
if chunks_to_save:
df_final = pd.concat(chunks_to_save, ignore_index=True)
print(f"Final dataframe shape: {df_final.shape}")
# Save to parquet
base_dir = Path(r"C:\new_AU_parquet_files")
os.makedirs(base_dir, exist_ok=True)
out_name = base_dir / f"cleaned_{i:04d}.parquet"
df_final.to_parquet(out_name, index=False)
print(f"Saved to {out_name}")
# Free memory
del df_final
del chunks_to_save
else:
print(f"No valid data found for Subject {i}")
print("All files processed!")
+42 -46
View File
@@ -4,27 +4,26 @@ import pandas as pd
from pathlib import Path from pathlib import Path
from sklearn.preprocessing import MinMaxScaler from sklearn.preprocessing import MinMaxScaler
from scipy.signal import welch from scipy.signal import welch
from pygazeanalyser.detectors import fixation_detection, saccade_detection from pygazeanalyser.detectors import fixation_detection, saccade_detection # not installed by default
############################################################################## ##############################################################################
# KONFIGURATION # CONFIGURATION
############################################################################## ##############################################################################
INPUT_DIR = Path(r"/home/jovyan/data-paulusjafahrsimulator-gpu/both_mod_parquet_files") INPUT_DIR = Path(r"") # directory that stores the parquet files (one file per subject)
OUTPUT_FILE = Path(r"/home/jovyan/data-paulusjafahrsimulator-gpu/new_datasets/50s_25Hz_dataset.parquet") OUTPUT_FILE = Path(r"") # path for resulting dataset
WINDOW_SIZE_SAMPLES = 25*50 # 50s at 25Hz
WINDOW_SIZE_SAMPLES = 25*50 # 50s bei 25Hz STEP_SIZE_SAMPLES = 125 # 5s at 25Hz
STEP_SIZE_SAMPLES = 125 # 5s bei 25Hz
SAMPLING_RATE = 25 # Hz SAMPLING_RATE = 25 # Hz
MIN_DUR_BLINKS = 2 # x * 40ms MIN_DUR_BLINKS = 2 # x * 40ms
############################################################################## ##############################################################################
# EYE-TRACKING FUNKTIONEN # EYE-TRACKING FUNCTIONS
############################################################################## ##############################################################################
def clean_eye_df(df): def clean_eye_df(df):
"""Extrahiert nur Eye-Tracking Spalten und entfernt leere Zeilen.""" """Extracts Eye-Tracking columns only and removes empty rows."""
eye_cols = [c for c in df.columns if c.startswith("EYE_")] eye_cols = [c for c in df.columns if c.startswith("EYE_")]
if not eye_cols: if not eye_cols:
@@ -38,7 +37,7 @@ def clean_eye_df(df):
def extract_gaze_signal(df): def extract_gaze_signal(df):
"""Extrahiert 2D-Gaze-Positionen, maskiert ungültige Samples und interpoliert.""" """Extracts 2D gaze positions, masks invalid samples, and interpolates."""
gx_L = df["EYE_LEFT_GAZE_POINT_ON_DISPLAY_AREA_X"].astype(float).copy() gx_L = df["EYE_LEFT_GAZE_POINT_ON_DISPLAY_AREA_X"].astype(float).copy()
gy_L = df["EYE_LEFT_GAZE_POINT_ON_DISPLAY_AREA_Y"].astype(float).copy() gy_L = df["EYE_LEFT_GAZE_POINT_ON_DISPLAY_AREA_Y"].astype(float).copy()
gx_R = df["EYE_RIGHT_GAZE_POINT_ON_DISPLAY_AREA_X"].astype(float).copy() gx_R = df["EYE_RIGHT_GAZE_POINT_ON_DISPLAY_AREA_X"].astype(float).copy()
@@ -51,14 +50,14 @@ def extract_gaze_signal(df):
for arr in [gx_L, gy_L, gx_R, gy_R]: for arr in [gx_L, gy_L, gx_R, gy_R]:
arr.replace([np.inf, -np.inf], np.nan, inplace=True) arr.replace([np.inf, -np.inf], np.nan, inplace=True)
# Ungültige maskieren # Mask invalids
gx_L[~val_L] = np.nan gx_L[~val_L] = np.nan
gy_L[~val_L] = np.nan gy_L[~val_L] = np.nan
gx_R[~val_R] = np.nan gx_R[~val_R] = np.nan
gy_R[~val_R] = np.nan gy_R[~val_R] = np.nan
# Mittelwert beider Augen # Mean of both eyes
gx = np.mean(np.column_stack([gx_L, gx_R]), axis=1) gx = np.mean(np.column_stack([gx_L, gx_R]), axis=1)
gy = np.mean(np.column_stack([gy_L, gy_R]), axis=1) gy = np.mean(np.column_stack([gy_L, gy_R]), axis=1)
@@ -66,7 +65,7 @@ def extract_gaze_signal(df):
gx = pd.Series(gx).interpolate(limit=None, limit_direction="both").bfill().ffill() gx = pd.Series(gx).interpolate(limit=None, limit_direction="both").bfill().ffill()
gy = pd.Series(gy).interpolate(limit=None, limit_direction="both").bfill().ffill() gy = pd.Series(gy).interpolate(limit=None, limit_direction="both").bfill().ffill()
# MinMax Skalierung # MinMax scaling
xscaler = MinMaxScaler() xscaler = MinMaxScaler()
gxscale = xscaler.fit_transform(gx.values.reshape(-1, 1)) gxscale = xscaler.fit_transform(gx.values.reshape(-1, 1))
@@ -77,7 +76,7 @@ def extract_gaze_signal(df):
def extract_pupil(df): def extract_pupil(df):
"""Extrahiert Pupillengröße (beide Augen gemittelt).""" """Extract pupil size (average of both eyes)."""
pl = df["EYE_LEFT_PUPIL_DIAMETER"].replace([np.inf, -np.inf], np.nan) pl = df["EYE_LEFT_PUPIL_DIAMETER"].replace([np.inf, -np.inf], np.nan)
pr = df["EYE_RIGHT_PUPIL_DIAMETER"].replace([np.inf, -np.inf], np.nan) pr = df["EYE_RIGHT_PUPIL_DIAMETER"].replace([np.inf, -np.inf], np.nan)
@@ -96,7 +95,7 @@ def extract_pupil(df):
def detect_blinks(pupil_validity, min_duration=5): def detect_blinks(pupil_validity, min_duration=5):
"""Erkennt Blinks: Validity=0 → Blink.""" """Detect blinks: Validity=0 → Blink."""
blinks = [] blinks = []
start = None start = None
@@ -120,13 +119,13 @@ def compute_IPA(pupil, fs=25):
def extract_eye_features_window(df_eye_window, fs=25, min_dur_blinks=2): def extract_eye_features_window(df_eye_window, fs=25, min_dur_blinks=2):
""" """
Extrahiert Eye-Tracking Features für ein einzelnes Window. Extracts eye tracking features for a single window.
Gibt Dictionary mit allen Eye-Features zurück. Returns a dictionary containing all eye features.
""" """
# Gaze # Gaze
gaze = extract_gaze_signal(df_eye_window) gaze = extract_gaze_signal(df_eye_window)
# Pupille # Pupil
pupil, pupil_validity = extract_pupil(df_eye_window) pupil, pupil_validity = extract_pupil(df_eye_window)
window_size = len(df_eye_window) window_size = len(df_eye_window)
@@ -143,7 +142,6 @@ def extract_eye_features_window(df_eye_window, fs=25, min_dur_blinks=2):
fixation_durations = [f[2] for f in efix if np.isfinite(f[2]) and f[2] > 0] fixation_durations = [f[2] for f in efix if np.isfinite(f[2]) and f[2] > 0]
# Kategorien
F_short = sum(66 <= d <= 150 for d in fixation_durations) F_short = sum(66 <= d <= 150 for d in fixation_durations)
F_medium = sum(300 <= d <= 500 for d in fixation_durations) F_medium = sum(300 <= d <= 500 for d in fixation_durations)
F_long = sum(d >= 1000 for d in fixation_durations) F_long = sum(d >= 1000 for d in fixation_durations)
@@ -197,27 +195,27 @@ def extract_eye_features_window(df_eye_window, fs=25, min_dur_blinks=2):
############################################################################## ##############################################################################
# KOMBINIERTE FEATURE-EXTRAKTION # Combined feature extraction
############################################################################## ##############################################################################
def process_combined_features(input_dir, output_file, window_size, step_size, fs=25,min_duration_blinks=2): def process_combined_features(input_dir, output_file, window_size, step_size, fs=25,min_duration_blinks=2):
""" """
Verarbeitet Parquet-Dateien mit FACE_AU und EYE Spalten. Processes Parquet files with FACE_AU and EYE columns.
Extrahiert beide Feature-Sets und kombiniert sie. Extracts both feature sets and combines them.
""" """
input_path = Path(input_dir) input_path = Path(input_dir)
parquet_files = sorted(input_path.glob("*.parquet")) parquet_files = sorted(input_path.glob("*.parquet"))
if not parquet_files: if not parquet_files:
print(f"FEHLER: Keine Parquet-Dateien in {input_dir} gefunden!") print(f"Error: No parquet-files found in {input_dir}!")
return None return None
print(f"\n{'='*70}") print(f"\n{'='*70}")
print(f"KOMBINIERTE FEATURE-EXTRAKTION") print(f"Combined feature-extraction")
print(f"{'='*70}") print(f"{'='*70}")
print(f"Dateien: {len(parquet_files)}") print(f"Files: {len(parquet_files)}")
print(f"Window: {window_size} Samples ({window_size/fs:.1f}s bei {fs}Hz)") print(f"Window: {window_size} Samples ({window_size/fs:.1f}s at {fs}Hz)")
print(f"Step: {step_size} Samples ({step_size/fs:.1f}s bei {fs}Hz)") print(f"Step: {step_size} Samples ({step_size/fs:.1f}s at {fs}Hz)")
print(f"{'='*70}\n") print(f"{'='*70}\n")
all_windows = [] all_windows = []
@@ -227,24 +225,22 @@ def process_combined_features(input_dir, output_file, window_size, step_size, fs
try: try:
df = pd.read_parquet(parquet_file) df = pd.read_parquet(parquet_file)
print(f" Einträge: {len(df)}") print(f" Entries: {len(df)}")
# Identifiziere Spalten
au_columns = [col for col in df.columns if col.startswith('FACE_AU')] au_columns = [col for col in df.columns if col.startswith('FACE_AU')]
eye_columns = [col for col in df.columns if col.startswith('EYE_')] eye_columns = [col for col in df.columns if col.startswith('EYE_')]
print(f" AU-Spalten: {len(au_columns)}") print(f" AU-columns: {len(au_columns)}")
print(f" Eye-Spalten: {len(eye_columns)}") print(f" Eye-columns: {len(eye_columns)}")
has_au = len(au_columns) > 0 has_au = len(au_columns) > 0
has_eye = len(eye_columns) > 0 has_eye = len(eye_columns) > 0
if not has_au and not has_eye: if not has_au and not has_eye:
print(f" WARNUNG: Keine AU oder Eye Spalten gefunden!") print(f" Warning: No AU or eye tracking columns found!")
continue continue
# Gruppiere nach STUDY, LEVEL, PHASE # Group by STUDY, LEVEL, PHASE
group_cols = [col for col in ['STUDY', 'LEVEL', 'PHASE'] if col in df.columns] group_cols = [col for col in ['STUDY', 'LEVEL', 'PHASE'] if col in df.columns]
if group_cols: if group_cols:
@@ -258,7 +254,7 @@ def process_combined_features(input_dir, output_file, window_size, step_size, fs
group_df = group_df.reset_index(drop=True) group_df = group_df.reset_index(drop=True)
# Berechne Anzahl Windows # calculate number of windows
num_windows = (len(group_df) - window_size) // step_size + 1 num_windows = (len(group_df) - window_size) // step_size + 1
if num_windows <= 0: if num_windows <= 0:
@@ -272,7 +268,7 @@ def process_combined_features(input_dir, output_file, window_size, step_size, fs
window_df = group_df.iloc[start_idx:end_idx] window_df = group_df.iloc[start_idx:end_idx]
# Basis-Metadaten # basic metadata
result = { result = {
'subjectID': window_df['subjectID'].iloc[0], 'subjectID': window_df['subjectID'].iloc[0],
'start_time': window_df['rowID'].iloc[0], 'start_time': window_df['rowID'].iloc[0],
@@ -281,12 +277,12 @@ def process_combined_features(input_dir, output_file, window_size, step_size, fs
'PHASE': window_df['PHASE'].iloc[0] if 'PHASE' in window_df.columns else np.nan 'PHASE': window_df['PHASE'].iloc[0] if 'PHASE' in window_df.columns else np.nan
} }
# FACE AU Features # FACE AU features
if has_au: if has_au:
for au_col in au_columns: for au_col in au_columns:
result[f'{au_col}_mean'] = window_df[au_col].mean() result[f'{au_col}_mean'] = window_df[au_col].mean()
# Eye-Tracking Features # Eye-tracking features
if has_eye: if has_eye:
try: try:
# clean dataframe from all nan rows # clean dataframe from all nan rows
@@ -296,7 +292,7 @@ def process_combined_features(input_dir, output_file, window_size, step_size, fs
result.update(eye_features) result.update(eye_features)
except Exception as e: except Exception as e:
print(f" WARNUNG: Eye-Features fehlgeschlagen: {str(e)}") print(f" WARNUNG: Eye-Features fehlgeschlagen: {str(e)}")
# Füge NaN-Werte für Eye-Features hinzu # Add NaN-values for eye-features
result.update({ result.update({
"Fix_count_short_66_150": np.nan, "Fix_count_short_66_150": np.nan,
"Fix_count_medium_300_500": np.nan, "Fix_count_medium_300_500": np.nan,
@@ -325,7 +321,7 @@ def process_combined_features(input_dir, output_file, window_size, step_size, fs
traceback.print_exc() traceback.print_exc()
continue continue
# Kombiniere alle Windows # Combine all windows
if not all_windows: if not all_windows:
print("\nKEINE FEATURES EXTRAHIERT!") print("\nKEINE FEATURES EXTRAHIERT!")
return None return None
@@ -340,7 +336,7 @@ def process_combined_features(input_dir, output_file, window_size, step_size, fs
print(f"Spalten: {len(result_df.columns)}") print(f"Spalten: {len(result_df.columns)}")
print(f"Subjects: {result_df['subjectID'].nunique()}") print(f"Subjects: {result_df['subjectID'].nunique()}")
# Speichern # Save
output_path = Path(output_file) output_path = Path(output_file)
output_path.parent.mkdir(parents=True, exist_ok=True) output_path.parent.mkdir(parents=True, exist_ok=True)
result_df.to_parquet(output_file, index=False) result_df.to_parquet(output_file, index=False)
@@ -357,7 +353,7 @@ def process_combined_features(input_dir, output_file, window_size, step_size, fs
def main(): def main():
print("\n" + "="*70) print("\n" + "="*70)
print("KOMBINIERTE FEATURE-EXTRAKTION (AU + EYE)") print("Combined extraction (AU + EYE)")
print("="*70) print("="*70)
result = process_combined_features( result = process_combined_features(
@@ -370,16 +366,16 @@ def main():
) )
if result is not None: if result is not None:
print("\nErste 5 Zeilen:") print("\First 5 rows:")
print(result.head()) print(result.head())
print("\nSpalten-Übersicht:") print("\nColumns overview:")
print(result.dtypes) print(result.dtypes)
print("\nStatistik:") print("\Statistics:")
print(result.describe()) print(result.describe())
print("\n✓ FERTIG!\n") print("\nDone!\n")
if __name__ == "__main__": if __name__ == "__main__":
-113
View File
@@ -1,113 +0,0 @@
import pandas as pd
import numpy as np
from pathlib import Path
def process_parquet_files(input_dir, output_file, window_size=1250, step_size=125):
"""
Verarbeitet Parquet-Dateien mit Sliding Window Aggregation.
Parameters:
-----------
input_dir : str
Verzeichnis mit Parquet-Dateien
output_file : str
Pfad für die Ausgabe-Parquet-Datei
window_size : int
Größe des Sliding Windows (default: 3000)
step_size : int
Schrittweite in Einträgen (default: 250 = 10 Sekunden bei 25 Hz)
"""
input_path = Path(input_dir)
parquet_files = sorted(input_path.glob("*.parquet"))
if not parquet_files:
print(f"Keine Parquet-Dateien in {input_dir} gefunden!")
return
print(f"Gefundene Dateien: {len(parquet_files)}")
all_windows = []
for file_idx, parquet_file in enumerate(parquet_files):
print(f"\nVerarbeite Datei {file_idx + 1}/{len(parquet_files)}: {parquet_file.name}")
# Lade Parquet-Datei
df = pd.read_parquet(parquet_file)
print(f" Einträge: {len(df)}")
# Identifiziere AU-Spalten
au_columns = [col for col in df.columns if col.startswith('FACE_AU')]
print(f" AU-Spalten: {len(au_columns)}")
# Gruppiere nach STUDY, LEVEL, PHASE (um Übergänge zu vermeiden)
for (study_val, level_val, phase_val), level_df in df.groupby(['STUDY', 'LEVEL', 'PHASE'], sort=False):
print(f" STUDY {study_val}, LEVEL {level_val}, PHASE {phase_val}: {len(level_df)} Einträge")
# Reset index für korrekte Position-Berechnung
level_df = level_df.reset_index(drop=True)
# Sliding Window über dieses Level
num_windows = (len(level_df) - window_size) // step_size + 1
if num_windows <= 0:
print(f" Zu wenige Einträge für Window (benötigt {window_size})")
continue
for i in range(num_windows):
start_idx = i * step_size
end_idx = start_idx + window_size
window_df = level_df.iloc[start_idx:end_idx]
# Erstelle aggregiertes Ergebnis
result = {
'subjectID': window_df['subjectID'].iloc[0],
'start_time': window_df['rowID'].iloc[0], # rowID als start_time
'STUDY': window_df['STUDY'].iloc[0],
'LEVEL': window_df['LEVEL'].iloc[0],
'PHASE': window_df['PHASE'].iloc[0]
}
# Summiere alle AU-Spalten
for au_col in au_columns:
# result[f'{au_col}_sum'] = window_df[au_col].sum()
result[f'{au_col}_mean'] = window_df[au_col].mean()
all_windows.append(result)
print(f" Windows erstellt: {num_windows}")
# Erstelle finalen DataFrame
result_df = pd.DataFrame(all_windows)
print(f"\n{'='*60}")
print(f"Gesamt Windows erstellt: {len(result_df)}")
print(f"Spalten: {list(result_df.columns)}")
# Speichere Ergebnis
result_df.to_parquet(output_file, index=False)
print(f"\nErgebnis gespeichert in: {output_file}")
return result_df
# Beispiel-Verwendung
if __name__ == "__main__":
# Anpassen an deine Pfade
input_directory = Path(r"/home/jovyan/data-paulusjafahrsimulator-gpu/new_AU_parquet_files")
output_file = Path(r"/home/jovyan/data-paulusjafahrsimulator-gpu/new_AU_dataset_mean/AU_dataset_mean.parquet")
result = process_parquet_files(
input_dir=input_directory,
output_file=output_file,
window_size=1250,
step_size=125
)
# Zeige erste Zeilen
if result is not None:
print("\nErste 5 Zeilen des Ergebnisses:")
print(result.head())
@@ -1,56 +0,0 @@
from pathlib import Path
import pandas as pd
def main():
"""
USER CONFIGURATION
------------------
Specify input files and output directory here.
"""
# Input parquet files (single-modality datasets)
file_modality_1 = Path("/home/jovyan/data-paulusjafahrsimulator-gpu/new_datasets/AU_dataset_mean.parquet")
file_modality_2 = Path("/home/jovyan/data-paulusjafahrsimulator-gpu/new_datasets/new_eye_dataset.parquet")
# Output directory and file name
output_dir = Path("/home/jovyan/data-paulusjafahrsimulator-gpu/new_datasets/")
output_file = output_dir / "merged_dataset.parquet"
# Column names (adjust only if your schema differs)
subject_col = "subjectID"
time_col = "start_time"
# ------------------------------------------------------------------
# Load datasets
# ------------------------------------------------------------------
df1 = pd.read_parquet(file_modality_1)
df2 = pd.read_parquet(file_modality_2)
# ------------------------------------------------------------------
# Keep only subjects that appear in BOTH datasets
# ------------------------------------------------------------------
common_subjects = set(df1[subject_col]).intersection(df2[subject_col])
df1 = df1[df1[subject_col].isin(common_subjects)]
df2 = df2[df2[subject_col].isin(common_subjects)]
# ------------------------------------------------------------------
# Inner join on subject ID AND start_time
# ------------------------------------------------------------------
merged_df = pd.merge(
df1,
df2,
on=[subject_col, time_col],
how="inner",
)
# ------------------------------------------------------------------
# Save merged dataset
# ------------------------------------------------------------------
output_dir.mkdir(parents=True, exist_ok=True)
merged_df.to_parquet(output_file, index=False)
if __name__ == "__main__":
main()
@@ -1,6 +1,5 @@
# pip install pyocclient
import yaml import yaml
import owncloud import owncloud # pip install pyocclient
import pandas as pd import pandas as pd
import h5py import h5py
import os import os
@@ -26,7 +25,7 @@ for i in range(num_files):
# Download file from ownCloud # Download file from ownCloud
oc.get_file(file_name, local_tmp) oc.get_file(file_name, local_tmp)
print(f"{file_name} geoeffnet") print(f"Opened: {file_name}")
# Load into memory and extract needed columns # Load into memory and extract needed columns
# with h5py.File(local_tmp, "r") as f: # with h5py.File(local_tmp, "r") as f:
# # Adjust this path depending on actual dataset layout inside .h5py file # # Adjust this path depending on actual dataset layout inside .h5py file
@@ -35,14 +34,9 @@ for i in range(num_files):
with pd.HDFStore(local_tmp, mode="r") as store: with pd.HDFStore(local_tmp, mode="r") as store:
cols = store.select("SIGNALS", start=0, stop=1).columns # get column names cols = store.select("SIGNALS", start=0, stop=1).columns # get column names
# Step 2: Filter columns that start with "AU"
au_cols = [c for c in cols if c.startswith("AU")]
print(au_cols)
if len(au_cols)==0:
print(f"keine AU Signale in Subject {i}")
continue
# Step 3: Read only those columns (plus any others you want) # Step 3: Read only those columns (plus any others you want)
df = pd.read_hdf(local_tmp, key="SIGNALS", columns=["STUDY", "LEVEL", "PHASE"] + au_cols) df = pd.read_hdf(local_tmp, key="SIGNALS", columns=["STUDY", "LEVEL", "PHASE"] + cols)
print("load done") print("load done")
@@ -63,7 +57,7 @@ for i in range(num_files):
# Save to parquet # Save to parquet
os.makedirs("ParquetFiles", exist_ok=True) os.makedirs("ParquetFiles", exist_ok=True) # TODO: change for custom directory
out_name = f"ParquetFiles/cleaned_{i:04d}.parquet" out_name = f"ParquetFiles/cleaned_{i:04d}.parquet"
df.to_parquet(out_name, index=False) df.to_parquet(out_name, index=False)
-323
View File
@@ -1,323 +0,0 @@
import numpy as np
import pandas as pd
import h5py
import yaml
import os
from sklearn.preprocessing import MinMaxScaler
from scipy.signal import welch
from pygazeanalyser.detectors import fixation_detection, saccade_detection
##############################################################################
# 1. HELFERFUNKTIONEN
##############################################################################
def clean_eye_df(df):
"""
Entfernt alle Zeilen, die keine echten Eyetracking-Daten enthalten.
Löst das Problem, dass das Haupt-DataFrame NaN-Zeilen für andere Sensoren enthält.
"""
eye_cols = [c for c in df.columns if ("LEFT_" in c or "RIGHT_" in c)]
df_eye = df[eye_cols]
# INF → NaN
df_eye = df_eye.replace([np.inf, -np.inf], np.nan)
# Nur Zeilen behalten, wo es echte Eyetracking-Daten gibt
df_eye = df_eye.dropna(subset=eye_cols, how="all")
print("Eyetracking-Zeilen vorher:", len(df))
print("Eyetracking-Zeilen nachher:", len(df_eye))
#Index zurücksetzen
return df_eye.reset_index(drop=True)
def extract_gaze_signal(df):
"""
Extrahiert 2D-Gaze-Positionen auf dem Display,
maskiert ungültige Samples und interpoliert Lücken.
"""
print("→ extract_gaze_signal(): Eingabegröße:", df.shape)
# Gaze-Spalten
gx_L = df["LEFT_GAZE_POINT_ON_DISPLAY_AREA_X"].astype(float).copy()
gy_L = df["LEFT_GAZE_POINT_ON_DISPLAY_AREA_Y"].astype(float).copy()
gx_R = df["RIGHT_GAZE_POINT_ON_DISPLAY_AREA_X"].astype(float).copy()
gy_R = df["RIGHT_GAZE_POINT_ON_DISPLAY_AREA_Y"].astype(float).copy()
# Validity-Spalten (1 = gültig)
val_L = (df["LEFT_GAZE_POINT_VALIDITY"] == 1)
val_R = (df["RIGHT_GAZE_POINT_VALIDITY"] == 1)
# Inf ersetzen mit NaN (kommt bei Tobii bei Blinks vor)
gx_L.replace([np.inf, -np.inf], np.nan, inplace=True)
gy_L.replace([np.inf, -np.inf], np.nan, inplace=True)
gx_R.replace([np.inf, -np.inf], np.nan, inplace=True)
gy_R.replace([np.inf, -np.inf], np.nan, inplace=True)
# Ungültige Werte maskieren
gx_L[~val_L] = np.nan
gy_L[~val_L] = np.nan
gx_R[~val_R] = np.nan
gy_R[~val_R] = np.nan
# Mittelwert der beiden Augen pro Sample (nanmean ist robust)
gx = np.mean(np.column_stack([gx_L, gx_R]), axis=1)
gy = np.mean(np.column_stack([gy_L, gy_R]), axis=1)
# Interpolation (wichtig für PyGaze!)
gx = pd.Series(gx).interpolate(limit=50, limit_direction="both").bfill().ffill()
gy = pd.Series(gy).interpolate(limit=50, limit_direction="both").bfill().ffill()
# xscaler = MinMaxScaler()
# gxscale = xscaler.fit_transform(gx.values.reshape(-1, 1))
# yscaler = MinMaxScaler()
# gyscale = yscaler.fit_transform(gx.values.reshape(-1, 1))
#print("xmax ymax", gxscale.max(), gyscale.max())
#out = np.column_stack((gxscale, gyscale))
out = np.column_stack((gx, gy))
print("→ extract_gaze_signal(): Ausgabegröße:", out.shape)
return out
def extract_pupil(df):
"""Extrahiert Pupillengröße (beide Augen gemittelt)."""
pl = df["LEFT_PUPIL_DIAMETER"].replace([np.inf, -np.inf], np.nan)
pr = df["RIGHT_PUPIL_DIAMETER"].replace([np.inf, -np.inf], np.nan)
vl = df.get("LEFT_PUPIL_VALIDITY")
vr = df.get("RIGHT_PUPIL_VALIDITY")
if vl is None or vr is None:
# Falls Validity-Spalten nicht vorhanden sind, versuchen wir grobe Heuristik:
# gültig, wenn Pupillendurchmesser nicht NaN.
validity = (~pl.isna() | ~pr.isna()).astype(int).to_numpy()
else:
# Falls vorhanden: 1 wenn mindestens eines der Augen gültig ist
validity = ( (vl == 1) | (vr == 1) ).astype(int).to_numpy()
# Mittelwert der verfügbaren Pupillen
p = np.mean(np.column_stack([pl, pr]), axis=1)
# INF/NaN reparieren
p = pd.Series(p).interpolate(limit=50, limit_direction="both").bfill().ffill()
p = p.to_numpy()
print("→ extract_pupil(): Pupillensignal Länge:", len(p))
return p, validity
def detect_blinks(pupil_validity, min_duration=5):
"""Erkennt Blinks: Validity=0 → Blink."""
blinks = []
start = None
for i, v in enumerate(pupil_validity):
if v == 0 and start is None:
start = i
elif v == 1 and start is not None:
if i - start >= min_duration:
blinks.append([start, i])
start = None
return blinks
def compute_IPA(pupil, fs=250):
"""
IPA = Index of Pupillary Activity (nach Duchowski 2018).
Hochfrequenzanteile der Pupillenzeitreihe.
"""
f, Pxx = welch(pupil, fs=fs, nperseg=int(fs*2)) # 2 Sekunden Fenster
hf_band = (f >= 0.6) & (f <= 2.0)
ipa = np.sum(Pxx[hf_band])
return ipa
##############################################################################
# 2. FEATURE-EXTRAKTION (HAUPTFUNKTION)
##############################################################################
def extract_eye_features(df, window_length_sec=50, fs=250):
"""
df = Tobii DataFrame
window_length_sec = Fenstergröße (z.B. W=1s)
"""
print("→ extract_eye_features(): Starte Feature-Berechnung...")
print(" Fensterlänge W =", window_length_sec, "s")
W = int(window_length_sec * fs) # Window größe in Samples
# Gaze
gaze = extract_gaze_signal(df)
gx, gy = gaze[:, 0], gaze[:, 1]
print("Gültige Werte (gx):", np.sum(~np.isnan(gx)), "von", len(gx))
print("Range:", np.nanmin(gx), np.nanmax(gx))
print("Gültige Werte (gy):", np.sum(~np.isnan(gy)), "von", len(gy))
print("Range:", np.nanmin(gy), np.nanmax(gy))
# Pupille
pupil, pupil_validity = extract_pupil(df)
features = []
# Sliding windows
for start in range(0, len(df), W):
end = start + W
if end > len(df):
break #das letzte Fenster wird ignoriert
w_gaze = gaze[start:end]
w_pupil = pupil[start:end]
w_valid = pupil_validity[start:end]
# ----------------------------
# FIXATIONS (PyGaze)
# ----------------------------
time_ms = np.arange(W) * 1000.0 / fs
# print("gx im Fenster:", w_gaze[:,0][:20])
# print("gy im Fenster:", w_gaze[:,1][:20])
# print("gx diff:", np.mean(np.abs(np.diff(w_gaze[:,0]))))
# print("Werte X im Fenster:", w_gaze[:,0])
# print("Werte Y im Fenster:", w_gaze[:,1])
# print("X-Stats: min/max/diff", np.nanmin(w_gaze[:,0]), np.nanmax(w_gaze[:,0]), np.nanmean(np.abs(np.diff(w_gaze[:,0]))))
# print("Y-Stats: min/max/diff", np.nanmin(w_gaze[:,1]), np.nanmax(w_gaze[:,1]), np.nanmean(np.abs(np.diff(w_gaze[:,1]))))
print("time_ms:", time_ms)
fix, efix = fixation_detection(
x=w_gaze[:, 0], y=w_gaze[:, 1], time=time_ms,
missing=0.0, maxdist=0.003, mindur=10 # mindur=100ms
)
#print("Raw Fixation Output:", efix[0])
if start == 0:
print("DEBUG fix raw:", fix[:10])
# Robust fixations: PyGaze may return malformed entries
fixation_durations = []
for f in efix:
print("Efix:", f[2])
# start_t = f[1] # in ms
# end_t = f[2] # in ms
# duration = (end_t - start_t) / 1000.0 # in Sekunden
#duration = f[2] / 1000.0
if np.isfinite(f[2]) and f[2] > 0:
fixation_durations.append(f[2])
# Kategorien laut Paper
F_short = sum(66 <= d <= 150 for d in fixation_durations)
F_medium = sum(300 <= d <= 500 for d in fixation_durations)
F_long = sum(d >= 1000 for d in fixation_durations)
F_hundred = sum(d > 100 for d in fixation_durations)
F_Cancel = sum(66 < d for d in fixation_durations)
# ----------------------------
# SACCADES
# ----------------------------
sac, esac = saccade_detection(
x=w_gaze[:, 0], y=w_gaze[:, 1], time=time_ms, missing=0, minlen=12, maxvel=0.2, maxacc=1
)
sac_durations = [s[2] for s in esac]
sac_amplitudes = [((s[5]-s[3])**2 + (s[6]-s[4])**2)**0.5 for s in esac]
# ----------------------------
# BLINKS
# ----------------------------
blinks = detect_blinks(w_valid)
blink_durations = [(b[1] - b[0]) / fs for b in blinks]
# ----------------------------
# PUPIL
# ----------------------------
if np.all(np.isnan(w_pupil)):
mean_pupil = np.nan
ipa = np.nan
else:
mean_pupil = np.nanmean(w_pupil)
ipa = compute_IPA(w_pupil, fs=fs)
# ----------------------------
# FEATURE-TABELLE FÜLLEN
# ----------------------------
features.append({
"Fix_count_short_66_150": F_short,
"Fix_count_medium_300_500": F_medium,
"Fix_count_long_gt_1000": F_long,
"Fix_count_100": F_hundred,
"Fix_cancel": F_Cancel,
"Fix_mean_duration": np.mean(fixation_durations) if fixation_durations else 0,
"Fix_median_duration": np.median(fixation_durations) if fixation_durations else 0,
"Sac_count": len(sac),
"Sac_mean_amp": np.mean(sac_amplitudes) if sac_amplitudes else 0,
"Sac_mean_dur": np.mean(sac_durations) if sac_durations else 0,
"Sac_median_dur": np.median(sac_durations) if sac_durations else 0,
"Blink_count": len(blinks),
"Blink_mean_dur": np.mean(blink_durations) if blink_durations else 0,
"Blink_median_dur": np.median(blink_durations) if blink_durations else 0,
"Pupil_mean": mean_pupil,
"Pupil_IPA": ipa
})
result = pd.DataFrame(features)
print("→ extract_eye_features(): Fertig! Ergebnisgröße:", result.shape)
return result
##############################################################################
# 3. MAIN FUNKTION
##############################################################################
def main():
print("### STARTE FEATURE-EXTRAKTION ###")
print("Aktueller Arbeitsordner:", os.getcwd())
#df = pd.read_hdf("tmp22.h5", "SIGNALS", mode="r")
df = pd.read_parquet("cleaned_0001.parquet")
print("DataFrame geladen:", df.shape)
# Nur Eye-Tracking auswählen
#eye_cols = [c for c in df.columns if "EYE_" in c]
#df_eye = df[eye_cols]
#print("Eye-Tracking-Spalten:", len(eye_cols))
#print("→", eye_cols[:10], " ...")
print("Reinige Eyetracking-Daten ...")
df_eye = clean_eye_df(df)
# Feature Extraction
features = extract_eye_features(df_eye, window_length_sec=50, fs=250)
print("\n### FEATURE-MATRIX (HEAD) ###")
print(features.head())
print("\nSpeichere Output in features.csv ...")
features.to_csv("features4.csv", index=False)
print("FERTIG!")
if __name__ == "__main__":
main()
-441
View File
@@ -1,441 +0,0 @@
import numpy as np
import pandas as pd
import h5py
import yaml
import os
from pathlib import Path
from sklearn.preprocessing import MinMaxScaler
from scipy.signal import welch
from pygazeanalyser.detectors import fixation_detection, saccade_detection
##############################################################################
# KONFIGURATION - HIER ANPASSEN!
##############################################################################
INPUT_DIR = Path(r"/home/jovyan/data-paulusjafahrsimulator-gpu/new_ET_Parquet_files/")
OUTPUT_FILE = Path(r"/home/jovyan/data-paulusjafahrsimulator-gpu/Eye_dataset_old/new_eye_dataset.parquet")
WINDOW_SIZE_SAMPLES = 12500 # Anzahl Samples pro Window (z.B. 1250 = 50s bei 25Hz, oder 5s bei 250Hz)
STEP_SIZE_SAMPLES = 1250 # Schrittweite (z.B. 125 = 5s bei 25Hz, oder 0.5s bei 250Hz)
SAMPLING_RATE = 250 # Hz
##############################################################################
# 1. HELFERFUNKTIONEN
##############################################################################
def clean_eye_df(df):
"""
Entfernt alle Zeilen, die keine echten Eyetracking-Daten enthalten.
Löst das Problem, dass das Haupt-DataFrame NaN-Zeilen für andere Sensoren enthält.
"""
eye_cols = [c for c in df.columns if c.startswith("EYE_")]
df_eye = df[eye_cols]
# INF → NaN
df_eye = df_eye.replace([np.inf, -np.inf], np.nan)
# Nur Zeilen behalten, wo es echte Eyetracking-Daten gibt
df_eye = df_eye.dropna(subset=eye_cols, how="all")
print(f" Eyetracking-Zeilen: {len(df)} → {len(df_eye)}")
return df_eye.reset_index(drop=True)
def extract_gaze_signal(df):
"""
Extrahiert 2D-Gaze-Positionen auf dem Display,
maskiert ungültige Samples und interpoliert Lücken.
"""
# Gaze-Spalten
gx_L = df["EYE_LEFT_GAZE_POINT_ON_DISPLAY_AREA_X"].astype(float).copy()
gy_L = df["EYE_LEFT_GAZE_POINT_ON_DISPLAY_AREA_Y"].astype(float).copy()
gx_R = df["EYE_RIGHT_GAZE_POINT_ON_DISPLAY_AREA_X"].astype(float).copy()
gy_R = df["EYE_RIGHT_GAZE_POINT_ON_DISPLAY_AREA_Y"].astype(float).copy()
# Validity-Spalten (1 = gültig)
val_L = (df["EYE_LEFT_GAZE_POINT_VALIDITY"] == 1)
val_R = (df["EYE_RIGHT_GAZE_POINT_VALIDITY"] == 1)
# Inf ersetzen mit NaN (kommt bei Tobii bei Blinks vor)
gx_L.replace([np.inf, -np.inf], np.nan, inplace=True)
gy_L.replace([np.inf, -np.inf], np.nan, inplace=True)
gx_R.replace([np.inf, -np.inf], np.nan, inplace=True)
gy_R.replace([np.inf, -np.inf], np.nan, inplace=True)
# Ungültige Werte maskieren
gx_L[~val_L] = np.nan
gy_L[~val_L] = np.nan
gx_R[~val_R] = np.nan
gy_R[~val_R] = np.nan
# Mittelwert der beiden Augen pro Sample (nanmean ist robust)
gx = np.mean(np.column_stack([gx_L, gx_R]), axis=1)
gy = np.mean(np.column_stack([gy_L, gy_R]), axis=1)
# Interpolation (wichtig für PyGaze!)
gx = pd.Series(gx).interpolate(limit=50, limit_direction="both").bfill().ffill()
gy = pd.Series(gy).interpolate(limit=50, limit_direction="both").bfill().ffill()
xscaler = MinMaxScaler()
gxscale = xscaler.fit_transform(gx.values.reshape(-1, 1))
yscaler = MinMaxScaler()
gyscale = yscaler.fit_transform(gy.values.reshape(-1, 1))
out = np.column_stack((gxscale, gyscale))
return out
def extract_pupil(df):
"""Extrahiert Pupillengröße (beide Augen gemittelt)."""
pl = df["EYE_LEFT_PUPIL_DIAMETER"].replace([np.inf, -np.inf], np.nan)
pr = df["EYE_RIGHT_PUPIL_DIAMETER"].replace([np.inf, -np.inf], np.nan)
vl = df.get("EYE_LEFT_PUPIL_VALIDITY")
vr = df.get("EYE_RIGHT_PUPIL_VALIDITY")
if vl is None or vr is None:
validity = (~pl.isna() | ~pr.isna()).astype(int).to_numpy()
else:
validity = ((vl == 1) | (vr == 1)).astype(int).to_numpy()
# Mittelwert der verfügbaren Pupillen
p = np.mean(np.column_stack([pl, pr]), axis=1)
# INF/NaN reparieren
p = pd.Series(p).interpolate(limit=50, limit_direction="both").bfill().ffill()
p = p.to_numpy()
return p, validity
def detect_blinks(pupil_validity, min_duration=5):
"""Erkennt Blinks: Validity=0 → Blink."""
blinks = []
start = None
for i, v in enumerate(pupil_validity):
if v == 0 and start is None:
start = i
elif v == 1 and start is not None:
if i - start >= min_duration:
blinks.append([start, i])
start = None
return blinks
def compute_IPA(pupil, fs=250):
"""
IPA = Index of Pupillary Activity (nach Duchowski 2018).
Hochfrequenzanteile der Pupillenzeitreihe.
"""
f, Pxx = welch(pupil, fs=fs, nperseg=int(fs*2)) # 2 Sekunden Fenster
hf_band = (f >= 0.6) & (f <= 2.0)
ipa = np.sum(Pxx[hf_band])
return ipa
##############################################################################
# 2. FEATURE-EXTRAKTION MIT SLIDING WINDOW
##############################################################################
def extract_eye_features_sliding(df_eye, df_meta, window_size, step_size, fs=250):
"""
Extrahiert Features mit Sliding Window aus einem einzelnen Level/Phase.
Parameters:
-----------
df_eye : DataFrame
Eye-Tracking Daten (bereits gereinigt)
df_meta : DataFrame
Metadaten (subjectID, rowID, STUDY, LEVEL, PHASE)
window_size : int
Anzahl Samples pro Window
step_size : int
Schrittweite in Samples
fs : int
Sampling Rate in Hz
"""
# Gaze
gaze = extract_gaze_signal(df_eye)
# Pupille
pupil, pupil_validity = extract_pupil(df_eye)
features = []
num_windows = (len(df_eye) - window_size) // step_size + 1
if num_windows <= 0:
return pd.DataFrame()
for i in range(num_windows):
start_idx = i * step_size
end_idx = start_idx + window_size
w_gaze = gaze[start_idx:end_idx]
w_pupil = pupil[start_idx:end_idx]
w_valid = pupil_validity[start_idx:end_idx]
# Metadaten für dieses Window
meta_row = df_meta.iloc[start_idx]
# ----------------------------
# FIXATIONS (PyGaze)
# ----------------------------
time_ms = np.arange(window_size) * 1000.0 / fs
fix, efix = fixation_detection(
x=w_gaze[:, 0], y=w_gaze[:, 1], time=time_ms,
missing=0.0, maxdist=0.003, mindur=10
)
fixation_durations = []
for f in efix:
if np.isfinite(f[2]) and f[2] > 0:
fixation_durations.append(f[2])
# Kategorien laut Paper
F_short = sum(66 <= d <= 150 for d in fixation_durations)
F_medium = sum(300 <= d <= 500 for d in fixation_durations)
F_long = sum(d >= 1000 for d in fixation_durations)
F_hundred = sum(d > 100 for d in fixation_durations)
# F_Cancel = sum(66 < d for d in fixation_durations)
# ----------------------------
# SACCADES
# ----------------------------
sac, esac = saccade_detection(
x=w_gaze[:, 0], y=w_gaze[:, 1], time=time_ms,
missing=0, minlen=12, maxvel=0.2, maxacc=1
)
sac_durations = [s[2] for s in esac]
sac_amplitudes = [((s[5]-s[3])**2 + (s[6]-s[4])**2)**0.5 for s in esac]
# ----------------------------
# BLINKS
# ----------------------------
blinks = detect_blinks(w_valid)
blink_durations = [(b[1] - b[0]) / fs for b in blinks]
# ----------------------------
# PUPIL
# ----------------------------
if np.all(np.isnan(w_pupil)):
mean_pupil = np.nan
ipa = np.nan
else:
mean_pupil = np.nanmean(w_pupil)
ipa = compute_IPA(w_pupil, fs=fs)
# ----------------------------
# FEATURE-DICTIONARY
# ----------------------------
features.append({
# Metadaten
'subjectID': meta_row['subjectID'],
'start_time': meta_row['rowID'],
'STUDY': meta_row.get('STUDY', np.nan),
'LEVEL': meta_row.get('LEVEL', np.nan),
'PHASE': meta_row.get('PHASE', np.nan),
# Fixation Features
"Fix_count_short_66_150": F_short,
"Fix_count_medium_300_500": F_medium,
"Fix_count_long_gt_1000": F_long,
"Fix_count_100": F_hundred,
# "Fix_cancel": F_Cancel,
"Fix_mean_duration": np.mean(fixation_durations) if fixation_durations else 0,
"Fix_median_duration": np.median(fixation_durations) if fixation_durations else 0,
# Saccade Features
"Sac_count": len(sac),
"Sac_mean_amp": np.mean(sac_amplitudes) if sac_amplitudes else 0,
"Sac_mean_dur": np.mean(sac_durations) if sac_durations else 0,
"Sac_median_dur": np.median(sac_durations) if sac_durations else 0,
# Blink Features
"Blink_count": len(blinks),
"Blink_mean_dur": np.mean(blink_durations) if blink_durations else 0,
"Blink_median_dur": np.median(blink_durations) if blink_durations else 0,
# Pupil Features
"Pupil_mean": mean_pupil,
"Pupil_IPA": ipa
})
return pd.DataFrame(features)
##############################################################################
# 3. BATCH-VERARBEITUNG
##############################################################################
def process_parquet_directory(input_dir, output_file, window_size, step_size, fs=250):
"""
Verarbeitet alle Parquet-Dateien in einem Verzeichnis.
Parameters:
-----------
input_dir : str
Pfad zum Verzeichnis mit Parquet-Dateien
output_file : str
Pfad für die Ausgabe-Parquet-Datei
window_size : int
Window-Größe in Samples
step_size : int
Schrittweite in Samples
fs : int
Sampling Rate in Hz
"""
input_path = Path(input_dir)
parquet_files = sorted(input_path.glob("*.parquet"))
if not parquet_files:
print(f"FEHLER: Keine Parquet-Dateien in {input_dir} gefunden!")
return
print(f"\n{'='*70}")
print(f"STARTE BATCH-VERARBEITUNG")
print(f"{'='*70}")
print(f"Gefundene Dateien: {len(parquet_files)}")
print(f"Window Size: {window_size} Samples ({window_size/fs:.1f}s bei {fs}Hz)")
print(f"Step Size: {step_size} Samples ({step_size/fs:.1f}s bei {fs}Hz)")
print(f"{'='*70}\n")
all_features = []
for file_idx, parquet_file in enumerate(parquet_files, 1):
print(f"\n[{file_idx}/{len(parquet_files)}] Verarbeite: {parquet_file.name}")
try:
# Lade Parquet-Datei
df = pd.read_parquet(parquet_file)
print(f" Einträge geladen: {len(df)}")
# Prüfe ob benötigte Spalten vorhanden sind
required_cols = ['subjectID', 'rowID']
missing_cols = [col for col in required_cols if col not in df.columns]
if missing_cols:
print(f" WARNUNG: Fehlende Spalten: {missing_cols} - Überspringe Datei")
continue
# Reinige Eye-Tracking-Daten
df_eye = clean_eye_df(df)
if len(df_eye) == 0:
print(f" WARNUNG: Keine gültigen Eye-Tracking-Daten - Überspringe Datei")
continue
# Metadaten extrahieren (aligned mit df_eye)
meta_cols = ['subjectID', 'rowID']
if 'STUDY' in df.columns:
meta_cols.append('STUDY')
if 'LEVEL' in df.columns:
meta_cols.append('LEVEL')
if 'PHASE' in df.columns:
meta_cols.append('PHASE')
df_meta = df[meta_cols].iloc[df_eye.index].reset_index(drop=True)
# Gruppiere nach STUDY, LEVEL, PHASE (falls vorhanden)
group_cols = [col for col in ['STUDY', 'LEVEL', 'PHASE'] if col in df_meta.columns]
if group_cols:
print(f" Gruppiere nach: {', '.join(group_cols)}")
for group_vals, group_df in df_meta.groupby(group_cols, sort=False):
group_eye = df_eye.iloc[group_df.index].reset_index(drop=True)
group_meta = group_df.reset_index(drop=True)
print(f" Gruppe {group_vals}: {len(group_eye)} Samples", end=" → ")
features_df = extract_eye_features_sliding(
group_eye, group_meta, window_size, step_size, fs
)
if not features_df.empty:
all_features.append(features_df)
print(f"{len(features_df)} Windows")
else:
print("Zu wenige Daten")
else:
# Keine Gruppierung
print(f" Keine Gruppierungsspalten gefunden")
features_df = extract_eye_features_sliding(
df_eye, df_meta, window_size, step_size, fs
)
if not features_df.empty:
all_features.append(features_df)
print(f" → {len(features_df)} Windows erstellt")
else:
print(f" → Zu wenige Daten")
except Exception as e:
print(f" FEHLER bei Verarbeitung: {str(e)}")
import traceback
traceback.print_exc()
continue
# Kombiniere alle Features
if not all_features:
print("\nKEINE FEATURES EXTRAHIERT!")
return None
print(f"\n{'='*70}")
print(f"ZUSAMMENFASSUNG")
print(f"{'='*70}")
final_df = pd.concat(all_features, ignore_index=True)
print(f"Gesamt Windows: {len(final_df)}")
print(f"Spalten: {len(final_df.columns)}")
print(f"Subjects: {final_df['subjectID'].nunique()}")
# Speichere Ergebnis
output_path = Path(output_file)
output_path.parent.mkdir(parents=True, exist_ok=True)
final_df.to_parquet(output_file, index=False)
print(f"\n✓ Ergebnis gespeichert: {output_file}")
print(f"{'='*70}\n")
return final_df
##############################################################################
# 4. MAIN
##############################################################################
def main():
print("\n" + "="*70)
print("EYE-TRACKING FEATURE EXTRAKTION - BATCH MODE")
print("="*70)
result = process_parquet_directory(
input_dir=INPUT_DIR,
output_file=OUTPUT_FILE,
window_size=WINDOW_SIZE_SAMPLES,
step_size=STEP_SIZE_SAMPLES,
fs=SAMPLING_RATE
)
if result is not None:
print("\nErste 5 Zeilen des Ergebnisses:")
print(result.head())
print("\nSpalten-Übersicht:")
print(result.columns.tolist())
print("\nDatentypen:")
print(result.dtypes)
print("\n✓ FERTIG!\n")
if __name__ == "__main__":
main()
-323
View File
@@ -1,323 +0,0 @@
import numpy as np
import pandas as pd
import h5py
import yaml
import owncloud
import os
from sklearn.preprocessing import MinMaxScaler
from scipy.signal import welch
from pygazeanalyser.detectors import fixation_detection, saccade_detection
##############################################################################
# 1. HELFERFUNKTIONEN
##############################################################################
def clean_eye_df(df):
"""
Entfernt alle Zeilen, die keine echten Eyetracking-Daten enthalten.
Löst das Problem, dass das Haupt-DataFrame NaN-Zeilen für andere Sensoren enthält.
"""
eye_cols = [c for c in df.columns if "EYE_" in c]
df_eye = df[eye_cols]
# INF → NaN
df_eye = df_eye.replace([np.inf, -np.inf], np.nan)
# Nur Zeilen behalten, wo es echte Eyetracking-Daten gibt
df_eye = df_eye.dropna(subset=eye_cols, how="all")
print("Eyetracking-Zeilen vorher:", len(df))
print("Eyetracking-Zeilen nachher:", len(df_eye))
#Index zurücksetzen
return df_eye.reset_index(drop=True)
def extract_gaze_signal(df):
"""
Extrahiert 2D-Gaze-Positionen auf dem Display,
maskiert ungültige Samples und interpoliert Lücken.
"""
print("→ extract_gaze_signal(): Eingabegröße:", df.shape)
# Gaze-Spalten
gx_L = df["EYE_LEFT_GAZE_POINT_ON_DISPLAY_AREA_X"].astype(float).copy()
gy_L = df["EYE_LEFT_GAZE_POINT_ON_DISPLAY_AREA_Y"].astype(float).copy()
gx_R = df["EYE_RIGHT_GAZE_POINT_ON_DISPLAY_AREA_X"].astype(float).copy()
gy_R = df["EYE_RIGHT_GAZE_POINT_ON_DISPLAY_AREA_Y"].astype(float).copy()
# Validity-Spalten (1 = gültig)
val_L = (df["EYE_LEFT_GAZE_POINT_VALIDITY"] == 1)
val_R = (df["EYE_RIGHT_GAZE_POINT_VALIDITY"] == 1)
# Inf ersetzen mit NaN (kommt bei Tobii bei Blinks vor)
gx_L.replace([np.inf, -np.inf], np.nan, inplace=True)
gy_L.replace([np.inf, -np.inf], np.nan, inplace=True)
gx_R.replace([np.inf, -np.inf], np.nan, inplace=True)
gy_R.replace([np.inf, -np.inf], np.nan, inplace=True)
# Ungültige Werte maskieren
gx_L[~val_L] = np.nan
gy_L[~val_L] = np.nan
gx_R[~val_R] = np.nan
gy_R[~val_R] = np.nan
# Mittelwert der beiden Augen pro Sample (nanmean ist robust)
gx = np.mean(np.column_stack([gx_L, gx_R]), axis=1)
gy = np.mean(np.column_stack([gy_L, gy_R]), axis=1)
# Interpolation (wichtig für PyGaze!)
gx = pd.Series(gx).interpolate(limit=50, limit_direction="both").bfill().ffill()
gy = pd.Series(gy).interpolate(limit=50, limit_direction="both").bfill().ffill()
xscaler = MinMaxScaler()
gxscale = xscaler.fit_transform(gx.values.reshape(-1, 1))
yscaler = MinMaxScaler()
gyscale = yscaler.fit_transform(gx.values.reshape(-1, 1))
print("xmax ymax", gxscale.max(), gyscale.max())
out = np.column_stack((gxscale, gyscale))
print("→ extract_gaze_signal(): Ausgabegröße:", out.shape)
return out
def extract_pupil(df):
"""Extrahiert Pupillengröße (beide Augen gemittelt)."""
pl = df["EYE_LEFT_PUPIL_DIAMETER"].replace([np.inf, -np.inf], np.nan)
pr = df["EYE_RIGHT_PUPIL_DIAMETER"].replace([np.inf, -np.inf], np.nan)
vl = df.get("EYE_LEFT_PUPIL_VALIDITY")
vr = df.get("EYE_RIGHT_PUPIL_VALIDITY")
if vl is None or vr is None:
# Falls Validity-Spalten nicht vorhanden sind, versuchen wir grobe Heuristik:
# gültig, wenn Pupillendurchmesser nicht NaN.
validity = (~pl.isna() | ~pr.isna()).astype(int).to_numpy()
else:
# Falls vorhanden: 1 wenn mindestens eines der Augen gültig ist
validity = ( (vl == 1) | (vr == 1) ).astype(int).to_numpy()
# Mittelwert der verfügbaren Pupillen
p = np.mean(np.column_stack([pl, pr]), axis=1)
# INF/NaN reparieren
p = pd.Series(p).interpolate(limit=50, limit_direction="both").bfill().ffill()
p = p.to_numpy()
print("→ extract_pupil(): Pupillensignal Länge:", len(p))
return p, validity
def detect_blinks(pupil_validity, min_duration=5):
"""Erkennt Blinks: Validity=0 → Blink."""
blinks = []
start = None
for i, v in enumerate(pupil_validity):
if v == 0 and start is None:
start = i
elif v == 1 and start is not None:
if i - start >= min_duration:
blinks.append([start, i])
start = None
return blinks
def compute_IPA(pupil, fs=250):
"""
IPA = Index of Pupillary Activity (nach Duchowski 2018).
Hochfrequenzanteile der Pupillenzeitreihe.
"""
f, Pxx = welch(pupil, fs=fs, nperseg=int(fs*2)) # 2 Sekunden Fenster
hf_band = (f >= 0.6) & (f <= 2.0)
ipa = np.sum(Pxx[hf_band])
return ipa
##############################################################################
# 2. FEATURE-EXTRAKTION (HAUPTFUNKTION)
##############################################################################
def extract_eye_features(df, window_length_sec=50, fs=250):
"""
df = Tobii DataFrame
window_length_sec = Fenstergröße (z.B. W=1s)
"""
print("→ extract_eye_features(): Starte Feature-Berechnung...")
print(" Fensterlänge W =", window_length_sec, "s")
W = int(window_length_sec * fs) # Window größe in Samples
# Gaze
gaze = extract_gaze_signal(df)
gx, gy = gaze[:, 0], gaze[:, 1]
print("Gültige Werte (gx):", np.sum(~np.isnan(gx)), "von", len(gx))
print("Range:", np.nanmin(gx), np.nanmax(gx))
print("Gültige Werte (gy):", np.sum(~np.isnan(gy)), "von", len(gy))
print("Range:", np.nanmin(gy), np.nanmax(gy))
# Pupille
pupil, pupil_validity = extract_pupil(df)
features = []
# Sliding windows
for start in range(0, len(df), W):
end = start + W
if end > len(df):
break #das letzte Fenster wird ignoriert
w_gaze = gaze[start:end]
w_pupil = pupil[start:end]
w_valid = pupil_validity[start:end]
# ----------------------------
# FIXATIONS (PyGaze)
# ----------------------------
time_ms = np.arange(W) * 1000.0 / fs
# print("gx im Fenster:", w_gaze[:,0][:20])
# print("gy im Fenster:", w_gaze[:,1][:20])
# print("gx diff:", np.mean(np.abs(np.diff(w_gaze[:,0]))))
# print("Werte X im Fenster:", w_gaze[:,0])
# print("Werte Y im Fenster:", w_gaze[:,1])
# print("X-Stats: min/max/diff", np.nanmin(w_gaze[:,0]), np.nanmax(w_gaze[:,0]), np.nanmean(np.abs(np.diff(w_gaze[:,0]))))
# print("Y-Stats: min/max/diff", np.nanmin(w_gaze[:,1]), np.nanmax(w_gaze[:,1]), np.nanmean(np.abs(np.diff(w_gaze[:,1]))))
print("time_ms:", time_ms)
fix, efix = fixation_detection(
x=w_gaze[:, 0], y=w_gaze[:, 1], time=time_ms,
missing=0.0, maxdist=0.001, mindur=65 # mindur=100ms
)
#print("Raw Fixation Output:", efix[0])
if start == 0:
print("DEBUG fix raw:", fix[:10])
# Robust fixations: PyGaze may return malformed entries
fixation_durations = []
for f in efix:
print("Efix:", f[2])
# start_t = f[1] # in ms
# end_t = f[2] # in ms
# duration = (end_t - start_t) / 1000.0 # in Sekunden
#duration = f[2] / 1000.0
if np.isfinite(f[2]) and f[2] > 0:
fixation_durations.append(f[2])
# Kategorien laut Paper
F_short = sum(66 <= d <= 150 for d in fixation_durations)
F_medium = sum(300 <= d <= 500 for d in fixation_durations)
F_long = sum(d >= 1000 for d in fixation_durations)
F_hundred = sum(d > 100 for d in fixation_durations)
F_Cancel = sum(66 < d for d in fixation_durations)
# ----------------------------
# SACCADES
# ----------------------------
sac, esac = saccade_detection(
x=w_gaze[:, 0], y=w_gaze[:, 1], time=time_ms, missing=0, minlen=12, maxvel=0.2, maxacc=1
)
sac_durations = [s[2] for s in esac]
sac_amplitudes = [((s[5]-s[3])**2 + (s[6]-s[4])**2)**0.5 for s in esac]
# ----------------------------
# BLINKS
# ----------------------------
blinks = detect_blinks(w_valid)
blink_durations = [(b[1] - b[0]) / fs for b in blinks]
# ----------------------------
# PUPIL
# ----------------------------
if np.all(np.isnan(w_pupil)):
mean_pupil = np.nan
ipa = np.nan
else:
mean_pupil = np.nanmean(w_pupil)
ipa = compute_IPA(w_pupil, fs=fs)
# ----------------------------
# FEATURE-TABELLE FÜLLEN
# ----------------------------
features.append({
"Fix_count_short_66_150": F_short,
"Fix_count_medium_300_500": F_medium,
"Fix_count_long_gt_1000": F_long,
"Fix_count_100": F_hundred,
"Fix_cancel": F_Cancel,
"Fix_mean_duration": np.mean(fixation_durations) if fixation_durations else 0,
"Fix_median_duration": np.median(fixation_durations) if fixation_durations else 0,
"Sac_count": len(sac),
"Sac_mean_amp": np.mean(sac_amplitudes) if sac_amplitudes else 0,
"Sac_mean_dur": np.mean(sac_durations) if sac_durations else 0,
"Sac_median_dur": np.median(sac_durations) if sac_durations else 0,
"Blink_count": len(blinks),
"Blink_mean_dur": np.mean(blink_durations) if blink_durations else 0,
"Blink_median_dur": np.median(blink_durations) if blink_durations else 0,
"Pupil_mean": mean_pupil,
"Pupil_IPA": ipa
})
result = pd.DataFrame(features)
print("→ extract_eye_features(): Fertig! Ergebnisgröße:", result.shape)
return result
##############################################################################
# 3. MAIN FUNKTION
##############################################################################
def main():
print("### STARTE FEATURE-EXTRAKTION ###")
print("Aktueller Arbeitsordner:", os.getcwd())
df = pd.read_hdf("tmp22.h5", "SIGNALS", mode="r")
#df = pd.read_parquet("cleaned_0001.parquet")
print("DataFrame geladen:", df.shape)
# Nur Eye-Tracking auswählen
#eye_cols = [c for c in df.columns if "EYE_" in c]
#df_eye = df[eye_cols]
#print("Eye-Tracking-Spalten:", len(eye_cols))
#print("→", eye_cols[:10], " ...")
print("Reinige Eyetracking-Daten ...")
df_eye = clean_eye_df(df)
# Feature Extraction
features = extract_eye_features(df_eye, window_length_sec=50, fs=250)
print("\n### FEATURE-MATRIX (HEAD) ###")
print(features.head())
print("\nSpeichere Output in features.csv ...")
features.to_csv("features2.csv", index=False)
print("FERTIG!")
if __name__ == "__main__":
main()
+36 -29
View File
@@ -1,72 +1,79 @@
import math import math
def fixation_radius_normalized(theta_deg: float,
def fixation_radius_normalized(
theta_deg: float,
distance_cm: float, distance_cm: float,
screen_width_cm: float, screen_width_cm: float,
screen_height_cm: float, screen_height_cm: float,
resolution_x: int, resolution_x: int,
resolution_y: int, resolution_y: int,
method: str = "max"): method: str = "max",
):
""" """
Berechnet den PyGaze-Fixationsradius für normierte Gaze-Daten in [0,1]. Compute the PyGaze fixation radius for normalized gaze data in [0, 1].
""" """
# Schritt 1: visueller Winkel → physische Distanz (cm) # Visual angle to physical distance (cm)
delta_cm = 2 * distance_cm * math.tan(math.radians(theta_deg) / 2) delta_cm = 2 * distance_cm * math.tan(math.radians(theta_deg) / 2)
# Schritt 2: physische Distanz → Pixel # Physical distance to pixels
delta_px_x = delta_cm * (resolution_x / screen_width_cm) delta_px_x = delta_cm * (resolution_x / screen_width_cm)
delta_px_y = delta_cm * (resolution_y / screen_height_cm) delta_px_y = delta_cm * (resolution_y / screen_height_cm)
# Pixelradius # Pixel radius
if method == "max": if method == "max":
r_px = max(delta_px_x, delta_px_y) r_px = max(delta_px_x, delta_px_y)
else: else:
r_px = math.sqrt(delta_px_x**2 + delta_px_y**2) r_px = math.sqrt(delta_px_x**2 + delta_px_y**2)
# Schritt 3: Pixelradius → normierter Radius # Pixel radius to normalized radius
r_norm_x = r_px / resolution_x r_norm_x = r_px / resolution_x
r_norm_y = r_px / resolution_y r_norm_y = r_px / resolution_y
if method == "max": if method == "max":
return max(r_norm_x, r_norm_y) return max(r_norm_x, r_norm_y)
else:
return math.sqrt(r_norm_x**2 + r_norm_y**2) return math.sqrt(r_norm_x**2 + r_norm_y**2)
def run_example():
# Example: 55" 4k monitor
screen_width_cm = 3 * 121.8
screen_height_cm = 68.5
resolution_x = 3 * 3840
resolution_y = 2160
distance_to_screen_cm = 120
max_angle = 1.0
maxdist_px = fixation_radius_normalized(
theta_deg=max_angle,
# Beispiel: 55" 4k Monitor
screen_width_cm = 3*121.8
screen_height_cm = 68.5
resolution_x = 3*3840
resolution_y = 2160
distance_to_screen_cm = 120
method = 'max'
max_angle= 1.0
maxdist_px = fixation_radius_normalized(theta_deg=max_angle,
distance_cm=distance_to_screen_cm, distance_cm=distance_to_screen_cm,
screen_width_cm=screen_width_cm, screen_width_cm=screen_width_cm,
screen_height_cm=screen_height_cm, screen_height_cm=screen_height_cm,
resolution_x=resolution_x, resolution_x=resolution_x,
resolution_y=resolution_y, resolution_y=resolution_y,
method=method) method="max",
)
print("PyGaze max_dist (max):", maxdist_px)
print("PyGaze max_dist (max):", maxdist_px) maxdist_px = fixation_radius_normalized(
theta_deg=max_angle,
method = 'euclid'
maxdist_px = fixation_radius_normalized(theta_deg=max_angle,
distance_cm=distance_to_screen_cm, distance_cm=distance_to_screen_cm,
screen_width_cm=screen_width_cm, screen_width_cm=screen_width_cm,
screen_height_cm=screen_height_cm, screen_height_cm=screen_height_cm,
resolution_x=resolution_x, resolution_x=resolution_x,
resolution_y=resolution_y, resolution_y=resolution_y,
method=method) method="euclid",
)
print("PyGaze max_dist (euclid):", maxdist_px)
print("PyGaze max_dist (euclid):", maxdist_px)
# Passt noch nicht zu der Breite def main():
run_example()
if __name__ == "__main__":
main()
# Reference
# https://osdoc.cogsci.nl/4.0/de/visualangle/ # https://osdoc.cogsci.nl/4.0/de/visualangle/
# https://reference.org/facts/Visual_angle/LUw29zy7 # https://reference.org/facts/Visual_angle/LUw29zy7
-155
View File
@@ -1,155 +0,0 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "2b3fface",
"metadata": {},
"outputs": [],
"source": [
"import pandas as pd"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "74f1f5ec",
"metadata": {},
"outputs": [],
"source": [
"df= pd.read_parquet(r\" \")\n",
"print(df.shape)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "05775454",
"metadata": {},
"outputs": [],
"source": [
"df.head()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "99e17328",
"metadata": {},
"outputs": [],
"source": [
"df.tail()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "69e53731",
"metadata": {},
"outputs": [],
"source": [
"df.info()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "3754c664",
"metadata": {},
"outputs": [],
"source": [
"# Zeigt alle Kombinationen mit Häufigkeit\n",
"df[['STUDY', 'PHASE', 'LEVEL']].value_counts(ascending=True)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "f83b595c",
"metadata": {},
"outputs": [],
"source": [
"high_nback = df[\n",
" (df[\"STUDY\"]==\"n-back\") &\n",
" (df[\"LEVEL\"].isin([2, 3, 5, 6])) &\n",
" (df[\"PHASE\"].isin([\"train\", \"test\"]))\n",
"]\n",
"high_nback.shape"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "c0940343",
"metadata": {},
"outputs": [],
"source": [
"low_all = df[\n",
" ((df[\"PHASE\"] == \"baseline\") |\n",
" ((df[\"STUDY\"] == \"n-back\") & (df[\"PHASE\"] != \"baseline\") & (df[\"LEVEL\"].isin([1,4]))))\n",
"]\n",
"print(low_all.shape)\n",
"high_kdrive = df[\n",
" (df[\"STUDY\"] == \"k-drive\") & (df[\"PHASE\"] != \"baseline\")\n",
"]\n",
"print(high_kdrive.shape)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "f7ce38d3",
"metadata": {},
"outputs": [],
"source": [
"print((df.shape[0]==(high_kdrive.shape[0]+high_nback.shape[0]+low_all.shape[0])))\n",
"print(df.shape[0])\n",
"print((high_kdrive.shape[0]+high_nback.shape[0]+low_all.shape[0]))"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "48ba0379",
"metadata": {},
"outputs": [],
"source": [
"high_all = pd.concat([high_nback, high_kdrive])\n",
"high_all.shape"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "77dda26c",
"metadata": {},
"outputs": [],
"source": [
"print(f\"Gesamt: {df.shape[0]}=={low_all.shape[0]+high_all.shape[0]}\")\n",
"print(f\"Anzahl an low load Samples: {low_all.shape[0]}\")\n",
"print(f\"Anzahl an high load Samples: {high_all.shape[0]}\")\n"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "base",
"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.11.5"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
@@ -1,8 +1,10 @@
import os import os
import pandas as pd import pandas as pd
from pathlib import Path from pathlib import Path
# TODO: Set paths correctly
data_dir = Path("") # path to the directory with all .h5 files
base_dir = Path(r"") # directory to store the parquet files in
data_dir = Path("/home/jovyan/data-paulusjafahrsimulator-gpu/raw_data")
# Get all .h5 files and sort them # Get all .h5 files and sort them
matching_files = sorted(data_dir.glob("*.h5")) matching_files = sorted(data_dir.glob("*.h5"))
@@ -11,8 +13,8 @@ matching_files = sorted(data_dir.glob("*.h5"))
CHUNK_SIZE = 50_000 CHUNK_SIZE = 50_000
for i, file_path in enumerate(matching_files): for i, file_path in enumerate(matching_files):
print(f"Subject {i} gestartet") print(f"Starting with subject {i}")
print(f"{file_path} geoeffnet") print(f"Opened: {file_path}")
# Step 1: Get total number of rows and column names # Step 1: Get total number of rows and column names
with pd.HDFStore(file_path, mode="r") as store: with pd.HDFStore(file_path, mode="r") as store:
@@ -81,7 +83,7 @@ for i, file_path in enumerate(matching_files):
print(f"Final dataframe shape: {df_final.shape}") print(f"Final dataframe shape: {df_final.shape}")
# Save to parquet # Save to parquet
base_dir = Path(r"/home/jovyan/data-paulusjafahrsimulator-gpu/both_mod_parquet_files")
os.makedirs(base_dir, exist_ok=True) os.makedirs(base_dir, exist_ok=True)
out_name = base_dir / f"both_mod_{i:04d}.parquet" out_name = base_dir / f"both_mod_{i:04d}.parquet"
+61 -282
View File
@@ -41,7 +41,6 @@
"import time\n", "import time\n",
"base_dir = os.path.abspath(os.path.join(os.getcwd(), \"..\"))\n", "base_dir = os.path.abspath(os.path.join(os.getcwd(), \"..\"))\n",
"sys.path.append(base_dir)\n", "sys.path.append(base_dir)\n",
"print(base_dir)\n",
"\n", "\n",
"from Fahrsimulator_MSY2526_AI.model_training.tools import evaluation_tools, scaler, mad_outlier_removal, performance_split\n", "from Fahrsimulator_MSY2526_AI.model_training.tools import evaluation_tools, scaler, mad_outlier_removal, performance_split\n",
"from sklearn.preprocessing import StandardScaler, MinMaxScaler\n", "from sklearn.preprocessing import StandardScaler, MinMaxScaler\n",
@@ -51,7 +50,7 @@
"import tensorflow as tf\n", "import tensorflow as tf\n",
"from tensorflow.keras import layers, models, regularizers\n", "from tensorflow.keras import layers, models, regularizers\n",
"import pickle\n", "import pickle\n",
"from sklearn.metrics import (roc_auc_score, accuracy_score, precision_score, recall_score, f1_score, confusion_matrix, classification_report, balanced_accuracy_score, ConfusionMatrixDisplay, auc, roc_curve) " "from sklearn.metrics import (accuracy_score, auc, roc_curve, f1_score) "
] ]
}, },
{ {
@@ -89,15 +88,40 @@
"id": "f00a477c", "id": "f00a477c",
"metadata": {}, "metadata": {},
"source": [ "source": [
"### Data Preprocessing" "### Configuration of paths and data preprocessing"
] ]
}, },
{ {
"cell_type": "markdown", "cell_type": "code",
"id": "504c1df7", "execution_count": null,
"id": "5136fcec",
"metadata": {}, "metadata": {},
"outputs": [],
"source": [ "source": [
"Laden der Daten" "# TODO: set path where to save normalizer\n",
"normalizer_path=Path('.pkl') # TODO: set manually"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "c2115f65",
"metadata": {},
"outputs": [],
"source": [
"performance_path = Path(r\".csv\") # TODO: set manually\n",
"performance_df = pd.read_csv(performance_path)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "559eb8d2",
"metadata": {},
"outputs": [],
"source": [
"encoder_save_path = Path('.keras') # TODO: set manually\n",
"deep_svdd_save_path = Path('.keras') # TODO: set manually"
] ]
}, },
{ {
@@ -107,8 +131,7 @@
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"dataset_path = Path(r\"data-paulusjafahrsimulator-gpu/new_datasets/combined_dataset_25hz.parquet\")\n", "dataset_path = Path(r\".parquet\") # TODO: set manually"
"# dataset_path = Path(r\"/home/jovyan/data-paulusjafahrsimulator-gpu/new_datasets/120s_combined_dataset_25hz.parquet\")"
] ]
}, },
{ {
@@ -121,17 +144,6 @@
"df = pd.read_parquet(path=dataset_path)" "df = pd.read_parquet(path=dataset_path)"
] ]
}, },
{
"cell_type": "code",
"execution_count": null,
"id": "c2115f65",
"metadata": {},
"outputs": [],
"source": [
"performance_path = Path(r\"/home/jovyan/data-paulusjafahrsimulator-gpu/subject_performance/3new_au_performance.csv\")\n",
"performance_df = pd.read_csv(performance_path)"
]
},
{ {
"cell_type": "markdown", "cell_type": "markdown",
"id": "c045c46d", "id": "c045c46d",
@@ -215,7 +227,7 @@
"\n", "\n",
"data = pd.concat([low, high], ignore_index=True)\n", "data = pd.concat([low, high], ignore_index=True)\n",
"df = data.drop_duplicates()\n", "df = data.drop_duplicates()\n",
"\n", "df = df.dropna()\n",
"print(\"Label distribution:\")\n", "print(\"Label distribution:\")\n",
"print(df[\"label\"].value_counts())" "print(df[\"label\"].value_counts())"
] ]
@@ -275,210 +287,6 @@
"Normalization" "Normalization"
] ]
}, },
{
"cell_type": "code",
"execution_count": null,
"id": "acec4a03",
"metadata": {},
"outputs": [],
"source": [
"def fit_normalizer(train_data, au_columns, method='standard', scope='global'):\n",
" \"\"\"\n",
" Fit normalization scalers on training data.\n",
" \n",
" Parameters:\n",
" -----------\n",
" train_data : pd.DataFrame\n",
" Training dataframe with AU columns and subjectID\n",
" au_columns : list\n",
" List of AU column names to normalize\n",
" method : str, default='standard'\n",
" Normalization method: 'standard' for StandardScaler or 'minmax' for MinMaxScaler\n",
" scope : str, default='global'\n",
" Normalization scope: 'subject' for per-subject or 'global' for across all subjects\n",
" \n",
" Returns:\n",
" --------\n",
" dict\n",
" Dictionary containing fitted scalers and statistics for new subjects\n",
" \"\"\"\n",
" if method == 'standard':\n",
" Scaler = StandardScaler\n",
" elif method == 'minmax':\n",
" Scaler = MinMaxScaler\n",
" else:\n",
" raise ValueError(\"method must be 'standard' or 'minmax'\")\n",
" \n",
" scalers = {}\n",
" if scope == 'subject':\n",
" # Fit one scaler per subject\n",
" subject_stats = []\n",
" \n",
" for subject in train_data['subjectID'].unique():\n",
" subject_mask = train_data['subjectID'] == subject\n",
" scaler = Scaler()\n",
" scaler.fit(train_data.loc[subject_mask, au_columns].values)\n",
" scalers[subject] = scaler\n",
" \n",
" # Store statistics for averaging\n",
" if method == 'standard':\n",
" subject_stats.append({\n",
" 'mean': scaler.mean_,\n",
" 'std': scaler.scale_\n",
" })\n",
" elif method == 'minmax':\n",
" subject_stats.append({\n",
" 'min': scaler.data_min_,\n",
" 'max': scaler.data_max_\n",
" })\n",
" \n",
" # Calculate average statistics for new subjects\n",
" if method == 'standard':\n",
" avg_mean = np.mean([s['mean'] for s in subject_stats], axis=0)\n",
" avg_std = np.mean([s['std'] for s in subject_stats], axis=0)\n",
" fallback_scaler = StandardScaler()\n",
" fallback_scaler.mean_ = avg_mean\n",
" fallback_scaler.scale_ = avg_std\n",
" fallback_scaler.var_ = avg_std ** 2\n",
" fallback_scaler.n_features_in_ = len(au_columns)\n",
" elif method == 'minmax':\n",
" avg_min = np.mean([s['min'] for s in subject_stats], axis=0)\n",
" avg_max = np.mean([s['max'] for s in subject_stats], axis=0)\n",
" fallback_scaler = MinMaxScaler()\n",
" fallback_scaler.data_min_ = avg_min\n",
" fallback_scaler.data_max_ = avg_max\n",
" fallback_scaler.data_range_ = avg_max - avg_min\n",
" fallback_scaler.scale_ = 1.0 / fallback_scaler.data_range_\n",
" fallback_scaler.min_ = -avg_min * fallback_scaler.scale_\n",
" fallback_scaler.n_features_in_ = len(au_columns)\n",
" \n",
" scalers['_fallback'] = fallback_scaler\n",
" \n",
" elif scope == 'global':\n",
" # Fit one scaler for all subjects\n",
" scaler = Scaler()\n",
" scaler.fit(train_data[au_columns].values)\n",
" scalers['global'] = scaler\n",
" \n",
" else:\n",
" raise ValueError(\"scope must be 'subject' or 'global'\")\n",
" \n",
" return {'scalers': scalers, 'method': method, 'scope': scope}\n",
"\n",
"def apply_normalizer(data, columns, normalizer_dict):\n",
" \"\"\"\n",
" Apply fitted normalization scalers to data.\n",
" \n",
" Parameters:\n",
" -----------\n",
" data : pd.DataFrame\n",
" Dataframe with AU columns and subjectID\n",
" au_columns : list\n",
" List of AU column names to normalize\n",
" normalizer_dict : dict\n",
" Dictionary containing fitted scalers from fit_normalizer()\n",
" \n",
" Returns:\n",
" --------\n",
" pd.DataFrame\n",
" DataFrame with normalized AU columns\n",
" \"\"\"\n",
" normalized_data = data.copy()\n",
" scalers = normalizer_dict['scalers']\n",
" scope = normalizer_dict['scope']\n",
" normalized_data[columns] = normalized_data[columns].astype(np.float64)\n",
"\n",
" if scope == 'subject':\n",
" # Apply per-subject normalization\n",
" for subject in data['subjectID'].unique():\n",
" subject_mask = data['subjectID'] == subject\n",
" \n",
" # Use the subject's scaler if available, otherwise use fallback\n",
" if subject in scalers:\n",
" scaler = scalers[subject]\n",
" else:\n",
" # Use averaged scaler for new subjects\n",
" scaler = scalers['_fallback']\n",
" print(f\"Info: Subject {subject} not in training data. Using averaged scaler from training subjects.\")\n",
" \n",
" normalized_data.loc[subject_mask, columns] = scaler.transform(\n",
" data.loc[subject_mask, columns].values\n",
" )\n",
" \n",
" elif scope == 'global':\n",
" # Apply global normalization\n",
" scaler = scalers['global']\n",
" normalized_data[columns] = scaler.transform(data[columns].values)\n",
" \n",
" return normalized_data\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "53c6ee6f",
"metadata": {},
"outputs": [],
"source": [
"def save_normalizer(normalizer_dict, filepath):\n",
" \"\"\"\n",
" Save fitted normalizer to disk.\n",
"\n",
" Parameters:\n",
" -----------\n",
" normalizer_dict : dict\n",
" Dictionary containing fitted scalers from fit_normalizer()\n",
" filepath : str\n",
" Path to save the normalizer (e.g., 'normalizer.pkl')\n",
" \"\"\"\n",
" # Create directory if it does not exist\n",
" dirpath = os.path.dirname(filepath)\n",
" if dirpath:\n",
" os.makedirs(dirpath, exist_ok=True)\n",
"\n",
" with open(filepath, 'wb') as f:\n",
" pickle.dump(normalizer_dict, f)\n",
"\n",
" print(f\"Normalizer saved to {filepath}\")\n",
"\n",
"def load_normalizer(filepath):\n",
" \"\"\"\n",
" Load fitted normalizer from disk.\n",
" \n",
" Parameters:\n",
" -----------\n",
" filepath : str\n",
" Path to the saved normalizer file\n",
" \n",
" Returns:\n",
" --------\n",
" dict\n",
" Dictionary containing fitted scalers\n",
" \"\"\"\n",
" with open(filepath, 'rb') as f:\n",
" normalizer_dict = pickle.load(f)\n",
" print(f\"Normalizer loaded from {filepath}\")\n",
" return normalizer_dict"
]
},
{
"cell_type": "markdown",
"id": "7280f64f",
"metadata": {},
"source": [
"save Normalizer"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "8420afc2",
"metadata": {},
"outputs": [],
"source": [
"normalizer_path=Path('data-paulusjafahrsimulator-gpu/saved_models/deepsvdd_save/normalizer_min_max_global.pkl')"
]
},
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": null,
@@ -495,8 +303,10 @@
"print(len(eye_cols))\n", "print(len(eye_cols))\n",
"all_signal_columns = face_au_cols+eye_cols\n", "all_signal_columns = face_au_cols+eye_cols\n",
"print(len(all_signal_columns))\n", "print(len(all_signal_columns))\n",
"normalizer = fit_normalizer(train_df, all_signal_columns, method='minmax', scope='global')\n", "\n",
"save_normalizer(normalizer, normalizer_path )" "# fit and save normalizer\n",
"normalizer = scaler.fit_normalizer(train_df, all_signal_columns, method='minmax', scope='global')\n",
"scaler.save_normalizer(normalizer, normalizer_path )"
] ]
}, },
{ {
@@ -506,11 +316,11 @@
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"normalizer = load_normalizer(normalizer_path)\n", "normalizer = scaler.load_normalizer(normalizer_path)\n",
"# 3. Apply normalization to all sets\n", "# Apply normalization to all sets\n",
"train_df_norm = apply_normalizer(train_df, all_signal_columns, normalizer)\n", "train_df_norm = scaler.apply_normalizer(train_df, all_signal_columns, normalizer)\n",
"val_df_norm = apply_normalizer(val_df, all_signal_columns, normalizer)\n", "val_df_norm = scaler.apply_normalizer(val_df, all_signal_columns, normalizer)\n",
"test_df_norm = apply_normalizer(test_df, all_signal_columns, normalizer)" "test_df_norm = scaler.apply_normalizer(test_df, all_signal_columns, normalizer)"
] ]
}, },
{ {
@@ -566,13 +376,13 @@
"def build_intermediate_fusion_autoencoder(\n", "def build_intermediate_fusion_autoencoder(\n",
" input_dim_mod1=15,\n", " input_dim_mod1=15,\n",
" input_dim_mod2=20,\n", " input_dim_mod2=20,\n",
" encoder_hidden_dim_mod1=12, # individuell\n", " encoder_hidden_dim_mod1=12, # TODO: set manually\n",
" encoder_hidden_dim_mod2=20, # individuell\n", " encoder_hidden_dim_mod2=20, # TODO: set manually\n",
" latent_dim=6, # Änderung: Bottleneck vergrößert für stabilere Repräsentation\n", " latent_dim=6, # TODO: set manually\n",
" dropout_rate=0.4, # Dropout in Hidden Layers\n", " dropout_rate=0.4, # TODO: set manually\n",
" neg_slope=0.1,\n", " neg_slope=0.1, # TODO: set manually\n",
" weight_decay=1e-4,\n", " weight_decay=1e-4, # TODO: set manually\n",
" decoder_hidden_dims=[16, 32] # Änderung: Decoder größer für bessere Rekonstruktion\n", " decoder_hidden_dims=[16, 32] # TODO: set manually\n",
"):\n", "):\n",
" \"\"\"\n", " \"\"\"\n",
" Verbesserter Intermediate-Fusion Autoencoder für Deep SVDD.\n", " Verbesserter Intermediate-Fusion Autoencoder für Deep SVDD.\n",
@@ -597,10 +407,10 @@
" kernel_regularizer=l2\n", " kernel_regularizer=l2\n",
" )(x1_in)\n", " )(x1_in)\n",
" e1 = act(e1)\n", " e1 = act(e1)\n",
" e1 = layers.Dropout(dropout_rate)(e1) # Dropout nur hier\n", " e1 = layers.Dropout(dropout_rate)(e1) \n",
"\n", "\n",
" e1 = layers.Dense(\n", " e1 = layers.Dense(\n",
" 16, # Änderung: Hidden Layer größer für stabilere Fusion\n", " 16, \n",
" use_bias=False,\n", " use_bias=False,\n",
" kernel_regularizer=l2\n", " kernel_regularizer=l2\n",
" )(e1)\n", " )(e1)\n",
@@ -613,20 +423,20 @@
" kernel_regularizer=l2\n", " kernel_regularizer=l2\n",
" )(x2_in)\n", " )(x2_in)\n",
" e2 = act(e2)\n", " e2 = act(e2)\n",
" e2 = layers.Dropout(dropout_rate)(e2) # Dropout nur hier\n", " e2 = layers.Dropout(dropout_rate)(e2) \n",
"\n", "\n",
" e2 = layers.Dense(\n", " e2 = layers.Dense(\n",
" 16, # Änderung: Hidden Layer größer\n", " 16, \n",
" use_bias=False,\n", " use_bias=False,\n",
" kernel_regularizer=l2\n", " kernel_regularizer=l2\n",
" )(e2)\n", " )(e2)\n",
" e2 = act(e2)\n", " e2 = act(e2)\n",
"\n", "\n",
" # -------- Intermediate Fusion --------\n", " # -------- Intermediate Fusion --------\n",
" fused = layers.Concatenate(name=\"fusion\")([e1, e2]) # 16+16=32 Dimensionen\n", " fused = layers.Concatenate(name=\"fusion\")([e1, e2]) # 16+16=32 dimensions\n",
"\n", "\n",
" # -------- Joint Encoder / Bottleneck --------\n", " # -------- Joint Encoder / Bottleneck --------\n",
" # sinnvoll kleiner als Fusion\n", "\n",
" h = layers.Dense(\n", " h = layers.Dense(\n",
" latent_dim,\n", " latent_dim,\n",
" use_bias=False,\n", " use_bias=False,\n",
@@ -637,16 +447,16 @@
"\n", "\n",
" z = layers.Dense(\n", " z = layers.Dense(\n",
" latent_dim,\n", " latent_dim,\n",
" activation=None, # linear, für Deep SVDD\n", " activation=None, # linear for Deep SVDD\n",
" use_bias=False,\n", " use_bias=False,\n",
" kernel_regularizer=l2,\n", " kernel_regularizer=l2,\n",
" name=\"latent\"\n", " name=\"latent\"\n",
" )(h)\n", " )(h)\n",
" # Dropout entfernt direkt vor Bottleneck\n", "\n",
"\n", "\n",
" # -------- Decoder --------\n", " # -------- Decoder --------\n",
" d = layers.Dense(\n", " d = layers.Dense(\n",
" decoder_hidden_dims[0], # größerer Decoder\n", " decoder_hidden_dims[0], \n",
" use_bias=False,\n", " use_bias=False,\n",
" kernel_regularizer=l2\n", " kernel_regularizer=l2\n",
" )(z)\n", " )(z)\n",
@@ -692,10 +502,10 @@
"model = build_intermediate_fusion_autoencoder(\n", "model = build_intermediate_fusion_autoencoder(\n",
" input_dim_mod1=len(face_au_cols),\n", " input_dim_mod1=len(face_au_cols),\n",
" input_dim_mod2=len(eye_cols),\n", " input_dim_mod2=len(eye_cols),\n",
" encoder_hidden_dim_mod1=12, # individuell\n", " encoder_hidden_dim_mod1=12, # TODO: set manually\n",
" encoder_hidden_dim_mod2=8, # individuell\n", " encoder_hidden_dim_mod2=8, # TODO: set manually\n",
" latent_dim=4,\n", " latent_dim=4,\n",
" dropout_rate=0.7, # einstellbar\n", " dropout_rate=0.7, # TODO: set manually\n",
" neg_slope=0.1,\n", " neg_slope=0.1,\n",
" weight_decay=1e-3\n", " weight_decay=1e-3\n",
")\n", ")\n",
@@ -780,7 +590,6 @@
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"encoder_save_path =Path('data-paulusjafahrsimulator-gpu/saved_models/deepsvdd_save/encoder_8_deep.keras')\n",
"encoder.save(encoder_save_path)" "encoder.save(encoder_save_path)"
] ]
}, },
@@ -876,22 +685,6 @@
"center = get_center(deep_svdd_net, [X_face, X_eye])" "center = get_center(deep_svdd_net, [X_face, X_eye])"
] ]
}, },
{
"cell_type": "code",
"execution_count": null,
"id": "da140072",
"metadata": {},
"outputs": [],
"source": [
"# def get_radius(nu, dataset):\n",
"# x_face, x_eye = dataset # <-- zwingend entpacken\n",
"\n",
"# dataset_tuple=[x_face, x_eye]\n",
"\n",
"# dists = dist_per_sample(deep_svdd_net.predict(dataset_tuple), center)\n",
"# return np.quantile(np.sqrt(dists), 1-nu).astype(np.float32)"
]
},
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": null,
@@ -944,10 +737,9 @@
" return get_radius_from_arrays(nu, X_face, X_eye)\n", " return get_radius_from_arrays(nu, X_face, X_eye)\n",
"\n", "\n",
"\n", "\n",
"nu = 0.25\n", "nu = 0.05 # Set nu respectively\n",
"\n", "\n",
"train_dataset = tf.data.Dataset.from_tensor_slices((X_face, X_eye)).shuffle(64).batch(64)\n", "train_dataset = tf.data.Dataset.from_tensor_slices((X_face, X_eye)).shuffle(64).batch(64)\n",
"# train_dataset = tf.data.Dataset.from_tensor_slices((X_face, X_eye))\n",
"\n", "\n",
"optimizer = tf.keras.optimizers.Adam(1e-3)\n", "optimizer = tf.keras.optimizers.Adam(1e-3)\n",
"train(train_dataset, epochs=150, nu=nu)\n", "train(train_dataset, epochs=150, nu=nu)\n",
@@ -1019,7 +811,6 @@
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"deep_svdd_save_path =Path('data-paulusjafahrsimulator-gpu/saved_models/deepsvdd_save/deep_svdd_06.keras')\n",
"deep_svdd_net.save(deep_svdd_save_path)" "deep_svdd_net.save(deep_svdd_save_path)"
] ]
}, },
@@ -1104,18 +895,6 @@
"display_name": "Python 3 (ipykernel)", "display_name": "Python 3 (ipykernel)",
"language": "python", "language": "python",
"name": "python3" "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.12.10"
} }
}, },
"nbformat": 4, "nbformat": 4,
@@ -28,7 +28,7 @@
"sys.path.append(base_dir)\n", "sys.path.append(base_dir)\n",
"print(base_dir)\n", "print(base_dir)\n",
"\n", "\n",
"from tools import evaluation_tools\n", "from Fahrsimulator_MSY2526_AI.model_training.tools import evaluation_tools, scaler\n",
"from sklearn.preprocessing import StandardScaler, MinMaxScaler\n", "from sklearn.preprocessing import StandardScaler, MinMaxScaler\n",
"from sklearn.ensemble import IsolationForest\n", "from sklearn.ensemble import IsolationForest\n",
"from sklearn.model_selection import GridSearchCV, KFold\n", "from sklearn.model_selection import GridSearchCV, KFold\n",
@@ -52,7 +52,7 @@
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"data_path = Path(r\"C:\\Users\\micha\\FAUbox\\WS2526_Fahrsimulator_MSY (Celina Korzer)\\AU_dataset\\output_windowed.parquet\")" "data_path = Path(r\".parquet\") # TODO: set manually"
] ]
}, },
{ {
@@ -115,118 +115,6 @@
"print(f\"high all: {high_all.shape}\")" "print(f\"high all: {high_all.shape}\")"
] ]
}, },
{
"cell_type": "code",
"execution_count": null,
"id": "47a0f44d",
"metadata": {},
"outputs": [],
"source": [
"def fit_normalizer(train_data, au_columns, method='standard', scope='global'):\n",
" \"\"\"\n",
" Fit normalization scalers on training data.\n",
" \n",
" Parameters:\n",
" -----------\n",
" train_data : pd.DataFrame\n",
" Training dataframe with AU columns and subjectID\n",
" au_columns : list\n",
" List of AU column names to normalize\n",
" method : str, default='standard'\n",
" Normalization method: 'standard' for StandardScaler or 'minmax' for MinMaxScaler\n",
" scope : str, default='global'\n",
" Normalization scope: 'subject' for per-subject or 'global' for across all subjects\n",
" \n",
" Returns:\n",
" --------\n",
" dict\n",
" Dictionary containing fitted scalers\n",
" \"\"\"\n",
" # Select scaler based on method\n",
" if method == 'standard':\n",
" Scaler = StandardScaler\n",
" elif method == 'minmax':\n",
" Scaler = MinMaxScaler\n",
" else:\n",
" raise ValueError(\"method must be 'standard' or 'minmax'\")\n",
" \n",
" scalers = {}\n",
" \n",
" if scope == 'subject':\n",
" # Fit one scaler per subject\n",
" for subject in train_data['subjectID'].unique():\n",
" subject_mask = train_data['subjectID'] == subject\n",
" scaler = Scaler()\n",
" scaler.fit(train_data.loc[subject_mask, au_columns])\n",
" scalers[subject] = scaler\n",
" \n",
" elif scope == 'global':\n",
" # Fit one scaler for all subjects\n",
" scaler = Scaler()\n",
" scaler.fit(train_data[au_columns])\n",
" scalers['global'] = scaler\n",
" \n",
" else:\n",
" raise ValueError(\"scope must be 'subject' or 'global'\")\n",
" \n",
" return {'scalers': scalers, 'method': method, 'scope': scope}"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "642d0017",
"metadata": {},
"outputs": [],
"source": [
"def apply_normalizer(data, au_columns, normalizer_dict):\n",
" \"\"\"\n",
" Apply fitted normalization scalers to data.\n",
" \n",
" Parameters:\n",
" -----------\n",
" data : pd.DataFrame\n",
" Dataframe with AU columns and subjectID\n",
" au_columns : list\n",
" List of AU column names to normalize\n",
" normalizer_dict : dict\n",
" Dictionary containing fitted scalers from fit_normalizer()\n",
" \n",
" Returns:\n",
" --------\n",
" pd.DataFrame\n",
" DataFrame with normalized AU columns\n",
" \"\"\"\n",
" normalized_data = data.copy()\n",
" scalers = normalizer_dict['scalers']\n",
" scope = normalizer_dict['scope']\n",
" \n",
" if scope == 'subject':\n",
" # Apply per-subject normalization\n",
" for subject in data['subjectID'].unique():\n",
" subject_mask = data['subjectID'] == subject\n",
" \n",
" # Use the subject's scaler if available, otherwise use a fitted scaler from training\n",
" if subject in scalers:\n",
" scaler = scalers[subject]\n",
" else:\n",
" # For new subjects not seen in training, use the first available scaler\n",
" # (This is a fallback - ideally all test subjects should be in training for subject-level normalization)\n",
" print(f\"Warning: Subject {subject} not found in training data. Using fallback scaler.\")\n",
" scaler = list(scalers.values())[0]\n",
" \n",
" normalized_data.loc[subject_mask, au_columns] = scaler.transform(\n",
" data.loc[subject_mask, au_columns]\n",
" )\n",
" \n",
" elif scope == 'global':\n",
" # Apply global normalization\n",
" scaler = scalers['global']\n",
" normalized_data[au_columns] = scaler.transform(data[au_columns])\n",
" \n",
" return normalized_data"
]
},
{ {
"cell_type": "markdown", "cell_type": "markdown",
"id": "697b3cf7", "id": "697b3cf7",
@@ -301,20 +189,26 @@
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"# Cell 2: Get AU columns and prepare datasets\n", "au_columns = [col for col in low_all.columns if \"face\" in col.lower()] \n",
"# Get all column names that start with 'AU'\n",
"au_columns = [col for col in low_all.columns if col.startswith('AU')]\n",
"\n", "\n",
"eye_columns = [ \n",
" 'Fix_count_short_66_150','Fix_count_medium_300_500','Fix_count_long_gt_1000', \n",
" 'Fix_count_100','Fix_mean_duration','Fix_median_duration', \n",
" 'Sac_count','Sac_mean_amp','Sac_mean_dur','Sac_median_dur', \n",
" 'Blink_count','Blink_mean_dur','Blink_median_dur', \n",
" 'Pupil_mean','Pupil_IPA' \n",
"] \n",
"cols = au_columns +eye_columns\n",
"# Prepare training data (only normal/low data)\n", "# Prepare training data (only normal/low data)\n",
"train_data = low_all[low_all['subjectID'].isin(train_subjects)][['subjectID'] + au_columns].copy()\n", "train_data = low_all[low_all['subjectID'].isin(train_subjects)][['subjectID'] + cols].copy()\n",
"\n", "\n",
"# Prepare validation data (normal and anomaly)\n", "# Prepare validation data (normal and anomaly)\n",
"val_normal_data = low_all[low_all['subjectID'].isin(val_subjects)][['subjectID'] + au_columns].copy()\n", "val_normal_data = low_all[low_all['subjectID'].isin(val_subjects)][['subjectID'] + cols].copy()\n",
"val_high_data = high_all[high_all['subjectID'].isin(val_subjects)][['subjectID'] + au_columns].copy()\n", "val_high_data = high_all[high_all['subjectID'].isin(val_subjects)][['subjectID'] + cols].copy()\n",
"\n", "\n",
"# Prepare test data (normal and anomaly)\n", "# Prepare test data (normal and anomaly)\n",
"test_normal_data = low_all[low_all['subjectID'].isin(test_subjects)][['subjectID'] + au_columns].copy()\n", "test_normal_data = low_all[low_all['subjectID'].isin(test_subjects)][['subjectID'] + cols].copy()\n",
"test_high_data = high_all[high_all['subjectID'].isin(test_subjects)][['subjectID'] + au_columns].copy()\n", "test_high_data = high_all[high_all['subjectID'].isin(test_subjects)][['subjectID'] + cols].copy()\n",
"\n", "\n",
"print(f\"Train samples: {len(train_data)}\")\n", "print(f\"Train samples: {len(train_data)}\")\n",
"print(f\"Val normal samples: {len(val_normal_data)}, Val high samples: {len(val_high_data)}\")\n", "print(f\"Val normal samples: {len(val_normal_data)}, Val high samples: {len(val_high_data)}\")\n",
@@ -328,8 +222,8 @@
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"# Cell 3: Fit normalizer on training data\n", "# Fit normalizer on training data\n",
"normalizer = fit_normalizer(train_data, au_columns, method='minmax', scope='global')\n", "normalizer = scaler.fit_normalizer(train_data, cols, method='minmax', scope='global')\n",
"print(\"Normalizer fitted on training data\")" "print(\"Normalizer fitted on training data\")"
] ]
}, },
@@ -340,12 +234,12 @@
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"# Cell 4: Apply normalization to all datasets\n", "# Apply normalization to all datasets\n",
"train_normalized = apply_normalizer(train_data, au_columns, normalizer)\n", "train_normalized = scaler.apply_normalizer(train_data, cols, normalizer)\n",
"val_normal_normalized = apply_normalizer(val_normal_data, au_columns, normalizer)\n", "val_normal_normalized = scaler.apply_normalizer(val_normal_data, cols, normalizer)\n",
"val_high_normalized = apply_normalizer(val_high_data, au_columns, normalizer)\n", "val_high_normalized = scaler.apply_normalizer(val_high_data, cols, normalizer)\n",
"test_normal_normalized = apply_normalizer(test_normal_data, au_columns, normalizer)\n", "test_normal_normalized = scaler.apply_normalizer(test_normal_data, cols, normalizer)\n",
"test_high_normalized = apply_normalizer(test_high_data, au_columns, normalizer)\n", "test_high_normalized = scaler.apply_normalizer(test_high_data, cols, normalizer)\n",
"\n", "\n",
"print(\"Normalization applied to all datasets\")" "print(\"Normalization applied to all datasets\")"
] ]
@@ -357,11 +251,9 @@
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"# Cell 5: Extract AU columns and create labels for grid search\n", "X_train = train_normalized[cols].copy()\n",
"# Extract only AU columns (drop subjectID)\n", "X_val_normal = val_normal_normalized[cols].copy()\n",
"X_train = train_normalized[au_columns].copy()\n", "X_val_high = val_high_normalized[cols].copy()\n",
"X_val_normal = val_normal_normalized[au_columns].copy()\n",
"X_val_high = val_high_normalized[au_columns].copy()\n",
"\n", "\n",
"# Combine train and validation sets for grid search\n", "# Combine train and validation sets for grid search\n",
"X_grid_search = pd.concat([X_train, X_val_normal, X_val_high], ignore_index=True)\n", "X_grid_search = pd.concat([X_train, X_val_normal, X_val_high], ignore_index=True)\n",
@@ -416,7 +308,7 @@
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"# Cell 7: Train final model with best parameters on training data\n", "# Train final model with best parameters on training data\n",
"final_model = IsolationForest(**best_params, random_state=42)\n", "final_model = IsolationForest(**best_params, random_state=42)\n",
"final_model.fit(X_train.values)\n", "final_model.fit(X_train.values)\n",
"\n", "\n",
@@ -430,9 +322,9 @@
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"# Cell 8: Prepare independent test set\n", "# Prepare independent test set\n",
"X_test_normal = test_normal_normalized[au_columns].copy()\n", "X_test_normal = test_normal_normalized[cols].copy()\n",
"X_test_high = test_high_normalized[au_columns].copy()\n", "X_test_high = test_high_normalized[cols].copy()\n",
"\n", "\n",
"# Combine test sets\n", "# Combine test sets\n",
"X_test = pd.concat([X_test_normal, X_test_high], ignore_index=True)\n", "X_test = pd.concat([X_test_normal, X_test_high], ignore_index=True)\n",
@@ -483,21 +375,9 @@
], ],
"metadata": { "metadata": {
"kernelspec": { "kernelspec": {
"display_name": "base", "display_name": "Python 3 (ipykernel)",
"language": "python", "language": "python",
"name": "python3" "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.11.5"
} }
}, },
"nbformat": 4, "nbformat": 4,
+102 -280
View File
@@ -20,10 +20,10 @@
"from pathlib import Path\n", "from pathlib import Path\n",
"import sys\n", "import sys\n",
"import os\n", "import os\n",
"import tensorflow as tf\n",
"\n", "\n",
"base_dir = os.path.abspath(os.path.join(os.getcwd(), \"..\"))\n", "base_dir = os.path.abspath(os.path.join(os.getcwd(), \"..\"))\n",
"sys.path.append(base_dir)\n", "sys.path.append(base_dir)\n",
"print(base_dir)\n",
"\n", "\n",
"from sklearn.pipeline import Pipeline\n", "from sklearn.pipeline import Pipeline\n",
"from sklearn.svm import OneClassSVM\n", "from sklearn.svm import OneClassSVM\n",
@@ -31,7 +31,7 @@
"import matplotlib.pyplot as plt\n", "import matplotlib.pyplot as plt\n",
"import tensorflow as tf\n", "import tensorflow as tf\n",
"import pickle\n", "import pickle\n",
"from tools import evaluation_tools, scaler\n", "from Fahrsimulator_MSY2526_AI.model_training.tools import evaluation_tools, scaler\n",
"from sklearn.metrics import (balanced_accuracy_score, accuracy_score, precision_score, \n", "from sklearn.metrics import (balanced_accuracy_score, accuracy_score, precision_score, \n",
" recall_score, f1_score, confusion_matrix, classification_report) " " recall_score, f1_score, confusion_matrix, classification_report) "
] ]
@@ -44,6 +44,17 @@
"### Load data" "### Load data"
] ]
}, },
{
"cell_type": "code",
"execution_count": null,
"id": "30a4c885",
"metadata": {},
"outputs": [],
"source": [
"enconder_path = Path(\".keras\")\n",
"model_path = Path(\".pkl\")"
]
},
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": null,
@@ -51,7 +62,7 @@
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"data_path = Path(r\"/home/jovyan/data-paulusjafahrsimulator-gpu/first_AU_dataset/output_windowed.parquet\")" "data_path = Path(r\".parquet\")"
] ]
}, },
{ {
@@ -61,7 +72,8 @@
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"df = pd.read_parquet(path=data_path)" "df = pd.read_parquet(path=data_path)\n",
"df = df.dropna()"
] ]
}, },
{ {
@@ -150,20 +162,29 @@
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"au_columns = [col for col in low_all.columns if col.startswith('AU')]\n", "au_columns = [col for col in low_all.columns if \"face\" in col.lower()] \n",
"\n",
"eye_columns = [ \n",
" 'Fix_count_short_66_150','Fix_count_medium_300_500','Fix_count_long_gt_1000', \n",
" 'Fix_count_100','Fix_mean_duration','Fix_median_duration', \n",
" 'Sac_count','Sac_mean_amp','Sac_mean_dur','Sac_median_dur', \n",
" 'Blink_count','Blink_mean_dur','Blink_median_dur', \n",
" 'Pupil_mean','Pupil_IPA' \n",
"] \n",
"cols = au_columns +eye_columns\n",
"\n", "\n",
"# Prepare training data (only normal/low data)\n", "# Prepare training data (only normal/low data)\n",
"train_data = low_all[low_all['subjectID'].isin(train_subjects)][['subjectID'] + au_columns].copy()\n", "train_data = low_all[low_all['subjectID'].isin(train_subjects)][['subjectID'] + cols].copy()\n",
"\n", "\n",
"# Prepare validation data (normal and anomaly) \n", "# Prepare validation data (normal and anomaly) \n",
"val_normal_data = low_all[low_all['subjectID'].isin(val_subjects)][['subjectID'] + au_columns].copy()\n", "val_normal_data = low_all[low_all['subjectID'].isin(val_subjects)][['subjectID'] + cols].copy()\n",
"val_high_data = high_all[high_all['subjectID'].isin(val_subjects)][['subjectID'] + au_columns].copy()\n", "val_high_data = high_all[high_all['subjectID'].isin(val_subjects)][['subjectID'] + cols].copy()\n",
"val_normal_data = val_normal_data.sample(n=1000, random_state=42)\n", "val_normal_data = val_normal_data.sample(n=500, random_state=42)\n",
"val_high_data = val_high_data.sample(n=1000, random_state=42)\n", "val_high_data = val_high_data.sample(n=500, random_state=42)\n",
"\n", "\n",
"# Prepare test data (normal and anomaly) - 1000 samples each\n", "# Prepare test data (normal and anomaly) - 1000 samples each\n",
"test_normal_data = low_all[low_all['subjectID'].isin(test_subjects)][['subjectID'] + au_columns].copy()\n", "test_normal_data = low_all[low_all['subjectID'].isin(test_subjects)][['subjectID'] + cols].copy()\n",
"test_high_data = high_all[high_all['subjectID'].isin(test_subjects)][['subjectID'] + au_columns].copy()\n", "test_high_data = high_all[high_all['subjectID'].isin(test_subjects)][['subjectID'] + cols].copy()\n",
"test_normal_data = test_normal_data.sample(n=500, random_state=42)\n", "test_normal_data = test_normal_data.sample(n=500, random_state=42)\n",
"test_high_data = test_high_data.sample(n=500, random_state=42)\n", "test_high_data = test_high_data.sample(n=500, random_state=42)\n",
"\n", "\n",
@@ -186,7 +207,7 @@
"outputs": [], "outputs": [],
"source": [ "source": [
"# Cell 3: Fit normalizer on training data\n", "# Cell 3: Fit normalizer on training data\n",
"normalizer = scaler.fit_normalizer(train_data, au_columns, method='minmax', scope='global')\n", "normalizer = scaler.fit_normalizer(train_data, cols, method='minmax', scope='global')\n",
"print(\"Normalizer fitted on training data\")" "print(\"Normalizer fitted on training data\")"
] ]
}, },
@@ -198,11 +219,11 @@
"outputs": [], "outputs": [],
"source": [ "source": [
"# Cell 4: Apply normalization to all datasets\n", "# Cell 4: Apply normalization to all datasets\n",
"train_normalized = scaler.apply_normalizer(train_data, au_columns, normalizer)\n", "train_normalized = scaler.apply_normalizer(train_data, cols, normalizer)\n",
"val_normal_normalized = scaler.apply_normalizer(val_normal_data, au_columns, normalizer)\n", "val_normal_normalized = scaler.apply_normalizer(val_normal_data, cols, normalizer)\n",
"val_high_normalized = scaler.apply_normalizer(val_high_data, au_columns, normalizer)\n", "val_high_normalized = scaler.apply_normalizer(val_high_data, cols, normalizer)\n",
"test_normal_normalized = scaler.apply_normalizer(test_normal_data, au_columns, normalizer)\n", "test_normal_normalized = scaler.apply_normalizer(test_normal_data, cols, normalizer)\n",
"test_high_normalized = scaler.apply_normalizer(test_high_data, au_columns, normalizer)\n", "test_high_normalized = scaler.apply_normalizer(test_high_data, cols, normalizer)\n",
"\n", "\n",
"print(\"Normalization applied to all datasets\")" "print(\"Normalization applied to all datasets\")"
] ]
@@ -214,13 +235,11 @@
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"# Cell 5: Extract AU columns and create labels for grid search\n", "X_train = train_normalized[cols].copy()\n",
"# Extract only AU columns (drop subjectID)\n", "X_val_normal = val_normal_normalized[cols].copy()\n",
"X_train = train_normalized[au_columns].copy()\n", "X_val_high = val_high_normalized[cols].copy()\n",
"X_val_normal = val_normal_normalized[au_columns].copy()\n", "X_test_high = test_high_normalized[cols].copy()\n",
"X_val_high = val_high_normalized[au_columns].copy()\n", "X_test_normal = test_normal_normalized[cols].copy()\n",
"X_test_high = test_high_normalized[au_columns].copy()\n",
"X_test_normal = test_normal_normalized[au_columns].copy()\n",
"\n", "\n",
"\n", "\n",
"# Create labels for grid search\n", "# Create labels for grid search\n",
@@ -241,116 +260,12 @@
"X_train.shape" "X_train.shape"
] ]
}, },
{
"cell_type": "code",
"execution_count": null,
"id": "50fc80dc-fe16-4917-aad6-0dbaa1ce5ef9",
"metadata": {},
"outputs": [],
"source": [
"!pip install keras-tuner --quiet # nur einmal nötig\n",
"\n",
"import tensorflow as tf\n",
"from tensorflow import keras\n",
"from kerastuner import HyperModel\n",
"from kerastuner.tuners import RandomSearch\n",
"\n",
"# 1️⃣ HyperModel definieren\n",
"class AutoencoderHyperModel(HyperModel):\n",
" def __init__(self, input_dim):\n",
" self.input_dim = input_dim\n",
"\n",
" def build(self, hp):\n",
" reg = hp.Float(\"l2_reg\", min_value=1e-5, max_value=0.01, sampling=\"log\")\n",
" lr = hp.Float(\"learning_rate\", 1e-4, 1e-2, sampling=\"log\")\n",
"\n",
" # Encoder\n",
" encoder = keras.Sequential([\n",
" keras.layers.Dense(\n",
" units=hp.Int(\"enc_units1\", min_value=10, max_value=self.input_dim, step=10),\n",
" activation=None,\n",
" kernel_regularizer=keras.regularizers.l2(reg)\n",
" ),\n",
" keras.layers.LeakyReLU(alpha=0.1),\n",
" keras.layers.Dense(\n",
" units=hp.Int(\"enc_units2\", min_value=5, max_value=20, step=1),\n",
" activation=tf.keras.layers.LeakyReLU(alpha=0.1),\n",
" kernel_regularizer=keras.regularizers.l2(reg)\n",
" ),\n",
" keras.layers.Dense(\n",
" units=2, # Bottleneck\n",
" activation='linear',\n",
" kernel_regularizer=keras.regularizers.l2(reg)\n",
" ),\n",
" ])\n",
"\n",
" # Decoder\n",
" decoder = keras.Sequential([\n",
" keras.layers.Dense(\n",
" units=hp.Int(\"dec_units1\", min_value=5, max_value=20, step=1),\n",
" activation=tf.keras.layers.LeakyReLU(alpha=0.1),\n",
" kernel_regularizer=keras.regularizers.l2(reg)\n",
" ),\n",
" keras.layers.Dense(\n",
" units=hp.Int(\"dec_units2\", min_value=10, max_value=self.input_dim, step=10),\n",
" activation=tf.keras.layers.LeakyReLU(alpha=0.1),\n",
" kernel_regularizer=keras.regularizers.l2(reg)\n",
" ),\n",
" keras.layers.Dense(\n",
" units=self.input_dim,\n",
" activation='linear',\n",
" kernel_regularizer=keras.regularizers.l2(reg)\n",
" ),\n",
" ])\n",
"\n",
" # Autoencoder\n",
" inputs = keras.Input(shape=(self.input_dim,))\n",
" encoded = encoder(inputs)\n",
" decoded = decoder(encoded)\n",
" autoencoder = keras.Model(inputs, decoded)\n",
"\n",
" autoencoder.compile(\n",
" optimizer=keras.optimizers.Adam(learning_rate=lr),\n",
" loss='mse'\n",
" )\n",
"\n",
" return autoencoder\n",
"\n",
"# 2️⃣ RandomSearch-Tuner\n",
"hypermodel = AutoencoderHyperModel(input_dim=X_train.shape[1])\n",
"\n",
"tuner = RandomSearch(\n",
" hypermodel,\n",
" objective='val_loss',\n",
" max_trials=10, # Anzahl der getesteten Kombinationen\n",
" executions_per_trial=1, # Anzahl Trainings pro Kombination\n",
" directory='tuner_dir',\n",
" project_name='oc_ae'\n",
")\n",
"\n",
"# 3️⃣ Hyperparameter-Tuning starten\n",
"tuner.search(\n",
" X_train, X_train,\n",
" epochs=100,\n",
" batch_size=64,\n",
" validation_data=(X_val_normal, X_val_normal),\n",
" verbose=0\n",
")\n",
"\n",
"# 4️⃣ Beste Architektur holen\n",
"best_model = tuner.get_best_models(num_models=1)[0]\n",
"best_hyperparameters = tuner.get_best_hyperparameters(1)[0]\n",
"\n",
"print(\"Beste Hyperparameter:\", best_hyperparameters.values)\n"
]
},
{ {
"cell_type": "markdown", "cell_type": "markdown",
"id": "362c0a6f", "id": "362c0a6f",
"metadata": {}, "metadata": {},
"source": [ "source": [
"\n", "Build model"
"Beste Hyperparameter: {'l2_reg': 1.3757411430582133e-05, 'learning_rate': 0.007321002854350309, 'enc_units1': 20, 'enc_units2': 16, 'dec_units1': 14, 'dec_units2': 10}"
] ]
}, },
{ {
@@ -360,26 +275,6 @@
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"# reg = 0.1\n",
"# encoder = tf.keras.Sequential(\n",
"# [\n",
"# tf.keras.layers.Dense(units=X_train.shape[1], activation='relu', kernel_regularizer=tf.keras.regularizers.l2(reg)),\n",
"# tf.keras.layers.Dense(units=10, activation='relu', kernel_regularizer=tf.keras.regularizers.l2(reg)),\n",
"# tf.keras.layers.Dense(units=5, activation='relu', kernel_regularizer=tf.keras.regularizers.l2(reg)),\n",
" \n",
"# ]\n",
"# )\n",
"\n",
"# decoder = tf.keras.Sequential(\n",
"# [\n",
"# tf.keras.layers.Dense(units=5,activation='relu', kernel_regularizer=tf.keras.regularizers.l2(reg)),\n",
"# tf.keras.layers.Dense(units=10, activation='relu', kernel_regularizer=tf.keras.regularizers.l2(reg)),\n",
"# tf.keras.layers.Dense(units=X_train.shape[1], activation='linear', kernel_regularizer=tf.keras.regularizers.l2(reg))\n",
"# ]\n",
"# )\n",
"\n",
"\n",
"\n",
"reg = 1e-5\n", "reg = 1e-5\n",
"\n", "\n",
"# ENCODER\n", "# ENCODER\n",
@@ -389,14 +284,15 @@
" activation=None,\n", " activation=None,\n",
" kernel_regularizer=tf.keras.regularizers.l2(reg)\n", " kernel_regularizer=tf.keras.regularizers.l2(reg)\n",
" ),\n", " ),\n",
" tf.keras.layers.LeakyReLU(alpha=0.1),\n", " tf.keras.layers.LeakyReLU(negative_slope=0.1),\n",
" \n", "\n",
" tf.keras.layers.Dense(\n", " tf.keras.layers.Dense(\n",
" units=12,\n", " units=12,\n",
" activation=tf.keras.layers.LeakyReLU(negative_slope=0.1),\n", " activation=None,\n",
" kernel_regularizer=tf.keras.regularizers.l2(reg)\n", " kernel_regularizer=tf.keras.regularizers.l2(reg)\n",
" ),\n", " ),\n",
" \n", " tf.keras.layers.LeakyReLU(negative_slope=0.1),\n",
"\n",
" tf.keras.layers.Dense(\n", " tf.keras.layers.Dense(\n",
" units=8,\n", " units=8,\n",
" activation='linear', # Bottleneck stays linear\n", " activation='linear', # Bottleneck stays linear\n",
@@ -408,20 +304,24 @@
"decoder = tf.keras.Sequential([\n", "decoder = tf.keras.Sequential([\n",
" tf.keras.layers.Dense(\n", " tf.keras.layers.Dense(\n",
" units=8,\n", " units=8,\n",
" activation=tf.keras.layers.LeakyReLU(negative_slope=0.1),\n", " activation=None,\n",
" kernel_regularizer=tf.keras.regularizers.l2(reg)\n", " kernel_regularizer=tf.keras.regularizers.l2(reg)\n",
" ),\n", " ),\n",
" tf.keras.layers.LeakyReLU(negative_slope=0.1),\n",
"\n",
" tf.keras.layers.Dense(\n", " tf.keras.layers.Dense(\n",
" units=12,\n", " units=12,\n",
" activation=tf.keras.layers.LeakyReLU(negative_slope=0.1),\n", " activation=None,\n",
" kernel_regularizer=tf.keras.regularizers.l2(reg)\n", " kernel_regularizer=tf.keras.regularizers.l2(reg)\n",
" ),\n", " ),\n",
" tf.keras.layers.LeakyReLU(negative_slope=0.1),\n",
"\n",
" tf.keras.layers.Dense(\n", " tf.keras.layers.Dense(\n",
" units=X_train.shape[1],\n", " units=X_train.shape[1],\n",
" activation='linear',\n", " activation='linear',\n",
" kernel_regularizer=tf.keras.regularizers.l2(reg)\n", " kernel_regularizer=tf.keras.regularizers.l2(reg)\n",
" ),\n", " ),\n",
"])\n" "])"
] ]
}, },
{ {
@@ -456,7 +356,7 @@
"source": [ "source": [
"history = autoencoder.fit(\n", "history = autoencoder.fit(\n",
" X_train, X_train, # Input and target are the same for autoencoder\n", " X_train, X_train, # Input and target are the same for autoencoder\n",
" epochs=200,\n", " epochs=50,\n",
" batch_size=64,\n", " batch_size=64,\n",
" validation_data=(X_val_normal, X_val_normal),\n", " validation_data=(X_val_normal, X_val_normal),\n",
" verbose=1\n", " verbose=1\n",
@@ -470,8 +370,7 @@
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"save_path = Path(\"/home/jovyan/data-paulusjafahrsimulator-gpu/saved_models/encoder_model_2_neurons_minmax.keras\")\n", "encoder.save(enconder_path)"
"encoder.save(save_path)"
] ]
}, },
{ {
@@ -489,8 +388,7 @@
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"load_path = Path(\"/home/jovyan/data-paulusjafahrsimulator-gpu/saved_models/encoder_model_2_neurons_minmax.keras\")\n", "encoder = tf.keras.models.load_model(enconder_path)"
"encoder = tf.keras.models.load_model(load_path)"
] ]
}, },
{ {
@@ -541,25 +439,6 @@
"test_predictions.shape" "test_predictions.shape"
] ]
}, },
{
"cell_type": "code",
"execution_count": null,
"id": "759118d8-989d-489c-9d35-331454b4795e",
"metadata": {},
"outputs": [],
"source": [
"all_zero = {\n",
" \"X_train_encoded\": np.all(X_train_encoded == 0),\n",
" \"X_val_normal_encoded\": np.all(X_val_normal_encoded == 0),\n",
" \"X_val_high_encoded\": np.all(X_val_high_encoded == 0),\n",
" \"X_test_normal_encoded\": np.all(X_test_normal_encoded == 0),\n",
" \"X_test_high_encoded\": np.all(X_test_high_encoded == 0),\n",
"}\n",
"\n",
"print(all_zero)\n",
"print(X_train_encoded.shape)"
]
},
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": null,
@@ -567,81 +446,49 @@
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"# fig, axes = plt.subplots(1, 3, figsize=(18, 5))\n", "fig, axes = plt.subplots(1, 3, figsize=(18, 5))\n",
"\n", "\n",
"# # Subplot A: Normal\n", "# Subplot A: Normal\n",
"# axes[0].scatter(\n", "axes[0].scatter(\n",
"# X_val_normal_encoded[:, 0],\n", " X_val_normal_encoded[:, 0],\n",
"# X_val_normal_encoded[:, 1],\n", " X_val_normal_encoded[:, 1],\n",
"# color=\"blue\",\n", " color=\"blue\",\n",
"# label=\"Normal\"\n", " label=\"Normal\"\n",
"# )\n", ")\n",
"# axes[0].set_title(\"Val Normal (encoded)\")\n", "axes[0].set_title(\"Val Normal (encoded)\")\n",
"# axes[0].set_xlabel(\"latent feature 1\")\n", "axes[0].set_xlabel(\"latent feature 1\")\n",
"# axes[0].set_ylabel(\"latent feature 2\")\n", "axes[0].set_ylabel(\"latent feature 2\")\n",
"# axes[0].legend()\n", "axes[0].legend()\n",
"\n",
"# # Subplot B: High\n",
"# axes[1].scatter(\n",
"# X_val_high_encoded[:, 0],\n",
"# X_val_high_encoded[:, 1],\n",
"# color=\"orange\",\n",
"# label=\"High\"\n",
"# )\n",
"# axes[1].set_title(\"ValHigh (encoded)\")\n",
"# axes[1].set_xlabel(\"latent feature 1\")\n",
"# axes[1].set_ylabel(\"latent feature 2\")\n",
"# axes[1].legend()\n",
"\n",
"# # Subplot C: Both\n",
"# axes[2].scatter(\n",
"# X_val_normal_encoded[:, 0],\n",
"# X_val_normal_encoded[:, 1],\n",
"# color=\"blue\",\n",
"# label=\"Normal\"\n",
"# )\n",
"# axes[2].scatter(\n",
"# X_val_high_encoded[:, 0],\n",
"# X_val_high_encoded[:, 1],\n",
"# color=\"orange\",\n",
"# label=\"High\"\n",
"# )\n",
"# axes[2].set_title(\"Normal vs High (encoded)\")\n",
"# axes[2].set_xlabel(\"latent feature 1\")\n",
"# axes[2].set_ylabel(\"latent feature 2\")\n",
"# axes[2].legend()\n",
"\n",
"\n",
"\n",
"latent_dim = 8\n",
"fig, axes = plt.subplots(2, 4, figsize=(20, 10))\n",
"axes = axes.flatten() # flatten to index easily\n",
"\n",
"for i in range(latent_dim):\n",
" axes[i].scatter(\n",
" X_val_normal_encoded[:, i],\n",
" [0]*X_val_normal_encoded.shape[0], # optional: place on a line for 1D visualization\n",
" color='blue',\n",
" label='Normal',\n",
" alpha=0.6\n",
" )\n",
" axes[i].scatter(\n",
" X_val_high_encoded[:, i],\n",
" [0]*X_val_high_encoded.shape[0], # same for High\n",
" color='orange',\n",
" label='High',\n",
" alpha=0.6\n",
" )\n",
" axes[i].set_title(f'Latent dim {i+1}')\n",
" axes[i].set_xlabel(f'Feature {i+1}')\n",
" axes[i].set_yticks([]) # hide y-axis as it's just a 1D comparison\n",
" axes[i].legend()\n",
" axes[i].grid(True)\n",
"\n",
"plt.tight_layout()\n",
"plt.show()\n",
"\n", "\n",
"# Subplot B: High\n",
"axes[1].scatter(\n",
" X_val_high_encoded[:, 0],\n",
" X_val_high_encoded[:, 1],\n",
" color=\"orange\",\n",
" label=\"High\"\n",
")\n",
"axes[1].set_title(\"ValHigh (encoded)\")\n",
"axes[1].set_xlabel(\"latent feature 1\")\n",
"axes[1].set_ylabel(\"latent feature 2\")\n",
"axes[1].legend()\n",
"\n", "\n",
"# Subplot C: Both\n",
"axes[2].scatter(\n",
" X_val_normal_encoded[:, 0],\n",
" X_val_normal_encoded[:, 1],\n",
" color=\"blue\",\n",
" label=\"Normal\"\n",
")\n",
"axes[2].scatter(\n",
" X_val_high_encoded[:, 0],\n",
" X_val_high_encoded[:, 1],\n",
" color=\"orange\",\n",
" label=\"High\"\n",
")\n",
"axes[2].set_title(\"Normal vs High (encoded)\")\n",
"axes[2].set_xlabel(\"latent feature 1\")\n",
"axes[2].set_ylabel(\"latent feature 2\")\n",
"axes[2].legend()\n",
"\n", "\n",
"plt.tight_layout()\n", "plt.tight_layout()\n",
"plt.show()\n", "plt.show()\n",
@@ -708,11 +555,11 @@
"outputs": [], "outputs": [],
"source": [ "source": [
"# Save\n", "# Save\n",
"with open('ocsvm_model.pkl', 'wb') as f:\n", "with open(model_path, \"wb\") as f:\n",
" pickle.dump(ocsvm, f)\n", " pickle.dump(ocsvm, f)\n",
"\n", "\n",
"# Load later\n", "# Load later\n",
"with open('ocsvm_model.pkl', 'rb') as f:\n", "with open(model_path, \"rb\") as f:\n",
" ocsvm_loaded = pickle.load(f)" " ocsvm_loaded = pickle.load(f)"
] ]
}, },
@@ -731,11 +578,6 @@
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"# X_combined = np.concatenate([X_train_encoded, X_val_normal_encoded, X_val_high_encoded], axis=0)\n",
"# y_combined = np.concatenate([\n",
"# np.ones(X_train_encoded.shape[0]+X_val_normal_encoded.shape[0]), # normal = 1\n",
"# -np.ones(X_val_high_encoded.shape[0]) # anomaly = -1\n",
"# ], axis=0)\n",
"X_combined = np.concatenate([X_train_encoded, X_val_high_encoded], axis=0)\n", "X_combined = np.concatenate([X_train_encoded, X_val_high_encoded], axis=0)\n",
"y_combined = np.concatenate([\n", "y_combined = np.concatenate([\n",
" np.ones(X_train_encoded.shape[0]), # normal = 1\n", " np.ones(X_train_encoded.shape[0]), # normal = 1\n",
@@ -856,14 +698,6 @@
"source": [ "source": [
"f1_score(y_true=np.concatenate([y_test_normal, y_test_high]), y_pred=predictions)" "f1_score(y_true=np.concatenate([y_test_normal, y_test_high]), y_pred=predictions)"
] ]
},
{
"cell_type": "code",
"execution_count": null,
"id": "9e412041-7534-40fe-8486-ee97349a6168",
"metadata": {},
"outputs": [],
"source": []
} }
], ],
"metadata": { "metadata": {
@@ -871,18 +705,6 @@
"display_name": "Python 3 (ipykernel)", "display_name": "Python 3 (ipykernel)",
"language": "python", "language": "python",
"name": "python3" "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.12.10"
} }
}, },
"nbformat": 4, "nbformat": 4,
File diff suppressed because it is too large Load Diff
-877
View File
@@ -1,877 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "708c9745",
"metadata": {},
"source": [
"### Imports"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "53b10294",
"metadata": {},
"outputs": [],
"source": [
"import pandas as pd\n",
"import numpy as np\n",
"from pathlib import Path\n",
"import sys\n",
"import os\n",
"\n",
"base_dir = os.path.abspath(os.path.join(os.getcwd(), \"..\"))\n",
"sys.path.append(base_dir)\n",
"print(base_dir)\n",
"\n",
"from Fahrsimulator_MSY2526_AI.model_training.tools import evaluation_tools, scaler, mad_outlier_removal\n",
"from sklearn.preprocessing import StandardScaler, MinMaxScaler\n",
"from sklearn.svm import OneClassSVM\n",
"from sklearn.model_selection import GridSearchCV, KFold, ParameterGrid, train_test_split\n",
"import matplotlib.pyplot as plt\n",
"import tensorflow as tf\n",
"import pickle\n",
"from sklearn.metrics import (roc_auc_score, accuracy_score, precision_score, \n",
" recall_score, f1_score, confusion_matrix, classification_report) "
]
},
{
"cell_type": "markdown",
"id": "68101229",
"metadata": {},
"source": [
"### load Dataset"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "24a765e8",
"metadata": {},
"outputs": [],
"source": [
"dataset_path = Path(r\"/home/jovyan/data-paulusjafahrsimulator-gpu/first_AU_dataset/output_windowed.parquet\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "471001b0",
"metadata": {},
"outputs": [],
"source": [
"df = pd.read_parquet(path=dataset_path)"
]
},
{
"cell_type": "markdown",
"id": "0fdecdaa",
"metadata": {},
"source": [
"### Load Performance data and Subject Split"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "692d1b47",
"metadata": {},
"outputs": [],
"source": [
"performance_path = Path(r\"/home/jovyan/data-paulusjafahrsimulator-gpu/subject_performance/3new_au_performance.csv\")\n",
"performance_df = pd.read_csv(performance_path)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "ea617e3f",
"metadata": {},
"outputs": [],
"source": [
"# Subject IDs aus dem Haupt-Dataset nehmen\n",
"subjects_from_df = df[\"subjectID\"].unique()\n",
"\n",
"# Performance-Subset nur für vorhandene Subjects\n",
"perf_filtered = performance_df[\n",
" performance_df[\"subjectID\"].isin(subjects_from_df)\n",
"][[\"subjectID\", \"overall_score\"]]\n",
"\n",
"# Merge: nur Subjects, die sowohl im df als auch im Performance-CSV vorkommen\n",
"merged = (\n",
" pd.DataFrame({\"subjectID\": subjects_from_df})\n",
" .merge(perf_filtered, on=\"subjectID\", how=\"inner\")\n",
")\n",
"\n",
"# Sicherstellen, dass keine Scores fehlen\n",
"if merged[\"overall_score\"].isna().any():\n",
" raise ValueError(\"Es fehlen Score-Werte für manche Subjects.\")\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "ae43df8d",
"metadata": {},
"outputs": [],
"source": [
"merged_sorted = merged.sort_values(\"overall_score\", ascending=False).reset_index(drop=True)\n",
"\n",
"scores = merged_sorted[\"overall_score\"].values\n",
"n_total = len(merged_sorted)\n",
"n_small = n_total // 3\n",
"n_large = n_total - n_small\n",
"\n",
"# Schritt 1: zufällige Start-Aufteilung\n",
"idx = np.arange(n_total)\n",
"np.random.shuffle(idx)\n",
"\n",
"small_idx = idx[:n_small]\n",
"large_idx = idx[n_small:]\n",
"\n",
"def score_diff(small_idx, large_idx):\n",
" return abs(scores[small_idx].mean() - scores[large_idx].mean())\n",
"\n",
"diff = score_diff(small_idx, large_idx)\n",
"threshold = 0.01\n",
"max_iter = 100\n",
"count = 0\n",
"\n",
"# Schritt 2: random swaps bis Differenz klein genug\n",
"while diff > threshold and count < max_iter:\n",
" # Zwei zufällige Elemente auswählen\n",
" si = np.random.choice(small_idx)\n",
" li = np.random.choice(large_idx)\n",
" \n",
" # Tausch durchführen\n",
" new_small_idx = small_idx.copy()\n",
" new_large_idx = large_idx.copy()\n",
" \n",
" new_small_idx[new_small_idx == si] = li\n",
" new_large_idx[new_large_idx == li] = si\n",
"\n",
" # neue Differenz berechnen\n",
" new_diff = score_diff(new_small_idx, new_large_idx)\n",
"\n",
" # Swap akzeptieren, wenn es besser wird\n",
" if new_diff < diff:\n",
" small_idx = new_small_idx\n",
" large_idx = new_large_idx\n",
" diff = new_diff\n",
"\n",
" count += 1\n",
"\n",
"# Finalgruppen\n",
"group_small = merged_sorted.loc[small_idx].reset_index(drop=True)\n",
"group_large = merged_sorted.loc[large_idx].reset_index(drop=True)\n",
"\n",
"print(\"Finale Score-Differenz:\", diff)\n",
"print(\"Größe Gruppe 1:\", len(group_small))\n",
"print(\"Größe Gruppe 2:\", len(group_large))\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "9d1b414e",
"metadata": {},
"outputs": [],
"source": [
"group_large['overall_score'].mean()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "fa71f9a5",
"metadata": {},
"outputs": [],
"source": [
"group_small['overall_score'].mean()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "79ecb4a2",
"metadata": {},
"outputs": [],
"source": [
"training_subjects = group_large['subjectID'].values\n",
"test_subjects = group_small['subjectID'].values\n",
"print(training_subjects)\n",
"print(test_subjects)"
]
},
{
"cell_type": "markdown",
"id": "4353f87c",
"metadata": {},
"source": [
"### Data cleaning with mad"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "76610052",
"metadata": {},
"outputs": [],
"source": [
"# SET\n",
"threshold_mad = 5\n",
"column_praefix ='AU'\n",
"\n",
"au_columns = [col for col in df.columns if col.startswith(column_praefix)]\n",
"cleaned_df = mad_outlier_removal.mad_outlier_removal(df,columns=au_columns, threshold=threshold_mad)\n",
"print(cleaned_df.shape)\n",
"print(df.shape)"
]
},
{
"cell_type": "markdown",
"id": "9a6c1732",
"metadata": {},
"source": [
"#### TO DO\n",
" * pipeline aus Autoencoder und SVM\n",
" * group k fold\n",
" * AE überpüfen, loss dokumentieren"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "877309d9",
"metadata": {},
"outputs": [],
"source": [
"### Variational Autoencoder with Classifier Head\n",
"import pandas as pd\n",
"import numpy as np\n",
"import tensorflow as tf\n",
"from tensorflow import keras\n",
"from tensorflow.keras import layers, Model\n",
"from sklearn.model_selection import GroupKFold\n",
"from sklearn.preprocessing import StandardScaler\n",
"from sklearn.metrics import (\n",
" accuracy_score, precision_score, recall_score, f1_score, \n",
" roc_auc_score, confusion_matrix, classification_report\n",
")\n",
"import matplotlib.pyplot as plt\n",
"from collections import defaultdict\n",
"\n",
"# ============================================================================\n",
"# 1. CREATE LABELS\n",
"# ============================================================================\n",
"\n",
"# Low workload: baseline + n-back level 1,4\n",
"low_all = cleaned_df[\n",
" ((cleaned_df[\"PHASE\"] == \"baseline\") |\n",
" ((cleaned_df[\"STUDY\"] == \"n-back\") & (cleaned_df[\"PHASE\"] != \"baseline\") & (cleaned_df[\"LEVEL\"].isin([1,4]))))\n",
"].copy()\n",
"low_all['label'] = 0\n",
"print(f\"Low workload samples: {low_all.shape[0]}\")\n",
"\n",
"# High workload n-back: level 2,3,5,6\n",
"high_nback = cleaned_df[\n",
" (cleaned_df[\"STUDY\"]==\"n-back\") &\n",
" (cleaned_df[\"LEVEL\"].isin([2, 3, 5, 6])) &\n",
" (cleaned_df[\"PHASE\"].isin([\"train\", \"test\"]))\n",
"].copy()\n",
"high_nback['label'] = 1\n",
"print(f\"High n-back samples: {high_nback.shape[0]}\")\n",
"\n",
"# High workload k-drive\n",
"high_kdrive = cleaned_df[\n",
" (cleaned_df[\"STUDY\"] == \"k-drive\") & (cleaned_df[\"PHASE\"] != \"baseline\")\n",
"].copy()\n",
"high_kdrive['label'] = 1\n",
"print(f\"High k-drive samples: {high_kdrive.shape[0]}\")\n",
"\n",
"# Combine all high workload\n",
"high_all = pd.concat([high_nback, high_kdrive])\n",
"print(f\"Total high workload samples: {high_all.shape[0]}\")\n",
"\n",
"# Complete labeled dataset\n",
"labeled_df = pd.concat([low_all, high_all]).reset_index(drop=True)\n",
"print(f\"\\nTotal labeled samples: {labeled_df.shape[0]}\")\n",
"print(f\"Class distribution:\\n{labeled_df['label'].value_counts()}\")\n",
"\n",
"# ============================================================================\n",
"# 2. TRAIN/TEST SPLIT BY SUBJECTS\n",
"# ============================================================================\n",
"\n",
"train_df = labeled_df[labeled_df['subjectID'].isin(training_subjects)].copy()\n",
"test_df = labeled_df[labeled_df['subjectID'].isin(test_subjects)].copy()\n",
"\n",
"print(f\"\\nTraining subjects: {training_subjects}\")\n",
"print(f\"Test subjects: {test_subjects}\")\n",
"print(f\"Train samples: {train_df.shape[0]}, Test samples: {test_df.shape[0]}\")\n",
"\n",
"# Extract features and labels\n",
"au_columns = [col for col in labeled_df.columns if col.startswith('AU')]\n",
"print(f\"\\nUsing {len(au_columns)} AU features: {au_columns}\")\n",
"\n",
"X_train = train_df[au_columns].values\n",
"y_train = train_df['label'].values\n",
"groups_train = train_df['subjectID'].values\n",
"\n",
"X_test = test_df[au_columns].values\n",
"y_test = test_df['label'].values\n",
"\n",
"# Normalize features\n",
"scaler = StandardScaler()\n",
"X_train_scaled = scaler.fit_transform(X_train)\n",
"X_test_scaled = scaler.transform(X_test)\n",
"\n",
"print(f\"\\nTrain class distribution: {np.bincount(y_train)}\")\n",
"print(f\"Test class distribution: {np.bincount(y_test)}\")\n",
"\n",
"# ============================================================================\n",
"# 3. VAE WITH CLASSIFIER HEAD MODEL\n",
"# ============================================================================\n",
"\n",
"class Sampling(layers.Layer):\n",
" \"\"\"Reparameterization trick for VAE\"\"\"\n",
" def call(self, inputs):\n",
" z_mean, z_log_var = inputs\n",
" batch = tf.shape(z_mean)[0]\n",
" dim = tf.shape(z_mean)[1]\n",
" epsilon = tf.random.normal(shape=(batch, dim))\n",
" return z_mean + tf.exp(0.5 * z_log_var) * epsilon\n",
"\n",
"def build_vae_classifier(input_dim, latent_dim, encoder_dims=[32, 16], \n",
" decoder_dims=[16, 32], classifier_dims=[16]):\n",
" \"\"\"\n",
" Build VAE with classifier head\n",
" \n",
" Args:\n",
" input_dim: Number of input features (20 AUs)\n",
" latent_dim: Dimension of latent space (2-5)\n",
" encoder_dims: Hidden layer sizes for encoder\n",
" decoder_dims: Hidden layer sizes for decoder\n",
" classifier_dims: Hidden layer sizes for classifier\n",
" \"\"\"\n",
" \n",
" # ---- ENCODER ----\n",
" encoder_inputs = keras.Input(shape=(input_dim,), name='encoder_input')\n",
" x = encoder_inputs\n",
" \n",
" for i, dim in enumerate(encoder_dims):\n",
" x = layers.Dense(dim, activation='relu', name=f'encoder_dense_{i}')(x)\n",
" x = layers.BatchNormalization(name=f'encoder_bn_{i}')(x)\n",
" x = layers.Dropout(0.2, name=f'encoder_dropout_{i}')(x)\n",
" \n",
" z_mean = layers.Dense(latent_dim, name='z_mean')(x)\n",
" z_log_var = layers.Dense(latent_dim, name='z_log_var')(x)\n",
" z = Sampling()([z_mean, z_log_var])\n",
" \n",
" encoder = Model(encoder_inputs, [z_mean, z_log_var, z], name='encoder')\n",
" \n",
" # ---- DECODER ----\n",
" latent_inputs = keras.Input(shape=(latent_dim,), name='latent_input')\n",
" x = latent_inputs\n",
" \n",
" for i, dim in enumerate(decoder_dims):\n",
" x = layers.Dense(dim, activation='relu', name=f'decoder_dense_{i}')(x)\n",
" x = layers.BatchNormalization(name=f'decoder_bn_{i}')(x)\n",
" \n",
" decoder_outputs = layers.Dense(input_dim, activation='linear', name='decoder_output')(x)\n",
" decoder = Model(latent_inputs, decoder_outputs, name='decoder')\n",
" \n",
" # ---- CLASSIFIER HEAD ----\n",
" x = latent_inputs\n",
" for i, dim in enumerate(classifier_dims):\n",
" x = layers.Dense(dim, activation='relu', name=f'classifier_dense_{i}')(x)\n",
" x = layers.Dropout(0.3, name=f'classifier_dropout_{i}')(x)\n",
" \n",
" classifier_output = layers.Dense(1, activation='sigmoid', name='classifier_output')(x)\n",
" classifier = Model(latent_inputs, classifier_output, name='classifier')\n",
" \n",
" # ---- FULL MODEL ----\n",
" inputs = keras.Input(shape=(input_dim,), name='vae_input')\n",
" z_mean, z_log_var, z = encoder(inputs)\n",
" reconstructed = decoder(z)\n",
" classification = classifier(z)\n",
" \n",
" model = Model(inputs, [reconstructed, classification], name='vae_classifier')\n",
" \n",
" return model, encoder, decoder, classifier\n",
"\n",
"# ============================================================================\n",
"# 4. CUSTOM TRAINING LOOP WITH COMBINED LOSS\n",
"# ============================================================================\n",
"\n",
"class VAEClassifier(keras.Model):\n",
" def __init__(self, encoder, decoder, classifier, **kwargs):\n",
" super().__init__(**kwargs)\n",
" self.encoder = encoder\n",
" self.decoder = decoder\n",
" self.classifier = classifier\n",
" self.total_loss_tracker = keras.metrics.Mean(name=\"total_loss\")\n",
" self.reconstruction_loss_tracker = keras.metrics.Mean(name=\"reconstruction_loss\")\n",
" self.kl_loss_tracker = keras.metrics.Mean(name=\"kl_loss\")\n",
" self.classification_loss_tracker = keras.metrics.Mean(name=\"classification_loss\")\n",
" self.accuracy_tracker = keras.metrics.BinaryAccuracy(name=\"accuracy\")\n",
" \n",
" @property\n",
" def metrics(self):\n",
" return [\n",
" self.total_loss_tracker,\n",
" self.reconstruction_loss_tracker,\n",
" self.kl_loss_tracker,\n",
" self.classification_loss_tracker,\n",
" self.accuracy_tracker,\n",
" ]\n",
" \n",
" def train_step(self, data):\n",
" x, y = data\n",
" \n",
" with tf.GradientTape() as tape:\n",
" # Forward pass\n",
" z_mean, z_log_var, z = self.encoder(x, training=True)\n",
" reconstruction = self.decoder(z, training=True)\n",
" classification = self.classifier(z, training=True)\n",
" \n",
" # Reconstruction loss (MSE)\n",
" reconstruction_loss = tf.reduce_mean(\n",
" keras.losses.mse(x, reconstruction))\n",
" \n",
" # KL divergence loss\n",
" kl_loss = -0.5 * tf.reduce_mean(\n",
" tf.reduce_sum(\n",
" 1 + z_log_var - tf.square(z_mean) - tf.exp(z_log_var),\n",
" axis=1\n",
" )\n",
" )\n",
" \n",
" # Classification loss (binary crossentropy)\n",
" # Classification loss (binary crossentropy)\n",
" classification_loss = tf.reduce_mean(\n",
" keras.losses.binary_crossentropy(tf.expand_dims(y, -1), classification)\n",
" )\n",
" \n",
" # Combined loss with weights\n",
" total_loss = reconstruction_loss + kl_loss + classification_loss\n",
" \n",
" # Backpropagation\n",
" grads = tape.gradient(total_loss, self.trainable_weights)\n",
" self.optimizer.apply_gradients(zip(grads, self.trainable_weights))\n",
" \n",
" # Update metrics\n",
" self.total_loss_tracker.update_state(total_loss)\n",
" self.reconstruction_loss_tracker.update_state(reconstruction_loss)\n",
" self.kl_loss_tracker.update_state(kl_loss)\n",
" self.classification_loss_tracker.update_state(classification_loss)\n",
" self.accuracy_tracker.update_state(y, classification)\n",
" \n",
" return {\n",
" \"total_loss\": self.total_loss_tracker.result(),\n",
" \"reconstruction_loss\": self.reconstruction_loss_tracker.result(),\n",
" \"kl_loss\": self.kl_loss_tracker.result(),\n",
" \"classification_loss\": self.classification_loss_tracker.result(),\n",
" \"accuracy\": self.accuracy_tracker.result(),\n",
" }\n",
" \n",
" def test_step(self, data):\n",
" x, y = data\n",
" \n",
" z_mean, z_log_var, z = self.encoder(x, training=False)\n",
" reconstruction = self.decoder(z, training=False)\n",
" classification = self.classifier(z, training=False)\n",
" \n",
" # Reconstruction loss (MSE)\n",
" reconstruction_loss = tf.reduce_mean(\n",
" keras.losses.mse(x, reconstruction))\n",
" kl_loss = -0.5 * tf.reduce_mean(\n",
" tf.reduce_sum(1 + z_log_var - tf.square(z_mean) - tf.exp(z_log_var), axis=1)\n",
" )\n",
" # Classification loss (binary crossentropy)\n",
" classification_loss = tf.reduce_mean(\n",
" keras.losses.binary_crossentropy(tf.expand_dims(y, -1), classification)\n",
" )\n",
" total_loss = reconstruction_loss + kl_loss + classification_loss\n",
" \n",
" self.total_loss_tracker.update_state(total_loss)\n",
" self.reconstruction_loss_tracker.update_state(reconstruction_loss)\n",
" self.kl_loss_tracker.update_state(kl_loss)\n",
" self.classification_loss_tracker.update_state(classification_loss)\n",
" self.accuracy_tracker.update_state(y, classification)\n",
" \n",
" return {\n",
" \"total_loss\": self.total_loss_tracker.result(),\n",
" \"reconstruction_loss\": self.reconstruction_loss_tracker.result(),\n",
" \"kl_loss\": self.kl_loss_tracker.result(),\n",
" \"classification_loss\": self.classification_loss_tracker.result(),\n",
" \"accuracy\": self.accuracy_tracker.result(),\n",
" }\n",
"\n",
"# ============================================================================\n",
"# 5. GROUP K-FOLD CROSS-VALIDATION WITH GRID SEARCH\n",
"# ============================================================================\n",
"\n",
"# Hyperparameter grid\n",
"param_grid = {\n",
" 'latent_dim': [2, 5],\n",
" 'encoder_dims': [[32, 16], [64, 32]],\n",
" 'learning_rate': [0.001, 0.005],\n",
" 'batch_size': [32, 64],\n",
"}\n",
"\n",
"# Generate all combinations\n",
"from itertools import product\n",
"keys = param_grid.keys()\n",
"values = param_grid.values()\n",
"param_combinations = [dict(zip(keys, v)) for v in product(*values)]\n",
"\n",
"print(f\"\\nTotal hyperparameter combinations: {len(param_combinations)}\")\n",
"\n",
"# Group K-Fold setup\n",
"n_splits = 5\n",
"gkf = GroupKFold(n_splits=n_splits)\n",
"\n",
"# Store results\n",
"cv_results = []\n",
"\n",
"# Grid search with cross-validation\n",
"for idx, params in enumerate(param_combinations):\n",
" print(f\"\\n{'='*80}\")\n",
" print(f\"Testing combination {idx+1}/{len(param_combinations)}: {params}\")\n",
" print(f\"{'='*80}\")\n",
" \n",
" fold_results = []\n",
" \n",
" for fold, (train_idx, val_idx) in enumerate(gkf.split(X_train_scaled, y_train, groups_train)):\n",
" print(f\"\\nFold {fold+1}/{n_splits}\")\n",
" \n",
" X_fold_train, X_fold_val = X_train_scaled[train_idx], X_train_scaled[val_idx]\n",
" y_fold_train, y_fold_val = y_train[train_idx], y_train[val_idx]\n",
" \n",
" # Build model\n",
" model, encoder, decoder, classifier = build_vae_classifier(\n",
" input_dim=len(au_columns),\n",
" latent_dim=params['latent_dim'],\n",
" encoder_dims=params['encoder_dims'],\n",
" decoder_dims=list(reversed(params['encoder_dims'])),\n",
" classifier_dims=[16]\n",
" )\n",
" \n",
" vae_classifier = VAEClassifier(encoder, decoder, classifier)\n",
" vae_classifier.compile(optimizer=keras.optimizers.Adam(params['learning_rate']))\n",
" \n",
" # Early stopping\n",
" early_stop = keras.callbacks.EarlyStopping(\n",
" monitor='val_total_loss',\n",
" patience=10,\n",
" restore_best_weights=True,\n",
" mode='min'\n",
" )\n",
" \n",
" # Train\n",
" history = vae_classifier.fit(\n",
" X_fold_train, y_fold_train,\n",
" validation_data=(X_fold_val, y_fold_val),\n",
" epochs=60,\n",
" batch_size=params['batch_size'],\n",
" callbacks=[early_stop],\n",
" verbose=0\n",
" )\n",
" \n",
" # Evaluate on validation fold\n",
" z_mean_val, _, _ = encoder.predict(X_fold_val, verbose=0)\n",
" y_pred_proba = classifier.predict(z_mean_val, verbose=0).flatten()\n",
" y_pred = (y_pred_proba > 0.5).astype(int)\n",
" \n",
" fold_metrics = {\n",
" 'accuracy': accuracy_score(y_fold_val, y_pred),\n",
" 'precision': precision_score(y_fold_val, y_pred, zero_division=0),\n",
" 'recall': recall_score(y_fold_val, y_pred, zero_division=0),\n",
" 'f1': f1_score(y_fold_val, y_pred, zero_division=0),\n",
" 'roc_auc': roc_auc_score(y_fold_val, y_pred_proba),\n",
" 'final_recon_loss': history.history['val_reconstruction_loss'][-1],\n",
" 'final_kl_loss': history.history['val_kl_loss'][-1],\n",
" 'final_class_loss': history.history['val_classification_loss'][-1],\n",
" }\n",
" \n",
" fold_results.append(fold_metrics)\n",
" print(f\" Accuracy: {fold_metrics['accuracy']:.4f}, F1: {fold_metrics['f1']:.4f}, AUC: {fold_metrics['roc_auc']:.4f}\")\n",
" \n",
" # Clear session to free memory\n",
" keras.backend.clear_session()\n",
" \n",
" # Average across folds\n",
" avg_results = {\n",
" 'params': params,\n",
" 'mean_accuracy': np.mean([r['accuracy'] for r in fold_results]),\n",
" 'std_accuracy': np.std([r['accuracy'] for r in fold_results]),\n",
" 'mean_f1': np.mean([r['f1'] for r in fold_results]),\n",
" 'std_f1': np.std([r['f1'] for r in fold_results]),\n",
" 'mean_roc_auc': np.mean([r['roc_auc'] for r in fold_results]),\n",
" 'std_roc_auc': np.std([r['roc_auc'] for r in fold_results]),\n",
" 'mean_recon_loss': np.mean([r['final_recon_loss'] for r in fold_results]),\n",
" 'mean_kl_loss': np.mean([r['final_kl_loss'] for r in fold_results]),\n",
" 'mean_class_loss': np.mean([r['final_class_loss'] for r in fold_results]),\n",
" 'fold_results': fold_results\n",
" }\n",
" \n",
" cv_results.append(avg_results)\n",
" \n",
" print(f\"\\nMean CV Accuracy: {avg_results['mean_accuracy']:.4f} ± {avg_results['std_accuracy']:.4f}\")\n",
" print(f\"Mean CV F1: {avg_results['mean_f1']:.4f} ± {avg_results['std_f1']:.4f}\")\n",
" print(f\"Mean CV AUC: {avg_results['mean_roc_auc']:.4f} ± {avg_results['std_roc_auc']:.4f}\")\n",
"\n",
"# ============================================================================\n",
"# 6. SELECT BEST MODEL AND EVALUATE ON TEST SET\n",
"# ============================================================================\n",
"\n",
"# Find best hyperparameters based on mean F1 score\n",
"best_idx = np.argmax([r['mean_f1'] for r in cv_results])\n",
"best_params = cv_results[best_idx]['params']\n",
"\n",
"print(f\"\\n{'='*80}\")\n",
"print(\"BEST HYPERPARAMETERS (based on CV F1 score):\")\n",
"print(f\"{'='*80}\")\n",
"for key, value in best_params.items():\n",
" print(f\"{key}: {value}\")\n",
"print(f\"\\nCV Performance:\")\n",
"print(f\" Accuracy: {cv_results[best_idx]['mean_accuracy']:.4f} ± {cv_results[best_idx]['std_accuracy']:.4f}\")\n",
"print(f\" F1 Score: {cv_results[best_idx]['mean_f1']:.4f} ± {cv_results[best_idx]['std_f1']:.4f}\")\n",
"print(f\" ROC-AUC: {cv_results[best_idx]['mean_roc_auc']:.4f} ± {cv_results[best_idx]['std_roc_auc']:.4f}\")\n",
"\n",
"# Train final model on all training data\n",
"print(f\"\\n{'='*80}\")\n",
"print(\"TRAINING FINAL MODEL ON ALL TRAINING DATA\")\n",
"print(f\"{'='*80}\")\n",
"\n",
"final_model, final_encoder, final_decoder, final_classifier = build_vae_classifier(\n",
" input_dim=len(au_columns),\n",
" latent_dim=best_params['latent_dim'],\n",
" encoder_dims=best_params['encoder_dims'],\n",
" decoder_dims=list(reversed(best_params['encoder_dims'])),\n",
" classifier_dims=[16]\n",
")\n",
"\n",
"final_vae_classifier = VAEClassifier(final_encoder, final_decoder, final_classifier)\n",
"final_vae_classifier.compile(optimizer=keras.optimizers.Adam(best_params['learning_rate']))\n",
"\n",
"final_history = final_vae_classifier.fit(\n",
" X_train_scaled, y_train,\n",
" validation_split=0.2,\n",
" epochs=100,\n",
" batch_size=best_params['batch_size'],\n",
" callbacks=[keras.callbacks.EarlyStopping(monitor='val_total_loss', patience=15, restore_best_weights=True, mode='min')],\n",
" verbose=1\n",
")\n",
"\n",
"# Evaluate on held-out test set\n",
"print(f\"\\n{'='*80}\")\n",
"print(\"EVALUATION ON HELD-OUT TEST SET\")\n",
"print(f\"{'='*80}\")\n",
"\n",
"z_mean_test, _, _ = final_encoder.predict(X_test_scaled, verbose=0)\n",
"y_test_pred_proba = final_classifier.predict(z_mean_test, verbose=0).flatten()\n",
"y_test_pred = (y_test_pred_proba > 0.5).astype(int)\n",
"\n",
"test_metrics = {\n",
" 'accuracy': accuracy_score(y_test, y_test_pred),\n",
" 'precision': precision_score(y_test, y_test_pred),\n",
" 'recall': recall_score(y_test, y_test_pred),\n",
" 'f1': f1_score(y_test, y_test_pred),\n",
" 'roc_auc': roc_auc_score(y_test, y_test_pred_proba),\n",
"}\n",
"\n",
"print(\"\\nTest Set Performance:\")\n",
"for metric, value in test_metrics.items():\n",
" print(f\" {metric.capitalize()}: {value:.4f}\")\n",
"\n",
"print(\"\\nConfusion Matrix:\")\n",
"print(confusion_matrix(y_test, y_test_pred))\n",
"\n",
"print(\"\\nClassification Report:\")\n",
"print(classification_report(y_test, y_test_pred, target_names=['Low Workload', 'High Workload']))\n",
"\n",
"# ============================================================================\n",
"# 7. VISUALIZATION\n",
"# ============================================================================\n",
"\n",
"# Plot training history\n",
"fig, axes = plt.subplots(2, 2, figsize=(15, 10))\n",
"\n",
"axes[0, 0].plot(final_history.history['reconstruction_loss'], label='Train')\n",
"axes[0, 0].plot(final_history.history['val_reconstruction_loss'], label='Val')\n",
"axes[0, 0].set_title('Reconstruction Loss')\n",
"axes[0, 0].set_xlabel('Epoch')\n",
"axes[0, 0].set_ylabel('Loss')\n",
"axes[0, 0].legend()\n",
"axes[0, 0].grid(True)\n",
"\n",
"axes[0, 1].plot(final_history.history['kl_loss'], label='Train')\n",
"axes[0, 1].plot(final_history.history['val_kl_loss'], label='Val')\n",
"axes[0, 1].set_title('KL Divergence Loss')\n",
"axes[0, 1].set_xlabel('Epoch')\n",
"axes[0, 1].set_ylabel('Loss')\n",
"axes[0, 1].legend()\n",
"axes[0, 1].grid(True)\n",
"\n",
"axes[1, 0].plot(final_history.history['classification_loss'], label='Train')\n",
"axes[1, 0].plot(final_history.history['val_classification_loss'], label='Val')\n",
"axes[1, 0].set_title('Classification Loss')\n",
"axes[1, 0].set_xlabel('Epoch')\n",
"axes[1, 0].set_ylabel('Loss')\n",
"axes[1, 0].legend()\n",
"axes[1, 0].grid(True)\n",
"\n",
"axes[1, 1].plot(final_history.history['accuracy'], label='Train')\n",
"axes[1, 1].plot(final_history.history['val_accuracy'], label='Val')\n",
"axes[1, 1].set_title('Classification Accuracy')\n",
"axes[1, 1].set_xlabel('Epoch')\n",
"axes[1, 1].set_ylabel('Accuracy')\n",
"axes[1, 1].legend()\n",
"axes[1, 1].grid(True)\n",
"\n",
"plt.tight_layout()\n",
"plt.show()\n",
"\n",
"# Visualize latent space (if 2D or 3D)\n",
"if best_params['latent_dim'] == 2:\n",
" z_mean_train, _, _ = final_encoder.predict(X_train_scaled, verbose=0)\n",
" \n",
" plt.figure(figsize=(10, 8))\n",
" scatter = plt.scatter(z_mean_train[:, 0], z_mean_train[:, 1], \n",
" c=y_train, cmap='RdYlBu', alpha=0.6, edgecolors='k')\n",
" plt.colorbar(scatter, label='Workload (0=Low, 1=High)')\n",
" plt.xlabel('Latent Dimension 1')\n",
" plt.ylabel('Latent Dimension 2')\n",
" plt.title('2D Latent Space Representation (Training Data)')\n",
" plt.grid(True, alpha=0.3)\n",
" plt.show()\n",
" \n",
" # Test set latent space\n",
" plt.figure(figsize=(10, 8))\n",
" scatter = plt.scatter(z_mean_test[:, 0], z_mean_test[:, 1], \n",
" c=y_test, cmap='RdYlBu', alpha=0.6, edgecolors='k')\n",
" plt.colorbar(scatter, label='Workload (0=Low, 1=High)')\n",
" plt.xlabel('Latent Dimension 1')\n",
" plt.ylabel('Latent Dimension 2')\n",
" plt.title('2D Latent Space Representation (Test Data)')\n",
" plt.grid(True, alpha=0.3)\n",
" plt.show()\n",
"\n",
"print(\"\\n\" + \"=\"*80)\n",
"print(\"TRAINING COMPLETE!\")\n",
"print(\"=\"*80)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "79bcfc58",
"metadata": {},
"outputs": [],
"source": [
"### Save Trained VAE Classifier Model\n",
"from pathlib import Path\n",
"from datetime import datetime\n",
"\n",
"# Define save path\n",
"model_dir = Path(\"/home/jovyan/data-paulusjafahrsimulator-gpu/trained_models\")\n",
"model_dir.mkdir(parents=True, exist_ok=True)\n",
"\n",
"timestamp = datetime.now().strftime(\"%Y%m%d_%H%M%S\")\n",
"model_path = model_dir / f\"vae_classifier_{timestamp}.keras\"\n",
"\n",
"# Save the complete model\n",
"final_vae_classifier.save(model_path)\n",
"\n",
"print(f\"Model saved to: {model_path}\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "d700e517",
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": null,
"id": "30d8d100",
"metadata": {},
"outputs": [],
"source": [
"### Plot Confusion Matrix for Final Model\n",
"from sklearn.metrics import ConfusionMatrixDisplay\n",
"x = Path(\"/home/jovyan/data-paulusjafahrsimulator-gpu/trained_models/vae_classifier_20251210_230121.keras\")\n",
"# Load the saved model\n",
"print(f\"Loading model from: {x}\")\n",
"# loaded_vae_classifier = tf.keras.models.load_model(x)\n",
"loaded_vae_classifier = final_vae_classifier\n",
"print(\"✓ Model loaded successfully!\")\n",
"\n",
"# Extract encoder and classifier from loaded model\n",
"loaded_encoder = loaded_vae_classifier.encoder\n",
"loaded_classifier = loaded_vae_classifier.classifier\n",
"\n",
"# Get predictions on test set\n",
"z_mean_test, _, _ = loaded_encoder.predict(X_test_scaled, verbose=0)\n",
"y_test_pred_proba = loaded_classifier.predict(z_mean_test, verbose=0).flatten()\n",
"y_test_pred = (y_test_pred_proba > 0.5).astype(int)\n",
"\n",
"# Create and plot confusion matrix\n",
"cm = confusion_matrix(y_test, y_test_pred)\n",
"disp = ConfusionMatrixDisplay(confusion_matrix=cm, \n",
" display_labels=['Low Workload', 'High Workload'])\n",
"\n",
"fig, ax = plt.subplots(figsize=(8, 6))\n",
"disp.plot(ax=ax, cmap='Blues', values_format='d')\n",
"plt.title('Confusion Matrix - Test Set (Loaded Model)')\n",
"plt.tight_layout()\n",
"plt.show()\n",
"\n",
"# Print metrics\n",
"print(f\"\\nTest Set Performance (Loaded Model):\")\n",
"print(f\" Accuracy: {accuracy_score(y_test, y_test_pred):.4f}\")\n",
"print(f\" Precision: {precision_score(y_test, y_test_pred):.4f}\")\n",
"print(f\" Recall: {recall_score(y_test, y_test_pred):.4f}\")\n",
"print(f\" F1 Score: {f1_score(y_test, y_test_pred):.4f}\")\n",
"print(f\" ROC-AUC: {roc_auc_score(y_test, y_test_pred_proba):.4f}\")"
]
},
{
"cell_type": "markdown",
"id": "e826a998",
"metadata": {},
"source": [
"TO DO\n",
" * autoencoder langsam anfangen mit 19 schichten\n",
" * dann AE und SVM mit hybridem training wie bei claude?!\n",
" * dataset aus eyetracking verwenden?"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"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.12.10"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
@@ -21,3 +21,42 @@ def mad_outlier_removal(df, columns, threshold=3.5, c=1.4826):
final_mask = np.logical_and.reduce(masks) final_mask = np.logical_and.reduce(masks)
return df_clean[final_mask] return df_clean[final_mask]
def calculate_mad_params(df, columns):
"""
Calculate median and MAD parameters for each column.
This should be run ONLY on the training data.
Returns a dictionary: {col: (median, mad)}
"""
params = {}
for col in columns:
median = df[col].median()
mad = np.median(np.abs(df[col] - median))
params[col] = (median, mad)
return params
def apply_mad_filter(df, params, threshold=3.5):
"""
Apply MAD-based outlier removal using precomputed parameters.
Works on training, validation, and test data.
df: DataFrame to filter
params: dictionary {col: (median, mad)} from training data
threshold: cutoff for robust Z-score
"""
df_clean = df.copy()
for col, (median, mad) in params.items():
if mad == 0:
continue # no spread; nothing to remove for this column
robust_z = 0.6745 * (df_clean[col] - median) / mad
outlier_mask = np.abs(robust_z) > threshold
# Remove values only in this specific column
df_clean.loc[outlier_mask, col] = median
print(df_clean.shape)
return df_clean
+110
View File
@@ -0,0 +1,110 @@
database:
path: "/home/edgekit/MSY_FS/databases/database.sqlite"
table: feature_table
key: _Id
model:
path: "/home/edgekit/MSY_FS/fahrsimulator_msy2526_ai/predict_pipeline/cnn_crossVal_EarlyFusion_V2_0103.keras"
scaler:
use_scaling: true
path: "/home/edgekit/MSY_FS/fahrsimulator_msy2526_ai/predict_pipeline/scaler_crossVal_EarlyFusion_V2_0103.joblib"
mqtt:
enabled: true
host: "141.75.223.13"
port: 1883
topic: "PREDICTION"
client_id: "jetson-board"
qos: 0
retain: false
# username: ""
# password: ""
tls:
enabled: false
# ca_cert: ""
# client_cert: ""
# client_key: ""
publish_format:
result_key: prediction # where to store the predicted value in payload
include_metadata: true # e.g., timestamps, rowid, etc.
sample:
columns:
- _Id
- start_time
- FACE_AU01_mean
- FACE_AU02_mean
- FACE_AU04_mean
- FACE_AU05_mean
- FACE_AU06_mean
- FACE_AU07_mean
- FACE_AU09_mean
- FACE_AU10_mean
- FACE_AU11_mean
- FACE_AU12_mean
- FACE_AU14_mean
- FACE_AU15_mean
- FACE_AU17_mean
- FACE_AU20_mean
- FACE_AU23_mean
- FACE_AU24_mean
- FACE_AU25_mean
- FACE_AU26_mean
- FACE_AU28_mean
- FACE_AU43_mean
- Fix_count_short_66_150
- Fix_count_medium_300_500
- Fix_count_long_gt_1000
- Fix_count_100
- Fix_mean_duration
- Fix_median_duration
- Sac_count
- Sac_mean_amp
- Sac_mean_dur
- Sac_median_dur
- Blink_count
- Blink_mean_dur
- Blink_median_dur
- Pupil_mean
- Pupil_IPA
fill_nan_with_median: true
discard_if_all_nan: true
fallback:
FACE_AU01_mean: 0.7645230925040001
FACE_AU02_mean: 0.731433810144
FACE_AU04_mean: 0.19544571909800001
FACE_AU05_mean: 0.5459417841199999
FACE_AU06_mean: 0.11525241050400001
FACE_AU07_mean: 0.012
FACE_AU09_mean: 0.1025071305288
FACE_AU10_mean: 0.018860388261559197
FACE_AU11_mean: 0.4
FACE_AU12_mean: 0.06147405784940001
FACE_AU14_mean: 0.3035830324256
FACE_AU15_mean: 0.429531116458
FACE_AU17_mean: 0.59837751402
FACE_AU20_mean: 0.0
FACE_AU23_mean: 0.36847432157119997
FACE_AU24_mean: 0.460720551004
FACE_AU25_mean: 0.08549070376580001
FACE_AU26_mean: 0.15669224557279998
FACE_AU28_mean: 0.4071362423348
FACE_AU43_mean: 0.10835549767080001
Fix_count_short_66_150: 1.0
Fix_count_medium_300_500: 0.0
Fix_count_long_gt_1000: 0.0
Fix_count_100: 1.0
Fix_mean_duration: 60.869565217391305
Fix_median_duration: 40.0
Sac_count: 98.0
Sac_mean_amp: 0.12010199338955968
Sac_mean_dur: 263.5705727294512
Sac_median_dur: 160.0
Blink_count: 14.0
Blink_mean_dur: 0.38857142857142857
Blink_median_dur: 0.2
Pupil_mean: 3.2823675201416016
Pupil_IPA: 0.0036347377340156025
+253
View File
@@ -0,0 +1,253 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "fb68b447",
"metadata": {},
"source": [
"## Database creation and filling (for live system) "
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "0d70a13f",
"metadata": {},
"outputs": [],
"source": [
"import sys\n",
"sys.path.append('/home/edgekit/MSY_FS/fahrsimulator_msy2526_ai/tools')\n",
"import pandas as pd\n",
"from pathlib import Path\n",
"import db_helpers"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "ce696366",
"metadata": {},
"outputs": [],
"source": [
"# TODO: set paths and table name\n",
"database_path = Path(r\"database.sqlite\") # this path references an empty, but already created sqlite file\n",
"parquet_path = Path(r\"...parquet\") # this path leads to the data that should be used to fill the databse\n",
"table_name = \"XXX\" # name of the new table"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "b1aa9398",
"metadata": {},
"outputs": [],
"source": [
"dataset = pd.read_parquet(parquet_path)\n",
"dataset.head()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "b183746e",
"metadata": {},
"outputs": [],
"source": [
"dataset.dtypes"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "24ed769d",
"metadata": {},
"outputs": [],
"source": [
"con, cursor = db_helpers.connect_db(database_path)"
]
},
{
"cell_type": "markdown",
"id": "7007c68f",
"metadata": {},
"source": [
"Select a subset to insert into database "
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "e604ed30",
"metadata": {},
"outputs": [],
"source": [
"df_clean = dataset.drop(columns=['subjectID','rowID', 'STUDY', 'LEVEL', 'PHASE'])\n",
"df_first_100 = df_clean.head(200)\n",
"df_first_100 = df_first_100.reset_index(drop=True)\n",
"df_first_100.insert(0, '_Id', df_first_100.index + 1)"
]
},
{
"cell_type": "markdown",
"id": "92171186",
"metadata": {},
"source": [
"Type conversion"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "e77a812e",
"metadata": {},
"outputs": [],
"source": [
"def pandas_to_sqlite_dtype(dtype):\n",
" if pd.api.types.is_integer_dtype(dtype):\n",
" return \"INTEGER\"\n",
" if pd.api.types.is_float_dtype(dtype):\n",
" return \"REAL\"\n",
" if pd.api.types.is_bool_dtype(dtype):\n",
" return \"INTEGER\"\n",
" if pd.api.types.is_datetime64_any_dtype(dtype):\n",
" return \"TEXT\"\n",
" return \"TEXT\"\n"
]
},
{
"cell_type": "markdown",
"id": "45af9956",
"metadata": {},
"source": [
"Define constraints and primary key"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "0e8897b2",
"metadata": {},
"outputs": [],
"source": [
"columns = {\n",
" col: pandas_to_sqlite_dtype(dtype)\n",
" for col, dtype in df_first_100.dtypes.items()\n",
"}\n",
"\n",
"constraints = {\n",
" \"_Id\": [\"NOT NULL\"]\n",
"}\n",
"\n",
"primary_key = {\n",
" \"pk_df_first_100\": [\"_Id\"]\n",
"}\n"
]
},
{
"cell_type": "markdown",
"id": "133e92ee",
"metadata": {},
"source": [
"Create the table"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "4ab57624",
"metadata": {},
"outputs": [],
"source": [
"sql = db_helpers.create_table(\n",
" conn=con,\n",
" cursor=cursor,\n",
" table_name=table_name,\n",
" columns=columns,\n",
" constraints=constraints,\n",
" primary_key=primary_key,\n",
" commit=True\n",
")\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "25096a7f",
"metadata": {},
"outputs": [],
"source": [
"columns_to_insert = {\n",
" col: df_first_100[col].tolist()\n",
" for col in df_first_100.columns\n",
"}"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "7a5a3aa8",
"metadata": {},
"outputs": [],
"source": [
"db_helpers.insert_rows_into_table(\n",
" conn=con,\n",
" cursor=cursor,\n",
" table_name=table_name,\n",
" columns=columns_to_insert,\n",
" commit=True\n",
")\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "b56beae2",
"metadata": {},
"outputs": [],
"source": [
"request = db_helpers.get_data_from_table(conn=con, table_name='rawdata',columns_list=['*'])"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "a4a74a9d",
"metadata": {},
"outputs": [],
"source": [
"request.head()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "da0f8737",
"metadata": {},
"outputs": [],
"source": [
"db_helpers.disconnect_db(con, cursor)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "310",
"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.19"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
+11
View File
@@ -0,0 +1,11 @@
[Unit]
Description=Predict latest sample and send message
After=network.target
StartLimitIntervalSec=0
[Service]
Type=oneshot
User=edgekit
ExecStart=/home/edgekit/anaconda3/envs/p310_FS_TF/bin/python /home/edgekit/MSY_FS/fahrsimulator_msy2526_ai/predict_pipeline/predict_sample.py
[Install]
WantedBy=multi-user.target
+12
View File
@@ -0,0 +1,12 @@
[Unit]
Description=Run predict sample every 5 seconds
[Timer]
OnActiveSec=60
OnUnitActiveSec=5
AccuracySec=1s
Unit=predict.service
[Install]
WantedBy=timers.target
+247
View File
@@ -0,0 +1,247 @@
# Imports
import pandas as pd
import json
from pathlib import Path
import numpy as np
import sys
import yaml
import pickle
sys.path.append('/home/edgekit/MSY_FS/fahrsimulator_msy2526_ai/tools')
import db_helpers
import joblib
import paho.mqtt.client as mqtt
def _load_serialized(path: Path):
suffix = path.suffix.lower()
if suffix == ".pkl":
with path.open("rb") as f:
return pickle.load(f)
if suffix == ".joblib":
return joblib.load(path)
raise ValueError(f"Unsupported file format: {suffix}. Use .pkl or .joblib.")
def getLastEntryFromSQLite(path, table_name, key="_Id"):
conn, cursor = db_helpers.connect_db(path)
try:
row_df = db_helpers.get_data_from_table(
conn=conn,
table_name=table_name,
order_by={key: "DESC"},
limit=1,
)
finally:
db_helpers.disconnect_db(conn, cursor, commit=False)
if row_df.empty:
return pd.Series(dtype="object")
return row_df.iloc[0]
def callModel(sample, model_path):
if callable(sample):
raise TypeError(
f"Invalid sample type: got callable `{getattr(sample, '__name__', type(sample).__name__)}`. "
"Expected numpy array / pandas row."
)
model_path = Path(model_path)
if not model_path.is_absolute():
model_path = Path.cwd() / model_path
model_path = model_path.resolve()
suffix = model_path.suffix.lower()
if suffix in {".pkl", ".joblib"}:
model = _load_serialized(model_path)
elif suffix == ".keras":
import tensorflow
tensorflow.get_logger().setLevel("ERROR")
model = tensorflow.keras.models.load_model(model_path)
else:
raise ValueError(f"Unsupported model format: {suffix}. Use .pkl, .joblib, or .keras.")
x = np.asarray(sample, dtype=np.float32)
if x.ndim == 1:
x = x.reshape(1, -1)
if suffix == ".keras":
x_full = x
prediction = (model.predict(x_full[:, :35], verbose=0) > 0.5).astype(int)
else:
if hasattr(model, "predict"):
prediction = model.predict(x[:,:20])
elif callable(model):
prediction = model(x[:,:20])
else:
raise TypeError("Loaded model has no .predict(...) and is not callable.")
prediction = np.asarray(prediction)
if prediction.size == 1:
return prediction.item()
return prediction.squeeze()
def buildMessage(valid, result: np.int32, config_file_path, sample=None):
with Path(config_file_path).open("r", encoding="utf-8") as f:
cfg = yaml.safe_load(f)
mqtt_cfg = cfg.get("mqtt", {})
result_key = mqtt_cfg.get("publish_format", {}).get("result_key", "prediction")
sample_id = None
if isinstance(sample, pd.Series):
sample_id = sample.get("_Id", sample.get("_id"))
elif isinstance(sample, dict):
sample_id = sample.get("_Id", sample.get("_id"))
message = {
"valid": bool(valid),
"_id": sample_id,
result_key: np.asarray(result).tolist() if isinstance(result, np.ndarray) else result,
}
return message
def convert_int64(obj):
if isinstance(obj, np.int64):
return int(obj)
# If the object is a dictionary or list, recursively convert its values
elif isinstance(obj, dict):
return {key: convert_int64(value) for key, value in obj.items()}
elif isinstance(obj, list):
return [convert_int64(item) for item in obj]
return obj
def sendMessage(config_file_path, message):
# Load the configuration
with Path(config_file_path).open("r", encoding="utf-8") as f:
cfg = yaml.safe_load(f)
# Get MQTT configuration
mqtt_cfg = cfg.get("mqtt", {})
topic = mqtt_cfg.get("topic", "ml/predictions")
# Convert message to ensure no np.int64 values remain
message = convert_int64(message)
# Serialize the message to JSON
payload = json.dumps(message, ensure_ascii=False)
print(payload)
# publish via MQTT using config parameters above.
client = mqtt.Client(client_id=mqtt_cfg.get("client_id", "predictor-01"),
callback_api_version=mqtt.CallbackAPIVersion.VERSION2)
callback_api_version=mqtt.CallbackAPIVersion.VERSION2
if "username" in mqtt_cfg and mqtt_cfg.get("username"):
client.username_pw_set(mqtt_cfg["username"], mqtt_cfg.get("password"))
client.connect(mqtt_cfg.get("host", "localhost"), int(mqtt_cfg.get("port", 1883)), 60)
client.publish(
topic=topic,
payload=payload,
qos=int(mqtt_cfg.get("qos", 1)),
retain=bool(mqtt_cfg.get("retain", False)),
)
client.disconnect()
return
def replace_nan(sample, config_file_path: Path):
with config_file_path.open("r", encoding="utf-8") as f:
cfg = yaml.safe_load(f)
fallback_map = cfg.get("fallback", {})
if sample.empty:
return False, sample
nan_ratio = sample.isna().mean()
valid = nan_ratio <= 0.5
if valid and fallback_map:
sample = sample.fillna(value=fallback_map)
return valid, sample
def sample_to_numpy(sample, drop_cols=("_Id", "start_time")):
if isinstance(sample, pd.Series):
sample = sample.drop(labels=list(drop_cols), errors="ignore")
return sample.to_numpy()
if isinstance(sample, pd.DataFrame):
sample = sample.drop(columns=list(drop_cols), errors="ignore")
return sample.to_numpy()
return np.asarray(sample)
def scale_sample(sample, use_scaling=False, scaler_path=None):
if not use_scaling or scaler_path is None:
return sample
scaler_path = Path(scaler_path)
if not scaler_path.is_absolute():
scaler_path = Path.cwd() / scaler_path
scaler_path = scaler_path.resolve()
normalizer = _load_serialized(scaler_path)
# normalizer format from model_training/tools/scaler.py:
# {"scalers": {...}, "method": "...", "scope": "..."}
scalers = normalizer.get("scalers", {}) if isinstance(normalizer, dict) else {}
scope = normalizer.get("scope", "global") if isinstance(normalizer, dict) else "global"
if scope == "global":
scaler = scalers.get("global")
else:
scaler = scalers.get("global", next(iter(scalers.values()), None))
# Optional fallback if the stored object is already a raw scaler.
if scaler is None and hasattr(normalizer, "transform"):
scaler = normalizer
if scaler is None or not hasattr(scaler, "transform"):
return sample
df = sample.to_frame().T if isinstance(sample, pd.Series) else sample.copy()
feature_names = getattr(scaler, "feature_names_in_", None)
if feature_names is None:
return sample
# Keep columns not in the normalizer unchanged.
cols_to_scale = [c for c in df.columns if c in set(feature_names)]
if cols_to_scale:
df.loc[:, cols_to_scale] = scaler.transform(df.loc[:, cols_to_scale])
return df.iloc[0] if isinstance(sample, pd.Series) else df
def main():
pd.set_option('future.no_silent_downcasting', True) # kann ggf raus
config_file_path = Path("/home/edgekit/MSY_FS/fahrsimulator_msy2526_ai/predict_pipeline/config.yaml")
with config_file_path.open("r", encoding="utf-8") as f:
cfg = yaml.safe_load(f)
database_path = cfg["database"]["path"]
table_name = cfg["database"]["table"]
row_key = cfg["database"]["key"]
sample = getLastEntryFromSQLite(database_path, table_name, row_key)
valid, sample = replace_nan(sample, config_file_path=config_file_path)
if not valid:
print("Sample invalid: more than 50% NaN.")
message = buildMessage(valid, None, config_file_path, sample=sample)
sendMessage(config_file_path, message)
return
model_path = cfg["model"]["path"]
scaler_path = cfg["scaler"]["path"]
use_scaling = cfg["scaler"]["use_scaling"]
sample = scale_sample(sample, use_scaling=use_scaling, scaler_path=scaler_path)
sample_np = sample_to_numpy(sample)
prediction = callModel(model_path=model_path, sample=sample_np)
message = buildMessage(valid, prediction, config_file_path, sample=sample)
sendMessage(config_file_path, message)
if __name__ == "__main__":
main()
@@ -0,0 +1,216 @@
# Predict Service and Timer Documentation
## Overview
This setup uses **systemd services and timers** to repeatedly execute a
Python script that performs prediction on the latest sample and sends a
message.
The systemd unit files are typically stored in:
/etc/systemd/system/
For this setup, the relevant files are:
/etc/systemd/system/predict.service
/etc/systemd/system/predict.timer
These files define the service execution and the timer scheduling.
- `predict.service` -- defines how the Python script is executed
- `predict.timer` -- schedules the repeated execution of the service
The timer triggers the service **every 5 seconds** after the first
activation.
------------------------------------------------------------------------
# Systemd Timer
File: `predict.timer`
``` ini
[Unit]
Description=Run predict sample every 5 seconds
[Timer]
OnActiveSec=60
OnUnitActiveSec=5
AccuracySec=1s
Unit=predict.service
[Install]
WantedBy=timers.target
```
## Behavior
- **OnActiveSec=60**\
The timer starts **60 seconds after it is activated**.
- **OnUnitActiveSec=5**\
After the service has run once, it will be triggered again **every 5
seconds**.
- **AccuracySec=1s**\
Allows systemd to schedule the timer with **1 second precision**.
- **Unit=predict.service**\
Defines which service should be triggered by the timer.
------------------------------------------------------------------------
# Systemd Service
File: `predict.service`
``` ini
[Unit]
Description=Predict latest sample and send message
After=network.target
StartLimitIntervalSec=0
[Service]
Type=oneshot
User=edgekit
ExecStart=/home/edgekit/anaconda3/envs/p310_FS_TF/bin/python /home/edgekit/MSY_FS/fahrsimulator_msy2526_ai/predict_pipeline/predict_sample.py
[Install]
WantedBy=multi-user.target
```
## Behavior
- **Type=oneshot**\
The service runs the script once and then exits.
- **User=edgekit**\
The script is executed under the `edgekit` user.
- **ExecStart**\
Executes the Python script using the specified conda environment.
- **After=network.target**\
Ensures the service only runs after the network is available.
------------------------------------------------------------------------
# Execution Flow
1. The **timer starts** after it is enabled.
2. After **60 seconds**, the first execution happens (this results from the duration of the camera processing initialization)
3. The timer triggers `predict.service`.
4. The service runs `predict_sample.py`.
5. Once the script finishes, the service exits.
6. The timer triggers the service again **every 5 seconds**.
------------------------------------------------------------------------
# Debugging and Monitoring
## View Live Output
All `print()` output from the Python script is written to the **systemd
journal**.
Follow the output live with:
``` bash
journalctl -u predict.service -f
```
This command is typically the most useful for debugging.
------------------------------------------------------------------------
# Common Systemd Commands
## Check Service Status
``` bash
systemctl status predict.service
```
Shows the last execution result and recent log lines.
------------------------------------------------------------------------
## Check Timer Status
``` bash
systemctl status predict.timer
```
Shows when the timer last ran and when it will run next.
------------------------------------------------------------------------
## List All Timers
``` bash
systemctl list-timers
```
Displays all active timers and their next scheduled execution.
------------------------------------------------------------------------
# Manual Execution
To run the service manually once:
``` bash
systemctl start predict.service
```
------------------------------------------------------------------------
# Restarting the Systemd Units
## Restart the Service
``` bash
systemctl restart predict.service
```
## Restart the Timer
``` bash
systemctl restart predict.timer
```
------------------------------------------------------------------------
## Reload Systemd After Changes
If `.service` or `.timer` files were modified:
``` bash
systemctl daemon-reload
systemctl restart predict.timer
systemctl restart predict.service
```
------------------------------------------------------------------------
## Enabling the Timer
To ensure the timer starts automatically on system boot:
``` bash
systemctl enable predict.timer
systemctl start predict.timer
```
------------------------------------------------------------------------
# Summary
- `predict.timer` schedules periodic execution.
- `predict.service` runs the Python prediction script.
- The script runs **every 5 seconds** after the initial delay.
- Logs and script output are available through:
``` bash
journalctl -u predict.service -f
```
+196
View File
@@ -0,0 +1,196 @@
name: 'prediction_env'
channels:
- defaults
- conda-forge
dependencies:
- _py-xgboost-mutex=2.0=cpu_2
- absl-py=2.3.1=py310haa95532_0
- aom=3.12.1=h00a0c3c_0
- arrow-cpp=21.0.0=hcdc3a1c_2
- asttokens=3.0.1=pyhd8ed1ab_0
- astunparse=1.6.3=py_0
- aws-c-auth=0.9.0=h02ab6af_2
- aws-c-cal=0.9.2=h02ab6af_1
- aws-c-common=0.12.4=h02ab6af_0
- aws-c-compression=0.3.1=h02ab6af_2
- aws-c-event-stream=0.5.6=h02ab6af_0
- aws-c-http=0.10.4=h02ab6af_0
- aws-c-io=0.21.4=h02ab6af_0
- aws-c-mqtt=0.13.3=h02ab6af_0
- aws-c-s3=0.8.7=h02ab6af_0
- aws-c-sdkutils=0.2.4=h02ab6af_1
- aws-checksums=0.2.7=h02ab6af_1
- aws-crt-cpp=0.34.0=h885b0b7_0
- aws-sdk-cpp=1.11.638=hf0af688_0
- blas=1.0=mkl
- brotlicffi=1.2.0.0=py310h885b0b7_0
- bzip2=1.0.8=h2bbff1b_6
- c-ares=1.34.6=h2c209ce_0
- ca-certificates=2026.1.4=h4c7d964_0
- cairo=1.18.4=he9e932c_0
- certifi=2026.01.04=py310haa95532_0
- cffi=2.0.0=py310h02ab6af_1
- charset-normalizer=3.4.4=py310haa95532_0
- colorama=0.4.6=pyhd8ed1ab_1
- comm=0.2.3=pyhe01879c_0
- dav1d=1.2.1=h2bbff1b_0
- debugpy=1.8.20=py310h699e580_0
- decorator=5.2.1=pyhd8ed1ab_0
- exceptiongroup=1.3.1=pyhd8ed1ab_0
- executing=2.2.1=pyhd8ed1ab_0
- expat=2.7.4=hd7fb8db_0
- flatbuffers=24.3.25=h21716d4_0
- fontconfig=2.15.0=hd211d86_0
- freeglut=3.8.0=hfcef157_0
- freetype=2.14.1=hfbffc0b_0
- fribidi=1.0.16=haf45083_0
- gast=0.7.0=pyhd3eb1b0_0
- gflags=2.2.2=hd77b12b_1
- giflib=5.2.2=h7edc060_0
- glog=0.5.0=hd77b12b_1
- google-pasta=0.2.0=pyhd3eb1b0_0
- graphite2=1.3.14=hd77b12b_1
- grpcio=1.74.1=py310h5c751cc_0
- h5py=3.15.1=py310he283ef2_1
- harfbuzz=12.3.0=h3ef6528_1
- hdf5=1.14.5=ha36df97_2
- icc_rt=2022.1.0=h6049295_2
- icu=73.1=h6c2663c_0
- idna=3.11=py310haa95532_0
- intel-openmp=2025.0.0=haa95532_1164
- ipykernel=7.2.0=pyh6dadd2b_1
- ipython=8.37.0=pyha7b4d00_0
- jedi=0.19.2=pyhd8ed1ab_1
- joblib=1.5.3=py310haa95532_0
- jpeg=9f=ha349fce_0
- jupyter_client=8.8.0=pyhcf101f3_0
- jupyter_core=5.9.1=pyh6dadd2b_0
- keras=3.11.2=py310h51baaa3_0
- krb5=1.21.3=hdf4eb48_0
- lcms2=2.17=h3732fa5_0
- lerc=4.0.0=h5da7b33_0
- libabseil=20250814.1=cxx17_hcd311fc_0
- libavif=1.3.0=h5bd13ec_0
- libbrotlicommon=1.2.0=h907acca_0
- libbrotlidec=1.2.0=h02c67a5_0
- libbrotlienc=1.2.0=h483e6b9_0
- libcurl=8.17.0=h6e672f4_1
- libdeflate=1.22=h5bf469e_0
- libexpat=2.7.4=hd7fb8db_0
- libffi=3.4.4=hd77b12b_1
- libglib=2.86.3=h9bccc14_0
- libgrpc=1.74.1=hde67744_0
- libhwloc=2.12.1=default_hfa10c62_1000
- libiconv=1.16=h2bbff1b_3
- libkrb5=1.22.1=hb237eb7_0
- libopenjpeg=2.5.4=h02ab6af_1
- libpng=1.6.54=ha15c746_0
- libprotobuf=6.33.0=h2a56892_1
- libre2-11=2025.11.05=ha6b10e7_0
- libsodium=1.0.20=hc70643c_0
- libssh2=1.11.1=h2addb87_0
- libthrift=0.22.0=ha2884a9_0
- libtiff=4.7.1=h3a18249_0
- libwebp-base=1.6.0=hbf3958f_0
- libxgboost=3.1.2=h585ebfc_0
- libxml2=2.13.9=h6201b9f_0
- libzlib=1.3.1=h02ab6af_0
- lz4-c=1.9.4=h2bbff1b_1
- m2w64-gcc-libgfortran=5.3.0=6
- m2w64-gcc-libs=5.3.0=7
- m2w64-gcc-libs-core=5.3.0=7
- m2w64-gmp=6.1.0=2
- m2w64-libwinpthread-git=5.0.0.4634.697f757=2
- markdown=3.10=py310haa95532_0
- markdown-it-py=2.2.0=py310haa95532_1
- markupsafe=3.0.2=py310h827c3e9_0
- matplotlib-inline=0.2.1=pyhd8ed1ab_0
- mdurl=0.1.2=py310haa95532_0
- mkl=2025.0.0=h5da7b33_930
- mkl-service=2.5.2=py310h0b37514_0
- mkl_fft=2.1.1=py310h300f80d_0
- mkl_random=1.3.0=py310ha5e6156_0
- ml_dtypes=0.5.4=py310h42c1672_0
- mpi=1.0=msmpi
- mpi4py=4.0.3=py310h02ab6af_1
- msmpi=10.1.1=had4844c_0
- msys2-conda-epoch=20160418=1
- namex=0.1.0=py310haa95532_0
- nest-asyncio=1.6.0=pyhd8ed1ab_1
- numpy-base=2.1.3=py310he4e2855_3
- openssl=3.6.1=hf411b9b_1
- opt_einsum=3.3.0=pyhd3eb1b0_1
- optree=0.18.0=py310h03f52e7_0
- orc=2.2.0=h79e1e1e_1
- packaging=25.0=py310haa95532_1
- paho-mqtt=2.1.0=pyhe01879c_1
- parso=0.8.6=pyhcf101f3_0
- pcre2=10.46=h5740b90_0
- pickleshare=0.7.5=pyhd8ed1ab_1004
- pillow=12.1.0=py310h6b7a805_0
- pip=26.0.1=pyhc872135_0
- pixman=0.46.4=h4043f72_0
- platformdirs=4.9.2=pyhcf101f3_0
- prompt-toolkit=3.0.52=pyha770c72_0
- protobuf=6.33.0=py310ha4c6e68_0
- psutil=7.2.2=py310h1637853_0
- pure_eval=0.2.3=pyhd8ed1ab_1
- py-xgboost=3.1.2=py310haa95532_0
- pyarrow=21.0.0=py310h42c1672_1
- pycparser=2.23=py310haa95532_0
- pygments=2.19.2=pyhd8ed1ab_0
- pysocks=1.7.1=py310haa95532_1
- python=3.10.19=h981015d_0
- python-dateutil=2.9.0.post0=pyhe01879c_2
- python-flatbuffers=24.3.25=py310haa95532_0
- python_abi=3.10=2_cp310
- pywin32=311=py310h282bd7d_1
- pyyaml=6.0.3=py310hb9a58be_0
- pyzmq=27.1.0=py310h535538e_0
- re2=2025.11.05=hc24cdf5_0
- requests=2.32.5=py310haa95532_1
- rich=14.2.0=py310haa95532_0
- scipy=1.15.3=py310h1bbe36f_1
- setuptools=80.10.2=py310haa95532_0
- six=1.17.0=pyhe01879c_1
- snappy=1.2.2=hab6b7b3_1
- sqlite=3.51.1=hda9a48d_0
- stack_data=0.6.3=pyhd8ed1ab_1
- tbb=2022.3.0=h90c84d6_0
- tbb-devel=2022.3.0=h90c84d6_0
- tensorboard=2.20.0=py310haa95532_0
- tensorboard-data-server=0.7.0=py310haa95532_1
- tensorflow=2.20.0=cpu_py310h6605a60_0
- tensorflow-base=2.20.0=cpu_py310hce87ebc_0
- termcolor=3.2.0=py310haa95532_0
- threadpoolctl=3.5.0=py310h4442805_1
- tk=8.6.15=hf199647_0
- tornado=6.5.4=py310h29418f3_0
- traitlets=5.14.3=pyhd8ed1ab_1
- typing-extensions=4.15.0=py310haa95532_0
- typing_extensions=4.15.0=py310haa95532_0
- ucrt=10.0.22621.0=haa95532_0
- urllib3=2.6.3=py310haa95532_0
- utf8proc=2.6.1=h2bbff1b_1
- vc=14.42=haa95532_5
- vc14_runtime=14.44.35208=h4927774_10
- vs2015_runtime=14.44.35208=ha6b5a95_10
- wcwidth=0.6.0=pyhd8ed1ab_0
- werkzeug=3.1.3=py310haa95532_0
- wheel=0.46.3=py310haa95532_0
- win_inet_pton=1.1.0=py310haa95532_1
- wrapt=2.0.1=py310h02ab6af_0
- xgboost=3.1.2=py310haa95532_0
- xz=5.6.4=h4754444_1
- yaml=0.2.5=he774522_0
- zeromq=4.3.5=h5bddc39_9
- zlib=1.3.1=h02ab6af_0
- zstd=1.5.7=h56299aa_0
- pip:
- numpy==1.24.4
- pandas==2.3.0
- pyocclient==0.6
- pytz==2025.2
- scikit-learn==1.6.1
- tzdata==2025.3
+551
View File
@@ -0,0 +1,551 @@
# Project Report: Multimodal Driver State Analysis
## 1) Project Scope
This repository implements an end-to-end workflow for multimodal driver-state analysis in a simulator setup.
The system combines:
- Facial Action Units (AUs)
- Eye-tracking features (fixations, saccades, blinks, pupil behavior)
Apart from this, several machine learning model architectures are presented and evaluated.
Content:
- Dataset generation
- Exploratory data analysis
- Model training experiments
- Real-time inference with SQlite, systemd and MQTT
- Repository file inventory
- Additional nformation
## 2) Dataset generation
### 2.1 Data Access, Filtering, and Data Conversion
Main scripts:
- `dataset_creation/create_parquet_files_from_owncloud.py`
- `dataset_creation/parquet_file_creation.py`
Purpose:
- Download and/or access dataset files (either download first via ```EDA/owncloud_file_access.ipynb``` or all in one with ```dataset_creation/create_parquet_files_from_owncloud.py```
- Keep relevant columns (FACE_AUs and eye-tracking raw values)
- Filter invalid samples (e.g., invalid level segments): Make sure not to drop rows where NaN is necessary for later feature creation, therefore use subset argument in dropNa()!
- Export subject-level parquet files
- Before running the scripts: be aware that the whole dataset contains 30 files with around 900 Mbytes each, provide enough storage and expect this to take a while.
### 2.2 Feature Engineering (Offline)
Main script:
- `dataset_creation/combined_feature_creation.py`
Behavior:
- Builds fixed-size sliding windows over subject time series (window size and step size can be adjusted)
- Uses prepared parquet files from 2.1
- Aggregates AU statistics per window (e.g., `FACE_AUxx_mean`)
- Computes eye-feature aggregates (fix/sacc/blink/pupil metrics)
- Produces training-ready feature tables = dataset
- Parameter ```MIN_DUR_BLINKS``` can be adjusted, although this value needs to make sense in combination with your sampling frequency
- With low videostream rates, consider to reevaluate the meaningfulness of some eye-tracking features, especially the fixations
- running the script requires a manual installation of [pygaze Analyser library](https://github.com/esdalmaijer/PyGazeAnalyser.git) from github
### 2.3 Online Camera + Eye + AU Feature Extraction
Main scripts:
- `dataset_creation/camera_handling/camera_stream_AU_and_ET_new.py`
- `dataset_creation/camera_handling/eyeFeature_new.py`
- `dataset_creation/camera_handling/db_helper.py`
Runtime behavior:
- Captures webcam stream with OpenCV
- Extracts gaze/iris-based signals via MediaPipe
- Records overlapping windows (`VIDEO_DURATION=50s`, `START_INTERVAL=5s`, `FPS=25`)
- Runs AU extraction (`py-feat`) from recorded video segments
- Explanation of the py-feat functionality is located in `dataset_creation/AU_creation/pyfeat_docu.ipynb`
- Computes eye-feature summary from generated gaze parquet
- Writes merged rows to SQLite table `feature_table`
Operational note:
- `DB_PATH` and other paths are currently code-configured and must be adapted per deployment.
### 2.4 Two Approaches to Eye-Tracking Data Collection
Eye-tracking can be implemented using two main approaches:
Used Approach: Relative Iris Position
- Tracks the position of the pupil within the eye region
- The position is normalized relative to the eye itself
- No reference to the screen or physical environment is required
Not Used Approach: Screen Calibration
- Requires the user to look at 9 predefined points on the screen
- A mapping model is trained based on these points
- Establishes a relationship between eye movement and screen coordinates
Important Considerations (for both methods)
- Keep the head as still as possible
- Ensure consistent and even lighting conditions
## 3) EDA
The directory EDA provides several files to get insights into both the raw data from AdaBase and your own dataset.
- `EDA.ipynb` - Main EDA notebook: recreates the plot from AdaBase documentation, lists all experiments and in general serves as a playground for you to get to know the files.
- `distribution_plots.ipynb` - This notebook aimes to visualize the data distributions for each experiment - the goal is the find out, whether the split of experiments into high and low cognitive load is clearer if some experiments are dropped.
- `histogramms.ipynb` - Histogram analysis of low load vs high load per feature. Additionaly, scatter plots per feature are available.
- `researchOnSubjectPerformance.ipynb` - This noteboooks aims to see how the performance values range for the 30 subjects. The code creates and saves a table in csv-format, which will later be used as the foundation of the performance based split in ```model_training/tools/performance_based_split```
- `owncloud_file_access.ipynb` - Get access to the files via owncloud and safe them as .h5 files, in correspondence to the parquet file creation script
- `login.yaml` -Used to store URL and password to access files from owncloud, used in previous notebook
- `calculate_replacement_values.ipynb` -Fallback / median computation notebook for deployment, creation of yaml syntax embedding
General information:
- Due to their size, its absolutely recommended to download and save the dataset files once in the beginning
- For better data understanding, read the [AdaBase publication](https://www.mdpi.com/1424-8220/23/1/340)
## 4) Model Training
Included model families:
- CNN variants (different fusion strategies)
- XGBoost
- Isolation Forest*
- OCSVM*
- DeepSVDD*
\* These training strategies are unsupervised, which means only low cognitive load samples are used for training. Validation then also considers high low samples.
Supporting utilities in ```model_training/tools```:
- `scaler.py`: Functions to fit, transform, save and load either MinMaxScaler or StandardScaler, subject-wise and globally - for new subjects, a fallback scaler (using mean of all subjects scaling parameters) is used
- `performance_split.py`: Provides a function to split a group of subjects based on their performance in the AdaBase experiments, based on the results created in `researchOnSubjectPerformance.ipynb`. To split into three groups for train, validation & test, call the function twice
- `mad_outlier_removal.py`: Functions to fit and transform data with MAD outlier removal
- `evaluation_tools.py`: Especially used for Isolation Forest, Functions for ROC curve as well as confusion matrix
### 4.1 CNNs
This section summarizes all CNN‑based supervised learning approaches implemented in the project.
All models operate on facial Action Unit (AU) features and, depending on the notebook, additional eye‑tracking features.
The notebooks differ in evaluation methodology, fusion strategy, and experimental intention.
### 4.1.1 Baseline CNN (Notebook: *CNN_simple*)
The first notebook implements a simple 1D CNN to establish a baseline for AU‑only classification.
The model uses two convolutional layers, batch normalization, max pooling, and a regularized dense head.
A single subject‑exclusive train/validation/test split is used.
The intention behind this notebook is to:
- Provide a baseline performance level
- Validate that AU features contain discriminative information
- Identify overfitting tendencies before moving to more rigorous evaluation
### 4.1.2 Cross‑Validated CNN (Notebook: *CNN_crossVal*)
This notebook introduces 5‑fold GroupKFold cross‑validation, ensuring subject‑exclusive folds.
The architecture is similar to the baseline but includes stronger regularization and a lower learning rate.
The intention behind this notebook is to:
- Provide robust generalization estimates
- Reduce variance caused by single‑split evaluation
- Establish a cross‑validated AU‑only benchmark
### 4.1.3 Cross‑Validated CNN (Face AUs Only) (Notebook: *CNN_crossVal_faceAUs*)
This notebook is a streamlined version of the previous one, removing unused eye‑tracking features and focusing exclusively on AUs.
The intention behind this notebook is to:
- Provide a clean AU‑only benchmark
- Improve reproducibility and interpretability
- Prepare for multimodal comparisons
### 4.1.4 Cross‑Validated CNN with Early Fusion (AUs + Eye Features)
(Notebook: *CNN_crossVal_faceAUs_eyeFeatures*)
This notebook introduces early fusion, concatenating AU and eye‑tracking features into a single input vector.
The architecture remains identical to AU‑only models.
The intention behind this notebook is to:
- Evaluate whether multimodal early fusion improves performance
- Establish a first multimodal baseline
- Analyze class‑specific behavior via confusion matrices
This notebook didn't lead to any useful results.
### 4.1.5 Cross‑Validated CNN with Early Fusion (Refined Version) (Notebook: *CNN_crossVal_EarlyFusion*)
This notebook refines the early‑fusion approach by removing samples with missing values and ensuring consistent multimodal input quality.
The intention behind this notebook is to:
- Provide a clean and fully validated early‑fusion model
- Investigate multimodal complementarity under rigorous CV
- Improve interpretability through aggregated confusion matrices
### 4.1.6 Cross‑Validated CNN with Early Fusion and Subset Filtering (Notebook: *CNN_crossVal_EarlyFusion_Filter*)
This notebook applies domain‑specific filtering to isolate a more homogeneous subset of cognitive states before training.
The intention behind this notebook is to:
- Evaluate whether subset filtering improves multimodal learning
- Reduce dataset heterogeneity
- Provide a controlled multimodal benchmark
### 4.1.7 Hybrid‑Fusion CNN (Notebook: *CNN_crossVal_HybridFusion*)
This notebook introduces a hybrid‑fusion architecture with two modality‑specific branches:
- A 1D CNN for AUs
- A dense MLP for eye‑tracking features
The branches are fused before classification.
The intention behind this notebook is to:
- Allow each modality to learn specialized representations
- Evaluate whether hybrid fusion outperforms early fusion
- Provide a strong multimodal benchmark
### 4.1.8 Early‑Fusion CNN with Independent Test Evaluation (Notebook: *CNN_crossVal_EarlyFusion_Test_Eval*)
This notebook introduces the first true held‑out test evaluation for an early‑fusion CNN.
A subject‑exclusive train/test split is created before cross‑validation.
The intention behind this notebook is to:
- Provide a deployment‑realistic performance estimate
- Compare validation‑fold behavior with true test‑set behavior
- Visualize ROC and PR curves for threshold analysis
| Metric / Model | CNN_crossVal_EarlyFusion_Test_Eval |
|----------------|-------------------------------------|
| Test Accuracy | 0.913 |
| Test F1 | 0.927 |
| Test AUC | 0.967 |
| Balanced Accuracy | 0.907 |
| Precision | 0.918 |
| Recall | 0.937 |
#### Confusion Matrix
![Konfusionsmatrix](results/Konfusionsmatrix_EarlyFusion.png)
*Figure 4.1.8.1: Confusion matrix of the Early‑Fusion model.*
#### ROC-Curve
![ROC-Kurve](results/ROC_EarlyFusion.png)
*Figure 4.1.8.2: ROC-Curve of the Early‑Fusion model.*
### 4.1.9 Hybrid‑Fusion CNN with Independent Test Evaluation (Notebook: *CNN_crossVal_HybridFusion_Test_Eval*)
This notebook extends hybrid fusion with a subject‑exclusive train/test split and full test‑set evaluation.
The intention behind this notebook is to:
- Evaluate hybrid fusion under realistic deployment conditions
- Compare hybrid vs. early fusion on unseen subjects
- Provide full diagnostic plots (ROC, PR, confusion matrices)
| Metric / Model | CNN_crossVal_HybridFusion_Test_Eval |
|----------------|--------------------------------------|
| Test Accuracy | 0.950 |
| Test F1 | 0.959 |
| Test AUC | 0.983 |
| Balanced Accuracy | 0.942 |
| Precision | 0.933 |
| Recall | 0.986 |
#### Confusion Matrix
![Konfusionsmatrix](results/Konfusionsmatrix_HybridFusion.png)
*Figure 4.1.9.1: Confusion matrix of the Hybrid‑Fusion model.*
#### ROC-Curve
![ROC-Kurve](results/ROC_HybridFusion.png)
*Figure 4.1.9.2: ROC-Curve of the Hybrid‑Fusion model.*
### 4.1.10 Summary
Across all nine notebooks, the project progresses from a simple AU‑only baseline to advanced multimodal hybrid‑fusion architectures with independent test evaluation.
The final experiments revealed that hybrid fusion provides a measurable performance advantage over early fusion. While both approaches achieve strong results, the hybrid‑fusion model reaches higher overall accuracy (95% vs. 91.3%) and substantially stronger recall (98.6% vs. 93.7%), indicating that it is more effective at correctly identifying high‑workload samples.
Early fusion, however, shows slightly better precision, suggesting that it produces fewer false positives.
Looking ahead, further improvements could likely be achieved through more extensive hyperparameter tuning, as the current results suggest that additional optimization headroom remains.
### 4.2 XGBoost
This documentation outlines the evolution of the XGBoost classification pipeline for cognitive workload detection. The project transitioned from a basic unimodal setup to a sophisticated, multi-stage hybrid system incorporating advanced statistical filtering and deep feature extraction.
During the model creation several methods were used to improve the model accuracy. During training, the biggest challenge was always the high overfitting of the model. Even in the last version with explicit regulation parameters the overall accuracy couldn't be improved more than the different methods before.
The model overall was not that good, as the highest accuracy we could achieve was around 65%, which is a bit higher than Fraunhofer achieved in the ADABase-Paper.
### 4.2.1 Classical XGBoost Baseline
To establish a performance baseline, a classical Extreme Gradient Boosting (XGBoost) model was implemented. XGBoost was selected for its ability to handle non-linear relationships and its inherent regularization, which helps prevent overfitting in high-dimensional feature spaces like Facial Action Units. XGBoost was picked because of its usage in the ADABase Paper. Initially, the model utilized raw Action Unit sums with global normalization to determine the basic predictability of workload from facial muscle activity alone.
| Metric / Model | Classical XGBoost |
| --- | --- |
| Accuracy | 0.581 |
| AUC | 0.562 |
| F1-Score | 0.652 |
### 4.2.2 XGBoost with GroupKFold Validation
To address the challenge of inter-subject variability, the validation strategy was upgraded to `GroupKFold`. In behavioral data, samples from the same subject are highly correlated. Standard cross-validation often leads to data leakage, where the model memorizes individual facial characteristics. By ensuring that a subject's data is never shared between the training and validation sets, this iteration provides a scientifically rigorous measure of how the model generalizes to entirely unseen individuals.
| Metric / Model | XGBoost (GroupKFold) |
| --- | --- |
| Accuracy | 0.586 |
| AUC | 0.573 |
| F1-Score | 0.651 |
### 4.2.3 Hybrid XGBoost with Autoencoder
To improve feature quality, a hybrid approach was introduced by pre-training a deep Autoencoder. The encoder branch was used to compress 20 raw Action Units into a 5-dimensional latent space. This non-linear dimensionality reduction aims to capture muscle synergies and filter out noise that decision trees might struggle with. The XGBoost classifier was then trained on these machine-learned representations rather than raw inputs.
| Metric / Model | XGBoost + Autoencoder |
| --- | --- |
| Accuracy | 0.589 |
| AUC | 0.575 |
| F1-Score | 0.650 |
### 4.2.4 Robust XGBoost with MAD Outlier Removal
Recognizing that physiological and AU data often contain sensor artifacts, a robust preprocessing layer was added using Median Absolute Deviation (MAD). Unlike standard deviation, MAD is resilient to extreme outliers. By calculating a Robust Z-score and filtering signals in the training set, the model learned from a "clean" representation of cognitive states, significantly improving the stability of the gradient boosting process.
| Metric / Model | XGBoost + MAD |
| --- | --- |
| Accuracy | 0.641 |
| AUC | 0.610 |
| F1-Score | 0.733 |
### 4.2.5 Combined Dataset of Action Units and EyeTracking
This iteration involved training, which refined a robust pipeline on a new, expanded dataset. This dataset integrated both high-frequency facial action units and advanced eye-tracking metrics (pupillometry and fixations).
Since the recreation of the EyeTracking data in the lab was in doubt, only Action Units were used in the first XGBoost models. Now the model also implemented the EyeTracking data as features.
By applying performance-based subject splitting, we ensured that the training and test sets were balanced not only by label but by the subjects' underlying skill levels, resulting in the most deployable version of the AI.
| Metric / Model | Final Combined Model |
| --- | --- |
| Accuracy | 0.659 |
| AUC | 0.621 |
| F1-Score | 0.715 |
### 4.2.6 Regularized XGBoost with Complexity Control
Building upon the robust preprocessing of the previous steps, this iteration focuses on strict **complexity control** within the XGBoost architecture. To mitigate the 100% training accuracy observed in earlier unimodal tests—a clear indicator of overfitting—we introduced explicit **L1 (reg_alpha)** and **L2 (reg_lambda)** regularization parameters into the GridSearch space.
By penalizing large weights and promoting feature sparsity, the model is forced to prioritize the most globally relevant Action Units. Furthermore, the tree depth was intentionally restricted (`max_depth`: 2-4), and an **Early Stopping** callback with a 30-round patience window was implemented. This ensures that training terminates at the point of optimal generalization, capturing the essential physiological trends of cognitive load while ignoring subject-specific noise.
| Metric / Model | Regularized XGBoost |
| --- | --- |
| Accuracy | 0.665 |
| AUC | 0.646 |
| F1-Score | 0.727 |
### 4.3 Isolation Forest
To start with unsupervised learning techniques, `IsolationForest.ipynb`was created to research how well a simple ensemble classificator performs on the created dataset.
The notebook comes with one class grid search for hyperparameter tuning as well as a ROC curve that allows manual fine tuning.
Overall, our experiments have shown, that this approach is not sufficient, with the following results:
| Metric / Model | Isolation Forest |
|----------------|---------|
| Best Balanced Accuracy |0.57|
| Best AUC | 0.61|
In detail, the classificator tends to classify the majority of samples as low load and is therefore not sufficient to be used for later deployment.
### 4.4 One Class SVM with Autoencoder
The training of an On Class SVM on the data from the dataset resulted in every sample was predicted as an anomaly.
In the next step, an autoencoder is pretrained to learn representation of the data. Afterwards, the encoder is used for preprocessing, which leads to OCSVM training on encoder output.
The training includes hyperparameter tuning through gridsearch cv.
Encoder output is visualized with print statements and plots that show the encoded data for both low and high load samples.
We see that the encoder struggles to represent the unseen high load samples differently. As a consequence, the One Class SVM also does not achieve sufficient performance.
| Metric / Model | One Class SVM |
|----------------|---------|
| Best Balanced Accuracy |0.62|
When the notebook is run completely, both the trained encoder and svm are saved for later use given the save paths are set correctly.
### 4.5 Deep SVDD
Similar to the OCSVM training, an autoencoder is used to preprocess the data before the actual Deep SVDD training. Nevertheless, the usage is partialy different. The Dee SVDD uses a pretrained encoder to fine tune it by apllying a different loss function (which results from the theoretical concept behind Deep SVDD). This means that the encoder weights are still modified in the actual Deep SVDD training.
Also, this approach includes **hybrid fusion of modalities**. Instead of putting all features into the same input layer, the neural network is divided into two branches, that process action units and eye-tracking features separately.
Then, after two Dense layers each, the branches are fusioned by concatenation. From there, another two Dense layers process the data.
The decoder is not exactly similar, as the split of the modalities happens are the very end.
To compute the total loss, loss from both modalities is combined by sum. Users are able to change loss weights. Training includes 2x2 phases, both autoencoder and later Deep SVDD are first trained with larger learning rate, then fine tuned with a smaller learning rate.
| Metric / Model | Deep SVDD |
|----------------|---------|
| Best Balanced Accuracy |0.60|
| Best AUC | 0.57|
### 4.6 General information on unsupervised approaches
As described above, the approachs didn't meet the requirements in terms of prediction performance. For all models, both MinMax-Scaling as well as Standard-Scaling was done. Also, both subjectwise and globally. Unfortunately, the differences were not that large, which may explain why preprocessing wasn't mentioned above.
Future research should always keep in mind while subject-wise scaling might be better for training, it makes deployment on new subjects more difficult. Our solution, as implemented in `model_training/tools/scaler.py` calculates a fallback scaler (using mean of all subjects scaling parameters).
## 5) Real-Time Prediction and Messaging
Main script:
- `predict_pipeline/predict_sample.py`
Pipeline:
- Loads runtime config (`predict_pipeline/config.yaml`)
- Pulls latest row from SQLite
- Replaces missing values using `fallback` map from config file - if more than 50% of values need to be replaced, the sample is dropped and "valid=False"
- Optionally applies scaler (`.pkl`/`.joblib`) - set via config file
- Loads model (`.keras`, `.pkl`, `.joblib`) and predicts
- Publishes JSON payload to MQTT topic
Expected payload form:
```json
{
"valid": true, # false only if too many signals are invalid
"_id": 123, # this is the sample ID from the database
"prediction": 0 # 0 for low load, 1 for high load
}
```
### 5.1 Scheduled Prediction (Linux)
Files:
- `predict_pipeline/predict.service`
- `predict_pipeline/predict.timer`
Role:
- Run inference repeatedly without manual execution
- Timer/service configuration can be customized
More information on how to use and interact with the system service and timer can be found in [predict_service_timer_documentation.md](/predict_pipeline/predict_service_timer_documentation.md)
## 5.2 Runtime Configuration
Primary config file:
- `predict_pipeline/config.yaml`
Sections:
- `database`: SQLite location + table + sort key
- `model`: model path
- `scaler`: scaler usage + path
- `mqtt`: broker and publish format
- `sample.columns`: expected feature order
- `fallback`: default values for NaN replacement
Important:
- The repository currently uses environment-specific absolute paths in some scripts/configs to ensure functionality on Ohm-UX driving simulator.
## 5.3 Data and Feature Expectations
Prediction expects SQLite rows containing:
- `_Id`
- `start_time` - this is not yet used for either predictions or messages
- All configured model features (AUs + eye metrics)
Common feature groups (similar to own dataset):
- `FACE_AUxx_mean` columns
- Fixation counters and duration statistics
- Saccade count/amplitude/duration statistics
- Blink count/duration statistics
- Pupil mean and IPA
## 5.4 Create database from scratch
To (re-)create the custom database for deployment, use `fill_db.ipynb`. Enter the path to your dataset, drop unnecessary columns and insert a subset of data with tool functions from `tools/db_helpers`
## 6) Installation and Dependencies
Due to unsolvable dependency conflicts, several environemnts need to be used in the same time.
### 6.1 Environemnt for camera handling
The setup of a virtual environment for the camera handling is difficult due to vary dependency conflicts.
Therefore it is necessary to create the virtual environment with every package in the specific version and each package in the specific order.
Furthermore the environment needs to be based on Python 3.10. The specific versions and order of the packages are described int the file:
`requirements.txt`
### 6.2 Environment for predictions
If you want to use the existing deployment on Ohm-UX driving simulator's jetson board, activate conda environment `p310_FS_TF`, a python 3.10 environment including tensorflow and all other packages required to run `predict_sample.py`
Otherwise, as described in `readme.md: Setup`, you can use `prediction_env.yaml`, to create a new environment that fulfills the requirements.
## 7) Repository File Inventory
### Root
- `.gitignore` - Git ignore rules
- `readme.md` - minimal quickstart documentation
- `project_report.md` - full technical documentation (this file)
- `requirements.txt` - Python dependencies
### Dataset Creation
- `dataset_creation/parquet_file_creation.py` - local files to parquet conversion
- `dataset_creation/create_parquet_files_from_owncloud.py` - ownCloud download + parquet conversion
- `dataset_creation/combined_feature_creation.py` - sliding-window multimodal feature generation
- `dataset_creation/maxDist.py` - helper/statistical utility script for eye-tracking feature creation
#### AU Creation
- `dataset_creation/AU_creation/pyfeat_docu.ipynb` - py-feat exploratory notes
#### Camera Handling
- `dataset_creation/camera_handling/camera_stream_AU_and_ET_new.py` - current camera + AU + eye online pipeline
- `dataset_creation/camera_handling/eyeFeature_new.py` - eye-feature extraction from gaze parquet
- `dataset_creation/camera_handling/db_helper.py` - SQLite helper functions (camera pipeline)
- `dataset_creation/camera_handling/camera_stream.py` - baseline camera streaming script
- `dataset_creation/camera_handling/eyeFeature_kalibrierung.py` - eye-feature extraction with calibration
- `dataset_creation/camera_handling/db_test.py` - DB test utility
### EDA
- `EDA/EDA.ipynb` - main EDA notebook
- `EDA/distribution_plots.ipynb` - distribution visualization
- `EDA/histogramms.ipynb` - histogram analysis
- `EDA/researchOnSubjectPerformance.ipynb` - subject-level analysis
- `EDA/owncloud_file_access.ipynb` - ownCloud exploration/access notebook
- `EDA/calculate_replacement_values.ipynb` - fallback/median computation notebook
- `EDA/login.yaml` - local auth/config artifact for EDA workflows
### Model Training
#### CNN
- `model_training/CNN/CNN_simple.ipynb`
- `model_training/CNN/CNN_crossVal.ipynb`
- `model_training/CNN/CNN_crossVal_EarlyFusion.ipynb`
- `model_training/CNN/CNN_crossVal_EarlyFusion_Filter.ipynb`
- `model_training/CNN/CNN_crossVal_EarlyFusion_Test_Eval.ipynb`
- `model_training/CNN/CNN_crossVal_faceAUs.ipynb`
- `model_training/CNN/CNN_crossVal_faceAUs_eyeFeatures.ipynb`
- `model_training/CNN/CNN_crossVal_HybridFusion.ipynb`
- `model_training/CNN/CNN_crossVal_HybridFusion_Test_Eval.ipynb`
- `model_training/CNN/deployment_pipeline.ipynb`
#### XGBoost
- `model_training/xgboost/xgboost.ipynb`
- `model_training/xgboost/xgboost_groupfold.ipynb`
- `model_training/xgboost/xgboost_new_dataset.ipynb`
- `model_training/xgboost/xgboost_regulated.ipynb`
- `model_training/xgboost/xgboost_with_AE.ipynb`
- `model_training/xgboost/xgboost_with_MAD.ipynb`
#### Isolation Forest
- `model_training/IsolationForest/iforest_training.ipynb`
#### OCSVM
- `model_training/OCSVM/ocsvm_with_AE.ipynb`
#### DeepSVDD
- `model_training/DeepSVDD/deepSVDD.ipynb`
#### MAD Outlier Removal
- `model_training/MAD_outlier_removal/mad_outlier_removal.ipynb`
- `model_training/MAD_outlier_removal/mad_outlier_removal_median.ipynb`
#### Shared Training Tools
- `model_training/tools/scaler.py`
- `model_training/tools/performance_split.py`
- `model_training/tools/mad_outlier_removal.py`
- `model_training/tools/evaluation_tools.py`
### Prediction Pipeline
- `predict_pipeline/predict_sample.py` - runtime prediction + MQTT publish
- `predict_pipeline/config.yaml` - runtime database/model/scaler/mqtt config
- `predict_pipeline/fill_db.ipynb` - helper notebook for DB setup/testing
- `predict_pipeline/predict.service` - systemd service unit
- `predict_pipeline/predict.timer` - systemd timer unit
- `predict_pipeline/predict_service_timer_documentation.md` - Linux service/timer guide
### Generic Tools
- `tools/db_helpers.py` - common SQLite utilities used to get newest sample for prediction
## 8) Additional Information
- Several paths are hardcoded on purpose to ensure compability with the jetsonboard at the OHM-UX driving simulator.
- Camera and AU processing are resource-intensive; version pinning and hardware validation are recommended.
- To access our dataset and other valuables, the `data-paulusjafahrsimulator-gpu`
directory contains the raw data, performance results, parquet files, and the final dataset.
+43 -56
View File
@@ -1,67 +1,54 @@
# Multimodal Driver State Analysis # Multimodal Driver State Analysis
Ein umfassendes Framework zur Analyse von Fahrerverhalten durch kombinierte Feature-Extraktion aus Facial Action Units (AU) und Eye-Tracking Daten. Short overview: this repository contains the data, feature, training, and inference pipeline for multimodal driver-state analysis using facial AUs and eye-tracking signals.
## 📋 Projektübersicht For full documentation, see [project_report.md](project_report.md).
Dieses Projekt verarbeitet multimodale Sensordaten aus Fahrsimulator-Studien und extrahiert zeitbasierte Features für die Analyse von Fahrerzuständen. Die Pipeline kombiniert: ## Quickstart
- **Facial Action Units (AU)**: 20 Gesichtsaktionseinheiten zur Emotionserkennung ### 1) Setup
- **Eye-Tracking**: Fixationen, Sakkaden, Blinks und Pupillenmetriken Activate the conda-repository "camera_stream_AU_ET_test".
## 🎯 Features
### Datenverarbeitung
- **Sliding Window Aggregation**: 50-Sekunden-Fenster mit 5-Sekunden-Schrittweite
- **Hierarchische Gruppierung**: Automatische Segmentierung nach STUDY/LEVEL/PHASE
- **Robuste Fehlerbehandlung**: Graceful Degradation bei fehlenden Modalitäten
### Extrahierte Features
#### Facial Action Units (20 AUs)
Für jede AU wird der Mittelwert pro Window berechnet:
- AU01 (Inner Brow Raiser) bis AU43 (Eyes Closed)
- Aggregation: `mean` über 50s Window
#### Eye-Tracking Features
**Fixationen:**
- Anzahl nach Dauer-Kategorien (66-150ms, 300-500ms, >1000ms, >100ms)
- Mittelwert und Median der Fixationsdauer
**Sakkaden:**
- Anzahl, mittlere Amplitude, mittlere/mediane Dauer
**Blinks:**
- Anzahl, mittlere/mediane Dauer
**Pupille:**
- Mittlere Pupillengröße
- Index of Pupillary Activity (IPA) - Hochfrequenzkomponente (0.6-2.0 Hz)
## 🏗️ Projektstruktur
to be continued.
## 🚀 Installation
### Voraussetzungen
```bash ```bash
Python 3.12 conda activate camera_stream_AU_ET_test
```
**Make sure, another environment that fulfills prediction_env.yaml is available**, matching with predict_pipeline/predict.service
See `predict_pipeline/predict_service_timer_documentation.md`
to get an overview over all available conda environments on your device, use this command in anaconda prompt terminal:
```bash
conda info --envs
```
Optionally, create a new environment based on the yaml-file:
```bash
conda env create -f prediction_env.yaml
```
Ohm-UX driving simulator jetson board only: The conda-environment `p310_FS_TF` is used for predictions.
### 2) Camera AU + Eye Pipeline (`camera_stream_AU_and_ET_new.py`)
1. Open `dataset_creation/camera_handling/camera_stream_AU_and_ET_new.py` and adjust:
- `DB_PATH`
- `CAMERA_INDEX`
- `OUTPUT_DIR` (optional)
2. Start camera capture and feature extraction:
```bash
python dataset_creation/camera_handling/camera_stream_AU_and_ET_new.py
``` ```
### Dependencies 3. Stop with `q` in the camera window.
### 3) Predict Pipeline (`predict_pipeline/predict_sample.py`)
1. Edit `predict_pipeline/config.yaml` and set:
- `database.path`, `database.table`, `database.key`
- `model.path`
- `scaler.path` (if `use_scaling: true`)
- MQTT settings under `mqtt`
2. Run one prediction cycle:
```bash ```bash
pip install -r requirements.txt python predict_pipeline/predict_sample.py
``` ```
**Wichtigste Pakete:** 3. Use [predict_service_timer_documentation.md](/predict_pipeline/predict_service_timer_documentation.md) to see how to use the service and timer for automation. On Ohm-UX driving simulator's jetson board, the service runs in the background and starts automatically when the device is booting.
- `pandas`, `numpy` - Datenverarbeitung
- `scipy` - Signalverarbeitung
- `scikit-learn` - Feature-Skalierung & ML
- `pygazeanalyser` - Eye-Tracking Analyse
- `pyarrow` - Parquet I/O
## 💻 Usage
### 1. Feature-Extraktion
to be continued
+27
View File
@@ -0,0 +1,27 @@
# Core data + ML utilities
numpy
pandas
scipy
scikit-learn
pyarrow
joblib
PyYAML
matplotlib
# Prediction pipeline
paho-mqtt
tensorflow
# Camera / feature extraction stack
# It is necessary to create an extra environment for the extraction pipeline, because of the different version needs
Python==3.10
numpy==1.24.4
scipy==1.10.1
opencv-python==4.7.0.72
mediapipe==0.10.13
py-feat
# Data ingestion (ownCloud + HDF)
pyocclient
h5py
tables
+166
View File
@@ -0,0 +1,166 @@
import os
import sqlite3
import pandas as pd
def connect_db(path_to_file: os.PathLike) -> tuple[sqlite3.Connection, sqlite3.Cursor]:
''' Establishes a connection with a sqlite3 database. '''
conn = sqlite3.connect(path_to_file)
cursor = conn.cursor()
return conn, cursor
def disconnect_db(conn: sqlite3.Connection, cursor: sqlite3.Cursor, commit: bool = True) -> None:
''' Commits all remaining changes and closes the connection with an sqlite3 database. '''
cursor.close()
if commit: conn.commit() # commit all pending changes made to the sqlite3 database before closing
conn.close()
def create_table(
conn: sqlite3.Connection,
cursor: sqlite3.Cursor,
table_name: str,
columns: dict,
constraints: dict,
primary_key: dict,
commit: bool = True
) -> str:
'''
Creates a new empty table with the given columns, constraints and primary key.
:param columns: dict with column names (=keys) and dtypes (=values) (e.g. BIGINT, INT, ...)
:param constraints: dict with column names (=keys) and list of constraints (=values) (like [\'NOT NULL\'(,...)])
:param primary_key: dict with primary key name (=key) and list of attributes which combined define the table's primary key (=values, like [\'att1\'(,...)])
'''
assert len(primary_key.keys()) == 1
sql = f'CREATE TABLE {table_name} (\n '
for column,dtype in columns.items():
sql += f'{column} {dtype}{" "+" ".join(constraints[column]) if column in constraints.keys() else ""},\n '
if list(primary_key.keys())[0]: sql += f'CONSTRAINT {list(primary_key.keys())[0]} '
sql += f'PRIMARY KEY ({", ".join(list(primary_key.values())[0])})\n)'
cursor.execute(sql)
if commit: conn.commit()
return sql
def add_columns_to_table(
conn: sqlite3.Connection,
cursor: sqlite3.Cursor,
table_name: str,
columns: dict,
constraints: dict = dict(),
commit: bool = True
) -> str:
''' Adds one/multiple columns (each with a list of constraints) to the given table. '''
sql_total = ''
for column,dtype in columns.items(): # sqlite can only add one column per query
sql = f'ALTER TABLE {table_name}\n '
sql += f'ADD "{column}" {dtype}{" "+" ".join(constraints[column]) if column in constraints.keys() else ""}'
sql_total += sql + '\n'
cursor.execute(sql)
if commit: conn.commit()
return sql_total
def insert_rows_into_table(
conn: sqlite3.Connection,
cursor: sqlite3.Cursor,
table_name: str,
columns: dict,
commit: bool = True
) -> str:
'''
Inserts values as multiple rows into the given table.
:param columns: dict with column names (=keys) and values to insert as lists with at least one element (=values)
Note: The number of given values per attribute must match the number of rows to insert!
Note: The values for the rows must be of normal python types (e.g. list, str, int, ...) instead of e.g. numpy arrays!
'''
assert len(set(map(len, columns.values()))) == 1, 'ERROR: Provide equal number of values for each column!'
assert len(set(list(map(type,columns.values())))) == 1 and isinstance(list(columns.values())[0], list), 'ERROR: Provide values as Python lists!'
assert set([type(a) for b in list(columns.values()) for a in b]).issubset({str,int,float,bool}), 'ERROR: Provide values as basic Python data types!'
values = list(zip(*columns.values()))
sql = f'INSERT INTO {table_name} ({", ".join(columns.keys())})\n VALUES ({("?,"*len(values[0]))[:-1]})'
cursor.executemany(sql, values)
if commit: conn.commit()
return sql
def update_multiple_rows_in_table(
conn: sqlite3.Connection,
cursor: sqlite3.Cursor,
table_name: str,
new_vals: dict,
conditions: str,
commit: bool = True
) -> str:
'''
Updates attribute values of some rows in the given table.
:param new_vals: dict with column names (=keys) and the new values to set (=values)
:param conditions: string which defines all concatenated conditions (e.g. \'cond1 AND (cond2 OR cond3)\' with cond1: att1=5, ...)
'''
assignments = ', '.join([f'{k}={v}' for k,v in zip(new_vals.keys(), new_vals.values())])
sql = f'UPDATE {table_name}\n SET {assignments}\n WHERE {conditions}'
cursor.execute(sql)
if commit: conn.commit()
return sql
def delete_rows_from_table(
conn: sqlite3.Connection,
cursor: sqlite3.Cursor,
table_name: str,
conditions: str,
commit: bool = True
) -> str:
'''
Deletes rows from the given table.
:param conditions: string which defines all concatenated conditions (e.g. \'cond1 AND (cond2 OR cond3)\' with cond1: att1=5, ...)
'''
sql = f'DELETE FROM {table_name} WHERE {conditions}'
cursor.execute(sql)
if commit: conn.commit()
return sql
def get_data_from_table(
conn: sqlite3.Connection,
table_name: str,
columns_list: list = ['*'],
aggregations: [None,dict] = None,
where_conditions: [None,str] = None,
order_by: [None, dict] = None,
limit: [None, int] = None,
offset: [None, int] = None
) -> pd.DataFrame:
'''
Helper function which returns (if desired: aggregated) contents from the given table as a pandas DataFrame. The rows can be filtered by providing the condition as a string.
:param columns_list: use if no aggregation is needed to select which columns to get from the table
:param (optional) aggregations: use to apply aggregations on the data from the table; dictionary with column(s) as key(s) and aggregation(s) as corresponding value(s) (e.g. {'col1': 'MIN', 'col2': 'AVG', ...} or {'*': 'COUNT'})
:param (optional) where_conditions: string which defines all concatenated conditions (e.g. \'cond1 AND (cond2 OR cond3)\' with cond1: att1=5, ...) applied on table.
:param (optional) order_by: dict defining the ordering of the outputs with column(s) as key(s) and ordering as corresponding value(s) (e.g. {'col1': 'ASC'})
:param (optional) limit: use to limit the number of returned rows
:param (optional) offset: use to skip the first n rows before displaying
Note: If aggregations is set, the columns_list is ignored.
Note: Get all data as a DataFrame with get_data_from_table(conn, table_name).
Note: If one output is wanted (e.g. count(*) or similar), get it with get_data_from_table(...).iloc[0,0] from the DataFrame.
'''
assert columns_list or aggregations
if aggregations:
selection = [f'{agg}({col})' for col,agg in aggregations.items()]
else:
selection = columns_list
selection = ", ".join(selection)
where_conditions = 'WHERE ' + where_conditions if where_conditions else ''
order_by = 'ORDER BY ' + ', '.join([f'{k} {v}' for k,v in order_by.items()]) if order_by else ''
limit = f'LIMIT {limit}' if limit else ''
offset = f'OFFSET {offset}' if offset else ''
sql = f'SELECT {selection} FROM {table_name} {where_conditions} {order_by} {limit} {offset}'
return pd.read_sql_query(sql, conn)