Aryan Mishra commited on
Commit
119802d
·
1 Parent(s): 2f9be58

feat: Phase 3 - baseline and XLM-R fine-tuning

Browse files
Files changed (30) hide show
  1. data/tokenized/absa_cls_dataset/dataset_dict.json +1 -0
  2. data/tokenized/absa_cls_dataset/test/data-00000-of-00001.arrow +3 -0
  3. data/tokenized/absa_cls_dataset/test/dataset_info.json +26 -0
  4. data/tokenized/absa_cls_dataset/test/state.json +13 -0
  5. data/tokenized/absa_cls_dataset/train/data-00000-of-00001.arrow +3 -0
  6. data/tokenized/absa_cls_dataset/train/dataset_info.json +26 -0
  7. data/tokenized/absa_cls_dataset/train/state.json +13 -0
  8. data/tokenized/absa_cls_dataset/validation/data-00000-of-00001.arrow +3 -0
  9. data/tokenized/absa_cls_dataset/validation/dataset_info.json +26 -0
  10. data/tokenized/absa_cls_dataset/validation/state.json +13 -0
  11. data/tokenized/absa_ner_dataset/dataset_dict.json +1 -0
  12. data/tokenized/absa_ner_dataset/test/data-00000-of-00001.arrow +3 -0
  13. data/tokenized/absa_ner_dataset/test/dataset_info.json +29 -0
  14. data/tokenized/absa_ner_dataset/test/state.json +13 -0
  15. data/tokenized/absa_ner_dataset/train/data-00000-of-00001.arrow +3 -0
  16. data/tokenized/absa_ner_dataset/train/dataset_info.json +29 -0
  17. data/tokenized/absa_ner_dataset/train/state.json +13 -0
  18. data/tokenized/absa_ner_dataset/validation/data-00000-of-00001.arrow +3 -0
  19. data/tokenized/absa_ner_dataset/validation/dataset_info.json +29 -0
  20. data/tokenized/absa_ner_dataset/validation/state.json +13 -0
  21. mlruns/1/f7e324a252a94812b0d4158371276cb0/artifacts/confusion_matrix.json +28 -0
  22. notebooks/03_model_comparison.ipynb +101 -0
  23. notebooks/03_train_colab.ipynb +128 -0
  24. scripts/generate_notebooks.py +73 -0
  25. scripts/mlflow_ui.sh +10 -0
  26. src/data/bio_tagger.py +97 -0
  27. src/data/hf_dataset.py +156 -0
  28. src/evaluation/cross_lingual_eval.py +114 -0
  29. src/training/mlflow_utils.py +73 -0
  30. tests/test_bio_tagger.py +51 -0
data/tokenized/absa_cls_dataset/dataset_dict.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"splits": ["train", "validation", "test"]}
data/tokenized/absa_cls_dataset/test/data-00000-of-00001.arrow ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1eb8d8230357e82431b68c2226770b209f0a61fe34425c0fd037b17f913a028c
3
+ size 101864
data/tokenized/absa_cls_dataset/test/dataset_info.json ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "citation": "",
3
+ "description": "",
4
+ "features": {
5
+ "label": {
6
+ "dtype": "int64",
7
+ "_type": "Value"
8
+ },
9
+ "input_ids": {
10
+ "feature": {
11
+ "dtype": "int32",
12
+ "_type": "Value"
13
+ },
14
+ "_type": "Sequence"
15
+ },
16
+ "attention_mask": {
17
+ "feature": {
18
+ "dtype": "int8",
19
+ "_type": "Value"
20
+ },
21
+ "_type": "Sequence"
22
+ }
23
+ },
24
+ "homepage": "",
25
+ "license": ""
26
+ }
data/tokenized/absa_cls_dataset/test/state.json ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_data_files": [
3
+ {
4
+ "filename": "data-00000-of-00001.arrow"
5
+ }
6
+ ],
7
+ "_fingerprint": "b686d9c13fd54a9d",
8
+ "_format_columns": null,
9
+ "_format_kwargs": {},
10
+ "_format_type": null,
11
+ "_output_all_columns": false,
12
+ "_split": null
13
+ }
data/tokenized/absa_cls_dataset/train/data-00000-of-00001.arrow ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:74827f37b8915b4946a33e030d5833d109bfcf9d9d847edf54fd830fe33a0a3d
3
+ size 808112
data/tokenized/absa_cls_dataset/train/dataset_info.json ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "citation": "",
3
+ "description": "",
4
+ "features": {
5
+ "label": {
6
+ "dtype": "int64",
7
+ "_type": "Value"
8
+ },
9
+ "input_ids": {
10
+ "feature": {
11
+ "dtype": "int32",
12
+ "_type": "Value"
13
+ },
14
+ "_type": "Sequence"
15
+ },
16
+ "attention_mask": {
17
+ "feature": {
18
+ "dtype": "int8",
19
+ "_type": "Value"
20
+ },
21
+ "_type": "Sequence"
22
+ }
23
+ },
24
+ "homepage": "",
25
+ "license": ""
26
+ }
data/tokenized/absa_cls_dataset/train/state.json ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_data_files": [
3
+ {
4
+ "filename": "data-00000-of-00001.arrow"
5
+ }
6
+ ],
7
+ "_fingerprint": "59c1808a1a0be248",
8
+ "_format_columns": null,
9
+ "_format_kwargs": {},
10
+ "_format_type": null,
11
+ "_output_all_columns": false,
12
+ "_split": null
13
+ }
data/tokenized/absa_cls_dataset/validation/data-00000-of-00001.arrow ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a9da21b3401a16a246766047cda0695dcf07ef7d3c7eb64804ed9f59f0746f08
3
+ size 105136
data/tokenized/absa_cls_dataset/validation/dataset_info.json ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "citation": "",
3
+ "description": "",
4
+ "features": {
5
+ "label": {
6
+ "dtype": "int64",
7
+ "_type": "Value"
8
+ },
9
+ "input_ids": {
10
+ "feature": {
11
+ "dtype": "int32",
12
+ "_type": "Value"
13
+ },
14
+ "_type": "Sequence"
15
+ },
16
+ "attention_mask": {
17
+ "feature": {
18
+ "dtype": "int8",
19
+ "_type": "Value"
20
+ },
21
+ "_type": "Sequence"
22
+ }
23
+ },
24
+ "homepage": "",
25
+ "license": ""
26
+ }
data/tokenized/absa_cls_dataset/validation/state.json ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_data_files": [
3
+ {
4
+ "filename": "data-00000-of-00001.arrow"
5
+ }
6
+ ],
7
+ "_fingerprint": "a33b9cfd2402e633",
8
+ "_format_columns": null,
9
+ "_format_kwargs": {},
10
+ "_format_type": null,
11
+ "_output_all_columns": false,
12
+ "_split": null
13
+ }
data/tokenized/absa_ner_dataset/dataset_dict.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"splits": ["train", "validation", "test"]}
data/tokenized/absa_ner_dataset/test/data-00000-of-00001.arrow ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e1ec218e3b5b0c88fd86358c8e87f44709510cff43c41a81dac98274ce4eb0e7
3
+ size 141712
data/tokenized/absa_ner_dataset/test/dataset_info.json ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "citation": "",
3
+ "description": "",
4
+ "features": {
5
+ "input_ids": {
6
+ "feature": {
7
+ "dtype": "int32",
8
+ "_type": "Value"
9
+ },
10
+ "_type": "Sequence"
11
+ },
12
+ "attention_mask": {
13
+ "feature": {
14
+ "dtype": "int8",
15
+ "_type": "Value"
16
+ },
17
+ "_type": "Sequence"
18
+ },
19
+ "labels": {
20
+ "feature": {
21
+ "dtype": "int64",
22
+ "_type": "Value"
23
+ },
24
+ "_type": "Sequence"
25
+ }
26
+ },
27
+ "homepage": "",
28
+ "license": ""
29
+ }
data/tokenized/absa_ner_dataset/test/state.json ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_data_files": [
3
+ {
4
+ "filename": "data-00000-of-00001.arrow"
5
+ }
6
+ ],
7
+ "_fingerprint": "0fa99b895566c7b5",
8
+ "_format_columns": null,
9
+ "_format_kwargs": {},
10
+ "_format_type": null,
11
+ "_output_all_columns": false,
12
+ "_split": null
13
+ }
data/tokenized/absa_ner_dataset/train/data-00000-of-00001.arrow ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:76c719d76d95127fc82691bbe1455e988c0c593fcdd510f270deac3e625cb929
3
+ size 1101368
data/tokenized/absa_ner_dataset/train/dataset_info.json ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "citation": "",
3
+ "description": "",
4
+ "features": {
5
+ "input_ids": {
6
+ "feature": {
7
+ "dtype": "int32",
8
+ "_type": "Value"
9
+ },
10
+ "_type": "Sequence"
11
+ },
12
+ "attention_mask": {
13
+ "feature": {
14
+ "dtype": "int8",
15
+ "_type": "Value"
16
+ },
17
+ "_type": "Sequence"
18
+ },
19
+ "labels": {
20
+ "feature": {
21
+ "dtype": "int64",
22
+ "_type": "Value"
23
+ },
24
+ "_type": "Sequence"
25
+ }
26
+ },
27
+ "homepage": "",
28
+ "license": ""
29
+ }
data/tokenized/absa_ner_dataset/train/state.json ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_data_files": [
3
+ {
4
+ "filename": "data-00000-of-00001.arrow"
5
+ }
6
+ ],
7
+ "_fingerprint": "535b22b0ddf3661d",
8
+ "_format_columns": null,
9
+ "_format_kwargs": {},
10
+ "_format_type": null,
11
+ "_output_all_columns": false,
12
+ "_split": null
13
+ }
data/tokenized/absa_ner_dataset/validation/data-00000-of-00001.arrow ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f94a6981561bcaa124a70fb560b93e355e57df287e9e721a2e6e887b6689b236
3
+ size 144312
data/tokenized/absa_ner_dataset/validation/dataset_info.json ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "citation": "",
3
+ "description": "",
4
+ "features": {
5
+ "input_ids": {
6
+ "feature": {
7
+ "dtype": "int32",
8
+ "_type": "Value"
9
+ },
10
+ "_type": "Sequence"
11
+ },
12
+ "attention_mask": {
13
+ "feature": {
14
+ "dtype": "int8",
15
+ "_type": "Value"
16
+ },
17
+ "_type": "Sequence"
18
+ },
19
+ "labels": {
20
+ "feature": {
21
+ "dtype": "int64",
22
+ "_type": "Value"
23
+ },
24
+ "_type": "Sequence"
25
+ }
26
+ },
27
+ "homepage": "",
28
+ "license": ""
29
+ }
data/tokenized/absa_ner_dataset/validation/state.json ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_data_files": [
3
+ {
4
+ "filename": "data-00000-of-00001.arrow"
5
+ }
6
+ ],
7
+ "_fingerprint": "caedb7425c742cc1",
8
+ "_format_columns": null,
9
+ "_format_kwargs": {},
10
+ "_format_type": null,
11
+ "_output_all_columns": false,
12
+ "_split": null
13
+ }
mlruns/1/f7e324a252a94812b0d4158371276cb0/artifacts/confusion_matrix.json ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "confusion_matrix": [
3
+ [
4
+ 246,
5
+ 26,
6
+ 31,
7
+ 13
8
+ ],
9
+ [
10
+ 14,
11
+ 120,
12
+ 26,
13
+ 7
14
+ ],
15
+ [
16
+ 20,
17
+ 29,
18
+ 60,
19
+ 1
20
+ ],
21
+ [
22
+ 3,
23
+ 7,
24
+ 0,
25
+ 3
26
+ ]
27
+ ]
28
+ }
notebooks/03_model_comparison.ipynb ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "metadata": {},
6
+ "source": [
7
+ "# Model Comparison\n",
8
+ "\n",
9
+ "This notebook connects to the MLflow tracking server and compares the results of our models.\n"
10
+ ]
11
+ },
12
+ {
13
+ "cell_type": "code",
14
+ "metadata": {},
15
+ "execution_count": null,
16
+ "outputs": [],
17
+ "source": [
18
+ "import mlflow\n",
19
+ "import pandas as pd\n",
20
+ "import matplotlib.pyplot as plt\n",
21
+ "import seaborn as sns\n",
22
+ "import json\n",
23
+ "\n",
24
+ "mlflow.set_tracking_uri('sqlite:///mlflow/mlflow.db')\n"
25
+ ]
26
+ },
27
+ {
28
+ "cell_type": "code",
29
+ "metadata": {},
30
+ "execution_count": null,
31
+ "outputs": [],
32
+ "source": [
33
+ "# Load all runs\n",
34
+ "experiment = mlflow.get_experiment_by_name('multilingual-absa')\n",
35
+ "df = mlflow.search_runs(experiment_ids=[experiment.experiment_id])\n",
36
+ "display(df.head())\n"
37
+ ]
38
+ },
39
+ {
40
+ "cell_type": "code",
41
+ "metadata": {},
42
+ "execution_count": null,
43
+ "outputs": [],
44
+ "source": [
45
+ "# Bar chart: macro-F1 comparison\n",
46
+ "metrics = df[['tags.mlflow.runName', 'metrics.eval_macro_f1', 'metrics.test_f1', 'metrics.test_macro_f1', 'metrics.hindi_zero_shot_macro_f1']].fillna(0)\n",
47
+ "metrics['Best F1'] = metrics[['metrics.eval_macro_f1', 'metrics.test_f1', 'metrics.test_macro_f1']].max(axis=1)\n",
48
+ "\n",
49
+ "plt.figure(figsize=(10, 6))\n",
50
+ "sns.barplot(data=metrics, x='tags.mlflow.runName', y='Best F1')\n",
51
+ "plt.title('Model Comparison by Macro-F1 / Span-F1')\n",
52
+ "plt.xticks(rotation=45)\n",
53
+ "plt.show()\n"
54
+ ]
55
+ },
56
+ {
57
+ "cell_type": "code",
58
+ "metadata": {},
59
+ "execution_count": null,
60
+ "outputs": [],
61
+ "source": [
62
+ "# Load confusion matrix for best sentiment classifier\n",
63
+ "# Note: Assuming the confusion_matrix.json artifact was downloaded or parsed.\n",
64
+ "print('Confusion Matrix (Placeholder for artifact loading)')\n"
65
+ ]
66
+ },
67
+ {
68
+ "cell_type": "code",
69
+ "metadata": {},
70
+ "execution_count": null,
71
+ "outputs": [],
72
+ "source": [
73
+ "# 5 Example Predictions\n",
74
+ "print('Example 1: The food was great but service was slow.')\n",
75
+ "print('Example 2: El sistema operativo es muy estable.')\n",
76
+ "print('... (Load pipeline and infer here)')\n"
77
+ ]
78
+ }
79
+ ],
80
+ "metadata": {
81
+ "kernelspec": {
82
+ "display_name": "Python 3",
83
+ "language": "python",
84
+ "name": "python3"
85
+ },
86
+ "language_info": {
87
+ "codemirror_mode": {
88
+ "name": "ipython",
89
+ "version": 3
90
+ },
91
+ "file_extension": ".py",
92
+ "mimetype": "text/x-python",
93
+ "name": "python",
94
+ "nbconvert_exporter": "python",
95
+ "pygments_lexer": "ipython3",
96
+ "version": "3.11.0"
97
+ }
98
+ },
99
+ "nbformat": 4,
100
+ "nbformat_minor": 4
101
+ }
notebooks/03_train_colab.ipynb ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "metadata": {},
6
+ "source": [
7
+ "# Google Colab Training Notebook\n",
8
+ "\n",
9
+ "This notebook is intended to be run on Google Colab with a T4 GPU. It clones the repo, installs dependencies, and runs the training scripts.\n"
10
+ ]
11
+ },
12
+ {
13
+ "cell_type": "code",
14
+ "metadata": {},
15
+ "execution_count": null,
16
+ "outputs": [],
17
+ "source": [
18
+ "!git clone https://github.com/Aryanmishra-dev/Multilingual-Absa.git\n",
19
+ "%cd Multilingual-Absa\n",
20
+ "!pip install -r requirements.txt\n"
21
+ ]
22
+ },
23
+ {
24
+ "cell_type": "code",
25
+ "metadata": {},
26
+ "execution_count": null,
27
+ "outputs": [],
28
+ "source": [
29
+ "# Mount Google Drive to save models and MLflow logs persistently\n",
30
+ "from google.colab import drive\n",
31
+ "drive.mount('/content/drive')\n"
32
+ ]
33
+ },
34
+ {
35
+ "cell_type": "code",
36
+ "metadata": {},
37
+ "execution_count": null,
38
+ "outputs": [],
39
+ "source": [
40
+ "# Create symlinks or copy data if needed\n",
41
+ "# Assuming data is in the repo for now\n",
42
+ "!mkdir -p /content/drive/MyDrive/ABSA_models\n"
43
+ ]
44
+ },
45
+ {
46
+ "cell_type": "code",
47
+ "metadata": {},
48
+ "execution_count": null,
49
+ "outputs": [],
50
+ "source": [
51
+ "# Prepare dataset\n",
52
+ "!PYTHONPATH=. python src/data/hf_dataset.py\n"
53
+ ]
54
+ },
55
+ {
56
+ "cell_type": "code",
57
+ "metadata": {},
58
+ "execution_count": null,
59
+ "outputs": [],
60
+ "source": [
61
+ "# Run Aspect Extraction Training\n",
62
+ "!PYTHONPATH=. python src/models/train_aspect_extraction.py\n"
63
+ ]
64
+ },
65
+ {
66
+ "cell_type": "code",
67
+ "metadata": {},
68
+ "execution_count": null,
69
+ "outputs": [],
70
+ "source": [
71
+ "# Run Sentiment Classification Training\n",
72
+ "!PYTHONPATH=. python src/models/train_sentiment.py\n"
73
+ ]
74
+ },
75
+ {
76
+ "cell_type": "code",
77
+ "metadata": {},
78
+ "execution_count": null,
79
+ "outputs": [],
80
+ "source": [
81
+ "# Run Baseline as well\n",
82
+ "!PYTHONPATH=. python src/models/baseline.py\n"
83
+ ]
84
+ },
85
+ {
86
+ "cell_type": "code",
87
+ "metadata": {},
88
+ "execution_count": null,
89
+ "outputs": [],
90
+ "source": [
91
+ "# Cross-lingual Evaluation\n",
92
+ "!PYTHONPATH=. python src/evaluation/cross_lingual_eval.py\n"
93
+ ]
94
+ },
95
+ {
96
+ "cell_type": "code",
97
+ "metadata": {},
98
+ "execution_count": null,
99
+ "outputs": [],
100
+ "source": [
101
+ "# Copy models back to Drive\n",
102
+ "!cp -r models/* /content/drive/MyDrive/ABSA_models/\n",
103
+ "!cp -r mlflow /content/drive/MyDrive/ABSA_models/\n"
104
+ ]
105
+ }
106
+ ],
107
+ "metadata": {
108
+ "kernelspec": {
109
+ "display_name": "Python 3",
110
+ "language": "python",
111
+ "name": "python3"
112
+ },
113
+ "language_info": {
114
+ "codemirror_mode": {
115
+ "name": "ipython",
116
+ "version": 3
117
+ },
118
+ "file_extension": ".py",
119
+ "mimetype": "text/x-python",
120
+ "name": "python",
121
+ "nbconvert_exporter": "python",
122
+ "pygments_lexer": "ipython3",
123
+ "version": "3.11.0"
124
+ }
125
+ },
126
+ "nbformat": 4,
127
+ "nbformat_minor": 4
128
+ }
scripts/generate_notebooks.py ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from pathlib import Path
3
+
4
+ def create_notebook(filename: str, cells_content: list):
5
+ cells = []
6
+ for content, cell_type in cells_content:
7
+ cells.append({
8
+ "cell_type": cell_type,
9
+ "metadata": {},
10
+ "execution_count": None if cell_type == "code" else None,
11
+ "outputs": [] if cell_type == "code" else None,
12
+ "source": [line + "\n" for line in content.split("\n")]
13
+ })
14
+
15
+ # Clean up outputs/execution_count for markdown
16
+ if cell_type == "markdown":
17
+ del cells[-1]["execution_count"]
18
+ del cells[-1]["outputs"]
19
+
20
+ notebook = {
21
+ "cells": cells,
22
+ "metadata": {
23
+ "kernelspec": {
24
+ "display_name": "Python 3",
25
+ "language": "python",
26
+ "name": "python3"
27
+ },
28
+ "language_info": {
29
+ "codemirror_mode": {"name": "ipython", "version": 3},
30
+ "file_extension": ".py",
31
+ "mimetype": "text/x-python",
32
+ "name": "python",
33
+ "nbconvert_exporter": "python",
34
+ "pygments_lexer": "ipython3",
35
+ "version": "3.11.0"
36
+ }
37
+ },
38
+ "nbformat": 4,
39
+ "nbformat_minor": 4
40
+ }
41
+
42
+ Path("notebooks").mkdir(parents=True, exist_ok=True)
43
+ with open(f"notebooks/{filename}", "w") as f:
44
+ json.dump(notebook, f, indent=2)
45
+
46
+ def main():
47
+ colab_cells = [
48
+ ("# Google Colab Training Notebook\n\nThis notebook is intended to be run on Google Colab with a T4 GPU. It clones the repo, installs dependencies, and runs the training scripts.", "markdown"),
49
+ ("!git clone https://github.com/Aryanmishra-dev/Multilingual-Absa.git\n%cd Multilingual-Absa\n!pip install -r requirements.txt", "code"),
50
+ ("# Mount Google Drive to save models and MLflow logs persistently\nfrom google.colab import drive\ndrive.mount('/content/drive')", "code"),
51
+ ("# Create symlinks or copy data if needed\n# Assuming data is in the repo for now\n!mkdir -p /content/drive/MyDrive/ABSA_models", "code"),
52
+ ("# Prepare dataset\n!PYTHONPATH=. python src/data/hf_dataset.py", "code"),
53
+ ("# Run Aspect Extraction Training\n!PYTHONPATH=. python src/models/train_aspect_extraction.py", "code"),
54
+ ("# Run Sentiment Classification Training\n!PYTHONPATH=. python src/models/train_sentiment.py", "code"),
55
+ ("# Run Baseline as well\n!PYTHONPATH=. python src/models/baseline.py", "code"),
56
+ ("# Cross-lingual Evaluation\n!PYTHONPATH=. python src/evaluation/cross_lingual_eval.py", "code"),
57
+ ("# Copy models back to Drive\n!cp -r models/* /content/drive/MyDrive/ABSA_models/\n!cp -r mlflow /content/drive/MyDrive/ABSA_models/", "code")
58
+ ]
59
+
60
+ comparison_cells = [
61
+ ("# Model Comparison\n\nThis notebook connects to the MLflow tracking server and compares the results of our models.", "markdown"),
62
+ ("import mlflow\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nimport json\n\nmlflow.set_tracking_uri('sqlite:///mlflow/mlflow.db')", "code"),
63
+ ("# Load all runs\nexperiment = mlflow.get_experiment_by_name('multilingual-absa')\ndf = mlflow.search_runs(experiment_ids=[experiment.experiment_id])\ndisplay(df.head())", "code"),
64
+ ("# Bar chart: macro-F1 comparison\nmetrics = df[['tags.mlflow.runName', 'metrics.eval_macro_f1', 'metrics.test_f1', 'metrics.test_macro_f1', 'metrics.hindi_zero_shot_macro_f1']].fillna(0)\nmetrics['Best F1'] = metrics[['metrics.eval_macro_f1', 'metrics.test_f1', 'metrics.test_macro_f1']].max(axis=1)\n\nplt.figure(figsize=(10, 6))\nsns.barplot(data=metrics, x='tags.mlflow.runName', y='Best F1')\nplt.title('Model Comparison by Macro-F1 / Span-F1')\nplt.xticks(rotation=45)\nplt.show()", "code"),
65
+ ("# Load confusion matrix for best sentiment classifier\n# Note: Assuming the confusion_matrix.json artifact was downloaded or parsed.\nprint('Confusion Matrix (Placeholder for artifact loading)')", "code"),
66
+ ("# 5 Example Predictions\nprint('Example 1: The food was great but service was slow.')\nprint('Example 2: El sistema operativo es muy estable.')\nprint('... (Load pipeline and infer here)')", "code")
67
+ ]
68
+
69
+ create_notebook("03_train_colab.ipynb", colab_cells)
70
+ create_notebook("03_model_comparison.ipynb", comparison_cells)
71
+
72
+ if __name__ == '__main__':
73
+ main()
scripts/mlflow_ui.sh ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # Script to launch MLflow UI
3
+ # Use sqlite backend as specified
4
+
5
+ # Ensure the directory exists
6
+ mkdir -p mlflow
7
+
8
+ echo "Starting MLflow UI with SQLite backend..."
9
+ echo "Access the UI at http://localhost:5000"
10
+ mlflow ui --backend-store-uri sqlite:///mlflow/mlflow.db --host 0.0.0.0 --port 5000
src/data/bio_tagger.py ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ from typing import List, Dict, Any, Tuple
3
+
4
+ def tokenize(text: str) -> List[Tuple[str, int, int]]:
5
+ """
6
+ Tokenizes text by words, returning tokens and their start/end character offsets.
7
+ Uses simple regex based tokenization to preserve whitespace semantics for BIO tagging.
8
+ """
9
+ tokens = []
10
+ # Match non-whitespace characters
11
+ for match in re.finditer(r'\S+', text):
12
+ tokens.append((match.group(), match.start(), match.end()))
13
+ return tokens
14
+
15
+ def convert_to_bio(text: str, aspect_terms: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
16
+ """
17
+ Converts text and aspect spans to BIO tagged tokens.
18
+
19
+ Args:
20
+ text: The input review string.
21
+ aspect_terms: List of dictionaries with 'term', 'from', and 'to' keys.
22
+
23
+ Returns:
24
+ List of dictionaries with 'token' and 'label' (B-ASP, I-ASP, O).
25
+ """
26
+ tokens = tokenize(text)
27
+
28
+ # Sort aspects by start index
29
+ sorted_aspects = sorted(aspect_terms, key=lambda x: x['from'])
30
+
31
+ bio_tags = []
32
+ aspect_idx = 0
33
+ num_aspects = len(sorted_aspects)
34
+
35
+ for token_str, t_start, t_end in tokens:
36
+ label = "O"
37
+
38
+ # Move aspect pointer if we've passed the current aspect completely
39
+ while aspect_idx < num_aspects and sorted_aspects[aspect_idx]['to'] <= t_start:
40
+ aspect_idx += 1
41
+
42
+ if aspect_idx < num_aspects:
43
+ curr_aspect = sorted_aspects[aspect_idx]
44
+ a_start = curr_aspect['from']
45
+ a_end = curr_aspect['to']
46
+
47
+ # Check overlap
48
+ if not (t_end <= a_start or t_start >= a_end):
49
+ # There is overlap
50
+ # If this token overlaps with the start of the aspect
51
+ if t_start <= a_start or (len(bio_tags) > 0 and bio_tags[-1]['label'] == "O" and t_start > a_start):
52
+ label = "B-ASP"
53
+ else:
54
+ # Check if previous tag was B-ASP or I-ASP for the *same* aspect
55
+ if len(bio_tags) > 0 and bio_tags[-1]['label'] in ("B-ASP", "I-ASP"):
56
+ label = "I-ASP"
57
+ else:
58
+ label = "B-ASP"
59
+
60
+ bio_tags.append({"token": token_str, "label": label})
61
+
62
+ return bio_tags
63
+
64
+ def bio_to_aspects(tokens: List[str], labels: List[str]) -> List[str]:
65
+ """
66
+ Converts BIO tags back to a list of aspect terms.
67
+
68
+ Args:
69
+ tokens: List of string tokens.
70
+ labels: List of BIO labels corresponding to the tokens.
71
+
72
+ Returns:
73
+ List of extracted aspect term strings.
74
+ """
75
+ aspects = []
76
+ current_aspect = []
77
+
78
+ for token, label in zip(tokens, labels):
79
+ if label == "B-ASP":
80
+ if current_aspect:
81
+ aspects.append(" ".join(current_aspect))
82
+ current_aspect = [token]
83
+ elif label == "I-ASP":
84
+ if current_aspect:
85
+ current_aspect.append(token)
86
+ else:
87
+ # Invalid sequence (I-ASP without B-ASP), treat as B-ASP
88
+ current_aspect = [token]
89
+ else: # O
90
+ if current_aspect:
91
+ aspects.append(" ".join(current_aspect))
92
+ current_aspect = []
93
+
94
+ if current_aspect:
95
+ aspects.append(" ".join(current_aspect))
96
+
97
+ return aspects
src/data/hf_dataset.py ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import numpy as np
3
+ import pandas as pd
4
+ from pathlib import Path
5
+ from typing import List, Dict, Any, Tuple
6
+ from datasets import Dataset, DatasetDict
7
+ from transformers import AutoTokenizer
8
+ from sklearn.model_selection import train_test_split
9
+ from src.data.bio_tagger import convert_to_bio
10
+
11
+ np.random.seed(42)
12
+
13
+ def load_data(file_paths: List[Path]) -> List[Dict[str, Any]]:
14
+ data = []
15
+ for path in file_paths:
16
+ with open(path, 'r', encoding='utf-8') as f:
17
+ for line in f:
18
+ if line.strip():
19
+ data.append(json.loads(line))
20
+ return data
21
+
22
+ def prepare_ner_data(data: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
23
+ """Prepares data for Token Classification (NER)."""
24
+ ner_data = []
25
+ label_map = {"O": 0, "B-ASP": 1, "I-ASP": 2}
26
+
27
+ for item in data:
28
+ text = item['text']
29
+ aspects = item.get('aspect_terms', [])
30
+ bio_tags = convert_to_bio(text, aspects)
31
+
32
+ tokens = [t['token'] for t in bio_tags]
33
+ ner_tags = [label_map[t['label']] for t in bio_tags]
34
+
35
+ ner_data.append({
36
+ "tokens": tokens,
37
+ "ner_tags": ner_tags,
38
+ "id": item.get("id", str(len(ner_data)))
39
+ })
40
+ return ner_data
41
+
42
+ def prepare_cls_data(data: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
43
+ """Prepares data for Sequence Classification (Sentiment)."""
44
+ cls_data = []
45
+ sentiment_map = {"positive": 0, "negative": 1, "neutral": 2, "conflict": 3}
46
+
47
+ for item in data:
48
+ text = item['text']
49
+ aspects = item.get('aspect_terms', [])
50
+
51
+ for aspect in aspects:
52
+ term = aspect['term']
53
+ polarity = aspect['polarity']
54
+
55
+ if polarity not in sentiment_map:
56
+ continue
57
+
58
+ cls_data.append({
59
+ "text": text,
60
+ "aspect_term": term,
61
+ "label": sentiment_map[polarity],
62
+ "id": f"{item.get('id', str(len(cls_data)))}_{term}"
63
+ })
64
+ return cls_data
65
+
66
+ def align_labels_with_tokens(labels, word_ids):
67
+ new_labels = []
68
+ current_word = None
69
+ for word_id in word_ids:
70
+ if word_id is None:
71
+ new_labels.append(-100)
72
+ elif word_id != current_word:
73
+ new_labels.append(labels[word_id])
74
+ current_word = word_id
75
+ else:
76
+ new_labels.append(-100)
77
+ return new_labels
78
+
79
+ def main():
80
+ data_dir = Path("data/processed")
81
+ output_dir = Path("data/tokenized")
82
+ output_dir.mkdir(parents=True, exist_ok=True)
83
+
84
+ # Load all English SemEval data
85
+ train_path = data_dir / "semeval_train.jsonl"
86
+ test_path = data_dir / "semeval_test.jsonl"
87
+ all_data = load_data([train_path, test_path])
88
+
89
+ # Prepare datasets
90
+ ner_data = prepare_ner_data(all_data)
91
+ cls_data = prepare_cls_data(all_data)
92
+
93
+ tokenizer = AutoTokenizer.from_pretrained("xlm-roberta-base")
94
+
95
+ # ---------------------------------------------------------
96
+ # 1. Token Classification (NER) Dataset
97
+ # ---------------------------------------------------------
98
+ ner_df = pd.DataFrame(ner_data)
99
+
100
+ # Split 80/10/10
101
+ # For NER, we don't have a single sentiment to stratify on easily, so random split
102
+ train_ner, temp_ner = train_test_split(ner_df, test_size=0.2, random_state=42)
103
+ val_ner, test_ner = train_test_split(temp_ner, test_size=0.5, random_state=42)
104
+
105
+ def tokenize_and_align_labels(examples):
106
+ tokenized_inputs = tokenizer(
107
+ examples["tokens"], truncation=True, is_split_into_words=True, max_length=128
108
+ )
109
+ labels = []
110
+ for i, label in enumerate(examples[f"ner_tags"]):
111
+ word_ids = tokenized_inputs.word_ids(batch_index=i)
112
+ labels.append(align_labels_with_tokens(label, word_ids))
113
+ tokenized_inputs["labels"] = labels
114
+ return tokenized_inputs
115
+
116
+ ner_dataset = DatasetDict({
117
+ "train": Dataset.from_pandas(train_ner, preserve_index=False),
118
+ "validation": Dataset.from_pandas(val_ner, preserve_index=False),
119
+ "test": Dataset.from_pandas(test_ner, preserve_index=False),
120
+ })
121
+
122
+ tokenized_ner = ner_dataset.map(tokenize_and_align_labels, batched=True, remove_columns=["tokens", "ner_tags", "id"])
123
+ tokenized_ner.save_to_disk(str(output_dir / "absa_ner_dataset"))
124
+ print(f"NER Dataset saved to {output_dir / 'absa_ner_dataset'}")
125
+
126
+ # ---------------------------------------------------------
127
+ # 2. Sequence Classification (Sentiment) Dataset
128
+ # ---------------------------------------------------------
129
+ cls_df = pd.DataFrame(cls_data)
130
+
131
+ # Stratified split 80/10/10 based on label
132
+ train_cls, temp_cls = train_test_split(cls_df, test_size=0.2, random_state=42, stratify=cls_df['label'])
133
+ val_cls, test_cls = train_test_split(temp_cls, test_size=0.5, random_state=42, stratify=temp_cls['label'])
134
+
135
+ def tokenize_cls(examples):
136
+ # Format: [CLS] text [SEP] aspect_term [SEP]
137
+ return tokenizer(
138
+ examples["text"],
139
+ examples["aspect_term"],
140
+ truncation=True,
141
+ max_length=128,
142
+ padding=False
143
+ )
144
+
145
+ cls_dataset = DatasetDict({
146
+ "train": Dataset.from_pandas(train_cls, preserve_index=False),
147
+ "validation": Dataset.from_pandas(val_cls, preserve_index=False),
148
+ "test": Dataset.from_pandas(test_cls, preserve_index=False),
149
+ })
150
+
151
+ tokenized_cls = cls_dataset.map(tokenize_cls, batched=True, remove_columns=["text", "aspect_term", "id"])
152
+ tokenized_cls.save_to_disk(str(output_dir / "absa_cls_dataset"))
153
+ print(f"CLS Dataset saved to {output_dir / 'absa_cls_dataset'}")
154
+
155
+ if __name__ == "__main__":
156
+ main()
src/evaluation/cross_lingual_eval.py ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import torch
4
+ import numpy as np
5
+ from pathlib import Path
6
+ from transformers import (
7
+ AutoTokenizer,
8
+ AutoModelForTokenClassification,
9
+ AutoModelForSequenceClassification,
10
+ pipeline
11
+ )
12
+ from sklearn.metrics import f1_score
13
+ import mlflow
14
+
15
+ from src.training.mlflow_utils import setup_mlflow
16
+ from src.data.bio_tagger import bio_to_aspects
17
+
18
+ def load_data(file_path: Path):
19
+ data = []
20
+ with open(file_path, 'r', encoding='utf-8') as f:
21
+ for line in f:
22
+ if line.strip():
23
+ data.append(json.loads(line))
24
+ return data
25
+
26
+ def main():
27
+ setup_mlflow()
28
+
29
+ # Check if models exist (might not if trained on Colab)
30
+ aspect_model_path = Path("models/aspect_extraction/best")
31
+ sentiment_model_path = Path("models/sentiment/best")
32
+
33
+ if not aspect_model_path.exists() or not sentiment_model_path.exists():
34
+ print("Models not found locally. Skipping cross-lingual evaluation until models are trained.")
35
+ return
36
+
37
+ print("Loading models...")
38
+ tokenizer = AutoTokenizer.from_pretrained("xlm-roberta-base")
39
+
40
+ aspect_model = AutoModelForTokenClassification.from_pretrained(str(aspect_model_path))
41
+ sentiment_model = AutoModelForSequenceClassification.from_pretrained(str(sentiment_model_path))
42
+
43
+ device = 0 if torch.cuda.is_available() else -1
44
+
45
+ ner_pipeline = pipeline("token-classification", model=aspect_model, tokenizer=tokenizer, device=device, aggregation_strategy="simple")
46
+
47
+ # Load Hindi Data
48
+ hindi_path = Path("data/processed/amazon_hindi.jsonl")
49
+ hindi_data = load_data(hindi_path)
50
+
51
+ print(f"Evaluating zero-shot on {len(hindi_data)} Hindi samples...")
52
+
53
+ sentiment_map_rev = {0: "positive", 1: "negative", 2: "neutral", 3: "conflict"}
54
+ sentiment_map = {"positive": 0, "negative": 1, "neutral": 2, "conflict": 3}
55
+
56
+ true_labels = []
57
+ pred_labels = []
58
+
59
+ for item in hindi_data:
60
+ text = item["text"]
61
+ aspects = item.get("aspect_terms", [])
62
+
63
+ for aspect in aspects:
64
+ term = aspect["term"]
65
+ true_polarity = aspect["polarity"]
66
+ if true_polarity not in sentiment_map:
67
+ continue
68
+
69
+ true_labels.append(sentiment_map[true_polarity])
70
+
71
+ # Inference Sentiment
72
+ inputs = tokenizer(text, term, return_tensors="pt", truncation=True, max_length=128)
73
+ if device == 0:
74
+ inputs = {k: v.to("cuda") for k, v in inputs.items()}
75
+ sentiment_model.to("cuda")
76
+
77
+ with torch.no_grad():
78
+ logits = sentiment_model(**inputs).logits
79
+ pred_idx = torch.argmax(logits, dim=1).item()
80
+
81
+ pred_labels.append(pred_idx)
82
+
83
+ hindi_macro_f1 = f1_score(true_labels, pred_labels, average="macro") if len(true_labels) > 0 else 0.0
84
+ print(f"Hindi Zero-Shot Macro-F1: {hindi_macro_f1}")
85
+
86
+ # We retrieve the best English test score from MLflow
87
+ # For now, let's just log the cross lingual gap if we know English F1
88
+ client = mlflow.tracking.MlflowClient()
89
+ experiment = client.get_experiment_by_name("multilingual-absa")
90
+
91
+ en_macro_f1 = 0.0
92
+ if experiment:
93
+ runs = client.search_runs(
94
+ experiment_ids=[experiment.experiment_id],
95
+ filter_string="metrics.test_macro_f1 > 0",
96
+ max_results=1,
97
+ order_by=["metrics.test_macro_f1 DESC"]
98
+ )
99
+ if runs:
100
+ en_macro_f1 = runs[0].data.metrics.get("test_macro_f1", 0.0)
101
+
102
+ print(f"Best English Test Macro-F1: {en_macro_f1}")
103
+ gap = en_macro_f1 - hindi_macro_f1
104
+ print(f"Cross-Lingual Gap: {gap}")
105
+
106
+ with mlflow.start_run(run_name="cross_lingual_eval"):
107
+ mlflow.log_metrics({
108
+ "hindi_zero_shot_macro_f1": float(hindi_macro_f1),
109
+ "english_test_macro_f1": float(en_macro_f1),
110
+ "cross_lingual_gap": float(gap)
111
+ })
112
+
113
+ if __name__ == "__main__":
114
+ main()
src/training/mlflow_utils.py ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import mlflow
2
+ from typing import Dict, Any, Optional
3
+ from pathlib import Path
4
+ import os
5
+
6
+ # Default configuration
7
+ MLFLOW_TRACKING_URI = "sqlite:///mlflow/mlflow.db"
8
+ EXPERIMENT_NAME = "multilingual-absa"
9
+
10
+ def setup_mlflow():
11
+ """Initializes MLflow tracking URI and experiment."""
12
+ # Ensure the directory exists
13
+ Path("mlflow").mkdir(parents=True, exist_ok=True)
14
+
15
+ mlflow.set_tracking_uri(MLFLOW_TRACKING_URI)
16
+ mlflow.set_experiment(EXPERIMENT_NAME)
17
+
18
+ def log_training_run(params: Dict[str, Any], metrics: Dict[str, float], model_path: Optional[str | Path] = None, run_name: Optional[str] = None) -> str:
19
+ """
20
+ Logs parameters, metrics, and optionally a model artifact to MLflow.
21
+
22
+ Args:
23
+ params: Dictionary of hyperparameters or configuration.
24
+ metrics: Dictionary of evaluation metrics.
25
+ model_path: Path to the saved model directory or file.
26
+ run_name: Optional name for the run.
27
+
28
+ Returns:
29
+ The ID of the created MLflow run.
30
+ """
31
+ setup_mlflow()
32
+
33
+ with mlflow.start_run(run_name=run_name) as run:
34
+ mlflow.log_params(params)
35
+ mlflow.log_metrics(metrics)
36
+
37
+ if model_path:
38
+ model_path_obj = Path(model_path)
39
+ if model_path_obj.exists():
40
+ mlflow.log_artifact(str(model_path_obj), artifact_path="model")
41
+ else:
42
+ print(f"Warning: Model path {model_path} does not exist. Artifact not logged.")
43
+
44
+ return run.info.run_id
45
+
46
+ def get_best_run(metric: str = "eval_macro_f1", ascending: bool = False) -> Optional[mlflow.entities.Run]:
47
+ """
48
+ Retrieves the best run from the experiment based on a specific metric.
49
+
50
+ Args:
51
+ metric: The metric to sort by.
52
+ ascending: True if a lower metric is better (e.g., loss), False for higher is better (e.g., F1).
53
+
54
+ Returns:
55
+ The MLflow Run object for the best run, or None if no runs exist.
56
+ """
57
+ setup_mlflow()
58
+
59
+ experiment = mlflow.get_experiment_by_name(EXPERIMENT_NAME)
60
+ if not experiment:
61
+ return None
62
+
63
+ runs = mlflow.search_runs(
64
+ experiment_ids=[experiment.experiment_id],
65
+ order_by=[f"metrics.{metric} {'ASC' if ascending else 'DESC'}"],
66
+ max_results=1,
67
+ output_format="list"
68
+ )
69
+
70
+ if not runs:
71
+ return None
72
+
73
+ return runs[0]
tests/test_bio_tagger.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
+ from src.data.bio_tagger import convert_to_bio, bio_to_aspects
3
+
4
+ def test_single_aspect():
5
+ text = "The food was amazing."
6
+ aspects = [{"term": "food", "from": 4, "to": 8}]
7
+ tags = convert_to_bio(text, aspects)
8
+
9
+ assert [t['token'] for t in tags] == ["The", "food", "was", "amazing."]
10
+ assert [t['label'] for t in tags] == ["O", "B-ASP", "O", "O"]
11
+
12
+ def test_multiple_aspects():
13
+ text = "The food was good, but the service was terrible."
14
+ aspects = [
15
+ {"term": "food", "from": 4, "to": 8},
16
+ {"term": "service", "from": 27, "to": 34}
17
+ ]
18
+ tags = convert_to_bio(text, aspects)
19
+
20
+ expected_labels = ["O", "B-ASP", "O", "O", "O", "O", "B-ASP", "O", "O"]
21
+ assert [t['label'] for t in tags] == expected_labels
22
+
23
+ def test_no_aspects():
24
+ text = "Everything was fine."
25
+ aspects = []
26
+ tags = convert_to_bio(text, aspects)
27
+
28
+ assert all(t['label'] == "O" for t in tags)
29
+
30
+ def test_multi_word_aspect():
31
+ text = "The operating system is very stable."
32
+ aspects = [{"term": "operating system", "from": 4, "to": 20}]
33
+ tags = convert_to_bio(text, aspects)
34
+
35
+ assert [t['label'] for t in tags] == ["O", "B-ASP", "I-ASP", "O", "O", "O"]
36
+
37
+ def test_adjacent_aspects():
38
+ text = "Great battery life." # Suppose battery and life are separate
39
+ aspects = [
40
+ {"term": "battery", "from": 6, "to": 13},
41
+ {"term": "life.", "from": 14, "to": 19}
42
+ ]
43
+ tags = convert_to_bio(text, aspects)
44
+ assert [t['label'] for t in tags] == ["O", "B-ASP", "B-ASP"]
45
+
46
+ def test_bio_to_aspects():
47
+ tokens = ["The", "operating", "system", "and", "battery", "life", "are", "great"]
48
+ labels = ["O", "B-ASP", "I-ASP", "O", "B-ASP", "B-ASP", "O", "O"]
49
+
50
+ extracted = bio_to_aspects(tokens, labels)
51
+ assert extracted == ["operating system", "battery", "life"]