First commit
Browse files- .gitattributes +6 -35
- .gitignore +7 -0
- Data/beijing-sentence-corpus/BSC.EMD/BSC.EMD.txt +3 -0
- Data/beijing-sentence-corpus/BSC.Word.Info.v2.xlsx +3 -0
- Data/beijing-sentence-corpus/README.md +7 -0
- Data/feature_norm_BSC.pickle +0 -0
- Data/feature_norm_celer.pickle +0 -0
- Eyettention_examples.ipynb +0 -0
- Eyettention_examples_chinese.ipynb +761 -0
- LICENSE.txt +21 -0
- README.md +132 -3
- __init__.py +29 -0
- app.py +353 -0
- handler.py +83 -0
- main_BSC.py +406 -0
- main_BSC_NRS_setting.py +390 -0
- main_BSC_reader_identifier.py +358 -0
- main_celer.py +439 -0
- main_celer_NRS_setting.py +419 -0
- main_celer_reader_identifier.py +374 -0
- model.py +702 -0
- raw_text_inference.py +98 -0
- requirements.txt +10 -0
- results/BSC/Eyettention_chinese.pth +3 -0
- results/CELER/Eyettention_english.pth +3 -0
- tests/test_imports.py +83 -0
- tests/test_raw_text_inference.py +198 -0
- utils.py +716 -0
.gitattributes
CHANGED
|
@@ -1,35 +1,6 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
-
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
-
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
-
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
-
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
-
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
-
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
-
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
-
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
-
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
-
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
-
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
-
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
-
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
-
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
-
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
-
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
-
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 1 |
+
Data/celer/data_v2.0/sent_fix.tsv filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
Data/celer/data_v2.0/sent_ia.tsv filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
Data/beijing-sentence-corpus/BSC.EMD/BSC.EMD.txt filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
Data/beijing-sentence-corpus/BSC.Word.Info.v2.xlsx filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
results/BSC/Eyettention_chinese.pth filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
results/CELER/Eyettention_english.pth filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.gitignore
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python bytecode/cache files
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
.cache/
|
| 6 |
+
results/
|
| 7 |
+
/Data/
|
Data/beijing-sentence-corpus/BSC.EMD/BSC.EMD.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:35b388e9355bec51b2a0ae0e872cbe03abaf5325ee9be8bf89d37a2eefd0e55d
|
| 3 |
+
size 25362710
|
Data/beijing-sentence-corpus/BSC.Word.Info.v2.xlsx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:5c96e829a3de8203739893eef6b54e6ebddf055976919d9b29b2797053d81876
|
| 3 |
+
size 208488
|
Data/beijing-sentence-corpus/README.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
### not part of the original data repository, written by david
|
| 2 |
+
|
| 3 |
+
for more info see: https://link.springer.com/article/10.3758/s13428-021-01730-2
|
| 4 |
+
lena, paul, shuwen and david have the paper.
|
| 5 |
+
|
| 6 |
+
data from: https://osf.io/vr3k8/
|
| 7 |
+
downloadable via osfclient
|
Data/feature_norm_BSC.pickle
ADDED
|
Binary file (17.6 kB). View file
|
|
|
Data/feature_norm_celer.pickle
ADDED
|
Binary file (364 Bytes). View file
|
|
|
Eyettention_examples.ipynb
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
Eyettention_examples_chinese.ipynb
ADDED
|
@@ -0,0 +1,761 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"cells": [
|
| 3 |
+
{
|
| 4 |
+
"cell_type": "markdown",
|
| 5 |
+
"id": "nuclear-dream",
|
| 6 |
+
"metadata": {
|
| 7 |
+
"id": "nuclear-dream"
|
| 8 |
+
},
|
| 9 |
+
"source": [
|
| 10 |
+
"# Eyettention"
|
| 11 |
+
]
|
| 12 |
+
},
|
| 13 |
+
{
|
| 14 |
+
"cell_type": "markdown",
|
| 15 |
+
"id": "g-FLEOyvYJUG",
|
| 16 |
+
"metadata": {
|
| 17 |
+
"id": "g-FLEOyvYJUG"
|
| 18 |
+
},
|
| 19 |
+
"source": [
|
| 20 |
+
"**Configuration**"
|
| 21 |
+
]
|
| 22 |
+
},
|
| 23 |
+
{
|
| 24 |
+
"cell_type": "code",
|
| 25 |
+
"execution_count": 1,
|
| 26 |
+
"id": "neutral-montreal",
|
| 27 |
+
"metadata": {
|
| 28 |
+
"id": "neutral-montreal"
|
| 29 |
+
},
|
| 30 |
+
"outputs": [],
|
| 31 |
+
"source": [
|
| 32 |
+
"import numpy as np\n",
|
| 33 |
+
"#configuration\n",
|
| 34 |
+
"#Chinese\n",
|
| 35 |
+
"cf = {\"model_pretrained\": \"bert-base-chinese\",\n",
|
| 36 |
+
" \"atten_type\": 'local-g',\n",
|
| 37 |
+
" #batch computation for training (BSC dataset)\n",
|
| 38 |
+
" \"max_sn_len\": 27,\n",
|
| 39 |
+
" \"max_sp_len\": 40\n",
|
| 40 |
+
" }"
|
| 41 |
+
]
|
| 42 |
+
},
|
| 43 |
+
{
|
| 44 |
+
"cell_type": "code",
|
| 45 |
+
"execution_count": 2,
|
| 46 |
+
"id": "uhLxInukW82r",
|
| 47 |
+
"metadata": {
|
| 48 |
+
"colab": {
|
| 49 |
+
"base_uri": "https://localhost:8080/"
|
| 50 |
+
},
|
| 51 |
+
"id": "uhLxInukW82r",
|
| 52 |
+
"outputId": "76c33a63-4d13-4496-976d-126fe4ebb1b0"
|
| 53 |
+
},
|
| 54 |
+
"outputs": [
|
| 55 |
+
{
|
| 56 |
+
"name": "stdout",
|
| 57 |
+
"output_type": "stream",
|
| 58 |
+
"text": [
|
| 59 |
+
"Requirement already satisfied: transformers==4.27.0 in c:\\users\\shuwe\\anaconda3\\envs\\wenwen\\lib\\site-packages (4.27.0)\n",
|
| 60 |
+
"Requirement already satisfied: packaging>=20.0 in c:\\users\\shuwe\\appdata\\roaming\\python\\python38\\site-packages (from transformers==4.27.0) (20.9)\n",
|
| 61 |
+
"Requirement already satisfied: numpy>=1.17 in c:\\users\\shuwe\\appdata\\roaming\\python\\python38\\site-packages (from transformers==4.27.0) (1.20.3)\n",
|
| 62 |
+
"Requirement already satisfied: huggingface-hub<1.0,>=0.11.0 in c:\\users\\shuwe\\anaconda3\\envs\\wenwen\\lib\\site-packages (from transformers==4.27.0) (0.15.1)\n",
|
| 63 |
+
"Requirement already satisfied: tokenizers!=0.11.3,<0.14,>=0.11.1 in c:\\users\\shuwe\\anaconda3\\envs\\wenwen\\lib\\site-packages (from transformers==4.27.0) (0.12.1)\n",
|
| 64 |
+
"Requirement already satisfied: requests in c:\\users\\shuwe\\appdata\\roaming\\python\\python38\\site-packages (from transformers==4.27.0) (2.25.1)\n",
|
| 65 |
+
"Requirement already satisfied: regex!=2019.12.17 in c:\\users\\shuwe\\anaconda3\\envs\\wenwen\\lib\\site-packages (from transformers==4.27.0) (2021.4.4)\n",
|
| 66 |
+
"Requirement already satisfied: tqdm>=4.27 in c:\\users\\shuwe\\anaconda3\\envs\\wenwen\\lib\\site-packages (from transformers==4.27.0) (4.65.0)\n",
|
| 67 |
+
"Requirement already satisfied: pyyaml>=5.1 in c:\\users\\shuwe\\appdata\\roaming\\python\\python38\\site-packages (from transformers==4.27.0) (5.4.1)\n",
|
| 68 |
+
"Requirement already satisfied: filelock in c:\\users\\shuwe\\anaconda3\\envs\\wenwen\\lib\\site-packages (from transformers==4.27.0) (3.0.12)\n",
|
| 69 |
+
"Requirement already satisfied: typing-extensions>=3.7.4.3 in c:\\users\\shuwe\\anaconda3\\envs\\wenwen\\lib\\site-packages (from huggingface-hub<1.0,>=0.11.0->transformers==4.27.0) (4.5.0)\n",
|
| 70 |
+
"Requirement already satisfied: fsspec in c:\\users\\shuwe\\anaconda3\\envs\\wenwen\\lib\\site-packages (from huggingface-hub<1.0,>=0.11.0->transformers==4.27.0) (2023.6.0)\n",
|
| 71 |
+
"Requirement already satisfied: pyparsing>=2.0.2 in c:\\users\\shuwe\\appdata\\roaming\\python\\python38\\site-packages (from packaging>=20.0->transformers==4.27.0) (2.4.7)\n",
|
| 72 |
+
"Requirement already satisfied: colorama in c:\\users\\shuwe\\anaconda3\\envs\\wenwen\\lib\\site-packages (from tqdm>=4.27->transformers==4.27.0) (0.4.4)\n",
|
| 73 |
+
"Requirement already satisfied: chardet<5,>=3.0.2 in c:\\users\\shuwe\\anaconda3\\envs\\wenwen\\lib\\site-packages (from requests->transformers==4.27.0) (3.0.4)\n",
|
| 74 |
+
"Requirement already satisfied: certifi>=2017.4.17 in c:\\users\\shuwe\\appdata\\roaming\\python\\python38\\site-packages (from requests->transformers==4.27.0) (2020.12.5)\n",
|
| 75 |
+
"Requirement already satisfied: idna<3,>=2.5 in c:\\users\\shuwe\\appdata\\roaming\\python\\python38\\site-packages (from requests->transformers==4.27.0) (2.10)\n",
|
| 76 |
+
"Requirement already satisfied: urllib3<1.27,>=1.21.1 in c:\\users\\shuwe\\appdata\\roaming\\python\\python38\\site-packages (from requests->transformers==4.27.0) (1.26.5)\n"
|
| 77 |
+
]
|
| 78 |
+
}
|
| 79 |
+
],
|
| 80 |
+
"source": [
|
| 81 |
+
"!pip install --upgrade transformers==4.27.0"
|
| 82 |
+
]
|
| 83 |
+
},
|
| 84 |
+
{
|
| 85 |
+
"cell_type": "code",
|
| 86 |
+
"execution_count": 3,
|
| 87 |
+
"id": "8-8qDf4EXljd",
|
| 88 |
+
"metadata": {
|
| 89 |
+
"colab": {
|
| 90 |
+
"base_uri": "https://localhost:8080/"
|
| 91 |
+
},
|
| 92 |
+
"id": "8-8qDf4EXljd",
|
| 93 |
+
"outputId": "97cffda3-ffde-4231-c80b-0bfbc9f98a68"
|
| 94 |
+
},
|
| 95 |
+
"outputs": [
|
| 96 |
+
{
|
| 97 |
+
"name": "stderr",
|
| 98 |
+
"output_type": "stream",
|
| 99 |
+
"text": [
|
| 100 |
+
"fatal: destination path 'Eyettention' already exists and is not an empty directory.\n"
|
| 101 |
+
]
|
| 102 |
+
}
|
| 103 |
+
],
|
| 104 |
+
"source": [
|
| 105 |
+
"# You can also just download the file model.py, which is only used in this tutorial\n",
|
| 106 |
+
"!git clone https://github.com/aeye-lab/Eyettention"
|
| 107 |
+
]
|
| 108 |
+
},
|
| 109 |
+
{
|
| 110 |
+
"cell_type": "markdown",
|
| 111 |
+
"id": "jbqmeRpRY5Sg",
|
| 112 |
+
"metadata": {
|
| 113 |
+
"id": "jbqmeRpRY5Sg"
|
| 114 |
+
},
|
| 115 |
+
"source": [
|
| 116 |
+
"**Load the model and download pre-trained weights**"
|
| 117 |
+
]
|
| 118 |
+
},
|
| 119 |
+
{
|
| 120 |
+
"cell_type": "code",
|
| 121 |
+
"execution_count": 4,
|
| 122 |
+
"id": "stylish-member",
|
| 123 |
+
"metadata": {
|
| 124 |
+
"colab": {
|
| 125 |
+
"base_uri": "https://localhost:8080/"
|
| 126 |
+
},
|
| 127 |
+
"id": "stylish-member",
|
| 128 |
+
"outputId": "2307c733-ee9e-4ffb-92ef-f9d973f53fd6"
|
| 129 |
+
},
|
| 130 |
+
"outputs": [
|
| 131 |
+
{
|
| 132 |
+
"name": "stdout",
|
| 133 |
+
"output_type": "stream",
|
| 134 |
+
"text": [
|
| 135 |
+
"keeping Bert with pre-trained weights\n"
|
| 136 |
+
]
|
| 137 |
+
},
|
| 138 |
+
{
|
| 139 |
+
"name": "stderr",
|
| 140 |
+
"output_type": "stream",
|
| 141 |
+
"text": [
|
| 142 |
+
"Some weights of the model checkpoint at bert-base-chinese were not used when initializing BertModel: ['cls.predictions.transform.dense.bias', 'cls.predictions.bias', 'cls.predictions.transform.LayerNorm.bias', 'cls.predictions.decoder.weight', 'cls.predictions.transform.LayerNorm.weight', 'cls.predictions.transform.dense.weight', 'cls.seq_relationship.weight', 'cls.seq_relationship.bias']\n",
|
| 143 |
+
"- This IS expected if you are initializing BertModel from the checkpoint of a model trained on another task or with another architecture (e.g. initializing a BertForSequenceClassification model from a BertForPreTraining model).\n",
|
| 144 |
+
"- This IS NOT expected if you are initializing BertModel from the checkpoint of a model that you expect to be exactly identical (initializing a BertForSequenceClassification model from a BertForSequenceClassification model).\n"
|
| 145 |
+
]
|
| 146 |
+
}
|
| 147 |
+
],
|
| 148 |
+
"source": [
|
| 149 |
+
"from Eyettention import model\n",
|
| 150 |
+
"import torch\n",
|
| 151 |
+
"from torch.utils import model_zoo\n",
|
| 152 |
+
"\n",
|
| 153 |
+
"#DEVICE = 'cuda'\n",
|
| 154 |
+
"DEVICE = 'cpu'\n",
|
| 155 |
+
"\n",
|
| 156 |
+
"pretrained=True\n",
|
| 157 |
+
"\n",
|
| 158 |
+
"#load model\n",
|
| 159 |
+
"dnn = model.Eyettention(cf)\n",
|
| 160 |
+
"dnn.eval()\n",
|
| 161 |
+
"\n",
|
| 162 |
+
"if pretrained: \n",
|
| 163 |
+
" #for Chinese\n",
|
| 164 |
+
" dnn.load_state_dict(model_zoo.load_url('https://github.com/aeye-lab/Eyettention/releases/download/v1.1/Eyettention_chinese.pth', map_location=torch.device('cpu')))"
|
| 165 |
+
]
|
| 166 |
+
},
|
| 167 |
+
{
|
| 168 |
+
"cell_type": "markdown",
|
| 169 |
+
"id": "f0DvCfMrZMYg",
|
| 170 |
+
"metadata": {
|
| 171 |
+
"id": "f0DvCfMrZMYg"
|
| 172 |
+
},
|
| 173 |
+
"source": [
|
| 174 |
+
"**Tokenization**\n",
|
| 175 |
+
"<br>\n",
|
| 176 |
+
"**Introduction**: https://huggingface.co/learn/nlp-course/chapter2/4?fw=pt"
|
| 177 |
+
]
|
| 178 |
+
},
|
| 179 |
+
{
|
| 180 |
+
"cell_type": "code",
|
| 181 |
+
"execution_count": 5,
|
| 182 |
+
"id": "severe-findings",
|
| 183 |
+
"metadata": {
|
| 184 |
+
"id": "severe-findings"
|
| 185 |
+
},
|
| 186 |
+
"outputs": [],
|
| 187 |
+
"source": [
|
| 188 |
+
"from transformers import BertTokenizerFast\n",
|
| 189 |
+
"#initialize tokenizer\n",
|
| 190 |
+
"tokenizer = BertTokenizerFast.from_pretrained(cf['model_pretrained'])"
|
| 191 |
+
]
|
| 192 |
+
},
|
| 193 |
+
{
|
| 194 |
+
"cell_type": "code",
|
| 195 |
+
"execution_count": 6,
|
| 196 |
+
"id": "funded-validity",
|
| 197 |
+
"metadata": {
|
| 198 |
+
"colab": {
|
| 199 |
+
"base_uri": "https://localhost:8080/"
|
| 200 |
+
},
|
| 201 |
+
"id": "funded-validity",
|
| 202 |
+
"outputId": "fbc6c028-3049-42ab-8187-6034e787c59d"
|
| 203 |
+
},
|
| 204 |
+
"outputs": [],
|
| 205 |
+
"source": [
|
| 206 |
+
"sentence = '中国选手在男子滑雪比赛中有望蝉联冠军'\n",
|
| 207 |
+
"#add special tokens\n",
|
| 208 |
+
"#text = ('[CLS]' + ' ' + sentence + ' ' + '[SEP]').split()\n",
|
| 209 |
+
"#print(text)\n",
|
| 210 |
+
"tokens = tokenizer(sentence, add_special_tokens=True, max_length=cf['max_sn_len'], padding='max_length')"
|
| 211 |
+
]
|
| 212 |
+
},
|
| 213 |
+
{
|
| 214 |
+
"cell_type": "code",
|
| 215 |
+
"execution_count": 7,
|
| 216 |
+
"id": "vulnerable-figure",
|
| 217 |
+
"metadata": {
|
| 218 |
+
"colab": {
|
| 219 |
+
"base_uri": "https://localhost:8080/"
|
| 220 |
+
},
|
| 221 |
+
"id": "vulnerable-figure",
|
| 222 |
+
"outputId": "9dcba3a3-b8b8-4d77-bed9-3b6059f9cea8"
|
| 223 |
+
},
|
| 224 |
+
"outputs": [
|
| 225 |
+
{
|
| 226 |
+
"name": "stdout",
|
| 227 |
+
"output_type": "stream",
|
| 228 |
+
"text": [
|
| 229 |
+
"['[CLS]', '中', '国', '选', '手', '在', '男', '子', '滑', '雪', '比', '赛', '中', '有', '望', '蝉', '联', '冠', '军', '[SEP]']\n"
|
| 230 |
+
]
|
| 231 |
+
}
|
| 232 |
+
],
|
| 233 |
+
"source": [
|
| 234 |
+
"print(tokenizer.tokenize(sentence, add_special_tokens=True))"
|
| 235 |
+
]
|
| 236 |
+
},
|
| 237 |
+
{
|
| 238 |
+
"cell_type": "code",
|
| 239 |
+
"execution_count": 8,
|
| 240 |
+
"id": "inappropriate-marketplace",
|
| 241 |
+
"metadata": {
|
| 242 |
+
"colab": {
|
| 243 |
+
"base_uri": "https://localhost:8080/"
|
| 244 |
+
},
|
| 245 |
+
"id": "inappropriate-marketplace",
|
| 246 |
+
"outputId": "6ca3b171-cd18-4a82-8770-871fd6fad840"
|
| 247 |
+
},
|
| 248 |
+
"outputs": [
|
| 249 |
+
{
|
| 250 |
+
"name": "stdout",
|
| 251 |
+
"output_type": "stream",
|
| 252 |
+
"text": [
|
| 253 |
+
"{'input_ids': [101, 704, 1744, 6848, 2797, 1762, 4511, 2094, 3998, 7434, 3683, 6612, 704, 3300, 3307, 6068, 5468, 1094, 1092, 102, 0, 0, 0, 0, 0, 0, 0], 'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0]}\n"
|
| 254 |
+
]
|
| 255 |
+
}
|
| 256 |
+
],
|
| 257 |
+
"source": [
|
| 258 |
+
"print(tokens)"
|
| 259 |
+
]
|
| 260 |
+
},
|
| 261 |
+
{
|
| 262 |
+
"cell_type": "code",
|
| 263 |
+
"execution_count": 9,
|
| 264 |
+
"id": "intelligent-express",
|
| 265 |
+
"metadata": {
|
| 266 |
+
"colab": {
|
| 267 |
+
"base_uri": "https://localhost:8080/"
|
| 268 |
+
},
|
| 269 |
+
"id": "intelligent-express",
|
| 270 |
+
"outputId": "3a185313-5e28-450b-cd8c-4b885bac07f0"
|
| 271 |
+
},
|
| 272 |
+
"outputs": [],
|
| 273 |
+
"source": [
|
| 274 |
+
"#we do not need word index in Chinese language since each character is encoded independently. "
|
| 275 |
+
]
|
| 276 |
+
},
|
| 277 |
+
{
|
| 278 |
+
"cell_type": "markdown",
|
| 279 |
+
"id": "Zz6pcnFualMb",
|
| 280 |
+
"metadata": {
|
| 281 |
+
"id": "Zz6pcnFualMb"
|
| 282 |
+
},
|
| 283 |
+
"source": [
|
| 284 |
+
"**Prepare features**"
|
| 285 |
+
]
|
| 286 |
+
},
|
| 287 |
+
{
|
| 288 |
+
"cell_type": "code",
|
| 289 |
+
"execution_count": 10,
|
| 290 |
+
"id": "specified-stomach",
|
| 291 |
+
"metadata": {
|
| 292 |
+
"id": "specified-stomach"
|
| 293 |
+
},
|
| 294 |
+
"outputs": [],
|
| 295 |
+
"source": [
|
| 296 |
+
"def compute_BSC_word_length(sentence, lac):\n",
|
| 297 |
+
" word_string = lac.run(sentence)\n",
|
| 298 |
+
" #print(word_string)\n",
|
| 299 |
+
" word_len = [len(i) for i in word_string]\n",
|
| 300 |
+
" wl_list = []\n",
|
| 301 |
+
" for wl in word_len:\n",
|
| 302 |
+
" wl_list.extend([wl]*wl)\n",
|
| 303 |
+
" arr = np.asarray(wl_list, dtype=np.float32)\n",
|
| 304 |
+
" #length of a punctuation is 0, plus an epsilon to avoid division output inf\n",
|
| 305 |
+
" arr[arr==0] = 1/(0+0.5)\n",
|
| 306 |
+
" arr[arr!=0] = 1/(arr[arr!=0])\n",
|
| 307 |
+
" return arr\n",
|
| 308 |
+
"\n",
|
| 309 |
+
"def pad_seq(seqs, max_len, dtype=np.int32, fill_value=np.nan):\n",
|
| 310 |
+
" padded = np.full((len(seqs), max_len), fill_value=fill_value, dtype=dtype)\n",
|
| 311 |
+
" for i, seq in enumerate(seqs):\n",
|
| 312 |
+
" padded[i, 1:(len(seq)+1)] = seq\n",
|
| 313 |
+
" return padded"
|
| 314 |
+
]
|
| 315 |
+
},
|
| 316 |
+
{
|
| 317 |
+
"cell_type": "code",
|
| 318 |
+
"execution_count": 11,
|
| 319 |
+
"id": "59e9c3cf",
|
| 320 |
+
"metadata": {},
|
| 321 |
+
"outputs": [
|
| 322 |
+
{
|
| 323 |
+
"name": "stdout",
|
| 324 |
+
"output_type": "stream",
|
| 325 |
+
"text": [
|
| 326 |
+
"Requirement already satisfied: LAC in c:\\users\\shuwe\\anaconda3\\envs\\wenwen\\lib\\site-packages (2.1.2)\n"
|
| 327 |
+
]
|
| 328 |
+
}
|
| 329 |
+
],
|
| 330 |
+
"source": [
|
| 331 |
+
"#segment sentene into word (not character) to calculate word length features\n",
|
| 332 |
+
"!pip install LAC"
|
| 333 |
+
]
|
| 334 |
+
},
|
| 335 |
+
{
|
| 336 |
+
"cell_type": "code",
|
| 337 |
+
"execution_count": 12,
|
| 338 |
+
"id": "ce1a958a",
|
| 339 |
+
"metadata": {},
|
| 340 |
+
"outputs": [
|
| 341 |
+
{
|
| 342 |
+
"name": "stdout",
|
| 343 |
+
"output_type": "stream",
|
| 344 |
+
"text": [
|
| 345 |
+
"Requirement already satisfied: paddlepaddle==2.5.2 in c:\\users\\shuwe\\anaconda3\\envs\\wenwen\\lib\\site-packages (2.5.2)\n",
|
| 346 |
+
"Requirement already satisfied: astor in c:\\users\\shuwe\\anaconda3\\envs\\wenwen\\lib\\site-packages (from paddlepaddle==2.5.2) (0.8.1)\n",
|
| 347 |
+
"Requirement already satisfied: numpy>=1.13 in c:\\users\\shuwe\\appdata\\roaming\\python\\python38\\site-packages (from paddlepaddle==2.5.2) (1.20.3)\n",
|
| 348 |
+
"Requirement already satisfied: protobuf<=3.20.2,>=3.1.0 in c:\\users\\shuwe\\anaconda3\\envs\\wenwen\\lib\\site-packages (from paddlepaddle==2.5.2) (3.14.0)\n",
|
| 349 |
+
"Requirement already satisfied: opt-einsum==3.3.0 in c:\\users\\shuwe\\anaconda3\\envs\\wenwen\\lib\\site-packages (from paddlepaddle==2.5.2) (3.3.0)\n",
|
| 350 |
+
"Requirement already satisfied: Pillow in c:\\users\\shuwe\\anaconda3\\envs\\wenwen\\lib\\site-packages (from paddlepaddle==2.5.2) (10.1.0)\n",
|
| 351 |
+
"Requirement already satisfied: httpx in c:\\users\\shuwe\\anaconda3\\envs\\wenwen\\lib\\site-packages (from paddlepaddle==2.5.2) (0.26.0)\n",
|
| 352 |
+
"Requirement already satisfied: decorator in c:\\users\\shuwe\\appdata\\roaming\\python\\python38\\site-packages (from paddlepaddle==2.5.2) (4.4.2)\n",
|
| 353 |
+
"Requirement already satisfied: six>=1.9 in c:\\users\\shuwe\\appdata\\roaming\\python\\python38\\site-packages (from protobuf<=3.20.2,>=3.1.0->paddlepaddle==2.5.2) (1.16.0)\n",
|
| 354 |
+
"Requirement already satisfied: sniffio in c:\\users\\shuwe\\anaconda3\\envs\\wenwen\\lib\\site-packages (from httpx->paddlepaddle==2.5.2) (1.2.0)\n",
|
| 355 |
+
"Requirement already satisfied: certifi in c:\\users\\shuwe\\appdata\\roaming\\python\\python38\\site-packages (from httpx->paddlepaddle==2.5.2) (2020.12.5)\n",
|
| 356 |
+
"Requirement already satisfied: idna in c:\\users\\shuwe\\appdata\\roaming\\python\\python38\\site-packages (from httpx->paddlepaddle==2.5.2) (2.10)\n",
|
| 357 |
+
"Requirement already satisfied: httpcore==1.* in c:\\users\\shuwe\\anaconda3\\envs\\wenwen\\lib\\site-packages (from httpx->paddlepaddle==2.5.2) (1.0.2)\n",
|
| 358 |
+
"Requirement already satisfied: anyio in c:\\users\\shuwe\\anaconda3\\envs\\wenwen\\lib\\site-packages (from httpx->paddlepaddle==2.5.2) (2.2.0)\n",
|
| 359 |
+
"Requirement already satisfied: h11<0.15,>=0.13 in c:\\users\\shuwe\\anaconda3\\envs\\wenwen\\lib\\site-packages (from httpcore==1.*->httpx->paddlepaddle==2.5.2) (0.14.0)\n"
|
| 360 |
+
]
|
| 361 |
+
}
|
| 362 |
+
],
|
| 363 |
+
"source": [
|
| 364 |
+
"!pip install paddlepaddle==2.5.2\n",
|
| 365 |
+
"from LAC import LAC\n",
|
| 366 |
+
"lac = LAC(mode=\"seg\")"
|
| 367 |
+
]
|
| 368 |
+
},
|
| 369 |
+
{
|
| 370 |
+
"cell_type": "code",
|
| 371 |
+
"execution_count": 13,
|
| 372 |
+
"id": "proprietary-efficiency",
|
| 373 |
+
"metadata": {
|
| 374 |
+
"colab": {
|
| 375 |
+
"base_uri": "https://localhost:8080/"
|
| 376 |
+
},
|
| 377 |
+
"id": "proprietary-efficiency",
|
| 378 |
+
"outputId": "a2b21281-ff87-4757-d3ff-88b9702436de"
|
| 379 |
+
},
|
| 380 |
+
"outputs": [
|
| 381 |
+
{
|
| 382 |
+
"name": "stdout",
|
| 383 |
+
"output_type": "stream",
|
| 384 |
+
"text": [
|
| 385 |
+
"[[nan 0.5 0.5 0.5 0.5 1. 0.5 0.5 0.5 0.5 0.5 0.5 1. 0.5 0.5 0.5 0.5 0.5\n",
|
| 386 |
+
" 0.5 nan nan nan nan nan nan nan nan]]\n"
|
| 387 |
+
]
|
| 388 |
+
}
|
| 389 |
+
],
|
| 390 |
+
"source": [
|
| 391 |
+
"#compute the word length feature\n",
|
| 392 |
+
"text_word_len = [compute_BSC_word_length(txt, lac) for txt in [sentence]]\n",
|
| 393 |
+
"text_word_len = pad_seq(text_word_len, cf['max_sn_len'], fill_value=np.nan, dtype=np.float32)\n",
|
| 394 |
+
"print(text_word_len)"
|
| 395 |
+
]
|
| 396 |
+
},
|
| 397 |
+
{
|
| 398 |
+
"cell_type": "code",
|
| 399 |
+
"execution_count": 14,
|
| 400 |
+
"id": "severe-industry",
|
| 401 |
+
"metadata": {
|
| 402 |
+
"colab": {
|
| 403 |
+
"base_uri": "https://localhost:8080/"
|
| 404 |
+
},
|
| 405 |
+
"id": "severe-industry",
|
| 406 |
+
"outputId": "93474602-f510-4841-c612-dca9de230a9b"
|
| 407 |
+
},
|
| 408 |
+
"outputs": [
|
| 409 |
+
{
|
| 410 |
+
"name": "stderr",
|
| 411 |
+
"output_type": "stream",
|
| 412 |
+
"text": [
|
| 413 |
+
"'wget' is not recognized as an internal or external command,\n",
|
| 414 |
+
"operable program or batch file.\n"
|
| 415 |
+
]
|
| 416 |
+
}
|
| 417 |
+
],
|
| 418 |
+
"source": [
|
| 419 |
+
"#Download precomputed sn_word_len mean and std (from CELER dataset) for feature normalization\n",
|
| 420 |
+
"import pickle\n",
|
| 421 |
+
"!wget https://github.com/aeye-lab/Eyettention/releases/download/v1.1/feature_norm_BSC.pickle\n",
|
| 422 |
+
"path = 'feature_norm_BSC.pickle'\n",
|
| 423 |
+
"file_to_read = open(path, \"rb\")\n",
|
| 424 |
+
"loaded_dictionary = pickle.load(file_to_read)\n",
|
| 425 |
+
"sn_word_len_mean = loaded_dictionary['sn_word_len_mean'].numpy()\n",
|
| 426 |
+
"sn_word_len_std = loaded_dictionary['sn_word_len_std'].numpy()"
|
| 427 |
+
]
|
| 428 |
+
},
|
| 429 |
+
{
|
| 430 |
+
"cell_type": "code",
|
| 431 |
+
"execution_count": 15,
|
| 432 |
+
"id": "dried-biodiversity",
|
| 433 |
+
"metadata": {
|
| 434 |
+
"id": "dried-biodiversity"
|
| 435 |
+
},
|
| 436 |
+
"outputs": [],
|
| 437 |
+
"source": [
|
| 438 |
+
"#normalize word length feature\n",
|
| 439 |
+
"text_word_len = (text_word_len - sn_word_len_mean)/sn_word_len_std\n",
|
| 440 |
+
"text_word_len = np.nan_to_num(text_word_len)"
|
| 441 |
+
]
|
| 442 |
+
},
|
| 443 |
+
{
|
| 444 |
+
"cell_type": "code",
|
| 445 |
+
"execution_count": 16,
|
| 446 |
+
"id": "according-decimal",
|
| 447 |
+
"metadata": {
|
| 448 |
+
"colab": {
|
| 449 |
+
"base_uri": "https://localhost:8080/"
|
| 450 |
+
},
|
| 451 |
+
"id": "according-decimal",
|
| 452 |
+
"outputId": "e88d7af1-1d6a-4d65-86cc-f6bb50dd3c2a"
|
| 453 |
+
},
|
| 454 |
+
"outputs": [],
|
| 455 |
+
"source": [
|
| 456 |
+
"tokens['input_ids'] = torch.tensor([tokens['input_ids']]).to(DEVICE)\n",
|
| 457 |
+
"tokens['attention_mask'] = torch.tensor([tokens['attention_mask']]).to(DEVICE)\n",
|
| 458 |
+
"word_len = torch.tensor([text_word_len.squeeze()]).to(DEVICE)"
|
| 459 |
+
]
|
| 460 |
+
},
|
| 461 |
+
{
|
| 462 |
+
"cell_type": "markdown",
|
| 463 |
+
"id": "Nrm-Rq4Ya2TX",
|
| 464 |
+
"metadata": {
|
| 465 |
+
"id": "Nrm-Rq4Ya2TX"
|
| 466 |
+
},
|
| 467 |
+
"source": [
|
| 468 |
+
"**Generate synthetic scanpath**"
|
| 469 |
+
]
|
| 470 |
+
},
|
| 471 |
+
{
|
| 472 |
+
"cell_type": "code",
|
| 473 |
+
"execution_count": 17,
|
| 474 |
+
"id": "novel-monaco",
|
| 475 |
+
"metadata": {
|
| 476 |
+
"colab": {
|
| 477 |
+
"base_uri": "https://localhost:8080/",
|
| 478 |
+
"height": 74
|
| 479 |
+
},
|
| 480 |
+
"id": "novel-monaco",
|
| 481 |
+
"outputId": "8169a529-a161-4e48-d324-0c4399d8ff70"
|
| 482 |
+
},
|
| 483 |
+
"outputs": [
|
| 484 |
+
{
|
| 485 |
+
"data": {
|
| 486 |
+
"text/plain": [
|
| 487 |
+
"LabelEncoder()"
|
| 488 |
+
]
|
| 489 |
+
},
|
| 490 |
+
"execution_count": 17,
|
| 491 |
+
"metadata": {},
|
| 492 |
+
"output_type": "execute_result"
|
| 493 |
+
}
|
| 494 |
+
],
|
| 495 |
+
"source": [
|
| 496 |
+
"from sklearn.preprocessing import LabelEncoder\n",
|
| 497 |
+
"#prepare the saccade range classes for model predictions, cf[\"max_sn_len\"]-1 -> stop class\n",
|
| 498 |
+
"le = LabelEncoder()\n",
|
| 499 |
+
"le.fit(np.append(np.arange(-cf[\"max_sn_len\"]+3, cf[\"max_sn_len\"]-1), cf[\"max_sn_len\"]-1))\n",
|
| 500 |
+
"#le.classes_"
|
| 501 |
+
]
|
| 502 |
+
},
|
| 503 |
+
{
|
| 504 |
+
"cell_type": "code",
|
| 505 |
+
"execution_count": 27,
|
| 506 |
+
"id": "mathematical-dimension",
|
| 507 |
+
"metadata": {
|
| 508 |
+
"id": "mathematical-dimension"
|
| 509 |
+
},
|
| 510 |
+
"outputs": [],
|
| 511 |
+
"source": [
|
| 512 |
+
"syn_scanpath, density_pred = dnn.scanpath_generation(sn_emd=tokens['input_ids'],\n",
|
| 513 |
+
" sn_mask=tokens['attention_mask'],\n",
|
| 514 |
+
" word_ids_sn=None,\n",
|
| 515 |
+
" sn_word_len = word_len,\n",
|
| 516 |
+
" le=le,\n",
|
| 517 |
+
" max_pred_len=50)"
|
| 518 |
+
]
|
| 519 |
+
},
|
| 520 |
+
{
|
| 521 |
+
"cell_type": "code",
|
| 522 |
+
"execution_count": 19,
|
| 523 |
+
"id": "bacterial-cardiff",
|
| 524 |
+
"metadata": {
|
| 525 |
+
"id": "bacterial-cardiff"
|
| 526 |
+
},
|
| 527 |
+
"outputs": [],
|
| 528 |
+
"source": [
|
| 529 |
+
"def post_process_scanpath(syn_scanpath, sn_len):\n",
|
| 530 |
+
" syn_scanpath = syn_scanpath.detach().to('cpu').numpy()\n",
|
| 531 |
+
" max_sp_len = syn_scanpath.shape[1]\n",
|
| 532 |
+
"\n",
|
| 533 |
+
" #Find the number \"sn_len+1\" -> the stop class\n",
|
| 534 |
+
" stop_indx = []\n",
|
| 535 |
+
" for i in range(syn_scanpath.shape[0]):\n",
|
| 536 |
+
" stop = np.where(syn_scanpath[i,:]==(sn_len[i]+1))[0]\n",
|
| 537 |
+
" if len(stop)==0:#no stop class can be find -> exceeds the maximum length of the generated scanpath\n",
|
| 538 |
+
" stop_indx.append(max_sp_len-1)\n",
|
| 539 |
+
" else:\n",
|
| 540 |
+
" stop_indx.append(stop[0])\n",
|
| 541 |
+
"\n",
|
| 542 |
+
" #Truncating data after the stop class\n",
|
| 543 |
+
" syn_scanpath_cut = [syn_scanpath[i][1:stop_indx[i]] for i in range(syn_scanpath.shape[0])]\n",
|
| 544 |
+
" return syn_scanpath_cut"
|
| 545 |
+
]
|
| 546 |
+
},
|
| 547 |
+
{
|
| 548 |
+
"cell_type": "code",
|
| 549 |
+
"execution_count": 20,
|
| 550 |
+
"id": "southern-cycling",
|
| 551 |
+
"metadata": {
|
| 552 |
+
"colab": {
|
| 553 |
+
"base_uri": "https://localhost:8080/"
|
| 554 |
+
},
|
| 555 |
+
"id": "southern-cycling",
|
| 556 |
+
"outputId": "32c519fa-5251-4869-fc78-242bae3cc5f3"
|
| 557 |
+
},
|
| 558 |
+
"outputs": [],
|
| 559 |
+
"source": [
|
| 560 |
+
"sn_len = (torch.sum(tokens['attention_mask'], axis=1) - 2).detach().to('cpu').numpy()"
|
| 561 |
+
]
|
| 562 |
+
},
|
| 563 |
+
{
|
| 564 |
+
"cell_type": "code",
|
| 565 |
+
"execution_count": 28,
|
| 566 |
+
"id": "563c5aa6",
|
| 567 |
+
"metadata": {},
|
| 568 |
+
"outputs": [
|
| 569 |
+
{
|
| 570 |
+
"name": "stdout",
|
| 571 |
+
"output_type": "stream",
|
| 572 |
+
"text": [
|
| 573 |
+
"[ 1 2 4 6 9 12 16]\n"
|
| 574 |
+
]
|
| 575 |
+
}
|
| 576 |
+
],
|
| 577 |
+
"source": [
|
| 578 |
+
"syn_scanpath = post_process_scanpath(syn_scanpath, sn_len)\n",
|
| 579 |
+
"print(syn_scanpath[0])"
|
| 580 |
+
]
|
| 581 |
+
},
|
| 582 |
+
{
|
| 583 |
+
"cell_type": "code",
|
| 584 |
+
"execution_count": 29,
|
| 585 |
+
"id": "imposed-passing",
|
| 586 |
+
"metadata": {
|
| 587 |
+
"colab": {
|
| 588 |
+
"base_uri": "https://localhost:8080/"
|
| 589 |
+
},
|
| 590 |
+
"id": "imposed-passing",
|
| 591 |
+
"outputId": "2400559b-3d51-461b-8df2-19f1ccc621da"
|
| 592 |
+
},
|
| 593 |
+
"outputs": [
|
| 594 |
+
{
|
| 595 |
+
"name": "stdout",
|
| 596 |
+
"output_type": "stream",
|
| 597 |
+
"text": [
|
| 598 |
+
"['中', '国', '手', '男', '雪', '中', '联']\n"
|
| 599 |
+
]
|
| 600 |
+
}
|
| 601 |
+
],
|
| 602 |
+
"source": [
|
| 603 |
+
"fixated_character = [sentence[idx-1] for idx in syn_scanpath[0]] #python index start from 0, our model prediction index start from 1\n",
|
| 604 |
+
"print(fixated_character)"
|
| 605 |
+
]
|
| 606 |
+
},
|
| 607 |
+
{
|
| 608 |
+
"cell_type": "markdown",
|
| 609 |
+
"id": "Q9ojBr5rba-R",
|
| 610 |
+
"metadata": {
|
| 611 |
+
"id": "Q9ojBr5rba-R"
|
| 612 |
+
},
|
| 613 |
+
"source": [
|
| 614 |
+
"**Prediction visualization**"
|
| 615 |
+
]
|
| 616 |
+
},
|
| 617 |
+
{
|
| 618 |
+
"cell_type": "code",
|
| 619 |
+
"execution_count": 30,
|
| 620 |
+
"id": "dental-knock",
|
| 621 |
+
"metadata": {
|
| 622 |
+
"id": "dental-knock"
|
| 623 |
+
},
|
| 624 |
+
"outputs": [],
|
| 625 |
+
"source": [
|
| 626 |
+
"import matplotlib.pyplot as plt\n",
|
| 627 |
+
"import seaborn as sns\n",
|
| 628 |
+
"plt.rcParams[\"font.sans-serif\"]=['SimHei']\n",
|
| 629 |
+
"def visualize_distribution(syn_scanpath, density_pred, timestep, text):\n",
|
| 630 |
+
" #visualize density map\n",
|
| 631 |
+
" plt.figure(figsize = (25,5))\n",
|
| 632 |
+
" sac_range = np.append(np.arange(-cf[\"max_sn_len\"]+3, cf[\"max_sn_len\"]-1), cf[\"max_sn_len\"]-1)\n",
|
| 633 |
+
"\n",
|
| 634 |
+
" #current gaze position\n",
|
| 635 |
+
" cur_loc = syn_scanpath[timestep]\n",
|
| 636 |
+
" #convert the saccade range classes to the target word indexes\n",
|
| 637 |
+
" target_loc = sac_range + cur_loc\n",
|
| 638 |
+
"\n",
|
| 639 |
+
" prob_valid = density_pred[np.where((0 <= target_loc) & (target_loc <= (len(text)-2)))]#exclude SEP token\n",
|
| 640 |
+
" prob_valid = np.append(prob_valid, density_pred[-1])\n",
|
| 641 |
+
"\n",
|
| 642 |
+
" plt.subplot(2, 1, 2)\n",
|
| 643 |
+
" sns.heatmap(prob_valid[np.newaxis, :], annot=np.array(text)[np.newaxis,:], fmt=\"\", linewidths=2, cmap=\"YlGnBu\", xticklabels=False, yticklabels=False, cbar=True, annot_kws={\"fontsize\":15}, cbar_kws = dict(use_gridspec=False,location=\"top\"))\n",
|
| 644 |
+
" y = 3\n",
|
| 645 |
+
" for past_ts in range(timestep):\n",
|
| 646 |
+
" dy = 0\n",
|
| 647 |
+
" dx = syn_scanpath[past_ts+1] - syn_scanpath[past_ts]\n",
|
| 648 |
+
" #if refixation, display on next line\n",
|
| 649 |
+
" if dx <= 0:\n",
|
| 650 |
+
" dy = -0.5\n",
|
| 651 |
+
" plt.arrow(syn_scanpath[past_ts]+0.5, y, dx, dy, color='b', ec ='blue', head_width=0.2)\n",
|
| 652 |
+
" y = y + dy\n",
|
| 653 |
+
" else:\n",
|
| 654 |
+
" plt.arrow(syn_scanpath[past_ts]+0.5, y, dx, dy, color='b', ec ='blue', head_width=0.2)\n",
|
| 655 |
+
"\n",
|
| 656 |
+
" plt.arrow(syn_scanpath[timestep]+0.5, y, (syn_scanpath[timestep+1]-syn_scanpath[timestep]), 0, color='r', ec ='red', head_width=0.2)\n",
|
| 657 |
+
" plt.xlim([0, len(text)])\n",
|
| 658 |
+
" plt.ylim([0, 3.5])\n",
|
| 659 |
+
" ax = plt.gca()\n",
|
| 660 |
+
" ax.axes.get_xaxis().set_visible(False)\n",
|
| 661 |
+
" ax.axes.get_yaxis().set_visible(False)"
|
| 662 |
+
]
|
| 663 |
+
},
|
| 664 |
+
{
|
| 665 |
+
"cell_type": "code",
|
| 666 |
+
"execution_count": 32,
|
| 667 |
+
"id": "cross-commercial",
|
| 668 |
+
"metadata": {
|
| 669 |
+
"colab": {
|
| 670 |
+
"base_uri": "https://localhost:8080/",
|
| 671 |
+
"height": 130
|
| 672 |
+
},
|
| 673 |
+
"id": "cross-commercial",
|
| 674 |
+
"outputId": "66a6e06d-7473-412f-9dba-10b9db130b9a"
|
| 675 |
+
},
|
| 676 |
+
"outputs": [
|
| 677 |
+
{
|
| 678 |
+
"data": {
|
| 679 |
+
"image/png": "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",
|
| 680 |
+
"text/plain": [
|
| 681 |
+
"<Figure size 1800x360 with 2 Axes>"
|
| 682 |
+
]
|
| 683 |
+
},
|
| 684 |
+
"metadata": {
|
| 685 |
+
"needs_background": "light"
|
| 686 |
+
},
|
| 687 |
+
"output_type": "display_data"
|
| 688 |
+
}
|
| 689 |
+
],
|
| 690 |
+
"source": [
|
| 691 |
+
"sn_char = [c for c in sentence]\n",
|
| 692 |
+
"sn_char = [''] + sn_char + ['SEP']\n",
|
| 693 |
+
"sample_index=0\n",
|
| 694 |
+
"timestep = 2\n",
|
| 695 |
+
"visualize_distribution(syn_scanpath=syn_scanpath[sample_index],\n",
|
| 696 |
+
" density_pred=density_pred[timestep][sample_index].detach().numpy(),\n",
|
| 697 |
+
" timestep=timestep,\n",
|
| 698 |
+
" text=sn_char)"
|
| 699 |
+
]
|
| 700 |
+
},
|
| 701 |
+
{
|
| 702 |
+
"cell_type": "code",
|
| 703 |
+
"execution_count": 33,
|
| 704 |
+
"id": "orange-persian",
|
| 705 |
+
"metadata": {
|
| 706 |
+
"colab": {
|
| 707 |
+
"base_uri": "https://localhost:8080/",
|
| 708 |
+
"height": 130
|
| 709 |
+
},
|
| 710 |
+
"id": "orange-persian",
|
| 711 |
+
"outputId": "df44ad8a-8a17-4f20-9c1d-3dea8be063a7"
|
| 712 |
+
},
|
| 713 |
+
"outputs": [
|
| 714 |
+
{
|
| 715 |
+
"data": {
|
| 716 |
+
"image/png": "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",
|
| 717 |
+
"text/plain": [
|
| 718 |
+
"<Figure size 1800x360 with 2 Axes>"
|
| 719 |
+
]
|
| 720 |
+
},
|
| 721 |
+
"metadata": {
|
| 722 |
+
"needs_background": "light"
|
| 723 |
+
},
|
| 724 |
+
"output_type": "display_data"
|
| 725 |
+
}
|
| 726 |
+
],
|
| 727 |
+
"source": [
|
| 728 |
+
"sample_index=0\n",
|
| 729 |
+
"timestep = 3\n",
|
| 730 |
+
"visualize_distribution(syn_scanpath=syn_scanpath[sample_index],\n",
|
| 731 |
+
" density_pred=density_pred[timestep][sample_index].detach().numpy(),\n",
|
| 732 |
+
" timestep=timestep,\n",
|
| 733 |
+
" text=sn_char)"
|
| 734 |
+
]
|
| 735 |
+
}
|
| 736 |
+
],
|
| 737 |
+
"metadata": {
|
| 738 |
+
"colab": {
|
| 739 |
+
"provenance": []
|
| 740 |
+
},
|
| 741 |
+
"kernelspec": {
|
| 742 |
+
"display_name": "Python 3",
|
| 743 |
+
"language": "python",
|
| 744 |
+
"name": "python3"
|
| 745 |
+
},
|
| 746 |
+
"language_info": {
|
| 747 |
+
"codemirror_mode": {
|
| 748 |
+
"name": "ipython",
|
| 749 |
+
"version": 3
|
| 750 |
+
},
|
| 751 |
+
"file_extension": ".py",
|
| 752 |
+
"mimetype": "text/x-python",
|
| 753 |
+
"name": "python",
|
| 754 |
+
"nbconvert_exporter": "python",
|
| 755 |
+
"pygments_lexer": "ipython3",
|
| 756 |
+
"version": "3.10.7"
|
| 757 |
+
}
|
| 758 |
+
},
|
| 759 |
+
"nbformat": 4,
|
| 760 |
+
"nbformat_minor": 5
|
| 761 |
+
}
|
LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2023 AEye
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
README.md
CHANGED
|
@@ -1,3 +1,132 @@
|
|
| 1 |
-
--
|
| 2 |
-
|
| 3 |
-
---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Eyettention: An Attention-based Dual-Sequence Model for Predicting Human Scanpaths during Reading
|
| 2 |
+
|
| 3 |
+
In this paper, we develop Eyettention, the first dual-sequence model that simultaneously processes the sequence of words and the chronological sequence of fixations. The alignment of the two sequences is achieved by a cross-sequence attention mechanism. We show that Eyettention outperforms state-of-the-art models in predicting scanpaths. We provide an extensive within- and across-data set evaluation on different languages. An ablation study and qualitative analysis support an in-depth understanding of the model's behavior.
|
| 4 |
+
|
| 5 |
+
The overview above describes the [original authors' work](https://arxiv.org/abs/2304.10784). This project uses the [original Eyettention implementation](https://github.com/aeye-lab/Eyettention), reorganized as the `Eyettention` package, with raw-text inference, scanpath-prefix replay, an endpoint handler, and a Gradio interface.
|
| 6 |
+
|
| 7 |
+
## Setup
|
| 8 |
+
|
| 9 |
+
For this checkout, run setup and inference commands from the project root (the directory containing `Eyettention/`). Install dependencies:
|
| 10 |
+
|
| 11 |
+
```bash
|
| 12 |
+
python -m pip install -r Eyettention/requirements.txt
|
| 13 |
+
```
|
| 14 |
+
|
| 15 |
+
The requirements retain historical PyTorch and Transformers pins that may need adjustment for your Python version and platform. The local preprocessing code also requires `LAC` and a compatible PaddlePaddle installation; BSC Excel loading requires `openpyxl`. Install `gradio` if using the web interface. Use a separate environment from ScanDL2 because their dependency versions differ.
|
| 16 |
+
|
| 17 |
+
## Dataset
|
| 18 |
+
|
| 19 |
+
For the CELER dataset, follow the instructions at [the CELER repository](https://github.com/berzak/celer). In order to run experiments, place the downloaded dataset in `Eyettention/Data/celer/`. The local loaders expect `data_v2.0/sent_fix.tsv`, `data_v2.0/sent_ia.tsv`, and `metadata.tsv` beneath that directory.
|
| 20 |
+
|
| 21 |
+
The Chinese [Beijing Sentence Corpus (BSC)](https://osf.io/vr3k8/) belongs in `Eyettention/Data/beijing-sentence-corpus/`, with `BSC.Word.Info.v2.xlsx` and `BSC.EMD/BSC.EMD.txt`. See [utils.py](utils.py) for the dataset loaders.
|
| 22 |
+
|
| 23 |
+
Raw-text inference uses the trained checkpoints and normalization files already stored under `results/` and `Data/`; it does not require the full training corpora.
|
| 24 |
+
|
| 25 |
+
## Run Experiments
|
| 26 |
+
|
| 27 |
+
The experiment scripts use paths relative to `Eyettention/`. Starting at the project root:
|
| 28 |
+
|
| 29 |
+
```bash
|
| 30 |
+
cd Eyettention
|
| 31 |
+
mkdir -p results/BSC results/CELER
|
| 32 |
+
```
|
| 33 |
+
|
| 34 |
+
Keep the project root on `PYTHONPATH` when running the package modules.
|
| 35 |
+
|
| 36 |
+
### For Chinese BSC dataset
|
| 37 |
+
|
| 38 |
+
```bash
|
| 39 |
+
PYTHONPATH=.. python -m Eyettention.main_BSC --test_mode text --gpu 0
|
| 40 |
+
PYTHONPATH=.. python -m Eyettention.main_BSC --test_mode subject --gpu 0
|
| 41 |
+
PYTHONPATH=.. python -m Eyettention.main_BSC_NRS_setting --gpu 0
|
| 42 |
+
PYTHONPATH=.. python -m Eyettention.main_BSC_reader_identifier --gpu 0
|
| 43 |
+
```
|
| 44 |
+
|
| 45 |
+
### For English CELER dataset
|
| 46 |
+
|
| 47 |
+
```bash
|
| 48 |
+
PYTHONPATH=.. python -m Eyettention.main_celer --test_mode text --gpu 0
|
| 49 |
+
PYTHONPATH=.. python -m Eyettention.main_celer --test_mode subject --gpu 0
|
| 50 |
+
PYTHONPATH=.. python -m Eyettention.main_celer_NRS_setting --gpu 0
|
| 51 |
+
PYTHONPATH=.. python -m Eyettention.main_celer_reader_identifier --gpu 0
|
| 52 |
+
```
|
| 53 |
+
|
| 54 |
+
`text` evaluates new sentences; `subject` evaluates new readers. Select an available GPU with `--gpu`. Several historical experiment scripts call CUDA directly, so use a CUDA-enabled environment for these commands. Review output paths before running; `--save_data_folder` selects the results directory.
|
| 55 |
+
|
| 56 |
+
## Raw-text inference
|
| 57 |
+
|
| 58 |
+
Run the following from the project root:
|
| 59 |
+
|
| 60 |
+
```python
|
| 61 |
+
from Eyettention import EyettentionRawTextInference
|
| 62 |
+
|
| 63 |
+
model = EyettentionRawTextInference(
|
| 64 |
+
checkpoint_path="Eyettention/results/CELER/Eyettention_english.pth",
|
| 65 |
+
dataset="celer",
|
| 66 |
+
device="cpu",
|
| 67 |
+
)
|
| 68 |
+
scanpath, density = model.generate_from_english_text(
|
| 69 |
+
"The quick brown fox jumps.",
|
| 70 |
+
max_pred_len=20,
|
| 71 |
+
# Optional: replay observed fixation positions before sampling.
|
| 72 |
+
# previous_scanpath=[0, 1, 2],
|
| 73 |
+
)
|
| 74 |
+
print(scanpath[0].tolist())
|
| 75 |
+
```
|
| 76 |
+
|
| 77 |
+
For Chinese, use `dataset="BSC"`, checkpoint `Eyettention/results/BSC/Eyettention_chinese.pth`, and `generate_from_chinese_text(...)`. Dataset names are case-sensitive. Use `device="cuda"` for GPU inference. BERT assets must be downloadable from Hugging Face or cached locally.
|
| 78 |
+
|
| 79 |
+
Keep `Data/feature_norm_celer.pickle` and `Data/feature_norm_BSC.pickle` alongside the package. The output contains fixation indices and per-step probability distributions, not fixation durations. Index `0` denotes CLS, positions `1..N` refer to words or Chinese characters, and `N+1` denotes SEP; stop at the first SEP when interpreting fixations. Use short sentences within the checkpoint's configured input limits. Prefix replay extends the generation loop, so `max_pred_len` is not a strict total-length cap when a prefix is provided.
|
| 80 |
+
|
| 81 |
+
## Endpoint handler
|
| 82 |
+
|
| 83 |
+
The added [handler.py](handler.py) wraps inference in a dictionary-based interface:
|
| 84 |
+
|
| 85 |
+
```python
|
| 86 |
+
from Eyettention.handler import EndpointHandler
|
| 87 |
+
|
| 88 |
+
handler = EndpointHandler()
|
| 89 |
+
result = handler({
|
| 90 |
+
"inputs": "The quick brown fox jumps.",
|
| 91 |
+
"parameters": {
|
| 92 |
+
"dataset": "celer",
|
| 93 |
+
"device": "cpu",
|
| 94 |
+
"max_pred_len": 20,
|
| 95 |
+
},
|
| 96 |
+
})
|
| 97 |
+
```
|
| 98 |
+
|
| 99 |
+
It returns `scanpath` and `density_steps`. Optional parameters include `checkpoint_path` and `previous_scanpath`. The handler defaults to BSC on CPU and does not itself start an HTTP server.
|
| 100 |
+
|
| 101 |
+
## Gradio interface
|
| 102 |
+
|
| 103 |
+
The added [app.py](app.py) provides a web interface. From the project root, point it to the local checkpoints and launch:
|
| 104 |
+
|
| 105 |
+
```bash
|
| 106 |
+
export EYETTENTION_BSC_CHECKPOINT="Eyettention/results/BSC/Eyettention_chinese.pth"
|
| 107 |
+
export EYETTENTION_CELER_CHECKPOINT="Eyettention/results/CELER/Eyettention_english.pth"
|
| 108 |
+
python -m Eyettention.app
|
| 109 |
+
```
|
| 110 |
+
|
| 111 |
+
Open `http://localhost:7860`, choose the dataset, enter text, and click **Run**. The interface also supports an observed scanpath prefix. It selects CUDA when available, otherwise CPU. Set `PORT` to change the default port; the app binds to `0.0.0.0`.
|
| 112 |
+
|
| 113 |
+
## Cite our work
|
| 114 |
+
|
| 115 |
+
If you use this code for your research, please consider citing the original authors' paper:
|
| 116 |
+
|
| 117 |
+
```bibtex
|
| 118 |
+
@article{deng2023eyettention,
|
| 119 |
+
title={Eyettention: {A}n Attention-based Dual-Sequence Model for Predicting Human Scanpaths during Reading},
|
| 120 |
+
author={Deng, Shuwen and Reich, David R and Prasse, Paul and Haller, Patrick and Scheffer, Tobias and J{\"a}ger, Lena A},
|
| 121 |
+
journal={Proceedings of the {ACM} on Human-Computer Interaction},
|
| 122 |
+
volume={7},
|
| 123 |
+
number={ETRA},
|
| 124 |
+
pages={1--24},
|
| 125 |
+
year={2023},
|
| 126 |
+
publisher={ACM New York, NY, USA}
|
| 127 |
+
}
|
| 128 |
+
```
|
| 129 |
+
|
| 130 |
+
## License
|
| 131 |
+
|
| 132 |
+
The original code is provided under the [MIT License](LICENSE.txt), copyright © 2023 AEye. Include the copyright and permission notice when redistributing copies or substantial portions of the software. See the license file for the full terms and warranty disclaimer. Datasets and dependencies retain their own licenses.
|
__init__.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# API implementation
|
| 2 |
+
|
| 3 |
+
from Eyettention.model import Eyettention, Eyettention_readerID
|
| 4 |
+
from Eyettention.raw_text_inference import EyettentionRawTextInference
|
| 5 |
+
|
| 6 |
+
from Eyettention.utils import (
|
| 7 |
+
BSCdataset,
|
| 8 |
+
calculate_mean_std,
|
| 9 |
+
celerdataset,
|
| 10 |
+
eval_log_llh,
|
| 11 |
+
gradient_clipping,
|
| 12 |
+
load_corpus,
|
| 13 |
+
load_label,
|
| 14 |
+
prepare_scanpath,
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
__all__ = [
|
| 18 |
+
"Eyettention",
|
| 19 |
+
"Eyettention_readerID",
|
| 20 |
+
"EyettentionRawTextInference",
|
| 21 |
+
"BSCdataset",
|
| 22 |
+
"calculate_mean_std",
|
| 23 |
+
"celerdataset",
|
| 24 |
+
"eval_log_llh",
|
| 25 |
+
"gradient_clipping",
|
| 26 |
+
"load_corpus",
|
| 27 |
+
"load_label",
|
| 28 |
+
"prepare_scanpath",
|
| 29 |
+
]
|
app.py
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
import json
|
| 4 |
+
from typing import Any, Dict, List, Optional, Tuple
|
| 5 |
+
|
| 6 |
+
import gradio as gr
|
| 7 |
+
import torch
|
| 8 |
+
import numpy as np
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
from Eyettention.raw_text_inference import EyettentionRawTextInference
|
| 12 |
+
|
| 13 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 14 |
+
|
| 15 |
+
BSC_CHECKPOINT = os.environ.get(
|
| 16 |
+
"EYETTENTION_BSC_CHECKPOINT",
|
| 17 |
+
os.path.join(os.path.dirname(__file__), "checkpoints", "bsc.pt"),
|
| 18 |
+
)
|
| 19 |
+
CELER_CHECKPOINT = os.environ.get(
|
| 20 |
+
"EYETTENTION_CELER_CHECKPOINT",
|
| 21 |
+
os.path.join(os.path.dirname(__file__), "checkpoints", "celer.pt"),
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
_MODELS: Dict[str, EyettentionRawTextInference] = {}
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def get_model(dataset: str) -> EyettentionRawTextInference:
|
| 28 |
+
"""Load and cache an Eyettention model for the given dataset."""
|
| 29 |
+
if dataset not in _MODELS:
|
| 30 |
+
if dataset == "BSC":
|
| 31 |
+
ckpt = BSC_CHECKPOINT
|
| 32 |
+
elif dataset == "celer":
|
| 33 |
+
ckpt = CELER_CHECKPOINT
|
| 34 |
+
else:
|
| 35 |
+
raise ValueError(f"Unsupported dataset: {dataset}")
|
| 36 |
+
|
| 37 |
+
if not os.path.exists(ckpt):
|
| 38 |
+
raise FileNotFoundError(
|
| 39 |
+
f"Checkpoint not found at {ckpt!r}. "
|
| 40 |
+
f"Set EYETTENTION_{dataset.upper()}_CHECKPOINT to the correct path."
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 44 |
+
_MODELS[dataset] = EyettentionRawTextInference(
|
| 45 |
+
checkpoint_path=ckpt,
|
| 46 |
+
dataset=dataset,
|
| 47 |
+
device=device,
|
| 48 |
+
)
|
| 49 |
+
return _MODELS[dataset]
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def words_for_text(text: str, dataset: str) -> List[str]:
|
| 53 |
+
"""
|
| 54 |
+
Return the list of words that the model's fixation indices refer to.
|
| 55 |
+
|
| 56 |
+
- For BSC (Chinese): the model tokenizes per character, so we return the
|
| 57 |
+
list of characters in the input (excluding whitespace).
|
| 58 |
+
- For CELER (English): the model tokenizes with a BERT wordpiece tokenizer
|
| 59 |
+
on whitespace-split words, so we return the whitespace-split words.
|
| 60 |
+
"""
|
| 61 |
+
if dataset == "BSC":
|
| 62 |
+
|
| 63 |
+
return [ch for ch in text if not ch.isspace()]
|
| 64 |
+
else:
|
| 65 |
+
return text.split()
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
# ---------------------------------------------------------------------------
|
| 69 |
+
# Decoding helpers
|
| 70 |
+
# ---------------------------------------------------------------------------
|
| 71 |
+
def decode_scanpath(
|
| 72 |
+
scanpath_indices: torch.Tensor,
|
| 73 |
+
words: List[str],
|
| 74 |
+
dataset: str,
|
| 75 |
+
) -> List[Dict[str, Any]]:
|
| 76 |
+
"""
|
| 77 |
+
Convert a single predicted scanpath (indices into the word/char sequence)
|
| 78 |
+
into a list of dicts describing each fixation.
|
| 79 |
+
|
| 80 |
+
Convention in Eyettention's `scanpath_generation`:
|
| 81 |
+
* index 0 corresponds to the CLS token (sentence start),
|
| 82 |
+
* indices 1..len(words) correspond to words/characters,
|
| 83 |
+
* index len(words)+1 corresponds to SEP (sentence end).
|
| 84 |
+
|
| 85 |
+
We drop the leading CLS and any trailing SEP here.
|
| 86 |
+
"""
|
| 87 |
+
if isinstance(scanpath_indices, torch.Tensor):
|
| 88 |
+
scanpath_indices = scanpath_indices.detach().cpu().tolist()
|
| 89 |
+
|
| 90 |
+
fixations: List[Dict[str, Any]] = []
|
| 91 |
+
n_words = len(words)
|
| 92 |
+
|
| 93 |
+
for step, idx in enumerate(scanpath_indices):
|
| 94 |
+
idx = int(idx)
|
| 95 |
+
|
| 96 |
+
if step == 0 and idx == 0:
|
| 97 |
+
continue
|
| 98 |
+
|
| 99 |
+
if idx == 0:
|
| 100 |
+
word = "<CLS>"
|
| 101 |
+
elif idx == n_words + 1:
|
| 102 |
+
word = "<SEP>"
|
| 103 |
+
elif 1 <= idx <= n_words:
|
| 104 |
+
word = words[idx - 1]
|
| 105 |
+
else:
|
| 106 |
+
word = f"<OOR:{idx}>"
|
| 107 |
+
|
| 108 |
+
fixations.append(
|
| 109 |
+
{
|
| 110 |
+
"step": len(fixations) + 1,
|
| 111 |
+
"word": word,
|
| 112 |
+
"word_index": idx,
|
| 113 |
+
}
|
| 114 |
+
)
|
| 115 |
+
|
| 116 |
+
return fixations
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def fixations_to_markdown(
|
| 120 |
+
fixations: List[Dict[str, Any]],
|
| 121 |
+
original_text: str,
|
| 122 |
+
dataset: str,
|
| 123 |
+
) -> str:
|
| 124 |
+
"""Render a scanpath as a Markdown table."""
|
| 125 |
+
lines = [
|
| 126 |
+
f"**Input ({dataset}):** {original_text}",
|
| 127 |
+
"",
|
| 128 |
+
"| Step | Fixated unit | Index |",
|
| 129 |
+
"|------|--------------|-------|",
|
| 130 |
+
]
|
| 131 |
+
for f in fixations:
|
| 132 |
+
lines.append(f"| {f['step']} | {f['word']} | {f['word_index']} |")
|
| 133 |
+
return "\n".join(lines)
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def predict(
|
| 137 |
+
text: str,
|
| 138 |
+
dataset: str,
|
| 139 |
+
max_pred_len: int,
|
| 140 |
+
use_previous_scanpath: bool,
|
| 141 |
+
previous_scanpath: str,
|
| 142 |
+
progress=gr.Progress(track_tqdm=True),
|
| 143 |
+
) -> Tuple[str, str, str]:
|
| 144 |
+
"""
|
| 145 |
+
Run Eyettention on the input text and return:
|
| 146 |
+
- a Markdown table of the predicted scanpath,
|
| 147 |
+
- a space-separated string of the predicted word/char sequence,
|
| 148 |
+
- the raw JSON of the scanpath indices and fixated units.
|
| 149 |
+
"""
|
| 150 |
+
if text is None or text.strip() == "":
|
| 151 |
+
raise gr.Error("Please provide some input text.")
|
| 152 |
+
|
| 153 |
+
if dataset == "celer" and not any(c.isalpha() for c in text):
|
| 154 |
+
raise gr.Error("CELER is the English model — please provide English text.")
|
| 155 |
+
if dataset == "BSC" and not any("\u4e00" <= c <= "\u9fff" for c in text):
|
| 156 |
+
|
| 157 |
+
pass
|
| 158 |
+
|
| 159 |
+
if max_pred_len <= 0:
|
| 160 |
+
raise gr.Error("max_pred_len must be a positive integer.")
|
| 161 |
+
|
| 162 |
+
progress(0.05, desc="Loading Eyettention model...")
|
| 163 |
+
model = get_model(dataset)
|
| 164 |
+
|
| 165 |
+
prev: Optional[List[int]] = None
|
| 166 |
+
if use_previous_scanpath and previous_scanpath.strip():
|
| 167 |
+
try:
|
| 168 |
+
prev = [int(x) for x in previous_scanpath.replace(",", " ").split()]
|
| 169 |
+
except ValueError:
|
| 170 |
+
raise gr.Error(
|
| 171 |
+
"previous_scanpath must be a whitespace/comma-separated list of integers."
|
| 172 |
+
)
|
| 173 |
+
|
| 174 |
+
progress(0.25, desc="Running autoregressive scanpath generation...")
|
| 175 |
+
with torch.no_grad():
|
| 176 |
+
if dataset == "BSC":
|
| 177 |
+
scanpath, _density = model.generate_from_chinese_text(
|
| 178 |
+
text=text,
|
| 179 |
+
max_pred_len=max_pred_len,
|
| 180 |
+
previous_scanpath=prev,
|
| 181 |
+
)
|
| 182 |
+
else:
|
| 183 |
+
scanpath, _density = model.generate_from_english_text(
|
| 184 |
+
text=text,
|
| 185 |
+
max_pred_len=max_pred_len,
|
| 186 |
+
previous_scanpath=prev,
|
| 187 |
+
)
|
| 188 |
+
|
| 189 |
+
scanpath = scanpath[0]
|
| 190 |
+
|
| 191 |
+
progress(0.9, desc="Formatting output...")
|
| 192 |
+
words = words_for_text(text, dataset)
|
| 193 |
+
fixations = decode_scanpath(scanpath, words, dataset)
|
| 194 |
+
|
| 195 |
+
markdown = fixations_to_markdown(fixations, text, dataset)
|
| 196 |
+
word_seq = " ".join(f["word"] for f in fixations)
|
| 197 |
+
json_out = json.dumps(
|
| 198 |
+
{
|
| 199 |
+
"dataset": dataset,
|
| 200 |
+
"input": text,
|
| 201 |
+
"scanpath_indices": [int(i) for i in scanpath.detach().cpu().tolist()],
|
| 202 |
+
"fixated_units": [f["word"] for f in fixations],
|
| 203 |
+
},
|
| 204 |
+
ensure_ascii=False,
|
| 205 |
+
indent=2,
|
| 206 |
+
)
|
| 207 |
+
|
| 208 |
+
return markdown, word_seq, json_out
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
DESCRIPTION = """
|
| 212 |
+
# Eyettention — Scanpath Prediction
|
| 213 |
+
|
| 214 |
+
**Eyettention** predicts human-like **eye-movement scanpaths** from raw text.
|
| 215 |
+
Given a sentence, it autoregressively generates a sequence of fixation
|
| 216 |
+
locations (word or character indices) that approximate where a reader would
|
| 217 |
+
look, in order.
|
| 218 |
+
|
| 219 |
+
Two checkpoints are supported:
|
| 220 |
+
|
| 221 |
+
| Dataset | Language | Tokenization |
|
| 222 |
+
|---------|----------|--------------|
|
| 223 |
+
| **BSC** | Chinese | Character-level |
|
| 224 |
+
| **CELER** | English | Word-level (BERT wordpieces pooled) |
|
| 225 |
+
|
| 226 |
+
### How to use
|
| 227 |
+
1. Choose the **dataset / language**.
|
| 228 |
+
2. Paste your text.
|
| 229 |
+
3. Adjust `max_pred_len` if you want shorter or longer scanpaths.
|
| 230 |
+
4. Click **Run**.
|
| 231 |
+
|
| 232 |
+
### Optional: replay an observed prefix
|
| 233 |
+
Enable **Use previous scanpath** and provide a whitespace- or comma-separated
|
| 234 |
+
list of integer fixation indices. Those fixations will be replayed before the
|
| 235 |
+
model starts sampling new ones (useful for conditional generation / prefix
|
| 236 |
+
completion).
|
| 237 |
+
"""
|
| 238 |
+
|
| 239 |
+
EXAMPLES = [
|
| 240 |
+
[
|
| 241 |
+
"He said BankEast's offer appears to be \"attractive to the bank's shareholders.\"",
|
| 242 |
+
"celer",
|
| 243 |
+
20,
|
| 244 |
+
False,
|
| 245 |
+
"",
|
| 246 |
+
],
|
| 247 |
+
["中国选手在男子滑雪比赛中有望蝉联冠军", "BSC", 20, False, ""],
|
| 248 |
+
]
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
def build_demo() -> gr.Blocks:
|
| 252 |
+
with gr.Blocks(
|
| 253 |
+
title="Eyettention — Scanpath Prediction",
|
| 254 |
+
theme=gr.themes.Soft(),
|
| 255 |
+
) as demo:
|
| 256 |
+
gr.Markdown(DESCRIPTION)
|
| 257 |
+
|
| 258 |
+
with gr.Row():
|
| 259 |
+
with gr.Column(scale=3):
|
| 260 |
+
text_in = gr.Textbox(
|
| 261 |
+
label="Input text",
|
| 262 |
+
placeholder="Enter a sentence...",
|
| 263 |
+
lines=5,
|
| 264 |
+
)
|
| 265 |
+
dataset_in = gr.Radio(
|
| 266 |
+
choices=["celer", "BSC"],
|
| 267 |
+
value="celer",
|
| 268 |
+
label="Dataset / language",
|
| 269 |
+
info="celer = English, BSC = Chinese",
|
| 270 |
+
)
|
| 271 |
+
max_len_in = gr.Slider(
|
| 272 |
+
minimum=1,
|
| 273 |
+
maximum=120,
|
| 274 |
+
value=30,
|
| 275 |
+
step=1,
|
| 276 |
+
label="max_pred_len",
|
| 277 |
+
info="Maximum number of fixations to generate.",
|
| 278 |
+
)
|
| 279 |
+
with gr.Accordion("Advanced: replay an observed prefix", open=False):
|
| 280 |
+
use_prev_in = gr.Checkbox(
|
| 281 |
+
value=False,
|
| 282 |
+
label="Use previous scanpath",
|
| 283 |
+
)
|
| 284 |
+
prev_in = gr.Textbox(
|
| 285 |
+
label="Previous scanpath (integer indices)",
|
| 286 |
+
placeholder="e.g. 0 1 3 5 4",
|
| 287 |
+
lines=2,
|
| 288 |
+
)
|
| 289 |
+
with gr.Row():
|
| 290 |
+
run_btn = gr.Button("Run", variant="primary")
|
| 291 |
+
clear_btn = gr.Button("Clear")
|
| 292 |
+
|
| 293 |
+
with gr.Column(scale=4):
|
| 294 |
+
table_out = gr.Markdown(
|
| 295 |
+
label="Predicted scanpath",
|
| 296 |
+
value="_Results will appear here._",
|
| 297 |
+
)
|
| 298 |
+
seq_out = gr.Textbox(
|
| 299 |
+
label="Fixated units (sequence)",
|
| 300 |
+
lines=3,
|
| 301 |
+
interactive=False,
|
| 302 |
+
)
|
| 303 |
+
json_out = gr.Code(
|
| 304 |
+
label="Raw JSON output",
|
| 305 |
+
language="json",
|
| 306 |
+
value="{}",
|
| 307 |
+
)
|
| 308 |
+
|
| 309 |
+
gr.Examples(
|
| 310 |
+
examples=EXAMPLES,
|
| 311 |
+
inputs=[text_in, dataset_in, max_len_in, use_prev_in, prev_in],
|
| 312 |
+
)
|
| 313 |
+
|
| 314 |
+
def _run(text, dataset, max_pred_len, use_prev, prev):
|
| 315 |
+
md, seq, js = predict(
|
| 316 |
+
text=text,
|
| 317 |
+
dataset=dataset,
|
| 318 |
+
max_pred_len=int(max_pred_len),
|
| 319 |
+
use_previous_scanpath=use_prev,
|
| 320 |
+
previous_scanpath=prev,
|
| 321 |
+
)
|
| 322 |
+
return md, seq, js
|
| 323 |
+
|
| 324 |
+
run_btn.click(
|
| 325 |
+
fn=_run,
|
| 326 |
+
inputs=[text_in, dataset_in, max_len_in, use_prev_in, prev_in],
|
| 327 |
+
outputs=[table_out, seq_out, json_out],
|
| 328 |
+
)
|
| 329 |
+
clear_btn.click(
|
| 330 |
+
fn=lambda: ("", "celer", 30, False, "", "_Results will appear here._", "", "{}"),
|
| 331 |
+
inputs=None,
|
| 332 |
+
outputs=[
|
| 333 |
+
text_in,
|
| 334 |
+
dataset_in,
|
| 335 |
+
max_len_in,
|
| 336 |
+
use_prev_in,
|
| 337 |
+
prev_in,
|
| 338 |
+
table_out,
|
| 339 |
+
seq_out,
|
| 340 |
+
json_out,
|
| 341 |
+
],
|
| 342 |
+
)
|
| 343 |
+
|
| 344 |
+
return demo
|
| 345 |
+
|
| 346 |
+
|
| 347 |
+
if __name__ == "__main__":
|
| 348 |
+
demo = build_demo()
|
| 349 |
+
demo.queue(max_size=16).launch(
|
| 350 |
+
server_name="0.0.0.0",
|
| 351 |
+
server_port=int(os.environ.get("PORT", 7860)),
|
| 352 |
+
show_error=True,
|
| 353 |
+
)
|
handler.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pathlib import Path
|
| 2 |
+
import torch
|
| 3 |
+
|
| 4 |
+
from Eyettention import Eyettention
|
| 5 |
+
from Eyettention import EyettentionRawTextInference
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class EndpointHandler:
|
| 9 |
+
def __init__(self, path: str = ""):
|
| 10 |
+
self.path = Path(path) if path else Path(__file__).resolve().parent
|
| 11 |
+
self.inference = None
|
| 12 |
+
self.dataset = None
|
| 13 |
+
self.checkpoint_path = None
|
| 14 |
+
self.device = None
|
| 15 |
+
|
| 16 |
+
def __call__(self, data):
|
| 17 |
+
inputs = data.get("inputs", data)
|
| 18 |
+
parameters = data.get("parameters", {})
|
| 19 |
+
dataset = parameters.get("dataset", "BSC") # default = BSC
|
| 20 |
+
max_pred_len = parameters.get("max_pred_len", 60)
|
| 21 |
+
previous_scanpath = parameters.get("previous_scanpath")
|
| 22 |
+
|
| 23 |
+
inference = self._get_inference(dataset, parameters)
|
| 24 |
+
|
| 25 |
+
if isinstance(inputs, str):
|
| 26 |
+
if dataset == "BSC":
|
| 27 |
+
scanpath, density = inference.generate_from_chinese_text(
|
| 28 |
+
text=inputs, max_pred_len=max_pred_len, previous_scanpath=previous_scanpath
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
elif dataset == "celer":
|
| 32 |
+
scanpath, density = inference.generate_from_english_text(
|
| 33 |
+
text=inputs, max_pred_len=max_pred_len, previous_scanpath=previous_scanpath
|
| 34 |
+
)
|
| 35 |
+
else:
|
| 36 |
+
raise ValueError(f"Unsupported dataset: {dataset}")
|
| 37 |
+
|
| 38 |
+
elif isinstance(inputs, dict):
|
| 39 |
+
with torch.no_grad():
|
| 40 |
+
scanpath, density = inference.model.scanpath_generation(
|
| 41 |
+
sn_emd=inputs["sn_input_ids"],
|
| 42 |
+
sn_mask=inputs["sn_mask"],
|
| 43 |
+
word_ids_sn=inputs.get("word_ids_sn"),
|
| 44 |
+
sn_word_len=inputs["sn_word_len"],
|
| 45 |
+
le=inference.label_encoder,
|
| 46 |
+
max_pred_len=max_pred_len or inference.cf["max_pred_len"],
|
| 47 |
+
previous_scanpath=previous_scanpath,
|
| 48 |
+
)
|
| 49 |
+
else:
|
| 50 |
+
raise ValueError("'inputs' must be raw text or model-ready tensors.")
|
| 51 |
+
|
| 52 |
+
return {
|
| 53 |
+
"scanpath": scanpath.tolist(),
|
| 54 |
+
"density_steps": len(density),
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
def _get_inference(self, dataset, parameters):
|
| 58 |
+
device = parameters.get("device", "cpu")
|
| 59 |
+
checkpoint_path = parameters.get("checkpoint_path") or self._default_checkpoint_path(
|
| 60 |
+
dataset
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
if (
|
| 64 |
+
self.inference is None
|
| 65 |
+
or self.dataset != dataset
|
| 66 |
+
or self.checkpoint_path != checkpoint_path
|
| 67 |
+
or self.device != device
|
| 68 |
+
):
|
| 69 |
+
self.inference = EyettentionRawTextInference(
|
| 70 |
+
checkpoint_path=str(checkpoint_path), dataset=dataset, device=device
|
| 71 |
+
)
|
| 72 |
+
self.dataset = dataset
|
| 73 |
+
self.checkpoint_path = checkpoint_path
|
| 74 |
+
self.device = device
|
| 75 |
+
|
| 76 |
+
return self.inference
|
| 77 |
+
|
| 78 |
+
def _default_checkpoint_path(self, dataset):
|
| 79 |
+
if dataset == "BSC":
|
| 80 |
+
return self.path / "results" / "BSC" / "Eyettention_chinese.pth"
|
| 81 |
+
if dataset == "celer":
|
| 82 |
+
return self.path / "results" / "CELER" / "Eyettention_english.pth"
|
| 83 |
+
raise ValueError(f"Unsupported dataset: {dataset}")
|
main_BSC.py
ADDED
|
@@ -0,0 +1,406 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import os
|
| 4 |
+
from sklearn.model_selection import StratifiedKFold, KFold
|
| 5 |
+
import torch
|
| 6 |
+
import torch.nn as nn
|
| 7 |
+
from torch.utils.data import DataLoader
|
| 8 |
+
from torch.optim import Adam, RMSprop
|
| 9 |
+
from transformers import BertTokenizer
|
| 10 |
+
from sklearn.preprocessing import LabelEncoder, OneHotEncoder
|
| 11 |
+
from torch.nn.functional import cross_entropy, softmax
|
| 12 |
+
from collections import deque
|
| 13 |
+
import pickle
|
| 14 |
+
import json
|
| 15 |
+
import matplotlib.pyplot as plt
|
| 16 |
+
import argparse
|
| 17 |
+
|
| 18 |
+
from Eyettention.utils import *
|
| 19 |
+
from Eyettention.model import Eyettention
|
| 20 |
+
|
| 21 |
+
if __name__ == "__main__":
|
| 22 |
+
parser = argparse.ArgumentParser(description="run Eyettention on BSC dataset")
|
| 23 |
+
parser.add_argument(
|
| 24 |
+
"--test_mode",
|
| 25 |
+
help="New Sentence Split: text, New Reader Split: subject",
|
| 26 |
+
type=str,
|
| 27 |
+
default="text",
|
| 28 |
+
)
|
| 29 |
+
parser.add_argument(
|
| 30 |
+
"--atten_type", help="attention type: global, local, local-g", type=str, default="local-g"
|
| 31 |
+
)
|
| 32 |
+
parser.add_argument(
|
| 33 |
+
"--save_data_folder",
|
| 34 |
+
help="folder path for saving results",
|
| 35 |
+
type=str,
|
| 36 |
+
default="./results/BSC/",
|
| 37 |
+
)
|
| 38 |
+
parser.add_argument(
|
| 39 |
+
"--scanpath_gen_flag", help="whether to generate scanpath", type=int, default=1
|
| 40 |
+
)
|
| 41 |
+
parser.add_argument(
|
| 42 |
+
"--max_pred_len",
|
| 43 |
+
help="if scanpath_gen_flag is True, you can determine the longest scanpath that you want to generate, which should depend on the sentence length",
|
| 44 |
+
type=int,
|
| 45 |
+
default=60,
|
| 46 |
+
)
|
| 47 |
+
parser.add_argument("--gpu", help="gpu index", type=int, default=7)
|
| 48 |
+
args = parser.parse_args()
|
| 49 |
+
gpu = args.gpu
|
| 50 |
+
os.makedirs(args.save_data_folder, exist_ok=True)
|
| 51 |
+
|
| 52 |
+
torch.set_default_tensor_type("torch.FloatTensor")
|
| 53 |
+
availbl = torch.cuda.is_available()
|
| 54 |
+
print(torch.cuda.is_available())
|
| 55 |
+
if availbl:
|
| 56 |
+
device = f"cuda:{gpu}"
|
| 57 |
+
else:
|
| 58 |
+
device = "cpu"
|
| 59 |
+
if availbl:
|
| 60 |
+
torch.cuda.set_device(gpu)
|
| 61 |
+
|
| 62 |
+
cf = build_bsc_config(
|
| 63 |
+
atten_type=args.atten_type,
|
| 64 |
+
max_pred_len=args.max_pred_len,
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
+
# Encode the label into interger categories, setting the exclusive category 'cf["max_sn_len"]-1' as the end sign
|
| 68 |
+
le = build_label_encoder(cf)
|
| 69 |
+
# le.classes_
|
| 70 |
+
|
| 71 |
+
# load corpus
|
| 72 |
+
word_info_df, pos_info_df, eyemovement_df = load_corpus(cf["dataset"])
|
| 73 |
+
# Make list with sentence index
|
| 74 |
+
sn_list = np.unique(eyemovement_df.sn.values).tolist()
|
| 75 |
+
# Make list with reader index
|
| 76 |
+
reader_list = np.unique(eyemovement_df.id.values).tolist()
|
| 77 |
+
|
| 78 |
+
# Split training&test sets by text or reader, depending on configuration
|
| 79 |
+
if args.test_mode == "text":
|
| 80 |
+
print("Start evaluating on new sentences.")
|
| 81 |
+
split_list = sn_list
|
| 82 |
+
elif args.test_mode == "subject":
|
| 83 |
+
print("Start evaluating on new readers.")
|
| 84 |
+
split_list = reader_list
|
| 85 |
+
|
| 86 |
+
n_folds = cf["n_folds"]
|
| 87 |
+
kf = KFold(n_splits=n_folds, shuffle=True, random_state=0)
|
| 88 |
+
fold_indx = 0
|
| 89 |
+
# for scanpath generation
|
| 90 |
+
sp_dnn_list = []
|
| 91 |
+
sp_human_list = []
|
| 92 |
+
for train_idx, test_idx in kf.split(split_list):
|
| 93 |
+
loss_dict = {"val_loss": [], "train_loss": [], "test_ll": []}
|
| 94 |
+
list_train = [split_list[i] for i in train_idx]
|
| 95 |
+
list_test = [split_list[i] for i in test_idx]
|
| 96 |
+
|
| 97 |
+
# create train validation split for training the models:
|
| 98 |
+
kf_val = KFold(n_splits=n_folds, shuffle=True, random_state=0)
|
| 99 |
+
for train_index, val_index in kf_val.split(list_train):
|
| 100 |
+
# we only evaluate a single fold
|
| 101 |
+
break
|
| 102 |
+
list_train_net = [list_train[i] for i in train_index]
|
| 103 |
+
list_val_net = [list_train[i] for i in val_index]
|
| 104 |
+
|
| 105 |
+
if args.test_mode == "text":
|
| 106 |
+
sn_list_train = list_train_net
|
| 107 |
+
sn_list_val = list_val_net
|
| 108 |
+
sn_list_test = list_test
|
| 109 |
+
reader_list_train, reader_list_val, reader_list_test = (
|
| 110 |
+
reader_list,
|
| 111 |
+
reader_list,
|
| 112 |
+
reader_list,
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
elif args.test_mode == "subject":
|
| 116 |
+
reader_list_train = list_train_net
|
| 117 |
+
reader_list_val = list_val_net
|
| 118 |
+
reader_list_test = list_test
|
| 119 |
+
sn_list_train, sn_list_val, sn_list_test = sn_list, sn_list, sn_list
|
| 120 |
+
|
| 121 |
+
# initialize tokenizer
|
| 122 |
+
tokenizer = BertTokenizer.from_pretrained(cf["model_pretrained"])
|
| 123 |
+
# Preparing batch data
|
| 124 |
+
dataset_train = BSCdataset(
|
| 125 |
+
word_info_df, eyemovement_df, cf, reader_list_train, sn_list_train, tokenizer
|
| 126 |
+
)
|
| 127 |
+
train_dataloaderr = DataLoader(
|
| 128 |
+
dataset_train, batch_size=cf["batch_size"], shuffle=True, drop_last=True
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
+
dataset_val = BSCdataset(
|
| 132 |
+
word_info_df, eyemovement_df, cf, reader_list_val, sn_list_val, tokenizer
|
| 133 |
+
)
|
| 134 |
+
val_dataloaderr = DataLoader(
|
| 135 |
+
dataset_val, batch_size=cf["batch_size"], shuffle=False, drop_last=True
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
+
dataset_test = BSCdataset(
|
| 139 |
+
word_info_df, eyemovement_df, cf, reader_list_test, sn_list_test, tokenizer
|
| 140 |
+
)
|
| 141 |
+
test_dataloaderr = DataLoader(
|
| 142 |
+
dataset_test, batch_size=cf["batch_size"], shuffle=False, drop_last=False
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
# z-score normalization for gaze features
|
| 146 |
+
fix_dur_mean, fix_dur_std = calculate_mean_std(
|
| 147 |
+
dataloader=train_dataloaderr, feat_key="sp_fix_dur", padding_value=0, scale=1000
|
| 148 |
+
)
|
| 149 |
+
landing_pos_mean, landing_pos_std = calculate_mean_std(
|
| 150 |
+
dataloader=train_dataloaderr, feat_key="sp_landing_pos", padding_value=0
|
| 151 |
+
)
|
| 152 |
+
sn_word_len_mean, sn_word_len_std = calculate_mean_std(
|
| 153 |
+
dataloader=train_dataloaderr, feat_key="sn_word_len"
|
| 154 |
+
)
|
| 155 |
+
|
| 156 |
+
# load model
|
| 157 |
+
dnn = Eyettention(cf)
|
| 158 |
+
|
| 159 |
+
# training
|
| 160 |
+
episode = 0
|
| 161 |
+
optimizer = Adam(dnn.parameters(), lr=cf["lr"])
|
| 162 |
+
dnn.train()
|
| 163 |
+
dnn.to(device)
|
| 164 |
+
av_score = deque(maxlen=100)
|
| 165 |
+
old_score = 1e10
|
| 166 |
+
save_ep_couter = 0
|
| 167 |
+
print("Start training")
|
| 168 |
+
for episode_i in range(episode, cf["n_epochs"] + 1):
|
| 169 |
+
dnn.train()
|
| 170 |
+
print("episode:", episode_i)
|
| 171 |
+
counter = 0
|
| 172 |
+
for batchh in train_dataloaderr:
|
| 173 |
+
counter += 1
|
| 174 |
+
batchh.keys()
|
| 175 |
+
sn_input_ids = batchh["sn_input_ids"].to(device)
|
| 176 |
+
sn_attention_mask = batchh["sn_attention_mask"].to(device)
|
| 177 |
+
sp_input_ids = batchh["sp_input_ids"].to(device)
|
| 178 |
+
sp_attention_mask = batchh["sp_attention_mask"].to(device)
|
| 179 |
+
sp_pos = batchh["sp_pos"].to(device)
|
| 180 |
+
sp_landing_pos = batchh["sp_landing_pos"].to(device)
|
| 181 |
+
sp_fix_dur = (batchh["sp_fix_dur"] / 1000).to(device)
|
| 182 |
+
sn_word_len = batchh["sn_word_len"].to(device)
|
| 183 |
+
|
| 184 |
+
# normalize gaze features
|
| 185 |
+
mask = ~torch.eq(sp_fix_dur, 0)
|
| 186 |
+
sp_fix_dur = (sp_fix_dur - fix_dur_mean) / fix_dur_std * mask
|
| 187 |
+
sp_fix_dur = torch.nan_to_num(sp_fix_dur)
|
| 188 |
+
sp_landing_pos = (sp_landing_pos - landing_pos_mean) / landing_pos_std * mask
|
| 189 |
+
sp_landing_pos = torch.nan_to_num(sp_landing_pos)
|
| 190 |
+
sn_word_len = (sn_word_len - sn_word_len_mean) / sn_word_len_std
|
| 191 |
+
sn_word_len = torch.nan_to_num(sn_word_len)
|
| 192 |
+
|
| 193 |
+
# zero old gradients
|
| 194 |
+
optimizer.zero_grad()
|
| 195 |
+
# predict output with DNN
|
| 196 |
+
dnn_out, atten_weights = dnn(
|
| 197 |
+
sn_emd=sn_input_ids,
|
| 198 |
+
sn_mask=sn_attention_mask,
|
| 199 |
+
sp_emd=sp_input_ids,
|
| 200 |
+
sp_pos=sp_pos,
|
| 201 |
+
word_ids_sn=None,
|
| 202 |
+
word_ids_sp=None,
|
| 203 |
+
sp_fix_dur=sp_fix_dur,
|
| 204 |
+
sp_landing_pos=sp_landing_pos,
|
| 205 |
+
sn_word_len=sn_word_len,
|
| 206 |
+
) # [batch, step, dec_o_dim]
|
| 207 |
+
|
| 208 |
+
dnn_out = dnn_out.permute(0, 2, 1) # [batch, dec_o_dim, step]
|
| 209 |
+
|
| 210 |
+
# prepare label and mask
|
| 211 |
+
pad_mask, label = load_label(sp_pos, cf, le, device)
|
| 212 |
+
loss = nn.CrossEntropyLoss(reduction="none")
|
| 213 |
+
batch_error = torch.mean(torch.masked_select(loss(dnn_out, label), ~pad_mask))
|
| 214 |
+
|
| 215 |
+
# backpropagate loss
|
| 216 |
+
batch_error.backward()
|
| 217 |
+
# clip gradients
|
| 218 |
+
gradient_clipping(dnn, cf["max_grad_norm"])
|
| 219 |
+
|
| 220 |
+
# learn
|
| 221 |
+
optimizer.step()
|
| 222 |
+
av_score.append(batch_error.to("cpu").detach().numpy())
|
| 223 |
+
print("counter:", counter)
|
| 224 |
+
print(
|
| 225 |
+
"\rSample {}\tAverage Error: {:.10f} ".format(counter, np.mean(av_score)),
|
| 226 |
+
end=" ",
|
| 227 |
+
)
|
| 228 |
+
loss_dict["train_loss"].append(np.mean(av_score))
|
| 229 |
+
|
| 230 |
+
val_loss = []
|
| 231 |
+
dnn.eval()
|
| 232 |
+
for batchh in val_dataloaderr:
|
| 233 |
+
with torch.no_grad():
|
| 234 |
+
sn_input_ids_val = batchh["sn_input_ids"].to(device)
|
| 235 |
+
sn_attention_mask_val = batchh["sn_attention_mask"].to(device)
|
| 236 |
+
sp_input_ids_val = batchh["sp_input_ids"].to(device)
|
| 237 |
+
sp_attention_mask_val = batchh["sp_attention_mask"].to(device)
|
| 238 |
+
sp_pos_val = batchh["sp_pos"].to(device)
|
| 239 |
+
sp_landing_pos_val = batchh["sp_landing_pos"].to(device)
|
| 240 |
+
sp_fix_dur_val = (batchh["sp_fix_dur"] / 1000).to(device)
|
| 241 |
+
sn_word_len_val = batchh["sn_word_len"].to(device)
|
| 242 |
+
|
| 243 |
+
# normalize gaze features
|
| 244 |
+
mask = ~torch.eq(sp_fix_dur_val, 0)
|
| 245 |
+
sp_fix_dur_val = (sp_fix_dur_val - fix_dur_mean) / fix_dur_std * mask
|
| 246 |
+
sp_landing_pos_val = (
|
| 247 |
+
(sp_landing_pos_val - landing_pos_mean) / landing_pos_std * mask
|
| 248 |
+
)
|
| 249 |
+
sp_fix_dur_val = torch.nan_to_num(sp_fix_dur_val)
|
| 250 |
+
sp_landing_pos_val = torch.nan_to_num(sp_landing_pos_val)
|
| 251 |
+
sn_word_len_val = (sn_word_len_val - sn_word_len_mean) / sn_word_len_std
|
| 252 |
+
sn_word_len_val = torch.nan_to_num(sn_word_len_val)
|
| 253 |
+
|
| 254 |
+
dnn_out_val, atten_weights_val = dnn(
|
| 255 |
+
sn_emd=sn_input_ids_val,
|
| 256 |
+
sn_mask=sn_attention_mask_val,
|
| 257 |
+
sp_emd=sp_input_ids_val,
|
| 258 |
+
sp_pos=sp_pos_val,
|
| 259 |
+
word_ids_sn=None,
|
| 260 |
+
word_ids_sp=None,
|
| 261 |
+
sp_fix_dur=sp_fix_dur_val,
|
| 262 |
+
sp_landing_pos=sp_landing_pos_val,
|
| 263 |
+
sn_word_len=sn_word_len_val,
|
| 264 |
+
) # [batch, step, dec_o_dim]
|
| 265 |
+
dnn_out_val = dnn_out_val.permute(0, 2, 1) # [batch, dec_o_dim, step
|
| 266 |
+
|
| 267 |
+
# prepare label and mask
|
| 268 |
+
loss = nn.CrossEntropyLoss(reduction="none")
|
| 269 |
+
pad_mask_val, label_val = load_label(sp_pos_val, cf, le, device)
|
| 270 |
+
batch_error_val = torch.mean(
|
| 271 |
+
torch.masked_select(loss(dnn_out_val, label_val), ~pad_mask_val)
|
| 272 |
+
)
|
| 273 |
+
val_loss.append(batch_error_val.detach().to("cpu").numpy())
|
| 274 |
+
print("\nvalidation loss is {} \n".format(np.mean(val_loss)))
|
| 275 |
+
loss_dict["val_loss"].append(np.mean(val_loss))
|
| 276 |
+
|
| 277 |
+
if np.mean(val_loss) < old_score:
|
| 278 |
+
# save model if val loss is smallest
|
| 279 |
+
torch.save(
|
| 280 |
+
dnn.state_dict(),
|
| 281 |
+
"{}/CELoss_BSC_{}_eyettention_{}_newloss_fold{}.pth".format(
|
| 282 |
+
args.save_data_folder, args.test_mode, args.atten_type, fold_indx
|
| 283 |
+
),
|
| 284 |
+
)
|
| 285 |
+
old_score = np.mean(val_loss)
|
| 286 |
+
print("\nsaved model state dict\n")
|
| 287 |
+
save_ep_couter = episode_i
|
| 288 |
+
else:
|
| 289 |
+
# early stopping
|
| 290 |
+
if episode_i - save_ep_couter >= cf["earlystop_patience"]:
|
| 291 |
+
break
|
| 292 |
+
|
| 293 |
+
# evaluation
|
| 294 |
+
dnn.eval()
|
| 295 |
+
res_llh = []
|
| 296 |
+
dnn.load_state_dict(
|
| 297 |
+
torch.load(
|
| 298 |
+
os.path.join(
|
| 299 |
+
args.save_data_folder,
|
| 300 |
+
f"CELoss_BSC_{args.test_mode}_eyettention_{args.atten_type}_newloss_fold{fold_indx}.pth",
|
| 301 |
+
),
|
| 302 |
+
map_location="cpu",
|
| 303 |
+
)
|
| 304 |
+
)
|
| 305 |
+
dnn.to(device)
|
| 306 |
+
batch_indx = 0
|
| 307 |
+
for batchh in test_dataloaderr:
|
| 308 |
+
with torch.no_grad():
|
| 309 |
+
sn_input_ids_test = batchh["sn_input_ids"].to(device)
|
| 310 |
+
sn_attention_mask_test = batchh["sn_attention_mask"].to(device)
|
| 311 |
+
sp_input_ids_test = batchh["sp_input_ids"].to(device)
|
| 312 |
+
sp_attention_mask_test = batchh["sp_attention_mask"].to(device)
|
| 313 |
+
sp_pos_test = batchh["sp_pos"].to(device) # 28: '<Sep>', 29: '<'Pad'>'
|
| 314 |
+
sp_landing_pos_test = batchh["sp_landing_pos"].to(device)
|
| 315 |
+
sp_fix_dur_test = (batchh["sp_fix_dur"] / 1000).to(device)
|
| 316 |
+
sn_word_len_test = batchh["sn_word_len"].to(device)
|
| 317 |
+
|
| 318 |
+
# normalize gaze features
|
| 319 |
+
mask = ~torch.eq(sp_fix_dur_test, 0)
|
| 320 |
+
sp_fix_dur_test = (sp_fix_dur_test - fix_dur_mean) / fix_dur_std * mask
|
| 321 |
+
sp_landing_pos_test = (
|
| 322 |
+
(sp_landing_pos_test - landing_pos_mean) / landing_pos_std * mask
|
| 323 |
+
)
|
| 324 |
+
sp_fix_dur_test = torch.nan_to_num(sp_fix_dur_test)
|
| 325 |
+
sp_landing_pos_test = torch.nan_to_num(sp_landing_pos_test)
|
| 326 |
+
sn_word_len_test = (sn_word_len_test - sn_word_len_mean) / sn_word_len_std
|
| 327 |
+
sn_word_len_test = torch.nan_to_num(sn_word_len_test)
|
| 328 |
+
|
| 329 |
+
dnn_out_test, atten_weights_test = dnn(
|
| 330 |
+
sn_emd=sn_input_ids_test,
|
| 331 |
+
sn_mask=sn_attention_mask_test,
|
| 332 |
+
sp_emd=sp_input_ids_test,
|
| 333 |
+
sp_pos=sp_pos_test,
|
| 334 |
+
word_ids_sn=None,
|
| 335 |
+
word_ids_sp=None,
|
| 336 |
+
sp_fix_dur=sp_fix_dur_test,
|
| 337 |
+
sp_landing_pos=sp_landing_pos_test,
|
| 338 |
+
sn_word_len=sn_word_len_test,
|
| 339 |
+
) # [batch, step, dec_o_dim]
|
| 340 |
+
|
| 341 |
+
# We do not use nn.CrossEntropyLoss here to calculate the likelihood because it combines nn.LogSoftmax and nn.NLL,
|
| 342 |
+
# while nn.LogSoftmax returns a log value based on e, we want 2 instead
|
| 343 |
+
# m = nn.LogSoftmax(dim=2) -- base e, we want base 2
|
| 344 |
+
m = nn.Softmax(dim=2)
|
| 345 |
+
dnn_out_test = m(dnn_out_test).detach().to("cpu").numpy()
|
| 346 |
+
|
| 347 |
+
# prepare label and mask
|
| 348 |
+
pad_mask_test, label_test = load_label(sp_pos_test, cf, le, "cpu")
|
| 349 |
+
|
| 350 |
+
# compute log likelihood for the batch samples
|
| 351 |
+
res_batch = eval_log_llh(dnn_out_test, label_test, pad_mask_test)
|
| 352 |
+
res_llh.append(np.array(res_batch))
|
| 353 |
+
|
| 354 |
+
if bool(args.scanpath_gen_flag) == True:
|
| 355 |
+
sn_len = (
|
| 356 |
+
(torch.sum(sn_attention_mask_test, axis=1) - 2).detach().to("cpu").numpy()
|
| 357 |
+
)
|
| 358 |
+
# compute the scan path generated from the model when the first CLS token is given
|
| 359 |
+
sp_dnn = dnn.scanpath_generation(
|
| 360 |
+
sn_emd=sn_input_ids_test,
|
| 361 |
+
sn_mask=sn_attention_mask_test,
|
| 362 |
+
word_ids_sn=None,
|
| 363 |
+
sn_word_len=sn_word_len_test,
|
| 364 |
+
le=le,
|
| 365 |
+
max_pred_len=cf["max_pred_len"],
|
| 366 |
+
)
|
| 367 |
+
|
| 368 |
+
sp_dnn, sp_human = prepare_scanpath(
|
| 369 |
+
sp_dnn.detach().to("cpu").numpy(), sn_len, sp_pos_test, cf
|
| 370 |
+
)
|
| 371 |
+
sp_dnn_list.extend(sp_dnn)
|
| 372 |
+
sp_human_list.extend(sp_human)
|
| 373 |
+
|
| 374 |
+
batch_indx += 1
|
| 375 |
+
|
| 376 |
+
res_llh = np.concatenate(res_llh).ravel()
|
| 377 |
+
loss_dict["test_ll"].append(res_llh)
|
| 378 |
+
loss_dict["fix_dur_mean"] = fix_dur_mean
|
| 379 |
+
loss_dict["fix_dur_std"] = fix_dur_std
|
| 380 |
+
loss_dict["landing_pos_mean"] = landing_pos_mean
|
| 381 |
+
loss_dict["landing_pos_std"] = landing_pos_std
|
| 382 |
+
loss_dict["sn_word_len_mean"] = sn_word_len_mean
|
| 383 |
+
loss_dict["sn_word_len_std"] = sn_word_len_std
|
| 384 |
+
|
| 385 |
+
print("\nTest likelihood is {} \n".format(np.mean(res_llh)))
|
| 386 |
+
# save results
|
| 387 |
+
with open(
|
| 388 |
+
"{}/res_BSC_{}_eyettention_{}_Fold{}.pickle".format(
|
| 389 |
+
args.save_data_folder, args.test_mode, args.atten_type, fold_indx
|
| 390 |
+
),
|
| 391 |
+
"wb",
|
| 392 |
+
) as handle:
|
| 393 |
+
pickle.dump(loss_dict, handle, protocol=pickle.HIGHEST_PROTOCOL)
|
| 394 |
+
fold_indx += 1
|
| 395 |
+
|
| 396 |
+
if bool(args.scanpath_gen_flag) == True:
|
| 397 |
+
# save results
|
| 398 |
+
dic = {"sp_dnn": sp_dnn_list, "sp_human": sp_human_list}
|
| 399 |
+
with open(
|
| 400 |
+
os.path.join(
|
| 401 |
+
args.save_data_folder,
|
| 402 |
+
f"BSC_scanpath_generation_eyettention_{args.test_mode}_{args.atten_type}.pickle",
|
| 403 |
+
),
|
| 404 |
+
"wb",
|
| 405 |
+
) as handle:
|
| 406 |
+
pickle.dump(dic, handle, protocol=pickle.HIGHEST_PROTOCOL)
|
main_BSC_NRS_setting.py
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import os
|
| 4 |
+
from sklearn.model_selection import StratifiedKFold, KFold
|
| 5 |
+
import torch
|
| 6 |
+
import torch.nn as nn
|
| 7 |
+
from torch.utils.data import DataLoader
|
| 8 |
+
from torch.optim import Adam, RMSprop
|
| 9 |
+
from transformers import BertTokenizer
|
| 10 |
+
from sklearn.preprocessing import LabelEncoder, OneHotEncoder
|
| 11 |
+
from torch.nn.functional import cross_entropy, softmax
|
| 12 |
+
from collections import deque
|
| 13 |
+
import pickle
|
| 14 |
+
import json
|
| 15 |
+
import random
|
| 16 |
+
import matplotlib.pyplot as plt
|
| 17 |
+
import argparse
|
| 18 |
+
|
| 19 |
+
from Eyettention.utils import *
|
| 20 |
+
from Eyettention.model import Eyettention
|
| 21 |
+
|
| 22 |
+
if __name__ == "__main__":
|
| 23 |
+
parser = argparse.ArgumentParser(description="run seq2seq model evaluation")
|
| 24 |
+
parser.add_argument(
|
| 25 |
+
"--test_mode", help="test mode: New Reader / New Sentence Split", type=str, default="NRS"
|
| 26 |
+
)
|
| 27 |
+
parser.add_argument("--atten_type", help="attention type", type=str, default="local-g")
|
| 28 |
+
parser.add_argument(
|
| 29 |
+
"--save_data_folder",
|
| 30 |
+
help="folder path for saving results",
|
| 31 |
+
type=str,
|
| 32 |
+
default="./results/BSC/",
|
| 33 |
+
)
|
| 34 |
+
parser.add_argument(
|
| 35 |
+
"--scanpath_gen_flag", help="whether to generate scanpath", type=int, default=1
|
| 36 |
+
)
|
| 37 |
+
parser.add_argument(
|
| 38 |
+
"--max_pred_len",
|
| 39 |
+
help="if scanpath_gen_flag is True, you can determine the longest scanpath that you want to generate, which should depend on the sentence length",
|
| 40 |
+
type=int,
|
| 41 |
+
default=60,
|
| 42 |
+
)
|
| 43 |
+
parser.add_argument("--sampl_time", help="Sampling times", type=int, default=5)
|
| 44 |
+
parser.add_argument("--gpu", help="gpu index", type=int, default=6)
|
| 45 |
+
args = parser.parse_args()
|
| 46 |
+
gpu = args.gpu
|
| 47 |
+
|
| 48 |
+
torch.set_default_tensor_type("torch.FloatTensor")
|
| 49 |
+
availbl = torch.cuda.is_available()
|
| 50 |
+
print(torch.cuda.is_available())
|
| 51 |
+
if availbl:
|
| 52 |
+
device = f"cuda:{gpu}"
|
| 53 |
+
else:
|
| 54 |
+
device = "cpu"
|
| 55 |
+
torch.cuda.set_device(gpu)
|
| 56 |
+
|
| 57 |
+
cf = {
|
| 58 |
+
"model_pretrained": "bert-base-chinese",
|
| 59 |
+
"lr": 1e-3,
|
| 60 |
+
"max_grad_norm": 10,
|
| 61 |
+
"n_epochs": 1000,
|
| 62 |
+
"dataset": "BSC",
|
| 63 |
+
"atten_type": args.atten_type,
|
| 64 |
+
"batch_size": 256,
|
| 65 |
+
"max_sn_len": 27, # include start token and end token
|
| 66 |
+
"max_sp_len": 40, # include start token and end token
|
| 67 |
+
"norm_type": "z-score",
|
| 68 |
+
"earlystop_patience": 20,
|
| 69 |
+
"max_pred_len": args.max_pred_len,
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
# Encode the label into interger categories, setting the exclusive category 'cf["max_sn_len"]-1' as the end sign
|
| 73 |
+
le = LabelEncoder()
|
| 74 |
+
le.fit(np.append(np.arange(-cf["max_sn_len"] + 3, cf["max_sn_len"] - 1), cf["max_sn_len"] - 1))
|
| 75 |
+
# le.classes_
|
| 76 |
+
|
| 77 |
+
# load corpus
|
| 78 |
+
word_info_df, pos_info_df, eyemovement_df = load_corpus(cf["dataset"])
|
| 79 |
+
# Make list with sentence index
|
| 80 |
+
sn_list = np.unique(eyemovement_df.sn.values).tolist()
|
| 81 |
+
# Make list with reader index
|
| 82 |
+
reader_list = np.unique(eyemovement_df.id.values).tolist()
|
| 83 |
+
|
| 84 |
+
random.seed(0)
|
| 85 |
+
fold_indx = 0
|
| 86 |
+
# for scanpath generation
|
| 87 |
+
sp_dnn_list = []
|
| 88 |
+
sp_human_list = []
|
| 89 |
+
for i in range(args.sampl_time):
|
| 90 |
+
print("time:", i)
|
| 91 |
+
loss_dict = {"val_loss": [], "train_loss": [], "test_ll": []}
|
| 92 |
+
readers_test = random.sample(reader_list, int(np.ceil(len(reader_list) * 0.3)))
|
| 93 |
+
reader_list_val = random.sample(readers_test, int(np.ceil(len(readers_test) * 0.3)))
|
| 94 |
+
reader_list_test = set(readers_test) - set(reader_list_val)
|
| 95 |
+
reader_list_test = list(reader_list_test)
|
| 96 |
+
reader_list_train = set(reader_list) - set(reader_list_test) - set(reader_list_val)
|
| 97 |
+
reader_list_train = list(reader_list_train)
|
| 98 |
+
|
| 99 |
+
sn_test = random.sample(sn_list, int(np.ceil(len(sn_list) * 0.3)))
|
| 100 |
+
sn_list_val = random.sample(sn_test, int(np.ceil(len(sn_test) * 0.3)))
|
| 101 |
+
sn_list_test = set(sn_test) - set(sn_list_val)
|
| 102 |
+
sn_list_test = list(sn_list_test)
|
| 103 |
+
sn_list_train = set(sn_list) - set(sn_list_test) - set(sn_list_val)
|
| 104 |
+
sn_list_train = list(sn_list_train)
|
| 105 |
+
|
| 106 |
+
# initialize tokenizer
|
| 107 |
+
tokenizer = BertTokenizer.from_pretrained(cf["model_pretrained"])
|
| 108 |
+
# Preparing batch data
|
| 109 |
+
dataset_train = BSCdataset(
|
| 110 |
+
word_info_df, eyemovement_df, cf, reader_list_train, sn_list_train, tokenizer
|
| 111 |
+
)
|
| 112 |
+
train_dataloaderr = DataLoader(
|
| 113 |
+
dataset_train, batch_size=cf["batch_size"], shuffle=True, drop_last=True
|
| 114 |
+
)
|
| 115 |
+
|
| 116 |
+
dataset_val = BSCdataset(
|
| 117 |
+
word_info_df, eyemovement_df, cf, reader_list_val, sn_list_val, tokenizer
|
| 118 |
+
)
|
| 119 |
+
val_dataloaderr = DataLoader(
|
| 120 |
+
dataset_val, batch_size=cf["batch_size"], shuffle=False, drop_last=False
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
dataset_test = BSCdataset(
|
| 124 |
+
word_info_df, eyemovement_df, cf, reader_list_test, sn_list_test, tokenizer
|
| 125 |
+
)
|
| 126 |
+
test_dataloaderr = DataLoader(
|
| 127 |
+
dataset_test, batch_size=cf["batch_size"], shuffle=False, drop_last=False
|
| 128 |
+
)
|
| 129 |
+
|
| 130 |
+
# z-score normalization for gaze features
|
| 131 |
+
fix_dur_mean, fix_dur_std = calculate_mean_std(
|
| 132 |
+
dataloader=train_dataloaderr, feat_key="sp_fix_dur", padding_value=0, scale=1000
|
| 133 |
+
)
|
| 134 |
+
landing_pos_mean, landing_pos_std = calculate_mean_std(
|
| 135 |
+
dataloader=train_dataloaderr, feat_key="sp_landing_pos", padding_value=0
|
| 136 |
+
)
|
| 137 |
+
sn_word_len_mean, sn_word_len_std = calculate_mean_std(
|
| 138 |
+
dataloader=train_dataloaderr, feat_key="sn_word_len"
|
| 139 |
+
)
|
| 140 |
+
|
| 141 |
+
# load model
|
| 142 |
+
dnn = Eyettention(cf)
|
| 143 |
+
|
| 144 |
+
# training
|
| 145 |
+
episode = 0
|
| 146 |
+
optimizer = Adam(dnn.parameters(), lr=cf["lr"])
|
| 147 |
+
dnn.train()
|
| 148 |
+
dnn.to(device)
|
| 149 |
+
av_score = deque(maxlen=100)
|
| 150 |
+
old_score = 1e10
|
| 151 |
+
save_ep_couter = 0
|
| 152 |
+
print("Start training")
|
| 153 |
+
for episode_i in range(episode, cf["n_epochs"] + 1):
|
| 154 |
+
dnn.train()
|
| 155 |
+
print("episode:", episode_i)
|
| 156 |
+
counter = 0
|
| 157 |
+
for batchh in train_dataloaderr:
|
| 158 |
+
counter += 1
|
| 159 |
+
batchh.keys()
|
| 160 |
+
sn_input_ids = batchh["sn_input_ids"].to(device)
|
| 161 |
+
sn_attention_mask = batchh["sn_attention_mask"].to(device)
|
| 162 |
+
sp_input_ids = batchh["sp_input_ids"].to(device)
|
| 163 |
+
sp_attention_mask = batchh["sp_attention_mask"].to(device)
|
| 164 |
+
sp_pos = batchh["sp_pos"].to(device)
|
| 165 |
+
sp_landing_pos = batchh["sp_landing_pos"].to(device)
|
| 166 |
+
sp_fix_dur = (batchh["sp_fix_dur"] / 1000).to(device)
|
| 167 |
+
sn_word_len = batchh["sn_word_len"].to(device)
|
| 168 |
+
|
| 169 |
+
# normalize gaze features
|
| 170 |
+
mask = ~torch.eq(sp_fix_dur, 0)
|
| 171 |
+
sp_fix_dur = (sp_fix_dur - fix_dur_mean) / fix_dur_std * mask
|
| 172 |
+
sp_landing_pos = (sp_landing_pos - landing_pos_mean) / landing_pos_std * mask
|
| 173 |
+
sp_fix_dur = torch.nan_to_num(sp_fix_dur)
|
| 174 |
+
sp_landing_pos = torch.nan_to_num(sp_landing_pos)
|
| 175 |
+
sn_word_len = (sn_word_len - sn_word_len_mean) / sn_word_len_std
|
| 176 |
+
sn_word_len = torch.nan_to_num(sn_word_len)
|
| 177 |
+
|
| 178 |
+
# zero old gradients
|
| 179 |
+
optimizer.zero_grad()
|
| 180 |
+
# predict output with DNN
|
| 181 |
+
dnn_out, atten_weights = dnn(
|
| 182 |
+
sn_emd=sn_input_ids,
|
| 183 |
+
sn_mask=sn_attention_mask,
|
| 184 |
+
sp_emd=sp_input_ids,
|
| 185 |
+
sp_pos=sp_pos,
|
| 186 |
+
word_ids_sn=None,
|
| 187 |
+
word_ids_sp=None,
|
| 188 |
+
sp_fix_dur=sp_fix_dur,
|
| 189 |
+
sp_landing_pos=sp_landing_pos,
|
| 190 |
+
sn_word_len=sn_word_len,
|
| 191 |
+
) # [batch, step, dec_o_dim]
|
| 192 |
+
|
| 193 |
+
dnn_out = dnn_out.permute(0, 2, 1) # [batch, dec_o_dim, step]
|
| 194 |
+
|
| 195 |
+
# prepare label and mask
|
| 196 |
+
pad_mask, label = load_label(sp_pos, cf, le, device)
|
| 197 |
+
loss = nn.CrossEntropyLoss(reduction="none")
|
| 198 |
+
batch_error = torch.mean(torch.masked_select(loss(dnn_out, label), ~pad_mask))
|
| 199 |
+
|
| 200 |
+
# backpropagate loss
|
| 201 |
+
batch_error.backward()
|
| 202 |
+
# clip gradients
|
| 203 |
+
gradient_clipping(dnn, cf["max_grad_norm"])
|
| 204 |
+
|
| 205 |
+
# learn
|
| 206 |
+
optimizer.step()
|
| 207 |
+
av_score.append(batch_error.to("cpu").detach().numpy())
|
| 208 |
+
print("counter:", counter)
|
| 209 |
+
print(
|
| 210 |
+
"\rSample {}\tAverage Error: {:.10f} ".format(counter, np.mean(av_score)),
|
| 211 |
+
end=" ",
|
| 212 |
+
)
|
| 213 |
+
loss_dict["train_loss"].append(np.mean(av_score))
|
| 214 |
+
|
| 215 |
+
val_loss = []
|
| 216 |
+
dnn.eval()
|
| 217 |
+
for batchh in val_dataloaderr:
|
| 218 |
+
with torch.no_grad():
|
| 219 |
+
sn_input_ids_val = batchh["sn_input_ids"].to(device)
|
| 220 |
+
sn_attention_mask_val = batchh["sn_attention_mask"].to(device)
|
| 221 |
+
sp_input_ids_val = batchh["sp_input_ids"].to(device)
|
| 222 |
+
sp_attention_mask_val = batchh["sp_attention_mask"].to(device)
|
| 223 |
+
sp_pos_val = batchh["sp_pos"].to(device)
|
| 224 |
+
sp_landing_pos_val = batchh["sp_landing_pos"].to(device)
|
| 225 |
+
sp_fix_dur_val = (batchh["sp_fix_dur"] / 1000).to(device)
|
| 226 |
+
sn_word_len_val = batchh["sn_word_len"].to(device)
|
| 227 |
+
|
| 228 |
+
# normalize gaze features
|
| 229 |
+
mask = ~torch.eq(sp_fix_dur_val, 0)
|
| 230 |
+
sp_fix_dur_val = (sp_fix_dur_val - fix_dur_mean) / fix_dur_std * mask
|
| 231 |
+
sp_landing_pos_val = (
|
| 232 |
+
(sp_landing_pos_val - landing_pos_mean) / landing_pos_std * mask
|
| 233 |
+
)
|
| 234 |
+
sp_fix_dur_val = torch.nan_to_num(sp_fix_dur_val)
|
| 235 |
+
sp_landing_pos_val = torch.nan_to_num(sp_landing_pos_val)
|
| 236 |
+
sn_word_len_val = (sn_word_len_val - sn_word_len_mean) / sn_word_len_std
|
| 237 |
+
sn_word_len_val = torch.nan_to_num(sn_word_len_val)
|
| 238 |
+
|
| 239 |
+
dnn_out_val, atten_weights_val = dnn(
|
| 240 |
+
sn_emd=sn_input_ids_val,
|
| 241 |
+
sn_mask=sn_attention_mask_val,
|
| 242 |
+
sp_emd=sp_input_ids_val,
|
| 243 |
+
sp_pos=sp_pos_val,
|
| 244 |
+
word_ids_sn=None,
|
| 245 |
+
word_ids_sp=None,
|
| 246 |
+
sp_fix_dur=sp_fix_dur_val,
|
| 247 |
+
sp_landing_pos=sp_landing_pos_val,
|
| 248 |
+
sn_word_len=sn_word_len_val,
|
| 249 |
+
) # [batch, step, dec_o_dim]
|
| 250 |
+
dnn_out_val = dnn_out_val.permute(0, 2, 1) # [batch, dec_o_dim, step]
|
| 251 |
+
|
| 252 |
+
# prepare label and mask
|
| 253 |
+
pad_mask_val, label_val = load_label(sp_pos_val, cf, le, device)
|
| 254 |
+
batch_error_val = torch.mean(
|
| 255 |
+
torch.masked_select(loss(dnn_out_val, label_val), ~pad_mask_val)
|
| 256 |
+
)
|
| 257 |
+
val_loss.append(batch_error_val.detach().to("cpu").numpy())
|
| 258 |
+
print("\nvalidation loss is {} \n".format(np.mean(val_loss)))
|
| 259 |
+
loss_dict["val_loss"].append(np.mean(val_loss))
|
| 260 |
+
|
| 261 |
+
if np.mean(val_loss) < old_score:
|
| 262 |
+
# save model if val loss is smallest
|
| 263 |
+
torch.save(
|
| 264 |
+
dnn.state_dict(),
|
| 265 |
+
"{}/CELoss_BSC_NRS_eyettention_{}_newloss_fold{}.pth".format(
|
| 266 |
+
args.save_data_folder, args.atten_type, fold_indx
|
| 267 |
+
),
|
| 268 |
+
)
|
| 269 |
+
old_score = np.mean(val_loss)
|
| 270 |
+
print("\nsaved model state dict\n")
|
| 271 |
+
save_ep_couter = episode_i
|
| 272 |
+
else:
|
| 273 |
+
# early stopping
|
| 274 |
+
if episode_i - save_ep_couter >= cf["earlystop_patience"]:
|
| 275 |
+
break
|
| 276 |
+
|
| 277 |
+
# evaluation
|
| 278 |
+
dnn.eval()
|
| 279 |
+
res_llh = []
|
| 280 |
+
dnn.load_state_dict(
|
| 281 |
+
torch.load(
|
| 282 |
+
os.path.join(
|
| 283 |
+
args.save_data_folder,
|
| 284 |
+
f"CELoss_BSC_NRS_eyettention_{args.atten_type}_newloss_fold{fold_indx}.pth",
|
| 285 |
+
),
|
| 286 |
+
map_location="cpu",
|
| 287 |
+
)
|
| 288 |
+
)
|
| 289 |
+
dnn.to(device)
|
| 290 |
+
batch_indx = 0
|
| 291 |
+
for batchh in test_dataloaderr:
|
| 292 |
+
with torch.no_grad():
|
| 293 |
+
sn_input_ids_test = batchh["sn_input_ids"].to(device)
|
| 294 |
+
sn_attention_mask_test = batchh["sn_attention_mask"].to(device)
|
| 295 |
+
sp_input_ids_test = batchh["sp_input_ids"].to(device)
|
| 296 |
+
sp_attention_mask_test = batchh["sp_attention_mask"].to(device)
|
| 297 |
+
sp_pos_test = batchh["sp_pos"].to(device) # 28: '<Sep>', 29: '<'Pad'>'
|
| 298 |
+
sp_landing_pos_test = batchh["sp_landing_pos"].to(device)
|
| 299 |
+
sp_fix_dur_test = (batchh["sp_fix_dur"] / 1000).to(device)
|
| 300 |
+
sn_word_len_test = batchh["sn_word_len"].to(device)
|
| 301 |
+
|
| 302 |
+
# normalize gaze features
|
| 303 |
+
mask = ~torch.eq(sp_fix_dur_test, 0)
|
| 304 |
+
sp_fix_dur_test = (sp_fix_dur_test - fix_dur_mean) / fix_dur_std * mask
|
| 305 |
+
sp_landing_pos_test = (
|
| 306 |
+
(sp_landing_pos_test - landing_pos_mean) / landing_pos_std * mask
|
| 307 |
+
)
|
| 308 |
+
sp_fix_dur_test = torch.nan_to_num(sp_fix_dur_test)
|
| 309 |
+
sp_landing_pos_test = torch.nan_to_num(sp_landing_pos_test)
|
| 310 |
+
sn_word_len_test = (sn_word_len_test - sn_word_len_mean) / sn_word_len_std
|
| 311 |
+
sn_word_len_test = torch.nan_to_num(sn_word_len_test)
|
| 312 |
+
|
| 313 |
+
dnn_out_test, atten_weights_test = dnn(
|
| 314 |
+
sn_emd=sn_input_ids_test,
|
| 315 |
+
sn_mask=sn_attention_mask_test,
|
| 316 |
+
sp_emd=sp_input_ids_test,
|
| 317 |
+
sp_pos=sp_pos_test,
|
| 318 |
+
word_ids_sn=None,
|
| 319 |
+
word_ids_sp=None,
|
| 320 |
+
sp_fix_dur=sp_fix_dur_test,
|
| 321 |
+
sp_landing_pos=sp_landing_pos_test,
|
| 322 |
+
sn_word_len=sn_word_len_test,
|
| 323 |
+
) # [batch, step, dec_o_dim]
|
| 324 |
+
|
| 325 |
+
# We do not use nn.CrossEntropyLoss here to calculate the likelihood because it combines nn.LogSoftmax and nn.NLL,
|
| 326 |
+
# while nn.LogSoftmax returns a log value based on e, we want 2 instead
|
| 327 |
+
# m = nn.LogSoftmax(dim=2) -- base e, we want base 2
|
| 328 |
+
m = nn.Softmax(dim=2)
|
| 329 |
+
dnn_out_test = m(dnn_out_test).detach().to("cpu").numpy()
|
| 330 |
+
|
| 331 |
+
# prepare label and mask
|
| 332 |
+
pad_mask_test, label_test = load_label(sp_pos_test, cf, le, "cpu")
|
| 333 |
+
pred = dnn_out_test.argmax(axis=2)
|
| 334 |
+
# compute log likelihood for the batch samples
|
| 335 |
+
res_batch = eval_log_llh(dnn_out_test, label_test, pad_mask_test)
|
| 336 |
+
res_llh.append(np.array(res_batch))
|
| 337 |
+
|
| 338 |
+
if bool(args.scanpath_gen_flag) == True:
|
| 339 |
+
sn_len = (
|
| 340 |
+
(torch.sum(sn_attention_mask_test, axis=1) - 2).detach().to("cpu").numpy()
|
| 341 |
+
)
|
| 342 |
+
# compute the scan path generated from the model when the first CLS token is given
|
| 343 |
+
sp_dnn = dnn.scanpath_generation(
|
| 344 |
+
sn_emd=sn_input_ids_test,
|
| 345 |
+
sn_mask=sn_attention_mask_test,
|
| 346 |
+
word_ids_sn=None,
|
| 347 |
+
sn_word_len=sn_word_len_test,
|
| 348 |
+
le=le,
|
| 349 |
+
max_pred_len=cf["max_pred_len"],
|
| 350 |
+
)
|
| 351 |
+
|
| 352 |
+
sp_dnn, sp_human = prepare_scanpath(
|
| 353 |
+
sp_dnn.detach().to("cpu").numpy(), sn_len, sp_pos_test, cf
|
| 354 |
+
)
|
| 355 |
+
sp_dnn_list.extend(sp_dnn)
|
| 356 |
+
sp_human_list.extend(sp_human)
|
| 357 |
+
|
| 358 |
+
batch_indx += 1
|
| 359 |
+
|
| 360 |
+
res_llh = np.concatenate(res_llh).ravel()
|
| 361 |
+
loss_dict["test_ll"].append(res_llh)
|
| 362 |
+
loss_dict["fix_dur_mean"] = fix_dur_mean
|
| 363 |
+
loss_dict["fix_dur_std"] = fix_dur_std
|
| 364 |
+
loss_dict["landing_pos_mean"] = landing_pos_mean
|
| 365 |
+
loss_dict["landing_pos_std"] = landing_pos_std
|
| 366 |
+
loss_dict["sn_word_len_mean"] = sn_word_len_mean
|
| 367 |
+
loss_dict["sn_word_len_std"] = sn_word_len_std
|
| 368 |
+
|
| 369 |
+
print("\nTest likelihood is {} \n".format(np.mean(res_llh)))
|
| 370 |
+
# save results
|
| 371 |
+
with open(
|
| 372 |
+
"{}/res_BSC_NRS_eyettention_{}_Fold{}.pickle".format(
|
| 373 |
+
args.save_data_folder, args.atten_type, fold_indx
|
| 374 |
+
),
|
| 375 |
+
"wb",
|
| 376 |
+
) as handle:
|
| 377 |
+
pickle.dump(loss_dict, handle, protocol=pickle.HIGHEST_PROTOCOL)
|
| 378 |
+
fold_indx += 1
|
| 379 |
+
|
| 380 |
+
if bool(args.scanpath_gen_flag) == True:
|
| 381 |
+
# save results
|
| 382 |
+
dic = {"sp_dnn": sp_dnn_list, "sp_human": sp_human_list}
|
| 383 |
+
with open(
|
| 384 |
+
os.path.join(
|
| 385 |
+
args.save_data_folder,
|
| 386 |
+
f"BSC_scanpath_generation_eyettention_NRS_{args.atten_type}.pickle",
|
| 387 |
+
),
|
| 388 |
+
"wb",
|
| 389 |
+
) as handle:
|
| 390 |
+
pickle.dump(dic, handle, protocol=pickle.HIGHEST_PROTOCOL)
|
main_BSC_reader_identifier.py
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import os
|
| 4 |
+
from sklearn.model_selection import StratifiedKFold, KFold
|
| 5 |
+
import torch
|
| 6 |
+
import torch.nn as nn
|
| 7 |
+
from torch.utils.data import DataLoader
|
| 8 |
+
from torch.optim import Adam, RMSprop
|
| 9 |
+
from transformers import BertTokenizer
|
| 10 |
+
from sklearn.preprocessing import LabelEncoder, OneHotEncoder
|
| 11 |
+
from torch.nn.functional import cross_entropy, softmax
|
| 12 |
+
from collections import deque
|
| 13 |
+
import pickle
|
| 14 |
+
import json
|
| 15 |
+
import matplotlib.pyplot as plt
|
| 16 |
+
import argparse
|
| 17 |
+
|
| 18 |
+
from Eyettention.utils import *
|
| 19 |
+
from Eyettention.model import Eyettention_readerID
|
| 20 |
+
|
| 21 |
+
if __name__ == "__main__":
|
| 22 |
+
parser = argparse.ArgumentParser(description="run uniform baseline")
|
| 23 |
+
parser.add_argument("--test_mode", help="test mode: text", type=str, default="text")
|
| 24 |
+
parser.add_argument("--atten_type", help="attention type", type=str, default="local-g")
|
| 25 |
+
parser.add_argument(
|
| 26 |
+
"--save_data_folder",
|
| 27 |
+
help="folder path for saving results",
|
| 28 |
+
type=str,
|
| 29 |
+
default="./results/BSC/",
|
| 30 |
+
)
|
| 31 |
+
parser.add_argument("--gpu", help="gpu index", type=int, default=3)
|
| 32 |
+
parser.add_argument("--emb_size", help="readerID embedding size", type=int, default=16)
|
| 33 |
+
args = parser.parse_args()
|
| 34 |
+
gpu = args.gpu
|
| 35 |
+
|
| 36 |
+
torch.set_default_tensor_type("torch.FloatTensor")
|
| 37 |
+
availbl = torch.cuda.is_available()
|
| 38 |
+
print(torch.cuda.is_available())
|
| 39 |
+
if availbl:
|
| 40 |
+
device = f"cuda:{gpu}"
|
| 41 |
+
else:
|
| 42 |
+
device = "cpu"
|
| 43 |
+
torch.cuda.set_device(gpu)
|
| 44 |
+
|
| 45 |
+
cf = {
|
| 46 |
+
"model_pretrained": "bert-base-chinese",
|
| 47 |
+
"lr": 1e-3,
|
| 48 |
+
"max_grad_norm": 10,
|
| 49 |
+
"n_epochs": 1000,
|
| 50 |
+
"n_folds": 5,
|
| 51 |
+
"dataset": "BSC",
|
| 52 |
+
"atten_type": args.atten_type,
|
| 53 |
+
"subid_emb_size": args.emb_size,
|
| 54 |
+
"batch_size": 256,
|
| 55 |
+
"max_sn_len": 27, # include start token and end token
|
| 56 |
+
"max_sp_len": 40, # include start token and end token
|
| 57 |
+
"norm_type": "z-score",
|
| 58 |
+
"earlystop_patience": 20,
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
# Encode the label into interger categories, setting the exclusive category 'cf["max_sn_len"]-1' as the end sign
|
| 62 |
+
le = LabelEncoder()
|
| 63 |
+
le.fit(np.append(np.arange(-cf["max_sn_len"] + 3, cf["max_sn_len"] - 1), cf["max_sn_len"] - 1))
|
| 64 |
+
# le.classes_
|
| 65 |
+
|
| 66 |
+
# load corpus
|
| 67 |
+
word_info_df, pos_info_df, eyemovement_df = load_corpus(cf["dataset"])
|
| 68 |
+
# Make list with sentence index
|
| 69 |
+
sn_list = np.unique(eyemovement_df.sn.values).tolist()
|
| 70 |
+
# Make list with reader index
|
| 71 |
+
reader_list = np.unique(eyemovement_df.id.values).tolist()
|
| 72 |
+
|
| 73 |
+
# Split training&test sets by text
|
| 74 |
+
print("Start evaluating on new sentences.")
|
| 75 |
+
split_list = sn_list
|
| 76 |
+
|
| 77 |
+
n_folds = cf["n_folds"]
|
| 78 |
+
kf = KFold(n_splits=n_folds, shuffle=True, random_state=0)
|
| 79 |
+
fold_indx = 0
|
| 80 |
+
for train_idx, test_idx in kf.split(split_list):
|
| 81 |
+
loss_dict = {"val_loss": [], "train_loss": [], "test_ll": []}
|
| 82 |
+
list_train = [split_list[i] for i in train_idx]
|
| 83 |
+
list_test = [split_list[i] for i in test_idx]
|
| 84 |
+
|
| 85 |
+
# create train validation split for training the models:
|
| 86 |
+
kf_val = KFold(n_splits=n_folds, shuffle=True, random_state=0)
|
| 87 |
+
for train_index, val_index in kf_val.split(list_train):
|
| 88 |
+
# we only evaluate a single fold
|
| 89 |
+
break
|
| 90 |
+
list_train_net = [list_train[i] for i in train_index]
|
| 91 |
+
list_val_net = [list_train[i] for i in val_index]
|
| 92 |
+
|
| 93 |
+
sn_list_train = list_train_net
|
| 94 |
+
sn_list_val = list_val_net
|
| 95 |
+
sn_list_test = list_test
|
| 96 |
+
reader_list_train, reader_list_val, reader_list_test = reader_list, reader_list, reader_list
|
| 97 |
+
|
| 98 |
+
# initialize tokenizer
|
| 99 |
+
tokenizer = BertTokenizer.from_pretrained(cf["model_pretrained"])
|
| 100 |
+
# Preparing batch data
|
| 101 |
+
dataset_train = BSCdataset(
|
| 102 |
+
word_info_df, eyemovement_df, cf, reader_list_train, sn_list_train, tokenizer
|
| 103 |
+
)
|
| 104 |
+
train_dataloaderr = DataLoader(
|
| 105 |
+
dataset_train, batch_size=cf["batch_size"], shuffle=True, drop_last=True
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
dataset_val = BSCdataset(
|
| 109 |
+
word_info_df, eyemovement_df, cf, reader_list_val, sn_list_val, tokenizer
|
| 110 |
+
)
|
| 111 |
+
val_dataloaderr = DataLoader(
|
| 112 |
+
dataset_val, batch_size=cf["batch_size"], shuffle=False, drop_last=True
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
dataset_test = BSCdataset(
|
| 116 |
+
word_info_df, eyemovement_df, cf, reader_list_test, sn_list_test, tokenizer
|
| 117 |
+
)
|
| 118 |
+
test_dataloaderr = DataLoader(
|
| 119 |
+
dataset_test, batch_size=cf["batch_size"], shuffle=False, drop_last=False
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
# z-score normalization for gaze features
|
| 123 |
+
fix_dur_mean, fix_dur_std = calculate_mean_std(
|
| 124 |
+
dataloader=train_dataloaderr, feat_key="sp_fix_dur", padding_value=0, scale=1000
|
| 125 |
+
)
|
| 126 |
+
landing_pos_mean, landing_pos_std = calculate_mean_std(
|
| 127 |
+
dataloader=train_dataloaderr, feat_key="sp_landing_pos", padding_value=0
|
| 128 |
+
)
|
| 129 |
+
sn_word_len_mean, sn_word_len_std = calculate_mean_std(
|
| 130 |
+
dataloader=train_dataloaderr, feat_key="sn_word_len"
|
| 131 |
+
)
|
| 132 |
+
|
| 133 |
+
# load model here
|
| 134 |
+
dnn = Eyettention_readerID(cf)
|
| 135 |
+
|
| 136 |
+
# training
|
| 137 |
+
episode = 0
|
| 138 |
+
optimizer = Adam(dnn.parameters(), lr=cf["lr"])
|
| 139 |
+
dnn.train()
|
| 140 |
+
dnn.to(device)
|
| 141 |
+
av_score = deque(maxlen=100)
|
| 142 |
+
old_score = 1e10
|
| 143 |
+
save_ep_couter = 0
|
| 144 |
+
print("Start training")
|
| 145 |
+
for episode_i in range(episode, cf["n_epochs"] + 1):
|
| 146 |
+
dnn.train()
|
| 147 |
+
print("episode:", episode_i)
|
| 148 |
+
counter = 0
|
| 149 |
+
for batchh in train_dataloaderr:
|
| 150 |
+
counter += 1
|
| 151 |
+
batchh.keys()
|
| 152 |
+
sn_input_ids = batchh["sn_input_ids"].to(device)
|
| 153 |
+
sn_attention_mask = batchh["sn_attention_mask"].to(device)
|
| 154 |
+
sp_input_ids = batchh["sp_input_ids"].to(device)
|
| 155 |
+
sp_attention_mask = batchh["sp_attention_mask"].to(device)
|
| 156 |
+
sp_pos = batchh["sp_pos"].to(device)
|
| 157 |
+
sp_landing_pos = batchh["sp_landing_pos"].to(device)
|
| 158 |
+
sp_fix_dur = (batchh["sp_fix_dur"] / 1000).to(device)
|
| 159 |
+
sn_word_len = batchh["sn_word_len"].to(device)
|
| 160 |
+
sub_id = batchh["sub_id"].to(device)
|
| 161 |
+
|
| 162 |
+
# normalize gaze features
|
| 163 |
+
mask = ~torch.eq(sp_fix_dur, 0)
|
| 164 |
+
sp_fix_dur = (sp_fix_dur - fix_dur_mean) / fix_dur_std * mask
|
| 165 |
+
sp_landing_pos = (sp_landing_pos - landing_pos_mean) / landing_pos_std * mask
|
| 166 |
+
sp_fix_dur = torch.nan_to_num(sp_fix_dur)
|
| 167 |
+
sp_landing_pos = torch.nan_to_num(sp_landing_pos)
|
| 168 |
+
sn_word_len = (sn_word_len - sn_word_len_mean) / sn_word_len_std
|
| 169 |
+
sn_word_len = torch.nan_to_num(sn_word_len)
|
| 170 |
+
|
| 171 |
+
# zero old gradients
|
| 172 |
+
optimizer.zero_grad()
|
| 173 |
+
# predict output with DNN
|
| 174 |
+
dnn_out, atten_weights = dnn(
|
| 175 |
+
sn_emd=sn_input_ids,
|
| 176 |
+
sn_mask=sn_attention_mask,
|
| 177 |
+
sp_emd=sp_input_ids,
|
| 178 |
+
sp_pos=sp_pos,
|
| 179 |
+
word_ids_sn=None,
|
| 180 |
+
word_ids_sp=None,
|
| 181 |
+
sp_fix_dur=sp_fix_dur,
|
| 182 |
+
sp_landing_pos=sp_landing_pos,
|
| 183 |
+
sn_word_len=sn_word_len,
|
| 184 |
+
sub_id=sub_id,
|
| 185 |
+
)
|
| 186 |
+
|
| 187 |
+
dnn_out = dnn_out.permute(0, 2, 1) # [batch, dec_o_dim, step]
|
| 188 |
+
|
| 189 |
+
# prepare label and mask
|
| 190 |
+
pad_mask, label = load_label(sp_pos, cf, le, device)
|
| 191 |
+
loss = nn.CrossEntropyLoss(reduction="none")
|
| 192 |
+
batch_error = torch.mean(torch.masked_select(loss(dnn_out, label), ~pad_mask))
|
| 193 |
+
|
| 194 |
+
# backpropagate loss
|
| 195 |
+
batch_error.backward()
|
| 196 |
+
# clip gradients
|
| 197 |
+
gradient_clipping(dnn, cf["max_grad_norm"])
|
| 198 |
+
|
| 199 |
+
# learn
|
| 200 |
+
optimizer.step()
|
| 201 |
+
av_score.append(batch_error.to("cpu").detach().numpy())
|
| 202 |
+
print("counter:", counter)
|
| 203 |
+
print(
|
| 204 |
+
"\rSample {}\tAverage Error: {:.10f} ".format(counter, np.mean(av_score)),
|
| 205 |
+
end=" ",
|
| 206 |
+
)
|
| 207 |
+
loss_dict["train_loss"].append(np.mean(av_score))
|
| 208 |
+
|
| 209 |
+
val_loss = []
|
| 210 |
+
dnn.eval()
|
| 211 |
+
for batchh in val_dataloaderr:
|
| 212 |
+
with torch.no_grad():
|
| 213 |
+
sn_input_ids_val = batchh["sn_input_ids"].to(device)
|
| 214 |
+
sn_attention_mask_val = batchh["sn_attention_mask"].to(device)
|
| 215 |
+
sp_input_ids_val = batchh["sp_input_ids"].to(device)
|
| 216 |
+
sp_attention_mask_val = batchh["sp_attention_mask"].to(device)
|
| 217 |
+
sp_pos_val = batchh["sp_pos"].to(device)
|
| 218 |
+
sp_landing_pos_val = batchh["sp_landing_pos"].to(device)
|
| 219 |
+
sp_fix_dur_val = (batchh["sp_fix_dur"] / 1000).to(device)
|
| 220 |
+
sn_word_len_val = batchh["sn_word_len"].to(device)
|
| 221 |
+
sub_id_val = batchh["sub_id"].to(device)
|
| 222 |
+
|
| 223 |
+
# normalize gaze features
|
| 224 |
+
mask = ~torch.eq(sp_fix_dur_val, 0)
|
| 225 |
+
sp_fix_dur_val = (sp_fix_dur_val - fix_dur_mean) / fix_dur_std * mask
|
| 226 |
+
sp_landing_pos_val = (
|
| 227 |
+
(sp_landing_pos_val - landing_pos_mean) / landing_pos_std * mask
|
| 228 |
+
)
|
| 229 |
+
sp_fix_dur_val = torch.nan_to_num(sp_fix_dur_val)
|
| 230 |
+
sp_landing_pos_val = torch.nan_to_num(sp_landing_pos_val)
|
| 231 |
+
sn_word_len_val = (sn_word_len_val - sn_word_len_mean) / sn_word_len_std
|
| 232 |
+
sn_word_len_val = torch.nan_to_num(sn_word_len_val)
|
| 233 |
+
|
| 234 |
+
dnn_out_val, atten_weights_val = dnn(
|
| 235 |
+
sn_emd=sn_input_ids_val,
|
| 236 |
+
sn_mask=sn_attention_mask_val,
|
| 237 |
+
sp_emd=sp_input_ids_val,
|
| 238 |
+
sp_pos=sp_pos_val,
|
| 239 |
+
word_ids_sn=None,
|
| 240 |
+
word_ids_sp=None,
|
| 241 |
+
sp_fix_dur=sp_fix_dur_val,
|
| 242 |
+
sp_landing_pos=sp_landing_pos_val,
|
| 243 |
+
sn_word_len=sn_word_len_val,
|
| 244 |
+
sub_id=sub_id_val,
|
| 245 |
+
)
|
| 246 |
+
|
| 247 |
+
dnn_out_val = dnn_out_val.permute(0, 2, 1) # [batch, dec_o_dim, step
|
| 248 |
+
|
| 249 |
+
# prepare label and mask
|
| 250 |
+
loss = nn.CrossEntropyLoss(reduction="none")
|
| 251 |
+
pad_mask_val, label_val = load_label(sp_pos_val, cf, le, device)
|
| 252 |
+
batch_error_val = torch.mean(
|
| 253 |
+
torch.masked_select(loss(dnn_out_val, label_val), ~pad_mask_val)
|
| 254 |
+
)
|
| 255 |
+
val_loss.append(batch_error_val.detach().to("cpu").numpy())
|
| 256 |
+
print("\nvalidation loss is {} \n".format(np.mean(val_loss)))
|
| 257 |
+
loss_dict["val_loss"].append(np.mean(val_loss))
|
| 258 |
+
|
| 259 |
+
if np.mean(val_loss) < old_score:
|
| 260 |
+
# save model if val loss is smallest
|
| 261 |
+
torch.save(
|
| 262 |
+
dnn.state_dict(),
|
| 263 |
+
"{}/CELoss_BSC_text_eyettention_readerID_{}_emb{}_newloss_fold{}.pth".format(
|
| 264 |
+
args.save_data_folder, args.atten_type, args.emb_size, fold_indx
|
| 265 |
+
),
|
| 266 |
+
)
|
| 267 |
+
old_score = np.mean(val_loss)
|
| 268 |
+
print("\nsaved model state dict\n")
|
| 269 |
+
save_ep_couter = episode_i
|
| 270 |
+
else:
|
| 271 |
+
# early stopping
|
| 272 |
+
if episode_i - save_ep_couter >= cf["earlystop_patience"]:
|
| 273 |
+
break
|
| 274 |
+
|
| 275 |
+
# evaluation
|
| 276 |
+
dnn.eval()
|
| 277 |
+
res_llh = []
|
| 278 |
+
dnn.load_state_dict(
|
| 279 |
+
torch.load(
|
| 280 |
+
os.path.join(
|
| 281 |
+
args.save_data_folder,
|
| 282 |
+
f"CELoss_BSC_text_eyettention_readerID_{args.atten_type}_emb{args.emb_size}_newloss_fold{fold_indx}.pth",
|
| 283 |
+
),
|
| 284 |
+
map_location="cpu",
|
| 285 |
+
)
|
| 286 |
+
)
|
| 287 |
+
dnn.to(device)
|
| 288 |
+
batch_indx = 0
|
| 289 |
+
for batchh in test_dataloaderr:
|
| 290 |
+
with torch.no_grad():
|
| 291 |
+
sn_input_ids_test = batchh["sn_input_ids"].to(device)
|
| 292 |
+
sn_attention_mask_test = batchh["sn_attention_mask"].to(device)
|
| 293 |
+
sp_input_ids_test = batchh["sp_input_ids"].to(device)
|
| 294 |
+
sp_attention_mask_test = batchh["sp_attention_mask"].to(device)
|
| 295 |
+
sp_pos_test = batchh["sp_pos"].to(device) # 28: '<Sep>', 29: '<'Pad'>'
|
| 296 |
+
sp_landing_pos_test = batchh["sp_landing_pos"].to(device)
|
| 297 |
+
sp_fix_dur_test = (batchh["sp_fix_dur"] / 1000).to(device)
|
| 298 |
+
sn_word_len_test = batchh["sn_word_len"].to(device)
|
| 299 |
+
sub_id_test = batchh["sub_id"].to(device)
|
| 300 |
+
|
| 301 |
+
# normalize gaze features
|
| 302 |
+
mask = ~torch.eq(sp_fix_dur_test, 0)
|
| 303 |
+
sp_fix_dur_test = (sp_fix_dur_test - fix_dur_mean) / fix_dur_std * mask
|
| 304 |
+
sp_landing_pos_test = (
|
| 305 |
+
(sp_landing_pos_test - landing_pos_mean) / landing_pos_std * mask
|
| 306 |
+
)
|
| 307 |
+
sp_fix_dur_test = torch.nan_to_num(sp_fix_dur_test)
|
| 308 |
+
sp_landing_pos_test = torch.nan_to_num(sp_landing_pos_test)
|
| 309 |
+
sn_word_len_test = (sn_word_len_test - sn_word_len_mean) / sn_word_len_std
|
| 310 |
+
sn_word_len_test = torch.nan_to_num(sn_word_len_test)
|
| 311 |
+
|
| 312 |
+
dnn_out_test, atten_weights_test = dnn(
|
| 313 |
+
sn_emd=sn_input_ids_test,
|
| 314 |
+
sn_mask=sn_attention_mask_test,
|
| 315 |
+
sp_emd=sp_input_ids_test,
|
| 316 |
+
sp_pos=sp_pos_test,
|
| 317 |
+
word_ids_sn=None,
|
| 318 |
+
word_ids_sp=None,
|
| 319 |
+
sp_fix_dur=sp_fix_dur_test,
|
| 320 |
+
sp_landing_pos=sp_landing_pos_test,
|
| 321 |
+
sn_word_len=sn_word_len_test,
|
| 322 |
+
sub_id=sub_id_test,
|
| 323 |
+
)
|
| 324 |
+
|
| 325 |
+
# We do not use nn.CrossEntropyLoss here to calculate the likelihood because it combines nn.LogSoftmax and nn.NLL,
|
| 326 |
+
# while nn.LogSoftmax returns a log value based on e, we want 2 instead
|
| 327 |
+
# m = nn.LogSoftmax(dim=2) -- base e, we want base 2
|
| 328 |
+
m = nn.Softmax(dim=2)
|
| 329 |
+
dnn_out_test = m(dnn_out_test).detach().to("cpu").numpy()
|
| 330 |
+
|
| 331 |
+
# prepare label and mask
|
| 332 |
+
pad_mask_test, label_test = load_label(sp_pos_test, cf, le, "cpu")
|
| 333 |
+
pred = dnn_out_test.argmax(axis=2)
|
| 334 |
+
# compute log likelihood for the batch samples
|
| 335 |
+
res_batch = eval_log_llh(dnn_out_test, label_test, pad_mask_test)
|
| 336 |
+
res_llh.append(np.array(res_batch))
|
| 337 |
+
|
| 338 |
+
batch_indx += 1
|
| 339 |
+
|
| 340 |
+
res_llh = np.concatenate(res_llh).ravel()
|
| 341 |
+
loss_dict["test_ll"].append(res_llh)
|
| 342 |
+
loss_dict["fix_dur_mean"] = fix_dur_mean
|
| 343 |
+
loss_dict["fix_dur_std"] = fix_dur_std
|
| 344 |
+
loss_dict["landing_pos_mean"] = landing_pos_mean
|
| 345 |
+
loss_dict["landing_pos_std"] = landing_pos_std
|
| 346 |
+
loss_dict["sn_word_len_mean"] = sn_word_len_mean
|
| 347 |
+
loss_dict["sn_word_len_std"] = sn_word_len_std
|
| 348 |
+
print("\nTest likelihood is {} \n".format(np.mean(res_llh)))
|
| 349 |
+
# save results
|
| 350 |
+
with open(
|
| 351 |
+
"{}/res_BSC_NRS_eyettention_readerID_{}_emb{}_Fold{}.pickle".format(
|
| 352 |
+
args.save_data_folder, args.atten_type, args.emb_size, fold_indx
|
| 353 |
+
),
|
| 354 |
+
"wb",
|
| 355 |
+
) as handle:
|
| 356 |
+
pickle.dump(loss_dict, handle, protocol=pickle.HIGHEST_PROTOCOL)
|
| 357 |
+
|
| 358 |
+
fold_indx += 1
|
main_celer.py
ADDED
|
@@ -0,0 +1,439 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import os
|
| 4 |
+
from sklearn.model_selection import StratifiedKFold, KFold
|
| 5 |
+
import torch
|
| 6 |
+
import torch.nn as nn
|
| 7 |
+
from torch.utils.data import DataLoader
|
| 8 |
+
from torch.optim import Adam, RMSprop
|
| 9 |
+
from transformers import BertTokenizerFast
|
| 10 |
+
from sklearn.preprocessing import LabelEncoder, OneHotEncoder
|
| 11 |
+
from torch.nn.functional import cross_entropy, softmax
|
| 12 |
+
from collections import deque
|
| 13 |
+
import pickle
|
| 14 |
+
import json
|
| 15 |
+
import matplotlib.pyplot as plt
|
| 16 |
+
import argparse
|
| 17 |
+
|
| 18 |
+
from Eyettention.utils import *
|
| 19 |
+
from Eyettention.model import Eyettention
|
| 20 |
+
|
| 21 |
+
if __name__ == "__main__":
|
| 22 |
+
parser = argparse.ArgumentParser(description="run Eyettention on CELER dataset")
|
| 23 |
+
parser.add_argument(
|
| 24 |
+
"--test_mode",
|
| 25 |
+
help="New Sentence Split: text, New Reader Split: subject",
|
| 26 |
+
type=str,
|
| 27 |
+
default="text",
|
| 28 |
+
)
|
| 29 |
+
parser.add_argument(
|
| 30 |
+
"--atten_type", help="attention type: global, local, local-g", type=str, default="local-g"
|
| 31 |
+
)
|
| 32 |
+
parser.add_argument(
|
| 33 |
+
"--save_data_folder",
|
| 34 |
+
help="folder path for saving results",
|
| 35 |
+
type=str,
|
| 36 |
+
default="./results/CELER/",
|
| 37 |
+
)
|
| 38 |
+
parser.add_argument(
|
| 39 |
+
"--scanpath_gen_flag", help="whether to generate scanpath", type=int, default=1
|
| 40 |
+
)
|
| 41 |
+
parser.add_argument(
|
| 42 |
+
"--max_pred_len",
|
| 43 |
+
help="if scanpath_gen_flag is True, you can determine the longest scanpath that you want to generate, which should depend on the sentence length",
|
| 44 |
+
type=int,
|
| 45 |
+
default=60,
|
| 46 |
+
)
|
| 47 |
+
parser.add_argument("--gpu", help="gpu index", type=int, default=6)
|
| 48 |
+
args = parser.parse_args()
|
| 49 |
+
gpu = args.gpu
|
| 50 |
+
|
| 51 |
+
# use FastTokenizer lead to warning -> The current process just got forked
|
| 52 |
+
os.environ["TOKENIZERS_PARALLELISM"] = "false"
|
| 53 |
+
torch.set_default_tensor_type("torch.FloatTensor")
|
| 54 |
+
availbl = torch.cuda.is_available()
|
| 55 |
+
print(torch.cuda.is_available())
|
| 56 |
+
if availbl:
|
| 57 |
+
device = f"cuda:{gpu}"
|
| 58 |
+
else:
|
| 59 |
+
device = "cpu"
|
| 60 |
+
print(device)
|
| 61 |
+
torch.cuda.set_device(gpu)
|
| 62 |
+
|
| 63 |
+
cf = {
|
| 64 |
+
"model_pretrained": "bert-base-cased",
|
| 65 |
+
"lr": 1e-3,
|
| 66 |
+
"max_grad_norm": 10,
|
| 67 |
+
"n_epochs": 1000,
|
| 68 |
+
"n_folds": 5,
|
| 69 |
+
"dataset": "celer",
|
| 70 |
+
"atten_type": args.atten_type,
|
| 71 |
+
"batch_size": 256,
|
| 72 |
+
"max_sn_len": 24, # max number of words in a sentence, include start token and end token,
|
| 73 |
+
"max_sn_token": 35, # maximum number of tokens a sentence includes. include start token and end token,
|
| 74 |
+
"max_sp_len": 52, # max number of words in a scanpath, include start token and end token
|
| 75 |
+
"max_sp_token": 395, # maximum number of tokens a scanpath includes. include start token and end token
|
| 76 |
+
"norm_type": "z-score",
|
| 77 |
+
"earlystop_patience": 20,
|
| 78 |
+
"max_pred_len": args.max_pred_len,
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
# Encode the label into interger categories, setting the exclusive category 'cf["max_sn_len"]-1' as the end sign
|
| 82 |
+
le = LabelEncoder()
|
| 83 |
+
le.fit(np.append(np.arange(-cf["max_sn_len"] + 3, cf["max_sn_len"] - 1), cf["max_sn_len"] - 1))
|
| 84 |
+
# le.classes_
|
| 85 |
+
|
| 86 |
+
# load corpus
|
| 87 |
+
word_info_df, _, eyemovement_df = load_corpus(cf["dataset"])
|
| 88 |
+
|
| 89 |
+
# only use native speaker
|
| 90 |
+
# Make list with reader index
|
| 91 |
+
reader_list = celer_load_native_speaker()
|
| 92 |
+
|
| 93 |
+
# Make list with sentence index
|
| 94 |
+
sn_list = np.unique(
|
| 95 |
+
word_info_df[word_info_df["list"].isin(reader_list)].sentenceid.values
|
| 96 |
+
).tolist()
|
| 97 |
+
|
| 98 |
+
# Split training&test sets by text or reader, depending on configuration
|
| 99 |
+
if args.test_mode == "text":
|
| 100 |
+
print("Start evaluating on new sentences.")
|
| 101 |
+
split_list = sn_list
|
| 102 |
+
elif args.test_mode == "subject":
|
| 103 |
+
print("Start evaluating on new readers.")
|
| 104 |
+
split_list = reader_list
|
| 105 |
+
|
| 106 |
+
n_folds = cf["n_folds"]
|
| 107 |
+
kf = KFold(n_splits=n_folds, shuffle=True, random_state=0)
|
| 108 |
+
fold_indx = 0
|
| 109 |
+
# for scanpath generation
|
| 110 |
+
sp_dnn_list = []
|
| 111 |
+
sp_human_list = []
|
| 112 |
+
for train_idx, test_idx in kf.split(split_list):
|
| 113 |
+
loss_dict = {"val_loss": [], "train_loss": [], "test_ll": [], "test_AUC": []}
|
| 114 |
+
list_train = [split_list[i] for i in train_idx]
|
| 115 |
+
list_test = [split_list[i] for i in test_idx]
|
| 116 |
+
|
| 117 |
+
# create train validation split for training the models:
|
| 118 |
+
kf_val = KFold(n_splits=n_folds, shuffle=True, random_state=0)
|
| 119 |
+
for train_index, val_index in kf_val.split(list_train):
|
| 120 |
+
# we only evaluate a single fold
|
| 121 |
+
break
|
| 122 |
+
list_train_net = [list_train[i] for i in train_index]
|
| 123 |
+
list_val_net = [list_train[i] for i in val_index]
|
| 124 |
+
|
| 125 |
+
if args.test_mode == "text":
|
| 126 |
+
sn_list_train = list_train_net
|
| 127 |
+
sn_list_val = list_val_net
|
| 128 |
+
sn_list_test = list_test
|
| 129 |
+
reader_list_train, reader_list_val, reader_list_test = (
|
| 130 |
+
reader_list,
|
| 131 |
+
reader_list,
|
| 132 |
+
reader_list,
|
| 133 |
+
)
|
| 134 |
+
|
| 135 |
+
elif args.test_mode == "subject":
|
| 136 |
+
reader_list_train = list_train_net
|
| 137 |
+
reader_list_val = list_val_net
|
| 138 |
+
reader_list_test = list_test
|
| 139 |
+
sn_list_train, sn_list_val, sn_list_test = sn_list, sn_list, sn_list
|
| 140 |
+
|
| 141 |
+
# initialize tokenizer
|
| 142 |
+
tokenizer = BertTokenizerFast.from_pretrained(cf["model_pretrained"])
|
| 143 |
+
# Preparing batch data
|
| 144 |
+
dataset_train = celerdataset(
|
| 145 |
+
word_info_df, eyemovement_df, cf, reader_list_train, sn_list_train, tokenizer
|
| 146 |
+
)
|
| 147 |
+
train_dataloaderr = DataLoader(
|
| 148 |
+
dataset_train, batch_size=cf["batch_size"], shuffle=True, drop_last=True
|
| 149 |
+
)
|
| 150 |
+
|
| 151 |
+
dataset_val = celerdataset(
|
| 152 |
+
word_info_df, eyemovement_df, cf, reader_list_val, sn_list_val, tokenizer
|
| 153 |
+
)
|
| 154 |
+
val_dataloaderr = DataLoader(
|
| 155 |
+
dataset_val, batch_size=cf["batch_size"], shuffle=False, drop_last=True
|
| 156 |
+
)
|
| 157 |
+
|
| 158 |
+
dataset_test = celerdataset(
|
| 159 |
+
word_info_df, eyemovement_df, cf, reader_list_test, sn_list_test, tokenizer
|
| 160 |
+
)
|
| 161 |
+
test_dataloaderr = DataLoader(
|
| 162 |
+
dataset_test, batch_size=cf["batch_size"], shuffle=False, drop_last=False
|
| 163 |
+
)
|
| 164 |
+
|
| 165 |
+
# z-score normalization for gaze features
|
| 166 |
+
fix_dur_mean, fix_dur_std = calculate_mean_std(
|
| 167 |
+
dataloader=train_dataloaderr, feat_key="sp_fix_dur", padding_value=0, scale=1000
|
| 168 |
+
)
|
| 169 |
+
landing_pos_mean, landing_pos_std = calculate_mean_std(
|
| 170 |
+
dataloader=train_dataloaderr, feat_key="sp_landing_pos", padding_value=0
|
| 171 |
+
)
|
| 172 |
+
sn_word_len_mean, sn_word_len_std = calculate_mean_std(
|
| 173 |
+
dataloader=train_dataloaderr, feat_key="sn_word_len"
|
| 174 |
+
)
|
| 175 |
+
|
| 176 |
+
# load model
|
| 177 |
+
dnn = Eyettention(cf)
|
| 178 |
+
|
| 179 |
+
# training
|
| 180 |
+
episode = 0
|
| 181 |
+
optimizer = Adam(dnn.parameters(), lr=cf["lr"])
|
| 182 |
+
dnn.train()
|
| 183 |
+
dnn.to(device)
|
| 184 |
+
av_score = deque(maxlen=100)
|
| 185 |
+
old_score = 1e10
|
| 186 |
+
save_ep_couter = 0
|
| 187 |
+
print("Start training")
|
| 188 |
+
for episode_i in range(episode, cf["n_epochs"] + 1):
|
| 189 |
+
dnn.train()
|
| 190 |
+
print("episode:", episode_i)
|
| 191 |
+
counter = 0
|
| 192 |
+
for batchh in train_dataloaderr:
|
| 193 |
+
counter += 1
|
| 194 |
+
batchh.keys()
|
| 195 |
+
sn_input_ids = batchh["sn_input_ids"].to(device)
|
| 196 |
+
sn_attention_mask = batchh["sn_attention_mask"].to(device)
|
| 197 |
+
word_ids_sn = batchh["word_ids_sn"].to(device)
|
| 198 |
+
sn_word_len = batchh["sn_word_len"].to(device)
|
| 199 |
+
|
| 200 |
+
sp_input_ids = batchh["sp_input_ids"].to(device)
|
| 201 |
+
sp_attention_mask = batchh["sp_attention_mask"].to(device)
|
| 202 |
+
word_ids_sp = batchh["word_ids_sp"].to(device)
|
| 203 |
+
|
| 204 |
+
sp_pos = batchh["sp_pos"].to(device)
|
| 205 |
+
sp_landing_pos = batchh["sp_landing_pos"].to(device)
|
| 206 |
+
sp_fix_dur = (batchh["sp_fix_dur"] / 1000).to(device)
|
| 207 |
+
|
| 208 |
+
# normalize gaze features
|
| 209 |
+
mask = ~torch.eq(sp_fix_dur, 0)
|
| 210 |
+
sp_fix_dur = (sp_fix_dur - fix_dur_mean) / fix_dur_std * mask
|
| 211 |
+
sp_landing_pos = (sp_landing_pos - landing_pos_mean) / landing_pos_std * mask
|
| 212 |
+
sp_fix_dur = torch.nan_to_num(sp_fix_dur)
|
| 213 |
+
sp_landing_pos = torch.nan_to_num(sp_landing_pos)
|
| 214 |
+
sn_word_len = (sn_word_len - sn_word_len_mean) / sn_word_len_std
|
| 215 |
+
sn_word_len = torch.nan_to_num(sn_word_len)
|
| 216 |
+
|
| 217 |
+
# zero old gradients
|
| 218 |
+
optimizer.zero_grad()
|
| 219 |
+
# predict output with DNN
|
| 220 |
+
dnn_out, atten_weights = dnn(
|
| 221 |
+
sn_emd=sn_input_ids,
|
| 222 |
+
sn_mask=sn_attention_mask,
|
| 223 |
+
sp_emd=sp_input_ids,
|
| 224 |
+
sp_pos=sp_pos,
|
| 225 |
+
word_ids_sn=word_ids_sn,
|
| 226 |
+
word_ids_sp=word_ids_sp,
|
| 227 |
+
sp_fix_dur=sp_fix_dur,
|
| 228 |
+
sp_landing_pos=sp_landing_pos,
|
| 229 |
+
sn_word_len=sn_word_len,
|
| 230 |
+
)
|
| 231 |
+
|
| 232 |
+
dnn_out = dnn_out.permute(0, 2, 1) # [batch, dec_o_dim, step]
|
| 233 |
+
|
| 234 |
+
# prepare label and mask
|
| 235 |
+
pad_mask, label = load_label(sp_pos, cf, le, device)
|
| 236 |
+
loss = nn.CrossEntropyLoss(reduction="none")
|
| 237 |
+
batch_error = torch.mean(torch.masked_select(loss(dnn_out, label), ~pad_mask))
|
| 238 |
+
|
| 239 |
+
# backpropagate loss
|
| 240 |
+
batch_error.backward()
|
| 241 |
+
# clip gradients
|
| 242 |
+
gradient_clipping(dnn, cf["max_grad_norm"])
|
| 243 |
+
|
| 244 |
+
# learn
|
| 245 |
+
optimizer.step()
|
| 246 |
+
av_score.append(batch_error.to("cpu").detach().numpy())
|
| 247 |
+
print("counter:", counter)
|
| 248 |
+
print(
|
| 249 |
+
"\rSample {}\tAverage Error: {:.10f} ".format(counter, np.mean(av_score)),
|
| 250 |
+
end=" ",
|
| 251 |
+
)
|
| 252 |
+
loss_dict["train_loss"].append(np.mean(av_score))
|
| 253 |
+
|
| 254 |
+
val_loss = []
|
| 255 |
+
dnn.eval()
|
| 256 |
+
for batchh in val_dataloaderr:
|
| 257 |
+
with torch.no_grad():
|
| 258 |
+
sn_input_ids_val = batchh["sn_input_ids"].to(device)
|
| 259 |
+
sn_attention_mask_val = batchh["sn_attention_mask"].to(device)
|
| 260 |
+
word_ids_sn_val = batchh["word_ids_sn"].to(device)
|
| 261 |
+
sn_word_len_val = batchh["sn_word_len"].to(device)
|
| 262 |
+
|
| 263 |
+
sp_input_ids_val = batchh["sp_input_ids"].to(device)
|
| 264 |
+
sp_attention_mask_val = batchh["sp_attention_mask"].to(device)
|
| 265 |
+
word_ids_sp_val = batchh["word_ids_sp"].to(device)
|
| 266 |
+
|
| 267 |
+
sp_pos_val = batchh["sp_pos"].to(device)
|
| 268 |
+
sp_landing_pos_val = batchh["sp_landing_pos"].to(device)
|
| 269 |
+
sp_fix_dur_val = (batchh["sp_fix_dur"] / 1000).to(device)
|
| 270 |
+
|
| 271 |
+
# normalize gaze features
|
| 272 |
+
mask = ~torch.eq(sp_fix_dur_val, 0)
|
| 273 |
+
sp_fix_dur_val = (sp_fix_dur_val - fix_dur_mean) / fix_dur_std * mask
|
| 274 |
+
sp_landing_pos_val = (
|
| 275 |
+
(sp_landing_pos_val - landing_pos_mean) / landing_pos_std * mask
|
| 276 |
+
)
|
| 277 |
+
sp_fix_dur_val = torch.nan_to_num(sp_fix_dur_val)
|
| 278 |
+
sp_landing_pos_val = torch.nan_to_num(sp_landing_pos_val)
|
| 279 |
+
sn_word_len_val = (sn_word_len_val - sn_word_len_mean) / sn_word_len_std
|
| 280 |
+
sn_word_len_val = torch.nan_to_num(sn_word_len_val)
|
| 281 |
+
|
| 282 |
+
dnn_out_val, atten_weights_val = dnn(
|
| 283 |
+
sn_emd=sn_input_ids_val,
|
| 284 |
+
sn_mask=sn_attention_mask_val,
|
| 285 |
+
sp_emd=sp_input_ids_val,
|
| 286 |
+
sp_pos=sp_pos_val,
|
| 287 |
+
word_ids_sn=word_ids_sn_val,
|
| 288 |
+
word_ids_sp=word_ids_sp_val,
|
| 289 |
+
sp_fix_dur=sp_fix_dur_val,
|
| 290 |
+
sp_landing_pos=sp_landing_pos_val,
|
| 291 |
+
sn_word_len=sn_word_len_val,
|
| 292 |
+
)
|
| 293 |
+
dnn_out_val = dnn_out_val.permute(0, 2, 1) # [batch, dec_o_dim, step
|
| 294 |
+
|
| 295 |
+
# prepare label and mask
|
| 296 |
+
pad_mask_val, label_val = load_label(sp_pos_val, cf, le, device)
|
| 297 |
+
batch_error_val = torch.mean(
|
| 298 |
+
torch.masked_select(loss(dnn_out_val, label_val), ~pad_mask_val)
|
| 299 |
+
)
|
| 300 |
+
val_loss.append(batch_error_val.detach().to("cpu").numpy())
|
| 301 |
+
print("\nvalidation loss is {} \n".format(np.mean(val_loss)))
|
| 302 |
+
loss_dict["val_loss"].append(np.mean(val_loss))
|
| 303 |
+
|
| 304 |
+
if np.mean(val_loss) < old_score:
|
| 305 |
+
# save model if val loss is smallest
|
| 306 |
+
torch.save(
|
| 307 |
+
dnn.state_dict(),
|
| 308 |
+
"{}/CELoss_CELER_{}_eyettention_{}_newloss_fold{}.pth".format(
|
| 309 |
+
args.save_data_folder, args.test_mode, args.atten_type, fold_indx
|
| 310 |
+
),
|
| 311 |
+
)
|
| 312 |
+
old_score = np.mean(val_loss)
|
| 313 |
+
print("\nsaved model state dict\n")
|
| 314 |
+
save_ep_couter = episode_i
|
| 315 |
+
else:
|
| 316 |
+
# early stopping
|
| 317 |
+
if episode_i - save_ep_couter >= cf["earlystop_patience"]:
|
| 318 |
+
break
|
| 319 |
+
|
| 320 |
+
# evaluation
|
| 321 |
+
dnn.eval()
|
| 322 |
+
res_llh = []
|
| 323 |
+
dnn.load_state_dict(
|
| 324 |
+
torch.load(
|
| 325 |
+
os.path.join(
|
| 326 |
+
args.save_data_folder,
|
| 327 |
+
f"CELoss_CELER_{args.test_mode}_eyettention_{args.atten_type}_newloss_fold{fold_indx}.pth",
|
| 328 |
+
),
|
| 329 |
+
map_location="cpu",
|
| 330 |
+
)
|
| 331 |
+
)
|
| 332 |
+
dnn.to(device)
|
| 333 |
+
batch_indx = 0
|
| 334 |
+
for batchh in test_dataloaderr:
|
| 335 |
+
with torch.no_grad():
|
| 336 |
+
sn_input_ids_test = batchh["sn_input_ids"].to(device)
|
| 337 |
+
sn_attention_mask_test = batchh["sn_attention_mask"].to(device)
|
| 338 |
+
word_ids_sn_test = batchh["word_ids_sn"].to(device)
|
| 339 |
+
sn_word_len_test = batchh["sn_word_len"].to(device)
|
| 340 |
+
|
| 341 |
+
sp_input_ids_test = batchh["sp_input_ids"].to(device)
|
| 342 |
+
sp_attention_mask_test = batchh["sp_attention_mask"].to(device)
|
| 343 |
+
word_ids_sp_test = batchh["word_ids_sp"].to(device)
|
| 344 |
+
|
| 345 |
+
sp_pos_test = batchh["sp_pos"].to(device)
|
| 346 |
+
sp_landing_pos_test = batchh["sp_landing_pos"].to(device)
|
| 347 |
+
sp_fix_dur_test = (batchh["sp_fix_dur"] / 1000).to(device)
|
| 348 |
+
|
| 349 |
+
# normalize gaze features
|
| 350 |
+
mask = ~torch.eq(sp_fix_dur_test, 0)
|
| 351 |
+
sp_fix_dur_test = (sp_fix_dur_test - fix_dur_mean) / fix_dur_std * mask
|
| 352 |
+
sp_landing_pos_test = (
|
| 353 |
+
(sp_landing_pos_test - landing_pos_mean) / landing_pos_std * mask
|
| 354 |
+
)
|
| 355 |
+
sp_fix_dur_test = torch.nan_to_num(sp_fix_dur_test)
|
| 356 |
+
sp_landing_pos_test = torch.nan_to_num(sp_landing_pos_test)
|
| 357 |
+
sn_word_len_test = (sn_word_len_test - sn_word_len_mean) / sn_word_len_std
|
| 358 |
+
sn_word_len_test = torch.nan_to_num(sn_word_len_test)
|
| 359 |
+
|
| 360 |
+
dnn_out_test, atten_weights_test = dnn(
|
| 361 |
+
sn_emd=sn_input_ids_test,
|
| 362 |
+
sn_mask=sn_attention_mask_test,
|
| 363 |
+
sp_emd=sp_input_ids_test,
|
| 364 |
+
sp_pos=sp_pos_test,
|
| 365 |
+
word_ids_sn=word_ids_sn_test,
|
| 366 |
+
word_ids_sp=word_ids_sp_test,
|
| 367 |
+
sp_fix_dur=sp_fix_dur_test,
|
| 368 |
+
sp_landing_pos=sp_landing_pos_test,
|
| 369 |
+
sn_word_len=sn_word_len_test,
|
| 370 |
+
)
|
| 371 |
+
|
| 372 |
+
# We do not use nn.CrossEntropyLoss here to calculate the likelihood because it combines nn.LogSoftmax and nn.NLL,
|
| 373 |
+
# while nn.LogSoftmax returns a log value based on e, we want 2 instead
|
| 374 |
+
# m = nn.LogSoftmax(dim=2) -- base e, we want base 2
|
| 375 |
+
m = nn.Softmax(dim=2)
|
| 376 |
+
dnn_out_test = m(dnn_out_test).detach().to("cpu").numpy()
|
| 377 |
+
|
| 378 |
+
# prepare label and mask
|
| 379 |
+
pad_mask_test, label_test = load_label(sp_pos_test, cf, le, "cpu")
|
| 380 |
+
pred = dnn_out_test.argmax(axis=2)
|
| 381 |
+
# compute log likelihood for the batch samples
|
| 382 |
+
res_batch = eval_log_llh(dnn_out_test, label_test, pad_mask_test)
|
| 383 |
+
res_llh.append(np.array(res_batch))
|
| 384 |
+
|
| 385 |
+
if bool(args.scanpath_gen_flag) == True:
|
| 386 |
+
sn_len = (
|
| 387 |
+
(torch.max(torch.nan_to_num(word_ids_sn_test), dim=1)[0] + 1 - 2)
|
| 388 |
+
.detach()
|
| 389 |
+
.to("cpu")
|
| 390 |
+
.numpy()
|
| 391 |
+
)
|
| 392 |
+
# compute the scan path generated from the model when the first few fixed points are given
|
| 393 |
+
sp_dnn = dnn.scanpath_generation(
|
| 394 |
+
sn_emd=sn_input_ids_test,
|
| 395 |
+
sn_mask=sn_attention_mask_test,
|
| 396 |
+
word_ids_sn=word_ids_sn_test,
|
| 397 |
+
sn_word_len=sn_word_len_test,
|
| 398 |
+
le=le,
|
| 399 |
+
max_pred_len=cf["max_pred_len"],
|
| 400 |
+
)
|
| 401 |
+
|
| 402 |
+
sp_dnn, sp_human = prepare_scanpath(
|
| 403 |
+
sp_dnn.detach().to("cpu").numpy(), sn_len, sp_pos_test, cf
|
| 404 |
+
)
|
| 405 |
+
sp_dnn_list.extend(sp_dnn)
|
| 406 |
+
sp_human_list.extend(sp_human)
|
| 407 |
+
|
| 408 |
+
batch_indx += 1
|
| 409 |
+
|
| 410 |
+
res_llh = np.concatenate(res_llh).ravel()
|
| 411 |
+
loss_dict["test_ll"].append(res_llh)
|
| 412 |
+
loss_dict["fix_dur_mean"] = fix_dur_mean
|
| 413 |
+
loss_dict["fix_dur_std"] = fix_dur_std
|
| 414 |
+
loss_dict["landing_pos_mean"] = landing_pos_mean
|
| 415 |
+
loss_dict["landing_pos_std"] = landing_pos_std
|
| 416 |
+
loss_dict["sn_word_len_mean"] = sn_word_len_mean
|
| 417 |
+
loss_dict["sn_word_len_std"] = sn_word_len_std
|
| 418 |
+
print("\nTest likelihood is {} \n".format(np.mean(res_llh)))
|
| 419 |
+
# save results
|
| 420 |
+
with open(
|
| 421 |
+
"{}/res_CELER_{}_eyettention_{}_Fold{}.pickle".format(
|
| 422 |
+
args.save_data_folder, args.test_mode, args.atten_type, fold_indx
|
| 423 |
+
),
|
| 424 |
+
"wb",
|
| 425 |
+
) as handle:
|
| 426 |
+
pickle.dump(loss_dict, handle, protocol=pickle.HIGHEST_PROTOCOL)
|
| 427 |
+
fold_indx += 1
|
| 428 |
+
|
| 429 |
+
if bool(args.scanpath_gen_flag) == True:
|
| 430 |
+
# save results
|
| 431 |
+
dic = {"sp_dnn": sp_dnn_list, "sp_human": sp_human_list}
|
| 432 |
+
with open(
|
| 433 |
+
os.path.join(
|
| 434 |
+
args.save_data_folder,
|
| 435 |
+
f"CELER_scanpath_generation_eyettention_{args.test_mode}_{args.atten_type}.pickle",
|
| 436 |
+
),
|
| 437 |
+
"wb",
|
| 438 |
+
) as handle:
|
| 439 |
+
pickle.dump(dic, handle, protocol=pickle.HIGHEST_PROTOCOL)
|
main_celer_NRS_setting.py
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import os
|
| 4 |
+
from sklearn.model_selection import StratifiedKFold, KFold
|
| 5 |
+
import torch
|
| 6 |
+
import torch.nn as nn
|
| 7 |
+
from torch.utils.data import DataLoader
|
| 8 |
+
from torch.optim import Adam, RMSprop
|
| 9 |
+
from transformers import BertTokenizerFast
|
| 10 |
+
from sklearn.preprocessing import LabelEncoder, OneHotEncoder
|
| 11 |
+
from torch.nn.functional import cross_entropy, softmax
|
| 12 |
+
from collections import deque
|
| 13 |
+
import pickle
|
| 14 |
+
import json
|
| 15 |
+
import random
|
| 16 |
+
import matplotlib.pyplot as plt
|
| 17 |
+
import argparse
|
| 18 |
+
|
| 19 |
+
from Eyettention.utils import *
|
| 20 |
+
from Eyettention.model import Eyettention
|
| 21 |
+
|
| 22 |
+
if __name__ == "__main__":
|
| 23 |
+
parser = argparse.ArgumentParser(description="run uniform baseline")
|
| 24 |
+
parser.add_argument(
|
| 25 |
+
"--test_mode", help="test mode: New Reader / New Sentence Split", type=str, default="NRS"
|
| 26 |
+
)
|
| 27 |
+
parser.add_argument("--atten_type", help="attention type", type=str, default="local-g")
|
| 28 |
+
parser.add_argument(
|
| 29 |
+
"--save_data_folder",
|
| 30 |
+
help="folder path for saving results",
|
| 31 |
+
type=str,
|
| 32 |
+
default="./results/CELER/",
|
| 33 |
+
)
|
| 34 |
+
parser.add_argument(
|
| 35 |
+
"--scanpath_gen_flag", help="whether to generate scanpath", type=int, default=1
|
| 36 |
+
)
|
| 37 |
+
parser.add_argument(
|
| 38 |
+
"--max_pred_len",
|
| 39 |
+
help="if scanpath_gen_flag is True, you can determine the longest scanpath that you want to generate, which should depend on the sentence length",
|
| 40 |
+
type=int,
|
| 41 |
+
default=60,
|
| 42 |
+
)
|
| 43 |
+
parser.add_argument("--sampl_time", help="Sampling times", type=int, default=5)
|
| 44 |
+
parser.add_argument("--gpu", help="gpu index", type=int, default=6)
|
| 45 |
+
args = parser.parse_args()
|
| 46 |
+
gpu = args.gpu
|
| 47 |
+
|
| 48 |
+
# use FastTokenizer lead to warning -> The current process just got forked
|
| 49 |
+
os.environ["TOKENIZERS_PARALLELISM"] = "false"
|
| 50 |
+
torch.set_default_tensor_type("torch.FloatTensor")
|
| 51 |
+
availbl = torch.cuda.is_available()
|
| 52 |
+
print(torch.cuda.is_available())
|
| 53 |
+
if availbl:
|
| 54 |
+
device = f"cuda:{gpu}"
|
| 55 |
+
else:
|
| 56 |
+
device = "cpu"
|
| 57 |
+
print(device)
|
| 58 |
+
torch.cuda.set_device(gpu)
|
| 59 |
+
|
| 60 |
+
cf = {
|
| 61 |
+
"model_pretrained": "bert-base-cased",
|
| 62 |
+
"lr": 1e-3,
|
| 63 |
+
"max_grad_norm": 10,
|
| 64 |
+
"n_epochs": 1000,
|
| 65 |
+
"dataset": "celer",
|
| 66 |
+
"atten_type": args.atten_type,
|
| 67 |
+
"batch_size": 256,
|
| 68 |
+
"max_sn_len": 24, # max number of words in a sentence, include start token and end token,
|
| 69 |
+
"max_sn_token": 35, # maximum number of tokens a sentence includes. include start token and end token,
|
| 70 |
+
"max_sp_len": 52, # max number of words in a scanpath, include start token and end token
|
| 71 |
+
"max_sp_token": 395, # maximum number of tokens a scanpath includes. include start token and end token
|
| 72 |
+
"norm_type": "z-score",
|
| 73 |
+
"earlystop_patience": 20,
|
| 74 |
+
"max_pred_len": args.max_pred_len,
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
# Encode the label into interger categories, setting the exclusive category 'cf["max_sn_len"]-1' as the end sign
|
| 78 |
+
le = LabelEncoder()
|
| 79 |
+
le.fit(np.append(np.arange(-cf["max_sn_len"] + 3, cf["max_sn_len"] - 1), cf["max_sn_len"] - 1))
|
| 80 |
+
# le.classes_
|
| 81 |
+
|
| 82 |
+
# load corpus
|
| 83 |
+
word_info_df, _, eyemovement_df = load_corpus(cf["dataset"])
|
| 84 |
+
|
| 85 |
+
# only use native speaker
|
| 86 |
+
# Make list with reader index
|
| 87 |
+
reader_list = celer_load_native_speaker()
|
| 88 |
+
# Make list with sentence index
|
| 89 |
+
# sn_list = np.unique(eyemovement_df.sentenceid.values).tolist()
|
| 90 |
+
sn_list = np.unique(
|
| 91 |
+
word_info_df[word_info_df["list"].isin(reader_list)].sentenceid.values
|
| 92 |
+
).tolist()
|
| 93 |
+
|
| 94 |
+
random.seed(0)
|
| 95 |
+
fold_indx = 0
|
| 96 |
+
# for scanpath generation
|
| 97 |
+
sp_dnn_list = []
|
| 98 |
+
sp_human_list = []
|
| 99 |
+
for i in range(args.sampl_time):
|
| 100 |
+
print("time:", i)
|
| 101 |
+
loss_dict = {"val_loss": [], "train_loss": [], "test_ll": []}
|
| 102 |
+
readers_test = random.sample(reader_list, int(np.ceil(len(reader_list) * 0.3)))
|
| 103 |
+
reader_list_val = random.sample(readers_test, int(np.ceil(len(readers_test) * 0.3)))
|
| 104 |
+
reader_list_test = set(readers_test) - set(reader_list_val)
|
| 105 |
+
reader_list_test = list(reader_list_test)
|
| 106 |
+
reader_list_train = set(reader_list) - set(reader_list_test) - set(reader_list_val)
|
| 107 |
+
reader_list_train = list(reader_list_train)
|
| 108 |
+
# Make the order of the test samples unchanged on each re-run
|
| 109 |
+
reader_list_test.sort()
|
| 110 |
+
|
| 111 |
+
sn_test = random.sample(sn_list, int(np.ceil(len(sn_list) * 0.3)))
|
| 112 |
+
sn_list_val = random.sample(sn_test, int(np.ceil(len(sn_test) * 0.3)))
|
| 113 |
+
sn_list_test = set(sn_test) - set(sn_list_val)
|
| 114 |
+
sn_list_test = list(sn_list_test)
|
| 115 |
+
sn_list_train = set(sn_list) - set(sn_list_test) - set(sn_list_val)
|
| 116 |
+
sn_list_train = list(sn_list_train)
|
| 117 |
+
# Make the order of the test samples unchanged on each re-run
|
| 118 |
+
sn_list_test.sort()
|
| 119 |
+
|
| 120 |
+
# initialize tokenizer
|
| 121 |
+
tokenizer = BertTokenizerFast.from_pretrained(cf["model_pretrained"])
|
| 122 |
+
# Preparing batch data
|
| 123 |
+
dataset_train = celerdataset(
|
| 124 |
+
word_info_df, eyemovement_df, cf, reader_list_train, sn_list_train, tokenizer
|
| 125 |
+
)
|
| 126 |
+
train_dataloaderr = DataLoader(
|
| 127 |
+
dataset_train, batch_size=cf["batch_size"], shuffle=True, drop_last=True
|
| 128 |
+
)
|
| 129 |
+
|
| 130 |
+
dataset_val = celerdataset(
|
| 131 |
+
word_info_df, eyemovement_df, cf, reader_list_val, sn_list_val, tokenizer
|
| 132 |
+
)
|
| 133 |
+
val_dataloaderr = DataLoader(
|
| 134 |
+
dataset_val, batch_size=cf["batch_size"], shuffle=False, drop_last=False
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
dataset_test = celerdataset(
|
| 138 |
+
word_info_df, eyemovement_df, cf, reader_list_test, sn_list_test, tokenizer
|
| 139 |
+
)
|
| 140 |
+
test_dataloaderr = DataLoader(
|
| 141 |
+
dataset_test, batch_size=cf["batch_size"], shuffle=False, drop_last=False
|
| 142 |
+
)
|
| 143 |
+
|
| 144 |
+
# z-score normalization for gaze features
|
| 145 |
+
fix_dur_mean, fix_dur_std = calculate_mean_std(
|
| 146 |
+
dataloader=train_dataloaderr, feat_key="sp_fix_dur", padding_value=0, scale=1000
|
| 147 |
+
)
|
| 148 |
+
landing_pos_mean, landing_pos_std = calculate_mean_std(
|
| 149 |
+
dataloader=train_dataloaderr, feat_key="sp_landing_pos", padding_value=0
|
| 150 |
+
)
|
| 151 |
+
sn_word_len_mean, sn_word_len_std = calculate_mean_std(
|
| 152 |
+
dataloader=train_dataloaderr, feat_key="sn_word_len"
|
| 153 |
+
)
|
| 154 |
+
|
| 155 |
+
# load model
|
| 156 |
+
dnn = Eyettention(cf)
|
| 157 |
+
|
| 158 |
+
# training
|
| 159 |
+
episode = 0
|
| 160 |
+
optimizer = Adam(dnn.parameters(), lr=cf["lr"])
|
| 161 |
+
dnn.train()
|
| 162 |
+
dnn.to(device)
|
| 163 |
+
av_score = deque(maxlen=100)
|
| 164 |
+
old_score = 1e10
|
| 165 |
+
save_ep_couter = 0
|
| 166 |
+
print("Start training")
|
| 167 |
+
for episode_i in range(episode, cf["n_epochs"] + 1):
|
| 168 |
+
dnn.train()
|
| 169 |
+
print("episode:", episode_i)
|
| 170 |
+
counter = 0
|
| 171 |
+
for batchh in train_dataloaderr:
|
| 172 |
+
counter += 1
|
| 173 |
+
batchh.keys()
|
| 174 |
+
sn_input_ids = batchh["sn_input_ids"].to(device)
|
| 175 |
+
sn_attention_mask = batchh["sn_attention_mask"].to(device)
|
| 176 |
+
word_ids_sn = batchh["word_ids_sn"].to(device)
|
| 177 |
+
sn_word_len = batchh["sn_word_len"].to(device)
|
| 178 |
+
|
| 179 |
+
sp_input_ids = batchh["sp_input_ids"].to(device)
|
| 180 |
+
sp_attention_mask = batchh["sp_attention_mask"].to(device)
|
| 181 |
+
word_ids_sp = batchh["word_ids_sp"].to(device)
|
| 182 |
+
|
| 183 |
+
sp_pos = batchh["sp_pos"].to(device)
|
| 184 |
+
sp_landing_pos = batchh["sp_landing_pos"].to(device)
|
| 185 |
+
sp_fix_dur = (batchh["sp_fix_dur"] / 1000).to(device)
|
| 186 |
+
|
| 187 |
+
# normalize gaze features
|
| 188 |
+
mask = ~torch.eq(sp_fix_dur, 0)
|
| 189 |
+
sp_fix_dur = (sp_fix_dur - fix_dur_mean) / fix_dur_std * mask
|
| 190 |
+
sp_landing_pos = (sp_landing_pos - landing_pos_mean) / landing_pos_std * mask
|
| 191 |
+
sp_fix_dur = torch.nan_to_num(sp_fix_dur)
|
| 192 |
+
sp_landing_pos = torch.nan_to_num(sp_landing_pos)
|
| 193 |
+
sn_word_len = (sn_word_len - sn_word_len_mean) / sn_word_len_std
|
| 194 |
+
sn_word_len = torch.nan_to_num(sn_word_len)
|
| 195 |
+
|
| 196 |
+
# zero old gradients
|
| 197 |
+
optimizer.zero_grad()
|
| 198 |
+
# predict output with DNN
|
| 199 |
+
dnn_out, atten_weights = dnn(
|
| 200 |
+
sn_emd=sn_input_ids,
|
| 201 |
+
sn_mask=sn_attention_mask,
|
| 202 |
+
sp_emd=sp_input_ids,
|
| 203 |
+
sp_pos=sp_pos,
|
| 204 |
+
word_ids_sn=word_ids_sn,
|
| 205 |
+
word_ids_sp=word_ids_sp,
|
| 206 |
+
sp_fix_dur=sp_fix_dur,
|
| 207 |
+
sp_landing_pos=sp_landing_pos,
|
| 208 |
+
sn_word_len=sn_word_len,
|
| 209 |
+
)
|
| 210 |
+
|
| 211 |
+
dnn_out = dnn_out.permute(0, 2, 1) # [batch, dec_o_dim, step]
|
| 212 |
+
|
| 213 |
+
# prepare label and mask
|
| 214 |
+
pad_mask, label = load_label(sp_pos, cf, le, device)
|
| 215 |
+
loss = nn.CrossEntropyLoss(reduction="none")
|
| 216 |
+
batch_error = torch.mean(torch.masked_select(loss(dnn_out, label), ~pad_mask))
|
| 217 |
+
|
| 218 |
+
# backpropagate loss
|
| 219 |
+
batch_error.backward()
|
| 220 |
+
# clip gradients
|
| 221 |
+
gradient_clipping(dnn, cf["max_grad_norm"])
|
| 222 |
+
|
| 223 |
+
# learn
|
| 224 |
+
optimizer.step()
|
| 225 |
+
av_score.append(batch_error.to("cpu").detach().numpy())
|
| 226 |
+
print("counter:", counter)
|
| 227 |
+
print(
|
| 228 |
+
"\rSample {}\tAverage Error: {:.10f} ".format(counter, np.mean(av_score)),
|
| 229 |
+
end=" ",
|
| 230 |
+
)
|
| 231 |
+
loss_dict["train_loss"].append(np.mean(av_score))
|
| 232 |
+
|
| 233 |
+
val_loss = []
|
| 234 |
+
dnn.eval()
|
| 235 |
+
for batchh in val_dataloaderr:
|
| 236 |
+
with torch.no_grad():
|
| 237 |
+
sn_input_ids_val = batchh["sn_input_ids"].to(device)
|
| 238 |
+
sn_attention_mask_val = batchh["sn_attention_mask"].to(device)
|
| 239 |
+
word_ids_sn_val = batchh["word_ids_sn"].to(device)
|
| 240 |
+
sn_word_len_val = batchh["sn_word_len"].to(device)
|
| 241 |
+
|
| 242 |
+
sp_input_ids_val = batchh["sp_input_ids"].to(device)
|
| 243 |
+
sp_attention_mask_val = batchh["sp_attention_mask"].to(device)
|
| 244 |
+
word_ids_sp_val = batchh["word_ids_sp"].to(device)
|
| 245 |
+
|
| 246 |
+
sp_pos_val = batchh["sp_pos"].to(device)
|
| 247 |
+
sp_landing_pos_val = batchh["sp_landing_pos"].to(device)
|
| 248 |
+
sp_fix_dur_val = (batchh["sp_fix_dur"] / 1000).to(device)
|
| 249 |
+
|
| 250 |
+
# normalize gaze features
|
| 251 |
+
mask = ~torch.eq(sp_fix_dur_val, 0)
|
| 252 |
+
sp_fix_dur_val = (sp_fix_dur_val - fix_dur_mean) / fix_dur_std * mask
|
| 253 |
+
sp_landing_pos_val = (
|
| 254 |
+
(sp_landing_pos_val - landing_pos_mean) / landing_pos_std * mask
|
| 255 |
+
)
|
| 256 |
+
sp_fix_dur_val = torch.nan_to_num(sp_fix_dur_val)
|
| 257 |
+
sp_landing_pos_val = torch.nan_to_num(sp_landing_pos_val)
|
| 258 |
+
sn_word_len_val = (sn_word_len_val - sn_word_len_mean) / sn_word_len_std
|
| 259 |
+
sn_word_len_val = torch.nan_to_num(sn_word_len_val)
|
| 260 |
+
|
| 261 |
+
dnn_out_val, atten_weights_val = dnn(
|
| 262 |
+
sn_emd=sn_input_ids_val,
|
| 263 |
+
sn_mask=sn_attention_mask_val,
|
| 264 |
+
sp_emd=sp_input_ids_val,
|
| 265 |
+
sp_pos=sp_pos_val,
|
| 266 |
+
word_ids_sn=word_ids_sn_val,
|
| 267 |
+
word_ids_sp=word_ids_sp_val,
|
| 268 |
+
sp_fix_dur=sp_fix_dur_val,
|
| 269 |
+
sp_landing_pos=sp_landing_pos_val,
|
| 270 |
+
sn_word_len=sn_word_len_val,
|
| 271 |
+
)
|
| 272 |
+
dnn_out_val = dnn_out_val.permute(0, 2, 1) # [batch, dec_o_dim, step
|
| 273 |
+
|
| 274 |
+
# prepare label and mask
|
| 275 |
+
pad_mask_val, label_val = load_label(sp_pos_val, cf, le, device)
|
| 276 |
+
batch_error_val = torch.mean(
|
| 277 |
+
torch.masked_select(loss(dnn_out_val, label_val), ~pad_mask_val)
|
| 278 |
+
)
|
| 279 |
+
val_loss.append(batch_error_val.detach().to("cpu").numpy())
|
| 280 |
+
print("\nvalidation loss is {} \n".format(np.mean(val_loss)))
|
| 281 |
+
loss_dict["val_loss"].append(np.mean(val_loss))
|
| 282 |
+
|
| 283 |
+
if np.mean(val_loss) < old_score:
|
| 284 |
+
# save model if val loss is smallest
|
| 285 |
+
torch.save(
|
| 286 |
+
dnn.state_dict(),
|
| 287 |
+
"{}/CELoss_CELER_NRS_eyettention_{}_newloss_fold{}.pth".format(
|
| 288 |
+
args.save_data_folder, args.atten_type, fold_indx
|
| 289 |
+
),
|
| 290 |
+
)
|
| 291 |
+
old_score = np.mean(val_loss)
|
| 292 |
+
print("\nsaved model state dict\n")
|
| 293 |
+
save_ep_couter = episode_i
|
| 294 |
+
else:
|
| 295 |
+
# early stopping
|
| 296 |
+
if episode_i - save_ep_couter >= cf["earlystop_patience"]:
|
| 297 |
+
break
|
| 298 |
+
|
| 299 |
+
# evaluation
|
| 300 |
+
dnn.eval()
|
| 301 |
+
res_llh = []
|
| 302 |
+
dnn.load_state_dict(
|
| 303 |
+
torch.load(
|
| 304 |
+
os.path.join(
|
| 305 |
+
args.save_data_folder,
|
| 306 |
+
f"CELoss_CELER_NRS_eyettention_{args.atten_type}_newloss_fold{fold_indx}.pth",
|
| 307 |
+
),
|
| 308 |
+
map_location="cpu",
|
| 309 |
+
)
|
| 310 |
+
)
|
| 311 |
+
dnn.to(device)
|
| 312 |
+
batch_indx = 0
|
| 313 |
+
for batchh in test_dataloaderr:
|
| 314 |
+
with torch.no_grad():
|
| 315 |
+
sn_input_ids_test = batchh["sn_input_ids"].to(device)
|
| 316 |
+
sn_attention_mask_test = batchh["sn_attention_mask"].to(device)
|
| 317 |
+
word_ids_sn_test = batchh["word_ids_sn"].to(device)
|
| 318 |
+
sn_word_len_test = batchh["sn_word_len"].to(device)
|
| 319 |
+
|
| 320 |
+
sp_input_ids_test = batchh["sp_input_ids"].to(device)
|
| 321 |
+
sp_attention_mask_test = batchh["sp_attention_mask"].to(device)
|
| 322 |
+
word_ids_sp_test = batchh["word_ids_sp"].to(device)
|
| 323 |
+
|
| 324 |
+
sp_pos_test = batchh["sp_pos"].to(device)
|
| 325 |
+
sp_landing_pos_test = batchh["sp_landing_pos"].to(device)
|
| 326 |
+
sp_fix_dur_test = (batchh["sp_fix_dur"] / 1000).to(device)
|
| 327 |
+
|
| 328 |
+
# normalize gaze features
|
| 329 |
+
mask = ~torch.eq(sp_fix_dur_test, 0)
|
| 330 |
+
sp_fix_dur_test = (sp_fix_dur_test - fix_dur_mean) / fix_dur_std * mask
|
| 331 |
+
sp_landing_pos_test = (
|
| 332 |
+
(sp_landing_pos_test - landing_pos_mean) / landing_pos_std * mask
|
| 333 |
+
)
|
| 334 |
+
sp_fix_dur_test = torch.nan_to_num(sp_fix_dur_test)
|
| 335 |
+
sp_landing_pos_test = torch.nan_to_num(sp_landing_pos_test)
|
| 336 |
+
sn_word_len_test = (sn_word_len_test - sn_word_len_mean) / sn_word_len_std
|
| 337 |
+
sn_word_len_test = torch.nan_to_num(sn_word_len_test)
|
| 338 |
+
|
| 339 |
+
dnn_out_test, atten_weights_test = dnn(
|
| 340 |
+
sn_emd=sn_input_ids_test,
|
| 341 |
+
sn_mask=sn_attention_mask_test,
|
| 342 |
+
sp_emd=sp_input_ids_test,
|
| 343 |
+
sp_pos=sp_pos_test,
|
| 344 |
+
word_ids_sn=word_ids_sn_test,
|
| 345 |
+
word_ids_sp=word_ids_sp_test,
|
| 346 |
+
sp_fix_dur=sp_fix_dur_test,
|
| 347 |
+
sp_landing_pos=sp_landing_pos_test,
|
| 348 |
+
sn_word_len=sn_word_len_test,
|
| 349 |
+
)
|
| 350 |
+
|
| 351 |
+
# We do not use nn.CrossEntropyLoss here to calculate the likelihood because it combines nn.LogSoftmax and nn.NLL,
|
| 352 |
+
# while nn.LogSoftmax returns a log value based on e, we want 2 instead
|
| 353 |
+
# m = nn.LogSoftmax(dim=2) -- base e, we want base 2
|
| 354 |
+
m = nn.Softmax(dim=2)
|
| 355 |
+
dnn_out_test = m(dnn_out_test).detach().to("cpu").numpy()
|
| 356 |
+
|
| 357 |
+
# prepare label and mask
|
| 358 |
+
pad_mask_test, label_test = load_label(sp_pos_test, cf, le, "cpu")
|
| 359 |
+
pred = dnn_out_test.argmax(axis=2)
|
| 360 |
+
# compute log likelihood for the batch samples
|
| 361 |
+
res_batch = eval_log_llh(dnn_out_test, label_test, pad_mask_test)
|
| 362 |
+
res_llh.append(np.array(res_batch))
|
| 363 |
+
|
| 364 |
+
# compute the scan path generated from the model when the first few fixed points are given
|
| 365 |
+
if bool(args.scanpath_gen_flag) == True:
|
| 366 |
+
sn_len = (
|
| 367 |
+
(torch.max(torch.nan_to_num(word_ids_sn_test), dim=1)[0] + 1 - 2)
|
| 368 |
+
.detach()
|
| 369 |
+
.to("cpu")
|
| 370 |
+
.numpy()
|
| 371 |
+
)
|
| 372 |
+
# compute the scan path generated from the model when the first few fixed points are given
|
| 373 |
+
sp_dnn = dnn.scanpath_generation(
|
| 374 |
+
sn_emd=sn_input_ids_test,
|
| 375 |
+
sn_mask=sn_attention_mask_test,
|
| 376 |
+
word_ids_sn=word_ids_sn_test,
|
| 377 |
+
sn_word_len=sn_word_len_test,
|
| 378 |
+
le=le,
|
| 379 |
+
max_pred_len=cf["max_pred_len"],
|
| 380 |
+
)
|
| 381 |
+
|
| 382 |
+
sp_dnn, sp_human = prepare_scanpath(
|
| 383 |
+
sp_dnn.detach().to("cpu").numpy(), sn_len, sp_pos_test, cf
|
| 384 |
+
)
|
| 385 |
+
sp_dnn_list.extend(sp_dnn)
|
| 386 |
+
sp_human_list.extend(sp_human)
|
| 387 |
+
|
| 388 |
+
batch_indx += 1
|
| 389 |
+
|
| 390 |
+
res_llh = np.concatenate(res_llh).ravel()
|
| 391 |
+
loss_dict["test_ll"].append(res_llh)
|
| 392 |
+
loss_dict["fix_dur_mean"] = fix_dur_mean
|
| 393 |
+
loss_dict["fix_dur_std"] = fix_dur_std
|
| 394 |
+
loss_dict["landing_pos_mean"] = landing_pos_mean
|
| 395 |
+
loss_dict["landing_pos_std"] = landing_pos_std
|
| 396 |
+
loss_dict["sn_word_len_mean"] = sn_word_len_mean
|
| 397 |
+
loss_dict["sn_word_len_std"] = sn_word_len_std
|
| 398 |
+
print("\nTest likelihood is {} \n".format(np.mean(res_llh)))
|
| 399 |
+
# save results
|
| 400 |
+
with open(
|
| 401 |
+
"{}/res_CELER_NRS_eyettention_{}_Fold{}.pickle".format(
|
| 402 |
+
args.save_data_folder, args.atten_type, fold_indx
|
| 403 |
+
),
|
| 404 |
+
"wb",
|
| 405 |
+
) as handle:
|
| 406 |
+
pickle.dump(loss_dict, handle, protocol=pickle.HIGHEST_PROTOCOL)
|
| 407 |
+
fold_indx += 1
|
| 408 |
+
|
| 409 |
+
if bool(args.scanpath_gen_flag) == True:
|
| 410 |
+
# save results
|
| 411 |
+
dic = {"sp_dnn": sp_dnn_list, "sp_human": sp_human_list}
|
| 412 |
+
with open(
|
| 413 |
+
os.path.join(
|
| 414 |
+
args.save_data_folder,
|
| 415 |
+
f"CELER_scanpath_generation_eyettention_NRS_{args.atten_type}.pickle",
|
| 416 |
+
),
|
| 417 |
+
"wb",
|
| 418 |
+
) as handle:
|
| 419 |
+
pickle.dump(dic, handle, protocol=pickle.HIGHEST_PROTOCOL)
|
main_celer_reader_identifier.py
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import os
|
| 4 |
+
from sklearn.model_selection import StratifiedKFold, KFold
|
| 5 |
+
import torch
|
| 6 |
+
import torch.nn as nn
|
| 7 |
+
from torch.utils.data import DataLoader
|
| 8 |
+
from torch.optim import Adam, RMSprop
|
| 9 |
+
from transformers import BertTokenizerFast
|
| 10 |
+
from sklearn.preprocessing import LabelEncoder, OneHotEncoder
|
| 11 |
+
from torch.nn.functional import cross_entropy, softmax
|
| 12 |
+
from collections import deque
|
| 13 |
+
import pickle
|
| 14 |
+
import json
|
| 15 |
+
import matplotlib.pyplot as plt
|
| 16 |
+
import argparse
|
| 17 |
+
|
| 18 |
+
from Eyettention.utils import *
|
| 19 |
+
from Eyettention.model import Eyettention_readerID
|
| 20 |
+
|
| 21 |
+
if __name__ == "__main__":
|
| 22 |
+
parser = argparse.ArgumentParser(description="run uniform baseline")
|
| 23 |
+
parser.add_argument("--test_mode", help="test mode: text", type=str, default="text")
|
| 24 |
+
parser.add_argument("--atten_type", help="attention type", type=str, default="local-g")
|
| 25 |
+
parser.add_argument(
|
| 26 |
+
"--save_data_folder",
|
| 27 |
+
help="folder path for saving results",
|
| 28 |
+
type=str,
|
| 29 |
+
default="./results/CELER/",
|
| 30 |
+
)
|
| 31 |
+
parser.add_argument("--gpu", help="gpu index", type=int, default=6)
|
| 32 |
+
parser.add_argument("--emb_size", help="readerID embedding size", type=int, default=32)
|
| 33 |
+
args = parser.parse_args()
|
| 34 |
+
gpu = args.gpu
|
| 35 |
+
|
| 36 |
+
# use FastTokenizer lead to warning -> The current process just got forked
|
| 37 |
+
os.environ["TOKENIZERS_PARALLELISM"] = "false"
|
| 38 |
+
torch.set_default_tensor_type("torch.FloatTensor")
|
| 39 |
+
availbl = torch.cuda.is_available()
|
| 40 |
+
print(torch.cuda.is_available())
|
| 41 |
+
if availbl:
|
| 42 |
+
device = f"cuda:{gpu}"
|
| 43 |
+
else:
|
| 44 |
+
device = "cpu"
|
| 45 |
+
print(device)
|
| 46 |
+
torch.cuda.set_device(gpu)
|
| 47 |
+
|
| 48 |
+
cf = {
|
| 49 |
+
"model_pretrained": "bert-base-cased",
|
| 50 |
+
"lr": 1e-3,
|
| 51 |
+
"max_grad_norm": 10,
|
| 52 |
+
"n_epochs": 1000,
|
| 53 |
+
"n_folds": 5,
|
| 54 |
+
"dataset": "celer",
|
| 55 |
+
"atten_type": args.atten_type,
|
| 56 |
+
"subid_emb_size": args.emb_size,
|
| 57 |
+
"batch_size": 256,
|
| 58 |
+
"max_sn_len": 24, # max number of words in a sentence, include start token and end token,
|
| 59 |
+
"max_sn_token": 35, # maximum number of tokens a sentence includes. include start token and end token,
|
| 60 |
+
"max_sp_len": 52, # max number of words in a scanpath, include start token and end token
|
| 61 |
+
"max_sp_token": 395, # maximum number of tokens a scanpath includes. include start token and end token
|
| 62 |
+
"norm_type": "z-score",
|
| 63 |
+
"earlystop_patience": 20,
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
# Encode the label into interger categories, setting the exclusive category 'cf["max_sn_len"]-1' as the end sign
|
| 67 |
+
le = LabelEncoder()
|
| 68 |
+
le.fit(np.append(np.arange(-cf["max_sn_len"] + 3, cf["max_sn_len"] - 1), cf["max_sn_len"] - 1))
|
| 69 |
+
# le.classes_
|
| 70 |
+
|
| 71 |
+
# load corpus
|
| 72 |
+
word_info_df, _, eyemovement_df = load_corpus(cf["dataset"])
|
| 73 |
+
|
| 74 |
+
# only use native speaker
|
| 75 |
+
# Make list with reader index
|
| 76 |
+
reader_list = celer_load_native_speaker()
|
| 77 |
+
# Make list with sentence index
|
| 78 |
+
sn_list = np.unique(
|
| 79 |
+
word_info_df[word_info_df["list"].isin(reader_list)].sentenceid.values
|
| 80 |
+
).tolist()
|
| 81 |
+
|
| 82 |
+
# Split training&test sets by text
|
| 83 |
+
print("Start evaluating on new sentences.")
|
| 84 |
+
split_list = sn_list
|
| 85 |
+
|
| 86 |
+
n_folds = cf["n_folds"]
|
| 87 |
+
kf = KFold(n_splits=n_folds, shuffle=True, random_state=0)
|
| 88 |
+
fold_indx = 0
|
| 89 |
+
for train_idx, test_idx in kf.split(split_list):
|
| 90 |
+
loss_dict = {"val_loss": [], "train_loss": [], "test_ll": [], "test_AUC": []}
|
| 91 |
+
list_train = [split_list[i] for i in train_idx]
|
| 92 |
+
list_test = [split_list[i] for i in test_idx]
|
| 93 |
+
# create train validation split for training the models:
|
| 94 |
+
kf_val = KFold(n_splits=n_folds, shuffle=True, random_state=0)
|
| 95 |
+
for train_index, val_index in kf_val.split(list_train):
|
| 96 |
+
# we only evaluate a single fold
|
| 97 |
+
break
|
| 98 |
+
list_train_net = [list_train[i] for i in train_index]
|
| 99 |
+
list_val_net = [list_train[i] for i in val_index]
|
| 100 |
+
|
| 101 |
+
sn_list_train = list_train_net
|
| 102 |
+
sn_list_val = list_val_net
|
| 103 |
+
sn_list_test = list_test
|
| 104 |
+
reader_list_train, reader_list_val, reader_list_test = reader_list, reader_list, reader_list
|
| 105 |
+
|
| 106 |
+
# initialize tokenizer
|
| 107 |
+
tokenizer = BertTokenizerFast.from_pretrained(cf["model_pretrained"])
|
| 108 |
+
# Preparing batch data
|
| 109 |
+
dataset_train = celerdataset(
|
| 110 |
+
word_info_df, eyemovement_df, cf, reader_list_train, sn_list_train, tokenizer
|
| 111 |
+
)
|
| 112 |
+
train_dataloaderr = DataLoader(
|
| 113 |
+
dataset_train, batch_size=cf["batch_size"], shuffle=True, drop_last=True
|
| 114 |
+
)
|
| 115 |
+
dataset_val = celerdataset(
|
| 116 |
+
word_info_df, eyemovement_df, cf, reader_list_val, sn_list_val, tokenizer
|
| 117 |
+
)
|
| 118 |
+
val_dataloaderr = DataLoader(
|
| 119 |
+
dataset_val, batch_size=cf["batch_size"], shuffle=False, drop_last=True
|
| 120 |
+
)
|
| 121 |
+
dataset_test = celerdataset(
|
| 122 |
+
word_info_df, eyemovement_df, cf, reader_list_test, sn_list_test, tokenizer
|
| 123 |
+
)
|
| 124 |
+
test_dataloaderr = DataLoader(
|
| 125 |
+
dataset_test, batch_size=cf["batch_size"], shuffle=False, drop_last=False
|
| 126 |
+
)
|
| 127 |
+
|
| 128 |
+
# z-score normalization for gaze features
|
| 129 |
+
fix_dur_mean, fix_dur_std = calculate_mean_std(
|
| 130 |
+
dataloader=train_dataloaderr, feat_key="sp_fix_dur", padding_value=0, scale=1000
|
| 131 |
+
)
|
| 132 |
+
landing_pos_mean, landing_pos_std = calculate_mean_std(
|
| 133 |
+
dataloader=train_dataloaderr, feat_key="sp_landing_pos", padding_value=0
|
| 134 |
+
)
|
| 135 |
+
sn_word_len_mean, sn_word_len_std = calculate_mean_std(
|
| 136 |
+
dataloader=train_dataloaderr, feat_key="sn_word_len"
|
| 137 |
+
)
|
| 138 |
+
|
| 139 |
+
# load model
|
| 140 |
+
dnn = Eyettention_readerID(cf)
|
| 141 |
+
|
| 142 |
+
# training
|
| 143 |
+
episode = 0
|
| 144 |
+
optimizer = Adam(dnn.parameters(), lr=cf["lr"])
|
| 145 |
+
dnn.train()
|
| 146 |
+
dnn.to(device)
|
| 147 |
+
av_score = deque(maxlen=100)
|
| 148 |
+
old_score = 1e10
|
| 149 |
+
save_ep_couter = 0
|
| 150 |
+
print("Start training")
|
| 151 |
+
for episode_i in range(episode, cf["n_epochs"] + 1):
|
| 152 |
+
dnn.train()
|
| 153 |
+
print("episode:", episode_i)
|
| 154 |
+
counter = 0
|
| 155 |
+
for batchh in train_dataloaderr:
|
| 156 |
+
counter += 1
|
| 157 |
+
batchh.keys()
|
| 158 |
+
sn_input_ids = batchh["sn_input_ids"].to(device)
|
| 159 |
+
sn_attention_mask = batchh["sn_attention_mask"].to(device)
|
| 160 |
+
word_ids_sn = batchh["word_ids_sn"].to(device)
|
| 161 |
+
sn_word_len = batchh["sn_word_len"].to(device)
|
| 162 |
+
|
| 163 |
+
sp_input_ids = batchh["sp_input_ids"].to(device)
|
| 164 |
+
sp_attention_mask = batchh["sp_attention_mask"].to(device)
|
| 165 |
+
word_ids_sp = batchh["word_ids_sp"].to(device)
|
| 166 |
+
|
| 167 |
+
sp_pos = batchh["sp_pos"].to(device)
|
| 168 |
+
sp_landing_pos = batchh["sp_landing_pos"].to(device)
|
| 169 |
+
sp_fix_dur = (batchh["sp_fix_dur"] / 1000).to(device)
|
| 170 |
+
sub_id = batchh["sub_id"].to(device)
|
| 171 |
+
|
| 172 |
+
# normalize gaze features
|
| 173 |
+
mask = ~torch.eq(sp_fix_dur, 0)
|
| 174 |
+
sp_fix_dur = (sp_fix_dur - fix_dur_mean) / fix_dur_std * mask
|
| 175 |
+
sp_landing_pos = (sp_landing_pos - landing_pos_mean) / landing_pos_std * mask
|
| 176 |
+
sp_fix_dur = torch.nan_to_num(sp_fix_dur)
|
| 177 |
+
sp_landing_pos = torch.nan_to_num(sp_landing_pos)
|
| 178 |
+
sn_word_len = (sn_word_len - sn_word_len_mean) / sn_word_len_std
|
| 179 |
+
sn_word_len = torch.nan_to_num(sn_word_len)
|
| 180 |
+
|
| 181 |
+
# zero old gradients
|
| 182 |
+
optimizer.zero_grad()
|
| 183 |
+
# predict output with DNN
|
| 184 |
+
dnn_out, atten_weights = dnn(
|
| 185 |
+
sn_emd=sn_input_ids,
|
| 186 |
+
sn_mask=sn_attention_mask,
|
| 187 |
+
sp_emd=sp_input_ids,
|
| 188 |
+
sp_pos=sp_pos,
|
| 189 |
+
word_ids_sn=word_ids_sn,
|
| 190 |
+
word_ids_sp=word_ids_sp,
|
| 191 |
+
sp_fix_dur=sp_fix_dur,
|
| 192 |
+
sp_landing_pos=sp_landing_pos,
|
| 193 |
+
sn_word_len=sn_word_len,
|
| 194 |
+
sub_id=sub_id,
|
| 195 |
+
)
|
| 196 |
+
|
| 197 |
+
dnn_out = dnn_out.permute(0, 2, 1) # [batch, dec_o_dim, step]
|
| 198 |
+
|
| 199 |
+
# prepare label and mask
|
| 200 |
+
pad_mask, label = load_label(sp_pos, cf, le, device)
|
| 201 |
+
loss = nn.CrossEntropyLoss(reduction="none")
|
| 202 |
+
batch_error = torch.mean(torch.masked_select(loss(dnn_out, label), ~pad_mask))
|
| 203 |
+
|
| 204 |
+
# backpropagate loss
|
| 205 |
+
batch_error.backward()
|
| 206 |
+
# clip gradients
|
| 207 |
+
gradient_clipping(dnn, cf["max_grad_norm"])
|
| 208 |
+
|
| 209 |
+
# learn
|
| 210 |
+
optimizer.step()
|
| 211 |
+
av_score.append(batch_error.to("cpu").detach().numpy())
|
| 212 |
+
print("counter:", counter)
|
| 213 |
+
print(
|
| 214 |
+
"\rSample {}\tAverage Error: {:.10f} ".format(counter, np.mean(av_score)),
|
| 215 |
+
end=" ",
|
| 216 |
+
)
|
| 217 |
+
loss_dict["train_loss"].append(np.mean(av_score))
|
| 218 |
+
|
| 219 |
+
val_loss = []
|
| 220 |
+
dnn.eval()
|
| 221 |
+
for batchh in val_dataloaderr:
|
| 222 |
+
with torch.no_grad():
|
| 223 |
+
sn_input_ids_val = batchh["sn_input_ids"].to(device)
|
| 224 |
+
sn_attention_mask_val = batchh["sn_attention_mask"].to(device)
|
| 225 |
+
word_ids_sn_val = batchh["word_ids_sn"].to(device)
|
| 226 |
+
sn_word_len_val = batchh["sn_word_len"].to(device)
|
| 227 |
+
|
| 228 |
+
sp_input_ids_val = batchh["sp_input_ids"].to(device)
|
| 229 |
+
sp_attention_mask_val = batchh["sp_attention_mask"].to(device)
|
| 230 |
+
word_ids_sp_val = batchh["word_ids_sp"].to(device)
|
| 231 |
+
|
| 232 |
+
sp_pos_val = batchh["sp_pos"].to(device)
|
| 233 |
+
sp_landing_pos_val = batchh["sp_landing_pos"].to(device)
|
| 234 |
+
sp_fix_dur_val = (batchh["sp_fix_dur"] / 1000).to(device)
|
| 235 |
+
sub_id_val = batchh["sub_id"].to(device)
|
| 236 |
+
|
| 237 |
+
# normalize gaze features
|
| 238 |
+
mask = ~torch.eq(sp_fix_dur_val, 0)
|
| 239 |
+
sp_fix_dur_val = (sp_fix_dur_val - fix_dur_mean) / fix_dur_std * mask
|
| 240 |
+
sp_landing_pos_val = (
|
| 241 |
+
(sp_landing_pos_val - landing_pos_mean) / landing_pos_std * mask
|
| 242 |
+
)
|
| 243 |
+
sp_fix_dur_val = torch.nan_to_num(sp_fix_dur_val)
|
| 244 |
+
sp_landing_pos_val = torch.nan_to_num(sp_landing_pos_val)
|
| 245 |
+
sn_word_len_val = (sn_word_len_val - sn_word_len_mean) / sn_word_len_std
|
| 246 |
+
sn_word_len_val = torch.nan_to_num(sn_word_len_val)
|
| 247 |
+
|
| 248 |
+
dnn_out_val, atten_weights_val = dnn(
|
| 249 |
+
sn_emd=sn_input_ids_val,
|
| 250 |
+
sn_mask=sn_attention_mask_val,
|
| 251 |
+
sp_emd=sp_input_ids_val,
|
| 252 |
+
sp_pos=sp_pos_val,
|
| 253 |
+
word_ids_sn=word_ids_sn_val,
|
| 254 |
+
word_ids_sp=word_ids_sp_val,
|
| 255 |
+
sp_fix_dur=sp_fix_dur_val,
|
| 256 |
+
sp_landing_pos=sp_landing_pos_val,
|
| 257 |
+
sn_word_len=sn_word_len_val,
|
| 258 |
+
sub_id=sub_id_val,
|
| 259 |
+
)
|
| 260 |
+
dnn_out_val = dnn_out_val.permute(0, 2, 1) # [batch, dec_o_dim, step
|
| 261 |
+
|
| 262 |
+
# prepare label and mask
|
| 263 |
+
pad_mask_val, label_val = load_label(sp_pos_val, cf, le, device)
|
| 264 |
+
batch_error_val = torch.mean(
|
| 265 |
+
torch.masked_select(loss(dnn_out_val, label_val), ~pad_mask_val)
|
| 266 |
+
)
|
| 267 |
+
val_loss.append(batch_error_val.detach().to("cpu").numpy())
|
| 268 |
+
print("\nvalidation loss is {} \n".format(np.mean(val_loss)))
|
| 269 |
+
loss_dict["val_loss"].append(np.mean(val_loss))
|
| 270 |
+
|
| 271 |
+
if np.mean(val_loss) < old_score:
|
| 272 |
+
# save model if val loss is smallest
|
| 273 |
+
torch.save(
|
| 274 |
+
dnn.state_dict(),
|
| 275 |
+
"{}/CELoss_CELER_text_eyettention_readerID_{}_emb{}_newloss_fold{}.pth".format(
|
| 276 |
+
args.save_data_folder, args.atten_type, args.emb_size, fold_indx
|
| 277 |
+
),
|
| 278 |
+
)
|
| 279 |
+
old_score = np.mean(val_loss)
|
| 280 |
+
print("\nsaved model state dict\n")
|
| 281 |
+
save_ep_couter = episode_i
|
| 282 |
+
else:
|
| 283 |
+
# early stopping
|
| 284 |
+
if episode_i - save_ep_couter >= cf["earlystop_patience"]:
|
| 285 |
+
break
|
| 286 |
+
|
| 287 |
+
# evaluation
|
| 288 |
+
dnn.eval()
|
| 289 |
+
res_llh = []
|
| 290 |
+
dnn.load_state_dict(
|
| 291 |
+
torch.load(
|
| 292 |
+
os.path.join(
|
| 293 |
+
args.save_data_folder,
|
| 294 |
+
f"CELoss_CELER_text_eyettention_readerID_{args.atten_type}_emb{args.emb_size}_newloss_fold{fold_indx}.pth",
|
| 295 |
+
),
|
| 296 |
+
map_location="cpu",
|
| 297 |
+
)
|
| 298 |
+
)
|
| 299 |
+
dnn.to(device)
|
| 300 |
+
batch_indx = 0
|
| 301 |
+
for batchh in test_dataloaderr:
|
| 302 |
+
with torch.no_grad():
|
| 303 |
+
sn_input_ids_test = batchh["sn_input_ids"].to(device)
|
| 304 |
+
sn_attention_mask_test = batchh["sn_attention_mask"].to(device)
|
| 305 |
+
word_ids_sn_test = batchh["word_ids_sn"].to(device)
|
| 306 |
+
sn_word_len_test = batchh["sn_word_len"].to(device)
|
| 307 |
+
|
| 308 |
+
sp_input_ids_test = batchh["sp_input_ids"].to(device)
|
| 309 |
+
sp_attention_mask_test = batchh["sp_attention_mask"].to(device)
|
| 310 |
+
word_ids_sp_test = batchh["word_ids_sp"].to(device)
|
| 311 |
+
|
| 312 |
+
sp_pos_test = batchh["sp_pos"].to(device)
|
| 313 |
+
sp_landing_pos_test = batchh["sp_landing_pos"].to(device)
|
| 314 |
+
sp_fix_dur_test = (batchh["sp_fix_dur"] / 1000).to(device)
|
| 315 |
+
sub_id_test = batchh["sub_id"].to(device)
|
| 316 |
+
|
| 317 |
+
# normalize gaze features
|
| 318 |
+
mask = ~torch.eq(sp_fix_dur_test, 0)
|
| 319 |
+
sp_fix_dur_test = (sp_fix_dur_test - fix_dur_mean) / fix_dur_std * mask
|
| 320 |
+
sp_landing_pos_test = (
|
| 321 |
+
(sp_landing_pos_test - landing_pos_mean) / landing_pos_std * mask
|
| 322 |
+
)
|
| 323 |
+
sp_fix_dur_test = torch.nan_to_num(sp_fix_dur_test)
|
| 324 |
+
sp_landing_pos_test = torch.nan_to_num(sp_landing_pos_test)
|
| 325 |
+
sn_word_len_test = (sn_word_len_test - sn_word_len_mean) / sn_word_len_std
|
| 326 |
+
sn_word_len_test = torch.nan_to_num(sn_word_len_test)
|
| 327 |
+
|
| 328 |
+
dnn_out_test, atten_weights_test = dnn(
|
| 329 |
+
sn_emd=sn_input_ids_test,
|
| 330 |
+
sn_mask=sn_attention_mask_test,
|
| 331 |
+
sp_emd=sp_input_ids_test,
|
| 332 |
+
sp_pos=sp_pos_test,
|
| 333 |
+
word_ids_sn=word_ids_sn_test,
|
| 334 |
+
word_ids_sp=word_ids_sp_test,
|
| 335 |
+
sp_fix_dur=sp_fix_dur_test,
|
| 336 |
+
sp_landing_pos=sp_landing_pos_test,
|
| 337 |
+
sn_word_len=sn_word_len_test,
|
| 338 |
+
sub_id=sub_id_test,
|
| 339 |
+
)
|
| 340 |
+
|
| 341 |
+
# We do not use nn.CrossEntropyLoss here to calculate the likelihood because it combines nn.LogSoftmax and nn.NLL,
|
| 342 |
+
# while nn.LogSoftmax returns a log value based on e, we want 2 instead
|
| 343 |
+
# m = nn.LogSoftmax(dim=2) -- base e, we want base 2
|
| 344 |
+
m = nn.Softmax(dim=2)
|
| 345 |
+
dnn_out_test = m(dnn_out_test).detach().to("cpu").numpy()
|
| 346 |
+
|
| 347 |
+
# prepare label and mask
|
| 348 |
+
pad_mask_test, label_test = load_label(sp_pos_test, cf, le, "cpu")
|
| 349 |
+
pred = dnn_out_test.argmax(axis=2)
|
| 350 |
+
# compute log likelihood for the batch samples
|
| 351 |
+
res_batch = eval_log_llh(dnn_out_test, label_test, pad_mask_test)
|
| 352 |
+
res_llh.append(np.array(res_batch))
|
| 353 |
+
|
| 354 |
+
batch_indx += 1
|
| 355 |
+
|
| 356 |
+
res_llh = np.concatenate(res_llh).ravel()
|
| 357 |
+
loss_dict["test_ll"].append(res_llh)
|
| 358 |
+
loss_dict["fix_dur_mean"] = fix_dur_mean
|
| 359 |
+
loss_dict["fix_dur_std"] = fix_dur_std
|
| 360 |
+
loss_dict["landing_pos_mean"] = landing_pos_mean
|
| 361 |
+
loss_dict["landing_pos_std"] = landing_pos_std
|
| 362 |
+
loss_dict["sn_word_len_mean"] = sn_word_len_mean
|
| 363 |
+
loss_dict["sn_word_len_std"] = sn_word_len_std
|
| 364 |
+
print("\nTest likelihood is {} \n".format(np.mean(res_llh)))
|
| 365 |
+
# save results
|
| 366 |
+
with open(
|
| 367 |
+
"{}/res_CELER_text_eyettention_readerID_{}_emb{}_Fold{}.pickle".format(
|
| 368 |
+
args.save_data_folder, args.atten_type, args.emb_size, fold_indx
|
| 369 |
+
),
|
| 370 |
+
"wb",
|
| 371 |
+
) as handle:
|
| 372 |
+
pickle.dump(loss_dict, handle, protocol=pickle.HIGHEST_PROTOCOL)
|
| 373 |
+
|
| 374 |
+
fold_indx += 1
|
model.py
ADDED
|
@@ -0,0 +1,702 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import torch.nn as nn
|
| 3 |
+
from torch.autograd import Variable
|
| 4 |
+
import torch.nn.functional as F
|
| 5 |
+
import numpy as np
|
| 6 |
+
from torch.nn.functional import cross_entropy, softmax
|
| 7 |
+
from transformers import BertModel, BertConfig
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class Eyettention(nn.Module):
|
| 11 |
+
def __init__(self, cf):
|
| 12 |
+
super(Eyettention, self).__init__()
|
| 13 |
+
self.cf = cf
|
| 14 |
+
self.window_width = 1 # D
|
| 15 |
+
self.atten_type = cf["atten_type"]
|
| 16 |
+
self.hidden_size = 128
|
| 17 |
+
|
| 18 |
+
# Word-Sequence Encoder
|
| 19 |
+
encoder_config = BertConfig.from_pretrained(self.cf["model_pretrained"])
|
| 20 |
+
encoder_config.output_hidden_states = True
|
| 21 |
+
# initiate Bert with pre-trained weights
|
| 22 |
+
print("keeping Bert with pre-trained weights")
|
| 23 |
+
self.encoder = BertModel.from_pretrained(self.cf["model_pretrained"], config=encoder_config)
|
| 24 |
+
self.encoder.eval()
|
| 25 |
+
# freeze the parameters in Bert model
|
| 26 |
+
for param in self.encoder.parameters():
|
| 27 |
+
param.requires_grad = False
|
| 28 |
+
|
| 29 |
+
self.embedding_dropout = nn.Dropout(0.4)
|
| 30 |
+
self.encoder_lstm = nn.LSTM(
|
| 31 |
+
input_size=768, # BERT embedding size
|
| 32 |
+
hidden_size=int(self.hidden_size / 2),
|
| 33 |
+
num_layers=8,
|
| 34 |
+
batch_first=True,
|
| 35 |
+
bidirectional=True,
|
| 36 |
+
dropout=0.2,
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
# Fixation-Sequence Encoder
|
| 40 |
+
self.position_embeddings = nn.Embedding(
|
| 41 |
+
encoder_config.max_position_embeddings, encoder_config.hidden_size
|
| 42 |
+
)
|
| 43 |
+
self.LayerNorm = nn.LayerNorm(encoder_config.hidden_size, eps=encoder_config.layer_norm_eps)
|
| 44 |
+
|
| 45 |
+
# The scanpath is generated in an autoregressive manner, the output of the previous timestep is fed to the input of the next time step.
|
| 46 |
+
# So we use decoder cells and loop over all timesteps.
|
| 47 |
+
# initialize eight decoder cells
|
| 48 |
+
self.decoder_cell1 = nn.LSTMCell(
|
| 49 |
+
768 + 2, self.hidden_size
|
| 50 |
+
) # first layer input size = #BERT embedding size + two fixation attributes:landing position and fixiation duration
|
| 51 |
+
self.decoder_cell2 = nn.LSTMCell(self.hidden_size, self.hidden_size)
|
| 52 |
+
self.decoder_cell3 = nn.LSTMCell(self.hidden_size, self.hidden_size)
|
| 53 |
+
self.decoder_cell4 = nn.LSTMCell(self.hidden_size, self.hidden_size)
|
| 54 |
+
self.decoder_cell5 = nn.LSTMCell(self.hidden_size, self.hidden_size)
|
| 55 |
+
self.decoder_cell6 = nn.LSTMCell(self.hidden_size, self.hidden_size)
|
| 56 |
+
self.decoder_cell7 = nn.LSTMCell(self.hidden_size, self.hidden_size)
|
| 57 |
+
self.decoder_cell8 = nn.LSTMCell(self.hidden_size, self.hidden_size)
|
| 58 |
+
self.dropout_LSTM = nn.Dropout(0.2)
|
| 59 |
+
|
| 60 |
+
# Cross-Attention
|
| 61 |
+
self.attn = nn.Linear(
|
| 62 |
+
self.hidden_size, self.hidden_size + 1
|
| 63 |
+
) # +1 acount for the word length feature
|
| 64 |
+
|
| 65 |
+
# Decoder
|
| 66 |
+
# initialize five dense layers
|
| 67 |
+
self.dropout_dense = nn.Dropout(0.2)
|
| 68 |
+
self.decoder_dense = nn.Sequential(
|
| 69 |
+
self.dropout_dense,
|
| 70 |
+
nn.Linear(self.hidden_size * 2 + 1, 512),
|
| 71 |
+
nn.ReLU(),
|
| 72 |
+
self.dropout_dense,
|
| 73 |
+
nn.Linear(512, 256),
|
| 74 |
+
nn.ReLU(),
|
| 75 |
+
self.dropout_dense,
|
| 76 |
+
nn.Linear(256, 256),
|
| 77 |
+
nn.ReLU(),
|
| 78 |
+
self.dropout_dense,
|
| 79 |
+
nn.Linear(256, 256),
|
| 80 |
+
nn.ReLU(),
|
| 81 |
+
nn.Linear(256, self.cf["max_sn_len"] * 2 - 3), # number of output classes
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
# for scanpath generation
|
| 85 |
+
self.softmax = nn.Softmax(dim=1)
|
| 86 |
+
|
| 87 |
+
def pool_subword_to_word(self, subword_emb, word_ids_sn, target, pool_method="sum"):
|
| 88 |
+
# batching computing
|
| 89 |
+
# Pool bert token (subword) to word level
|
| 90 |
+
if target == "sn":
|
| 91 |
+
max_len = self.cf["max_sn_len"] # CLS and SEP included
|
| 92 |
+
elif target == "sp":
|
| 93 |
+
max_len = self.cf["max_sp_len"] - 1 # do not account the 'SEP' token
|
| 94 |
+
|
| 95 |
+
merged_word_emb = torch.empty(subword_emb.shape[0], 0, 768).to(subword_emb.device)
|
| 96 |
+
for word_idx in range(max_len):
|
| 97 |
+
word_mask = (word_ids_sn == word_idx).unsqueeze(2).repeat(1, 1, 768)
|
| 98 |
+
# pooling method -> sum
|
| 99 |
+
if pool_method == "sum":
|
| 100 |
+
pooled_word_emb = torch.sum(subword_emb * word_mask, 1).unsqueeze(
|
| 101 |
+
1
|
| 102 |
+
) # [batch, 1, 768]
|
| 103 |
+
elif pool_method == "mean":
|
| 104 |
+
pooled_word_emb = torch.mean(subword_emb * word_mask, 1).unsqueeze(
|
| 105 |
+
1
|
| 106 |
+
) # [batch, 1, 768]
|
| 107 |
+
merged_word_emb = torch.cat([merged_word_emb, pooled_word_emb], dim=1)
|
| 108 |
+
|
| 109 |
+
mask_word = torch.sum(merged_word_emb, 2).bool()
|
| 110 |
+
return merged_word_emb, mask_word
|
| 111 |
+
|
| 112 |
+
def encode(self, sn_emd, sn_mask, word_ids_sn, sn_word_len):
|
| 113 |
+
# Word-Sequence Encoder
|
| 114 |
+
outputs = self.encoder(input_ids=sn_emd, attention_mask=sn_mask)
|
| 115 |
+
hidden_rep_orig, pooled_rep = outputs[0], outputs[1]
|
| 116 |
+
if word_ids_sn != None:
|
| 117 |
+
# Pool bert subword to word level for english corpus
|
| 118 |
+
merged_word_emb, sn_mask_word = self.pool_subword_to_word(
|
| 119 |
+
hidden_rep_orig, word_ids_sn, target="sn", pool_method="sum"
|
| 120 |
+
)
|
| 121 |
+
else: # no pooling for Chinese corpus
|
| 122 |
+
merged_word_emb, sn_mask_word = hidden_rep_orig, None
|
| 123 |
+
|
| 124 |
+
hidden_rep = self.embedding_dropout(merged_word_emb)
|
| 125 |
+
x, (hn, hc) = self.encoder_lstm(hidden_rep, None)
|
| 126 |
+
|
| 127 |
+
# concatenate with the word length feature
|
| 128 |
+
x = torch.cat((x, sn_word_len[:, :, None]), dim=2)
|
| 129 |
+
return x, sn_mask_word
|
| 130 |
+
|
| 131 |
+
def cross_attention(self, ht, hs, sn_mask, cur_word_index):
|
| 132 |
+
# General Attention:
|
| 133 |
+
# score(ht,hs) = (ht^T)(Wa)hs
|
| 134 |
+
# hs is the output from word-Sequence Encoder
|
| 135 |
+
# ht is the previous hidden state from Fixation-Sequence Encoder
|
| 136 |
+
# self.attn(o): [batch, step, units]
|
| 137 |
+
attn_prod = torch.matmul(
|
| 138 |
+
self.attn(ht.unsqueeze(1)), hs.permute(0, 2, 1)
|
| 139 |
+
) # [batch, 1, step]
|
| 140 |
+
if self.atten_type == "global": # global attention
|
| 141 |
+
attn_prod += (~sn_mask).unsqueeze(1) * -1e9
|
| 142 |
+
att_weight = softmax(attn_prod, dim=2) # [batch, 1, step]
|
| 143 |
+
|
| 144 |
+
else: # local attention
|
| 145 |
+
# current fixated word index
|
| 146 |
+
aligned_position = cur_word_index
|
| 147 |
+
# Get window borders
|
| 148 |
+
left = torch.where(
|
| 149 |
+
aligned_position - self.window_width >= 0, aligned_position - self.window_width, 0
|
| 150 |
+
)
|
| 151 |
+
right = torch.where(
|
| 152 |
+
aligned_position + self.window_width <= self.cf["max_sn_len"] - 1,
|
| 153 |
+
aligned_position + self.window_width,
|
| 154 |
+
self.cf["max_sn_len"] - 1,
|
| 155 |
+
)
|
| 156 |
+
|
| 157 |
+
# exclude padding tokens
|
| 158 |
+
# only consider words in the window
|
| 159 |
+
sen_seq = (
|
| 160 |
+
torch.arange(self.cf["max_sn_len"])[None, :]
|
| 161 |
+
.expand(sn_mask.shape[0], self.cf["max_sn_len"])
|
| 162 |
+
.to(sn_mask.device)
|
| 163 |
+
)
|
| 164 |
+
outside_win_mask = (sen_seq < left.unsqueeze(1)) + (sen_seq > right.unsqueeze(1))
|
| 165 |
+
|
| 166 |
+
attn_prod += (~sn_mask + outside_win_mask).unsqueeze(1) * -1e9
|
| 167 |
+
att_weight = softmax(attn_prod, dim=2) # [batch, 1, step]
|
| 168 |
+
|
| 169 |
+
if self.atten_type == "local-g": # local attention with Gaussian Kernel
|
| 170 |
+
gauss = lambda s: torch.exp(
|
| 171 |
+
-torch.square(s - aligned_position.unsqueeze(1))
|
| 172 |
+
/ (2 * torch.square(torch.tensor(self.window_width / 2)))
|
| 173 |
+
)
|
| 174 |
+
gauss_factor = gauss(sen_seq)
|
| 175 |
+
att_weight = att_weight * gauss_factor.unsqueeze(1)
|
| 176 |
+
|
| 177 |
+
return att_weight
|
| 178 |
+
|
| 179 |
+
def decode(self, sp_emd, sn_mask, sp_pos, enc_out, sp_fix_dur, sp_landing_pos, word_ids_sp):
|
| 180 |
+
# Fixation-Sequence Encoder + Decoder
|
| 181 |
+
# Initialize hidden state and cell state with zeros,
|
| 182 |
+
hn = torch.zeros(8, sp_emd.shape[0], self.hidden_size).to(sp_emd.device)
|
| 183 |
+
hc = torch.zeros(8, sp_emd.shape[0], self.hidden_size).to(sp_emd.device)
|
| 184 |
+
hx, cx = hn[0, :, :], hc[0, :, :]
|
| 185 |
+
hx2, cx2 = hn[1, :, :], hc[1, :, :]
|
| 186 |
+
hx3, cx3 = hn[2, :, :], hc[2, :, :]
|
| 187 |
+
hx4, cx4 = hn[3, :, :], hc[3, :, :]
|
| 188 |
+
hx5, cx5 = hn[4, :, :], hc[4, :, :]
|
| 189 |
+
hx6, cx6 = hn[5, :, :], hc[5, :, :]
|
| 190 |
+
hx7, cx7 = hn[6, :, :], hc[6, :, :]
|
| 191 |
+
hx8, cx8 = hn[7, :, :], hc[7, :, :]
|
| 192 |
+
|
| 193 |
+
dec_emb_in = self.encoder.embeddings.word_embeddings(sp_emd[:, :-1])
|
| 194 |
+
if word_ids_sp is not None:
|
| 195 |
+
# Pool bert subword to word level for English corpus
|
| 196 |
+
sp_merged_word_emd, sp_mask_word = self.pool_subword_to_word(
|
| 197 |
+
dec_emb_in, word_ids_sp[:, :-1], target="sp", pool_method="sum"
|
| 198 |
+
)
|
| 199 |
+
else: # no pooling for Chinese corpus
|
| 200 |
+
sp_merged_word_emd, sp_mask_word = dec_emb_in, None
|
| 201 |
+
|
| 202 |
+
# add positional embeddings
|
| 203 |
+
position_embeddings = self.position_embeddings(sp_pos[:, :-1])
|
| 204 |
+
dec_emb_in = sp_merged_word_emd + position_embeddings
|
| 205 |
+
dec_emb_in = self.LayerNorm(dec_emb_in)
|
| 206 |
+
dec_emb_in = dec_emb_in.permute(1, 0, 2) # [step, n, emb_dim]
|
| 207 |
+
dec_emb_in = self.embedding_dropout(dec_emb_in)
|
| 208 |
+
|
| 209 |
+
# concatenate two additional gaze features
|
| 210 |
+
if sp_landing_pos is not None:
|
| 211 |
+
dec_emb_in = torch.cat((dec_emb_in, sp_landing_pos.permute(1, 0)[:-1, :, None]), dim=2)
|
| 212 |
+
|
| 213 |
+
if sp_fix_dur is not None:
|
| 214 |
+
dec_emb_in = torch.cat((dec_emb_in, sp_fix_dur.permute(1, 0)[:-1, :, None]), dim=2)
|
| 215 |
+
|
| 216 |
+
# Predict output for each time step in turn
|
| 217 |
+
output = []
|
| 218 |
+
# save attention scores for visualization
|
| 219 |
+
atten_weights_batch = torch.empty(sp_emd.shape[0], 0, self.cf["max_sn_len"]).to(
|
| 220 |
+
sp_emd.device
|
| 221 |
+
)
|
| 222 |
+
for i in range(dec_emb_in.shape[0]):
|
| 223 |
+
hx, cx = self.decoder_cell1(dec_emb_in[i], (hx, cx)) # [batch, units]
|
| 224 |
+
hx2, cx2 = self.decoder_cell2(self.dropout_LSTM(hx), (hx2, cx2))
|
| 225 |
+
hx3, cx3 = self.decoder_cell3(self.dropout_LSTM(hx2), (hx3, cx3))
|
| 226 |
+
hx4, cx4 = self.decoder_cell4(self.dropout_LSTM(hx3), (hx4, cx4))
|
| 227 |
+
hx5, cx5 = self.decoder_cell5(self.dropout_LSTM(hx4), (hx5, cx5))
|
| 228 |
+
hx6, cx6 = self.decoder_cell6(self.dropout_LSTM(hx5), (hx6, cx6))
|
| 229 |
+
hx7, cx7 = self.decoder_cell7(self.dropout_LSTM(hx6), (hx7, cx7))
|
| 230 |
+
hx8, cx8 = self.decoder_cell8(self.dropout_LSTM(hx7), (hx8, cx8))
|
| 231 |
+
|
| 232 |
+
att_weight = self.cross_attention(
|
| 233 |
+
ht=hx8, hs=enc_out, sn_mask=sn_mask, cur_word_index=sp_pos[:, i]
|
| 234 |
+
)
|
| 235 |
+
atten_weights_batch = torch.cat([atten_weights_batch, att_weight], dim=1)
|
| 236 |
+
|
| 237 |
+
context = torch.matmul(att_weight, enc_out) # [batch, 1, units]
|
| 238 |
+
|
| 239 |
+
# Decoder
|
| 240 |
+
hc = torch.cat([context.squeeze(1), hx8], dim=1) # [batch, units *2]
|
| 241 |
+
result = self.decoder_dense(hc) # [batch, dec_o_dim]
|
| 242 |
+
output.append(result)
|
| 243 |
+
|
| 244 |
+
output = torch.stack(output, dim=0) # [step, batch, dec_o_dim]
|
| 245 |
+
# output = F.softmax(output, dim=2) # cross entropy in pytorch includes softmax
|
| 246 |
+
return output.permute(1, 0, 2), atten_weights_batch # [batch, step, dec_o_dim]
|
| 247 |
+
|
| 248 |
+
def forward(
|
| 249 |
+
self,
|
| 250 |
+
sn_emd,
|
| 251 |
+
sn_mask,
|
| 252 |
+
sp_emd,
|
| 253 |
+
sp_pos,
|
| 254 |
+
word_ids_sn,
|
| 255 |
+
word_ids_sp,
|
| 256 |
+
sp_fix_dur,
|
| 257 |
+
sp_landing_pos,
|
| 258 |
+
sn_word_len,
|
| 259 |
+
):
|
| 260 |
+
x, sn_mask_word = self.encode(
|
| 261 |
+
sn_emd, sn_mask, word_ids_sn, sn_word_len
|
| 262 |
+
) # [batch, step, units], [batch, units]
|
| 263 |
+
|
| 264 |
+
if sn_mask_word is None: # for Chinese dataset without token pooling
|
| 265 |
+
sn_mask = torch.Tensor.bool(sn_mask)
|
| 266 |
+
pred, atten_weights = self.decode(
|
| 267 |
+
sp_emd, sn_mask, sp_pos, x, sp_fix_dur, sp_landing_pos, word_ids_sp
|
| 268 |
+
) # [batch, step, dec_o_dim]
|
| 269 |
+
|
| 270 |
+
else: # for English dataset with token pooling
|
| 271 |
+
pred, atten_weights = self.decode(
|
| 272 |
+
sp_emd, sn_mask_word, sp_pos, x, sp_fix_dur, sp_landing_pos, word_ids_sp
|
| 273 |
+
) # [batch, step, dec_o_dim]
|
| 274 |
+
|
| 275 |
+
return pred, atten_weights
|
| 276 |
+
|
| 277 |
+
def scanpath_generation(
|
| 278 |
+
self, sn_emd, sn_mask, word_ids_sn, sn_word_len, le, max_pred_len=60, previous_scanpath=None
|
| 279 |
+
):
|
| 280 |
+
|
| 281 |
+
if max_pred_len <= 0:
|
| 282 |
+
raise ValueError("max_pred_len must be positive.")
|
| 283 |
+
|
| 284 |
+
prev_scanpath_len = 0
|
| 285 |
+
|
| 286 |
+
if previous_scanpath is not None:
|
| 287 |
+
if (
|
| 288 |
+
len(previous_scanpath) == 0 or previous_scanpath[0] != 0
|
| 289 |
+
): # prepend with 0 if no 0 at the start of previous scanpaths
|
| 290 |
+
previous_scanpath = [0] + previous_scanpath
|
| 291 |
+
|
| 292 |
+
prev_scanpath_len = len(previous_scanpath)
|
| 293 |
+
|
| 294 |
+
previous_scanpath = torch.as_tensor(
|
| 295 |
+
previous_scanpath, dtype=torch.long, device=sn_emd.device
|
| 296 |
+
)
|
| 297 |
+
if previous_scanpath.ndim == 1:
|
| 298 |
+
previous_scanpath = previous_scanpath.unsqueeze(0)
|
| 299 |
+
# if input includes CLS: [0, 1, 2, ...]
|
| 300 |
+
previous_scanpath = previous_scanpath[:, 1:]
|
| 301 |
+
|
| 302 |
+
# compute the scan path generated from the model when the first CLS taken is given
|
| 303 |
+
enc_out, sn_mask_word = self.encode(sn_emd, sn_mask, word_ids_sn, sn_word_len)
|
| 304 |
+
if sn_mask_word is None:
|
| 305 |
+
sn_mask = torch.Tensor.bool(sn_mask)
|
| 306 |
+
else:
|
| 307 |
+
sn_mask = sn_mask_word
|
| 308 |
+
sn_len = torch.sum(sn_mask, axis=1) - 2
|
| 309 |
+
|
| 310 |
+
# decode
|
| 311 |
+
# Initialize hidden state and cell state with zeros,
|
| 312 |
+
hn = torch.zeros(8, sn_emd.shape[0], self.hidden_size).to(sn_emd.device)
|
| 313 |
+
hc = torch.zeros(8, sn_emd.shape[0], self.hidden_size).to(sn_emd.device)
|
| 314 |
+
hx, cx = hn[0, :, :], hc[0, :, :]
|
| 315 |
+
hx2, cx2 = hn[1, :, :], hc[1, :, :]
|
| 316 |
+
hx3, cx3 = hn[2, :, :], hc[2, :, :]
|
| 317 |
+
hx4, cx4 = hn[3, :, :], hc[3, :, :]
|
| 318 |
+
hx5, cx5 = hn[4, :, :], hc[4, :, :]
|
| 319 |
+
hx6, cx6 = hn[5, :, :], hc[5, :, :]
|
| 320 |
+
hx7, cx7 = hn[6, :, :], hc[6, :, :]
|
| 321 |
+
hx8, cx8 = hn[7, :, :], hc[7, :, :]
|
| 322 |
+
|
| 323 |
+
# use CLS token (101) as start token
|
| 324 |
+
dec_in_start = (torch.ones(sn_mask.shape[0]) * 101).long().to(sn_mask.device)
|
| 325 |
+
dec_emb_in = self.encoder.embeddings.word_embeddings(dec_in_start) # [batch, emb_dim]
|
| 326 |
+
# dec_in_start = sp_emd[:, 0]
|
| 327 |
+
# dec_emb_in = self.encoder.embeddings.word_embeddings(dec_in_start) # [batch, emb_dim]
|
| 328 |
+
|
| 329 |
+
# add positional embeddings
|
| 330 |
+
start_pos = torch.zeros(sn_mask.shape[0]).to(sn_mask.device)
|
| 331 |
+
position_embeddings = self.position_embeddings(start_pos.long())
|
| 332 |
+
dec_emb_in = dec_emb_in + position_embeddings
|
| 333 |
+
dec_emb_in = self.LayerNorm(dec_emb_in)
|
| 334 |
+
|
| 335 |
+
# concatenate two additional gaze features, which are set to zeros for CLS token
|
| 336 |
+
dec_in = torch.cat(
|
| 337 |
+
(dec_emb_in, torch.zeros(dec_emb_in.shape[0], 2).to(sn_emd.device)), dim=1
|
| 338 |
+
)
|
| 339 |
+
|
| 340 |
+
# generate fixation one by one in an autoregressive way
|
| 341 |
+
output = []
|
| 342 |
+
density_prediction = []
|
| 343 |
+
pred_counter = 0
|
| 344 |
+
# output.append(sp_pos[:, pred_counter])
|
| 345 |
+
output.append(start_pos.long())
|
| 346 |
+
for p in range(prev_scanpath_len + max_pred_len - 1):
|
| 347 |
+
hx, cx = self.decoder_cell1(dec_in, (hx, cx)) # [batch, units]
|
| 348 |
+
hx2, cx2 = self.decoder_cell2(self.dropout_LSTM(hx), (hx2, cx2))
|
| 349 |
+
hx3, cx3 = self.decoder_cell3(self.dropout_LSTM(hx2), (hx3, cx3))
|
| 350 |
+
hx4, cx4 = self.decoder_cell4(self.dropout_LSTM(hx3), (hx4, cx4))
|
| 351 |
+
hx5, cx5 = self.decoder_cell5(self.dropout_LSTM(hx4), (hx5, cx5))
|
| 352 |
+
hx6, cx6 = self.decoder_cell6(self.dropout_LSTM(hx5), (hx6, cx6))
|
| 353 |
+
hx7, cx7 = self.decoder_cell7(self.dropout_LSTM(hx6), (hx7, cx7))
|
| 354 |
+
hx8, cx8 = self.decoder_cell8(self.dropout_LSTM(hx7), (hx8, cx8))
|
| 355 |
+
|
| 356 |
+
att_weight = self.cross_attention(
|
| 357 |
+
ht=hx8, hs=enc_out, sn_mask=sn_mask, cur_word_index=output[-1]
|
| 358 |
+
)
|
| 359 |
+
|
| 360 |
+
context = torch.matmul(att_weight, enc_out) # [batch, 1, units]
|
| 361 |
+
hc = torch.cat([context.squeeze(1), hx8], dim=1) # [batch, units *2]
|
| 362 |
+
|
| 363 |
+
result = self.decoder_dense(hc) # [batch, dec_o_dim]
|
| 364 |
+
result = self.softmax(result) # [batch, dec_o_dim]
|
| 365 |
+
density_prediction.append(result)
|
| 366 |
+
|
| 367 |
+
# we can either take argmax or sampling from the output distribution,
|
| 368 |
+
# we do sampling in the paper
|
| 369 |
+
# pred_indx = result.argmax(dim=1)
|
| 370 |
+
# sampling next fixation location according to the distribution
|
| 371 |
+
if previous_scanpath is not None and p < previous_scanpath.shape[1]:
|
| 372 |
+
# replay observed fixation
|
| 373 |
+
pred_pos = previous_scanpath[:, p].clone()
|
| 374 |
+
else:
|
| 375 |
+
pred_indx = torch.multinomial(result, 1)
|
| 376 |
+
pred_class = [le.classes_[pred_indx[i]] for i in torch.arange(result.shape[0])]
|
| 377 |
+
pred_class = torch.from_numpy(np.array(pred_class)).to(sn_emd.device)
|
| 378 |
+
# predict fixation word index = last fixation word index + predicted saccade range
|
| 379 |
+
pred_pos = output[-1] + pred_class
|
| 380 |
+
|
| 381 |
+
# larger than sentence max length -- set to sentence length+1, i.e. token <'SEP'>
|
| 382 |
+
# prepare the input to the next timstep
|
| 383 |
+
input_ids = []
|
| 384 |
+
for i in range(pred_pos.shape[0]):
|
| 385 |
+
if pred_pos[i] > sn_len[i]:
|
| 386 |
+
pred_pos[i] = sn_len[i] + 1
|
| 387 |
+
elif pred_pos[i] < 1:
|
| 388 |
+
pred_pos[i] = 1
|
| 389 |
+
|
| 390 |
+
if word_ids_sn is not None:
|
| 391 |
+
input_ids.append(sn_emd[i, word_ids_sn[i, :] == pred_pos[i]])
|
| 392 |
+
else:
|
| 393 |
+
input_ids.append(sn_emd[i, pred_pos[i]])
|
| 394 |
+
output.append(pred_pos)
|
| 395 |
+
|
| 396 |
+
# prepare next timestamp input token
|
| 397 |
+
pred_counter += 1
|
| 398 |
+
if word_ids_sn is not None:
|
| 399 |
+
# merge tokens
|
| 400 |
+
dec_emb_in = torch.empty(0, 768).to(sn_emd.device)
|
| 401 |
+
for id in input_ids:
|
| 402 |
+
dec_emb_in = torch.cat(
|
| 403 |
+
[
|
| 404 |
+
dec_emb_in,
|
| 405 |
+
torch.sum(self.encoder.embeddings.word_embeddings(id), axis=0)[None, :],
|
| 406 |
+
],
|
| 407 |
+
dim=0,
|
| 408 |
+
)
|
| 409 |
+
|
| 410 |
+
else:
|
| 411 |
+
input_ids = torch.stack(input_ids)
|
| 412 |
+
dec_emb_in = self.encoder.embeddings.word_embeddings(input_ids) # [batch, emb_dim]
|
| 413 |
+
# add positional embeddings
|
| 414 |
+
position_embeddings = self.position_embeddings(output[-1])
|
| 415 |
+
dec_emb_in = dec_emb_in + position_embeddings
|
| 416 |
+
dec_emb_in = self.LayerNorm(dec_emb_in)
|
| 417 |
+
# concatenate two additional gaze features
|
| 418 |
+
dec_in = torch.cat(
|
| 419 |
+
(dec_emb_in, torch.zeros(dec_emb_in.shape[0], 2).to(sn_emd.device)), dim=1
|
| 420 |
+
)
|
| 421 |
+
|
| 422 |
+
output = torch.stack(output, dim=0) # [step, batch]
|
| 423 |
+
return output.permute(1, 0), density_prediction # [batch, step]
|
| 424 |
+
|
| 425 |
+
|
| 426 |
+
class Eyettention_readerID(nn.Module):
|
| 427 |
+
def __init__(self, cf):
|
| 428 |
+
super(Eyettention_readerID, self).__init__()
|
| 429 |
+
self.cf = cf
|
| 430 |
+
self.window_width = 1 # D
|
| 431 |
+
self.atten_type = cf["atten_type"]
|
| 432 |
+
self.hidden_size = 128
|
| 433 |
+
self.sub_emb_size = cf["subid_emb_size"]
|
| 434 |
+
|
| 435 |
+
# Word-Sequence Encoder
|
| 436 |
+
encoder_config = BertConfig.from_pretrained(self.cf["model_pretrained"])
|
| 437 |
+
encoder_config.output_hidden_states = True
|
| 438 |
+
# initiate Bert with pre-trained weights
|
| 439 |
+
print("keeping Bert with pre-trained weights")
|
| 440 |
+
self.encoder = BertModel.from_pretrained(self.cf["model_pretrained"], config=encoder_config)
|
| 441 |
+
self.encoder.eval()
|
| 442 |
+
# freeze the parameters in Bert model
|
| 443 |
+
for param in self.encoder.parameters():
|
| 444 |
+
param.requires_grad = False
|
| 445 |
+
|
| 446 |
+
self.embedding_dropout = nn.Dropout(0.4)
|
| 447 |
+
self.encoder_lstm = nn.LSTM(
|
| 448 |
+
input_size=768, # BERT embedding size
|
| 449 |
+
hidden_size=int(self.hidden_size / 2),
|
| 450 |
+
num_layers=8,
|
| 451 |
+
batch_first=True,
|
| 452 |
+
bidirectional=True,
|
| 453 |
+
dropout=0.2,
|
| 454 |
+
)
|
| 455 |
+
|
| 456 |
+
# Fixation-Sequence Encoder
|
| 457 |
+
self.position_embeddings = nn.Embedding(
|
| 458 |
+
encoder_config.max_position_embeddings, encoder_config.hidden_size
|
| 459 |
+
)
|
| 460 |
+
self.LayerNorm = nn.LayerNorm(encoder_config.hidden_size, eps=encoder_config.layer_norm_eps)
|
| 461 |
+
# for reader-specific setting
|
| 462 |
+
self.sub_embeddings = nn.Embedding(400, self.sub_emb_size)
|
| 463 |
+
|
| 464 |
+
# The scanpath is generated in an autoregressive manner, the output of the previous timestep is fed to the input of the next time step.
|
| 465 |
+
# So we use decoder cells and loop over all timesteps.
|
| 466 |
+
# initialize eight decoder cells
|
| 467 |
+
self.decoder_cell1 = nn.LSTMCell(
|
| 468 |
+
768 + 2 + self.sub_emb_size, self.hidden_size
|
| 469 |
+
) # first layer input size = #BERT embedding size + two fixation attributes:landing position and fixiation duration
|
| 470 |
+
self.decoder_cell2 = nn.LSTMCell(self.hidden_size, self.hidden_size)
|
| 471 |
+
self.decoder_cell3 = nn.LSTMCell(self.hidden_size, self.hidden_size)
|
| 472 |
+
self.decoder_cell4 = nn.LSTMCell(self.hidden_size, self.hidden_size)
|
| 473 |
+
self.decoder_cell5 = nn.LSTMCell(self.hidden_size, self.hidden_size)
|
| 474 |
+
self.decoder_cell6 = nn.LSTMCell(self.hidden_size, self.hidden_size)
|
| 475 |
+
self.decoder_cell7 = nn.LSTMCell(self.hidden_size, self.hidden_size)
|
| 476 |
+
self.decoder_cell8 = nn.LSTMCell(self.hidden_size, self.hidden_size)
|
| 477 |
+
self.dropout_LSTM = nn.Dropout(0.2)
|
| 478 |
+
|
| 479 |
+
# Cross-Attention
|
| 480 |
+
self.attn = nn.Linear(
|
| 481 |
+
self.hidden_size, self.hidden_size + 1
|
| 482 |
+
) # +1 acoount for the word length feature
|
| 483 |
+
|
| 484 |
+
# Decoder
|
| 485 |
+
# initialize five dense layers
|
| 486 |
+
self.dropout_dense = nn.Dropout(0.2)
|
| 487 |
+
self.decoder_dense = nn.Sequential(
|
| 488 |
+
self.dropout_dense,
|
| 489 |
+
nn.Linear(self.hidden_size * 2 + 1, 512),
|
| 490 |
+
nn.ReLU(),
|
| 491 |
+
self.dropout_dense,
|
| 492 |
+
nn.Linear(512, 256),
|
| 493 |
+
nn.ReLU(),
|
| 494 |
+
self.dropout_dense,
|
| 495 |
+
nn.Linear(256, 256),
|
| 496 |
+
nn.ReLU(),
|
| 497 |
+
self.dropout_dense,
|
| 498 |
+
nn.Linear(256, 256),
|
| 499 |
+
nn.ReLU(),
|
| 500 |
+
nn.Linear(256, self.cf["max_sn_len"] * 2 - 3), # number of output classes
|
| 501 |
+
)
|
| 502 |
+
|
| 503 |
+
# for scanpath generation
|
| 504 |
+
self.softmax = nn.Softmax(dim=1)
|
| 505 |
+
|
| 506 |
+
def pool_subword_to_word(self, subword_emb, word_ids_sn, target, pool_method="sum"):
|
| 507 |
+
# batching computing
|
| 508 |
+
# Pool bert token (subword) to word level
|
| 509 |
+
if target == "sn":
|
| 510 |
+
max_len = self.cf["max_sn_len"] # CLS and SEP included
|
| 511 |
+
elif target == "sp":
|
| 512 |
+
max_len = self.cf["max_sp_len"] - 1 # do not account the 'SEP' token
|
| 513 |
+
|
| 514 |
+
merged_word_emb = torch.empty(subword_emb.shape[0], 0, 768).to(subword_emb.device)
|
| 515 |
+
for word_idx in range(max_len):
|
| 516 |
+
word_mask = (word_ids_sn == word_idx).unsqueeze(2).repeat(1, 1, 768)
|
| 517 |
+
# pooling method -> sum
|
| 518 |
+
if pool_method == "sum":
|
| 519 |
+
pooled_word_emb = torch.sum(subword_emb * word_mask, 1).unsqueeze(
|
| 520 |
+
1
|
| 521 |
+
) # [batch, 1, 768]
|
| 522 |
+
elif pool_method == "mean":
|
| 523 |
+
pooled_word_emb = torch.mean(subword_emb * word_mask, 1).unsqueeze(
|
| 524 |
+
1
|
| 525 |
+
) # [batch, 1, 768]
|
| 526 |
+
merged_word_emb = torch.cat([merged_word_emb, pooled_word_emb], dim=1)
|
| 527 |
+
|
| 528 |
+
mask_word = torch.sum(merged_word_emb, 2).bool()
|
| 529 |
+
return merged_word_emb, mask_word
|
| 530 |
+
|
| 531 |
+
def encode(self, sn_emd, sn_mask, word_ids_sn, sn_word_len):
|
| 532 |
+
# Word-Sequence Encoder
|
| 533 |
+
outputs = self.encoder(input_ids=sn_emd, attention_mask=sn_mask)
|
| 534 |
+
hidden_rep_orig, pooled_rep = outputs[0], outputs[1]
|
| 535 |
+
if word_ids_sn != None:
|
| 536 |
+
# Pool bert subword to word level for english corpus
|
| 537 |
+
merged_word_emb, sn_mask_word = self.pool_subword_to_word(
|
| 538 |
+
hidden_rep_orig, word_ids_sn, target="sn", pool_method="sum"
|
| 539 |
+
)
|
| 540 |
+
else: # no pooling for Chinese corpus
|
| 541 |
+
merged_word_emb, sn_mask_word = hidden_rep_orig, None
|
| 542 |
+
|
| 543 |
+
hidden_rep = self.embedding_dropout(merged_word_emb)
|
| 544 |
+
x, (hn, hc) = self.encoder_lstm(hidden_rep, None)
|
| 545 |
+
|
| 546 |
+
# concatenate with the word length feature
|
| 547 |
+
x = torch.cat((x, sn_word_len[:, :, None]), dim=2)
|
| 548 |
+
return x, sn_mask_word
|
| 549 |
+
|
| 550 |
+
def cross_attention(self, ht, hs, sn_mask, cur_word_index):
|
| 551 |
+
# General Attention:
|
| 552 |
+
# score(ht,hs) = (ht^T)(Wa)hs
|
| 553 |
+
# hs is the output from word-Sequence Encoder
|
| 554 |
+
# ht is the previous hidden state from Fixation-Sequence Encoder
|
| 555 |
+
# self.attn(o): [batch, step, units]
|
| 556 |
+
attn_prod = torch.matmul(
|
| 557 |
+
self.attn(ht.unsqueeze(1)), hs.permute(0, 2, 1)
|
| 558 |
+
) # [batch, 1, step]
|
| 559 |
+
if self.atten_type == "global": # global attention
|
| 560 |
+
attn_prod += (~sn_mask).unsqueeze(1) * -1e9
|
| 561 |
+
att_weight = softmax(attn_prod, dim=2) # [batch, 1, step]
|
| 562 |
+
|
| 563 |
+
else: # local attention
|
| 564 |
+
# current fixated word index
|
| 565 |
+
aligned_position = cur_word_index
|
| 566 |
+
# Get window borders
|
| 567 |
+
left = torch.where(
|
| 568 |
+
aligned_position - self.window_width >= 0, aligned_position - self.window_width, 0
|
| 569 |
+
)
|
| 570 |
+
right = torch.where(
|
| 571 |
+
aligned_position + self.window_width <= self.cf["max_sn_len"] - 1,
|
| 572 |
+
aligned_position + self.window_width,
|
| 573 |
+
self.cf["max_sn_len"] - 1,
|
| 574 |
+
)
|
| 575 |
+
|
| 576 |
+
# exclude padding tokens
|
| 577 |
+
# only consider words in the window
|
| 578 |
+
sen_seq = (
|
| 579 |
+
torch.arange(self.cf["max_sn_len"])[None, :]
|
| 580 |
+
.expand(sn_mask.shape[0], self.cf["max_sn_len"])
|
| 581 |
+
.to(sn_mask.device)
|
| 582 |
+
)
|
| 583 |
+
outside_win_mask = (sen_seq < left.unsqueeze(1)) + (sen_seq > right.unsqueeze(1))
|
| 584 |
+
|
| 585 |
+
attn_prod += (~sn_mask + outside_win_mask).unsqueeze(1) * -1e9
|
| 586 |
+
att_weight = softmax(attn_prod, dim=2) # [batch, 1, step]
|
| 587 |
+
|
| 588 |
+
if self.atten_type == "local-g": # local attention with Gaussian Kernel
|
| 589 |
+
gauss = lambda s: torch.exp(
|
| 590 |
+
-torch.square(s - aligned_position.unsqueeze(1))
|
| 591 |
+
/ (2 * torch.square(torch.tensor(self.window_width / 2)))
|
| 592 |
+
)
|
| 593 |
+
gauss_factor = gauss(sen_seq)
|
| 594 |
+
att_weight = att_weight * gauss_factor.unsqueeze(1)
|
| 595 |
+
|
| 596 |
+
return att_weight
|
| 597 |
+
|
| 598 |
+
def decode(
|
| 599 |
+
self, sp_emd, sn_mask, sp_pos, enc_out, sp_fix_dur, sp_landing_pos, word_ids_sp, sub_id
|
| 600 |
+
):
|
| 601 |
+
# Fixation-Sequence Encoder + Decoder
|
| 602 |
+
# Initialize hidden state and cell state with zeros
|
| 603 |
+
hn = torch.zeros(8, sp_emd.shape[0], self.hidden_size).to(sp_emd.device)
|
| 604 |
+
hc = torch.zeros(8, sp_emd.shape[0], self.hidden_size).to(sp_emd.device)
|
| 605 |
+
hx, cx = hn[0, :, :], hc[0, :, :]
|
| 606 |
+
hx2, cx2 = hn[1, :, :], hc[1, :, :]
|
| 607 |
+
hx3, cx3 = hn[2, :, :], hc[2, :, :]
|
| 608 |
+
hx4, cx4 = hn[3, :, :], hc[3, :, :]
|
| 609 |
+
hx5, cx5 = hn[4, :, :], hc[4, :, :]
|
| 610 |
+
hx6, cx6 = hn[5, :, :], hc[5, :, :]
|
| 611 |
+
hx7, cx7 = hn[6, :, :], hc[6, :, :]
|
| 612 |
+
hx8, cx8 = hn[7, :, :], hc[7, :, :]
|
| 613 |
+
|
| 614 |
+
dec_emb_in = self.encoder.embeddings.word_embeddings(sp_emd[:, :-1])
|
| 615 |
+
if word_ids_sp is not None:
|
| 616 |
+
# Pool bert subword to word level for English corpus
|
| 617 |
+
sp_merged_word_emd, sp_mask_word = self.pool_subword_to_word(
|
| 618 |
+
dec_emb_in, word_ids_sp[:, :-1], target="sp", pool_method="sum"
|
| 619 |
+
)
|
| 620 |
+
else: # no pooling for Chinese
|
| 621 |
+
sp_merged_word_emd, sp_mask_word = dec_emb_in, None
|
| 622 |
+
|
| 623 |
+
# add positional embeddings and layer normalization
|
| 624 |
+
position_embeddings = self.position_embeddings(sp_pos[:, :-1])
|
| 625 |
+
dec_emb_in = sp_merged_word_emd + position_embeddings
|
| 626 |
+
dec_emb_in = self.LayerNorm(dec_emb_in)
|
| 627 |
+
dec_emb_in = dec_emb_in.permute(1, 0, 2) # [step, n, emb_dim]
|
| 628 |
+
dec_emb_in = self.embedding_dropout(dec_emb_in)
|
| 629 |
+
|
| 630 |
+
# concatenate two additional gaze features
|
| 631 |
+
if sp_landing_pos is not None:
|
| 632 |
+
dec_emb_in = torch.cat((dec_emb_in, sp_landing_pos.permute(1, 0)[:-1, :, None]), dim=2)
|
| 633 |
+
|
| 634 |
+
if sp_fix_dur is not None:
|
| 635 |
+
dec_emb_in = torch.cat((dec_emb_in, sp_fix_dur.permute(1, 0)[:-1, :, None]), dim=2)
|
| 636 |
+
|
| 637 |
+
# concatenate subject id for Eyettention_reader setting
|
| 638 |
+
if sub_id is not None:
|
| 639 |
+
dec_emb_in = torch.cat(
|
| 640 |
+
(dec_emb_in, self.sub_embeddings(sub_id).repeat(dec_emb_in.shape[0], 1, 1)), dim=2
|
| 641 |
+
)
|
| 642 |
+
|
| 643 |
+
# Predict output for each time step in turn
|
| 644 |
+
output = []
|
| 645 |
+
# save attention scores for visualization
|
| 646 |
+
atten_weights_batch = torch.empty(sp_emd.shape[0], 0, self.cf["max_sn_len"]).to(
|
| 647 |
+
sp_emd.device
|
| 648 |
+
)
|
| 649 |
+
for i in range(dec_emb_in.shape[0]):
|
| 650 |
+
hx, cx = self.decoder_cell1(dec_emb_in[i], (hx, cx)) # [batch, units]
|
| 651 |
+
hx2, cx2 = self.decoder_cell2(self.dropout_LSTM(hx), (hx2, cx2))
|
| 652 |
+
hx3, cx3 = self.decoder_cell3(self.dropout_LSTM(hx2), (hx3, cx3))
|
| 653 |
+
hx4, cx4 = self.decoder_cell4(self.dropout_LSTM(hx3), (hx4, cx4))
|
| 654 |
+
hx5, cx5 = self.decoder_cell5(self.dropout_LSTM(hx4), (hx5, cx5))
|
| 655 |
+
hx6, cx6 = self.decoder_cell6(self.dropout_LSTM(hx5), (hx6, cx6))
|
| 656 |
+
hx7, cx7 = self.decoder_cell7(self.dropout_LSTM(hx6), (hx7, cx7))
|
| 657 |
+
hx8, cx8 = self.decoder_cell8(self.dropout_LSTM(hx7), (hx8, cx8))
|
| 658 |
+
|
| 659 |
+
att_weight = self.cross_attention(
|
| 660 |
+
ht=hx8, hs=enc_out, sn_mask=sn_mask, cur_word_index=sp_pos[:, i]
|
| 661 |
+
)
|
| 662 |
+
atten_weights_batch = torch.cat([atten_weights_batch, att_weight], dim=1)
|
| 663 |
+
|
| 664 |
+
context = torch.matmul(att_weight, enc_out) # [batch, 1, units]
|
| 665 |
+
|
| 666 |
+
# Decoder
|
| 667 |
+
hc = torch.cat([context.squeeze(1), hx8], dim=1) # [batch, units *2]
|
| 668 |
+
result = self.decoder_dense(hc) # [batch, dec_o_dim]
|
| 669 |
+
output.append(result)
|
| 670 |
+
|
| 671 |
+
output = torch.stack(output, dim=0) # [step, batch, dec_o_dim]
|
| 672 |
+
# output = F.softmax(output, dim=2) # cross entropy in pytorch includes softmax
|
| 673 |
+
return output.permute(1, 0, 2), atten_weights_batch # [batch, step, dec_o_dim]
|
| 674 |
+
|
| 675 |
+
def forward(
|
| 676 |
+
self,
|
| 677 |
+
sn_emd,
|
| 678 |
+
sn_mask,
|
| 679 |
+
sp_emd,
|
| 680 |
+
sp_pos,
|
| 681 |
+
word_ids_sn,
|
| 682 |
+
word_ids_sp,
|
| 683 |
+
sp_fix_dur,
|
| 684 |
+
sp_landing_pos,
|
| 685 |
+
sn_word_len,
|
| 686 |
+
sub_id,
|
| 687 |
+
):
|
| 688 |
+
x, sn_mask_word = self.encode(
|
| 689 |
+
sn_emd, sn_mask, word_ids_sn, sn_word_len
|
| 690 |
+
) # [batch, step, units], [batch, units]
|
| 691 |
+
|
| 692 |
+
if sn_mask_word is None: # for Chinese dataset without token pooling
|
| 693 |
+
sn_mask = torch.Tensor.bool(sn_mask)
|
| 694 |
+
pred, atten_weights = self.decode(
|
| 695 |
+
sp_emd, sn_mask, sp_pos, x, sp_fix_dur, sp_landing_pos, word_ids_sp, sub_id
|
| 696 |
+
) # [batch, step, dec_o_dim]
|
| 697 |
+
|
| 698 |
+
else: # for English dataset with token pooling
|
| 699 |
+
pred, atten_weights = self.decode(
|
| 700 |
+
sp_emd, sn_mask_word, sp_pos, x, sp_fix_dur, sp_landing_pos, word_ids_sp, sub_id
|
| 701 |
+
) # [batch, step, dec_o_dim]
|
| 702 |
+
return pred, atten_weights
|
raw_text_inference.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
from transformers import BertTokenizer, BertTokenizerFast
|
| 3 |
+
|
| 4 |
+
from Eyettention.model import Eyettention
|
| 5 |
+
from Eyettention.utils import (
|
| 6 |
+
build_bsc_config,
|
| 7 |
+
build_celer_config,
|
| 8 |
+
build_label_encoder,
|
| 9 |
+
text_to_bsc_inputs,
|
| 10 |
+
text_to_celer_inputs,
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class EyettentionRawTextInference:
|
| 15 |
+
def __init__(self, checkpoint_path, dataset="BSC", cf=None, device="cpu"):
|
| 16 |
+
if cf is not None:
|
| 17 |
+
self.cf = cf
|
| 18 |
+
elif dataset == "BSC":
|
| 19 |
+
self.cf = build_bsc_config()
|
| 20 |
+
elif dataset == "celer":
|
| 21 |
+
self.cf = build_celer_config()
|
| 22 |
+
else:
|
| 23 |
+
raise ValueError(f"Unsupported dataset: {dataset}")
|
| 24 |
+
|
| 25 |
+
self.device = device
|
| 26 |
+
if self.cf["dataset"] == "celer":
|
| 27 |
+
self.tokenizer = BertTokenizerFast.from_pretrained(self.cf["model_pretrained"])
|
| 28 |
+
else:
|
| 29 |
+
self.tokenizer = BertTokenizer.from_pretrained(self.cf["model_pretrained"])
|
| 30 |
+
self.label_encoder = build_label_encoder(self.cf)
|
| 31 |
+
|
| 32 |
+
self.model = Eyettention(self.cf)
|
| 33 |
+
state_dict = torch.load(checkpoint_path, map_location=device)
|
| 34 |
+
if (
|
| 35 |
+
"encoder.embeddings.position_ids" not in self.model.state_dict()
|
| 36 |
+
): # Based on transformers version
|
| 37 |
+
state_dict.pop("encoder.embeddings.position_ids", None)
|
| 38 |
+
self.model.load_state_dict(state_dict)
|
| 39 |
+
self.model.to(device)
|
| 40 |
+
self.model.eval()
|
| 41 |
+
|
| 42 |
+
def generate_from_chinese_text(self, text, max_pred_len=None, previous_scanpath=None):
|
| 43 |
+
"""Generate from raw Chinese text."""
|
| 44 |
+
if self.cf["dataset"] != "BSC":
|
| 45 |
+
raise ValueError("generate_from_chinese_text requires a BSC config.")
|
| 46 |
+
if max_pred_len is not None and max_pred_len <= 0:
|
| 47 |
+
raise ValueError("max_pred_len must be positive.")
|
| 48 |
+
if not isinstance(text, str):
|
| 49 |
+
raise TypeError("text must be a string.")
|
| 50 |
+
if not text.strip():
|
| 51 |
+
raise ValueError("text must not be empty.")
|
| 52 |
+
|
| 53 |
+
sn_input_ids, sn_mask, sn_word_len = text_to_bsc_inputs(
|
| 54 |
+
sn_str=text,
|
| 55 |
+
tokenizer=self.tokenizer,
|
| 56 |
+
cf=self.cf,
|
| 57 |
+
device=self.device,
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
with torch.no_grad():
|
| 61 |
+
return self.model.scanpath_generation(
|
| 62 |
+
sn_emd=sn_input_ids,
|
| 63 |
+
sn_mask=sn_mask,
|
| 64 |
+
word_ids_sn=None,
|
| 65 |
+
sn_word_len=sn_word_len,
|
| 66 |
+
le=self.label_encoder,
|
| 67 |
+
max_pred_len=max_pred_len or self.cf["max_pred_len"],
|
| 68 |
+
previous_scanpath=previous_scanpath,
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
def generate_from_english_text(self, text, max_pred_len=None, previous_scanpath=None):
|
| 72 |
+
"""Generate from raw English text."""
|
| 73 |
+
if self.cf["dataset"] != "celer":
|
| 74 |
+
raise ValueError("generate_from_english_text requires a CELER config.")
|
| 75 |
+
if max_pred_len is not None and max_pred_len <= 0:
|
| 76 |
+
raise ValueError("max_pred_len must be positive.")
|
| 77 |
+
if not isinstance(text, str):
|
| 78 |
+
raise TypeError("text must be a string.")
|
| 79 |
+
if not text.strip():
|
| 80 |
+
raise ValueError("text must not be empty.")
|
| 81 |
+
|
| 82 |
+
sn_input_ids, sn_mask, word_ids_sn, sn_word_len = text_to_celer_inputs(
|
| 83 |
+
sn_str=text,
|
| 84 |
+
tokenizer=self.tokenizer,
|
| 85 |
+
cf=self.cf,
|
| 86 |
+
device=self.device,
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
with torch.no_grad():
|
| 90 |
+
return self.model.scanpath_generation(
|
| 91 |
+
sn_emd=sn_input_ids,
|
| 92 |
+
sn_mask=sn_mask,
|
| 93 |
+
word_ids_sn=word_ids_sn,
|
| 94 |
+
sn_word_len=sn_word_len,
|
| 95 |
+
le=self.label_encoder,
|
| 96 |
+
max_pred_len=max_pred_len or self.cf["max_pred_len"],
|
| 97 |
+
previous_scanpath=previous_scanpath,
|
| 98 |
+
)
|
requirements.txt
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
matplotlib==3.8.4
|
| 2 |
+
numpy==1.23.5
|
| 3 |
+
pandas==1.5.3
|
| 4 |
+
scikit_learn==1.6.1
|
| 5 |
+
seaborn==0.12.2
|
| 6 |
+
torch==2.4.1
|
| 7 |
+
tqdm==4.66.4
|
| 8 |
+
transformers==4.27.0
|
| 9 |
+
paddlepaddle==2.4.2
|
| 10 |
+
--find-links https://www.paddlepaddle.org.cn/whl/windows/mkl/avx/stable.html
|
results/BSC/Eyettention_chinese.pth
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0f5b992f0fa720f6f2d9c9d932b2062e2e9eeea4f5d3fd421dcd1bd68f14b059
|
| 3 |
+
size 422488401
|
results/CELER/Eyettention_english.pth
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d51b0dc3cedbb93519054d0c43a0c768b434a74b6774311458810e28293b4e50
|
| 3 |
+
size 446653649
|
tests/test_imports.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import ast
|
| 2 |
+
import sys
|
| 3 |
+
import unittest
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
+
EYETTENTION_ROOT = Path(__file__).resolve().parents[1]
|
| 7 |
+
PROJECT_ROOT = EYETTENTION_ROOT.parent
|
| 8 |
+
EXCLUDED_DIRS = {"__pycache__", ".git", "tests"}
|
| 9 |
+
EXPECTED_IMPORT_FAILURES = {
|
| 10 |
+
("Eyettention/app.py", "import gradio as gr"),
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
if str(PROJECT_ROOT) not in sys.path:
|
| 14 |
+
sys.path.insert(0, str(PROJECT_ROOT))
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class EyettentionImportTests(unittest.TestCase):
|
| 18 |
+
def test_import_lines(self):
|
| 19 |
+
failures = []
|
| 20 |
+
|
| 21 |
+
for file_path in _python_files(EYETTENTION_ROOT):
|
| 22 |
+
source = file_path.read_text()
|
| 23 |
+
tree = ast.parse(source, filename=str(file_path))
|
| 24 |
+
|
| 25 |
+
for node in ast.walk(tree):
|
| 26 |
+
if not isinstance(node, (ast.Import, ast.ImportFrom)):
|
| 27 |
+
continue
|
| 28 |
+
if isinstance(node, ast.ImportFrom) and node.module == "__future__":
|
| 29 |
+
continue
|
| 30 |
+
|
| 31 |
+
import_line = ast.get_source_segment(source, node)
|
| 32 |
+
try:
|
| 33 |
+
exec(
|
| 34 |
+
compile(import_line, str(file_path), "exec"),
|
| 35 |
+
_import_globals(file_path),
|
| 36 |
+
)
|
| 37 |
+
except Exception as exc:
|
| 38 |
+
relative_path = str(file_path.relative_to(PROJECT_ROOT))
|
| 39 |
+
if _is_expected_failure(relative_path, import_line):
|
| 40 |
+
continue
|
| 41 |
+
failures.append(
|
| 42 |
+
f"{relative_path}:{node.lineno}\n"
|
| 43 |
+
f"{import_line}\n"
|
| 44 |
+
f"{type(exc).__name__}: {exc}"
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
if failures:
|
| 48 |
+
self.fail("Failed import line(s):\n\n" + "\n\n".join(failures))
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def _python_files(root):
|
| 52 |
+
for file_path in root.rglob("*.py"):
|
| 53 |
+
if any(part in EXCLUDED_DIRS for part in file_path.parts):
|
| 54 |
+
continue
|
| 55 |
+
yield file_path
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def _import_globals(file_path):
|
| 59 |
+
module_path = file_path.relative_to(PROJECT_ROOT).with_suffix("")
|
| 60 |
+
module_parts = module_path.parts
|
| 61 |
+
|
| 62 |
+
if module_parts[-1] == "__init__":
|
| 63 |
+
module_name = ".".join(module_parts[:-1])
|
| 64 |
+
package = module_name
|
| 65 |
+
else:
|
| 66 |
+
module_name = ".".join(module_parts)
|
| 67 |
+
package = ".".join(module_parts[:-1])
|
| 68 |
+
|
| 69 |
+
return {
|
| 70 |
+
"__name__": module_name,
|
| 71 |
+
"__package__": package,
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def _is_expected_failure(relative_path, import_line):
|
| 76 |
+
return any(
|
| 77 |
+
relative_path == expected_path and import_line.startswith(expected_import)
|
| 78 |
+
for expected_path, expected_import in EXPECTED_IMPORT_FAILURES
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
if __name__ == "__main__":
|
| 83 |
+
unittest.main()
|
tests/test_raw_text_inference.py
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import functools
|
| 2 |
+
import sys
|
| 3 |
+
import unittest
|
| 4 |
+
import warnings
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
import numpy as np
|
| 8 |
+
import torch
|
| 9 |
+
|
| 10 |
+
warnings.simplefilter("ignore")
|
| 11 |
+
|
| 12 |
+
EYETTENTION_ROOT = Path(__file__).resolve().parents[1]
|
| 13 |
+
PROJECT_ROOT = EYETTENTION_ROOT.parent
|
| 14 |
+
if str(PROJECT_ROOT) not in sys.path:
|
| 15 |
+
sys.path.insert(0, str(PROJECT_ROOT))
|
| 16 |
+
|
| 17 |
+
BSC_CHECKPOINT = EYETTENTION_ROOT / "results" / "BSC" / "Eyettention_chinese.pth"
|
| 18 |
+
CELER_CHECKPOINT = EYETTENTION_ROOT / "results" / "CELER" / "Eyettention_english.pth"
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def _import_utils():
|
| 22 |
+
try:
|
| 23 |
+
from Eyettention.utils import (
|
| 24 |
+
build_bsc_config,
|
| 25 |
+
build_celer_config,
|
| 26 |
+
build_label_encoder,
|
| 27 |
+
text_to_bsc_inputs,
|
| 28 |
+
text_to_celer_inputs,
|
| 29 |
+
)
|
| 30 |
+
except ImportError as exc:
|
| 31 |
+
raise unittest.SkipTest(f"Eyettention utils dependencies are unavailable: {exc}") from exc
|
| 32 |
+
|
| 33 |
+
return (
|
| 34 |
+
build_bsc_config,
|
| 35 |
+
build_celer_config,
|
| 36 |
+
build_label_encoder,
|
| 37 |
+
text_to_bsc_inputs,
|
| 38 |
+
text_to_celer_inputs,
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def _load_tokenizer(tokenizer_cls, model_name):
|
| 43 |
+
try:
|
| 44 |
+
return tokenizer_cls.from_pretrained(model_name)
|
| 45 |
+
except OSError as exc:
|
| 46 |
+
raise unittest.SkipTest(
|
| 47 |
+
f"{model_name} is not available in the local Hugging Face cache"
|
| 48 |
+
) from exc
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
@functools.lru_cache(maxsize=None)
|
| 52 |
+
def _raw_text_runner(dataset, checkpoint_path):
|
| 53 |
+
from Eyettention.raw_text_inference import EyettentionRawTextInference
|
| 54 |
+
|
| 55 |
+
if not Path(checkpoint_path).exists():
|
| 56 |
+
raise unittest.SkipTest(f"Missing checkpoint: {checkpoint_path}")
|
| 57 |
+
|
| 58 |
+
try:
|
| 59 |
+
return EyettentionRawTextInference(
|
| 60 |
+
checkpoint_path=str(checkpoint_path),
|
| 61 |
+
dataset=dataset,
|
| 62 |
+
device="cpu",
|
| 63 |
+
)
|
| 64 |
+
except OSError as exc:
|
| 65 |
+
raise unittest.SkipTest(f"Pretrained model for {dataset} is not available locally") from exc
|
| 66 |
+
except ImportError as exc:
|
| 67 |
+
raise unittest.SkipTest(f"Raw text dependency for {dataset} is unavailable: {exc}") from exc
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
class ConfigAndInputTests(unittest.TestCase):
|
| 71 |
+
|
| 72 |
+
def test_text_to_bsc_inputs_returns_model_ready_tensors(self):
|
| 73 |
+
from transformers import BertTokenizer
|
| 74 |
+
|
| 75 |
+
build_bsc_config, _, _, text_to_bsc_inputs, _ = _import_utils()
|
| 76 |
+
cf = build_bsc_config(max_pred_len=5)
|
| 77 |
+
tokenizer = _load_tokenizer(BertTokenizer, cf["model_pretrained"])
|
| 78 |
+
|
| 79 |
+
sn_input_ids, sn_mask, sn_word_len = text_to_bsc_inputs(
|
| 80 |
+
"中国选手在男子滑雪比赛中有望蝉联冠军",
|
| 81 |
+
tokenizer,
|
| 82 |
+
cf,
|
| 83 |
+
device="cpu",
|
| 84 |
+
)
|
| 85 |
+
|
| 86 |
+
self.assertEqual(tuple(sn_input_ids.shape), (1, cf["max_sn_len"]))
|
| 87 |
+
self.assertEqual(tuple(sn_mask.shape), (1, cf["max_sn_len"]))
|
| 88 |
+
self.assertEqual(tuple(sn_word_len.shape), (1, cf["max_sn_len"]))
|
| 89 |
+
self.assertTrue(torch.is_floating_point(sn_mask))
|
| 90 |
+
self.assertTrue(torch.isfinite(sn_word_len).all())
|
| 91 |
+
|
| 92 |
+
def test_text_to_celer_inputs_returns_model_ready_tensors(self):
|
| 93 |
+
from transformers import BertTokenizerFast
|
| 94 |
+
|
| 95 |
+
_, build_celer_config, _, _, text_to_celer_inputs = _import_utils()
|
| 96 |
+
cf = build_celer_config(max_pred_len=5)
|
| 97 |
+
tokenizer = _load_tokenizer(BertTokenizerFast, cf["model_pretrained"])
|
| 98 |
+
|
| 99 |
+
sn_input_ids, sn_mask, word_ids_sn, sn_word_len = text_to_celer_inputs(
|
| 100 |
+
"He said BankEast's offer appears to be \"attractive to the bank's shareholders.\"",
|
| 101 |
+
tokenizer,
|
| 102 |
+
cf,
|
| 103 |
+
device="cpu",
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
self.assertEqual(tuple(sn_input_ids.shape), (1, cf["max_sn_token"]))
|
| 107 |
+
self.assertEqual(tuple(sn_mask.shape), (1, cf["max_sn_token"]))
|
| 108 |
+
self.assertEqual(tuple(word_ids_sn.shape), (1, cf["max_sn_token"]))
|
| 109 |
+
self.assertEqual(tuple(sn_word_len.shape), (1, cf["max_sn_len"]))
|
| 110 |
+
self.assertTrue(torch.is_floating_point(sn_mask))
|
| 111 |
+
self.assertTrue(torch.isfinite(sn_word_len).all())
|
| 112 |
+
self.assertGreaterEqual(np.nanmax(word_ids_sn.numpy()), 1)
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
class RawTextInferenceSmokeTests(unittest.TestCase):
|
| 116 |
+
def test_chinese_raw_text_generation_smoke(self):
|
| 117 |
+
runner = _raw_text_runner("BSC", BSC_CHECKPOINT)
|
| 118 |
+
torch.manual_seed(0)
|
| 119 |
+
|
| 120 |
+
scanpath, density = runner.generate_from_chinese_text(
|
| 121 |
+
"中国选手在男子滑雪比赛中有望蝉联冠军",
|
| 122 |
+
max_pred_len=5,
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
self.assertEqual(tuple(scanpath.shape), (1, 5))
|
| 126 |
+
self.assertEqual(len(density), 4)
|
| 127 |
+
self.assertEqual(scanpath[0, 0].item(), 0)
|
| 128 |
+
|
| 129 |
+
def test_english_raw_text_generation_smoke(self):
|
| 130 |
+
runner = _raw_text_runner("celer", CELER_CHECKPOINT)
|
| 131 |
+
torch.manual_seed(0)
|
| 132 |
+
|
| 133 |
+
scanpath, density = runner.generate_from_english_text(
|
| 134 |
+
"The quick brown fox jumps.",
|
| 135 |
+
max_pred_len=5,
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
+
self.assertEqual(tuple(scanpath.shape), (1, 5))
|
| 139 |
+
self.assertEqual(len(density), 4)
|
| 140 |
+
self.assertEqual(scanpath[0, 0].item(), 0)
|
| 141 |
+
|
| 142 |
+
def test_previous_scanpath_is_replayed_before_sampling(self):
|
| 143 |
+
runner = _raw_text_runner("BSC", BSC_CHECKPOINT)
|
| 144 |
+
torch.manual_seed(0)
|
| 145 |
+
|
| 146 |
+
scanpath, _ = runner.generate_from_chinese_text(
|
| 147 |
+
"中国选手在男子滑雪比赛中有望蝉联冠军",
|
| 148 |
+
max_pred_len=3,
|
| 149 |
+
previous_scanpath=[0, 1, 2],
|
| 150 |
+
)
|
| 151 |
+
|
| 152 |
+
self.assertEqual(scanpath[0, :3].tolist(), [0, 1, 2])
|
| 153 |
+
self.assertEqual(scanpath.shape[1], 6)
|
| 154 |
+
|
| 155 |
+
def test_dataset_specific_methods_guard_against_wrong_config(self):
|
| 156 |
+
from Eyettention.raw_text_inference import EyettentionRawTextInference
|
| 157 |
+
|
| 158 |
+
bsc_runner = object.__new__(EyettentionRawTextInference)
|
| 159 |
+
bsc_runner.cf = {"dataset": "celer"}
|
| 160 |
+
with self.assertRaises(ValueError):
|
| 161 |
+
bsc_runner.generate_from_chinese_text("中国选手在男子滑雪比赛中有望蝉联冠军")
|
| 162 |
+
|
| 163 |
+
celer_runner = object.__new__(EyettentionRawTextInference)
|
| 164 |
+
celer_runner.cf = {"dataset": "BSC"}
|
| 165 |
+
with self.assertRaises(ValueError):
|
| 166 |
+
celer_runner.generate_from_english_text(
|
| 167 |
+
"He said BankEast's offer appears to be \"attractive to the bank's shareholders.\""
|
| 168 |
+
)
|
| 169 |
+
|
| 170 |
+
def test_text_is_none(self):
|
| 171 |
+
from Eyettention.raw_text_inference import EyettentionRawTextInference
|
| 172 |
+
|
| 173 |
+
bsc_runner = object.__new__(EyettentionRawTextInference)
|
| 174 |
+
bsc_runner.cf = {"dataset": "celer"}
|
| 175 |
+
with self.assertRaises(ValueError):
|
| 176 |
+
bsc_runner.generate_from_chinese_text(None)
|
| 177 |
+
|
| 178 |
+
celer_runner = object.__new__(EyettentionRawTextInference)
|
| 179 |
+
celer_runner.cf = {"dataset": "BSC"}
|
| 180 |
+
with self.assertRaises(ValueError):
|
| 181 |
+
celer_runner.generate_from_english_text(None)
|
| 182 |
+
|
| 183 |
+
def test_text_is_empty(self):
|
| 184 |
+
from Eyettention.raw_text_inference import EyettentionRawTextInference
|
| 185 |
+
|
| 186 |
+
bsc_runner = object.__new__(EyettentionRawTextInference)
|
| 187 |
+
bsc_runner.cf = {"dataset": "celer"}
|
| 188 |
+
with self.assertRaises(ValueError):
|
| 189 |
+
bsc_runner.generate_from_chinese_text(" ")
|
| 190 |
+
|
| 191 |
+
celer_runner = object.__new__(EyettentionRawTextInference)
|
| 192 |
+
celer_runner.cf = {"dataset": "BSC"}
|
| 193 |
+
with self.assertRaises(ValueError):
|
| 194 |
+
celer_runner.generate_from_english_text(" ")
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
if __name__ == "__main__":
|
| 198 |
+
unittest.main()
|
utils.py
ADDED
|
@@ -0,0 +1,716 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# coding=utf-8
|
| 2 |
+
import numpy as np
|
| 3 |
+
import pandas as pd
|
| 4 |
+
import os
|
| 5 |
+
import seaborn as sns
|
| 6 |
+
from typing import Tuple
|
| 7 |
+
from torch.utils.data import Dataset, DataLoader
|
| 8 |
+
from transformers import BertTokenizerFast, BertTokenizer, BertForTokenClassification
|
| 9 |
+
import torch
|
| 10 |
+
import torch.nn.functional as F
|
| 11 |
+
import seaborn as sns
|
| 12 |
+
import matplotlib.pyplot as plt
|
| 13 |
+
from tqdm import tqdm
|
| 14 |
+
import json
|
| 15 |
+
from collections import Counter
|
| 16 |
+
import torch.nn as nn
|
| 17 |
+
from sklearn.preprocessing import LabelEncoder
|
| 18 |
+
import pickle
|
| 19 |
+
from LAC import LAC
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def build_bsc_config(
|
| 23 |
+
atten_type="local-g",
|
| 24 |
+
max_pred_len=60,
|
| 25 |
+
batch_size=256,
|
| 26 |
+
n_epochs=1000,
|
| 27 |
+
n_folds=5,
|
| 28 |
+
):
|
| 29 |
+
return {
|
| 30 |
+
"model_pretrained": "bert-base-chinese",
|
| 31 |
+
"lr": 1e-3,
|
| 32 |
+
"max_grad_norm": 10,
|
| 33 |
+
"n_epochs": n_epochs,
|
| 34 |
+
"n_folds": n_folds,
|
| 35 |
+
"dataset": "BSC",
|
| 36 |
+
"atten_type": atten_type,
|
| 37 |
+
"batch_size": batch_size,
|
| 38 |
+
"max_sn_len": 27,
|
| 39 |
+
"max_sp_len": 40,
|
| 40 |
+
"norm_type": "z-score",
|
| 41 |
+
"earlystop_patience": 20,
|
| 42 |
+
"max_pred_len": max_pred_len,
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def build_celer_config(
|
| 47 |
+
atten_type="local-g",
|
| 48 |
+
max_pred_len=60,
|
| 49 |
+
batch_size=256,
|
| 50 |
+
n_epochs=1000,
|
| 51 |
+
n_folds=5,
|
| 52 |
+
):
|
| 53 |
+
return {
|
| 54 |
+
"model_pretrained": "bert-base-cased",
|
| 55 |
+
"lr": 1e-3,
|
| 56 |
+
"max_grad_norm": 10,
|
| 57 |
+
"n_epochs": n_epochs,
|
| 58 |
+
"n_folds": n_folds,
|
| 59 |
+
"dataset": "celer",
|
| 60 |
+
"atten_type": atten_type,
|
| 61 |
+
"batch_size": batch_size,
|
| 62 |
+
"max_sn_len": 24,
|
| 63 |
+
"max_sn_token": 35,
|
| 64 |
+
"max_sp_len": 52,
|
| 65 |
+
"max_sp_token": 395,
|
| 66 |
+
"norm_type": "z-score",
|
| 67 |
+
"earlystop_patience": 20,
|
| 68 |
+
"max_pred_len": max_pred_len,
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def build_label_encoder(cf):
|
| 73 |
+
le = LabelEncoder()
|
| 74 |
+
le.fit(np.append(np.arange(-cf["max_sn_len"] + 3, cf["max_sn_len"] - 1), cf["max_sn_len"] - 1))
|
| 75 |
+
return le
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
# Raw text to tokenised inputs for both BSC and CELER
|
| 79 |
+
def text_to_bsc_inputs(sn_str, tokenizer, cf, device="cpu"):
|
| 80 |
+
lac = LAC(mode="seg")
|
| 81 |
+
tokens = tokenizer.encode_plus(
|
| 82 |
+
sn_str,
|
| 83 |
+
add_special_tokens=True,
|
| 84 |
+
max_length=cf["max_sn_len"],
|
| 85 |
+
padding="max_length",
|
| 86 |
+
)
|
| 87 |
+
|
| 88 |
+
sn_input_ids = torch.tensor([tokens["input_ids"]], device=device)
|
| 89 |
+
sn_mask = torch.tensor([tokens["attention_mask"]], dtype=torch.float32, device=device)
|
| 90 |
+
|
| 91 |
+
sn_word_len = compute_BSC_word_length_for_input(sn_str, lac)
|
| 92 |
+
sn_word_len = pad_seq_with_nan([sn_word_len], max_len=cf["max_sn_len"], dtype=np.float32)
|
| 93 |
+
|
| 94 |
+
# Load precomputed BSC statistics for feature normalization.
|
| 95 |
+
path = os.path.join(os.path.dirname(__file__), "Data", "feature_norm_BSC.pickle")
|
| 96 |
+
with open(path, "rb") as file_to_read:
|
| 97 |
+
loaded_dictionary = pickle.load(file_to_read)
|
| 98 |
+
sn_word_len_mean = loaded_dictionary["sn_word_len_mean"].numpy()
|
| 99 |
+
sn_word_len_std = loaded_dictionary["sn_word_len_std"].numpy()
|
| 100 |
+
|
| 101 |
+
sn_word_len = (sn_word_len - sn_word_len_mean) / sn_word_len_std
|
| 102 |
+
sn_word_len = np.nan_to_num(sn_word_len)
|
| 103 |
+
sn_word_len = torch.tensor(sn_word_len, dtype=torch.float32, device=device)
|
| 104 |
+
|
| 105 |
+
return sn_input_ids, sn_mask, sn_word_len
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def text_to_celer_inputs(sn_str, tokenizer, cf, device="cpu"):
|
| 109 |
+
text = sn_str.split()
|
| 110 |
+
|
| 111 |
+
tokens = tokenizer(
|
| 112 |
+
[tokenizer.cls_token, *text, tokenizer.sep_token],
|
| 113 |
+
add_special_tokens=False,
|
| 114 |
+
max_length=cf["max_sn_token"],
|
| 115 |
+
padding="max_length",
|
| 116 |
+
is_split_into_words=True,
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
sn_input_ids = torch.tensor([tokens["input_ids"]], device=device)
|
| 120 |
+
sn_mask = torch.tensor([tokens["attention_mask"]], dtype=torch.float32, device=device)
|
| 121 |
+
|
| 122 |
+
word_ids_sn = tokens.word_ids()
|
| 123 |
+
word_ids_sn = [val if val is not None else np.nan for val in word_ids_sn]
|
| 124 |
+
word_ids_sn = torch.tensor([word_ids_sn], dtype=torch.float32, device=device)
|
| 125 |
+
|
| 126 |
+
word_lengths = np.asarray([len(t) for t in text], dtype=np.float32)
|
| 127 |
+
sn_word_len = compute_word_length_celer(word_lengths)
|
| 128 |
+
sn_word_len = pad_seq_with_nan([sn_word_len], max_len=cf["max_sn_len"], dtype=np.float32)
|
| 129 |
+
|
| 130 |
+
# Load precomputed CELER statistics for feature normalization.
|
| 131 |
+
path = os.path.join(os.path.dirname(__file__), "Data", "feature_norm_celer.pickle")
|
| 132 |
+
with open(path, "rb") as file_to_read:
|
| 133 |
+
loaded_dictionary = pickle.load(file_to_read)
|
| 134 |
+
sn_word_len_mean = float(loaded_dictionary["sn_word_len_mean"])
|
| 135 |
+
sn_word_len_std = float(loaded_dictionary["sn_word_len_std"])
|
| 136 |
+
|
| 137 |
+
sn_word_len = (sn_word_len - sn_word_len_mean) / sn_word_len_std
|
| 138 |
+
sn_word_len = np.nan_to_num(sn_word_len)
|
| 139 |
+
sn_word_len = torch.tensor(sn_word_len, dtype=torch.float32, device=device)
|
| 140 |
+
|
| 141 |
+
return sn_input_ids, sn_mask, word_ids_sn, sn_word_len
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def load_bsc() -> Tuple[pd.DataFrame, ...]:
|
| 145 |
+
"""
|
| 146 |
+
:return: word info dataframe, part-of-speech info, eye movements
|
| 147 |
+
"""
|
| 148 |
+
bsc_path = "./Data/beijing-sentence-corpus/"
|
| 149 |
+
info_path = os.path.join(bsc_path, "BSC.Word.Info.v2.xlsx")
|
| 150 |
+
bsc_emd_path = os.path.join(bsc_path, "BSC.EMD/BSC.EMD.txt")
|
| 151 |
+
word_info_df = pd.read_excel(info_path, "word")
|
| 152 |
+
pos_info_df = pd.read_excel(info_path, header=None)
|
| 153 |
+
eyemovement_df = pd.read_csv(bsc_emd_path, delimiter="\t")
|
| 154 |
+
return word_info_df, pos_info_df, eyemovement_df
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
def load_corpus(corpus, task=None):
|
| 158 |
+
if corpus == "BSC":
|
| 159 |
+
# load word data, POS data, EM data
|
| 160 |
+
word_info_df, pos_info_df, eyemovement_df = load_bsc()
|
| 161 |
+
return word_info_df, pos_info_df, eyemovement_df
|
| 162 |
+
elif corpus == "celer":
|
| 163 |
+
eyemovement_df = pd.read_csv("./Data/celer/data_v2.0/sent_fix.tsv", delimiter="\t")
|
| 164 |
+
eyemovement_df["CURRENT_FIX_INTEREST_AREA_LABEL"] = (
|
| 165 |
+
eyemovement_df.CURRENT_FIX_INTEREST_AREA_LABEL.replace("\t(.*)", "", regex=True)
|
| 166 |
+
)
|
| 167 |
+
word_info_df = pd.read_csv("./Data/celer/data_v2.0/sent_ia.tsv", delimiter="\t")
|
| 168 |
+
word_info_df["IA_LABEL"] = word_info_df.IA_LABEL.replace("\t(.*)", "", regex=True)
|
| 169 |
+
return word_info_df, None, eyemovement_df
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
def compute_BSC_word_length(sn_df):
|
| 173 |
+
word_len = sn_df.LEN.values
|
| 174 |
+
wl_list = []
|
| 175 |
+
for wl in word_len:
|
| 176 |
+
wl_list.extend([wl] * wl)
|
| 177 |
+
arr = np.asarray(wl_list, dtype=np.float32)
|
| 178 |
+
# length of a punctuation is 0, plus an epsilon to avoid division output inf
|
| 179 |
+
arr[arr == 0] = 1 / (0 + 0.5)
|
| 180 |
+
arr[arr != 0] = 1 / (arr[arr != 0])
|
| 181 |
+
return arr
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
def compute_BSC_word_length_for_input(sentence, lac):
|
| 185 |
+
word_string = lac.run(sentence)
|
| 186 |
+
# print(word_string)
|
| 187 |
+
word_len = [len(i) for i in word_string]
|
| 188 |
+
wl_list = []
|
| 189 |
+
for wl in word_len:
|
| 190 |
+
wl_list.extend([wl] * wl)
|
| 191 |
+
arr = np.asarray(wl_list, dtype=np.float32)
|
| 192 |
+
# length of a punctuation is 0, plus an epsilon to avoid division output inf
|
| 193 |
+
arr[arr == 0] = 1 / (0 + 0.5)
|
| 194 |
+
arr[arr != 0] = 1 / (arr[arr != 0])
|
| 195 |
+
return arr
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
def pad_seq(seqs, max_len, pad_value, dtype=np.compat.long):
|
| 199 |
+
padded = np.full((len(seqs), max_len), fill_value=pad_value, dtype=dtype)
|
| 200 |
+
for i, seq in enumerate(seqs):
|
| 201 |
+
padded[i, 0] = 0
|
| 202 |
+
padded[i, 1 : (len(seq) + 1)] = seq
|
| 203 |
+
if pad_value != 0:
|
| 204 |
+
padded[i, len(seq) + 1] = pad_value - 1
|
| 205 |
+
|
| 206 |
+
return padded
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
def pad_seq_with_nan(seqs, max_len, dtype=np.compat.long):
|
| 210 |
+
padded = np.full((len(seqs), max_len), fill_value=np.nan, dtype=dtype)
|
| 211 |
+
for i, seq in enumerate(seqs):
|
| 212 |
+
padded[i, 1 : (len(seq) + 1)] = seq
|
| 213 |
+
return padded
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
def _process_BSC_corpus(sn_list, reader_list, word_info_df, eyemovement_df, tokenizer, cf):
|
| 217 |
+
"""
|
| 218 |
+
SN_token_embedding <CLS>, bla, bla, <SEP>
|
| 219 |
+
SP_token_embedding <CLS>, bla, bla, <SEP>
|
| 220 |
+
SP_ordinal_pos 0, bla, bla, max_sp_len
|
| 221 |
+
SP_fix_dur 0, bla, bla, 0
|
| 222 |
+
SN_len original sentence length without start and end tokens
|
| 223 |
+
"""
|
| 224 |
+
SN_input_ids, SN_attention_mask, SN_WORD_len = [], [], []
|
| 225 |
+
SP_input_ids, SP_attention_mask = [], []
|
| 226 |
+
SP_ordinal_pos, SP_landing_pos, SP_fix_dur = [], [], []
|
| 227 |
+
sub_id_list = []
|
| 228 |
+
for sn_id in sn_list:
|
| 229 |
+
# print('sub_id:', sub_id)
|
| 230 |
+
# process sentence sequence
|
| 231 |
+
sn_df = eyemovement_df[eyemovement_df.sn == sn_id]
|
| 232 |
+
sn = word_info_df[word_info_df.SN == sn_id]
|
| 233 |
+
sn_str = "".join(sn.WORD.values)
|
| 234 |
+
sn_word_len = compute_BSC_word_length(sn)
|
| 235 |
+
|
| 236 |
+
# tokenization and padding
|
| 237 |
+
tokenizer.padding_side = "right"
|
| 238 |
+
tokens = tokenizer.encode_plus(
|
| 239 |
+
sn_str,
|
| 240 |
+
add_special_tokens=True,
|
| 241 |
+
truncation=True,
|
| 242 |
+
max_length=cf["max_sn_len"],
|
| 243 |
+
padding="max_length",
|
| 244 |
+
return_attention_mask=True,
|
| 245 |
+
)
|
| 246 |
+
encoded_sn = tokens["input_ids"]
|
| 247 |
+
mask_sn = tokens["attention_mask"]
|
| 248 |
+
|
| 249 |
+
# process fixation sequence
|
| 250 |
+
for sub_id in reader_list:
|
| 251 |
+
sub_df = sn_df[sn_df.id == sub_id]
|
| 252 |
+
if len(sub_df) == 0:
|
| 253 |
+
# no scanpath data found for the subject
|
| 254 |
+
continue
|
| 255 |
+
|
| 256 |
+
# last fixation go back to the first character with fl = 0 -- seems to be outlier point? remove
|
| 257 |
+
if sub_df.iloc[-1].wn == 1 and sub_df.iloc[-1].fl == 0:
|
| 258 |
+
sub_df = sub_df.iloc[:-1]
|
| 259 |
+
|
| 260 |
+
sp_word_pos, sp_fix_loc, sp_fix_dur = (
|
| 261 |
+
sub_df.wn.values,
|
| 262 |
+
sub_df.fl.values,
|
| 263 |
+
sub_df.dur.values,
|
| 264 |
+
)
|
| 265 |
+
sp_landing_pos_char = np.modf(sp_fix_loc)[0]
|
| 266 |
+
SP_landing_pos.append(sp_landing_pos_char)
|
| 267 |
+
|
| 268 |
+
# Convert word-based ordinal positions to token(character)-based ordinal positions
|
| 269 |
+
# When the fixated word index is less than 0, set it to 0
|
| 270 |
+
sp_fix_loc = np.where(sp_fix_loc < 0, 0, sp_fix_loc)
|
| 271 |
+
sp_ordinal_pos = [
|
| 272 |
+
np.sum(sn[sn.NW < value].LEN) + np.ceil(sp_fix_loc[count] + 1e-10)
|
| 273 |
+
for count, value in enumerate(sp_word_pos)
|
| 274 |
+
]
|
| 275 |
+
SP_ordinal_pos.append(sp_ordinal_pos)
|
| 276 |
+
SP_fix_dur.append(sp_fix_dur)
|
| 277 |
+
|
| 278 |
+
# tokenization and padding for scanpath, i.e. fixated word sequence
|
| 279 |
+
sp_token = [sn_str[int(i - 1)] for i in sp_ordinal_pos]
|
| 280 |
+
sp_token_str = "[CLS]" + "".join(sp_token) + "[SEP]"
|
| 281 |
+
sp_tokens = tokenizer.encode_plus(
|
| 282 |
+
sp_token_str,
|
| 283 |
+
add_special_tokens=False,
|
| 284 |
+
truncation=True,
|
| 285 |
+
max_length=cf["max_sp_len"],
|
| 286 |
+
padding="max_length",
|
| 287 |
+
return_attention_mask=True,
|
| 288 |
+
)
|
| 289 |
+
encoded_sp = sp_tokens["input_ids"]
|
| 290 |
+
mask_sp = sp_tokens["attention_mask"]
|
| 291 |
+
SP_input_ids.append(encoded_sp)
|
| 292 |
+
SP_attention_mask.append(mask_sp)
|
| 293 |
+
|
| 294 |
+
# sentence information
|
| 295 |
+
SN_input_ids.append(encoded_sn)
|
| 296 |
+
SN_attention_mask.append(mask_sn)
|
| 297 |
+
SN_WORD_len.append(sn_word_len)
|
| 298 |
+
sub_id_list.append(sub_id)
|
| 299 |
+
|
| 300 |
+
# padding for batch computation
|
| 301 |
+
SP_ordinal_pos = pad_seq(SP_ordinal_pos, max_len=(cf["max_sp_len"]), pad_value=cf["max_sn_len"])
|
| 302 |
+
SP_fix_dur = pad_seq(SP_fix_dur, max_len=(cf["max_sp_len"]), pad_value=0)
|
| 303 |
+
SP_landing_pos = pad_seq(SP_landing_pos, cf["max_sp_len"], pad_value=0, dtype=np.float32)
|
| 304 |
+
SN_WORD_len = pad_seq_with_nan(SN_WORD_len, cf["max_sn_len"], dtype=np.float32)
|
| 305 |
+
|
| 306 |
+
# assign type
|
| 307 |
+
SN_input_ids = np.asarray(SN_input_ids, dtype=np.int64)
|
| 308 |
+
SN_attention_mask = np.asarray(SN_attention_mask, dtype=np.float32)
|
| 309 |
+
SP_input_ids = np.asarray(SP_input_ids, dtype=np.int64)
|
| 310 |
+
SP_attention_mask = np.asarray(SP_attention_mask, dtype=np.float32)
|
| 311 |
+
sub_id_list = np.asarray(sub_id_list, dtype=np.int64)
|
| 312 |
+
|
| 313 |
+
data = {
|
| 314 |
+
"SN_input_ids": SN_input_ids,
|
| 315 |
+
"SN_attention_mask": SN_attention_mask,
|
| 316 |
+
"SN_WORD_len": SN_WORD_len,
|
| 317 |
+
"SP_input_ids": SP_input_ids,
|
| 318 |
+
"SP_attention_mask": SP_attention_mask,
|
| 319 |
+
"SP_ordinal_pos": np.array(SP_ordinal_pos),
|
| 320 |
+
"SP_landing_pos": np.array(SP_landing_pos),
|
| 321 |
+
"SP_fix_dur": np.array(SP_fix_dur),
|
| 322 |
+
"sub_id": sub_id_list,
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
return data
|
| 326 |
+
|
| 327 |
+
|
| 328 |
+
class BSCdataset(Dataset):
|
| 329 |
+
"""Return BSC dataset."""
|
| 330 |
+
|
| 331 |
+
def __init__(self, word_info_df, eyemovement_df, cf, reader_list, sn_list, tokenizer):
|
| 332 |
+
self.data = _process_BSC_corpus(
|
| 333 |
+
sn_list, reader_list, word_info_df, eyemovement_df, tokenizer, cf
|
| 334 |
+
)
|
| 335 |
+
|
| 336 |
+
def __len__(self):
|
| 337 |
+
return len(self.data["SN_input_ids"])
|
| 338 |
+
|
| 339 |
+
def __getitem__(self, idx):
|
| 340 |
+
sample = {}
|
| 341 |
+
sample["sn_input_ids"] = self.data["SN_input_ids"][idx, :]
|
| 342 |
+
sample["sn_attention_mask"] = self.data["SN_attention_mask"][idx, :]
|
| 343 |
+
sample["sn_word_len"] = self.data["SN_WORD_len"][idx, :]
|
| 344 |
+
|
| 345 |
+
sample["sp_input_ids"] = self.data["SP_input_ids"][idx, :]
|
| 346 |
+
sample["sp_attention_mask"] = self.data["SP_attention_mask"][idx, :]
|
| 347 |
+
|
| 348 |
+
sample["sp_pos"] = self.data["SP_ordinal_pos"][idx, :]
|
| 349 |
+
sample["sp_fix_dur"] = self.data["SP_fix_dur"][idx, :]
|
| 350 |
+
sample["sp_landing_pos"] = self.data["SP_landing_pos"][idx, :]
|
| 351 |
+
|
| 352 |
+
sample["sub_id"] = self.data["sub_id"][idx]
|
| 353 |
+
|
| 354 |
+
return sample
|
| 355 |
+
|
| 356 |
+
|
| 357 |
+
def calculate_mean_std(dataloader, feat_key, padding_value=0, scale=1):
|
| 358 |
+
# calculate mean
|
| 359 |
+
total_sum = 0
|
| 360 |
+
total_num = 0
|
| 361 |
+
for batchh in dataloader:
|
| 362 |
+
batchh.keys()
|
| 363 |
+
feat = batchh[feat_key] / scale
|
| 364 |
+
feat = torch.nan_to_num(feat)
|
| 365 |
+
total_num += len(feat.view(-1).nonzero())
|
| 366 |
+
total_sum += feat.sum()
|
| 367 |
+
feat_mean = total_sum / total_num
|
| 368 |
+
# calculate std
|
| 369 |
+
sum_of_squared_error = 0
|
| 370 |
+
for batchh in dataloader:
|
| 371 |
+
batchh.keys()
|
| 372 |
+
feat = batchh[feat_key] / scale
|
| 373 |
+
feat = torch.nan_to_num(feat)
|
| 374 |
+
mask = ~torch.eq(feat, padding_value)
|
| 375 |
+
sum_of_squared_error += (((feat - feat_mean).pow(2)) * mask).sum()
|
| 376 |
+
feat_std = torch.sqrt(sum_of_squared_error / total_num)
|
| 377 |
+
return feat_mean, feat_std
|
| 378 |
+
|
| 379 |
+
|
| 380 |
+
def load_label(sp_pos, cf, labelencoder, device):
|
| 381 |
+
# prepare label and mask
|
| 382 |
+
pad_mask = torch.eq(sp_pos[:, 1:], cf["max_sn_len"])
|
| 383 |
+
end_mask = torch.eq(sp_pos[:, 1:], cf["max_sn_len"] - 1)
|
| 384 |
+
mask = pad_mask + end_mask
|
| 385 |
+
sac_amp = sp_pos[:, 1:] - sp_pos[:, :-1]
|
| 386 |
+
label = sp_pos[:, 1:] * mask + sac_amp * ~mask
|
| 387 |
+
label = (
|
| 388 |
+
torch.where(label > cf["max_sn_len"] - 1, cf["max_sn_len"] - 1, label)
|
| 389 |
+
.to("cpu")
|
| 390 |
+
.detach()
|
| 391 |
+
.numpy()
|
| 392 |
+
)
|
| 393 |
+
label = labelencoder.transform(label.reshape(-1)).reshape(label.shape[0], label.shape[1])
|
| 394 |
+
label = torch.from_numpy(label).long().to(device)
|
| 395 |
+
pad_mask = pad_mask.to(device)
|
| 396 |
+
return pad_mask, label
|
| 397 |
+
|
| 398 |
+
|
| 399 |
+
def likelihood(pred, label, mask):
|
| 400 |
+
if torch.is_tensor(label):
|
| 401 |
+
label = label.detach().to("cpu").numpy()
|
| 402 |
+
if torch.is_tensor(mask):
|
| 403 |
+
mask = mask.detach().to("cpu").numpy()
|
| 404 |
+
# test
|
| 405 |
+
# res = F.nll_loss(torch.tensor(pred), torch.tensor(label))
|
| 406 |
+
label = one_hot_encode(label, pred.shape[1])
|
| 407 |
+
res = np.sum(np.multiply(pred, label), axis=1)
|
| 408 |
+
res = np.sum(res * ~mask) / np.sum(~mask)
|
| 409 |
+
return res
|
| 410 |
+
|
| 411 |
+
|
| 412 |
+
def eval_log_llh(dnn_out, label, pad_mask):
|
| 413 |
+
res = []
|
| 414 |
+
dnn_out = np.log2(dnn_out + 1e-10)
|
| 415 |
+
# For each scanpath calculate the likelihood and then find the average
|
| 416 |
+
for sp_indx in range(dnn_out.shape[0]):
|
| 417 |
+
out = likelihood(dnn_out[sp_indx, :, :], label[sp_indx, :], pad_mask[sp_indx, :])
|
| 418 |
+
res.append(out)
|
| 419 |
+
|
| 420 |
+
return res
|
| 421 |
+
|
| 422 |
+
|
| 423 |
+
def prepare_scanpath(sp_dnn, sn_len, sp_human, cf):
|
| 424 |
+
max_sp_len = sp_dnn.shape[1]
|
| 425 |
+
sp_human = sp_human.detach().to("cpu").numpy()
|
| 426 |
+
|
| 427 |
+
# stop_indx = [np.where(sp_dnn[i,:]==(sn_len[i]+1))[0][0] for i in range(sp_dnn.shape[0])]
|
| 428 |
+
# Find the number "sn_len+1" -> the end point
|
| 429 |
+
stop_indx = []
|
| 430 |
+
for i in range(sp_dnn.shape[0]):
|
| 431 |
+
stop = np.where(sp_dnn[i, :] == (sn_len[i] + 1))[0]
|
| 432 |
+
if (
|
| 433 |
+
len(stop) == 0
|
| 434 |
+
): # no end point can be find -> exceeds the maximum length of the generated scanpath
|
| 435 |
+
stop_indx.append(max_sp_len - 1)
|
| 436 |
+
else:
|
| 437 |
+
stop_indx.append(stop[0])
|
| 438 |
+
|
| 439 |
+
# Truncating data after the end point
|
| 440 |
+
sp_dnn_cut = [sp_dnn[i][: stop_indx[i] + 1] for i in range(sp_dnn.shape[0])]
|
| 441 |
+
# replace the last teminal number to cf["max_sn_len"]-1, keep the same as the human scanpath label
|
| 442 |
+
for i in range(len(sp_dnn_cut)):
|
| 443 |
+
sp_dnn_cut[i][-1] = cf["max_sn_len"] - 1
|
| 444 |
+
|
| 445 |
+
# process the human scanpath data, truncating data after the end point
|
| 446 |
+
stop_indx = [
|
| 447 |
+
np.where(sp_human[i, :] == cf["max_sn_len"] - 1)[0][0] for i in range(sp_human.shape[0])
|
| 448 |
+
]
|
| 449 |
+
sp_human_cut = [sp_human[i][: stop_indx[i] + 1] for i in range(sp_human.shape[0])]
|
| 450 |
+
return sp_dnn_cut, sp_human_cut
|
| 451 |
+
|
| 452 |
+
|
| 453 |
+
def celer_load_native_speaker():
|
| 454 |
+
sub_metadata_path = "./Data/celer/metadata.tsv"
|
| 455 |
+
sub_infor = pd.read_csv(sub_metadata_path, delimiter="\t")
|
| 456 |
+
native_sub_list = sub_infor[sub_infor.L1 == "English"].List.values
|
| 457 |
+
return native_sub_list.tolist()
|
| 458 |
+
|
| 459 |
+
|
| 460 |
+
def compute_word_length_celer(arr):
|
| 461 |
+
# length of a punctuation is 0, plus an epsilon to avoid division output inf
|
| 462 |
+
arr = arr.astype("float64")
|
| 463 |
+
arr[arr == 0] = 1 / (0 + 0.5)
|
| 464 |
+
arr[arr != 0] = 1 / (arr[arr != 0])
|
| 465 |
+
return arr
|
| 466 |
+
|
| 467 |
+
|
| 468 |
+
def _process_celer(sn_list, reader_list, word_info_df, eyemovement_df, tokenizer, cf):
|
| 469 |
+
"""
|
| 470 |
+
SN_token_embedding <CLS>, bla, bla, <SEP>
|
| 471 |
+
SP_token_embedding <CLS>, bla, bla, <SEP>
|
| 472 |
+
SP_ordinal_pos 0, bla, bla, max_sp_len
|
| 473 |
+
SP_fix_dur 0, bla, bla, 0
|
| 474 |
+
"""
|
| 475 |
+
SN_input_ids, SN_attention_mask, SN_WORD_len, WORD_ids_sn = [], [], [], []
|
| 476 |
+
SP_input_ids, SP_attention_mask, WORD_ids_sp = [], [], []
|
| 477 |
+
SP_ordinal_pos, SP_landing_pos, SP_fix_dur = [], [], []
|
| 478 |
+
sub_id_list = []
|
| 479 |
+
for sn_id in tqdm(sn_list):
|
| 480 |
+
# process sentence sequence
|
| 481 |
+
sn_df = eyemovement_df[eyemovement_df.sentenceid == sn_id]
|
| 482 |
+
# notice: Each sentence is recorded multiple times in file |word_info_df|.
|
| 483 |
+
sn = word_info_df[word_info_df.sentenceid == sn_id]
|
| 484 |
+
sn = sn[sn["list"] == sn.list.values.tolist()[0]]
|
| 485 |
+
# compute word length for each word
|
| 486 |
+
sn_word_len = compute_word_length_celer(sn.WORD_LEN.values)
|
| 487 |
+
|
| 488 |
+
sn_str = sn.sentence.iloc[-1]
|
| 489 |
+
# nessacery sanity check, when split sentence to words, the length of sentence should match the sentence length recorded in celer dataset
|
| 490 |
+
if sn_id in [
|
| 491 |
+
"1987/w7_019/w7_019.295-3",
|
| 492 |
+
"1987/w7_036/w7_036.147-43",
|
| 493 |
+
"1987/w7_091/w7_091.360-6",
|
| 494 |
+
]:
|
| 495 |
+
# extra inverted commas at the end of the sentence
|
| 496 |
+
sn_str = sn_str[:-3] + sn_str[-1:]
|
| 497 |
+
if sn_id == "1987/w7_085/w7_085.200-18":
|
| 498 |
+
sn_str = sn_str[:43] + sn_str[44:]
|
| 499 |
+
sn_len = len(sn_str.split())
|
| 500 |
+
|
| 501 |
+
# tokenization and padding
|
| 502 |
+
tokenizer.padding_side = "right"
|
| 503 |
+
sn_str = "[CLS]" + " " + sn_str + " " + "[SEP]"
|
| 504 |
+
# pre-tokenized input
|
| 505 |
+
tokens = tokenizer.encode_plus(
|
| 506 |
+
sn_str.split(),
|
| 507 |
+
add_special_tokens=False,
|
| 508 |
+
truncation=False,
|
| 509 |
+
max_length=cf["max_sn_token"],
|
| 510 |
+
padding="max_length",
|
| 511 |
+
return_attention_mask=True,
|
| 512 |
+
is_split_into_words=True,
|
| 513 |
+
)
|
| 514 |
+
encoded_sn = tokens["input_ids"]
|
| 515 |
+
mask_sn = tokens["attention_mask"]
|
| 516 |
+
# use offset mapping to determine if two tokens are in the same word.
|
| 517 |
+
# index start from 0, CLS -> 0 and SEP -> last index
|
| 518 |
+
word_ids_sn = tokens.word_ids()
|
| 519 |
+
word_ids_sn = [val if val is not None else np.nan for val in word_ids_sn]
|
| 520 |
+
|
| 521 |
+
# process fixation sequence
|
| 522 |
+
for sub_id in reader_list:
|
| 523 |
+
sub_df = sn_df[sn_df.list == sub_id]
|
| 524 |
+
# remove fixations on non-words
|
| 525 |
+
sub_df = sub_df.loc[sub_df.CURRENT_FIX_INTEREST_AREA_LABEL != "."]
|
| 526 |
+
if len(sub_df) == 0:
|
| 527 |
+
# no scanpath data found for the subject
|
| 528 |
+
continue
|
| 529 |
+
|
| 530 |
+
# prepare decoder input and output
|
| 531 |
+
sp_word_pos, sp_fix_loc, sp_fix_dur = (
|
| 532 |
+
sub_df.CURRENT_FIX_INTEREST_AREA_ID.values,
|
| 533 |
+
sub_df.CURRENT_FIX_NEAREST_INTEREST_AREA_DISTANCE.values,
|
| 534 |
+
sub_df.CURRENT_FIX_DURATION.values,
|
| 535 |
+
)
|
| 536 |
+
|
| 537 |
+
# dataset is noisy -> sanity check
|
| 538 |
+
# 1) check if recorded fixation duration are within reasonable limits
|
| 539 |
+
# Less than 50ms attempt to merge with neighbouring fixation if fixate is on the same word, otherwise delete
|
| 540 |
+
outlier_indx = np.where(sp_fix_dur < 50)[0]
|
| 541 |
+
if outlier_indx.size > 0:
|
| 542 |
+
for out_idx in range(len(outlier_indx)):
|
| 543 |
+
outlier_i = outlier_indx[out_idx]
|
| 544 |
+
merge_flag = False
|
| 545 |
+
|
| 546 |
+
# outliers are commonly found in the fixation of the last record and the first record, and are removed directly
|
| 547 |
+
if outlier_i == len(sp_fix_dur) - 1 or outlier_i == 0:
|
| 548 |
+
merge_flag = True
|
| 549 |
+
|
| 550 |
+
else:
|
| 551 |
+
if outlier_i - 1 >= 0 and merge_flag == False:
|
| 552 |
+
# try to merge with the left fixation
|
| 553 |
+
if (
|
| 554 |
+
sub_df.iloc[outlier_i].CURRENT_FIX_INTEREST_AREA_LABEL
|
| 555 |
+
== sub_df.iloc[outlier_i - 1].CURRENT_FIX_INTEREST_AREA_LABEL
|
| 556 |
+
):
|
| 557 |
+
sp_fix_dur[outlier_i - 1] = (
|
| 558 |
+
sp_fix_dur[outlier_i - 1] + sp_fix_dur[outlier_i]
|
| 559 |
+
)
|
| 560 |
+
merge_flag = True
|
| 561 |
+
|
| 562 |
+
if outlier_i + 1 < len(sp_fix_dur) and merge_flag == False:
|
| 563 |
+
# try to merge with the right fixation
|
| 564 |
+
if (
|
| 565 |
+
sub_df.iloc[outlier_i].CURRENT_FIX_INTEREST_AREA_LABEL
|
| 566 |
+
== sub_df.iloc[outlier_i + 1].CURRENT_FIX_INTEREST_AREA_LABEL
|
| 567 |
+
):
|
| 568 |
+
sp_fix_dur[outlier_i + 1] = (
|
| 569 |
+
sp_fix_dur[outlier_i + 1] + sp_fix_dur[outlier_i]
|
| 570 |
+
)
|
| 571 |
+
merge_flag = True
|
| 572 |
+
|
| 573 |
+
sp_word_pos = np.delete(sp_word_pos, outlier_i)
|
| 574 |
+
sp_fix_loc = np.delete(sp_fix_loc, outlier_i)
|
| 575 |
+
sp_fix_dur = np.delete(sp_fix_dur, outlier_i)
|
| 576 |
+
sub_df.drop(sub_df.index[outlier_i], axis=0, inplace=True)
|
| 577 |
+
outlier_indx = outlier_indx - 1
|
| 578 |
+
|
| 579 |
+
# 2) scanpath too long, remove outliers, speed up the inference
|
| 580 |
+
if len(sp_word_pos) > 50: # 72/10684
|
| 581 |
+
continue
|
| 582 |
+
# 3)scanpath too short for a normal length sentence
|
| 583 |
+
if len(sp_word_pos) <= 1 and sn_len > 10:
|
| 584 |
+
continue
|
| 585 |
+
|
| 586 |
+
# 4) check landing position feature
|
| 587 |
+
# assign missing value to 'nan'
|
| 588 |
+
sp_fix_loc = np.where(sp_fix_loc == ".", np.nan, sp_fix_loc)
|
| 589 |
+
# convert string of number of float type
|
| 590 |
+
sp_fix_loc = [float(i) for i in sp_fix_loc]
|
| 591 |
+
|
| 592 |
+
# Outliers in calculated landing positions due to lack of valid AOI data, assign to 'nan'
|
| 593 |
+
if np.nanmax(sp_fix_loc) > 35:
|
| 594 |
+
missing_idx = np.where(np.array(sp_fix_loc) > 5)[0]
|
| 595 |
+
for miss in missing_idx:
|
| 596 |
+
if sub_df.iloc[miss].CURRENT_FIX_INTEREST_AREA_LEFT in [
|
| 597 |
+
"NONE",
|
| 598 |
+
"BEFORE",
|
| 599 |
+
"AFTER",
|
| 600 |
+
"BOTH",
|
| 601 |
+
]:
|
| 602 |
+
sp_fix_loc[miss] = np.nan
|
| 603 |
+
else:
|
| 604 |
+
print(
|
| 605 |
+
"Landing position calculation error. Unknown cause, needs to be checked"
|
| 606 |
+
)
|
| 607 |
+
|
| 608 |
+
sp_ordinal_pos = sp_word_pos.astype(int)
|
| 609 |
+
SP_ordinal_pos.append(sp_ordinal_pos)
|
| 610 |
+
SP_fix_dur.append(sp_fix_dur)
|
| 611 |
+
SP_landing_pos.append(sp_fix_loc)
|
| 612 |
+
|
| 613 |
+
sp_token = [sn_str.split()[int(i)] for i in sp_ordinal_pos]
|
| 614 |
+
sp_token_str = "[CLS]" + " " + " ".join(sp_token) + " " + "[SEP]"
|
| 615 |
+
|
| 616 |
+
# tokenization and padding for scanpath, i.e. fixated word sequence
|
| 617 |
+
sp_tokens = tokenizer.encode_plus(
|
| 618 |
+
sp_token_str.split(),
|
| 619 |
+
add_special_tokens=False,
|
| 620 |
+
truncation=False,
|
| 621 |
+
max_length=cf["max_sp_token"],
|
| 622 |
+
padding="max_length",
|
| 623 |
+
return_attention_mask=True,
|
| 624 |
+
is_split_into_words=True,
|
| 625 |
+
)
|
| 626 |
+
encoded_sp = sp_tokens["input_ids"]
|
| 627 |
+
mask_sp = sp_tokens["attention_mask"]
|
| 628 |
+
# index start from 0, CLS -> 0 and SEP -> last index
|
| 629 |
+
word_ids_sp = sp_tokens.word_ids()
|
| 630 |
+
word_ids_sp = [val if val is not None else np.nan for val in word_ids_sp]
|
| 631 |
+
SP_input_ids.append(encoded_sp)
|
| 632 |
+
SP_attention_mask.append(mask_sp)
|
| 633 |
+
WORD_ids_sp.append(word_ids_sp)
|
| 634 |
+
|
| 635 |
+
# sentence information
|
| 636 |
+
SN_input_ids.append(encoded_sn)
|
| 637 |
+
SN_attention_mask.append(mask_sn)
|
| 638 |
+
SN_WORD_len.append(sn_word_len)
|
| 639 |
+
WORD_ids_sn.append(word_ids_sn)
|
| 640 |
+
sub_id_list.append(int(sub_id))
|
| 641 |
+
|
| 642 |
+
# padding for batch computation
|
| 643 |
+
SP_ordinal_pos = pad_seq(SP_ordinal_pos, max_len=(cf["max_sp_len"]), pad_value=cf["max_sn_len"])
|
| 644 |
+
SP_fix_dur = pad_seq(SP_fix_dur, max_len=(cf["max_sp_len"]), pad_value=0)
|
| 645 |
+
SP_landing_pos = pad_seq(SP_landing_pos, cf["max_sp_len"], pad_value=0, dtype=np.float32)
|
| 646 |
+
SN_WORD_len = pad_seq_with_nan(SN_WORD_len, cf["max_sn_len"], dtype=np.float32)
|
| 647 |
+
|
| 648 |
+
# assign type
|
| 649 |
+
SN_input_ids = np.asarray(SN_input_ids, dtype=np.int64)
|
| 650 |
+
SN_attention_mask = np.asarray(SN_attention_mask, dtype=np.float32)
|
| 651 |
+
SP_input_ids = np.asarray(SP_input_ids, dtype=np.int64)
|
| 652 |
+
SP_attention_mask = np.asarray(SP_attention_mask, dtype=np.float32)
|
| 653 |
+
sub_id_list = np.asarray(sub_id_list, dtype=np.int64)
|
| 654 |
+
WORD_ids_sn = np.asarray(WORD_ids_sn)
|
| 655 |
+
WORD_ids_sp = np.asarray(WORD_ids_sp)
|
| 656 |
+
|
| 657 |
+
data = {
|
| 658 |
+
"SN_input_ids": SN_input_ids,
|
| 659 |
+
"SN_attention_mask": SN_attention_mask,
|
| 660 |
+
"SN_WORD_len": SN_WORD_len,
|
| 661 |
+
"WORD_ids_sn": WORD_ids_sn,
|
| 662 |
+
"SP_input_ids": SP_input_ids,
|
| 663 |
+
"SP_attention_mask": SP_attention_mask,
|
| 664 |
+
"WORD_ids_sp": WORD_ids_sp,
|
| 665 |
+
"SP_ordinal_pos": np.array(SP_ordinal_pos),
|
| 666 |
+
"SP_landing_pos": np.array(SP_landing_pos),
|
| 667 |
+
"SP_fix_dur": np.array(SP_fix_dur),
|
| 668 |
+
"sub_id": sub_id_list,
|
| 669 |
+
}
|
| 670 |
+
|
| 671 |
+
return data
|
| 672 |
+
|
| 673 |
+
|
| 674 |
+
class celerdataset(Dataset):
|
| 675 |
+
"""Return celer dataset."""
|
| 676 |
+
|
| 677 |
+
def __init__(self, word_info_df, eyemovement_df, cf, reader_list, sn_list, tokenizer):
|
| 678 |
+
|
| 679 |
+
self.data = _process_celer(
|
| 680 |
+
sn_list, reader_list, word_info_df, eyemovement_df, tokenizer, cf
|
| 681 |
+
)
|
| 682 |
+
|
| 683 |
+
def __len__(self):
|
| 684 |
+
return len(self.data["SN_input_ids"])
|
| 685 |
+
|
| 686 |
+
def __getitem__(self, idx):
|
| 687 |
+
sample = {}
|
| 688 |
+
sample["sn_input_ids"] = self.data["SN_input_ids"][idx, :]
|
| 689 |
+
sample["sn_attention_mask"] = self.data["SN_attention_mask"][idx, :]
|
| 690 |
+
sample["sn_word_len"] = self.data["SN_WORD_len"][idx, :]
|
| 691 |
+
sample["word_ids_sn"] = self.data["WORD_ids_sn"][idx, :]
|
| 692 |
+
|
| 693 |
+
sample["sp_input_ids"] = self.data["SP_input_ids"][idx, :]
|
| 694 |
+
sample["sp_attention_mask"] = self.data["SP_attention_mask"][idx, :]
|
| 695 |
+
sample["word_ids_sp"] = self.data["WORD_ids_sp"][idx, :]
|
| 696 |
+
|
| 697 |
+
sample["sp_pos"] = self.data["SP_ordinal_pos"][idx, :]
|
| 698 |
+
sample["sp_fix_dur"] = self.data["SP_fix_dur"][idx, :]
|
| 699 |
+
sample["sp_landing_pos"] = self.data["SP_landing_pos"][idx, :]
|
| 700 |
+
|
| 701 |
+
sample["sub_id"] = self.data["sub_id"][idx]
|
| 702 |
+
|
| 703 |
+
return sample
|
| 704 |
+
|
| 705 |
+
|
| 706 |
+
def one_hot_encode(arr, dim):
|
| 707 |
+
# one hot encode
|
| 708 |
+
onehot_encoded = np.zeros((arr.shape[0], dim))
|
| 709 |
+
for idx, value in enumerate(arr):
|
| 710 |
+
onehot_encoded[idx, value] = 1
|
| 711 |
+
|
| 712 |
+
return onehot_encoded
|
| 713 |
+
|
| 714 |
+
|
| 715 |
+
def gradient_clipping(dnn_model, clip=10):
|
| 716 |
+
torch.nn.utils.clip_grad_norm_(dnn_model.parameters(), clip)
|