kimic commited on
Commit
6f9bfc0
1 Parent(s): ad7e752

Initial commit for LSTM with GloVe embeddings

Browse files
.gitattributes CHANGED
@@ -33,3 +33,5 @@ saved_model/**/* 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
 
 
 
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
36
+ *.csv filter=lfs diff=lfs merge=lfs -text
37
+ *.txt filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py,cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # poetry
98
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102
+ #poetry.lock
103
+
104
+ # pdm
105
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
106
+ #pdm.lock
107
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
108
+ # in version control.
109
+ # https://pdm.fming.dev/#use-with-ide
110
+ .pdm.toml
111
+
112
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
113
+ __pypackages__/
114
+
115
+ # Celery stuff
116
+ celerybeat-schedule
117
+ celerybeat.pid
118
+
119
+ # SageMath parsed files
120
+ *.sage.py
121
+
122
+ # Environments
123
+ .env
124
+ .venv
125
+ env/
126
+ venv/
127
+ ENV/
128
+ env.bak/
129
+ venv.bak/
130
+
131
+ # Spyder project settings
132
+ .spyderproject
133
+ .spyproject
134
+
135
+ # Rope project settings
136
+ .ropeproject
137
+
138
+ # mkdocs documentation
139
+ /site
140
+
141
+ # mypy
142
+ .mypy_cache/
143
+ .dmypy.json
144
+ dmypy.json
145
+
146
+ # Pyre type checker
147
+ .pyre/
148
+
149
+ # pytype static type analyzer
150
+ .pytype/
151
+
152
+ # Cython debug symbols
153
+ cython_debug/
154
+
155
+ # PyCharm
156
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
157
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
158
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
159
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
160
+ #.idea/
GloVe/glove.6B.100d.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:be4367dd257eb945217234f16307c5c74236b648a222cc0b4ffd0dda6a3350b6
3
+ size 347117594
GloVe/glove.6B.300d.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a12599d41e3589c7160be27fffe5b0080eccd0f0c75f46666c59f90188093c40
3
+ size 1037965801
data_1/Fake.csv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:bebf8bcfe95678bf2c732bf413a2ce5f621af0102c82bf08083b2e5d3c693d0c
3
+ size 62789876
data_1/True.csv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ba0844414a65dc6ae7402b8eee5306da24b6b56488d6767135af466c7dcb2775
3
+ size 53582940
data_2/WELFake_Dataset.csv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:665331424230fc452e9482c3547a6a199a2c29745ade8d236950d1d105223773
3
+ size 245086152
data_3/news_articles.csv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:53855240e9036a7d6c204e72bd0fa9d37a10f8e1bd2b2fdf34b962569ef271c6
3
+ size 10969548
data_loader.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from torch.utils.data import Dataset, DataLoader
2
+ import torch
3
+
4
+
5
+ class NewsDataset(Dataset):
6
+ def __init__(self, titles, texts, labels=None):
7
+ self.titles = titles
8
+ self.texts = texts
9
+ self.labels = labels
10
+
11
+ def __len__(self):
12
+ return len(self.titles)
13
+
14
+ def __getitem__(self, idx):
15
+ if self.labels is not None:
16
+ return self.titles[idx], self.texts[idx], self.labels[idx]
17
+ return self.titles[idx], self.texts[idx]
18
+
19
+
20
+ def create_data_loader(titles, texts, labels=None, batch_size=32, shuffle=False, num_workers=6):
21
+ dataset = NewsDataset(titles, texts, labels)
22
+ return DataLoader(dataset, batch_size=batch_size, shuffle=shuffle, num_workers=num_workers, pin_memory=True, persistent_workers=True)
inference.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import pandas as pd
3
+ from sklearn.metrics import accuracy_score, f1_score, roc_auc_score
4
+ from model import LSTMModel
5
+
6
+
7
+ def load_model(model_path, vocab_size):
8
+ model = LSTMModel(vocab_size)
9
+ model.load_state_dict(torch.load(model_path))
10
+ model.eval()
11
+ return model
12
+
13
+
14
+ def predict(model, titles, texts, device):
15
+ titles, texts = titles.to(device), texts.to(device)
16
+ model.to(device)
17
+ with torch.no_grad():
18
+ outputs = model(titles, texts).squeeze()
19
+ return outputs
20
+
21
+
22
+ def evaluate_model(model, data_loader, device, labels):
23
+ model.to(device)
24
+ model.eval()
25
+ predictions = []
26
+ labels = torch.tensor(labels).to(device)
27
+ for titles, texts in data_loader:
28
+ titles, texts = titles.to(device), texts.to(device)
29
+ outputs = predict(model, titles, texts, device)
30
+ predictions.extend(outputs.cpu().numpy())
31
+
32
+ labels = labels.cpu().numpy() # Convert labels to NumPy array for consistency
33
+ predicted_labels = [1 if p > 0.5 else 0 for p in predictions]
34
+
35
+ # Calculate metrics
36
+ accuracy = accuracy_score(labels, predicted_labels)
37
+ f1 = f1_score(labels, predicted_labels)
38
+ auc_roc = roc_auc_score(labels, predictions)
39
+
40
+ return accuracy, f1, auc_roc, labels, predicted_labels
inference_analysis.ipynb ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "code",
5
+ "execution_count": 4,
6
+ "metadata": {},
7
+ "outputs": [
8
+ {
9
+ "name": "stderr",
10
+ "output_type": "stream",
11
+ "text": [
12
+ "C:\\Users\\kimi\\AppData\\Local\\Temp\\ipykernel_1768\\401420358.py:5: MatplotlibDeprecationWarning: The seaborn styles shipped by Matplotlib are deprecated since 3.6, as they no longer correspond to the styles shipped by seaborn. However, they will remain available as 'seaborn-v0_8-<style>'. Alternatively, directly use the seaborn API instead.\n",
13
+ " plt.style.use(\"seaborn-whitegrid\")\n"
14
+ ]
15
+ },
16
+ {
17
+ "data": {
18
+ "image/png": "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aKelpen999/XkCFDtGHDhruOvTt58qQ6duwoOzu7HLfx9PTUBx98YEhKb277s88+q8zMTIWFhWnSpEk6ffq0evTooePHj0u6kap26dJF33//vdmHIkn66quvVLlyZTVo0ECSNH78eM2cOVNdunTRBx98oHbt2mny5MmaPXv2bY8jW1ZWltLT03O83WratGlycnJSZGSknnzySS1ZskRdu3bV6dOn9c4776hHjx5asWKFofhcsmSJrl69qhkzZqh///5avny5xowZY1ofGxur0NBQValSRbNnz9agQYO0atUqDRw40FQM/Pzzz+rTp4+KFCmi999/Xy+++KLhw25mZqZeeuklbdq0SUOHDtWUKVO0d+9erV69+q7n4W7vGZGRkZo2bZrat2+vOXPmqE6dOhoyZIjZPsLCwrR582aNGDHCVCxOmTIlz2OGpbz9XcaOHavJkyerdevWmjt3rkJCQvTJJ5+Ynb9bvfPOO5o9e7a6deumyMhIeXh46N133zXbJjfP09wYN26cqlevrurVq2vZsmW3HYbUrFkzOTg46MUXX1RkZKR++uknpaWlSZLpA1f2/7fff/9dPXr00IULFxQREaGwsDCdPHlSzz77rC5cuKDSpUubPvQNGDDA9LMkFS5cWK1atcrxwxUefDbXBXzx4kVdu3ZNFStWzNX2iYmJmjt3rrp37242HqZq1aoKCQnRl19+qeeee07SjTFaYWFhCggIkCRVrlxZrVq10ubNm9WnTx9T91HZsmXz1JW0c+dOVahQQSEhIbK3t1fDhg3l6uqqixcv5rj9Rx99pISEBH377bemJKpFixbq1auXpk6dqk6dOsne3t50HDeP+9i/f7+hqLudDh06KDo6WgcPHjQVPKtXr1abNm3k7Oxstu2RI0dUrVo1vf/++6bCunHjxtqxY4fi4+P1yiuvqHr16rftIn3llVfUsmXLHNthZ2enyZMnq0uXLnrnnXfk5OSkhIQELVq06I5pWVxcXI5JZW7MnDlTw4YN01dffaWvvvpKdnZ28vb21uOPP65evXqpePHi/2q/0o0xea6urlqzZo369OkjSVq3bp08PT3l5+d3x/u6urqqYcOG2rFjh6nLdceOHapRo4Z27dpl2m7Lli2mfyI3K1u2rGks6a1/g1q1amnq1KmSpICAAO3fv19btmy5bVuyE4bKlSubLc/KylJG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3t5eBw4c0KJFi9S8eXPTh5+BAweqR48e6t+/v5599lkVLlxYy5Yt0/r1603DVLI/lO/YsUOPPvqo6tSpY3qsPXv2qH79+rkawoAHi00WgNKNcUfVq1dXdHS0wsPDdenSJZUtW1bNmzfXK6+8YtYd9L///U8lS5bUJ598ouXLl8vd3V3t2rXT4MGD//OLokmTJhoxYoSWLFmitWvXmt4Yb/46sB49eigtLU1Lly7Vp59+KmdnZwUEBGjYsGE5dreWKlVKn332md59912FhYUpLS1Njz32mObMmWMYzGwJHTp00MKFC9W+ffvbFpf9+/fXxYsXtXjxYs2ePVvlypXTk08+aboCMzExUcWLF1efPn00efJk9e3b97ap0a0mTJigpKQks3FRb731ljp06KBRo0Zp8eLFOV480bZtW8XGxurs2bOGKXnmzJmT42Nlz67/zjvvyN/fXytXrtSYMWOUnJwsT09PNWnSROHh4f95aIB04yrpYsWKqVy5cobhB3fj5+enokWLqmTJkqZjq1atmooXLy4PDw+zyWNv9cQTT2jlypUaOXKkgoODNX78+P9yGBoyZIhatWqlpUuXKjIyUmfPnpWzs7O8vLw0aNAgBQcH3/abNAICAvTRRx9p5syZev311+Xk5KT69etrypQp8vb2NmzfsGFDVaxYUSdPnsxxeEF4eLg+/PBDU6FeokQJdejQQYMHD77rpLmbN282fHVdNldXV4t8e8qAAQN06NAhvfLKKypatKheeukls0K3e/fucnNz04IFC7Rs2TK5urqqXr16mjZtmuk5V7lyZX3yySeKiIjQkCFDVKJECY0YMUIRERFmj5U9XcvMmTN17do1dejQQU8//bRhvtC86t+/vzIzM7Vs2TItXLhQderU0dChQxUeHm4aT/b2229rxowZWrRokc6dO6cSJUooODhYr732mmk/gwYNUoUKFSw6T2NYWJgefvhhffHFF1q4cKFKly6tnj17KjQ09I7vXa6urvr444/18ccfy9fXVyNGjDB7XeTmeVqoUCHNnDlTkydP1uuvv66SJUvqxRdf1G+//WZK7SQpJCREBw8e1Msvv6zw8PAcv8GmYsWKiomJ0YcffqjY2FjNnz9fWVlZevjhh9W3b1+98MILpve8xx57TNHR0Zo+fbqGDx+urKwsVa1aVbNnzzb9PyhSpIh69+6tZcuW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0qSJafn27du1dOlSrVq1Svb29urVq5e8vb21ZcsWffrpp9q+fbupi3fHjh16++23NWnSJMXHx6tOnTo6cOBAjo/35Zdfas6cOZo6dar27NmjZ599VgMGDJCPj48mTJig8uXLa+/evSpTpoymT5+uTZs2KSoqSlu3blWdOnXUp08fXbt2TWlpaerfv7+8vb0VFxen9957T+vXr8/VMe/fv1+TJk3S+PHjtXPnTn377bf6448/tHjxYtM2Bw8eVM2aNbVt2zaNGTNGY8aM0e7duyXpju0CgAcZBSBgxSZMmKD69eurfv36CggI0MSJE9W7d289//zzpm2aN2+uMmXKqFixYtq0aZOuX7+u119/XYULF1a5cuX02muvKTo6WpK0atUqPfHEEwoICJCDg4Oee+45Va9ePcfHjomJ0TPPPCNfX1/Z29ure/fuWrRokZydnc22y8rK0tKlS/X666+rUqVKKly4sEJDQ5WWlqZNmzZp7969On36tIYPH67ChQvL29tbvXv3ztXxV61aVV9//bVq166txMREnT17Vp6enjpz5oxpm2rVqun555+Xo6OjmjRporZt22rlypV3bRcAPMjoAgas2Lhx48zGAOakdOnSpp9PnTqlhIQENWjQwLQsKytLaWlpunDhgs6cOaMaNWqY3b9SpUo57vfcuXMqX7682bJ69eoZtktISFBycrJee+012dv/32fStLQ0U7eth4eHWeH40EMP3fGYstnb22vx4sWKjY2Vq6urfHx8dPXqVbPxhxUrVjS7T7ly5XTkyJG7tgsAHmQUgICNs7OzM/1ctmxZPfTQQ1qzZo1p2dWrV3XhwgV5enqqbNmyOnnypNn9//nnH3l7exv2W65cOZ0+fdps2fTp09WlSxezZR4eHipcuLAWLVqkunXrmpb/9ttvKlOmjH755RclJCQoKSlJbm5upsfMjaioKP3www+KjY1VyZIlJUmvvPKK2TZnz541+/3kyZOqUKHCXdsFAA8yuoABmLRq1UpJSUlasGCBrl+/rsuXL2vEiBEaMmSI7Ozs1K1bN61fv14bN25Uenq6YmJitG/fvhz3FRQUpGXLlmn//v3KzMzUF198oejoaFNhlZKSovT0dNnb2ys4OFjvvvuu/vnnH2VmZiomJkadOnXSn3/+KV9fXz3yyCOaNGmSUlJS9Oeff2rRokW5Op6rV6/KwcFBjo6OSk9P18qVK7V161alpaWZttm/f7+++OILpaWlaePGjdqwYYO6d+9+13YBwIOMBBCASZEiRRQVFaWIiAgtWLBAmZmZ8vf319y5cyVJfn5+mjp1qiIiIjRkyBA1atTI7IKSm3Xu3FmXL1/WsGHDdO7cOXl5eWn+/Pny9PRUgwYNVKJECTVo0EBLly7ViBEjNGvWLD333HO6dOmSKlWqpJkzZ5rGF86bN09jx45V48aNVbJkSbVp00Zr16696/H06dNHR44cUatWrVS4cGFVr15dzz33nOLi4kzbNG7cWN9//70mTZqkihUr6v333zc97t3aBQAPKrssJuMCAACwKXQBAwAA2BgKQAAAABtDAQgAAGBjKAABAABsDAUgAACAjaEABAAAsDEUgAAAADaGAhAAAMDGUAACAADYGApAAAAAG0MBCAAAYGP+H03oGM9pnkvmAAAAAElFTkSuQmCC",
19
+ "text/plain": [
20
+ "<Figure size 800x600 with 2 Axes>"
21
+ ]
22
+ },
23
+ "metadata": {},
24
+ "output_type": "display_data"
25
+ }
26
+ ],
27
+ "source": [
28
+ "import matplotlib.pyplot as plt\n",
29
+ "import seaborn as sns\n",
30
+ "import pandas as pd\n",
31
+ "\n",
32
+ "plt.style.use(\"seaborn-whitegrid\")\n",
33
+ "\n",
34
+ "version = 2\n",
35
+ "\n",
36
+ "# Read confusion matrix from CSV\n",
37
+ "cm_df = pd.read_csv(\n",
38
+ " f\"./output/version_{version}/confusion_matrix_inference_{version}.csv\"\n",
39
+ ")\n",
40
+ "cm = cm_df.values\n",
41
+ "\n",
42
+ "# Plotting\n",
43
+ "plt.figure(figsize=(8, 6))\n",
44
+ "sns.heatmap(cm, annot=True, fmt=\"d\", cmap=\"Blues\")\n",
45
+ "plt.title(\"Confusion Matrix (LSTM with GloVe Embeddings, Holdout Set)\")\n",
46
+ "plt.ylabel(\"True label\")\n",
47
+ "plt.xlabel(\"Predicted label\")\n",
48
+ "plt.show()"
49
+ ]
50
+ },
51
+ {
52
+ "cell_type": "code",
53
+ "execution_count": null,
54
+ "metadata": {},
55
+ "outputs": [],
56
+ "source": []
57
+ }
58
+ ],
59
+ "metadata": {
60
+ "kernelspec": {
61
+ "display_name": "torch",
62
+ "language": "python",
63
+ "name": "python3"
64
+ },
65
+ "language_info": {
66
+ "codemirror_mode": {
67
+ "name": "ipython",
68
+ "version": 3
69
+ },
70
+ "file_extension": ".py",
71
+ "mimetype": "text/x-python",
72
+ "name": "python",
73
+ "nbconvert_exporter": "python",
74
+ "pygments_lexer": "ipython3",
75
+ "version": "3.10.11"
76
+ }
77
+ },
78
+ "nbformat": 4,
79
+ "nbformat_minor": 2
80
+ }
inference_main.py ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import pandas as pd
3
+ from preprocessing import (
4
+ preprocess_text,
5
+ load_tokenizer,
6
+ prepare_data,
7
+ load_glove_embeddings,
8
+ )
9
+ from data_loader import create_data_loader
10
+ from inference import load_model, evaluate_model
11
+ from sklearn.metrics import confusion_matrix
12
+ import os
13
+
14
+ version = 2
15
+
16
+
17
+ def run_evaluation(model_path, tokenizer_path, device):
18
+ cleaned_path = f"./output/version_{version}/cleaned_inference_data_{version}.csv"
19
+ # Load data
20
+ if os.path.exists(cleaned_path):
21
+ df = pd.read_csv(cleaned_path)
22
+ df.dropna(inplace=True)
23
+ print("Cleaned data found.")
24
+ else:
25
+ print("No cleaned data found. Cleaning data now...")
26
+
27
+ df = pd.read_csv("./data_3/news_articles.csv")
28
+ df.drop(
29
+ columns=[
30
+ "author",
31
+ "published",
32
+ "site_url",
33
+ "main_img_url",
34
+ "type",
35
+ "text_without_stopwords",
36
+ "title_without_stopwords",
37
+ "hasImage",
38
+ ],
39
+ inplace=True,
40
+ )
41
+ # Map Real to 1 and Fake to 0
42
+ df["label"] = df["label"].map({"Real": 1, "Fake": 0})
43
+ df = df[df["label"].isin([1, 0])]
44
+
45
+ # Drop rows where the language is not 'english'
46
+ df = df[df["language"] == "english"]
47
+ df.drop(columns=["language"], inplace=True)
48
+
49
+ # Convert "no title" to empty string
50
+ df["title"] = df["title"].apply(lambda x: "" if x == "no title" else x)
51
+
52
+ df.dropna(inplace=True)
53
+ df["title"] = df["title"].apply(preprocess_text)
54
+ df["text"] = df["text"].apply(preprocess_text)
55
+
56
+ df.to_csv(cleaned_path, index=False)
57
+ df.dropna(inplace=True)
58
+ print("Cleaned data saved.")
59
+
60
+ labels = df["label"].values
61
+
62
+ # Load tokenizer
63
+ tokenizer = load_tokenizer(tokenizer_path)
64
+
65
+ embedding_matrix = load_glove_embeddings(
66
+ "./GloVe/glove.6B.300d.txt", tokenizer.word_index, embedding_dim=300
67
+ )
68
+
69
+ model = load_model(model_path, embedding_matrix)
70
+ model.to(device)
71
+
72
+ # Prepare data
73
+ titles = prepare_data(df["title"], tokenizer)
74
+ texts = prepare_data(df["text"], tokenizer)
75
+
76
+ # Create DataLoader
77
+ data_loader = create_data_loader(titles, texts, batch_size=32, shuffle=False)
78
+
79
+ # Evaluate
80
+ accuracy, f1, auc_roc, y_true, y_pred = evaluate_model(
81
+ model, data_loader, device, labels
82
+ )
83
+
84
+ # Generate and save confusion matrix
85
+ cm = confusion_matrix(y_true, y_pred)
86
+ cm_df = pd.DataFrame(cm)
87
+ cm_filename = f"./output/version_{version}/confusion_matrix_inference_{version}.csv"
88
+ cm_df.to_csv(cm_filename, index=False)
89
+ print(f"Confusion Matrix saved to {cm_filename}")
90
+ return accuracy, f1, auc_roc
91
+
92
+
93
+ if __name__ == "__main__":
94
+ model_path = f"./output/version_{version}/best_model_{version}.pth"
95
+ tokenizer_path = f"./output/version_{version}/tokenizer_{version}.pickle"
96
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
97
+ print(f"Device: {device}")
98
+
99
+ accuracy, f1, auc_roc = run_evaluation(model_path, tokenizer_path, device)
100
+ print(f"Accuracy: {accuracy:.4f}, F1 Score: {f1:.4f}, AUC-ROC: {auc_roc:.4f}")
model.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+
4
+
5
+ class LSTMModel(nn.Module):
6
+ def __init__(self, embedding_matrix, hidden_size=256, num_layers=2, dropout=0.2):
7
+ super(LSTMModel, self).__init__()
8
+ num_embeddings, embedding_dim = embedding_matrix.shape
9
+ self.embedding = nn.Embedding(num_embeddings, embedding_dim)
10
+ self.embedding.weight = nn.Parameter(
11
+ torch.tensor(embedding_matrix, dtype=torch.float32)
12
+ )
13
+ self.embedding.weight.requires_grad = False # Do not train the embedding layer
14
+
15
+ self.lstm = nn.LSTM(
16
+ input_size=embedding_matrix.shape[1],
17
+ hidden_size=hidden_size,
18
+ num_layers=num_layers,
19
+ batch_first=True,
20
+ dropout=dropout,
21
+ )
22
+ self.fc = nn.Linear(hidden_size, 1)
23
+
24
+ def forward(self, title, text):
25
+ title_emb = self.embedding(title)
26
+ text_emb = self.embedding(text)
27
+ combined = torch.cat((title_emb, text_emb), dim=1)
28
+ output, (hidden, _) = self.lstm(combined)
29
+ out = self.fc(hidden[-1])
30
+ return torch.sigmoid(out)
output/version_1/best_model_1.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:77d30e665e657e8f6260f868c1c9970c9392ade0b99a7e4649b0c4bea285c11e
3
+ size 79915896
output/version_1/cleaned_inference_data_1.csv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:29cd7b40d7e925e4613e986b5e68420c0ca252544aa3fa6a435723b11d2a0a01
3
+ size 3873531
output/version_1/cleaned_news_data_1.csv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c0cae611f708ed033cb431b4ff525901cdfbc27e81eeacc872087a4efd6e8310
3
+ size 154593478
output/version_1/confusion_matrix_data_1.csv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8173915c45bc1ed9645ff497e81c20739b0ede7e27c23549708ac81ad8dcce5a
3
+ size 127312
output/version_1/confusion_matrix_inference_1.csv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ad69122350f62707b4aec7636d092e5966d92cce58104ba991a45931ee662342
3
+ size 22
output/version_1/tokenizer_1.pickle ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b52cb8f36b1e030a019b804f94af45e5359192b22ff87b7a59e64caadc195dd5
3
+ size 8809775
output/version_1/training_metrics_1.csv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b86ae48975027d778e0263e25fd5cb003f9edf6e87bad410b026f848fb941ea8
3
+ size 843
output/version_2/best_model_2.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ef554563669e21d8816565a91660f189213ee164c30294e9ed3a8f2fedd2a15b
3
+ size 233415928
output/version_2/cleaned_inference_data_2.csv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:29cd7b40d7e925e4613e986b5e68420c0ca252544aa3fa6a435723b11d2a0a01
3
+ size 3873531
output/version_2/cleaned_news_data_2.csv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c0cae611f708ed033cb431b4ff525901cdfbc27e81eeacc872087a4efd6e8310
3
+ size 154593478
output/version_2/confusion_matrix_data_2.csv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6b014987606b6fa47b404f5ce85542d6dea22cb784ed9608af815c02bcd15dbe
3
+ size 127312
output/version_2/confusion_matrix_inference_2.csv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d678327086193512c3524bbea55a8a3ad2ef860f582a25a525c6521003b7ab87
3
+ size 22
output/version_2/tokenizer_2.pickle ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:bd1fbf39ff07f24276cfd86e8409d56b4a23e9405744cd60d4e5d41e6db245d1
3
+ size 8809775
output/version_2/training_metrics_2.csv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9ca8d5b3d99bbe56fc4ada3c2270bff8a69a481db062ff7e51ad4aaa7463df39
3
+ size 609
preprocessing.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ import spacy
3
+ from keras.preprocessing.text import Tokenizer
4
+ from keras_preprocessing.sequence import pad_sequences
5
+ import pickle
6
+ import numpy as np
7
+
8
+
9
+ # Load spaCy's English model
10
+ nlp = spacy.load("en_core_web_sm")
11
+
12
+
13
+ def preprocess_text(text):
14
+ # Remove patterns like "COUNTRY or STATE NAME (Reuters) -" or just "(Reuters)"
15
+ text = re.sub(
16
+ r"(\b[A-Z]{2,}(?:\s[A-Z]{2,})*\s\(Reuters\)\s-|\(Reuters\))", "", text
17
+ )
18
+
19
+ # Remove patterns like "Featured image via author name / image place"
20
+ text = re.sub(r"Featured image via .+?\.($|\s)", "", text)
21
+
22
+ # Process text with spaCy
23
+ doc = nlp(text)
24
+
25
+ lemmatized_text = []
26
+ for token in doc:
27
+ # Preserve named entities in their original form
28
+ if token.ent_type_:
29
+ lemmatized_text.append(token.text)
30
+ # Lemmatize other tokens and exclude non-alpha tokens if necessary
31
+ elif token.is_alpha and not token.is_stop:
32
+ lemmatized_text.append(token.lemma_.lower())
33
+
34
+ return " ".join(lemmatized_text)
35
+
36
+
37
+ def load_tokenizer(tokenizer_path):
38
+ with open(tokenizer_path, "rb") as handle:
39
+ tokenizer = pickle.load(handle)
40
+ return tokenizer
41
+
42
+
43
+ def prepare_data(texts, tokenizer, max_length=500):
44
+ sequences = tokenizer.texts_to_sequences(texts)
45
+ padded = pad_sequences(sequences, maxlen=max_length)
46
+ return padded
47
+
48
+
49
+ def load_glove_embeddings(glove_file, word_index, embedding_dim=100):
50
+ embeddings_index = {}
51
+ with open(glove_file, encoding="utf8") as f:
52
+ for line in f:
53
+ values = line.split()
54
+ word = values[0]
55
+ coefs = np.asarray(values[1:], dtype="float32")
56
+ embeddings_index[word] = coefs
57
+
58
+ embedding_matrix = np.zeros((len(word_index) + 1, embedding_dim))
59
+ for word, i in word_index.items():
60
+ embedding_vector = embeddings_index.get(word)
61
+ if embedding_vector is not None:
62
+ embedding_matrix[i] = embedding_vector
63
+
64
+ return embedding_matrix
train.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import pandas as pd
3
+ import time
4
+ from torch.nn.utils import clip_grad_norm_
5
+
6
+
7
+ def train(model, train_loader, val_loader, criterion, optimizer, epochs, device, version, max_grad_norm=1.0, early_stopping_patience=5, early_stopping_delta=0.001):
8
+ best_accuracy = 0.0
9
+ best_model_path = f'./output/version_{version}/best_model_{version}.pth'
10
+ best_epoch = 0
11
+ early_stopping_counter = 0
12
+ total_batches = len(train_loader)
13
+ metrics = {
14
+ 'epoch': [], 'train_loss': [], 'val_loss': [], 'train_accuracy': [], 'val_accuracy': []
15
+ }
16
+
17
+ for epoch in range(epochs):
18
+ model.train()
19
+ total_loss, train_correct, train_total = 0, 0, 0
20
+ for batch_idx, (titles, texts, labels) in enumerate(train_loader):
21
+ start_time = time.time() # Start time for the batch
22
+
23
+ titles, texts, labels = titles.to(device), texts.to(
24
+ device), labels.to(device).float()
25
+
26
+ # Forward pass
27
+ outputs = model(titles, texts).squeeze()
28
+ loss = criterion(outputs, labels)
29
+
30
+ # Backward and optimize
31
+ optimizer.zero_grad()
32
+ loss.backward()
33
+ if max_grad_norm:
34
+ clip_grad_norm_(model.parameters(), max_norm=max_grad_norm)
35
+ optimizer.step()
36
+
37
+ total_loss += loss.item()
38
+ train_pred = (outputs > 0.5).float()
39
+ train_correct += (train_pred == labels).sum().item()
40
+ train_total += labels.size(0)
41
+
42
+ # Calculate and print batch processing time
43
+ batch_time = time.time() - start_time
44
+ print(
45
+ f'Epoch: {epoch+1}, Batch: {batch_idx+1}/{total_batches}, Batch Processing Time: {batch_time:.4f} seconds')
46
+
47
+ train_accuracy = 100 * train_correct / train_total
48
+ metrics['train_loss'].append(total_loss / len(train_loader))
49
+ metrics['train_accuracy'].append(train_accuracy)
50
+
51
+ # Validation
52
+ model.eval()
53
+ val_loss, val_correct, val_total = 0, 0, 0
54
+ with torch.no_grad():
55
+ for titles, texts, labels in val_loader:
56
+ titles, texts, labels = titles.to(device), texts.to(
57
+ device), labels.to(device).float()
58
+ outputs = model(titles, texts).squeeze()
59
+ loss = criterion(outputs, labels)
60
+ val_loss += loss.item()
61
+ predicted = (outputs > 0.5).float()
62
+ val_total += labels.size(0)
63
+ val_correct += (predicted == labels).sum().item()
64
+
65
+ val_accuracy = 100 * val_correct / val_total
66
+ metrics['val_loss'].append(val_loss / len(val_loader))
67
+ metrics['val_accuracy'].append(val_accuracy)
68
+ metrics['epoch'].append(epoch + 1)
69
+
70
+ # Early stopping logic
71
+ if val_accuracy > best_accuracy + early_stopping_delta:
72
+ best_accuracy = val_accuracy
73
+ early_stopping_counter = 0
74
+ best_epoch = epoch + 1
75
+ torch.save(model.state_dict(), best_model_path)
76
+ else:
77
+ early_stopping_counter += 1
78
+
79
+ if early_stopping_counter >= early_stopping_patience:
80
+ print(f"Early stopping triggered at epoch {epoch + 1}")
81
+ break
82
+
83
+ print(
84
+ f'Epoch [{epoch+1}/{epochs}], Loss: {total_loss/len(train_loader):.4f}, Validation Accuracy: {val_accuracy:.2f}%')
85
+
86
+ pd.DataFrame(metrics).to_csv(
87
+ f'./output/version_{version}/training_metrics_{version}.csv', index=False)
88
+
89
+ return model, best_accuracy, best_epoch
train_analysis.ipynb ADDED
The diff for this file is too large to render. See raw diff
 
train_main.py ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ import pandas as pd
4
+ from model import LSTMModel
5
+ from preprocessing import preprocess_text, load_glove_embeddings
6
+ from data_loader import create_data_loader
7
+ from sklearn.model_selection import train_test_split
8
+ from sklearn.metrics import f1_score, roc_auc_score
9
+ from keras.preprocessing.text import Tokenizer
10
+ from keras_preprocessing.sequence import pad_sequences
11
+ import pickle
12
+ import train as tr
13
+ from torch.utils.data import Dataset, DataLoader
14
+ from data_loader import NewsDataset
15
+ import os
16
+
17
+ version = 2
18
+
19
+ if __name__ == "__main__":
20
+ data_path = "./data_2/WELFake_Dataset.csv"
21
+ cleaned_path = f"./output/version_{version}/cleaned_news_data_{version}.csv"
22
+ # Load data
23
+ if os.path.exists(cleaned_path):
24
+ df = pd.read_csv(cleaned_path)
25
+ df.dropna(inplace=True)
26
+ print("Cleaned data found.")
27
+ else:
28
+ print("No cleaned data found. Cleaning data now...")
29
+ df = pd.read_csv(data_path)
30
+
31
+ # Drop index
32
+ df.drop(df.columns[0], axis=1, inplace=True)
33
+ df.dropna(inplace=True)
34
+
35
+ # Swapping labels around since it originally is the opposite
36
+ df["label"] = df["label"].map({0: 1, 1: 0})
37
+
38
+ df["title"] = df["title"].apply(preprocess_text)
39
+ df["text"] = df["text"].apply(preprocess_text)
40
+
41
+ # Create the directory if it does not exist
42
+ os.makedirs(os.path.dirname(cleaned_path), exist_ok=True)
43
+ df.to_csv(cleaned_path, index=False)
44
+ print("Cleaned data saved.")
45
+
46
+ # Splitting the data
47
+ train_val, test = train_test_split(df, test_size=0.2, random_state=42)
48
+ train, val = train_test_split(
49
+ train_val, test_size=0.25, random_state=42
50
+ ) # 0.25 * 0.8 = 0.2
51
+
52
+ # Initialize the tokenizer
53
+ tokenizer = Tokenizer()
54
+
55
+ # Fit the tokenizer on the training data
56
+ tokenizer.fit_on_texts(train["title"] + train["text"])
57
+
58
+ with open(f"./output/version_{version}/tokenizer_{version}.pickle", "wb") as handle:
59
+ pickle.dump(tokenizer, handle, protocol=pickle.HIGHEST_PROTOCOL)
60
+
61
+ # Tokenize the data
62
+ X_train_title = tokenizer.texts_to_sequences(train["title"])
63
+ X_train_text = tokenizer.texts_to_sequences(train["text"])
64
+ X_val_title = tokenizer.texts_to_sequences(val["title"])
65
+ X_val_text = tokenizer.texts_to_sequences(val["text"])
66
+ X_test_title = tokenizer.texts_to_sequences(test["title"])
67
+ X_test_text = tokenizer.texts_to_sequences(test["text"])
68
+
69
+ # GloVe embeddings
70
+ embedding_matrix = load_glove_embeddings(
71
+ "./GloVe/glove.6B.300d.txt", tokenizer.word_index, embedding_dim=300
72
+ )
73
+
74
+ # Padding sequences
75
+ max_length = 500
76
+ X_train_title = pad_sequences(X_train_title, maxlen=max_length)
77
+ X_train_text = pad_sequences(X_train_text, maxlen=max_length)
78
+ X_val_title = pad_sequences(X_val_title, maxlen=max_length)
79
+ X_val_text = pad_sequences(X_val_text, maxlen=max_length)
80
+ X_test_title = pad_sequences(X_test_title, maxlen=max_length)
81
+ X_test_text = pad_sequences(X_test_text, maxlen=max_length)
82
+
83
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
84
+ print(f"Device: {device}")
85
+
86
+ model = LSTMModel(embedding_matrix).to(device)
87
+
88
+ # Convert data to PyTorch tensors
89
+ train_data = NewsDataset(
90
+ torch.tensor(X_train_title),
91
+ torch.tensor(X_train_text),
92
+ torch.tensor(train["label"].values),
93
+ )
94
+ val_data = NewsDataset(
95
+ torch.tensor(X_val_title),
96
+ torch.tensor(X_val_text),
97
+ torch.tensor(val["label"].values),
98
+ )
99
+ test_data = NewsDataset(
100
+ torch.tensor(X_test_title),
101
+ torch.tensor(X_test_text),
102
+ torch.tensor(test["label"].values),
103
+ )
104
+
105
+ train_loader = DataLoader(
106
+ train_data,
107
+ batch_size=32,
108
+ shuffle=True,
109
+ num_workers=6,
110
+ pin_memory=True,
111
+ persistent_workers=True,
112
+ )
113
+ val_loader = DataLoader(
114
+ val_data,
115
+ batch_size=32,
116
+ shuffle=False,
117
+ num_workers=6,
118
+ pin_memory=True,
119
+ persistent_workers=True,
120
+ )
121
+ test_loader = DataLoader(
122
+ test_data,
123
+ batch_size=32,
124
+ shuffle=False,
125
+ num_workers=6,
126
+ pin_memory=True,
127
+ persistent_workers=True,
128
+ )
129
+
130
+ criterion = nn.BCELoss()
131
+ optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
132
+
133
+ trained_model, best_accuracy, best_epoch = tr.train(
134
+ model=model,
135
+ train_loader=train_loader,
136
+ val_loader=val_loader,
137
+ criterion=criterion,
138
+ optimizer=optimizer,
139
+ version=version,
140
+ epochs=10,
141
+ device=device,
142
+ max_grad_norm=1.0,
143
+ early_stopping_patience=3,
144
+ early_stopping_delta=0.01,
145
+ )
146
+
147
+ print(f"Best model was saved at epoch: {best_epoch}")
148
+
149
+ # Load the best model before testing
150
+ best_model_path = f"./output/version_{version}/best_model_{version}.pth"
151
+ model.load_state_dict(torch.load(best_model_path, map_location=device))
152
+
153
+ # Testing
154
+ model.eval()
155
+ true_labels = []
156
+ predicted_labels = []
157
+ predicted_probs = []
158
+
159
+ with torch.no_grad():
160
+ correct = 0
161
+ total = 0
162
+ for titles, texts, labels in test_loader:
163
+ titles, texts, labels = (
164
+ titles.to(device),
165
+ texts.to(device),
166
+ labels.to(device).float(),
167
+ )
168
+ outputs = model(titles, texts).squeeze()
169
+
170
+ predicted = (outputs > 0.5).float()
171
+ total += labels.size(0)
172
+ correct += (predicted == labels).sum().item()
173
+ true_labels.extend(labels.cpu().numpy())
174
+ predicted_labels.extend(predicted.cpu().numpy())
175
+ predicted_probs.extend(outputs.cpu().numpy())
176
+
177
+ test_accuracy = 100 * correct / total
178
+ f1 = f1_score(true_labels, predicted_labels)
179
+ auc_roc = roc_auc_score(true_labels, predicted_probs)
180
+
181
+ print(
182
+ f"Test Accuracy: {test_accuracy:.2f}%, F1 Score: {f1:.4f}, AUC-ROC: {auc_roc:.4f}"
183
+ )
184
+
185
+ # Create DataFrame and Save to CSV
186
+ confusion_data = pd.DataFrame({"True": true_labels, "Predicted": predicted_labels})
187
+ confusion_data.to_csv(
188
+ f"./output/version_{version}/confusion_matrix_data_{version}.csv", index=False
189
+ )