diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/.DS_Store differ diff --git a/1.fake-news-LogisticRegression.ipynb b/1.fake-news-LogisticRegression.ipynb new file mode 100644 index 0000000..d2326aa --- /dev/null +++ b/1.fake-news-LogisticRegression.ipynb @@ -0,0 +1,863 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 15, + "id": "4dc82578", + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "from sklearn.model_selection import train_test_split\n", + "from sklearn.feature_extraction.text import TfidfVectorizer\n", + "from sklearn.linear_model import LogisticRegression\n", + "from sklearn.metrics import classification_report, confusion_matrix\n", + "import seaborn as sns\n", + "import matplotlib.pyplot as plt\n" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "1ff89aef", + "metadata": {}, + "outputs": [], + "source": [ + "df = pd.read_csv(\"dataset/data.csv\")\n", + "\n", + "# remove empty rows\n", + "df = df[df['title'] != '']\n", + "df = df[df['text'] != '']" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "4763a7f6", + "metadata": {}, + "outputs": [], + "source": [ + "# Remove duplicate rows based on the 'text' column\n", + "df = df.drop_duplicates(subset=['text']) \n", + "\n", + "# Remove rows with 'text' is NaN\n", + "df = df.dropna(subset=['text']) \n", + "\n", + "# Remove rows with 'label' is NaN\n", + "df = df.dropna(subset=['label']) \n", + "\n", + "# Remove rows with 'text' empty or only with whitespace\n", + "df = df[df['text'].str.strip() != ''] " + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "41340b02", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[nltk_data] Downloading package wordnet to\n", + "[nltk_data] /Users/luis.guimaraes/nltk_data...\n", + "[nltk_data] Package wordnet is already up-to-date!\n" + ] + } + ], + "source": [ + "import re\n", + "import string\n", + "import nltk\n", + "from nltk.tokenize import word_tokenize\n", + "from nltk.stem import WordNetLemmatizer\n", + "\n", + "# Download required NLTK data if not already downloaded\n", + "nltk.download('wordnet')\n", + "\n", + "def clean_text(text):\n", + " text = text.lower()\n", + " text = re.sub(r'\\[.*?\\]', '', text)\n", + " text = re.sub(r'http\\S+|www\\S+|https\\S+', '', text)\n", + " text = re.sub(r'<.*?>+', '', text)\n", + " text = re.sub(r'[%s]' % re.escape(string.punctuation), '', text)\n", + " text = re.sub(r'\\n', '', text)\n", + " text = re.sub(r'\\w*\\d\\w*', '', text)\n", + " \n", + " # Tokenize the text\n", + " tokens = word_tokenize(text)\n", + " \n", + " # Initialize Lemmatizer\n", + " lemmatizer = WordNetLemmatizer()\n", + " \n", + " # Lemmatize each token\n", + " lemmatized_tokens = [lemmatizer.lemmatize(token) for token in tokens]\n", + " \n", + " # Join tokens back into a string\n", + " text = ' '.join(lemmatized_tokens)\n", + " return text\n", + "\n", + "df['text_clean'] = df['title'] + \" \" + df['text']\n", + "df['text_clean'] = df['text_clean'].apply(clean_text)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "296fe39d", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "# Split the data into training and testing sets\n", + "X = df['text_clean']\n", + "y = df['label']\n", + "\n", + "# train\n", + "X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "a64b0b16", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "vectorizer = TfidfVectorizer(\n", + " max_features=8000, # limit the number of features\n", + " stop_words='english',\n", + " min_df=5, # ignore rare words\n", + " max_df=0.8 # ignore overly common words\n", + ")\n", + "\n", + "# Fit only on training, transform both\n", + "X_train_vec = vectorizer.fit_transform(X_train)\n", + "X_test_vec = vectorizer.transform(X_test)" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "c381617c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
LogisticRegression(C=0.5, class_weight='balanced', max_iter=1000, penalty='l1',\n",
+       "                   random_state=42, solver='saga')
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
" + ], + "text/plain": [ + "LogisticRegression(C=0.5, class_weight='balanced', max_iter=1000, penalty='l1',\n", + " random_state=42, solver='saga')" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "model = LogisticRegression(\n", + " class_weight='balanced',\n", + " solver='saga', # Algorithm to use in the optimization problem\n", + " penalty='l1', # Specify the norm of the penalty\n", + " C=0.5, # Inverse of regularization strength; smaller values specify stronger regularization\n", + " max_iter=1000, # Maximum number of iterations taken for the solvers to converge\n", + " random_state=42 # For reproducibility\n", + ")\n", + "\n", + "model.fit(X_train_vec, y_train)\n", + "\n", + "\n", + "#model = LogisticRegression(class_weight='balanced')\n", + "#model.fit(X_train_vec, y_train)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "484d6523", + "metadata": {}, + "outputs": [], + "source": [ + "# Predict on the test set\n", + "y_pred = model.predict(X_test_vec)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "5f194883", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['tfidf_vectorizer.pkl']" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import joblib\n", + "\n", + "# Save model\n", + "joblib.dump(model, 'logistic_model.pkl')\n", + "\n", + "# Save TF-IDF vectorizer\n", + "joblib.dump(vectorizer, 'tfidf_vectorizer.pkl')\n" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "2ef43d5f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " precision recall f1-score support\n", + "\n", + " 0 0.99 0.99 0.99 3241\n", + " 1 0.99 0.99 0.99 3954\n", + "\n", + " accuracy 0.99 7195\n", + " macro avg 0.99 0.99 0.99 7195\n", + "weighted avg 0.99 0.99 0.99 7195\n", + "\n" + ] + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "\n", + "# Classification report and confusion matrix\n", + "print(classification_report(y_test, y_pred))\n", + "sns.heatmap(confusion_matrix(y_test, y_pred), annot=True, fmt='d')\n", + "plt.title(\"Confusion Matrix\")\n", + "plt.show()\n" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "ab90e014", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Baseline accuracy: 0.54954829742877\n" + ] + } + ], + "source": [ + "from sklearn.dummy import DummyClassifier\n", + "dummy = DummyClassifier(strategy=\"most_frequent\")\n", + "dummy.fit(X_train_vec, y_train)\n", + "print(\"Baseline accuracy:\", dummy.score(X_test_vec, y_test))\n" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "a787659a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Probabilities: [[0.99015316 0.00984684]]\n" + ] + } + ], + "source": [ + "sample_text = [\"This is a sample news article.\"] # Replace with your sample text\n", + "sample_vec = vectorizer.transform(sample_text)\n", + "\n", + "probs = model.predict_proba(sample_vec)\n", + "print(\"Probabilities:\", probs)\n" + ] + }, + { + "cell_type": "markdown", + "id": "37fa4daf", + "metadata": {}, + "source": [ + "# Load validation_data.csv" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "e984f239", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[nltk_data] Downloading package punkt to\n", + "[nltk_data] /Users/luis.guimaraes/nltk_data...\n", + "[nltk_data] Package punkt is already up-to-date!\n" + ] + } + ], + "source": [ + "\n", + "from nltk.stem import PorterStemmer\n", + "from nltk.tokenize import word_tokenize\n", + "nltk.download('punkt')\n", + "\n", + "# Define stemmer\n", + "stemmer = PorterStemmer()\n", + "\n", + "# Define stem_text function\n", + "def stem_text(text):\n", + "\tif isinstance(text, str):\n", + "\t\t# Tokenize the text\n", + "\t\ttokens = word_tokenize(text.lower())\n", + "\t\t# Apply stemming\n", + "\t\tstemmed_tokens = [stemmer.stem(token) for token in tokens]\n", + "\t\t# Join tokens back into a string\n", + "\t\treturn ' '.join(stemmed_tokens)\n", + "\treturn ''\n", + "\n", + "# Load validation data and prepare it for prediction\n", + "validation_df = pd.read_csv(\"dataset/validation_data.csv\")\n", + "\n", + "# Clean NaNs before applying\n", + "validation_df['title'] = validation_df['title'].fillna('')\n", + "validation_df['text'] = validation_df['text'].fillna('')\n", + "\n", + "# Apply stemming\n", + "validation_df['title'] = validation_df['title'].apply(stem_text)\n", + "validation_df['text'] = validation_df['text'].apply(stem_text)\n", + "\n", + "# Combine title and text\n", + "validation_df['text_clean'] = (validation_df['title'] + ' ' + validation_df['text']).str.strip()\n", + "validation_df = validation_df[validation_df['text_clean'] != '']\n", + "\n", + "# Prepare features (ignore label column as instructed)\n", + "X_val = validation_df['text_clean']\n", + "\n", + "# Transform using the same vectorizer used for training\n", + "X_val_vec = vectorizer.transform(X_val)\n", + "\n", + "# Get predictions (0 or 1)\n", + "predictions = model.predict(X_val_vec)\n", + "\n", + "# Add predictions to the validation dataframe\n", + "validation_df['predicted_label'] = predictions\n", + "\n", + "\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "id": "217a1684", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "First few predictions:\n", + " title predicted_label\n", + "0 uk 's may 'receiv regular updat ' on london tu... 0\n", + "1 uk transport polic lead investig of london inc... 0\n", + "2 pacif nation crack down on north korean ship a... 0\n", + "3 three suspect al qaeda milit kill in yemen dro... 0\n", + "4 chines academ prod beij to consid north korea ... 1\n", + "\n", + "Prediction counts:\n", + "predicted_label\n", + "0 4668\n", + "1 288\n", + "Name: count, dtype: int64\n" + ] + } + ], + "source": [ + "# Display the first few predictions\n", + "print(\"First few predictions:\")\n", + "print(validation_df[['title', 'predicted_label']].head())\n", + "\n", + "validation_df[['title', 'predicted_label']].to_csv('validation_predictions.csv', index=False)\n", + "\n", + "# Create a copy with index as id\n", + "#result_df = pd.DataFrame({\n", + "# 'id': validation_df.index,\n", + "# 'predicted_label': validation_df['predicted_label']\n", + "#})\n", + "\n", + "validation_df.to_csv('validation_predictions.csv', index=False)\n", + "\n", + "# Count of each prediction class\n", + "print(\"\\nPrediction counts:\")\n", + "print(validation_df['predicted_label'].value_counts())\n", + "\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "3.10.12", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/1.fake-news-RandonForest copy.ipynb b/1.fake-news-RandonForest copy.ipynb new file mode 100644 index 0000000..da31f09 --- /dev/null +++ b/1.fake-news-RandonForest copy.ipynb @@ -0,0 +1,861 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "4dc82578", + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "from sklearn.model_selection import train_test_split\n", + "from sklearn.feature_extraction.text import TfidfVectorizer\n", + "from sklearn.ensemble import RandomForestClassifier\n", + "from sklearn.metrics import classification_report, confusion_matrix\n", + "import seaborn as sns\n", + "import matplotlib.pyplot as plt\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "1ff89aef", + "metadata": {}, + "outputs": [], + "source": [ + "df = pd.read_csv(\"dataset/data.csv\")\n", + "\n", + "# remove empty rows\n", + "df = df[df['title'] != '']\n", + "df = df[df['text'] != '']" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "4763a7f6", + "metadata": {}, + "outputs": [], + "source": [ + "# Remove duplicate rows based on the 'text' column\n", + "df = df.drop_duplicates(subset=['text']) \n", + "\n", + "# Remove rows with 'text' is NaN\n", + "df = df.dropna(subset=['text']) \n", + "\n", + "# Remove rows with 'label' is NaN\n", + "df = df.dropna(subset=['label']) \n", + "\n", + "# Remove rows with 'text' empty or only with whitespace\n", + "df = df[df['text'].str.strip() != ''] " + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "41340b02", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[nltk_data] Downloading package wordnet to\n", + "[nltk_data] /Users/luis.guimaraes/nltk_data...\n", + "[nltk_data] Package wordnet is already up-to-date!\n", + "[nltk_data] Downloading package punkt to\n", + "[nltk_data] /Users/luis.guimaraes/nltk_data...\n", + "[nltk_data] Package punkt is already up-to-date!\n" + ] + } + ], + "source": [ + "import re\n", + "import string\n", + "import nltk\n", + "from nltk.tokenize import word_tokenize\n", + "from nltk.stem import WordNetLemmatizer\n", + "\n", + "# Download required NLTK data if not already downloaded\n", + "nltk.download('wordnet')\n", + "nltk.download('punkt')\n", + "\n", + "def clean_text(text):\n", + " text = text.lower()\n", + " text = re.sub(r'\\[.*?\\]', '', text)\n", + " text = re.sub(r'http\\S+|www\\S+|https\\S+', '', text)\n", + " text = re.sub(r'<.*?>+', '', text)\n", + " text = re.sub(r'[%s]' % re.escape(string.punctuation), '', text)\n", + " text = re.sub(r'\\n', '', text)\n", + " text = re.sub(r'\\w*\\d\\w*', '', text)\n", + " \n", + " # Tokenize the text\n", + " tokens = word_tokenize(text)\n", + " \n", + " # Initialize Lemmatizer\n", + " lemmatizer = WordNetLemmatizer()\n", + " \n", + " # Lemmatize each token\n", + " lemmatized_tokens = [lemmatizer.lemmatize(token) for token in tokens]\n", + " \n", + " # Join tokens back into a string\n", + " text = ' '.join(lemmatized_tokens)\n", + " return text\n", + "\n", + "df['text_clean'] = df['title'] + \" \" + df['text']\n", + "df['text_clean'] = df['text_clean'].apply(clean_text)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "296fe39d", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "# Split the data into training and testing sets\n", + "X = df['text_clean']\n", + "y = df['label']\n", + "\n", + "# train\n", + "X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "a64b0b16", + "metadata": {}, + "outputs": [], + "source": [ + "#vectorizer = TfidfVectorizer(max_features=5000, stop_words='english')\n", + "vectorizer = TfidfVectorizer(\n", + " max_features=8000, # limit the number of features\n", + " stop_words='english',\n", + " min_df=5, # ignore rare words\n", + " max_df=0.8 # ignore overly common words\n", + ")\n", + "\n", + "# Fit only on training, transform both\n", + "X_train_vec = vectorizer.fit_transform(X_train)\n", + "X_test_vec = vectorizer.transform(X_test)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "c381617c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
RandomForestClassifier(class_weight='balanced', n_jobs=-1, random_state=42)
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
" + ], + "text/plain": [ + "RandomForestClassifier(class_weight='balanced', n_jobs=-1, random_state=42)" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\n", + "model = RandomForestClassifier(\n", + " n_estimators=100, # Number of trees\n", + " max_depth=None, # Maximum depth of trees (None means unlimited)\n", + " min_samples_split=2,\n", + " min_samples_leaf=1,\n", + " class_weight='balanced', # Same as your Logistic Regression\n", + " random_state=42, # For reproducibility\n", + " n_jobs=-1 # Use all available cores\n", + ")\n", + "\n", + "# Train the model (this stays the same)\n", + "model.fit(X_train_vec, y_train)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "484d6523", + "metadata": {}, + "outputs": [], + "source": [ + "# Predict on the test set\n", + "y_pred = model.predict(X_test_vec)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "5f194883", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['tfidf_vectorizer_rf.pkl']" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import joblib\n", + "\n", + "# Save model\n", + "joblib.dump(model, 'random_forest_model.pkl')\n", + "\n", + "# Save TF-IDF vectorizer (this stays the same)\n", + "joblib.dump(vectorizer, 'tfidf_vectorizer_rf.pkl')" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "2ef43d5f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " precision recall f1-score support\n", + "\n", + " 0 1.00 0.99 0.99 3241\n", + " 1 0.99 1.00 1.00 3954\n", + "\n", + " accuracy 1.00 7195\n", + " macro avg 1.00 1.00 1.00 7195\n", + "weighted avg 1.00 1.00 1.00 7195\n", + "\n" + ] + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "\n", + "# Classification report and confusion matrix\n", + "print(classification_report(y_test, y_pred))\n", + "sns.heatmap(confusion_matrix(y_test, y_pred), annot=True, fmt='d')\n", + "plt.title(\"Confusion Matrix\")\n", + "plt.show()\n" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "ab90e014", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Baseline accuracy: 0.54954829742877\n" + ] + } + ], + "source": [ + "from sklearn.dummy import DummyClassifier\n", + "dummy = DummyClassifier(strategy=\"most_frequent\")\n", + "dummy.fit(X_train_vec, y_train)\n", + "print(\"Baseline accuracy:\", dummy.score(X_test_vec, y_test))\n" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "a787659a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Probabilities: [[0.99 0.01]]\n" + ] + } + ], + "source": [ + "sample_text = [\"This is a sample news article.\"] # Replace with your sample text\n", + "sample_vec = vectorizer.transform(sample_text)\n", + "\n", + "probs = model.predict_proba(sample_vec)\n", + "print(\"Probabilities:\", probs)\n" + ] + }, + { + "cell_type": "markdown", + "id": "37fa4daf", + "metadata": {}, + "source": [ + "# Load validation_data.csv" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "e984f239", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[nltk_data] Downloading package punkt to\n", + "[nltk_data] /Users/luis.guimaraes/nltk_data...\n", + "[nltk_data] Package punkt is already up-to-date!\n" + ] + } + ], + "source": [ + "\n", + "# Import stemmer from nltk\n", + "from nltk.stem import PorterStemmer\n", + "nltk.download('punkt') # Need this for word_tokenize\n", + "\n", + "# Initialize stemmer\n", + "stemmer = PorterStemmer()\n", + "\n", + "# Define stem_text function\n", + "def stem_text(text):\n", + "\tif isinstance(text, str):\n", + "\t\t# Tokenize the text\n", + "\t\ttokens = word_tokenize(text.lower())\n", + "\t\t# Apply stemming\n", + "\t\tstemmed_tokens = [stemmer.stem(token) for token in tokens]\n", + "\t\t# Join tokens back into a string\n", + "\t\treturn ' '.join(stemmed_tokens)\n", + "\treturn ''\n", + "\n", + "# Load validation data and prepare it for prediction\n", + "validation_df = pd.read_csv(\"dataset/validation_data.csv\")\n", + "\n", + "# Clean NaNs before applying\n", + "validation_df['title'] = validation_df['title'].fillna('')\n", + "validation_df['text'] = validation_df['text'].fillna('')\n", + "\n", + "# Apply stemming\n", + "validation_df['title'] = validation_df['title'].apply(stem_text)\n", + "validation_df['text'] = validation_df['text'].apply(stem_text)\n", + "\n", + "# Combine title and text\n", + "validation_df['text_clean'] = (validation_df['title'] + ' ' + validation_df['text']).str.strip()\n", + "validation_df = validation_df[validation_df['text_clean'] != '']\n", + "\n", + "# Prepare features (ignore label column as instructed)\n", + "X_val = validation_df['text_clean']\n", + "\n", + "# Transform using the same vectorizer used for training\n", + "X_val_vec = vectorizer.transform(X_val)\n", + "\n", + "# Get predictions (0 or 1)\n", + "predictions = model.predict(X_val_vec)\n", + "\n", + "# Add predictions to the validation dataframe\n", + "validation_df['predicted_label'] = predictions\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "217a1684", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "First few predictions:\n", + " title predicted_label\n", + "0 uk 's may 'receiv regular updat ' on london tu... 1\n", + "1 uk transport polic lead investig of london inc... 0\n", + "2 pacif nation crack down on north korean ship a... 1\n", + "3 three suspect al qaeda milit kill in yemen dro... 1\n", + "4 chines academ prod beij to consid north korea ... 1\n", + "\n", + "Prediction counts:\n", + "predicted_label\n", + "0 4207\n", + "1 749\n", + "Name: count, dtype: int64\n" + ] + } + ], + "source": [ + "# Display the first few predictions\n", + "print(\"First few predictions:\")\n", + "print(validation_df[['title', 'predicted_label']].head())\n", + "\n", + "validation_df[['title', 'predicted_label']].to_csv('validation_predictions.csv', index=False)\n", + "\n", + "# Create a copy with index as id\n", + "#result_df = pd.DataFrame({\n", + "# 'id': validation_df.index,\n", + "# 'predicted_label': validation_df['predicted_label']\n", + "#})\n", + "\n", + "validation_df.to_csv('validation_predictions-rf.csv', index=False)\n", + "\n", + "# Count of each prediction class\n", + "print(\"\\nPrediction counts:\")\n", + "print(validation_df['predicted_label'].value_counts())\n", + "\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "3.10.12", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/1.fake-news-XGBoost.ipynb b/1.fake-news-XGBoost.ipynb new file mode 100644 index 0000000..6781eda --- /dev/null +++ b/1.fake-news-XGBoost.ipynb @@ -0,0 +1,859 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "4dc82578", + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "from sklearn.model_selection import train_test_split\n", + "from sklearn.feature_extraction.text import TfidfVectorizer\n", + "from sklearn.ensemble import RandomForestClassifier\n", + "from sklearn.metrics import classification_report, confusion_matrix\n", + "import seaborn as sns\n", + "import matplotlib.pyplot as plt\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "1ff89aef", + "metadata": {}, + "outputs": [], + "source": [ + "df = pd.read_csv(\"dataset/data.csv\")\n", + "\n", + "# remove empty rows\n", + "df = df[df['title'] != '']\n", + "df = df[df['text'] != '']" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "4763a7f6", + "metadata": {}, + "outputs": [], + "source": [ + "# Remove duplicate rows based on the 'text' column\n", + "df = df.drop_duplicates(subset=['text']) \n", + "\n", + "# Remove rows with 'text' is NaN\n", + "df = df.dropna(subset=['text']) \n", + "\n", + "# Remove rows with 'label' is NaN\n", + "df = df.dropna(subset=['label']) \n", + "\n", + "# Remove rows with 'text' empty or only with whitespace\n", + "df = df[df['text'].str.strip() != ''] " + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "41340b02", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[nltk_data] Downloading package wordnet to\n", + "[nltk_data] /Users/luis.guimaraes/nltk_data...\n", + "[nltk_data] Package wordnet is already up-to-date!\n" + ] + } + ], + "source": [ + "import re\n", + "import string\n", + "import nltk\n", + "from nltk.tokenize import word_tokenize\n", + "from nltk.stem import WordNetLemmatizer\n", + "\n", + "# Download required NLTK data if not already downloaded\n", + "nltk.download('wordnet')\n", + "\n", + "def clean_text(text):\n", + " text = text.lower()\n", + " text = re.sub(r'\\[.*?\\]', '', text)\n", + " text = re.sub(r'http\\S+|www\\S+|https\\S+', '', text)\n", + " text = re.sub(r'<.*?>+', '', text)\n", + " text = re.sub(r'[%s]' % re.escape(string.punctuation), '', text)\n", + " text = re.sub(r'\\n', '', text)\n", + " text = re.sub(r'\\w*\\d\\w*', '', text)\n", + " \n", + " # Tokenize the text\n", + " tokens = word_tokenize(text)\n", + " \n", + " # Initialize Lemmatizer\n", + " lemmatizer = WordNetLemmatizer()\n", + " \n", + " # Lemmatize each token\n", + " lemmatized_tokens = [lemmatizer.lemmatize(token) for token in tokens]\n", + " \n", + " # Join tokens back into a string\n", + " text = ' '.join(lemmatized_tokens)\n", + " return text\n", + "\n", + "df['text_clean'] = df['title'] + \" \" + df['text']\n", + "df['text_clean'] = df['text_clean'].apply(clean_text)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "296fe39d", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "# Split the data into training and testing sets\n", + "X = df['text_clean']\n", + "y = df['label']\n", + "\n", + "# train\n", + "X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "a64b0b16", + "metadata": {}, + "outputs": [], + "source": [ + "#vectorizer = TfidfVectorizer(max_features=5000, stop_words='english')\n", + "vectorizer = TfidfVectorizer(\n", + " max_features=8000, # limit the number of features\n", + " stop_words='english',\n", + " min_df=5, # ignore rare words\n", + " max_df=0.8 # ignore overly common words\n", + ")\n", + "\n", + "# Fit only on training, transform both\n", + "X_train_vec = vectorizer.fit_transform(X_train)\n", + "X_test_vec = vectorizer.transform(X_test)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c381617c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
RandomForestClassifier(class_weight='balanced', n_jobs=-1, random_state=42)
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
" + ], + "text/plain": [ + "RandomForestClassifier(class_weight='balanced', n_jobs=-1, random_state=42)" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Install XGBoost if needed\n", + "!pip install xgboost\n", + "\n", + "model = RandomForestClassifier(\n", + " n_estimators=100, # Number of trees\n", + " max_depth=None, # Maximum depth of trees (None means unlimited)\n", + " min_samples_split=2,\n", + " min_samples_leaf=1,\n", + " class_weight='balanced', # Same as your Logistic Regression\n", + " random_state=42, # For reproducibility\n", + " n_jobs=-1 # Use all available cores\n", + ")\n", + "\n", + "# Train the model (this stays the same)\n", + "model.fit(X_train_vec, y_train)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "484d6523", + "metadata": {}, + "outputs": [], + "source": [ + "# Predict on the test set\n", + "y_pred = model.predict(X_test_vec)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "5f194883", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['tfidf_vectorizer_rf.pkl']" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import joblib\n", + "\n", + "# Save model\n", + "joblib.dump(model, 'random_forest_model.pkl')\n", + "\n", + "# Save TF-IDF vectorizer (this stays the same)\n", + "joblib.dump(vectorizer, 'tfidf_vectorizer_rf.pkl')" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "2ef43d5f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " precision recall f1-score support\n", + "\n", + " 0 1.00 0.99 0.99 3241\n", + " 1 0.99 1.00 1.00 3954\n", + "\n", + " accuracy 1.00 7195\n", + " macro avg 1.00 1.00 1.00 7195\n", + "weighted avg 1.00 1.00 1.00 7195\n", + "\n" + ] + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "\n", + "# Classification report and confusion matrix\n", + "print(classification_report(y_test, y_pred))\n", + "sns.heatmap(confusion_matrix(y_test, y_pred), annot=True, fmt='d')\n", + "plt.title(\"Confusion Matrix\")\n", + "plt.show()\n" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "ab90e014", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Baseline accuracy: 0.54954829742877\n" + ] + } + ], + "source": [ + "from sklearn.dummy import DummyClassifier\n", + "dummy = DummyClassifier(strategy=\"most_frequent\")\n", + "dummy.fit(X_train_vec, y_train)\n", + "print(\"Baseline accuracy:\", dummy.score(X_test_vec, y_test))\n" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "a787659a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Probabilities: [[0.99 0.01]]\n" + ] + } + ], + "source": [ + "sample_text = [\"This is a sample news article.\"] # Replace with your sample text\n", + "sample_vec = vectorizer.transform(sample_text)\n", + "\n", + "probs = model.predict_proba(sample_vec)\n", + "print(\"Probabilities:\", probs)\n" + ] + }, + { + "cell_type": "markdown", + "id": "37fa4daf", + "metadata": {}, + "source": [ + "# Load validation_data.csv" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e984f239", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[nltk_data] Downloading package punkt to\n", + "[nltk_data] /Users/luis.guimaraes/nltk_data...\n", + "[nltk_data] Package punkt is already up-to-date!\n" + ] + } + ], + "source": [ + "\n", + "# Import stemmer from nltk\n", + "from nltk.stem import PorterStemmer\n", + "nltk.download('punkt') # Need this for word_tokenize\n", + "\n", + "# Initialize stemmer\n", + "stemmer = PorterStemmer()\n", + "\n", + "# Define stem_text function\n", + "def stem_text(text):\n", + "\tif isinstance(text, str):\n", + "\t\t# Tokenize the text\n", + "\t\ttokens = word_tokenize(text.lower())\n", + "\t\t# Apply stemming\n", + "\t\tstemmed_tokens = [stemmer.stem(token) for token in tokens]\n", + "\t\t# Join tokens back into a string\n", + "\t\treturn ' '.join(stemmed_tokens)\n", + "\treturn ''\n", + "\n", + "# Load validation data and prepare it for prediction\n", + "validation_df = pd.read_csv(\"dataset/validation_data.csv\")\n", + "\n", + "# Clean NaNs before applying\n", + "validation_df['title'] = validation_df['title'].fillna('')\n", + "validation_df['text'] = validation_df['text'].fillna('')\n", + "\n", + "# Apply stemming\n", + "validation_df['title'] = validation_df['title'].apply(stem_text)\n", + "validation_df['text'] = validation_df['text'].apply(stem_text)\n", + "\n", + "# Combine title and text\n", + "validation_df['text_clean'] = (validation_df['title'] + ' ' + validation_df['text']).str.strip()\n", + "validation_df = validation_df[validation_df['text_clean'] != '']\n", + "\n", + "# Prepare features (ignore label column as instructed)\n", + "X_val = validation_df['text_clean']\n", + "\n", + "# Transform using the same vectorizer used for training\n", + "X_val_vec = vectorizer.transform(X_val)\n", + "\n", + "# Get predictions (0 or 1)\n", + "predictions = model.predict(X_val_vec)\n", + "\n", + "# Add predictions to the validation dataframe\n", + "validation_df['predicted_label'] = predictions\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "217a1684", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "First few predictions:\n", + " title predicted_label\n", + "0 uk 's may 'receiv regular updat ' on london tu... 1\n", + "1 uk transport polic lead investig of london inc... 1\n", + "2 pacif nation crack down on north korean ship a... 1\n", + "3 three suspect al qaeda milit kill in yemen dro... 1\n", + "4 chines academ prod beij to consid north korea ... 1\n", + "\n", + "Prediction counts:\n", + "predicted_label\n", + "0 3688\n", + "1 1268\n", + "Name: count, dtype: int64\n" + ] + } + ], + "source": [ + "# Display the first few predictions\n", + "print(\"First few predictions:\")\n", + "print(validation_df[['title', 'predicted_label']].head())\n", + "\n", + "validation_df[['title', 'predicted_label']].to_csv('validation_predictions.csv', index=False)\n", + "\n", + "# Create a copy with index as id\n", + "#result_df = pd.DataFrame({\n", + "# 'id': validation_df.index,\n", + "# 'predicted_label': validation_df['predicted_label']\n", + "#})\n", + "\n", + "validation_df.to_csv('validation_predictions-rf.csv', index=False)\n", + "\n", + "# Count of each prediction class\n", + "print(\"\\nPrediction counts:\")\n", + "print(validation_df['predicted_label'].value_counts())\n", + "\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "3.10.12", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/main.py b/main.py new file mode 100644 index 0000000..980e6d2 --- /dev/null +++ b/main.py @@ -0,0 +1,182 @@ +import pandas as pd +from sklearn.model_selection import train_test_split +from sklearn.feature_extraction.text import TfidfVectorizer +from sklearn.linear_model import LogisticRegression +from sklearn.metrics import classification_report, confusion_matrix +from sklearn.base import BaseEstimator, TransformerMixin +from sklearn.pipeline import Pipeline +from sklearn.dummy import DummyClassifier +import seaborn as sns +import matplotlib.pyplot as plt +import re +import string +import nltk +from nltk.tokenize import word_tokenize +from nltk.stem import WordNetLemmatizer +import pandas as pd +from sklearn.model_selection import train_test_split +from sklearn.feature_extraction.text import TfidfVectorizer +from sklearn.linear_model import LogisticRegression +from sklearn.metrics import classification_report, confusion_matrix +from sklearn.base import BaseEstimator, TransformerMixin +from sklearn.pipeline import Pipeline +from sklearn.dummy import DummyClassifier +import seaborn as sns +import matplotlib.pyplot as plt +import re +import string +import nltk +from nltk.tokenize import word_tokenize +from nltk.stem import WordNetLemmatizer +import joblib +import os + +# Download required NLTK data +nltk.download('wordnet') +nltk.download('punkt') + +# Custom text preprocessor class +class TextPreprocessor(BaseEstimator, TransformerMixin): + def __init__(self): + self.lemmatizer = WordNetLemmatizer() + + def clean_text(self, text): + text = text.lower() + text = re.sub(r'\[.*?\]', '', text) + text = re.sub(r'http\S+|www\S+|https\S+', '', text) + text = re.sub(r'<.*?>+', '', text) + text = re.sub(r'[%s]' % re.escape(string.punctuation), '', text) + text = re.sub(r'\n', '', text) + text = re.sub(r'\w*\d\w*', '', text) + + tokens = word_tokenize(text) + lemmatized_tokens = [self.lemmatizer.lemmatize(token) for token in tokens] + return ' '.join(lemmatized_tokens) + + def fit(self, X, y=None): + return self + + def transform(self, X): + # Check if 'cleaned_text' column exists, if so, return it directly + if isinstance(X, pd.DataFrame) and 'cleaned_text' in X.columns: + return X['cleaned_text'] + + # Otherwise, perform cleaning + if isinstance(X, pd.DataFrame): + # Combine title and text for DataFrame input + return (X['title'] + " " + X['text']).apply(self.clean_text) + return X.apply(self.clean_text) + +# Define cache file paths +TRAIN_CACHE_PATH = 'dataset/train_cleaned.parquet' +TEST_CACHE_PATH = 'dataset/test_cleaned.parquet' + +# Load and prepare base data +df = pd.read_csv("dataset/data.csv") + +# Data cleaning +df = df[ + (df['title'] != '') & + (df['text'] != '') & + (df['text'].str.strip() != '') +].drop_duplicates(subset=['text']).dropna(subset=['text', 'label']) + +# Split data FIRST to prevent leakage +train_df, test_df = train_test_split( + df[['title', 'text', 'label']], # Keep raw text for pipeline processing + test_size=0.2, + random_state=42, + stratify=df['label'] +) + +print("\nTraining set class distribution:") +print(train_df['label'].value_counts()) +print("\nTest set class distribution:") +print(test_df['label'].value_counts()) + +# Check for cached cleaned data +if os.path.exists(TRAIN_CACHE_PATH) and os.path.exists(TEST_CACHE_PATH): + print("Loading cleaned data from cache...") + train_df = pd.read_parquet(TRAIN_CACHE_PATH) + test_df = pd.read_parquet(TEST_CACHE_PATH) + print("Cleaned data loaded.") +else: + print("Cleaning data and saving to cache...") + # Apply cleaning and add 'cleaned_text' column + preprocessor = TextPreprocessor() + train_df['cleaned_text'] = preprocessor.transform(train_df) + test_df['cleaned_text'] = preprocessor.transform(test_df) + + # Save cleaned data to parquet + train_df.to_parquet(TRAIN_CACHE_PATH, index=False) + test_df.to_parquet(TEST_CACHE_PATH, index=False) + print("Cleaned data saved to cache.") + + +# Create preprocessing pipeline +# The TextPreprocessor will now use the 'cleaned_text' column if it exists +pipeline = Pipeline([ + ('preprocessor', TextPreprocessor()), + ('vectorizer', TfidfVectorizer( + max_features=8000, + stop_words='english', + min_df=5, + max_df=0.8 + )), + ('classifier', LogisticRegression( + class_weight='balanced', + solver='saga', + penalty='l1', + C=0.5, + max_iter=1000, + random_state=42 + )) +]) + +# Train model +# The pipeline will now use the 'cleaned_text' column from the loaded dataframes +pipeline.fit(train_df, train_df['label']) + +# Evaluate +y_pred = pipeline.predict(test_df) +print("Evaluation on Test Set:") +print(classification_report(test_df['label'], y_pred)) +sns.heatmap(confusion_matrix(test_df['label'], y_pred), annot=True, fmt='d') +plt.title("Confusion Matrix (Test Set)") +plt.show() + +# Evaluate on Training Set +print("\nEvaluation on Training Set:") +y_train_pred = pipeline.predict(train_df) +print(classification_report(train_df['label'], y_train_pred)) +sns.heatmap(confusion_matrix(train_df['label'], y_train_pred), annot=True, fmt='d') +plt.title("Confusion Matrix (Training Set)") +plt.show() + +# Save entire pipeline +joblib.dump(pipeline, 'text_classification_pipeline.pkl') + +# Baseline evaluation +dummy = DummyClassifier(strategy="most_frequent") +# Use the original text columns for baseline evaluation +dummy.fit(train_df[['title', 'text']], train_df['label']) +print("Baseline accuracy:", dummy.score(test_df[['title', 'text']], test_df['label'])) + +# Validation processing (using same lemmatization) +validation_df = pd.read_csv("dataset/validation_data.csv").fillna({'title': '', 'text': ''}) +# The TextPreprocessor will clean the validation data as it's not cached +validation_df['cleaned_text'] = TextPreprocessor().transform(validation_df) +print("Validation data processed.") + +print("Generating predictions for validation data...") +# Predict using the cleaned text column +predictions = pipeline.predict(validation_df) +validation_df['predicted_label'] = predictions +print("Predictions generated.") + +# Save results +print("Saving validation predictions...") +validation_df[['title', 'predicted_label']].to_csv('validation_predictions.csv', index=False) +print("Validation predictions saved.") +print("\nPrediction counts:") +print(validation_df['predicted_label'].value_counts())