Di0nigi commited on
Commit
c689a69
·
verified ·
1 Parent(s): d687db2

First commit

Browse files
.gitattributes CHANGED
@@ -1,35 +1,6 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
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
- license: mit
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)