Initial commit for LSTM with GloVe embeddings
Browse files- .gitattributes +2 -0
- .gitignore +160 -0
- GloVe/glove.6B.100d.txt +3 -0
- GloVe/glove.6B.300d.txt +3 -0
- data_1/Fake.csv +3 -0
- data_1/True.csv +3 -0
- data_2/WELFake_Dataset.csv +3 -0
- data_3/news_articles.csv +3 -0
- data_loader.py +22 -0
- inference.py +40 -0
- inference_analysis.ipynb +80 -0
- inference_main.py +100 -0
- model.py +30 -0
- output/version_1/best_model_1.pth +3 -0
- output/version_1/cleaned_inference_data_1.csv +3 -0
- output/version_1/cleaned_news_data_1.csv +3 -0
- output/version_1/confusion_matrix_data_1.csv +3 -0
- output/version_1/confusion_matrix_inference_1.csv +3 -0
- output/version_1/tokenizer_1.pickle +3 -0
- output/version_1/training_metrics_1.csv +3 -0
- output/version_2/best_model_2.pth +3 -0
- output/version_2/cleaned_inference_data_2.csv +3 -0
- output/version_2/cleaned_news_data_2.csv +3 -0
- output/version_2/confusion_matrix_data_2.csv +3 -0
- output/version_2/confusion_matrix_inference_2.csv +3 -0
- output/version_2/tokenizer_2.pickle +3 -0
- output/version_2/training_metrics_2.csv +3 -0
- preprocessing.py +64 -0
- train.py +89 -0
- train_analysis.ipynb +0 -0
- train_main.py +189 -0
.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 |
+
)
|