{ "cells": [ { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "import os\n", "import logging\n", "import warnings\n", "\n", "os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2' # Suppresses INFO and WARNING messages\n", "\n", "# Configure logging to suppress TensorFlow messages\n", "logging.getLogger('tensorflow').setLevel(logging.ERROR) # Set level to ERROR to suppress INFO and WARNING messages\n" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "import pandas as pd\n", "import numpy as np\n", "from datasets import load_dataset\n", "\n", "import tensorflow as tf\n", "from tensorflow.keras import layers\n" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "dataset = load_dataset(\"x-g85/x_g85_fn_dataset\", streaming=True)\n", "\n", "train = pd.DataFrame(dataset[\"train\"])\n", "valid = pd.DataFrame(dataset[\"valid\"])\n", "test = pd.DataFrame(dataset[\"test\"])" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "X_train = train[\"text\"]\n", "y_train = train[\"label\"]\n", "\n", "X_valid = valid[\"text\"]\n", "y_vaild = valid[\"label\"]\n", "\n", "X_test = test[\"text\"]\n", "y_test = test[\"label\"]\n" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['John',\n", " 'McCain',\n", " 'says',\n", " 'NSA',\n", " 'chief',\n", " 'Keith',\n", " 'Alexander',\n", " \"'should\",\n", " 'resign',\n", " 'or',\n", " 'be',\n", " \"fired'.\",\n", " 'Senator',\n", " 'gives',\n", " 'interview',\n", " 'to',\n", " 'Der',\n", " 'Spiegel,',\n", " 'saying',\n", " 'general',\n", " 'should',\n", " \"'be\",\n", " 'held',\n", " \"accountable'\",\n", " 'for',\n", " 'Edward',\n", " 'Snowden',\n", " 'leaks.']" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "X_train[0].split()" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "207" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Calculate the rounded average number of words per sentence in one line\n", "rounded_average_words_per_sentence = round(sum(len(sentence.split()) for sentence in X_train) / len(X_train))\n", "\n", "rounded_average_words_per_sentence" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": [ "# Text Tokenization | Vectorization Parameters\n", "max_vocab_length = 5000 # how many unique words to use (i.e num rows in embedding vector)\n", "max_length = 300 # max number of words in a comment to use; default = 300\n", "embed_dim = 256 # how big is each word vector" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [], "source": [ "# Setup Text Vectorization\n", "# Serialization Issue: https://github.com/onnx/tensorflow-onnx/issues/1886\n", "\n", "text_vectorizer = layers.TextVectorization(\n", " max_tokens= max_vocab_length,\n", " output_mode=\"int\",\n", " output_sequence_length=max_length,\n", " name=\"TextVec\",\n", " \n", ")\n", "\n", "text_vectorizer.adapt(X_train, batch_size=32)" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [], "source": [ "# # Setup Text Tokenizer\n", "# from tensorflow.keras.preprocessing.text import Tokenizer\n", "# from tensorflow.keras.preprocessing.sequence import pad_sequences\n", "\n", "# tokenizer = Tokenizer(num_words=max_vocab_length)\n", "# tokenizer.fit_on_texts(X_train)\n", "\n", "\n", "# tokenized_train = tokenizer.texts_to_sequences(X_train)\n", "# tokenized_valid= tokenizer.texts_to_sequences(X_valid)\n", "# tokenized_test = tokenizer.texts_to_sequences(X_test)\n", "\n", "# X_train = pad_sequences(tokenized_train, maxlen=max_length)\n", "# X_valid = pad_sequences(tokenized_valid, maxlen=max_length)\n", "# X_test = pad_sequences(tokenized_test, maxlen=max_length)\n", "\n", "\n", "# # Save the tokenizer to a JSON file\n", "# tokenizer_json = tokenizer.to_json()\n", "# with open('model/tokenizer.json', 'w') as file:\n", "# file.write(tokenizer_json)" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [], "source": [ "\n", "# Model Creation\n", "# Issue(Serialization): https://github.com/tflearn/tflearn/issues/605\n", "\n", "# Input\n", "inputs = layers.Input(shape=(1,), dtype=tf.string, name=\"InputLayer\") # For TextVectorization\n", "x = text_vectorizer(inputs) # For TextVectorization\n", "\n", "# inputs = layers.Input(shape=(max_length,), name=\"InputLayer\")\n", "\n", "# Embedding layer\n", "\n", "x = layers.Embedding(input_dim=max_vocab_length, output_dim=embed_dim)(x) # For TextVectorization\n", "# x = layers.Embedding(input_dim=max_vocab_length, output_dim=embed_dim)(inputs) \n", "\n", "# LSTM layers\n", "x = layers.LSTM(100, use_cudnn=False)(x) # LSTM layer without return_sequences\n", "x = layers.Dropout(0.5)(x) # Reduce dropout rate slightly\n", "\n", "# Fully connected layers\n", "x = layers.Dense(64, activation=\"relu\")(x)\n", "x = layers.Dropout(0.3)(x)\n", "\n", "x = layers.Dense(32, activation=\"relu\")(x)\n", "x = layers.Dropout(0.2)(x)\n", "\n", "# Output layer\n", "outputs = layers.Dense(1, activation=\"sigmoid\")(x) # Binary classification\n", "\n" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [], "source": [ "model_01= tf.keras.Model(inputs, outputs, name = \"model_01\")" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
Model: \"model_01\"\n",
       "
\n" ], "text/plain": [ "\u001b[1mModel: \"model_01\"\u001b[0m\n" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓\n",
       "┃ Layer (type)                     Output Shape                  Param # ┃\n",
       "┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩\n",
       "│ InputLayer (InputLayer)         │ (None, 1)              │             0 │\n",
       "├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
       "│ TextVec (TextVectorization)     │ (None, 300)            │             0 │\n",
       "├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
       "│ embedding (Embedding)           │ (None, 300, 256)       │     1,280,000 │\n",
       "├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
       "│ lstm (LSTM)                     │ (None, 100)            │       142,800 │\n",
       "├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
       "│ dropout (Dropout)               │ (None, 100)            │             0 │\n",
       "├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
       "│ dense (Dense)                   │ (None, 64)             │         6,464 │\n",
       "├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
       "│ dropout_1 (Dropout)             │ (None, 64)             │             0 │\n",
       "├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
       "│ dense_1 (Dense)                 │ (None, 32)             │         2,080 │\n",
       "├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
       "│ dropout_2 (Dropout)             │ (None, 32)             │             0 │\n",
       "├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
       "│ dense_2 (Dense)                 │ (None, 1)              │            33 │\n",
       "└─────────────────────────────────┴────────────────────────┴───────────────┘\n",
       "
\n" ], "text/plain": [ "┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓\n", "┃\u001b[1m \u001b[0m\u001b[1mLayer (type) \u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mOutput Shape \u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1m Param #\u001b[0m\u001b[1m \u001b[0m┃\n", "┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩\n", "│ InputLayer (\u001b[38;5;33mInputLayer\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m1\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │\n", "├─────────────────────────────────┼────────────────────────┼───────────────┤\n", "│ TextVec (\u001b[38;5;33mTextVectorization\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m300\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │\n", "├─────────────────────────────────┼────────────────────────┼───────────────┤\n", "│ embedding (\u001b[38;5;33mEmbedding\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m300\u001b[0m, \u001b[38;5;34m256\u001b[0m) │ \u001b[38;5;34m1,280,000\u001b[0m │\n", "├─────────────────────────────────┼────────────────────────┼───────────────┤\n", "│ lstm (\u001b[38;5;33mLSTM\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m100\u001b[0m) │ \u001b[38;5;34m142,800\u001b[0m │\n", "├─────────────────────────────────┼────────────────────────┼───────────────┤\n", "│ dropout (\u001b[38;5;33mDropout\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m100\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │\n", "├─────────────────────────────────┼────────────────────────┼───────────────┤\n", "│ dense (\u001b[38;5;33mDense\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m64\u001b[0m) │ \u001b[38;5;34m6,464\u001b[0m │\n", "├─────────────────────────────────┼────────────────────────┼───────────────┤\n", "│ dropout_1 (\u001b[38;5;33mDropout\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m64\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │\n", "├─────────────────────────────────┼────────────────────────┼───────────────┤\n", "│ dense_1 (\u001b[38;5;33mDense\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m32\u001b[0m) │ \u001b[38;5;34m2,080\u001b[0m │\n", "├─────────────────────────────────┼────────────────────────┼───────────────┤\n", "│ dropout_2 (\u001b[38;5;33mDropout\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m32\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │\n", "├─────────────────────────────────┼────────────────────────┼───────────────┤\n", "│ dense_2 (\u001b[38;5;33mDense\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m1\u001b[0m) │ \u001b[38;5;34m33\u001b[0m │\n", "└─────────────────────────────────┴────────────────────────┴───────────────┘\n" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "
 Total params: 1,431,377 (5.46 MB)\n",
       "
\n" ], "text/plain": [ "\u001b[1m Total params: \u001b[0m\u001b[38;5;34m1,431,377\u001b[0m (5.46 MB)\n" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "
 Trainable params: 1,431,377 (5.46 MB)\n",
       "
\n" ], "text/plain": [ "\u001b[1m Trainable params: \u001b[0m\u001b[38;5;34m1,431,377\u001b[0m (5.46 MB)\n" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "
 Non-trainable params: 0 (0.00 B)\n",
       "
\n" ], "text/plain": [ "\u001b[1m Non-trainable params: \u001b[0m\u001b[38;5;34m0\u001b[0m (0.00 B)\n" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# Get the summary\n", "model_01.summary()" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [], "source": [ "# Model Compile\n", "from tensorflow.keras.metrics import AUC, Precision \n", "\n", "model_01.compile(loss=\"binary_crossentropy\",\n", " optimizer = tf.keras.optimizers.Adam(learning_rate=0.001),\n", " metrics = [\"accuracy\", Precision(), AUC()])" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Folder already exists at: model_logs\n" ] } ], "source": [ "import os\n", "\n", "model_logs = \"model_logs\"\n", "\n", "# Check if the `model_logs` directory exists, create it if not\n", "if not os.path.exists(model_logs):\n", " os.makedirs(model_logs)\n", " print(f\"Folder created at: {model_logs}\")\n", "else:\n", " print(f\"Folder already exists at: {model_logs}\")" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Epoch 1/10\n", "\u001b[1m2674/2674\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1773s\u001b[0m 659ms/step - accuracy: 0.5918 - auc: 0.6342 - loss: 0.6441 - precision: 0.5722 - val_accuracy: 0.7242 - val_auc: 0.8294 - val_loss: 0.4581 - val_precision: 0.6573\n", "Epoch 2/10\n", "\u001b[1m2674/2674\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1743s\u001b[0m 652ms/step - accuracy: 0.7226 - auc: 0.8324 - loss: 0.4403 - precision: 0.6735 - val_accuracy: 0.7320 - val_auc: 0.8443 - val_loss: 0.4240 - val_precision: 0.6555\n", "Epoch 3/10\n", "\u001b[1m2674/2674\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1795s\u001b[0m 671ms/step - accuracy: 0.7393 - auc: 0.8508 - loss: 0.3992 - precision: 0.6759 - val_accuracy: 0.7465 - val_auc: 0.8536 - val_loss: 0.3860 - val_precision: 0.6696\n", "Epoch 4/10\n", "\u001b[1m2674/2674\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1776s\u001b[0m 664ms/step - accuracy: 0.7488 - auc: 0.8573 - loss: 0.3855 - precision: 0.6789 - val_accuracy: 0.7474 - val_auc: 0.8587 - val_loss: 0.3812 - val_precision: 0.6682\n", "Epoch 5/10\n", "\u001b[1m2674/2674\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1795s\u001b[0m 671ms/step - accuracy: 0.7519 - auc: 0.8631 - loss: 0.3734 - precision: 0.6765 - val_accuracy: 0.7493 - val_auc: 0.8576 - val_loss: 0.3811 - val_precision: 0.6726\n", "Epoch 6/10\n", "\u001b[1m2674/2674\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1810s\u001b[0m 677ms/step - accuracy: 0.7557 - auc: 0.8653 - loss: 0.3688 - precision: 0.6772 - val_accuracy: 0.7472 - val_auc: 0.8545 - val_loss: 0.3816 - val_precision: 0.6679\n", "Epoch 7/10\n", "\u001b[1m2674/2674\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2580s\u001b[0m 965ms/step - accuracy: 0.7593 - auc: 0.8674 - loss: 0.3672 - precision: 0.6814 - val_accuracy: 0.7459 - val_auc: 0.8539 - val_loss: 0.3945 - val_precision: 0.6731\n", "Epoch 8/10\n", "\u001b[1m2674/2674\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1879s\u001b[0m 703ms/step - accuracy: 0.7632 - auc: 0.8724 - loss: 0.3648 - precision: 0.6854 - val_accuracy: 0.7499 - val_auc: 0.8559 - val_loss: 0.3984 - val_precision: 0.6770\n", "Epoch 9/10\n", "\u001b[1m2674/2674\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1897s\u001b[0m 709ms/step - accuracy: 0.7656 - auc: 0.8766 - loss: 0.3627 - precision: 0.6930 - val_accuracy: 0.7480 - val_auc: 0.8540 - val_loss: 0.3958 - val_precision: 0.6733\n", "Epoch 10/10\n", "\u001b[1m2674/2674\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1933s\u001b[0m 723ms/step - accuracy: 0.7743 - auc: 0.8889 - loss: 0.3518 - precision: 0.7123 - val_accuracy: 0.7478 - val_auc: 0.8558 - val_loss: 0.3971 - val_precision: 0.6801\n" ] } ], "source": [ "\n", "# Early Stopping\n", "# from tensorflow.keras.callbacks import EarlyStopping\n", "# early_stopping = EarlyStopping(monitor='val_loss', patience=5)\n", "\n", "# Model Fit\n", "\n", "history_model_01 = model_01.fit(X_train, y_train, epochs=10, batch_size=32,\n", " validation_data = (X_valid, y_vaild),\n", " callbacks = [tf.keras.callbacks.TensorBoard(\"model_logs\")])" ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\u001b[1m149/149\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m33s\u001b[0m 211ms/step\n" ] }, { "data": { "text/plain": [ "array([[0.9999999 ],\n", " [0.5088127 ],\n", " [0.51265997],\n", " [0.51331687],\n", " [0.51750326]], dtype=float32)" ] }, "execution_count": 16, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Model Prediction\n", "model_01_pred_probs = model_01.predict(X_test)\n", "model_01_pred_probs[:5]" ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [], "source": [ "# Helper Functions\n", "\n", "import itertools\n", "import matplotlib.pyplot as plt\n", "import numpy as np\n", "from sklearn.metrics import confusion_matrix, accuracy_score, precision_recall_fscore_support\n", "\n", "\n", "\n", "def calculate_results(y_true, y_pred):\n", " \"\"\"\n", " Calculates model accuracy, precision, recall and f1 score of a binary classification model.\n", "\n", " Args:\n", " y_true: true labels in the form of a 1D array\n", " y_pred: predicted labels in the form of a 1D array\n", "\n", " Returns a dictionary of accuracy, precision, recall, f1-score.\n", " \"\"\"\n", " # Calculate model accuracy\n", " model_accuracy = accuracy_score(y_true, y_pred) * 100\n", " # Calculate model precision, recall and f1 score using \"weighted average\n", " model_precision, model_recall, model_f1, _ = precision_recall_fscore_support(y_true, y_pred, average=\"weighted\")\n", " model_results = {\"accuracy\": model_accuracy,\n", " \"precision\": model_precision,\n", " \"recall\": model_recall,\n", " \"f1\": model_f1}\n", " return model_results\n", "\n", "\n", "def make_confusion_matrix(y_true, y_pred, classes=None, figsize=(10, 10), text_size=15, norm=False, savefig=False): \n", " \"\"\"Makes a labelled confusion matrix comparing predictions and ground truth labels.\n", "\n", " If classes is passed, confusion matrix will be labelled, if not, integer class values\n", " will be used.\n", "\n", " Args:\n", " y_true: Array of truth labels (must be same shape as y_pred).\n", " y_pred: Array of predicted labels (must be same shape as y_true).\n", " classes: Array of class labels (e.g. string form). If `None`, integer labels are used.\n", " figsize: Size of output figure (default=(10, 10)).\n", " text_size: Size of output figure text (default=15).\n", " norm: normalize values or not (default=False).\n", " savefig: save confusion matrix to file (default=False).\n", " \n", " Returns:\n", " A labelled confusion matrix plot comparing y_true and y_pred.\n", "\n", " Example usage:\n", " make_confusion_matrix(y_true=test_labels, # ground truth test labels\n", " y_pred=y_preds, # predicted labels\n", " classes=class_names, # array of class label names\n", " figsize=(15, 15),\n", " text_size=10)\n", " \"\"\" \n", " # Create the confustion matrix\n", " cm = confusion_matrix(y_true, y_pred)\n", " cm_norm = cm.astype(\"float\") / cm.sum(axis=1)[:, np.newaxis] # normalize it\n", " n_classes = cm.shape[0] # find the number of classes we're dealing with\n", "\n", " # Plot the figure and make it pretty\n", " fig, ax = plt.subplots(figsize=figsize)\n", " cax = ax.matshow(cm, cmap=plt.cm.Blues) # colors will represent how 'correct' a class is, darker == better\n", " fig.colorbar(cax)\n", "\n", " # Are there a list of classes?\n", " if classes:\n", " labels = classes\n", " else:\n", " labels = np.arange(cm.shape[0])\n", " \n", " # Label the axes\n", " ax.set(title=\"Confusion Matrix\",\n", " xlabel=\"Predicted label\",\n", " ylabel=\"True label\",\n", " xticks=np.arange(n_classes), # create enough axis slots for each class\n", " yticks=np.arange(n_classes), \n", " xticklabels=labels, # axes will labeled with class names (if they exist) or ints\n", " yticklabels=labels)\n", " \n", " # Make x-axis labels appear on bottom\n", " ax.xaxis.set_label_position(\"bottom\")\n", " ax.xaxis.tick_bottom()\n", "\n", " # Set the threshold for different colors\n", " threshold = (cm.max() + cm.min()) / 2.\n", "\n", " # Plot the text on each cell\n", " for i, j in itertools.product(range(cm.shape[0]), range(cm.shape[1])):\n", " if norm:\n", " plt.text(j, i, f\"{cm[i, j]} ({cm_norm[i, j]*100:.1f}%)\",\n", " horizontalalignment=\"center\",\n", " color=\"white\" if cm[i, j] > threshold else \"black\",\n", " size=text_size)\n", " else:\n", " plt.text(j, i, f\"{cm[i, j]}\",\n", " horizontalalignment=\"center\",\n", " color=\"white\" if cm[i, j] > threshold else \"black\",\n", " size=text_size)\n", "\n", " # Save the figure to the current working directory\n", " if savefig:\n", " fig.savefig(\"confusion_matrix.png\")" ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 18, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Convert model 2 pred probability to labels\n", "model_01_preds = tf.squeeze(tf.round(model_01_pred_probs))\n", "model_01_preds[:1]" ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'accuracy': 74.98948254101809,\n", " 'precision': 0.7918142582139361,\n", " 'recall': 0.7498948254101809,\n", " 'f1': 0.7373717047030267}" ] }, "execution_count": 19, "metadata": {}, "output_type": "execute_result" } ], "source": [ "calculate_results(y_true=y_test, y_pred=model_01_preds)" ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [ { "data": { "image/png": "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0qWL/Pz8ZBiGFixYUKzn379/vwzDUExMTLGe92rWoUMHdejQoaTDAACUABIQAJawZ88ePfzww6pdu7a8vLzk6+urm266SW+88YbOnj3r1GtHRkZq27ZtevHFF/XJJ5/ouuuuc+r1zNS3b18ZhiFfX98CP8ddu3bJMAwZhqFXX321yOc/fPiwoqOjFR8fXwzRAkAJyZsFy0pLKcYzIABK3OLFi/Xf//5Xnp6eevDBB9W4cWOlp6dr7dq1Gj58uHbs2KH333/fKdc+e/as4uLi9Nxzz2nw4MFOuUZYWJjOnj2rsmXLOuX8l1KmTBmdOXNGCxcu1D333OOwb/bs2fLy8tK5c+cu69yHDx/WuHHjVLNmTTVv3rzQxy1duvSyrgcAuPqRgAAoUfv27VOvXr0UFhamFStWqGrVqvZ9UVFR2r17txYvXuy06x87dkyS5O/v77RrGIYhLy8vp53/Ujw9PXXTTTfpf//7X74EZM6cOerWrZu+/PJLU2I5c+aMypUrJw8PD1OuBwCwntJd3wFgeZMmTVJKSopmzJjhkHzkqVu3rp544gn7emZmpiZMmKA6derI09NTNWvW1LPPPqu0tDSH42rWrKk77rhDa9eu1fXXXy8vLy/Vrl1bH3/8sX1MdHS0wsLCJEnDhw+XYRiqWbOmpNzWpbzv/yk6OlrGv2YniY2N1c033yx/f39VqFBB9evX17PPPmvff6FnQFasWKG2bduqfPny8vf31913362dO3cWeL3du3erb9++8vf3l5+fn/r166czZ85c+IP9l969e+u7777TqVOn7Nt++ukn7dq1S7179843/uTJk3r66afVpEkTVahQQb6+vuratat++eUX+5iVK1eqdevWkqR+/frZW7ny7rNDhw5q3LixNm/erHbt2qlcuXL2z+Xfz4BERkbKy8sr3/1HRESoYsWKOnz4cKHvFQCKLG8WLCstpRgJCIAStXDhQtWuXVs33nhjocYPGDBAY8aMUcuWLfXaa6+pffv2mjhxonr16pVv7O7du/Wf//xHt956q6ZMmaKKFSuqb9++2rFjhySpR48eeu211yRJ9913nz755BO9/vrrRYp/x44duuOOO5SWlqbx48drypQpuuuuu/Tjjz9e9Lhly5YpIiJCR48eVXR0tIYNG6Z169bppptu0v79+/ONv+eee3T69GlNnDhR99xzj2Ji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6afKzs7Whx9+KON8w+3MmTPl7++vlStXqkuXLnr99dc1atQo9ejRQ5L07rvv6vvvv7/gtX///Xd99tlnio2NVefOnSVJtWvXtu/P+2tUYGCg/P39JeVWTF566SUtW7ZM4eHh9mPWrl2r9957T+3bt9c777yjOnXqaMqUKZKk+vXra9u2bXrllVeK9Nk8//zz9u9r1qypp59+WnPnznVIQM6dO6ePP/5Y1apVkyS99dZb6tatm6ZMmaLg4GCNHTtWU6ZMsX8mtWrV0q+//qr33nuPBAQAitHBgwfl6+trX/f09LzkMVFRUdq+fbvWrl3rsH3QoEH275s0aaKqVauqU6dO2rNnj+rUqVN8QRcCCQiAUmPRokWqUKGCMjIylJ2drd69ezv8pahJkyYOPay//PKLdu/eLR8fH4fznDt3Tnv27FFycrKOHDmiNm3a2PeVKVNG1113Xb42rDzx8fFyd3dX+/btCx337t27debMGd16660O29PT09WiRQtJ0s6dOx3ikGRPVopi3rx5evPNN7Vnzx6lpKQoMzPT4R83SapRo4Y9+ci7TnZ2thISEuTj46M9e/aof//+GjhwoH1MZmam/Pz8ihwPAODCfH198/2MvpjBgwdr0aJFWr16tapXr37RsXn/puzevVt16tRRcHCwNm7c6DAmKSlJki743MjlIgEBUGp07NhR77zzjjw8PBQSEqIyZRx/xJUvX95hPSUlRa1atbK3Fv1TlSpVLiuGvJaqokhJSZEkLV682OEXf6lwf+0qrLi4OPXp00fjxo1TRESE/Pz8NHfuXHtVpSixfvDBB/kSInd392KLFQDMdDkzTzlTUWPJycnRkCFDNH/+fK1cuVK1atW65DF5U6VXrVpVUu4fm1588UUdPXpUgYGBkqTY2Fj5+vqqUaNGRQvoEkhAAJQa5cuXV926dQs9vmXLlpo3b54CAwMv+BemqlWrasOGDfZZRDIzM7V582a1bNmywPFNmjRRdna2Vq1aZW/B+qe8CkxWVpZ9W6NGjeTp6akDBw5csHLSsGFD+wP1edavX3/pm/yHdevWKSwsTM8995x92x9//JFv3IEDB3T48GGFhITYr+Pm5qb69esrKChIISEh2rt3r/r06VOk6wMAnCMqKkpz5szR119/LR8fH/szG35+fvL29taePXs0Z84c3X777apUqZK2bt2qoUOHql27dmratKkkqUuXLmrUqJEeeOABTZo0SYmJiXr++ecVFRVVrH8Mk5gFC4AL69OnjypXrqy7775ba9as0b59+7Ry5Uo9/vjj+vPPPyVJTzzxhF5++WUtWLBAv/32mx577LGLvsOjZs2aioyM1EMPPaQFCxbYz/nZZ59JksLCwmQYhhYtWqRjx44pJSVFPj4+evrppzV06FDNmjVLe/bs0ZYtW/TWW29p1qxZkqRHHnlEu3bt0vDhw5WQkKA5c+YoJiamSPd7zTXX6MCBA5o7d6727NmjN998U/Pnz883zsvLS5GRkfrll1+0Zs0aPf7447rnnnvsJfhx48Zp4sSJevPNN/X7779r27ZtmjlzpqZOnVqkeAAAxeOdd95RcnKyOnTooKpVq9qXefPmScr949eyZcvUpUsXNWjQQE899ZR69uyphQsX2s/h7u6uRYsWyd3dXeHh4br//vv14IMPavz48cUeLxUQAC6rXLlyWr16tUaOHKkePXro9OnTqlatmjp16mSviDz11FM6cuSIIiMj5ebmpoceekj/93//l29a2n9655139Oyzz+qxxx7TiRMnVKNGDT377LOSpGrVqmncuHF65pln1K9fPz344IOKiYnRhAkTVKVKFU2cOFF79+6Vv7+/WrZsaT+uRo0a+vLLLzV06FC99dZbuv766/XSSy/poYceKvT93nXXXRo6dKgGDx6stLQ0devWTaNHj843o0rdunXVo0cP3X777Tp58qTuuOMOh2l2BwwYoHLlymny5MkaPny4ypcvryZNmpTI2+ABALrgc4l5QkNDtWrVqkueJywsTN9++21xhXVBRs6lIgYAAABKIZvNJj8/P+0/crJID3s7m81mU82qAUpOTrZUXMWFFiwAAAAApqEFCwAAAC7tap8F62pDBQQAAACAaUhAAAAAAJiGFiwAAAC4NOP8l1VYKRZnoAICAAAAwDQkIAAAAABMQwsWAAAAXBqzYJmLCggAAAAA05CAAAAAADANLVgAAABwacb5xSqsFIszUAEBAAAAYBoSEAAAAACmoQULAAAAro0eLFNRAQEAAABgGhIQAAAAAKahBQsAAAAuzTj/ZRVWisUZqIAAAAAAMA0JCAAAAADT0IIFAAAAl2YYuYtVWCkWZ6ACAgAAAMA0JCAAAAAATEMLFgAAAFwa7yE0FxUQAAAAAKYhAQEAAABgGlqwAAAA4NrowTIVFRAAAAAApiEBAQAAAGAaWrAAAADg0ozzX1ZhpVicgQoIAAAAANOQgAAAAAAwDS1YAAAAcGmGkbtYhZVicQYqIAAAAABMQwUEAAAALs1ms5V0CA6sFk9xIwEBAACAS/Lw8FBwcLCuqRVa0qHkExwcLA8Pj5IOwymMnJycnJIOAgAAACgJ586dU3p6ekmHkY+Hh4e8vLxKOgynIAEBAAAAYBoeQgcAAABgGhIQAAAAAKYhAQEAAABgGhIQAAAAAKYhAQEAAABgGhIQAAAAAKYhAQEAAABgmv8HwgEjRl50iAUAAAAASUVORK5CYII=", "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "make_confusion_matrix(y_true = y_test, y_pred = model_01_preds)" ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [], "source": [ "import tf2onnx\n", "import onnx" ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [], "source": [ "\n", "model_01.save(\"model/x_g85_lstm.keras\")" ] }, { "cell_type": "code", "execution_count": 23, "metadata": {}, "outputs": [], "source": [ "# # Define the input signature\n", "# input_signature = [tf.TensorSpec((None, 1), dtype=tf.string, name=\"input\")] # For `TextVectorization`\n", "\n", "# # input_signature = [tf.TensorSpec((None, max_length), name=\"input\")]\n", "\n", "# # Convert the Keras model to ONNX\n", "# onnx_model, _ = tf2onnx.convert.from_keras(model_01, input_signature, opset=18, \n", "# custom_ops={\n", "# \"StaticRegexReplace\": \"ai.onnx.contrib\", # For TextVectorization\n", "# \"StringSplitV2\": \"ai.onnx.contrib\", # For TextVectorization\n", "# })\n", "\n", "# # Save the ONNX model\n", "# onnx.save(onnx_model, \"model/x_g85_lstm.onnx\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "deep_learning", "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.12.4" } }, "nbformat": 4, "nbformat_minor": 2 }