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')
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')
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
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
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
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
In [ ]: