Import the relevant libraries¶

In [61]:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.preprocessing import LabelEncoder, StandardScaler, OneHotEncoder
from sklearn.model_selection import RandomizedSearchCV, GridSearchCV
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.metrics import classification_report, confusion_matrix, accuracy_score
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline

Configures the overall plot style and adjusts the default appearance of the figures¶

In [62]:
sns.set(style="whitegrid")
plt.rcParams['figure.figsize'] = (12, 6)

1.Load datasets, check are there any missing values.¶

In [63]:
train_df = pd.read_csv('.//NSLKDD//kdd_train.csv')
test_df = pd.read_csv('.//NSLKDD//kdd_test.csv')

print(f"Shape of Training data: {train_df.shape}")
print(f"Shape of Testing data: {test_df.shape}")

print("\nNull in Training data:\n", train_df.isnull().sum().sum())
print("Null in Testing data:\n", test_df.isnull().sum().sum())
Shape of Training data: (125973, 42)
Shape of Testing data: (22544, 42)

Null in Training data:
 0
Null in Testing data:
 0

View Datasets¶

In [64]:
train_df
Out[64]:
duration protocol_type service flag src_bytes dst_bytes land wrong_fragment urgent hot ... dst_host_srv_count dst_host_same_srv_rate dst_host_diff_srv_rate dst_host_same_src_port_rate dst_host_srv_diff_host_rate dst_host_serror_rate dst_host_srv_serror_rate dst_host_rerror_rate dst_host_srv_rerror_rate labels
0 0 tcp ftp_data SF 491 0 0 0 0 0 ... 25 0.17 0.03 0.17 0.00 0.00 0.00 0.05 0.00 normal
1 0 udp other SF 146 0 0 0 0 0 ... 1 0.00 0.60 0.88 0.00 0.00 0.00 0.00 0.00 normal
2 0 tcp private S0 0 0 0 0 0 0 ... 26 0.10 0.05 0.00 0.00 1.00 1.00 0.00 0.00 neptune
3 0 tcp http SF 232 8153 0 0 0 0 ... 255 1.00 0.00 0.03 0.04 0.03 0.01 0.00 0.01 normal
4 0 tcp http SF 199 420 0 0 0 0 ... 255 1.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 normal
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
125968 0 tcp private S0 0 0 0 0 0 0 ... 25 0.10 0.06 0.00 0.00 1.00 1.00 0.00 0.00 neptune
125969 8 udp private SF 105 145 0 0 0 0 ... 244 0.96 0.01 0.01 0.00 0.00 0.00 0.00 0.00 normal
125970 0 tcp smtp SF 2231 384 0 0 0 0 ... 30 0.12 0.06 0.00 0.00 0.72 0.00 0.01 0.00 normal
125971 0 tcp klogin S0 0 0 0 0 0 0 ... 8 0.03 0.05 0.00 0.00 1.00 1.00 0.00 0.00 neptune
125972 0 tcp ftp_data SF 151 0 0 0 0 0 ... 77 0.30 0.03 0.30 0.00 0.00 0.00 0.00 0.00 normal

125973 rows × 42 columns

In [65]:
test_df
Out[65]:
duration protocol_type service flag src_bytes dst_bytes land wrong_fragment urgent hot ... dst_host_srv_count dst_host_same_srv_rate dst_host_diff_srv_rate dst_host_same_src_port_rate dst_host_srv_diff_host_rate dst_host_serror_rate dst_host_srv_serror_rate dst_host_rerror_rate dst_host_srv_rerror_rate labels
0 5 tcp smtp SF 2429 475 0 0 0 0 ... 57 0.11 0.02 0.01 0.04 0.00 0.0 0.87 0.0 normal
1 0 udp domain_u SF 45 134 0 0 0 0 ... 155 0.80 0.02 0.01 0.00 0.00 0.0 0.00 0.0 normal
2 0 udp domain_u SF 45 80 0 0 0 0 ... 255 1.00 0.00 0.01 0.00 0.00 0.0 0.00 0.0 normal
3 1979 udp other SF 145 105 0 0 0 0 ... 2 0.01 0.84 1.00 0.00 0.00 0.0 0.00 0.0 normal
4 14462 tcp other RSTR 1 0 0 0 0 0 ... 2 0.01 0.68 1.00 0.00 0.00 0.0 1.00 1.0 portsweep
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
22539 0 tcp private REJ 0 0 0 0 0 0 ... 1 0.00 1.00 0.00 0.00 0.09 0.0 0.91 1.0 satan
22540 0 tcp http SF 254 2277 0 0 0 0 ... 255 1.00 0.00 0.04 0.04 0.00 0.0 0.00 0.0 normal
22541 0 tcp private S0 0 0 0 0 0 0 ... 25 0.10 0.07 0.00 0.00 1.00 1.0 0.00 0.0 neptune
22542 0 tcp private RSTR 0 0 0 0 0 0 ... 1 0.00 0.58 0.57 0.00 0.00 0.0 0.58 1.0 portsweep
22543 0 udp domain_u SF 44 129 0 0 0 0 ... 255 1.00 0.00 0.00 0.00 0.00 0.0 0.00 0.0 normal

22544 rows × 42 columns

Visualize the training and test sets.¶

In [66]:
# View distribution of Labels
plt.figure(figsize=(16, 6))
sns.countplot(x=train_df['labels'], order=train_df['labels'].value_counts().index, palette='viridis')
plt.title('Distribution of Attack Types in Training Set')
plt.xticks(rotation=90)
plt.xlabel('Attack')
plt.ylabel('Count')
plt.tight_layout()
plt.show()

plt.figure(figsize=(16, 6))
sns.countplot(x=test_df['labels'], order=test_df['labels'].value_counts().index, palette='viridis')
plt.title('Distribution of Attack Types in Testing Set')
plt.xticks(rotation=90)
plt.xlabel('Attack')
plt.ylabel('Count')
plt.tight_layout()
plt.show()
C:\Users\g1986\AppData\Local\Temp\ipykernel_34516\1014181271.py:3: FutureWarning: 

Passing `palette` without assigning `hue` is deprecated and will be removed in v0.14.0. Assign the `x` variable to `hue` and set `legend=False` for the same effect.

  sns.countplot(x=train_df['labels'], order=train_df['labels'].value_counts().index, palette='viridis')
No description has been provided for this image
C:\Users\g1986\AppData\Local\Temp\ipykernel_34516\1014181271.py:12: FutureWarning: 

Passing `palette` without assigning `hue` is deprecated and will be removed in v0.14.0. Assign the `x` variable to `hue` and set `legend=False` for the same effect.

  sns.countplot(x=test_df['labels'], order=test_df['labels'].value_counts().index, palette='viridis')
No description has been provided for this image

Check whether the training and test sets contain the same label categories.¶

In [67]:
# Identify if label in Training data and Testing data are same
train_labels = set(train_df['labels'].unique())
test_labels = set(test_df['labels'].unique())
print(f"\nNumber of Labels in Training: {len(train_labels)}")
print(f"Number of Labels in Testing: {len(test_labels)}")

if train_labels == test_labels:
    print("Totally Same")
else:
    print("Not Same:")
    print(f"Training data: {train_labels - test_labels}")
    print(f"Testing data: {test_labels - train_labels}")
Number of Labels in Training: 23
Number of Labels in Testing: 36
Not Same:
Training data: {'spy'}
Testing data: {'mscan', 'mailbomb', 'named', 'apache2', 'xsnoop', 'ps', 'sendmail', 'processtable', 'saint', 'xlock', 'xterm', 'snmpguess', 'snmpgetattack', 'httptunnel'}

Setting Mapping standard¶

In [72]:
# Because the number of labels in training data and testing data are different, I have to do label mapping to make sure numbers of labels are same

mapping = {
    'normal': 'Normal',
    
    #DoS
    'back': 'DoS', 'land': 'DoS', 'neptune': 'DoS', 'pod': 'DoS', 'smurf': 'DoS', 
    'teardrop': 'DoS', 'mailbomb': 'DoS', 'apache2': 'DoS', 'processtable': 'DoS', 
    'udpstorm': 'DoS', 'worm': 'DoS',
    
    # Probe
    'satan': 'Probe', 'ipsweep': 'Probe', 'nmap': 'Probe', 'portsweep': 'Probe', 
    'mscan': 'Probe', 'saint': 'Probe',
    
    #R2L
    'guess_passwd': 'R2L', 'ftp_write': 'R2L', 'imap': 'R2L', 'phf': 'R2L', 
    'multihop': 'R2L', 'warezmaster': 'R2L', 'warezclient': 'R2L', 'spy': 'R2L', 
    'xlock': 'R2L', 'xsnoop': 'R2L', 'snmpguess': 'R2L', 'snmpgetattack': 'R2L', 
    'httptunnel': 'R2L', 'sendmail': 'R2L', 'named': 'R2L',
    
    # U2R
    'buffer_overflow': 'U2R', 'loadmodule': 'U2R', 'rootkit': 'U2R', 'perl': 'U2R', 
    'sqlattack': 'U2R', 'xterm': 'U2R', 'ps': 'U2R'
}

Apply Mapping, and check the structure of each dataset¶

In [69]:
# Mapping
train_df['attack_class'] = train_df['labels'].map(lambda x: mapping.get(x, 'Normal'))
test_df['attack_class'] = test_df['labels'].map(lambda x: mapping.get(x, 'Normal'))

# View mapping results
print("\nDistribution of Training labels:")
print(train_df['attack_class'].value_counts())
print("\nDistribution of Testing labels:")
print(test_df['attack_class'].value_counts())

y_train = train_df['attack_class']
y_test = test_df['attack_class']
train_df = train_df.drop(['labels', 'attack_class'], axis=1)
test_df = test_df.drop(['labels', 'attack_class'], axis=1)
Distribution of Training labels:
attack_class
Normal    67343
DoS       45927
Probe     11656
R2L         995
U2R          52
Name: count, dtype: int64

Distribution of Testing labels:
attack_class
Normal    11245
DoS        8095
Probe      2157
R2L        1009
U2R          38
Name: count, dtype: int64

2.Data preprocessing, including one-hot encoding, standardization, and label encoding¶

In [27]:
# Combine 2 datasets
train_df['type'] = 'train'
test_df['type'] = 'test'
train_and_test_df = pd.concat([train_df, test_df], axis=0)

# One-Hot encode
cate_cols = ['protocol_type', 'service', 'flag']
train_and_test_encoded = pd.get_dummies(train_and_test_df, columns=cate_cols, drop_first=True)

# Split 2 datasets
X_train = train_and_test_encoded[train_and_test_encoded['type'] == 'train'].drop('type', axis=1)
X_test = train_and_test_encoded[train_and_test_encoded['type'] == 'test'].drop('type', axis=1)

# Standard
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

# Transform lebel into numbers
le = LabelEncoder()
y_train = le.fit_transform(y_train)
y_test = le.transform(y_test)
In [30]:
print(dict(zip(le.classes_, le.transform(le.classes_))))
{'DoS': 0, 'Normal': 1, 'Probe': 2, 'R2L': 3, 'U2R': 4}

The evaluate_model function trains and tests the model, and generates a test report and confusion matrix.¶

In [34]:
def evaluate_model(model, model_name):
    print(model_name)
    print("Training:")
    model.fit(X_train_scaled, y_train)
    
    print("Testing:")
    y_pred = model.predict(X_test_scaled)
    
    # Evaluation
    acc = accuracy_score(y_test, y_pred)
    print(f"Accuracy: {acc:.4f}")
    print("\nReport:")
    print(classification_report(y_test, y_pred, target_names=le.classes_, zero_division=0))
    
    # Confusion matrix
    cm = confusion_matrix(y_test, y_pred)
    plt.figure(figsize=(8, 6))
    sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', 
                xticklabels=le.classes_, yticklabels=le.classes_)
    plt.title(f'Confusion Matrix of {model_name}')
    plt.ylabel('Actual')
    plt.xlabel('Predicted')
    plt.show()

    return model

3.Train and test the Random Forest algorithm¶

In [35]:
rf = RandomForestClassifier(n_estimators=100, random_state=42, n_jobs=-1)
rf_trained = evaluate_model(rf, "Random Forest")
Random Forest
Training:
Testing:
Accuracy: 0.9198

Report:
              precision    recall  f1-score   support

         DoS       0.99      0.94      0.96      8095
      Normal       0.87      0.99      0.93     11245
       Probe       0.96      0.86      0.91      2157
         R2L       0.99      0.14      0.24      1009
         U2R       1.00      0.34      0.51        38

    accuracy                           0.92     22544
   macro avg       0.96      0.65      0.71     22544
weighted avg       0.93      0.92      0.91     22544

No description has been provided for this image

Train and test the Gradient Boosting algorithm.¶

In [36]:
# Gradient Boosting
gb = GradientBoostingClassifier(n_estimators=50, learning_rate=0.1, max_depth=5, random_state=42)
gb_trained = evaluate_model(gb, "Gradient Boosting")
Gradient Boosting
Training:
Testing:
Accuracy: 0.9339

Report:
              precision    recall  f1-score   support

         DoS       0.99      0.95      0.97      8095
      Normal       0.90      0.99      0.94     11245
       Probe       0.94      0.88      0.91      2157
         R2L       0.98      0.27      0.42      1009
         U2R       0.90      0.47      0.62        38

    accuracy                           0.93     22544
   macro avg       0.94      0.71      0.77     22544
weighted avg       0.94      0.93      0.93     22544

No description has been provided for this image

4.Use randomized cross-validation to perform parameter tuning for the Random Forest model.¶

In [42]:
#Hyperparameter Tuning - RF
param_dist = {
    'n_estimators': [25, 50, 100, 150, 200],
    'max_depth': [5, 10, 20, 30, None],
    'min_samples_split': [2, 5],
}
rf_random = RandomizedSearchCV(RandomForestClassifier(random_state=42), 
                               param_distributions=param_dist, 
                               n_iter=5, cv=3, verbose=1, n_jobs=-1, random_state=42)
rf_random.fit(X_train_scaled, y_train)

print(f"The optimal parameters: {rf_random.best_params_}")
best_rf = rf_random.best_estimator_
Fitting 3 folds for each of 5 candidates, totalling 15 fits
The optimal parameters: {'n_estimators': 200, 'min_samples_split': 5, 'max_depth': 30}

Apply the same method to tune the parameters of the Gradient Boosting model.¶

In [46]:
#Hyperparameter Tuning - GB
param_dist_gb = {
    'n_estimators': [50, 100],           
    'learning_rate': [0.05, 0.1],       
    'max_depth': [3, 5],                
    'min_samples_split': [2, 5],
    'min_samples_leaf': [1, 2],
    'subsample': [0.8, 1.0]
}

gb_random = RandomizedSearchCV(
    GradientBoostingClassifier(random_state=42),
    param_distributions=param_dist_gb,
    n_iter=5,        
    cv=3,            
    verbose=1,
    n_jobs=-1,
    random_state=42
)

gb_random.fit(X_train_scaled, y_train)

print(f"The optimal parameters: {gb_random.best_params_}")
best_gb = gb_random.best_estimator_
Fitting 3 folds for each of 5 candidates, totalling 15 fits
The optimal parameters: {'subsample': 0.8, 'n_estimators': 50, 'min_samples_split': 5, 'min_samples_leaf': 2, 'max_depth': 3, 'learning_rate': 0.1}

5.The evaluate_optimal_model function tests the optimal model, generates a test report and confusion matrix, and visualizes feature importance.¶

In [70]:
def evaluate_optimal_model(model, model_rd, model_name):
    print(f"The optimal {model_name} parameters: {model_rd.best_params_}")
    y_pred = model.predict(X_test_scaled)
    
    # Evaluation
    acc = accuracy_score(y_test, y_pred)
    print(f"Accuracy: {acc:.4f}")
    print("\nReport:")
    print(classification_report(y_test, y_pred, target_names=le.classes_, zero_division=0))
    
    # Confusion matrix
    cm = confusion_matrix(y_test, y_pred)
    plt.figure(figsize=(8, 6))
    sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', 
                xticklabels=le.classes_, yticklabels=le.classes_)
    plt.title(f'Confusion Matrix of {model_name}')
    plt.ylabel('Actual')
    plt.xlabel('Predicted')
    plt.show()

    # important features
    importances = model.feature_importances_
    indices = np.argsort(importances)[::-1]
    top_n = 15
    plt.figure(figsize=(12, 6))
    plt.title(f"Top {top_n} Important Features of Best {model_name}")
    plt.bar(range(top_n), importances[indices[:top_n]], align="center", color='teal')
    plt.xticks(range(top_n), X_train.columns[indices[:top_n]], rotation=45, ha='right')
    plt.tight_layout()
    plt.show()

Generate the test results of the optimal Random Forest model¶

In [52]:
evaluate_optimal_model(best_rf, rf_random, "Random Forest")
The optimal Random Forest parameters: {'n_estimators': 200, 'min_samples_split': 5, 'max_depth': 30}
Accuracy: 0.9240

Report:
              precision    recall  f1-score   support

         DoS       0.99      0.95      0.97      8095
      Normal       0.88      0.99      0.93     11245
       Probe       0.95      0.86      0.90      2157
         R2L       0.99      0.14      0.25      1009
         U2R       1.00      0.32      0.48        38

    accuracy                           0.92     22544
   macro avg       0.96      0.65      0.71     22544
weighted avg       0.93      0.92      0.91     22544

No description has been provided for this image
No description has been provided for this image

Generate the test results of the optimal Gradient Boosting model¶

In [53]:
evaluate_optimal_model(best_gb, gb_random, "Gradient Boosting")
The optimal Gradient Boosting parameters: {'subsample': 0.8, 'n_estimators': 50, 'min_samples_split': 5, 'min_samples_leaf': 2, 'max_depth': 3, 'learning_rate': 0.1}
Accuracy: 0.9295

Report:
              precision    recall  f1-score   support

         DoS       0.99      0.95      0.97      8095
      Normal       0.89      0.99      0.94     11245
       Probe       0.96      0.87      0.91      2157
         R2L       0.84      0.20      0.32      1009
         U2R       0.67      0.11      0.18        38

    accuracy                           0.93     22544
   macro avg       0.87      0.62      0.66     22544
weighted avg       0.93      0.93      0.92     22544

No description has been provided for this image
No description has been provided for this image
In [ ]: